mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-03 18:58:24 +00:00 
			
		
		
		
	Compare commits
	
		
			34 Commits
		
	
	
		
			v3.2.3
			...
			288-signat
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d33dcacc46 | |||
| 8d97df9f96 | |||
| 2822800c76 | |||
| 8973b7c20b | |||
| 7f144da1a7 | |||
| ab4193938d | |||
| e2426ba1d8 | |||
| 8209990437 | |||
| b1885de3e2 | |||
| 218280304c | |||
| 8a7b48b201 | |||
| 52a9aab73f | |||
| 8f358112b1 | |||
| 57a07af3db | |||
| fd216ff66e | |||
| 689c2c574a | |||
| a8de18beac | |||
| babca5fc0f | |||
| f2c5663b05 | |||
| ba95687f46 | |||
| 
						
						
							
						
						a309cc0774
	
				 | 
					
					
						|||
| 
						
						
							
						
						3db4fff80d
	
				 | 
					
					
						|||
| 
						
						
							
						
						c9d54a5fea
	
				 | 
					
					
						|||
| 
						
						
							
						
						86c862e69d
	
				 | 
					
					
						|||
| 
						
						
							
						
						9bc6fe6aff
	
				 | 
					
					
						|||
| 
						
						
							
						
						18a03fd740
	
				 | 
					
					
						|||
| 
						
						
							
						
						610239930b
	
				 | 
					
					
						|||
| b65e2c62c4 | |||
| 
						
						
							
						
						89f5231649
	
				 | 
					
					
						|||
| 
						
						
							
						
						99818c211d
	
				 | 
					
					
						|||
| 
						
						
							
						
						a9f0059743
	
				 | 
					
					
						|||
| 
						
						
							
						
						5bc542a567
	
				 | 
					
					
						|||
| 
						
						
							
						
						c8ccce83fd
	
				 | 
					
					
						|||
| 4a229ebf6b | 
							
								
								
									
										8
									
								
								.changes/unreleased/Feature-20240614-153236.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.changes/unreleased/Feature-20240614-153236.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: |-
 | 
			
		||||
  Electronic signature
 | 
			
		||||
 | 
			
		||||
  Implementation of the electronic signature for documents within chill.
 | 
			
		||||
time: 2024-06-14T15:32:36.875891692+02:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: ""
 | 
			
		||||
							
								
								
									
										5
									
								
								.changes/unreleased/Feature-20240718-151233.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/unreleased/Feature-20240718-151233.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: Metadata form added for person signatures
 | 
			
		||||
time: 2024-07-18T15:12:33.8134266+02:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: "288"
 | 
			
		||||
@@ -31,6 +31,7 @@
 | 
			
		||||
        "phpoffice/phpspreadsheet": "^1.16",
 | 
			
		||||
        "ramsey/uuid-doctrine": "^1.7",
 | 
			
		||||
        "sensio/framework-extra-bundle": "^5.5",
 | 
			
		||||
        "smalot/pdfparser": "^2.10",
 | 
			
		||||
        "spomky-labs/base64url": "^2.0",
 | 
			
		||||
        "symfony/asset": "^5.4",
 | 
			
		||||
        "symfony/browser-kit": "^5.4",
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
<?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\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
 | 
			
		||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\Messenger\MessageBusInterface;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
 | 
			
		||||
class SignatureRequestController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private MessageBusInterface $messageBus,
 | 
			
		||||
        private StoredObjectManagerInterface $storedObjectManager,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
 | 
			
		||||
    public function processSignature(StoredObject $storedObject): Response
 | 
			
		||||
    {
 | 
			
		||||
        $content = $this->storedObjectManager->read($storedObject);
 | 
			
		||||
 | 
			
		||||
        $this->messageBus->dispatch(new RequestPdfSignMessage(
 | 
			
		||||
            0,
 | 
			
		||||
            new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
 | 
			
		||||
            0,
 | 
			
		||||
            'test signature',
 | 
			
		||||
            'Mme Caroline Diallo',
 | 
			
		||||
            $content
 | 
			
		||||
        ));
 | 
			
		||||
 | 
			
		||||
        return new Response('<html><head><title>test</title></head><body><p>ok</p></body></html>');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
<?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\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Message which is received when a pdf is signed.
 | 
			
		||||
 */
 | 
			
		||||
final readonly class PdfSignedMessage
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        public readonly int $signatureId,
 | 
			
		||||
        public readonly string $content
 | 
			
		||||
    ) {}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
