mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-04 11:18:25 +00:00 
			
		
		
		
	Compare commits
	
		
			110 Commits
		
	
	
		
			v4.0.0
			...
			move-docum
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						0541995a60
	
				 | 
					
					
						|||
| 
						
						
							
						
						29e054bd10
	
				 | 
					
					
						|||
| 
						
						
							
						
						da0099aafc
	
				 | 
					
					
						|||
| 
						
						
							
						
						3a18ea42fe
	
				 | 
					
					
						|||
| e60435b8cc | |||
| ab6ab19499 | |||
| 2a1762ea8d | |||
| 18ababbca9 | |||
| f6179cd3a3 | |||
| ddf8da4cee | |||
| bf2181c2f1 | |||
| d508fde8d2 | |||
| 14dba22181 | |||
| 7dc7e77c62 | |||
| 9d58904969 | |||
| 4d90c7028f | |||
| 3abb76d268 | |||
| d62dd4396e | |||
| 59e8d9d516 | |||
| 7dcb8abe38 | |||
| a0b2d92ba2 | |||
| 7843e5dfd1 | |||
| 32c847267b | |||
| c0826bc65c | |||
| 904f4e5ed9 | |||
| 481f82b4c7 | |||
| f5668592ca | |||
| 9b353f4d1b | |||
| 81a858f07a | |||
| 6a2ee232a9 | |||
| 56c43a0a76 | |||
| aa085a1562 | |||
| 2754251fdc | |||
| 2f6cef4238 | |||
| 2309636eae | |||
| 56ec8fb516 | |||
| fe6e6e54c1 | |||
| 2a09594b4a | |||
| 7c798e1f63 | |||
| ab8da4ab7a | |||
| 5bdb2df929 | |||
| e3a6b60fa2 | |||
| 5f01673404 | |||
| 63d0a52ea1 | |||
| 837089ff5d | |||
| f383fab578 | |||
| f3cc4a89af | |||
| 703f5dc32d | |||
| b870e71f77 | |||
| 
						
						
							
						
						a7e278204f
	
				 | 
					
					
						|||
| 
						
						
							
						
						4cfdcb2f02
	
				 | 
					
					
						|||
| eb724a730c | |||
| 18f98b6795 | |||
| d73994edd0 | |||
| 70603570c8 | |||
| df09dd2017 | |||
| 1c87280b1e | |||
| 445e093a28 | |||
| 3f91c65b30 | |||
| 9bc3c16b58 | |||
| 12dff82248 | |||
| ab23a4efb5 | |||
| 204fb20475 | |||
| f430d97152 | |||
| 4fa4d3b65c | |||
| bd4c34cc1d | |||
| 4cea678e93 | |||
| 5e6833975b | |||
| f523b9adb3 | |||
| a211549432 | |||
| 17b1363113 | |||
| 3356ed8e57 | |||
| 2a7fa517ee | |||
| 85781c8e14 | |||
| 00eb435896 | |||
| ed71cffd6a | |||
| ae679e6997 | |||
| e1d308fd97 | |||
| d9acda67e3 | |||
| e88da74882 | |||
| 591c44d1a0 | |||
| bf04b7981c | |||
| df33eec30f | |||
| c657c98918 | |||
| ef5eb5b907 | |||
| d683fe002d | |||
| 555bbca59b | |||
| e9e9d5c458 | |||
| b1842a33ae | |||
| 6afeaccf24 | |||
| fb76bac480 | |||
| 6ded185289 | |||
| 95adc29f9d | |||
| 4d0c3e683f | |||
| 018aafc773 | |||
| c4aea4efc2 | |||
| 225e3ca13f | |||
| 8c1fa7956a | |||
| e253d1b276 | |||
| a52aac2d98 | |||
| 9e8cf60dd8 | |||
| 7682d81d50 | |||
| 5d31ce96c1 | |||
| 81ef64a246 | |||
| 49d1f78001 | |||
| 0d0f3528e2 | |||
| d97d5e689a | |||
| 95d80ce13e | |||
| 668720984d | |||
| 245c3fa121 | 
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250211-142243.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250211-142243.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: Allow the merge of two accompanying period works
 | 
			
		||||
time: 2025-02-11T14:22:43.134106669+01:00
 | 
			
		||||
custom:
 | 
			
		||||
    Issue: "359"
 | 
			
		||||
    SchemaChange: No schema change
 | 
			
		||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250403-100311.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250403-100311.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: Duplication of a document to another accompanying period work evaluation
 | 
			
		||||
time: 2025-04-03T10:03:11.796736107+02:00
 | 
			
		||||
custom:
 | 
			
		||||
    Issue: "369"
 | 
			
		||||
    SchemaChange: No schema change
 | 
			
		||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250403-100857.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250403-100857.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: Fusion of two accompanying period works
 | 
			
		||||
time: 2025-04-03T10:08:57.25079018+02:00
 | 
			
		||||
custom:
 | 
			
		||||
    Issue: "359"
 | 
			
		||||
    SchemaChange: No schema change
 | 
			
		||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250717-110850.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250717-110850.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: Add filter to social actions list to filter out actions where current user intervenes
 | 
			
		||||
time: 2025-07-17T11:08:50.128269232+02:00
 | 
			
		||||
custom:
 | 
			
		||||
    Issue: "400"
 | 
			
		||||
    SchemaChange: No schema change
 | 
			
		||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250722-155039.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250722-155039.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: Show filters on list pages unfolded by default
 | 
			
		||||
time: 2025-07-22T15:50:39.338057044+02:00
 | 
			
		||||
custom:
 | 
			
		||||
    Issue: "399"
 | 
			
		||||
    SchemaChange: No schema change
 | 
			
		||||
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20250806-134609.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20250806-134609.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
kind: Fixed
 | 
			
		||||
body: adjust display logic for accompanying period dates, include closing date if period is closed.
 | 
			
		||||
time: 2025-08-06T13:46:09.241584292+02:00
 | 
			
		||||
custom:
 | 
			
		||||
    Issue: "382"
 | 
			
		||||
    SchemaChange: No schema change
 | 
			
		||||
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20250806-173527.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20250806-173527.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
kind: Fixed
 | 
			
		||||
body: add min and step attributes to integer field in DateIntervalType
 | 
			
		||||
time: 2025-08-06T17:35:27.413787704+02:00
 | 
			
		||||
custom:
 | 
			
		||||
    Issue: "384"
 | 
			
		||||
    SchemaChange: No schema change
 | 
			
		||||
							
								
								
									
										6
									
								
								.changes/unreleased/UX-20250722-132637.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/UX-20250722-132637.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
kind: UX
 | 
			
		||||
body: Limit display of participations in event list
 | 
			
		||||
time: 2025-07-22T13:26:37.500656935+02:00
 | 
			
		||||
custom:
 | 
			
		||||
    Issue: ""
 | 
			
		||||
    SchemaChange: No schema change
 | 
			
		||||
							
								
								
									
										4
									
								
								.changes/v4.0.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.changes/v4.0.1.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
## v4.0.1 - 2025-07-08
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix package.json for compilation
 | 
			
		||||
   
 | 
			
		||||
							
								
								
									
										4
									
								
								.changes/v4.0.2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.changes/v4.0.2.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
## v4.0.2 - 2025-07-09
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix add missing translation   
 | 
			
		||||
* Fix the transfer of evaluations and documents during of accompanyingperiodwork   
 | 
			
		||||
@@ -185,14 +185,57 @@ When we need to use a DateTime or DateTimeImmutable that need to express "now",
 | 
			
		||||
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
 | 
			
		||||
where injection does not work when restoring an entity from database, but usually possible in services.
 | 
			
		||||
 | 
			
		||||
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
 | 
			
		||||
where we have full and easy control of the date.
 | 
			
		||||
 | 
			
		||||
### Testing Information
 | 
			
		||||
 | 
			
		||||
The project uses PHPUnit for testing. Each bundle has its own test suite, and there's also a global test suite at the root level.
 | 
			
		||||
 | 
			
		||||
#### Use of mock in tests
 | 
			
		||||
 | 
			
		||||
##### General mocking
 | 
			
		||||
 | 
			
		||||
For creating mock, we prefer using prophecy (library phpspec/prophecy).
 | 
			
		||||
 | 
			
		||||
##### Useful helpers and tips that avoid create a mock
 | 
			
		||||
 | 
			
		||||
Some notable implementations that are tests helper, and avoid to create a mock:
 | 
			
		||||
 | 
			
		||||
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
 | 
			
		||||
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
 | 
			
		||||
- `\Symfony\Component\HttpClient\MockHttpClient`, an implementation of `\Symfony\Contracts\HttpClient\HttpClientInterface`;
 | 
			
		||||
- When using `\Symfony\Component\Mailer\MailerInterface`, we can create the mock with "InMemoryTransport":
 | 
			
		||||
 | 
			
		||||
    ```php
 | 
			
		||||
    use Symfony\Component\Mailer\Transport\InMemoryTransport;
 | 
			
		||||
    use \Symfony\Component\Mailer\Mailer;
 | 
			
		||||
 | 
			
		||||
    $transport = new InMemoryTransport();
 | 
			
		||||
    $mailer = new Mailer($transport);
 | 
			
		||||
 | 
			
		||||
    // After sending:
 | 
			
		||||
    $messages = $transport->getSent(); // array of SentMessage
 | 
			
		||||
    ```
 | 
			
		||||
- When using `\Symfony\Contracts\EventDispatcher\EventDispatcherInterface`, we can use directly an instance of `\Symfony\Component\EventDispatcher\EventDispatcher`;
 | 
			
		||||
 | 
			
		||||
##### When we prefer not creating a mock
 | 
			
		||||
 | 
			
		||||
- When we use Doctrine Entities related to the project, we prefer not to use a mock: we instantiate them directly (unless it requires too much code to write);
 | 
			
		||||
 | 
			
		||||
##### Mocking final and readonly classes
 | 
			
		||||
 | 
			
		||||
Classes marked as final can't be mocked. To avoid that, either:
 | 
			
		||||
 | 
			
		||||
- we remove the `final` keyword from the class;
 | 
			
		||||
- we extract an interface from the final class.
 | 
			
		||||
 | 
			
		||||
This must be a decision made by a human, not by an AI. Every AI task must abort with an explicit message in that case.
 | 
			
		||||
 | 
			
		||||
#### Running Tests
 | 
			
		||||
 | 
			
		||||
The tests are run from the project's root (not from the bundle's root).
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Run all tests
 | 
			
		||||
vendor/bin/phpunit
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -6,6 +6,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
 | 
			
		||||
and is generated by [Changie](https://github.com/miniscruff/changie).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## v4.0.2 - 2025-07-09
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix add missing translation   
 | 
			
		||||
* Fix the transfer of evaluations and documents during of accompanyingperiodwork   
 | 
			
		||||
 | 
			
		||||
## v4.0.1 - 2025-07-08
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix package.json for compilation
 | 
			
		||||
   
 | 
			
		||||
 | 
			
		||||
## v4.0.0 - 2025-07-08
 | 
			
		||||
### Feature
 | 
			
		||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
 | 
			
		||||
 
 | 
			
		||||
@@ -62,8 +62,10 @@ framework:
 | 
			
		||||
            'Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage': priority
 | 
			
		||||
            'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async
 | 
			
		||||
            'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async
 | 
			
		||||
            'Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage': async
 | 
			
		||||
            'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
 | 
			
		||||
            'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async
 | 
			
		||||
            'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async
 | 
			
		||||
            # end of routes added by chill-bundles recipes
 | 
			
		||||
            # Route your messages to the transports
 | 
			
		||||
            # 'App\Message\YourMessage': async
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@
 | 
			
		||||
    "@hotwired/stimulus": "^3.0.0",
 | 
			
		||||
    "@luminateone/eslint-baseline": "^1.0.9",
 | 
			
		||||
    "@symfony/stimulus-bridge": "^3.2.0",
 | 
			
		||||
    "@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
 | 
			
		||||
    "@symfony/webpack-encore": "^4.1.0",
 | 
			
		||||
    "@tsconfig/node20": "^20.1.4",
 | 
			
		||||
    "@types/dompurify": "^3.0.5",
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,11 @@ use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
 | 
			
		||||
class CalendarForShortMessageProvider
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly CalendarRepository $calendarRepository, private readonly EntityManagerInterface $em, private readonly RangeGeneratorInterface $rangeGenerator) {}
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly CalendarRepository $calendarRepository,
 | 
			
		||||
        private readonly EntityManagerInterface $em,
 | 
			
		||||
        private readonly RangeGeneratorInterface $rangeGenerator,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Generate calendars instance.
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,6 @@ namespace Chill\CalendarBundle\Tests\Service\ShortMessageNotification;
 | 
			
		||||
use Chill\CalendarBundle\Entity\Calendar;
 | 
			
		||||
use Chill\CalendarBundle\Repository\CalendarRepository;
 | 
			
		||||
use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider;
 | 
			
		||||
use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultRangeGenerator;
 | 
			
		||||
use Chill\CalendarBundle\Service\ShortMessageNotification\RangeGeneratorInterface;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
@@ -82,10 +81,16 @@ final class CalendarForShortMessageProviderTest extends TestCase
 | 
			
		||||
        $em = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
        $em->clear()->shouldBeCalled();
 | 
			
		||||
 | 
			
		||||
        $calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
 | 
			
		||||
        $calendarRangeGenerator->generateRange(Argument::any())->willReturn([
 | 
			
		||||
            'startDate' => new \DateTimeImmutable('yesterday'),
 | 
			
		||||
            'endDate' => new \DateTimeImmutable('now'),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $provider = new CalendarForShortMessageProvider(
 | 
			
		||||
            $calendarRepository->reveal(),
 | 
			
		||||
            $em->reveal(),
 | 
			
		||||
            new DefaultRangeGenerator()
 | 
			
		||||
            $calendarRangeGenerator->reveal(),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now')));
 | 
			
		||||
@@ -103,26 +108,32 @@ final class CalendarForShortMessageProviderTest extends TestCase
 | 
			
		||||
            Argument::type(\DateTimeImmutable::class),
 | 
			
		||||
            Argument::type('int'),
 | 
			
		||||
            Argument::exact(0)
 | 
			
		||||
        )->will(static fn ($args) => array_fill(0, 1, new Calendar()))->shouldBeCalledTimes(1);
 | 
			
		||||
        )->will(static fn ($args) => array_fill(0, 10, new Calendar()))->shouldBeCalledTimes(1);
 | 
			
		||||
        $calendarRepository->findByNotificationAvailable(
 | 
			
		||||
            Argument::type(\DateTimeImmutable::class),
 | 
			
		||||
            Argument::type(\DateTimeImmutable::class),
 | 
			
		||||
            Argument::type('int'),
 | 
			
		||||
            Argument::not(0)
 | 
			
		||||
            Argument::exact(10)
 | 
			
		||||
        )->will(static fn ($args) => [])->shouldBeCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
        $em = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
        $em->clear()->shouldBeCalled();
 | 
			
		||||
 | 
			
		||||
        $calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
 | 
			
		||||
        $calendarRangeGenerator->generateRange(Argument::any())->willReturn([
 | 
			
		||||
            'startDate' => new \DateTimeImmutable('yesterday'),
 | 
			
		||||
            'endDate' => new \DateTimeImmutable('now'),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $provider = new CalendarForShortMessageProvider(
 | 
			
		||||
            $calendarRepository->reveal(),
 | 
			
		||||
            $em->reveal(),
 | 
			
		||||
            new DefaultRangeGenerator()
 | 
			
		||||
            $calendarRangeGenerator->reveal(),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now')));
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(1, \count($calendars));
 | 
			
		||||
        $this->assertEquals(10, \count($calendars));
 | 
			
		||||
        $this->assertContainsOnly(Calendar::class, $calendars);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import { StoredObject, StoredObjectVersion } from "../../types";
 | 
			
		||||
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
 | 
			
		||||
import { computed, reactive } from "vue";
 | 
			
		||||
import { useToast } from "vue-toast-notification";
 | 
			
		||||
import { DOCUMENT_REPLACE, DOCUMENT_ADD, trans } from "translator";
 | 
			
		||||
 | 
			
		||||
interface DropFileConfig {
 | 
			
		||||
    allowRemove: boolean;
 | 
			
		||||
@@ -75,10 +76,10 @@ function closeModal(): void {
 | 
			
		||||
        @click="openModal"
 | 
			
		||||
        class="btn btn-create"
 | 
			
		||||
    >
 | 
			
		||||
        Ajouter un document
 | 
			
		||||
        {{ trans(DOCUMENT_ADD) }}
 | 
			
		||||
    </button>
 | 
			
		||||
    <button v-else @click="openModal" class="btn btn-edit">
 | 
			
		||||
        Remplacer le document
 | 
			
		||||
    <button v-else @click="openModal" class="dropdown-item">
 | 
			
		||||
        {{ trans(DOCUMENT_REPLACE) }}
 | 
			
		||||
    </button>
 | 
			
		||||
    <modal
 | 
			
		||||
        v-if="state.showModal"
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,8 @@ See the document: Voir le document
 | 
			
		||||
 | 
			
		||||
document:
 | 
			
		||||
    Any title: Aucun titre
 | 
			
		||||
    replace: Remplacer
 | 
			
		||||
    Add: Ajouter un document
 | 
			
		||||
 | 
			
		||||
generic_doc:
 | 
			
		||||
    filter:
 | 
			
		||||
 
 | 
			
		||||
@@ -54,14 +54,14 @@ block js %}
 | 
			
		||||
            {% if e.participations|length > 0 %}
 | 
			
		||||
            <div class="item-row separator">
 | 
			
		||||
                <strong>{{ "Participations" | trans }} : </strong>
 | 
			
		||||
                {% for part in e.participations|slice(0, 20) %} {% include
 | 
			
		||||
                {% for part in e.participations|slice(0, 5) %} {% include
 | 
			
		||||
                '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
 | 
			
		||||
                targetEntity: { name: 'person', id: part.person.id }, action:
 | 
			
		||||
                'show', displayBadge: true, buttonText:
 | 
			
		||||
                part.person|chill_entity_render_string, isDead:
 | 
			
		||||
                part.person.deathdate is not null } %} {% endfor %} {% if
 | 
			
		||||
                e.participations|length > 20 %}
 | 
			
		||||
                {{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 20}) }}
 | 
			
		||||
                part.person.deathdate is not null } %} {% endfor %}
 | 
			
		||||
                {% if e.participations|length > 5 %}
 | 
			
		||||
                {{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 5}) }}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompiler
 | 
			
		||||
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
 | 
			
		||||
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
 | 
			
		||||
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
 | 
			
		||||
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
 | 
			
		||||
use Chill\MainBundle\Notification\NotificationHandlerInterface;
 | 
			
		||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
 | 
			
		||||
use Chill\MainBundle\Search\SearchApiInterface;
 | 
			
		||||
@@ -61,6 +62,8 @@ class ChillMainBundle extends Bundle
 | 
			
		||||
            ->addTag('chill_main.entity_info_provider');
 | 
			
		||||
        $container->registerForAutoconfiguration(ProvideRoleInterface::class)
 | 
			
		||||
            ->addTag('chill_main.provide_role');
 | 
			
		||||
        $container->registerForAutoconfiguration(NotificationFlagProviderInterface::class)
 | 
			
		||||
            ->addTag('chill_main.notification_flag_provider');
 | 
			
		||||
 | 
			
		||||
        $container->addCompilerPass(new SearchableServicesCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
 | 
			
		||||
        $container->addCompilerPass(new ConfigConsistencyCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\NotificationComment;
 | 
			
		||||
use Chill\MainBundle\Form\NotificationCommentType;
 | 
			
		||||
use Chill\MainBundle\Form\NotificationType;
 | 
			
		||||
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
 | 
			
		||||
use Chill\MainBundle\Notification\FlagProviders\NotificationByUserFlagProvider;
 | 
			
		||||
use Chill\MainBundle\Notification\NotificationHandlerManager;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\MainBundle\Repository\NotificationRepository;
 | 
			
		||||
@@ -57,7 +58,8 @@ class NotificationController extends AbstractController
 | 
			
		||||
        $notification
 | 
			
		||||
            ->setRelatedEntityClass($request->query->get('entityClass'))
 | 
			
		||||
            ->setRelatedEntityId($request->query->getInt('entityId'))
 | 
			
		||||
            ->setSender($this->security->getUser());
 | 
			
		||||
            ->setSender($this->security->getUser())
 | 
			
		||||
            ->setType(NotificationByUserFlagProvider::FLAG);
 | 
			
		||||
 | 
			
		||||
        $tos = $request->query->all('tos');
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,14 +11,11 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Form\UserPhonenumberType;
 | 
			
		||||
use Chill\MainBundle\Form\UserProfileType;
 | 
			
		||||
use Chill\MainBundle\Security\ChillSecurity;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
 | 
			
		||||
use Symfony\Component\Form\FormInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 | 
			
		||||
use Symfony\Component\Security\Core\User\UserInterface;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
 | 
			
		||||
@@ -41,16 +38,19 @@ final class UserProfileController extends AbstractController
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $user = $this->security->getUser();
 | 
			
		||||
        $editForm = $this->createPhonenumberEditForm($user);
 | 
			
		||||
        $editForm = $this->createForm(UserProfileType::class, $user);
 | 
			
		||||
 | 
			
		||||
        $editForm->get('notificationFlags')->setData($user->getNotificationFlags());
 | 
			
		||||
 | 
			
		||||
        $editForm->handleRequest($request);
 | 
			
		||||
 | 
			
		||||
        if ($editForm->isSubmitted() && $editForm->isValid()) {
 | 
			
		||||
            $phonenumber = $editForm->get('phonenumber')->getData();
 | 
			
		||||
            $notificationFlagsData = $editForm->get('notificationFlags')->getData();
 | 
			
		||||
            $user->setNotificationFlags($notificationFlagsData);
 | 
			
		||||
 | 
			
		||||
            $user->setPhonenumber($phonenumber);
 | 
			
		||||
 | 
			
		||||
            $this->managerRegistry->getManager()->flush();
 | 
			
		||||
            $this->addFlash('success', $this->translator->trans('user.profile.Phonenumber successfully updated!'));
 | 
			
		||||
            $em = $this->managerRegistry->getManager();
 | 
			
		||||
            $em->flush();
 | 
			
		||||
            $this->addFlash('success', $this->translator->trans('user.profile.Profile successfully updated!'));
 | 
			
		||||
 | 
			
		||||
            return $this->redirectToRoute('chill_main_user_profile');
 | 
			
		||||
        }
 | 
			
		||||
@@ -60,13 +60,4 @@ final class UserProfileController extends AbstractController
 | 
			
		||||
            'form' => $editForm->createView(),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function createPhonenumberEditForm(UserInterface $user): FormInterface
 | 
			
		||||
    {
 | 
			
		||||
        return $this->createForm(
 | 
			
		||||
            UserPhonenumberType::class,
 | 
			
		||||
            $user,
 | 
			
		||||
        )
 | 
			
		||||
            ->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ namespace Chill\MainBundle\Entity;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Doctrine\Common\Collections\ArrayCollection;
 | 
			
		||||
use Doctrine\Common\Collections\Collection;
 | 
			
		||||
use Doctrine\DBAL\Types\Types;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
 | 
			
		||||
@@ -21,10 +22,10 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
 | 
			
		||||
#[ORM\Entity]
 | 
			
		||||
#[ORM\HasLifecycleCallbacks]
 | 
			
		||||
#[ORM\Table(name: 'chill_main_notification')]
 | 
			
		||||
#[ORM\Index(name: 'chill_main_notification_related_entity_idx', columns: ['relatedentityclass', 'relatedentityid'])]
 | 
			
		||||
#[ORM\Index(columns: ['relatedentityclass', 'relatedentityid'], name: 'chill_main_notification_related_entity_idx')]
 | 
			
		||||
class Notification implements TrackUpdateInterface
 | 
			
		||||
{
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false)]
 | 
			
		||||
    #[ORM\Column(type: Types::TEXT, nullable: false)]
 | 
			
		||||
    private string $accessKey;
 | 
			
		||||
 | 
			
		||||
    private array $addedAddresses = [];
 | 
			
		||||
@@ -36,12 +37,19 @@ class Notification implements TrackUpdateInterface
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_notification_addresses_user')]
 | 
			
		||||
    private Collection $addressees;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<int, UserGroup>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: UserGroup::class)]
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_notification_addressee_user_group')]
 | 
			
		||||
    private Collection $addresseeUserGroups;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * a list of destinee which will receive notifications.
 | 
			
		||||
     *
 | 
			
		||||
     * @var array|string[]
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
 | 
			
		||||
    #[ORM\Column(type: Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
 | 
			
		||||
    private array $addressesEmails = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -60,21 +68,21 @@ class Notification implements TrackUpdateInterface
 | 
			
		||||
    #[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])]
 | 
			
		||||
    private Collection $comments;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)]
 | 
			
		||||
    #[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
 | 
			
		||||
    private \DateTimeImmutable $date;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\GeneratedValue]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
 | 
			
		||||
    #[ORM\Column(type: Types::INTEGER)]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
 | 
			
		||||
    #[ORM\Column(type: Types::TEXT)]
 | 
			
		||||
    private string $message = '';
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
 | 
			
		||||
    #[ORM\Column(type: Types::STRING, length: 255)]
 | 
			
		||||
    private string $relatedEntityClass = '';
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
 | 
			
		||||
    #[ORM\Column(type: Types::INTEGER)]
 | 
			
		||||
    private int $relatedEntityId;
 | 
			
		||||
 | 
			
		||||
    private array $removedAddresses = [];
 | 
			
		||||
