mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-24 22:23:13 +00:00 
			
		
		
		
	Compare commits
	
		
			215 Commits
		
	
	
		
			fix_saved_
			...
			321-text-e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b5c9e65986 | |||
| 8b2af35e97 | |||
| dc44c46667 | |||
| ba571c1a69 | |||
| 6a364705f2 | |||
| b6d454691a | |||
| 6d7a6932a9 | |||
|  | 2faf194b15 | ||
| f207599d86 | |||
| b0959f8cc5 | |||
| 4c5dee5f0a | |||
| f6c98aa0d5 | |||
| 6d13d184d5 | |||
| af36eccfaf | |||
| 483a20a43f | |||
| 6d8e2ad825 | |||
| 86388a63a8 | |||
|  | 5ea55ebfe5 | ||
| f97dc8f931 | |||
|  | a9c3aab528 | ||
| 1181377bd6 | |||
|  | 2275b7c560 | ||
| 4a8d298ae5 | |||
| 3e7f03d331 | |||
| b830952b9e | |||
| ad17313c61 | |||
| 620515ad15 | |||
| 50c377ee22 | |||
| cc7e7a90ee | |||
| 1d4ef19051 | |||
| 8337a724d1 | |||
| 8ca377d5d4 | |||
| 224e0bae43 | |||
| 3aa4fac80d | |||
|  | a7517eb647 | ||
| e278e636e0 | |||
|  | 40e373a9c7 | ||
| 1c1f418b18 | |||
| bf0e14b43a | |||
| 203a098054 | |||
| d58acff541 | |||
| 5858e05a42 | |||
| b9b4fafe14 | |||
| fe6949ea26 | |||
| 8a444a12f4 | |||
| 8b7b5ceed7 | |||
| b0e826d05a | |||
| 6fb9c3af3f | |||
| 7f101ba616 | |||
| cea82fac10 | |||
| 1344fc33e1 | |||
| c8e09a28e6 | |||
| c52d4b2a0e | |||
| 7f326d5441 | |||
| 2840c06476 | |||
| 7ddf84ea5a | |||
| f202625ea8 | |||
| 7a9168fcdb | |||
| 40eb71f95a | |||
| 84b7cc8145 | |||
| 08af530726 | |||
| 03b2496817 | |||
| f638ce71fd | |||
| b39997f00a | |||
| 38b21a2159 | |||
| 17db571244 | |||
| 8c8c16c1a1 | |||
| 046d3ec9f1 | |||
| 596833f1a5 | |||
| 66e4bab558 | |||
| d1c9926bb1 | |||
| a71d066765 | |||
| 217ac7b9e7 | |||
| be901822bc | |||
| 2dcce7b826 | |||
| 7ed10efcd1 | |||
| 350661a4fa | |||
| 08207b656a | |||
| 8de63de6d6 | |||
| 51804b10c0 | |||
| 02f555efae | |||
| d2fcb6945b | |||
| dcd1777a70 | |||
| a6eb28175a | |||
| c89e3785ef | |||
| 9f17ec4841 | |||
| b277a7749a | |||
| c8b6b6e33a | |||
| 10eaebf610 | |||
| 7d78512823 | |||
| 0a34f9086f | |||
| 739e0b1692 | |||
| 8db8f5fdf5 | |||
| d0cd4792d6 | |||
| 6d196ead94 | |||
| 4047d5fd5b | |||
| 9aac80d834 | |||
| 7560dc57c6 | |||
| 10314845f6 | |||
| 9b84bc4d69 | |||
| a2fcf039be | |||
| b4d887a372 | |||
| 0aaa7122da | |||
| 1bc7f85874 | |||
| 1d2fd000aa | |||
| fc32f9eca9 | |||
| ab35e8c034 | |||
| 2aded2974f | |||
| f84c1632b2 | |||
|  | 03717a1a87 | ||
|  | 02c524dd79 | ||
| 506df432b0 | |||
| c32c18b0e2 | |||
| 321d569ee9 | |||
| cd40eb3932 | |||
| f0f2531fa3 | |||
| 183a220e7b | |||
| 9df127a82c | |||
| 04a1412562 | |||
| 3aef0a185e | |||
| 578bce31b9 | |||
| 99e4824137 | |||
| dacaaea235 | |||
| 096466e79e | |||
| 7285e5c2b0 | |||
| 37227a3aeb | |||
| 7569667189 | |||
| b0993f4062 | |||
| 7c79b65f48 | |||
| b8f25bcd45 | |||
| f4efb0e975 | |||
| c641baec78 | |||
| cc150e32f0 | |||
|  | bc7f0907ab | ||
| 26cf6459b4 | |||
| d0fa6dd512 | |||
|  | fbdc0d32f0 | ||
|  | 5f31473c90 | ||
|  | 98cf167040 | ||
|  | 6c37d798bf | ||
| 03748a7e84 | |||
| 9e3431f397 | |||
| 912861dbff | |||
| 35f25daf7c | |||
| 21274155b5 | |||
| 3f7c136d6b | |||
| 5d9c573853 | |||
| 9a5fd67842 | |||
| 2755bc12c4 | |||
| 9e191f1b5b | |||
| ab684a20ad | |||
| bc92b52498 | |||
| be5655e537 | |||
| ceb0bd982e | |||
| 47c0af3623 | |||
| f6f2efee2c | |||
| 59fd9fc63f | |||
| ec2c08681e | |||
| fedcbb9a70 | |||
| 3f1a4fe353 | |||
| fc27c73dab | |||
| 20bfd5b717 | |||
| 5e3a1eb2ab | |||
| b02820407c | |||
| 594ed4a5b4 | |||
| 88fbf7bc1c | |||
| aa26e67f6f | |||
| 21ac3eaab4 | |||
| 2ff500b00e | |||
| 19fa308c06 | |||
| 1b831bc424 | |||
| 573118e514 | |||
| 0cabf5654a | |||
| cfb547d55f | |||
| a915c35026 | |||
| 018f8aef5c | |||
| de6385ba21 | |||
| edb51dd3cd | |||
| c379bccad4 | |||
| bd9ad8a569 | |||
| 0cdd9184a3 | |||
| cb5fd2b69d | |||
| feebcf6662 | |||
| 2a61197999 | |||
| 0a53a9a9d1 | |||
| eea1e40663 | |||
| 1b0771eb07 | |||
| 3a74c48104 | |||
| 6de4861b98 | |||
| b4a1e824ac | |||
| d87cf925e2 | |||
| ce3cce7b95 | |||
| 6c97654e5e | |||
| 0787e61c22 | |||
| 73bcfb82b7 | |||
| 812e4047d0 | |||
| 999ac3af2b | |||
| 0c628c39db | |||
| c65f1d495d | |||
| 83f7086bb0 | |||
| c1e449f48e | |||
| 1f6de3cb11 | |||
| 3a2548ed89 | |||
| d7652658f2 | |||
| 67b5bc6dba | |||
| e25c1e1816 | |||
| 282b7f7fbb | |||
| ab311eaecb | |||
| b37d7fb907 | |||
| 57b8dacba0 | |||
| edcc01149b | |||
| 27b2d77fdb | |||
| 96bb98f854 | |||
| 68ed2db51e | |||
| 60386ae9ac | 
							
								
								
									
										6
									
								
								.changes/unreleased/DX-20250430-144550.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/DX-20250430-144550.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: DX | ||||
| body: Remove dead code for wopi-link module | ||||
| time: 2025-04-30T14:45:50.406111606+02:00 | ||||
| custom: | ||||
|     Issue: "352" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										7
									
								
								.changes/unreleased/Feature-20250424-142211.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.changes/unreleased/Feature-20250424-142211.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| kind: Feature | ||||
| body: Add the document file name to the document title when a user upload a document, | ||||
|   unless there is already a document title. | ||||
| time: 2025-04-24T14:22:11.800975422+02:00 | ||||
| custom: | ||||
|   Issue: "377" | ||||
|   SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250520-095628.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250520-095628.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Feature | ||||
| body: Add desactivation date for social action and issue csv export | ||||
| time: 2025-05-20T09:56:28.108941934+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										7
									
								
								.changes/unreleased/Fixed-20250424-133943.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.changes/unreleased/Fixed-20250424-133943.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| kind: Fixed | ||||
| body: trying to prevent bug of typeerror in doc-history + improved display of document | ||||
|   history | ||||
| time: 2025-04-24T13:39:43.878468232+02:00 | ||||
| custom: | ||||
|   Issue: "376" | ||||
|   SchemaChange: No schema change | ||||
							
								
								
									
										7
									
								
								.changes/unreleased/Fixed-20250424-163746.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.changes/unreleased/Fixed-20250424-163746.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| kind: Fixed | ||||
| body: Display previous participation in acc course work even if the person has left | ||||
|   the acc course | ||||
| time: 2025-04-24T16:37:46.970203594+02:00 | ||||
| custom: | ||||
|   Issue: "381" | ||||
|   SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20250505-102715.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20250505-102715.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Fixed | ||||
| body: Fix display of text in calendar events | ||||
| time: 2025-05-05T10:27:15.461493066+02:00 | ||||
| custom: | ||||
|     Issue: "372" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20250514-145339.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20250514-145339.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Fixed | ||||
| body: Add missing translation for user_group.no_user_groups | ||||
| time: 2025-05-14T14:53:39.53927329+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/UX-20250423-172624.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/UX-20250423-172624.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: UX | ||||
| body: Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page | ||||
| time: 2025-04-23T17:26:24.45777387+02:00 | ||||
| custom: | ||||
|     Issue: "374" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										6
									
								
								.changes/v3.10.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/v3.10.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| ## v3.10.0 - 2025-03-17 | ||||
