mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 17:28:23 +00:00 
			
		
		
		
	Compare commits
	
		
			150 Commits
		
	
	
		
			signature-
			...
			288-signat
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d33dcacc46 | |||
| 8d97df9f96 | |||
| 2822800c76 | |||
| 8973b7c20b | |||
| 7f144da1a7 | |||
| ab4193938d | |||
| e2426ba1d8 | |||
| 8209990437 | |||
| b1885de3e2 | |||
| 218280304c | |||
| 8a7b48b201 | |||
| 52a9aab73f | |||
| 8f358112b1 | |||
| 57a07af3db | |||
| fd216ff66e | |||
| 689c2c574a | |||
| a8de18beac | |||
| babca5fc0f | |||
| f2c5663b05 | |||
| ba95687f46 | |||
| a309cc0774 | |||
| 3db4fff80d | |||
| c9d54a5fea | |||
| 86c862e69d | |||
| d88b5a0098 | |||
| 9bc6fe6aff | |||
| 18a03fd740 | |||
| 4c3bfc90b5 | |||
| bd41308bbd | |||
| f8fa96d836 | |||
| d28cec3786 | |||
| 7cd36cd483 | |||
| d3d98cdec2 | |||
| 49dd7f94fa | |||
| 916724c0c5 | |||
|  | 102d0dad94 | ||
|  | 8d225dd68c | ||
|  | 61d0005be8 | ||
| 47f4cfddbb | |||
| e95f9e9846 | |||
| 1f4bef754d | |||
| 19e34d5dc0 | |||
| fab00f679c | |||
| 791b3776c5 | |||
| 6bd38f1a58 | |||
| 68d21c9267 | |||
| e7ca89e0c1 | |||
| fc8bc33ba9 | |||
| cbd9489810 | |||
| 90b615c5b2 | |||
| 5ca222b501 | |||
| 3e4495dd6e | |||
| bca0d04201 | |||
| f66ac50571 | |||
| b454774836 | |||
| 008f344e49 | |||
| 90bfd87ec6 | |||
| cc0030c1cd | |||
| d60ba3ecb2 | |||
| cd5001ac74 | |||
| 98f47ac512 | |||
| 31b541d12f | |||
| 72045ce082 | |||
| 0bfb3de465 | |||
| 9ec4c77fb7 | |||
| 77c53972c8 | |||
| 350d991a85 | |||
| 0ce9cdd07a | |||
| 1993fac1c4 | |||
| 83883567a2 | |||
| 29d57934a1 | |||
| f43d79c940 | |||
| be730679c8 | |||
| f62f1891d8 | |||
| ebb856fe85 | |||
| 61877e0157 | |||
| 4c3f082163 | |||
| 35109133f6 | |||
| a220dad83b | |||
| 9eb571549b | |||
| db8257d230 | |||
| bce93efe83 | |||
| 06401af801 | |||
| ea1d4c48f2 | |||
|  | 33cba27dd4 | ||
| a7ec7c9f37 | |||
| c9e13be736 | |||
| b9b342fe44 | |||
| 31f29f0bc5 | |||
| 0bc9fff825 | |||
| 25f93e8a89 | |||
| 4e0d8e4def | |||
| 1ecc825945 | |||
| addc623add | |||
| 1b96deb4ee | |||
| f510acd170 | |||
| 835409cb94 | |||
| 2121b3ef28 | |||
| 6c9101c167 | |||
| b46883fe36 | |||
| 8d58805abd | |||
| c3a799cb7d | |||
| bc683b28d6 | |||
| d91b1a70bf | |||
| 853014d8d2 | |||
| ad6154a1e4 | |||
| 50c04382ef | |||
| d62e9ce269 | |||
| 2149ef1cb4 | |||
| d15fbadd27 | |||
| fbbf421d8b | |||
| fe695f1a14 | |||
| d0ec6f9819 | |||
| 0b739fda34 | |||
| 9b8e143855 | |||
| a533ab77ed | |||
| 087032881b | |||
| 82667a1c0f | |||
| db6408926b | |||
| f5c7ab6ef0 | |||
| a13ada2937 | |||
| 3be8a39a1a | |||
| d7eb1e01da | |||
| bd62202d22 | |||
| 0e3de2ec8a | |||
| aa2a398f9e | |||
| 33187448a0 | |||
| a4482ad28b | |||
| 8ed5a023e8 | |||
| 653ac1d62b | |||
| 499009ac43 | |||
| 192b161e78 | |||
| 1b1f355123 | |||
| 39a863448c | |||
| 0c1a4a5f59 | |||
| 6f358ee1a9 | |||
| 0f36b9349b | |||
| d18cc29acf | |||
| 4220d1a2d3 | |||
| 1ae27152c2 | |||
| b946f8c10a | |||
| 62d6106801 | |||
| 89fb87f71f | |||
| 1337360690 | |||
| 9324c33caf | |||
| c2dd9ef676 | |||
| a42d7231d9 | |||
| 38deaf6f36 | |||
| 04fc5b6614 | |||
| 384b2be577 | 
							
								
								
									
										5
									
								
								.changes/unreleased/Feature-20240718-151233.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/unreleased/Feature-20240718-151233.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| kind: Feature | ||||
| body: Metadata form added for person signatures | ||||
| time: 2024-07-18T15:12:33.8134266+02:00 | ||||
| custom: | ||||
|   Issue: "288" | ||||
							
								
								
									
										21
									
								
								.changes/v2.20.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.changes/v2.20.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| ## v2.20.0 - 2024-06-05 | ||||