@@ -84,7 +92,7 @@ class Notification implements TrackUpdateInterface
 | 
			
		||||
    private ?User $sender = null;
 | 
			
		||||
 | 
			
		||||
    #[Assert\NotBlank(message: 'notification.Title must be defined')]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
 | 
			
		||||
    #[ORM\Column(type: Types::TEXT, options: ['default' => ''])]
 | 
			
		||||
    private string $title = '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -94,31 +102,46 @@ class Notification implements TrackUpdateInterface
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')]
 | 
			
		||||
    private Collection $unreadBy;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)]
 | 
			
		||||
    #[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
 | 
			
		||||
    private ?\DateTimeImmutable $updatedAt = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: User::class)]
 | 
			
		||||
    private ?User $updatedBy = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(name: 'type', type: Types::STRING, nullable: true)]
 | 
			
		||||
    private string $type = '';
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        $this->addressees = new ArrayCollection();
 | 
			
		||||
        $this->addresseeUserGroups = new ArrayCollection();
 | 
			
		||||
        $this->unreadBy = new ArrayCollection();
 | 
			
		||||
        $this->comments = new ArrayCollection();
 | 
			
		||||
        $this->setDate(new \DateTimeImmutable());
 | 
			
		||||
        $this->accessKey = bin2hex(openssl_random_pseudo_bytes(24));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addAddressee(User $addressee): self
 | 
			
		||||
    public function addAddressee(User|UserGroup $addressee): self
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->addressees->contains($addressee)) {
 | 
			
		||||
            $this->addressees[] = $addressee;
 | 
			
		||||
            $this->addedAddresses[] = $addressee;
 | 
			
		||||
        if ($addressee instanceof User) {
 | 
			
		||||
            if (!$this->addressees->contains($addressee)) {
 | 
			
		||||
                $this->addressees->add($addressee);
 | 
			
		||||
                $this->addedAddresses[] = $addressee;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return $this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!$this->addresseeUserGroups->contains($addressee)) {
 | 
			
		||||
            $this->addresseeUserGroups->add($addressee);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @deprecated
 | 
			
		||||
     */
 | 
			
		||||
    public function addAddressesEmail(string $email)
 | 
			
		||||
    {
 | 
			
		||||
        if (!\in_array($email, $this->addressesEmails, true)) {
 | 
			
		||||
@@ -152,13 +175,23 @@ class Notification implements TrackUpdateInterface
 | 
			
		||||
    #[Assert\Callback]
 | 
			
		||||
    public function assertCountAddresses(ExecutionContextInterface $context, $payload): void
 | 
			
		||||
    {
 | 
			
		||||
        if (0 === (\count($this->getAddressesEmails()) + \count($this->getAddressees()))) {
 | 
			
		||||
        if (0 === (\count($this->getAddresseeUserGroups()) + \count($this->getAddressees()))) {
 | 
			
		||||
            $context->buildViolation('notification.At least one addressee')
 | 
			
		||||
                ->atPath('addressees')
 | 
			
		||||
                ->addViolation();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAddresseeUserGroups(): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return $this->addresseeUserGroups;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setAddresseeUserGroups(Collection $addresseeUserGroups): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addresseeUserGroups = $addresseeUserGroups;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAccessKey(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->accessKey;
 | 
			
		||||
@@ -182,6 +215,23 @@ class Notification implements TrackUpdateInterface
 | 
			
		||||
        return $this->addressees;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAllAddressees(): array
 | 
			
		||||
    {
 | 
			
		||||
        $allUsers = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($this->getAddressees() as $user) {
 | 
			
		||||
            $allUsers[$user->getId()] = $user;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($this->getAddresseeUserGroups() as $userGroup) {
 | 
			
		||||
            foreach ($userGroup->getUsers() as $user) {
 | 
			
		||||
                $allUsers[$user->getId()] = $user;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return array_values($allUsers);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return array|string[]
 | 
			
		||||
     */
 | 
			
		||||
@@ -303,12 +353,18 @@ class Notification implements TrackUpdateInterface
 | 
			
		||||
        $this->addressesOnLoad = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function removeAddressee(User $addressee): self
 | 
			
		||||
    public function removeAddressee(User|UserGroup $addressee): self
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->addressees->removeElement($addressee)) {
 | 
			
		||||
            $this->removedAddresses[] = $addressee;
 | 
			
		||||
        if ($addressee instanceof User) {
 | 
			
		||||
            if ($this->addressees->contains($addressee)) {
 | 
			
		||||
                $this->addressees->removeElement($addressee);
 | 
			
		||||
 | 
			
		||||
                return $this;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->addresseeUserGroups->removeElement($addressee);
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -378,7 +434,7 @@ class Notification implements TrackUpdateInterface
 | 
			
		||||
 | 
			
		||||
    public function setUpdatedAt(\DateTimeInterface $datetime): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->updatedAt = $datetime;
 | 
			
		||||
        $this->updatedAt = \DateTimeImmutable::createFromInterface($datetime);
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
@@ -389,4 +445,16 @@ class Notification implements TrackUpdateInterface
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setType(string $type): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->type = $type;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getType(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->type;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,9 @@ use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
 | 
			
		||||
#[ORM\Table(name: 'users')]
 | 
			
		||||
class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInterface
 | 
			
		||||
{
 | 
			
		||||
    public const NOTIF_FLAG_IMMEDIATE_EMAIL = 'immediate-email';
 | 
			
		||||
    public const NOTIF_FLAG_DAILY_DIGEST = 'daily-digest';
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
 | 
			
		||||
    #[ORM\GeneratedValue(strategy: 'AUTO')]
 | 
			
		||||
@@ -116,6 +119,12 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
 | 
			
		||||
    #[PhonenumberConstraint]
 | 
			
		||||
    private ?PhoneNumber $phonenumber = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var array<string, list<string>>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
 | 
			
		||||
    private array $notificationFlags = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * User constructor.
 | 
			
		||||
     */
 | 
			
		||||
@@ -613,4 +622,57 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the current object is an instance of User.
 | 
			
		||||
     *
 | 
			
		||||
     * @return bool returns true if the current object is an instance of User, false otherwise
 | 
			
		||||
     */
 | 
			
		||||
    public function isUser(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getNotificationFlags(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->notificationFlags;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setNotificationFlags(array $notificationFlags)
 | 
			
		||||
    {
 | 
			
		||||
        $this->notificationFlags = $notificationFlags;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getNotificationFlagData(string $flag): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->notificationFlags[$flag] ?? [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setNotificationFlagData(string $flag, array $data): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->notificationFlags[$flag] = $data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isNotificationSendImmediately(string $type): bool
 | 
			
		||||
    {
 | 
			
		||||
        if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isNotificationDailyDigest(string $type): bool
 | 
			
		||||
    {
 | 
			
		||||
        if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLocale(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'fr';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,75 @@
 | 
			
		||||
<?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\DataMapper;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Symfony\Component\Form\DataMapperInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class NotificationFlagDataMapper implements DataMapperInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private array $notificationFlagProviders) {}
 | 
			
		||||
 | 
			
		||||
    public function mapDataToForms($viewData, $forms): void
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $viewData) {
 | 
			
		||||
            $viewData = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $formsArray = iterator_to_array($forms);
 | 
			
		||||
 | 
			
		||||
        foreach ($this->notificationFlagProviders as $flagProvider) {
 | 
			
		||||
            $flag = $flagProvider->getFlag();
 | 
			
		||||
 | 
			
		||||
            if (isset($formsArray[$flag])) {
 | 
			
		||||
                $flagForm = $formsArray[$flag];
 | 
			
		||||
 | 
			
		||||
                $immediateEmailChecked = in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $viewData[$flag] ?? [], true)
 | 
			
		||||
                    || !array_key_exists($flag, $viewData);
 | 
			
		||||
                $dailyEmailChecked = in_array(User::NOTIF_FLAG_DAILY_DIGEST, $viewData[$flag] ?? [], true);
 | 
			
		||||
 | 
			
		||||
                if ($flagForm->has('immediate_email')) {
 | 
			
		||||
                    $flagForm->get('immediate_email')->setData($immediateEmailChecked);
 | 
			
		||||
                }
 | 
			
		||||
                if ($flagForm->has('daily_email')) {
 | 
			
		||||
                    $flagForm->get('daily_email')->setData($dailyEmailChecked);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function mapFormsToData($forms, &$viewData): void
 | 
			
		||||
    {
 | 
			
		||||
        $formsArray = iterator_to_array($forms);
 | 
			
		||||
        $viewData = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($this->notificationFlagProviders as $flagProvider) {
 | 
			
		||||
            $flag = $flagProvider->getFlag();
 | 
			
		||||
 | 
			
		||||
            if (isset($formsArray[$flag])) {
 | 
			
		||||
                $flagForm = $formsArray[$flag];
 | 
			
		||||
                $viewData[$flag] = [];
 | 
			
		||||
 | 
			
		||||
                if (true === $flagForm['immediate_email']->getData()) {
 | 
			
		||||
                    $viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (true === $flagForm['daily_email']->getData()) {
 | 
			
		||||
                    $viewData[$flag][] = User::NOTIF_FLAG_DAILY_DIGEST;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if ([] === $viewData[$flag]) {
 | 
			
		||||
                    $viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,17 +12,12 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\MainBundle\Form;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Notification;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
use Symfony\Component\OptionsResolver\OptionsResolver;
 | 
			
		||||
use Symfony\Component\Validator\Constraints\Email;
 | 
			
		||||
use Symfony\Component\Validator\Constraints\NotBlank;
 | 
			
		||||
use Symfony\Component\Validator\Constraints\NotNull;
 | 
			
		||||
 | 
			
		||||
class NotificationType extends AbstractType
 | 
			
		||||
{
 | 
			
		||||
@@ -33,29 +28,14 @@ class NotificationType extends AbstractType
 | 
			
		||||
                'label' => 'Title',
 | 
			
		||||
                'required' => true,
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('addressees', PickUserDynamicType::class, [
 | 
			
		||||
            ->add('addressees', PickUserGroupOrUserDynamicType::class, [
 | 
			
		||||
                'multiple' => true,
 | 
			
		||||
                'required' => false,
 | 
			
		||||
                'label' => 'notification.Pick user or user group',
 | 
			
		||||
                'empty_data' => '[]',
 | 
			
		||||
                'required' => true,
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('message', ChillTextareaType::class, [
 | 
			
		||||
                'required' => false,
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('addressesEmails', ChillCollectionType::class, [
 | 
			
		||||
                'label' => 'notification.dest by email',
 | 
			
		||||
                'help' => 'notification.dest by email help',
 | 
			
		||||
                'by_reference' => false,
 | 
			
		||||
                'allow_add' => true,
 | 
			
		||||
                'allow_delete' => true,
 | 
			
		||||
                'entry_type' => EmailType::class,
 | 
			
		||||
                'button_add_label' => 'notification.Add an email',
 | 
			
		||||
                'button_remove_label' => 'notification.Remove an email',
 | 
			
		||||
                'empty_collection_explain' => 'notification.Any email',
 | 
			
		||||
                'entry_options' => [
 | 
			
		||||
                    'constraints' => [
 | 
			
		||||
                        new NotNull(), new NotBlank(), new Email(),
 | 
			
		||||
                    ],
 | 
			
		||||
                    'label' => 'Email',
 | 
			
		||||
                ],
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -55,6 +55,10 @@ class DateIntervalType extends AbstractType
 | 
			
		||||
    {
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('n', IntegerType::class, [
 | 
			
		||||
                'attr' => [
 | 
			
		||||
                    'min' => 0,
 | 
			
		||||
                    'step' => 1,
 | 
			
		||||
                ],
 | 
			
		||||
                'constraints' => [
 | 
			
		||||
                    new GreaterThan([
 | 
			
		||||
                        'value' => 0,
 | 
			
		||||
 
 | 
			
		||||
@@ -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\MainBundle\Form\Type;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper;
 | 
			
		||||
use Chill\MainBundle\Notification\NotificationFlagManager;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
use Symfony\Component\OptionsResolver\OptionsResolver;
 | 
			
		||||
 | 
			
		||||
class NotificationFlagsType extends AbstractType
 | 
			
		||||
{
 | 
			
		||||
    private readonly array $notificationFlagProviders;
 | 
			
		||||
 | 
			
		||||
    public function __construct(NotificationFlagManager $notificationFlagManager)
 | 
			
		||||
    {
 | 
			
		||||
        $this->notificationFlagProviders = $notificationFlagManager->getAllNotificationFlagProviders();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder, array $options): void
 | 
			
		||||
    {
 | 
			
		||||
        $builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders));
 | 
			
		||||
 | 
			
		||||
        foreach ($this->notificationFlagProviders as $flagProvider) {
 | 
			
		||||
            $flag = $flagProvider->getFlag();
 | 
			
		||||
            $builder->add($flag, FormType::class, [
 | 
			
		||||
                'label' => $flagProvider->getLabel(),
 | 
			
		||||
                'required' => false,
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            $builder->get($flag)
 | 
			
		||||
                ->add('immediate_email', CheckboxType::class, [
 | 
			
		||||
                    'label' => false,
 | 
			
		||||
                    'required' => false,
 | 
			
		||||
                    'mapped' => false,
 | 
			
		||||
                ])
 | 
			
		||||
                ->add('daily_email', CheckboxType::class, [
 | 
			
		||||
                    'label' => false,
 | 
			
		||||
                    'required' => false,
 | 
			
		||||
                    'mapped' => false,
 | 
			
		||||
                ])
 | 
			
		||||
            ;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function configureOptions(OptionsResolver $resolver): void
 | 
			
		||||
    {
 | 
			
		||||
        $resolver->setDefaults([
 | 
			
		||||
            'data_class' => null,
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/Bundle/ChillMainBundle/Form/UserProfileType.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/Bundle/ChillMainBundle/Form/UserProfileType.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Form;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\NotificationFlagsType;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
use Symfony\Component\OptionsResolver\OptionsResolver;
 | 
			
		||||
 | 
			
		||||
class UserProfileType extends AbstractType
 | 
			
		||||
{
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder, array $options)
 | 
			
		||||
    {
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('phonenumber', ChillPhoneNumberType::class, [
 | 
			
		||||
                'required' => false,
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('notificationFlags', NotificationFlagsType::class, [
 | 
			
		||||
                'label' => false,
 | 
			
		||||
                'mapped' => false,
 | 
			
		||||
            ])
 | 
			
		||||
        ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function configureOptions(OptionsResolver $resolver)
 | 
			
		||||
    {
 | 
			
		||||
        $resolver->setDefaults([
 | 
			
		||||
            'data_class' => \Chill\MainBundle\Entity\User::class,
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,102 @@
 | 
			
		||||
<?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\Notification\Email;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Cron\CronJobInterface;
 | 
			
		||||
use Chill\MainBundle\Entity\CronJobExecution;
 | 
			
		||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
 | 
			
		||||
use Doctrine\DBAL\Connection;
 | 
			
		||||
use Doctrine\DBAL\Exception;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\Clock\ClockInterface;
 | 
			
		||||
use Symfony\Component\Messenger\MessageBusInterface;
 | 
			
		||||
 | 
			
		||||
readonly class DailyNotificationDigestCronjob implements CronJobInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private ClockInterface $clock,
 | 
			
		||||
        private Connection $connection,
 | 
			
		||||
        private MessageBusInterface $messageBus,
 | 
			
		||||
        private LoggerInterface $logger,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function canRun(?CronJobExecution $cronJobExecution): bool
 | 
			
		||||
    {
 | 
			
		||||
        $now = $this->clock->now();
 | 
			
		||||
 | 
			
		||||
        if (null !== $cronJobExecution && $now->sub(new \DateInterval('PT23H45M')) < $cronJobExecution->getLastStart()) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Run between 6 and 9 AM
 | 
			
		||||
        return in_array((int) $now->format('H'), [6, 7, 8], true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getKey(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'daily-notification-digest';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws \DateInvalidOperationException
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function run(array $lastExecutionData): ?array
 | 
			
		||||
    {
 | 
			
		||||
        $now = $this->clock->now();
 | 
			
		||||
        if (isset($lastExecutionData['last_execution'])) {
 | 
			
		||||
            $lastExecution = \DateTimeImmutable::createFromFormat(
 | 
			
		||||
                \DateTimeImmutable::ATOM,
 | 
			
		||||
                $lastExecutionData['last_execution']
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            $lastExecution = $now->sub(new \DateInterval('P1D'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get distinct users who received notifications since the last execution
 | 
			
		||||
        $sql = <<<'SQL'
 | 
			
		||||
        SELECT DISTINCT cmnau.user_id
 | 
			
		||||
        FROM chill_main_notification cmn
 | 
			
		||||
        JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id
 | 
			
		||||
        WHERE cmn.date >= :lastExecution AND cmn.date <= :now
 | 
			
		||||
        SQL;
 | 
			
		||||
 | 
			
		||||
        $sqlStatement = $this->connection->prepare($sql);
 | 
			
		||||
        $sqlStatement->bindValue('lastExecution', $lastExecution->format(\DateTimeInterface::RFC3339));
 | 
			
		||||
        $sqlStatement->bindValue('now', $now->format(\DateTimeInterface::RFC3339));
 | 
			
		||||
        $result = $sqlStatement->executeQuery();
 | 
			
		||||
 | 
			
		||||
        $count = 0;
 | 
			
		||||
        foreach ($result->fetchAllAssociative() as $row) {
 | 
			
		||||
            $userId = (int) $row['user_id'];
 | 
			
		||||
 | 
			
		||||
            $message = new ScheduleDailyNotificationDigestMessage(
 | 
			
		||||
                $userId,
 | 
			
		||||
                $lastExecution,
 | 
			
		||||
                $now
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            $this->messageBus->dispatch($message);
 | 
			
		||||
            ++$count;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->logger->info('[DailyNotificationDigestCronjob] Dispatched daily digest messages', [
 | 
			
		||||
            'user_count' => $count,
 | 
			
		||||
            'last_execution' => $lastExecution->format('Y-m-d-H:i:s.u e'),
 | 
			
		||||
            'current_time' => $now->format('Y-m-d-H:i:s.u e'),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'last_execution' => $now->format('Y-m-d-H:i:s.u e'),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,75 @@
 | 
			
		||||
<?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\Notification\Email\NotificationEmailHandlers;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
 | 
			
		||||
use Chill\MainBundle\Notification\Email\NotificationMailer;
 | 
			
		||||
use Chill\MainBundle\Repository\NotificationRepository;
 | 
			
		||||
use Chill\MainBundle\Repository\UserRepository;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
 | 
			
		||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
 | 
			
		||||
 | 
			
		||||
#[AsMessageHandler]
 | 
			
		||||
readonly class ScheduleDailyNotificationDigestHandler
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private NotificationRepository $notificationRepository,
 | 
			
		||||
        private UserRepository $userRepository,
 | 
			
		||||
        private NotificationMailer $notificationMailer,
 | 
			
		||||
        private LoggerInterface $logger,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws TransportExceptionInterface
 | 
			
		||||
     */
 | 
			
		||||
    public function __invoke(ScheduleDailyNotificationDigestMessage $message): void
 | 
			
		||||
    {
 | 
			
		||||
        $userId = $message->getUserId();
 | 
			
		||||
        $lastExecutionDate = $message->getLastExecutionDateTime();
 | 
			
		||||
        $currentDate = $message->getCurrentDateTime();
 | 
			
		||||
 | 
			
		||||
        $user = $this->userRepository->find($userId);
 | 
			
		||||
        if (null === $user) {
 | 
			
		||||
            $this->logger->warning('[ScheduleDailyNotificationDigestHandler] User not found', [
 | 
			
		||||
                'user_id' => $userId,
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            throw new \InvalidArgumentException(sprintf('User with ID %s not found', $userId));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get all notifications for this user between last execution and current date
 | 
			
		||||
        $notifications = $this->notificationRepository->findNotificationsForUserBetweenDates(
 | 
			
		||||
            $userId,
 | 
			
		||||
            $lastExecutionDate,
 | 
			
		||||
            $currentDate
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Filter out notifications that should be sent in a daily digest
 | 
			
		||||
        $dailyNotifications = array_filter($notifications, fn ($notification) => $user->isNotificationDailyDigest($notification->getType()));
 | 
			
		||||
 | 
			
		||||
        if ([] === $dailyNotifications) {
 | 
			
		||||
            $this->logger->info('[ScheduleDailyNotificationDigestHandler] No daily notifications found for user', [
 | 
			
		||||
                'user_id' => $userId,
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->notificationMailer->sendDailyDigest($user, $dailyNotifications);
 | 
			
		||||
 | 
			
		||||
        $this->logger->info('[ScheduleDailyNotificationDigestHandler] Sent daily digest', [
 | 
			
		||||
            'user_id' => $userId,
 | 
			
		||||
            'notification_count' => count($dailyNotifications),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
<?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\Notification\Email\NotificationEmailHandlers;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
 | 
			
		||||
use Chill\MainBundle\Notification\Email\NotificationMailer;
 | 
			
		||||
use Chill\MainBundle\Repository\NotificationRepository;
 | 
			
		||||
use Chill\MainBundle\Repository\UserRepository;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
 | 
			
		||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
 | 
			
		||||
 | 
			
		||||
#[AsMessageHandler]
 | 
			
		||||
readonly class SendImmediateNotificationEmailHandler
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private NotificationRepository $notificationRepository,
 | 
			
		||||
        private UserRepository $userRepository,
 | 
			
		||||
        private NotificationMailer $notificationMailer,
 | 
			
		||||
        private LoggerInterface $logger,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws TransportExceptionInterface
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function __invoke(SendImmediateNotificationEmailMessage $message): void
 | 
			
		||||
    {
 | 
			
		||||
        $notification = $this->notificationRepository->find($message->getNotificationId());
 | 
			
		||||
        $addressee = $this->userRepository->find($message->getAddresseeId());
 | 
			
		||||
 | 
			
		||||
        if (null === $notification) {
 | 
			
		||||
            $this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [
 | 
			
		||||
                'notification_id' => $message->getNotificationId(),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            throw new \InvalidArgumentException(sprintf('Notification with ID %s not found', $message->getNotificationId()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (null === $addressee) {
 | 
			
		||||
            $this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [
 | 
			
		||||
                'addressee_id' => $message->getAddresseeId(),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            throw new \InvalidArgumentException(sprintf('User with ID %s not found', $message->getAddresseeId()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $this->notificationMailer->sendEmailToAddressee($notification, $addressee);
 | 
			
		||||
        } catch (\Exception $e) {
 | 
			
		||||
            $this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [
 | 
			
		||||
                'notification_id' => $message->getNotificationId(),
 | 
			
		||||
                'addressee_id' => $message->getAddresseeId(),
 | 
			
		||||
                'stacktrace' => $e->getTraceAsString(),
 | 
			
		||||
            ]);
 | 
			
		||||
            throw $e;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
<?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\Notification\Email\NotificationEmailMessages;
 | 
			
		||||
 | 
			
		||||
readonly class ScheduleDailyNotificationDigestMessage
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private int $userId,
 | 
			
		||||
        private \DateTimeInterface $lastExecutionDate,
 | 
			
		||||
        private \DateTimeInterface $currentDate,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function getUserId(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->userId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLastExecutionDateTime(): \DateTimeInterface
 | 
			
		||||
    {
 | 
			
		||||
        return $this->lastExecutionDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getCurrentDateTime(): \DateTimeInterface
 | 
			
		||||
    {
 | 
			
		||||
        return $this->currentDate;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
<?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\Notification\Email\NotificationEmailMessages;
 | 
			
		||||
 | 
			
		||||
readonly class SendImmediateNotificationEmailMessage
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private int $notificationId,
 | 
			
		||||
        private int $addresseeId,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function getNotificationId(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->notificationId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAddresseeId(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->addresseeId;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,22 +13,32 @@ namespace Chill\MainBundle\Notification\Email;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Notification;
 | 
			
		||||
use Chill\MainBundle\Entity\NotificationComment;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
 | 
			
		||||
use Doctrine\ORM\Event\PostPersistEventArgs;
 | 
			
		||||
use Doctrine\ORM\Event\PostUpdateEventArgs;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
 | 
			
		||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
 | 
			
		||||
use Symfony\Component\Mailer\MailerInterface;
 | 
			
		||||
use Symfony\Component\Mime\Email;
 | 
			
		||||
use Symfony\Component\Messenger\MessageBusInterface;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
 | 
			
		||||
class NotificationMailer
 | 
			
		||||
readonly class NotificationMailer
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly MailerInterface $mailer, private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator) {}
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private MailerInterface $mailer,
 | 
			
		||||
        private LoggerInterface $logger,
 | 
			
		||||
        private MessageBusInterface $messageBus,
 | 
			
		||||
        private TranslatorInterface $translator,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
 | 
			
		||||
    {
 | 
			
		||||
        $dests = [$comment->getNotification()->getSender(), ...$comment->getNotification()->getAddressees()->toArray()];
 | 
			
		||||
        $dests = [
 | 
			
		||||
            $comment->getNotification()->getSender(),
 | 
			
		||||
            ...$comment->getNotification()->getAddressees()->toArray(),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $uniqueDests = [];
 | 
			
		||||
        foreach ($dests as $dest) {
 | 
			
		||||
@@ -69,55 +79,147 @@ class NotificationMailer
 | 
			
		||||
     */
 | 
			
		||||
    public function postPersistNotification(Notification $notification, PostPersistEventArgs $eventArgs): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->sendNotificationEmailsToAddresses($notification);
 | 
			
		||||
        $this->sendNotificationEmailsToAddressees($notification);
 | 
			
		||||
        $this->sendNotificationEmailsToAddressesEmails($notification);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function postUpdateNotification(Notification $notification, PostUpdateEventArgs $eventArgs): void
 | 
			
		||||
    private function sendNotificationEmailsToAddressees(Notification $notification): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->sendNotificationEmailsToAddressesEmails($notification);
 | 
			
		||||
    }
 | 
			
		||||
        if ('' === $notification->getType()) {
 | 
			
		||||
            $this->logger->warning('[NotificationMailer] Notification has no type, skipping email processing', [
 | 
			
		||||
                'notification_id' => $notification->getId(),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
    private function sendNotificationEmailsToAddresses(Notification $notification): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($notification->getAddressees() as $addressee) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($notification->getAllAddressees() as $addressee) {
 | 
			
		||||
            if (null === $addressee->getEmail()) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ($notification->isSystem()) {
 | 
			
		||||
                $email = new Email();
 | 
			
		||||
                $email
 | 
			
		||||
                    ->text($notification->getMessage());
 | 
			
		||||
            } else {
 | 
			
		||||
                $email = new TemplatedEmail();
 | 
			
		||||
                $email
 | 
			
		||||
                    ->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
 | 
			
		||||
                    ->context([
 | 
			
		||||
                        'notification' => $notification,
 | 
			
		||||
                        'dest' => $addressee,
 | 
			
		||||
                    ]);
 | 
			
		||||
            }
 | 
			
		||||
            $this->processNotificationForAddressee($notification, $addressee);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function processNotificationForAddressee(Notification $notification, User $addressee): void
 | 
			
		||||
    {
 | 
			
		||||
        $notificationType = $notification->getType();
 | 
			
		||||
 | 
			
		||||
        if ($addressee->isNotificationSendImmediately($notificationType)) {
 | 
			
		||||
            $this->scheduleImmediateEmail($notification, $addressee);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function scheduleImmediateEmail(Notification $notification, User $addressee): void
 | 
			
		||||
    {
 | 
			
		||||
        $message = new SendImmediateNotificationEmailMessage(
 | 
			
		||||
            $notification->getId(),
 | 
			
		||||
            $addressee->getId()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $this->messageBus->dispatch($message);
 | 
			
		||||
 | 
			
		||||
        $this->logger->info('[NotificationMailer] Scheduled immediate email', [
 | 
			
		||||
            'notification_id' => $notification->getId(),
 | 
			
		||||
            'addressee_email' => $addressee->getEmail(),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This method sends the email but is now called by the immediate notification email message handler.
 | 
			
		||||
     *
 | 
			
		||||
     * @throws TransportExceptionInterface
 | 
			
		||||
     */
 | 
			
		||||
    public function sendEmailToAddressee(Notification $notification, User $addressee): void
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $addressee->getEmail()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($notification->isSystem()) {
 | 
			
		||||
            $email = new Email();
 | 
			
		||||
            $email->text($notification->getMessage());
 | 
			
		||||
        } else {
 | 
			
		||||
            $email = new TemplatedEmail();
 | 
			
		||||
            $email
 | 
			
		||||
                ->subject($notification->getTitle())
 | 
			
		||||
                ->to($addressee->getEmail());
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                $this->mailer->send($email);
 | 
			
		||||
            } catch (TransportExceptionInterface $e) {
 | 
			
		||||
                $this->logger->warning('[NotificationMailer] could not send an email notification', [
 | 
			
		||||
                    'to' => $addressee->getEmail(),
 | 
			
		||||
                    'error_message' => $e->getMessage(),
 | 
			
		||||
                    'error_trace' => $e->getTraceAsString(),
 | 
			
		||||
                ->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
 | 
			
		||||
                ->context([
 | 
			
		||||
                    'notification' => $notification,
 | 
			
		||||
                    'dest' => $addressee,
 | 
			
		||||
                ]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $email
 | 
			
		||||
            ->subject($notification->getTitle())
 | 
			
		||||
            ->to($addressee->getEmail());
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $this->mailer->send($email);
 | 
			
		||||
            $this->logger->info('[NotificationMailer] Email sent successfully', [
 | 
			
		||||
                'notification_id' => $notification->getId(),
 | 
			
		||||
                'addressee_email' => $addressee->getEmail(),
 | 
			
		||||
            ]);
 | 
			
		||||
        } catch (TransportExceptionInterface $e) {
 | 
			
		||||
            $this->logger->warning('[NotificationMailer] Could not send an email notification', [
 | 
			
		||||
                'to' => $addressee->getEmail(),
 | 
			
		||||
                'notification_id' => $notification->getId(),
 | 
			
		||||
                'error_message' => $e->getMessage(),
 | 
			
		||||
                'error_trace' => $e->getTraceAsString(),
 | 
			
		||||
            ]);
 | 
			
		||||
            throw $e;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send daily digest email with multiple notifications to a user.
 | 
			
		||||
     *
 | 
			
		||||
     * @throws TransportExceptionInterface
 | 
			
		||||
     */
 | 
			
		||||
    public function sendDailyDigest(User $user, array $notifications): void
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $user->getEmail() || [] === $notifications) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $email = new TemplatedEmail();
 | 
			
		||||
        $email
 | 
			
		||||
            ->htmlTemplate('@ChillMain/Notification/email_daily_digest.fr.md.twig')
 | 
			
		||||
            ->context([
 | 
			
		||||
                'user' => $user,
 | 
			
		||||
                'notifications' => $notifications,
 | 
			
		||||
                'notification_count' => count($notifications),
 | 
			
		||||
            ])
 | 
			
		||||
            ->subject($this->translator->trans('notification.Daily Notification Digest'))
 | 
			
		||||
            ->to($user->getEmail());
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $this->mailer->send($email);
 | 
			
		||||
            $this->logger->info('[NotificationMailer] Daily digest email sent successfully', [
 | 
			
		||||
                'user_email' => $user->getEmail(),
 | 
			
		||||
                'notification_count' => count($notifications),
 | 
			
		||||
            ]);
 | 
			
		||||
        } catch (TransportExceptionInterface $e) {
 | 
			
		||||
            $this->logger->warning('[NotificationMailer] Could not send daily digest email', [
 | 
			
		||||
                'to' => $user->getEmail(),
 | 
			
		||||
                'notification_count' => count($notifications),
 | 
			
		||||
                'error_message' => $e->getMessage(),
 | 
			
		||||
                'error_trace' => $e->getTraceAsString(),
 | 
			
		||||
            ]);
 | 
			
		||||
            throw $e;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function sendNotificationEmailsToAddressesEmails(Notification $notification): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($notification->getAddressesEmailsAdded() as $emailAddress) {
 | 
			
		||||
        foreach ($notification->getAddresseeUserGroups() as $userGroup) {
 | 
			
		||||
 | 
			
		||||
            if (!$userGroup->hasEmail()) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $emailAddress = $userGroup->getEmail();
 | 
			
		||||
 | 
			
		||||
            $email = new TemplatedEmail();
 | 
			
		||||
            $email
 | 
			
		||||
                ->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig')
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
<?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\Notification\FlagProviders;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Translation\TranslatableMessage;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatableInterface;
 | 
			
		||||
 | 
			
		||||
class NotificationByUserFlagProvider implements NotificationFlagProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    public const FLAG = 'notif-by-user';
 | 
			
		||||
 | 
			
		||||
    public function getFlag(): string
 | 
			
		||||
    {
 | 
			
		||||
        return self::FLAG;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabel(): TranslatableInterface
 | 
			
		||||
    {
 | 
			
		||||
        return new TranslatableMessage('notification.flags.by-user');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
<?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\Notification\FlagProviders;
 | 
			
		||||
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatableInterface;
 | 
			
		||||
 | 
			
		||||
interface NotificationFlagProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    public function getFlag(): string;
 | 
			
		||||
 | 
			
		||||
    public function getLabel(): TranslatableInterface;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
<?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\Notification\FlagProviders;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Translation\TranslatableMessage;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatableInterface;
 | 
			
		||||
 | 
			
		||||
class WorkflowTransitionNotificationFlagProvider implements NotificationFlagProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    public const FLAG = 'workflow-trans-notif';
 | 
			
		||||
 | 
			
		||||
    public function getFlag(): string
 | 
			
		||||
    {
 | 
			
		||||
        return self::FLAG;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabel(): TranslatableInterface
 | 
			
		||||
    {
 | 
			
		||||
        return new TranslatableMessage('notification.flags.workflow-trans');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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\MainBundle\Notification;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class NotificationFlagManager
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var array<NotificationFlagProviderInterface>
 | 
			
		||||
     */
 | 
			
		||||
    private array $notificationFlagProviders;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        iterable $notificationFlagProviders,
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->notificationFlagProviders = iterator_to_array($notificationFlagProviders);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAllNotificationFlagProviders(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->notificationFlagProviders;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -290,12 +290,19 @@ final class NotificationRepository implements ObjectRepository
 | 
			
		||||
        return $qb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder
 | 
			
		||||
    private function queryByAddressee(User $addressee): QueryBuilder
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->repository->createQueryBuilder('n');
 | 
			
		||||
 | 
			
		||||
        $qb
 | 
			
		||||
            ->where($qb->expr()->isMemberOf(':addressee', 'n.addressees'))
 | 
			
		||||
            ->leftJoin('n.addresseeUserGroups', 'aug')
 | 
			
		||||
            ->leftJoin('aug.users', 'ugu')
 | 
			
		||||
            ->where(
 | 
			
		||||
                $qb->expr()->orX(
 | 
			
		||||
                    $qb->expr()->isMemberOf(':addressee', 'n.addressees'),
 | 
			
		||||
                    $qb->expr()->eq('ugu.id', ':addressee')
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            ->setParameter('addressee', $addressee);
 | 
			
		||||
 | 
			
		||||
        return $qb;
 | 
			
		||||
@@ -393,4 +400,30 @@ final class NotificationRepository implements ObjectRepository
 | 
			
		||||
 | 
			
		||||
        return $nq->getResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find all notifications for a user that were created between two dates.
 | 
			
		||||
     *
 | 
			
		||||
     * @return array|Notification[]
 | 
			
		||||
     */
 | 
			
		||||
    public function findNotificationsForUserBetweenDates(int $userId, \DateTimeInterface $startDate, \DateTimeInterface $endDate): array
 | 
			
		||||
    {
 | 
			
		||||
        $rsm = new Query\ResultSetMappingBuilder($this->em);
 | 
			
		||||
        $rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn');
 | 
			
		||||
 | 
			
		||||
        $sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '.
 | 
			
		||||
            'FROM chill_main_notification cmn '.
 | 
			
		||||
            'JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id '.
 | 
			
		||||
            'WHERE cmnau.user_id = :userId '.
 | 
			
		||||
            'AND cmn.date >= :startDate '.
 | 
			
		||||
            'AND cmn.date <= :endDate '.
 | 
			
		||||
            'ORDER BY cmn.date DESC';
 | 
			
		||||
 | 
			
		||||
        $nq = $this->em->createNativeQuery($sql, $rsm)
 | 
			
		||||
            ->setParameter('userId', $userId)
 | 
			
		||||
            ->setParameter('startDate', $startDate, Types::DATETIME_MUTABLE)
 | 
			
		||||
            ->setParameter('endDate', $endDate, Types::DATETIME_MUTABLE);
 | 
			
		||||
 | 
			
		||||
        return $nq->getResult();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -64,3 +64,5 @@ const props = defineProps({
 | 
			
		||||
    entity: Object,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
thirdparty_duplicate: merge: Fussioner find: 'Désigner un tiers doublon'
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
    {% endblock crud_content_header %}
 | 
			
		||||
 | 
			
		||||
    {% block crud_content_view %}
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        {% block crud_content_view_details %}
 | 
			
		||||
            <dl class="chill_view_data">
 | 
			
		||||
                <dt>id</dt>
 | 
			
		||||
@@ -20,7 +20,7 @@
 | 
			
		||||
                        {{ 'Cancel'|trans }}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endblock %} 
 | 
			
		||||
            {% endblock %}
 | 
			
		||||
            {% block content_view_actions_before %}{% endblock %}
 | 
			
		||||
            {% block content_form_actions_delete %}
 | 
			
		||||
                {% if chill_crud_action_exists(crud_name, 'delete') %}
 | 
			
		||||
@@ -32,7 +32,7 @@
 | 
			
		||||
                </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            {% endblock content_form_actions_delete %} 
 | 
			
		||||
            {% endblock content_form_actions_delete %}
 | 
			
		||||
            {% block content_view_actions_duplicate_link %}
 | 
			
		||||
                {% if chill_crud_action_exists(crud_name, 'new') %}
 | 
			
		||||
                {% if is_granted(chill_crud_config('role', crud_name, 'new'), entity) %}
 | 
			
		||||
@@ -44,6 +44,17 @@
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            {% endblock content_view_actions_duplicate_link %}
 | 
			
		||||
            {% block content_view_actions_merge %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    <a href="{{ chill_path_add_return_path('chill_thirdparty_find_duplicate',
 | 
			
		||||
                        { 'thirdparty_id': entity.id }) }}"
 | 
			
		||||
                       title="{{ 'Merge'|trans }}"
 | 
			
		||||
                       class="btn btn-misc">
 | 
			
		||||
                        <i class="bi bi-chevron-contract"></i>
 | 
			
		||||
                        {{ 'Merge'|trans }}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endblock %}
 | 
			
		||||
            {% block content_view_actions_edit_link %}
 | 
			
		||||
                {% if chill_crud_action_exists(crud_name, 'edit') %}
 | 
			
		||||
                {% if is_granted(chill_crud_config('role', crud_name, 'edit'), entity) %}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
            <strong><i class="fa fa-fw fa-filter"></i>Filtrer la liste</strong>
 | 
			
		||||
        </button>
 | 
			
		||||
    </h2>
 | 
			
		||||
    <div class="accordion-collapse collapse" id="filterOrderCollapse" aria-labelledby="filterOrderHeading" data-bs-parent="#filterOrderAccordion">
 | 
			
		||||
    <div id="filterOrderCollapse" aria-labelledby="filterOrderHeading" data-bs-parent="#filterOrderAccordion">
 | 
			
		||||
        {% set btnSubmit = 0 %}
 | 
			
		||||
        <div class="accordion-body chill_filter_order container-xxl p-5 py-2">
 | 
			
		||||
            <div class="row my-2">
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,9 @@
 | 
			
		||||
    {%- endif -%}
 | 
			
		||||
{%- endblock form_label %}
 | 
			
		||||
 | 
			
		||||
{# this has been rewritten for chill #}
 | 
			
		||||
{% block form_label_class -%}
 | 
			
		||||
    col-sm-4
 | 
			
		||||
    {% if 'div_col_width' in label_attr|default({})|keys %}{% if label_attr['div_col_width'] is not same as false %}{{ label_attr['div_col_width'] }}{% endif %}{% else %}col-sm-4{% endif %}
 | 
			
		||||
{%- endblock form_label_class %}
 | 
			
		||||
 | 
			
		||||
{# Rows #}
 | 
			
		||||
 
 | 
			
		||||
@@ -280,11 +280,17 @@
 | 
			
		||||
    </div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block pick_linked_entities_widget %}
 | 
			
		||||
    <input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}" />
 | 
			
		||||
    <div data-input-uniqid="{{ form.vars['uniqid'] }}" data-module="pick-linked-entities" data-pick-entities-type="{{ form.vars['pick-entities-type'] }}"
 | 
			
		||||
    ></div>
 | 
			
		||||
 | 
			
		||||
{% block pick_linked_entities_widget %}
 | 
			
		||||
    <input type="hidden" {{ block('widget_attributes') }}
 | 
			
		||||
        {% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %}
 | 
			
		||||
       data-input-uniqid="{{ form.vars['uniqid'] }}"/>
 | 
			
		||||
    <div
 | 
			
		||||
        data-input-uniqid="{{ form.vars['uniqid'] }}"
 | 
			
		||||
        data-module="pick-linked-entities"
 | 
			
		||||
        data-pick-entities-type="{{ form.vars['pick-entities-type'] }}"
 | 
			
		||||
         data-suggested="{{ form.vars['suggested']|json_encode|escape('html_attr') }}"
 | 
			
		||||
    ></div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block pick_postal_code_widget %}
 | 
			
		||||
 
 | 
			
		||||
@@ -69,41 +69,44 @@
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if c.notification.addressees|length > 0 %}
 | 
			
		||||
                {% if c.notification.addressees|length > 0 or c.notification.addresseeUserGroups|length > 0 %}
 | 
			
		||||
                    <li class="notification-to">
 | 
			
		||||
                        {% if c.notification_cc is defined %}
 | 
			
		||||
                            {% if c.notification_cc %}
 | 
			
		||||
                                <span class="item-key">
 | 
			
		||||
                    <abbr title="{{ 'notification.sent_cc' | trans }}">
 | 
			
		||||
                        {{ "notification.cc" | trans }} :
 | 
			
		||||
                    </abbr>
 | 
			
		||||
                </span>
 | 
			
		||||
                                    <abbr title="{{ 'notification.sent_cc' | trans }}">
 | 
			
		||||
                                        {{ "notification.cc" | trans }} :
 | 
			
		||||
                                    </abbr>
 | 
			
		||||
                                </span>
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                                <span class="item-key">
 | 
			
		||||
                    <abbr title="{{ 'notification.sent_to' | trans }}">
 | 
			
		||||
                        {{ "notification.to" | trans }} :
 | 
			
		||||
                    </abbr>
 | 
			
		||||
                </span>
 | 
			
		||||
                                    <abbr title="{{ 'notification.sent_to' | trans }}">
 | 
			
		||||
                                        {{ "notification.to" | trans }} :
 | 
			
		||||
                                    </abbr>
 | 
			
		||||
                                </span>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            <span class="item-key">
 | 
			
		||||
                    <abbr title="{{ 'notification.sent_to' | trans }}">
 | 
			
		||||
                        {{ "notification.to" | trans }} :
 | 
			
		||||
                    </abbr>
 | 
			
		||||
                </span>
 | 
			
		||||
                                <abbr title="{{ 'notification.sent_to' | trans }}">
 | 
			
		||||
                                    {{ "notification.to" | trans }} :
 | 
			
		||||
                                </abbr>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% for a in c.notification.addressees %}
 | 
			
		||||
                            <span class="badge-user">
 | 
			
		||||
                    {{ a | chill_entity_render_string({'at_date': c.notification.date}) }}
 | 
			
		||||
                </span>
 | 
			
		||||
                                {{ a | chill_entity_render_string({'at_date': c.notification.date}) }}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                        {% for a in c.notification.addressesEmails %}
 | 
			
		||||
                            <span
 | 
			
		||||
                                class="badge-user"
 | 
			
		||||
                                title="{{ 'notification.Email with access link'|trans|e('html_attr') }}"
 | 
			
		||||
                            >
 | 
			
		||||
                    {{ a }}
 | 
			
		||||
                </span>
 | 
			
		||||
                                {{ a }}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                        {% for ug in c.notification.addresseeUserGroups %}
 | 
			
		||||
                            {{ ug|chill_entity_render_box }}
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,6 @@
 | 
			
		||||
    {{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
 | 
			
		||||
    {{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
 | 
			
		||||
 | 
			
		||||
    {{ form_row(form.addressesEmails) }}
 | 
			
		||||
 | 
			
		||||
    {% include handler.template(notification) with handler.templateData(notification) %}
 | 
			
		||||
 | 
			
		||||
    <div class="mb-3 row">
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
{% apply markdown_to_html %}
 | 
			
		||||
# {{ 'notification.daily_digest.title'|trans }}
 | 
			
		||||
 | 
			
		||||
{{ 'notification.daily_digest.greeting'|trans({'%user%': user.label ?? user.email}) }},
 | 
			
		||||
 | 
			
		||||
{{ 'daily_notifications'|trans({'notification_count': notification_count}) }}
 | 
			
		||||
 | 
			
		||||
{% for notification in notifications %}
 | 
			
		||||
## {{ notification.title }}
 | 
			
		||||
 | 
			
		||||
{{ notification.message }}
 | 
			
		||||
 | 
			
		||||
{{ 'notification.daily_digest.view_notification'|trans }}
 | 
			
		||||
 | 
			
		||||
{{ absolute_url(path('chill_main_notification_show', {'_locale': user.locale, 'id': notification.id }, false)) }}
 | 
			
		||||
 | 
			
		||||
{% if not loop.last %}
 | 
			
		||||
---
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endfor %}
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
{{ 'notification.daily_digest.signature'|trans }}
 | 
			
		||||
{% endapply %}
 | 
			
		||||
@@ -20,7 +20,7 @@
 | 
			
		||||
{%  extends "@ChillMain/layout.html.twig" %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block title %}{{"My profile"|trans}}{% endblock %}
 | 
			
		||||
{% block title %}{{"user.profile.title"|trans}}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <div class="justify-content-center col-10">
 | 
			
		||||
@@ -45,9 +45,35 @@
 | 
			
		||||
            {{ form_start(form) }}
 | 
			
		||||
            {{ form_row(form.phonenumber) }}
 | 
			
		||||
 | 
			
		||||
            <h2 class="mb-4">{{ 'user.profile.notification_preferences'|trans }}</h2>
 | 
			
		||||
            <table class="table table-striped align-middle">
 | 
			
		||||
                <thead>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th>{{ 'notification.flags.type'|trans }}</th>
 | 
			
		||||
                    <th class="text-center">{{ 'notification.flags.preferences.immediate_email'|trans }}</th>
 | 
			
		||||
                    <th class="text-center">{{ 'notification.flags.preferences.daily_email'|trans }}</th>
 | 
			
		||||
                </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody class="table-hover table-group-divider">
 | 
			
		||||
                {% for flag in form.notificationFlags %}
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <td class="col-sm-6">
 | 
			
		||||
                           {{ form_label(flag, null, {'label_attr': {'div_col_width': false}}) }}
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td class="text-center">
 | 
			
		||||
                            {{ form_widget(flag.immediate_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td class="text-center">
 | 
			
		||||
                            {{ form_widget(flag.daily_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
 | 
			
		||||
            <ul class="record_actions">
 | 
			
		||||
                <li>
 | 
			
		||||
                    {{ form_widget(form.submit, { 'attr': { 'class': 'btn btn-save' } } ) }}
 | 
			
		||||
                    <button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ declare(strict_types=1);
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Entity;
 | 
			
		||||
namespace Chill\MainBundle\Tests\Entity;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Notification;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
@@ -49,8 +49,8 @@ final class NotificationTest extends KernelTestCase
 | 
			
		||||
        $notification = new Notification();
 | 
			
		||||
        $notification->addAddressee($user1 = new User());
 | 
			
		||||
        $notification->addAddressee($user2 = new User());
 | 
			
		||||
        $notification->getAddressees()->add($user3 = new User());
 | 
			
		||||
        $notification->getAddressees()->add($user4 = new User());
 | 
			
		||||
        $notification->addAddressee($user3 = new User());
 | 
			
		||||
        $notification->addAddressee($user4 = new User());
 | 
			
		||||
 | 
			
		||||
        $this->assertCount(4, $notification->getAddressees());
 | 
			
		||||
 | 
			
		||||
@@ -85,6 +85,30 @@ final class NotificationTest extends KernelTestCase
 | 
			
		||||
        $this->assertNotContains('other', $notification->getAddressesEmailsAdded());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testIsSendImmediately(): void
 | 
			
		||||
    {
 | 
			
		||||
        $notification = new Notification();
 | 
			
		||||
        $notification->setType('test_notification_type');
 | 
			
		||||
 | 
			
		||||
        $user = new User();
 | 
			
		||||
 | 
			
		||||
        // no notification flags
 | 
			
		||||
        $this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when no notification flags are set, by default immediate email');
 | 
			
		||||
 | 
			
		||||
        // immediate-email preference
 | 
			
		||||
        $user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_IMMEDIATE_EMAIL, User::NOTIF_FLAG_DAILY_DIGEST]]);
 | 
			
		||||
        $this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when preferences contain immediate-email');
 | 
			
		||||
 | 
			
		||||
        // daily-email preference
 | 
			
		||||
        $user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_DAILY_DIGEST]]);
 | 
			
		||||
        $this->assertFalse($user->isNotificationSendImmediately($notification->getType()), 'Should return false when preference is daily-email only');
 | 
			
		||||
        $this->assertTrue($user->isNotificationDailyDigest($notification->getType()), 'Should return true when preference is daily-email');
 | 
			
		||||
 | 
			
		||||
        // a different notification type
 | 
			
		||||
        $notification->setType('other_notification_type');
 | 
			
		||||
        $this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return false when notification type does not match any preference');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider generateNotificationData
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -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\MainBundle\Tests\Notification\Email;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Notification\Email\DailyNotificationDigestCronjob;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Run functional test on the cronjob.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class DailyNotificationDigestCronJobFunctionalTest extends KernelTestCase
 | 
			
		||||
{
 | 
			
		||||
    private DailyNotificationDigestCronjob $dailyNotificationDigestCronjob;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
 | 
			
		||||
        $this->dailyNotificationDigestCronjob = self::getContainer()->get(DailyNotificationDigestCronjob::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testRunWithNullPreviousExecutionData(): void
 | 
			
		||||
    {
 | 
			
		||||
        $actual = $this->dailyNotificationDigestCronjob->run([]);
 | 
			
		||||
 | 
			
		||||
        self::assertArrayHasKey('last_execution', $actual);
 | 
			
		||||
        self::assertInstanceOf(
 | 
			
		||||
            \DateTimeImmutable::class,
 | 
			
		||||
            \DateTimeImmutable::createFromFormat('Y-m-d-H:i:s.u e', $actual['last_execution']),
 | 
			
		||||
            'test that the string can be converted to a date'
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,81 @@
 | 
			
		||||
<?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\Notification\Email;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Notification\Email\DailyNotificationDigestCronjob;
 | 
			
		||||
use Doctrine\DBAL\Connection;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\Clock\ClockInterface;
 | 
			
		||||
use Symfony\Component\Messenger\MessageBusInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class DailyNotificationDigestCronJobTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    private ClockInterface $clock;
 | 
			
		||||
    private Connection $connection;
 | 
			
		||||
    private MessageBusInterface $messageBus;
 | 
			
		||||
    private LoggerInterface $logger;
 | 
			
		||||
    private DailyNotificationDigestCronjob $cronjob;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->clock = $this->createMock(ClockInterface::class);
 | 
			
		||||
        $this->connection = $this->createMock(Connection::class);
 | 
			
		||||
        $this->messageBus = $this->createMock(MessageBusInterface::class);
 | 
			
		||||
        $this->logger = $this->createMock(LoggerInterface::class);
 | 
			
		||||
 | 
			
		||||
        $this->cronjob = new DailyNotificationDigestCronjob(
 | 
			
		||||
            $this->clock,
 | 
			
		||||
            $this->connection,
 | 
			
		||||
            $this->messageBus,
 | 
			
		||||
            $this->logger
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testGetKey(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->assertEquals('daily-notification-digest', $this->cronjob->getKey());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider canRunTimeDataProvider
 | 
			
		||||
     */
 | 
			
		||||
    public function testCanRunWithNullCronJobExecution(int $hour, bool $expected): void
 | 
			
		||||
    {
 | 
			
		||||
        $now = new \DateTimeImmutable("2024-01-01 {$hour}:00:00");
 | 
			
		||||
        $this->clock->expects($this->once())
 | 
			
		||||
            ->method('now')
 | 
			
		||||
            ->willReturn($now);
 | 
			
		||||
 | 
			
		||||
        $result = $this->cronjob->canRun(null);
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals($expected, $result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function canRunTimeDataProvider(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'hour 5 - should not run' => [5, false],
 | 
			
		||||
            'hour 6 - should run' => [6, true],
 | 
			
		||||
            'hour 7 - should run' => [7, true],
 | 
			
		||||
            'hour 8 - should run' => [8, true],
 | 
			
		||||
            'hour 9 - should not run' => [9, false],
 | 
			
		||||
            'hour 10 - should not run' => [10, false],
 | 
			
		||||
            'hour 23 - should not run' => [23, false],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,13 +17,18 @@ use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Notification\Email\NotificationMailer;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\Event\PostPersistEventArgs;
 | 
			
		||||
use PHPUnit\Framework\MockObject\Exception;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Psr\Log\NullLogger;
 | 
			
		||||
use Symfony\Component\Mailer\MailerInterface;
 | 
			
		||||
use Symfony\Component\Messenger\Envelope;
 | 
			
		||||
use Symfony\Component\Messenger\MessageBusInterface;
 | 
			
		||||
use Symfony\Component\Mime\Email;
 | 
			
		||||
use Symfony\Component\Translation\Translator;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
@@ -112,13 +117,149 @@ class NotificationMailerTest extends TestCase
 | 
			
		||||
        $mailer->postPersistComment($comment, new PostPersistEventArgs($comment, $objectManager->reveal()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws \ReflectionException
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function testProcessNotificationForAddresseeWithImmediateEmailPreference(): void
 | 
			
		||||
    {
 | 
			
		||||
        // Create a real notification entity
 | 
			
		||||
        $notification = new Notification();
 | 
			
		||||
        $notification->setType('test_notification_type');
 | 
			
		||||
 | 
			
		||||
        // Use reflection to set the ID since it's normally generated by the database
 | 
			
		||||
        $reflectionNotification = new \ReflectionClass(Notification::class);
 | 
			
		||||
        $idProperty = $reflectionNotification->getProperty('id');
 | 
			
		||||
        $idProperty->setAccessible(true);
 | 
			
		||||
        $idProperty->setValue($notification, 123);
 | 
			
		||||
 | 
			
		||||
        // Create a real user entity
 | 
			
		||||
        $user = new User();
 | 
			
		||||
        $user->setEmail('user@example.com');
 | 
			
		||||
 | 
			
		||||
        // Use reflection to set the ID since it's normally generated by the database
 | 
			
		||||
        $reflectionUser = new \ReflectionClass(User::class);
 | 
			
		||||
        $idProperty = $reflectionUser->getProperty('id');
 | 
			
		||||
        $idProperty->setAccessible(true);
 | 
			
		||||
        $idProperty->setValue($user, 456);
 | 
			
		||||
 | 
			
		||||
        // Set notification flags for the user
 | 
			
		||||
        $user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_IMMEDIATE_EMAIL]]);
 | 
			
		||||
 | 
			
		||||
        $messageBus = $this->createMock(MessageBusInterface::class);
 | 
			
		||||
        $messageBus->expects($this->once())
 | 
			
		||||
            ->method('dispatch')
 | 
			
		||||
            ->with($this->callback(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId()
 | 
			
		||||
                && 456 === $message->getAddresseeId()))
 | 
			
		||||
            ->willReturn(new Envelope(new \stdClass()));
 | 
			
		||||
 | 
			
		||||
        $mailer = $this->buildNotificationMailer(null, $messageBus);
 | 
			
		||||
 | 
			
		||||
        // Call the method that processes notifications
 | 
			
		||||
        $reflection = new \ReflectionClass(NotificationMailer::class);
 | 
			
		||||
        $method = $reflection->getMethod('processNotificationForAddressee');
 | 
			
		||||
        $method->setAccessible(true);
 | 
			
		||||
        $method->invoke($mailer, $notification, $user);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testSendDailyDigest(): void
 | 
			
		||||
    {
 | 
			
		||||
        // Create a user
 | 
			
		||||
        $user = new User();
 | 
			
		||||
        $user->setEmail('user@example.com');
 | 
			
		||||
 | 
			
		||||
        // Create some notifications
 | 
			
		||||
        $notification = $this->prophesize(Notification::class);
 | 
			
		||||
        $notification->getTitle()->willReturn('Notification 1');
 | 
			
		||||
        $notification->getMessage()->willReturn('Message 1');
 | 
			
		||||
        $notification->getId()->willReturn(123);
 | 
			
		||||
 | 
			
		||||
        $notification2 = $this->prophesize(Notification::class);
 | 
			
		||||
        $notification2->getTitle()->willReturn('Notification 2');
 | 
			
		||||
        $notification2->getMessage()->willReturn('Message 2');
 | 
			
		||||
        $notification2->getId()->willReturn(456);
 | 
			
		||||
 | 
			
		||||
        $notifications = [$notification, $notification2];
 | 
			
		||||
 | 
			
		||||
        // Mock the mailer to verify that an email is sent with the correct parameters
 | 
			
		||||
        $mailer = $this->prophesize(MailerInterface::class);
 | 
			
		||||
        $mailer->send(Argument::that(function ($email) use ($user) {
 | 
			
		||||
            // Verify that the email is sent to the correct user
 | 
			
		||||
            foreach ($email->getTo() as $address) {
 | 
			
		||||
                if ($user->getEmail() === $address->getAddress()) {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }))->shouldBeCalledOnce();
 | 
			
		||||
 | 
			
		||||
        // Create a translator that returns a fixed string for the subject
 | 
			
		||||
        $translator = $this->prophesize(TranslatorInterface::class);
 | 
			
		||||
        $translator->trans('notification.Daily Notification Digest')->willReturn('Daily Digest');
 | 
			
		||||
 | 
			
		||||
        // Create the notification mailer with the mocked mailer and translator
 | 
			
		||||
        $notificationMailer = $this->buildNotificationMailer($mailer->reveal(), null, $translator->reveal());
 | 
			
		||||
 | 
			
		||||
        // Call the sendDailyDigest method
 | 
			
		||||
        $notificationMailer->sendDailyDigest($user, $notifications);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testSendDailyDigestWithNoNotifications(): void
 | 
			
		||||
    {
 | 
			
		||||
        // Create a user
 | 
			
		||||
        $user = new User();
 | 
			
		||||
        $user->setEmail('user@example.com');
 | 
			
		||||
 | 
			
		||||
        // Empty notifications array
 | 
			
		||||
        $notifications = [];
 | 
			
		||||
 | 
			
		||||
        // Mock the mailer to verify that no email is sent
 | 
			
		||||
        $mailer = $this->prophesize(MailerInterface::class);
 | 
			
		||||
        $mailer->send(Argument::any())->shouldNotBeCalled();
 | 
			
		||||
 | 
			
		||||
        // Create the notification mailer with the mocked mailer
 | 
			
		||||
        $notificationMailer = $this->buildNotificationMailer($mailer->reveal());
 | 
			
		||||
 | 
			
		||||
        // Call the sendDailyDigest method
 | 
			
		||||
        $notificationMailer->sendDailyDigest($user, $notifications);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testSendDailyDigestWithUserHavingNoEmail(): void
 | 
			
		||||
    {
 | 
			
		||||
        // Create a user with no email
 | 
			
		||||
        $user = new User();
 | 
			
		||||
        $user->setEmail(null);
 | 
			
		||||
 | 
			
		||||
        // Create some notifications
 | 
			
		||||
        $notification = $this->prophesize(Notification::class);
 | 
			
		||||
        $notification->getTitle()->willReturn('Notification 1');
 | 
			
		||||
        $notification->getMessage()->willReturn('Message 1');
 | 
			
		||||
        $notification->getId()->willReturn(123);
 | 
			
		||||
 | 
			
		||||
        $notifications = [$notification];
 | 
			
		||||
 | 
			
		||||
        // Mock the mailer to verify that no email is sent
 | 
			
		||||
        $mailer = $this->prophesize(MailerInterface::class);
 | 
			
		||||
        $mailer->send(Argument::any())->shouldNotBeCalled();
 | 
			
		||||
 | 
			
		||||
        // Create the notification mailer with the mocked mailer
 | 
			
		||||
        $notificationMailer = $this->buildNotificationMailer($mailer->reveal());
 | 
			
		||||
 | 
			
		||||
        // Call the sendDailyDigest method
 | 
			
		||||
        $notificationMailer->sendDailyDigest($user, $notifications);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildNotificationMailer(
 | 
			
		||||
        ?MailerInterface $mailer = null,
 | 
			
		||||
        ?MessageBusInterface $messageBus = null,
 | 
			
		||||
        ?TranslatorInterface $translator = null,
 | 
			
		||||
    ): NotificationMailer {
 | 
			
		||||
        return new NotificationMailer(
 | 
			
		||||
            $mailer,
 | 
			
		||||
            $mailer ?? $this->prophesize(MailerInterface::class)->reveal(),
 | 
			
		||||
            new NullLogger(),
 | 
			
		||||
            new Translator('fr')
 | 
			
		||||
            $messageBus ?? $this->prophesize(MessageBusInterface::class)->reveal(),
 | 
			
		||||
            $translator ?? new Translator('fr')
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Notification;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Notification\FlagProviders\WorkflowTransitionNotificationFlagProvider;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
@@ -125,7 +126,8 @@ class NotificationOnTransition implements EventSubscriberInterface
 | 
			
		||||
                ->setRelatedEntityClass(EntityWorkflow::class)
 | 
			
		||||
                ->setTitle($this->engine->render('@ChillMain/Workflow/workflow_notification_on_transition_completed_title.fr.txt.twig', $context))
 | 
			
		||||
                ->setMessage($this->engine->render('@ChillMain/Workflow/workflow_notification_on_transition_completed_content.fr.txt.twig', $context))
 | 
			
		||||
                ->addAddressee($subscriber);
 | 
			
		||||
                ->addAddressee($subscriber)
 | 
			
		||||
                ->setType(WorkflowTransitionNotificationFlagProvider::FLAG);
 | 
			
		||||
            $this->entityManager->persist($notification);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -139,6 +139,11 @@ services:
 | 
			
		||||
        autowire: true
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
 | 
			
		||||
    Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper:
 | 
			
		||||
        autowire: true
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
 | 
			
		||||
    Chill\MainBundle\Form\UserProfileType: ~
 | 
			
		||||
    Chill\MainBundle\Form\AbsenceType: ~
 | 
			
		||||
    Chill\MainBundle\Form\DataMapper\RegroupmentDataMapper: ~
 | 
			
		||||
    Chill\MainBundle\Form\RegroupmentType: ~
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,10 @@ services:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $routeParameters: '%chill_main.notifications%'
 | 
			
		||||
 | 
			
		||||
    Chill\MainBundle\Notification\NotificationFlagManager:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $notificationFlagProviders: !tagged_iterator chill_main.notification_flag_provider
 | 
			
		||||
 | 
			
		||||
    Chill\MainBundle\Notification\NotificationHandlerManager:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $handlers: !tagged_iterator chill_main.notification_handler
 | 
			
		||||
@@ -55,14 +59,6 @@ services:
 | 
			
		||||
                lazy: true
 | 
			
		||||
                method: 'postPersistNotification'
 | 
			
		||||
 | 
			
		||||
            -
 | 
			
		||||
                name: 'doctrine.orm.entity_listener'
 | 
			
		||||
                event: 'postUpdate'
 | 
			
		||||
                entity: 'Chill\MainBundle\Entity\Notification'
 | 
			
		||||
                # set the 'lazy' option to TRUE to only instantiate listeners when they are used
 | 
			
		||||
                lazy: true
 | 
			
		||||
                method: 'postUpdateNotification'
 | 
			
		||||
 | 
			
		||||
            -
 | 
			
		||||
                name: 'doctrine.orm.entity_listener'
 | 
			
		||||
                event: 'postPersist'
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
<?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 Version20250610102953 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Add notification flags property to User';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            ALTER TABLE users ADD notificationFlags JSONB DEFAULT '[]' NOT NULL
 | 
			
		||||
        SQL);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            ALTER TABLE users DROP notificationFlags
 | 
			
		||||
        SQL);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
<?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 Version20250618115938 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Add type property to notifications';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            ALTER TABLE chill_main_notification ADD type VARCHAR(255) NOT NULL DEFAULT 'default_notification_type'
 | 
			
		||||
        SQL);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            ALTER TABLE chill_main_notification DROP type
 | 
			
		||||
        SQL);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
<?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 Version20250623120824 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Add addressee user groups to notifications';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            CREATE TABLE chill_main_notification_addressee_user_group (notification_id INT NOT NULL, usergroup_id INT NOT NULL, PRIMARY KEY(notification_id, usergroup_id))
 | 
			
		||||
        SQL);
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            CREATE INDEX IDX_ECF81C07EF1A9D84 ON chill_main_notification_addressee_user_group (notification_id)
 | 
			
		||||
        SQL);
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            CREATE INDEX IDX_ECF81C07D2112630 ON chill_main_notification_addressee_user_group (usergroup_id)
 | 
			
		||||
        SQL);
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            ALTER TABLE chill_main_notification_addressee_user_group ADD CONSTRAINT FK_ECF81C07EF1A9D84 FOREIGN KEY (notification_id) REFERENCES chill_main_notification (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
 | 
			
		||||
        SQL);
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            ALTER TABLE chill_main_notification_addressee_user_group ADD CONSTRAINT FK_ECF81C07D2112630 FOREIGN KEY (usergroup_id) REFERENCES chill_main_user_group (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
 | 
			
		||||
        SQL);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            ALTER TABLE chill_main_notification_addressee_user_group DROP CONSTRAINT FK_ECF81C07EF1A9D84
 | 
			
		||||
        SQL);
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            ALTER TABLE chill_main_notification_addressee_user_group DROP CONSTRAINT FK_ECF81C07D2112630
 | 
			
		||||
        SQL);
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            DROP TABLE chill_main_notification_addressee_user_group
 | 
			
		||||
        SQL);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -49,6 +49,12 @@ notification:
 | 
			
		||||
            other {# commentaires}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
daily_notifications: >-
 | 
			
		||||
   {notification_count, plural,
 | 
			
		||||
        =1 {Voici votre notification du jour :}
 | 
			
		||||
        other {Voici vos # notifications du jour :}
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
workflow:
 | 
			
		||||
    My workflows with counter: >-
 | 
			
		||||
        {wc, plural,
 | 
			
		||||
 
 | 
			
		||||
@@ -52,9 +52,10 @@ user:
 | 
			
		||||
    current_user: Utilisateur courant
 | 
			
		||||
    profile:
 | 
			
		||||
        title: Mon profil
 | 
			
		||||
        Phonenumber successfully updated!: Numéro de téléphone mis à jour!
 | 
			
		||||
        Profile successfully updated!: Votre profil a été mis à jour!
 | 
			
		||||
        no job: Pas de métier assigné
 | 
			
		||||
        no scope: Pas de cercle assigné
 | 
			
		||||
        notification_preferences: Préférences pour mes notifications
 | 
			
		||||
 | 
			
		||||
user_group:
 | 
			
		||||
    inactive: Inactif
 | 
			
		||||
@@ -674,6 +675,7 @@ Subscribe all steps: Recevoir une notification à chaque étape
 | 
			
		||||
CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION: Appliquer les transitions sur tous les workflows
 | 
			
		||||
 | 
			
		||||
notification:
 | 
			
		||||
    Daily Notification Digest: Résumé des notifications quotidiennes
 | 
			
		||||
    Notification: Notification
 | 
			
		||||
    Notifications: Notifications
 | 
			
		||||
    My own notifications: Mes notifications
 | 
			
		||||
@@ -712,13 +714,36 @@ notification:
 | 
			
		||||
    dest by email help: Les adresses email mentionnées ici recevront un lien d'accès. Un compte utilisateur sera toujours nécessaire.
 | 
			
		||||
    Remove an email: Supprimer l'adresse email
 | 
			
		||||
    Email with access link: Adresse email ayant reçu un lien d'accès
 | 
			
		||||
    Pick user or user group: Selectionner un utilisateur / groupe d'utilisateurs
 | 
			
		||||
 | 
			
		||||
    mark_as_read: Marquer comme lu
 | 
			
		||||
    mark_as_unread: Marquer comme non-lu
 | 
			
		||||
 | 
			
		||||
    flags:
 | 
			
		||||
        type: Type de notification
 | 
			
		||||
        by-user: Lorsqu'un utilisateur vous envoie une notification personnelle
 | 
			
		||||
        referrer-acc-course: Lorsqu'un autre utilisateur vous désigne comme référent d'un parcours
 | 
			
		||||
        person-address-move: Lorsqu'un autre utilisateur enregistre le déménagement d'un usager concerné par un parcours dont vous êtes le référent.
 | 
			
		||||
        person: Notification sur un usager
 | 
			
		||||
        workflow-trans: Lorsqu'un autre utilisateur applique une transition à un workflow.
 | 
			
		||||
        none selected message: Si vous ne sélectionnez aucune option, vous ne recevrez pas d'email concernant ce type de notification.
 | 
			
		||||
        preferences:
 | 
			
		||||
            column_title: Préférences
 | 
			
		||||
            immediate_email: Email immédiat
 | 
			
		||||
            daily_email: Récapitulatif quotidien
 | 
			
		||||
            no_email: Ne pas recevoir un email
 | 
			
		||||
 | 
			
		||||
    daily_digest:
 | 
			
		||||
        title: "Résumé quotidien des notifications"
 | 
			
		||||
        greeting: "Bonjour %user%"
 | 
			
		||||
        intro: "Vous avez reçu %notification_count% nouvelle(s) notification(s)."
 | 
			
		||||
        view_notification: "Vous pouvez visualiser la notification et y répondre ici:"
 | 
			
		||||
        signature: "Le logiciel Chill"
 | 
			
		||||
 | 
			
		||||
CHILL_MAIN_COMPOSE_EXPORT: Exécuter des exports et les sauvegarder
 | 
			
		||||
CHILL_MAIN_GENERATE_SAVED_EXPORT: Exécuter et modifier des exports préalablement sauvegardés
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export:
 | 
			
		||||
    role:
 | 
			
		||||
        export_role: Exports
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\Notification;
 | 
			
		||||
use Chill\MainBundle\Notification\NotificationPersisterInterface;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
 | 
			
		||||
use Chill\PersonBundle\Notification\FlagProviders\PersonAddressMoveNotificationFlagProvider;
 | 
			
		||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
@@ -65,7 +66,8 @@ class PersonAddressMoveEventSubscriber implements EventSubscriberInterface
 | 
			
		||||
                        ->setMessage($this->engine->render('@ChillPerson/AccompanyingPeriod/notification_location_user_on_period_has_moved.fr.txt.twig', [
 | 
			
		||||
                            'oldPersonLocation' => $person,
 | 
			
		||||
                            'period' => $period,
 | 
			
		||||
                        ]));
 | 
			
		||||
                        ]))
 | 
			
		||||
                        ->setType(PersonAddressMoveNotificationFlagProvider::FLAG);
 | 
			
		||||
 | 
			
		||||
                    $this->notificationPersister->persist($notification);
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Notification;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Notification\NotificationPersisterInterface;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Chill\PersonBundle\Notification\FlagProviders\DesignatedReferrerNotificationFlagProvider;
 | 
			
		||||
use Doctrine\Persistence\Event\LifecycleEventArgs;
 | 
			
		||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
@@ -73,7 +74,8 @@ class UserRefEventSubscriber implements EventSubscriberInterface
 | 
			
		||||
                    'accompanyingCourse' => $period,
 | 
			
		||||
                ]
 | 
			
		||||
            ))
 | 
			
		||||
            ->addAddressee($period->getUser());
 | 
			
		||||
            ->addAddressee($period->getUser())
 | 
			
		||||
            ->setType(DesignatedReferrerNotificationFlagProvider::FLAG);
 | 
			
		||||
 | 
			
		||||
        $this->notificationPersister->persist($notification);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\PersonBundle;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
 | 
			
		||||
use Chill\PersonBundle\Actions\Remove\PersonMoveSqlHandlerInterface;
 | 
			
		||||
use Chill\PersonBundle\DependencyInjection\CompilerPass\AccompanyingPeriodTimelineCompilerPass;
 | 
			
		||||
use Chill\PersonBundle\Export\Helper\CustomizeListPersonHelperInterface;
 | 
			
		||||
@@ -35,5 +36,7 @@ class ChillPersonBundle extends Bundle
 | 
			
		||||
            ->addTag('chill_person.person_move_handler');
 | 
			
		||||
        $container->registerForAutoconfiguration(CustomizeListPersonHelperInterface::class)
 | 
			
		||||
            ->addTag('chill_person.list_person_customizer');
 | 
			
		||||
        $container->registerForAutoconfiguration(NotificationFlagProviderInterface::class)
 | 
			
		||||
            ->addTag('chill_main.notification_flag_provider');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\PersonBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
 | 
			
		||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
 | 
			
		||||
@@ -130,6 +131,7 @@ final class AccompanyingCourseWorkController extends AbstractController
 | 
			
		||||
        $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::SEE, $period);
 | 
			
		||||
 | 
			
		||||
        $filter = $this->buildFilterOrder($period);
 | 
			
		||||
        $currentUser = $this->getUser();
 | 
			
		||||
 | 
			
		||||
        $filterData = [
 | 
			
		||||
            'types' => $filter->hasEntityChoice('typesFilter') ? $filter->getEntityChoiceData('typesFilter') : [],
 | 
			
		||||
@@ -138,6 +140,10 @@ final class AccompanyingCourseWorkController extends AbstractController
 | 
			
		||||
            'user' => $filter->getUserPickerData('userFilter'),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if ($filter->getSingleCheckboxData('currentUserFilter') && $currentUser instanceof User) {
 | 
			
		||||
            $filterData['currentUser'] = $currentUser;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $totalItems = $this->workRepository->countByAccompanyingPeriod($period);
 | 
			
		||||
        $paginator = $this->paginator->create($totalItems);
 | 
			
		||||
 | 
			
		||||
@@ -201,6 +207,8 @@ final class AccompanyingCourseWorkController extends AbstractController
 | 
			
		||||
            ->addUserPicker('userFilter', 'accompanying_course_work.user_filter', ['required' => false])
 | 
			
		||||
        ;
 | 
			
		||||
 | 
			
		||||
        $filterBuilder->addSingleCheckbox('currentUserFilter', 'accompanying_course_work.my_actions_filter');
 | 
			
		||||
 | 
			
		||||
        return $filterBuilder->build();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepos
 | 
			
		||||
use Chill\PersonBundle\Service\AccompanyingPeriodWork\AccompanyingPeriodWorkMergeService;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Session\Session;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Translation\TranslatableMessage;
 | 
			
		||||
@@ -32,7 +33,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
 | 
			
		||||
     * @ParamConverter("accompanyingPeriodWork", options={"id": "acpw_id"})
 | 
			
		||||
     */
 | 
			
		||||
    #[Route(path: '{_locale}/person/accompanying-period/work/{id}/assign-duplicate', name: 'chill_person_accompanying_period_work_assign_duplicate')]
 | 
			
		||||
    public function assignDuplicate(AccompanyingPeriodWork $acpw, Request $request)
 | 
			
		||||
    public function assignDuplicate(AccompanyingPeriodWork $acpw, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        $accompanyingPeriod = $acpw->getAccompanyingPeriod();
 | 
			
		||||
 | 
			
		||||
@@ -79,7 +80,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
 | 
			
		||||
     * @ParamConverter("acpw2", options={"id": "acpw2_id"})
 | 
			
		||||
     */
 | 
			
		||||
    #[Route(path: '/{_locale}/person/{acpw1_id}/duplicate/{acpw2_id}/confirm', name: 'chill_person_acpw_duplicate_confirm')]
 | 
			
		||||
    public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request)
 | 
			
		||||
    public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        $accompanyingPeriod = $acpw1->getAccompanyingPeriod();
 | 
			
		||||
 | 
			
		||||
@@ -98,6 +99,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
 | 
			
		||||
                    $session->getFlashBag()->add('success', new TranslatableMessage('acpw_duplicate.Successfully merged'));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                return $this->redirectToRoute('chill_person_accompanying_period_work_show', ['id' => $acpw1->getId()]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\PersonBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
 | 
			
		||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
 | 
			
		||||
use Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument\AccompanyingPeriodWorkEvaluationDocumentDuplicator;
 | 
			
		||||
@@ -24,15 +25,16 @@ use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
 | 
			
		||||
use Symfony\Component\Serializer\SerializerInterface;
 | 
			
		||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
 | 
			
		||||
 | 
			
		||||
class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
 | 
			
		||||
readonly class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly AccompanyingPeriodWorkEvaluationDocumentDuplicator $duplicator,
 | 
			
		||||
        private readonly Security $security,
 | 
			
		||||
        private readonly SerializerInterface $serializer,
 | 
			
		||||
        private readonly EntityManagerInterface $entityManager,
 | 
			
		||||
        private readonly ChillUrlGeneratorInterface $urlGenerator,
 | 
			
		||||
        private AccompanyingPeriodWorkEvaluationDocumentDuplicator $duplicator,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private SerializerInterface $serializer,
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
        private ChillUrlGeneratorInterface $urlGenerator,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{id}/duplicate', methods: ['POST'])]
 | 
			
		||||
@@ -56,6 +58,32 @@ class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ParamConverter("document", options={"id": "document_id"})
 | 
			
		||||
     * @ParamConverter("evaluation", options={"id": "evaluation_id"})
 | 
			
		||||
     */
 | 
			
		||||
    #[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/duplicate', methods: ['POST'])]
 | 
			
		||||
    public function duplicateToEvaluationApi(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): Response
 | 
			
		||||
    {
 | 
			
		||||
        $work = $evaluation->getAccompanyingPeriodWork();
 | 
			
		||||
 | 
			
		||||
        if (!$this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $work)) {
 | 
			
		||||
            throw new AccessDeniedHttpException('not allowed to edit this accompanying period work');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $duplicatedDocument = $this->duplicator->duplicateToEvaluation($document, $evaluation);
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->persist($duplicatedDocument);
 | 
			
		||||
        $this->entityManager->persist($duplicatedDocument->getStoredObject());
 | 
			
		||||
        $this->entityManager->persist($evaluation);
 | 
			
		||||
        $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse(
 | 
			
		||||
            $this->serializer->serialize($duplicatedDocument, 'json', [AbstractNormalizer::GROUPS => ['read']]),
 | 
			
		||||
            json: true
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Route('/{_locale}/person/accompanying-course-work-evaluation-document/{id}/duplicate', name: 'chill_person_accompanying_period_work_evaluation_document_duplicate', methods: ['POST'])]
 | 
			
		||||
    public function duplicate(AccompanyingPeriodWorkEvaluationDocument $document): Response
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -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\PersonBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
 | 
			
		||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
 | 
			
		||||
use Symfony\Component\Serializer\SerializerInterface;
 | 
			
		||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
 | 
			
		||||
 | 
			
		||||
readonly class AccompanyingPeriodWorkEvaluationDocumentMoveController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private SerializerInterface $serializer,
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ParamConverter("document", options={"id": "document_id"})
 | 
			
		||||
     * @ParamConverter("evaluation", options={"id": "evaluation_id"})
 | 
			
		||||
     */
 | 
			
		||||
    #[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/move', methods: ['POST'])]
 | 
			
		||||
    public function moveToEvaluationApi(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): Response
 | 
			
		||||
    {
 | 
			
		||||
        $work = $evaluation->getAccompanyingPeriodWork();
 | 
			
		||||
 | 
			
		||||
        if (!$this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $work)) {
 | 
			
		||||
            throw new AccessDeniedHttpException('not allowed to edit this accompanying period work');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $document->setAccompanyingPeriodWorkEvaluation($evaluation);
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->persist($document);
 | 
			
		||||
        $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse(
 | 
			
		||||
            $this->serializer->serialize($document, 'json', [AbstractNormalizer::GROUPS => ['read']]),
 | 
			
		||||
            json: true
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -98,16 +98,6 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
 | 
			
		||||
 | 
			
		||||
    public function setAccompanyingPeriodWorkEvaluation(?AccompanyingPeriodWorkEvaluation $accompanyingPeriodWorkEvaluation): AccompanyingPeriodWorkEvaluationDocument
 | 
			
		||||
    {
 | 
			
		||||
        // if an evaluation is already associated, we cannot change the association (removing the association,
 | 
			
		||||
        // by setting a null value, is allowed.
 | 
			
		||||
        if (
 | 
			
		||||
            $this->accompanyingPeriodWorkEvaluation instanceof AccompanyingPeriodWorkEvaluation
 | 
			
		||||
            && $accompanyingPeriodWorkEvaluation instanceof AccompanyingPeriodWorkEvaluation
 | 
			
		||||
        ) {
 | 
			
		||||
            if ($this->accompanyingPeriodWorkEvaluation !== $accompanyingPeriodWorkEvaluation) {
 | 
			
		||||
                throw new \RuntimeException('It is not allowed to change the evaluation for a document');
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $this->accompanyingPeriodWorkEvaluation = $accompanyingPeriodWorkEvaluation;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ class FindAccompanyingPeriodWorkType extends AbstractType
 | 
			
		||||
    {
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('acpw', PickLinkedAccompanyingPeriodWorkType::class, [
 | 
			
		||||
                'label' => 'Social action',
 | 
			
		||||
                'label' => 'Accompanying period work',
 | 
			
		||||
                'multiple' => false,
 | 
			
		||||
                'accompanyingPeriod' => $options['accompanyingPeriod'],
 | 
			
		||||
            ])
 | 
			
		||||
 
 | 
			
		||||
@@ -16,18 +16,26 @@ use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\FormInterface;
 | 
			
		||||
use Symfony\Component\Form\FormView;
 | 
			
		||||
use Symfony\Component\OptionsResolver\OptionsResolver;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
			
		||||
 | 
			
		||||
class PickLinkedAccompanyingPeriodWorkType extends AbstractType
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly NormalizerInterface $normalizer) {}
 | 
			
		||||
 | 
			
		||||
    public function buildView(FormView $view, FormInterface $form, array $options)
 | 
			
		||||
    {
 | 
			
		||||
        $view->vars['multiple'] = $options['multiple'];
 | 
			
		||||
        $view->vars['types'] = ['acpw'];
 | 
			
		||||
        $view->vars['uniqid'] = uniqid('pick_acpw_dyn');
 | 
			
		||||
        $view->vars['suggested'] = [];
 | 
			
		||||
        $view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
 | 
			
		||||
        $view->vars['submit_on_adding_new_entity'] = false;
 | 
			
		||||
        $view->vars['pick-entities-type'] = 'acpw';
 | 
			
		||||
        $view->vars['attr']['data-accompanying-period-id'] = $options['accompanyingPeriod']->getId();
 | 
			
		||||
 | 
			
		||||
        foreach ($options['suggested'] as $suggestion) {
 | 
			
		||||
            $view->vars['suggested'][] = $this->normalizer->normalize($suggestion, 'json', ['groups' => 'read']);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function configureOptions(OptionsResolver $resolver)
 | 
			
		||||
@@ -38,6 +46,7 @@ class PickLinkedAccompanyingPeriodWorkType extends AbstractType
 | 
			
		||||
            ->setDefault('multiple', false)
 | 
			
		||||
            ->setAllowedTypes('multiple', ['bool'])
 | 
			
		||||
            ->setDefault('compound', false)
 | 
			
		||||
            ->setDefault('suggested', [])
 | 
			
		||||
            ->setDefault('as_id', false)
 | 
			
		||||
            ->setAllowedTypes('as_id', ['bool'])
 | 
			
		||||
            ->setDefault('submit_on_adding_new_entity', false)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
<?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\PersonBundle\Notification\FlagProviders;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
 | 
			
		||||
use Symfony\Component\Translation\TranslatableMessage;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatableInterface;
 | 
			
		||||
 | 
			
		||||
class DesignatedReferrerNotificationFlagProvider implements NotificationFlagProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    public const FLAG = 'referrer-acc-course-notif';
 | 
			
		||||
 | 
			
		||||
    public function getFlag(): string
 | 
			
		||||
    {
 | 
			
		||||
        return self::FLAG;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabel(): TranslatableInterface
 | 
			
		||||
    {
 | 
			
		||||
        return new TranslatableMessage('notification.flags.referrer-acc-course');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
<?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\PersonBundle\Notification\FlagProviders;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
 | 
			
		||||
use Symfony\Component\Translation\TranslatableMessage;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatableInterface;
 | 
			
		||||
 | 
			
		||||
class PersonAddressMoveNotificationFlagProvider implements NotificationFlagProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    public const FLAG = 'person-move-notif';
 | 
			
		||||
 | 
			
		||||
    public function getFlag(): string
 | 
			
		||||
    {
 | 
			
		||||
        return self::FLAG;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabel(): TranslatableInterface
 | 
			
		||||
    {
 | 
			
		||||
        return new TranslatableMessage('notification.flags.person-address-move');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -90,7 +90,7 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository
 | 
			
		||||
     * * first, opened works
 | 
			
		||||
     * * then, closed works
 | 
			
		||||
     *
 | 
			
		||||
     * @param array{types?: list<SocialAction>, user?: list<User>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
 | 
			
		||||
     * @param array{types?: list<SocialAction>, user?: list<User>, currentUser?: User, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
 | 
			
		||||
     *
 | 
			
		||||
     * @return AccompanyingPeriodWork[]
 | 
			
		||||
     */
 | 
			
		||||
@@ -101,6 +101,7 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository
 | 
			
		||||
 | 
			
		||||
        $sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w
 | 
			
		||||
                    LEFT JOIN chill_person_accompanying_period_work_referrer AS rw ON accompanyingperiodwork_id = w.id
 | 
			
		||||
                    AND (rw.enddate IS NULL OR rw.enddate > CURRENT_DATE)
 | 
			
		||||
                    WHERE accompanyingPeriod_id = :periodId";
 | 
			
		||||
 | 
			
		||||
        // implement filters
 | 
			
		||||
@@ -119,6 +120,10 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository
 | 
			
		||||
                .')';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isset($filters['currentUser'])) {
 | 
			
		||||
            $sql .= ' AND rw.user_id = :currentUser';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $sql .= " AND daterange(:after::date, :before::date) && daterange(w.startDate, w.endDate, '[]')";
 | 
			
		||||
 | 
			
		||||
        // if the start and end date were inversed, we inverse the order to avoid an error
 | 
			
		||||
@@ -152,6 +157,11 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository
 | 
			
		||||
            ->setParameter('limit', $limit, Types::INTEGER)
 | 
			
		||||
            ->setParameter('offset', $offset, Types::INTEGER);
 | 
			
		||||
 | 
			
		||||
        if (isset($filters['currentUser'])) {
 | 
			
		||||
            $nq->setParameter('currentUser', $filters['currentUser']->getId());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        foreach ($filters['user'] as $key => $user) {
 | 
			
		||||
            $nq->setParameter('user_'.$key, $user);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,8 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
            methods: {
 | 
			
		||||
                pickWork: function (payload: { work: AccompanyingPeriodWork }) {
 | 
			
		||||
                    console.log("payload", payload);
 | 
			
		||||
                    input.value = payload.work.id.toString();
 | 
			
		||||
 | 
			
		||||
                    input.value = payload.work.id?.toString() ?? "";
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -84,7 +84,7 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AccompanyingPeriodWork {
 | 
			
		||||
    id: number;
 | 
			
		||||
    id?: number;
 | 
			
		||||
    accompanyingPeriod?: AccompanyingPeriod;
 | 
			
		||||
    accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[];
 | 
			
		||||
    createdAt?: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -972,7 +972,7 @@ div#workEditor {
 | 
			
		||||
                font-size: 85%;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            i.fa {
 | 
			
		||||
            & > i.fa {
 | 
			
		||||
                padding: 0.25rem;
 | 
			
		||||
                color: $white;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <label class="col-sm-4 col-form-label visually-hidden">{{
 | 
			
		||||
            trans(EVALUATION_PUBLIC_COMMENT)
 | 
			
		||||
        }}</label>
 | 
			
		||||
        <div class="col-sm-12">
 | 
			
		||||
            <ckeditor
 | 
			
		||||
                :editor="ClassicEditor"
 | 
			
		||||
                :config="classicEditorConfig"
 | 
			
		||||
                :placeholder="trans(EVALUATION_COMMENT_PLACEHOLDER)"
 | 
			
		||||
                :value="comment"
 | 
			
		||||
                @input="$emit('update:comment', $event)"
 | 
			
		||||
                tag-name="textarea"
 | 
			
		||||
            ></ckeditor>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
 | 
			
		||||
import { ClassicEditor } from "ckeditor5";
 | 
			
		||||
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
 | 
			
		||||
import {
 | 
			
		||||
    EVALUATION_PUBLIC_COMMENT,
 | 
			
		||||
    EVALUATION_COMMENT_PLACEHOLDER,
 | 
			
		||||
    trans,
 | 
			
		||||
} from "translator";
 | 
			
		||||
 | 
			
		||||
defineProps(["comment"]);
 | 
			
		||||
defineEmits(["update:comment"]);
 | 
			
		||||
</script>
 | 
			
		||||
@@ -0,0 +1,71 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
 | 
			
		||||
            {{ trans(EVALUATION_STARTDATE) }}
 | 
			
		||||
        </label>
 | 
			
		||||
        <div class="col-8 col-sm-4 col-md-8 col-lg-4">
 | 
			
		||||
            <input
 | 
			
		||||
                class="form-control form-control-sm"
 | 
			
		||||
                type="date"
 | 
			
		||||
                :value="startDate"
 | 
			
		||||
                @input="$emit('update:startDate', $event.target.value)"
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
 | 
			
		||||
            {{ trans(EVALUATION_ENDDATE) }}
 | 
			
		||||
        </label>
 | 
			
		||||
        <div class="col-8 col-sm-4 col-md-8 col-lg-4">
 | 
			
		||||
            <input
 | 
			
		||||
                class="form-control form-control-sm"
 | 
			
		||||
                type="date"
 | 
			
		||||
                :value="endDate"
 | 
			
		||||
                @input="$emit('update:endDate', $event.target.value)"
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
 | 
			
		||||
            {{ trans(EVALUATION_MAXDATE) }}
 | 
			
		||||
        </label>
 | 
			
		||||
        <div class="col-8 col-sm-4 col-md-8 col-lg-4">
 | 
			
		||||
            <input
 | 
			
		||||
                class="form-control form-control-sm"
 | 
			
		||||
                type="date"
 | 
			
		||||
                :value="maxDate"
 | 
			
		||||
                @input="$emit('update:maxDate', $event.target.value)"
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
 | 
			
		||||
            {{ trans(EVALUATION_WARNING_INTERVAL) }}
 | 
			
		||||
        </label>
 | 
			
		||||
        <div class="col-8 col-sm-4 col-md-8 col-lg-4">
 | 
			
		||||
            <input
 | 
			
		||||
                class="form-control form-control-sm"
 | 
			
		||||
                type="number"
 | 
			
		||||
                :value="warningInterval"
 | 
			
		||||
                @input="$emit('update:warningInterval', $event.target.value)"
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {
 | 
			
		||||
    EVALUATION_STARTDATE,
 | 
			
		||||
    EVALUATION_ENDDATE,
 | 
			
		||||
    EVALUATION_MAXDATE,
 | 
			
		||||
    EVALUATION_WARNING_INTERVAL,
 | 
			
		||||
    trans,
 | 
			
		||||
} from "translator";
 | 
			
		||||
 | 
			
		||||
defineProps(["startDate", "endDate", "maxDate", "warningInterval"]);
 | 
			
		||||
defineEmits([
 | 
			
		||||
    "update:startDate",
 | 
			
		||||
    "update:endDate",
 | 
			
		||||
    "update:maxDate",
 | 
			
		||||
    "update:warningInterval",
 | 
			
		||||
]);
 | 
			
		||||
</script>
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <h6>{{ trans(EVALUATION_DOCUMENT_ADD) }} :</h6>
 | 
			
		||||
        <pick-template
 | 
			
		||||
            entityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation"
 | 
			
		||||
            :id="evaluation.id"
 | 
			
		||||
            :templates="templates"
 | 
			
		||||
            :preventDefaultMoveToGenerate="true"
 | 
			
		||||
            @go-to-generate-document="$emit('submitBeforeGenerate', $event)"
 | 
			
		||||
        >
 | 
			
		||||
            <template v-slot:title>
 | 
			
		||||
                <label class="col-form-label">{{
 | 
			
		||||
                    trans(EVALUATION_GENERATE_A_DOCUMENT)
 | 
			
		||||
                }}</label>
 | 
			
		||||
            </template>
 | 
			
		||||
        </pick-template>
 | 
			
		||||
        <div>
 | 
			
		||||
            <label class="col-form-label">{{
 | 
			
		||||
                trans(EVALUATION_DOCUMENT_UPLOAD)
 | 
			
		||||
            }}</label>
 | 
			
		||||
            <ul class="record_actions document-upload">
 | 
			
		||||
                <li>
 | 
			
		||||
                    <drop-file-modal
 | 
			
		||||
                        :allow-remove="false"
 | 
			
		||||
                        @add-document="$emit('addDocument', $event)"
 | 
			
		||||
                    ></drop-file-modal>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import PickTemplate from "ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue";
 | 
			
		||||
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
 | 
			
		||||
import {
 | 
			
		||||
    EVALUATION_DOCUMENT_ADD,
 | 
			
		||||
    EVALUATION_DOCUMENT_UPLOAD,
 | 
			
		||||
    EVALUATION_GENERATE_A_DOCUMENT,
 | 
			
		||||
    trans,
 | 
			
		||||
} from "translator";
 | 
			
		||||
 | 
			
		||||
defineProps(["evaluation", "templates"]);
 | 
			
		||||
defineEmits(["addDocument", "submitBeforeGenerate"]);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
ul.document-upload {
 | 
			
		||||
    justify-content: flex-start;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,345 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <h5>{{ trans(EVALUATION_DOCUMENTS) }} :</h5>
 | 
			
		||||
        <div class="flex-table">
 | 
			
		||||
            <div
 | 
			
		||||
                class="item-bloc"
 | 
			
		||||
                v-for="(d, i) in documents"
 | 
			
		||||
                :key="d.id"
 | 
			
		||||
                :class="[
 | 
			
		||||
                    parseInt(docAnchorId) === d.id ? 'bg-blink' : 'nothing',
 | 
			
		||||
                ]"
 | 
			
		||||
            >
 | 
			
		||||
                <div :id="'document_' + d.id" class="item-row">
 | 
			
		||||
                    <div class="input-group input-group-lg mb-3 row">
 | 
			
		||||
                        <label class="col-sm-3 col-form-label"
 | 
			
		||||
                            >Titre du document:</label
 | 
			
		||||
                        >
 | 
			
		||||
                        <div class="col-sm-9">
 | 
			
		||||
                            <input
 | 
			
		||||
                                class="form-control document-title"
 | 
			
		||||
                                type="text"
 | 
			
		||||
                                :value="d.title"
 | 
			
		||||
                                :id="d.id"
 | 
			
		||||
                                :data-key="i"
 | 
			
		||||
                                @input="$emit('inputDocumentTitle', $event)"
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="item-row">
 | 
			
		||||
                    <div class="item-col item-meta">
 | 
			
		||||
                        <p v-if="d.createdBy" class="createdBy">
 | 
			
		||||
                            Créé par {{ d.createdBy.text }}<br />
 | 
			
		||||
                            Le
 | 
			
		||||
                            {{
 | 
			
		||||
                                $d(ISOToDatetime(d.createdAt.datetime), "long")
 | 
			
		||||
                            }}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="item-row">
 | 
			
		||||
                    <div class="item-col">
 | 
			
		||||
                        <ul class="record_actions">
 | 
			
		||||
                            <li
 | 
			
		||||
                                v-if="
 | 
			
		||||
                                    d.workflows_availables.length > 0 ||
 | 
			
		||||
                                    d.workflows.length > 0
 | 
			
		||||
                                "
 | 
			
		||||
                            >
 | 
			
		||||
                                <list-workflow-modal
 | 
			
		||||
                                    :workflows="d.workflows"
 | 
			
		||||
                                    :allowCreate="true"
 | 
			
		||||
                                    relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument"
 | 
			
		||||
                                    :relatedEntityId="d.id"
 | 
			
		||||
                                    :workflowsAvailables="
 | 
			
		||||
                                        d.workflows_availables
 | 
			
		||||
                                    "
 | 
			
		||||
                                    :preventDefaultMoveToGenerate="true"
 | 
			
		||||
                                    :goToGenerateWorkflowPayload="{ doc: d }"
 | 
			
		||||
                                    @go-to-generate-workflow="
 | 
			
		||||
                                        $emit('goToGenerateWorkflow', $event)
 | 
			
		||||
                                    "
 | 
			
		||||
                                ></list-workflow-modal>
 | 
			
		||||
                            </li>
 | 
			
		||||
                            <li>
 | 
			
		||||
                                <button
 | 
			
		||||
                                    v-if="AmIRefferer"
 | 
			
		||||
                                    class="btn btn-notify"
 | 
			
		||||
                                    @click="
 | 
			
		||||
                                        $emit(
 | 
			
		||||
                                            'goToGenerateNotification',
 | 
			
		||||
                                            d,
 | 
			
		||||
                                            false,
 | 
			
		||||
                                        )
 | 
			
		||||
                                    "
 | 
			
		||||
                                ></button>
 | 
			
		||||
                                <template v-else>
 | 
			
		||||
                                    <button
 | 
			
		||||
                                        id="btnGroupNotifyButtons"
 | 
			
		||||
                                        type="button"
 | 
			
		||||
                                        class="btn btn-notify dropdown-toggle"
 | 
			
		||||
                                        :title="
 | 
			
		||||
                                            trans(EVALUATION_NOTIFICATION_SEND)
 | 
			
		||||
                                        "
 | 
			
		||||
                                        data-bs-toggle="dropdown"
 | 
			
		||||
                                        aria-expanded="false"
 | 
			
		||||
                                    >
 | 
			
		||||
                                         
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <ul
 | 
			
		||||
                                        class="dropdown-menu"
 | 
			
		||||
                                        aria-labelledby="btnGroupNotifyButtons"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <li>
 | 
			
		||||
                                            <a
 | 
			
		||||
                                                class="dropdown-item"
 | 
			
		||||
                                                @click="
 | 
			
		||||
                                                    $emit(
 | 
			
		||||
                                                        'goToGenerateNotification',
 | 
			
		||||
                                                        d,
 | 
			
		||||
                                                        true,
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                "
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {{
 | 
			
		||||
                                                    trans(
 | 
			
		||||
                                                        EVALUATION_NOTIFICATION_NOTIFY_REFERRER,
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                }}
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                        <li>
 | 
			
		||||
                                            <a
 | 
			
		||||
                                                class="dropdown-item"
 | 
			
		||||
                                                @click="
 | 
			
		||||
                                                    $emit(
 | 
			
		||||
                                                        'goToGenerateNotification',
 | 
			
		||||
                                                        d,
 | 
			
		||||
                                                        false,
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                "
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {{
 | 
			
		||||
                                                    trans(
 | 
			
		||||
                                                        EVALUATION_NOTIFICATION_NOTIFY_ANY,
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                }}
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                    </ul>
 | 
			
		||||
                                </template>
 | 
			
		||||
                            </li>
 | 
			
		||||
                            <li>
 | 
			
		||||
                                <document-action-buttons-group
 | 
			
		||||
                                    :stored-object="d.storedObject"
 | 
			
		||||
                                    :filename="d.title"
 | 
			
		||||
                                    :can-edit="true"
 | 
			
		||||
                                    :execute-before-leave="
 | 
			
		||||
                                        submitBeforeLeaveToEditor
 | 
			
		||||
                                    "
 | 
			
		||||
                                    :davLink="
 | 
			
		||||
                                        d.storedObject._links?.dav_link.href
 | 
			
		||||
                                    "
 | 
			
		||||
                                    :davLinkExpiration="
 | 
			
		||||
                                        d.storedObject._links?.dav_link
 | 
			
		||||
                                            .expiration
 | 
			
		||||
                                    "
 | 
			
		||||
                                    @on-stored-object-status-change="
 | 
			
		||||
                                        $emit('statusDocumentChanged', $event)
 | 
			
		||||
                                    "
 | 
			
		||||
                                ></document-action-buttons-group>
 | 
			
		||||
                            </li>
 | 
			
		||||
                            <li v-if="Number.isInteger(d.id)">
 | 
			
		||||
                                <div class="duplicate-dropdown">
 | 
			
		||||
                                    <button
 | 
			
		||||
                                        class="btn btn-edit dropdown-toggle"
 | 
			
		||||
                                        type="button"
 | 
			
		||||
                                        data-bs-toggle="dropdown"
 | 
			
		||||
                                        aria-expanded="false"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {{ trans(EVALUATION_DOCUMENT_EDIT) }}
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <ul class="dropdown-menu">
 | 
			
		||||
                                        <!--delete-->
 | 
			
		||||
                                        <li v-if="d.workflows.length === 0">
 | 
			
		||||
                                            <a
 | 
			
		||||
                                                class="dropdown-item"
 | 
			
		||||
                                                @click="
 | 
			
		||||
                                                    $emit('removeDocument', d)
 | 
			
		||||
                                                "
 | 
			
		||||
                                            >
 | 
			
		||||
                                                <i class="fa fa-trash-o" aria-hidden="true"></i>
 | 
			
		||||
                                                {{
 | 
			
		||||
                                                    trans(
 | 
			
		||||
                                                        EVALUATION_DOCUMENT_DELETE,
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                }}
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                        <!--replace document-->
 | 
			
		||||
                                        <li
 | 
			
		||||
                                            v-if="
 | 
			
		||||
                                                d.storedObject._permissions
 | 
			
		||||
                                                    .canEdit
 | 
			
		||||
                                            "
 | 
			
		||||
                                        >
 | 
			
		||||
                                            <drop-file-modal
 | 
			
		||||
                                                :existing-doc="d.storedObject"
 | 
			
		||||
                                                :allow-remove="false"
 | 
			
		||||
                                                @add-document="
 | 
			
		||||
                                                    (arg) =>
 | 
			
		||||
                                                        $emit(
 | 
			
		||||
                                                            'replaceDocument',
 | 
			
		||||
                                                            d,
 | 
			
		||||
                                                            arg.stored_object,
 | 
			
		||||
                                                            arg.stored_object_version,
 | 
			
		||||
                                                        )
 | 
			
		||||
                                                "
 | 
			
		||||
                                            ></drop-file-modal>
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                        <!--duplicate document-->
 | 
			
		||||
                                        <li>
 | 
			
		||||
                                            <a
 | 
			
		||||
                                                class="dropdown-item"
 | 
			
		||||
                                                @click="
 | 
			
		||||
                                                    $emit(
 | 
			
		||||
                                                        'duplicateDocument',
 | 
			
		||||
                                                        d,
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                "
 | 
			
		||||
                                                >
 | 
			
		||||
                                                <i class="fa fa-copy" aria-hidden="true"></i>
 | 
			
		||||
                                                {{
 | 
			
		||||
                                                    trans(
 | 
			
		||||
                                                        EVALUATION_DOCUMENT_DUPLICATE_HERE,
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                }}
 | 
			
		||||
                                            </a
 | 
			
		||||
                                            >
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                        <li>
 | 
			
		||||
                                            <a
 | 
			
		||||
                                                class="dropdown-item"
 | 
			
		||||
                                                @click="
 | 
			
		||||
                                                    prepareDocumentDuplicationToWork(
 | 
			
		||||
                                                        d,
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                "
 | 
			
		||||
                                                >
 | 
			
		||||
                                                <i class="fa fa-copy" aria-hidden="true"></i>
 | 
			
		||||
                                                {{
 | 
			
		||||
                                                    trans(
 | 
			
		||||
                                                        EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                }}</a
 | 
			
		||||
                                            >
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                        <!--move document-->
 | 
			
		||||
                                        <li
 | 
			
		||||
                                            v-if="
 | 
			
		||||
                                                d.storedObject._permissions
 | 
			
		||||
                                                    .canEdit
 | 
			
		||||
                                            "
 | 
			
		||||
                                        >
 | 
			
		||||
                                            <a
 | 
			
		||||
                                                class="dropdown-item"
 | 
			
		||||
                                                @click="
 | 
			
		||||
                                                    prepareDocumentMoveToWork(d)
 | 
			
		||||
                                                "
 | 
			
		||||
                                                >
 | 
			
		||||
                                                <i class="fa fa-arrows" aria-hidden="true"></i>
 | 
			
		||||
                                                {{
 | 
			
		||||
                                                    trans(
 | 
			
		||||
                                                        EVALUATION_DOCUMENT_MOVE,
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                }}</a
 | 
			
		||||
                                            >
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                    </ul>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <AccompanyingPeriodWorkSelectorModal
 | 
			
		||||
        v-if="showAccompanyingPeriodSelector"
 | 
			
		||||
        v-model:selectedAcpw="selectedAcpw"
 | 
			
		||||
        :accompanying-period-id="accompanyingPeriodId"
 | 
			
		||||
        :is-evaluation-selector="true"
 | 
			
		||||
        :ignore-accompanying-period-work-ids="[]"
 | 
			
		||||
        @close-modal="showAccompanyingPeriodSelector = false"
 | 
			
		||||
        @update:selectedEvaluation="selectedEvaluation = $event"
 | 
			
		||||
    />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
 | 
			
		||||
import ListWorkflowModal from "ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue";
 | 
			
		||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
 | 
			
		||||
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
 | 
			
		||||
import {
 | 
			
		||||
    EVALUATION_NOTIFICATION_NOTIFY_REFERRER,
 | 
			
		||||
    EVALUATION_NOTIFICATION_NOTIFY_ANY,
 | 
			
		||||
    EVALUATION_NOTIFICATION_SEND,
 | 
			
		||||
    EVALUATION_DOCUMENTS,
 | 
			
		||||
    EVALUATION_DOCUMENT_MOVE,
 | 
			
		||||
    EVALUATION_DOCUMENT_DELETE,
 | 
			
		||||
    EVALUATION_DOCUMENT_EDIT,
 | 
			
		||||
    EVALUATION_DOCUMENT_DUPLICATE_HERE,
 | 
			
		||||
    EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
 | 
			
		||||
    trans,
 | 
			
		||||
} from "translator";
 | 
			
		||||
import { ref, watch } from "vue";
 | 
			
		||||
import AccompanyingPeriodWorkSelectorModal from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue";
 | 
			
		||||
 | 
			
		||||
defineProps(["documents", "docAnchorId", "accompanyingPeriodId", "accompanyingPeriodWorkId"]);
 | 
			
		||||
const emit = defineEmits([
 | 
			
		||||
    "inputDocumentTitle",
 | 
			
		||||
    "removeDocument",
 | 
			
		||||
    "duplicateDocument",
 | 
			
		||||
    "statusDocumentChanged",
 | 
			
		||||
    "goToGenerateWorkflow",
 | 
			
		||||
    "goToGenerateNotification",
 | 
			
		||||
    "duplicateDocumentToWork",
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
const showAccompanyingPeriodSelector = ref(false);
 | 
			
		||||
const selectedEvaluation = ref(null);
 | 
			
		||||
const selectedDocumentToDuplicate = ref(null);
 | 
			
		||||
const selectedDocumentToMove = ref(null);
 | 
			
		||||
 | 
			
		||||
const prepareDocumentDuplicationToWork = (d) => {
 | 
			
		||||
    selectedDocumentToDuplicate.value = d;
 | 
			
		||||
    /** ensure selectedDocumentToMove is null */
 | 
			
		||||
    selectedDocumentToMove.value = null;
 | 
			
		||||
 | 
			
		||||
    showAccompanyingPeriodSelector.value = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const prepareDocumentMoveToWork = (d) => {
 | 
			
		||||
    selectedDocumentToMove.value = d;
 | 
			
		||||
    /** ensure selectedDocumentToDuplicate is null */
 | 
			
		||||
    selectedDocumentToDuplicate.value = null;
 | 
			
		||||
 | 
			
		||||
    showAccompanyingPeriodSelector.value = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
watch(selectedEvaluation, (val) => {
 | 
			
		||||
    if (selectedDocumentToDuplicate.value) {
 | 
			
		||||
        emit("duplicateDocumentToEvaluation", {
 | 
			
		||||
            evaluation: val,
 | 
			
		||||
            document: selectedDocumentToDuplicate.value,
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
        emit("moveDocumentToEvaluation", {
 | 
			
		||||
            evaluationDest: val,
 | 
			
		||||
            document: selectedDocumentToMove.value,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
 | 
			
		||||
            {{ trans(EVALUATION_TIME_SPENT) }}
 | 
			
		||||
        </label>
 | 
			
		||||
        <div class="col-8 col-sm-4 col-md-8 col-lg-4">
 | 
			
		||||
            <select
 | 
			
		||||
                class="form-control form-control-sm"
 | 
			
		||||
                :value="timeSpent"
 | 
			
		||||
                @input="$emit('update:timeSpent', $event.target.value)"
 | 
			
		||||
            >
 | 
			
		||||
                <option disabled value="">
 | 
			
		||||
                    {{ trans(EVALUATION_TIME_SPENT) }}
 | 
			
		||||
                </option>
 | 
			
		||||
                <option
 | 
			
		||||
                    v-for="time in timeSpentChoices"
 | 
			
		||||
                    :value="time.value"
 | 
			
		||||
                    :key="time.value"
 | 
			
		||||
                >
 | 
			
		||||
                    {{ time.text }}
 | 
			
		||||
                </option>
 | 
			
		||||
            </select>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import { EVALUATION_TIME_SPENT, trans } from "translator";
 | 
			
		||||
 | 
			
		||||
defineProps(["timeSpent", "timeSpentChoices"]);
 | 
			
		||||
defineEmits(["update:timeSpent"]);
 | 
			
		||||
</script>
 | 
			
		||||
@@ -11,12 +11,13 @@ import { findSocialActionsBySocialIssue } from "ChillPersonAssets/vuejs/_api/Soc
 | 
			
		||||
import { create } from "ChillPersonAssets/vuejs/_api/AccompanyingCourseWork.js";
 | 
			
		||||
import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods.ts";
 | 
			
		||||
import { fetchTemplates } from "ChillDocGeneratorAssets/api/pickTemplate.js";
 | 
			
		||||
import { duplicate } from "../_api/accompanyingCourseWorkEvaluationDocument";
 | 
			
		||||
import {
 | 
			
		||||
  duplicate,
 | 
			
		||||
  duplicateDocumentToEvaluation,
 | 
			
		||||
  moveDocumentToEvaluation,
 | 
			
		||||
} from "../_api/accompanyingCourseWorkEvaluationDocument";
 | 
			
		||||
 | 
			
		||||
const debug = process.env.NODE_ENV !== "production";
 | 
			
		||||
const evalFQDN = encodeURIComponent(
 | 
			
		||||
  "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation",
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const store = createStore({
 | 
			
		||||
  strict: debug,
 | 
			
		||||
@@ -298,15 +299,47 @@ const store = createStore({
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    addDuplicatedDocument(state, { document, evaluation_key }) {
 | 
			
		||||
      console.log("add duplicated document", document);
 | 
			
		||||
      console.log("add duplicated dcuemnt - evaluation key", evaluation_key);
 | 
			
		||||
 | 
			
		||||
      let evaluation = state.evaluationsPicked.find(
 | 
			
		||||
        (e) => e.key === evaluation_key,
 | 
			
		||||
      );
 | 
			
		||||
      document.key = evaluation.documents.length + 1;
 | 
			
		||||
      evaluation.documents.splice(0, 0, document);
 | 
			
		||||
    },
 | 
			
		||||
    addDuplicatedDocumentToEvaluation(state, { document, evaluation }) {
 | 
			
		||||
      let evaluationDest = state.evaluationsPicked.find(
 | 
			
		||||
        (e) => e.id === evaluation.id,
 | 
			
		||||
      );
 | 
			
		||||
      if (evaluationDest) {
 | 
			
		||||
        console.log("add duplicated document to evaluation", evaluationDest);
 | 
			
		||||
        document.key = evaluationDest.documents.length + 1;
 | 
			
		||||
        evaluationDest.documents.splice(0, 0, document);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    moveDocumentToEvaluation(
 | 
			
		||||
      state,
 | 
			
		||||
      { evaluationInitial, evaluationDest, document },
 | 
			
		||||
    ) {
 | 
			
		||||
      let evaluationA = state.evaluationsPicked.find(
 | 
			
		||||
        (e) => e.id === evaluationInitial.id,
 | 
			
		||||
      );
 | 
			
		||||
      let evaluationB = state.evaluationsPicked.find(
 | 
			
		||||
        (e) => e.id === evaluationDest.id,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (evaluationB) {
 | 
			
		||||
        // add document to chosen evaluation if evaluation is part of the same social work
 | 
			
		||||
        document.key = evaluationB.documents.length + 1;
 | 
			
		||||
        evaluationB.documents.splice(0, 0, document);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // remove document from original evaluation
 | 
			
		||||
      const indexToRemove = evaluationA.documents.findIndex(
 | 
			
		||||
        (doc) => doc.id === document.id,
 | 
			
		||||
      );
 | 
			
		||||
      if (indexToRemove !== -1) {
 | 
			
		||||
        evaluationA.documents.splice(indexToRemove, 1);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * Replaces a document in the state with a new document.
 | 
			
		||||
     *
 | 
			
		||||
@@ -603,6 +636,44 @@ const store = createStore({
 | 
			
		||||
      const newDoc = await duplicate(document.id);
 | 
			
		||||
      commit("addDuplicatedDocument", { document: newDoc, evaluation_key });
 | 
			
		||||
    },
 | 
			
		||||
    async duplicateDocumentToEvaluation({ commit }, { document, evaluation }) {
 | 
			
		||||
      try {
 | 
			
		||||
        const newDoc = await duplicateDocumentToEvaluation(
 | 
			
		||||
          document.id,
 | 
			
		||||
          evaluation.id,
 | 
			
		||||
        );
 | 
			
		||||
        commit("addDuplicatedDocumentToEvaluation", {
 | 
			
		||||
          document: newDoc,
 | 
			
		||||
          evaluation,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return newDoc;
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error("Failed to move document:", error);
 | 
			
		||||
        throw error;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    async moveDocumentToEvaluation(
 | 
			
		||||
      { commit },
 | 
			
		||||
      { evaluationInitial, evaluationDest, document },
 | 
			
		||||
    ) {
 | 
			
		||||
      try {
 | 
			
		||||
        const response = await moveDocumentToEvaluation(
 | 
			
		||||
          document.id,
 | 
			
		||||
          evaluationDest.id,
 | 
			
		||||
        );
 | 
			
		||||
        commit("moveDocumentToEvaluation", {
 | 
			
		||||
          evaluationInitial,
 | 
			
		||||
          evaluationDest,
 | 
			
		||||
          document,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return response;
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error("Failed to move document:", error);
 | 
			
		||||
        throw error;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    removeDocument({ commit }, payload) {
 | 
			
		||||
      commit("removeDocument", payload);
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -9,3 +9,23 @@ export const duplicate = async (
 | 
			
		||||
        `/api/1.0/person/accompanying-course-work-evaluation-document/${id}/duplicate`,
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const duplicateDocumentToEvaluation = async (
 | 
			
		||||
    document_id: number,
 | 
			
		||||
    evaluation_id: number,
 | 
			
		||||
): Promise<AccompanyingPeriodWorkEvaluationDocument> => {
 | 
			
		||||
    return makeFetch<null, AccompanyingPeriodWorkEvaluationDocument>(
 | 
			
		||||
        "POST",
 | 
			
		||||
        `/api/1.0/person/accompanying-course-work-evaluation-document/${document_id}/evaluation/${evaluation_id}/duplicate`,
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const moveDocumentToEvaluation = async (
 | 
			
		||||
    document_id: number,
 | 
			
		||||
    evaluation_id: number,
 | 
			
		||||
): Promise<AccompanyingPeriodWorkEvaluationDocument> => {
 | 
			
		||||
    return makeFetch<null, AccompanyingPeriodWorkEvaluationDocument>(
 | 
			
		||||
        "POST",
 | 
			
		||||
        `/api/1.0/person/accompanying-course-work-evaluation-document/${document_id}/evaluation/${evaluation_id}/move`,
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,70 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="container">
 | 
			
		||||
        <div class="item-bloc">
 | 
			
		||||
            <div class="item-row">
 | 
			
		||||
                <h2 class="badge-title">
 | 
			
		||||
                    <span class="title_label"></span>
 | 
			
		||||
                    <span class="title_action">
 | 
			
		||||
                        <span>
 | 
			
		||||
                            {{ trans(EVALUATION) }}:
 | 
			
		||||
                            <span class="badge bg-light text-dark">
 | 
			
		||||
                                {{ eval?.evaluation?.title.fr }}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </span>
 | 
			
		||||
 | 
			
		||||
                        <ul class="small_in_title columns mt-1">
 | 
			
		||||
                            <li>
 | 
			
		||||
                                <span class="item-key">
 | 
			
		||||
                                    {{
 | 
			
		||||
                                        trans(
 | 
			
		||||
                                            ACCOMPANYING_COURSE_WORK_START_DATE,
 | 
			
		||||
                                        )
 | 
			
		||||
                                    }}
 | 
			
		||||
                                    :
 | 
			
		||||
                                </span>
 | 
			
		||||
                                <b>{{ formatDate(eval.startDate) }}</b>
 | 
			
		||||
                            </li>
 | 
			
		||||
 | 
			
		||||
                            <li v-if="eval.endDate">
 | 
			
		||||
                                <span class="item-key">
 | 
			
		||||
                                    {{
 | 
			
		||||
                                        trans(ACCOMPANYING_COURSE_WORK_END_DATE)
 | 
			
		||||
                                    }}
 | 
			
		||||
                                    :
 | 
			
		||||
                                </span>
 | 
			
		||||
                                <b>{{ formatDate(eval.endDate) }}</b>
 | 
			
		||||
                            </li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </span>
 | 
			
		||||
                </h2>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import {
 | 
			
		||||
    ACCOMPANYING_COURSE_WORK_END_DATE,
 | 
			
		||||
    ACCOMPANYING_COURSE_WORK_START_DATE,
 | 
			
		||||
    EVALUATION,
 | 
			
		||||
    trans,
 | 
			
		||||
} from "translator";
 | 
			
		||||
import { ISOToDate } from "ChillMainAssets/chill/js/date";
 | 
			
		||||
import { DateTime } from "ChillMainAssets/types";
 | 
			
		||||
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
const props = defineProps<{ eval: AccompanyingPeriodWorkEvaluation }>();
 | 
			
		||||
const formatDate = (dateObject: DateTime) => {
 | 
			
		||||
    if (dateObject) {
 | 
			
		||||
        const parsedDate = ISOToDate(dateObject.datetime);
 | 
			
		||||
        if (parsedDate) {
 | 
			
		||||
            return new Intl.DateTimeFormat("default", {
 | 
			
		||||
                dateStyle: "short",
 | 
			
		||||
            }).format(parsedDate);
 | 
			
		||||
        } else {
 | 
			
		||||
            return "";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -0,0 +1,47 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="results">
 | 
			
		||||
        <div
 | 
			
		||||
            v-for="evaluation in evaluations"
 | 
			
		||||
            :key="evaluation.id"
 | 
			
		||||
            class="list-item"
 | 
			
		||||
        >
 | 
			
		||||
            <label class="acpw-item">
 | 
			
		||||
                <div>
 | 
			
		||||
                    <input
 | 
			
		||||
                        type="radio"
 | 
			
		||||
                        :value="evaluation"
 | 
			
		||||
                        v-model="selectedEvaluation"
 | 
			
		||||
                        name="item"
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <accompanying-period-work-evaluation-item :eval="evaluation" />
 | 
			
		||||
            </label>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
 | 
			
		||||
import { defineProps, ref, watch } from "vue";
 | 
			
		||||
import AccompanyingPeriodWorkEvaluationItem from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkEvaluationItem.vue";
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
    evaluations: AccompanyingPeriodWorkEvaluation[];
 | 
			
		||||
}>();
 | 
			
		||||
const selectedEvaluation = ref<AccompanyingPeriodWorkEvaluation | null>(null);
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line vue/valid-define-emits
 | 
			
		||||
const emit = defineEmits();
 | 
			
		||||
 | 
			
		||||
watch(selectedEvaluation, (newValue) => {
 | 
			
		||||
    emit("update:selectedEvaluation", newValue);
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
.acpw-item {
 | 
			
		||||
    display: flex;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -26,14 +26,24 @@ import AccompanyingPeriodWorkItem from "./AccompanyingPeriodWorkItem.vue";
 | 
			
		||||
import { AccompanyingPeriodWork } from "../../../types";
 | 
			
		||||
import { defineProps, ref, watch } from "vue";
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
    accompanyingPeriodWorks: AccompanyingPeriodWork[];
 | 
			
		||||
    selectedAcpw?: AccompanyingPeriodWork | null;
 | 
			
		||||
}>();
 | 
			
		||||
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
 | 
			
		||||
const selectedAcpw = ref<AccompanyingPeriodWork | null>(
 | 
			
		||||
    props.selectedAcpw ?? null,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line vue/valid-define-emits
 | 
			
		||||
const emit = defineEmits();
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
    "update:selectedAcpw": [value: AccompanyingPeriodWork | null];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
    () => props.selectedAcpw,
 | 
			
		||||
    (val) => {
 | 
			
		||||
        selectedAcpw.value = val ?? null;
 | 
			
		||||
    },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
watch(selectedAcpw, (newValue) => {
 | 
			
		||||
    emit("update:selectedAcpw", newValue);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <div class="row justify-content-end">
 | 
			
		||||
        <div class="row justify-content-end" v-if="!isEvaluationSelector">
 | 
			
		||||
            <div class="col-md-6 col-sm-10" v-if="selectedAcpw">
 | 
			
		||||
                <ul class="list-suggest remove-items">
 | 
			
		||||
                    <li>
 | 
			
		||||
@@ -14,7 +14,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <ul class="record_actions">
 | 
			
		||||
        <ul v-if="!showModal" class="record_actions">
 | 
			
		||||
            <li>
 | 
			
		||||
                <a class="btn btn-sm btn-create mt-3" @click="openModal">
 | 
			
		||||
                    {{ trans(ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK) }}
 | 
			
		||||
@@ -40,9 +40,15 @@
 | 
			
		||||
 | 
			
		||||
                <template #body>
 | 
			
		||||
                    <accompanying-period-work-list
 | 
			
		||||
                        v-if="evaluations.length === 0"
 | 
			
		||||
                        :accompanying-period-works="accompanyingPeriodWorks"
 | 
			
		||||
                        v-model:selectedAcpw="selectedAcpw"
 | 
			
		||||
                    />
 | 
			
		||||
                    <accompanying-period-work-evaluation-list
 | 
			
		||||
                        v-if="evaluations.length > 0"
 | 
			
		||||
                        :evaluations="evaluations"
 | 
			
		||||
                        v-model:selectedEvaluation="selectedEvaluation"
 | 
			
		||||
                    />
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
                <template #footer>
 | 
			
		||||
@@ -60,58 +66,107 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { onMounted, ref } from "vue";
 | 
			
		||||
import { ref, watch, onMounted } from "vue";
 | 
			
		||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
 | 
			
		||||
import AccompanyingPeriodWorkList from "./AccompanyingPeriodWorkList.vue";
 | 
			
		||||
import { AccompanyingPeriodWork } from "../../../types";
 | 
			
		||||
import {
 | 
			
		||||
    trans,
 | 
			
		||||
    ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK,
 | 
			
		||||
    CONFIRM,
 | 
			
		||||
    trans,
 | 
			
		||||
} from "translator";
 | 
			
		||||
import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
 | 
			
		||||
 | 
			
		||||
interface AccompanyingPeriodWorkSelectorModalProps {
 | 
			
		||||
    accompanyingPeriodId: number;
 | 
			
		||||
}
 | 
			
		||||
import {fetchResults, makeFetch} from "ChillMainAssets/lib/api/apiMethods";
 | 
			
		||||
import AccompanyingPeriodWorkEvaluationList from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkEvaluationList.vue";
 | 
			
		||||
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
 | 
			
		||||
 | 
			
		||||
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
 | 
			
		||||
const selectedEvaluation = ref<AccompanyingPeriodWorkEvaluation | null>(null);
 | 
			
		||||
const showModal = ref(false);
 | 
			
		||||
const accompanyingPeriodWorks = ref<AccompanyingPeriodWork[]>([]);
 | 
			
		||||
const props = defineProps<AccompanyingPeriodWorkSelectorModalProps>();
 | 
			
		||||
const evaluations = ref<AccompanyingPeriodWorkEvaluation[]>([]);
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
    accompanyingPeriodId: string,
 | 
			
		||||
    isEvaluationSelector: boolean,
 | 
			
		||||
    ignoreAccompanyingPeriodWorkIds: number[],
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
    pickWork: [payload: { work: AccompanyingPeriodWork | null }];
 | 
			
		||||
    closeModal: [];
 | 
			
		||||
    "update:selectedEvaluation": [evaluation: AccompanyingPeriodWorkEvaluation];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    if (props.accompanyingPeriodId) {
 | 
			
		||||
        getAccompanyingPeriodWorks(props.accompanyingPeriodId);
 | 
			
		||||
        getAccompanyingPeriodWorks(parseInt(props.accompanyingPeriodId));
 | 
			
		||||
    } else {
 | 
			
		||||
        console.error("No accompanyingperiod id was given");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showModal.value = true;
 | 
			
		||||
});
 | 
			
		||||
const getAccompanyingPeriodWorks = async (periodId: number) => {
 | 
			
		||||
    const url = `/api/1.0/person/accompanying-course/${periodId}/works.json`;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        accompanyingPeriodWorks.value = await fetchResults(url);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.log(error);
 | 
			
		||||
    const accompanyingPeriodWorksFetched = await fetchResults<AccompanyingPeriodWork>(url);
 | 
			
		||||
    if (props.isEvaluationSelector) {
 | 
			
		||||
        accompanyingPeriodWorks.value = accompanyingPeriodWorksFetched.filter(
 | 
			
		||||
            (acpw: AccompanyingPeriodWork) => acpw.accompanyingPeriodWorkEvaluations.length > 0
 | 
			
		||||
                && typeof acpw.id !== "undefined"
 | 
			
		||||
                && !props.ignoreAccompanyingPeriodWorkIds.includes(acpw.id),
 | 
			
		||||
        );
 | 
			
		||||
    } else {
 | 
			
		||||
        accompanyingPeriodWorks.value = accompanyingPeriodWorksFetched
 | 
			
		||||
    }
 | 
			
		||||
    /*  makeFetch<number, AccompanyingPeriodWork[]>("GET", url)
 | 
			
		||||
    .then((response) => {
 | 
			
		||||
      accompanyingPeriodWorks.value = response;
 | 
			
		||||
    })
 | 
			
		||||
    .catch((error) => {
 | 
			
		||||
      console.log(error);
 | 
			
		||||
    });*/
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const openModal = () => (showModal.value = true);
 | 
			
		||||
const closeModal = () => (showModal.value = false);
 | 
			
		||||
watch(selectedAcpw, (newValue) => {
 | 
			
		||||
    const inputField = document.getElementById(
 | 
			
		||||
        "find_accompanying_period_work_acpw",
 | 
			
		||||
    ) as HTMLInputElement;
 | 
			
		||||
    if (inputField) {
 | 
			
		||||
        inputField.value = String(newValue?.id || "");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /*  if (!props.isEvaluationSelector) {
 | 
			
		||||
    console.log("Emitting from watch:", { work: newValue });
 | 
			
		||||
    emit("pickWork", { work: newValue });
 | 
			
		||||
  }*/
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const openModal = () => {
 | 
			
		||||
    showModal.value = true;
 | 
			
		||||
};
 | 
			
		||||
const closeModal = () => {
 | 
			
		||||
    showModal.value = false;
 | 
			
		||||
    selectedEvaluation.value = null;
 | 
			
		||||
    // selectedAcpw.value = null;
 | 
			
		||||
    emit("closeModal");
 | 
			
		||||
};
 | 
			
		||||
const confirmSelection = () => {
 | 
			
		||||
    emit("pickWork", { work: selectedAcpw.value });
 | 
			
		||||
    closeModal();
 | 
			
		||||
    selectedAcpw.value = selectedAcpw.value;
 | 
			
		||||
    console.log("selectedAcpw", selectedAcpw.value);
 | 
			
		||||
 | 
			
		||||
    if (!props.isEvaluationSelector) {
 | 
			
		||||
        if (selectedAcpw.value) {
 | 
			
		||||
            // only emit if something is actually selected!
 | 
			
		||||
            emit("pickWork", { work: selectedAcpw.value });
 | 
			
		||||
            closeModal();
 | 
			
		||||
        }
 | 
			
		||||
        // optionally show some error or warning if not selected
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (selectedAcpw.value && props.isEvaluationSelector) {
 | 
			
		||||
        evaluations.value =
 | 
			
		||||
            selectedAcpw.value.accompanyingPeriodWorkEvaluations;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (selectedEvaluation.value && props.isEvaluationSelector) {
 | 
			
		||||
        // console.log('evaluation log in modal', selectedEvaluation.value)
 | 
			
		||||
        emit("update:selectedEvaluation", selectedEvaluation.value);
 | 
			
		||||
        closeModal();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
{%- macro details(w, accompanyingCourse, options) -%}
 | 
			
		||||
    {% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with {
 | 
			
		||||
        'displayAction': false,
 | 
			
		||||
        'displayAction': true,
 | 
			
		||||
        'displayContent': 'short',
 | 
			
		||||
        'displayFontSmall': true,
 | 
			
		||||
        'itemBlocClass': '',
 | 
			
		||||
        'displayNotification': false
 | 
			
		||||
        'displayNotification': true
 | 
			
		||||
    } %}
 | 
			
		||||
{% endmacro %}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,10 @@
 | 
			
		||||
 | 
			
		||||
{% set activeRouteKey = 'chill_person_accompanying_period_work_assign_duplicate' %}
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ 'Assign an accompanying period work duplicate' }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% import '@ChillPerson/AccompanyingPeriodWorkDuplicate/_details.html.twig' as details %}
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ 'Assign an accompanying period work duplicate' }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <div class="person-duplicate">
 | 
			
		||||
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <h3>{{ 'acpw_duplicate.Assign duplicate'|trans }}</h3>
 | 
			
		||||
        <h1>{{ 'acpw_duplicate.Assign duplicate'|trans }}</h1>
 | 
			
		||||
        {{ form_start(form) }}
 | 
			
		||||
        {%- if form.acpw is defined -%}
 | 
			
		||||
            {{ form_row(form.acpw) }}
 | 
			
		||||
 
 | 
			
		||||
@@ -32,9 +32,16 @@
 | 
			
		||||
                    <div class="wl-col list">
 | 
			
		||||
                        <div class="d-flex flex-column justify-content-center">
 | 
			
		||||
                            {% if app != null %}
 | 
			
		||||
                                <div class="date">
 | 
			
		||||
                                    {{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }}
 | 
			
		||||
                                </div>
 | 
			
		||||
                                {% if acp.closingDate != null %}
 | 
			
		||||
                                    {{ 'accompanying_period.dates_from_%opening_date%_to_%closing_date%'|trans({
 | 
			
		||||
                                        '%opening_date%': acp.openingDate|format_date('long'),
 | 
			
		||||
                                        '%closing_date%': acp.closingDate|format_date('long')}
 | 
			
		||||
                                    ) }}
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                    <div class="date">
 | 
			
		||||
                                        {{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }}
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
 | 
			
		||||
                            {% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', acp.id) %}
 | 
			
		||||
@@ -70,6 +77,20 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {% if acp.step == 'CLOSED' and acp.closingMotive is not null %}
 | 
			
		||||
                    <div class="wl-row">
 | 
			
		||||
                        <div class="wl-col title">
 | 
			
		||||
                            <h3 class="closingMotive">{{ 'Closing motive'|trans }}</h3>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="wl-col list">
 | 
			
		||||
                            <div>
 | 
			
		||||
                                {{ acp.closingMotive.name|localize_translatable_string }}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                {% if acp.user is not null %}
 | 
			
		||||
                    <div class="wl-row">
 | 
			
		||||
                        <div class="wl-col title">
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,9 @@ use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
/**
 | 
			
		||||
 * Service for merging two AccompanyingPeriodWork entities into a single entity.
 | 
			
		||||
 */
 | 
			
		||||
class AccompanyingPeriodWorkMergeService
 | 
			
		||||
readonly class AccompanyingPeriodWorkMergeService
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly EntityManagerInterface $em) {}
 | 
			
		||||
    public function __construct(private EntityManagerInterface $em) {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Merges two AccompanyingPeriodWork entities into one by transferring relevant data and removing the obsolete entity.
 | 
			
		||||
@@ -35,8 +35,9 @@ class AccompanyingPeriodWorkMergeService
 | 
			
		||||
            $this->alterStartDate($toKeep, $toDelete);
 | 
			
		||||
            $this->alterEndDate($toKeep, $toDelete);
 | 
			
		||||
            $this->concatenateComments($toKeep, $toDelete);
 | 
			
		||||
            $this->transferEvaluationsSQL($toKeep, $toDelete);
 | 
			
		||||
            $this->transferWorkflowsSQL($toKeep, $toDelete);
 | 
			
		||||
            $this->updateReferencesSQL($toKeep, $toDelete);
 | 
			
		||||
            $this->updateReferences($toKeep, $toDelete);
 | 
			
		||||
            $entityManager->remove($toDelete);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -54,6 +55,16 @@ class AccompanyingPeriodWorkMergeService
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function transferEvaluationsSQL(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->em->getConnection()->executeQuery(
 | 
			
		||||
            'UPDATE chill_person_accompanying_period_work_evaluation cpapwe
 | 
			
		||||
                SET accompanyingperiodwork_id = :toKeepId
 | 
			
		||||
                WHERE cpapwe.accompanyingperiodwork_id = :toDeleteId',
 | 
			
		||||
            ['toKeepId' => $toKeep->getId(), 'toDeleteId' => $toDelete->getId()]
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function alterStartDate(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
 | 
			
		||||
    {
 | 
			
		||||
        $startDate = min($toKeep->getStartDate(), $toDelete->getStartDate());
 | 
			
		||||
@@ -74,16 +85,17 @@ class AccompanyingPeriodWorkMergeService
 | 
			
		||||
 | 
			
		||||
    private function concatenateComments(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
 | 
			
		||||
    {
 | 
			
		||||
        $toKeep->setNote($toKeep->getNote()."\n\n-----------------\n\n".$toDelete->getNote());
 | 
			
		||||
        $toKeep->getPrivateComment()->concatenateComments($toDelete->getPrivateComment());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function updateReferencesSQL(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($toDelete->getAccompanyingPeriodWorkEvaluations() as $evaluation) {
 | 
			
		||||
            $toKeep->addAccompanyingPeriodWorkEvaluation($evaluation);
 | 
			
		||||
        if ('' !== $toDelete->getNote()) {
 | 
			
		||||
            $toKeep->setNote($toKeep->getNote()."\n\n-----------------\n\n".$toDelete->getNote());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (count($toDelete->getPrivateComment()->getComments()) > 0) {
 | 
			
		||||
            $toKeep->getPrivateComment()->concatenateComments($toDelete->getPrivateComment());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function updateReferences(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($toDelete->getReferrers() as $referrer) {
 | 
			
		||||
            // we only keep the current referrer
 | 
			
		||||
            $toKeep->addReferrer($referrer);
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
 | 
			
		||||
use Symfony\Component\Clock\ClockInterface;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
@@ -36,4 +37,17 @@ class AccompanyingPeriodWorkEvaluationDocumentDuplicator
 | 
			
		||||
 | 
			
		||||
        return $newDocument;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function duplicateToEvaluation(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): AccompanyingPeriodWorkEvaluationDocument
 | 
			
		||||
    {
 | 
			
		||||
        $newDocument = new AccompanyingPeriodWorkEvaluationDocument();
 | 
			
		||||
        $newDocument
 | 
			
		||||
            ->setTitle($document->getTitle().' ('.$this->translator->trans('accompanying_course_evaluation_document.duplicated_at', ['at' => $this->clock->now()]).')')
 | 
			
		||||
            ->setStoredObject($this->storedObjectDuplicate->duplicate($document->getStoredObject()))
 | 
			
		||||
        ;
 | 
			
		||||
 | 
			
		||||
        $evaluation->addDocument($newDocument);
 | 
			
		||||
 | 
			
		||||
        return $newDocument;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,22 +14,20 @@ namespace Chill\PersonBundle\Tests\Service\AccompanyingPeriodWork;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkGoal;
 | 
			
		||||
use Chill\PersonBundle\Entity\SocialWork\Result;
 | 
			
		||||
use Chill\PersonBundle\Service\AccompanyingPeriodWork\AccompanyingPeriodWorkMergeService;
 | 
			
		||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
 | 
			
		||||
use Doctrine\DBAL\Connection;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Monolog\Test\TestCase;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class AccompanyingPeriodWorkMergeServiceTest extends TestCase
 | 
			
		||||
class AccompanyingPeriodWorkMergeServiceTest extends KernelTestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
@@ -160,46 +158,62 @@ class AccompanyingPeriodWorkMergeServiceTest extends TestCase
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testMerge(): void
 | 
			
		||||
    public function testMergeAccompanyingPeriodWorks(): void
 | 
			
		||||
    {
 | 
			
		||||
        $accompanyingPeriodWork = new AccompanyingPeriodWork();
 | 
			
		||||
        $accompanyingPeriodWork->setStartDate(new \DateTime('2022-01-01'));
 | 
			
		||||
        $accompanyingPeriodWork->addReferrer($userA = new User());
 | 
			
		||||
        $accompanyingPeriodWork->addReferrer($userC = new User());
 | 
			
		||||
        $accompanyingPeriodWork->addAccompanyingPeriodWorkEvaluation($evaluationA = new AccompanyingPeriodWorkEvaluation());
 | 
			
		||||
        $accompanyingPeriodWork->setNote('blabla');
 | 
			
		||||
        $accompanyingPeriodWork->addThirdParty($thirdPartyA = new ThirdParty());
 | 
			
		||||
        $em = self::getContainer()->get(EntityManagerInterface::class);
 | 
			
		||||
 | 
			
		||||
        $userA = new User();
 | 
			
		||||
        $userA->setUsername('someUser');
 | 
			
		||||
        $userA->setEmail('someUser@example.com');
 | 
			
		||||
        $em->persist($userA);
 | 
			
		||||
 | 
			
		||||
        $toKeep = new AccompanyingPeriodWork();
 | 
			
		||||
        $toKeep->setStartDate(new \DateTime('2022-01-02'));
 | 
			
		||||
        $toKeep->setNote('Keep note');
 | 
			
		||||
        $toKeep->setCreatedBy($userA);
 | 
			
		||||
        $toKeep->setUpdatedBy($userA);
 | 
			
		||||
        $toKeep->addReferrer($userA);
 | 
			
		||||
        $em->persist($toKeep);
 | 
			
		||||
 | 
			
		||||
        $userB = new User();
 | 
			
		||||
        $userB->setUsername('anotherUser');
 | 
			
		||||
        $userB->setEmail('anotherUser@example.com');
 | 
			
		||||
        $em->persist($userB);
 | 
			
		||||
 | 
			
		||||
        $toDelete = new AccompanyingPeriodWork();
 | 
			
		||||
        $toDelete->setStartDate(new \DateTime('2022-01-01'));
 | 
			
		||||
        $toDelete->addReferrer($userB = new User());
 | 
			
		||||
        $toDelete->addReferrer($userC);
 | 
			
		||||
        $toDelete->addAccompanyingPeriodWorkEvaluation($evaluationB = new AccompanyingPeriodWorkEvaluation());
 | 
			
		||||
        $toDelete->setNote('boum');
 | 
			
		||||
        $toDelete->addThirdParty($thirdPartyB = new ThirdParty());
 | 
			
		||||
        $toDelete->addGoal($goalA = new AccompanyingPeriodWorkGoal());
 | 
			
		||||
        $toDelete->addResult($resultA = new Result());
 | 
			
		||||
        $toDelete->setNote('Delete note');
 | 
			
		||||
        $toDelete->setCreatedBy($userB);
 | 
			
		||||
        $toDelete->setUpdatedBy($userB);
 | 
			
		||||
        $toDelete->addReferrer($userB);
 | 
			
		||||
        $em->persist($toDelete);
 | 
			
		||||
 | 
			
		||||
        $service = $this->buildMergeService($toDelete);
 | 
			
		||||
        $service->merge($accompanyingPeriodWork, $toDelete);
 | 
			
		||||
        $evaluation = new AccompanyingPeriodWorkEvaluation();
 | 
			
		||||
        $evaluation->setAccompanyingPeriodWork($toDelete);
 | 
			
		||||
        $em->persist($evaluation);
 | 
			
		||||
 | 
			
		||||
        self::assertTrue($accompanyingPeriodWork->getReferrers()->contains($userA));
 | 
			
		||||
        self::assertTrue($accompanyingPeriodWork->getReferrers()->contains($userB));
 | 
			
		||||
        self::assertTrue($accompanyingPeriodWork->getReferrers()->contains($userC));
 | 
			
		||||
        $em->flush();
 | 
			
		||||
 | 
			
		||||
        self::assertTrue($accompanyingPeriodWork->getAccompanyingPeriodWorkEvaluations()->contains($evaluationA));
 | 
			
		||||
        self::assertTrue($accompanyingPeriodWork->getAccompanyingPeriodWorkEvaluations()->contains($evaluationB));
 | 
			
		||||
        foreach ($accompanyingPeriodWork->getAccompanyingPeriodWorkEvaluations() as $evaluation) {
 | 
			
		||||
            self::assertSame($accompanyingPeriodWork, $evaluation->getAccompanyingPeriodWork());
 | 
			
		||||
        }
 | 
			
		||||
        $service = new AccompanyingPeriodWorkMergeService($em);
 | 
			
		||||
        $merged = $service->merge($toKeep, $toDelete);
 | 
			
		||||
 | 
			
		||||
        self::assertStringContainsString('blabla', $accompanyingPeriodWork->getNote());
 | 
			
		||||
        self::assertStringContainsString('boum', $toDelete->getNote());
 | 
			
		||||
        $em->refresh($merged);
 | 
			
		||||
 | 
			
		||||
        self::assertTrue($accompanyingPeriodWork->getThirdParties()->contains($thirdPartyA));
 | 
			
		||||
        self::assertTrue($accompanyingPeriodWork->getThirdParties()->contains($thirdPartyB));
 | 
			
		||||
        // Assertions
 | 
			
		||||
 | 
			
		||||
        self::assertTrue($accompanyingPeriodWork->getGoals()->contains($goalA));
 | 
			
		||||
        self::assertTrue($accompanyingPeriodWork->getResults()->contains($resultA));
 | 
			
		||||
        $this->assertEquals(new \DateTime('2022-01-01'), $merged->getStartDate());
 | 
			
		||||
 | 
			
		||||
        $this->assertStringContainsString('Keep note', $merged->getNote());
 | 
			
		||||
        $this->assertStringContainsString('Delete note', $merged->getNote());
 | 
			
		||||
 | 
			
		||||
        $em->refresh($evaluation);
 | 
			
		||||
        $this->assertEquals($toKeep->getId(), $evaluation->getAccompanyingPeriodWork()->getId());
 | 
			
		||||
 | 
			
		||||
        $em->remove($evaluation);
 | 
			
		||||
        $em->remove($toKeep);
 | 
			
		||||
        $em->remove($toDelete);
 | 
			
		||||
        $em->remove($userA);
 | 
			
		||||
        $em->remove($userB);
 | 
			
		||||
        $em->flush();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1993,3 +1993,33 @@ paths:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: object
 | 
			
		||||
 | 
			
		||||
  /1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/duplicate:
 | 
			
		||||
    post:
 | 
			
		||||
      tags:
 | 
			
		||||
        - accompanying-course-work-evaluation-document
 | 
			
		||||
      summary: Dupliate an an accompanying period work evaluation document to another evaluation
 | 
			
		||||
      parameters:
 | 
			
		||||
        - in: path
 | 
			
		||||
          name: document_id
 | 
			
		||||
          required: true
 | 
			
		||||
          description: The document's id
 | 
			
		||||
          schema:
 | 
			
		||||
            type: integer
 | 
			
		||||
            format: integer
 | 
			
		||||
            minimum: 1
 | 
			
		||||
        - in: path
 | 
			
		||||
          name: evaluation_id
 | 
			
		||||
          required: true
 | 
			
		||||
          description: The evaluation's id
 | 
			
		||||
          schema:
 | 
			
		||||
            type: integer
 | 
			
		||||
            format: integer
 | 
			
		||||
            minimum: 1
 | 
			
		||||
      responses:
 | 
			
		||||
        200:
 | 
			
		||||
          description: "OK"
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: object
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,8 @@
 | 
			
		||||
services:
 | 
			
		||||
    _defaults:
 | 
			
		||||
        autowire: true
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
 | 
			
		||||
    Chill\PersonBundle\Notification\AccompanyingPeriodNotificationHandler:
 | 
			
		||||
        autowire: true
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
@@ -8,3 +12,5 @@ services:
 | 
			
		||||
    Chill\PersonBundle\Notification\AccompanyingPeriodWorkEvaluationDocumentNotificationHandler:
 | 
			
		||||
        autowire: true
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
    Chill\PersonBundle\Notification\FlagProviders\DesignatedReferrerNotificationFlagProvider: ~
 | 
			
		||||
    Chill\PersonBundle\Notification\FlagProviders\PersonAddressMoveNotificationFlagProvider: ~
 | 
			
		||||
 
 | 
			
		||||
@@ -750,6 +750,42 @@ evaluation:
 | 
			
		||||
    delay: Délai
 | 
			
		||||
    notificationDelay: Délai de notification
 | 
			
		||||
    url: Lien internet
 | 
			
		||||
    title: Ecrire une évaluation
 | 
			
		||||
    status: Statut
 | 
			
		||||
    choose_a_status: Choisir un statut
 | 
			
		||||
    startdate: Date d'ouverture
 | 
			
		||||
    enddate: Date de fin
 | 
			
		||||
    maxdate: Date d'échéance
 | 
			
		||||
    warning_interval: Rappel (jours)
 | 
			
		||||
    public_comment: Note publique
 | 
			
		||||
    comment_placeholder: Commencez à écrire ...
 | 
			
		||||
    generate_a_document: Générer un document
 | 
			
		||||
    choose_a_template: Choisir un modèle
 | 
			
		||||
    add_a_document: Ajouter un document
 | 
			
		||||
    add: Ajouter une évaluation
 | 
			
		||||
    time_spent: Temps de rédaction
 | 
			
		||||
    select_time_spent: Indiquez le temps de rédaction
 | 
			
		||||
    Documents: Documents
 | 
			
		||||
    document_add: Générer ou téléverser un document
 | 
			
		||||
    document_upload: Téléverser un document
 | 
			
		||||
    document_title: Titre du document
 | 
			
		||||
    template_title: Nom du template
 | 
			
		||||
    browse: Ajouter un document
 | 
			
		||||
    replace: Remplacer
 | 
			
		||||
    download: Télécharger le fichier existant
 | 
			
		||||
    notification_notify_referrer: Notifier le référent
 | 
			
		||||
    notification_notify_any: Notifier d'autres utilisateurs
 | 
			
		||||
    notification_send: Envoyer une notification
 | 
			
		||||
    document:
 | 
			
		||||
      edit: Modifier
 | 
			
		||||
      delete: Supprimer
 | 
			
		||||
      move: Déplacer
 | 
			
		||||
      duplicate: Dupliquer
 | 
			
		||||
      duplicate_here: Dupliquer ici
 | 
			
		||||
      duplicate_to_other_evaluation: Dupliquer vers une autre évaluation
 | 
			
		||||
      duplicate_success: Le document d'évaluation a été dupliquer
 | 
			
		||||
      move_success: Le document d'évaluation a été déplacer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
goal:
 | 
			
		||||
   desactivationDate: Date de désactivation
 | 
			
		||||
@@ -774,7 +810,6 @@ relation:
 | 
			
		||||
    reverseTitle: Deuxième membre
 | 
			
		||||
 | 
			
		||||
days: jours
 | 
			
		||||
months: mois
 | 
			
		||||
years: années
 | 
			
		||||
 | 
			
		||||
# specific to closing motive
 | 
			
		||||
@@ -926,7 +961,7 @@ accompanying_course_work:
 | 
			
		||||
    types_filter: Filtrer par type d'action
 | 
			
		||||
    user_filter: Filtrer par intervenant
 | 
			
		||||
    On-going works over total: Actions en cours / Actions du parcours
 | 
			
		||||
 | 
			
		||||
    my_actions_filter: Mes actions (où j'interviens)
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
Person addresses: Adresses de résidence
 | 
			
		||||
@@ -1513,6 +1548,7 @@ acpw_duplicate:
 | 
			
		||||
    to keep: Action d'accompagnement à conserver
 | 
			
		||||
    to delete: Action d'accompagnement à supprimer
 | 
			
		||||
    Successfully merged: Action d'accompagnement fusionnée avec succès.
 | 
			
		||||
    You cannot merge a accompanying period work with itself. Please choose a different one: Vous ne pouvez pas fusionner un action d'accompagnement avec lui-même. Veuillez en choisir un autre.
 | 
			
		||||
 | 
			
		||||
my_parcours_filters:
 | 
			
		||||
  referrer_parcours_and_acpw: Agent traitant ou réferent
 | 
			
		||||
@@ -1521,3 +1557,6 @@ my_parcours_filters:
 | 
			
		||||
  parcours_intervening: Intervenant
 | 
			
		||||
  is_open: Parcours ouverts
 | 
			
		||||
  is_closed: Parcours clôturés
 | 
			
		||||
 | 
			
		||||
document_duplicate:
 | 
			
		||||
    to_evaluation_success: "Le document a été dupliquer"
 | 
			
		||||
 
 | 
			
		||||
@@ -624,8 +624,7 @@ final class SingleTaskController extends AbstractController
 | 
			
		||||
            ->addCheckbox('status', $statuses, $statuses, $statusTrans);
 | 
			
		||||
 | 
			
		||||
        $states = $this->singleTaskStateRepository->findAllExistingStates();
 | 
			
		||||
        $checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['in_progress', 'closed', 'canceled', 'validated'], true)));
 | 
			
		||||
 | 
			
		||||
        $checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['to_validate', 'in_progress', 'closed', 'canceled', 'validated'], true)));
 | 
			
		||||
        if ([] !== $states) {
 | 
			
		||||
            $filterBuilder
 | 
			
		||||
                ->addCheckbox('states', $states, $checked);
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,125 @@
 | 
			
		||||
<?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\ThirdPartyBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\PersonBundle\Form\PersonConfimDuplicateType;
 | 
			
		||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
 | 
			
		||||
use Chill\ThirdPartyBundle\Form\ThirdpartyFindDuplicateType;
 | 
			
		||||
use Chill\ThirdPartyBundle\Service\ThirdpartyMergeService;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Session\Session;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
 | 
			
		||||
use Symfony\Component\Translation\TranslatableMessage;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
 | 
			
		||||
class ThirdpartyDuplicateController extends AbstractController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly ThirdpartyMergeService $thirdPartyMergeService, private readonly TranslatorInterface $translator) {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ParamConverter("thirdparty", options={"id": "thirdparty_id"})
 | 
			
		||||
     */
 | 
			
		||||
    #[Route(path: '/{_locale}/3party/{thirdparty_id}/find-manually', name: 'chill_thirdparty_find_duplicate')]
 | 
			
		||||
    public function findManuallyDuplicateAction(ThirdParty $thirdparty, Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $suggested = [];
 | 
			
		||||
 | 
			
		||||
        if ('child' === $thirdparty->getKind()) {
 | 
			
		||||
            $suggested = $thirdparty->getParent()->getChildren();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $form = $this->createForm(ThirdpartyFindDuplicateType::class, null, ['suggested' => $suggested]);
 | 
			
		||||
 | 
			
		||||
        $form->handleRequest($request);
 | 
			
		||||
 | 
			
		||||
        if ($form->isSubmitted() && $form->isValid()) {
 | 
			
		||||
            $thirdparty2 = $form->get('thirdparty')->getData();
 | 
			
		||||
 | 
			
		||||
            $direction = $form->get('direction')->getData();
 | 
			
		||||
 | 
			
		||||
            if ('starting' === $direction) {
 | 
			
		||||
                $params = [
 | 
			
		||||
                    'thirdparty1_id' => $thirdparty->getId(),
 | 
			
		||||
                    'thirdparty2_id' => $thirdparty2->getId(),
 | 
			
		||||
                ];
 | 
			
		||||
            } else {
 | 
			
		||||
                $params = [
 | 
			
		||||
                    'thirdparty1_id' => $thirdparty2->getId(),
 | 
			
		||||
                    'thirdparty2_id' => $thirdparty->getId(),
 | 
			
		||||
                ];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return $this->redirectToRoute('chill_thirdparty_duplicate_confirm', $params);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->render('@ChillThirdParty/ThirdPartyDuplicate/find_duplicate.html.twig', [
 | 
			
		||||
            'thirdparty' => $thirdparty,
 | 
			
		||||
            'form' => $form->createView(),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ParamConverter("thirdparty1", options={"id": "thirdparty1_id"})
 | 
			
		||||
     * @ParamConverter("thirdparty2", options={"id": "thirdparty2_id"})
 | 
			
		||||
     */
 | 
			
		||||
    #[Route(path: '/{_locale}/3party/{thirdparty1_id}/duplicate/{thirdparty2_id}/confirm', name: 'chill_thirdparty_duplicate_confirm')]
 | 
			
		||||
    public function confirmAction(ThirdParty $thirdparty1, ThirdParty $thirdparty2, Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $this->validateThirdpartyMerge($thirdparty1, $thirdparty2);
 | 
			
		||||
            $form = $this->createForm(PersonConfimDuplicateType::class);
 | 
			
		||||
 | 
			
		||||
            $form->handleRequest($request);
 | 
			
		||||
 | 
			
		||||
            if ($form->isSubmitted() && $form->isValid()) {
 | 
			
		||||
 | 
			
		||||
                $this->thirdPartyMergeService->merge($thirdparty1, $thirdparty2);
 | 
			
		||||
 | 
			
		||||
                $session = $request->getSession();
 | 
			
		||||
                if ($session instanceof Session) {
 | 
			
		||||
                    $session->getFlashBag()->add('success', new TranslatableMessage('thirdparty_duplicate.Merge successful'));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return $this->redirectToRoute('chill_crud_3party_3party_view', ['id' => $thirdparty1->getId()]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return $this->render('@ChillThirdParty/ThirdPartyDuplicate/confirm.html.twig', [
 | 
			
		||||
                'thirdparty' => $thirdparty1,
 | 
			
		||||
                'thirdparty2' => $thirdparty2,
 | 
			
		||||
                'form' => $form->createView(),
 | 
			
		||||
            ]);
 | 
			
		||||
        } catch (\InvalidArgumentException $e) {
 | 
			
		||||
            $this->addFlash('error', $this->translator->trans($e->getMessage()));
 | 
			
		||||
 | 
			
		||||
            return $this->redirectToRoute('chill_thirdparty_find_duplicate', [
 | 
			
		||||
                'thirdparty_id' => $thirdparty1->getId(),
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function validateThirdpartyMerge(ThirdParty $thirdparty1, ThirdParty $thirdparty2): void
 | 
			
		||||
    {
 | 
			
		||||
        $constraints = [
 | 
			
		||||
            [$thirdparty1 === $thirdparty2, 'thirdparty_duplicate.You cannot merge a thirdparty with itself. Please choose a different thirdparty'],
 | 
			
		||||
            [$thirdparty1->getKind() !== $thirdparty2->getKind(), 'thirdparty_duplicate.A thirdparty can only be merged with a thirdparty of the same kind'],
 | 
			
		||||
            [$thirdparty1->getParent() !== $thirdparty2->getParent(), 'thirdparty_duplicate.Two child thirdparties must have the same parent'],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        foreach ($constraints as [$condition, $message]) {
 | 
			
		||||
            if ($condition) {
 | 
			
		||||
                throw new \InvalidArgumentException($message);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user