| ### Feature | ||||
| * ([#363](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/363)) Display social actions grouped per social issue within activity form    | ||||
| ### Fixed | ||||
| * ([#362](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/362)) Fix Dependency Injection, which prevented to save the CalendarRange    | ||||
| * ([#368](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/368)) fix search query for user groups    | ||||
							
								
								
									
										3
									
								
								.changes/v3.10.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.10.1.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v3.10.1 - 2025-03-17 | ||||
| ### DX | ||||
| * Remove yarn dependency to symfony/ux-translator, to ease the build process | ||||
							
								
								
									
										3
									
								
								.changes/v3.10.2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.10.2.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v3.10.2 - 2025-03-17 | ||||
| ### Fixed | ||||
| * Replace a ts-expect-error with a ts-ignore    | ||||
							
								
								
									
										3
									
								
								.changes/v3.10.3.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.10.3.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v3.10.3 - 2025-03-18 | ||||
| ### DX | ||||
| * Eslint fixes    | ||||
							
								
								
									
										19
									
								
								.changes/v3.11.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								.changes/v3.11.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| ## v3.11.0 - 2025-04-17 | ||||
| ### Feature | ||||
| * ([#365](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/365)) Add counters of actions and activities, with 2 boxes to (1) show the number of active actions on total actions and (2) show the number of activities in a accompanying period, and pills in menus for showing the number of active actions and the number of activities. | ||||
| * ([#364](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/364)) Added a second phone number "telephone2" to the thirdParty entity. Adapted twig templates and vuejs apps to handle this phone number | ||||
|  | ||||
|   **Schema Change**: Add columns or tables | ||||
| * Signature: add a button to go directly to the signature zone, even if there is only one | ||||
| ### Fixed | ||||
| * Fixed wrong translations in the on-the-fly for creation of thirdParty | ||||
| * Fixed update of phone number in on-the-fly edition of thirdParty | ||||
| * Fixed closing of modal when editing thirdParty in accompanying course works | ||||
| * Shorten the delay between two execution of AccompanyingPeriodStepChangeCronjob, to ensure at least one execution in a day | ||||
| * ([#102](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/102)) Fix display of title in document list | ||||
| * When cleaning the old stored object versions, do not throw an error if the stored object is not found on disk | ||||
| * Add consistent log prefix and key to logs when stale workflows are automatically canceled | ||||
| * ([#380](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/380)) Remove the "not null" validation constraint on recently added properties on HouseholdComposition | ||||
|  | ||||
| ### DX | ||||
| * Add new chill-col style for displaying title and aside in a flex table | ||||
							
								
								
									
										3
									
								
								.changes/v3.5.3.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.5.3.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v3.5.3 - 2025-01-07 | ||||
| ### Fixed | ||||
| * Fix the EntityToJsonTransformer to return an empty array if the value is ""  | ||||
							
								
								
									
										9
									
								
								.changes/v3.6.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.changes/v3.6.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| ## v3.6.0 - 2025-01-16 | ||||
| ### Feature | ||||
| * Importer for addresses does not fails when the postal code is not found with some addresses, and compute a recap list of all addresses that could not be imported. This recap list can be send by email.  | ||||
| * ([#346](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/346)) Create a driver for storing documents on disk (instead of openstack object store) | ||||
|   | ||||
| * Add address importer from french Base d'Adresse Nationale (BAN)  | ||||
| * ([#343](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/343)) Add csv export for social issues and social actions  | ||||
| ### Fixed | ||||
| * Export: fix missing alias in activity between certain dates filter. Condition added for alias.  | ||||
							
								
								
									
										62
									
								
								.changes/v3.7.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								.changes/v3.7.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| ## v3.7.0 - 2025-01-21 | ||||
| ### Feature | ||||
| * Use the Notifier component from Symfony to sens short messages (SMS). This allow to use more provider. | ||||
| ### Fixed | ||||
| * ([#348](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/348)) [export] Fix aggregation of referrer's scope and job: fix the date range comparison | ||||
|  | ||||
| ### Warning on configuration of Notifier component | ||||
|  | ||||
| If installed in an symfony app where the recipes are activated, this configuration should be added automatically: | ||||
|  | ||||
| ```yaml | ||||
| framework: | ||||
|     notifier: | ||||
|         chatter_transports: | ||||
|         texter_transports: | ||||
|             ovhcloud: '%env(OVHCLOUD_DSN)%' | ||||
|         channel_policy: | ||||
|             # use chat/slack, chat/telegram, sms/twilio or sms/nexmo | ||||
|             urgent: ['email'] | ||||
|             high: ['email'] | ||||
|             medium: ['email'] | ||||
|             low: ['email'] | ||||
|         admin_recipients: | ||||
|             - { email: admin@example.com } | ||||
| ``` | ||||
|  | ||||
| Actually, you should either: | ||||
|  | ||||
| - remove the configuration of ovhcloud added by the recipe | ||||
| - or remove the previous configuration of chill, to avoid keeping legacy configuration | ||||
|  | ||||
| #### Remove the added configuration and keep the legacy configuration | ||||
|  | ||||
| To remove the configuration: | ||||
|  | ||||
| ```diff | ||||
| framework: | ||||
|     notifier: | ||||
|         chatter_transports: | ||||
|         texter_transports: | ||||
| -            ovhcloud: '%env(OVHCLOUD_DSN)%' | ||||
| ``` | ||||
|  | ||||
| In that case, the previous configuration, which was stored under the `chill_main.short_messages.dsn` will be reconfigured into the Notifier component's configuration. | ||||
|  | ||||
| #### Properly configure SMS | ||||
|  | ||||
| You can also properly configure it, as [described in the OVH cloud provider repository](https://github.com/symfony/ovh-cloud-notifier/tree/5.4?tab=readme-ov-file#dsn-example) (where the scheme is `ovhcloud`): | ||||
|  | ||||
| **NOTE**: You have access to all notifier available with the [Notifier component](https://symfony.com/doc/current/notifier.html#notifier-sms-channel). You are not restricted to use OVH as a provider. | ||||
|  | ||||
| ```diff | ||||
| framework: | ||||
|     notifier: | ||||
|         chatter_transports: | ||||
|         texter_transports: | ||||
| +            ovhcloud: '%env(OVHCLOUD_DSN)%' # this value should be located in a variable, and have `ovhcloud://` as a scheme | ||||
|  | ||||
| chill_main: | ||||
| -    short_messages: | ||||
| -        dsn: '%env(string:SHORT_MESSAGE_DSN)%' | ||||
| ``` | ||||
							
								
								
									
										3
									
								
								.changes/v3.7.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.7.1.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v3.7.1 - 2025-01-21 | ||||
| ### Fixed | ||||
| * Fix legacy configuration processor for notifier component    | ||||
							
								
								
									
										11
									
								
								.changes/v3.8.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.changes/v3.8.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| ## v3.8.0 - 2025-02-03 | ||||
| ### Feature | ||||
| * Improve the UX of the news item admin form to prevent wrong usage | ||||
| * ([#319](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/319)) Notification list: display the concerned person's badges in the list | ||||
| * ([#320](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/320)) Show the first 3 persons directly in the accompanying period's banner | ||||
| * ([#334](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/334)) Suggest current user when creating an activity | ||||
| * ([#331](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/331)) Add attachments to workflows | ||||
| ### Fixed | ||||
| * ([#350](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/350)) Add validation error to manual selection of person in PersonDuplicateController | ||||
| * ([#354](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/354)) Fix document category creation | ||||
| * ([#351](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/351)) Add definitive whitespace between span elements in vue PersonText component | ||||
							
								
								
									
										3
									
								
								.changes/v3.8.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.8.1.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v3.8.1 - 2025-02-05 | ||||
| ### Fixed | ||||
| * Fix household link in the parcours banner    | ||||
							
								
								
									
										3
									
								
								.changes/v3.8.2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.8.2.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v3.8.2 - 2025-02-10 | ||||
| ### Fixed | ||||
| * ([#358](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/358)) Remove "filter" button on list of documents in the workflow's "add attachement" modal    | ||||
							
								
								
									
										10
									
								
								.changes/v3.9.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.changes/v3.9.0.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| ## v3.9.0 - 2025-02-27 | ||||
| ### Feature | ||||
| * ([#349](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/349)) Suggest all referrers within actions of the accompanying period when creating an activity    | ||||
| * ([#343](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/343)) Add possibility to export a csv with all social issues and social actions    | ||||
| * ([#360](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/360)) Restore document to previous kept version when a workflow is canceled    | ||||
| * ([#341](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/341)) Add a list of third parties from within the admin (csv download)    | ||||
| ### Fixed | ||||
| * fix generation of document with accompanying period context, and list of activities and works    | ||||
| ### DX | ||||
| * ([#333](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/333)) Create an unique source of trust for translations    | ||||
							
								
								
									
										3
									
								
								.changes/v3.9.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.9.1.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v3.9.1 - 2025-02-27 | ||||
| ### Fixed | ||||
| * Fix post/patch request with missing 'type' property for gender    | ||||
							
								
								
									
										3
									
								
								.changes/v3.9.2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.9.2.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v3.9.2 - 2025-02-27 | ||||
| ### Fixed | ||||
| * Use fetchResults method to fetch all social issues instead of only the first page    | ||||
| @@ -7,15 +7,29 @@ versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}' | ||||
| kindFormat: '### {{.Kind}}' | ||||
| # Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description. | ||||
| changeFormat: >- | ||||
|     * {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{.Body}} {{ if and (.Custom.Long) (not (eq .Custom.Long "")) }} | ||||
|     * {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ .Body }}   {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }} | ||||
|  | ||||
|       **Schema Change**: {{ .Custom.SchemaChange }} | ||||
|       {{- end -}} | ||||
|  | ||||
|     {{ if and (.Custom.Long) (not (eq .Custom.Long "")) }}{{ .Custom.Long }}{{ end }} | ||||
|  | ||||
|       {{ .Custom.Long }}{{ end }} | ||||
| custom: | ||||
|     - key: SchemaChange | ||||
|       label: Is a schema change required? | ||||
|       optional: false | ||||
|       type: enum | ||||
|       enumOptions: | ||||
|           - "No schema change" | ||||
|           - "Add columns or tables" | ||||
|           - "Drop or rename table or columns, or enforce new constraint that must be manually fixed" | ||||
|  | ||||
|     -   key: Issue | ||||
|         label: Issue number (on chill-bundles repository) (optional) | ||||
|         optional: true | ||||
|         type: int | ||||
|         minInt: 1 | ||||
|  | ||||
| body: | ||||
|     # allow multiline messages | ||||
|     block: true | ||||
|   | ||||
							
								
								
									
										4
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								.env
									
									
									
									
									
								
							| @@ -88,3 +88,7 @@ REDIS_HOST=redis | ||||
| REDIS_PORT=6379 | ||||
| REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT} | ||||
| ###< chill-project/chill-bundles ### | ||||
|  | ||||
| ###> symfony/ovh-cloud-notifier ### | ||||
| # OVHCLOUD_DSN=ovhcloud://APPLICATION_KEY:APPLICATION_SECRET@default?consumer_key=CONSUMER_KEY&service_name=SERVICE_NAME | ||||
| ###< symfony/ovh-cloud-notifier ### | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -5,12 +5,15 @@ composer.lock | ||||
| docs/build/ | ||||
| .php_cs.cache | ||||
| .cache/* | ||||
| yarn.lock | ||||
|  | ||||
| docker/db/data | ||||
| docker/rabbitmq/data | ||||
|  | ||||
| # in this development bundle, we want to ignore directories related to a real app | ||||
| assets/* | ||||
| !assets/translator.ts | ||||
| !assets/ux-translator | ||||
| migrations/* | ||||
| templates/* | ||||
| translations/* | ||||
|   | ||||
| @@ -113,7 +113,7 @@ lint: | ||||
|         - export PATH="./node_modules/.bin:$PATH" | ||||
|     script: | ||||
|         - yarn install --ignore-optional | ||||
|         - npx eslint-baseline "**/*.{js,vue}" | ||||
|         - npx eslint-baseline "src/**/*.{js,ts,vue}" | ||||
|     cache: | ||||
|         paths: | ||||
|             - node_modules/ | ||||
|   | ||||
| @@ -25,7 +25,7 @@ $config = new PhpCsFixer\Config(); | ||||
| $config | ||||
|     ->setFinder($finder) | ||||
|     ->setRiskyAllowed(true) | ||||
|     ->setCacheFile('.cache/php-cs-fixer.cache') | ||||
|     ->setCacheFile('var/php-cs-fixer.cache') | ||||
|     ->setUsingCache(true) | ||||
|     ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) | ||||
| ; | ||||
|   | ||||
							
								
								
									
										139
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										139
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -6,6 +6,145 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), | ||||
| and is generated by [Changie](https://github.com/miniscruff/changie). | ||||
|  | ||||
|  | ||||
| ## v3.10.3 - 2025-03-18 | ||||
| ### DX | ||||
| * Eslint fixes    | ||||
|  | ||||
| ## v3.10.2 - 2025-03-17 | ||||
| ### Fixed | ||||
| * Replace a ts-expect-error with a ts-ignore    | ||||
|  | ||||
| ## v3.10.1 - 2025-03-17 | ||||
| ### DX | ||||
| * Remove yarn dependency to symfony/ux-translator, to ease the build process | ||||
|  | ||||
| ## v3.10.0 - 2025-03-17 | ||||
| ### Feature | ||||
| * ([#363](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/363)) Display social actions grouped per social issue within activity form    | ||||
| ### Fixed | ||||
| * ([#362](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/362)) Fix Dependency Injection, which prevented to save the CalendarRange    | ||||
| * ([#368](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/368)) fix search query for user groups    | ||||
|  | ||||
| ## v3.9.2 - 2025-02-27 | ||||
| ### Fixed | ||||
| * Use fetchResults method to fetch all social issues instead of only the first page    | ||||
|  | ||||
| ## v3.9.1 - 2025-02-27 | ||||
| ### Fixed | ||||
| * Fix post/patch request with missing 'type' property for gender    | ||||
|  | ||||
| ## v3.9.0 - 2025-02-27 | ||||
| ### Feature | ||||
| * ([#349](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/349)) Suggest all referrers within actions of the accompanying period when creating an activity    | ||||
| * ([#343](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/343)) Add possibility to export a csv with all social issues and social actions    | ||||
| * ([#360](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/360)) Restore document to previous kept version when a workflow is canceled    | ||||
| * ([#341](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/341)) Add a list of third parties from within the admin (csv download)    | ||||
| ### Fixed | ||||
| * fix generation of document with accompanying period context, and list of activities and works    | ||||
| ### DX | ||||
| * ([#333](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/333)) Create an unique source of trust for translations    | ||||
|  | ||||
| ## v3.8.2 - 2025-02-10 | ||||
| ### Fixed | ||||
| * ([#358](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/358)) Remove "filter" button on list of documents in the workflow's "add attachement" modal    | ||||
|  | ||||
| ## v3.8.1 - 2025-02-05 | ||||
| ### Fixed | ||||
| * Fix household link in the parcours banner    | ||||
|  | ||||
| ## v3.8.0 - 2025-02-03 | ||||
| ### Feature | ||||
| * Improve the UX of the news item admin form to prevent wrong usage | ||||
| * ([#319](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/319)) Notification list: display the concerned person's badges in the list | ||||
| * ([#320](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/320)) Show the first 3 persons directly in the accompanying period's banner | ||||
| * ([#334](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/334)) Suggest current user when creating an activity | ||||
| * ([#331](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/331)) Add attachments to workflows | ||||
| ### Fixed | ||||
| * ([#350](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/350)) Add validation error to manual selection of person in PersonDuplicateController | ||||
| * ([#354](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/354)) Fix document category creation | ||||
| * ([#351](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/351)) Add definitive whitespace between span elements in vue PersonText component | ||||
|  | ||||
| ## v3.7.1 - 2025-01-21 | ||||
| ### Fixed | ||||
| * Fix legacy configuration processor for notifier component    | ||||
|  | ||||
| ## v3.7.0 - 2025-01-21 | ||||
| ### Feature | ||||
| * Use the Notifier component from Symfony to sens short messages (SMS). This allow to use more provider. | ||||
| ### Fixed | ||||
| * ([#348](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/348)) [export] Fix aggregation of referrer's scope and job: fix the date range comparison | ||||
|  | ||||
| ### Warning on configuration of Notifier component | ||||
|  | ||||
| If installed in an symfony app where the recipes are activated, this configuration should be added automatically: | ||||
|  | ||||
| ```yaml | ||||
| framework: | ||||
|     notifier: | ||||
|         chatter_transports: | ||||
|         texter_transports: | ||||
|             ovhcloud: '%env(OVHCLOUD_DSN)%' | ||||
|         channel_policy: | ||||
|             # use chat/slack, chat/telegram, sms/twilio or sms/nexmo | ||||
|             urgent: ['email'] | ||||
|             high: ['email'] | ||||
|             medium: ['email'] | ||||
|             low: ['email'] | ||||
|         admin_recipients: | ||||
|             - { email: admin@example.com } | ||||
| ``` | ||||
|  | ||||
| Actually, you should either: | ||||
|  | ||||
| - remove the configuration of ovhcloud added by the recipe | ||||
| - or remove the previous configuration of chill, to avoid keeping legacy configuration | ||||
|  | ||||
| #### Remove the added configuration and keep the legacy configuration | ||||
|  | ||||
| To remove the configuration: | ||||
|  | ||||
| ```diff | ||||
| framework: | ||||
|     notifier: | ||||
|         chatter_transports: | ||||
|         texter_transports: | ||||
| -            ovhcloud: '%env(OVHCLOUD_DSN)%' | ||||
| ``` | ||||
|  | ||||
| In that case, the previous configuration, which was stored under the `chill_main.short_messages.dsn` will be reconfigured into the Notifier component's configuration. | ||||
|  | ||||
| #### Properly configure SMS | ||||
|  | ||||
| You can also properly configure it, as [described in the OVH cloud provider repository](https://github.com/symfony/ovh-cloud-notifier/tree/5.4?tab=readme-ov-file#dsn-example) (where the scheme is `ovhcloud`): | ||||
|  | ||||
| **NOTE**: You have access to all notifier available with the [Notifier component](https://symfony.com/doc/current/notifier.html#notifier-sms-channel). You are not restricted to use OVH as a provider. | ||||
|  | ||||
| ```diff | ||||
| framework: | ||||
|     notifier: | ||||
|         chatter_transports: | ||||
|         texter_transports: | ||||
| +            ovhcloud: '%env(OVHCLOUD_DSN)%' # this value should be located in a variable, and have `ovhcloud://` as a scheme | ||||
|  | ||||
| chill_main: | ||||
| -    short_messages: | ||||
| -        dsn: '%env(string:SHORT_MESSAGE_DSN)%' | ||||
| ``` | ||||
|  | ||||
| ## v3.6.0 - 2025-01-16 | ||||
| ### Feature | ||||
| * Importer for addresses does not fails when the postal code is not found with some addresses, and compute a recap list of all addresses that could not be imported. This recap list can be send by email.  | ||||
| * ([#346](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/346)) Create a driver for storing documents on disk (instead of openstack object store) | ||||
|   | ||||
| * Add address importer from french Base d'Adresse Nationale (BAN)  | ||||
| * ([#343](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/343)) Add csv export for social issues and social actions  | ||||
| ### Fixed | ||||
| * Export: fix missing alias in activity between certain dates filter. Condition added for alias.  | ||||
|  | ||||
| ## v3.5.3 - 2025-01-07 | ||||
| ### Fixed | ||||
| * Fix the EntityToJsonTransformer to return an empty array if the value is ""  | ||||
|  | ||||
| ## v3.5.2 - 2024-12-19 | ||||
| ### Fixed | ||||
| * ([#345](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/345)) Export: activity filtering of users that were associated to an activity between certain dates. Results contained activities that were not within the specified date range"  | ||||
|   | ||||
							
								
								
									
										7
									
								
								assets/translator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								assets/translator.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import { trans, setLocale, setLocaleFallbacks } from "./ux-translator"; | ||||
|  | ||||
| setLocaleFallbacks({"en": "fr", "nl": "fr", "fr": "en"}); | ||||
| setLocale('fr'); | ||||
|  | ||||
| export { trans }; | ||||
| export * from '../var/translations'; | ||||
							
								
								
									
										3
									
								
								assets/ux-translator/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								assets/ux-translator/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| This directory import the symfony ux-translator files directly into chill-bundles. | ||||
|  | ||||
| This remove the yarn dependencies from the real package, which breaks our installation. | ||||
							
								
								
									
										1
									
								
								assets/ux-translator/dist/formatters/formatter.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/ux-translator/dist/formatters/formatter.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export declare function format(id: string, parameters: Record<string, string | number>, locale: string): string; | ||||
							
								
								
									
										1
									
								
								assets/ux-translator/dist/formatters/intl-formatter.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/ux-translator/dist/formatters/intl-formatter.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export declare function formatIntl(id: string, parameters: Record<string, string | number>, locale: string): string; | ||||
							
								
								
									
										27
									
								
								assets/ux-translator/dist/translator.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								assets/ux-translator/dist/translator.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| export type DomainType = string; | ||||
| export type LocaleType = string; | ||||
| export type TranslationsType = Record<DomainType, { | ||||
|     parameters: ParametersType; | ||||
| }>; | ||||
| export type NoParametersType = Record<string, never>; | ||||
| export type ParametersType = Record<string, string | number | Date> | NoParametersType; | ||||
| export type RemoveIntlIcuSuffix<T> = T extends `${infer U}+intl-icu` ? U : T; | ||||
| export type DomainsOf<M> = M extends Message<infer Translations, LocaleType> ? keyof Translations : never; | ||||
| export type LocaleOf<M> = M extends Message<TranslationsType, infer Locale> ? Locale : never; | ||||
| export type ParametersOf<M, D extends DomainType> = M extends Message<infer Translations, LocaleType> ? Translations[D] extends { | ||||
|     parameters: infer Parameters; | ||||
| } ? Parameters : never : never; | ||||
| export interface Message<Translations extends TranslationsType, Locale extends LocaleType> { | ||||
|     id: string; | ||||
|     translations: { | ||||
|         [domain in DomainType]: { | ||||
|             [locale in Locale]: string; | ||||
|         }; | ||||
|     }; | ||||
| } | ||||
| export declare function setLocale(locale: LocaleType | null): void; | ||||
| export declare function getLocale(): LocaleType; | ||||
| export declare function throwWhenNotFound(enabled: boolean): void; | ||||
| export declare function setLocaleFallbacks(localeFallbacks: Record<LocaleType, LocaleType>): void; | ||||
| export declare function getLocaleFallbacks(): Record<LocaleType, LocaleType>; | ||||
| export declare function trans<M extends Message<TranslationsType, LocaleType>, D extends DomainsOf<M>, P extends ParametersOf<M, D>>(...args: P extends NoParametersType ? [message: M, parameters?: P, domain?: RemoveIntlIcuSuffix<D>, locale?: LocaleOf<M>] : [message: M, parameters: P, domain?: RemoveIntlIcuSuffix<D>, locale?: LocaleOf<M>]): string; | ||||
							
								
								
									
										1
									
								
								assets/ux-translator/dist/translator_controller.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/ux-translator/dist/translator_controller.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export * from './translator'; | ||||
							
								
								
									
										283
									
								
								assets/ux-translator/dist/translator_controller.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								assets/ux-translator/dist/translator_controller.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,283 @@ | ||||
| import { IntlMessageFormat } from 'intl-messageformat'; | ||||
|  | ||||
| function strtr(string, replacePairs) { | ||||
|     const regex = Object.entries(replacePairs).map(([from]) => { | ||||
|         return from.replace(/([-[\]{}()*+?.\\^$|#,])/g, '\\$1'); | ||||
|     }); | ||||
|     if (regex.length === 0) { | ||||
|         return string; | ||||
|     } | ||||
|     return string.replace(new RegExp(regex.join('|'), 'g'), (matched) => replacePairs[matched].toString()); | ||||
| } | ||||
|  | ||||
| function format(id, parameters, locale) { | ||||
|     if (null === id || '' === id) { | ||||
|         return ''; | ||||
|     } | ||||
|     if (typeof parameters['%count%'] === 'undefined' || Number.isNaN(parameters['%count%'])) { | ||||
|         return strtr(id, parameters); | ||||
|     } | ||||
|     const number = Number(parameters['%count%']); | ||||
|     let parts = []; | ||||
|     if (/^\|+$/.test(id)) { | ||||
|         parts = id.split('|'); | ||||
|     } | ||||
|     else { | ||||
|         parts = id.match(/(?:\|\||[^|])+/g) || []; | ||||
|     } | ||||
|     const intervalRegex = /^(?<interval>({\s*(-?\d+(\.\d+)?[\s*,\s*\-?\d+(.\d+)?]*)\s*})|(?<left_delimiter>[[\]])\s*(?<left>-Inf|-?\d+(\.\d+)?)\s*,\s*(?<right>\+?Inf|-?\d+(\.\d+)?)\s*(?<right_delimiter>[[\]]))\s*(?<message>.*?)$/s; | ||||
|     const standardRules = []; | ||||
|     for (let part of parts) { | ||||
|         part = part.trim().replace(/\|\|/g, '|'); | ||||
|         const matches = part.match(intervalRegex); | ||||
|         if (matches) { | ||||
|             const matchGroups = matches.groups || {}; | ||||
|             if (matches[2]) { | ||||
|                 for (const n of matches[3].split(',')) { | ||||
|                     if (number === Number(n)) { | ||||
|                         return strtr(matchGroups.message, parameters); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|                 const leftNumber = '-Inf' === matchGroups.left ? Number.NEGATIVE_INFINITY : Number(matchGroups.left); | ||||
|                 const rightNumber = ['Inf', '+Inf'].includes(matchGroups.right) | ||||
|                     ? Number.POSITIVE_INFINITY | ||||
|                     : Number(matchGroups.right); | ||||
|                 if (('[' === matchGroups.left_delimiter ? number >= leftNumber : number > leftNumber) && | ||||
|                     (']' === matchGroups.right_delimiter ? number <= rightNumber : number < rightNumber)) { | ||||
|                     return strtr(matchGroups.message, parameters); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             const ruleMatch = part.match(/^\w+:\s*(.*?)$/); | ||||
|             standardRules.push(ruleMatch ? ruleMatch[1] : part); | ||||
|         } | ||||
|     } | ||||
|     const position = getPluralizationRule(number, locale); | ||||
|     if (typeof standardRules[position] === 'undefined') { | ||||
|         if (1 === parts.length && typeof standardRules[0] !== 'undefined') { | ||||
|             return strtr(standardRules[0], parameters); | ||||
|         } | ||||
|         throw new Error(`Unable to choose a translation for "${id}" with locale "${locale}" for value "${number}". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %count% apples").`); | ||||
|     } | ||||
|     return strtr(standardRules[position], parameters); | ||||
| } | ||||
| function getPluralizationRule(number, locale) { | ||||
|     number = Math.abs(number); | ||||
|     let _locale = locale; | ||||
|     if (locale === 'pt_BR' || locale === 'en_US_POSIX') { | ||||
|         return 0; | ||||
|     } | ||||
|     _locale = _locale.length > 3 ? _locale.substring(0, _locale.indexOf('_')) : _locale; | ||||
|     switch (_locale) { | ||||
|         case 'af': | ||||
|         case 'bn': | ||||
|         case 'bg': | ||||
|         case 'ca': | ||||
|         case 'da': | ||||
|         case 'de': | ||||
|         case 'el': | ||||
|         case 'en': | ||||
|         case 'en_US_POSIX': | ||||
|         case 'eo': | ||||
|         case 'es': | ||||
|         case 'et': | ||||
|         case 'eu': | ||||
|         case 'fa': | ||||
|         case 'fi': | ||||
|         case 'fo': | ||||
|         case 'fur': | ||||
|         case 'fy': | ||||
|         case 'gl': | ||||
|         case 'gu': | ||||
|         case 'ha': | ||||
|         case 'he': | ||||
|         case 'hu': | ||||
|         case 'is': | ||||
|         case 'it': | ||||
|         case 'ku': | ||||
|         case 'lb': | ||||
|         case 'ml': | ||||
|         case 'mn': | ||||
|         case 'mr': | ||||
|         case 'nah': | ||||
|         case 'nb': | ||||
|         case 'ne': | ||||
|         case 'nl': | ||||
|         case 'nn': | ||||
|         case 'no': | ||||
|         case 'oc': | ||||
|         case 'om': | ||||
|         case 'or': | ||||
|         case 'pa': | ||||
|         case 'pap': | ||||
|         case 'ps': | ||||
|         case 'pt': | ||||
|         case 'so': | ||||
|         case 'sq': | ||||
|         case 'sv': | ||||
|         case 'sw': | ||||
|         case 'ta': | ||||
|         case 'te': | ||||
|         case 'tk': | ||||
|         case 'ur': | ||||
|         case 'zu': | ||||
|             return 1 === number ? 0 : 1; | ||||
|         case 'am': | ||||
|         case 'bh': | ||||
|         case 'fil': | ||||
|         case 'fr': | ||||
|         case 'gun': | ||||
|         case 'hi': | ||||
|         case 'hy': | ||||
|         case 'ln': | ||||
|         case 'mg': | ||||
|         case 'nso': | ||||
|         case 'pt_BR': | ||||
|         case 'ti': | ||||
|         case 'wa': | ||||
|             return number < 2 ? 0 : 1; | ||||
|         case 'be': | ||||
|         case 'bs': | ||||
|         case 'hr': | ||||
|         case 'ru': | ||||
|         case 'sh': | ||||
|         case 'sr': | ||||
|         case 'uk': | ||||
|             return 1 === number % 10 && 11 !== number % 100 | ||||
|                 ? 0 | ||||
|                 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20) | ||||
|                     ? 1 | ||||
|                     : 2; | ||||
|         case 'cs': | ||||
|         case 'sk': | ||||
|             return 1 === number ? 0 : number >= 2 && number <= 4 ? 1 : 2; | ||||
|         case 'ga': | ||||
|             return 1 === number ? 0 : 2 === number ? 1 : 2; | ||||
|         case 'lt': | ||||
|             return 1 === number % 10 && 11 !== number % 100 | ||||
|                 ? 0 | ||||
|                 : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20) | ||||
|                     ? 1 | ||||
|                     : 2; | ||||
|         case 'sl': | ||||
|             return 1 === number % 100 ? 0 : 2 === number % 100 ? 1 : 3 === number % 100 || 4 === number % 100 ? 2 : 3; | ||||
|         case 'mk': | ||||
|             return 1 === number % 10 ? 0 : 1; | ||||
|         case 'mt': | ||||
|             return 1 === number | ||||
|                 ? 0 | ||||
|                 : 0 === number || (number % 100 > 1 && number % 100 < 11) | ||||
|                     ? 1 | ||||
|                     : number % 100 > 10 && number % 100 < 20 | ||||
|                         ? 2 | ||||
|                         : 3; | ||||
|         case 'lv': | ||||
|             return 0 === number ? 0 : 1 === number % 10 && 11 !== number % 100 ? 1 : 2; | ||||
|         case 'pl': | ||||
|             return 1 === number | ||||
|                 ? 0 | ||||
|                 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14) | ||||
|                     ? 1 | ||||
|                     : 2; | ||||
|         case 'cy': | ||||
|             return 1 === number ? 0 : 2 === number ? 1 : 8 === number || 11 === number ? 2 : 3; | ||||
|         case 'ro': | ||||
|             return 1 === number ? 0 : 0 === number || (number % 100 > 0 && number % 100 < 20) ? 1 : 2; | ||||
|         case 'ar': | ||||
|             return 0 === number | ||||
|                 ? 0 | ||||
|                 : 1 === number | ||||
|                     ? 1 | ||||
|                     : 2 === number | ||||
|                         ? 2 | ||||
|                         : number % 100 >= 3 && number % 100 <= 10 | ||||
|                             ? 3 | ||||
|                             : number % 100 >= 11 && number % 100 <= 99 | ||||
|                                 ? 4 | ||||
|                                 : 5; | ||||
|         default: | ||||
|             return 0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function formatIntl(id, parameters, locale) { | ||||
|     if (id === '') { | ||||
|         return ''; | ||||
|     } | ||||
|     const intlMessage = new IntlMessageFormat(id, [locale.replace('_', '-')], undefined, { ignoreTag: true }); | ||||
|     parameters = { ...parameters }; | ||||
|     Object.entries(parameters).forEach(([key, value]) => { | ||||
|         if (key.includes('%') || key.includes('{')) { | ||||
|             delete parameters[key]; | ||||
|             parameters[key.replace(/[%{} ]/g, '').trim()] = value; | ||||
|         } | ||||
|     }); | ||||
|     return intlMessage.format(parameters); | ||||
| } | ||||
|  | ||||
| let _locale = null; | ||||
| let _localeFallbacks = {}; | ||||
| let _throwWhenNotFound = false; | ||||
| function setLocale(locale) { | ||||
|     _locale = locale; | ||||
| } | ||||
| function getLocale() { | ||||
|     return (_locale || | ||||
|         document.documentElement.getAttribute('data-symfony-ux-translator-locale') || | ||||
|         (document.documentElement.lang ? document.documentElement.lang.replace('-', '_') : null) || | ||||
|         'en'); | ||||
| } | ||||
| function throwWhenNotFound(enabled) { | ||||
|     _throwWhenNotFound = enabled; | ||||
| } | ||||
| function setLocaleFallbacks(localeFallbacks) { | ||||
|     _localeFallbacks = localeFallbacks; | ||||
| } | ||||
| function getLocaleFallbacks() { | ||||
|     return _localeFallbacks; | ||||
| } | ||||
| function trans(message, parameters = {}, domain = 'messages', locale = null) { | ||||
|     if (typeof domain === 'undefined') { | ||||
|         domain = 'messages'; | ||||
|     } | ||||
|     if (typeof locale === 'undefined' || null === locale) { | ||||
|         locale = getLocale(); | ||||
|     } | ||||
|     if (typeof message.translations === 'undefined') { | ||||
|         return message.id; | ||||
|     } | ||||
|     const localesFallbacks = getLocaleFallbacks(); | ||||
|     const translationsIntl = message.translations[`${domain}+intl-icu`]; | ||||
|     if (typeof translationsIntl !== 'undefined') { | ||||
|         while (typeof translationsIntl[locale] === 'undefined') { | ||||
|             locale = localesFallbacks[locale]; | ||||
|             if (!locale) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         if (locale) { | ||||
|             return formatIntl(translationsIntl[locale], parameters, locale); | ||||
|         } | ||||
|     } | ||||
|     const translations = message.translations[domain]; | ||||
|     if (typeof translations !== 'undefined') { | ||||
|         while (typeof translations[locale] === 'undefined') { | ||||
|             locale = localesFallbacks[locale]; | ||||
|             if (!locale) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         if (locale) { | ||||
|             return format(translations[locale], parameters, locale); | ||||
|         } | ||||
|     } | ||||
|     if (_throwWhenNotFound) { | ||||
|         throw new Error(`No translation message found with id "${message.id}".`); | ||||
|     } | ||||
|     return message.id; | ||||
| } | ||||
|  | ||||
| export { getLocale, getLocaleFallbacks, setLocale, setLocaleFallbacks, throwWhenNotFound, trans }; | ||||
							
								
								
									
										1
									
								
								assets/ux-translator/dist/utils.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/ux-translator/dist/utils.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export declare function strtr(string: string, replacePairs: Record<string, string | number>): string; | ||||
							
								
								
									
										34
									
								
								assets/ux-translator/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								assets/ux-translator/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| { | ||||
|     "name": "@symfony/ux-translator", | ||||
|     "description": "Symfony Translator for JavaScript", | ||||
|     "license": "MIT", | ||||
|     "version": "1.0.0", | ||||
|     "main": "dist/translator_controller.js", | ||||
|     "types": "dist/translator_controller.d.ts", | ||||
|     "scripts": { | ||||
|         "build": "node ../../../bin/build_package.js .", | ||||
|         "watch": "node ../../../bin/build_package.js . --watch", | ||||
|         "test": "../../../bin/test_package.sh .", | ||||
|         "check": "biome check", | ||||
|         "ci": "biome ci" | ||||
|     }, | ||||
|     "symfony": { | ||||
|         "importmap": { | ||||
|             "intl-messageformat": "^10.5.11", | ||||
|             "@symfony/ux-translator": "path:%PACKAGE%/dist/translator_controller.js", | ||||
|             "@app/translations": "path:var/translations/index.js", | ||||
|             "@app/translations/configuration": "path:var/translations/configuration.js" | ||||
|         } | ||||
|     }, | ||||
|     "peerDependencies": { | ||||
|         "intl-messageformat": "^10.5.11" | ||||
|     }, | ||||
|     "peerDependenciesMeta": { | ||||
|         "intl-messageformat": { | ||||
|             "optional": false | ||||
|         } | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "intl-messageformat": "^10.5.11" | ||||
|     } | ||||
| } | ||||
| @@ -13,10 +13,11 @@ | ||||
|         "ext-json": "*", | ||||
|         "ext-openssl": "*", | ||||
|         "ext-redis": "*", | ||||
|         "ext-zlib": "*", | ||||
|         "champs-libres/wopi-bundle": "dev-master@dev", | ||||
|         "champs-libres/wopi-lib": "dev-master@dev", | ||||
|         "doctrine/doctrine-bundle": "^2.1", | ||||
|         "doctrine/data-fixtures": "^1.8", | ||||
|         "doctrine/doctrine-bundle": "^2.1", | ||||
|         "doctrine/doctrine-migrations-bundle": "^3.0", | ||||
|         "doctrine/orm": "^2.13.0", | ||||
|         "erusev/parsedown": "^1.7", | ||||
| @@ -57,7 +58,9 @@ | ||||
|         "symfony/messenger": "^5.4", | ||||
|         "symfony/mime": "^5.4", | ||||
|         "symfony/monolog-bundle": "^3.5", | ||||
|         "symfony/notifier": "^5.4", | ||||
|         "symfony/options-resolver": "^5.4", | ||||
|         "symfony/ovh-cloud-notifier": "^5.4", | ||||
|         "symfony/process": "^5.4", | ||||
|         "symfony/property-access": "^5.4", | ||||
|         "symfony/property-info": "^5.4", | ||||
| @@ -72,6 +75,7 @@ | ||||
|         "symfony/templating": "^5.4", | ||||
|         "symfony/translation": "^5.4", | ||||
|         "symfony/twig-bundle": "^5.4", | ||||
|         "symfony/ux-translator": "^2.22", | ||||
|         "symfony/validator": "^5.4", | ||||
|         "symfony/webpack-encore-bundle": "^1.11", | ||||
|         "symfony/workflow": "^5.4", | ||||
| @@ -159,7 +163,9 @@ | ||||
|             "cache:clear": "symfony-cmd", | ||||
|             "assets:install %PUBLIC_DIR%": "symfony-cmd" | ||||
|         }, | ||||
|         "php-cs-fixer": "php-cs-fixer fix --config=./.php-cs-fixer.dist.php --show-progress=none" | ||||
|         "php-cs-fixer": "php-cs-fixer fix --config=./.php-cs-fixer.dist.php --show-progress=none", | ||||
|         "phpstan":  "phpstan --no-progress", | ||||
|         "rector": "rector --no-progress-bar" | ||||
|     }, | ||||
|     "extra": { | ||||
|         "symfony": { | ||||
|   | ||||
| @@ -36,4 +36,5 @@ return [ | ||||
|     Chill\BudgetBundle\ChillBudgetBundle::class => ['all' => true], | ||||
|     Chill\WopiBundle\ChillWopiBundle::class => ['all' => true], | ||||
|     Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], | ||||
|     Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true], | ||||
| ]; | ||||
|   | ||||
| @@ -1,4 +1,7 @@ | ||||
| chill_doc_store: | ||||
|     use_driver: openstack | ||||
|     local_storage: | ||||
|         storage_path: '%kernel.project_dir%/var/storage' | ||||
|     openstack: | ||||
|         temp_url: | ||||
|             temp_url_key:         '%env(resolve:ASYNC_UPLOAD_TEMP_URL_KEY)%'       # Required | ||||
|   | ||||
							
								
								
									
										13
									
								
								config/packages/notifier.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								config/packages/notifier.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| framework: | ||||
|     notifier: | ||||
|         texter_transports: | ||||
|             #ovhcloud: '%env(OVHCLOUD_DSN)%' | ||||
|             #ovhcloud: '%env(SHORT_MESSAGE_DSN)%' | ||||
|         channel_policy: | ||||
|             # use chat/slack, chat/telegram, sms/twilio or sms/nexmo | ||||
|             urgent: ['email'] | ||||
|             high: ['email'] | ||||
|             medium: ['email'] | ||||
|             low: ['email'] | ||||
|         admin_recipients: | ||||
|             - { email: admin@example.com } | ||||
							
								
								
									
										3
									
								
								config/packages/ux_translator.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								config/packages/ux_translator.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ux_translator: | ||||
|     # The directory where the JavaScript translations are dumped | ||||
|     dump_directory: '%kernel.project_dir%/var/translations' | ||||
| @@ -220,6 +220,7 @@ framework: | ||||
|                         - attenteModification | ||||
|                         - attenteMiseEnForme | ||||
|                         - attenteValidationMiseEnForme | ||||
|                         - attenteSignature | ||||
|                         - attenteVisa | ||||
|                         - postSignature | ||||
|                         - attenteTraitement | ||||
|   | ||||
							
								
								
									
										19
									
								
								config/routes/chill_assets_dev.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								config/routes/chill_assets_dev.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| when@dev: | ||||
|     sass_assets: | ||||
|         path: /_dev/assets | ||||
|         controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController | ||||
|         defaults: | ||||
|             template: '@ChillMain/Dev/dev.assets.html.twig' | ||||
|  | ||||
|     sass_assets_test1: | ||||
|         path: /_dev/assets_test1 | ||||
|         controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController | ||||
|         defaults: | ||||
|             template: '@ChillMain/Dev/dev.assets.test1.html.twig' | ||||
|  | ||||
|     sass_assets_test2: | ||||
|         path: /_dev/assets_test2 | ||||
|         controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController | ||||
|         defaults: | ||||
|             template: '@ChillMain/Dev/dev.assets.test2.html.twig' | ||||
|  | ||||
							
								
								
									
										12
									
								
								config/routes/chill_swagger.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								config/routes/chill_swagger.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| when@dev: | ||||
|     swagger_ui: | ||||
|         path: /_dev/swagger | ||||
|         controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController | ||||
|         defaults: | ||||
|             template: '@ChillMain/Dev/swagger-ui/index.html.twig' | ||||
|  | ||||
|     swagger_specs: | ||||
|         path: /_dev/specs.yaml | ||||
|         controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController | ||||
|         defaults: | ||||
|             template: api/specs.yaml | ||||
| @@ -12,6 +12,8 @@ This runs eslint **not** taking the baseline into account, thus showing all exis | ||||
| A script was also added to package.json allowing you to execute ``yarn run eslint``. | ||||
| This will run eslint, but **taking the baseline into account**, thus only alerting to newly created errors. | ||||
|  | ||||
| The eslint command is configured to also run ``prettier`` which will simply format the code to look more uniform (takes care indentation for example). | ||||
|  | ||||
| Interesting options that can be used in combination with eslint are: | ||||
|  | ||||
| - ``--quiet`` to only get errors and silence the warnings | ||||
|   | ||||
| @@ -16,7 +16,7 @@ Welcome to Chill documentation! | ||||
|  | ||||
| Chill is a free software for social workers. | ||||
|  | ||||
| Chill rely on the php framework `Symfony <http://symfony.com>`_.  | ||||
| Chill rely on the php framework `Symfony <http://symfony.com>`_. | ||||
|  | ||||
| Contents of this documentation: | ||||
|  | ||||
| @@ -42,7 +42,7 @@ Contribute | ||||
| User manual | ||||
| =========== | ||||
|  | ||||
| An user manual exists in French and currently focuses on describing the main concept of the software.  | ||||
| An user manual exists in French and currently focuses on describing the main concept of the software. | ||||
|  | ||||
| `Read (and contribute) to the manual <https://fr.wikibooks.org/wiki/Chill>`_ | ||||
|  | ||||
| @@ -55,12 +55,11 @@ Available bundles | ||||
|   * Chill Person, to deal with persons, | ||||
|   * chill custom fields, to add custom fields to some entities, | ||||
|   * chill activity: to add activities to people, | ||||
|   * chill report: to add report to people,  | ||||
|   * chill report: to add report to people, | ||||
|   * chill event: to gather people into events, | ||||
|   * chill docs store: to store documents to people, but also entities, | ||||
|   * chill task: to register task with people, | ||||
|   * chill third party: to register third parties, | ||||
|   * chill family members: to register family members | ||||
|  | ||||
| You will also found the following projects : | ||||
|  | ||||
|   | ||||
							
								
								
									
										84
									
								
								docs/source/installation/document-storage.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								docs/source/installation/document-storage.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| Document storage | ||||
| ################ | ||||
|  | ||||
| You can store document on two different ways: | ||||
|  | ||||
| - on disk | ||||
| - in the cloud, using object storage: currently only `openstack swift <https://docs.openstack.org/api-ref/object-store/index.html>`_ is supported. | ||||
|  | ||||
| Comparison | ||||
| ========== | ||||
|  | ||||
| Storing documents within the cloud is particularily suitable for "portable" deployments, like in kubernetes, or within container | ||||
| without having to manage volumes to store documents. But you'll have to subscribe on a commercial offer. | ||||
|  | ||||
| Storing documents on disk is more easy to configure, but more difficult to manage: if you use container, you will have to | ||||
| manager volumes to attach documents on disk. You'll have to do some backup of the directory. If chill is load-balanced (and | ||||
| multiple instances of chill are run), you will have to find a way to share the directories in read-write mode for every instance. | ||||
|  | ||||
| On Disk | ||||
| ======= | ||||
|  | ||||
| Configure Chill like this: | ||||
|  | ||||
| .. code-block:: yaml | ||||
|  | ||||
|    # file config/packages/chill_doc_store.yaml | ||||
|    chill_doc_store: | ||||
|        use_driver: local_storage | ||||
|        local_storage: | ||||
|            storage_path: '%kernel.project_dir%/var/storage' | ||||
|  | ||||
| In this configuration, documents will be stored in :code:`var/storage` within your app directory. But this path can be | ||||
| elsewhere on the disk. Be aware that the directory must be writable by the user executing the chill app (php-fpm or www-data). | ||||
|  | ||||
| Documents will be stored in subpathes within that directory. The files will be encrypted, the key is stored in the database. | ||||
|  | ||||
| In the cloud, using openstack object store | ||||
| ########################################## | ||||
|  | ||||
| You must subscribe to a commercial offer for object store. | ||||
|  | ||||
| Chill use some features to allow documents to be stored in the cloud without being uploaded first to the chill server: | ||||
|  | ||||
| - `Form POST Middelware <https://docs.openstack.org/swift/latest/api/form_post_middleware.html>`_; | ||||
| - `Temporary URL Middelware <https://docs.openstack.org/swift/latest/api/temporary_url_middleware.html>`_. | ||||
|  | ||||
| A secret key must be generated and configured, and CORS must be configured depending on the domain you will use to serve Chill. | ||||
|  | ||||
| At first, create a container and get the base path to the container. For instance, on OVH, if you create a container named "mychill", | ||||
| you will be able to retrieve the base path of the container within the OVH interface, like this: | ||||
|  | ||||
| - base_path: :code:`https://storage.gra.cloud.ovh.net/v1/AUTH_123456789/mychill/` => will be variable :code:`ASYNC_UPLOAD_TEMP_URL_BASE_PATH` | ||||
| - container: :code:`mychill` => will be variable :code:`ASYNC_UPLOAD_TEMP_URL_CONTAINER` | ||||
|  | ||||
| You can also generate a key, which should have at least 20 characters. This key will go in the variable :code:`ASYNC_UPLOAD_TEMP_URL_KEY`. | ||||
|  | ||||
| .. note:: | ||||
|  | ||||
|    See the `documentation of symfony <https://symfony.com/doc/current/configuration.html#config-env-vars>`_ on how to store variables, and how to encrypt them if needed. | ||||
|  | ||||
| Configure the storage like this: | ||||
|  | ||||
| .. code-block:: yaml | ||||
|  | ||||
|    # file config/packages/chill_doc_store.yaml | ||||
|    chill_doc_store: | ||||
|        use_driver: openstack | ||||
|        openstack: | ||||
|            temp_url: | ||||
|                temp_url_key:         '%env(resolve:ASYNC_UPLOAD_TEMP_URL_KEY)%'       # Required | ||||
|                container:            '%env(resolve:ASYNC_UPLOAD_TEMP_URL_CONTAINER)%' # Required | ||||
|                temp_url_base_path:   '%env(resolve:ASYNC_UPLOAD_TEMP_URL_BASE_PATH)%' # Required | ||||
|  | ||||
| Chill is able to configure the container in order to store document. Grab an Openstack Token (for instance, using :code:`openstack token issue` or | ||||
| the web interface of your openstack provider), and run this command: | ||||
|  | ||||
| .. code-block:: bash | ||||
|  | ||||
|    symfony console async-upload:configure --os_token=OPENSTACK_TOKEN -d https://mychill.mydomain.example | ||||
|  | ||||
|    # or, without symfony-cli | ||||
|    bin/console async-upload:configure --os_token=OPENSTACK_TOKEN -d https://mychill.mydomain.example | ||||
|  | ||||
|  | ||||
| @@ -29,8 +29,7 @@ We strongly encourage you to initialize a git repository at this step, to track | ||||
|    # add the flex endpoints required for custom recipes | ||||
|    cat <<< "$(jq '.extra.symfony += {"endpoint": ["flex://defaults", "https://gitlab.com/api/v4/projects/57371968/repository/files/index.json/raw?ref=main"]}' composer.json)" > composer.json | ||||
|    # install chill and some dependencies | ||||
|    # TODO fix the suffix "alpha1" and replace by ^3.0.0 when version 3.0.0 will be released | ||||
|    symfony composer require chill-project/chill-bundles v3.0.0-RC3 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev | ||||
|    symfony composer require chill-project/chill-bundles ^3.7.1 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev symfony/amqp-messenger | ||||
|  | ||||
| We encourage you to accept the inclusion of the "Docker configuration from recipes": this is the documented way to run the database. | ||||
| You must also accept to configure recipes from the contrib repository, unless you want to configure the bundles manually). | ||||
| @@ -48,7 +47,7 @@ You must also accept to configure recipes from the contrib repository, unless yo | ||||
|  | ||||
|    If you encounter this error during assets compilation (:code:`yarn run encore production`) (repeated multiple times): | ||||
|  | ||||
|    .. code-block:: txt | ||||
|    .. code-block:: | ||||
|  | ||||
|       [tsl] ERROR in /tmp/chill/v1/public/bundles/chillcalendar/types.ts(2,65) | ||||
|             TS2307: Cannot find module '../../../ChillMainBundle/Resources/public/types' or its corresponding type declarations. | ||||
| @@ -74,14 +73,22 @@ or in the :code:`.env.local` file, which should not be committed to the git repo | ||||
| 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. | ||||
|  | ||||
| The only required variable is the :code:`ADMIN_PASSWORD`. You can generate a hashed and salted admin password using the command | ||||
| :code:`symfony console security:hash-password <your password> 'Symfony\Component\Security\Core\User\User'`. Then, | ||||
| The required variables are: | ||||
|  | ||||
| - the :code:`ADMIN_PASSWORD`; | ||||
| - the :code:`OVHCLOUD_DSN` variable; | ||||
|  | ||||
| :code:`ADMIN_PASSWORD` | ||||
| ^^^^^^^^^^^^^^^^^^^^^^ | ||||
|  | ||||
| You can generate a hashed and salted admin password using the command | ||||
| :code:`symfony console security:hash-password <your password> 'Symfony\Component\Security\Core\User\User'`.Then, | ||||
| you can either: | ||||
|  | ||||
| - add this password to the :code:`.env.local` file, you must escape the character :code:`$`: if the generated password | ||||
|   is :code:`$2y$13$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm`, your :code:`.env.local` file will be: | ||||
|  | ||||
|   .. code-block:: env | ||||
|   .. code-block:: bash | ||||
|  | ||||
|      ADMIN_PASSWORD=\$2y\$13\$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm | ||||
|      # note: if you copy-paste the line above, the password will be "admin". | ||||
| @@ -89,12 +96,24 @@ 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 | ||||
| :code:`OVHCLOUD_DSN` and sending SMS messages | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
|  | ||||
| This is a temporary dependency, for ensuring compatibility for previous behaviour. | ||||
|  | ||||
| You can set it to :code:`null://null` if you do not plan to use sending SMS. | ||||
|  | ||||
| .. code-block:: bash | ||||
|  | ||||
|    OVHCLOUD_DSN=null://null | ||||
|  | ||||
| If you plan to do it, you can configure the notifier component `as described in the symfony documentation <https://symfony.com/doc/current/notifier.html#notifier-sms-channel>`_. | ||||
|  | ||||
|  | ||||
| Some environment variables are available for the JWT authentication bundle in the :code:`.env` file. | ||||
|  | ||||
| Prepare migrations and other tools | ||||
| ---------------------------------- | ||||
| Prepare database, messenger queue, and other configuration | ||||
| ---------------------------------------------------------- | ||||
|  | ||||
| To continue the installation process, you will have to run migrations: | ||||
|  | ||||
| @@ -109,17 +128,22 @@ To continue the installation process, you will have to run migrations: | ||||
|    symfony console messenger:setup-transports | ||||
|    # prepare some views | ||||
|    symfony console chill:db:sync-views | ||||
|    # load languages data | ||||
|    symfony console chill:main:languages:populate | ||||
|    # generate jwt token, required for some api features (webdav access, ...) | ||||
|    symfony console lexik:jwt:generate-keypair | ||||
|  | ||||
| .. warning:: | ||||
| .. note:: | ||||
|  | ||||
|    If you encounter an error while running :code:`symfony console messenger:setup-transports`, you can set up the messenger | ||||
|    transport to redis, by adding this in the :code:`.env.local` or :code:`.env` file: | ||||
|    If you encounter this error: | ||||
|  | ||||
|    .. code-block:: | ||||
|  | ||||
|      No transport supports the given Messenger DSN. | ||||
|  | ||||
|    Please check that you installed the package `symfony/amqp-messenger`. | ||||
|  | ||||
|    .. code-block:: env | ||||
|  | ||||
|       MESSENGER_TRANSPORT_DSN=redis://${REDIS_HOST}:${REDIS_PORT}/messages | ||||
|  | ||||
| Start your web server locally | ||||
| ----------------------------- | ||||
| @@ -323,6 +347,7 @@ Going further | ||||
|    :maxdepth: 2 | ||||
|  | ||||
|    prod.rst | ||||
|    document-storage.rst | ||||
|    load-addresses.rst | ||||
|    prod-calendar-sms-sending.rst | ||||
|    msgraph-configure.rst | ||||
|   | ||||
| @@ -41,16 +41,18 @@ Postal code are loaded from this database. There is no need to load postal codes | ||||
| The data are prepared for Chill (`See this repository <https://gitea.champs-libres.be/Chill-project/belgian-bestaddresses-transform/releases>`_). | ||||
| One can select postal code by his first number (:code:`1xxx` for postal codes from 1000 to 1999), or a limited list for development purpose. | ||||
|  | ||||
| The command expects a language code as first argument. | ||||
|  | ||||
| .. code-block:: bash | ||||
|  | ||||
|    # load postal code from 1000 to 3999: | ||||
|    bin/console chill:main:address-ref-from-best-addresse 1xxx 2xxx 3xxx | ||||
|    bin/console chill:main:address-ref-from-best-addresse fr 1xxx 2xxx 3xxx | ||||
|  | ||||
|    # load only an extract (for dev purposes) | ||||
|    bin/console chill:main:address-ref-from-best-addresse extract | ||||
|    bin/console chill:main:address-ref-from-best-addresse fr extract | ||||
|  | ||||
|    # load full addresses (discouraged) | ||||
|    bin/console chill:main:address-ref-from-best-addresse full | ||||
|    bin/console chill:main:address-ref-from-best-addresse fr full | ||||
|  | ||||
| .. note:: | ||||
|  | ||||
|   | ||||
| @@ -34,6 +34,7 @@ export default ts.config( | ||||
|             // override/add rules settings here, such as: | ||||
|             "vue/multi-word-component-names": "off", | ||||
|             "@typescript-eslint/no-require-imports": "off", | ||||
|             "@typescript-eslint/ban-ts-comment": "off" | ||||
|         }, | ||||
|     }, | ||||
| ); | ||||
|   | ||||
							
								
								
									
										28
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								package.json
									
									
									
									
									
								
							| @@ -6,31 +6,29 @@ | ||||
|     "@apidevtools/swagger-cli": "^4.0.4", | ||||
|     "@babel/core": "^7.20.5", | ||||
|     "@babel/preset-env": "^7.20.2", | ||||
|     "@ckeditor/ckeditor5-build-classic": "^41.4.2", | ||||
|     "@ckeditor/ckeditor5-dev-translations": "^40.2.0", | ||||
|     "@ckeditor/ckeditor5-dev-utils": "^40.2.0", | ||||
|     "@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13", | ||||
|     "@ckeditor/ckeditor5-markdown-gfm": "^41.4.2", | ||||
|     "@ckeditor/ckeditor5-theme-lark": "^41.4.2", | ||||
|     "@ckeditor/ckeditor5-vue": "^5.1.0", | ||||
|     "@ckeditor/ckeditor5-vue": "^7.3.0", | ||||
|     "@eslint/js": "^9.14.0", | ||||
|     "@hotwired/stimulus": "^3.0.0", | ||||
|     "@luminateone/eslint-baseline": "^1.0.9", | ||||
|     "@symfony/stimulus-bridge": "^3.2.0", | ||||
|     "@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets", | ||||
|     "@symfony/webpack-encore": "^4.1.0", | ||||
|     "@tsconfig/node14": "^1.0.1", | ||||
|     "@tsconfig/node20": "^20.1.4", | ||||
|     "@types/dompurify": "^3.0.5", | ||||
|     "@types/eslint__js": "^8.42.3", | ||||
|     "@typescript-eslint/parser": "^8.12.2", | ||||
|     "bindings": "^1.5.0", | ||||
|     "bootstrap": "5.2.3", | ||||
|     "chokidar": "^3.5.1", | ||||
|     "ckeditor5": "^44.1.0", | ||||
|     "dompurify": "^3.1.0", | ||||
|     "eslint": "^9.14.0", | ||||
|     "eslint-config-prettier": "^9.1.0", | ||||
|     "eslint-plugin-prettier": "^5.2.1", | ||||
|     "eslint-plugin-vue": "^9.30.0", | ||||
|     "fork-awesome": "^1.1.7", | ||||
|     "intl-messageformat": "^10.5.11", | ||||
|     "jquery": "^3.6.0", | ||||
|     "marked": "^12.0.1", | ||||
|     "node-sass": "^8.0.0", | ||||
|     "popper.js": "^1.16.1", | ||||
|     "postcss-loader": "^7.0.2", | ||||
| @@ -55,11 +53,13 @@ | ||||
|     "@fullcalendar/timegrid": "^6.1.4", | ||||
|     "@fullcalendar/vue3": "^6.1.4", | ||||
|     "@popperjs/core": "^2.9.2", | ||||
|     "@tsconfig/node20": "^20.1.4", | ||||
|     "@types/dompurify": "^3.0.5", | ||||
|     "@types/leaflet": "^1.9.3", | ||||
|     "bootstrap-icons": "^1.11.3", | ||||
|     "dropzone": "^5.7.6", | ||||
|     "es6-promise": "^4.2.8", | ||||
|     "intl-messageformat": "^10.5.11", | ||||
|     "leaflet": "^1.7.1", | ||||
|     "marked": "^12.0.2", | ||||
|     "masonry-layout": "^4.2.2", | ||||
| @@ -73,15 +73,19 @@ | ||||
|     "vuex": "^4.0.0" | ||||
|   }, | ||||
|   "browserslist": [ | ||||
|     "Firefox ESR" | ||||
|     "defaults and fully supports es6-module and not dead" | ||||
|   ], | ||||
|   "scripts": { | ||||
|     "dev-server": "encore dev-server", | ||||
|     "dev": "encore dev", | ||||
|     "prettier": "prettier --write \"**/*.{js,ts,vue}\"", | ||||
|     "watch": "encore dev --watch", | ||||
|     "build": "encore production --progress", | ||||
|     "eslint": "npx eslint-baseline \"**/*.{js,ts,vue}\"" | ||||
|     "specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml> templates/api/specs.yaml", | ||||
|     "specs-validate": "swagger-cli validate templates/api/specs.yaml", | ||||
|     "specs-create-dir": "mkdir -p templates/api", | ||||
|     "specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate", | ||||
|     "version": "node --version", | ||||
|     "eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\"" | ||||
|   }, | ||||
|   "private": true | ||||
| } | ||||
|   | ||||
| @@ -20,6 +20,10 @@ return static function (RectorConfig $rectorConfig): void { | ||||
|         __DIR__ . '/src', | ||||
|     ]); | ||||
|  | ||||
|     $rectorConfig->skip([ | ||||
|         \Rector\Php55\Rector\String_\StringClassNameToClassConstantRector::class => __DIR__ . 'src/Bundle/ChillMainBundle/Service/Notifier/LegacyOvhCloudFactory.php' | ||||
|     ]); | ||||
|  | ||||
|     $rectorConfig->symfonyContainerXml(__DIR__ . '/var/cache/dev/test/App_KernelTestDebugContainer.xml  '); | ||||
|     $rectorConfig->symfonyContainerPhp(__DIR__ . '/tests/symfony-container.php'); | ||||
|  | ||||
|   | ||||
| @@ -55,7 +55,9 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem | ||||
|             .' AND ' | ||||
|             .'(person_person_having_activity.id = person.id OR person MEMBER OF activity_person_having_activity.persons)'); | ||||
|  | ||||
|         $sqb->andWhere('activity_person_having_activity.id = activity.id'); | ||||
|         if (\in_array('activity', $qb->getAllAliases(), true)) { | ||||
|             $sqb->andWhere('activity_person_having_activity.id = activity.id'); | ||||
|         } | ||||
|  | ||||
|         if (isset($data['reasons']) && [] !== $data['reasons']) { | ||||
|             // add clause activity reason | ||||
|   | ||||
| @@ -11,6 +11,7 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\ActivityBundle\Menu; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\Activity; | ||||
| use Chill\ActivityBundle\Security\Authorization\ActivityVoter; | ||||
| use Chill\MainBundle\Routing\LocalMenuBuilderInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| @@ -23,22 +24,30 @@ use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  */ | ||||
| class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface | ||||
| { | ||||
|     public function __construct(protected Security $security, protected TranslatorInterface $translator) {} | ||||
|     public function __construct( | ||||
|         protected Security $security, | ||||
|         protected TranslatorInterface $translator, | ||||
|         private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry, | ||||
|     ) {} | ||||
|  | ||||
|     public function buildMenu($menuId, MenuItem $menu, array $parameters) | ||||
|     { | ||||
|         $period = $parameters['accompanyingCourse']; | ||||
|  | ||||
|         $activities = $this->managerRegistry->getManager()->getRepository(Activity::class)->findBy( | ||||
|             ['accompanyingPeriod' => $period] | ||||
|         ); | ||||
|  | ||||
|         if ( | ||||
|             AccompanyingPeriod::STEP_DRAFT !== $period->getStep() | ||||
|             && $this->security->isGranted(ActivityVoter::SEE, $period) | ||||
|         ) { | ||||
|             $menu->addChild($this->translator->trans('Activity'), [ | ||||
|             $menu->addChild($this->translator->trans('Activities'), [ | ||||
|                 'route' => 'chill_activity_activity_list', | ||||
|                 'routeParameters' => [ | ||||
|                     'accompanying_period_id' => $period->getId(), | ||||
|                 ], ]) | ||||
|                 ->setExtras(['order' => 40]); | ||||
|                 ->setExtras(['order' => 40, 'counter' => count($activities) > 0 ? count($activities) : null]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -11,6 +11,7 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\ActivityBundle\Menu; | ||||
|  | ||||
| use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface; | ||||
| use Chill\ActivityBundle\Security\Authorization\ActivityVoter; | ||||
| use Chill\MainBundle\Routing\LocalMenuBuilderInterface; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| @@ -23,13 +24,20 @@ use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  */ | ||||
| final readonly class PersonMenuBuilder implements LocalMenuBuilderInterface | ||||
| { | ||||
|     public function __construct(private AuthorizationCheckerInterface $authorizationChecker, private TranslatorInterface $translator) {} | ||||
|     public function __construct( | ||||
|         private readonly ActivityACLAwareRepositoryInterface $activityACLAwareRepository, | ||||
|         private AuthorizationCheckerInterface $authorizationChecker, | ||||
|         private TranslatorInterface $translator, | ||||
|     ) {} | ||||
|  | ||||
|     public function buildMenu($menuId, MenuItem $menu, array $parameters) | ||||
|     { | ||||
|         /** @var Person $person */ | ||||
|         $person = $parameters['person']; | ||||
|  | ||||
|  | ||||
|         $count = $this->activityACLAwareRepository->countByPerson($person, ActivityVoter::SEE); | ||||
|  | ||||
|         if ($this->authorizationChecker->isGranted(ActivityVoter::SEE, $person)) { | ||||
|             $menu->addChild( | ||||
|                 $this->translator->trans('Activities'), | ||||
| @@ -38,7 +46,7 @@ final readonly class PersonMenuBuilder implements LocalMenuBuilderInterface | ||||
|                     'routeParameters' => ['person_id' => $person->getId()], | ||||
|                 ] | ||||
|             ) | ||||
|                 ->setExtra('order', 201); | ||||
|                 ->setExtras(['order' => 201, 'counter' => $count > 0 ? $count : null]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -15,10 +15,13 @@ use Chill\ActivityBundle\Entity\Activity; | ||||
| use Chill\ActivityBundle\Repository\ActivityRepository; | ||||
| use Chill\MainBundle\Entity\Notification; | ||||
| use Chill\MainBundle\Notification\NotificationHandlerInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Symfony\Component\Translation\TranslatableMessage; | ||||
| use Symfony\Contracts\Translation\TranslatableInterface; | ||||
|  | ||||
| final readonly class ActivityNotificationHandler implements NotificationHandlerInterface | ||||
| { | ||||
|     public function __construct(private ActivityRepository $activityRepository) {} | ||||
|     public function __construct(private ActivityRepository $activityRepository, private TranslatableStringHelperInterface $translatableStringHelper) {} | ||||
|  | ||||
|     public function getTemplate(Notification $notification, array $options = []): string | ||||
|     { | ||||
| @@ -37,4 +40,30 @@ final readonly class ActivityNotificationHandler implements NotificationHandlerI | ||||
|     { | ||||
|         return Activity::class === $notification->getRelatedEntityClass(); | ||||
|     } | ||||
|  | ||||
|     public function getTitle(Notification $notification, array $options = []): TranslatableInterface | ||||
|     { | ||||
|         if (null === $activity = $this->getRelatedEntity($notification)) { | ||||
|             return new TranslatableMessage('activity.deleted'); | ||||
|         } | ||||
|  | ||||
|         return new TranslatableMessage('activity.title', [ | ||||
|             'date' => $activity->getDate(), | ||||
|             'type' => $this->translatableStringHelper->localize($activity->getActivityType()->getName()), | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     public function getAssociatedPersons(Notification $notification, array $options = []): array | ||||
|     { | ||||
|         if (null === $activity = $this->getRelatedEntity($notification)) { | ||||
|             return []; | ||||
|         } | ||||
|  | ||||
|         return $activity->getPersonsAssociated(); | ||||
|     } | ||||
|  | ||||
|     public function getRelatedEntity(Notification $notification): ?Activity | ||||
|     { | ||||
|         return $this->activityRepository->find($notification->getRelatedEntityId()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -120,3 +120,34 @@ li.document-list-item { | ||||
|         vertical-align: baseline; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .badge-activity-type-simple { | ||||
|     @extend .badge; | ||||
|     display: inline-block; | ||||
|     margin: 0.2rem 0; | ||||
|     padding-left: 0; | ||||
|     padding-right: 0.5rem; | ||||
|  | ||||
|     border-left: 20px groove #9acd32; | ||||
|     border-radius: $badge-border-radius; | ||||
|  | ||||
|     color: black; | ||||
|     font-weight: normal; | ||||
|     font-size: unset; | ||||
|     max-width: 100%; | ||||
|     background-color: $gray-100; | ||||
|  | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     text-indent: 5px hanging; | ||||
|     text-align: left; | ||||
|  | ||||
|     &::before { | ||||
|         margin-right: 3px; | ||||
|         position: relative; | ||||
|         left: -0.5px; | ||||
|         font-family: ForkAwesome; | ||||
|         content: '\f04b'; | ||||
|         color: #9acd32; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import Location from "./components/Location.vue"; | ||||
|  | ||||
| export default { | ||||
|     name: "App", | ||||
|     props: ["hasSocialIssues", "hasLocation", "hasPerson"], | ||||
|     props: ["hasSocialIssues", "hasLocation", "hasPerson", "isSimpleEditor"], | ||||
|     components: { | ||||
|         ConcernedGroups, | ||||
|         SocialIssuesAcc, | ||||
|   | ||||
| @@ -30,13 +30,14 @@ | ||||
|         <ul class="record_actions"> | ||||
|             <li class="add-persons"> | ||||
|                 <add-persons | ||||
|                     button-title="activity.add_persons" | ||||
|                     modal-title="activity.add_persons" | ||||
|                     :key="addPersons.key" | ||||
|                     :options="addPersonsOptions" | ||||
|                     @add-new-persons="addNewPersons" | ||||
|                     :buttonTitle="trans(ACTIVITY_ADD_PERSONS)" | ||||
|                     :modalTitle="trans(ACTIVITY_ADD_PERSONS)" | ||||
|                     v-bind:key="addPersons.key" | ||||
|                     v-bind:options="addPersonsOptions" | ||||
|                     @addNewPersons="addNewPersons" | ||||
|                     ref="addPersons" | ||||
|                 /> | ||||
|                 > | ||||
|                 </add-persons> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </teleport> | ||||
| @@ -47,6 +48,14 @@ import { mapState, mapGetters } from "vuex"; | ||||
| import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue"; | ||||
| import PersonsBloc from "./ConcernedGroups/PersonsBloc.vue"; | ||||
| import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue"; | ||||
| import { | ||||
|     ACTIVITY_BLOC_PERSONS, | ||||
|     ACTIVITY_BLOC_PERSONS_ASSOCIATED, | ||||
|     ACTIVITY_BLOC_THIRDPARTY, | ||||
|     ACTIVITY_BLOC_USERS, | ||||
|     ACTIVITY_ADD_PERSONS, | ||||
|     trans, | ||||
| } from "translator"; | ||||
|  | ||||
| export default { | ||||
|     name: "ConcernedGroups", | ||||
| @@ -55,18 +64,24 @@ export default { | ||||
|         PersonsBloc, | ||||
|         PersonText, | ||||
|     }, | ||||
|     setup() { | ||||
|         return { | ||||
|             trans, | ||||
|             ACTIVITY_ADD_PERSONS, | ||||
|         }; | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             personsBlocs: [ | ||||
|                 { | ||||
|                     key: "persons", | ||||
|                     title: "activity.bloc_persons", | ||||
|                     title: trans(ACTIVITY_BLOC_PERSONS), | ||||
|                     persons: [], | ||||
|                     included: false, | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "personsAssociated", | ||||
|                     title: "activity.bloc_persons_associated", | ||||
|                     title: trans(ACTIVITY_BLOC_PERSONS_ASSOCIATED), | ||||
|                     persons: [], | ||||
|                     included: window.activity | ||||
|                         ? window.activity.activityType.personsVisible !== 0 | ||||
| @@ -82,7 +97,7 @@ export default { | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "thirdparty", | ||||
|                     title: "activity.bloc_thirdparty", | ||||
|                     title: trans(ACTIVITY_BLOC_THIRDPARTY), | ||||
|                     persons: [], | ||||
|                     included: window.activity | ||||
|                         ? window.activity.activityType.thirdPartiesVisible !== 0 | ||||
| @@ -90,7 +105,7 @@ export default { | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "users", | ||||
|                     title: "activity.bloc_users", | ||||
|                     title: trans(ACTIVITY_BLOC_USERS), | ||||
|                     persons: [], | ||||
|                     included: window.activity | ||||
|                         ? window.activity.activityType.usersVisible !== 0 | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|     <teleport to="#location"> | ||||
|         <div class="mb-3 row"> | ||||
|             <label :class="locationClassList"> | ||||
|                 {{ $t("activity.location") }} | ||||
|                 {{ trans(ACTIVITY_LOCATION) }} | ||||
|             </label> | ||||
|             <div class="col-sm-8"> | ||||
|                 <VueMultiselect | ||||
| @@ -13,17 +13,17 @@ | ||||
|                     open-direction="top" | ||||
|                     :multiple="false" | ||||
|                     :searchable="true" | ||||
|                     :placeholder="$t('activity.choose_location')" | ||||
|                     :placeholder="trans(ACTIVITY_CHOOSE_LOCATION)" | ||||
|                     :custom-label="customLabel" | ||||
|                     :select-label="$t('multiselect.select_label')" | ||||
|                     :deselect-label="$t('multiselect.deselect_label')" | ||||
|                     :selected-label="$t('multiselect.selected_label')" | ||||
|                     :select-label="trans(MULTISELECT_SELECT_LABEL)" | ||||
|                     :deselect-label="trans(MULTISELECT_DESELECT_LABEL)" | ||||
|                     :selected-label="trans(MULTISELECT_SELECTED_LABEL)" | ||||
|                     :options="availableLocations" | ||||
|                     group-values="locations" | ||||
|                     group-label="locationGroup" | ||||
|                     v-model="location" | ||||
|                 /> | ||||
|                 <new-location :available-locations="availableLocations" /> | ||||
|                 <new-location v-bind:available-locations="availableLocations" /> | ||||
|             </div> | ||||
|         </div> | ||||
|     </teleport> | ||||
| @@ -33,6 +33,14 @@ | ||||
| import { mapState, mapGetters } from "vuex"; | ||||
| import VueMultiselect from "vue-multiselect"; | ||||
| import NewLocation from "./Location/NewLocation.vue"; | ||||
| import { | ||||
|     trans, | ||||
|     ACTIVITY_LOCATION, | ||||
|     ACTIVITY_CHOOSE_LOCATION, | ||||
|     MULTISELECT_SELECT_LABEL, | ||||
|     MULTISELECT_DESELECT_LABEL, | ||||
|     MULTISELECT_SELECTED_LABEL, | ||||
| } from "translator"; | ||||
|  | ||||
| export default { | ||||
|     name: "Location", | ||||
| @@ -40,6 +48,16 @@ export default { | ||||
|         NewLocation, | ||||
|         VueMultiselect, | ||||
|     }, | ||||
|     setup() { | ||||
|         return { | ||||
|             trans, | ||||
|             ACTIVITY_LOCATION, | ||||
|             ACTIVITY_CHOOSE_LOCATION, | ||||
|             MULTISELECT_SELECT_LABEL, | ||||
|             MULTISELECT_DESELECT_LABEL, | ||||
|             MULTISELECT_SELECTED_LABEL, | ||||
|         }; | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             locationClassList: `col-form-label col-sm-4 ${document.querySelector("input#chill_activitybundle_activity_location").getAttribute("required") ? "required" : ""}`, | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|         <ul class="record_actions"> | ||||
|             <li> | ||||
|                 <a class="btn btn-sm btn-create" @click="openModal"> | ||||
|                     {{ $t("activity.create_new_location") }} | ||||
|                     {{ trans(ACTIVITY_CREATE_NEW_LOCATION) }} | ||||
|                 </a> | ||||
|             </li> | ||||
|         </ul> | ||||
| @@ -11,12 +11,12 @@ | ||||
|         <teleport to="body"> | ||||
|             <modal | ||||
|                 v-if="modal.showModal" | ||||
|                 :modal-dialog-class="modal.modalDialogClass" | ||||
|                 :modalDialogClass="modal.modalDialogClass" | ||||
|                 @close="modal.showModal = false" | ||||
|             > | ||||
|                 <template #header> | ||||
|                     <h3 class="modal-title"> | ||||
|                         {{ $t("activity.create_new_location") }} | ||||
|                         {{ trans(ACTIVITY_CREATE_NEW_LOCATION) }} | ||||
|                     </h3> | ||||
|                 </template> | ||||
|                 <template #body> | ||||
| @@ -37,7 +37,7 @@ | ||||
|                                 v-model="selectType" | ||||
|                             > | ||||
|                                 <option selected disabled value=""> | ||||
|                                     {{ $t("activity.choose_location_type") }} | ||||
|                                     {{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }} | ||||
|                                 </option> | ||||
|                                 <option | ||||
|                                     v-for="t in locationTypes" | ||||
| @@ -48,7 +48,7 @@ | ||||
|                                 </option> | ||||
|                             </select> | ||||
|                             <label>{{ | ||||
|                                 $t("activity.location_fields.type") | ||||
|                                 trans(ACTIVITY_LOCATION_FIELDS_TYPE) | ||||
|                             }}</label> | ||||
|                         </div> | ||||
|  | ||||
| @@ -60,14 +60,14 @@ | ||||
|                                 placeholder | ||||
|                             /> | ||||
|                             <label for="name">{{ | ||||
|                                 $t("activity.location_fields.name") | ||||
|                                 trans(ACTIVITY_LOCATION_FIELDS_NAME) | ||||
|                             }}</label> | ||||
|                         </div> | ||||
|  | ||||
|                         <add-address | ||||
|                             :context="addAddress.context" | ||||
|                             :options="addAddress.options" | ||||
|                             :address-changed-callback="submitNewAddress" | ||||
|                             :addressChangedCallback="submitNewAddress" | ||||
|                             v-if="showAddAddress" | ||||
|                             ref="addAddress" | ||||
|                         /> | ||||
| @@ -80,7 +80,7 @@ | ||||
|                                 placeholder | ||||
|                             /> | ||||
|                             <label for="phonenumber1">{{ | ||||
|                                 $t("activity.location_fields.phonenumber1") | ||||
|                                 trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER1) | ||||
|                             }}</label> | ||||
|                         </div> | ||||
|                         <div class="form-floating mb-3" v-if="hasPhonenumber1"> | ||||
| @@ -91,7 +91,7 @@ | ||||
|                                 placeholder | ||||
|                             /> | ||||
|                             <label for="phonenumber2">{{ | ||||
|                                 $t("activity.location_fields.phonenumber2") | ||||
|                                 trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER2) | ||||
|                             }}</label> | ||||
|                         </div> | ||||
|                         <div class="form-floating mb-3" v-if="showContactData"> | ||||
| @@ -102,7 +102,7 @@ | ||||
|                                 placeholder | ||||
|                             /> | ||||
|                             <label for="email">{{ | ||||
|                                 $t("activity.location_fields.email") | ||||
|                                 trans(ACTIVITY_LOCATION_FIELDS_EMAIL) | ||||
|                             }}</label> | ||||
|                         </div> | ||||
|                     </form> | ||||
| @@ -112,7 +112,7 @@ | ||||
|                         class="btn btn-save" | ||||
|                         @click.prevent="saveNewLocation" | ||||
|                     > | ||||
|                         {{ $t("action.save") }} | ||||
|                         {{ trans(SAVE) }} | ||||
|                     </button> | ||||
|                 </template> | ||||
|             </modal> | ||||
| @@ -126,6 +126,17 @@ import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue" | ||||
| import { mapState } from "vuex"; | ||||
| import { getLocationTypes } from "../../api"; | ||||
| import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; | ||||
| import { | ||||
|     SAVE, | ||||
|     ACTIVITY_LOCATION_FIELDS_EMAIL, | ||||
|     ACTIVITY_LOCATION_FIELDS_PHONENUMBER1, | ||||
|     ACTIVITY_LOCATION_FIELDS_PHONENUMBER2, | ||||
|     ACTIVITY_LOCATION_FIELDS_NAME, | ||||
|     ACTIVITY_LOCATION_FIELDS_TYPE, | ||||
|     ACTIVITY_CHOOSE_LOCATION_TYPE, | ||||
|     ACTIVITY_CREATE_NEW_LOCATION, | ||||
|     trans, | ||||
| } from "translator"; | ||||
|  | ||||
| export default { | ||||
|     name: "NewLocation", | ||||
| @@ -133,6 +144,19 @@ export default { | ||||
|         Modal, | ||||
|         AddAddress, | ||||
|     }, | ||||
|     setup() { | ||||
|         return { | ||||
|             trans, | ||||
|             SAVE, | ||||
|             ACTIVITY_LOCATION_FIELDS_EMAIL, | ||||
|             ACTIVITY_LOCATION_FIELDS_PHONENUMBER1, | ||||
|             ACTIVITY_LOCATION_FIELDS_PHONENUMBER2, | ||||
|             ACTIVITY_LOCATION_FIELDS_NAME, | ||||
|             ACTIVITY_LOCATION_FIELDS_TYPE, | ||||
|             ACTIVITY_CHOOSE_LOCATION_TYPE, | ||||
|             ACTIVITY_CREATE_NEW_LOCATION, | ||||
|         }; | ||||
|     }, | ||||
|     props: ["availableLocations"], | ||||
|     data() { | ||||
|         return { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|         <div class="mb-3 row"> | ||||
|             <div class="col-4"> | ||||
|                 <label :class="socialIssuesClassList">{{ | ||||
|                     $t("activity.social_issues") | ||||
|                     trans(ACTIVITY_SOCIAL_ISSUES) | ||||
|                 }}</label> | ||||
|             </div> | ||||
|             <div class="col-8"> | ||||
| @@ -12,8 +12,9 @@ | ||||
|                     :key="issue.id" | ||||
|                     :issue="issue" | ||||
|                     :selection="socialIssuesSelected" | ||||
|                     @update-selected="updateIssuesSelected" | ||||
|                 /> | ||||
|                     @updateSelected="updateIssuesSelected" | ||||
|                 > | ||||
|                 </check-social-issue> | ||||
|  | ||||
|                 <div class="my-3"> | ||||
|                     <VueMultiselect | ||||
| @@ -31,10 +32,11 @@ | ||||
|                         :allow-empty="true" | ||||
|                         :show-labels="false" | ||||
|                         :loading="issueIsLoading" | ||||
|                         :placeholder="$t('activity.choose_other_social_issue')" | ||||
|                         :placeholder="trans(ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE)" | ||||
|                         :options="socialIssuesOther" | ||||
|                         @select="addIssueInList" | ||||
|                     /> | ||||
|                     > | ||||
|                     </VueMultiselect> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
| @@ -42,35 +44,46 @@ | ||||
|         <div class="mb-3 row"> | ||||
|             <div class="col-4"> | ||||
|                 <label :class="socialActionsClassList">{{ | ||||
|                     $t("activity.social_actions") | ||||
|                     trans(ACTIVITY_SOCIAL_ACTIONS) | ||||
|                 }}</label> | ||||
|             </div> | ||||
|             <div class="col-8"> | ||||
|                 <div v-if="actionIsLoading === true"> | ||||
|                     <i class="chill-green fa fa-circle-o-notch fa-spin fa-lg" /> | ||||
|                     <i | ||||
|                         class="chill-green fa fa-circle-o-notch fa-spin fa-lg" | ||||
|                     ></i> | ||||
|                 </div> | ||||
|  | ||||
|                 <span | ||||
|                     v-else-if="socialIssuesSelected.length === 0" | ||||
|                     class="inline-choice chill-no-data-statement mt-3" | ||||
|                 > | ||||
|                     {{ $t("activity.select_first_a_social_issue") }} | ||||
|                     {{ trans(ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE) }} | ||||
|                 </span> | ||||
|  | ||||
|                 <template v-else-if="socialActionsList.length > 0"> | ||||
|                 <template | ||||
|                     v-else-if=" | ||||
|                         socialActionsList.length > 0 && | ||||
|                         (socialIssuesSelected.length || | ||||
|                             socialActionsSelected.length) | ||||
|                     " | ||||
|                 > | ||||
|                     <div | ||||
|                         v-if=" | ||||
|                             socialIssuesSelected.length || | ||||
|                             socialActionsSelected.length | ||||
|                         " | ||||
|                         id="actionsList" | ||||
|                         v-for="group in socialActionsList" | ||||
|                         :key="group.issue" | ||||
|                     > | ||||
|                         <span class="badge bg-chill-l-gray text-dark">{{ | ||||
|                             group.issue | ||||
|                         }}</span> | ||||
|                         <check-social-action | ||||
|                             v-for="action in socialActionsList" | ||||
|                             v-for="action in group.actions" | ||||
|                             :key="action.id" | ||||
|                             :action="action" | ||||
|                             :selection="socialActionsSelected" | ||||
|                             @update-selected="updateActionsSelected" | ||||
|                         /> | ||||
|                             @updateSelected="updateActionsSelected" | ||||
|                         > | ||||
|                         </check-social-action> | ||||
|                     </div> | ||||
|                 </template> | ||||
|  | ||||
| @@ -80,7 +93,7 @@ | ||||
|                     " | ||||
|                     class="inline-choice chill-no-data-statement mt-3" | ||||
|                 > | ||||
|                     {{ $t("activity.social_action_list_empty") }} | ||||
|                     {{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }} | ||||
|                 </span> | ||||
|             </div> | ||||
|         </div> | ||||
| @@ -92,6 +105,14 @@ import VueMultiselect from "vue-multiselect"; | ||||
| import CheckSocialIssue from "./SocialIssuesAcc/CheckSocialIssue.vue"; | ||||
| import CheckSocialAction from "./SocialIssuesAcc/CheckSocialAction.vue"; | ||||
| import { getSocialIssues, getSocialActionByIssue } from "../api.js"; | ||||
| import { | ||||
|     ACTIVITY_SOCIAL_ACTION_LIST_EMPTY, | ||||
|     ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE, | ||||
|     ACTIVITY_SOCIAL_ACTIONS, | ||||
|     ACTIVITY_SOCIAL_ISSUES, | ||||
|     ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE, | ||||
|     trans, | ||||
| } from "translator"; | ||||
|  | ||||
| export default { | ||||
|     name: "SocialIssuesAcc", | ||||
| @@ -100,6 +121,16 @@ export default { | ||||
|         CheckSocialAction, | ||||
|         VueMultiselect, | ||||
|     }, | ||||
|     setup() { | ||||
|         return { | ||||
|             trans, | ||||
|             ACTIVITY_SOCIAL_ACTION_LIST_EMPTY, | ||||
|             ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE, | ||||
|             ACTIVITY_SOCIAL_ACTIONS, | ||||
|             ACTIVITY_SOCIAL_ISSUES, | ||||
|             ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE, | ||||
|         }; | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             issueIsLoading: false, | ||||
| @@ -127,53 +158,44 @@ export default { | ||||
|         }, | ||||
|     }, | ||||
|     mounted() { | ||||
|         /* Load others issues in multiselect | ||||
|          */ | ||||
|         /* Load other issues in multiselect */ | ||||
|         this.issueIsLoading = true; | ||||
|         this.actionAreLoaded = false; | ||||
|         getSocialIssues().then( | ||||
|             (response) => | ||||
|                 new Promise((resolve, reject) => { | ||||
|                     this.$store.commit("updateIssuesOther", response.results); | ||||
|  | ||||
|                     /* Add in list the issues already associated (if not yet listed) | ||||
|                      */ | ||||
|                     this.socialIssuesSelected.forEach((issue) => { | ||||
|                         if ( | ||||
|                             this.socialIssuesList.filter( | ||||
|                                 (i) => i.id === issue.id, | ||||
|                             ).length !== 1 | ||||
|                         ) { | ||||
|                             this.$store.commit("addIssueInList", issue); | ||||
|                         } | ||||
|                     }, this); | ||||
|         getSocialIssues().then((response) => { | ||||
|             /* Add issues to the store */ | ||||
|             this.$store.commit("updateIssuesOther", response); | ||||
|  | ||||
|                     /* Remove from multiselect the issues that are not yet in checkbox list | ||||
|                      */ | ||||
|                     this.socialIssuesList.forEach((issue) => { | ||||
|                         this.$store.commit("removeIssueInOther", issue); | ||||
|                     }, this); | ||||
|             /* Add in list the issues already associated (if not yet listed) */ | ||||
|             this.socialIssuesSelected.forEach((issue) => { | ||||
|                 if ( | ||||
|                     this.socialIssuesList.filter((i) => i.id === issue.id) | ||||
|                         .length !== 1 | ||||
|                 ) { | ||||
|                     this.$store.commit("addIssueInList", issue); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|                     /* Filter issues | ||||
|                      */ | ||||
|                     this.$store.commit("filterList", "issues"); | ||||
|             /* Remove from multiselect the issues that are not yet in the checkbox list */ | ||||
|             this.socialIssuesList.forEach((issue) => { | ||||
|                 this.$store.commit("removeIssueInOther", issue); | ||||
|             }); | ||||
|  | ||||
|                     /* Add in list the actions already associated (if not yet listed) | ||||
|                      */ | ||||
|                     this.socialActionsSelected.forEach((action) => { | ||||
|                         this.$store.commit("addActionInList", action); | ||||
|                     }, this); | ||||
|             /* Filter issues */ | ||||
|             this.$store.commit("filterList", "issues"); | ||||
|  | ||||
|                     /* Filter issues | ||||
|                      */ | ||||
|                     this.$store.commit("filterList", "actions"); | ||||
|             /* Add in list the actions already associated (if not yet listed) */ | ||||
|             this.socialActionsSelected.forEach((action) => { | ||||
|                 this.$store.commit("addActionInList", action); | ||||
|             }); | ||||
|  | ||||
|                     this.issueIsLoading = false; | ||||
|                     this.actionAreLoaded = true; | ||||
|                     this.updateActionsList(); | ||||
|                     resolve(); | ||||
|                 }), | ||||
|         ); | ||||
|             /* Filter actions */ | ||||
|             this.$store.commit("filterList", "actions"); | ||||
|  | ||||
|             this.issueIsLoading = false; | ||||
|             this.actionAreLoaded = true; | ||||
|             this.updateActionsList(); | ||||
|         }); | ||||
|     }, | ||||
|     methods: { | ||||
|         /* When choosing an issue in multiselect, add it in checkboxes (as selected), | ||||
| @@ -208,7 +230,7 @@ export default { | ||||
|                 this.actionIsLoading = true; | ||||
|                 getSocialActionByIssue(item.id).then( | ||||
|                     (actions) => | ||||
|                         new Promise((resolve, reject) => { | ||||
|                         new Promise((resolve) => { | ||||
|                             actions.results.forEach((action) => { | ||||
|                                 this.$store.commit("addActionInList", action); | ||||
|                             }, this); | ||||
| @@ -235,9 +257,24 @@ export default { | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style src="vue-multiselect/dist/vue-multiselect.css"></style> | ||||
| <style lang="scss" scoped> | ||||
| @import "ChillMainAssets/module/bootstrap/shared"; | ||||
| @import "ChillPersonAssets/chill/scss/mixins"; | ||||
| @import "ChillMainAssets/chill/scss/chill_variables"; | ||||
|  | ||||
| span.multiselect__single { | ||||
|     display: none !important; | ||||
| } | ||||
|  | ||||
| #actionsList { | ||||
|     border-radius: 0.5rem; | ||||
|     padding: 1rem; | ||||
|     margin: 0.5rem; | ||||
|     background-color: whitesmoke; | ||||
| } | ||||
|  | ||||
| span.badge { | ||||
|     margin-bottom: 0.5rem; | ||||
|     @include badge_social($social-issue-color); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -10,7 +10,9 @@ | ||||
|                 :value="action" | ||||
|             /> | ||||
|             <label class="form-check-label" :for="action.id"> | ||||
|                 <span class="badge bg-light text-dark">{{ action.text }}</span> | ||||
|                 <span class="badge bg-light text-dark" :title="action.text">{{ | ||||
|                     action.text | ||||
|                 }}</span> | ||||
|             </label> | ||||
|         </div> | ||||
|     </span> | ||||
| @@ -43,5 +45,9 @@ span.badge { | ||||
|     font-size: 95%; | ||||
|     margin-bottom: 5px; | ||||
|     margin-right: 1em; | ||||
|     max-width: 100%; /* Adjust as needed */ | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     white-space: nowrap; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -14,18 +14,21 @@ const i18n = _createI18n(activityMessages); | ||||
| const hasSocialIssues = document.querySelector("#social-issues-acc") !== null; | ||||
| const hasLocation = document.querySelector("#location") !== null; | ||||
| const hasPerson = document.querySelector("#add-persons") !== null; | ||||
| const isSimpleEditor = true; | ||||
|  | ||||
| const app = createApp({ | ||||
|   template: `<app | ||||
|        :hasSocialIssues="hasSocialIssues" | ||||
|        :hasLocation="hasLocation" | ||||
|        :hasPerson="hasPerson" | ||||
|        :isSimpleEditor = "isSimpleEditor" | ||||
|     ></app>`, | ||||
|   data() { | ||||
|     return { | ||||
|       hasSocialIssues, | ||||
|       hasLocation, | ||||
|       hasPerson, | ||||
|       isSimpleEditor | ||||
|     }; | ||||
|   }, | ||||
| }) | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import "es6-promise/auto"; | ||||
| import { createStore } from "vuex"; | ||||
| import { postLocation } from "./api"; | ||||
| import prepareLocations from "./store.locations.js"; | ||||
| import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; | ||||
|  | ||||
| const debug = process.env.NODE_ENV !== "production"; | ||||
| //console.log('window.activity', window.activity); | ||||
| @@ -23,7 +24,9 @@ const removeIdFromValue = (string, id) => { | ||||
| const store = createStore({ | ||||
|   strict: debug, | ||||
|   state: { | ||||
|     me: null, | ||||
|     activity: window.activity, | ||||
|     accompanyingPeriodWorks: [], | ||||
|     socialIssuesOther: [], | ||||
|     socialActionsList: [], | ||||
|     availableLocations: [], | ||||
| @@ -39,7 +42,7 @@ const store = createStore({ | ||||
|       const allEntities = [ | ||||
|         ...store.getters.suggestedPersons, | ||||
|         ...store.getters.suggestedRequestor, | ||||
|         ...store.getters.suggestedUser, | ||||
|         ...store.getters.suggestedUsers, | ||||
|         ...store.getters.suggestedResources, | ||||
|       ]; | ||||
|       const uniqueIds = [ | ||||
| @@ -78,16 +81,32 @@ const store = createStore({ | ||||
|             state.activity.activityType.thirdPartiesVisible !== 0), | ||||
|       ); | ||||
|     }, | ||||
|     suggestedUser(state) { | ||||
|     suggestedUsers(state) { | ||||
|       const existingUserIds = state.activity.users.map((p) => p.id); | ||||
|       return state.activity.activityType.usersVisible === 0 | ||||
|         ? [] | ||||
|         : [state.activity.accompanyingPeriod.user].filter( | ||||
|             (u) => u !== null && !existingUserIds.includes(u.id), | ||||
|           ); | ||||
|       let suggestedUsers = | ||||
|         state.activity.activityType.usersVisible === 0 | ||||
|           ? [] | ||||
|           : [state.activity.accompanyingPeriod.user].filter( | ||||
|               (u) => u !== null && !existingUserIds.includes(u.id), | ||||
|             ); | ||||
|  | ||||
|       state.accompanyingPeriodWorks.forEach((work) => { | ||||
|         work.referrers.forEach((r) => { | ||||
|           if (!existingUserIds.includes(r.id)) { | ||||
|             suggestedUsers.push(r); | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|       // Add the current user from the state | ||||
|       if (state.me && !existingUserIds.includes(state.me.id)) { | ||||
|         suggestedUsers.push(state.me); | ||||
|       } | ||||
|       // console.log("suggested users", suggestedUsers); | ||||
|  | ||||
|       return suggestedUsers; | ||||
|     }, | ||||
|     suggestedResources(state) { | ||||
|       const resources = state.activity.accompanyingPeriod.resources; | ||||
|       // const resources = state.activity.accompanyingPeriod.resources; | ||||
|       const existingPersonIds = state.activity.persons.map((p) => p.id); | ||||
|       const existingThirdPartyIds = state.activity.thirdParties.map( | ||||
|         (p) => p.id, | ||||
| @@ -105,12 +124,25 @@ const store = createStore({ | ||||
|         ); | ||||
|     }, | ||||
|     socialActionsListSorted(state) { | ||||
|       return [...state.socialActionsList].sort( | ||||
|         (a, b) => a.ordering - b.ordering, | ||||
|       ); | ||||
|       return [...state.socialActionsList] | ||||
|         .sort((a, b) => a.ordering - b.ordering) | ||||
|         .reduce((acc, action) => { | ||||
|           const issueText = action.issue?.text || "Uncategorized"; | ||||
|           // Find if the group for the issue already exists | ||||
|           let group = acc.find((item) => item.issue === issueText); | ||||
|           if (!group) { | ||||
|             group = { issue: issueText, actions: [] }; | ||||
|             acc.push(group); | ||||
|           } | ||||
|           group.actions.push(action); | ||||
|           return acc; | ||||
|         }, []); | ||||
|     }, | ||||
|   }, | ||||
|   mutations: { | ||||
|     setWhoAmI(state, me) { | ||||
|       state.me = me; | ||||
|     }, | ||||
|     // SocialIssueAcc | ||||
|     addIssueInList(state, issue) { | ||||
|       //console.log('add issue list', issue.id); | ||||
| @@ -208,6 +240,9 @@ const store = createStore({ | ||||
|     addAvailableLocationGroup(state, group) { | ||||
|       state.availableLocations.push(group); | ||||
|     }, | ||||
|     setAccompanyingPeriodWorks(state, works) { | ||||
|       state.accompanyingPeriodWorks = works; | ||||
|     }, | ||||
|   }, | ||||
|   actions: { | ||||
|     addIssueSelected({ commit }, issue) { | ||||
| @@ -326,9 +361,29 @@ const store = createStore({ | ||||
|       } | ||||
|       commit("updateLocation", value); | ||||
|     }, | ||||
|     async fetchAccompanyingPeriodWorks({ state, commit }) { | ||||
|       const accompanyingPeriodId = state.activity.accompanyingPeriod.id; | ||||
|       const url = `/api/1.0/person/accompanying-course/${accompanyingPeriodId}/works.json`; | ||||
|       try { | ||||
|         const works = await makeFetch("GET", url); | ||||
|         // console.log("works", works); | ||||
|         commit("setAccompanyingPeriodWorks", works); | ||||
|       } catch (error) { | ||||
|         console.error("Failed to fetch accompanying period works:", error); | ||||
|       } | ||||
|     }, | ||||
|     getWhoAmI({ commit }) { | ||||
|       const url = `/api/1.0/main/whoami.json`; | ||||
|       makeFetch("GET", url).then((user) => { | ||||
|         commit("setWhoAmI", user); | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| store.dispatch("getWhoAmI"); | ||||
|  | ||||
| prepareLocations(store); | ||||
| store.dispatch("fetchAccompanyingPeriodWorks"); | ||||
|  | ||||
| export default store; | ||||
|   | ||||
| @@ -126,4 +126,4 @@ | ||||
|  | ||||
| {% block css %} | ||||
|     {{ encore_entry_link_tags('mod_pickentity_type') }} | ||||
| {% endblock %} | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -1,83 +1,3 @@ | ||||
| {% import "@ChillDocStore/Macro/macro.html.twig" as m %} | ||||
| {% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} | ||||
| {% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %} | ||||
|  | ||||
| {% set person_id = null %} | ||||
| {% if activity.person %} | ||||
|     {% set person_id = activity.person.id %} | ||||
| {% endif %} | ||||
|  | ||||
| {% set accompanying_course_id = null %} | ||||
| {% if activity.accompanyingPeriod %} | ||||
|     {% set accompanying_course_id = activity.accompanyingPeriod.id %} | ||||
| {% endif %} | ||||
|  | ||||
| <div class="item-bloc activity-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}"> | ||||
|     <div class="item-row"> | ||||
|         <div class="item-col" style="width: unset"> | ||||
|             {% if document.isPending %} | ||||
|                 <div class="badge text-bg-info" data-docgen-is-pending="{{ document.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div> | ||||
|             {% elseif document.isFailure %} | ||||
|                 <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div> | ||||
|             {% endif %} | ||||
|  | ||||
|             <div> | ||||
|                 {% if activity.accompanyingPeriod is not null and context == 'person' %} | ||||
|                     <span class="badge bg-primary"> | ||||
|                         <i class="fa fa-random"></i> {{ activity.accompanyingPeriod.id }} | ||||
|                     </span>  | ||||
|                 {% endif %} | ||||
|                 <div class="badge-activity-type"> | ||||
|                     <span class="title_label"></span> | ||||
|                     <span class="title_action"> | ||||
|                     {{ activity.type.name | localize_translatable_string }} | ||||
|                         {% if activity.emergency %} | ||||
|                             <span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span> | ||||
|                         {% endif %} | ||||
|                     </span> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="denomination h2"> | ||||
|                 {{ document.title|chill_print_or_message("No title") }} | ||||
|             </div> | ||||
|             {% if document.hasTemplate %} | ||||
|                 <div> | ||||
|                     <p>{{ document.template.name|localize_translatable_string }}</p> | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|  | ||||
|         <div class="item-col"> | ||||
|             <div class="container"> | ||||
|                 <div class="dates row text-end"> | ||||
|                     <span>{{ document.createdAt|format_date('short') }}</span> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|  | ||||
|     <div class="item-row separator"> | ||||
|         <div class="item-col item-meta"> | ||||
|             {{ mmm.createdBy(document) }} | ||||
|         </div> | ||||
|         <ul class="item-col record_actions flex-shrink-1"> | ||||
|             {% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %} | ||||
|                 <li> | ||||
|                     {{ document|chill_document_button_group(document.title, is_granted('CHILL_ACTIVITY_UPDATE', activity), {small: false})  }} | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|             {% if is_granted('CHILL_ACTIVITY_SEE', activity)%} | ||||
|                 <li> | ||||
|                     <a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-show"></a> | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|             {% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %} | ||||
|                 <li> | ||||
|                     <a href="{{ chill_path_add_return_path('chill_activity_activity_edit', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-edit"></a> | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|         </ul> | ||||
|  | ||||
|     </div> | ||||
|     {{ include('@ChillActivity/GenericDoc/activity_document_row.html.twig') }} | ||||
| </div> | ||||
|   | ||||
| @@ -0,0 +1,81 @@ | ||||
| {% import "@ChillDocStore/Macro/macro.html.twig" as m %} | ||||
| {% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} | ||||
| {% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %} | ||||
|  | ||||
| {% set person_id = null %} | ||||
| {% if activity.person %} | ||||
|     {% set person_id = activity.person.id %} | ||||
| {% endif %} | ||||
|  | ||||
| {% set accompanying_course_id = null %} | ||||
| {% if activity.accompanyingPeriod %} | ||||
|     {% set accompanying_course_id = activity.accompanyingPeriod.id %} | ||||
| {% endif %} | ||||
|  | ||||
| <div class="item-row"> | ||||
|     <div class="item-two-col-grid"> | ||||
|         <div class="title"> | ||||
|             {% if document.isPending %} | ||||
|                 <div class="badge text-bg-info" data-docgen-is-pending="{{ document.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div> | ||||
|             {% elseif document.isFailure %} | ||||
|                 <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div> | ||||
|             {% endif %} | ||||
|  | ||||
|             <div> | ||||
|                 <div> | ||||
|                     <div class="badge-activity-type-simple"> | ||||
|                         {{ activity.type.name | localize_translatable_string }} | ||||
|                     </div> | ||||
|                     {% if activity.emergency %} | ||||
|                         <span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span> | ||||
|                     {% endif %} | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="denomination h2"> | ||||
|                 {{ document.title|chill_print_or_message("No title") }} | ||||
|             </div> | ||||
|             {% if document.hasTemplate %} | ||||
|                 <div> | ||||
|                     <p>{{ document.template.name|localize_translatable_string }}</p> | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|         <div class="aside"> | ||||
|             <div class="dates row text-end"> | ||||
|                 <span>{{ document.createdAt|format_date('short') }}</span> | ||||
|             </div> | ||||
|             {% if activity.accompanyingPeriod is not null and context == 'person' %} | ||||
|                 <div class="text-end"> | ||||
|                     <span class="badge bg-primary"> | ||||
|                         <i class="fa fa-random"></i> {{ activity.accompanyingPeriod.id }} | ||||
|                     </span>  | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| {% if show_actions %} | ||||
|     <div class="item-row separator"> | ||||
|         <div class="item-col item-meta"> | ||||
|             {{ mmm.createdBy(document) }} | ||||
|         </div> | ||||
|         <ul class="item-col record_actions flex-shrink-1"> | ||||
|             {% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %} | ||||
|                 <li> | ||||
|                     {{ document|chill_document_button_group(document.title, is_granted('CHILL_ACTIVITY_UPDATE', activity), {small: false})  }} | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|             {% if is_granted('CHILL_ACTIVITY_SEE', activity)%} | ||||
|                 <li> | ||||
|                     <a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-show"></a> | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|             {% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %} | ||||
|                 <li> | ||||
|                     <a href="{{ chill_path_add_return_path('chill_activity_activity_edit', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-edit"></a> | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|         </ul> | ||||
|     </div> | ||||
| {% endif %} | ||||
| @@ -143,7 +143,10 @@ class ListActivitiesByAccompanyingPeriodContext implements | ||||
|             array_filter( | ||||
|                 $works, | ||||
|                 function ($work) use ($user) { | ||||
|                     $workUsernames = array_map(static fn (User $user) => $user['username'], $work['referrers'] ?? []); | ||||
|                     $workUsernames = []; | ||||
|                     foreach ($work['referrers'] as $referrer) { | ||||
|                         $workUsernames[] = $referrer['username']; | ||||
|                     } | ||||
|  | ||||
|                     return \in_array($user->getUserIdentifier(), $workUsernames, true); | ||||
|                 } | ||||
|   | ||||
| @@ -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\ActivityBundle\Service\GenericDoc\Normalizer; | ||||
|  | ||||
| use Chill\ActivityBundle\Service\GenericDoc\Providers\AccompanyingPeriodActivityGenericDocProvider; | ||||
| use Chill\ActivityBundle\Service\GenericDoc\Renderers\AccompanyingPeriodActivityGenericDocRenderer; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
| use Twig\Environment; | ||||
|  | ||||
| final readonly class AccompanyingPeriodActivityGenericDocNormalizer implements GenericDocNormalizerInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private StoredObjectRepositoryInterface $storedObjectRepository, | ||||
|         private AccompanyingPeriodActivityGenericDocRenderer $renderer, | ||||
|         private Environment $twig, | ||||
|         private TranslatorInterface $translator, | ||||
|     ) {} | ||||
|  | ||||
|     public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool | ||||
|     { | ||||
|         return AccompanyingPeriodActivityGenericDocProvider::KEY === $genericDocDTO->key | ||||
|             && 'json' == $format; | ||||
|     } | ||||
|  | ||||
|     public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array | ||||
|     { | ||||
|         $storedObject = $this->storedObjectRepository->find($genericDocDTO->identifiers['id']); | ||||
|  | ||||
|         if (null === $storedObject) { | ||||
|             return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false]; | ||||
|         } | ||||
|  | ||||
|         return [ | ||||
|             'isPresent' => true, | ||||
|             'title' => $storedObject->getTitle(), | ||||
|             'html' => $this->twig->render( | ||||
|                 $this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]), | ||||
|                 $this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true]), | ||||
|             ), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -13,10 +13,12 @@ namespace Chill\ActivityBundle\Service\GenericDoc\Providers; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\Activity; | ||||
| use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepositoryInterface; | ||||
| use Chill\ActivityBundle\Repository\ActivityRepository; | ||||
| use Chill\ActivityBundle\Security\Authorization\ActivityVoter; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\GenericDoc\FetchQuery; | ||||
| use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| @@ -34,8 +36,47 @@ final readonly class AccompanyingPeriodActivityGenericDocProvider implements Gen | ||||
|         private EntityManagerInterface $em, | ||||
|         private Security $security, | ||||
|         private ActivityDocumentACLAwareRepositoryInterface $activityDocumentACLAwareRepository, | ||||
|         private ActivityRepository $activityRepository, | ||||
|     ) {} | ||||
|  | ||||
|     public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject | ||||
|     { | ||||
|         if (null === $activity = $this->getRelatedEntity($genericDocDTO->key, $genericDocDTO->identifiers)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         return $activity->getDocuments()->findFirst(fn (int $key, StoredObject $storedObject) => $storedObject->getId() === $genericDocDTO->identifiers['id']); | ||||
|     } | ||||
|  | ||||
|     public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool | ||||
|     { | ||||
|         return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers); | ||||
|     } | ||||
|  | ||||
|     public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool | ||||
|     { | ||||
|         return self::KEY === $key && array_key_exists('activity_id', $identifiers); | ||||
|     } | ||||
|  | ||||
|     private function getRelatedEntity(string $key, array $identifiers): ?Activity | ||||
|     { | ||||
|         return $this->activityRepository->find($identifiers['activity_id']); | ||||
|     } | ||||
|  | ||||
|     public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO | ||||
|     { | ||||
|         if (null === $activity = $this->getRelatedEntity($key, $identifiers)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         return new GenericDocDTO( | ||||
|             self::KEY, | ||||
|             $identifiers, | ||||
|             \DateTimeImmutable::createFromInterface($activity->getDate()), | ||||
|             $activity->getAccompanyingPeriod(), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface | ||||
|     { | ||||
|         $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); | ||||
|   | ||||
| @@ -18,6 +18,9 @@ use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; | ||||
| use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepository; | ||||
|  | ||||
| /** | ||||
|  * @implements GenericDocRendererInterface<array{row-only?: bool, show-actions?: bool}> | ||||
|  */ | ||||
| final readonly class AccompanyingPeriodActivityGenericDocRenderer implements GenericDocRendererInterface | ||||
| { | ||||
|     public function __construct(private StoredObjectRepository $objectRepository, private ActivityRepository $activityRepository) {} | ||||
| @@ -29,7 +32,8 @@ final readonly class AccompanyingPeriodActivityGenericDocRenderer implements Gen | ||||
|  | ||||
|     public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string | ||||
|     { | ||||
|         return '@ChillActivity/GenericDoc/activity_document.html.twig'; | ||||
|         return ($options['row-only'] ?? false) ? '@ChillActivity/GenericDoc/activity_document_row.html.twig' : | ||||
|             '@ChillActivity/GenericDoc/activity_document.html.twig'; | ||||
|     } | ||||
|  | ||||
|     public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array | ||||
| @@ -38,6 +42,7 @@ final readonly class AccompanyingPeriodActivityGenericDocRenderer implements Gen | ||||
|             'activity' => $this->activityRepository->find($genericDocDTO->identifiers['activity_id']), | ||||
|             'document' => $this->objectRepository->find($genericDocDTO->identifiers['id']), | ||||
|             'context' => $genericDocDTO->getContext(), | ||||
|             'show_actions' => $options['show-actions'] ?? true, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -14,3 +14,5 @@ export: | ||||
|             describe_action_with_subject: >- | ||||
|                 Filtré par personne ayant eu un échange entre le {date_from, date} et le {date_to, date}, et un de ces sujets choisis: {reasons} | ||||
|  | ||||
| activity: | ||||
|     title: Échange du {date, date, long} - {type} | ||||
|   | ||||
| @@ -101,6 +101,33 @@ activity: | ||||
|     Insert a document: Insérer un document | ||||
|     Remove a document: Supprimer le document | ||||
|     comment: Commentaire | ||||
|     deleted: Échange supprimé | ||||
|  | ||||
|     errors: Le formulaire contient des erreurs | ||||
|     social_issues: Problématiques sociales | ||||
|     choose_other_social_issue: Ajouter une autre problématique sociale... | ||||
|     social_actions: Actions d'accompagnement | ||||
|     select_first_a_social_issue: Sélectionnez d'abord une problématique sociale | ||||
|     social_action_list_empty: Aucune action sociale disponible | ||||
|     add_persons: Ajouter des personnes concernées | ||||
|     bloc_persons: Usagers | ||||
|     bloc_persons_associated: Usagers du parcours | ||||
|     bloc_persons_not_associated: Tiers non-pro. | ||||
|     bloc_thirdparty: Tiers professionnels | ||||
|     bloc_users: T(M)S | ||||
|     location: Localisation | ||||
|     choose_location: Choisissez une localisation | ||||
|     choose_location_type: Choisissez un type de localisation | ||||
|     create_new_location: Créer une nouvelle localisation | ||||
|     location_fields: | ||||
|         name: Nom | ||||
|         type: Type | ||||
|         phonenumber1: Téléphone | ||||
|         phonenumber2: Autre téléphone | ||||
|         email: Adresse courriel | ||||
|     create_address: Créer une adresse | ||||
|     edit_address: Modifier l'adresse | ||||
|  | ||||
| No documents: Aucun document | ||||
|  | ||||
| # activity filter in list page | ||||
|   | ||||
| @@ -21,9 +21,7 @@ namespace Chill\CalendarBundle\Command; | ||||
| use Chill\CalendarBundle\Entity\Calendar; | ||||
| use Chill\CalendarBundle\Service\ShortMessageNotification\ShortMessageForCalendarBuilderInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Chill\MainBundle\Service\ShortMessage\ShortMessageTransporterInterface; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\Repository\PersonRepository; | ||||
| use libphonenumber\PhoneNumber; | ||||
| @@ -36,6 +34,7 @@ use Symfony\Component\Console\Input\InputInterface; | ||||
| use Symfony\Component\Console\Output\OutputInterface; | ||||
| use Symfony\Component\Console\Question\ConfirmationQuestion; | ||||
| use Symfony\Component\Console\Question\Question; | ||||
| use Symfony\Component\Notifier\TexterInterface; | ||||
|  | ||||
| class SendTestShortMessageOnCalendarCommand extends Command | ||||
| { | ||||
| @@ -44,9 +43,8 @@ class SendTestShortMessageOnCalendarCommand extends Command | ||||
|     public function __construct( | ||||
|         private readonly PersonRepository $personRepository, | ||||
|         private readonly PhoneNumberUtil $phoneNumberUtil, | ||||
|         private readonly PhoneNumberHelperInterface $phoneNumberHelper, | ||||
|         private readonly ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder, | ||||
|         private readonly ShortMessageTransporterInterface $transporter, | ||||
|         private readonly TexterInterface $transporter, | ||||
|         private readonly UserRepositoryInterface $userRepository, | ||||
|     ) { | ||||
|         parent::__construct('chill:calendar:test-send-short-message'); | ||||
| @@ -152,10 +150,6 @@ class SendTestShortMessageOnCalendarCommand extends Command | ||||
|             return $phone; | ||||
|         }); | ||||
|  | ||||
|         $phone = $helper->ask($input, $output, $question); | ||||
|  | ||||
|         $question = new ConfirmationQuestion('really send the message to the phone ?'); | ||||
|         $reallySend = (bool) $helper->ask($input, $output, $question); | ||||
|  | ||||
|         $messages = $this->messageForCalendarBuilder->buildMessageForCalendar($calendar); | ||||
|  | ||||
| @@ -165,8 +159,12 @@ class SendTestShortMessageOnCalendarCommand extends Command | ||||
|  | ||||
|         foreach ($messages as $key => $message) { | ||||
|             $output->writeln("The short message for SMS {$key} will be: "); | ||||
|             $output->writeln($message->getContent()); | ||||
|             $message->setPhoneNumber($phone); | ||||
|             $output->writeln($message->getSubject()); | ||||
|             $output->writeln('The destination number will be:'); | ||||
|             $output->writeln($message->getPhone()); | ||||
|  | ||||
|             $question = new ConfirmationQuestion('really send the message to the phone ?'); | ||||
|             $reallySend = (bool) $helper->ask($input, $output, $question); | ||||
|  | ||||
|             if ($reallySend) { | ||||
|                 $this->transporter->send($message); | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\CalendarBundle\Repository; | ||||
|  | ||||
| use Chill\CalendarBundle\Entity\CalendarDoc; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\EntityRepository; | ||||
| use Doctrine\Persistence\ObjectRepository; | ||||
| @@ -49,4 +50,21 @@ class CalendarDocRepository implements ObjectRepository, CalendarDocRepositoryIn | ||||
|     { | ||||
|         return CalendarDoc::class; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param StoredObject|int $storedObject the StoredObject instance, or the id of the stored object | ||||
|      */ | ||||
|     public function findOneByStoredObject(StoredObject|int $storedObject): ?CalendarDoc | ||||
|     { | ||||
|         $storedObjectId = $storedObject instanceof StoredObject ? $storedObject->getId() : $storedObject; | ||||
|  | ||||
|         $qb = $this->repository->createQueryBuilder('c'); | ||||
|         $qb->where( | ||||
|             $qb->expr()->eq(':storedObject', 'c.storedObject') | ||||
|         ); | ||||
|  | ||||
|         $qb->setParameter('storedObject', $storedObjectId); | ||||
|  | ||||
|         return $qb->getQuery()->getOneOrNullResult(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\CalendarBundle\Repository; | ||||
|  | ||||
| use Chill\CalendarBundle\Entity\CalendarDoc; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
|  | ||||
| interface CalendarDocRepositoryInterface | ||||
| { | ||||
| @@ -29,5 +30,7 @@ interface CalendarDocRepositoryInterface | ||||
|  | ||||
|     public function findOneBy(array $criteria): ?CalendarDoc; | ||||
|  | ||||
|     public function findOneByStoredObject(StoredObject|int $storedObject): ?CalendarDoc; | ||||
|  | ||||
|     public function getClassName(); | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| @import '~ChillPersonAssets/chill/scss/mixins.scss'; | ||||
| @import '~ChillMainAssets/module/bootstrap/shared'; | ||||
| @import '~ChillPersonAssets/chill/scss/mixins.scss'; | ||||
| @import 'bootstrap/scss/_badge.scss'; | ||||
|  | ||||
| .badge-calendar { | ||||
|     display: inline-block; | ||||
| @@ -23,3 +24,35 @@ | ||||
|     } | ||||
| } | ||||
|  | ||||
| .badge-calendar-simple { | ||||
|     @extend .badge; | ||||
|     display: inline-block; | ||||
|     margin: 0.2rem 0; | ||||
|     padding-left: 0; | ||||
|     padding-right: 0.5rem; | ||||
|  | ||||
|     border-left: 20px groove $chill-l-gray; | ||||
|     border-radius: $badge-border-radius; | ||||
|  | ||||
|     max-width: 100%; | ||||
|     background-color: $gray-100; | ||||
|  | ||||
|     color: black; | ||||
|     font-weight: normal; | ||||
|     overflow: hidden; | ||||
|     font-weight: normal; | ||||
|     font-size: unset; | ||||
|     text-overflow: ellipsis; | ||||
|     text-indent: 5px hanging; | ||||
|     text-align: left; | ||||
|  | ||||
|     &::before { | ||||
|         margin-right: 3px; | ||||
|         position: relative; | ||||
|         left: -0.5px; | ||||
|         font-family: ForkAwesome; | ||||
|         content: '\f04b'; | ||||
|         color: $chill-l-gray; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -16,7 +16,7 @@ div.calendar-list { | ||||
|     } | ||||
|  | ||||
|     & > a.calendar-list__global { | ||||
|         display: inline-block;; | ||||
|         display: inline-block; | ||||
|         padding: 0.2rem; | ||||
|         min-width: 2rem; | ||||
|         border: 1px solid var(--bs-chill-blue); | ||||
|   | ||||
| @@ -96,23 +96,23 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|     <FullCalendar :options="calendarOptions" ref="calendarRef"> | ||||
|         <template v-slot:eventContent="arg: EventApi"> | ||||
|             <span :class="eventClasses(arg.event)"> | ||||
|                 <b v-if="arg.event.extendedProps.is === 'remote'">{{ | ||||
|                     arg.event.title | ||||
|         <template v-slot:eventContent="{ event }"> | ||||
|             <span :class="eventClasses(event)"> | ||||
|                 <b v-if="event.extendedProps.is === 'remote'">{{ | ||||
|                     event.title | ||||
|                 }}</b> | ||||
|                 <b v-else-if="arg.event.extendedProps.is === 'range'" | ||||
|                     >{{ arg.timeText }} - | ||||
|                     {{ arg.event.extendedProps.locationName }}</b | ||||
|                 <b v-else-if="event.extendedProps.is === 'range'" | ||||
|                     >{{ formatDate(event.startStr) }} - | ||||
|                     {{ event.extendedProps.locationName }}</b | ||||
|                 > | ||||
|                 <b v-else-if="arg.event.extendedProps.is === 'local'">{{ | ||||
|                     arg.event.title | ||||
|                 <b v-else-if="event.extendedProps.is === 'local'">{{ | ||||
|                     event.title | ||||
|                 }}</b> | ||||
|                 <b v-else>no 'is'</b> | ||||
|                 <a | ||||
|                     v-if="arg.event.extendedProps.is === 'range'" | ||||
|                     v-if="event.extendedProps.is === 'range'" | ||||
|                     class="fa fa-fw fa-times delete" | ||||
|                     @click.prevent="onClickDelete(arg.event)" | ||||
|                     @click.prevent="onClickDelete(event)" | ||||
|                 > | ||||
|                 </a> | ||||
|             </span> | ||||
| @@ -221,13 +221,12 @@ import type { | ||||
|     DatesSetArg, | ||||
|     EventInput, | ||||
| } from "@fullcalendar/core"; | ||||
| import { reactive, computed, ref, onMounted } from "vue"; | ||||
| import { computed, ref, onMounted } from "vue"; | ||||
| import { useStore } from "vuex"; | ||||
| import { key } from "./store"; | ||||
| import FullCalendar from "@fullcalendar/vue3"; | ||||
| import frLocale from "@fullcalendar/core/locales/fr"; | ||||
| import interactionPlugin, { | ||||
|     DropArg, | ||||
|     EventResizeDoneArg, | ||||
| } from "@fullcalendar/interaction"; | ||||
| import timeGridPlugin from "@fullcalendar/timegrid"; | ||||
| @@ -237,19 +236,13 @@ import { | ||||
|     EventDropArg, | ||||
|     EventClickArg, | ||||
| } from "@fullcalendar/core"; | ||||
| import { | ||||
|     dateToISO, | ||||
|     ISOToDate, | ||||
| } from "../../../../../ChillMainBundle/Resources/public/chill/js/date"; | ||||
| import { dateToISO, ISOToDate } from "ChillMainAssets/chill/js/date"; | ||||
| import VueMultiselect from "vue-multiselect"; | ||||
| import { Location } from "../../../../../ChillMainBundle/Resources/public/types"; | ||||
| import { Location } from "ChillMainAssets/types"; | ||||
| import EditLocation from "./Components/EditLocation.vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
|  | ||||
| const store = useStore(key); | ||||
|  | ||||
| const { t } = useI18n(); | ||||
|  | ||||
| const showWeekends = ref(false); | ||||
| const slotDuration = ref("00:15:00"); | ||||
| const slotMinTime = ref("09:00:00"); | ||||
| @@ -301,6 +294,11 @@ const nextWeeks = computed((): Weeks[] => | ||||
|     }), | ||||
| ); | ||||
|  | ||||
| const formatDate = (datetime: string) => { | ||||
|     console.log(typeof datetime); | ||||
|     return ISOToDate(datetime); | ||||
| }; | ||||
|  | ||||
| const baseOptions = ref<CalendarOptions>({ | ||||
|     locale: frLocale, | ||||
|     plugins: [interactionPlugin, timeGridPlugin], | ||||
| @@ -353,7 +351,7 @@ const pickedLocation = computed<Location | null>({ | ||||
|  * return the show classes for the event | ||||
|  * @param arg | ||||
|  */ | ||||
| const eventClasses = function (arg: EventApi): object { | ||||
| const eventClasses = function (): object { | ||||
|     return { calendarRangeItems: true }; | ||||
| }; | ||||
|  | ||||
| @@ -431,7 +429,6 @@ function onEventDropOrResize(payload: EventDropArg | EventResizeDoneArg) { | ||||
|     if (payload.event.extendedProps.is !== "range") { | ||||
|         return; | ||||
|     } | ||||
|     const changedEvent = payload.event; | ||||
|  | ||||
|     store.dispatch("calendarRanges/patchRangeTime", { | ||||
|         calendarRangeId: payload.event.extendedProps.calendarRangeId, | ||||
|   | ||||
| @@ -106,7 +106,10 @@ export default { | ||||
|             }); | ||||
|             state.key = state.key + toAdd.length; | ||||
|         }, | ||||
|         addExternals(state, externalEvents: (EventInput & { id: string })[]) { | ||||
|         addExternals( | ||||
|             state: CalendarRangesState, | ||||
|             externalEvents: (EventInput & { id: string })[], | ||||
|         ) { | ||||
|             const toAdd = externalEvents.filter( | ||||
|                 (r) => !state.rangesIndex.has(r.id), | ||||
|             ); | ||||
| @@ -160,7 +163,7 @@ export default { | ||||
|                 state.key = state.key + 1; | ||||
|             } | ||||
|         }, | ||||
|         updateRange(state, range: CalendarRange) { | ||||
|         updateRange(state: CalendarRangesState, range: CalendarRange) { | ||||
|             const found = state.ranges.find( | ||||
|                 (r) => r.calendarRangeId === range.id && r.is === "range", | ||||
|             ); | ||||
| @@ -207,7 +210,7 @@ export default { | ||||
|             }); | ||||
|         }, | ||||
|         createRange( | ||||
|             ctx, | ||||
|             ctx: Context, | ||||
|             { | ||||
|                 start, | ||||
|                 end, | ||||
| @@ -253,10 +256,10 @@ export default { | ||||
|                     throw error; | ||||
|                 }); | ||||
|         }, | ||||
|         deleteRange(ctx, calendarRangeId: number) { | ||||
|         deleteRange(ctx: Context, calendarRangeId: number) { | ||||
|             const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`; | ||||
|  | ||||
|             makeFetch<undefined, never>("DELETE", url).then((_) => { | ||||
|             makeFetch<undefined, never>("DELETE", url).then(() => { | ||||
|                 ctx.commit("removeRange", calendarRangeId); | ||||
|             }); | ||||
|         }, | ||||
| @@ -347,10 +350,10 @@ export default { | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             return Promise.all(promises).then((_) => Promise.resolve(null)); | ||||
|             return Promise.all(promises).then(() => Promise.resolve(null)); | ||||
|         }, | ||||
|         copyFromWeekToAnotherWeek( | ||||
|             ctx, | ||||
|             ctx: Context, | ||||
|             { fromMonday, toMonday }: { fromMonday: Date; toMonday: Date }, | ||||
|         ): Promise<null> { | ||||
|             const rangesToCopy: EventInputCalendarRange[] = | ||||
| @@ -371,7 +374,7 @@ export default { | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             return Promise.all(promises).then((_) => Promise.resolve(null)); | ||||
|             return Promise.all(promises).then(() => Promise.resolve(null)); | ||||
|         }, | ||||
|     }, | ||||
| } as Module<CalendarRangesState, State>; | ||||
|   | ||||
| @@ -5,71 +5,5 @@ | ||||
| {% set c = document.calendar %} | ||||
|  | ||||
| <div class="item-bloc"> | ||||
|     <div class="item-row"> | ||||
|         <div class="item-col" style="width: unset"> | ||||
|             {% if document.storedObject.isPending %} | ||||
|                 <div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div> | ||||
|             {% elseif document.storedObject.isFailure %} | ||||
|                 <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div> | ||||
|             {% endif %} | ||||
|  | ||||
|             <div> | ||||
|                 {% if c.accompanyingPeriod is not null and context == 'person' %} | ||||
|                     <span class="badge bg-primary"> | ||||
|                         <i class="fa fa-random"></i> {{ c.accompanyingPeriod.id }} | ||||
|                     </span>  | ||||
|                 {% endif %} | ||||
|  | ||||
|                 <span class="badge-calendar"> | ||||
|                     <span class="title_label"></span> | ||||
|                     <span class="title_action"> | ||||
|                         {{ 'Calendar'|trans }} | ||||
|                         {% if c.endDate.diff(c.startDate).days >= 1 %} | ||||
|                             {{ c.startDate|format_datetime('short', 'short') }} | ||||
|                             - {{ c.endDate|format_datetime('short', 'short') }} | ||||
|                         {% else %} | ||||
|                             {{ c.startDate|format_datetime('short', 'short') }} | ||||
|                             - {{ c.endDate|format_datetime('none', 'short') }} | ||||
|                         {% endif %} | ||||
|                     </span> | ||||
|                 </span> | ||||
|             </div> | ||||
|  | ||||
|             <div class="denomination h2"> | ||||
|                 {{ document.storedObject.title|chill_print_or_message("No title") }} | ||||
|             </div> | ||||
|             {% if document.storedObject.hasTemplate %} | ||||
|                 <div> | ||||
|                     <p>{{ document.storedObject.template.name|localize_translatable_string }}</p> | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|  | ||||
|         <div class="item-col"> | ||||
|             <div class="container"> | ||||
|                 <div class="dates row text-end"> | ||||
|                     <span>{{ document.storedObject.createdAt|format_date('short') }}</span> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="item-row separator"> | ||||
|         <div class="item-col item-meta"> | ||||
|             {{ mmm.createdBy(document) }} | ||||
|         </div> | ||||
|         <ul class="item-col record_actions flex-shrink-1"> | ||||
|             {% if is_granted('CHILL_CALENDAR_DOC_SEE', document) %} | ||||
|                 <li> | ||||
|                     {{ document.storedObject|chill_document_button_group(document.storedObject.title, is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c)) }} | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|             {% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %} | ||||
|                 <li> | ||||
|                     <a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', {'id': c.id, 'docId': document.id}) }}" class="btn btn-edit"></a> | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|         </ul> | ||||
|  | ||||
|     </div> | ||||
|     {{ include('@ChillCalendar/GenericDoc/calendar_document_row.html.twig') }} | ||||
| </div> | ||||
|   | ||||
| @@ -0,0 +1,73 @@ | ||||
| {% import "@ChillDocStore/Macro/macro.html.twig" as m %} | ||||
| {% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} | ||||
| {% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %} | ||||
|  | ||||
| {% set c = document.calendar %} | ||||
|  | ||||
|  | ||||
| <div class="item-row"> | ||||
|     <div class="item-two-col-grid"> | ||||
|         <div class="title"> | ||||
|             {% if document.storedObject.isPending %} | ||||
|                 <div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div> | ||||
|             {% elseif document.storedObject.isFailure %} | ||||
|                 <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div> | ||||
|             {% endif %} | ||||
|  | ||||
|             <div> | ||||
|  | ||||
|                 <span class="badge-calendar-simple"> | ||||
|                     {{ 'Calendar'|trans }} | ||||
|                     {% if c.endDate.diff(c.startDate).days >= 1 %} | ||||
|                         {{ c.startDate|format_datetime('short', 'short') }} | ||||
|                         - {{ c.endDate|format_datetime('short', 'short') }} | ||||
|                     {% else %} | ||||
|                         {{ c.startDate|format_datetime('short', 'short') }} | ||||
|                         - {{ c.endDate|format_datetime('none', 'short') }} | ||||
|                     {% endif %} | ||||
|                 </span> | ||||
|             </div> | ||||
|  | ||||
|             <div class="denomination h2"> | ||||
|                 {{ document.storedObject.title|chill_print_or_message("No title") }} | ||||
|             </div> | ||||
|             {% if document.storedObject.hasTemplate %} | ||||
|                 <div> | ||||
|                     <p>{{ document.storedObject.template.name|localize_translatable_string }}</p> | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|         <div class="aside"> | ||||
|             <div class="dates row text-end"> | ||||
|                 <span>{{ document.storedObject.createdAt|format_date('short') }}</span> | ||||
|             </div> | ||||
|             {% if c.accompanyingPeriod is not null and context == 'person' %} | ||||
|                 <div class="text-end"> | ||||
|                     <span class="badge bg-primary"> | ||||
|                         <i class="fa fa-random"></i> {{ c.accompanyingPeriod.id }} | ||||
|                     </span>  | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| {% if show_actions %} | ||||
|     <div class="item-row separator"> | ||||
|         <div class="item-col item-meta"> | ||||
|             {{ mmm.createdBy(document) }} | ||||
|         </div> | ||||
|         <ul class="item-col record_actions flex-shrink-1"> | ||||
|             {% if is_granted('CHILL_CALENDAR_DOC_SEE', document) %} | ||||
|                 <li> | ||||
|                     {{ document.storedObject|chill_document_button_group(document.storedObject.title, is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c)) }} | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|             {% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %} | ||||
|                 <li> | ||||
|                     <a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', {'id': c.id, 'docId': document.id}) }}" class="btn btn-edit"></a> | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|         </ul> | ||||
|     </div> | ||||
| {% endif %} | ||||
| @@ -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\CalendarBundle\Service\GenericDoc\Normalizer; | ||||
|  | ||||
| use Chill\CalendarBundle\Repository\CalendarDocRepositoryInterface; | ||||
| use Chill\CalendarBundle\Service\GenericDoc\Providers\AccompanyingPeriodCalendarGenericDocProvider; | ||||
| use Chill\CalendarBundle\Service\GenericDoc\Renderers\AccompanyingPeriodCalendarGenericDocRenderer; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
| use Twig\Environment; | ||||
|  | ||||
| final readonly class AccompanyingPeriodCalendarGenericDocNormalizer implements GenericDocNormalizerInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private AccompanyingPeriodCalendarGenericDocRenderer $renderer, | ||||
|         private CalendarDocRepositoryInterface $calendarDocRepository, | ||||
|         private Environment $twig, | ||||
|         private TranslatorInterface $translator, | ||||
|     ) {} | ||||
|  | ||||
|     public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool | ||||
|     { | ||||
|         return AccompanyingPeriodCalendarGenericDocProvider::KEY === $genericDocDTO->key && 'json' === $format; | ||||
|     } | ||||
|  | ||||
|     public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array | ||||
|     { | ||||
|         if (null === $calendarDoc = $this->calendarDocRepository->find($genericDocDTO->identifiers['id'])) { | ||||
|             return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false]; | ||||
|         } | ||||
|  | ||||
|         return [ | ||||
|             'isPresent' => true, | ||||
|             'title' => $calendarDoc->getStoredObject()->getTitle(), | ||||
|             'html' => $this->twig->render( | ||||
|                 $this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]), | ||||
|                 $this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true]) | ||||
|             ), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -13,10 +13,12 @@ namespace Chill\CalendarBundle\Service\GenericDoc\Providers; | ||||
|  | ||||
| use Chill\CalendarBundle\Entity\Calendar; | ||||
| use Chill\CalendarBundle\Entity\CalendarDoc; | ||||
| use Chill\CalendarBundle\Repository\CalendarDocRepositoryInterface; | ||||
| use Chill\CalendarBundle\Security\Voter\CalendarVoter; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\GenericDoc\FetchQuery; | ||||
| use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| @@ -38,8 +40,38 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen | ||||
|     public function __construct( | ||||
|         private Security $security, | ||||
|         private EntityManagerInterface $em, | ||||
|         private CalendarDocRepositoryInterface $calendarRepository, | ||||
|     ) {} | ||||
|  | ||||
|     public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject | ||||
|     { | ||||
|         return $this->calendarRepository->find($genericDocDTO->identifiers['id'])?->getStoredObject(); | ||||
|     } | ||||
|  | ||||
|     public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool | ||||
|     { | ||||
|         return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers); | ||||
|     } | ||||
|  | ||||
|     public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool | ||||
|     { | ||||
|         return self::KEY === $key && array_key_exists('id', $identifiers); | ||||
|     } | ||||
|  | ||||
|     public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO | ||||
|     { | ||||
|         if (null === $calendarDoc = $this->calendarRepository->find($identifiers['id'])) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         return new GenericDocDTO( | ||||
|             self::KEY, | ||||
|             $identifiers, | ||||
|             \DateTimeImmutable::createFromInterface($calendarDoc->getCreatedAt() ?? new \DateTimeImmutable('now')), | ||||
|             $calendarDoc->getCalendar()->getAccompanyingPeriod() ?? $calendarDoc->getCalendar()->getPerson() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throws MappingException | ||||
|      */ | ||||
| @@ -82,7 +114,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen | ||||
|             [Types::INTEGER] | ||||
|         ); | ||||
|  | ||||
|         return $query; | ||||
|         return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content); | ||||
|     } | ||||
|  | ||||
|     public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool | ||||
|   | ||||
| @@ -17,6 +17,9 @@ use Chill\CalendarBundle\Service\GenericDoc\Providers\PersonCalendarGenericDocPr | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; | ||||
| use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface; | ||||
|  | ||||
| /** | ||||
|  * @implements GenericDocRendererInterface<array{row-only?: bool, show-actions?: bool}> | ||||
|  */ | ||||
| final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements GenericDocRendererInterface | ||||
| { | ||||
|     public function __construct(private CalendarDocRepository $repository) {} | ||||
| @@ -28,7 +31,8 @@ final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements Gen | ||||
|  | ||||
|     public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string | ||||
|     { | ||||
|         return '@ChillCalendar/GenericDoc/calendar_document.html.twig'; | ||||
|         return $options['row-only'] ?? false ? '@ChillCalendar/GenericDoc/calendar_document_row.html.twig' | ||||
|             : '@ChillCalendar/GenericDoc/calendar_document.html.twig'; | ||||
|     } | ||||
|  | ||||
|     public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array | ||||
| @@ -36,6 +40,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements Gen | ||||
|         return [ | ||||
|             'document' => $this->repository->find($genericDocDTO->identifiers['id']), | ||||
|             'context' => $genericDocDTO->getContext(), | ||||
|             'show_actions' => $options['show-actions'] ?? true, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -21,11 +21,17 @@ namespace Chill\CalendarBundle\Service\ShortMessageNotification; | ||||
| use Chill\CalendarBundle\Entity\Calendar; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Component\Messenger\MessageBusInterface; | ||||
| use Symfony\Component\Notifier\TexterInterface; | ||||
|  | ||||
| class BulkCalendarShortMessageSender | ||||
| { | ||||
|     public function __construct(private readonly CalendarForShortMessageProvider $provider, private readonly EntityManagerInterface $em, private readonly LoggerInterface $logger, private readonly MessageBusInterface $messageBus, private readonly ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder) {} | ||||
|     public function __construct( | ||||
|         private readonly CalendarForShortMessageProvider $provider, | ||||
|         private readonly EntityManagerInterface $em, | ||||
|         private readonly LoggerInterface $logger, | ||||
|         private readonly TexterInterface $texter, | ||||
|         private readonly ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder, | ||||
|     ) {} | ||||
|  | ||||
|     public function sendBulkMessageToEligibleCalendars() | ||||
|     { | ||||
| @@ -36,7 +42,7 @@ class BulkCalendarShortMessageSender | ||||
|             $smses = $this->messageForCalendarBuilder->buildMessageForCalendar($calendar); | ||||
|  | ||||
|             foreach ($smses as $sms) { | ||||
|                 $this->messageBus->dispatch($sms); | ||||
|                 $this->texter->send($sms); | ||||
|                 ++$countSms; | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -19,12 +19,26 @@ declare(strict_types=1); | ||||
| namespace Chill\CalendarBundle\Service\ShortMessageNotification; | ||||
|  | ||||
| use Chill\CalendarBundle\Entity\Calendar; | ||||
| use Chill\MainBundle\Service\ShortMessage\ShortMessage; | ||||
| use libphonenumber\PhoneNumberFormat; | ||||
| use libphonenumber\PhoneNumberUtil; | ||||
| use Symfony\Component\Notifier\Message\SmsMessage; | ||||
|  | ||||
| class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBuilderInterface | ||||
| { | ||||
|     public function __construct(private readonly \Twig\Environment $engine) {} | ||||
|     private readonly PhoneNumberUtil $phoneUtil; | ||||
|  | ||||
|     public function __construct(private readonly \Twig\Environment $engine) | ||||
|     { | ||||
|         $this->phoneUtil = PhoneNumberUtil::getInstance(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return list<SmsMessage> | ||||
|      * | ||||
|      * @throws \Twig\Error\LoaderError | ||||
|      * @throws \Twig\Error\RuntimeError | ||||
|      * @throws \Twig\Error\SyntaxError | ||||
|      */ | ||||
|     public function buildMessageForCalendar(Calendar $calendar): array | ||||
|     { | ||||
|         if (true !== $calendar->getSendSMS()) { | ||||
| @@ -39,16 +53,14 @@ class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBu | ||||
|             } | ||||
|  | ||||
|             if (Calendar::SMS_PENDING === $calendar->getSmsStatus()) { | ||||
|                 $toUsers[] = new ShortMessage( | ||||
|                 $toUsers[] = new SmsMessage( | ||||
|                     $this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164), | ||||
|                     $this->engine->render('@ChillCalendar/CalendarShortMessage/short_message.txt.twig', ['calendar' => $calendar]), | ||||
|                     $person->getMobilenumber(), | ||||
|                     ShortMessage::PRIORITY_LOW | ||||
|                 ); | ||||
|             } elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus()) { | ||||
|                 $toUsers[] = new ShortMessage( | ||||
|                 $toUsers[] = new SmsMessage( | ||||
|                     $this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164), | ||||
|                     $this->engine->render('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig', ['calendar' => $calendar]), | ||||
|                     $person->getMobilenumber(), | ||||
|                     ShortMessage::PRIORITY_LOW | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -19,12 +19,12 @@ declare(strict_types=1); | ||||
| namespace Chill\CalendarBundle\Service\ShortMessageNotification; | ||||
|  | ||||
| use Chill\CalendarBundle\Entity\Calendar; | ||||
| use Chill\MainBundle\Service\ShortMessage\ShortMessage; | ||||
| use Symfony\Component\Notifier\Message\SmsMessage; | ||||
|  | ||||
| interface ShortMessageForCalendarBuilderInterface | ||||
| { | ||||
|     /** | ||||
|      * @return array|ShortMessage[] | ||||
|      * @return list<SmsMessage> | ||||
|      */ | ||||
|     public function buildMessageForCalendar(Calendar $calendar): array; | ||||
| } | ||||
|   | ||||
| @@ -23,17 +23,16 @@ use Chill\CalendarBundle\Service\ShortMessageNotification\BulkCalendarShortMessa | ||||
| use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider; | ||||
| use Chill\CalendarBundle\Service\ShortMessageNotification\ShortMessageForCalendarBuilderInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Service\ShortMessage\ShortMessage; | ||||
| use Chill\MainBundle\Test\PrepareUserTrait; | ||||
| use Chill\PersonBundle\DataFixtures\Helper\PersonRandomHelper; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use libphonenumber\PhoneNumberUtil; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Psr\Log\NullLogger; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
| use Symfony\Component\Messenger\Envelope; | ||||
| use Symfony\Component\Messenger\MessageBusInterface; | ||||
| use Symfony\Component\Notifier\Message\SentMessage; | ||||
| use Symfony\Component\Notifier\Message\SmsMessage; | ||||
| use Symfony\Component\Notifier\TexterInterface; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
| @@ -101,24 +100,23 @@ final class BulkCalendarShortMessageSenderTest extends KernelTestCase | ||||
|         $messageBuilder->buildMessageForCalendar(Argument::type(Calendar::class)) | ||||
|             ->willReturn( | ||||
|                 [ | ||||
|                     new ShortMessage( | ||||
|                     new SmsMessage( | ||||
|                         '+32470123456', | ||||
|                         'content', | ||||
|                         PhoneNumberUtil::getInstance()->parse('+32470123456', 'BE'), | ||||
|                         ShortMessage::PRIORITY_MEDIUM | ||||
|                     ), | ||||
|                 ] | ||||
|             ); | ||||
|  | ||||
|         $bus = $this->prophesize(MessageBusInterface::class); | ||||
|         $bus->dispatch(Argument::type(ShortMessage::class)) | ||||
|             ->willReturn(new Envelope(new \stdClass())) | ||||
|         $texter = $this->prophesize(TexterInterface::class); | ||||
|         $texter->send(Argument::type(SmsMessage::class)) | ||||
|             ->will(fn ($args): SentMessage => new SentMessage($args[0], 'sms')) | ||||
|             ->shouldBeCalledTimes(1); | ||||
|  | ||||
|         $bulk = new BulkCalendarShortMessageSender( | ||||
|             $provider->reveal(), | ||||
|             $em, | ||||
|             new NullLogger(), | ||||
|             $bus->reveal(), | ||||
|             $texter->reveal(), | ||||
|             $messageBuilder->reveal() | ||||
|         ); | ||||
|  | ||||
|   | ||||
| @@ -23,7 +23,6 @@ use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultShortMessageFor | ||||
| use Chill\MainBundle\Entity\Location; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use libphonenumber\PhoneNumberFormat; | ||||
| use libphonenumber\PhoneNumberUtil; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\Argument; | ||||
| @@ -90,10 +89,9 @@ final class DefaultShortMessageForCalendarBuilderTest extends TestCase | ||||
|         $this->assertCount(1, $sms); | ||||
|         $this->assertEquals( | ||||
|             '+32470123456', | ||||
|             $this->phoneNumberUtil->format($sms[0]->getPhoneNumber(), PhoneNumberFormat::E164) | ||||
|             $sms[0]->getPhone() | ||||
|         ); | ||||
|         $this->assertEquals('message content', $sms[0]->getContent()); | ||||
|         $this->assertEquals('low', $sms[0]->getPriority()); | ||||
|         $this->assertEquals('message content', $sms[0]->getSubject()); | ||||
|  | ||||
|         // if the calendar is canceled | ||||
|         $calendar | ||||
| @@ -105,9 +103,8 @@ final class DefaultShortMessageForCalendarBuilderTest extends TestCase | ||||
|         $this->assertCount(1, $sms); | ||||
|         $this->assertEquals( | ||||
|             '+32470123456', | ||||
|             $this->phoneNumberUtil->format($sms[0]->getPhoneNumber(), PhoneNumberFormat::E164) | ||||
|             $sms[0]->getRecipientId(), | ||||
|         ); | ||||
|         $this->assertEquals('message canceled', $sms[0]->getContent()); | ||||
|         $this->assertEquals('low', $sms[0]->getPriority()); | ||||
|         $this->assertEquals('message canceled', $sms[0]->getSubject()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,82 @@ | ||||
| <?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\Tests\Controller; | ||||
|  | ||||
| use Chill\DocStoreBundle\Controller\GenericDocForAccompanyingPeriodListApiController; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; | ||||
| use Chill\DocStoreBundle\GenericDoc\ManagerInterface; | ||||
| use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; | ||||
| use Chill\MainBundle\Pagination\Paginator; | ||||
| use Chill\MainBundle\Pagination\PaginatorFactoryInterface; | ||||
| use Chill\MainBundle\Serializer\Model\Collection; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Symfony\Component\HttpFoundation\JsonResponse; | ||||
| use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Component\Serializer\SerializerInterface; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class GenericDocForAccompanyingPeriodListApiControllerTest extends TestCase | ||||
| { | ||||
|     public function testSmokeTest(): void | ||||
|     { | ||||
|         $accompanyingPeriod = new AccompanyingPeriod(); | ||||
|  | ||||
|         $docs = [ | ||||
|             new GenericDocDTO('dummy', ['id' => 9], new \DateTimeImmutable('2024-08-01'), $accompanyingPeriod), | ||||
|             new GenericDocDTO('dummy', ['id' => 1], new \DateTimeImmutable('2024-09-01'), $accompanyingPeriod), | ||||
|         ]; | ||||
|  | ||||
|  | ||||
|         $manager = $this->createMock(ManagerInterface::class); | ||||
|         $manager->method('findDocForAccompanyingPeriod')->with($accompanyingPeriod)->willReturn($docs); | ||||
|         $manager->method('countDocForAccompanyingPeriod')->with($accompanyingPeriod)->willReturn(2); | ||||
|  | ||||
|         $paginatorFactory = $this->createMock(PaginatorFactoryInterface::class); | ||||
|         $paginatorFactory->method('create')->with(2)->willReturn(new Paginator( | ||||
|             2, | ||||
|             20, | ||||
|             1, | ||||
|             '/route', | ||||
|             [], | ||||
|             $this->createMock(UrlGeneratorInterface::class), | ||||
|             'page', | ||||
|             'item-per-page' | ||||
|         )); | ||||
|  | ||||
|         $serializer = $this->createMock(SerializerInterface::class); | ||||
|         $serializer->method('serialize')->with($this->isInstanceOf(Collection::class))->willReturn( | ||||
|             json_encode(['docs' => []]) | ||||
|         ); | ||||
|  | ||||
|         $security = $this->createMock(Security::class); | ||||
|         $security->expects($this->once())->method('isGranted') | ||||
|             ->with(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod)->willReturn(true); | ||||
|  | ||||
|         $controller = new GenericDocForAccompanyingPeriodListApiController( | ||||
|             $manager, | ||||
|             $security, | ||||
|             $paginatorFactory, | ||||
|             $serializer, | ||||
|         ); | ||||
|  | ||||
|         $response = $controller($accompanyingPeriod); | ||||
|  | ||||
|         $this->assertInstanceOf(JsonResponse::class, $response); | ||||
|         $this->assertEquals('{"docs":[]}', $response->getContent()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,227 @@ | ||||
| <?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\AsyncUpload\Driver\LocalStorage; | ||||
|  | ||||
| use Base64Url\Base64Url; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectVersion; | ||||
| use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
| use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | ||||
| use Symfony\Component\Filesystem\Filesystem; | ||||
| use Symfony\Component\Filesystem\Path; | ||||
|  | ||||
| class StoredObjectManager implements StoredObjectManagerInterface | ||||
| { | ||||
|     private readonly string $baseDir; | ||||
|  | ||||
|     private readonly Filesystem $filesystem; | ||||
|  | ||||
|     public function __construct( | ||||
|         ParameterBagInterface $parameterBag, | ||||
|         private readonly KeyGenerator $keyGenerator, | ||||
|     ) { | ||||
|         $this->baseDir = $parameterBag->get('chill_doc_store')['local_storage']['storage_path']; | ||||
|         $this->filesystem = new Filesystem(); | ||||
|     } | ||||
|  | ||||
|     public function getLastModified(StoredObject|StoredObjectVersion $document): \DateTimeInterface | ||||
|     { | ||||
|         $version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document; | ||||
|  | ||||
|         if (null === $version) { | ||||
|             throw StoredObjectManagerException::storedObjectDoesNotContainsVersion(); | ||||
|         } | ||||
|  | ||||
|         $path = $this->buildPath($version->getFilename()); | ||||
|  | ||||
|         if (false === $ts = filemtime($path)) { | ||||
|             throw StoredObjectManagerException::unableToReadDocumentOnDisk($path); | ||||
|         } | ||||
|  | ||||
|         return \DateTimeImmutable::createFromFormat('U', (string) $ts); | ||||
|     } | ||||
|  | ||||
|     public function getContentLength(StoredObject|StoredObjectVersion $document): int | ||||
|     { | ||||
|         return strlen($this->read($document)); | ||||
|     } | ||||
|  | ||||
|     public function exists(StoredObject|StoredObjectVersion $document): bool | ||||
|     { | ||||
|         $version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document; | ||||
|  | ||||
|         if (null === $version) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return $this->existsContent($version->getFilename()); | ||||
|     } | ||||
|  | ||||
|     public function read(StoredObject|StoredObjectVersion $document): string | ||||
|     { | ||||
|         $version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document; | ||||
|  | ||||
|         if (null === $version) { | ||||
|             throw StoredObjectManagerException::storedObjectDoesNotContainsVersion(); | ||||
|         } | ||||
|  | ||||
|         $content = $this->readContent($version->getFilename()); | ||||
|  | ||||
|         if (!$this->isVersionEncrypted($version)) { | ||||
|             return $content; | ||||
|         } | ||||
|  | ||||
|         $clearData = openssl_decrypt( | ||||
|             $content, | ||||
|             self::ALGORITHM, | ||||
|             // TODO: Why using this library and not use base64_decode() ? | ||||
|             Base64Url::decode($version->getKeyInfos()['k']), | ||||
|             \OPENSSL_RAW_DATA, | ||||
|             pack('C*', ...$version->getIv()) | ||||
|         ); | ||||
|  | ||||
|         if (false === $clearData) { | ||||
|             throw StoredObjectManagerException::unableToDecrypt(openssl_error_string()); | ||||
|         } | ||||
|  | ||||
|         return $clearData; | ||||
|     } | ||||
|  | ||||
|     public function write(StoredObject $document, string $clearContent, ?string $contentType = null): StoredObjectVersion | ||||
|     { | ||||
|         $newIv = $document->isEncrypted() ? $document->getIv() : $this->keyGenerator->generateIv(); | ||||
|         $newKey = $document->isEncrypted() ? $document->getKeyInfos() : $this->keyGenerator->generateKey(self::ALGORITHM); | ||||
|         $newType = $contentType ?? $document->getType(); | ||||
|         $version = $document->registerVersion( | ||||
|             $newIv, | ||||
|             $newKey, | ||||
|             $newType | ||||
|         ); | ||||
|  | ||||
|         $encryptedContent = $this->isVersionEncrypted($version) | ||||
|             ? openssl_encrypt( | ||||
|                 $clearContent, | ||||
|                 self::ALGORITHM, | ||||
|                 // TODO: Why using this library and not use base64_decode() ? | ||||
|                 Base64Url::decode($version->getKeyInfos()['k']), | ||||
|                 \OPENSSL_RAW_DATA, | ||||
|                 pack('C*', ...$version->getIv()) | ||||
|             ) | ||||
|             : $clearContent; | ||||
|  | ||||
|         if (false === $encryptedContent) { | ||||
|             throw StoredObjectManagerException::unableToEncryptDocument((string) openssl_error_string()); | ||||
|         } | ||||
|  | ||||
|         $this->writeContent($version->getFilename(), $encryptedContent); | ||||
|  | ||||
|         return $version; | ||||
|     } | ||||
|  | ||||
|     public function readContent(string $filename): string | ||||
|     { | ||||
|         $path = $this->buildPath($filename); | ||||
|  | ||||
|         if (!file_exists($path)) { | ||||
|             throw StoredObjectManagerException::unableToFindDocumentOnDisk($path); | ||||
|         } | ||||
|  | ||||
|         if (false === $content = file_get_contents($path)) { | ||||
|             throw StoredObjectManagerException::unableToReadDocumentOnDisk($path); | ||||
|         } | ||||
|  | ||||
|         return $content; | ||||
|     } | ||||
|  | ||||
|     public function writeContent(string $filename, string $encryptedContent): void | ||||
|     { | ||||
|         $fullPath = $this->buildPath($filename); | ||||
|         $dir = Path::getDirectory($fullPath); | ||||
|  | ||||
|         if (!$this->filesystem->exists($dir)) { | ||||
|             $this->filesystem->mkdir($dir); | ||||
|         } | ||||
|  | ||||
|         $result = file_put_contents($fullPath, $encryptedContent); | ||||
|  | ||||
|         if (false === $result) { | ||||
|             throw StoredObjectManagerException::unableToStoreDocumentOnDisk(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function existsContent(string $filename): bool | ||||
|     { | ||||
|         $path = $this->buildPath($filename); | ||||
|  | ||||
|         return $this->filesystem->exists($path); | ||||
|     } | ||||
|  | ||||
|     private function buildPath(string $filename): string | ||||
|     { | ||||
|         $dirs = [$this->baseDir]; | ||||
|  | ||||
|         for ($i = 0; $i < min(strlen($filename), 8); ++$i) { | ||||
|             $dirs[] = $filename[$i]; | ||||
|         } | ||||
|  | ||||
|         $dirs[] = $filename; | ||||
|  | ||||
|         return Path::canonicalize(implode(DIRECTORY_SEPARATOR, $dirs)); | ||||
|     } | ||||
|  | ||||
|     public function delete(StoredObjectVersion $storedObjectVersion): void | ||||
|     { | ||||
|         if (!$this->exists($storedObjectVersion)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $path = $this->buildPath($storedObjectVersion->getFilename()); | ||||
|  | ||||
|         $this->filesystem->remove($path); | ||||
|         $this->removeDirectoriesRecursively(Path::getDirectory($path)); | ||||
|     } | ||||
|  | ||||
|     private function removeDirectoriesRecursively(string $path): void | ||||
|     { | ||||
|         if ($path === $this->baseDir) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $files = scandir($path); | ||||
|  | ||||
|         // if it does contains only "." and "..", we can remove the directory | ||||
|         if (2 === count($files) && in_array('.', $files, true) && in_array('..', $files, true)) { | ||||
|             $this->filesystem->remove($path); | ||||
|             $this->removeDirectoriesRecursively(Path::getDirectory($path)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throws StoredObjectManagerException | ||||
|      */ | ||||
|     public function etag(StoredObject|StoredObjectVersion $document): string | ||||
|     { | ||||
|         return md5($this->read($document)); | ||||
|     } | ||||
|  | ||||
|     public function clearCache(): void | ||||
|     { | ||||
|         // there is no cache: nothing to do here ! | ||||
|     } | ||||
|  | ||||
|     private function isVersionEncrypted(StoredObjectVersion $storedObjectVersion): bool | ||||
|     { | ||||
|         return $storedObjectVersion->isEncrypted(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,107 @@ | ||||
| <?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\AsyncUpload\Driver\LocalStorage; | ||||
|  | ||||
| use Chill\DocStoreBundle\AsyncUpload\SignedUrl; | ||||
| use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost; | ||||
| use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Symfony\Component\Clock\ClockInterface; | ||||
| use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||||
|  | ||||
| class TempUrlLocalStorageGenerator implements TempUrlGeneratorInterface | ||||
| { | ||||
|     private const SIGNATURE_DURATION = 180; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly string $secret, | ||||
|         private readonly ClockInterface $clock, | ||||
|         private readonly UrlGeneratorInterface $urlGenerator, | ||||
|     ) {} | ||||
|  | ||||
|     public function generate(string $method, string $object_name, ?int $expire_delay = null): SignedUrl | ||||
|     { | ||||
|         $expiration = $this->clock->now()->getTimestamp() + min($expire_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION); | ||||
|  | ||||
|         return new SignedUrl( | ||||
|             strtoupper($method), | ||||
|             $this->urlGenerator->generate('chill_docstore_stored_object_operate', [ | ||||
|                 'object_name' => $object_name, | ||||
|                 'exp' => $expiration, | ||||
|                 'sig' => $this->sign(strtoupper($method), $object_name, $expiration), | ||||
|             ], UrlGeneratorInterface::ABSOLUTE_URL), | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) $expiration), | ||||
|             $object_name, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function generatePost(?int $expire_delay = null, ?int $submit_delay = null, int $max_file_count = 1, ?string $object_name = null): SignedUrlPost | ||||
|     { | ||||
|         $submitDelayComputed = min($submit_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION); | ||||
|         $expireDelayComputed = min($expire_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION); | ||||
|         $objectNameComputed = $object_name ?? StoredObject::generatePrefix(); | ||||
|         $expiration = $this->clock->now()->getTimestamp() + $expireDelayComputed + $submitDelayComputed; | ||||
|  | ||||
|         return new SignedUrlPost( | ||||
|             $this->urlGenerator->generate( | ||||
|                 'chill_docstore_storedobject_post', | ||||
|                 ['prefix' => $objectNameComputed], | ||||
|                 UrlGeneratorInterface::ABSOLUTE_URL | ||||
|             ), | ||||
|             \DateTimeImmutable::createFromFormat('U', (string) $expiration), | ||||
|             $objectNameComputed, | ||||
|             15_000_000, | ||||
|             1, | ||||
|             $submitDelayComputed, | ||||
|             '', | ||||
|             $objectNameComputed, | ||||
|             $this->sign('POST', $object_name, $expiration), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private function sign(string $method, string $object_name, int $expiration): string | ||||
|     { | ||||
|         return hash('sha512', sprintf('%s.%s.%s.%d', $method, $this->secret, $object_name, $expiration)); | ||||
|     } | ||||
|  | ||||
|     public function validateSignaturePost(string $signature, string $prefix, int $expiration, int $maxFileSize, int $maxFileCount): bool | ||||
|     { | ||||
|         if (15_000_000 !== $maxFileSize || 1 !== $maxFileCount) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return $this->internalValidateSignature($signature, 'POST', $prefix, $expiration); | ||||
|     } | ||||
|  | ||||
|     private function internalValidateSignature(string $signature, string $method, string $object_name, int $expiration): bool | ||||
|     { | ||||
|         if ($expiration < $this->clock->now()->format('U')) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if ('' === $object_name) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|  | ||||
|         return $this->sign($method, $object_name, $expiration) === $signature; | ||||
|     } | ||||
|  | ||||
|     public function validateSignature(string $signature, string $method, string $objectName, int $expiration): bool | ||||
|     { | ||||
|         if (!in_array($method, ['GET', 'HEAD'], true)) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return $this->internalValidateSignature($signature, $method, $objectName, $expiration); | ||||
|     } | ||||
| } | ||||
| @@ -9,7 +9,7 @@ declare(strict_types=1); | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
| 
 | ||||
| namespace Chill\DocStoreBundle\AsyncUpload\Command; | ||||
| namespace Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore; | ||||
| 
 | ||||
| use Symfony\Component\Console\Command\Command; | ||||
| use Symfony\Component\Console\Input\InputInterface; | ||||
| @@ -9,13 +9,14 @@ declare(strict_types=1); | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
| 
 | ||||
| namespace Chill\DocStoreBundle\Service; | ||||
| namespace Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore; | ||||
| 
 | ||||
| use Base64Url\Base64Url; | ||||
| use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Entity\StoredObjectVersion; | ||||
| use Chill\DocStoreBundle\Exception\StoredObjectManagerException; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; | ||||
| @@ -24,8 +25,6 @@ use Symfony\Contracts\HttpClient\ResponseInterface; | ||||
| 
 | ||||
| final class StoredObjectManager implements StoredObjectManagerInterface | ||||
| { | ||||
|     private const ALGORITHM = 'AES-256-CBC'; | ||||
| 
 | ||||
|     private array $inMemory = []; | ||||
| 
 | ||||
|     public function __construct( | ||||
| @@ -361,6 +360,6 @@ final class StoredObjectManager implements StoredObjectManagerInterface | ||||
| 
 | ||||
|     private function isVersionEncrypted(StoredObjectVersion $storedObjectVersion): bool | ||||
|     { | ||||
|         return ([] !== $storedObjectVersion->getKeyInfos()) && ([] !== $storedObjectVersion->getIv()); | ||||
|         return $storedObjectVersion->isEncrypted(); | ||||
|     } | ||||
| } | ||||
| @@ -11,8 +11,10 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\DocStoreBundle; | ||||
|  | ||||
| use Chill\DocStoreBundle\DependencyInjection\Compiler\StorageConfigurationCompilerPass; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; | ||||
| use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface; | ||||
| use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface; | ||||
| use Symfony\Component\DependencyInjection\ContainerBuilder; | ||||
| use Symfony\Component\HttpKernel\Bundle\Bundle; | ||||
| @@ -27,5 +29,9 @@ class ChillDocStoreBundle extends Bundle | ||||
|             ->addTag('chill_doc_store.generic_doc_person_provider'); | ||||
|         $container->registerForAutoconfiguration(GenericDocRendererInterface::class) | ||||
|             ->addTag('chill_doc_store.generic_doc_renderer'); | ||||
|         $container->registerForAutoconfiguration(GenericDocNormalizerInterface::class) | ||||
|             ->addTag('chill_doc_store.generic_doc_metadata_normalizer'); | ||||
|  | ||||
|         $container->addCompilerPass(new StorageConfigurationCompilerPass()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -92,13 +92,14 @@ class DocumentCategoryController extends AbstractController | ||||
|  | ||||
|         $nextId = $em | ||||
|             ->createQuery( | ||||
|                 'SELECT MAX(c.idInsideBundle) + 1 FROM ChillDocStoreBundle:DocumentCategory c' | ||||
|                 'SELECT (CASE WHEN MAX(c.idInsideBundle) IS NULL THEN 1 ELSE MAX(c.idInsideBundle) + 1 END) | ||||
|          FROM ChillDocStoreBundle:DocumentCategory c' | ||||
|             ) | ||||
|             ->getSingleResult(); | ||||
|             ->getSingleScalarResult(); | ||||
|  | ||||
|         $documentCategory = new DocumentCategory( | ||||
|             ChillDocStoreBundle::class, | ||||
|             reset($nextId) | ||||
|             $nextId | ||||
|         ); | ||||
|  | ||||
|         $documentCategory | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user