| ### Fixed | ||||
| * ([#170](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/170)) Display agents traitants instead of accompanying period referrer in export list social actions. | ||||
| * Added translations for choices of durations (> 5 hours) | ||||
| ### Feature | ||||
| * ([#145](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/145)) Allow to open documents in LibreOffice locally (need configuration within security); | ||||
|  | ||||
|   This endpoint should be added to make the endpoint works properly: | ||||
|  | ||||
|   ```yaml | ||||
|   security: | ||||
|       firewalls: | ||||
|           dav: | ||||
|                pattern: ^/dav | ||||
|                provider: chain_provider | ||||
|                stateless: true | ||||
|                guard: | ||||
|                    authenticators: | ||||
|                        - Chill\DocStoreBundle\Security\Guard\JWTOnDavUrlAuthenticator | ||||
|  | ||||
|   ``` | ||||
							
								
								
									
										3
									
								
								.changes/v2.20.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v2.20.1.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v2.20.1 - 2024-06-05 | ||||
| ### Fixed | ||||
| * Do not allow StoredObjectCreated for edit and convert buttons  | ||||
							
								
								
									
										31
									
								
								.changes/v2.21.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								.changes/v2.21.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| ## v2.21.0 - 2024-06-18 | ||||
| ### Feature | ||||
| * Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period | ||||
| * ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars | ||||
| * ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope" | ||||
| * ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range. | ||||
| * ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope" | ||||
| * ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs" | ||||
| * ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action: | ||||
|   now, the job and service is shown: | ||||
|   * at the activity's date, | ||||
|   * at the appointment's date, | ||||
|   * when the user is marked as referrer for an accompanying period work, | ||||
|   * when the user apply a transition in a workflow, | ||||
|   * when the user updates or creates "something" ("created/updated by ... at ..."), | ||||
|   * or when he wrote a comment, | ||||
|   * … | ||||
|  | ||||
| ### Traduction francophone | ||||
| * Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche; | ||||
| * Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables; | ||||
| * [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent; | ||||
| * Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché: | ||||
|   * à la date d'un échange, | ||||
|   * au jour d'un rendez-vous, | ||||
|   * quand l'utilisateur est devenu référent d'un parcours d'accompagnement, | ||||
|   * quand il a appliqué une transition sur un workflow, | ||||
|   * quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...", | ||||
|   * quand il a mis à jour un commentaire, | ||||
|   * … | ||||
|  | ||||
							
								
								
									
										58
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -6,6 +6,64 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), | ||||
| and is generated by [Changie](https://github.com/miniscruff/changie). | ||||
|  | ||||
|  | ||||
| ## v2.21.0 - 2024-06-18 | ||||
| ### Feature | ||||
| * Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period | ||||
| * ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars | ||||
| * ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope" | ||||
| * ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range. | ||||
| * ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope" | ||||
| * ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs" | ||||
| * ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action: | ||||
|   now, the job and service is shown: | ||||
|   * at the activity's date, | ||||
|   * at the appointment's date, | ||||
|   * when the user is marked as referrer for an accompanying period work, | ||||
|   * when the user apply a transition in a workflow, | ||||
|   * when the user updates or creates "something" ("created/updated by ... at ..."), | ||||
|   * or when he wrote a comment, | ||||
|   * … | ||||
|  | ||||
| ### Traduction francophone | ||||
| * Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche; | ||||
| * Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables; | ||||
| * [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent; | ||||
| * Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché: | ||||
|   * à la date d'un échange, | ||||
|   * au jour d'un rendez-vous, | ||||
|   * quand l'utilisateur est devenu référent d'un parcours d'accompagnement, | ||||
|   * quand il a appliqué une transition sur un workflow, | ||||
|   * quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...", | ||||
|   * quand il a mis à jour un commentaire, | ||||
|   * … | ||||
|  | ||||
|  | ||||
| ## v2.20.1 - 2024-06-05 | ||||
| ### Fixed | ||||
| * Do not allow StoredObjectCreated for edit and convert buttons  | ||||
|  | ||||
| ## v2.20.0 - 2024-06-05 | ||||
| ### Fixed | ||||
| * ([#170](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/170)) Display agents traitants instead of accompanying period referrer in export list social actions. | ||||
| * Added translations for choices of durations (> 5 hours) | ||||
| ### Feature | ||||
| * ([#145](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/145)) Allow to open documents in LibreOffice locally (need configuration within security); | ||||
|  | ||||
|   This endpoint should be added to make the endpoint works properly: | ||||
|  | ||||
|   ```yaml | ||||
|   security: | ||||
|       firewalls: | ||||
|           dav: | ||||
|                pattern: ^/dav | ||||
|                provider: chain_provider | ||||
|                stateless: true | ||||
|                guard: | ||||
|                    authenticators: | ||||
|                        - Chill\DocStoreBundle\Security\Guard\JWTOnDavUrlAuthenticator | ||||
|  | ||||
|   ``` | ||||
|  | ||||
| ## v2.19.0 - 2024-05-14 | ||||
| ### Feature | ||||
| * ([#197](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/197)) Make the script which subscribe to microsoft calendars changes more tolerant to errors or missing configuration on the microsoft side | ||||
|   | ||||
| @@ -19,7 +19,6 @@ | ||||
|         "doctrine/doctrine-migrations-bundle": "^3.0", | ||||
|         "doctrine/orm": "^2.13.0", | ||||
|         "erusev/parsedown": "^1.7", | ||||
|         "graylog2/gelf-php": "^1.5", | ||||
|         "knplabs/knp-menu-bundle": "^3.0", | ||||
|         "knplabs/knp-time-bundle": "^1.12", | ||||
|         "knpuniversity/oauth2-client-bundle": "^2.10", | ||||
| @@ -93,11 +92,12 @@ | ||||
|         "phpstan/phpstan": "^1.9", | ||||
|         "phpstan/phpstan-deprecation-rules": "^1.1", | ||||
|         "phpstan/phpstan-strict-rules": "^1.0", | ||||
|         "phpunit/phpunit": ">= 7.5", | ||||
|         "phpunit/phpunit": "^10.5.24", | ||||
|         "rector/rector": "^1.1.0", | ||||
|         "symfony/debug-bundle": "^5.4", | ||||
|         "symfony/dotenv": "^5.4", | ||||
|         "symfony/maker-bundle": "^1.20", | ||||
|         "symfony/phpunit-bridge": "^7.1", | ||||
|         "symfony/runtime": "^5.4", | ||||
|         "symfony/stopwatch": "^5.4", | ||||
|         "symfony/var-dumper": "^5.4" | ||||
|   | ||||
| @@ -95,7 +95,7 @@ custom developments. But most of the time, this should be fine. | ||||
|  | ||||
| You have to configure some local variables, which are described in the :code:`.env` file. The secrets should not be stored | ||||
| in this :code:`.env` file, but instead using the `secrets management tool <https://symfony.com/doc/current/configuration/secrets.html>`_ | ||||
| or in the :code:`.env.local` file, which should not be commited to the git repository. | ||||
| or in the :code:`.env.local` file, which should not be committed to the git repository. | ||||
|  | ||||
| You do not need to set variables for the smtp server, redis server and relatorio server, as they are generated automatically | ||||
| by the symfony server, from the docker compose services. | ||||
| @@ -114,6 +114,12 @@ you can either: | ||||
| - add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env, | ||||
|   not the password in clear text). | ||||
|  | ||||
| - set up the jwt authentication bundle | ||||
|  | ||||
| Some environment variables are available for the JWT authentication bundle in the :code:`.env` file. You must also run the command | ||||
| :code:`symfony console lexik:jwt:generate-keypair` to generate some keys that will be stored in the paths set up in the :code:`JWT_SECRET_KEY` | ||||
| and the :code:`JWT_PUBLIC_KEY` env variables. This is only required for using the stored documents in Chill. | ||||
|  | ||||
| Prepare migrations and other tools | ||||
| ********************************** | ||||
|  | ||||
| @@ -164,7 +170,7 @@ can rely on the whole chill framework, meaning there is no need to add them to t | ||||
| You will require some bundles to have the following development tools: | ||||
|  | ||||
| - add fixtures | ||||
| - add profiler and var-dumper to debug | ||||
| - add profiler and debug bundle | ||||
|  | ||||
| Install fixtures | ||||
| **************** | ||||
| @@ -179,7 +185,7 @@ Install fixtures | ||||
| This will generate user accounts, centers, and some basic configuration. | ||||
|  | ||||
| The accounts created are: :code:`center a_social`, :code:`center b_social`, :code:`center a_direction`, ...  The full list is | ||||
| visibile in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`. | ||||
| visible in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`. | ||||
|  | ||||
| The password is always :code:`password`. | ||||
|  | ||||
| @@ -192,7 +198,7 @@ Add web profiler and debugger | ||||
|  | ||||
| .. code-block:: bash | ||||
|  | ||||
|    symfony composer require --dev symfony/web-profiler-bundle symfony/var-dumper | ||||
|    symfony composer require --dev symfony/web-profiler-bundle symfony/debug-bundle | ||||
|  | ||||
| Working on chill bundles | ||||
| ************************ | ||||
|   | ||||
| @@ -46,9 +46,11 @@ | ||||
|     "@fullcalendar/vue3": "^6.1.4", | ||||
|     "@popperjs/core": "^2.9.2", | ||||
|     "@types/leaflet": "^1.9.3", | ||||
|     "@types/dompurify": "^3.0.5", | ||||
|     "dropzone": "^5.7.6", | ||||
|     "es6-promise": "^4.2.8", | ||||
|     "leaflet": "^1.7.1", | ||||
|     "marked": "^12.0.2", | ||||
|     "masonry-layout": "^4.2.2", | ||||
|     "mime": "^4.0.0", | ||||
|     "swagger-ui": "^4.15.5", | ||||
|   | ||||
| @@ -0,0 +1,49 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\ActivityBundle\Menu; | ||||
|  | ||||
| use Chill\ActivityBundle\Security\Authorization\ActivityVoter; | ||||
| use Chill\MainBundle\Routing\LocalMenuBuilderInterface; | ||||
| use Knp\Menu\MenuItem; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
|  | ||||
| final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface | ||||
| { | ||||
|     public function __construct(private Security $security) {} | ||||
|  | ||||
|     public static function getMenuIds(): array | ||||
|     { | ||||
|         return ['accompanying_course_quick_menu']; | ||||
|     } | ||||
|  | ||||
|     public function buildMenu($menuId, MenuItem $menu, array $parameters) | ||||
|     { | ||||
|         /** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */ | ||||
|         $accompanyingCourse = $parameters['accompanying-course']; | ||||
|  | ||||
|         if ($this->security->isGranted(ActivityVoter::CREATE, $accompanyingCourse)) { | ||||
|             $menu | ||||
|                 ->addChild('Create a new activity in accompanying course', [ | ||||
|                     'route' => 'chill_activity_activity_new', | ||||
|                     'routeParameters' => [ | ||||
|                         // 'activityType_id' => '', | ||||
|                         'accompanying_period_id' => $accompanyingCourse->getId(), | ||||
|                     ], | ||||
|                 ]) | ||||
|                 ->setExtras([ | ||||
|                     'order' => 10, | ||||
|                     'icon' => 'plus', | ||||
|                 ]) | ||||
|             ; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -68,7 +68,7 @@ | ||||
|                     <div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div> | ||||
|                     <div class="wl-col list"> | ||||
|                         <p class="wl-item"> | ||||
|                             <span class="badge-user">{{ activity.user|chill_entity_render_box }}</span> | ||||
|                             <span class="badge-user">{{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}</span> | ||||
|                         </p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|   | ||||
| @@ -87,7 +87,8 @@ | ||||
|                             <li> | ||||
|                                 {% if bloc.type == 'user' %} | ||||
|                                     <span class="badge-user"> | ||||
|                                         {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }} | ||||
|                                         hello | ||||
|                                         {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }} | ||||
|                                     </span> | ||||
|                                 {% else %} | ||||
|                                     {{ _self.insert_onthefly(bloc.type, item) }} | ||||
| @@ -114,7 +115,7 @@ | ||||
|                 <li> | ||||
|                     {% if bloc.type == 'user' %} | ||||
|                         <span class="badge-user"> | ||||
|                             {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }} | ||||
|                             {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }} | ||||
|                         </span> | ||||
|                     {% else %} | ||||
|                         {{ _self.insert_onthefly(bloc.type, item) }} | ||||
| @@ -142,7 +143,7 @@ | ||||
|                         <span class="wl-item"> | ||||
|                             {% if bloc.type == 'user' %} | ||||
|                                 <span class="badge-user"> | ||||
|                                     {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }} | ||||
|                                     {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }} | ||||
|                                     {%- if context == 'calendar_accompanyingCourse' or context == 'calendar_person' %} | ||||
|                                         {% set invite = entity.inviteForUser(item) %} | ||||
|                                         {% if invite is not null %} | ||||
|   | ||||
| @@ -41,7 +41,7 @@ | ||||
|                         {% if activity.user and t.userVisible %} | ||||
|                             <li> | ||||
|                                 <span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span> | ||||
|                                 <span class="badge-user">{{ activity.user|chill_entity_render_box }}</span> | ||||
|                                 <span class="badge-user">{{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}</span> | ||||
|                             </li> | ||||
|                         {% endif %} | ||||
|  | ||||
|   | ||||
| @@ -37,7 +37,7 @@ | ||||
|                 {%- if entity.user is not null %} | ||||
|                     <dt class="inline">{{ 'Referrer'|trans|capitalize }}</dt> | ||||
|                     <dd> | ||||
|                         <span class="badge-user">{{ entity.user|chill_entity_render_box }}</span> | ||||
|                         <span class="badge-user">{{ entity.user|chill_entity_render_box({'at_date': entity.date}) }}</span> | ||||
|                     </dd> | ||||
|                 {% endif %} | ||||
|  | ||||
|   | ||||
| @@ -145,7 +145,7 @@ class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn | ||||
|                 throw new \RuntimeException('Could not determine context of activity.'); | ||||
|             } | ||||
|         } elseif ($subject instanceof AccompanyingPeriod) { | ||||
|             if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) { | ||||
|             if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep() || AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) { | ||||
|                 if (\in_array($attribute, [self::UPDATE, self::CREATE, self::DELETE], true)) { | ||||
|                     return false; | ||||
|                 } | ||||
|   | ||||
| @@ -77,6 +77,18 @@ Choose a type: Choisir un type | ||||
| 4 hours: 4 heures | ||||
| 4 hours 30: 4 heures 30 | ||||
| 5 hours: 5 heures | ||||
| 5 hours 30: 5 heure 30 | ||||
| 6 hours: 6 heures | ||||
| 6 hours 30: 6 heure 30 | ||||
| 7 hours: 7 heures | ||||
| 7 hours 30: 7 heure 30 | ||||
| 8 hours: 8 heures | ||||
| 8 hours 30: 8 heure 30 | ||||
| 9 hours: 9 heures | ||||
| 9 hours 30: 9 heure 30 | ||||
| 10 hours: 10 heures | ||||
| 11 hours: 11 heures | ||||
| 12 hours: 12 heures | ||||
| Concerned groups: Parties concernées par l'échange | ||||
| Persons in accompanying course: Usagers du parcours | ||||
| Third persons: Tiers non-pro. | ||||
| @@ -210,6 +222,7 @@ Documents label: Libellé du champ Documents | ||||
| # activity type category admin | ||||
| ActivityTypeCategory list: Liste des catégories des types d'échange | ||||
| Create a new activity type category: Créer une nouvelle catégorie de type d'échange | ||||
| Create a new activity in accompanying course: Créer un échange dans le parcours | ||||
|  | ||||
| # activity delete | ||||
| Remove activity: Supprimer un échange | ||||
|   | ||||
| @@ -49,13 +49,13 @@ | ||||
| 										<li> | ||||
| 											<span> | ||||
| 												<abbr class="referrer" title={{ 'Created by'|trans }}>{{ 'By'|trans }}:</abbr> | ||||
| 												<b>{{ entity.createdBy|chill_entity_render_box }}</b> | ||||
| 												<b>{{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}</b> | ||||
| 											</span> | ||||
| 										</li> | ||||
| 										<li> | ||||
| 											<span> | ||||
| 												<abbr class="referrer" title={{ 'Created for'|trans }}>{{ 'For'|trans }}:</abbr> | ||||
| 												<b>{{ entity.agent|chill_entity_render_box }}</b> | ||||
| 												<b>{{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}</b> | ||||
|  | ||||
| 											</span> | ||||
| 										</li> | ||||
|   | ||||
| @@ -18,11 +18,11 @@ | ||||
| 				<dd>{{ entity.type|chill_entity_render_box }}</dd> | ||||
|  | ||||
| 				<dt class="inline">{{ 'Created by'|trans }}</dt> | ||||
| 				<dd>{{ entity.createdBy }}</dd> | ||||
| 				<dd>{{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}</dd> | ||||
|  | ||||
| 				<dt class="inline">{{ 'Created for'|trans }}</dt> | ||||
| 				<dd>{{ entity.agent }}</dd> | ||||
|                  | ||||
| 				<dd>{{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}</dd> | ||||
|  | ||||
|                 <dt class="inline">{{ 'Asideactivity location'|trans }}</dt> | ||||
|                 {%- if entity.location.name is defined -%} | ||||
|                     <dd>{{ entity.location.name }}</dd> | ||||
|   | ||||
| @@ -72,21 +72,21 @@ days: jours | ||||
| 1 hour 30: 1 heure 30 | ||||
| 1 hour 45: 1 heure 45 | ||||
| 2 hours: 2 heures | ||||
| 2 hours 30: 2 heure 30 | ||||
| 2 hours 30: 2 heures 30 | ||||
| 3 hours: 3 heures | ||||
| 3 hours 30: 3 heure 30 | ||||
| 3 hours 30: 3 heures 30 | ||||
| 4 hours: 4 heures | ||||
| 4 hours 30: 4 heure 30 | ||||
| 4 hours 30: 4 heures 30 | ||||
| 5 hours: 5 heures | ||||
| 5 hours 30: 5 heure 30 | ||||
| 5 hours 30: 5 heures 30 | ||||
| 6 hours: 6 heures | ||||
| 6 hours 30: 6 heure 30 | ||||
| 6 hours 30: 6 heures 30 | ||||
| 7 hours: 7 heures | ||||
| 7 hours 30: 7 heure 30 | ||||
| 7 hours 30: 7 heures 30 | ||||
| 8 hours: 8 heures | ||||
| 8 hours 30: 8 heure 30 | ||||
| 8 hours 30: 8 heures 30 | ||||
| 9 hours: 9 heures | ||||
| 9 hours 30: 9 heure 30 | ||||
| 9 hours 30: 9 heures 30 | ||||
| 10 hours: 10 heures | ||||
| 1/2 day: 1/2 jour | ||||
| 1 day: 1 jour | ||||
|   | ||||
| @@ -440,6 +440,16 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente | ||||
|         return $this->startDate; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * get the date of the calendar. | ||||
|      * | ||||
|      * Useful for showing the date of the calendar event, required by twig in some places. | ||||
|      */ | ||||
|     public function getDate(): ?\DateTimeImmutable | ||||
|     { | ||||
|         return $this->getStartDate(); | ||||
|     } | ||||
|  | ||||
|     public function getStatus(): ?string | ||||
|     { | ||||
|         return $this->status; | ||||
|   | ||||
| @@ -0,0 +1,48 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\CalendarBundle\Menu; | ||||
|  | ||||
| use Chill\CalendarBundle\Security\Voter\CalendarVoter; | ||||
| use Chill\MainBundle\Routing\LocalMenuBuilderInterface; | ||||
| use Knp\Menu\MenuItem; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
|  | ||||
| final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface | ||||
| { | ||||
|     public function __construct(private Security $security) {} | ||||
|  | ||||
|     public static function getMenuIds(): array | ||||
|     { | ||||
|         return ['accompanying_course_quick_menu']; | ||||
|     } | ||||
|  | ||||
|     public function buildMenu($menuId, MenuItem $menu, array $parameters) | ||||
|     { | ||||
|         /** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */ | ||||
|         $accompanyingCourse = $parameters['accompanying-course']; | ||||
|  | ||||
|         if ($this->security->isGranted(CalendarVoter::CREATE, $accompanyingCourse)) { | ||||
|             $menu | ||||
|                 ->addChild('Create a new calendar in accompanying course', [ | ||||
|                     'route' => 'chill_calendar_calendar_new', | ||||
|                     'routeParameters' => [ | ||||
|                         'accompanying_period_id' => $accompanyingCourse->getId(), | ||||
|                     ], | ||||
|                 ]) | ||||
|                 ->setExtras([ | ||||
|                     'order' => 20, | ||||
|                     'icon' => 'plus', | ||||
|                 ]) | ||||
|             ; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1 +1,2 @@ | ||||
| import './scss/badge.scss'; | ||||
| import './scss/calendar-list.scss'; | ||||
|   | ||||
| @@ -0,0 +1,26 @@ | ||||
| ul.calendar-list { | ||||
|     list-style-type: none; | ||||
|     padding: 0; | ||||
|     & > li { | ||||
|         display: inline-block; | ||||
|     } | ||||
|     & > li:nth-child(n+2) { | ||||
|         margin-left: 0.25rem; | ||||
|     } | ||||
| } | ||||
|  | ||||
| div.calendar-list { | ||||
|  | ||||
|     ul.calendar-list { | ||||
|         display: inline-block; | ||||
|     } | ||||
|  | ||||
|     & > a.calendar-list__global { | ||||
|         display: inline-block;; | ||||
|         padding: 0.2rem; | ||||
|         min-width: 2rem; | ||||
|         border: 1px solid var(--bs-chill-blue); | ||||
|         border-radius: 0.25rem; | ||||
|         text-align: center; | ||||
|     } | ||||
| } | ||||
| @@ -55,7 +55,7 @@ | ||||
|                         <div class="item-col"> | ||||
|                             <ul class="list-content"> | ||||
|                                 {% if calendar.mainUser is not empty %} | ||||
|                                     <span class="badge-user">{{ calendar.mainUser|chill_entity_render_box }}</span> | ||||
|                                     <span class="badge-user">{{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }}</span> | ||||
|                                 {% endif %} | ||||
|                             </ul> | ||||
|                         </div> | ||||
| @@ -132,7 +132,7 @@ | ||||
|                                             <li class="cancel"> | ||||
|                                                 <span class="createdBy"> | ||||
|                                                      {{ 'Created by'|trans }} | ||||
|                                                     <b>{{ calendar.activity.createdBy|chill_entity_render_string }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }} | ||||
|                                                     <b>{{ calendar.activity.createdBy|chill_entity_render_string({'at_date': calendar.activity.createdAt}) }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }} | ||||
|                                                 </span> | ||||
|                                             </li> | ||||
|                                             {% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %} | ||||
|   | ||||
| @@ -89,7 +89,7 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn | ||||
|             switch ($attribute) { | ||||
|                 case self::SEE: | ||||
|                 case self::CREATE: | ||||
|                     if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) { | ||||
|                     if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep() || AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) { | ||||
|                         return false; | ||||
|                     } | ||||
|  | ||||
|   | ||||
| @@ -26,6 +26,7 @@ The calendar item has been successfully removed.: Le rendez-vous a été supprim | ||||
| From the day: Du | ||||
| to the day: au | ||||
| Transform to activity: Transformer en échange | ||||
| Create a new calendar in accompanying course: Créer un rendez-vous dans le parcours | ||||
| Will send SMS: Un SMS de rappel sera envoyé | ||||
| Will not send SMS: Aucun SMS de rappel ne sera envoyé | ||||
| SMS already sent: Un SMS a été envoyé | ||||
|   | ||||
| @@ -0,0 +1,65 @@ | ||||
| <?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\DocGeneratorBundle\Test; | ||||
|  | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
| use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; | ||||
| use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | ||||
|  | ||||
| /** | ||||
|  * @template T of object | ||||
|  */ | ||||
| abstract class DocGenNormalizerTestAbstract extends KernelTestCase | ||||
| { | ||||
|     public function testNullValueHasSameKeysAsNull(): void | ||||
|     { | ||||
|         $normalizedObject = $this->getNormalizer()->normalize($this->provideNotNullObject(), 'docgen', [ | ||||
|             AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(), | ||||
|         ]); | ||||
|         $nullNormalizedObject = $this->getNormalizer()->normalize(null, 'docgen', [ | ||||
|             AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(), | ||||
|         ]); | ||||
|  | ||||
|         self::assertEqualsCanonicalizing(array_keys($normalizedObject), array_keys($nullNormalizedObject)); | ||||
|         self::assertArrayHasKey('isNull', $nullNormalizedObject, 'each object must have an "isNull" key'); | ||||
|         self::assertTrue($nullNormalizedObject['isNull'], 'isNull key must be true for null objects'); | ||||
|         self::assertFalse($normalizedObject['isNull'], 'isNull key must be false for null objects'); | ||||
|  | ||||
|         foreach ($normalizedObject as $key => $value) { | ||||
|             if (in_array($key, ['isNull', 'type'])) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             if (is_array($value)) { | ||||
|                 if (array_is_list($value)) { | ||||
|                     self::assertEquals([], $nullNormalizedObject[$key], "list must be serialized as an empty array, in {$key}"); | ||||
|                 } else { | ||||
|                     self::assertEqualsCanonicalizing(array_keys($value), array_keys($nullNormalizedObject[$key]), "sub-object must have the same keys, in {$key}"); | ||||
|                 } | ||||
|             } elseif (is_string($value)) { | ||||
|                 self::assertEquals('', $nullNormalizedObject[$key], 'strings must be '); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return T | ||||
|      */ | ||||
|     abstract public function provideNotNullObject(): object; | ||||
|  | ||||
|     /** | ||||
|      * @return class-string<T> | ||||
|      */ | ||||
|     abstract public function provideDocGenExpectClass(): string; | ||||
|  | ||||
|     abstract public function getNormalizer(): NormalizerInterface; | ||||
| } | ||||
| @@ -0,0 +1,46 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Controller; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage; | ||||
| use Chill\DocStoreBundle\Service\Signature\PDFPage; | ||||
| use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\Messenger\MessageBusInterface; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
|  | ||||
| class SignatureRequestController | ||||
| { | ||||
|     public function __construct( | ||||
|         private MessageBusInterface $messageBus, | ||||
|         private StoredObjectManagerInterface $storedObjectManager, | ||||
|     ) {} | ||||
|  | ||||
|     #[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')] | ||||
|     public function processSignature(StoredObject $storedObject): Response | ||||
|     { | ||||
|         $content = $this->storedObjectManager->read($storedObject); | ||||
|  | ||||
|         $this->messageBus->dispatch(new RequestPdfSignMessage( | ||||
|             0, | ||||
|             new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)), | ||||
|             0, | ||||
|             'test signature', | ||||
|             'Mme Caroline Diallo', | ||||
|             $content | ||||
|         )); | ||||
|  | ||||
|         return new Response('<html><head><title>test</title></head><body><p>ok</p></body></html>'); | ||||
|     } | ||||
| } | ||||
| @@ -313,4 +313,19 @@ class StoredObject implements Document, TrackCreationInterface | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function saveHistory(): void | ||||
|     { | ||||
|         if ('' === $this->getFilename()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $this->datas['history'][] = [ | ||||
|             'filename' => $this->getFilename(), | ||||
|             'iv' => $this->getIv(), | ||||
|             'key_infos' => $this->getKeyInfos(), | ||||
|             'type' => $this->getType(), | ||||
|             'before' => (new \DateTimeImmutable('now'))->getTimestamp(), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -57,8 +57,8 @@ class StoredObjectDataMapper implements DataMapperInterface | ||||
|  | ||||
|         /** @var StoredObject $viewData */ | ||||
|         if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) { | ||||
|             // we do not want to erase the previous object | ||||
|             $viewData = new StoredObject(); | ||||
|             // we want to keep the previous history | ||||
|             $viewData->saveHistory(); | ||||
|         } | ||||
|  | ||||
|         $viewData->setFilename($forms['stored_object']->getData()['filename']); | ||||
|   | ||||
| @@ -4,13 +4,13 @@ | ||||
|       Actions | ||||
|     </button> | ||||
|     <ul class="dropdown-menu"> | ||||
|       <li v-if="props.canEdit && is_extension_editable(props.storedObject.type)"> | ||||
|       <li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.storedObject.status !== 'stored_object_created'"> | ||||
|         <wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button> | ||||
|       </li> | ||||
|       <li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.davLink !== undefined && props.davLinkExpiration !== undefined"> | ||||
|         <desktop-edit-button :classes="{'dropdown-item': true}" :edit-link="props.davLink" :expiration-link="props.davLinkExpiration"></desktop-edit-button> | ||||
|       </li> | ||||
|       <li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf"> | ||||
|       <li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf && props.storedObject.status !== 'stored_object_created'"> | ||||
|         <convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button> | ||||
|       </li> | ||||
|       <li v-if="props.canDownload"> | ||||
|   | ||||
| @@ -13,7 +13,7 @@ import {reactive} from "vue"; | ||||
| import {StoredObject, StoredObjectCreated} from "../../types"; | ||||
|  | ||||
| interface ConvertButtonConfig { | ||||
|   storedObject: StoredObject|StoredObjectCreated, | ||||
|   storedObject: StoredObject, | ||||
|   classes: { [key: string]: boolean}, | ||||
|   filename?: string, | ||||
| }; | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import {build_wopi_editor_link} from "./helpers"; | ||||
| import {StoredObject, StoredObjectCreated, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types"; | ||||
|  | ||||
| interface WopiEditButtonConfig { | ||||
|   storedObject: StoredObject|StoredObjectCreated, | ||||
|   storedObject: StoredObject, | ||||
|   returnPath?: string, | ||||
|   classes: {[k: string] : boolean}, | ||||
|   executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction, | ||||
|   | ||||
| @@ -0,0 +1,23 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner; | ||||
|  | ||||
| /** | ||||
|  * Message which is received when a pdf is signed. | ||||
|  */ | ||||
| final readonly class PdfSignedMessage | ||||
| { | ||||
|     public function __construct( | ||||
|         public readonly int $signatureId, | ||||
|         public readonly string $content | ||||
|     ) {} | ||||
| } | ||||
| @@ -0,0 +1,32 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner; | ||||
|  | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | ||||
|  | ||||
| final readonly class PdfSignedMessageHandler implements MessageHandlerInterface | ||||
| { | ||||
|     /** | ||||
|      * log prefix. | ||||
|      */ | ||||
|     private const P = '[pdf signed message] '; | ||||
|  | ||||
|     public function __construct( | ||||
|         private LoggerInterface $logger, | ||||
|     ) {} | ||||
|  | ||||
|     public function __invoke(PdfSignedMessage $message): void | ||||
|     { | ||||
|         $this->logger->info(self::P.'a message is received', ['signaturedId' => $message->signatureId]); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,66 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner; | ||||
|  | ||||
| use Symfony\Component\Messenger\Envelope; | ||||
| use Symfony\Component\Messenger\Exception\MessageDecodingFailedException; | ||||
| use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; | ||||
|  | ||||
| /** | ||||
|  * Decode (and requeue) @see{PdfSignedMessage}, which comes from an external producer. | ||||
|  */ | ||||
| final readonly class PdfSignedMessageSerializer implements SerializerInterface | ||||
| { | ||||
|     public function decode(array $encodedEnvelope): Envelope | ||||
|     { | ||||
|         $body = $encodedEnvelope['body']; | ||||
|  | ||||
|         try { | ||||
|             $decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR); | ||||
|         } catch (\JsonException $e) { | ||||
|             throw new MessageDecodingFailedException('Could not deserialize message', previous: $e); | ||||
|         } | ||||
|  | ||||
|         if (!array_key_exists('signatureId', $decoded) || !array_key_exists('content', $decoded)) { | ||||
|             throw new MessageDecodingFailedException('Could not find expected keys: signatureId or content'); | ||||
|         } | ||||
|  | ||||
|         $content = base64_decode($decoded['content'], true); | ||||
|  | ||||
|         if (false === $content) { | ||||
|             throw new MessageDecodingFailedException('Invalid character found in the base64 encoded content'); | ||||
|         } | ||||
|  | ||||
|         $message = new PdfSignedMessage($decoded['signatureId'], $content); | ||||
|  | ||||
|         return new Envelope($message); | ||||
|     } | ||||
|  | ||||
|     public function encode(Envelope $envelope): array | ||||
|     { | ||||
|         $message = $envelope->getMessage(); | ||||
|  | ||||
|         if (!$message instanceof PdfSignedMessage) { | ||||
|             throw new MessageDecodingFailedException('Expected a PdfSignedMessage'); | ||||
|         } | ||||
|  | ||||
|         $data = [ | ||||
|             'signatureId' => $message->signatureId, | ||||
|             'content' => base64_encode($message->content), | ||||
|         ]; | ||||
|  | ||||
|         return [ | ||||
|             'body' => json_encode($data, JSON_THROW_ON_ERROR), | ||||
|             'headers' => [], | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,29 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner; | ||||
|  | ||||
| use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone; | ||||
|  | ||||
| /** | ||||
|  * Message which is sent when we request a signature on a pdf. | ||||
|  */ | ||||
| final readonly class RequestPdfSignMessage | ||||
| { | ||||
|     public function __construct( | ||||
|         public int $signatureId, | ||||
|         public PDFSignatureZone $PDFSignatureZone, | ||||
|         public int $signatureZoneIndex, | ||||
|         public string $reason, | ||||
|         public string $signerText, | ||||
|         public string $content, | ||||
|     ) {} | ||||
| } | ||||
| @@ -0,0 +1,105 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner; | ||||
|  | ||||
| use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone; | ||||
| use Symfony\Component\Messenger\Envelope; | ||||
| use Symfony\Component\Messenger\Exception\MessageDecodingFailedException; | ||||
| use Symfony\Component\Messenger\Stamp\NonSendableStampInterface; | ||||
| use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; | ||||
| use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; | ||||
| use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | ||||
| use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | ||||
|  | ||||
| /** | ||||
|  * Serialize a RequestPdfSignMessage, for external consumer. | ||||
|  */ | ||||
| final readonly class RequestPdfSignMessageSerializer implements SerializerInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private NormalizerInterface $normalizer, | ||||
|         private DenormalizerInterface $denormalizer, | ||||
|     ) {} | ||||
|  | ||||
|     public function decode(array $encodedEnvelope): Envelope | ||||
|     { | ||||
|         $body = $encodedEnvelope['body']; | ||||
|         $headers = $encodedEnvelope['headers']; | ||||
|  | ||||
|         if (RequestPdfSignMessage::class !== ($headers['Message'] ?? null)) { | ||||
|             throw new MessageDecodingFailedException('serializer does not support this message'); | ||||
|         } | ||||
|  | ||||
|         $data = json_decode($body, true); | ||||
|  | ||||
|         $zoneSignature = $this->denormalizer->denormalize($data['signatureZone'], PDFSignatureZone::class, 'json', [ | ||||
|             AbstractNormalizer::GROUPS => ['write'], | ||||
|         ]); | ||||
|  | ||||
|         $content = base64_decode($data['content'], true); | ||||
|  | ||||
|         if (false === $content) { | ||||
|             throw new MessageDecodingFailedException('the content could not be converted from base64 encoding'); | ||||
|         } | ||||
|  | ||||
|         $message = new RequestPdfSignMessage( | ||||
|             $data['signatureId'], | ||||
|             $zoneSignature, | ||||
|             $data['signatureZoneIndex'], | ||||
|             $data['reason'], | ||||
|             $data['signerText'], | ||||
|             $content, | ||||
|         ); | ||||
|  | ||||
|         // in case of redelivery, unserialize any stamps | ||||
|         $stamps = []; | ||||
|         if (isset($headers['stamps'])) { | ||||
|             $stamps = unserialize($headers['stamps']); | ||||
|         } | ||||
|  | ||||
|         return new Envelope($message, $stamps); | ||||
|     } | ||||
|  | ||||
|     public function encode(Envelope $envelope): array | ||||
|     { | ||||
|         $message = $envelope->getMessage(); | ||||
|  | ||||
|         if (!$message instanceof RequestPdfSignMessage) { | ||||
|             throw new MessageDecodingFailedException('Message is not a RequestPdfSignMessage'); | ||||
|         } | ||||
|  | ||||
|         $data = [ | ||||
|             'signatureId' => $message->signatureId, | ||||
|             'signatureZoneIndex' => $message->signatureZoneIndex, | ||||
|             'signatureZone' => $this->normalizer->normalize($message->PDFSignatureZone, 'json', [AbstractNormalizer::GROUPS => ['read']]), | ||||
|             'reason' => $message->reason, | ||||
|             'signerText' => $message->signerText, | ||||
|             'content' => base64_encode($message->content), | ||||
|         ]; | ||||
|  | ||||
|         $allStamps = []; | ||||
|         foreach ($envelope->all() as $stamp) { | ||||
|             if ($stamp instanceof NonSendableStampInterface) { | ||||
|                 continue; | ||||
|             } | ||||
|             $allStamps = [...$allStamps, ...$stamp]; | ||||
|         } | ||||
|  | ||||
|         return [ | ||||
|             'body' => json_encode($data, JSON_THROW_ON_ERROR, 512), | ||||
|             'headers' => [ | ||||
|                 'stamps' => serialize($allStamps), | ||||
|                 'Message' => RequestPdfSignMessage::class, | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,53 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Tests\Entity; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class StoredObjectTest extends KernelTestCase | ||||
| { | ||||
|     public function testSaveHistory(): void | ||||
|     { | ||||
|         $storedObject = new StoredObject(); | ||||
|         $storedObject | ||||
|             ->setFilename('test_0') | ||||
|             ->setIv([2, 4, 6, 8]) | ||||
|             ->setKeyInfos(['key' => ['data0' => 'data0']]) | ||||
|             ->setType('text/html'); | ||||
|  | ||||
|         $storedObject->saveHistory(); | ||||
|  | ||||
|         $storedObject | ||||
|             ->setFilename('test_1') | ||||
|             ->setIv([8, 10, 12]) | ||||
|             ->setKeyInfos(['key' => ['data1' => 'data1']]) | ||||
|             ->setType('text/text'); | ||||
|  | ||||
|         $storedObject->saveHistory(); | ||||
|  | ||||
|         self::assertEquals('test_0', $storedObject->getDatas()['history'][0]['filename']); | ||||
|         self::assertEquals([2, 4, 6, 8], $storedObject->getDatas()['history'][0]['iv']); | ||||
|         self::assertEquals(['key' => ['data0' => 'data0']], $storedObject->getDatas()['history'][0]['key_infos']); | ||||
|         self::assertEquals('text/html', $storedObject->getDatas()['history'][0]['type']); | ||||
|  | ||||
|         self::assertEquals('test_1', $storedObject->getDatas()['history'][1]['filename']); | ||||
|         self::assertEquals([8, 10, 12], $storedObject->getDatas()['history'][1]['iv']); | ||||
|         self::assertEquals(['key' => ['data1' => 'data1']], $storedObject->getDatas()['history'][1]['key_infos']); | ||||
|         self::assertEquals('text/text', $storedObject->getDatas()['history'][1]['type']); | ||||
|     } | ||||
| } | ||||
| @@ -56,14 +56,14 @@ class StoredObjectTypeTest extends TypeTestCase | ||||
|             {"filename":"abcdef","iv":[10, 15, 20, 30],"keyInfos":[],"type":"text/html","status":"object_store_created"} | ||||
|             JSON]; | ||||
|         $model = new StoredObject(); | ||||
|         $originalObjectId = spl_object_id($model); | ||||
|         $originalObjectId = spl_object_hash($model); | ||||
|         $form = $this->factory->create(StoredObjectType::class, $model, ['has_title' => true]); | ||||
|  | ||||
|         $form->submit($formData); | ||||
|  | ||||
|         $this->assertTrue($form->isSynchronized()); | ||||
|         $model = $form->getData(); | ||||
|         $this->assertNotEquals($originalObjectId, spl_object_hash($model)); | ||||
|         $this->assertEquals($originalObjectId, spl_object_hash($model)); | ||||
|         $this->assertEquals('abcdef', $model->getFilename()); | ||||
|         $this->assertEquals([10, 15, 20, 30], $model->getIv()); | ||||
|         $this->assertEquals('text/html', $model->getType()); | ||||
|   | ||||
| @@ -0,0 +1,63 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner; | ||||
|  | ||||
| use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessage; | ||||
| use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessageSerializer; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Symfony\Component\Messenger\Envelope; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class PdfSignedMessageSerializerTest extends TestCase | ||||
| { | ||||
|     public function testDecode(): void | ||||
|     { | ||||
|         $asString = <<<'JSON' | ||||
|             {"signatureId": 0, "content": "dGVzdAo="} | ||||
|             JSON; | ||||
|  | ||||
|         $actual = $this->buildSerializer()->decode(['body' => $asString]); | ||||
|  | ||||
|         self::assertInstanceOf(Envelope::class, $actual); | ||||
|         $message = $actual->getMessage(); | ||||
|         self::assertInstanceOf(PdfSignedMessage::class, $message); | ||||
|         self::assertEquals("test\n", $message->content); | ||||
|         self::assertEquals(0, $message->signatureId); | ||||
|     } | ||||
|  | ||||
|     public function testEncode(): void | ||||
|     { | ||||
|         $envelope = new Envelope( | ||||
|             new PdfSignedMessage(0, "test\n") | ||||
|         ); | ||||
|  | ||||
|         $actual = $this->buildSerializer()->encode($envelope); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|         self::assertArrayHasKey('body', $actual); | ||||
|         self::assertArrayHasKey('headers', $actual); | ||||
|         self::assertEquals([], $actual['headers']); | ||||
|  | ||||
|         self::assertEquals(<<<'JSON' | ||||
|                 {"signatureId":0,"content":"dGVzdAo="} | ||||
|                 JSON, $actual['body']); | ||||
|     } | ||||
|  | ||||
|     private function buildSerializer(): PdfSignedMessageSerializer | ||||
|     { | ||||
|         return new PdfSignedMessageSerializer(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,137 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner; | ||||
|  | ||||
| use Chill\DocStoreBundle\Service\Signature\PDFPage; | ||||
| use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone; | ||||
| use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage; | ||||
| use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessageSerializer; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Symfony\Component\Messenger\Envelope; | ||||
| use Symfony\Component\Serializer\Exception\UnexpectedValueException; | ||||
| use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | ||||
| use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | ||||
| use Symfony\Component\Serializer\Serializer; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class RequestPdfSignMessageSerializerTest extends TestCase | ||||
| { | ||||
|     public function testEncode(): void | ||||
|     { | ||||
|         $serializer = $this->buildSerializer(); | ||||
|  | ||||
|         $envelope = new Envelope( | ||||
|             $request = new RequestPdfSignMessage( | ||||
|                 0, | ||||
|                 new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)), | ||||
|                 0, | ||||
|                 'metadata to add to the signature', | ||||
|                 'Mme Caroline Diallo', | ||||
|                 'abc' | ||||
|             ), | ||||
|         ); | ||||
|  | ||||
|         $actual = $serializer->encode($envelope); | ||||
|         $expectedBody = json_encode([ | ||||
|             'signatureId' => $request->signatureId, | ||||
|             'signatureZoneIndex' => $request->signatureZoneIndex, | ||||
|             'signatureZone' => ['x' => 10.0], | ||||
|             'reason' => $request->reason, | ||||
|             'signerText' => $request->signerText, | ||||
|             'content' => base64_encode($request->content), | ||||
|         ]); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|         self::assertArrayHasKey('body', $actual); | ||||
|         self::assertArrayHasKey('headers', $actual); | ||||
|         self::assertEquals($expectedBody, $actual['body']); | ||||
|     } | ||||
|  | ||||
|     public function testDecode(): void | ||||
|     { | ||||
|         $serializer = $this->buildSerializer(); | ||||
|  | ||||
|         $request = new RequestPdfSignMessage( | ||||
|             0, | ||||
|             new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)), | ||||
|             0, | ||||
|             'metadata to add to the signature', | ||||
|             'Mme Caroline Diallo', | ||||
|             'abc' | ||||
|         ); | ||||
|  | ||||
|         $bodyAsString = json_encode([ | ||||
|             'signatureId' => $request->signatureId, | ||||
|             'signatureZoneIndex' => $request->signatureZoneIndex, | ||||
|             'signatureZone' => ['x' => 10.0], | ||||
|             'reason' => $request->reason, | ||||
|             'signerText' => $request->signerText, | ||||
|             'content' => base64_encode($request->content), | ||||
|         ], JSON_THROW_ON_ERROR); | ||||
|  | ||||
|         $actual = $serializer->decode([ | ||||
|             'body' => $bodyAsString, | ||||
|             'headers' => [ | ||||
|                 'Message' => RequestPdfSignMessage::class, | ||||
|             ], | ||||
|         ]); | ||||
|  | ||||
|         self::assertInstanceOf(RequestPdfSignMessage::class, $actual->getMessage()); | ||||
|         self::assertEquals($request->signatureId, $actual->getMessage()->signatureId); | ||||
|         self::assertEquals($request->signatureZoneIndex, $actual->getMessage()->signatureZoneIndex); | ||||
|         self::assertEquals($request->reason, $actual->getMessage()->reason); | ||||
|         self::assertEquals($request->signerText, $actual->getMessage()->signerText); | ||||
|         self::assertEquals($request->content, $actual->getMessage()->content); | ||||
|         self::assertNotNull($actual->getMessage()->PDFSignatureZone); | ||||
|     } | ||||
|  | ||||
|     private function buildSerializer(): RequestPdfSignMessageSerializer | ||||
|     { | ||||
|         $normalizer = | ||||
|             new class () implements NormalizerInterface { | ||||
|                 public function normalize($object, ?string $format = null, array $context = []): array | ||||
|                 { | ||||
|                     if (!$object instanceof PDFSignatureZone) { | ||||
|                         throw new UnexpectedValueException('expected RequestPdfSignMessage'); | ||||
|                     } | ||||
|  | ||||
|                     return [ | ||||
|                         'x' => $object->x, | ||||
|                     ]; | ||||
|                 } | ||||
|  | ||||
|                 public function supportsNormalization($data, ?string $format = null): bool | ||||
|                 { | ||||
|                     return $data instanceof PDFSignatureZone; | ||||
|                 } | ||||
|             }; | ||||
|         $denormalizer = new class () implements DenormalizerInterface { | ||||
|             public function denormalize($data, string $type, ?string $format = null, array $context = []) | ||||
|             { | ||||
|                 return new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)); | ||||
|             } | ||||
|  | ||||
|             public function supportsDenormalization($data, string $type, ?string $format = null) | ||||
|             { | ||||
|                 return PDFSignatureZone::class === $type; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         $serializer = new Serializer([$normalizer, $denormalizer]); | ||||
|  | ||||
|         return new RequestPdfSignMessageSerializer($serializer, $serializer); | ||||
|     } | ||||
| } | ||||
| @@ -121,9 +121,4 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler | ||||
|     { | ||||
|         return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass(); | ||||
|     } | ||||
|  | ||||
|     public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool | ||||
|     { | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -632,7 +632,7 @@ class ExportController extends AbstractController | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private function rebuildRawData(string $key): array | ||||
|     private function rebuildRawData(?string $key): array | ||||
|     { | ||||
|         if (null === $key) { | ||||
|             throw $this->createNotFoundException('key does not exists'); | ||||
|   | ||||
| @@ -61,8 +61,6 @@ final class PermissionsGroupController extends AbstractController | ||||
|         $form = $this->createAddRoleScopeForm($permissionsGroup); | ||||
|         $form->handleRequest($request); | ||||
|  | ||||
|         dump($form->isSubmitted()); | ||||
|  | ||||
|         if ($form->isSubmitted() && $form->isValid()) { | ||||
|             $roleScope = $this->getPersistentRoleScopeBy( | ||||
|                 $form['composed_role_scope']->getData()->getRole(), | ||||
|   | ||||
| @@ -13,15 +13,16 @@ namespace Chill\MainBundle\Controller; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; | ||||
| use Chill\MainBundle\Form\EntityWorkflowCommentType; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; | ||||
| use Chill\MainBundle\Form\WorkflowSignatureMetadataType; | ||||
| use Chill\MainBundle\Form\WorkflowStepType; | ||||
| use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; | ||||
| use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter; | ||||
| use Chill\MainBundle\Security\ChillSecurity; | ||||
| use Chill\MainBundle\Workflow\EntityWorkflowManager; | ||||
| use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||||
| use Symfony\Component\Form\Extension\Core\Type\FormType; | ||||
| @@ -276,10 +277,11 @@ class WorkflowController extends AbstractController | ||||
|         $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); | ||||
|         $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); | ||||
|         $errors = []; | ||||
|         $signatures = $entityWorkflow->getCurrentStep()->getSignatures(); | ||||
|  | ||||
|         if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) { | ||||
|             // possible transition | ||||
|  | ||||
|             $stepDTO = new WorkflowTransitionContextDTO($entityWorkflow); | ||||
|             $usersInvolved = $entityWorkflow->getUsersInvolved(); | ||||
|             $currentUserFound = array_search($this->security->getUser(), $usersInvolved, true); | ||||
|  | ||||
| @@ -289,9 +291,8 @@ class WorkflowController extends AbstractController | ||||
|  | ||||
|             $transitionForm = $this->createForm( | ||||
|                 WorkflowStepType::class, | ||||
|                 $entityWorkflow->getCurrentStep(), | ||||
|                 $stepDTO, | ||||
|                 [ | ||||
|                     'transition' => true, | ||||
|                     'entity_workflow' => $entityWorkflow, | ||||
|                     'suggested_users' => $usersInvolved, | ||||
|                 ] | ||||
| @@ -310,12 +311,7 @@ class WorkflowController extends AbstractController | ||||
|                     throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs))); | ||||
|                 } | ||||
|  | ||||
|                 // TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context) | ||||
|                 $entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? []; | ||||
|                 $entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? []; | ||||
|                 $entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? []; | ||||
|  | ||||
|                 $workflow->apply($entityWorkflow, $transition); | ||||
|                 $workflow->apply($entityWorkflow, $transition, ['context' => $stepDTO]); | ||||
|  | ||||
|                 $this->entityManager->flush(); | ||||
|  | ||||
| @@ -327,22 +323,6 @@ class WorkflowController extends AbstractController | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /* | ||||
|         $commentForm = $this->createForm(EntityWorkflowCommentType::class, $newComment = new EntityWorkflowComment()); | ||||
|         $commentForm->handleRequest($request); | ||||
|  | ||||
|         if ($commentForm->isSubmitted() && $commentForm->isValid()) { | ||||
|             $this->entityManager->persist($newComment); | ||||
|             $this->entityManager->flush(); | ||||
|  | ||||
|             $this->addFlash('success', $this->translator->trans('workflow.Comment added')); | ||||
|  | ||||
|             return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]); | ||||
|         } elseif ($commentForm->isSubmitted() && !$commentForm->isValid()) { | ||||
|             $this->addFlash('error', $this->translator->trans('This form contains errors')); | ||||
|         } | ||||
|          */ | ||||
|  | ||||
|         return $this->render( | ||||
|             '@ChillMain/Workflow/index.html.twig', | ||||
|             [ | ||||
| @@ -352,7 +332,7 @@ class WorkflowController extends AbstractController | ||||
|                 'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null, | ||||
|                 'entity_workflow' => $entityWorkflow, | ||||
|                 'transition_form_errors' => $errors, | ||||
|                 // 'comment_form' => $commentForm->createView(), | ||||
|                 'signatures' => $signatures, | ||||
|             ] | ||||
|         ); | ||||
|     } | ||||
| @@ -371,4 +351,47 @@ class WorkflowController extends AbstractController | ||||
|  | ||||
|         return $lines; | ||||
|     } | ||||
|  | ||||
|     #[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/metadata', name: 'chill_main_workflow_signature_metadata')] | ||||
|     public function addSignatureMetadata(int $signature_id, Request $request): Response | ||||
|     { | ||||
|         $signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id); | ||||
|  | ||||
|         if ($signature->getSigner() instanceof User) { | ||||
|             return $this->redirectToRoute('signature_route_user'); | ||||
|         } | ||||
|  | ||||
|         $metadataForm = $this->createForm(WorkflowSignatureMetadataType::class); | ||||
|         $metadataForm->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]); | ||||
|  | ||||
|         $metadataForm->handleRequest($request); | ||||
|  | ||||
|         if ($metadataForm->isSubmitted() && $metadataForm->isValid()) { | ||||
|             $data = $metadataForm->getData(); | ||||
|  | ||||
|             $signature->setSignatureMetadata( | ||||
|                 [ | ||||
|                     'base_signer' => [ | ||||
|                         'document_type' => $data['documentType'], | ||||
|                         'document_number' => $data['documentNumber'], | ||||
|                         'expiration_date' => $data['expirationDate'], | ||||
|                     ], | ||||
|                 ] | ||||
|             ); | ||||
|  | ||||
|             $this->entityManager->persist($signature); | ||||
|             $this->entityManager->flush(); | ||||
|  | ||||
|             // Todo should redirect to document for actual signing? To be adjusted still | ||||
|             return $this->redirectToRoute('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()]); | ||||
|         } | ||||
|  | ||||
|         return $this->render( | ||||
|             '@ChillMain/Workflow/_signature_metadata.html.twig', | ||||
|             [ | ||||
|                 'metadata_form' => $metadataForm->createView(), | ||||
|                 'person' => $signature->getSigner(), | ||||
|             ] | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -193,6 +193,11 @@ class ChillMainExtension extends Extension implements | ||||
|                 [] | ||||
|         ); | ||||
|  | ||||
|         $container->setParameter( | ||||
|             'chill_main.workflow_signatures.base_signer.document_kinds', | ||||
|             $config['workflow_signature']['base_signer']['document_kinds'] | ||||
|         ); | ||||
|  | ||||
|         $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); | ||||
|         $loader->load('services.yaml'); | ||||
|         $loader->load('services/doctrine.yaml'); | ||||
|   | ||||
| @@ -277,6 +277,32 @@ class Configuration implements ConfigurationInterface | ||||
|             ->end() // end of root | ||||
|         ; | ||||
|  | ||||
|         $rootNode->children() | ||||
|             ->arrayNode('workflow_signature') | ||||
|             ->children() | ||||
|             ->arrayNode('base_signer') | ||||
|             ->children() | ||||
|             ->arrayNode('document_kinds') | ||||
|             ->arrayPrototype() | ||||
|             ->children() | ||||
|             ->scalarNode('key')->cannotBeEmpty()->end() | ||||
|             ->arrayNode('labels') | ||||
|             ->arrayPrototype() | ||||
|             ->children() | ||||
|             ->scalarNode('lang')->cannotBeEmpty()->end() | ||||
|             ->scalarNode('label')->cannotBeEmpty()->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end(); | ||||
|  | ||||
|         $rootNode->children() | ||||
|             ->arrayNode('add_address')->addDefaultsIfNotSet()->children() | ||||
|             ->scalarNode('default_country')->cannotBeEmpty()->defaultValue('BE')->end() | ||||
|   | ||||
| @@ -216,13 +216,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter | ||||
|         return $this->mainLocation; | ||||
|     } | ||||
|  | ||||
|     public function getMainScope(?\DateTimeImmutable $at = null): ?Scope | ||||
|     public function getMainScope(?\DateTimeImmutable $atDate = null): ?Scope | ||||
|     { | ||||
|         $at ??= new \DateTimeImmutable('now'); | ||||
|         $atDate ??= new \DateTimeImmutable('now'); | ||||
|  | ||||
|         foreach ($this->scopeHistories as $scopeHistory) { | ||||
|             if ($at >= $scopeHistory->getStartDate() && ( | ||||
|                 null === $scopeHistory->getEndDate() || $at < $scopeHistory->getEndDate() | ||||
|             if ($atDate >= $scopeHistory->getStartDate() && ( | ||||
|                 null === $scopeHistory->getEndDate() || $atDate < $scopeHistory->getEndDate() | ||||
|             )) { | ||||
|                 return $scopeHistory->getScope(); | ||||
|             } | ||||
| @@ -265,13 +265,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter | ||||
|         return $this->salt; | ||||
|     } | ||||
|  | ||||
|     public function getUserJob(?\DateTimeImmutable $at = null): ?UserJob | ||||
|     public function getUserJob(?\DateTimeImmutable $atDate = null): ?UserJob | ||||
|     { | ||||
|         $at ??= new \DateTimeImmutable('now'); | ||||
|         $atDate ??= new \DateTimeImmutable('now'); | ||||
|  | ||||
|         foreach ($this->jobHistories as $jobHistory) { | ||||
|             if ($at >= $jobHistory->getStartDate() && ( | ||||
|                 null === $jobHistory->getEndDate() || $at < $jobHistory->getEndDate() | ||||
|             if ($atDate >= $jobHistory->getStartDate() && ( | ||||
|                 null === $jobHistory->getEndDate() || $atDate < $jobHistory->getEndDate() | ||||
|             )) { | ||||
|                 return $jobHistory->getJob(); | ||||
|             } | ||||
| @@ -285,6 +285,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter | ||||
|         return $this->jobHistories; | ||||
|     } | ||||
|  | ||||
|     public function getUserScopeHistories(): Collection | ||||
|     { | ||||
|         return $this->scopeHistories; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return ArrayCollection|UserJobHistory[] | ||||
|      */ | ||||
|   | ||||
| @@ -17,9 +17,9 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; | ||||
| use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation; | ||||
| use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; | ||||
| use Doctrine\Common\Collections\ArrayCollection; | ||||
| use Doctrine\Common\Collections\Collection; | ||||
| use Doctrine\Common\Collections\Order; | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
| use Symfony\Component\Serializer\Annotation as Serializer; | ||||
| use Symfony\Component\Validator\Constraints as Assert; | ||||
| @@ -34,35 +34,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface | ||||
|  | ||||
|     use TrackUpdateTrait; | ||||
|  | ||||
|     /** | ||||
|      * a list of future cc users for the next steps. | ||||
|      * | ||||
|      * @var array|User[] | ||||
|      */ | ||||
|     public array $futureCcUsers = []; | ||||
|  | ||||
|     /** | ||||
|      * a list of future dest emails for the next steps. | ||||
|      * | ||||
|      * This is in used in order to let controller inform who will be the future emails which will validate | ||||
|      * the next step. This is necessary to perform some computation about the next emails, before they are | ||||
|      * associated to the entity EntityWorkflowStep. | ||||
|      * | ||||
|      * @var array|string[] | ||||
|      */ | ||||
|     public array $futureDestEmails = []; | ||||
|  | ||||
|     /** | ||||
|      * a list of future dest users for the next steps. | ||||
|      * | ||||
|      * This is in used in order to let controller inform who will be the future users which will validate | ||||
|      * the next step. This is necessary to perform some computation about the next users, before they are | ||||
|      * associated to the entity EntityWorkflowStep. | ||||
|      * | ||||
|      * @var array|User[] | ||||
|      */ | ||||
|     public array $futureDestUsers = []; | ||||
|  | ||||
|     /** | ||||
|      * @var Collection<EntityWorkflowComment> | ||||
|      */ | ||||
| @@ -442,11 +413,23 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface | ||||
|      * | ||||
|      * @return $this | ||||
|      */ | ||||
|     public function setStep(string $step): self | ||||
|     public function setStep(string $step, WorkflowTransitionContextDTO $transitionContextDTO): self | ||||
|     { | ||||
|         $newStep = new EntityWorkflowStep(); | ||||
|         $newStep->setCurrentStep($step); | ||||
|  | ||||
|         foreach ($transitionContextDTO->futureCcUsers as $user) { | ||||
|             $newStep->addCcUser($user); | ||||
|         } | ||||
|  | ||||
|         foreach ($transitionContextDTO->futureDestUsers as $user) { | ||||
|             $newStep->addDestUser($user); | ||||
|         } | ||||
|  | ||||
|         foreach ($transitionContextDTO->futureDestEmails as $email) { | ||||
|             $newStep->addDestEmail($email); | ||||
|         } | ||||
|  | ||||
|         // copy the freeze | ||||
|         if ($this->isFreeze()) { | ||||
|             $newStep->setFreezeAfter(true); | ||||
|   | ||||
| @@ -0,0 +1,20 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Entity\Workflow; | ||||
|  | ||||
| enum EntityWorkflowSignatureStateEnum: string | ||||
| { | ||||
|     case PENDING = 'pending'; | ||||
|     case SIGNED = 'signed'; | ||||
|     case REJECTED = 'rejected'; | ||||
|     case CANCELED = 'canceled'; | ||||
| } | ||||
| @@ -42,19 +42,25 @@ class EntityWorkflowStep | ||||
|     private array $destEmail = []; | ||||
|  | ||||
|     /** | ||||
|      * @var Collection<User> | ||||
|      * @var Collection<int, User> | ||||
|      */ | ||||
|     #[ORM\ManyToMany(targetEntity: User::class)] | ||||
|     #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')] | ||||
|     private Collection $destUser; | ||||
|  | ||||
|     /** | ||||
|      * @var Collection<User> | ||||
|      * @var Collection<int, User> | ||||
|      */ | ||||
|     #[ORM\ManyToMany(targetEntity: User::class)] | ||||
|     #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')] | ||||
|     private Collection $destUserByAccessKey; | ||||
|  | ||||
|     /** | ||||
|      * @var Collection <int, EntityWorkflowStepSignature> | ||||
|      */ | ||||
|     #[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)] | ||||
|     private Collection $signatures; | ||||
|  | ||||
|     #[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')] | ||||
|     private ?EntityWorkflow $entityWorkflow = null; | ||||
|  | ||||
| @@ -97,6 +103,7 @@ class EntityWorkflowStep | ||||
|         $this->ccUser = new ArrayCollection(); | ||||
|         $this->destUser = new ArrayCollection(); | ||||
|         $this->destUserByAccessKey = new ArrayCollection(); | ||||
|         $this->signatures = new ArrayCollection(); | ||||
|         $this->accessKey = bin2hex(openssl_random_pseudo_bytes(32)); | ||||
|     } | ||||
|  | ||||
| @@ -136,6 +143,29 @@ class EntityWorkflowStep | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @internal use @see{EntityWorkflowStepSignature}'s constructor instead | ||||
|      */ | ||||
|     public function addSignature(EntityWorkflowStepSignature $signature): self | ||||
|     { | ||||
|         if (!$this->signatures->contains($signature)) { | ||||
|             $this->signatures[] = $signature; | ||||
|         } | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function removeSignature(EntityWorkflowStepSignature $signature): self | ||||
|     { | ||||
|         if ($this->signatures->contains($signature)) { | ||||
|             $this->signatures->removeElement($signature); | ||||
|         } | ||||
|  | ||||
|         $signature->detachEntityWorkflowStep(); | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getAccessKey(): string | ||||
|     { | ||||
|         return $this->accessKey; | ||||
| @@ -198,6 +228,14 @@ class EntityWorkflowStep | ||||
|         return $this->entityWorkflow; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return Collection<int, EntityWorkflowStepSignature> | ||||
|      */ | ||||
|     public function getSignatures(): Collection | ||||
|     { | ||||
|         return $this->signatures; | ||||
|     } | ||||
|  | ||||
|     public function getId(): ?int | ||||
|     { | ||||
|         return $this->id; | ||||
|   | ||||
| @@ -0,0 +1,156 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Entity\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; | ||||
| use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; | ||||
| use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; | ||||
| use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
|  | ||||
| #[ORM\Entity] | ||||
| #[ORM\Table(name: 'chill_main_workflow_entity_step_signature')] | ||||
| class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdateInterface | ||||
| { | ||||
|     use TrackCreationTrait; | ||||
|     use TrackUpdateTrait; | ||||
|  | ||||
|     #[ORM\Id] | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, unique: true)] | ||||
|     #[ORM\GeneratedValue(strategy: 'AUTO')] | ||||
|     private ?int $id = null; | ||||
|  | ||||
|     #[ORM\ManyToOne(targetEntity: User::class)] | ||||
|     #[ORM\JoinColumn(nullable: true)] | ||||
|     private ?User $userSigner = null; | ||||
|  | ||||
|     #[ORM\ManyToOne(targetEntity: Person::class)] | ||||
|     #[ORM\JoinColumn(nullable: true)] | ||||
|     private ?Person $personSigner = null; | ||||
|  | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 50, nullable: false, enumType: EntityWorkflowSignatureStateEnum::class)] | ||||
|     private EntityWorkflowSignatureStateEnum $state = EntityWorkflowSignatureStateEnum::PENDING; | ||||
|  | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIMETZ_IMMUTABLE, nullable: true, options: ['default' => null])] | ||||
|     private ?\DateTimeImmutable $stateDate = null; | ||||
|  | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])] | ||||
|     private array $signatureMetadata = []; | ||||
|  | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])] | ||||
|     private ?int $zoneSignatureIndex = null; | ||||
|  | ||||
|     #[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')] | ||||
|     #[ORM\JoinColumn(nullable: false)] | ||||
|     private ?EntityWorkflowStep $step = null; | ||||
|  | ||||
|     public function __construct( | ||||
|         EntityWorkflowStep $step, | ||||
|         User|Person $signer, | ||||
|     ) { | ||||
|         $this->step = $step; | ||||
|         $step->addSignature($this); | ||||
|         $this->setSigner($signer); | ||||
|     } | ||||
|  | ||||
|     private function setSigner(User|Person $signer): void | ||||
|     { | ||||
|         if ($signer instanceof User) { | ||||
|             $this->userSigner = $signer; | ||||
|         } else { | ||||
|             $this->personSigner = $signer; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function getId(): ?int | ||||
|     { | ||||
|         return $this->id; | ||||
|     } | ||||
|  | ||||
|     public function getStep(): EntityWorkflowStep | ||||
|     { | ||||
|         return $this->step; | ||||
|     } | ||||
|  | ||||
|     public function getSigner(): User|Person | ||||
|     { | ||||
|         if (null !== $this->userSigner) { | ||||
|             return $this->userSigner; | ||||
|         } | ||||
|  | ||||
|         return $this->personSigner; | ||||
|     } | ||||
|  | ||||
|     public function getSignatureMetadata(): array | ||||
|     { | ||||
|         return $this->signatureMetadata; | ||||
|     } | ||||
|  | ||||
|     public function setSignatureMetadata(array $signatureMetadata): EntityWorkflowStepSignature | ||||
|     { | ||||
|         $this->signatureMetadata = $signatureMetadata; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getState(): EntityWorkflowSignatureStateEnum | ||||
|     { | ||||
|         return $this->state; | ||||
|     } | ||||
|  | ||||
|     public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature | ||||
|     { | ||||
|         $this->state = $state; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getStateDate(): ?\DateTimeImmutable | ||||
|     { | ||||
|         return $this->stateDate; | ||||
|     } | ||||
|  | ||||
|     public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature | ||||
|     { | ||||
|         $this->stateDate = $stateDate; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getZoneSignatureIndex(): ?int | ||||
|     { | ||||
|         return $this->zoneSignatureIndex; | ||||
|     } | ||||
|  | ||||
|     public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature | ||||
|     { | ||||
|         $this->zoneSignatureIndex = $zoneSignatureIndex; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Detach from the @see{EntityWorkflowStep}. | ||||
|      * | ||||
|      * @internal used internally to remove the current signature | ||||
|      * | ||||
|      * @return $this | ||||
|      */ | ||||
|     public function detachEntityWorkflowStep(): self | ||||
|     { | ||||
|         $this->step = null; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,25 @@ | ||||
| <?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\Export; | ||||
|  | ||||
| /** | ||||
|  * Transform data from filter. | ||||
|  * | ||||
|  * This interface defines a method for transforming filter's form data before it is processed. | ||||
|  * | ||||
|  * You can implement this interface on @see{FilterInterface} or @see{AggregatorInterface}, to allow to transform existing data in saved exports | ||||
|  * and replace it with some default values, or new default values. | ||||
|  */ | ||||
| interface DataTransformerInterface | ||||
| { | ||||
|     public function transformData(?array $before): array; | ||||
| } | ||||
| @@ -32,6 +32,9 @@ interface FilterInterface extends ModifierInterface | ||||
|  | ||||
|     /** | ||||
|      * Get the default data, that can be use as "data" for the form. | ||||
|      * | ||||
|      * In case of adding new parameters to a filter, you can implement a @see{DataTransformerFilterInterface} to | ||||
|      * transforme the filters's data saved in an export to the desired state. | ||||
|      */ | ||||
|     public function getFormDefaultData(): array; | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,9 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\MainBundle\Form\Type\Export; | ||||
|  | ||||
| use Chill\MainBundle\Export\DataTransformerInterface; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\CallbackTransformer; | ||||
| use Symfony\Component\Form\Extension\Core\Type\CheckboxType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\FormType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| @@ -19,9 +21,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; | ||||
|  | ||||
| class AggregatorType extends AbstractType | ||||
| { | ||||
|     public function __construct() {} | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options) | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options): void | ||||
|     { | ||||
|         $exportManager = $options['export_manager']; | ||||
|         $aggregator = $exportManager->getAggregator($options['aggregator_alias']); | ||||
| @@ -32,17 +32,24 @@ class AggregatorType extends AbstractType | ||||
|                 'required' => false, | ||||
|             ]); | ||||
|  | ||||
|         $filterFormBuilder = $builder->create('form', FormType::class, [ | ||||
|         $aggregatorFormBuilder = $builder->create('form', FormType::class, [ | ||||
|             'compound' => true, | ||||
|             'required' => false, | ||||
|             'error_bubbling' => false, | ||||
|         ]); | ||||
|         $aggregator->buildForm($filterFormBuilder); | ||||
|         $aggregator->buildForm($aggregatorFormBuilder); | ||||
|  | ||||
|         $builder->add($filterFormBuilder); | ||||
|         if ($aggregator instanceof DataTransformerInterface) { | ||||
|             $aggregatorFormBuilder->addViewTransformer(new CallbackTransformer( | ||||
|                 fn (?array $data) => $data, | ||||
|                 fn (?array $data) => $aggregator->transformData($data), | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         $builder->add($aggregatorFormBuilder); | ||||
|     } | ||||
|  | ||||
|     public function configureOptions(OptionsResolver $resolver) | ||||
|     public function configureOptions(OptionsResolver $resolver): void | ||||
|     { | ||||
|         $resolver->setRequired('aggregator_alias') | ||||
|             ->setRequired('export_manager') | ||||
|   | ||||
| @@ -11,8 +11,10 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\MainBundle\Form\Type\Export; | ||||
|  | ||||
| use Chill\MainBundle\Export\DataTransformerInterface; | ||||
| use Chill\MainBundle\Export\FilterInterface; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\CallbackTransformer; | ||||
| use Symfony\Component\Form\Extension\Core\Type\CheckboxType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\FormType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| @@ -41,6 +43,13 @@ class FilterType extends AbstractType | ||||
|         ]); | ||||
|         $filter->buildForm($filterFormBuilder); | ||||
|  | ||||
|         if ($filter instanceof DataTransformerInterface) { | ||||
|             $filterFormBuilder->addViewTransformer(new CallbackTransformer( | ||||
|                 fn (?array $data) => $data, | ||||
|                 fn (?array $data) => $filter->transformData($data), | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         $builder->add($filterFormBuilder); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,62 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Form; | ||||
|  | ||||
| use Chill\MainBundle\Form\Type\ChillDateType; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\TextType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
|  | ||||
| class WorkflowSignatureMetadataType extends AbstractType | ||||
| { | ||||
|     public function __construct(private readonly ParameterBagInterface $parameterBag, private readonly TranslatableStringHelperInterface $translatableStringHelper) {} | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options): void | ||||
|     { | ||||
|         $documentTypeChoices = $this->parameterBag->get('chill_main.id_document_kinds'); | ||||
|  | ||||
|         $choices = []; | ||||
|  | ||||
|         foreach ($documentTypeChoices as $documentType) { | ||||
|             $labels = []; | ||||
|  | ||||
|             foreach ($documentType['labels'] as $label) { | ||||
|                 $labels[$label['lang']] = $label['label']; | ||||
|             } | ||||
|  | ||||
|             $localizedLabel = $this->translatableStringHelper->localize($labels); | ||||
|             if (null !== $localizedLabel) { | ||||
|                 $choices[$localizedLabel] = $documentType['key']; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         $builder | ||||
|             ->add('documentType', ChoiceType::class, [ | ||||
|                 'label' => 'workflow.signature_zone.metadata.docType', | ||||
|                 'expanded' => false, | ||||
|                 'required' => true, | ||||
|                 'choices' => $choices, | ||||
|             ]) | ||||
|             ->add('documentNumber', TextType::class, [ | ||||
|                 'required' => true, | ||||
|                 'label' => 'workflow.signature_zone.metadata.docNumber', | ||||
|             ]) | ||||
|             ->add('expirationDate', ChillDateType::class, [ | ||||
|                 'required' => true, | ||||
|                 'input' => 'datetime_immutable', | ||||
|                 'label' => 'workflow.signature_zone.metadata.docExpiration', | ||||
|             ]); | ||||
|     } | ||||
| } | ||||
| @@ -12,14 +12,12 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Form; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; | ||||
| use Chill\MainBundle\Form\Type\ChillCollectionType; | ||||
| use Chill\MainBundle\Form\Type\ChillTextareaType; | ||||
| use Chill\MainBundle\Form\Type\PickUserDynamicType; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Chill\MainBundle\Workflow\EntityWorkflowManager; | ||||
| use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\CheckboxType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\EmailType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| @@ -34,169 +32,151 @@ use Symfony\Component\Workflow\Transition; | ||||
|  | ||||
| class WorkflowStepType extends AbstractType | ||||
| { | ||||
|     public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly TranslatableStringHelperInterface $translatableStringHelper) {} | ||||
|     public function __construct( | ||||
|         private readonly Registry $registry, | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper | ||||
|     ) {} | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options) | ||||
|     { | ||||
|         /** @var EntityWorkflow $entityWorkflow */ | ||||
|         $entityWorkflow = $options['entity_workflow']; | ||||
|         $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); | ||||
|         $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); | ||||
|         $place = $workflow->getMarking($entityWorkflow); | ||||
|         $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]); | ||||
|  | ||||
|         if (true === $options['transition']) { | ||||
|             if (null === $options['entity_workflow']) { | ||||
|                 throw new \LogicException('if transition is true, entity_workflow should be defined'); | ||||
|             } | ||||
|         if (null === $options['entity_workflow']) { | ||||
|             throw new \LogicException('if transition is true, entity_workflow should be defined'); | ||||
|         } | ||||
|  | ||||
|             $transitions = $this->registry | ||||
|                 ->get($options['entity_workflow'], $entityWorkflow->getWorkflowName()) | ||||
|                 ->getEnabledTransitions($entityWorkflow); | ||||
|         $transitions = $this->registry | ||||
|             ->get($options['entity_workflow'], $entityWorkflow->getWorkflowName()) | ||||
|             ->getEnabledTransitions($entityWorkflow); | ||||
|  | ||||
|             $choices = array_combine( | ||||
|                 array_map( | ||||
|                     static fn (Transition $transition) => $transition->getName(), | ||||
|                     $transitions | ||||
|                 ), | ||||
|         $choices = array_combine( | ||||
|             array_map( | ||||
|                 static fn (Transition $transition) => $transition->getName(), | ||||
|                 $transitions | ||||
|             ); | ||||
|             ), | ||||
|             $transitions | ||||
|         ); | ||||
|  | ||||
|             if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) { | ||||
|                 $inputLabels = $placeMetadata['validationFilterInputLabels']; | ||||
|         if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) { | ||||
|             $inputLabels = $placeMetadata['validationFilterInputLabels']; | ||||
|  | ||||
|                 $builder->add('transitionFilter', ChoiceType::class, [ | ||||
|                     'multiple' => false, | ||||
|                     'label' => 'workflow.My decision', | ||||
|                     'choices' => [ | ||||
|                         'forward' => 'forward', | ||||
|                         'backward' => 'backward', | ||||
|                         'neutral' => 'neutral', | ||||
|                     ], | ||||
|                     'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]), | ||||
|                     'choice_attr' => static fn (string $key) => [ | ||||
|                         $key => $key, | ||||
|                     ], | ||||
|                     'mapped' => false, | ||||
|                     'expanded' => true, | ||||
|                     'data' => 'forward', | ||||
|                 ]); | ||||
|             } | ||||
|  | ||||
|             $builder | ||||
|                 ->add('transition', ChoiceType::class, [ | ||||
|                     'label' => 'workflow.Next step', | ||||
|                     'mapped' => false, | ||||
|                     'multiple' => false, | ||||
|                     'expanded' => true, | ||||
|                     'choices' => $choices, | ||||
|                     'constraints' => [new NotNull()], | ||||
|                     'choice_label' => function (Transition $transition) use ($workflow) { | ||||
|                         $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition); | ||||
|  | ||||
|                         if (\array_key_exists('label', $meta)) { | ||||
|                             return $this->translatableStringHelper->localize($meta['label']); | ||||
|                         } | ||||
|  | ||||
|                         return $transition->getName(); | ||||
|                     }, | ||||
|                     'choice_attr' => static function (Transition $transition) use ($workflow) { | ||||
|                         $toFinal = true; | ||||
|                         $isForward = 'neutral'; | ||||
|  | ||||
|                         $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition); | ||||
|  | ||||
|                         if (\array_key_exists('isForward', $metadata)) { | ||||
|                             if ($metadata['isForward']) { | ||||
|                                 $isForward = 'forward'; | ||||
|                             } else { | ||||
|                                 $isForward = 'backward'; | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         foreach ($transition->getTos() as $to) { | ||||
|                             $meta = $workflow->getMetadataStore()->getPlaceMetadata($to); | ||||
|  | ||||
|                             if ( | ||||
|                                 !\array_key_exists('isFinal', $meta) || false === $meta['isFinal'] | ||||
|                             ) { | ||||
|                                 $toFinal = false; | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         return [ | ||||
|                             'data-is-transition' => 'data-is-transition', | ||||
|                             'data-to-final' => $toFinal ? '1' : '0', | ||||
|                             'data-is-forward' => $isForward, | ||||
|                         ]; | ||||
|                     }, | ||||
|                 ]) | ||||
|                 ->add('future_dest_users', PickUserDynamicType::class, [ | ||||
|                     'label' => 'workflow.dest for next steps', | ||||
|                     'multiple' => true, | ||||
|                     'mapped' => false, | ||||
|                     'suggested' => $options['suggested_users'], | ||||
|                 ]) | ||||
|                 ->add('future_cc_users', PickUserDynamicType::class, [ | ||||
|                     'label' => 'workflow.cc for next steps', | ||||
|                     'multiple' => true, | ||||
|                     'mapped' => false, | ||||
|                     'required' => false, | ||||
|                     'suggested' => $options['suggested_users'], | ||||
|                 ]) | ||||
|                 ->add('future_dest_emails', ChillCollectionType::class, [ | ||||
|                     'label' => 'workflow.dest by email', | ||||
|                     'help' => 'workflow.dest by email help', | ||||
|                     'mapped' => false, | ||||
|                     'allow_add' => true, | ||||
|                     'entry_type' => EmailType::class, | ||||
|                     'button_add_label' => 'workflow.Add an email', | ||||
|                     'button_remove_label' => 'workflow.Remove an email', | ||||
|                     'empty_collection_explain' => 'workflow.Any email', | ||||
|                     'entry_options' => [ | ||||
|                         'constraints' => [ | ||||
|                             new NotNull(), new NotBlank(), new Email(), | ||||
|                         ], | ||||
|                         'label' => 'Email', | ||||
|                     ], | ||||
|                 ]); | ||||
|             $builder->add('transitionFilter', ChoiceType::class, [ | ||||
|                 'multiple' => false, | ||||
|                 'label' => 'workflow.My decision', | ||||
|                 'choices' => [ | ||||
|                     'forward' => 'forward', | ||||
|                     'backward' => 'backward', | ||||
|                     'neutral' => 'neutral', | ||||
|                 ], | ||||
|                 'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]), | ||||
|                 'choice_attr' => static fn (string $key) => [ | ||||
|                     $key => $key, | ||||
|                 ], | ||||
|                 'mapped' => false, | ||||
|                 'expanded' => true, | ||||
|                 'data' => 'forward', | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         if ( | ||||
|             $handler->supportsFreeze($entityWorkflow) | ||||
|             && !$entityWorkflow->isFreeze() | ||||
|         ) { | ||||
|             $builder | ||||
|                 ->add('freezeAfter', CheckboxType::class, [ | ||||
|                     'required' => false, | ||||
|                     'label' => 'workflow.Freeze', | ||||
|                     'help' => 'workflow.The associated element will be freezed', | ||||
|                 ]); | ||||
|         } | ||||
|         $builder | ||||
|             ->add('transition', ChoiceType::class, [ | ||||
|                 'label' => 'workflow.Next step', | ||||
|                 'mapped' => false, | ||||
|                 'multiple' => false, | ||||
|                 'expanded' => true, | ||||
|                 'choices' => $choices, | ||||
|                 'constraints' => [new NotNull()], | ||||
|                 'choice_label' => function (Transition $transition) use ($workflow) { | ||||
|                     $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition); | ||||
|  | ||||
|                     if (\array_key_exists('label', $meta)) { | ||||
|                         return $this->translatableStringHelper->localize($meta['label']); | ||||
|                     } | ||||
|  | ||||
|                     return $transition->getName(); | ||||
|                 }, | ||||
|                 'choice_attr' => static function (Transition $transition) use ($workflow) { | ||||
|                     $toFinal = true; | ||||
|                     $isForward = 'neutral'; | ||||
|  | ||||
|                     $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition); | ||||
|  | ||||
|                     if (\array_key_exists('isForward', $metadata)) { | ||||
|                         if ($metadata['isForward']) { | ||||
|                             $isForward = 'forward'; | ||||
|                         } else { | ||||
|                             $isForward = 'backward'; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     foreach ($transition->getTos() as $to) { | ||||
|                         $meta = $workflow->getMetadataStore()->getPlaceMetadata($to); | ||||
|  | ||||
|                         if ( | ||||
|                             !\array_key_exists('isFinal', $meta) || false === $meta['isFinal'] | ||||
|                         ) { | ||||
|                             $toFinal = false; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     return [ | ||||
|                         'data-is-transition' => 'data-is-transition', | ||||
|                         'data-to-final' => $toFinal ? '1' : '0', | ||||
|                         'data-is-forward' => $isForward, | ||||
|                     ]; | ||||
|                 }, | ||||
|             ]) | ||||
|             ->add('futureDestUsers', PickUserDynamicType::class, [ | ||||
|                 'label' => 'workflow.dest for next steps', | ||||
|                 'multiple' => true, | ||||
|                 'suggested' => $options['suggested_users'], | ||||
|             ]) | ||||
|             ->add('futureCcUsers', PickUserDynamicType::class, [ | ||||
|                 'label' => 'workflow.cc for next steps', | ||||
|                 'multiple' => true, | ||||
|                 'required' => false, | ||||
|                 'suggested' => $options['suggested_users'], | ||||
|             ]) | ||||
|             ->add('futureDestEmails', ChillCollectionType::class, [ | ||||
|                 'label' => 'workflow.dest by email', | ||||
|                 'help' => 'workflow.dest by email help', | ||||
|                 'allow_add' => true, | ||||
|                 'entry_type' => EmailType::class, | ||||
|                 'button_add_label' => 'workflow.Add an email', | ||||
|                 'button_remove_label' => 'workflow.Remove an email', | ||||
|                 'empty_collection_explain' => 'workflow.Any email', | ||||
|                 'entry_options' => [ | ||||
|                     'constraints' => [ | ||||
|                         new NotNull(), new NotBlank(), new Email(), | ||||
|                     ], | ||||
|                     'label' => 'Email', | ||||
|                 ], | ||||
|             ]); | ||||
|  | ||||
|         $builder | ||||
|             ->add('comment', ChillTextareaType::class, [ | ||||
|                 'required' => false, | ||||
|                 'label' => 'Comment', | ||||
|                 'empty_data' => '', | ||||
|             ]); | ||||
|     } | ||||
|  | ||||
|     public function configureOptions(OptionsResolver $resolver) | ||||
|     { | ||||
|         $resolver | ||||
|             ->setDefined('class') | ||||
|             ->setRequired('transition') | ||||
|             ->setAllowedTypes('transition', 'bool') | ||||
|             ->setDefault('data_class', WorkflowTransitionContextDTO::class) | ||||
|             ->setRequired('entity_workflow') | ||||
|             ->setAllowedTypes('entity_workflow', EntityWorkflow::class) | ||||
|             ->setDefault('suggested_users', []) | ||||
|             ->setDefault('constraints', [ | ||||
|                 new Callback( | ||||
|                     function ($step, ExecutionContextInterface $context, $payload) { | ||||
|                         /** @var EntityWorkflowStep $step */ | ||||
|                         $form = $context->getObject(); | ||||
|                         $workflow = $this->registry->get($step->getEntityWorkflow(), $step->getEntityWorkflow()->getWorkflowName()); | ||||
|                         $transition = $form['transition']->getData(); | ||||
|                     function (WorkflowTransitionContextDTO $step, ExecutionContextInterface $context, $payload) { | ||||
|                         $workflow = $this->registry->get($step->entityWorkflow, $step->entityWorkflow->getWorkflowName()); | ||||
|                         $transition = $step->transition; | ||||
|                         $toFinal = true; | ||||
|  | ||||
|                         if (null === $transition) { | ||||
| @@ -212,8 +192,8 @@ class WorkflowStepType extends AbstractType | ||||
|                                     $toFinal = false; | ||||
|                                 } | ||||
|                             } | ||||
|                             $destUsers = $form['future_dest_users']->getData(); | ||||
|                             $destEmails = $form['future_dest_emails']->getData(); | ||||
|                             $destUsers = $step->futureDestUsers; | ||||
|                             $destEmails = $step->futureDestEmails; | ||||
|  | ||||
|                             if (!$toFinal && [] === $destUsers && [] === $destEmails) { | ||||
|                                 $context | ||||
| @@ -224,20 +204,6 @@ class WorkflowStepType extends AbstractType | ||||
|                         } | ||||
|                     } | ||||
|                 ), | ||||
|                 new Callback( | ||||
|                     function ($step, ExecutionContextInterface $context, $payload) { | ||||
|                         $form = $context->getObject(); | ||||
|  | ||||
|                         foreach ($form->get('future_dest_users')->getData() as $u) { | ||||
|                             if (in_array($u, $form->get('future_cc_users')->getData(), true)) { | ||||
|                                 $context | ||||
|                                     ->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step') | ||||
|                                     ->atPath('ccUsers') | ||||
|                                     ->addViolation(); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 ), | ||||
|             ]); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,54 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Repository\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\Persistence\ObjectRepository; | ||||
|  | ||||
| /** | ||||
|  * @template-implements ObjectRepository<EntityWorkflowStepSignature> | ||||
|  */ | ||||
| class EntityWorkflowStepSignatureRepository implements ObjectRepository | ||||
| { | ||||
|     private \Doctrine\ORM\EntityRepository $repository; | ||||
|  | ||||
|     public function __construct(EntityManagerInterface $entityManager) | ||||
|     { | ||||
|         $this->repository = $entityManager->getRepository(EntityWorkflowStepSignature::class); | ||||
|     } | ||||
|  | ||||
|     public function find($id): ?EntityWorkflowStepSignature | ||||
|     { | ||||
|         return $this->repository->find($id); | ||||
|     } | ||||
|  | ||||
|     public function findAll(): array | ||||
|     { | ||||
|         return $this->repository->findAll(); | ||||
|     } | ||||
|  | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array | ||||
|     { | ||||
|         return $this->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
|  | ||||
|     public function findOneBy(array $criteria): ?EntityWorkflowStepSignature | ||||
|     { | ||||
|         return $this->findOneBy($criteria); | ||||
|     } | ||||
|  | ||||
|     public function getClassName(): string | ||||
|     { | ||||
|         return EntityWorkflowStepSignature::class; | ||||
|     } | ||||
| } | ||||
| @@ -43,7 +43,14 @@ export const download_report = (url, container) => { | ||||
|                 content = URL.createObjectURL(blob); | ||||
|             } | ||||
|  | ||||
|             extension = mime.getExtension(type); | ||||
|             const extensions = new Map(); | ||||
|             extensions.set('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlsx'); | ||||
|             extensions.set('application/vnd.oasis.opendocument.spreadsheet', 'ods'); | ||||
|             extensions.set('application/vnd.ms-excel', 'xlsx'); | ||||
|             extensions.set('text/csv', 'csv'); | ||||
|             extensions.set('text/csv; charset=utf-8', 'csv'); | ||||
|  | ||||
|             extension = extensions.get(type); | ||||
|  | ||||
|             link.appendChild(document.createTextNode(download_text)); | ||||
|             link.classList.add("btn", "btn-action"); | ||||
| @@ -55,7 +62,7 @@ export const download_report = (url, container) => { | ||||
|             container.innerHTML = ""; | ||||
|             container.appendChild(link); | ||||
|         }).catch(function(error) { | ||||
|             console.log(error); | ||||
|             console.error(error); | ||||
|             var problem_text = | ||||
|                     document.createTextNode("Problem during download"); | ||||
|  | ||||
|   | ||||
| @@ -12,5 +12,5 @@ window.addEventListener("DOMContentLoaded", function(e) { | ||||
|     container = document.querySelector("#download_container") | ||||
|   ; | ||||
|  | ||||
|   download_report(export_generate_url + "?" + query.toString(), container); | ||||
|   download_report(export_generate_url + query.toString(), container); | ||||
| }); | ||||
|   | ||||
| @@ -139,7 +139,7 @@ const postprocess = (html: string): string => { | ||||
| } | ||||
|  | ||||
| const convertMarkdownToHtml = (markdown: string): string => { | ||||
|     marked.use({'hooks': {postprocess, preprocess}}); | ||||
|     marked.use({'hooks': {postprocess, preprocess}, 'async': false}); | ||||
|     const rawHtml = marked(markdown) as string; | ||||
|     return rawHtml; | ||||
| }; | ||||
|   | ||||
| @@ -40,10 +40,10 @@ | ||||
|                         {{ 'by_user'|trans ~ ' ' }} | ||||
|                     {% endif %} | ||||
|                     <span class="user"> | ||||
|                         {{ user|chill_entity_render_box(options['user']) }} | ||||
|                         {{ user|chill_entity_render_box({'at_date': comment.date}) }} | ||||
|                     </span> | ||||
|                 {% endif %} | ||||
|             </div> | ||||
|         {% endif %} | ||||
|     </blockquote> | ||||
| {{ closing_box|raw }} | ||||
| {{ closing_box|raw }} | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| <span class="chill-entity entity-user"> | ||||
|     {{- user.label }} | ||||
|     {%- if opts['user_job'] and user.userJob(opts['at']) is not null %} | ||||
|         <span class="user-job">({{ user.userJob(opts['at']).label|localize_translatable_string }})</span> | ||||
|     {%- if opts['user_job'] and user.userJob(opts['at_date']) is not null %} | ||||
|         <span class="user-job">({{ user.userJob(opts['at_date']).label|localize_translatable_string }})</span> | ||||
|     {%- endif -%} | ||||
|     {%- if opts['main_scope'] and user.mainScope(opts['at']) is not null %} | ||||
|     <span class="main-scope">({{ user.mainScope(opts['at']).name|localize_translatable_string }})</span> | ||||
|     {%- if opts['main_scope'] and user.mainScope(opts['at_date']) is not null %} | ||||
|     <span class="main-scope">({{ user.mainScope(opts['at_date']).name|localize_translatable_string }})</span> | ||||
|     {%- endif -%} | ||||
|     {%- if opts['absence'] and user.isAbsent %} | ||||
|         <span class="badge bg-danger rounded-pill" title="{{ 'absence.Absent'|trans|escape('html_attr') }}">{{ 'absence.A'|trans }}</span> | ||||
|   | ||||
| @@ -0,0 +1,20 @@ | ||||
| {% if menus|length > 0 %} | ||||
| <li class="dropdown"> | ||||
|     <a class="dropdown-toggle btn btn-sm btn-outline-primary" | ||||
|         href="#" | ||||
|         role="button" | ||||
|         data-bs-toggle="dropdown" | ||||
|         aria-expanded="false"> | ||||
|         <i class="fa fa-flash"></i> | ||||
|     </a> | ||||
|     <div class="dropdown-menu"> | ||||
|         {% for menu in menus %} | ||||
|             <a class="dropdown-item" | ||||
|                 href="{{ menu.uri }}" | ||||
|                 ><i class="fa fa-{{- menu.extras.icon }} fa-fw"></i> | ||||
|                 {{ menu.label|trans }} | ||||
|             </a> | ||||
|         {% endfor %} | ||||
|     </div> | ||||
| </li> | ||||
| {% endif %} | ||||
| @@ -21,7 +21,7 @@ | ||||
|                         </span> | ||||
|                         {% if not c.notification.isSystem %} | ||||
|                             <span class="badge-user"> | ||||
|                                     {{ c.notification.sender|chill_entity_render_string }} | ||||
|                                     {{ c.notification.sender|chill_entity_render_string({'at_date': c.notification.date}) }} | ||||
|                                 </span> | ||||
|                         {% else %} | ||||
|                             <span class="badge-user system">{{ 'notification.is_system'|trans }}</span> | ||||
| @@ -53,7 +53,7 @@ | ||||
|                         {% endif %} | ||||
|                         {% for a in c.notification.addressees %} | ||||
|                             <span class="badge-user"> | ||||
|                                 {{ a|chill_entity_render_string }} | ||||
|                                 {{ a|chill_entity_render_string({'at_date': c.notification.date}) }} | ||||
|                             </span> | ||||
|                         {% endfor %} | ||||
|                         {% for a in c.notification.addressesEmails %} | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| {% extends "@ChillMain/layout.html.twig"  %} | ||||
|  | ||||
| {% block title 'notification.show notification from %sender%'|trans( | ||||
|     { '%sender%': notification.sender|chill_entity_render_string } | ||||
|     { '%sender%': notification.sender|chill_entity_render_string({'at_date': notification.date}) } | ||||
| ) ~ ' ' ~ notification.title %} | ||||
|  | ||||
| {% block js %} | ||||
|   | ||||
| @@ -31,14 +31,14 @@ | ||||
|                 <div class="row"> | ||||
|                     <div class="col-sm-12"> | ||||
|                         {{ 'By'|trans }} | ||||
|                         {{ step.previous.transitionBy|chill_entity_render_box }}, | ||||
|                         {{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}, | ||||
|                         {{ step.previous.transitionAt|format_datetime('short', 'short') }} | ||||
|                     </div> | ||||
|                 </div> | ||||
|             {% else %} | ||||
|                 <div class="row"> | ||||
|                     <div class="col-sm-4">{{ 'workflow.Created by'|trans }}</div> | ||||
|                     <div class="col-sm-8">{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</div> | ||||
|                     <div class="col-sm-8">{{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt}) }}</div> | ||||
|                 </div> | ||||
|                 <div class="row"> | ||||
|                     <div class="col-sm-4">{{ 'Le'|trans }}</div> | ||||
| @@ -58,17 +58,15 @@ | ||||
|         {{ form_row(transition_form.transition) }} | ||||
|     </div> | ||||
|  | ||||
|     {% if transition_form.freezeAfter is defined %} | ||||
|         {{ form_row(transition_form.freezeAfter) }} | ||||
|     {% endif %} | ||||
|  | ||||
|     <div id="futureDests"> | ||||
|         {{ form_row(transition_form.future_dest_users) }} | ||||
|         {{ form_row(transition_form.futureDestUsers) }} | ||||
|         {{ form_errors(transition_form.futureDestUsers) }} | ||||
|  | ||||
|         {{ form_row(transition_form.future_cc_users) }} | ||||
|         {{ form_row(transition_form.futureCcUsers) }} | ||||
|         {{ form_errors(transition_form.futureCcUsers) }} | ||||
|  | ||||
|         {{ form_row(transition_form.future_dest_emails) }} | ||||
|         {{ form_errors(transition_form.future_dest_users) }} | ||||
|         {{ form_row(transition_form.futureDestEmails) }} | ||||
|         {{ form_errors(transition_form.futureDestEmails) }} | ||||
|     </div> | ||||
|  | ||||
|     <p>{{ form_label(transition_form.comment) }}</p> | ||||
| @@ -110,8 +108,8 @@ | ||||
|             {% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %} | ||||
|                 <p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p> | ||||
|                 <ul> | ||||
|                     {% for u in entity_workflow.currentStep.destUserByAccessKey %} | ||||
|                         <li>{{ u|chill_entity_render_box }}</li> | ||||
|                     {% for u in entity_workflow.currentStepChained.destUserByAccessKey %} | ||||
|                         <li>{{ u|chill_entity_render_box({'at_date': entity_workflow.currentStepChained.previous.transitionAt }) }}</li> | ||||
|                     {% endfor %} | ||||
|                 </ul> | ||||
|             {% endif %} | ||||
|   | ||||
| @@ -42,7 +42,7 @@ | ||||
|                     <div class="item-col" style="width: inherit;"> | ||||
|                         {% if step.transitionBy is not null %} | ||||
|                             <div> | ||||
|                                 {{ step.transitionBy|chill_entity_render_box }} | ||||
|                                 {{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }} | ||||
|                             </div> | ||||
|                         {% endif %} | ||||
|                         <div> | ||||
| @@ -76,7 +76,7 @@ | ||||
|                             <p><b>{{ 'workflow.Users allowed to apply transition'|trans }} : </b></p> | ||||
|                             <ul> | ||||
|                                 {% for u in step.destUser %} | ||||
|                                     <li>{{ u|chill_entity_render_box }}</li> | ||||
|                                     <li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li> | ||||
|                                 {% endfor %} | ||||
|                             </ul> | ||||
|                         {% endif %} | ||||
| @@ -85,7 +85,7 @@ | ||||
|                             <p><b>{{ 'workflow.Users put in Cc'|trans }} : </b></p> | ||||
|                             <ul> | ||||
|                                 {% for u in step.ccUser %} | ||||
|                                     <li>{{ u|chill_entity_render_box }}</li> | ||||
|                                     <li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li> | ||||
|                                 {% endfor %} | ||||
|                             </ul> | ||||
|                         {% endif %} | ||||
| @@ -103,7 +103,7 @@ | ||||
|                             <p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p> | ||||
|                             <ul> | ||||
|                                 {% for u in step.destUserByAccessKey %} | ||||
|                                     <li>{{ u|chill_entity_render_box }}</li> | ||||
|                                     <li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li> | ||||
|                                 {% endfor %} | ||||
|                             </ul> | ||||
|                         {% endif %} | ||||
|   | ||||
| @@ -0,0 +1,19 @@ | ||||
| <h2>{{ 'workflow.signature_zone.title'|trans }}</h2> | ||||
|  | ||||
| <div class="flex-table justify-content-center"> | ||||
|     <div class="item-bloc"> | ||||
|         {% for s in signatures %} | ||||
|             <div class="item-row mb-2"> | ||||
|                     <div class="col-sm-6"><span>{{ s.signer|chill_entity_render_box }}</span></div> | ||||
|             <div class="col-sm-6"> | ||||
|                 <a class="btn btn-show" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}">{{ 'workflow.signature_zone.button_sign'|trans }}</a> | ||||
|                 {% if s.state is same as('signed') %} | ||||
|                     <p class="updatedBy">{{ s.stateDate }}</p> | ||||
|                 {% endif %} | ||||
|                 </div> | ||||
|             </div> | ||||
|         {% endfor %} | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
|  | ||||
| @@ -0,0 +1,24 @@ | ||||
| {% extends '@ChillMain/layout.html.twig' %} | ||||
|  | ||||
| {% block title %} | ||||
|     {{ 'Signature'|trans }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="col-10 workflow"> | ||||
|     <h1 class="mb-5">{{ 'workflow.signature_zone.metadata.sign_by'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname}) }}</h1> | ||||
|  | ||||
|     {% if metadata_form is not null %} | ||||
|         {{ form_start(metadata_form) }} | ||||
|         {{ form_row(metadata_form.documentType) }} | ||||
|         {{ form_row(metadata_form.documentNumber) }} | ||||
|         {{ form_row(metadata_form.expirationDate) }} | ||||
|         <ul class="record_actions"> | ||||
|             <li> | ||||
|             {{ form_widget(metadata_form.submit, { 'attr' : { 'class' : 'btn btn-submit' }} ) }} | ||||
|             </li> | ||||
|         </ul> | ||||
|         {{ form_end(metadata_form) }} | ||||
|     {% endif %} | ||||
| </div> | ||||
| {% endblock %} | ||||
| @@ -57,6 +57,9 @@ | ||||
|     </section> | ||||
|  | ||||
|     <section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section> | ||||
|     {% if signatures|length > 0 %} | ||||
|         <section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section> | ||||
|     {% endif %} | ||||
|     <section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>{# | ||||
|     <section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #} | ||||
|     <section class="step my-4">{% include '@ChillMain/Workflow/_history.html.twig' %}</section> | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|         {% if step.previous is not null %} | ||||
|             <li> | ||||
|                 <span class="item-key">{{ 'By'|trans ~ ' : ' }}</span> | ||||
|                 <b>{{ step.previous.transitionBy|chill_entity_render_box }}</b> | ||||
|                 <b>{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}</b> | ||||
|             </li> | ||||
|             <li> | ||||
|                 <span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span> | ||||
| @@ -12,19 +12,19 @@ | ||||
|             <li> | ||||
|                 <span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span> | ||||
|                 <b> | ||||
|                     {% for d in step.destUser %}{{ d|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %} | ||||
|                     {% for d in step.destUser %}{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}{% if not loop.last %}, {% endif %}{% endfor %} | ||||
|                 </b> | ||||
|             </li> | ||||
|             <li> | ||||
|                 <span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span> | ||||
|                 <b> | ||||
|                     {% for u in step.ccUser %}{{ u|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %} | ||||
|                     {% for u in step.ccUser %}{{ u|chill_entity_render_string({'at_date': step.previous.transitionAt }) }}{% if not loop.last %}, {% endif %}{% endfor %} | ||||
|                 </b> | ||||
|             </li> | ||||
|         {% else %} | ||||
|             <li> | ||||
|                 <span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span> | ||||
|                 <b>{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</b> | ||||
|                 <b>{{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt }) }}</b> | ||||
|             </li> | ||||
|             <li> | ||||
|                 <span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span> | ||||
|   | ||||
| @@ -11,8 +11,6 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\MainBundle\Routing; | ||||
|  | ||||
| use Symfony\Component\DependencyInjection\ContainerAwareInterface; | ||||
| use Symfony\Component\DependencyInjection\ContainerInterface; | ||||
| use Twig\Environment; | ||||
| use Twig\Extension\AbstractExtension; | ||||
| use Twig\TwigFunction; | ||||
| @@ -20,10 +18,8 @@ use Twig\TwigFunction; | ||||
| /** | ||||
|  * Add the filter 'chill_menu'. | ||||
|  */ | ||||
| class MenuTwig extends AbstractExtension implements ContainerAwareInterface | ||||
| class MenuTwig extends AbstractExtension | ||||
| { | ||||
|     private ?ContainerInterface $container = null; | ||||
|  | ||||
|     /** | ||||
|      * the default parameters for chillMenu. | ||||
|      * | ||||
| @@ -84,9 +80,4 @@ class MenuTwig extends AbstractExtension implements ContainerAwareInterface | ||||
|     { | ||||
|         return 'chill_menu'; | ||||
|     } | ||||
|  | ||||
|     public function setContainer(?ContainerInterface $container = null) | ||||
|     { | ||||
|         $this->container = $container; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -52,7 +52,7 @@ class EntityWorkflowStepNormalizer implements NormalizerAwareInterface, Normaliz | ||||
|             $data['transitionPreviousBy'] = $this->normalizer->normalize( | ||||
|                 $previous->getTransitionBy(), | ||||
|                 $format, | ||||
|                 $context | ||||
|                 [...$context, UserNormalizer::AT_DATE => $previous->getTransitionAt()] | ||||
|             ); | ||||
|             $data['transitionPreviousAt'] = $this->normalizer->normalize( | ||||
|                 $previous->getTransitionAt(), | ||||
|   | ||||
| @@ -45,7 +45,7 @@ class NotificationNormalizer implements NormalizerAwareInterface, NormalizerInte | ||||
|             'message' => $object->getMessage(), | ||||
|             'relatedEntityClass' => $object->getRelatedEntityClass(), | ||||
|             'relatedEntityId' => $object->getRelatedEntityId(), | ||||
|             'sender' => $this->normalizer->normalize($object->getSender(), $format, $context), | ||||
|             'sender' => $this->normalizer->normalize($object->getSender(), $format, [...$context, UserNormalizer::AT_DATE => $object->getDate()]), | ||||
|             'title' => $object->getTitle(), | ||||
|             'entity' => null !== $entity ? $this->normalizer->normalize($entity, $format, $context) : null, | ||||
|         ]; | ||||
|   | ||||
| @@ -19,6 +19,7 @@ use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Templating\Entity\UserRender; | ||||
| use libphonenumber\PhoneNumber; | ||||
| use Symfony\Component\Clock\ClockInterface; | ||||
| use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; | ||||
| use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; | ||||
| use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; | ||||
| @@ -27,6 +28,8 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware | ||||
| { | ||||
|     use NormalizerAwareTrait; | ||||
|  | ||||
|     final public const AT_DATE = 'chill:user:at_date'; | ||||
|  | ||||
|     final public const NULL_USER = [ | ||||
|         'type' => 'user', | ||||
|         'id' => '', | ||||
| @@ -38,10 +41,16 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware | ||||
|         'isAbsent' => false, | ||||
|     ]; | ||||
|  | ||||
|     public function __construct(private readonly UserRender $userRender) {} | ||||
|     public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock) {} | ||||
|  | ||||
|     /** | ||||
|      * @param mixed|null $format | ||||
|      * | ||||
|      * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface | ||||
|      */ | ||||
|     public function normalize($object, $format = null, array $context = []) | ||||
|     { | ||||
|         /** @var array{"chill:user:at_date"?: \DateTimeImmutable|\DateTime} $context */ | ||||
|         /** @var User $object */ | ||||
|         $userJobContext = array_merge( | ||||
|             $context, | ||||
| @@ -72,18 +81,23 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware | ||||
|             return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)]; | ||||
|         } | ||||
|  | ||||
|         $at = $context[self::AT_DATE] ?? $this->clock->now(); | ||||
|         if ($at instanceof \DateTime) { | ||||
|             $at = \DateTimeImmutable::createFromMutable($at); | ||||
|         } | ||||
|  | ||||
|         $data = [ | ||||
|             'type' => 'user', | ||||
|             'id' => $object->getId(), | ||||
|             'username' => $object->getUsername(), | ||||
|             'text' => $this->userRender->renderString($object, []), | ||||
|             'text' => $this->userRender->renderString($object, ['at_date' => $at]), | ||||
|             'text_without_absent' => $this->userRender->renderString($object, ['absence' => false]), | ||||
|             'label' => $object->getLabel(), | ||||
|             'email' => (string) $object->getEmail(), | ||||
|             'phonenumber' => $this->normalizer->normalize($object->getPhonenumber(), $format, $phonenumberContext), | ||||
|             'user_job' => $this->normalizer->normalize($object->getUserJob(), $format, $userJobContext), | ||||
|             'user_job' => $this->normalizer->normalize($object->getUserJob($at), $format, $userJobContext), | ||||
|             'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext), | ||||
|             'main_scope' => $this->normalizer->normalize($object->getMainScope(), $format, $scopeContext), | ||||
|             'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext), | ||||
|             'isAbsent' => $object->isAbsent(), | ||||
|         ]; | ||||
|  | ||||
|   | ||||
| @@ -12,8 +12,14 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Templating\Entity; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelper; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use DateTime; | ||||
| use DateTimeImmutable; | ||||
| use Symfony\Component\Clock\ClockInterface; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
| use Twig\Error\LoaderError; | ||||
| use Twig\Error\RuntimeError; | ||||
| use Twig\Error\SyntaxError; | ||||
|  | ||||
| /** | ||||
|  * @implements ChillEntityRenderInterface<User> | ||||
| @@ -24,15 +30,31 @@ class UserRender implements ChillEntityRenderInterface | ||||
|         'main_scope' => true, | ||||
|         'user_job' => true, | ||||
|         'absence' => true, | ||||
|         'at' => null, | ||||
|         'at_date' => null, // instanceof DateTimeInterface | ||||
|     ]; | ||||
|  | ||||
|     public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private readonly \Twig\Environment $engine, private readonly TranslatorInterface $translator) {} | ||||
|     public function __construct( | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper, | ||||
|         private readonly \Twig\Environment $engine, | ||||
|         private readonly TranslatorInterface $translator, | ||||
|         private readonly ClockInterface $clock, | ||||
|     ) {} | ||||
|  | ||||
|     /** | ||||
|      * @throws LoaderError | ||||
|      * @throws RuntimeError | ||||
|      * @throws SyntaxError | ||||
|      */ | ||||
|     public function renderBox($entity, array $options): string | ||||
|     { | ||||
|         $opts = \array_merge(self::DEFAULT_OPTIONS, $options); | ||||
|  | ||||
|         if (null === $opts['at_date']) { | ||||
|             $opts['at_date'] = $this->clock->now(); | ||||
|         } elseif ($opts['at_date'] instanceof \DateTime) { | ||||
|             $opts['at_date'] = \DateTimeImmutable::createFromMutable($opts['at_date']); | ||||
|         } | ||||
|  | ||||
|         return $this->engine->render('@ChillMain/Entity/user.html.twig', [ | ||||
|             'user' => $entity, | ||||
|             'opts' => $opts, | ||||
| @@ -43,16 +65,24 @@ class UserRender implements ChillEntityRenderInterface | ||||
|     { | ||||
|         $opts = \array_merge(self::DEFAULT_OPTIONS, $options); | ||||
|  | ||||
|         $str = $entity->getLabel(); | ||||
|         //        $immutableAtDate = $opts['at_date'] instanceOf DateTime ? DateTimeImmutable::createFromMutable($opts['at_date']) : $opts['at_date']; | ||||
|  | ||||
|         if (null !== $entity->getUserJob($opts['at']) && $opts['user_job']) { | ||||
|             $str .= ' ('.$this->translatableStringHelper | ||||
|                 ->localize($entity->getUserJob($opts['at'])->getLabel()).')'; | ||||
|         if (null === $opts['at_date']) { | ||||
|             $opts['at_date'] = $this->clock->now(); | ||||
|         } elseif ($opts['at_date'] instanceof \DateTime) { | ||||
|             $opts['at_date'] = \DateTimeImmutable::createFromMutable($opts['at_date']); | ||||
|         } | ||||
|  | ||||
|         if (null !== $entity->getMainScope($opts['at']) && $opts['main_scope']) { | ||||
|         $str = $entity->getLabel(); | ||||
|  | ||||
|         if (null !== $entity->getUserJob($opts['at_date']) && $opts['user_job']) { | ||||
|             $str .= ' ('.$this->translatableStringHelper | ||||
|                 ->localize($entity->getMainScope($opts['at'])->getName()).')'; | ||||
|                 ->localize($entity->getUserJob($opts['at_date'])->getLabel()).')'; | ||||
|         } | ||||
|  | ||||
|         if (null !== $entity->getMainScope($opts['at_date']) && $opts['main_scope']) { | ||||
|             $str .= ' ('.$this->translatableStringHelper | ||||
|                 ->localize($entity->getMainScope($opts['at_date'])->getName()).')'; | ||||
|         } | ||||
|  | ||||
|         if ($entity->isAbsent() && $opts['absence']) { | ||||
|   | ||||
| @@ -338,15 +338,11 @@ abstract class AbstractAggregatorTest extends KernelTestCase | ||||
|                     .'is a string or an be converted to a string', $key) | ||||
|             ); | ||||
|  | ||||
|             $this->assertTrue( | ||||
|                 // conditions | ||||
|                 \is_string((string) \call_user_func($closure, '_header')) | ||||
|                 && !empty(\call_user_func($closure, '_header')) | ||||
|                 && '_header' !== \call_user_func($closure, '_header'), | ||||
|                 // message | ||||
|                 sprintf('Test that the callable return by `getLabels` for key %s ' | ||||
|                 .'can provide an header', $key) | ||||
|             ); | ||||
|             $head = \call_user_func($closure, '_header'); | ||||
|  | ||||
|             self::assertIsString($head); | ||||
|             self::assertNotEquals('', $head); | ||||
|             self::assertNotEquals('_header', $head); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,69 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Tests\Entity\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class EntityWorkflowStepSignatureTest extends KernelTestCase | ||||
| { | ||||
|     private EntityManagerInterface $entityManager; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); | ||||
|     } | ||||
|  | ||||
|     public function testConstruct() | ||||
|     { | ||||
|         $workflow = new EntityWorkflow(); | ||||
|         $workflow->setWorkflowName('vendee_internal') | ||||
|             ->setRelatedEntityId(0) | ||||
|             ->setRelatedEntityClass(AccompanyingPeriodWorkEvaluationDocument::class); | ||||
|  | ||||
|         $step = $workflow->getCurrentStep(); | ||||
|  | ||||
|         $person = $this->entityManager->createQuery('SELECT p FROM '.Person::class.' p') | ||||
|             ->setMaxResults(1) | ||||
|             ->getSingleResult(); | ||||
|  | ||||
|         $signature = new EntityWorkflowStepSignature($step, $person); | ||||
|  | ||||
|         self::assertCount(1, $step->getSignatures()); | ||||
|         self::assertSame($signature, $step->getSignatures()->first()); | ||||
|  | ||||
|         $this->entityManager->getConnection()->beginTransaction(); | ||||
|         $this->entityManager->persist($workflow); | ||||
|         $this->entityManager->persist($step); | ||||
|         $this->entityManager->persist($signature); | ||||
|  | ||||
|         $this->entityManager->flush(); | ||||
|         $this->entityManager->getConnection()->commit(); | ||||
|  | ||||
|         $this->entityManager->clear(); | ||||
|  | ||||
|         $signatureBis = $this->entityManager->find(EntityWorkflowStepSignature::class, $signature->getId()); | ||||
|  | ||||
|         self::assertEquals($signature->getId(), $signatureBis->getId()); | ||||
|         self::assertEquals($step->getId(), $signatureBis->getStep()->getId()); | ||||
|     } | ||||
| } | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Tests\Entity\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; | ||||
| use PHPUnit\Framework\TestCase; | ||||
|  | ||||
| /** | ||||
| @@ -25,7 +26,7 @@ final class EntityWorkflowTest extends TestCase | ||||
|     { | ||||
|         $entityWorkflow = new EntityWorkflow(); | ||||
|  | ||||
|         $entityWorkflow->setStep('final'); | ||||
|         $entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow)); | ||||
|         $entityWorkflow->getCurrentStep()->setIsFinal(true); | ||||
|  | ||||
|         $this->assertTrue($entityWorkflow->isFinal()); | ||||
| @@ -37,16 +38,16 @@ final class EntityWorkflowTest extends TestCase | ||||
|  | ||||
|         $this->assertFalse($entityWorkflow->isFinal()); | ||||
|  | ||||
|         $entityWorkflow->setStep('two'); | ||||
|         $entityWorkflow->setStep('two', new WorkflowTransitionContextDTO($entityWorkflow)); | ||||
|  | ||||
|         $this->assertFalse($entityWorkflow->isFinal()); | ||||
|  | ||||
|         $entityWorkflow->setStep('previous_final'); | ||||
|         $entityWorkflow->setStep('previous_final', new WorkflowTransitionContextDTO($entityWorkflow)); | ||||
|  | ||||
|         $this->assertFalse($entityWorkflow->isFinal()); | ||||
|  | ||||
|         $entityWorkflow->getCurrentStep()->setIsFinal(true); | ||||
|         $entityWorkflow->setStep('final'); | ||||
|         $entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow)); | ||||
|  | ||||
|         $this->assertTrue($entityWorkflow->isFinal()); | ||||
|     } | ||||
| @@ -57,20 +58,20 @@ final class EntityWorkflowTest extends TestCase | ||||
|  | ||||
|         $this->assertFalse($entityWorkflow->isFreeze()); | ||||
|  | ||||
|         $entityWorkflow->setStep('step_one'); | ||||
|         $entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow)); | ||||
|  | ||||
|         $this->assertFalse($entityWorkflow->isFreeze()); | ||||
|  | ||||
|         $entityWorkflow->setStep('step_three'); | ||||
|         $entityWorkflow->setStep('step_three', new WorkflowTransitionContextDTO($entityWorkflow)); | ||||
|  | ||||
|         $this->assertFalse($entityWorkflow->isFreeze()); | ||||
|  | ||||
|         $entityWorkflow->setStep('freezed'); | ||||
|         $entityWorkflow->setStep('freezed', new WorkflowTransitionContextDTO($entityWorkflow)); | ||||
|         $entityWorkflow->getCurrentStep()->setFreezeAfter(true); | ||||
|  | ||||
|         $this->assertTrue($entityWorkflow->isFreeze()); | ||||
|  | ||||
|         $entityWorkflow->setStep('after_freeze'); | ||||
|         $entityWorkflow->setStep('after_freeze', new WorkflowTransitionContextDTO($entityWorkflow)); | ||||
|  | ||||
|         $this->assertTrue($entityWorkflow->isFreeze()); | ||||
|  | ||||
|   | ||||
| @@ -25,6 +25,7 @@ use libphonenumber\PhoneNumberUtil; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Symfony\Component\Clock\MockClock; | ||||
| use Symfony\Component\Serializer\Exception\ExceptionInterface; | ||||
| use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | ||||
|  | ||||
| @@ -122,7 +123,9 @@ final class UserNormalizerTest extends TestCase | ||||
|         $userRender = $this->prophesize(UserRender::class); | ||||
|         $userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn($user ? $user->getLabel() : ''); | ||||
|  | ||||
|         $normalizer = new UserNormalizer($userRender->reveal()); | ||||
|         $clock = new MockClock(new \DateTimeImmutable('now')); | ||||
|  | ||||
|         $normalizer = new UserNormalizer($userRender->reveal(), $clock); | ||||
|         $normalizer->setNormalizer(new class () implements NormalizerInterface { | ||||
|             public function normalize($object, ?string $format = null, array $context = []) | ||||
|             { | ||||
|   | ||||
| @@ -0,0 +1,109 @@ | ||||
| <?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 Templating\Entity; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Templating\Entity\UserRender; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Symfony\Component\Clock\MockClock; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
| use Twig\Environment; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class UserRenderTest extends TestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     public function testRenderUserWithJobAndScopeAtCertainDate(): void | ||||
|     { | ||||
|         // Create a user with a certain user job | ||||
|  | ||||
|         $user = new User(); | ||||
|         $userJobA = new UserJob(); | ||||
|         $scopeA = new Scope(); | ||||
|  | ||||
|         $userJobA->setLabel(['fr' => 'assistant social']) | ||||
|             ->setActive(true); | ||||
|         $scopeA->setName(['fr' => 'service A']); | ||||
|         $user->setLabel('BOB ISLA'); | ||||
|  | ||||
|         $userJobB = new UserJob(); | ||||
|         $scopeB = new Scope(); | ||||
|  | ||||
|         $userJobB->setLabel(['fr' => 'directrice']) | ||||
|             ->setActive(true); | ||||
|         $scopeB->setName(['fr' => 'service B']); | ||||
|  | ||||
|         $userJobHistoryA = (new User\UserJobHistory()) | ||||
|             ->setUser($user) | ||||
|             ->setJob($userJobA) | ||||
|             ->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00')) | ||||
|             ->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00')); | ||||
|  | ||||
|         $userScopeHistoryA = (new User\UserScopeHistory()) | ||||
|             ->setUser($user) | ||||
|             ->setScope($scopeA) | ||||
|             ->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00')) | ||||
|             ->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00')); | ||||
|  | ||||
|         $userJobHistoryB = (new User\UserJobHistory()) | ||||
|             ->setUser($user) | ||||
|             ->setJob($userJobB) | ||||
|             ->setStartDate(new \DateTimeImmutable('2023-12-01 12:00:00')); | ||||
|  | ||||
|         $userScopeHistoryB = (new User\UserScopeHistory()) | ||||
|             ->setUser($user) | ||||
|             ->setScope($scopeB) | ||||
|             ->setStartDate(new \DateTimeImmutable('2023-12-01 12:00:00')); | ||||
|  | ||||
|         $user->getUserJobHistories()->add($userJobHistoryA); | ||||
|         $user->getUserScopeHistories()->add($userScopeHistoryA); | ||||
|  | ||||
|         $user->getUserJobHistories()->add($userJobHistoryB); | ||||
|         $user->getUserScopeHistories()->add($userScopeHistoryB); | ||||
|  | ||||
|         // Create renderer | ||||
|         $translatableStringHelperMock = $this->prophesize(TranslatableStringHelperInterface::class); | ||||
|         $translatableStringHelperMock->localize(Argument::type('array'))->will(fn ($args) => $args[0]['fr']); | ||||
|  | ||||
|         $engineMock = $this->createMock(Environment::class); | ||||
|         $translatorMock = $this->createMock(TranslatorInterface::class); | ||||
|         $clock = new MockClock(new \DateTimeImmutable('2023-12-15 12:00:00')); | ||||
|  | ||||
|         $renderer = new UserRender($translatableStringHelperMock->reveal(), $engineMock, $translatorMock, $clock); | ||||
|  | ||||
|         $optionsNoDate['at_date'] = null; | ||||
|         $options['at_date'] = new \DateTime('2023-11-25 12:00:00'); | ||||
|         $optionsTwo['at_date'] = new \DateTime('2024-01-30 12:00:00'); | ||||
|  | ||||
|         // Check that the user render for the first activity corresponds with the first user job | ||||
|         $expectedStringA = 'BOB ISLA (assistant social) (service A)'; | ||||
|         $this->assertEquals($expectedStringA, $renderer->renderString($user, $options)); | ||||
|  | ||||
|         // Check that the user render for the second activity corresponds with the second user job | ||||
|         $expectedStringB = 'BOB ISLA (directrice) (service B)'; | ||||
|         $this->assertEquals($expectedStringB, $renderer->renderString($user, $optionsTwo)); | ||||
|  | ||||
|         // Check that the user renders the job and scope that is active now, when no date is given | ||||
|         $expectedStringC = 'BOB ISLA (directrice) (service B)'; | ||||
|         $this->assertEquals($expectedStringC, $renderer->renderString($user, $optionsNoDate)); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,61 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Tests\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore; | ||||
| use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Symfony\Component\Workflow\Marking; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class EntityWorkflowMarkingStoreTest extends TestCase | ||||
| { | ||||
|     public function testGetMarking(): void | ||||
|     { | ||||
|         $markingStore = $this->buildMarkingStore(); | ||||
|         $workflow = new EntityWorkflow(); | ||||
|  | ||||
|         $marking = $markingStore->getMarking($workflow); | ||||
|  | ||||
|         self::assertEquals(['initial' => 1], $marking->getPlaces()); | ||||
|     } | ||||
|  | ||||
|     public function testSetMarking(): void | ||||
|     { | ||||
|         $markingStore = $this->buildMarkingStore(); | ||||
|         $workflow = new EntityWorkflow(); | ||||
|  | ||||
|         $dto = new WorkflowTransitionContextDTO($workflow); | ||||
|         $dto->futureCcUsers[] = $user1 = new User(); | ||||
|         $dto->futureDestUsers[] = $user2 = new User(); | ||||
|         $dto->futureDestEmails[] = $email = 'test@example.com'; | ||||
|  | ||||
|         $markingStore->setMarking($workflow, new Marking(['foo' => 1]), ['context' => $dto]); | ||||
|  | ||||
|         $currentStep = $workflow->getCurrentStep(); | ||||
|         self::assertEquals('foo', $currentStep->getCurrentStep()); | ||||
|         self::assertContains($email, $currentStep->getDestEmail()); | ||||
|         self::assertContains($user1, $currentStep->getCcUser()); | ||||
|         self::assertContains($user2, $currentStep->getDestUser()); | ||||
|     } | ||||
|  | ||||
|     private function buildMarkingStore(): EntityWorkflowMarkingStore | ||||
|     { | ||||
|         return new EntityWorkflowMarkingStore(); | ||||
|     } | ||||
| } | ||||
| @@ -49,6 +49,4 @@ interface EntityWorkflowHandlerInterface | ||||
|     public function isObjectSupported(object $object): bool; | ||||
|  | ||||
|     public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool; | ||||
|  | ||||
|     public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool; | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,49 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Symfony\Component\Workflow\Marking; | ||||
| use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface; | ||||
|  | ||||
| final readonly class EntityWorkflowMarkingStore implements MarkingStoreInterface | ||||
| { | ||||
|     public function getMarking(object $subject): Marking | ||||
|     { | ||||
|         if (!$subject instanceof EntityWorkflow) { | ||||
|             throw new \UnexpectedValueException('Expected instance of EntityWorkflow'); | ||||
|         } | ||||
|         $step = $subject->getCurrentStep(); | ||||
|  | ||||
|         return new Marking([$step->getCurrentStep() => 1]); | ||||
|     } | ||||
|  | ||||
|     public function setMarking(object $subject, Marking $marking, array $context = []): void | ||||
|     { | ||||
|         if (!$subject instanceof EntityWorkflow) { | ||||
|             throw new \UnexpectedValueException('Expected instance of EntityWorkflow'); | ||||
|         } | ||||
|  | ||||
|         $places = $marking->getPlaces(); | ||||
|         if (1 < count($places)) { | ||||
|             throw new \LogicException('Expected maximum one place'); | ||||
|         } | ||||
|         $next = array_keys($places)[0]; | ||||
|  | ||||
|         $transitionDTO = $context['context'] ?? null; | ||||
|         if (!$transitionDTO instanceof WorkflowTransitionContextDTO) { | ||||
|             throw new \UnexpectedValueException(sprintf('Expected instance of %s', WorkflowTransitionContextDTO::class)); | ||||
|         } | ||||
|  | ||||
|         $subject->setStep($next, $transitionDTO); | ||||
|     } | ||||
| } | ||||
| @@ -21,31 +21,13 @@ use Symfony\Component\Workflow\Event\Event; | ||||
| use Symfony\Component\Workflow\Event\GuardEvent; | ||||
| use Symfony\Component\Workflow\TransitionBlocker; | ||||
|  | ||||
| class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface | ||||
| final readonly class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface | ||||
| { | ||||
|     public function __construct(private readonly LoggerInterface $chillLogger, private readonly Security $security, private readonly UserRender $userRender) {} | ||||
|  | ||||
|     public function addDests(Event $event): void | ||||
|     { | ||||
|         if (!$event->getSubject() instanceof EntityWorkflow) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         /** @var EntityWorkflow $entityWorkflow */ | ||||
|         $entityWorkflow = $event->getSubject(); | ||||
|  | ||||
|         foreach ($entityWorkflow->futureCcUsers as $user) { | ||||
|             $entityWorkflow->getCurrentStep()->addCcUser($user); | ||||
|         } | ||||
|  | ||||
|         foreach ($entityWorkflow->futureDestUsers as $user) { | ||||
|             $entityWorkflow->getCurrentStep()->addDestUser($user); | ||||
|         } | ||||
|  | ||||
|         foreach ($entityWorkflow->futureDestEmails as $email) { | ||||
|             $entityWorkflow->getCurrentStep()->addDestEmail($email); | ||||
|         } | ||||
|     } | ||||
|     public function __construct( | ||||
|         private LoggerInterface $chillLogger, | ||||
|         private Security $security, | ||||
|         private UserRender $userRender | ||||
|     ) {} | ||||
|  | ||||
|     public static function getSubscribedEvents(): array | ||||
|     { | ||||
| @@ -53,7 +35,6 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac | ||||
|             'workflow.transition' => 'onTransition', | ||||
|             'workflow.completed' => [ | ||||
|                 ['markAsFinal', 2048], | ||||
|                 ['addDests', 2048], | ||||
|             ], | ||||
|             'workflow.guard' => [ | ||||
|                 ['guardEntityWorkflow', 0], | ||||
| @@ -99,6 +80,10 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac | ||||
|  | ||||
|     public function markAsFinal(Event $event): void | ||||
|     { | ||||
|         // NOTE: it is not possible to move this method to the marking store, because | ||||
|         // there is dependency between the Workflow definition and the MarkingStoreInterface (the workflow | ||||
|         // constructor need a MarkingStoreInterface) | ||||
|  | ||||
|         if (!$event->getSubject() instanceof EntityWorkflow) { | ||||
|             return; | ||||
|         } | ||||
|   | ||||
| @@ -23,7 +23,13 @@ use Symfony\Component\Workflow\Registry; | ||||
|  | ||||
| class NotificationOnTransition implements EventSubscriberInterface | ||||
| { | ||||
|     public function __construct(private readonly EntityManagerInterface $entityManager, private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Security $security, private readonly Registry $registry) {} | ||||
|     public function __construct( | ||||
|         private readonly EntityManagerInterface $entityManager, | ||||
|         private readonly \Twig\Environment $engine, | ||||
|         private readonly MetadataExtractor $metadataExtractor, | ||||
|         private readonly Security $security, | ||||
|         private readonly Registry $registry | ||||
|     ) {} | ||||
|  | ||||
|     public static function getSubscribedEvents(): array | ||||
|     { | ||||
| @@ -85,7 +91,10 @@ class NotificationOnTransition implements EventSubscriberInterface | ||||
|                 'dest' => $subscriber, | ||||
|                 'place' => $place, | ||||
|                 'workflow' => $workflow, | ||||
|                 'is_dest' => \in_array($subscriber->getId(), array_map(static fn (User $u) => $u->getId(), $entityWorkflow->futureDestUsers), true), | ||||
|                 'is_dest' => \in_array($subscriber->getId(), array_map( | ||||
|                     static fn (User $u) => $u->getId(), | ||||
|                     $entityWorkflow->getCurrentStep()->getDestUser()->toArray() | ||||
|                 ), true), | ||||
|             ]; | ||||
|  | ||||
|             $notification = new Notification(); | ||||
|   | ||||
| @@ -20,7 +20,13 @@ use Symfony\Component\Workflow\Registry; | ||||
|  | ||||
| class SendAccessKeyEventSubscriber | ||||
| { | ||||
|     public function __construct(private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Registry $registry, private readonly EntityWorkflowManager $entityWorkflowManager, private readonly MailerInterface $mailer) {} | ||||
|     public function __construct( | ||||
|         private readonly \Twig\Environment $engine, | ||||
|         private readonly MetadataExtractor $metadataExtractor, | ||||
|         private readonly Registry $registry, | ||||
|         private readonly EntityWorkflowManager $entityWorkflowManager, | ||||
|         private readonly MailerInterface $mailer | ||||
|     ) {} | ||||
|  | ||||
|     public function postPersist(EntityWorkflowStep $step): void | ||||
|     { | ||||
| @@ -32,7 +38,7 @@ class SendAccessKeyEventSubscriber | ||||
|         ); | ||||
|         $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); | ||||
|  | ||||
|         foreach ($entityWorkflow->futureDestEmails as $emailAddress) { | ||||
|         foreach ($step->getDestEmail() as $emailAddress) { | ||||
|             $context = [ | ||||
|                 'entity_workflow' => $entityWorkflow, | ||||
|                 'dest' => $emailAddress, | ||||
|   | ||||
| @@ -0,0 +1,74 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Symfony\Component\Validator\Constraints as Assert; | ||||
| use Symfony\Component\Validator\Context\ExecutionContextInterface; | ||||
| use Symfony\Component\Workflow\Transition; | ||||
|  | ||||
| /** | ||||
|  * Context for a transition on an workflow entity. | ||||
|  */ | ||||
| class WorkflowTransitionContextDTO | ||||
| { | ||||
|     /** | ||||
|      * a list of future dest users for the next steps. | ||||
|      * | ||||
|      * This is in used in order to let controller inform who will be the future users which will validate | ||||
|      * the next step. This is necessary to perform some computation about the next users, before they are | ||||
|      * associated to the entity EntityWorkflowStep. | ||||
|      * | ||||
|      * @var array|User[] | ||||
|      */ | ||||
|     public array $futureDestUsers = []; | ||||
|  | ||||
|     /** | ||||
|      * a list of future cc users for the next steps. | ||||
|      * | ||||
|      * @var array|User[] | ||||
|      */ | ||||
|     public array $futureCcUsers = []; | ||||
|  | ||||
|     /** | ||||
|      * a list of future dest emails for the next steps. | ||||
|      * | ||||
|      * This is in used in order to let controller inform who will be the future emails which will validate | ||||
|      * the next step. This is necessary to perform some computation about the next emails, before they are | ||||
|      * associated to the entity EntityWorkflowStep. | ||||
|      * | ||||
|      * @var array|string[] | ||||
|      */ | ||||
|     public array $futureDestEmails = []; | ||||
|  | ||||
|     public ?Transition $transition = null; | ||||
|  | ||||
|     public string $comment = ''; | ||||
|  | ||||
|     public function __construct( | ||||
|         public EntityWorkflow $entityWorkflow | ||||
|     ) {} | ||||
|  | ||||
|     #[Assert\Callback()] | ||||
|     public function validateCCUserIsNotInDest(ExecutionContextInterface $context, $payload): void | ||||
|     { | ||||
|         foreach ($this->futureDestUsers as $u) { | ||||
|             if (in_array($u, $this->futureCcUsers, true)) { | ||||
|                 $context | ||||
|                     ->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step') | ||||
|                     ->atPath('ccUsers') | ||||
|                     ->addViolation(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -133,6 +133,8 @@ services: | ||||
|  | ||||
|     Chill\MainBundle\Form\WorkflowStepType: ~ | ||||
|  | ||||
|     Chill\MainBundle\Form\WorkflowSignatureMetadataType: ~ | ||||
|  | ||||
|     Chill\MainBundle\Form\DataMapper\PrivateCommentDataMapper: | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|   | ||||
| @@ -20,9 +20,5 @@ services: | ||||
|  | ||||
|     chill.main.twig.chill_menu: | ||||
|         class: Chill\MainBundle\Routing\MenuTwig | ||||
|         arguments: | ||||
|             - "@chill.main.menu_composer" | ||||
|         calls: | ||||
|             - [setContainer, ["@service_container"]] | ||||
|         tags: | ||||
|             - { name: twig.extension } | ||||
|   | ||||
| @@ -0,0 +1,51 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\Migrations\Main; | ||||
|  | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
|  | ||||
| final class Version20240628095159 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'Add signatures to workflow'; | ||||
|     } | ||||
|  | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('CREATE SEQUENCE chill_main_workflow_entity_step_signature_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); | ||||
|         $this->addSql('CREATE TABLE chill_main_workflow_entity_step_signature (id INT NOT NULL, step_id INT NOT NULL, '. | ||||
|             'state VARCHAR(50) NOT NULL, stateDate TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, signatureMetadata JSON DEFAULT \'[]\' NOT NULL,'. | ||||
|             ' zoneSignatureIndex INT DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,'. | ||||
|             ' userSigner_id INT DEFAULT NULL, personSigner_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); | ||||
|         $this->addSql('CREATE INDEX IDX_C47D4BA3D934E3A4 ON chill_main_workflow_entity_step_signature (userSigner_id)'); | ||||
|         $this->addSql('CREATE INDEX IDX_C47D4BA3ADFFA293 ON chill_main_workflow_entity_step_signature (personSigner_id)'); | ||||
|         $this->addSql('CREATE INDEX IDX_C47D4BA373B21E9C ON chill_main_workflow_entity_step_signature (step_id)'); | ||||
|         $this->addSql('CREATE INDEX IDX_C47D4BA33174800F ON chill_main_workflow_entity_step_signature (createdBy_id)'); | ||||
|         $this->addSql('CREATE INDEX IDX_C47D4BA365FF1AEC ON chill_main_workflow_entity_step_signature (updatedBy_id)'); | ||||
|         $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.stateDate IS \'(DC2Type:datetimetz_immutable)\''); | ||||
|         $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.createdAt IS \'(DC2Type:datetime_immutable)\''); | ||||
|         $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.updatedAt IS \'(DC2Type:datetime_immutable)\''); | ||||
|         $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3D934E3A4 FOREIGN KEY (userSigner_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3ADFFA293 FOREIGN KEY (personSigner_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA373B21E9C FOREIGN KEY (step_id) REFERENCES chill_main_workflow_entity_step (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA33174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA365FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|     } | ||||
|  | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('DROP SEQUENCE chill_main_workflow_entity_step_signature_id_seq CASCADE'); | ||||
|         $this->addSql('DROP TABLE chill_main_workflow_entity_step_signature'); | ||||
|     } | ||||
| } | ||||
| @@ -528,6 +528,15 @@ workflow: | ||||
|     This link grant any user to apply a transition: Le lien d'accès suivant permet d'appliquer une transition | ||||
|     The workflow may be accssed through this link: Une transition peut être appliquée sur ce workflow grâce au lien d'accès suivant | ||||
|  | ||||
|     signature_zone: | ||||
|         title: Appliquer les signatures électroniques | ||||
|         button_sign: Signer | ||||
|         metadata: | ||||
|             sign_by: 'Signature pour %name%' | ||||
|             docType: Type de document | ||||
|             docNumber: Numéro de document | ||||
|             docExpiration: Date d'expiration | ||||
|  | ||||
|  | ||||
| Subscribe final: Recevoir une notification à l'étape finale | ||||
| Subscribe all steps: Recevoir une notification à chaque étape | ||||
|   | ||||
| @@ -707,19 +707,24 @@ class AccompanyingPeriod implements | ||||
|     public function getNextCalendarsForPerson(Person $person, $limit = 5): ReadableCollection | ||||
|     { | ||||
|         $today = new \DateTimeImmutable('today'); | ||||
|         $criteria = Criteria::create() | ||||
|             ->where(Criteria::expr()->gte('startDate', $today)) | ||||
|             // ->andWhere(Criteria::expr()->memberOf('persons', $person)) | ||||
|             ->orderBy(['startDate' => 'DESC']) | ||||
|             ->setMaxResults($limit * 2); | ||||
|  | ||||
|         return $this->calendars->matching($criteria) | ||||
|             ->matching( | ||||
|                 // due to a bug, filter two times | ||||
|                 Criteria::create() | ||||
|                     ->where(Criteria::expr()->memberOf('persons', $person)) | ||||
|                     ->setMaxResults($limit) | ||||
|             ); | ||||
|         $criteria = Criteria::create(); | ||||
|         $expr = Criteria::expr(); | ||||
|  | ||||
|         $criteria | ||||
|             ->where( | ||||
|                 $expr->gte('startDate', $today), | ||||
|             ) | ||||
|             ->orderBy(['startDate' => 'ASC']); | ||||
|  | ||||
|         $criteriaByPerson = Criteria::create(); | ||||
|         $criteriaByPerson | ||||
|             ->where( | ||||
|                 $expr->memberOf('persons', $person) | ||||
|             ) | ||||
|             ->setMaxResults($limit); | ||||
|  | ||||
|         return $this->calendars->matching($criteria)->matching($criteriaByPerson); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -1332,6 +1337,16 @@ class AccompanyingPeriod implements | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getUserHistories(): ReadableCollection | ||||
|     { | ||||
|         return $this->userHistories; | ||||
|     } | ||||
|  | ||||
|     public function getCurrentUserHistory(): ?UserHistory | ||||
|     { | ||||
|         return $this->getUserHistories()->findFirst(fn (int $key, UserHistory $userHistory) => null === $userHistory->getEndDate()); | ||||
|     } | ||||
|  | ||||
|     private function addStepHistory(AccompanyingPeriodStepHistory $stepHistory, array $context = []): self | ||||
|     { | ||||
|         if (!$this->stepHistories->contains($stepHistory)) { | ||||
|   | ||||
| @@ -42,9 +42,10 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues | ||||
|     /** | ||||
|      * @var Collection<AccompanyingPeriodWorkEvaluation> | ||||
|      * | ||||
|      * @internal /!\ the serialization for write evaluations is handled in `AccompanyingPeriodWorkDenormalizer` | ||||
|      * @internal the serialization for write evaluations is handled in `accompanyingperiodworkdenormalizer` | ||||
|      * @internal the serialization for context docgen:read is handled in `accompanyingperiodworknormalizer` | ||||
|      */ | ||||
|     #[Serializer\Groups(['read', 'docgen:read'])] | ||||
|     #[Serializer\Groups(['read'])] | ||||
|     #[ORM\OneToMany(targetEntity: AccompanyingPeriodWorkEvaluation::class, mappedBy: 'accompanyingPeriodWork', cascade: ['remove', 'persist'], orphanRemoval: true)] | ||||
|     #[ORM\OrderBy(['startDate' => \Doctrine\Common\Collections\Criteria::DESC, 'id' => 'DESC'])] | ||||
|     private Collection $accompanyingPeriodWorkEvaluations; | ||||
| @@ -291,18 +292,20 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues | ||||
|     /** | ||||
|      * @return ReadableCollection<int, User> | ||||
|      */ | ||||
|     #[Serializer\Groups(['read', 'docgen:read', 'read:accompanyingPeriodWork:light', 'accompanying_period_work:edit', 'accompanying_period_work:create'])] | ||||
|     #[Serializer\Groups(['accompanying_period_work:edit'])] | ||||
|     public function getReferrers(): ReadableCollection | ||||
|     { | ||||
|         $users = $this->referrersHistory | ||||
|             ->filter(fn (AccompanyingPeriodWorkReferrerHistory $h) => null === $h->getEndDate()) | ||||
|             ->map(fn (AccompanyingPeriodWorkReferrerHistory $h) => $h->getUser()) | ||||
|             ->getValues() | ||||
|         ; | ||||
|             ->getValues(); | ||||
|  | ||||
|         return new ArrayCollection(array_values($users)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return Collection<int, AccompanyingPeriodWorkReferrerHistory> | ||||
|      */ | ||||
|     public function getReferrersHistory(): Collection | ||||
|     { | ||||
|         return $this->referrersHistory; | ||||
| @@ -470,9 +473,9 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function setCreatedBy(?User $createdBy): self | ||||
|     public function setCreatedBy(?User $user): self | ||||
|     { | ||||
|         $this->createdBy = $createdBy; | ||||
|         $this->createdBy = $user; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
| @@ -514,14 +517,14 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues | ||||
|  | ||||
|     public function setStartDate(\DateTimeInterface $startDate): self | ||||
|     { | ||||
|         $this->startDate = $startDate; | ||||
|         $this->startDate = $startDate instanceof \DateTime ? \DateTimeImmutable::createFromMutable($startDate) : $startDate; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function setUpdatedAt(\DateTimeInterface $datetime): TrackUpdateInterface | ||||
|     { | ||||
|         $this->updatedAt = $datetime; | ||||
|         $this->updatedAt = $datetime instanceof \DateTime ? \DateTimeImmutable::createFromMutable($datetime) : $datetime; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|   | ||||
| @@ -132,6 +132,11 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getCreatedBy(): ?User | ||||
|     { | ||||
|         return $this->getCreator(); | ||||
|     } | ||||
|  | ||||
|     public function setUpdatedAt(\DateTimeInterface $updatedAt): self | ||||
|     { | ||||
|         $this->updatedAt = $updatedAt; | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators; | ||||
|  | ||||
| use Chill\MainBundle\Export\AggregatorInterface; | ||||
| use Chill\MainBundle\Export\DataTransformerInterface; | ||||
| use Chill\MainBundle\Form\Type\PickRollingDateType; | ||||
| use Chill\MainBundle\Repository\UserRepository; | ||||
| use Chill\MainBundle\Service\RollingDate\RollingDate; | ||||
| @@ -21,13 +22,17 @@ use Chill\PersonBundle\Export\Declarations; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
|  | ||||
| final readonly class ReferrerAggregator implements AggregatorInterface | ||||
| final readonly class ReferrerAggregator implements AggregatorInterface, DataTransformerInterface | ||||
| { | ||||
|     private const A = 'acp_ref_agg_uhistory'; | ||||
|  | ||||
|     private const P = 'acp_ref_agg_date'; | ||||
|  | ||||
|     public function __construct(private UserRepository $userRepository, private UserRender $userRender, private RollingDateConverterInterface $rollingDateConverter) {} | ||||
|     public function __construct( | ||||
|         private UserRepository $userRepository, | ||||
|         private UserRender $userRender, | ||||
|         private RollingDateConverterInterface $rollingDateConverter | ||||
|     ) {} | ||||
|  | ||||
|     public function addRole(): ?string | ||||
|     { | ||||
| @@ -44,18 +49,16 @@ final readonly class ReferrerAggregator implements AggregatorInterface | ||||
|                 $qb->expr()->orX( | ||||
|                     $qb->expr()->isNull(self::A), | ||||
|                     $qb->expr()->andX( | ||||
|                         $qb->expr()->lte(self::A.'.startDate', ':'.self::P), | ||||
|                         $qb->expr()->lt(self::A.'.startDate', ':'.self::P.'_end_date'), | ||||
|                         $qb->expr()->orX( | ||||
|                             $qb->expr()->isNull(self::A.'.endDate'), | ||||
|                             $qb->expr()->gt(self::A.'.endDate', ':'.self::P) | ||||
|                             $qb->expr()->gte(self::A.'.endDate', ':'.self::P.'_start_date') | ||||
|                         ) | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|             ->setParameter( | ||||
|                 self::P, | ||||
|                 $this->rollingDateConverter->convert($data['date_calc']) | ||||
|             ); | ||||
|             ->setParameter(':'.self::P.'_end_date', $this->rollingDateConverter->convert($data['end_date'])) | ||||
|             ->setParameter(':'.self::P.'_start_date', $this->rollingDateConverter->convert($data['end_date'])); | ||||
|     } | ||||
|  | ||||
|     public function applyOn(): string | ||||
| @@ -66,15 +69,37 @@ final readonly class ReferrerAggregator implements AggregatorInterface | ||||
|     public function buildForm(FormBuilderInterface $builder) | ||||
|     { | ||||
|         $builder | ||||
|             ->add('date_calc', PickRollingDateType::class, [ | ||||
|                 'label' => 'export.aggregator.course.by_referrer.Computation date for referrer', | ||||
|             ->add('start_date', PickRollingDateType::class, [ | ||||
|                 'label' => 'export.aggregator.course.by_referrer.Referrer after', | ||||
|                 'required' => true, | ||||
|             ]) | ||||
|             ->add('end_date', PickRollingDateType::class, [ | ||||
|                 'label' => 'export.aggregator.course.by_referrer.Until', | ||||
|                 'required' => true, | ||||
|             ]); | ||||
|     } | ||||
|  | ||||
|     public function getFormDefaultData(): array | ||||
|     { | ||||
|         return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; | ||||
|         return [ | ||||
|             'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), | ||||
|             'end_date' => new RollingDate(RollingDate::T_TODAY), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function transformData(?array $before): array | ||||
|     { | ||||
|         $default = $this->getFormDefaultData(); | ||||
|         $data = []; | ||||
|  | ||||
|         if (null === $before) { | ||||
|             return $default; | ||||
|         } | ||||
|  | ||||
|         $data['start_date'] = $before['date_calc'] ?? $before['start_date'] ?? $default['start_date']; | ||||
|         $data['end_date'] = $before['date_calc'] ?? $before['end_date'] ?? $default['end_date']; | ||||
|  | ||||
|         return $data; | ||||
|     } | ||||
|  | ||||
|     public function getLabels($key, array $values, $data) | ||||
|   | ||||
| @@ -13,20 +13,25 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User\UserScopeHistory; | ||||
| use Chill\MainBundle\Export\AggregatorInterface; | ||||
| use Chill\MainBundle\Export\DataTransformerInterface; | ||||
| use Chill\MainBundle\Form\Type\PickRollingDateType; | ||||
| use Chill\MainBundle\Repository\ScopeRepositoryInterface; | ||||
| use Chill\MainBundle\Service\RollingDate\RollingDate; | ||||
| use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Chill\PersonBundle\Export\Declarations; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
|  | ||||
| readonly class ReferrerScopeAggregator implements AggregatorInterface | ||||
| readonly class ReferrerScopeAggregator implements AggregatorInterface, DataTransformerInterface | ||||
| { | ||||
|     private const PREFIX = 'acp_agg_referrer_scope'; | ||||
|  | ||||
|     public function __construct( | ||||
|         private ScopeRepositoryInterface $scopeRepository, | ||||
|         private TranslatableStringHelperInterface $translatableStringHelper, | ||||
|         private RollingDateConverterInterface $rollingDateConverter, | ||||
|     ) {} | ||||
|  | ||||
|     public function addRole(): ?string | ||||
| @@ -46,11 +51,16 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface | ||||
|                 $qb->expr()->andX( | ||||
|                     $qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'), | ||||
|                     $qb->expr()->andX( | ||||
|                         // check that the user is referrer when the accompanying period is opened | ||||
|                         $qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"), | ||||
|                         $qb->expr()->orX( | ||||
|                             $qb->expr()->isNull("{$p}_userHistory.endDate"), | ||||
|                             $qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate") | ||||
|                         ) | ||||
|                     ), | ||||
|                     $qb->expr()->andX( | ||||
|                         "{$p}_userHistory.startDate <= :{$p}_endDate", | ||||
|                         "COALESCE({$p}_userHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate" | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
| @@ -66,9 +76,15 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface | ||||
|                             $qb->expr()->isNull("{$p}_scopeHistory.endDate"), | ||||
|                             $qb->expr()->gt("{$p}_scopeHistory.endDate", "{$p}_userHistory.startDate") | ||||
|                         ) | ||||
|                     ), | ||||
|                     $qb->expr()->andX( | ||||
|                         "{$p}_scopeHistory.startDate <= :{$p}_endDate", | ||||
|                         "COALESCE({$p}_scopeHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate" | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|             ->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date'])) | ||||
|             ->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date'])) | ||||
|             ->addSelect("IDENTITY({$p}_scopeHistory.scope) AS {$p}_select") | ||||
|             ->addGroupBy("{$p}_select"); | ||||
|     } | ||||
| @@ -78,11 +94,36 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface | ||||
|         return Declarations::ACP_TYPE; | ||||
|     } | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder) {} | ||||
|     public function buildForm(FormBuilderInterface $builder) | ||||
|     { | ||||
|         $builder | ||||
|             ->add('start_date', PickRollingDateType::class, [ | ||||
|                 'label' => 'export.aggregator.course.by_referrer_scope.Referrer and scope after', | ||||
|                 'required' => true, | ||||
|             ]) | ||||
|             ->add('end_date', PickRollingDateType::class, [ | ||||
|                 'label' => 'export.aggregator.course.by_referrer_scope.Until', | ||||
|                 'required' => true, | ||||
|             ]); | ||||
|     } | ||||
|  | ||||
|     public function getFormDefaultData(): array | ||||
|     { | ||||
|         return []; | ||||
|         return [ | ||||
|             'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), | ||||
|             'end_date' => new RollingDate(RollingDate::T_TODAY), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function transformData(?array $before): array | ||||
|     { | ||||
|         $default = $this->getFormDefaultData(); | ||||
|         $data = []; | ||||
|  | ||||
|         $data['start_date'] = $before['start_date'] ?? new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')); | ||||
|         $data['end_date'] = $before['end_date'] ?? $default['end_date']; | ||||
|  | ||||
|         return $data; | ||||
|     } | ||||
|  | ||||
|     public function getLabels($key, array $values, $data) | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user