<?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\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * log prefix.
 | 
			
		||||
     */
 | 
			
		||||
    private const P = '[pdf signed message] ';
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private LoggerInterface $logger,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function __invoke(PdfSignedMessage $message): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->logger->info(self::P.'a message is received', ['signaturedId' => $message->signatureId]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,66 @@
 | 
			
		||||
<?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\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Messenger\Envelope;
 | 
			
		||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
 | 
			
		||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Decode (and requeue) @see{PdfSignedMessage}, which comes from an external producer.
 | 
			
		||||
 */
 | 
			
		||||
final readonly class PdfSignedMessageSerializer implements SerializerInterface
 | 
			
		||||
{
 | 
			
		||||
    public function decode(array $encodedEnvelope): Envelope
 | 
			
		||||
    {
 | 
			
		||||
        $body = $encodedEnvelope['body'];
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
 | 
			
		||||
        } catch (\JsonException $e) {
 | 
			
		||||
            throw new MessageDecodingFailedException('Could not deserialize message', previous: $e);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!array_key_exists('signatureId', $decoded) || !array_key_exists('content', $decoded)) {
 | 
			
		||||
            throw new MessageDecodingFailedException('Could not find expected keys: signatureId or content');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $content = base64_decode($decoded['content'], true);
 | 
			
		||||
 | 
			
		||||
        if (false === $content) {
 | 
			
		||||
            throw new MessageDecodingFailedException('Invalid character found in the base64 encoded content');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $message = new PdfSignedMessage($decoded['signatureId'], $content);
 | 
			
		||||
 | 
			
		||||
        return new Envelope($message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function encode(Envelope $envelope): array
 | 
			
		||||
    {
 | 
			
		||||
        $message = $envelope->getMessage();
 | 
			
		||||
 | 
			
		||||
        if (!$message instanceof PdfSignedMessage) {
 | 
			
		||||
            throw new MessageDecodingFailedException('Expected a PdfSignedMessage');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $data = [
 | 
			
		||||
            'signatureId' => $message->signatureId,
 | 
			
		||||
            'content' => base64_encode($message->content),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'body' => json_encode($data, JSON_THROW_ON_ERROR),
 | 
			
		||||
            'headers' => [],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
<?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\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Message which is sent when we request a signature on a pdf.
 | 
			
		||||
 */
 | 
			
		||||
final readonly class RequestPdfSignMessage
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        public int $signatureId,
 | 
			
		||||
        public PDFSignatureZone $PDFSignatureZone,
 | 
			
		||||
        public int $signatureZoneIndex,
 | 
			
		||||
        public string $reason,
 | 
			
		||||
        public string $signerText,
 | 
			
		||||
        public string $content,
 | 
			
		||||
    ) {}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,105 @@
 | 
			
		||||
<?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\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
 | 
			
		||||
use Symfony\Component\Messenger\Envelope;
 | 
			
		||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
 | 
			
		||||
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
 | 
			
		||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Serialize a RequestPdfSignMessage, for external consumer.
 | 
			
		||||
 */
 | 
			
		||||
final readonly class RequestPdfSignMessageSerializer implements SerializerInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private NormalizerInterface $normalizer,
 | 
			
		||||
        private DenormalizerInterface $denormalizer,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function decode(array $encodedEnvelope): Envelope
 | 
			
		||||
    {
 | 
			
		||||
        $body = $encodedEnvelope['body'];
 | 
			
		||||
        $headers = $encodedEnvelope['headers'];
 | 
			
		||||
 | 
			
		||||
        if (RequestPdfSignMessage::class !== ($headers['Message'] ?? null)) {
 | 
			
		||||
            throw new MessageDecodingFailedException('serializer does not support this message');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $data = json_decode($body, true);
 | 
			
		||||
 | 
			
		||||
        $zoneSignature = $this->denormalizer->denormalize($data['signatureZone'], PDFSignatureZone::class, 'json', [
 | 
			
		||||
            AbstractNormalizer::GROUPS => ['write'],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $content = base64_decode($data['content'], true);
 | 
			
		||||
 | 
			
		||||
        if (false === $content) {
 | 
			
		||||
            throw new MessageDecodingFailedException('the content could not be converted from base64 encoding');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $message = new RequestPdfSignMessage(
 | 
			
		||||
            $data['signatureId'],
 | 
			
		||||
            $zoneSignature,
 | 
			
		||||
            $data['signatureZoneIndex'],
 | 
			
		||||
            $data['reason'],
 | 
			
		||||
            $data['signerText'],
 | 
			
		||||
            $content,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // in case of redelivery, unserialize any stamps
 | 
			
		||||
        $stamps = [];
 | 
			
		||||
        if (isset($headers['stamps'])) {
 | 
			
		||||
            $stamps = unserialize($headers['stamps']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new Envelope($message, $stamps);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function encode(Envelope $envelope): array
 | 
			
		||||
    {
 | 
			
		||||
        $message = $envelope->getMessage();
 | 
			
		||||
 | 
			
		||||
        if (!$message instanceof RequestPdfSignMessage) {
 | 
			
		||||
            throw new MessageDecodingFailedException('Message is not a RequestPdfSignMessage');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $data = [
 | 
			
		||||
            'signatureId' => $message->signatureId,
 | 
			
		||||
            'signatureZoneIndex' => $message->signatureZoneIndex,
 | 
			
		||||
            'signatureZone' => $this->normalizer->normalize($message->PDFSignatureZone, 'json', [AbstractNormalizer::GROUPS => ['read']]),
 | 
			
		||||
            'reason' => $message->reason,
 | 
			
		||||
            'signerText' => $message->signerText,
 | 
			
		||||
            'content' => base64_encode($message->content),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $allStamps = [];
 | 
			
		||||
        foreach ($envelope->all() as $stamp) {
 | 
			
		||||
            if ($stamp instanceof NonSendableStampInterface) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            $allStamps = [...$allStamps, ...$stamp];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'body' => json_encode($data, JSON_THROW_ON_ERROR, 512),
 | 
			
		||||
            'headers' => [
 | 
			
		||||
                'stamps' => serialize($allStamps),
 | 
			
		||||
                'Message' => RequestPdfSignMessage::class,
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php
									
									
									
									
									
										Normal file
									
								
							@@ -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\DocStoreBundle\Service\Signature;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Serializer\Annotation\Groups;
 | 
			
		||||
 | 
			
		||||
final readonly class PDFPage
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public int $index,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public float $width,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public float $height,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function equals(self $page): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $page->index === $this->index
 | 
			
		||||
            && round($page->width, 2) === round($this->width, 2)
 | 
			
		||||
            && round($page->height, 2) === round($this->height, 2);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
<?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\Service\Signature;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Serializer\Annotation\Groups;
 | 
			
		||||
 | 
			
		||||
final readonly class PDFSignatureZone
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public float $x,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public float $y,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public float $height,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public float $width,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public PDFPage $PDFPage,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function equals(self $other): bool
 | 
			
		||||
    {
 | 
			
		||||
        return
 | 
			
		||||
            $this->x == $other->x
 | 
			
		||||
            && $this->y == $other->y
 | 
			
		||||
            && $this->height == $other->height
 | 
			
		||||
            && $this->width == $other->width
 | 
			
		||||
            && $this->PDFPage->equals($other->PDFPage);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,58 @@
 | 
			
		||||
<?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\Service\Signature;
 | 
			
		||||
 | 
			
		||||
use Smalot\PdfParser\Parser;
 | 
			
		||||
 | 
			
		||||
class PDFSignatureZoneParser
 | 
			
		||||
{
 | 
			
		||||
    public const ZONE_SIGNATURE_START = 'signature_zone';
 | 
			
		||||
 | 
			
		||||
    private Parser $parser;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        public float $defaultHeight = 180.0,
 | 
			
		||||
        public float $defaultWidth = 180.0,
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->parser = new Parser();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return list<PDFSignatureZone>
 | 
			
		||||
     */
 | 
			
		||||
    public function findSignatureZones(string $fileContent): array
 | 
			
		||||
    {
 | 
			
		||||
        $pdf = $this->parser->parseContent($fileContent);
 | 
			
		||||
        $zones = [];
 | 
			
		||||
 | 
			
		||||
        $defaults = $pdf->getObjectsByType('Pages');
 | 
			
		||||
        $defaultPage = reset($defaults);
 | 
			
		||||
        $defaultPageDetails = $defaultPage->getDetails();
 | 
			
		||||
 | 
			
		||||
        foreach ($pdf->getPages() as $index => $page) {
 | 
			
		||||
            $details = $page->getDetails();
 | 
			
		||||
            $pdfPage = new PDFPage(
 | 
			
		||||
                $index,
 | 
			
		||||
                (float) ($details['MediaBox'][2] ?? $defaultPageDetails['MediaBox'][2]),
 | 
			
		||||
                (float) ($details['MediaBox'][3] ?? $defaultPageDetails['MediaBox'][3]),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            foreach ($page->getDataTm() as $dataTm) {
 | 
			
		||||
                if (str_starts_with($dataTm[1], self::ZONE_SIGNATURE_START)) {
 | 
			
		||||
                    $zones[] = new PDFSignatureZone((float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $zones;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,63 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessage;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessageSerializer;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Symfony\Component\Messenger\Envelope;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class PdfSignedMessageSerializerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    public function testDecode(): void
 | 
			
		||||
    {
 | 
			
		||||
        $asString = <<<'JSON'
 | 
			
		||||
            {"signatureId": 0, "content": "dGVzdAo="}
 | 
			
		||||
            JSON;
 | 
			
		||||
 | 
			
		||||
        $actual = $this->buildSerializer()->decode(['body' => $asString]);
 | 
			
		||||
 | 
			
		||||
        self::assertInstanceOf(Envelope::class, $actual);
 | 
			
		||||
        $message = $actual->getMessage();
 | 
			
		||||
        self::assertInstanceOf(PdfSignedMessage::class, $message);
 | 
			
		||||
        self::assertEquals("test\n", $message->content);
 | 
			
		||||
        self::assertEquals(0, $message->signatureId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testEncode(): void
 | 
			
		||||
    {
 | 
			
		||||
        $envelope = new Envelope(
 | 
			
		||||
            new PdfSignedMessage(0, "test\n")
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $actual = $this->buildSerializer()->encode($envelope);
 | 
			
		||||
 | 
			
		||||
        self::assertIsArray($actual);
 | 
			
		||||
        self::assertArrayHasKey('body', $actual);
 | 
			
		||||
        self::assertArrayHasKey('headers', $actual);
 | 
			
		||||
        self::assertEquals([], $actual['headers']);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(<<<'JSON'
 | 
			
		||||
                {"signatureId":0,"content":"dGVzdAo="}
 | 
			
		||||
                JSON, $actual['body']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildSerializer(): PdfSignedMessageSerializer
 | 
			
		||||
    {
 | 
			
		||||
        return new PdfSignedMessageSerializer();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,137 @@
 | 
			
		||||
<?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\Tests\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessageSerializer;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Symfony\Component\Messenger\Envelope;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Serializer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class RequestPdfSignMessageSerializerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    public function testEncode(): void
 | 
			
		||||
    {
 | 
			
		||||
        $serializer = $this->buildSerializer();
 | 
			
		||||
 | 
			
		||||
        $envelope = new Envelope(
 | 
			
		||||
            $request = new RequestPdfSignMessage(
 | 
			
		||||
                0,
 | 
			
		||||
                new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
 | 
			
		||||
                0,
 | 
			
		||||
                'metadata to add to the signature',
 | 
			
		||||
                'Mme Caroline Diallo',
 | 
			
		||||
                'abc'
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $actual = $serializer->encode($envelope);
 | 
			
		||||
        $expectedBody = json_encode([
 | 
			
		||||
            'signatureId' => $request->signatureId,
 | 
			
		||||
            'signatureZoneIndex' => $request->signatureZoneIndex,
 | 
			
		||||
            'signatureZone' => ['x' => 10.0],
 | 
			
		||||
            'reason' => $request->reason,
 | 
			
		||||
            'signerText' => $request->signerText,
 | 
			
		||||
            'content' => base64_encode($request->content),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        self::assertIsArray($actual);
 | 
			
		||||
        self::assertArrayHasKey('body', $actual);
 | 
			
		||||
        self::assertArrayHasKey('headers', $actual);
 | 
			
		||||
        self::assertEquals($expectedBody, $actual['body']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testDecode(): void
 | 
			
		||||
    {
 | 
			
		||||
        $serializer = $this->buildSerializer();
 | 
			
		||||
 | 
			
		||||
        $request = new RequestPdfSignMessage(
 | 
			
		||||
            0,
 | 
			
		||||
            new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
 | 
			
		||||
            0,
 | 
			
		||||
            'metadata to add to the signature',
 | 
			
		||||
            'Mme Caroline Diallo',
 | 
			
		||||
            'abc'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $bodyAsString = json_encode([
 | 
			
		||||
            'signatureId' => $request->signatureId,
 | 
			
		||||
            'signatureZoneIndex' => $request->signatureZoneIndex,
 | 
			
		||||
            'signatureZone' => ['x' => 10.0],
 | 
			
		||||
            'reason' => $request->reason,
 | 
			
		||||
            'signerText' => $request->signerText,
 | 
			
		||||
            'content' => base64_encode($request->content),
 | 
			
		||||
        ], JSON_THROW_ON_ERROR);
 | 
			
		||||
 | 
			
		||||
        $actual = $serializer->decode([
 | 
			
		||||
            'body' => $bodyAsString,
 | 
			
		||||
            'headers' => [
 | 
			
		||||
                'Message' => RequestPdfSignMessage::class,
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        self::assertInstanceOf(RequestPdfSignMessage::class, $actual->getMessage());
 | 
			
		||||
        self::assertEquals($request->signatureId, $actual->getMessage()->signatureId);
 | 
			
		||||
        self::assertEquals($request->signatureZoneIndex, $actual->getMessage()->signatureZoneIndex);
 | 
			
		||||
        self::assertEquals($request->reason, $actual->getMessage()->reason);
 | 
			
		||||
        self::assertEquals($request->signerText, $actual->getMessage()->signerText);
 | 
			
		||||
        self::assertEquals($request->content, $actual->getMessage()->content);
 | 
			
		||||
        self::assertNotNull($actual->getMessage()->PDFSignatureZone);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildSerializer(): RequestPdfSignMessageSerializer
 | 
			
		||||
    {
 | 
			
		||||
        $normalizer =
 | 
			
		||||
            new class () implements NormalizerInterface {
 | 
			
		||||
                public function normalize($object, ?string $format = null, array $context = []): array
 | 
			
		||||
                {
 | 
			
		||||
                    if (!$object instanceof PDFSignatureZone) {
 | 
			
		||||
                        throw new UnexpectedValueException('expected RequestPdfSignMessage');
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return [
 | 
			
		||||
                        'x' => $object->x,
 | 
			
		||||
                    ];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                public function supportsNormalization($data, ?string $format = null): bool
 | 
			
		||||
                {
 | 
			
		||||
                    return $data instanceof PDFSignatureZone;
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        $denormalizer = new class () implements DenormalizerInterface {
 | 
			
		||||
            public function denormalize($data, string $type, ?string $format = null, array $context = [])
 | 
			
		||||
            {
 | 
			
		||||
                return new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public function supportsDenormalization($data, string $type, ?string $format = null)
 | 
			
		||||
            {
 | 
			
		||||
                return PDFSignatureZone::class === $type;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        $serializer = new Serializer([$normalizer, $denormalizer]);
 | 
			
		||||
 | 
			
		||||
        return new RequestPdfSignMessageSerializer($serializer, $serializer);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,77 @@
 | 
			
		||||
<?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 Tests\Service\Signature;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class PDFSignatureZoneParserTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    private static PDFSignatureZoneParser $parser;
 | 
			
		||||
 | 
			
		||||
    public static function setUpBeforeClass(): void
 | 
			
		||||
    {
 | 
			
		||||
        self::$parser = new PDFSignatureZoneParser();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider provideFiles
 | 
			
		||||
     *
 | 
			
		||||
     * @param list<PDFSignatureZone> $expected
 | 
			
		||||
     */
 | 
			
		||||
    public function testFindSignatureZones(string $filePath, array $expected): void
 | 
			
		||||
    {
 | 
			
		||||
        $content = file_get_contents($filePath);
 | 
			
		||||
 | 
			
		||||
        if (false === $content) {
 | 
			
		||||
            throw new \LogicException("Unable to read file {$filePath}");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $actual = self::$parser->findSignatureZones($content);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(count($expected), count($actual));
 | 
			
		||||
 | 
			
		||||
        foreach ($actual as $index => $signatureZone) {
 | 
			
		||||
            self::assertObjectEquals($expected[$index], $signatureZone);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function provideFiles(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        yield [
 | 
			
		||||
            __DIR__.'/data/signature_2_signature_page_1.pdf',
 | 
			
		||||
            [
 | 
			
		||||
                new PDFSignatureZone(
 | 
			
		||||
                    127.7,
 | 
			
		||||
                    95.289,
 | 
			
		||||
                    180.0,
 | 
			
		||||
                    180.0,
 | 
			
		||||
                    $page = new PDFPage(0, 595.30393, 841.8897)
 | 
			
		||||
                ),
 | 
			
		||||
                new PDFSignatureZone(
 | 
			
		||||
                    269.5,
 | 
			
		||||
                    95.289,
 | 
			
		||||
                    180.0,
 | 
			
		||||
                    180.0,
 | 
			
		||||
                    $page,
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@@ -121,9 +121,4 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
 | 
			
		||||
    {
 | 
			
		||||
        return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,15 +13,16 @@ namespace Chill\MainBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
 | 
			
		||||
use Chill\MainBundle\Form\EntityWorkflowCommentType;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
 | 
			
		||||
use Chill\MainBundle\Form\WorkflowSignatureMetadataType;
 | 
			
		||||
use Chill\MainBundle\Form\WorkflowStepType;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
 | 
			
		||||
use Chill\MainBundle\Security\ChillSecurity;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
 | 
			
		||||
@@ -276,10 +277,11 @@ class WorkflowController extends AbstractController
 | 
			
		||||
        $handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
 | 
			
		||||
        $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
 | 
			
		||||
        $errors = [];
 | 
			
		||||
        $signatures = $entityWorkflow->getCurrentStep()->getSignatures();
 | 
			
		||||
 | 
			
		||||
        if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
 | 
			
		||||
            // possible transition
 | 
			
		||||
 | 
			
		||||
            $stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
            $usersInvolved = $entityWorkflow->getUsersInvolved();
 | 
			
		||||
            $currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
 | 
			
		||||
 | 
			
		||||
@@ -289,9 +291,8 @@ class WorkflowController extends AbstractController
 | 
			
		||||
 | 
			
		||||
            $transitionForm = $this->createForm(
 | 
			
		||||
                WorkflowStepType::class,
 | 
			
		||||
                $entityWorkflow->getCurrentStep(),
 | 
			
		||||
                $stepDTO,
 | 
			
		||||
                [
 | 
			
		||||
                    'transition' => true,
 | 
			
		||||
                    'entity_workflow' => $entityWorkflow,
 | 
			
		||||
                    'suggested_users' => $usersInvolved,
 | 
			
		||||
                ]
 | 
			
		||||
@@ -310,12 +311,7 @@ class WorkflowController extends AbstractController
 | 
			
		||||
                    throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
 | 
			
		||||
                $entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? [];
 | 
			
		||||
                $entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? [];
 | 
			
		||||
                $entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? [];
 | 
			
		||||
 | 
			
		||||
                $workflow->apply($entityWorkflow, $transition);
 | 
			
		||||
                $workflow->apply($entityWorkflow, $transition, ['context' => $stepDTO]);
 | 
			
		||||
 | 
			
		||||
                $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
@@ -327,22 +323,6 @@ class WorkflowController extends AbstractController
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /*
 | 
			
		||||
        $commentForm = $this->createForm(EntityWorkflowCommentType::class, $newComment = new EntityWorkflowComment());
 | 
			
		||||
        $commentForm->handleRequest($request);
 | 
			
		||||
 | 
			
		||||
        if ($commentForm->isSubmitted() && $commentForm->isValid()) {
 | 
			
		||||
            $this->entityManager->persist($newComment);
 | 
			
		||||
            $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
            $this->addFlash('success', $this->translator->trans('workflow.Comment added'));
 | 
			
		||||
 | 
			
		||||
            return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
 | 
			
		||||
        } elseif ($commentForm->isSubmitted() && !$commentForm->isValid()) {
 | 
			
		||||
            $this->addFlash('error', $this->translator->trans('This form contains errors'));
 | 
			
		||||
        }
 | 
			
		||||
         */
 | 
			
		||||
 | 
			
		||||
        return $this->render(
 | 
			
		||||
            '@ChillMain/Workflow/index.html.twig',
 | 
			
		||||
            [
 | 
			
		||||
@@ -352,7 +332,7 @@ class WorkflowController extends AbstractController
 | 
			
		||||
                'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
 | 
			
		||||
                'entity_workflow' => $entityWorkflow,
 | 
			
		||||
                'transition_form_errors' => $errors,
 | 
			
		||||
                // 'comment_form' => $commentForm->createView(),
 | 
			
		||||
                'signatures' => $signatures,
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
@@ -371,4 +351,47 @@ class WorkflowController extends AbstractController
 | 
			
		||||
 | 
			
		||||
        return $lines;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/metadata', name: 'chill_main_workflow_signature_metadata')]
 | 
			
		||||
    public function addSignatureMetadata(int $signature_id, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        $signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id);
 | 
			
		||||
 | 
			
		||||
        if ($signature->getSigner() instanceof User) {
 | 
			
		||||
            return $this->redirectToRoute('signature_route_user');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
 | 
			
		||||
        $metadataForm->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
 | 
			
		||||
 | 
			
		||||
        $metadataForm->handleRequest($request);
 | 
			
		||||
 | 
			
		||||
        if ($metadataForm->isSubmitted() && $metadataForm->isValid()) {
 | 
			
		||||
            $data = $metadataForm->getData();
 | 
			
		||||
 | 
			
		||||
            $signature->setSignatureMetadata(
 | 
			
		||||
                [
 | 
			
		||||
                    'base_signer' => [
 | 
			
		||||
                        'document_type' => $data['documentType'],
 | 
			
		||||
                        'document_number' => $data['documentNumber'],
 | 
			
		||||
                        'expiration_date' => $data['expirationDate'],
 | 
			
		||||
                    ],
 | 
			
		||||
                ]
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            $this->entityManager->persist($signature);
 | 
			
		||||
            $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
            // Todo should redirect to document for actual signing? To be adjusted still
 | 
			
		||||
            return $this->redirectToRoute('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->render(
 | 
			
		||||
            '@ChillMain/Workflow/_signature_metadata.html.twig',
 | 
			
		||||
            [
 | 
			
		||||
                'metadata_form' => $metadataForm->createView(),
 | 
			
		||||
                'person' => $signature->getSigner(),
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -193,6 +193,11 @@ class ChillMainExtension extends Extension implements
 | 
			
		||||
                []
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $container->setParameter(
 | 
			
		||||
            'chill_main.workflow_signatures.base_signer.document_kinds',
 | 
			
		||||
            $config['workflow_signature']['base_signer']['document_kinds']
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
 | 
			
		||||
        $loader->load('services.yaml');
 | 
			
		||||
        $loader->load('services/doctrine.yaml');
 | 
			
		||||
 
 | 
			
		||||
@@ -277,6 +277,32 @@ class Configuration implements ConfigurationInterface
 | 
			
		||||
            ->end() // end of root
 | 
			
		||||
        ;
 | 
			
		||||
 | 
			
		||||
        $rootNode->children()
 | 
			
		||||
            ->arrayNode('workflow_signature')
 | 
			
		||||
            ->children()
 | 
			
		||||
            ->arrayNode('base_signer')
 | 
			
		||||
            ->children()
 | 
			
		||||
            ->arrayNode('document_kinds')
 | 
			
		||||
            ->arrayPrototype()
 | 
			
		||||
            ->children()
 | 
			
		||||
            ->scalarNode('key')->cannotBeEmpty()->end()
 | 
			
		||||
            ->arrayNode('labels')
 | 
			
		||||
            ->arrayPrototype()
 | 
			
		||||
            ->children()
 | 
			
		||||
            ->scalarNode('lang')->cannotBeEmpty()->end()
 | 
			
		||||
            ->scalarNode('label')->cannotBeEmpty()->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end();
 | 
			
		||||
 | 
			
		||||
        $rootNode->children()
 | 
			
		||||
            ->arrayNode('add_address')->addDefaultsIfNotSet()->children()
 | 
			
		||||
            ->scalarNode('default_country')->cannotBeEmpty()->defaultValue('BE')->end()
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,9 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
 | 
			
		||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
 | 
			
		||||
use Doctrine\Common\Collections\ArrayCollection;
 | 
			
		||||
use Doctrine\Common\Collections\Collection;
 | 
			
		||||
use Doctrine\Common\Collections\Order;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation as Serializer;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
@@ -34,35 +34,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
 | 
			
		||||
    use TrackUpdateTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * a list of future cc users for the next steps.
 | 
			
		||||
     *
 | 
			
		||||
     * @var array|User[]
 | 
			
		||||
     */
 | 
			
		||||
    public array $futureCcUsers = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * a list of future dest emails for the next steps.
 | 
			
		||||
     *
 | 
			
		||||
     * This is in used in order to let controller inform who will be the future emails which will validate
 | 
			
		||||
     * the next step. This is necessary to perform some computation about the next emails, before they are
 | 
			
		||||
     * associated to the entity EntityWorkflowStep.
 | 
			
		||||
     *
 | 
			
		||||
     * @var array|string[]
 | 
			
		||||
     */
 | 
			
		||||
    public array $futureDestEmails = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * a list of future dest users for the next steps.
 | 
			
		||||
     *
 | 
			
		||||
     * This is in used in order to let controller inform who will be the future users which will validate
 | 
			
		||||
     * the next step. This is necessary to perform some computation about the next users, before they are
 | 
			
		||||
     * associated to the entity EntityWorkflowStep.
 | 
			
		||||
     *
 | 
			
		||||
     * @var array|User[]
 | 
			
		||||
     */
 | 
			
		||||
    public array $futureDestUsers = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<EntityWorkflowComment>
 | 
			
		||||
     */
 | 
			
		||||
@@ -442,11 +413,23 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
     *
 | 
			
		||||
     * @return $this
 | 
			
		||||
     */
 | 
			
		||||
    public function setStep(string $step): self
 | 
			
		||||
    public function setStep(string $step, WorkflowTransitionContextDTO $transitionContextDTO): self
 | 
			
		||||
    {
 | 
			
		||||
        $newStep = new EntityWorkflowStep();
 | 
			
		||||
        $newStep->setCurrentStep($step);
 | 
			
		||||
 | 
			
		||||
        foreach ($transitionContextDTO->futureCcUsers as $user) {
 | 
			
		||||
            $newStep->addCcUser($user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($transitionContextDTO->futureDestUsers as $user) {
 | 
			
		||||
            $newStep->addDestUser($user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($transitionContextDTO->futureDestEmails as $email) {
 | 
			
		||||
            $newStep->addDestEmail($email);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // copy the freeze
 | 
			
		||||
        if ($this->isFreeze()) {
 | 
			
		||||
            $newStep->setFreezeAfter(true);
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
enum EntityWorkflowSignatureStateEnum: string
 | 
			
		||||
{
 | 
			
		||||
    case PENDING = 'pending';
 | 
			
		||||
    case SIGNED = 'signed';
 | 
			
		||||
    case REJECTED = 'rejected';
 | 
			
		||||
    case CANCELED = 'canceled';
 | 
			
		||||
}
 | 
			
		||||
@@ -42,19 +42,25 @@ class EntityWorkflowStep
 | 
			
		||||
    private array $destEmail = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<User>
 | 
			
		||||
     * @var Collection<int, User>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')]
 | 
			
		||||
    private Collection $destUser;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<User>
 | 
			
		||||
     * @var Collection<int, User>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')]
 | 
			
		||||
    private Collection $destUserByAccessKey;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection <int, EntityWorkflowStepSignature>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)]
 | 
			
		||||
    private Collection $signatures;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
 | 
			
		||||
    private ?EntityWorkflow $entityWorkflow = null;
 | 
			
		||||
 | 
			
		||||
@@ -97,6 +103,7 @@ class EntityWorkflowStep
 | 
			
		||||
        $this->ccUser = new ArrayCollection();
 | 
			
		||||
        $this->destUser = new ArrayCollection();
 | 
			
		||||
        $this->destUserByAccessKey = new ArrayCollection();
 | 
			
		||||
        $this->signatures = new ArrayCollection();
 | 
			
		||||
        $this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -136,6 +143,29 @@ class EntityWorkflowStep
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @internal use @see{EntityWorkflowStepSignature}'s constructor instead
 | 
			
		||||
     */
 | 
			
		||||
    public function addSignature(EntityWorkflowStepSignature $signature): self
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->signatures->contains($signature)) {
 | 
			
		||||
            $this->signatures[] = $signature;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function removeSignature(EntityWorkflowStepSignature $signature): self
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->signatures->contains($signature)) {
 | 
			
		||||
            $this->signatures->removeElement($signature);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $signature->detachEntityWorkflowStep();
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAccessKey(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->accessKey;
 | 
			
		||||
@@ -198,6 +228,14 @@ class EntityWorkflowStep
 | 
			
		||||
        return $this->entityWorkflow;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return Collection<int, EntityWorkflowStepSignature>
 | 
			
		||||
     */
 | 
			
		||||
    public function getSignatures(): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return $this->signatures;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,156 @@
 | 
			
		||||
<?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\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
 | 
			
		||||
#[ORM\Entity]
 | 
			
		||||
#[ORM\Table(name: 'chill_main_workflow_entity_step_signature')]
 | 
			
		||||
class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
{
 | 
			
		||||
    use TrackCreationTrait;
 | 
			
		||||
    use TrackUpdateTrait;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, unique: true)]
 | 
			
		||||
    #[ORM\GeneratedValue(strategy: 'AUTO')]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: true)]
 | 
			
		||||
    private ?User $userSigner = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: Person::class)]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: true)]
 | 
			
		||||
    private ?Person $personSigner = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 50, nullable: false, enumType: EntityWorkflowSignatureStateEnum::class)]
 | 
			
		||||
    private EntityWorkflowSignatureStateEnum $state = EntityWorkflowSignatureStateEnum::PENDING;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIMETZ_IMMUTABLE, nullable: true, options: ['default' => null])]
 | 
			
		||||
    private ?\DateTimeImmutable $stateDate = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
 | 
			
		||||
    private array $signatureMetadata = [];
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]
 | 
			
		||||
    private ?int $zoneSignatureIndex = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: false)]
 | 
			
		||||
    private ?EntityWorkflowStep $step = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        EntityWorkflowStep $step,
 | 
			
		||||
        User|Person $signer,
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->step = $step;
 | 
			
		||||
        $step->addSignature($this);
 | 
			
		||||
        $this->setSigner($signer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function setSigner(User|Person $signer): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($signer instanceof User) {
 | 
			
		||||
            $this->userSigner = $signer;
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->personSigner = $signer;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getStep(): EntityWorkflowStep
 | 
			
		||||
    {
 | 
			
		||||
        return $this->step;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getSigner(): User|Person
 | 
			
		||||
    {
 | 
			
		||||
        if (null !== $this->userSigner) {
 | 
			
		||||
            return $this->userSigner;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->personSigner;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getSignatureMetadata(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->signatureMetadata;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setSignatureMetadata(array $signatureMetadata): EntityWorkflowStepSignature
 | 
			
		||||
    {
 | 
			
		||||
        $this->signatureMetadata = $signatureMetadata;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getState(): EntityWorkflowSignatureStateEnum
 | 
			
		||||
    {
 | 
			
		||||
        return $this->state;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature
 | 
			
		||||
    {
 | 
			
		||||
        $this->state = $state;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getStateDate(): ?\DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->stateDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature
 | 
			
		||||
    {
 | 
			
		||||
        $this->stateDate = $stateDate;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getZoneSignatureIndex(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->zoneSignatureIndex;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature
 | 
			
		||||
    {
 | 
			
		||||
        $this->zoneSignatureIndex = $zoneSignatureIndex;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Detach from the @see{EntityWorkflowStep}.
 | 
			
		||||
     *
 | 
			
		||||
     * @internal used internally to remove the current signature
 | 
			
		||||
     *
 | 
			
		||||
     * @return $this
 | 
			
		||||
     */
 | 
			
		||||
    public function detachEntityWorkflowStep(): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->step = null;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,62 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Form;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillDateType;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
class WorkflowSignatureMetadataType extends AbstractType
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly ParameterBagInterface $parameterBag, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder, array $options): void
 | 
			
		||||
    {
 | 
			
		||||
        $documentTypeChoices = $this->parameterBag->get('chill_main.id_document_kinds');
 | 
			
		||||
 | 
			
		||||
        $choices = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($documentTypeChoices as $documentType) {
 | 
			
		||||
            $labels = [];
 | 
			
		||||
 | 
			
		||||
            foreach ($documentType['labels'] as $label) {
 | 
			
		||||
                $labels[$label['lang']] = $label['label'];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $localizedLabel = $this->translatableStringHelper->localize($labels);
 | 
			
		||||
            if (null !== $localizedLabel) {
 | 
			
		||||
                $choices[$localizedLabel] = $documentType['key'];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('documentType', ChoiceType::class, [
 | 
			
		||||
                'label' => 'workflow.signature_zone.metadata.docType',
 | 
			
		||||
                'expanded' => false,
 | 
			
		||||
                'required' => true,
 | 
			
		||||
                'choices' => $choices,
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('documentNumber', TextType::class, [
 | 
			
		||||
                'required' => true,
 | 
			
		||||
                'label' => 'workflow.signature_zone.metadata.docNumber',
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('expirationDate', ChillDateType::class, [
 | 
			
		||||
                'required' => true,
 | 
			
		||||
                'input' => 'datetime_immutable',
 | 
			
		||||
                'label' => 'workflow.signature_zone.metadata.docExpiration',
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,14 +12,12 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\MainBundle\Form;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
@@ -34,169 +32,151 @@ use Symfony\Component\Workflow\Transition;
 | 
			
		||||
 | 
			
		||||
class WorkflowStepType extends AbstractType
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly Registry $registry,
 | 
			
		||||
        private readonly TranslatableStringHelperInterface $translatableStringHelper
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder, array $options)
 | 
			
		||||
    {
 | 
			
		||||
        /** @var EntityWorkflow $entityWorkflow */
 | 
			
		||||
        $entityWorkflow = $options['entity_workflow'];
 | 
			
		||||
        $handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
 | 
			
		||||
        $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
 | 
			
		||||
        $place = $workflow->getMarking($entityWorkflow);
 | 
			
		||||
        $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]);
 | 
			
		||||
 | 
			
		||||
        if (true === $options['transition']) {
 | 
			
		||||
            if (null === $options['entity_workflow']) {
 | 
			
		||||
                throw new \LogicException('if transition is true, entity_workflow should be defined');
 | 
			
		||||
            }
 | 
			
		||||
        if (null === $options['entity_workflow']) {
 | 
			
		||||
            throw new \LogicException('if transition is true, entity_workflow should be defined');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            $transitions = $this->registry
 | 
			
		||||
                ->get($options['entity_workflow'], $entityWorkflow->getWorkflowName())
 | 
			
		||||
                ->getEnabledTransitions($entityWorkflow);
 | 
			
		||||
        $transitions = $this->registry
 | 
			
		||||
            ->get($options['entity_workflow'], $entityWorkflow->getWorkflowName())
 | 
			
		||||
            ->getEnabledTransitions($entityWorkflow);
 | 
			
		||||
 | 
			
		||||
            $choices = array_combine(
 | 
			
		||||
                array_map(
 | 
			
		||||
                    static fn (Transition $transition) => $transition->getName(),
 | 
			
		||||
                    $transitions
 | 
			
		||||
                ),
 | 
			
		||||
        $choices = array_combine(
 | 
			
		||||
            array_map(
 | 
			
		||||
                static fn (Transition $transition) => $transition->getName(),
 | 
			
		||||
                $transitions
 | 
			
		||||
            );
 | 
			
		||||
            ),
 | 
			
		||||
            $transitions
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
            if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) {
 | 
			
		||||
                $inputLabels = $placeMetadata['validationFilterInputLabels'];
 | 
			
		||||
        if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) {
 | 
			
		||||
            $inputLabels = $placeMetadata['validationFilterInputLabels'];
 | 
			
		||||
 | 
			
		||||
                $builder->add('transitionFilter', ChoiceType::class, [
 | 
			
		||||
                    'multiple' => false,
 | 
			
		||||
                    'label' => 'workflow.My decision',
 | 
			
		||||
                    'choices' => [
 | 
			
		||||
                        'forward' => 'forward',
 | 
			
		||||
                        'backward' => 'backward',
 | 
			
		||||
                        'neutral' => 'neutral',
 | 
			
		||||
                    ],
 | 
			
		||||
                    'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]),
 | 
			
		||||
                    'choice_attr' => static fn (string $key) => [
 | 
			
		||||
                        $key => $key,
 | 
			
		||||
                    ],
 | 
			
		||||
                    'mapped' => false,
 | 
			
		||||
                    'expanded' => true,
 | 
			
		||||
                    'data' => 'forward',
 | 
			
		||||
                ]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $builder
 | 
			
		||||
                ->add('transition', ChoiceType::class, [
 | 
			
		||||
                    'label' => 'workflow.Next step',
 | 
			
		||||
                    'mapped' => false,
 | 
			
		||||
                    'multiple' => false,
 | 
			
		||||
                    'expanded' => true,
 | 
			
		||||
                    'choices' => $choices,
 | 
			
		||||
                    'constraints' => [new NotNull()],
 | 
			
		||||
                    'choice_label' => function (Transition $transition) use ($workflow) {
 | 
			
		||||
                        $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
 | 
			
		||||
 | 
			
		||||
                        if (\array_key_exists('label', $meta)) {
 | 
			
		||||
                            return $this->translatableStringHelper->localize($meta['label']);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return $transition->getName();
 | 
			
		||||
                    },
 | 
			
		||||
                    'choice_attr' => static function (Transition $transition) use ($workflow) {
 | 
			
		||||
                        $toFinal = true;
 | 
			
		||||
                        $isForward = 'neutral';
 | 
			
		||||
 | 
			
		||||
                        $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
 | 
			
		||||
 | 
			
		||||
                        if (\array_key_exists('isForward', $metadata)) {
 | 
			
		||||
                            if ($metadata['isForward']) {
 | 
			
		||||
                                $isForward = 'forward';
 | 
			
		||||
                            } else {
 | 
			
		||||
                                $isForward = 'backward';
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        foreach ($transition->getTos() as $to) {
 | 
			
		||||
                            $meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
 | 
			
		||||
 | 
			
		||||
                            if (
 | 
			
		||||
                                !\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
 | 
			
		||||
                            ) {
 | 
			
		||||
                                $toFinal = false;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return [
 | 
			
		||||
                            'data-is-transition' => 'data-is-transition',
 | 
			
		||||
                            'data-to-final' => $toFinal ? '1' : '0',
 | 
			
		||||
                            'data-is-forward' => $isForward,
 | 
			
		||||
                        ];
 | 
			
		||||
                    },
 | 
			
		||||
                ])
 | 
			
		||||
                ->add('future_dest_users', PickUserDynamicType::class, [
 | 
			
		||||
                    'label' => 'workflow.dest for next steps',
 | 
			
		||||
                    'multiple' => true,
 | 
			
		||||
                    'mapped' => false,
 | 
			
		||||
                    'suggested' => $options['suggested_users'],
 | 
			
		||||
                ])
 | 
			
		||||
                ->add('future_cc_users', PickUserDynamicType::class, [
 | 
			
		||||
                    'label' => 'workflow.cc for next steps',
 | 
			
		||||
                    'multiple' => true,
 | 
			
		||||
                    'mapped' => false,
 | 
			
		||||
                    'required' => false,
 | 
			
		||||
                    'suggested' => $options['suggested_users'],
 | 
			
		||||
                ])
 | 
			
		||||
                ->add('future_dest_emails', ChillCollectionType::class, [
 | 
			
		||||
                    'label' => 'workflow.dest by email',
 | 
			
		||||
                    'help' => 'workflow.dest by email help',
 | 
			
		||||
                    'mapped' => false,
 | 
			
		||||
                    'allow_add' => true,
 | 
			
		||||
                    'entry_type' => EmailType::class,
 | 
			
		||||
                    'button_add_label' => 'workflow.Add an email',
 | 
			
		||||
                    'button_remove_label' => 'workflow.Remove an email',
 | 
			
		||||
                    'empty_collection_explain' => 'workflow.Any email',
 | 
			
		||||
                    'entry_options' => [
 | 
			
		||||
                        'constraints' => [
 | 
			
		||||
                            new NotNull(), new NotBlank(), new Email(),
 | 
			
		||||
                        ],
 | 
			
		||||
                        'label' => 'Email',
 | 
			
		||||
                    ],
 | 
			
		||||
                ]);
 | 
			
		||||
            $builder->add('transitionFilter', ChoiceType::class, [
 | 
			
		||||
                'multiple' => false,
 | 
			
		||||
                'label' => 'workflow.My decision',
 | 
			
		||||
                'choices' => [
 | 
			
		||||
                    'forward' => 'forward',
 | 
			
		||||
                    'backward' => 'backward',
 | 
			
		||||
                    'neutral' => 'neutral',
 | 
			
		||||
                ],
 | 
			
		||||
                'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]),
 | 
			
		||||
                'choice_attr' => static fn (string $key) => [
 | 
			
		||||
                    $key => $key,
 | 
			
		||||
                ],
 | 
			
		||||
                'mapped' => false,
 | 
			
		||||
                'expanded' => true,
 | 
			
		||||
                'data' => 'forward',
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            $handler->supportsFreeze($entityWorkflow)
 | 
			
		||||
            && !$entityWorkflow->isFreeze()
 | 
			
		||||
        ) {
 | 
			
		||||
            $builder
 | 
			
		||||
                ->add('freezeAfter', CheckboxType::class, [
 | 
			
		||||
                    'required' => false,
 | 
			
		||||
                    'label' => 'workflow.Freeze',
 | 
			
		||||
                    'help' => 'workflow.The associated element will be freezed',
 | 
			
		||||
                ]);
 | 
			
		||||
        }
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('transition', ChoiceType::class, [
 | 
			
		||||
                'label' => 'workflow.Next step',
 | 
			
		||||
                'mapped' => false,
 | 
			
		||||
                'multiple' => false,
 | 
			
		||||
                'expanded' => true,
 | 
			
		||||
                'choices' => $choices,
 | 
			
		||||
                'constraints' => [new NotNull()],
 | 
			
		||||
                'choice_label' => function (Transition $transition) use ($workflow) {
 | 
			
		||||
                    $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
 | 
			
		||||
 | 
			
		||||
                    if (\array_key_exists('label', $meta)) {
 | 
			
		||||
                        return $this->translatableStringHelper->localize($meta['label']);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return $transition->getName();
 | 
			
		||||
                },
 | 
			
		||||
                'choice_attr' => static function (Transition $transition) use ($workflow) {
 | 
			
		||||
                    $toFinal = true;
 | 
			
		||||
                    $isForward = 'neutral';
 | 
			
		||||
 | 
			
		||||
                    $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
 | 
			
		||||
 | 
			
		||||
                    if (\array_key_exists('isForward', $metadata)) {
 | 
			
		||||
                        if ($metadata['isForward']) {
 | 
			
		||||
                            $isForward = 'forward';
 | 
			
		||||
                        } else {
 | 
			
		||||
                            $isForward = 'backward';
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    foreach ($transition->getTos() as $to) {
 | 
			
		||||
                        $meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
 | 
			
		||||
 | 
			
		||||
                        if (
 | 
			
		||||
                            !\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
 | 
			
		||||
                        ) {
 | 
			
		||||
                            $toFinal = false;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return [
 | 
			
		||||
                        'data-is-transition' => 'data-is-transition',
 | 
			
		||||
                        'data-to-final' => $toFinal ? '1' : '0',
 | 
			
		||||
                        'data-is-forward' => $isForward,
 | 
			
		||||
                    ];
 | 
			
		||||
                },
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('futureDestUsers', PickUserDynamicType::class, [
 | 
			
		||||
                'label' => 'workflow.dest for next steps',
 | 
			
		||||
                'multiple' => true,
 | 
			
		||||
                'suggested' => $options['suggested_users'],
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('futureCcUsers', PickUserDynamicType::class, [
 | 
			
		||||
                'label' => 'workflow.cc for next steps',
 | 
			
		||||
                'multiple' => true,
 | 
			
		||||
                'required' => false,
 | 
			
		||||
                'suggested' => $options['suggested_users'],
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('futureDestEmails', ChillCollectionType::class, [
 | 
			
		||||
                'label' => 'workflow.dest by email',
 | 
			
		||||
                'help' => 'workflow.dest by email help',
 | 
			
		||||
                'allow_add' => true,
 | 
			
		||||
                'entry_type' => EmailType::class,
 | 
			
		||||
                'button_add_label' => 'workflow.Add an email',
 | 
			
		||||
                'button_remove_label' => 'workflow.Remove an email',
 | 
			
		||||
                'empty_collection_explain' => 'workflow.Any email',
 | 
			
		||||
                'entry_options' => [
 | 
			
		||||
                    'constraints' => [
 | 
			
		||||
                        new NotNull(), new NotBlank(), new Email(),
 | 
			
		||||
                    ],
 | 
			
		||||
                    'label' => 'Email',
 | 
			
		||||
                ],
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('comment', ChillTextareaType::class, [
 | 
			
		||||
                'required' => false,
 | 
			
		||||
                'label' => 'Comment',
 | 
			
		||||
                'empty_data' => '',
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function configureOptions(OptionsResolver $resolver)
 | 
			
		||||
    {
 | 
			
		||||
        $resolver
 | 
			
		||||
            ->setDefined('class')
 | 
			
		||||
            ->setRequired('transition')
 | 
			
		||||
            ->setAllowedTypes('transition', 'bool')
 | 
			
		||||
            ->setDefault('data_class', WorkflowTransitionContextDTO::class)
 | 
			
		||||
            ->setRequired('entity_workflow')
 | 
			
		||||
            ->setAllowedTypes('entity_workflow', EntityWorkflow::class)
 | 
			
		||||
            ->setDefault('suggested_users', [])
 | 
			
		||||
            ->setDefault('constraints', [
 | 
			
		||||
                new Callback(
 | 
			
		||||
                    function ($step, ExecutionContextInterface $context, $payload) {
 | 
			
		||||
                        /** @var EntityWorkflowStep $step */
 | 
			
		||||
                        $form = $context->getObject();
 | 
			
		||||
                        $workflow = $this->registry->get($step->getEntityWorkflow(), $step->getEntityWorkflow()->getWorkflowName());
 | 
			
		||||
                        $transition = $form['transition']->getData();
 | 
			
		||||
                    function (WorkflowTransitionContextDTO $step, ExecutionContextInterface $context, $payload) {
 | 
			
		||||
                        $workflow = $this->registry->get($step->entityWorkflow, $step->entityWorkflow->getWorkflowName());
 | 
			
		||||
                        $transition = $step->transition;
 | 
			
		||||
                        $toFinal = true;
 | 
			
		||||
 | 
			
		||||
                        if (null === $transition) {
 | 
			
		||||
@@ -212,8 +192,8 @@ class WorkflowStepType extends AbstractType
 | 
			
		||||
                                    $toFinal = false;
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            $destUsers = $form['future_dest_users']->getData();
 | 
			
		||||
                            $destEmails = $form['future_dest_emails']->getData();
 | 
			
		||||
                            $destUsers = $step->futureDestUsers;
 | 
			
		||||
                            $destEmails = $step->futureDestEmails;
 | 
			
		||||
 | 
			
		||||
                            if (!$toFinal && [] === $destUsers && [] === $destEmails) {
 | 
			
		||||
                                $context
 | 
			
		||||
@@ -224,20 +204,6 @@ class WorkflowStepType extends AbstractType
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
                new Callback(
 | 
			
		||||
                    function ($step, ExecutionContextInterface $context, $payload) {
 | 
			
		||||
                        $form = $context->getObject();
 | 
			
		||||
 | 
			
		||||
                        foreach ($form->get('future_dest_users')->getData() as $u) {
 | 
			
		||||
                            if (in_array($u, $form->get('future_cc_users')->getData(), true)) {
 | 
			
		||||
                                $context
 | 
			
		||||
                                    ->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step')
 | 
			
		||||
                                    ->atPath('ccUsers')
 | 
			
		||||
                                    ->addViolation();
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,54 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Repository\Workflow;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @template-implements ObjectRepository<EntityWorkflowStepSignature>
 | 
			
		||||
 */
 | 
			
		||||
class EntityWorkflowStepSignatureRepository implements ObjectRepository
 | 
			
		||||
{
 | 
			
		||||
    private \Doctrine\ORM\EntityRepository $repository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(EntityManagerInterface $entityManager)
 | 
			
		||||
    {
 | 
			
		||||
        $this->repository = $entityManager->getRepository(EntityWorkflowStepSignature::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function find($id): ?EntityWorkflowStepSignature
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->find($id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAll(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->findBy($criteria, $orderBy, $limit, $offset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findOneBy(array $criteria): ?EntityWorkflowStepSignature
 | 
			
		||||
    {
 | 
			
		||||
        return $this->findOneBy($criteria);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getClassName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return EntityWorkflowStepSignature::class;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -58,17 +58,15 @@
 | 
			
		||||
        {{ form_row(transition_form.transition) }}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    {% if transition_form.freezeAfter is defined %}
 | 
			
		||||
        {{ form_row(transition_form.freezeAfter) }}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <div id="futureDests">
 | 
			
		||||
        {{ form_row(transition_form.future_dest_users) }}
 | 
			
		||||
        {{ form_row(transition_form.futureDestUsers) }}
 | 
			
		||||
        {{ form_errors(transition_form.futureDestUsers) }}
 | 
			
		||||
 | 
			
		||||
        {{ form_row(transition_form.future_cc_users) }}
 | 
			
		||||
        {{ form_row(transition_form.futureCcUsers) }}
 | 
			
		||||
        {{ form_errors(transition_form.futureCcUsers) }}
 | 
			
		||||
 | 
			
		||||
        {{ form_row(transition_form.future_dest_emails) }}
 | 
			
		||||
        {{ form_errors(transition_form.future_dest_users) }}
 | 
			
		||||
        {{ form_row(transition_form.futureDestEmails) }}
 | 
			
		||||
        {{ form_errors(transition_form.futureDestEmails) }}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <p>{{ form_label(transition_form.comment) }}</p>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
<h2>{{ 'workflow.signature_zone.title'|trans }}</h2>
 | 
			
		||||
 | 
			
		||||
<div class="flex-table justify-content-center">
 | 
			
		||||
    <div class="item-bloc">
 | 
			
		||||
        {% for s in signatures %}
 | 
			
		||||
            <div class="item-row mb-2">
 | 
			
		||||
                    <div class="col-sm-6"><span>{{ s.signer|chill_entity_render_box }}</span></div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <a class="btn btn-show" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}">{{ 'workflow.signature_zone.button_sign'|trans }}</a>
 | 
			
		||||
                {% if s.state is same as('signed') %}
 | 
			
		||||
                    <p class="updatedBy">{{ s.stateDate }}</p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
{% extends '@ChillMain/layout.html.twig' %}
 | 
			
		||||
 | 
			
		||||
{% block title %}
 | 
			
		||||
    {{ 'Signature'|trans }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="col-10 workflow">
 | 
			
		||||
    <h1 class="mb-5">{{ 'workflow.signature_zone.metadata.sign_by'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname}) }}</h1>
 | 
			
		||||
 | 
			
		||||
    {% if metadata_form is not null %}
 | 
			
		||||
        {{ form_start(metadata_form) }}
 | 
			
		||||
        {{ form_row(metadata_form.documentType) }}
 | 
			
		||||
        {{ form_row(metadata_form.documentNumber) }}
 | 
			
		||||
        {{ form_row(metadata_form.expirationDate) }}
 | 
			
		||||
        <ul class="record_actions">
 | 
			
		||||
            <li>
 | 
			
		||||
            {{ form_widget(metadata_form.submit, { 'attr' : { 'class' : 'btn btn-submit' }} ) }}
 | 
			
		||||
            </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
        {{ form_end(metadata_form) }}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -57,6 +57,9 @@
 | 
			
		||||
    </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>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    <section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>{#
 | 
			
		||||
    <section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #}
 | 
			
		||||
    <section class="step my-4">{% include '@ChillMain/Workflow/_history.html.twig' %}</section>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Tests\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class EntityWorkflowStepSignatureTest extends KernelTestCase
 | 
			
		||||
{
 | 
			
		||||
    private EntityManagerInterface $entityManager;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
        $this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testConstruct()
 | 
			
		||||
    {
 | 
			
		||||
        $workflow = new EntityWorkflow();
 | 
			
		||||
        $workflow->setWorkflowName('vendee_internal')
 | 
			
		||||
            ->setRelatedEntityId(0)
 | 
			
		||||
            ->setRelatedEntityClass(AccompanyingPeriodWorkEvaluationDocument::class);
 | 
			
		||||
 | 
			
		||||
        $step = $workflow->getCurrentStep();
 | 
			
		||||
 | 
			
		||||
        $person = $this->entityManager->createQuery('SELECT p FROM '.Person::class.' p')
 | 
			
		||||
            ->setMaxResults(1)
 | 
			
		||||
            ->getSingleResult();
 | 
			
		||||
 | 
			
		||||
        $signature = new EntityWorkflowStepSignature($step, $person);
 | 
			
		||||
 | 
			
		||||
        self::assertCount(1, $step->getSignatures());
 | 
			
		||||
        self::assertSame($signature, $step->getSignatures()->first());
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->getConnection()->beginTransaction();
 | 
			
		||||
        $this->entityManager->persist($workflow);
 | 
			
		||||
        $this->entityManager->persist($step);
 | 
			
		||||
        $this->entityManager->persist($signature);
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->flush();
 | 
			
		||||
        $this->entityManager->getConnection()->commit();
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->clear();
 | 
			
		||||
 | 
			
		||||
        $signatureBis = $this->entityManager->find(EntityWorkflowStepSignature::class, $signature->getId());
 | 
			
		||||
 | 
			
		||||
        self::assertEquals($signature->getId(), $signatureBis->getId());
 | 
			
		||||
        self::assertEquals($step->getId(), $signatureBis->getStep()->getId());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\MainBundle\Tests\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -25,7 +26,7 @@ final class EntityWorkflowTest extends TestCase
 | 
			
		||||
    {
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow->setStep('final');
 | 
			
		||||
        $entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow));
 | 
			
		||||
        $entityWorkflow->getCurrentStep()->setIsFinal(true);
 | 
			
		||||
 | 
			
		||||
        $this->assertTrue($entityWorkflow->isFinal());
 | 
			
		||||
@@ -37,16 +38,16 @@ final class EntityWorkflowTest extends TestCase
 | 
			
		||||
 | 
			
		||||
        $this->assertFalse($entityWorkflow->isFinal());
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow->setStep('two');
 | 
			
		||||
        $entityWorkflow->setStep('two', new WorkflowTransitionContextDTO($entityWorkflow));
 | 
			
		||||
 | 
			
		||||
        $this->assertFalse($entityWorkflow->isFinal());
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow->setStep('previous_final');
 | 
			
		||||
        $entityWorkflow->setStep('previous_final', new WorkflowTransitionContextDTO($entityWorkflow));
 | 
			
		||||
 | 
			
		||||
        $this->assertFalse($entityWorkflow->isFinal());
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow->getCurrentStep()->setIsFinal(true);
 | 
			
		||||
        $entityWorkflow->setStep('final');
 | 
			
		||||
        $entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow));
 | 
			
		||||
 | 
			
		||||
        $this->assertTrue($entityWorkflow->isFinal());
 | 
			
		||||
    }
 | 
			
		||||
@@ -57,20 +58,20 @@ final class EntityWorkflowTest extends TestCase
 | 
			
		||||
 | 
			
		||||
        $this->assertFalse($entityWorkflow->isFreeze());
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow->setStep('step_one');
 | 
			
		||||
        $entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow));
 | 
			
		||||
 | 
			
		||||
        $this->assertFalse($entityWorkflow->isFreeze());
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow->setStep('step_three');
 | 
			
		||||
        $entityWorkflow->setStep('step_three', new WorkflowTransitionContextDTO($entityWorkflow));
 | 
			
		||||
 | 
			
		||||
        $this->assertFalse($entityWorkflow->isFreeze());
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow->setStep('freezed');
 | 
			
		||||
        $entityWorkflow->setStep('freezed', new WorkflowTransitionContextDTO($entityWorkflow));
 | 
			
		||||
        $entityWorkflow->getCurrentStep()->setFreezeAfter(true);
 | 
			
		||||
 | 
			
		||||
        $this->assertTrue($entityWorkflow->isFreeze());
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow->setStep('after_freeze');
 | 
			
		||||
        $entityWorkflow->setStep('after_freeze', new WorkflowTransitionContextDTO($entityWorkflow));
 | 
			
		||||
 | 
			
		||||
        $this->assertTrue($entityWorkflow->isFreeze());
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
<?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\Workflow;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
 | 
			
		||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Symfony\Component\Workflow\Marking;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class EntityWorkflowMarkingStoreTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    public function testGetMarking(): void
 | 
			
		||||
    {
 | 
			
		||||
        $markingStore = $this->buildMarkingStore();
 | 
			
		||||
        $workflow = new EntityWorkflow();
 | 
			
		||||
 | 
			
		||||
        $marking = $markingStore->getMarking($workflow);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(['initial' => 1], $marking->getPlaces());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testSetMarking(): void
 | 
			
		||||
    {
 | 
			
		||||
        $markingStore = $this->buildMarkingStore();
 | 
			
		||||
        $workflow = new EntityWorkflow();
 | 
			
		||||
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($workflow);
 | 
			
		||||
        $dto->futureCcUsers[] = $user1 = new User();
 | 
			
		||||
        $dto->futureDestUsers[] = $user2 = new User();
 | 
			
		||||
        $dto->futureDestEmails[] = $email = 'test@example.com';
 | 
			
		||||
 | 
			
		||||
        $markingStore->setMarking($workflow, new Marking(['foo' => 1]), ['context' => $dto]);
 | 
			
		||||
 | 
			
		||||
        $currentStep = $workflow->getCurrentStep();
 | 
			
		||||
        self::assertEquals('foo', $currentStep->getCurrentStep());
 | 
			
		||||
        self::assertContains($email, $currentStep->getDestEmail());
 | 
			
		||||
        self::assertContains($user1, $currentStep->getCcUser());
 | 
			
		||||
        self::assertContains($user2, $currentStep->getDestUser());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildMarkingStore(): EntityWorkflowMarkingStore
 | 
			
		||||
    {
 | 
			
		||||
        return new EntityWorkflowMarkingStore();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -49,6 +49,4 @@ interface EntityWorkflowHandlerInterface
 | 
			
		||||
    public function isObjectSupported(object $object): bool;
 | 
			
		||||
 | 
			
		||||
    public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool;
 | 
			
		||||
 | 
			
		||||
    public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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\MainBundle\Workflow;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Symfony\Component\Workflow\Marking;
 | 
			
		||||
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class EntityWorkflowMarkingStore implements MarkingStoreInterface
 | 
			
		||||
{
 | 
			
		||||
    public function getMarking(object $subject): Marking
 | 
			
		||||
    {
 | 
			
		||||
        if (!$subject instanceof EntityWorkflow) {
 | 
			
		||||
            throw new \UnexpectedValueException('Expected instance of EntityWorkflow');
 | 
			
		||||
        }
 | 
			
		||||
        $step = $subject->getCurrentStep();
 | 
			
		||||
 | 
			
		||||
        return new Marking([$step->getCurrentStep() => 1]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setMarking(object $subject, Marking $marking, array $context = []): void
 | 
			
		||||
    {
 | 
			
		||||
        if (!$subject instanceof EntityWorkflow) {
 | 
			
		||||
            throw new \UnexpectedValueException('Expected instance of EntityWorkflow');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $places = $marking->getPlaces();
 | 
			
		||||
        if (1 < count($places)) {
 | 
			
		||||
            throw new \LogicException('Expected maximum one place');
 | 
			
		||||
        }
 | 
			
		||||
        $next = array_keys($places)[0];
 | 
			
		||||
 | 
			
		||||
        $transitionDTO = $context['context'] ?? null;
 | 
			
		||||
        if (!$transitionDTO instanceof WorkflowTransitionContextDTO) {
 | 
			
		||||
            throw new \UnexpectedValueException(sprintf('Expected instance of %s', WorkflowTransitionContextDTO::class));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $subject->setStep($next, $transitionDTO);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -21,31 +21,13 @@ use Symfony\Component\Workflow\Event\Event;
 | 
			
		||||
use Symfony\Component\Workflow\Event\GuardEvent;
 | 
			
		||||
use Symfony\Component\Workflow\TransitionBlocker;
 | 
			
		||||
 | 
			
		||||
class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
 | 
			
		||||
final readonly class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly LoggerInterface $chillLogger, private readonly Security $security, private readonly UserRender $userRender) {}
 | 
			
		||||
 | 
			
		||||
    public function addDests(Event $event): void
 | 
			
		||||
    {
 | 
			
		||||
        if (!$event->getSubject() instanceof EntityWorkflow) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /** @var EntityWorkflow $entityWorkflow */
 | 
			
		||||
        $entityWorkflow = $event->getSubject();
 | 
			
		||||
 | 
			
		||||
        foreach ($entityWorkflow->futureCcUsers as $user) {
 | 
			
		||||
            $entityWorkflow->getCurrentStep()->addCcUser($user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($entityWorkflow->futureDestUsers as $user) {
 | 
			
		||||
            $entityWorkflow->getCurrentStep()->addDestUser($user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($entityWorkflow->futureDestEmails as $email) {
 | 
			
		||||
            $entityWorkflow->getCurrentStep()->addDestEmail($email);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private LoggerInterface $chillLogger,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private UserRender $userRender
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public static function getSubscribedEvents(): array
 | 
			
		||||
    {
 | 
			
		||||
@@ -53,7 +35,6 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
 | 
			
		||||
            'workflow.transition' => 'onTransition',
 | 
			
		||||
            'workflow.completed' => [
 | 
			
		||||
                ['markAsFinal', 2048],
 | 
			
		||||
                ['addDests', 2048],
 | 
			
		||||
            ],
 | 
			
		||||
            'workflow.guard' => [
 | 
			
		||||
                ['guardEntityWorkflow', 0],
 | 
			
		||||
@@ -99,6 +80,10 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
 | 
			
		||||
 | 
			
		||||
    public function markAsFinal(Event $event): void
 | 
			
		||||
    {
 | 
			
		||||
        // NOTE: it is not possible to move this method to the marking store, because
 | 
			
		||||
        // there is dependency between the Workflow definition and the MarkingStoreInterface (the workflow
 | 
			
		||||
        // constructor need a MarkingStoreInterface)
 | 
			
		||||
 | 
			
		||||
        if (!$event->getSubject() instanceof EntityWorkflow) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,13 @@ use Symfony\Component\Workflow\Registry;
 | 
			
		||||
 | 
			
		||||
class NotificationOnTransition implements EventSubscriberInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly EntityManagerInterface $entityManager, private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Security $security, private readonly Registry $registry) {}
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly EntityManagerInterface $entityManager,
 | 
			
		||||
        private readonly \Twig\Environment $engine,
 | 
			
		||||
        private readonly MetadataExtractor $metadataExtractor,
 | 
			
		||||
        private readonly Security $security,
 | 
			
		||||
        private readonly Registry $registry
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public static function getSubscribedEvents(): array
 | 
			
		||||
    {
 | 
			
		||||
@@ -85,7 +91,10 @@ class NotificationOnTransition implements EventSubscriberInterface
 | 
			
		||||
                'dest' => $subscriber,
 | 
			
		||||
                'place' => $place,
 | 
			
		||||
                'workflow' => $workflow,
 | 
			
		||||
                'is_dest' => \in_array($subscriber->getId(), array_map(static fn (User $u) => $u->getId(), $entityWorkflow->futureDestUsers), true),
 | 
			
		||||
                'is_dest' => \in_array($subscriber->getId(), array_map(
 | 
			
		||||
                    static fn (User $u) => $u->getId(),
 | 
			
		||||
                    $entityWorkflow->getCurrentStep()->getDestUser()->toArray()
 | 
			
		||||
                ), true),
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            $notification = new Notification();
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,13 @@ use Symfony\Component\Workflow\Registry;
 | 
			
		||||
 | 
			
		||||
class SendAccessKeyEventSubscriber
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Registry $registry, private readonly EntityWorkflowManager $entityWorkflowManager, private readonly MailerInterface $mailer) {}
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly \Twig\Environment $engine,
 | 
			
		||||
        private readonly MetadataExtractor $metadataExtractor,
 | 
			
		||||
        private readonly Registry $registry,
 | 
			
		||||
        private readonly EntityWorkflowManager $entityWorkflowManager,
 | 
			
		||||
        private readonly MailerInterface $mailer
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function postPersist(EntityWorkflowStep $step): void
 | 
			
		||||
    {
 | 
			
		||||
@@ -32,7 +38,7 @@ class SendAccessKeyEventSubscriber
 | 
			
		||||
        );
 | 
			
		||||
        $handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
 | 
			
		||||
 | 
			
		||||
        foreach ($entityWorkflow->futureDestEmails as $emailAddress) {
 | 
			
		||||
        foreach ($step->getDestEmail() as $emailAddress) {
 | 
			
		||||
            $context = [
 | 
			
		||||
                'entity_workflow' => $entityWorkflow,
 | 
			
		||||
                'dest' => $emailAddress,
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,74 @@
 | 
			
		||||
<?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\Workflow;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
 | 
			
		||||
use Symfony\Component\Workflow\Transition;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Context for a transition on an workflow entity.
 | 
			
		||||
 */
 | 
			
		||||
class WorkflowTransitionContextDTO
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * a list of future dest users for the next steps.
 | 
			
		||||
     *
 | 
			
		||||
     * This is in used in order to let controller inform who will be the future users which will validate
 | 
			
		||||
     * the next step. This is necessary to perform some computation about the next users, before they are
 | 
			
		||||
     * associated to the entity EntityWorkflowStep.
 | 
			
		||||
     *
 | 
			
		||||
     * @var array|User[]
 | 
			
		||||
     */
 | 
			
		||||
    public array $futureDestUsers = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * a list of future cc users for the next steps.
 | 
			
		||||
     *
 | 
			
		||||
     * @var array|User[]
 | 
			
		||||
     */
 | 
			
		||||
    public array $futureCcUsers = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * a list of future dest emails for the next steps.
 | 
			
		||||
     *
 | 
			
		||||
     * This is in used in order to let controller inform who will be the future emails which will validate
 | 
			
		||||
     * the next step. This is necessary to perform some computation about the next emails, before they are
 | 
			
		||||
     * associated to the entity EntityWorkflowStep.
 | 
			
		||||
     *
 | 
			
		||||
     * @var array|string[]
 | 
			
		||||
     */
 | 
			
		||||
    public array $futureDestEmails = [];
 | 
			
		||||
 | 
			
		||||
    public ?Transition $transition = null;
 | 
			
		||||
 | 
			
		||||
    public string $comment = '';
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        public EntityWorkflow $entityWorkflow
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Assert\Callback()]
 | 
			
		||||
    public function validateCCUserIsNotInDest(ExecutionContextInterface $context, $payload): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($this->futureDestUsers as $u) {
 | 
			
		||||
            if (in_array($u, $this->futureCcUsers, true)) {
 | 
			
		||||
                $context
 | 
			
		||||
                    ->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step')
 | 
			
		||||
                    ->atPath('ccUsers')
 | 
			
		||||
                    ->addViolation();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -133,6 +133,8 @@ services:
 | 
			
		||||
 | 
			
		||||
    Chill\MainBundle\Form\WorkflowStepType: ~
 | 
			
		||||
 | 
			
		||||
    Chill\MainBundle\Form\WorkflowSignatureMetadataType: ~
 | 
			
		||||
 | 
			
		||||
    Chill\MainBundle\Form\DataMapper\PrivateCommentDataMapper:
 | 
			
		||||
        autowire: true
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
<?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\Main;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20240628095159 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Add signatures to workflow';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('CREATE SEQUENCE chill_main_workflow_entity_step_signature_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
 | 
			
		||||
        $this->addSql('CREATE TABLE chill_main_workflow_entity_step_signature (id INT NOT NULL, step_id INT NOT NULL, '.
 | 
			
		||||
            'state VARCHAR(50) NOT NULL, stateDate TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, signatureMetadata JSON DEFAULT \'[]\' NOT NULL,'.
 | 
			
		||||
            ' zoneSignatureIndex INT DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,'.
 | 
			
		||||
            ' userSigner_id INT DEFAULT NULL, personSigner_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_C47D4BA3D934E3A4 ON chill_main_workflow_entity_step_signature (userSigner_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_C47D4BA3ADFFA293 ON chill_main_workflow_entity_step_signature (personSigner_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_C47D4BA373B21E9C ON chill_main_workflow_entity_step_signature (step_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_C47D4BA33174800F ON chill_main_workflow_entity_step_signature (createdBy_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_C47D4BA365FF1AEC ON chill_main_workflow_entity_step_signature (updatedBy_id)');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.stateDate IS \'(DC2Type:datetimetz_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.createdAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.updatedAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3D934E3A4 FOREIGN KEY (userSigner_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3ADFFA293 FOREIGN KEY (personSigner_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA373B21E9C FOREIGN KEY (step_id) REFERENCES chill_main_workflow_entity_step (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA33174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA365FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('DROP SEQUENCE chill_main_workflow_entity_step_signature_id_seq CASCADE');
 | 
			
		||||
        $this->addSql('DROP TABLE chill_main_workflow_entity_step_signature');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -528,6 +528,15 @@ workflow:
 | 
			
		||||
    This link grant any user to apply a transition: Le lien d'accès suivant permet d'appliquer une transition
 | 
			
		||||
    The workflow may be accssed through this link: Une transition peut être appliquée sur ce workflow grâce au lien d'accès suivant
 | 
			
		||||
 | 
			
		||||
    signature_zone:
 | 
			
		||||
        title: Appliquer les signatures électroniques
 | 
			
		||||
        button_sign: Signer
 | 
			
		||||
        metadata:
 | 
			
		||||
            sign_by: 'Signature pour %name%'
 | 
			
		||||
            docType: Type de document
 | 
			
		||||
            docNumber: Numéro de document
 | 
			
		||||
            docExpiration: Date d'expiration
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Subscribe final: Recevoir une notification à l'étape finale
 | 
			
		||||
Subscribe all steps: Recevoir une notification à chaque étape
 | 
			
		||||
 
 | 
			
		||||
@@ -123,9 +123,4 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW
 | 
			
		||||
    {
 | 
			
		||||
        return AccompanyingPeriodWorkEvaluationDocument::class === $entityWorkflow->getRelatedEntityClass();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -109,9 +109,4 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH
 | 
			
		||||
    {
 | 
			
		||||
        return AccompanyingPeriodWorkEvaluation::class === $entityWorkflow->getRelatedEntityClass();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -116,9 +116,4 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte
 | 
			
		||||
    {
 | 
			
		||||
        return AccompanyingPeriodWork::class === $entityWorkflow->getRelatedEntityClass();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										294
									
								
								tests/app/config/packages/workflow_chill.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								tests/app/config/packages/workflow_chill.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,294 @@
 | 
			
		||||
framework:
 | 
			
		||||
    workflows:
 | 
			
		||||
        vendee_internal:
 | 
			
		||||
            type: state_machine
 | 
			
		||||
            metadata:
 | 
			
		||||
                related_entity:
 | 
			
		||||
                        - Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument
 | 
			
		||||
                        - Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork
 | 
			
		||||
                        - Chill\DocStoreBundle\Entity\AccompanyingCourseDocument
 | 
			
		||||
                label:
 | 
			
		||||
                    fr: 'Suivi'
 | 
			
		||||
            support_strategy: Chill\MainBundle\Workflow\RelatedEntityWorkflowSupportsStrategy
 | 
			
		||||
            initial_marking: 'initial'
 | 
			
		||||
            marking_store:
 | 
			
		||||
                property: step
 | 
			
		||||
                type: method
 | 
			
		||||
            places:
 | 
			
		||||
                initial:
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: Étape initiale
 | 
			
		||||
                attenteModification:
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: En attente de modification du document
 | 
			
		||||
                        validationFilterInputLabels:
 | 
			
		||||
                            forward: {fr: Modification effectuée}
 | 
			
		||||
                            backward: {fr: Pas de modification effectuée}
 | 
			
		||||
                            neutral: {fr: Autre}
 | 
			
		||||
                attenteMiseEnForme:
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: En attente de mise en forme
 | 
			
		||||
                        validationFilterInputLabels:
 | 
			
		||||
                            forward: {fr: Mise en forme terminée}
 | 
			
		||||
                            backward: {fr: Pas de mise en forme effectuée}
 | 
			
		||||
                            neutral: {fr: Autre}
 | 
			
		||||
                attenteVisa:
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: En attente de visa
 | 
			
		||||
                        validationFilterInputLabels:
 | 
			
		||||
                            forward: {fr: Visa accordé}
 | 
			
		||||
                            backward: {fr: Visa refusé}
 | 
			
		||||
                            neutral: {fr: Autre}
 | 
			
		||||
                attenteSignature:
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: En attente de signature
 | 
			
		||||
                        validationFilterInputLabels:
 | 
			
		||||
                            forward: {fr: Signature accordée}
 | 
			
		||||
                            backward: {fr: Signature refusée}
 | 
			
		||||
                            neutral: {fr: Autre}
 | 
			
		||||
                attenteTraitement:
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: En attente de traitement
 | 
			
		||||
                        validationFilterInputLabels:
 | 
			
		||||
                            forward: {fr: Traitement terminé favorablement}
 | 
			
		||||
                            backward: {fr: Traitement terminé défavorablement}
 | 
			
		||||
                            neutral: {fr: Autre}
 | 
			
		||||
                attenteEnvoi:
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: En attente d'envoi
 | 
			
		||||
                        validationFilterInputLabels:
 | 
			
		||||
                            forward: {fr: Document envoyé}
 | 
			
		||||
                            backward: {fr: Document non envoyé}
 | 
			
		||||
                            neutral: {fr: Autre}
 | 
			
		||||
                attenteValidationMiseEnForme:
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: En attente de validation de la mise en forme
 | 
			
		||||
                        validationFilterInputLabels:
 | 
			
		||||
                            forward: {fr: Validation de la mise en forme}
 | 
			
		||||
                            backward: {fr: Refus de validation de la mise en forme}
 | 
			
		||||
                            neutral: {fr: Autre}
 | 
			
		||||
                annule:
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        isFinal: true
 | 
			
		||||
                        isFinalPositive: false
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: Annulé
 | 
			
		||||
                final:
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        isFinal: true
 | 
			
		||||
                        isFinalPositive: true
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: Finalisé
 | 
			
		||||
            transitions:
 | 
			
		||||
                # transition qui avancent
 | 
			
		||||
                demandeModificationDocument:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - initial
 | 
			
		||||
                    to: attenteModification
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: Demande de modification du document
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                demandeMiseEnForme:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - initial
 | 
			
		||||
                        - attenteModification
 | 
			
		||||
                    to: attenteMiseEnForme
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: Demande de mise en forme
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                demandeValidationMiseEnForme:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteMiseEnForme
 | 
			
		||||
                    to: attenteValidationMiseEnForme
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: Demande de validation de la mise en forme
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                demandeVisa:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - initial
 | 
			
		||||
                        - attenteModification
 | 
			
		||||
                        - attenteMiseEnForme
 | 
			
		||||
                        - attenteValidationMiseEnForme
 | 
			
		||||
                    to: attenteVisa
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: Demande de visa
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                demandeSignature:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - initial
 | 
			
		||||
                        - attenteModification
 | 
			
		||||
                        - attenteMiseEnForme
 | 
			
		||||
                        - attenteValidationMiseEnForme
 | 
			
		||||
                        - attenteVisa
 | 
			
		||||
                    to: attenteSignature
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Demande de signature}
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                demandeTraitement:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - initial
 | 
			
		||||
                        - attenteModification
 | 
			
		||||
                        - attenteMiseEnForme
 | 
			
		||||
                        - attenteValidationMiseEnForme
 | 
			
		||||
                        - attenteVisa
 | 
			
		||||
                        - attenteSignature
 | 
			
		||||
                    to: attenteTraitement
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Demande de traitement}
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                demandeEnvoi:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - initial
 | 
			
		||||
                        - attenteModification
 | 
			
		||||
                        - attenteMiseEnForme
 | 
			
		||||
                        - attenteValidationMiseEnForme
 | 
			
		||||
                        - attenteVisa
 | 
			
		||||
                        - attenteSignature
 | 
			
		||||
                        - attenteTraitement
 | 
			
		||||
                    to: attenteEnvoi
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Demande d'envoi}
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                annulation:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - initial
 | 
			
		||||
                        - attenteModification
 | 
			
		||||
                        - attenteMiseEnForme
 | 
			
		||||
                        - attenteValidationMiseEnForme
 | 
			
		||||
                        - attenteVisa
 | 
			
		||||
                        - attenteSignature
 | 
			
		||||
                        - attenteTraitement
 | 
			
		||||
                        - attenteEnvoi
 | 
			
		||||
                    to: annule
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Annulation}
 | 
			
		||||
                        isForward: false
 | 
			
		||||
                # transitions qui répètent l'étape
 | 
			
		||||
                demandeMiseEnFormeSupplementaire:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteMiseEnForme
 | 
			
		||||
                        - attenteValidationMiseEnForme
 | 
			
		||||
                    to: attenteMiseEnForme
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Demande de mise en forme supplémentaire}
 | 
			
		||||
                demandeVisaSupplementaire:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteVisa
 | 
			
		||||
                    to: attenteVisa
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Demande de visa supplémentaire}
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                demandeSignatureSupplementaire:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteSignature
 | 
			
		||||
                    to: attenteSignature
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Demande de signature supplémentaire}
 | 
			
		||||
                demandeTraitementSupplementaire:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteTraitement
 | 
			
		||||
                    to: attenteTraitement
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Demande de traitement supplémentaire}
 | 
			
		||||
                # transitions qui renvoient vers une étape précédente
 | 
			
		||||
                refusEtModificationDocument:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteVisa
 | 
			
		||||
                        - attenteSignature
 | 
			
		||||
                        - attenteTraitement
 | 
			
		||||
                        - attenteEnvoi
 | 
			
		||||
                    to: attenteModification
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label:
 | 
			
		||||
                            fr: Refus et demande de modification du document
 | 
			
		||||
                        isForward: false
 | 
			
		||||
                refusEtDemandeMiseEnForme:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteVisa
 | 
			
		||||
                        - attenteSignature
 | 
			
		||||
                        - attenteTraitement
 | 
			
		||||
                        - attenteEnvoi
 | 
			
		||||
                    to: attenteMiseEnForme
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Refus et demande de mise en forme}
 | 
			
		||||
                        isForward: false
 | 
			
		||||
                refusEtDemandeVisa:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteSignature
 | 
			
		||||
                        - attenteTraitement
 | 
			
		||||
                        - attenteEnvoi
 | 
			
		||||
                    to: attenteVisa
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Refus et demande de visa}
 | 
			
		||||
                        isForward: false
 | 
			
		||||
                refusEtDemandeSignature:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteTraitement
 | 
			
		||||
                        - attenteEnvoi
 | 
			
		||||
                    to: attenteSignature
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Refus et demande de signature}
 | 
			
		||||
                        isForward: false
 | 
			
		||||
                refusEtDemandeTraitement:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteEnvoi
 | 
			
		||||
                    to: attenteTraitement
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Refus et demande de traitement}
 | 
			
		||||
                        isForward: false
 | 
			
		||||
                # transition vers final
 | 
			
		||||
                initialToFinal:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - initial
 | 
			
		||||
                    to: final
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Clotûre immédiate et cloture positive}
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                attenteMiseEnFormeToFinal:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteMiseEnForme
 | 
			
		||||
                        - attenteValidationMiseEnForme
 | 
			
		||||
                    to: final
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Mise en forme terminée et cloture positive}
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                attenteVisaToFinal:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteVisa
 | 
			
		||||
                    to: final
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Accorde le visa et cloture positive}
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                attenteSignatureToFinal:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteSignature
 | 
			
		||||
                    to: final
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Accorde la signature et cloture positive}
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                attenteTraitementToFinal:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteTraitement
 | 
			
		||||
                    to: final
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Traitement terminé et cloture postive}
 | 
			
		||||
                        isForward: true
 | 
			
		||||
                attenteEnvoiToFinal:
 | 
			
		||||
                    from:
 | 
			
		||||
                        - attenteEnvoi
 | 
			
		||||
                    to: final
 | 
			
		||||
                    metadata:
 | 
			
		||||
                        label: {fr: Envoyé et cloture postive}
 | 
			
		||||
                        isForward: true
 | 
			
		||||
		Reference in New Issue
	
	Block a user