mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 01:08:26 +00:00 
			
		
		
		
	Compare commits
	
		
			316 Commits
		
	
	
		
			405-aside-
			...
			ticket/64-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3f6bbbc1b3 | |||
| 31e57d7507 | |||
| 5c098a336d | |||
| e107d20bea | |||
| 491fd81f9b | |||
| 8c2acbd166 | |||
| e291c7abec | |||
| 1e186fab58 | |||
| 83a2c04537 | |||
| e89b33bc1a | |||
| 6b208e9962 | |||
| b0c63fab91 | |||
| 6d4c4d2c74 | |||
| df6087d468 | |||
| ffac143ab9 | |||
| f1bf6023ff | |||
| 71e146e4f0 | |||
| 4234377b7e | |||
| 1be82b3049 | |||
| 808954df42 | |||
| 3f8bb6c5c0 | |||
| 23e1a0d36a | |||
| a594d86346 | |||
| 870907804b | |||
| e9e6c05e3d | |||
| 532f2dd842 | |||
| d14d4d4d8f | |||
| a22cbe0239 | |||
| 98902bdeb8 | |||
| 592a0f3698 | |||
| d469eb19ad | |||
| 4765d4fe28 | |||
|  | 30bcb85549 | ||
| 189a9337b4 | |||
| c030232a73 | |||
| d4f9726f90 | |||
| 8740025dbd | |||
| 6d8ef035ea | |||
| 60eab628ee | |||
| 1fd559b722 | |||
| b526e802d7 | |||
| 60937152c3 | |||
| 6d2e78ce55 | |||
| e566f60a4a | |||
| c06531cddb | |||
| 61ca700bbe | |||
|  | b43aeebc3c | ||
| 056e2dcc5f | |||
| e57d1ac696 | |||
| 4a1da25fee | |||
| 0eff1d2e79 | |||
| 3928b2cc7a | |||
| 02783e5391 | |||
| 4f51ef81ad | |||
| 4637dc692c | |||
| be3b9f0f56 | |||
| ee006f55d6 | |||
| 13b1c45271 | |||
| ad2b6d63ac | |||
| bfbde078b7 | |||
| d42a1296c4 | |||
| 4b7e3c1601 | |||
| 6ea9af588b | |||
| 0fd76d3fa8 | |||
| 34af53130b | |||
| a1fd395868 | |||
| b8a7cbb321 | |||
| 6124eb9e34 | |||
| a5b06de92a | |||
| 52404956d2 | |||
| 4207efd6bf | |||
| 840fde4ad4 | |||
| 3611ea2518 | |||
| bbd4292cb9 | |||
| 54f8c92240 | |||
| 5330befc8f | |||
| c19206be0c | |||
| 5ff374d2fa | |||
| 4a73aaae94 | |||
| ff2c567d05 | |||
| a734e84f28 | |||
| 4367ed086e | |||
| 3227bfcd3a | |||
| 8d29fb260a | |||
| bda0743c63 | |||
| d9b730627f | |||
| 27548ad654 | |||
| bec7297039 | |||
| 852523e644 | |||
| c05d0aad47 | |||
| 1c0ed9abc8 | |||
| 9aed5cc216 | |||
| e4fe5bff68 | |||
| 4c73c4d9d0 | |||
| 38935edb93 | |||
|  | e1ef65d4ca | ||
| ec9d0be70b | |||
|  | 0ba2cbc1e8 | ||
| e87429933a | |||
| 8e2e676e3d | |||
| e12ad563a3 | |||
|  | 711aa8db9b | ||
| e78d44953f | |||
| 18f67801c7 | |||
| c815e6bc69 | |||
| 807f2711fe | |||
|  | cd594cd580 | ||
| fb6b26bfb5 | |||
| c5cedb8bd6 | |||
| 2665e43a61 | |||
| 25561cdf63 | |||
| 10b73e06e1 | |||
|  | e7c04e34a9 | ||
| 164beee7c6 | |||
|  | 4d96eb9457 | ||
| fe2eba3b29 | |||
|  | 61d1232e31 | ||
| 6594d4f6a6 | |||
| 1a66a9e864 | |||
| 1b74c119dc | |||
| 14d88810f3 | |||
| 445a2c9358 | |||
| c8baf0a8aa | |||
| faed443a96 | |||
| bbf387d96f | |||
|  | b7c9b60744 | ||
| 2bd303bbbe | |||
| c5e6122d2c | |||
| 088b876e20 | |||
| 3400656d7c | |||
| 568c8be7fd | |||
| 538ecc42ea | |||
| 15d26d4b06 | |||
| d8bd9bd7cd | |||
| dcdfba5ccd | |||
| 0204bdd38d | |||
| 392fd01b56 | |||
| 35844f3b73 | |||
| 7506b918d7 | |||
| cfba291f2c | |||
|  | 04438c09d3 | ||
| 2a54d1b909 | |||
|  | 628eeac5e0 | ||
| a2263b3fa1 | |||
| 74796d0fb0 | |||
| c19481e40a | |||
|  | 6eeb717b1a | ||
| beb7c462da | |||
|  | dbf363a9e8 | ||
| 64a2f7c9ed | |||
| f26d9739c8 | |||
| afa5edc1d8 | |||
| 42d6c9e672 | |||
| 2b22d4cb7c | |||
| c8e5d0eb37 | |||
| 2bf8ad5d6c | |||
| 11698a52e3 | |||
| 70955573e8 | |||
|  | 3df4043eb9 | ||
| 06e8264dde | |||
| b451d2c4a3 | |||
| 4f93150874 | |||
| 0566ab0910 | |||
|  | f4eeee1598 | ||
| 33cf16fc13 | |||
| 0a331aab37 | |||
| d43b739654 | |||
| c72432efae | |||
| 95975fae55 | |||
| 95a7efa138 | |||
| 45e193ff6d | |||
| dfc146ff3f | |||
| b41fcf66a9 | |||
|  | a8dd1b3548 | ||
| 2b99a480ac | |||
| 7633e587bb | |||
| fc61dfdf3a | |||
| f1a5b5c49e | |||
| ec685dcd47 | |||
| 631ae3eedd | |||
| 440a7837ac | |||
| e0abf34784 | |||
| 377ae9a9dc | |||
| 034dc30e30 | |||
| d615111a0f | |||
| ffb756c712 | |||
| 69daccb860 | |||
| 16435423cf | |||
| 697b4ab436 | |||
| 67d804e28e | |||
| cf41fa9574 | |||
| b8b325f7d7 | |||
| e97bd8c4ef | |||
| e28d7df533 | |||
| 4b20b1bc01 | |||
| b15733076c | |||
| 25be5c9ea3 | |||
| b035020c6f | |||
| 128101dc46 | |||
| 5f2711023e | |||
| bdf2ed4bbd | |||
| 1df542603e | |||
| 80bcc68ce5 | |||
| 154fc3e2f6 | |||
| e45af94c78 | |||
| 166a6fde20 | |||
| 631f047338 | |||
| a777588bb8 | |||
| ca78d112c2 | |||
| bcfd317d83 | |||
| 348740f073 | |||
| 0d74f0980f | |||
| be19dc00db | |||
| 643028ffd6 | |||
| ac4e2e5bf2 | |||
| 498572b96e | |||
| d2a61ce69b | |||
| a9c0567ee1 | |||
| 76cec5b5a8 | |||
| efe8a67697 | |||
| 26dfa9b028 | |||
| 50025044d3 | |||
| e6202a2e34 | |||
| b863bd967d | |||
| e65bcf7275 | |||
| e00ece4200 | |||
| 640fd71402 | |||
| aae50ca290 | |||
| 1fa483598b | |||
| e4b6a468f8 | |||
|  | 66c7758023 | ||
|  | 4750d2c24e | ||
|  | ca05e3d979 | ||
|  | a20f9b4f86 | ||
|  | c73c1eb8d5 | ||
|  | 8778bb0731 | ||
|  | c7d20eebc5 | ||
|  | b9e130c159 | ||
|  | 3e8bc94af3 | ||
|  | 0c914c9f9f | ||
|  | 580a60c939 | ||
|  | 4996ac3b7c | ||
|  | 2a23bf19cb | ||
|  | 650d2596d9 | ||
|  | 2bdd5a329e | ||
| 78d1776733 | |||
| 66dc603c85 | |||
| 3a8154ecce | |||
| c81828e04f | |||
|  | ec17dd7de2 | ||
| 76c076a5f3 | |||
|  | f0045edd6c | ||
|  | d00b76ffcd | ||
|  | 8991f0ef3f | ||
|  | d6f5eae0c9 | ||
|  | 821fce3dd8 | ||
|  | 1d33ae1e39 | ||
|  | 19af0feb57 | ||
|  | 1c09e9a692 | ||
|  | d72e748388 | ||
|  | ab850b7b70 | ||
|  | 3f9745d8cf | ||
|  | 473765366a | ||
|  | 6500c24a7f | ||
|  | 1d00457141 | ||
|  | eb0bf56cff | ||
|  | 7b8cd90cf1 | ||
|  | a27d92aba0 | ||
|  | 85bdfb9e21 | ||
|  | 4cffcf4de1 | ||
|  | b2587a688f | ||
|  | c9f0e9843b | ||
|  | b40ad9e445 | ||
|  | 3e10e47e29 | ||
|  | 2a1963e993 | ||
| 34c171659b | |||
| 2d8b960d9e | |||
| 831ae03431 | |||
| 45828174d1 | |||
| ed45f14a45 | |||
| fa67835690 | |||
| b434d38091 | |||
|  | 800a952532 | ||
| 9f355032a8 | |||
| 0bc6e62d4d | |||
| 46fb1c04b5 | |||
| 3b2c3d1464 | |||
|  | 0bd6038160 | ||
|  | baab8e94ce | ||
| e2deb55fdb | |||
|  | 2cdfb50058 | ||
| 39d701feb2 | |||
| 613ee8b186 | |||
| 56a1a488de | |||
| 3f789ad0f4 | |||
| 467bea7cde | |||
| 670b8eb82b | |||
| a9760b323f | |||
| 71a3a1924a | |||
| ecdc1e25bf | |||
| dd37427be1 | |||
| c8467df1b1 | |||
| 4c89a954fa | |||
| 7c1f3b114d | |||
| 36bc4dab24 | |||
| 4b30d92282 | |||
| 75fbec5489 | |||
| 912fdd6349 | |||
| 5832542978 | |||
| 5c3585a1ed | |||
| a2f1e20ddf | |||
| 4d67702a76 | |||
| 18e442db29 | |||
|  | deb3d92189 | ||
| a59ea7db31 | |||
| a738b0cac9 | 
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20240530-160003.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20240530-160003.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Feature | ||||
| body: | | ||||
|   Upgrade import of address list to the last version of compiled addresses of belgian-best-address | ||||
| time: 2024-05-30T16:00:03.440767606+02:00 | ||||
| custom: | ||||
|   Issue: "" | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20240531-190242.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20240531-190242.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Feature | ||||
| body: | | ||||
|   Upgrade CKEditor and refactor configuration with use of typescript | ||||
| time: 2024-05-31T19:02:42.776662753+02:00 | ||||
| custom: | ||||
|   Issue: "" | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250904-181032.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250904-181032.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Feature | ||||
| body: Add a command to generate a list of permissions | ||||
| time: 2025-09-04T18:10:32.334524026+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
| @@ -1,6 +0,0 @@ | ||||
| kind: Fixed | ||||
| body: Fix the rendering of list of StoredObjectVersions, where there are kept version (before converting to pdf) and intermediate versions deleted | ||||
| time: 2025-10-03T22:40:44.685474863+02:00 | ||||
| custom: | ||||
|     Issue: "" | ||||
|     SchemaChange: No schema change | ||||
| @@ -1,6 +0,0 @@ | ||||
| kind: Fixed | ||||
| body: 'Notification: fix editing of sent notification by removing form.addressesEmails, a field that no longer exists' | ||||
| time: 2025-10-06T12:13:15.45905994+02:00 | ||||
| custom: | ||||
|     Issue: "434" | ||||
|     SchemaChange: No schema change | ||||
| @@ -1,10 +0,0 @@ | ||||
| ## v4.3.0 - 2025-09-08 | ||||
| ### Feature | ||||
| * ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges    | ||||
| * Add a command to generate a list of permissions    | ||||
| * ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date    | ||||
|  | ||||
|   **Schema Change**: Add columns or tables | ||||
| ### Fixed | ||||
| * fix date formatting in calendar range display    | ||||
| * Change route URL to avoid clash with person duplicate controller method    | ||||
| @@ -1,8 +0,0 @@ | ||||
| ## v4.4.0 - 2025-09-11 | ||||
| ### Feature | ||||
| * ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works    | ||||
| * ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation    | ||||
| * ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works    | ||||
| ### Fixed | ||||
| * Fix display of 'duplicate' and 'merge' buttons in CRUD templates    | ||||
| * Fix saving notification preferences in user's profile    | ||||
| @@ -1,3 +0,0 @@ | ||||
| ## v4.4.1 - 2025-09-11 | ||||
| ### Fixed | ||||
| * fix translations in duplicate evaluation document modal and realign close modal button    | ||||
| @@ -1,3 +0,0 @@ | ||||
| ## v4.4.2 - 2025-09-12 | ||||
| ### Fixed | ||||
| * Fix document generation and workflow generation do not work on accompanying period work documents    | ||||
| @@ -1,13 +0,0 @@ | ||||
| ## v4.5.0 - 2025-10-03 | ||||
| ### Feature | ||||
| * Only allow delete of attachment on workflows that are not final    | ||||
| * Move up signature buttons on index workflow page for easier access    | ||||
| * Filter out document from attachment list if it is the same as the workflow document    | ||||
| * Block edition on attached document on workflow, if the workflow is finalized or sent external    | ||||
| * Convert workflow's attached document to pdf while sending them external    | ||||
| * After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition    | ||||
| ### Fixed | ||||
| * ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance    | ||||
| * Fix permissions on storedObject which are subject by a workflow    | ||||
| ### DX | ||||
| * Introduce a WaitingScreen component to display a waiting screen    | ||||
| @@ -1,4 +0,0 @@ | ||||
| ## v4.5.1 - 2025-10-03 | ||||
| ### Fixed | ||||
| * Add missing javascript dependency    | ||||
| * Add exception handling for conversion of attachment on sending external, when documens are already in pdf    | ||||
| @@ -19,11 +19,11 @@ max_line_length = 80 | ||||
| [COMMIT_EDITMSG] | ||||
| max_line_length = 0 | ||||
|  | ||||
| [*.{js, vue, ts}] | ||||
| [*.{js,vue,ts}] | ||||
| indent_size = 2 | ||||
| indent_style = space | ||||
|  | ||||
| [.rst] | ||||
| ident_size = 3 | ||||
| ident_style = space | ||||
| [*.rst] | ||||
| indent_size = 3 | ||||
| indent_style = space | ||||
|  | ||||
|   | ||||
| @@ -234,20 +234,16 @@ This must be a decision made by a human, not by an AI. Every AI task must abort | ||||
|  | ||||
| #### Running Tests | ||||
|  | ||||
| The tests are run from the project's root (not from the bundle's root). | ||||
| The tests are run from the project's root (not from the bundle's root: so, do not change the directory to any bundle directory before running tests). | ||||
|  | ||||
| Tests must be run using the `symfony` command: | ||||
|  | ||||
| ```bash | ||||
| # Run all tests | ||||
| vendor/bin/phpunit | ||||
|  | ||||
| # Run tests for a specific bundle | ||||
| vendor/bin/phpunit --testsuite NameBundle | ||||
|  | ||||
| # Run a specific test file | ||||
| vendor/bin/phpunit path/to/TestFile.php | ||||
| symfony composer exec phpunit -- path/to/TestFile.php | ||||
|  | ||||
| # Run a specific test method | ||||
| vendor/bin/phpunit --filter methodName path/to/TestFile.php | ||||
| symfony composer exec phpunit -- --filter methodName path/to/TestFile.php | ||||
| ``` | ||||
|  | ||||
| #### Test Structure | ||||
|   | ||||
							
								
								
									
										4
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| { | ||||
|   "tabWidth": 2, | ||||
|   "useTabs": false | ||||
| } | ||||
							
								
								
									
										30
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| { | ||||
|     // Use IntelliSense to learn about possible attributes. | ||||
|     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||||
|     "version": "0.2.0", | ||||
|     "configurations": [ | ||||
|         { | ||||
|             "name": "Chill Debug", | ||||
|             "type": "php", | ||||
|             "request": "launch", | ||||
|             "port": 9000, | ||||
|             "pathMappings": { | ||||
|                 "/var/www/html": "${workspaceFolder}" | ||||
|             }, | ||||
|             "preLaunchTask": "symfony" | ||||
|         }, | ||||
|         { | ||||
|             "name": "Yarn Encore Dev (Watch)", | ||||
|             "type": "node-terminal", | ||||
|             "request": "launch", | ||||
|             "command": "yarn encore dev --watch", | ||||
|             "cwd": "${workspaceFolder}" | ||||
|         } | ||||
|     ], | ||||
|     "compounds": [ | ||||
|         { | ||||
|             "name": "Chill Debug + Yarn Encore Dev (Watch)", | ||||
|             "configurations": ["Chill Debug", "Yarn Encore Dev (Watch)"] | ||||
|         } | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										23
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| { | ||||
|     "tasks": [ | ||||
|         { | ||||
|             "type": "shell", | ||||
|             "command": "symfony", | ||||
|             "args": [ | ||||
|                 "server:start", | ||||
|                 "--allow-http", | ||||
|                 "--no-tls", | ||||
|                 "--port=8000", | ||||
|                 "--allow-all-ip", | ||||
|                 "-d" | ||||
|             ], | ||||
|             "label": "symfony" | ||||
|         }, | ||||
|         { | ||||
|             "type": "shell", | ||||
|             "command": "yarn", | ||||
|             "args": ["encore", "dev", "--watch"], | ||||
|             "label": "webpack" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										47
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -6,53 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), | ||||
| and is generated by [Changie](https://github.com/miniscruff/changie). | ||||
|  | ||||
|  | ||||
| ## v4.5.1 - 2025-10-03 | ||||
| ### Fixed | ||||
| * Add missing javascript dependency    | ||||
| * Add exception handling for conversion of attachment on sending external, when documens are already in pdf    | ||||
|  | ||||
| ## v4.5.0 - 2025-10-03 | ||||
| ### Feature | ||||
| * Only allow delete of attachment on workflows that are not final    | ||||
| * Move up signature buttons on index workflow page for easier access    | ||||
| * Filter out document from attachment list if it is the same as the workflow document    | ||||
| * Block edition on attached document on workflow, if the workflow is finalized or sent external    | ||||
| * Convert workflow's attached document to pdf while sending them external    | ||||
| * After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition    | ||||
| ### Fixed | ||||
| * ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance    | ||||
| * Fix permissions on storedObject which are subject by a workflow    | ||||
| ### DX | ||||
| * Introduce a WaitingScreen component to display a waiting screen    | ||||
|  | ||||
| ## v4.4.2 - 2025-09-12 | ||||
| ### Fixed | ||||
| * Fix document generation and workflow generation do not work on accompanying period work documents    | ||||
|  | ||||
| ## v4.4.1 - 2025-09-11 | ||||
| ### Fixed | ||||
| * fix translations in duplicate evaluation document modal and realign close modal button    | ||||
|  | ||||
| ## v4.4.0 - 2025-09-11 | ||||
| ### Feature | ||||
| * ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works    | ||||
| * ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation    | ||||
| * ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works    | ||||
| ### Fixed | ||||
| * Fix display of 'duplicate' and 'merge' buttons in CRUD templates    | ||||
| * Fix saving notification preferences in user's profile    | ||||
|  | ||||
| ## v4.3.0 - 2025-09-08 | ||||
| ### Feature | ||||
| * ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges    | ||||
| * Add a command to generate a list of permissions    | ||||
| * ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date    | ||||
|  | ||||
|   **Schema Change**: Add columns or tables | ||||
| ### Fixed | ||||
| * fix date formatting in calendar range display    | ||||
| * Change route URL to avoid clash with person duplicate controller method    | ||||
|  | ||||
| ## v4.2.1 - 2025-09-03 | ||||
| ### Fixed | ||||
| * Fix exports to work with DirectExportInterface    | ||||
|   | ||||
| @@ -54,7 +54,7 @@ Arborescence: | ||||
|     - person | ||||
|     - personvendee | ||||
|     - household_edit_metadata | ||||
|         - index.js | ||||
|         - index.ts | ||||
| ``` | ||||
|  | ||||
| ## Organisation des feuilles de styles | ||||
|   | ||||
| @@ -1,7 +1,12 @@ | ||||
| import { trans, setLocale, setLocaleFallbacks } from "./ux-translator"; | ||||
| import { | ||||
|   trans, | ||||
|   setLocale, | ||||
|   getLocale, | ||||
|   setLocaleFallbacks, | ||||
| } from "./ux-translator"; | ||||
|  | ||||
| setLocaleFallbacks({"en": "fr", "nl": "fr", "fr": "en"}); | ||||
| setLocale('fr'); | ||||
| setLocaleFallbacks({ en: "fr", nl: "fr", fr: "en" }); | ||||
| setLocale("fr"); | ||||
|  | ||||
| export { trans }; | ||||
| export * from '../var/translations'; | ||||
| export { trans, getLocale }; | ||||
| export * from "../var/translations"; | ||||
|   | ||||
| @@ -133,6 +133,7 @@ | ||||
|             "Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle", | ||||
|             "Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle", | ||||
|             "Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src", | ||||
|             "Chill\\TicketBundle\\": "src/Bundle/ChillTicketBundle/src", | ||||
|             "Chill\\Utils\\Rector\\": "utils/rector/src" | ||||
|         } | ||||
|     }, | ||||
|   | ||||
| @@ -35,6 +35,7 @@ return [ | ||||
|     Chill\ThirdPartyBundle\ChillThirdPartyBundle::class => ['all' => true], | ||||
|     Chill\BudgetBundle\ChillBudgetBundle::class => ['all' => true], | ||||
|     Chill\WopiBundle\ChillWopiBundle::class => ['all' => true], | ||||
|     Chill\TicketBundle\ChillTicketBundle::class => ['all' => true], | ||||
|     Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], | ||||
|     Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true], | ||||
| ]; | ||||
|   | ||||
| @@ -1,2 +0,0 @@ | ||||
| chill_aside_activity: | ||||
|     show_concerned_persons_count: hidden | ||||
| @@ -1,5 +1,5 @@ | ||||
| chill_doc_store: | ||||
|     use_driver: openstack | ||||
|     use_driver: local_storage | ||||
|     local_storage: | ||||
|         storage_path: '%kernel.project_dir%/var/storage' | ||||
|     openstack: | ||||
|   | ||||
							
								
								
									
										5
									
								
								config/packages/chill_ticket.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								config/packages/chill_ticket.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| chill_ticket: | ||||
|     ticket: | ||||
|         person_per_ticket:    one # One of "one"; "many" | ||||
|         response_time_exceeded_delay: PT12H | ||||
|  | ||||
| @@ -14,6 +14,7 @@ doctrine_migrations: | ||||
|         'Chill\Migrations\Calendar': '@ChillCalendarBundle/migrations' | ||||
|         'Chill\Migrations\Budget': '@ChillBudgetBundle/migrations' | ||||
|         'Chill\Migrations\Report': '@ChillReportBundle/migrations' | ||||
|         'Chill\Migrations\Ticket': '@ChillTicketBundle/migrations' | ||||
|     all_or_nothing: | ||||
|         true | ||||
|  | ||||
|   | ||||
| @@ -66,6 +66,7 @@ framework: | ||||
|             'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority | ||||
|             'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async | ||||
|             'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async | ||||
|             'Chill\TicketBundle\Messenger\PostTicketUpdateMessage': async | ||||
|             # end of routes added by chill-bundles recipes | ||||
|             # Route your messages to the transports | ||||
|             # 'App\Message\YourMessage': async | ||||
|   | ||||
							
								
								
									
										2
									
								
								config/routes/chill_ticket.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								config/routes/chill_ticket.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| chill_ticket_bundle: | ||||
|     resource: '@ChillTicketBundle/config/routes.yaml' | ||||
| @@ -11,24 +11,94 @@ | ||||
| Create a new bundle | ||||
| ******************* | ||||
|  | ||||
| Create your own bundle is not a trivial task. | ||||
|  | ||||
| The easiest way to achieve this is seems to be :  | ||||
|  | ||||
| 1. Prepare a fresh installation of the chill project, in a new directory | ||||
| 2. Create a new bundle in this project, in the src directory | ||||
| 3. Initialize a git repository **at the root bundle**, and create your initial commit. | ||||
| 4. Register the bundle with composer/packagist. If you do not plan to distribute your bundle with packagist, you may use a custom repository for achieve this [#f1]_ | ||||
| 5. Move to a development installation, made as described in the :ref:`installation-for-development` section, and add your new repository to the composer.json file | ||||
| 6. Work as :ref:`usual <editing-code-and-commiting>` | ||||
|  | ||||
| .. warning:: | ||||
|  | ||||
|     This part of the doc is not yet tested | ||||
|  | ||||
| TODO | ||||
| Create a new directory with Bundle class | ||||
| ---------------------------------------- | ||||
|  | ||||
| .. code-block:: bash | ||||
|  | ||||
|    mkdir -p src/Bundle/ChillSomeBundle/src/config | ||||
|    mkdir -p src/Bundle/ChillSomeBundle/src/Controller | ||||
|  | ||||
| Add a bundle file | ||||
|  | ||||
| .. code-block:: php | ||||
|  | ||||
|     <?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\SomeBundle; | ||||
|  | ||||
|     use Symfony\Component\HttpKernel\Bundle\Bundle; | ||||
|  | ||||
|     class ChillSomeBundle extends Bundle {} | ||||
|  | ||||
| And a route file: | ||||
|  | ||||
| .. code-block:: yaml | ||||
|  | ||||
|    chill_ticket_controller: | ||||
|        resource: '@ChillTicketBundle/Controller/' | ||||
|        type: annotation | ||||
|  | ||||
| Register the new psr-4 namespace | ||||
| -------------------------------- | ||||
|  | ||||
| In composer.json, add the new psr4 namespace | ||||
|  | ||||
| .. code-block:: diff | ||||
|  | ||||
|     { | ||||
|     "autoload": { | ||||
|         "psr-4": { | ||||
|     +        "Chill\\SomeBundle\\": "src/Bundle/ChillSomeBundle/src", | ||||
|         } | ||||
|     } | ||||
|     } | ||||
|  | ||||
|  | ||||
| .. rubric:: Footnotes | ||||
| Register the bundle | ||||
| ------------------- | ||||
|  | ||||
| Register in the file :code:`config/bundles.php`: | ||||
|  | ||||
| .. code-block:: php | ||||
|  | ||||
|     Vendor\Bundle\YourBundle\YourBundle::class => ['all' => true], | ||||
|  | ||||
| And import routes in :code:`config/routes/chill_some_bundle.yaml`: | ||||
|  | ||||
| .. code-block:: yaml | ||||
|  | ||||
|    chill_ticket_bundle: | ||||
|        resource: '@ChillSomeBundle/config/routes.yaml' | ||||
|  | ||||
| Add the doctrine_migration namespace | ||||
| ------------------------------------ | ||||
|  | ||||
| Add the namespace to :code:`config/packages/doctrine_migrations_chill.yaml` | ||||
|  | ||||
| .. code-block:: diff | ||||
|  | ||||
|    doctrine_migrations: | ||||
|        migrations_paths: | ||||
|    +        'Chill\Some\Ticket': '@ChillSomeBundle/migrations' | ||||
|  | ||||
| Dump autoloading | ||||
| ---------------- | ||||
|  | ||||
| .. code-block:: bash | ||||
|  | ||||
|    symfony composer dump-autoload | ||||
|  | ||||
| .. [#f1] Be aware that we use the Affero GPL Licence, which ensure that all users must have access to derivative works done with this software. | ||||
|   | ||||
| @@ -41,6 +41,7 @@ | ||||
|     "typescript": "^5.6.3", | ||||
|     "typescript-eslint": "^8.13.0", | ||||
|     "vue-loader": "^17.0.0", | ||||
|     "vue-tsc": "^3.1.1", | ||||
|     "webpack": "^5.75.0", | ||||
|     "webpack-cli": "^5.0.1" | ||||
|   }, | ||||
| @@ -55,7 +56,6 @@ | ||||
|     "@tsconfig/node20": "^20.1.4", | ||||
|     "@types/dompurify": "^3.0.5", | ||||
|     "@types/leaflet": "^1.9.3", | ||||
|     "@vueuse/core": "^13.9.0", | ||||
|     "bootstrap-icons": "^1.11.3", | ||||
|     "dropzone": "^5.7.6", | ||||
|     "es6-promise": "^4.2.8", | ||||
| @@ -80,12 +80,12 @@ | ||||
|     "dev": "encore dev", | ||||
|     "watch": "encore dev --watch", | ||||
|     "build": "encore production --progress", | ||||
|     "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-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 src/Bundle/ChillTicketBundle/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}\"" | ||||
|     "eslint": "eslint-baseline --fix \"src/**/*.{js,ts,vue}\"" | ||||
|   }, | ||||
|   "private": true | ||||
| } | ||||
|   | ||||
| @@ -58,6 +58,10 @@ | ||||
|       <!-- temporarily removed, the time to find a fix --> | ||||
|       <exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude> | ||||
|     </testsuite> | ||||
|  | ||||
|     <testsuite name="TicketBundle"> | ||||
|       <directory suffix="Test.php">src/Bundle/ChillTicketBundle/tests/</directory> | ||||
|     </testsuite> | ||||
|     <!-- | ||||
|         <testsuite name="ReportBundle"> | ||||
|             <directory suffix="Test.php">src/Bundle/ChillReportBundle/Tests/</directory> | ||||
|   | ||||
							
								
								
									
										8
									
								
								resources/ticket_motives_import/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								resources/ticket_motives_import/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| In this directory, you find an example of file for the command `chill:main:ticket_motives_import`. | ||||
|  | ||||
| This file contains a list of ticket motives to import into the system. Each entry is a dictionary with two keys: `code` and `label`. The `code` key contains the unique code for the ticket motive, and the `label` key contains the human-readable label for the ticket motive. | ||||
|  | ||||
| The `stored_objects` key contains the documents that will be associated with the tickets. They must be found in the same directory. | ||||
|  | ||||
| The command `chill:main:ticket_motives_import` uses this file to import the specified ticket motives into the system. | ||||
|  | ||||
							
								
								
									
										136
									
								
								resources/ticket_motives_import/motives.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								resources/ticket_motives_import/motives.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| - label: | ||||
|     fr: Appel famille pour annonce de décès | ||||
|   urgent: false | ||||
|   supplementary_informations: | ||||
|   - label: | ||||
|       fr: Date du décès | ||||
|   - label: | ||||
|       fr: lieu du décès (domicile ou hôpital) | ||||
|   - label: | ||||
|       fr: nom de l’hôpital | ||||
|   - label: | ||||
|       fr: service concerné | ||||
|   stored_objects: | ||||
|   - label: | ||||
|       fr: ☀️ De 07h à 21h | ||||
|     filename: 2_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🌙 De 21h à 07h du matin | ||||
|     filename: 3_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🗓️ Dimanches et jours fériés | ||||
|     filename: 4_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
| - label: | ||||
|     fr: 'Appel famille pour annonce absence : hospitalisation ou consultation' | ||||
|   urgent: false | ||||
|   supplementary_informations: | ||||
|   - label: | ||||
|       fr: Quel hôpital | ||||
|   - label: | ||||
|       fr: quel service | ||||
|   - label: | ||||
|       fr: pour quelles raisons | ||||
|   - label: | ||||
|       fr: 'consultation : date et heure' | ||||
|   - label: | ||||
|       fr: hospitalisation complète ou HDJ | ||||
|   stored_objects: | ||||
|   - label: | ||||
|       fr: ☀️ De 07h à 21h | ||||
|     filename: 5_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🌙 De 21h à 07h du matin | ||||
|     filename: 6_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🗓️ Dimanches et jours fériés | ||||
|     filename: 7_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
| - label: | ||||
|     fr: 'Appel famille pour annonce absence : interruption de prise en charge' | ||||
|   urgent: false | ||||
|   supplementary_informations: | ||||
|   - label: | ||||
|       fr: Pour quelles raisons ? Date | ||||
|   - label: | ||||
|       fr: durée | ||||
|   - label: | ||||
|       fr: accord médical ? | ||||
|   stored_objects: | ||||
|   - label: | ||||
|       fr: ☀️ De 07h à 21h | ||||
|     filename: 8_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🌙 De 21h à 07h du matin | ||||
|     filename: 9_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🗓️ Dimanches et jours fériés | ||||
|     filename: 10_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
| - label: | ||||
|     fr: 'Appel famille pour annonce absence : changement d’adresse' | ||||
|   urgent: false | ||||
|   supplementary_informations: | ||||
|   - label: | ||||
|       fr: Où | ||||
|   - label: | ||||
|       fr: Pourquoi ? Pour combien de temps ? Besoin d’un relais des soins ? Nouvelle adresse ? | ||||
|   stored_objects: | ||||
|   - label: | ||||
|       fr: ☀️ De 07h à 21h | ||||
|     filename: 11_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🌙 De 21h à 07h du matin | ||||
|     filename: 12_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🗓️ Dimanches et jours fériés | ||||
|     filename: 13_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
| - label: | ||||
|     fr: Appel famille pour altération de l’état général du patient | ||||
|   urgent: true | ||||
|   supplementary_informations: | ||||
|   - label: | ||||
|       fr: Recherche des symptômes | ||||
|   - label: | ||||
|       fr: Attentes par rapport à la demande | ||||
|   stored_objects: | ||||
|   - label: | ||||
|       fr: ☀️ De 07h à 21h | ||||
|     filename: 14_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🌙 De 21h à 07h du matin | ||||
|     filename: 15_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🗓️ Dimanches et jours fériés | ||||
|     filename: 16_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
| - label: | ||||
|     fr: Appel famille pour prise en charge de la douleur | ||||
|   urgent: true | ||||
|   supplementary_informations: | ||||
|   - label: | ||||
|       fr: Localisation douleur | ||||
|   - label: | ||||
|       fr: Horaire dernier passage | ||||
|   - label: | ||||
|       fr: Traitements en cours | ||||
|   stored_objects: | ||||
|   - label: | ||||
|       fr: ☀️ De 07h à 21h | ||||
|     filename: 17_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🌙 De 21h à 07h du matin | ||||
|     filename: 18_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🗓️ Dimanches et jours fériés | ||||
|     filename: 19_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
| - label: | ||||
|     fr: Appel famille pour information sur la date de prise en charge | ||||
|   urgent: false | ||||
|   supplementary_informations: [] | ||||
|   stored_objects: | ||||
|   - label: | ||||
|       fr: ☀️ De 07h à 21h | ||||
|     filename: 20_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🌙 De 21h à 07h du matin | ||||
|     filename: 21_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
|   - label: | ||||
|       fr: 🗓️ Dimanches et jours fériés | ||||
|     filename: 22_doc_20250402_Pelotons flux externes consolidés.pdf | ||||
							
								
								
									
										6
									
								
								resources/translation_override/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								resources/translation_override/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| In this directory, you find an example of file for the command `chill:main:override_translation`. | ||||
|  | ||||
| This file contains a list of translations to override in the translation catalogue. Each entry is a dictionary with two keys: `from` and `to`. The `from` key contains the original translation string, and the `to` key contains the replacement string. | ||||
|  | ||||
| The command `chill:main:override_translation` uses this file to generate a new translation catalogue with the specified overrides applied. | ||||
|  | ||||
							
								
								
									
										8
									
								
								resources/translation_override/overrides.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								resources/translation_override/overrides.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| - {from: "de l'usager", to: "du patient"} | ||||
| - {from: "l'usager", to: "le patient"} | ||||
| - {from: "L'usager", to: "Le patient"} | ||||
| - {from: "d'usagers", to: "de patients"} | ||||
| - {from: "usagers", to: "patients"} | ||||
| - {from: "Usagers", to: "Patients"} | ||||
| - {from: "usager", to: "patient"} | ||||
| - {from: "Usager", to: "Patient"} | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
|     <concerned-groups v-if="hasPerson" /> | ||||
|     <social-issues-acc v-if="hasSocialIssues" /> | ||||
|     <location v-if="hasLocation" /> | ||||
|   <concerned-groups v-if="hasPerson" /> | ||||
|   <social-issues-acc v-if="hasSocialIssues" /> | ||||
|   <location v-if="hasLocation" /> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| @@ -10,12 +10,12 @@ import SocialIssuesAcc from "./components/SocialIssuesAcc.vue"; | ||||
| import Location from "./components/Location.vue"; | ||||
|  | ||||
| export default { | ||||
|     name: "App", | ||||
|     props: ["hasSocialIssues", "hasLocation", "hasPerson"], | ||||
|     components: { | ||||
|         ConcernedGroups, | ||||
|         SocialIssuesAcc, | ||||
|         Location, | ||||
|     }, | ||||
|   name: "App", | ||||
|   props: ["hasSocialIssues", "hasLocation", "hasPerson"], | ||||
|   components: { | ||||
|     ConcernedGroups, | ||||
|     SocialIssuesAcc, | ||||
|     Location, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -1,46 +1,43 @@ | ||||
| <template> | ||||
|     <teleport to="#add-persons" v-if="isComponentVisible"> | ||||
|         <div class="flex-bloc concerned-groups" :class="getContext"> | ||||
|             <persons-bloc | ||||
|                 v-for="bloc in contextPersonsBlocs" | ||||
|                 :key="bloc.key" | ||||
|                 :bloc="bloc" | ||||
|                 :bloc-width="getBlocWidth" | ||||
|                 :set-persons-in-bloc="setPersonsInBloc" | ||||
|             /> | ||||
|         </div> | ||||
|         <div | ||||
|             v-if=" | ||||
|                 getContext === 'accompanyingCourse' && | ||||
|                 suggestedEntities.length > 0 | ||||
|             " | ||||
|   <teleport to="#add-persons" v-if="isComponentVisible"> | ||||
|     <div class="flex-bloc concerned-groups" :class="getContext"> | ||||
|       <persons-bloc | ||||
|         v-for="bloc in contextPersonsBlocs" | ||||
|         :key="bloc.key" | ||||
|         :bloc="bloc" | ||||
|         :bloc-width="getBlocWidth" | ||||
|         :set-persons-in-bloc="setPersonsInBloc" | ||||
|       /> | ||||
|     </div> | ||||
|     <div | ||||
|       v-if="getContext === 'accompanyingCourse' && suggestedEntities.length > 0" | ||||
|     > | ||||
|       <ul class="list-suggest add-items inline"> | ||||
|         <li | ||||
|           v-for="(p, i) in suggestedEntities" | ||||
|           @click="addSuggestedEntity(p)" | ||||
|           :key="`suggestedEntities-${i}`" | ||||
|         > | ||||
|             <ul class="list-suggest add-items inline"> | ||||
|                 <li | ||||
|                     v-for="(p, i) in suggestedEntities" | ||||
|                     @click="addSuggestedEntity(p)" | ||||
|                     :key="`suggestedEntities-${i}`" | ||||
|                 > | ||||
|                     <person-text v-if="p.type === 'person'" :person="p" /> | ||||
|                     <span v-else>{{ p.text }}</span> | ||||
|                 </li> | ||||
|             </ul> | ||||
|         </div> | ||||
|           <person-text v-if="p.type === 'person'" :person="p" /> | ||||
|           <span v-else>{{ p.text }}</span> | ||||
|         </li> | ||||
|       </ul> | ||||
|     </div> | ||||
|  | ||||
|         <ul class="record_actions"> | ||||
|             <li class="add-persons"> | ||||
|                 <add-persons | ||||
|                     :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> | ||||
|     <ul class="record_actions"> | ||||
|       <li class="add-persons"> | ||||
|         <add-persons | ||||
|           :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> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| @@ -49,208 +46,208 @@ 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, | ||||
|   ACTIVITY_BLOC_PERSONS, | ||||
|   ACTIVITY_BLOC_PERSONS_ASSOCIATED, | ||||
|   ACTIVITY_BLOC_THIRDPARTY, | ||||
|   ACTIVITY_BLOC_USERS, | ||||
|   ACTIVITY_ADD_PERSONS, | ||||
|   trans, | ||||
| } from "translator"; | ||||
|  | ||||
| export default { | ||||
|     name: "ConcernedGroups", | ||||
|     components: { | ||||
|         AddPersons, | ||||
|         PersonsBloc, | ||||
|         PersonText, | ||||
|   name: "ConcernedGroups", | ||||
|   components: { | ||||
|     AddPersons, | ||||
|     PersonsBloc, | ||||
|     PersonText, | ||||
|   }, | ||||
|   setup() { | ||||
|     return { | ||||
|       trans, | ||||
|       ACTIVITY_ADD_PERSONS, | ||||
|     }; | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       personsBlocs: [ | ||||
|         { | ||||
|           key: "persons", | ||||
|           title: trans(ACTIVITY_BLOC_PERSONS), | ||||
|           persons: [], | ||||
|           included: false, | ||||
|         }, | ||||
|         { | ||||
|           key: "personsAssociated", | ||||
|           title: trans(ACTIVITY_BLOC_PERSONS_ASSOCIATED), | ||||
|           persons: [], | ||||
|           included: window.activity | ||||
|             ? window.activity.activityType.personsVisible !== 0 | ||||
|             : true, | ||||
|         }, | ||||
|         { | ||||
|           key: "personsNotAssociated", | ||||
|           title: "activity.bloc_persons_not_associated", | ||||
|           persons: [], | ||||
|           included: window.activity | ||||
|             ? window.activity.activityType.personsVisible !== 0 | ||||
|             : true, | ||||
|         }, | ||||
|         { | ||||
|           key: "thirdparty", | ||||
|           title: trans(ACTIVITY_BLOC_THIRDPARTY), | ||||
|           persons: [], | ||||
|           included: window.activity | ||||
|             ? window.activity.activityType.thirdPartiesVisible !== 0 | ||||
|             : true, | ||||
|         }, | ||||
|         { | ||||
|           key: "users", | ||||
|           title: trans(ACTIVITY_BLOC_USERS), | ||||
|           persons: [], | ||||
|           included: window.activity | ||||
|             ? window.activity.activityType.usersVisible !== 0 | ||||
|             : true, | ||||
|         }, | ||||
|       ], | ||||
|       addPersons: { | ||||
|         key: "activity", | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     isComponentVisible() { | ||||
|       return window.activity | ||||
|         ? window.activity.activityType.personsVisible !== 0 || | ||||
|             window.activity.activityType.thirdPartiesVisible !== 0 || | ||||
|             window.activity.activityType.usersVisible !== 0 | ||||
|         : true; | ||||
|     }, | ||||
|     setup() { | ||||
|         return { | ||||
|             trans, | ||||
|             ACTIVITY_ADD_PERSONS, | ||||
|         }; | ||||
|     ...mapState({ | ||||
|       persons: (state) => state.activity.persons, | ||||
|       thirdParties: (state) => state.activity.thirdParties, | ||||
|       users: (state) => state.activity.users, | ||||
|       accompanyingCourse: (state) => state.activity.accompanyingPeriod, | ||||
|     }), | ||||
|     ...mapGetters(["suggestedEntities"]), | ||||
|     getContext() { | ||||
|       return this.accompanyingCourse ? "accompanyingCourse" : "person"; | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             personsBlocs: [ | ||||
|                 { | ||||
|                     key: "persons", | ||||
|                     title: trans(ACTIVITY_BLOC_PERSONS), | ||||
|                     persons: [], | ||||
|                     included: false, | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "personsAssociated", | ||||
|                     title: trans(ACTIVITY_BLOC_PERSONS_ASSOCIATED), | ||||
|                     persons: [], | ||||
|                     included: window.activity | ||||
|                         ? window.activity.activityType.personsVisible !== 0 | ||||
|                         : true, | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "personsNotAssociated", | ||||
|                     title: "activity.bloc_persons_not_associated", | ||||
|                     persons: [], | ||||
|                     included: window.activity | ||||
|                         ? window.activity.activityType.personsVisible !== 0 | ||||
|                         : true, | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "thirdparty", | ||||
|                     title: trans(ACTIVITY_BLOC_THIRDPARTY), | ||||
|                     persons: [], | ||||
|                     included: window.activity | ||||
|                         ? window.activity.activityType.thirdPartiesVisible !== 0 | ||||
|                         : true, | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "users", | ||||
|                     title: trans(ACTIVITY_BLOC_USERS), | ||||
|                     persons: [], | ||||
|                     included: window.activity | ||||
|                         ? window.activity.activityType.usersVisible !== 0 | ||||
|                         : true, | ||||
|                 }, | ||||
|             ], | ||||
|             addPersons: { | ||||
|                 key: "activity", | ||||
|             }, | ||||
|         }; | ||||
|     contextPersonsBlocs() { | ||||
|       return this.personsBlocs.filter((bloc) => bloc.included !== false); | ||||
|     }, | ||||
|     computed: { | ||||
|         isComponentVisible() { | ||||
|             return window.activity | ||||
|                 ? window.activity.activityType.personsVisible !== 0 || | ||||
|                       window.activity.activityType.thirdPartiesVisible !== 0 || | ||||
|                       window.activity.activityType.usersVisible !== 0 | ||||
|                 : true; | ||||
|         }, | ||||
|         ...mapState({ | ||||
|             persons: (state) => state.activity.persons, | ||||
|             thirdParties: (state) => state.activity.thirdParties, | ||||
|             users: (state) => state.activity.users, | ||||
|             accompanyingCourse: (state) => state.activity.accompanyingPeriod, | ||||
|         }), | ||||
|         ...mapGetters(["suggestedEntities"]), | ||||
|         getContext() { | ||||
|             return this.accompanyingCourse ? "accompanyingCourse" : "person"; | ||||
|         }, | ||||
|         contextPersonsBlocs() { | ||||
|             return this.personsBlocs.filter((bloc) => bloc.included !== false); | ||||
|         }, | ||||
|         addPersonsOptions() { | ||||
|             let optionsType = []; | ||||
|             if (window.activity) { | ||||
|                 if (window.activity.activityType.personsVisible !== 0) { | ||||
|                     optionsType.push("person"); | ||||
|                 } | ||||
|                 if (window.activity.activityType.thirdPartiesVisible !== 0) { | ||||
|                     optionsType.push("thirdparty"); | ||||
|                 } | ||||
|                 if (window.activity.activityType.usersVisible !== 0) { | ||||
|                     optionsType.push("user"); | ||||
|                 } | ||||
|             } else { | ||||
|                 optionsType = ["person", "thirdparty", "user"]; | ||||
|             } | ||||
|             return { | ||||
|                 type: optionsType, | ||||
|                 priority: null, | ||||
|                 uniq: false, | ||||
|                 button: { | ||||
|                     size: "btn-sm", | ||||
|                 }, | ||||
|             }; | ||||
|         }, | ||||
|         getBlocWidth() { | ||||
|             return Math.round(100 / this.contextPersonsBlocs.length) + "%"; | ||||
|     addPersonsOptions() { | ||||
|       let optionsType = []; | ||||
|       if (window.activity) { | ||||
|         if (window.activity.activityType.personsVisible !== 0) { | ||||
|           optionsType.push("person"); | ||||
|         } | ||||
|         if (window.activity.activityType.thirdPartiesVisible !== 0) { | ||||
|           optionsType.push("thirdparty"); | ||||
|         } | ||||
|         if (window.activity.activityType.usersVisible !== 0) { | ||||
|           optionsType.push("user"); | ||||
|         } | ||||
|       } else { | ||||
|         optionsType = ["person", "thirdparty", "user"]; | ||||
|       } | ||||
|       return { | ||||
|         type: optionsType, | ||||
|         priority: null, | ||||
|         uniq: false, | ||||
|         button: { | ||||
|           size: "btn-sm", | ||||
|         }, | ||||
|       }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.setPersonsInBloc(); | ||||
|     getBlocWidth() { | ||||
|       return Math.round(100 / this.contextPersonsBlocs.length) + "%"; | ||||
|     }, | ||||
|     methods: { | ||||
|         setPersonsInBloc() { | ||||
|             let groups; | ||||
|             if (this.accompanyingCourse) { | ||||
|                 groups = this.splitPersonsInGroups(); | ||||
|             } | ||||
|             this.personsBlocs.forEach((bloc) => { | ||||
|                 if (this.accompanyingCourse) { | ||||
|                     switch (bloc.key) { | ||||
|                         case "personsAssociated": | ||||
|                             bloc.persons = groups.personsAssociated; | ||||
|                             bloc.included = true; | ||||
|                             break; | ||||
|                         case "personsNotAssociated": | ||||
|                             bloc.persons = groups.personsNotAssociated; | ||||
|                             bloc.included = true; | ||||
|                             break; | ||||
|                     } | ||||
|                 } else { | ||||
|                     switch (bloc.key) { | ||||
|                         case "persons": | ||||
|                             bloc.persons = this.persons; | ||||
|                             bloc.included = true; | ||||
|                             break; | ||||
|                     } | ||||
|                 } | ||||
|                 switch (bloc.key) { | ||||
|                     case "thirdparty": | ||||
|                         bloc.persons = this.thirdParties; | ||||
|                         break; | ||||
|                     case "users": | ||||
|                         bloc.persons = this.users; | ||||
|                         break; | ||||
|                 } | ||||
|             }, groups); | ||||
|         }, | ||||
|         splitPersonsInGroups() { | ||||
|             let personsAssociated = []; | ||||
|             let personsNotAssociated = this.persons; | ||||
|             let participations = this.getCourseParticipations(); | ||||
|             this.persons.forEach((person) => { | ||||
|                 participations.forEach((participation) => { | ||||
|                     if (person.id === participation.id) { | ||||
|                         //console.log(person.id); | ||||
|                         personsAssociated.push(person); | ||||
|                         personsNotAssociated = personsNotAssociated.filter( | ||||
|                             (p) => p !== person, | ||||
|                         ); | ||||
|                     } | ||||
|                 }); | ||||
|             }); | ||||
|             return { | ||||
|                 personsAssociated: personsAssociated, | ||||
|                 personsNotAssociated: personsNotAssociated, | ||||
|             }; | ||||
|         }, | ||||
|         getCourseParticipations() { | ||||
|             let participations = []; | ||||
|             this.accompanyingCourse.participations.forEach((participation) => { | ||||
|                 if (!participation.endDate) { | ||||
|                     participations.push(participation.person); | ||||
|                 } | ||||
|             }); | ||||
|             return participations; | ||||
|         }, | ||||
|         addNewPersons({ selected, modal }) { | ||||
|             console.log("@@@ CLICK button addNewPersons", selected); | ||||
|             selected.forEach((item) => { | ||||
|                 this.$store.dispatch("addPersonsInvolved", item); | ||||
|             }, this); | ||||
|             this.$refs.addPersons.resetSearch(); // to cast child method | ||||
|             modal.showModal = false; | ||||
|             this.setPersonsInBloc(); | ||||
|         }, | ||||
|         addSuggestedEntity(person) { | ||||
|             this.$store.dispatch("addPersonsInvolved", { | ||||
|                 result: person, | ||||
|                 type: "person", | ||||
|             }); | ||||
|             this.setPersonsInBloc(); | ||||
|         }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.setPersonsInBloc(); | ||||
|   }, | ||||
|   methods: { | ||||
|     setPersonsInBloc() { | ||||
|       let groups; | ||||
|       if (this.accompanyingCourse) { | ||||
|         groups = this.splitPersonsInGroups(); | ||||
|       } | ||||
|       this.personsBlocs.forEach((bloc) => { | ||||
|         if (this.accompanyingCourse) { | ||||
|           switch (bloc.key) { | ||||
|             case "personsAssociated": | ||||
|               bloc.persons = groups.personsAssociated; | ||||
|               bloc.included = true; | ||||
|               break; | ||||
|             case "personsNotAssociated": | ||||
|               bloc.persons = groups.personsNotAssociated; | ||||
|               bloc.included = true; | ||||
|               break; | ||||
|           } | ||||
|         } else { | ||||
|           switch (bloc.key) { | ||||
|             case "persons": | ||||
|               bloc.persons = this.persons; | ||||
|               bloc.included = true; | ||||
|               break; | ||||
|           } | ||||
|         } | ||||
|         switch (bloc.key) { | ||||
|           case "thirdparty": | ||||
|             bloc.persons = this.thirdParties; | ||||
|             break; | ||||
|           case "users": | ||||
|             bloc.persons = this.users; | ||||
|             break; | ||||
|         } | ||||
|       }, groups); | ||||
|     }, | ||||
|     splitPersonsInGroups() { | ||||
|       let personsAssociated = []; | ||||
|       let personsNotAssociated = this.persons; | ||||
|       let participations = this.getCourseParticipations(); | ||||
|       this.persons.forEach((person) => { | ||||
|         participations.forEach((participation) => { | ||||
|           if (person.id === participation.id) { | ||||
|             //console.log(person.id); | ||||
|             personsAssociated.push(person); | ||||
|             personsNotAssociated = personsNotAssociated.filter( | ||||
|               (p) => p !== person, | ||||
|             ); | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|       return { | ||||
|         personsAssociated: personsAssociated, | ||||
|         personsNotAssociated: personsNotAssociated, | ||||
|       }; | ||||
|     }, | ||||
|     getCourseParticipations() { | ||||
|       let participations = []; | ||||
|       this.accompanyingCourse.participations.forEach((participation) => { | ||||
|         if (!participation.endDate) { | ||||
|           participations.push(participation.person); | ||||
|         } | ||||
|       }); | ||||
|       return participations; | ||||
|     }, | ||||
|     addNewPersons({ selected, modal }) { | ||||
|       console.log("@@@ CLICK button addNewPersons", selected); | ||||
|       selected.forEach((item) => { | ||||
|         this.$store.dispatch("addPersonsInvolved", item); | ||||
|       }, this); | ||||
|       this.$refs.addPersons.resetSearch(); // to cast child method | ||||
|       modal.showModal = false; | ||||
|       this.setPersonsInBloc(); | ||||
|     }, | ||||
|     addSuggestedEntity(person) { | ||||
|       this.$store.dispatch("addPersonsInvolved", { | ||||
|         result: person, | ||||
|         type: "person", | ||||
|       }); | ||||
|       this.setPersonsInBloc(); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -1,29 +1,29 @@ | ||||
| <template> | ||||
|     <li> | ||||
|         <span :title="person.text" @click.prevent="$emit('remove', person)"> | ||||
|             <span class="chill_denomination"> | ||||
|                 <person-text :person="person" :is-cut="true" /> | ||||
|             </span> | ||||
|         </span> | ||||
|     </li> | ||||
|   <li> | ||||
|     <span :title="person.text" @click.prevent="$emit('remove', person)"> | ||||
|       <span class="chill_denomination"> | ||||
|         <person-text :person="person" :is-cut="true" /> | ||||
|       </span> | ||||
|     </span> | ||||
|   </li> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue"; | ||||
|  | ||||
| export default { | ||||
|     name: "PersonBadge", | ||||
|     props: ["person"], | ||||
|     components: { | ||||
|         PersonText, | ||||
|     }, | ||||
|     // computed: { | ||||
|     //    textCutted() { | ||||
|     //       let more = (this.person.text.length > 15) ?'…' : ''; | ||||
|     //       return this.person.text.slice(0,15) + more; | ||||
|     //    } | ||||
|     // }, | ||||
|     emits: ["remove"], | ||||
|   name: "PersonBadge", | ||||
|   props: ["person"], | ||||
|   components: { | ||||
|     PersonText, | ||||
|   }, | ||||
|   // computed: { | ||||
|   //    textCutted() { | ||||
|   //       let more = (this.person.text.length > 15) ?'…' : ''; | ||||
|   //       return this.person.text.slice(0,15) + more; | ||||
|   //    } | ||||
|   // }, | ||||
|   emits: ["remove"], | ||||
| }; | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -1,38 +1,38 @@ | ||||
| <template> | ||||
|     <div class="item-bloc" :style="{ 'flex-basis': blocWidth }"> | ||||
|         <div class="item-row"> | ||||
|             <div class="item-col"> | ||||
|                 <h4>{{ $t(bloc.title) }}</h4> | ||||
|             </div> | ||||
|             <div class="item-col"> | ||||
|                 <ul class="list-suggest remove-items"> | ||||
|                     <person-badge | ||||
|                         v-for="person in bloc.persons" | ||||
|                         :key="person.id" | ||||
|                         :person="person" | ||||
|                         @remove="removePerson" | ||||
|                     /> | ||||
|                 </ul> | ||||
|             </div> | ||||
|         </div> | ||||
|   <div class="item-bloc" :style="{ 'flex-basis': blocWidth }"> | ||||
|     <div class="item-row"> | ||||
|       <div class="item-col"> | ||||
|         <h4>{{ $t(bloc.title) }}</h4> | ||||
|       </div> | ||||
|       <div class="item-col"> | ||||
|         <ul class="list-suggest remove-items"> | ||||
|           <person-badge | ||||
|             v-for="person in bloc.persons" | ||||
|             :key="person.id" | ||||
|             :person="person" | ||||
|             @remove="removePerson" | ||||
|           /> | ||||
|         </ul> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import PersonBadge from "./PersonBadge.vue"; | ||||
| export default { | ||||
|     name: "PersonsBloc", | ||||
|     components: { | ||||
|         PersonBadge, | ||||
|     }, | ||||
|     props: ["bloc", "setPersonsInBloc", "blocWidth"], | ||||
|     methods: { | ||||
|         removePerson(item) { | ||||
|             console.log("@@ CLICK remove person: item", item); | ||||
|             this.$store.dispatch("removePersonInvolved", item); | ||||
|             this.setPersonsInBloc(); | ||||
|         }, | ||||
|   name: "PersonsBloc", | ||||
|   components: { | ||||
|     PersonBadge, | ||||
|   }, | ||||
|   props: ["bloc", "setPersonsInBloc", "blocWidth"], | ||||
|   methods: { | ||||
|     removePerson(item) { | ||||
|       console.log("@@ CLICK remove person: item", item); | ||||
|       this.$store.dispatch("removePersonInvolved", item); | ||||
|       this.setPersonsInBloc(); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -1,32 +1,32 @@ | ||||
| <template> | ||||
|     <teleport to="#location"> | ||||
|         <div class="mb-3 row"> | ||||
|             <label :class="locationClassList"> | ||||
|                 {{ trans(ACTIVITY_LOCATION) }} | ||||
|             </label> | ||||
|             <div class="col-sm-8"> | ||||
|                 <VueMultiselect | ||||
|                     name="selectLocation" | ||||
|                     id="selectLocation" | ||||
|                     label="name" | ||||
|                     track-by="id" | ||||
|                     open-direction="top" | ||||
|                     :multiple="false" | ||||
|                     :searchable="true" | ||||
|                     :placeholder="trans(ACTIVITY_CHOOSE_LOCATION)" | ||||
|                     :custom-label="customLabel" | ||||
|                     :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 v-bind:available-locations="availableLocations" /> | ||||
|             </div> | ||||
|         </div> | ||||
|     </teleport> | ||||
|   <teleport to="#location"> | ||||
|     <div class="mb-3 row"> | ||||
|       <label :class="locationClassList"> | ||||
|         {{ trans(ACTIVITY_LOCATION) }} | ||||
|       </label> | ||||
|       <div class="col-sm-8"> | ||||
|         <VueMultiselect | ||||
|           name="selectLocation" | ||||
|           id="selectLocation" | ||||
|           label="name" | ||||
|           track-by="id" | ||||
|           open-direction="top" | ||||
|           :multiple="false" | ||||
|           :searchable="true" | ||||
|           :placeholder="trans(ACTIVITY_CHOOSE_LOCATION)" | ||||
|           :custom-label="customLabel" | ||||
|           :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 v-bind:available-locations="availableLocations" /> | ||||
|       </div> | ||||
|     </div> | ||||
|   </teleport> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| @@ -35,60 +35,60 @@ import VueMultiselect from "vue-multiselect"; | ||||
| import NewLocation from "./Location/NewLocation.vue"; | ||||
| import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; | ||||
| import { | ||||
|     trans, | ||||
|     ACTIVITY_LOCATION, | ||||
|     ACTIVITY_CHOOSE_LOCATION, | ||||
|     MULTISELECT_SELECT_LABEL, | ||||
|     MULTISELECT_DESELECT_LABEL, | ||||
|     MULTISELECT_SELECTED_LABEL, | ||||
|   trans, | ||||
|   ACTIVITY_LOCATION, | ||||
|   ACTIVITY_CHOOSE_LOCATION, | ||||
|   MULTISELECT_SELECT_LABEL, | ||||
|   MULTISELECT_DESELECT_LABEL, | ||||
|   MULTISELECT_SELECTED_LABEL, | ||||
| } from "translator"; | ||||
|  | ||||
| export default { | ||||
|     name: "Location", | ||||
|     components: { | ||||
|         NewLocation, | ||||
|         VueMultiselect, | ||||
|   name: "Location", | ||||
|   components: { | ||||
|     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" : ""}`, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState(["activity", "availableLocations"]), | ||||
|     ...mapGetters(["suggestedEntities"]), | ||||
|     location: { | ||||
|       get() { | ||||
|         return this.activity.location; | ||||
|       }, | ||||
|       set(value) { | ||||
|         this.$store.dispatch("updateLocation", value); | ||||
|       }, | ||||
|     }, | ||||
|     setup() { | ||||
|         return { | ||||
|             trans, | ||||
|             ACTIVITY_LOCATION, | ||||
|             ACTIVITY_CHOOSE_LOCATION, | ||||
|             MULTISELECT_SELECT_LABEL, | ||||
|             MULTISELECT_DESELECT_LABEL, | ||||
|             MULTISELECT_SELECTED_LABEL, | ||||
|         }; | ||||
|   }, | ||||
|   methods: { | ||||
|     labelAccompanyingCourseLocation(value) { | ||||
|       return `${value.address.text} (${localizeString(value.locationType.title)})`; | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             locationClassList: `col-form-label col-sm-4 ${document.querySelector("input#chill_activitybundle_activity_location").getAttribute("required") ? "required" : ""}`, | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         ...mapState(["activity", "availableLocations"]), | ||||
|         ...mapGetters(["suggestedEntities"]), | ||||
|         location: { | ||||
|             get() { | ||||
|                 return this.activity.location; | ||||
|             }, | ||||
|             set(value) { | ||||
|                 this.$store.dispatch("updateLocation", value); | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     methods: { | ||||
|         labelAccompanyingCourseLocation(value) { | ||||
|             return `${value.address.text} (${localizeString(value.locationType.title)})`; | ||||
|         }, | ||||
|         customLabel(value) { | ||||
|             return value.locationType | ||||
|                 ? value.name | ||||
|                     ? value.name === "__AccompanyingCourseLocation__" | ||||
|                         ? this.labelAccompanyingCourseLocation(value) | ||||
|                         : `${value.name} (${localizeString(value.locationType.title)})` | ||||
|                     : localizeString(value.locationType.title) | ||||
|                 : ""; | ||||
|         }, | ||||
|     customLabel(value) { | ||||
|       return value.locationType | ||||
|         ? value.name | ||||
|           ? value.name === "__AccompanyingCourseLocation__" | ||||
|             ? this.labelAccompanyingCourseLocation(value) | ||||
|             : `${value.name} (${localizeString(value.locationType.title)})` | ||||
|           : localizeString(value.locationType.title) | ||||
|         : ""; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -1,123 +1,114 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <ul class="record_actions"> | ||||
|             <li> | ||||
|                 <a class="btn btn-sm btn-create" @click="openModal"> | ||||
|                     {{ trans(ACTIVITY_CREATE_NEW_LOCATION) }} | ||||
|                 </a> | ||||
|             </li> | ||||
|         </ul> | ||||
|   <div> | ||||
|     <ul class="record_actions"> | ||||
|       <li> | ||||
|         <a class="btn btn-sm btn-create" @click="openModal"> | ||||
|           {{ trans(ACTIVITY_CREATE_NEW_LOCATION) }} | ||||
|         </a> | ||||
|       </li> | ||||
|     </ul> | ||||
|  | ||||
|         <teleport to="body"> | ||||
|             <modal | ||||
|                 v-if="modal.showModal" | ||||
|                 :modalDialogClass="modal.modalDialogClass" | ||||
|                 @close="modal.showModal = false" | ||||
|             > | ||||
|                 <template #header> | ||||
|                     <h3 class="modal-title"> | ||||
|                         {{ trans(ACTIVITY_CREATE_NEW_LOCATION) }} | ||||
|                     </h3> | ||||
|                 </template> | ||||
|                 <template #body> | ||||
|                     <form> | ||||
|                         <div class="alert alert-warning" v-if="errors.length"> | ||||
|                             <ul> | ||||
|                                 <li v-for="(e, i) in errors" :key="i"> | ||||
|                                     {{ e }} | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                         </div> | ||||
|     <teleport to="body"> | ||||
|       <modal | ||||
|         v-if="modal.showModal" | ||||
|         :modalDialogClass="modal.modalDialogClass" | ||||
|         @close="modal.showModal = false" | ||||
|       > | ||||
|         <template #header> | ||||
|           <h3 class="modal-title"> | ||||
|             {{ trans(ACTIVITY_CREATE_NEW_LOCATION) }} | ||||
|           </h3> | ||||
|         </template> | ||||
|         <template #body> | ||||
|           <form> | ||||
|             <div class="alert alert-warning" v-if="errors.length"> | ||||
|               <ul> | ||||
|                 <li v-for="(e, i) in errors" :key="i"> | ||||
|                   {{ e }} | ||||
|                 </li> | ||||
|               </ul> | ||||
|             </div> | ||||
|  | ||||
|                         <div class="form-floating mb-3"> | ||||
|                             <select | ||||
|                                 class="form-select form-select-lg" | ||||
|                                 id="type" | ||||
|                                 required | ||||
|                                 v-model="selectType" | ||||
|                             > | ||||
|                                 <option selected disabled value=""> | ||||
|                                     {{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }} | ||||
|                                 </option> | ||||
|                                 <option | ||||
|                                     v-for="t in locationTypes" | ||||
|                                     :value="t" | ||||
|                                     :key="t.id" | ||||
|                                 > | ||||
|                                     {{ localizeString(t.title) }} | ||||
|                                 </option> | ||||
|                             </select> | ||||
|                             <label>{{ | ||||
|                                 trans(ACTIVITY_LOCATION_FIELDS_TYPE) | ||||
|                             }}</label> | ||||
|                         </div> | ||||
|             <div class="form-floating mb-3"> | ||||
|               <select | ||||
|                 class="form-select form-select-lg" | ||||
|                 id="type" | ||||
|                 required | ||||
|                 v-model="selectType" | ||||
|               > | ||||
|                 <option selected disabled value=""> | ||||
|                   {{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }} | ||||
|                 </option> | ||||
|                 <option v-for="t in locationTypes" :value="t" :key="t.id"> | ||||
|                   {{ localizeString(t.title) }} | ||||
|                 </option> | ||||
|               </select> | ||||
|               <label>{{ trans(ACTIVITY_LOCATION_FIELDS_TYPE) }}</label> | ||||
|             </div> | ||||
|  | ||||
|                         <div class="form-floating mb-3"> | ||||
|                             <input | ||||
|                                 class="form-control form-control-lg" | ||||
|                                 id="name" | ||||
|                                 v-model="inputName" | ||||
|                                 placeholder | ||||
|                             /> | ||||
|                             <label for="name">{{ | ||||
|                                 trans(ACTIVITY_LOCATION_FIELDS_NAME) | ||||
|                             }}</label> | ||||
|                         </div> | ||||
|             <div class="form-floating mb-3"> | ||||
|               <input | ||||
|                 class="form-control form-control-lg" | ||||
|                 id="name" | ||||
|                 v-model="inputName" | ||||
|                 placeholder | ||||
|               /> | ||||
|               <label for="name">{{ | ||||
|                 trans(ACTIVITY_LOCATION_FIELDS_NAME) | ||||
|               }}</label> | ||||
|             </div> | ||||
|  | ||||
|                         <add-address | ||||
|                             :context="addAddress.context" | ||||
|                             :options="addAddress.options" | ||||
|                             :addressChangedCallback="submitNewAddress" | ||||
|                             v-if="showAddAddress" | ||||
|                             ref="addAddress" | ||||
|                         /> | ||||
|             <add-address | ||||
|               :context="addAddress.context" | ||||
|               :options="addAddress.options" | ||||
|               :addressChangedCallback="submitNewAddress" | ||||
|               v-if="showAddAddress" | ||||
|               ref="addAddress" | ||||
|             /> | ||||
|  | ||||
|                         <div class="form-floating mb-3" v-if="showContactData"> | ||||
|                             <input | ||||
|                                 class="form-control form-control-lg" | ||||
|                                 id="phonenumber1" | ||||
|                                 v-model="inputPhonenumber1" | ||||
|                                 placeholder | ||||
|                             /> | ||||
|                             <label for="phonenumber1">{{ | ||||
|                                 trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER1) | ||||
|                             }}</label> | ||||
|                         </div> | ||||
|                         <div class="form-floating mb-3" v-if="hasPhonenumber1"> | ||||
|                             <input | ||||
|                                 class="form-control form-control-lg" | ||||
|                                 id="phonenumber2" | ||||
|                                 v-model="inputPhonenumber2" | ||||
|                                 placeholder | ||||
|                             /> | ||||
|                             <label for="phonenumber2">{{ | ||||
|                                 trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER2) | ||||
|                             }}</label> | ||||
|                         </div> | ||||
|                         <div class="form-floating mb-3" v-if="showContactData"> | ||||
|                             <input | ||||
|                                 class="form-control form-control-lg" | ||||
|                                 id="email" | ||||
|                                 v-model="inputEmail" | ||||
|                                 placeholder | ||||
|                             /> | ||||
|                             <label for="email">{{ | ||||
|                                 trans(ACTIVITY_LOCATION_FIELDS_EMAIL) | ||||
|                             }}</label> | ||||
|                         </div> | ||||
|                     </form> | ||||
|                 </template> | ||||
|                 <template #footer> | ||||
|                     <button | ||||
|                         class="btn btn-save" | ||||
|                         @click.prevent="saveNewLocation" | ||||
|                     > | ||||
|                         {{ trans(SAVE) }} | ||||
|                     </button> | ||||
|                 </template> | ||||
|             </modal> | ||||
|         </teleport> | ||||
|     </div> | ||||
|             <div class="form-floating mb-3" v-if="showContactData"> | ||||
|               <input | ||||
|                 class="form-control form-control-lg" | ||||
|                 id="phonenumber1" | ||||
|                 v-model="inputPhonenumber1" | ||||
|                 placeholder | ||||
|               /> | ||||
|               <label for="phonenumber1">{{ | ||||
|                 trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER1) | ||||
|               }}</label> | ||||
|             </div> | ||||
|             <div class="form-floating mb-3" v-if="hasPhonenumber1"> | ||||
|               <input | ||||
|                 class="form-control form-control-lg" | ||||
|                 id="phonenumber2" | ||||
|                 v-model="inputPhonenumber2" | ||||
|                 placeholder | ||||
|               /> | ||||
|               <label for="phonenumber2">{{ | ||||
|                 trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER2) | ||||
|               }}</label> | ||||
|             </div> | ||||
|             <div class="form-floating mb-3" v-if="showContactData"> | ||||
|               <input | ||||
|                 class="form-control form-control-lg" | ||||
|                 id="email" | ||||
|                 v-model="inputEmail" | ||||
|                 placeholder | ||||
|               /> | ||||
|               <label for="email">{{ | ||||
|                 trans(ACTIVITY_LOCATION_FIELDS_EMAIL) | ||||
|               }}</label> | ||||
|             </div> | ||||
|           </form> | ||||
|         </template> | ||||
|         <template #footer> | ||||
|           <button class="btn btn-save" @click.prevent="saveNewLocation"> | ||||
|             {{ trans(SAVE) }} | ||||
|           </button> | ||||
|         </template> | ||||
|       </modal> | ||||
|     </teleport> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| @@ -128,237 +119,236 @@ import { getLocationTypes } from "../../api"; | ||||
| import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; | ||||
| import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; | ||||
| 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, | ||||
|   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", | ||||
|     components: { | ||||
|         Modal, | ||||
|         AddAddress, | ||||
|   name: "NewLocation", | ||||
|   components: { | ||||
|     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 { | ||||
|       errors: [], | ||||
|       selected: { | ||||
|         type: null, | ||||
|         name: null, | ||||
|         addressId: null, | ||||
|         phonenumber1: null, | ||||
|         phonenumber2: null, | ||||
|         email: null, | ||||
|       }, | ||||
|       locationTypes: [], | ||||
|       modal: { | ||||
|         showModal: false, | ||||
|         modalDialogClass: "modal-dialog-scrollable modal-xl", | ||||
|       }, | ||||
|       addAddress: { | ||||
|         options: { | ||||
|           button: { | ||||
|             text: { | ||||
|               create: "activity.create_address", | ||||
|               edit: "activity.edit_address", | ||||
|             }, | ||||
|             size: "btn-sm", | ||||
|           }, | ||||
|           title: { | ||||
|             create: "activity.create_address", | ||||
|             edit: "activity.edit_address", | ||||
|           }, | ||||
|         }, | ||||
|         context: { | ||||
|           target: { | ||||
|             //name, id | ||||
|           }, | ||||
|           edit: false, | ||||
|           addressId: null, | ||||
|           defaults: window.addaddress, | ||||
|         }, | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState(["activity"]), | ||||
|     selectType: { | ||||
|       get() { | ||||
|         return this.selected.type; | ||||
|       }, | ||||
|       set(value) { | ||||
|         this.selected.type = value; | ||||
|       }, | ||||
|     }, | ||||
|     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, | ||||
|     inputName: { | ||||
|       get() { | ||||
|         return this.selected.name; | ||||
|       }, | ||||
|       set(value) { | ||||
|         this.selected.name = value; | ||||
|       }, | ||||
|     }, | ||||
|     inputEmail: { | ||||
|       get() { | ||||
|         return this.selected.email; | ||||
|       }, | ||||
|       set(value) { | ||||
|         this.selected.email = value; | ||||
|       }, | ||||
|     }, | ||||
|     inputPhonenumber1: { | ||||
|       get() { | ||||
|         return this.selected.phonenumber1; | ||||
|       }, | ||||
|       set(value) { | ||||
|         this.selected.phonenumber1 = value; | ||||
|       }, | ||||
|     }, | ||||
|     inputPhonenumber2: { | ||||
|       get() { | ||||
|         return this.selected.phonenumber2; | ||||
|       }, | ||||
|       set(value) { | ||||
|         this.selected.phonenumber2 = value; | ||||
|       }, | ||||
|     }, | ||||
|     hasPhonenumber1() { | ||||
|       return ( | ||||
|         this.selected.phonenumber1 !== null && this.selected.phonenumber1 !== "" | ||||
|       ); | ||||
|     }, | ||||
|     showAddAddress() { | ||||
|       let cond = false; | ||||
|       if (this.selected.type) { | ||||
|         if (this.selected.type.addressRequired !== "never") { | ||||
|           cond = true; | ||||
|         } | ||||
|       } | ||||
|       return cond; | ||||
|     }, | ||||
|     showContactData() { | ||||
|       let cond = false; | ||||
|       if (this.selected.type) { | ||||
|         if (this.selected.type.contactData !== "never") { | ||||
|           cond = true; | ||||
|         } | ||||
|       } | ||||
|       return cond; | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.getLocationTypesList(); | ||||
|   }, | ||||
|   methods: { | ||||
|     localizeString, | ||||
|     checkForm() { | ||||
|       let cond = true; | ||||
|       this.errors = []; | ||||
|       if (!this.selected.type) { | ||||
|         this.errors.push("Type de localisation requis"); | ||||
|         cond = false; | ||||
|       } else { | ||||
|         if ( | ||||
|           this.selected.type.addressRequired === "required" && | ||||
|           !this.selected.addressId | ||||
|         ) { | ||||
|           this.errors.push("Adresse requise"); | ||||
|           cond = false; | ||||
|         } | ||||
|         if ( | ||||
|           this.selected.type.contactData === "required" && | ||||
|           !this.selected.phonenumber1 | ||||
|         ) { | ||||
|           this.errors.push("Numéro de téléphone requis"); | ||||
|           cond = false; | ||||
|         } | ||||
|         if ( | ||||
|           this.selected.type.contactData === "required" && | ||||
|           !this.selected.email | ||||
|         ) { | ||||
|           this.errors.push("Adresse email requise"); | ||||
|           cond = false; | ||||
|         } | ||||
|       } | ||||
|       return cond; | ||||
|     }, | ||||
|     getLocationTypesList() { | ||||
|       getLocationTypes().then((results) => { | ||||
|         this.locationTypes = results.filter( | ||||
|           (t) => t.availableForUsers === true, | ||||
|         ); | ||||
|       }); | ||||
|     }, | ||||
|     openModal() { | ||||
|       this.modal.showModal = true; | ||||
|     }, | ||||
|     saveNewLocation() { | ||||
|       if (this.checkForm()) { | ||||
|         let body = { | ||||
|           type: "location", | ||||
|           name: this.selected.name, | ||||
|           locationType: { | ||||
|             id: this.selected.type.id, | ||||
|             type: "location-type", | ||||
|           }, | ||||
|           phonenumber1: this.selected.phonenumber1, | ||||
|           phonenumber2: this.selected.phonenumber2, | ||||
|           email: this.selected.email, | ||||
|         }; | ||||
|     }, | ||||
|     props: ["availableLocations"], | ||||
|     data() { | ||||
|         return { | ||||
|             errors: [], | ||||
|             selected: { | ||||
|                 type: null, | ||||
|                 name: null, | ||||
|                 addressId: null, | ||||
|                 phonenumber1: null, | ||||
|                 phonenumber2: null, | ||||
|                 email: null, | ||||
|         if (this.selected.addressId) { | ||||
|           body = Object.assign(body, { | ||||
|             address: { | ||||
|               id: this.selected.addressId, | ||||
|             }, | ||||
|             locationTypes: [], | ||||
|             modal: { | ||||
|                 showModal: false, | ||||
|                 modalDialogClass: "modal-dialog-scrollable modal-xl", | ||||
|             }, | ||||
|             addAddress: { | ||||
|                 options: { | ||||
|                     button: { | ||||
|                         text: { | ||||
|                             create: "activity.create_address", | ||||
|                             edit: "activity.edit_address", | ||||
|                         }, | ||||
|                         size: "btn-sm", | ||||
|                     }, | ||||
|                     title: { | ||||
|                         create: "activity.create_address", | ||||
|                         edit: "activity.edit_address", | ||||
|                     }, | ||||
|                 }, | ||||
|                 context: { | ||||
|                     target: { | ||||
|                         //name, id | ||||
|                     }, | ||||
|                     edit: false, | ||||
|                     addressId: null, | ||||
|                     defaults: window.addaddress, | ||||
|                 }, | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         ...mapState(["activity"]), | ||||
|         selectType: { | ||||
|             get() { | ||||
|                 return this.selected.type; | ||||
|             }, | ||||
|             set(value) { | ||||
|                 this.selected.type = value; | ||||
|             }, | ||||
|         }, | ||||
|         inputName: { | ||||
|             get() { | ||||
|                 return this.selected.name; | ||||
|             }, | ||||
|             set(value) { | ||||
|                 this.selected.name = value; | ||||
|             }, | ||||
|         }, | ||||
|         inputEmail: { | ||||
|             get() { | ||||
|                 return this.selected.email; | ||||
|             }, | ||||
|             set(value) { | ||||
|                 this.selected.email = value; | ||||
|             }, | ||||
|         }, | ||||
|         inputPhonenumber1: { | ||||
|             get() { | ||||
|                 return this.selected.phonenumber1; | ||||
|             }, | ||||
|             set(value) { | ||||
|                 this.selected.phonenumber1 = value; | ||||
|             }, | ||||
|         }, | ||||
|         inputPhonenumber2: { | ||||
|             get() { | ||||
|                 return this.selected.phonenumber2; | ||||
|             }, | ||||
|             set(value) { | ||||
|                 this.selected.phonenumber2 = value; | ||||
|             }, | ||||
|         }, | ||||
|         hasPhonenumber1() { | ||||
|             return ( | ||||
|                 this.selected.phonenumber1 !== null && | ||||
|                 this.selected.phonenumber1 !== "" | ||||
|             ); | ||||
|         }, | ||||
|         showAddAddress() { | ||||
|             let cond = false; | ||||
|             if (this.selected.type) { | ||||
|                 if (this.selected.type.addressRequired !== "never") { | ||||
|                     cond = true; | ||||
|                 } | ||||
|             } | ||||
|             return cond; | ||||
|         }, | ||||
|         showContactData() { | ||||
|             let cond = false; | ||||
|             if (this.selected.type) { | ||||
|                 if (this.selected.type.contactData !== "never") { | ||||
|                     cond = true; | ||||
|                 } | ||||
|             } | ||||
|             return cond; | ||||
|         }, | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.getLocationTypesList(); | ||||
|     }, | ||||
|     methods: { | ||||
|         localizeString, | ||||
|         checkForm() { | ||||
|             let cond = true; | ||||
|             this.errors = []; | ||||
|             if (!this.selected.type) { | ||||
|                 this.errors.push("Type de localisation requis"); | ||||
|                 cond = false; | ||||
|             } else { | ||||
|                 if ( | ||||
|                     this.selected.type.addressRequired === "required" && | ||||
|                     !this.selected.addressId | ||||
|                 ) { | ||||
|                     this.errors.push("Adresse requise"); | ||||
|                     cond = false; | ||||
|                 } | ||||
|                 if ( | ||||
|                     this.selected.type.contactData === "required" && | ||||
|                     !this.selected.phonenumber1 | ||||
|                 ) { | ||||
|                     this.errors.push("Numéro de téléphone requis"); | ||||
|                     cond = false; | ||||
|                 } | ||||
|                 if ( | ||||
|                     this.selected.type.contactData === "required" && | ||||
|                     !this.selected.email | ||||
|                 ) { | ||||
|                     this.errors.push("Adresse email requise"); | ||||
|                     cond = false; | ||||
|                 } | ||||
|             } | ||||
|             return cond; | ||||
|         }, | ||||
|         getLocationTypesList() { | ||||
|             getLocationTypes().then((results) => { | ||||
|                 this.locationTypes = results.filter( | ||||
|                     (t) => t.availableForUsers === true, | ||||
|                 ); | ||||
|             }); | ||||
|         }, | ||||
|         openModal() { | ||||
|             this.modal.showModal = true; | ||||
|         }, | ||||
|         saveNewLocation() { | ||||
|             if (this.checkForm()) { | ||||
|                 let body = { | ||||
|                     type: "location", | ||||
|                     name: this.selected.name, | ||||
|                     locationType: { | ||||
|                         id: this.selected.type.id, | ||||
|                         type: "location-type", | ||||
|                     }, | ||||
|                     phonenumber1: this.selected.phonenumber1, | ||||
|                     phonenumber2: this.selected.phonenumber2, | ||||
|                     email: this.selected.email, | ||||
|                 }; | ||||
|                 if (this.selected.addressId) { | ||||
|                     body = Object.assign(body, { | ||||
|                         address: { | ||||
|                             id: this.selected.addressId, | ||||
|                         }, | ||||
|                     }); | ||||
|                 } | ||||
|           }); | ||||
|         } | ||||
|  | ||||
|                 makeFetch("POST", "/api/1.0/main/location.json", body) | ||||
|                     .then((response) => { | ||||
|                         this.$store.dispatch("addAvailableLocationGroup", { | ||||
|                             locationGroup: "Localisations nouvellement créées", | ||||
|                             locations: [response], | ||||
|                         }); | ||||
|                         this.$store.dispatch("updateLocation", response); | ||||
|                         this.modal.showModal = false; | ||||
|                     }) | ||||
|                     .catch((error) => { | ||||
|                         if (error.name === "ValidationException") { | ||||
|                             for (let v of error.violations) { | ||||
|                                 this.errors.push(v); | ||||
|                             } | ||||
|                         } else { | ||||
|                             this.errors.push("An error occurred"); | ||||
|                         } | ||||
|                     }); | ||||
|         makeFetch("POST", "/api/1.0/main/location.json", body) | ||||
|           .then((response) => { | ||||
|             this.$store.dispatch("addAvailableLocationGroup", { | ||||
|               locationGroup: "Localisations nouvellement créées", | ||||
|               locations: [response], | ||||
|             }); | ||||
|             this.$store.dispatch("updateLocation", response); | ||||
|             this.modal.showModal = false; | ||||
|           }) | ||||
|           .catch((error) => { | ||||
|             if (error.name === "ValidationException") { | ||||
|               for (let v of error.violations) { | ||||
|                 this.errors.push(v); | ||||
|               } | ||||
|             } else { | ||||
|               this.errors.push("An error occurred"); | ||||
|             } | ||||
|         }, | ||||
|         submitNewAddress(payload) { | ||||
|             this.selected.addressId = payload.addressId; | ||||
|             this.addAddress.context.addressId = payload.addressId; | ||||
|             this.addAddress.context.edit = true; | ||||
|         }, | ||||
|           }); | ||||
|       } | ||||
|     }, | ||||
|     submitNewAddress(payload) { | ||||
|       this.selected.addressId = payload.addressId; | ||||
|       this.addAddress.context.addressId = payload.addressId; | ||||
|       this.addAddress.context.edit = true; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -1,103 +1,98 @@ | ||||
| <template> | ||||
|     <teleport to="#social-issues-acc"> | ||||
|         <div class="mb-3 row"> | ||||
|             <div class="col-4"> | ||||
|                 <label :class="socialIssuesClassList">{{ | ||||
|                     trans(ACTIVITY_SOCIAL_ISSUES) | ||||
|                 }}</label> | ||||
|             </div> | ||||
|             <div class="col-8"> | ||||
|                 <check-social-issue | ||||
|                     v-for="issue in socialIssuesList" | ||||
|                     :key="issue.id" | ||||
|                     :issue="issue" | ||||
|                     :selection="socialIssuesSelected" | ||||
|                     @updateSelected="updateIssuesSelected" | ||||
|                 > | ||||
|                 </check-social-issue> | ||||
|   <teleport to="#social-issues-acc"> | ||||
|     <div class="mb-3 row"> | ||||
|       <div class="col-4"> | ||||
|         <label :class="socialIssuesClassList">{{ | ||||
|           trans(ACTIVITY_SOCIAL_ISSUES) | ||||
|         }}</label> | ||||
|       </div> | ||||
|       <div class="col-8"> | ||||
|         <check-social-issue | ||||
|           v-for="issue in socialIssuesList" | ||||
|           :key="issue.id" | ||||
|           :issue="issue" | ||||
|           :selection="socialIssuesSelected" | ||||
|           @updateSelected="updateIssuesSelected" | ||||
|         > | ||||
|         </check-social-issue> | ||||
|  | ||||
|                 <div class="my-3"> | ||||
|                     <VueMultiselect | ||||
|                         name="otherIssues" | ||||
|                         label="text" | ||||
|                         track-by="id" | ||||
|                         open-direction="bottom" | ||||
|                         :close-on-select="true" | ||||
|                         :preserve-search="false" | ||||
|                         :reset-after="true" | ||||
|                         :hide-selected="true" | ||||
|                         :taggable="false" | ||||
|                         :multiple="false" | ||||
|                         :searchable="true" | ||||
|                         :allow-empty="true" | ||||
|                         :show-labels="false" | ||||
|                         :loading="issueIsLoading" | ||||
|                         :placeholder="trans(ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE)" | ||||
|                         :options="socialIssuesOther" | ||||
|                         @select="addIssueInList" | ||||
|                     > | ||||
|                     </VueMultiselect> | ||||
|                 </div> | ||||
|             </div> | ||||
|         <div class="my-3"> | ||||
|           <VueMultiselect | ||||
|             name="otherIssues" | ||||
|             label="text" | ||||
|             track-by="id" | ||||
|             open-direction="bottom" | ||||
|             :close-on-select="true" | ||||
|             :preserve-search="false" | ||||
|             :reset-after="true" | ||||
|             :hide-selected="true" | ||||
|             :taggable="false" | ||||
|             :multiple="false" | ||||
|             :searchable="true" | ||||
|             :allow-empty="true" | ||||
|             :show-labels="false" | ||||
|             :loading="issueIsLoading" | ||||
|             :placeholder="trans(ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE)" | ||||
|             :options="socialIssuesOther" | ||||
|             @select="addIssueInList" | ||||
|           > | ||||
|           </VueMultiselect> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="mb-3 row"> | ||||
|       <div class="col-4"> | ||||
|         <label :class="socialActionsClassList">{{ | ||||
|           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> | ||||
|         </div> | ||||
|  | ||||
|         <div class="mb-3 row"> | ||||
|             <div class="col-4"> | ||||
|                 <label :class="socialActionsClassList">{{ | ||||
|                     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> | ||||
|                 </div> | ||||
|         <span | ||||
|           v-else-if="socialIssuesSelected.length === 0" | ||||
|           class="inline-choice chill-no-data-statement mt-3" | ||||
|         > | ||||
|           {{ trans(ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE) }} | ||||
|         </span> | ||||
|  | ||||
|                 <span | ||||
|                     v-else-if="socialIssuesSelected.length === 0" | ||||
|                     class="inline-choice chill-no-data-statement mt-3" | ||||
|                 > | ||||
|                     {{ trans(ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE) }} | ||||
|                 </span> | ||||
|         <template | ||||
|           v-else-if=" | ||||
|             socialActionsList.length > 0 && | ||||
|             (socialIssuesSelected.length || socialActionsSelected.length) | ||||
|           " | ||||
|         > | ||||
|           <div | ||||
|             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 group.actions" | ||||
|               :key="action.id" | ||||
|               :action="action" | ||||
|               :selection="socialActionsSelected" | ||||
|               @updateSelected="updateActionsSelected" | ||||
|             > | ||||
|             </check-social-action> | ||||
|           </div> | ||||
|         </template> | ||||
|  | ||||
|                 <template | ||||
|                     v-else-if=" | ||||
|                         socialActionsList.length > 0 && | ||||
|                         (socialIssuesSelected.length || | ||||
|                             socialActionsSelected.length) | ||||
|                     " | ||||
|                 > | ||||
|                     <div | ||||
|                         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 group.actions" | ||||
|                             :key="action.id" | ||||
|                             :action="action" | ||||
|                             :selection="socialActionsSelected" | ||||
|                             @updateSelected="updateActionsSelected" | ||||
|                         > | ||||
|                         </check-social-action> | ||||
|                     </div> | ||||
|                 </template> | ||||
|  | ||||
|                 <span | ||||
|                     v-else-if=" | ||||
|                         actionAreLoaded && socialActionsList.length === 0 | ||||
|                     " | ||||
|                     class="inline-choice chill-no-data-statement mt-3" | ||||
|                 > | ||||
|                     {{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }} | ||||
|                 </span> | ||||
|             </div> | ||||
|         </div> | ||||
|     </teleport> | ||||
|         <span | ||||
|           v-else-if="actionAreLoaded && socialActionsList.length === 0" | ||||
|           class="inline-choice chill-no-data-statement mt-3" | ||||
|         > | ||||
|           {{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }} | ||||
|         </span> | ||||
|       </div> | ||||
|     </div> | ||||
|   </teleport> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| @@ -106,154 +101,153 @@ 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, | ||||
|   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", | ||||
|     components: { | ||||
|         CheckSocialIssue, | ||||
|         CheckSocialAction, | ||||
|         VueMultiselect, | ||||
|   name: "SocialIssuesAcc", | ||||
|   components: { | ||||
|     CheckSocialIssue, | ||||
|     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, | ||||
|       actionIsLoading: false, | ||||
|       actionAreLoaded: false, | ||||
|       socialIssuesClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialIssues").getAttribute("required") ? "required" : ""}`, | ||||
|       socialActionsClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialActions").getAttribute("required") ? "required" : ""}`, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     socialIssuesList() { | ||||
|       return this.$store.state.activity.accompanyingPeriod.socialIssues; | ||||
|     }, | ||||
|     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, | ||||
|         }; | ||||
|     socialIssuesSelected() { | ||||
|       return this.$store.state.activity.socialIssues; | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             issueIsLoading: false, | ||||
|             actionIsLoading: false, | ||||
|             actionAreLoaded: false, | ||||
|             socialIssuesClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialIssues").getAttribute("required") ? "required" : ""}`, | ||||
|             socialActionsClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialActions").getAttribute("required") ? "required" : ""}`, | ||||
|         }; | ||||
|     socialIssuesOther() { | ||||
|       return this.$store.state.socialIssuesOther; | ||||
|     }, | ||||
|     computed: { | ||||
|         socialIssuesList() { | ||||
|             return this.$store.state.activity.accompanyingPeriod.socialIssues; | ||||
|         }, | ||||
|         socialIssuesSelected() { | ||||
|             return this.$store.state.activity.socialIssues; | ||||
|         }, | ||||
|         socialIssuesOther() { | ||||
|             return this.$store.state.socialIssuesOther; | ||||
|         }, | ||||
|         socialActionsList() { | ||||
|             return this.$store.getters.socialActionsListSorted; | ||||
|         }, | ||||
|         socialActionsSelected() { | ||||
|             return this.$store.state.activity.socialActions; | ||||
|         }, | ||||
|     socialActionsList() { | ||||
|       return this.$store.getters.socialActionsListSorted; | ||||
|     }, | ||||
|     mounted() { | ||||
|         /* Load other issues in multiselect */ | ||||
|         this.issueIsLoading = true; | ||||
|         this.actionAreLoaded = false; | ||||
|  | ||||
|         getSocialIssues().then((response) => { | ||||
|             /* Add issues to the store */ | ||||
|             this.$store.commit("updateIssuesOther", response); | ||||
|  | ||||
|             /* 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); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             /* Remove from multiselect the issues that are not yet in the checkbox list */ | ||||
|             this.socialIssuesList.forEach((issue) => { | ||||
|                 this.$store.commit("removeIssueInOther", issue); | ||||
|             }); | ||||
|  | ||||
|             /* Filter issues */ | ||||
|             this.$store.commit("filterList", "issues"); | ||||
|  | ||||
|             /* Add in list the actions already associated (if not yet listed) */ | ||||
|             this.socialActionsSelected.forEach((action) => { | ||||
|                 this.$store.commit("addActionInList", action); | ||||
|             }); | ||||
|  | ||||
|             /* Filter actions */ | ||||
|             this.$store.commit("filterList", "actions"); | ||||
|  | ||||
|             this.issueIsLoading = false; | ||||
|             this.actionAreLoaded = true; | ||||
|             this.updateActionsList(); | ||||
|         }); | ||||
|     socialActionsSelected() { | ||||
|       return this.$store.state.activity.socialActions; | ||||
|     }, | ||||
|     methods: { | ||||
|         /* When choosing an issue in multiselect, add it in checkboxes (as selected), | ||||
|   }, | ||||
|   mounted() { | ||||
|     /* Load other issues in multiselect */ | ||||
|     this.issueIsLoading = true; | ||||
|     this.actionAreLoaded = false; | ||||
|  | ||||
|     getSocialIssues().then((response) => { | ||||
|       /* Add issues to the store */ | ||||
|       this.$store.commit("updateIssuesOther", response); | ||||
|  | ||||
|       /* 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); | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|       /* Remove from multiselect the issues that are not yet in the checkbox list */ | ||||
|       this.socialIssuesList.forEach((issue) => { | ||||
|         this.$store.commit("removeIssueInOther", issue); | ||||
|       }); | ||||
|  | ||||
|       /* Filter issues */ | ||||
|       this.$store.commit("filterList", "issues"); | ||||
|  | ||||
|       /* Add in list the actions already associated (if not yet listed) */ | ||||
|       this.socialActionsSelected.forEach((action) => { | ||||
|         this.$store.commit("addActionInList", action); | ||||
|       }); | ||||
|  | ||||
|       /* 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), | ||||
|          remove it from multiselect, and add socialActions concerned | ||||
|       */ | ||||
|         addIssueInList(value) { | ||||
|             //console.log('addIssueInList', value); | ||||
|             this.$store.commit("addIssueInList", value); | ||||
|             this.$store.commit("removeIssueInOther", value); | ||||
|             this.$store.dispatch("addIssueSelected", value); | ||||
|             this.updateActionsList(); | ||||
|         }, | ||||
|         /* Update value for selected issues checkboxes | ||||
|          */ | ||||
|         updateIssuesSelected(issues) { | ||||
|             //console.log('updateIssuesSelected', issues); | ||||
|             this.$store.dispatch("updateIssuesSelected", issues); | ||||
|             this.updateActionsList(); | ||||
|         }, | ||||
|         /* Update value for selected actions checkboxes | ||||
|          */ | ||||
|         updateActionsSelected(actions) { | ||||
|             //console.log('updateActionsSelected', actions); | ||||
|             this.$store.dispatch("updateActionsSelected", actions); | ||||
|         }, | ||||
|         /* Add socialActions concerned: after reset, loop on each issue selected | ||||
|     addIssueInList(value) { | ||||
|       //console.log('addIssueInList', value); | ||||
|       this.$store.commit("addIssueInList", value); | ||||
|       this.$store.commit("removeIssueInOther", value); | ||||
|       this.$store.dispatch("addIssueSelected", value); | ||||
|       this.updateActionsList(); | ||||
|     }, | ||||
|     /* Update value for selected issues checkboxes | ||||
|      */ | ||||
|     updateIssuesSelected(issues) { | ||||
|       //console.log('updateIssuesSelected', issues); | ||||
|       this.$store.dispatch("updateIssuesSelected", issues); | ||||
|       this.updateActionsList(); | ||||
|     }, | ||||
|     /* Update value for selected actions checkboxes | ||||
|      */ | ||||
|     updateActionsSelected(actions) { | ||||
|       //console.log('updateActionsSelected', actions); | ||||
|       this.$store.dispatch("updateActionsSelected", actions); | ||||
|     }, | ||||
|     /* Add socialActions concerned: after reset, loop on each issue selected | ||||
|          to get social actions concerned | ||||
|       */ | ||||
|         updateActionsList() { | ||||
|             this.resetActionsList(); | ||||
|             this.socialIssuesSelected.forEach((item) => { | ||||
|                 this.actionIsLoading = true; | ||||
|                 getSocialActionByIssue(item.id).then( | ||||
|                     (actions) => | ||||
|                         new Promise((resolve) => { | ||||
|                             actions.results.forEach((action) => { | ||||
|                                 this.$store.commit("addActionInList", action); | ||||
|                             }, this); | ||||
|     updateActionsList() { | ||||
|       this.resetActionsList(); | ||||
|       this.socialIssuesSelected.forEach((item) => { | ||||
|         this.actionIsLoading = true; | ||||
|         getSocialActionByIssue(item.id).then( | ||||
|           (actions) => | ||||
|             new Promise((resolve) => { | ||||
|               actions.results.forEach((action) => { | ||||
|                 this.$store.commit("addActionInList", action); | ||||
|               }, this); | ||||
|  | ||||
|                             this.$store.commit("filterList", "actions"); | ||||
|               this.$store.commit("filterList", "actions"); | ||||
|  | ||||
|                             this.actionIsLoading = false; | ||||
|                             this.actionAreLoaded = true; | ||||
|                             resolve(); | ||||
|                         }), | ||||
|                 ); | ||||
|             }, this); | ||||
|         }, | ||||
|         /* Reset socialActions List: flush list and restore selected actions | ||||
|          */ | ||||
|         resetActionsList() { | ||||
|             this.$store.commit("resetActionsList"); | ||||
|             this.actionAreLoaded = false; | ||||
|             this.socialActionsSelected.forEach((item) => { | ||||
|                 this.$store.commit("addActionInList", item); | ||||
|             }, this); | ||||
|         }, | ||||
|               this.actionIsLoading = false; | ||||
|               this.actionAreLoaded = true; | ||||
|               resolve(); | ||||
|             }), | ||||
|         ); | ||||
|       }, this); | ||||
|     }, | ||||
|     /* Reset socialActions List: flush list and restore selected actions | ||||
|      */ | ||||
|     resetActionsList() { | ||||
|       this.$store.commit("resetActionsList"); | ||||
|       this.actionAreLoaded = false; | ||||
|       this.socialActionsSelected.forEach((item) => { | ||||
|         this.$store.commit("addActionInList", item); | ||||
|       }, this); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| @@ -263,18 +257,18 @@ export default { | ||||
| @import "ChillMainAssets/chill/scss/chill_variables"; | ||||
|  | ||||
| span.multiselect__single { | ||||
|     display: none !important; | ||||
|   display: none !important; | ||||
| } | ||||
|  | ||||
| #actionsList { | ||||
|     border-radius: 0.5rem; | ||||
|     padding: 1rem; | ||||
|     margin: 0.5rem; | ||||
|     background-color: whitesmoke; | ||||
|   border-radius: 0.5rem; | ||||
|   padding: 1rem; | ||||
|   margin: 0.5rem; | ||||
|   background-color: whitesmoke; | ||||
| } | ||||
|  | ||||
| span.badge { | ||||
|     margin-bottom: 0.5rem; | ||||
|     @include badge_social($social-issue-color); | ||||
|   margin-bottom: 0.5rem; | ||||
|   @include badge_social($social-issue-color); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,38 +1,38 @@ | ||||
| <template> | ||||
|     <span class="inline-choice"> | ||||
|         <div class="form-check"> | ||||
|             <input | ||||
|                 class="form-check-input" | ||||
|                 type="checkbox" | ||||
|                 v-model="selected" | ||||
|                 name="action" | ||||
|                 :id="action.id" | ||||
|                 :value="action" | ||||
|             /> | ||||
|             <label class="form-check-label" :for="action.id"> | ||||
|                 <span class="badge bg-light text-dark" :title="action.text">{{ | ||||
|                     action.text | ||||
|                 }}</span> | ||||
|             </label> | ||||
|         </div> | ||||
|     </span> | ||||
|   <span class="inline-choice"> | ||||
|     <div class="form-check"> | ||||
|       <input | ||||
|         class="form-check-input" | ||||
|         type="checkbox" | ||||
|         v-model="selected" | ||||
|         name="action" | ||||
|         :id="action.id" | ||||
|         :value="action" | ||||
|       /> | ||||
|       <label class="form-check-label" :for="action.id"> | ||||
|         <span class="badge bg-light text-dark" :title="action.text">{{ | ||||
|           action.text | ||||
|         }}</span> | ||||
|       </label> | ||||
|     </div> | ||||
|   </span> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     name: "CheckSocialAction", | ||||
|     props: ["action", "selection"], | ||||
|     emits: ["updateSelected"], | ||||
|     computed: { | ||||
|         selected: { | ||||
|             set(value) { | ||||
|                 this.$emit("updateSelected", value); | ||||
|             }, | ||||
|             get() { | ||||
|                 return this.selection; | ||||
|             }, | ||||
|         }, | ||||
|   name: "CheckSocialAction", | ||||
|   props: ["action", "selection"], | ||||
|   emits: ["updateSelected"], | ||||
|   computed: { | ||||
|     selected: { | ||||
|       set(value) { | ||||
|         this.$emit("updateSelected", value); | ||||
|       }, | ||||
|       get() { | ||||
|         return this.selection; | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| @@ -41,13 +41,13 @@ export default { | ||||
| @import "ChillPersonAssets/chill/scss/mixins"; | ||||
| @import "ChillMainAssets/chill/scss/chill_variables"; | ||||
| span.badge { | ||||
|     @include badge_social($social-action-color); | ||||
|     font-size: 95%; | ||||
|     margin-bottom: 5px; | ||||
|     margin-right: 1em; | ||||
|     max-width: 100%; /* Adjust as needed */ | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     white-space: nowrap; | ||||
|   @include badge_social($social-action-color); | ||||
|   font-size: 95%; | ||||
|   margin-bottom: 5px; | ||||
|   margin-right: 1em; | ||||
|   max-width: 100%; /* Adjust as needed */ | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,38 +1,36 @@ | ||||
| <template> | ||||
|     <span class="inline-choice"> | ||||
|         <div class="form-check"> | ||||
|             <input | ||||
|                 class="form-check-input" | ||||
|                 type="checkbox" | ||||
|                 v-model="selected" | ||||
|                 name="issue" | ||||
|                 :id="issue.id" | ||||
|                 :value="issue" | ||||
|             /> | ||||
|             <label class="form-check-label" :for="issue.id"> | ||||
|                 <span class="badge bg-chill-l-gray text-dark">{{ | ||||
|                     issue.text | ||||
|                 }}</span> | ||||
|             </label> | ||||
|         </div> | ||||
|     </span> | ||||
|   <span class="inline-choice"> | ||||
|     <div class="form-check"> | ||||
|       <input | ||||
|         class="form-check-input" | ||||
|         type="checkbox" | ||||
|         v-model="selected" | ||||
|         name="issue" | ||||
|         :id="issue.id" | ||||
|         :value="issue" | ||||
|       /> | ||||
|       <label class="form-check-label" :for="issue.id"> | ||||
|         <span class="badge bg-chill-l-gray text-dark">{{ issue.text }}</span> | ||||
|       </label> | ||||
|     </div> | ||||
|   </span> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     name: "CheckSocialIssue", | ||||
|     props: ["issue", "selection"], | ||||
|     emits: ["updateSelected"], | ||||
|     computed: { | ||||
|         selected: { | ||||
|             set(value) { | ||||
|                 this.$emit("updateSelected", value); | ||||
|             }, | ||||
|             get() { | ||||
|                 return this.selection; | ||||
|             }, | ||||
|         }, | ||||
|   name: "CheckSocialIssue", | ||||
|   props: ["issue", "selection"], | ||||
|   emits: ["updateSelected"], | ||||
|   computed: { | ||||
|     selected: { | ||||
|       set(value) { | ||||
|         this.$emit("updateSelected", value); | ||||
|       }, | ||||
|       get() { | ||||
|         return this.selection; | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| @@ -41,9 +39,9 @@ export default { | ||||
| @import "ChillPersonAssets/chill/scss/mixins"; | ||||
| @import "ChillMainAssets/chill/scss/chill_variables"; | ||||
| span.badge { | ||||
|     @include badge_social($social-issue-color); | ||||
|     font-size: 95%; | ||||
|     margin-bottom: 5px; | ||||
|     margin-right: 1em; | ||||
|   @include badge_social($social-issue-color); | ||||
|   font-size: 95%; | ||||
|   margin-bottom: 5px; | ||||
|   margin-right: 1em; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -25,7 +25,6 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte | ||||
|         $config = $this->processConfiguration($configuration, $configs); | ||||
|  | ||||
|         $container->setParameter('chill_aside_activity.form.time_duration', $config['form']['time_duration']); | ||||
|         $container->setParameter('chill_aside_activity.show_concerned_persons_count', 'visible' === $config['show_concerned_persons_count']); | ||||
|  | ||||
|         $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); | ||||
|         $loader->load('services.yaml'); | ||||
| @@ -39,24 +38,6 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte | ||||
|     { | ||||
|         $this->prependRoute($container); | ||||
|         $this->prependCruds($container); | ||||
|         $this->prependTwigConfig($container); | ||||
|     } | ||||
|  | ||||
|     protected function prependTwigConfig(ContainerBuilder $container) | ||||
|     { | ||||
|         // Get the configuration for this bundle | ||||
|         $chillAsideActivityConfig = $container->getExtensionConfig($this->getAlias()); | ||||
|         $config = $this->processConfiguration($this->getConfiguration($chillAsideActivityConfig, $container), $chillAsideActivityConfig); | ||||
|  | ||||
|         // Add configuration to twig globals | ||||
|         $twigConfig = [ | ||||
|             'globals' => [ | ||||
|                 'chill_aside_activity_config' => [ | ||||
|                     'show_concerned_persons_count' => 'visible' === $config['show_concerned_persons_count'], | ||||
|                 ], | ||||
|             ], | ||||
|         ]; | ||||
|         $container->prependExtensionConfig('twig', $twigConfig); | ||||
|     } | ||||
|  | ||||
|     protected function prependCruds(ContainerBuilder $container) | ||||
|   | ||||
| @@ -141,12 +141,6 @@ class Configuration implements ConfigurationInterface | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->end() | ||||
|             ->enumNode('show_concerned_persons_count') | ||||
|             ->values(['hidden', 'visible']) | ||||
|             ->defaultValue('hidden') | ||||
|             ->info('Show the concerned persons count field in aside activity forms and views') | ||||
|             ->end() | ||||
|             ->end(); | ||||
|  | ||||
|         return $treeBuilder; | ||||
|   | ||||
| @@ -62,10 +62,6 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface | ||||
|     #[ORM\ManyToOne(targetEntity: User::class)] | ||||
|     private User $updatedBy; | ||||
|  | ||||
|     #[Assert\GreaterThanOrEqual(0)] | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true)] | ||||
|     private ?int $concernedPersonsCount = 0; | ||||
|  | ||||
|     public function getAgent(): ?User | ||||
|     { | ||||
|         return $this->agent; | ||||
| @@ -190,16 +186,4 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getConcernedPersonsCount(): ?int | ||||
|     { | ||||
|         return $this->concernedPersonsCount; | ||||
|     } | ||||
|  | ||||
|     public function setConcernedPersonsCount(?int $concernedPersonsCount): self | ||||
|     { | ||||
|         $this->concernedPersonsCount = $concernedPersonsCount; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,86 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\AsideActivityBundle\Export\Aggregator; | ||||
|  | ||||
| use Chill\AsideActivityBundle\Export\Declarations; | ||||
| use Chill\MainBundle\Export\AggregatorInterface; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
|  | ||||
| class ByConcernedPersonsCountAggregator implements AggregatorInterface | ||||
| { | ||||
|     public function addRole(): ?string | ||||
|     { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void | ||||
|     { | ||||
|         $qb->addSelect('aside.concernedPersonsCount AS by_concerned_persons_count_aggregator') | ||||
|             ->addGroupBy('by_concerned_persons_count_aggregator'); | ||||
|     } | ||||
|  | ||||
|     public function applyOn(): string | ||||
|     { | ||||
|         return Declarations::ASIDE_ACTIVITY_TYPE; | ||||
|     } | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder): void | ||||
|     { | ||||
|         // No form needed | ||||
|     } | ||||
|  | ||||
|     public function getNormalizationVersion(): int | ||||
|     { | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
|     public function normalizeFormData(array $formData): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function denormalizeFormData(array $formData, int $fromVersion): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function getFormDefaultData(): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function getLabels($key, array $values, $data): callable | ||||
|     { | ||||
|         return function ($value): string { | ||||
|             if ('_header' === $value) { | ||||
|                 return 'export.aggregator.Concerned persons count'; | ||||
|             } | ||||
|  | ||||
|             if (null === $value) { | ||||
|                 return 'export.aggregator.No concerned persons count specified'; | ||||
|             } | ||||
|  | ||||
|             return (string) $value; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public function getQueryKeys($data): array | ||||
|     { | ||||
|         return ['by_concerned_persons_count_aggregator']; | ||||
|     } | ||||
|  | ||||
|     public function getTitle(): string | ||||
|     { | ||||
|         return 'export.aggregator.Group by concerned persons count'; | ||||
|     } | ||||
| } | ||||
| @@ -1,116 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\AsideActivityBundle\Export\Export; | ||||
|  | ||||
| use Chill\AsideActivityBundle\Export\Declarations; | ||||
| use Chill\AsideActivityBundle\Repository\AsideActivityRepository; | ||||
| use Chill\AsideActivityBundle\Security\AsideActivityVoter; | ||||
| use Chill\MainBundle\Export\ExportInterface; | ||||
| use Chill\MainBundle\Export\FormatterInterface; | ||||
| use Chill\MainBundle\Export\GroupedExportInterface; | ||||
| use Doctrine\ORM\Query; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
|  | ||||
| class SumConcernedPersonsCountAsideActivity implements ExportInterface, GroupedExportInterface | ||||
| { | ||||
|     public function __construct(private readonly AsideActivityRepository $repository) {} | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder) {} | ||||
|  | ||||
|     public function getNormalizationVersion(): int | ||||
|     { | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
|     public function normalizeFormData(array $formData): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function denormalizeFormData(array $formData, int $fromVersion): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function getFormDefaultData(): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function getAllowedFormattersTypes(): array | ||||
|     { | ||||
|         return [FormatterInterface::TYPE_TABULAR]; | ||||
|     } | ||||
|  | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'export.Sum concerned persons count for aside activities'; | ||||
|     } | ||||
|  | ||||
|     public function getGroup(): string | ||||
|     { | ||||
|         return 'export.Exports of aside activities'; | ||||
|     } | ||||
|  | ||||
|     public function getLabels($key, array $values, $data) | ||||
|     { | ||||
|         if ('export_sum_concerned_persons_count' !== $key) { | ||||
|             throw new \LogicException("the key {$key} is not used by this export"); | ||||
|         } | ||||
|  | ||||
|         $labels = array_combine($values, $values); | ||||
|         $labels['_header'] = $this->getTitle(); | ||||
|  | ||||
|         return static fn ($value) => $labels[$value]; | ||||
|     } | ||||
|  | ||||
|     public function getQueryKeys($data): array | ||||
|     { | ||||
|         return ['export_sum_concerned_persons_count']; | ||||
|     } | ||||
|  | ||||
|     public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array | ||||
|     { | ||||
|         return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); | ||||
|     } | ||||
|  | ||||
|     public function getTitle(): string | ||||
|     { | ||||
|         return 'export.Sum concerned persons count for aside activities'; | ||||
|     } | ||||
|  | ||||
|     public function getType(): string | ||||
|     { | ||||
|         return Declarations::ASIDE_ACTIVITY_TYPE; | ||||
|     } | ||||
|  | ||||
|     public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder | ||||
|     { | ||||
|         $qb = $this->repository->createQueryBuilder('aside'); | ||||
|  | ||||
|         $qb->select('SUM(COALESCE(aside.concernedPersonsCount, 0)) as export_sum_concerned_persons_count'); | ||||
|  | ||||
|         return $qb; | ||||
|     } | ||||
|  | ||||
|     public function requiredRole(): string | ||||
|     { | ||||
|         return AsideActivityVoter::STATS; | ||||
|     } | ||||
|  | ||||
|     public function supportsModifiers(): array | ||||
|     { | ||||
|         return [ | ||||
|             Declarations::ASIDE_ACTIVITY_TYPE, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -21,7 +21,6 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; | ||||
| use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\IntegerType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| use Symfony\Component\Form\FormEvent; | ||||
| use Symfony\Component\Form\FormEvents; | ||||
| @@ -30,13 +29,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver; | ||||
| final class AsideActivityFormType extends AbstractType | ||||
| { | ||||
|     private readonly array $timeChoices; | ||||
|     private readonly bool $showConcernedPersonsCount; | ||||
|  | ||||
|     public function __construct( | ||||
|         ParameterBagInterface $parameterBag, | ||||
|     ) { | ||||
|         $this->timeChoices = $parameterBag->get('chill_aside_activity.form.time_duration'); | ||||
|         $this->showConcernedPersonsCount = $parameterBag->get('chill_aside_activity.show_concerned_persons_count'); | ||||
|     } | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options) | ||||
| @@ -79,16 +76,6 @@ final class AsideActivityFormType extends AbstractType | ||||
|             ->add('location', PickUserLocationType::class) | ||||
|         ; | ||||
|  | ||||
|         if ($this->showConcernedPersonsCount) { | ||||
|             $builder->add('concernedPersonsCount', IntegerType::class, [ | ||||
|                 'label' => 'Concerned persons count', | ||||
|                 'required' => false, | ||||
|                 'attr' => [ | ||||
|                     'min' => 0, | ||||
|                 ], | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         foreach (['duration'] as $fieldName) { | ||||
|             $builder->get($fieldName) | ||||
|                 ->addModelTransformer($durationTimeTransformer); | ||||
|   | ||||
| @@ -42,11 +42,6 @@ | ||||
|                                 {%- if entity.location.name is defined -%} | ||||
|                                     <div><i class="fa fa-fw fa-map-marker"></i>{{ entity.location.name }}</div> | ||||
|                                 {%- endif -%} | ||||
|  | ||||
|                                 {%- if entity.concernedPersonsCount > 0 -%} | ||||
|                                     <div><i class="fa fa-fw fa-user"></i>{{ entity.concernedPersonsCount }}</div> | ||||
|                                 {%- endif -%} | ||||
|  | ||||
| 							</div> | ||||
| 							<div class="item-col" style="justify-content: flex-end;"> | ||||
| 								<div class="box"> | ||||
|   | ||||
| @@ -38,11 +38,6 @@ | ||||
| 				<dt class="inline">{{ 'Duration'|trans }}</dt> | ||||
| 				<dd>{{ entity.duration|date('H:i') }}</dd> | ||||
|  | ||||
|                 {% if chill_aside_activity_config.show_concerned_persons_count == 'visible' %} | ||||
|                     <dt class="inline">{{ 'Concerned persons count'|trans }}</dt> | ||||
|                     <dd>{{ entity.concernedPersonsCount }}</dd> | ||||
|                 {% endif %} | ||||
|  | ||||
| 				<dt class="inline">{{ 'Remark'|trans }}</dt> | ||||
| 				{%- if entity.note is empty -%} | ||||
| 					<dd> | ||||
| @@ -60,6 +55,5 @@ | ||||
| 			</dl> | ||||
|  | ||||
| 		{% endblock %} | ||||
|         {% block content_view_actions_duplicate_link %}{% endblock %} | ||||
| 	{% endembed %} | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -1,49 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\AsideActivityBundle\Tests\Export\Aggregator; | ||||
|  | ||||
| use Chill\AsideActivityBundle\Entity\AsideActivity; | ||||
| use Chill\AsideActivityBundle\Export\Aggregator\ByConcernedPersonsCountAggregator; | ||||
| use Chill\MainBundle\Test\Export\AbstractAggregatorTest; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class ByConcernedPersonsCountAggregatorTest extends AbstractAggregatorTest | ||||
| { | ||||
|     public function getAggregator() | ||||
|     { | ||||
|         return new ByConcernedPersonsCountAggregator(); | ||||
|     } | ||||
|  | ||||
|     public static function getFormData(): array | ||||
|     { | ||||
|         return [ | ||||
|             [], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public static function getQueryBuilders(): iterable | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $em = self::getContainer()->get(EntityManagerInterface::class); | ||||
|  | ||||
|         return [ | ||||
|             $em->createQueryBuilder() | ||||
|                 ->select('count(aside.id)') | ||||
|                 ->from(AsideActivity::class, 'aside'), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -1,50 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\AsideActivityBundle\Tests\Export\Export; | ||||
|  | ||||
| use Chill\AsideActivityBundle\Export\Export\SumConcernedPersonsCountAsideActivity; | ||||
| use Chill\AsideActivityBundle\Repository\AsideActivityRepository; | ||||
| use Chill\MainBundle\Test\Export\AbstractExportTest; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| final class SumConcernedPersonsCountAsideActivityTest extends AbstractExportTest | ||||
| { | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|     } | ||||
|  | ||||
|     public function getExport() | ||||
|     { | ||||
|         $repository = self::getContainer()->get(AsideActivityRepository::class); | ||||
|  | ||||
|         yield new SumConcernedPersonsCountAsideActivity($repository); | ||||
|     } | ||||
|  | ||||
|     public static function getFormData(): array | ||||
|     { | ||||
|         return [ | ||||
|             [], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public static function getModifiersCombination(): array | ||||
|     { | ||||
|         return [ | ||||
|             ['aside_activity'], | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -20,10 +20,6 @@ services: | ||||
|       tags: | ||||
|           - { name: chill.export, alias: 'avg_aside_activity_duration' } | ||||
|  | ||||
|   Chill\AsideActivityBundle\Export\Export\SumConcernedPersonsCountAsideActivity: | ||||
|       tags: | ||||
|           - { name: chill.export, alias: 'sum_aside_activity_concerned_persons_count' } | ||||
|  | ||||
|   ## Filters | ||||
|   chill.aside_activity.export.date_filter: | ||||
|     class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter | ||||
| @@ -74,7 +70,3 @@ services: | ||||
|   Chill\AsideActivityBundle\Export\Aggregator\ByLocationAggregator: | ||||
|       tags: | ||||
|           - { name: chill.export_aggregator, alias: 'aside_activity_location_aggregator' } | ||||
|  | ||||
|   Chill\AsideActivityBundle\Export\Aggregator\ByConcernedPersonsCountAggregator: | ||||
|       tags: | ||||
|           - { name: chill.export_aggregator, alias: 'aside_activity_concerned_persons_count_aggregator' } | ||||
|   | ||||
| @@ -27,7 +27,6 @@ Emergency: Urgent | ||||
| by: "Par " | ||||
| location: Lieu | ||||
| Asideactivity location: Localisation de l'activité | ||||
| Concerned persons count: Nombre d'usager concernés | ||||
|  | ||||
| # Crud | ||||
| crud: | ||||
| @@ -191,7 +190,6 @@ export: | ||||
|     Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères | ||||
|     Average aside activities duration: Durée moyenne des activités annexes | ||||
|     Sum aside activities duration: Durée des activités annexes | ||||
|     Sum concerned persons count for aside activities: Nombre d'usager concernés par les activités annexes | ||||
|     filter: | ||||
|         Filter by aside activity date: Filtrer les activités annexes par date | ||||
|         Filter by aside activity type: Filtrer les activités annexes par type d'activité | ||||
| @@ -212,8 +210,6 @@ export: | ||||
|         'Filtered by aside activity location: only %location%': "Filtré par localisation: uniquement %location%" | ||||
|     aggregator: | ||||
|         Group by aside activity type: Grouper les activités annexes par type d'activité | ||||
|         Group by concerned persons count: Grouper les activités annexes par nombre d'usagers conernés | ||||
|         Concerned persons count: Nombre d'usagers concernés | ||||
|         Aside activity type: Type d'activité annexe | ||||
|         by_user_job: | ||||
|             Aggregate by user job: Grouper les activités annexes par métier des utilisateurs | ||||
|   | ||||
| @@ -1,76 +1,74 @@ | ||||
| import { EventInput } from "@fullcalendar/core"; | ||||
| import { | ||||
|     DateTime, | ||||
|     Location, | ||||
|     User, | ||||
|     UserAssociatedInterface, | ||||
|   DateTime, | ||||
|   Location, | ||||
|   User, | ||||
|   UserAssociatedInterface, | ||||
| } from "../../../ChillMainBundle/Resources/public/types"; | ||||
| import { Person } from "../../../ChillPersonBundle/Resources/public/types"; | ||||
|  | ||||
| export interface CalendarRange { | ||||
|     id: number; | ||||
|     endDate: DateTime; | ||||
|     startDate: DateTime; | ||||
|     user: User; | ||||
|     location: Location; | ||||
|     createdAt: DateTime; | ||||
|     createdBy: User; | ||||
|     updatedAt: DateTime; | ||||
|     updatedBy: User; | ||||
|   id: number; | ||||
|   endDate: DateTime; | ||||
|   startDate: DateTime; | ||||
|   user: User; | ||||
|   location: Location; | ||||
|   createdAt: DateTime; | ||||
|   createdBy: User; | ||||
|   updatedAt: DateTime; | ||||
|   updatedBy: User; | ||||
| } | ||||
|  | ||||
| export interface CalendarRangeCreate { | ||||
|     user: UserAssociatedInterface; | ||||
|     startDate: DateTime; | ||||
|     endDate: DateTime; | ||||
|     location: Location; | ||||
|   user: UserAssociatedInterface; | ||||
|   startDate: DateTime; | ||||
|   endDate: DateTime; | ||||
|   location: Location; | ||||
| } | ||||
|  | ||||
| export interface CalendarRangeEdit { | ||||
|     startDate?: DateTime; | ||||
|     endDate?: DateTime; | ||||
|     location?: Location; | ||||
|   startDate?: DateTime; | ||||
|   endDate?: DateTime; | ||||
|   location?: Location; | ||||
| } | ||||
|  | ||||
| export interface Calendar { | ||||
|     id: number; | ||||
|   id: number; | ||||
| } | ||||
|  | ||||
| export interface CalendarLight { | ||||
|     id: number; | ||||
|     endDate: DateTime; | ||||
|     startDate: DateTime; | ||||
|     mainUser: User; | ||||
|     persons: Person[]; | ||||
|     status: "valid" | "moved" | "canceled"; | ||||
|   id: number; | ||||
|   endDate: DateTime; | ||||
|   startDate: DateTime; | ||||
|   mainUser: User; | ||||
|   persons: Person[]; | ||||
|   status: "valid" | "moved" | "canceled"; | ||||
| } | ||||
|  | ||||
| export interface CalendarRemote { | ||||
|     id: number; | ||||
|     endDate: DateTime; | ||||
|     startDate: DateTime; | ||||
|     title: string; | ||||
|     isAllDay: boolean; | ||||
|   id: number; | ||||
|   endDate: DateTime; | ||||
|   startDate: DateTime; | ||||
|   title: string; | ||||
|   isAllDay: boolean; | ||||
| } | ||||
|  | ||||
| export type EventInputCalendarRange = EventInput & { | ||||
|     id: string; | ||||
|     userId: number; | ||||
|     userLabel: string; | ||||
|     calendarRangeId: number; | ||||
|     locationId: number; | ||||
|     locationName: string; | ||||
|     start: string; | ||||
|     end: string; | ||||
|     is: "range"; | ||||
|   id: string; | ||||
|   userId: number; | ||||
|   userLabel: string; | ||||
|   calendarRangeId: number; | ||||
|   locationId: number; | ||||
|   locationName: string; | ||||
|   start: string; | ||||
|   end: string; | ||||
|   is: "range"; | ||||
| }; | ||||
|  | ||||
| export function isEventInputCalendarRange( | ||||
|     toBeDetermined: EventInputCalendarRange | EventInput, | ||||
|   toBeDetermined: EventInputCalendarRange | EventInput, | ||||
| ): toBeDetermined is EventInputCalendarRange { | ||||
|     return ( | ||||
|         typeof toBeDetermined.is === "string" && toBeDetermined.is === "range" | ||||
|     ); | ||||
|   return typeof toBeDetermined.is === "string" && toBeDetermined.is === "range"; | ||||
| } | ||||
|  | ||||
| export {}; | ||||
|   | ||||
| @@ -1,166 +1,146 @@ | ||||
| <template> | ||||
|     <teleport to="#mainUser"> | ||||
|         <h2 class="chill-red">Utilisateur principal</h2> | ||||
|         <div> | ||||
|             <div> | ||||
|                 <div v-if="null !== this.$store.getters.getMainUser"> | ||||
|                     <calendar-active :user="this.$store.getters.getMainUser" /> | ||||
|                 </div> | ||||
|                 <pick-entity | ||||
|                     :multiple="false" | ||||
|                     :types="['user']" | ||||
|                     :uniqid="'main_user_calendar'" | ||||
|                     :picked=" | ||||
|                         null !== this.$store.getters.getMainUser | ||||
|                             ? [this.$store.getters.getMainUser] | ||||
|                             : [] | ||||
|                     " | ||||
|                     :removable-if-set="false" | ||||
|                     :display-picked="false" | ||||
|                     :suggested="this.suggestedUsers" | ||||
|                     :label="'main_user'" | ||||
|                     @add-new-entity="setMainUser" | ||||
|                 /> | ||||
|             </div> | ||||
|   <teleport to="#mainUser"> | ||||
|     <h2 class="chill-red">Utilisateur principal</h2> | ||||
|     <div> | ||||
|       <div> | ||||
|         <div v-if="null !== this.$store.getters.getMainUser"> | ||||
|           <calendar-active :user="this.$store.getters.getMainUser" /> | ||||
|         </div> | ||||
|     </teleport> | ||||
|         <pick-entity | ||||
|           :multiple="false" | ||||
|           :types="['user']" | ||||
|           :uniqid="'main_user_calendar'" | ||||
|           :picked=" | ||||
|             null !== this.$store.getters.getMainUser | ||||
|               ? [this.$store.getters.getMainUser] | ||||
|               : [] | ||||
|           " | ||||
|           :removable-if-set="false" | ||||
|           :display-picked="false" | ||||
|           :suggested="this.suggestedUsers" | ||||
|           :label="'main_user'" | ||||
|           @add-new-entity="setMainUser" | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|   </teleport> | ||||
|  | ||||
|     <concerned-groups /> | ||||
|   <concerned-groups /> | ||||
|  | ||||
|     <teleport to="#schedule"> | ||||
|         <div class="row mb-3" v-if="activity.startDate !== null"> | ||||
|             <label class="col-form-label col-sm-4">Date</label> | ||||
|             <div class="col-sm-8"> | ||||
|                 {{ $d(activity.startDate, "long") }} - | ||||
|                 {{ $d(activity.endDate, "hoursOnly") }} | ||||
|                 <span v-if="activity.calendarRange === null" | ||||
|                     >(Pas de plage de disponibilité sélectionnée)</span | ||||
|                 > | ||||
|                 <span v-else>(Une plage de disponibilité sélectionnée)</span> | ||||
|             </div> | ||||
|         </div> | ||||
|     </teleport> | ||||
|  | ||||
|     <location /> | ||||
|  | ||||
|     <teleport to="#fullCalendar"> | ||||
|         <div class="calendar-actives"> | ||||
|             <template v-for="u in getActiveUsers" :key="u.id"> | ||||
|                 <calendar-active | ||||
|                     :user="u" | ||||
|                     :invite="this.$store.getters.getInviteForUser(u)" | ||||
|                 /> | ||||
|             </template> | ||||
|         </div> | ||||
|         <div | ||||
|             class="display-options row justify-content-between" | ||||
|             style="margin-top: 1rem" | ||||
|   <teleport to="#schedule"> | ||||
|     <div class="row mb-3" v-if="activity.startDate !== null"> | ||||
|       <label class="col-form-label col-sm-4">Date</label> | ||||
|       <div class="col-sm-8"> | ||||
|         {{ $d(activity.startDate, "long") }} - | ||||
|         {{ $d(activity.endDate, "hoursOnly") }} | ||||
|         <span v-if="activity.calendarRange === null" | ||||
|           >(Pas de plage de disponibilité sélectionnée)</span | ||||
|         > | ||||
|             <div class="col-sm-9 col-xs-12"> | ||||
|                 <div class="input-group mb-3"> | ||||
|                     <label class="input-group-text" for="slotDuration" | ||||
|                         >Durée des créneaux</label | ||||
|                     > | ||||
|                     <select | ||||
|                         v-model="slotDuration" | ||||
|                         id="slotDuration" | ||||
|                         class="form-select" | ||||
|                     > | ||||
|                         <option value="00:05:00">5 minutes</option> | ||||
|                         <option value="00:10:00">10 minutes</option> | ||||
|                         <option value="00:15:00">15 minutes</option> | ||||
|                         <option value="00:30:00">30 minutes</option> | ||||
|                         <option value="00:45:00">45 minutes</option> | ||||
|                         <option value="00:60:00">60 minutes</option> | ||||
|                     </select> | ||||
|                     <label class="input-group-text" for="slotMinTime">De</label> | ||||
|                     <select | ||||
|                         v-model="slotMinTime" | ||||
|                         id="slotMinTime" | ||||
|                         class="form-select" | ||||
|                     > | ||||
|                         <option value="00:00:00">0h</option> | ||||
|                         <option value="01:00:00">1h</option> | ||||
|                         <option value="02:00:00">2h</option> | ||||
|                         <option value="03:00:00">3h</option> | ||||
|                         <option value="04:00:00">4h</option> | ||||
|                         <option value="05:00:00">5h</option> | ||||
|                         <option value="06:00:00">6h</option> | ||||
|                         <option value="07:00:00">7h</option> | ||||
|                         <option value="08:00:00">8h</option> | ||||
|                         <option value="09:00:00">9h</option> | ||||
|                         <option value="10:00:00">10h</option> | ||||
|                         <option value="11:00:00">11h</option> | ||||
|                         <option value="12:00:00">12h</option> | ||||
|                     </select> | ||||
|                     <label class="input-group-text" for="slotMaxTime">À</label> | ||||
|                     <select | ||||
|                         v-model="slotMaxTime" | ||||
|                         id="slotMaxTime" | ||||
|                         class="form-select" | ||||
|                     > | ||||
|                         <option value="12:00:00">12h</option> | ||||
|                         <option value="13:00:00">13h</option> | ||||
|                         <option value="14:00:00">14h</option> | ||||
|                         <option value="15:00:00">15h</option> | ||||
|                         <option value="16:00:00">16h</option> | ||||
|                         <option value="17:00:00">17h</option> | ||||
|                         <option value="18:00:00">18h</option> | ||||
|                         <option value="19:00:00">19h</option> | ||||
|                         <option value="20:00:00">20h</option> | ||||
|                         <option value="21:00:00">21h</option> | ||||
|                         <option value="22:00:00">22h</option> | ||||
|                         <option value="23:00:00">23h</option> | ||||
|                         <option value="23:59:59">24h</option> | ||||
|                     </select> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="col-sm-3 col-xs-12"> | ||||
|                 <div class="float-end"> | ||||
|                     <div class="form-check input-group"> | ||||
|                         <span class="input-group-text"> | ||||
|                             <input | ||||
|                                 id="showHideWE" | ||||
|                                 class="mt-0" | ||||
|                                 type="checkbox" | ||||
|                                 v-model="hideWeekends" | ||||
|                             /> | ||||
|                         </span> | ||||
|                         <label | ||||
|                             for="showHideWE" | ||||
|                             class="form-check-label input-group-text" | ||||
|                             >Week-ends</label | ||||
|                         > | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         <span v-else>(Une plage de disponibilité sélectionnée)</span> | ||||
|       </div> | ||||
|     </div> | ||||
|   </teleport> | ||||
|  | ||||
|   <location /> | ||||
|  | ||||
|   <teleport to="#fullCalendar"> | ||||
|     <div class="calendar-actives"> | ||||
|       <template v-for="u in getActiveUsers" :key="u.id"> | ||||
|         <calendar-active | ||||
|           :user="u" | ||||
|           :invite="this.$store.getters.getInviteForUser(u)" | ||||
|         /> | ||||
|       </template> | ||||
|     </div> | ||||
|     <div | ||||
|       class="display-options row justify-content-between" | ||||
|       style="margin-top: 1rem" | ||||
|     > | ||||
|       <div class="col-sm-9 col-xs-12"> | ||||
|         <div class="input-group mb-3"> | ||||
|           <label class="input-group-text" for="slotDuration" | ||||
|             >Durée des créneaux</label | ||||
|           > | ||||
|           <select v-model="slotDuration" id="slotDuration" class="form-select"> | ||||
|             <option value="00:05:00">5 minutes</option> | ||||
|             <option value="00:10:00">10 minutes</option> | ||||
|             <option value="00:15:00">15 minutes</option> | ||||
|             <option value="00:30:00">30 minutes</option> | ||||
|           </select> | ||||
|           <label class="input-group-text" for="slotMinTime">De</label> | ||||
|           <select v-model="slotMinTime" id="slotMinTime" class="form-select"> | ||||
|             <option value="00:00:00">0h</option> | ||||
|             <option value="01:00:00">1h</option> | ||||
|             <option value="02:00:00">2h</option> | ||||
|             <option value="03:00:00">3h</option> | ||||
|             <option value="04:00:00">4h</option> | ||||
|             <option value="05:00:00">5h</option> | ||||
|             <option value="06:00:00">6h</option> | ||||
|             <option value="07:00:00">7h</option> | ||||
|             <option value="08:00:00">8h</option> | ||||
|             <option value="09:00:00">9h</option> | ||||
|             <option value="10:00:00">10h</option> | ||||
|             <option value="11:00:00">11h</option> | ||||
|             <option value="12:00:00">12h</option> | ||||
|           </select> | ||||
|           <label class="input-group-text" for="slotMaxTime">À</label> | ||||
|           <select v-model="slotMaxTime" id="slotMaxTime" class="form-select"> | ||||
|             <option value="12:00:00">12h</option> | ||||
|             <option value="13:00:00">13h</option> | ||||
|             <option value="14:00:00">14h</option> | ||||
|             <option value="15:00:00">15h</option> | ||||
|             <option value="16:00:00">16h</option> | ||||
|             <option value="17:00:00">17h</option> | ||||
|             <option value="18:00:00">18h</option> | ||||
|             <option value="19:00:00">19h</option> | ||||
|             <option value="20:00:00">20h</option> | ||||
|             <option value="21:00:00">21h</option> | ||||
|             <option value="22:00:00">22h</option> | ||||
|             <option value="23:00:00">23h</option> | ||||
|             <option value="23:59:59">24h</option> | ||||
|           </select> | ||||
|         </div> | ||||
|         <FullCalendar ref="fullCalendar" :options="calendarOptions"> | ||||
|             <template #eventContent="arg"> | ||||
|                 <span> | ||||
|                     <b v-if="arg.event.extendedProps.is === 'remote'">{{ | ||||
|                         arg.event.title | ||||
|                     }}</b> | ||||
|                     <b v-else-if="arg.event.extendedProps.is === 'range'" | ||||
|                         >{{ arg.timeText }} | ||||
|                         {{ arg.event.extendedProps.locationName }} | ||||
|                         <small>{{ | ||||
|                             arg.event.extendedProps.userLabel | ||||
|                         }}</small></b | ||||
|                     > | ||||
|                     <b v-else-if="arg.event.extendedProps.is === 'current'" | ||||
|                         >{{ arg.timeText }} {{ $t("current_selected") }} | ||||
|                     </b> | ||||
|                     <b v-else-if="arg.event.extendedProps.is === 'local'">{{ | ||||
|                         arg.event.title | ||||
|                     }}</b> | ||||
|                     <b v-else | ||||
|                         >{{ arg.timeText }} {{ $t("current_selected") }} | ||||
|                     </b> | ||||
|                 </span> | ||||
|             </template> | ||||
|         </FullCalendar> | ||||
|     </teleport> | ||||
|       </div> | ||||
|       <div class="col-sm-3 col-xs-12"> | ||||
|         <div class="float-end"> | ||||
|           <div class="form-check input-group"> | ||||
|             <span class="input-group-text"> | ||||
|               <input | ||||
|                 id="showHideWE" | ||||
|                 class="mt-0" | ||||
|                 type="checkbox" | ||||
|                 v-model="hideWeekends" | ||||
|               /> | ||||
|             </span> | ||||
|             <label for="showHideWE" class="form-check-label input-group-text" | ||||
|               >Week-ends</label | ||||
|             > | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <FullCalendar ref="fullCalendar" :options="calendarOptions"> | ||||
|       <template #eventContent="arg"> | ||||
|         <span> | ||||
|           <b v-if="arg.event.extendedProps.is === 'remote'">{{ | ||||
|             arg.event.title | ||||
|           }}</b> | ||||
|           <b v-else-if="arg.event.extendedProps.is === 'range'" | ||||
|             >{{ arg.timeText }} | ||||
|             {{ arg.event.extendedProps.locationName }} | ||||
|             <small>{{ arg.event.extendedProps.userLabel }}</small></b | ||||
|           > | ||||
|           <b v-else-if="arg.event.extendedProps.is === 'current'" | ||||
|             >{{ arg.timeText }} {{ $t("current_selected") }} | ||||
|           </b> | ||||
|           <b v-else-if="arg.event.extendedProps.is === 'local'">{{ | ||||
|             arg.event.title | ||||
|           }}</b> | ||||
|           <b v-else>{{ arg.timeText }} {{ $t("current_selected") }} </b> | ||||
|         </span> | ||||
|       </template> | ||||
|     </FullCalendar> | ||||
|   </teleport> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| @@ -177,219 +157,210 @@ import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue"; | ||||
| import { mapGetters, mapState } from "vuex"; | ||||
|  | ||||
| export default { | ||||
|     name: "App", | ||||
|     components: { | ||||
|         ConcernedGroups, | ||||
|         Location, | ||||
|         FullCalendar, | ||||
|         CalendarActive, | ||||
|         PickEntity, | ||||
|   name: "App", | ||||
|   components: { | ||||
|     ConcernedGroups, | ||||
|     Location, | ||||
|     FullCalendar, | ||||
|     CalendarActive, | ||||
|     PickEntity, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       errorMsg: [], | ||||
|       showMyCalendar: false, | ||||
|       slotDuration: "00:05:00", | ||||
|       slotMinTime: "09:00:00", | ||||
|       slotMaxTime: "18:00:00", | ||||
|       hideWeekEnds: true, | ||||
|       previousUser: [], | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters(["getMainUser"]), | ||||
|     ...mapState(["activity"]), | ||||
|     events() { | ||||
|       return this.$store.getters.getEventSources; | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             errorMsg: [], | ||||
|             showMyCalendar: false, | ||||
|             slotDuration: "00:05:00", | ||||
|             slotMinTime: "09:00:00", | ||||
|             slotMaxTime: "18:00:00", | ||||
|             hideWeekEnds: true, | ||||
|             previousUser: [], | ||||
|         }; | ||||
|     calendarOptions() { | ||||
|       return { | ||||
|         locale: frLocale, | ||||
|         plugins: [ | ||||
|           dayGridPlugin, | ||||
|           interactionPlugin, | ||||
|           timeGridPlugin, | ||||
|           dayGridPlugin, | ||||
|           listPlugin, | ||||
|         ], | ||||
|         initialView: "timeGridWeek", | ||||
|         initialDate: this.$store.getters.getInitialDate, | ||||
|         eventSources: this.events, | ||||
|         selectable: true, | ||||
|         slotMinTime: this.slotMinTime, | ||||
|         slotMaxTime: this.slotMaxTime, | ||||
|         scrollTimeReset: false, | ||||
|         datesSet: this.onDatesSet, | ||||
|         select: this.onDateSelect, | ||||
|         eventChange: this.onEventChange, | ||||
|         eventClick: this.onEventClick, | ||||
|         selectMirror: true, | ||||
|         editable: true, | ||||
|         weekends: !this.hideWeekEnds, | ||||
|         headerToolbar: { | ||||
|           left: "prev,next today", | ||||
|           center: "title", | ||||
|           right: "timeGridWeek,timeGridDay,listWeek", | ||||
|         }, | ||||
|         views: { | ||||
|           timeGrid: { | ||||
|             slotEventOverlap: false, | ||||
|             slotDuration: this.slotDuration, | ||||
|           }, | ||||
|         }, | ||||
|       }; | ||||
|     }, | ||||
|     computed: { | ||||
|         ...mapGetters(["getMainUser"]), | ||||
|         ...mapState(["activity"]), | ||||
|         events() { | ||||
|             return this.$store.getters.getEventSources; | ||||
|         }, | ||||
|         calendarOptions() { | ||||
|             return { | ||||
|                 locale: frLocale, | ||||
|                 plugins: [ | ||||
|                     dayGridPlugin, | ||||
|                     interactionPlugin, | ||||
|                     timeGridPlugin, | ||||
|                     dayGridPlugin, | ||||
|                     listPlugin, | ||||
|                 ], | ||||
|                 initialView: "timeGridWeek", | ||||
|                 initialDate: this.$store.getters.getInitialDate, | ||||
|                 eventSources: this.events, | ||||
|                 selectable: true, | ||||
|                 slotMinTime: this.slotMinTime, | ||||
|                 slotMaxTime: this.slotMaxTime, | ||||
|                 scrollTimeReset: false, | ||||
|                 datesSet: this.onDatesSet, | ||||
|                 select: this.onDateSelect, | ||||
|                 eventChange: this.onEventChange, | ||||
|                 eventClick: this.onEventClick, | ||||
|                 selectMirror: true, | ||||
|                 editable: true, | ||||
|                 weekends: !this.hideWeekEnds, | ||||
|                 headerToolbar: { | ||||
|                     left: "prev,next today", | ||||
|                     center: "title", | ||||
|                     right: "timeGridWeek,timeGridDay,listWeek", | ||||
|                 }, | ||||
|                 views: { | ||||
|                     timeGrid: { | ||||
|                         slotEventOverlap: false, | ||||
|                         slotDuration: this.slotDuration, | ||||
|                     }, | ||||
|                 }, | ||||
|             }; | ||||
|         }, | ||||
|         getActiveUsers() { | ||||
|             const users = []; | ||||
|             for (const id of this.$store.state.currentView.users.keys()) { | ||||
|                 users.push(this.$store.getters.getUserDataById(id).user); | ||||
|             } | ||||
|             return users; | ||||
|         }, | ||||
|         suggestedUsers() { | ||||
|             const suggested = []; | ||||
|  | ||||
|             this.$data.previousUser.forEach((u) => { | ||||
|                 if (u.id !== this.$store.getters.getMainUser.id) { | ||||
|                     suggested.push(u); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             return suggested; | ||||
|         }, | ||||
|     getActiveUsers() { | ||||
|       const users = []; | ||||
|       for (const id of this.$store.state.currentView.users.keys()) { | ||||
|         users.push(this.$store.getters.getUserDataById(id).user); | ||||
|       } | ||||
|       return users; | ||||
|     }, | ||||
|     methods: { | ||||
|         setMainUser({ entity }) { | ||||
|             const user = entity; | ||||
|             console.log("setMainUser APP", entity); | ||||
|     suggestedUsers() { | ||||
|       const suggested = []; | ||||
|  | ||||
|             if ( | ||||
|                 user.id !== this.$store.getters.getMainUser && | ||||
|                 (this.$store.state.activity.calendarRange !== null || | ||||
|                     this.$store.state.activity.startDate !== null || | ||||
|                     this.$store.state.activity.endDate !== null) | ||||
|             ) { | ||||
|                 if ( | ||||
|                     !window.confirm( | ||||
|                         this.$t("change_main_user_will_reset_event_data"), | ||||
|                     ) | ||||
|                 ) { | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|       this.$data.previousUser.forEach((u) => { | ||||
|         if (u.id !== this.$store.getters.getMainUser.id) { | ||||
|           suggested.push(u); | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|             // add the previous user, if any, in the previous user list (in use for suggestion) | ||||
|             if (null !== this.$store.getters.getMainUser) { | ||||
|                 const suggestedUids = new Set( | ||||
|                     this.$data.previousUser.map((u) => u.id), | ||||
|                 ); | ||||
|                 if (!suggestedUids.has(this.$store.getters.getMainUser.id)) { | ||||
|                     this.$data.previousUser.push( | ||||
|                         this.$store.getters.getMainUser, | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.$store.dispatch("setMainUser", user); | ||||
|             this.$store.commit("showUserOnCalendar", { | ||||
|                 user, | ||||
|                 ranges: true, | ||||
|                 remotes: true, | ||||
|             }); | ||||
|         }, | ||||
|         removeMainUser(user) { | ||||
|             console.log("removeMainUser APP", user); | ||||
|  | ||||
|             window.alert(this.$t("main_user_is_mandatory")); | ||||
|             return; | ||||
|         }, | ||||
|         onDatesSet(event) { | ||||
|             console.log("onDatesSet", event); | ||||
|             this.$store.dispatch("setCurrentDatesView", { | ||||
|                 start: event.start, | ||||
|                 end: event.end, | ||||
|             }); | ||||
|         }, | ||||
|         onDateSelect(payload) { | ||||
|             console.log("onDateSelect", payload); | ||||
|  | ||||
|             // show an alert if changing mainUser | ||||
|             if ( | ||||
|                 (this.$store.getters.getMainUser !== null && | ||||
|                     this.$store.state.me.id !== | ||||
|                         this.$store.getters.getMainUser.id) || | ||||
|                 this.$store.getters.getMainUser === null | ||||
|             ) { | ||||
|                 if (!window.confirm(this.$t("will_change_main_user_for_me"))) { | ||||
|                     return; | ||||
|                 } else { | ||||
|                     this.$store.commit("showUserOnCalendar", { | ||||
|                         user: this.$store.state.me, | ||||
|                         remotes: true, | ||||
|                         ranges: true, | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.$store.dispatch("setEventTimes", { | ||||
|                 start: payload.start, | ||||
|                 end: payload.end, | ||||
|             }); | ||||
|         }, | ||||
|         onEventChange(payload) { | ||||
|             console.log("onEventChange", payload); | ||||
|             if (this.$store.state.activity.calendarRange !== null) { | ||||
|                 throw new Error( | ||||
|                     "not allowed to edit a calendar associated with a calendar range", | ||||
|                 ); | ||||
|             } | ||||
|             this.$store.dispatch("setEventTimes", { | ||||
|                 start: payload.event.start, | ||||
|                 end: payload.event.end, | ||||
|             }); | ||||
|         }, | ||||
|         onEventClick(payload) { | ||||
|             if (payload.event.extendedProps.is !== "range") { | ||||
|                 // do nothing when clicking on remote | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // show an alert if changing mainUser | ||||
|             if ( | ||||
|                 this.$store.getters.getMainUser !== null && | ||||
|                 payload.event.extendedProps.userId !== | ||||
|                     this.$store.getters.getMainUser.id | ||||
|             ) { | ||||
|                 if ( | ||||
|                     !window.confirm( | ||||
|                         this.$t("this_calendar_range_will_change_main_user"), | ||||
|                     ) | ||||
|                 ) { | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.$store.dispatch("associateCalendarToRange", { | ||||
|                 range: payload.event, | ||||
|             }); | ||||
|         }, | ||||
|       return suggested; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     setMainUser({ entity }) { | ||||
|       const user = entity; | ||||
|       console.log("setMainUser APP", entity); | ||||
|  | ||||
|       if ( | ||||
|         user.id !== this.$store.getters.getMainUser && | ||||
|         (this.$store.state.activity.calendarRange !== null || | ||||
|           this.$store.state.activity.startDate !== null || | ||||
|           this.$store.state.activity.endDate !== null) | ||||
|       ) { | ||||
|         if ( | ||||
|           !window.confirm(this.$t("change_main_user_will_reset_event_data")) | ||||
|         ) { | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // add the previous user, if any, in the previous user list (in use for suggestion) | ||||
|       if (null !== this.$store.getters.getMainUser) { | ||||
|         const suggestedUids = new Set(this.$data.previousUser.map((u) => u.id)); | ||||
|         if (!suggestedUids.has(this.$store.getters.getMainUser.id)) { | ||||
|           this.$data.previousUser.push(this.$store.getters.getMainUser); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       this.$store.dispatch("setMainUser", user); | ||||
|       this.$store.commit("showUserOnCalendar", { | ||||
|         user, | ||||
|         ranges: true, | ||||
|         remotes: true, | ||||
|       }); | ||||
|     }, | ||||
|     removeMainUser(user) { | ||||
|       console.log("removeMainUser APP", user); | ||||
|  | ||||
|       window.alert(this.$t("main_user_is_mandatory")); | ||||
|       return; | ||||
|     }, | ||||
|     onDatesSet(event) { | ||||
|       console.log("onDatesSet", event); | ||||
|       this.$store.dispatch("setCurrentDatesView", { | ||||
|         start: event.start, | ||||
|         end: event.end, | ||||
|       }); | ||||
|     }, | ||||
|     onDateSelect(payload) { | ||||
|       console.log("onDateSelect", payload); | ||||
|  | ||||
|       // show an alert if changing mainUser | ||||
|       if ( | ||||
|         (this.$store.getters.getMainUser !== null && | ||||
|           this.$store.state.me.id !== this.$store.getters.getMainUser.id) || | ||||
|         this.$store.getters.getMainUser === null | ||||
|       ) { | ||||
|         if (!window.confirm(this.$t("will_change_main_user_for_me"))) { | ||||
|           return; | ||||
|         } else { | ||||
|           this.$store.commit("showUserOnCalendar", { | ||||
|             user: this.$store.state.me, | ||||
|             remotes: true, | ||||
|             ranges: true, | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       this.$store.dispatch("setEventTimes", { | ||||
|         start: payload.start, | ||||
|         end: payload.end, | ||||
|       }); | ||||
|     }, | ||||
|     onEventChange(payload) { | ||||
|       console.log("onEventChange", payload); | ||||
|       if (this.$store.state.activity.calendarRange !== null) { | ||||
|         throw new Error( | ||||
|           "not allowed to edit a calendar associated with a calendar range", | ||||
|         ); | ||||
|       } | ||||
|       this.$store.dispatch("setEventTimes", { | ||||
|         start: payload.event.start, | ||||
|         end: payload.event.end, | ||||
|       }); | ||||
|     }, | ||||
|     onEventClick(payload) { | ||||
|       if (payload.event.extendedProps.is !== "range") { | ||||
|         // do nothing when clicking on remote | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // show an alert if changing mainUser | ||||
|       if ( | ||||
|         this.$store.getters.getMainUser !== null && | ||||
|         payload.event.extendedProps.userId !== | ||||
|           this.$store.getters.getMainUser.id | ||||
|       ) { | ||||
|         if ( | ||||
|           !window.confirm(this.$t("this_calendar_range_will_change_main_user")) | ||||
|         ) { | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       this.$store.dispatch("associateCalendarToRange", { | ||||
|         range: payload.event, | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| .calendar-actives { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     flex-wrap: wrap; | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   flex-wrap: wrap; | ||||
| } | ||||
|  | ||||
| .display-options { | ||||
|     margin-top: 1rem; | ||||
|   margin-top: 1rem; | ||||
| } | ||||
|  | ||||
| /* for events which are range */ | ||||
| .fc-event.isrange { | ||||
|     border-width: 3px; | ||||
|   border-width: 3px; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,119 +1,105 @@ | ||||
| <template> | ||||
|     <div :style="style" class="calendar-active"> | ||||
|         <span class="badge-user"> | ||||
|             {{ user.text }} | ||||
|             <template v-if="invite !== null"> | ||||
|                 <i v-if="invite.status === 'accepted'" class="fa fa-check" /> | ||||
|                 <i | ||||
|                     v-else-if="invite.status === 'declined'" | ||||
|                     class="fa fa-times" | ||||
|                 /> | ||||
|                 <i | ||||
|                     v-else-if="invite.status === 'pending'" | ||||
|                     class="fa fa-question-o" | ||||
|                 /> | ||||
|                 <i | ||||
|                     v-else-if="invite.status === 'tentative'" | ||||
|                     class="fa fa-question" | ||||
|                 /> | ||||
|                 <span v-else="">{{ invite.status }}</span> | ||||
|             </template> | ||||
|         </span> | ||||
|         <span class="form-check-inline form-switch"> | ||||
|             <input | ||||
|                 class="form-check-input" | ||||
|                 type="checkbox" | ||||
|                 id="flexSwitchCheckDefault" | ||||
|                 v-model="rangeShow" | ||||
|             /> | ||||
|              <label | ||||
|                 class="form-check-label" | ||||
|                 for="flexSwitchCheckDefault" | ||||
|                 title="Disponibilités" | ||||
|                 ><i class="fa fa-calendar-check-o" | ||||
|             /></label> | ||||
|         </span> | ||||
|         <span class="form-check-inline form-switch"> | ||||
|             <input | ||||
|                 class="form-check-input" | ||||
|                 type="checkbox" | ||||
|                 id="flexSwitchCheckDefault" | ||||
|                 v-model="remoteShow" | ||||
|             /> | ||||
|              <label | ||||
|                 class="form-check-label" | ||||
|                 for="flexSwitchCheckDefault" | ||||
|                 title="Agenda" | ||||
|                 ><i class="fa fa-calendar" | ||||
|             /></label> | ||||
|         </span> | ||||
|     </div> | ||||
|   <div :style="style" class="calendar-active"> | ||||
|     <span class="badge-user"> | ||||
|       {{ user.text }} | ||||
|       <template v-if="invite !== null"> | ||||
|         <i v-if="invite.status === 'accepted'" class="fa fa-check" /> | ||||
|         <i v-else-if="invite.status === 'declined'" class="fa fa-times" /> | ||||
|         <i v-else-if="invite.status === 'pending'" class="fa fa-question-o" /> | ||||
|         <i v-else-if="invite.status === 'tentative'" class="fa fa-question" /> | ||||
|         <span v-else="">{{ invite.status }}</span> | ||||
|       </template> | ||||
|     </span> | ||||
|     <span class="form-check-inline form-switch"> | ||||
|       <input | ||||
|         class="form-check-input" | ||||
|         type="checkbox" | ||||
|         id="flexSwitchCheckDefault" | ||||
|         v-model="rangeShow" | ||||
|       /> | ||||
|        <label | ||||
|         class="form-check-label" | ||||
|         for="flexSwitchCheckDefault" | ||||
|         title="Disponibilités" | ||||
|         ><i class="fa fa-calendar-check-o" | ||||
|       /></label> | ||||
|     </span> | ||||
|     <span class="form-check-inline form-switch"> | ||||
|       <input | ||||
|         class="form-check-input" | ||||
|         type="checkbox" | ||||
|         id="flexSwitchCheckDefault" | ||||
|         v-model="remoteShow" | ||||
|       /> | ||||
|        <label | ||||
|         class="form-check-label" | ||||
|         for="flexSwitchCheckDefault" | ||||
|         title="Agenda" | ||||
|         ><i class="fa fa-calendar" | ||||
|       /></label> | ||||
|     </span> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from "vuex"; | ||||
|  | ||||
| export default { | ||||
|     name: "CalendarActive", | ||||
|     props: { | ||||
|         user: { | ||||
|             type: Object, | ||||
|             required: true, | ||||
|         }, | ||||
|         invite: { | ||||
|             type: Object, | ||||
|             required: false, | ||||
|             default: null, | ||||
|         }, | ||||
|   name: "CalendarActive", | ||||
|   props: { | ||||
|     user: { | ||||
|       type: Object, | ||||
|       required: true, | ||||
|     }, | ||||
|     computed: { | ||||
|         style() { | ||||
|             return { | ||||
|                 backgroundColor: this.$store.getters.getUserData(this.user) | ||||
|                     .mainColor, | ||||
|             }; | ||||
|         }, | ||||
|         rangeShow: { | ||||
|             set(value) { | ||||
|                 this.$store.commit("showUserOnCalendar", { | ||||
|                     user: this.user, | ||||
|                     ranges: value, | ||||
|                 }); | ||||
|             }, | ||||
|             get() { | ||||
|                 return this.$store.getters.isRangeShownOnCalendarForUser( | ||||
|                     this.user, | ||||
|                 ); | ||||
|             }, | ||||
|         }, | ||||
|         remoteShow: { | ||||
|             set(value) { | ||||
|                 this.$store.commit("showUserOnCalendar", { | ||||
|                     user: this.user, | ||||
|                     remotes: value, | ||||
|                 }); | ||||
|             }, | ||||
|             get() { | ||||
|                 return this.$store.getters.isRemoteShownOnCalendarForUser( | ||||
|                     this.user, | ||||
|                 ); | ||||
|             }, | ||||
|         }, | ||||
|     invite: { | ||||
|       type: Object, | ||||
|       required: false, | ||||
|       default: null, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     style() { | ||||
|       return { | ||||
|         backgroundColor: this.$store.getters.getUserData(this.user).mainColor, | ||||
|       }; | ||||
|     }, | ||||
|     rangeShow: { | ||||
|       set(value) { | ||||
|         this.$store.commit("showUserOnCalendar", { | ||||
|           user: this.user, | ||||
|           ranges: value, | ||||
|         }); | ||||
|       }, | ||||
|       get() { | ||||
|         return this.$store.getters.isRangeShownOnCalendarForUser(this.user); | ||||
|       }, | ||||
|     }, | ||||
|     remoteShow: { | ||||
|       set(value) { | ||||
|         this.$store.commit("showUserOnCalendar", { | ||||
|           user: this.user, | ||||
|           remotes: value, | ||||
|         }); | ||||
|       }, | ||||
|       get() { | ||||
|         return this.$store.getters.isRemoteShownOnCalendarForUser(this.user); | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| .calendar-active { | ||||
|     margin: 0 0.25rem 0.25rem 0; | ||||
|     padding: 0.5rem; | ||||
|   margin: 0 0.25rem 0.25rem 0; | ||||
|   padding: 0.5rem; | ||||
|  | ||||
|     border-radius: 0.5rem; | ||||
|   border-radius: 0.5rem; | ||||
|  | ||||
|     color: var(--bs-blue); | ||||
|   color: var(--bs-blue); | ||||
|  | ||||
|     & > .badge-user { | ||||
|         margin-right: 0.5rem; | ||||
|     } | ||||
|   & > .badge-user { | ||||
|     margin-right: 0.5rem; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -14,37 +14,37 @@ export { whoami } from "../../../../../ChillMainBundle/Resources/public/lib/api/ | ||||
|  * @return Promise | ||||
|  */ | ||||
| export const fetchCalendarRangeForUser = ( | ||||
|     user: User, | ||||
|     start: Date, | ||||
|     end: Date, | ||||
|   user: User, | ||||
|   start: Date, | ||||
|   end: Date, | ||||
| ): Promise<CalendarRange[]> => { | ||||
|     const uri = `/api/1.0/calendar/calendar-range-available/${user.id}.json`; | ||||
|     const dateFrom = datetimeToISO(start); | ||||
|     const dateTo = datetimeToISO(end); | ||||
|   const uri = `/api/1.0/calendar/calendar-range-available/${user.id}.json`; | ||||
|   const dateFrom = datetimeToISO(start); | ||||
|   const dateTo = datetimeToISO(end); | ||||
|  | ||||
|     return fetchResults<CalendarRange>(uri, { dateFrom, dateTo }); | ||||
|   return fetchResults<CalendarRange>(uri, { dateFrom, dateTo }); | ||||
| }; | ||||
|  | ||||
| export const fetchCalendarRemoteForUser = ( | ||||
|     user: User, | ||||
|     start: Date, | ||||
|     end: Date, | ||||
|   user: User, | ||||
|   start: Date, | ||||
|   end: Date, | ||||
| ): Promise<CalendarRemote[]> => { | ||||
|     const uri = `/api/1.0/calendar/proxy/calendar/by-user/${user.id}/events`; | ||||
|     const dateFrom = datetimeToISO(start); | ||||
|     const dateTo = datetimeToISO(end); | ||||
|   const uri = `/api/1.0/calendar/proxy/calendar/by-user/${user.id}/events`; | ||||
|   const dateFrom = datetimeToISO(start); | ||||
|   const dateTo = datetimeToISO(end); | ||||
|  | ||||
|     return fetchResults<CalendarRemote>(uri, { dateFrom, dateTo }); | ||||
|   return fetchResults<CalendarRemote>(uri, { dateFrom, dateTo }); | ||||
| }; | ||||
|  | ||||
| export const fetchCalendarLocalForUser = ( | ||||
|     user: User, | ||||
|     start: Date, | ||||
|     end: Date, | ||||
|   user: User, | ||||
|   start: Date, | ||||
|   end: Date, | ||||
| ): Promise<CalendarLight[]> => { | ||||
|     const uri = `/api/1.0/calendar/calendar/by-user/${user.id}.json`; | ||||
|     const dateFrom = datetimeToISO(start); | ||||
|     const dateTo = datetimeToISO(end); | ||||
|   const uri = `/api/1.0/calendar/calendar/by-user/${user.id}.json`; | ||||
|   const dateFrom = datetimeToISO(start); | ||||
|   const dateTo = datetimeToISO(end); | ||||
|  | ||||
|     return fetchResults<CalendarLight>(uri, { dateFrom, dateTo }); | ||||
|   return fetchResults<CalendarLight>(uri, { dateFrom, dateTo }); | ||||
| }; | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| const COLORS = [ | ||||
|     /* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */ | ||||
|     "#8dd3c7", | ||||
|     "#ffffb3", | ||||
|     "#bebada", | ||||
|     "#fb8072", | ||||
|     "#80b1d3", | ||||
|     "#fdb462", | ||||
|     "#b3de69", | ||||
|     "#fccde5", | ||||
|     "#d9d9d9", | ||||
|     "#bc80bd", | ||||
|     "#ccebc5", | ||||
|     "#ffed6f", | ||||
|   /* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */ | ||||
|   "#8dd3c7", | ||||
|   "#ffffb3", | ||||
|   "#bebada", | ||||
|   "#fb8072", | ||||
|   "#80b1d3", | ||||
|   "#fdb462", | ||||
|   "#b3de69", | ||||
|   "#fccde5", | ||||
|   "#d9d9d9", | ||||
|   "#bc80bd", | ||||
|   "#ccebc5", | ||||
|   "#ffed6f", | ||||
| ]; | ||||
|  | ||||
| export { COLORS }; | ||||
|   | ||||
| @@ -1,117 +1,117 @@ | ||||
| import { COLORS } from "../const"; | ||||
| import { ISOToDatetime } from "../../../../../../ChillMainBundle/Resources/public/chill/js/date"; | ||||
| import { | ||||
|     DateTime, | ||||
|     User, | ||||
|   DateTime, | ||||
|   User, | ||||
| } from "../../../../../../ChillMainBundle/Resources/public/types"; | ||||
| import { CalendarLight, CalendarRange, CalendarRemote } from "../../../types"; | ||||
| import type { EventInputCalendarRange } from "../../../types"; | ||||
| import { EventInput } from "@fullcalendar/core"; | ||||
|  | ||||
| export interface UserData { | ||||
|     user: User; | ||||
|     calendarRanges: CalendarRange[]; | ||||
|     calendarRangesLoaded: {}[]; | ||||
|     remotes: CalendarRemote[]; | ||||
|     remotesLoaded: {}[]; | ||||
|     locals: CalendarRemote[]; | ||||
|     localsLoaded: {}[]; | ||||
|     mainColor: string; | ||||
|   user: User; | ||||
|   calendarRanges: CalendarRange[]; | ||||
|   calendarRangesLoaded: {}[]; | ||||
|   remotes: CalendarRemote[]; | ||||
|   remotesLoaded: {}[]; | ||||
|   locals: CalendarRemote[]; | ||||
|   localsLoaded: {}[]; | ||||
|   mainColor: string; | ||||
| } | ||||
|  | ||||
| export const addIdToValue = (string: string, id: number): string => { | ||||
|     const array = string ? string.split(",") : []; | ||||
|     array.push(id.toString()); | ||||
|     const str = array.join(); | ||||
|     return str; | ||||
|   const array = string ? string.split(",") : []; | ||||
|   array.push(id.toString()); | ||||
|   const str = array.join(); | ||||
|   return str; | ||||
| }; | ||||
|  | ||||
| export const removeIdFromValue = (string: string, id: number) => { | ||||
|     let array = string.split(","); | ||||
|     array = array.filter((el) => el !== id.toString()); | ||||
|     const str = array.join(); | ||||
|     return str; | ||||
|   let array = string.split(","); | ||||
|   array = array.filter((el) => el !== id.toString()); | ||||
|   const str = array.join(); | ||||
|   return str; | ||||
| }; | ||||
|  | ||||
| /* | ||||
|  * Assign missing keys for the ConcernedGroups component | ||||
|  */ | ||||
| export const mapEntity = (entity: EventInput): EventInput => { | ||||
|     const calendar = { ...entity }; | ||||
|     Object.assign(calendar, { thirdParties: entity.professionals }); | ||||
|   const calendar = { ...entity }; | ||||
|   Object.assign(calendar, { thirdParties: entity.professionals }); | ||||
|  | ||||
|     if (entity.startDate !== null) { | ||||
|         calendar.startDate = ISOToDatetime(entity.startDate.datetime); | ||||
|     } | ||||
|     if (entity.endDate !== null) { | ||||
|         calendar.endDate = ISOToDatetime(entity.endDate.datetime); | ||||
|     } | ||||
|   if (entity.startDate !== null) { | ||||
|     calendar.startDate = ISOToDatetime(entity.startDate.datetime); | ||||
|   } | ||||
|   if (entity.endDate !== null) { | ||||
|     calendar.endDate = ISOToDatetime(entity.endDate.datetime); | ||||
|   } | ||||
|  | ||||
|     if (entity.calendarRange !== null) { | ||||
|         calendar.calendarRange.calendarRangeId = entity.calendarRange.id; | ||||
|         calendar.calendarRange.id = `range_${entity.calendarRange.id}`; | ||||
|     } | ||||
|   if (entity.calendarRange !== null) { | ||||
|     calendar.calendarRange.calendarRangeId = entity.calendarRange.id; | ||||
|     calendar.calendarRange.id = `range_${entity.calendarRange.id}`; | ||||
|   } | ||||
|  | ||||
|     return calendar; | ||||
|   return calendar; | ||||
| }; | ||||
|  | ||||
| export const createUserData = (user: User, colorIndex: number): UserData => { | ||||
|     const colorId = colorIndex % COLORS.length; | ||||
|   const colorId = colorIndex % COLORS.length; | ||||
|  | ||||
|     return { | ||||
|         user: user, | ||||
|         calendarRanges: [], | ||||
|         calendarRangesLoaded: [], | ||||
|         remotes: [], | ||||
|         remotesLoaded: [], | ||||
|         locals: [], | ||||
|         localsLoaded: [], | ||||
|         mainColor: COLORS[colorId], | ||||
|     }; | ||||
|   return { | ||||
|     user: user, | ||||
|     calendarRanges: [], | ||||
|     calendarRangesLoaded: [], | ||||
|     remotes: [], | ||||
|     remotesLoaded: [], | ||||
|     locals: [], | ||||
|     localsLoaded: [], | ||||
|     mainColor: COLORS[colorId], | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| // TODO move this function to a more global namespace, as it is also in use in MyCalendarRange app | ||||
| export const calendarRangeToFullCalendarEvent = ( | ||||
|     entity: CalendarRange, | ||||
|   entity: CalendarRange, | ||||
| ): EventInputCalendarRange => { | ||||
|     return { | ||||
|         id: `range_${entity.id}`, | ||||
|         title: "(" + entity.user.text + ")", | ||||
|         start: entity.startDate.datetime8601, | ||||
|         end: entity.endDate.datetime8601, | ||||
|         allDay: false, | ||||
|         userId: entity.user.id, | ||||
|         userLabel: entity.user.label, | ||||
|         calendarRangeId: entity.id, | ||||
|         locationId: entity.location.id, | ||||
|         locationName: entity.location.name, | ||||
|         is: "range", | ||||
|     }; | ||||
|   return { | ||||
|     id: `range_${entity.id}`, | ||||
|     title: "(" + entity.user.text + ")", | ||||
|     start: entity.startDate.datetime8601, | ||||
|     end: entity.endDate.datetime8601, | ||||
|     allDay: false, | ||||
|     userId: entity.user.id, | ||||
|     userLabel: entity.user.label, | ||||
|     calendarRangeId: entity.id, | ||||
|     locationId: entity.location.id, | ||||
|     locationName: entity.location.name, | ||||
|     is: "range", | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export const remoteToFullCalendarEvent = ( | ||||
|     entity: CalendarRemote, | ||||
|   entity: CalendarRemote, | ||||
| ): EventInput & { id: string } => { | ||||
|     return { | ||||
|         id: `range_${entity.id}`, | ||||
|         title: entity.title, | ||||
|         start: entity.startDate.datetime8601, | ||||
|         end: entity.endDate.datetime8601, | ||||
|         allDay: entity.isAllDay, | ||||
|         is: "remote", | ||||
|     }; | ||||
|   return { | ||||
|     id: `range_${entity.id}`, | ||||
|     title: entity.title, | ||||
|     start: entity.startDate.datetime8601, | ||||
|     end: entity.endDate.datetime8601, | ||||
|     allDay: entity.isAllDay, | ||||
|     is: "remote", | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export const localsToFullCalendarEvent = ( | ||||
|     entity: CalendarLight, | ||||
|   entity: CalendarLight, | ||||
| ): EventInput & { id: string; originId: number } => { | ||||
|     return { | ||||
|         id: `local_${entity.id}`, | ||||
|         title: entity.persons.map((p) => p.text).join(", "), | ||||
|         originId: entity.id, | ||||
|         start: entity.startDate.datetime8601, | ||||
|         end: entity.endDate.datetime8601, | ||||
|         allDay: false, | ||||
|         is: "local", | ||||
|     }; | ||||
|   return { | ||||
|     id: `local_${entity.id}`, | ||||
|     title: entity.persons.map((p) => p.text).join(", "), | ||||
|     originId: entity.id, | ||||
|     start: entity.startDate.datetime8601, | ||||
|     end: entity.endDate.datetime8601, | ||||
|     allDay: false, | ||||
|     is: "local", | ||||
|   }; | ||||
| }; | ||||
|   | ||||
| @@ -1,58 +1,50 @@ | ||||
| <template> | ||||
|     <div class="btn-group" role="group"> | ||||
|         <button | ||||
|             id="btnGroupDrop1" | ||||
|             type="button" | ||||
|             class="btn btn-misc dropdown-toggle" | ||||
|             data-bs-toggle="dropdown" | ||||
|             aria-expanded="false" | ||||
|   <div class="btn-group" role="group"> | ||||
|     <button | ||||
|       id="btnGroupDrop1" | ||||
|       type="button" | ||||
|       class="btn btn-misc dropdown-toggle" | ||||
|       data-bs-toggle="dropdown" | ||||
|       aria-expanded="false" | ||||
|     > | ||||
|       <template v-if="status === Statuses.PENDING"> | ||||
|         <span class="fa fa-hourglass"></span> {{ $t("Give_an_answer") }} | ||||
|       </template> | ||||
|       <template v-else-if="status === Statuses.ACCEPTED"> | ||||
|         <span class="fa fa-check"></span> {{ $t("Accepted") }} | ||||
|       </template> | ||||
|       <template v-else-if="status === Statuses.DECLINED"> | ||||
|         <span class="fa fa-times"></span> {{ $t("Declined") }} | ||||
|       </template> | ||||
|       <template v-else-if="status === Statuses.TENTATIVELY_ACCEPTED"> | ||||
|         <span class="fa fa-question"></span> {{ $t("Tentative") }} | ||||
|       </template> | ||||
|     </button> | ||||
|     <ul class="dropdown-menu" aria-labelledby="btnGroupDrop1"> | ||||
|       <li v-if="status !== Statuses.ACCEPTED"> | ||||
|         <a class="dropdown-item" @click="changeStatus(Statuses.ACCEPTED)" | ||||
|           ><i class="fa fa-check" aria-hidden="true"></i> {{ $t("Accept") }}</a | ||||
|         > | ||||
|             <template v-if="status === Statuses.PENDING"> | ||||
|                 <span class="fa fa-hourglass"></span> {{ $t("Give_an_answer") }} | ||||
|             </template> | ||||
|             <template v-else-if="status === Statuses.ACCEPTED"> | ||||
|                 <span class="fa fa-check"></span> {{ $t("Accepted") }} | ||||
|             </template> | ||||
|             <template v-else-if="status === Statuses.DECLINED"> | ||||
|                 <span class="fa fa-times"></span> {{ $t("Declined") }} | ||||
|             </template> | ||||
|             <template v-else-if="status === Statuses.TENTATIVELY_ACCEPTED"> | ||||
|                 <span class="fa fa-question"></span> {{ $t("Tentative") }} | ||||
|             </template> | ||||
|         </button> | ||||
|         <ul class="dropdown-menu" aria-labelledby="btnGroupDrop1"> | ||||
|             <li v-if="status !== Statuses.ACCEPTED"> | ||||
|                 <a | ||||
|                     class="dropdown-item" | ||||
|                     @click="changeStatus(Statuses.ACCEPTED)" | ||||
|                     ><i class="fa fa-check" aria-hidden="true"></i> | ||||
|                     {{ $t("Accept") }}</a | ||||
|                 > | ||||
|             </li> | ||||
|             <li v-if="status !== Statuses.DECLINED"> | ||||
|                 <a | ||||
|                     class="dropdown-item" | ||||
|                     @click="changeStatus(Statuses.DECLINED)" | ||||
|                     ><i class="fa fa-times" aria-hidden="true"></i> | ||||
|                     {{ $t("Decline") }}</a | ||||
|                 > | ||||
|             </li> | ||||
|             <li v-if="status !== Statuses.TENTATIVELY_ACCEPTED"> | ||||
|                 <a | ||||
|                     class="dropdown-item" | ||||
|                     @click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)" | ||||
|                     ><i class="fa fa-question"></i> | ||||
|                     {{ $t("Tentatively_accept") }}</a | ||||
|                 > | ||||
|             </li> | ||||
|             <li v-if="status !== Statuses.PENDING"> | ||||
|                 <a class="dropdown-item" @click="changeStatus(Statuses.PENDING)" | ||||
|                     ><i class="fa fa-hourglass-o"></i> | ||||
|                     {{ $t("Set_pending") }}</a | ||||
|                 > | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
|       </li> | ||||
|       <li v-if="status !== Statuses.DECLINED"> | ||||
|         <a class="dropdown-item" @click="changeStatus(Statuses.DECLINED)" | ||||
|           ><i class="fa fa-times" aria-hidden="true"></i> {{ $t("Decline") }}</a | ||||
|         > | ||||
|       </li> | ||||
|       <li v-if="status !== Statuses.TENTATIVELY_ACCEPTED"> | ||||
|         <a | ||||
|           class="dropdown-item" | ||||
|           @click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)" | ||||
|           ><i class="fa fa-question"></i> {{ $t("Tentatively_accept") }}</a | ||||
|         > | ||||
|       </li> | ||||
|       <li v-if="status !== Statuses.PENDING"> | ||||
|         <a class="dropdown-item" @click="changeStatus(Statuses.PENDING)" | ||||
|           ><i class="fa fa-hourglass-o"></i> {{ $t("Set_pending") }}</a | ||||
|         > | ||||
|       </li> | ||||
|     </ul> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| @@ -64,69 +56,67 @@ const PENDING = "pending"; | ||||
| const TENTATIVELY_ACCEPTED = "tentative"; | ||||
|  | ||||
| const i18n = { | ||||
|     messages: { | ||||
|         fr: { | ||||
|             Give_an_answer: "Répondre", | ||||
|             Accepted: "Accepté", | ||||
|             Declined: "Refusé", | ||||
|             Tentative: "Accepté provisoirement", | ||||
|             Accept: "Accepter", | ||||
|             Decline: "Refuser", | ||||
|             Tentatively_accept: "Accepter provisoirement", | ||||
|             Set_pending: "Ne pas répondre", | ||||
|         }, | ||||
|   messages: { | ||||
|     fr: { | ||||
|       Give_an_answer: "Répondre", | ||||
|       Accepted: "Accepté", | ||||
|       Declined: "Refusé", | ||||
|       Tentative: "Accepté provisoirement", | ||||
|       Accept: "Accepter", | ||||
|       Decline: "Refuser", | ||||
|       Tentatively_accept: "Accepter provisoirement", | ||||
|       Set_pending: "Ne pas répondre", | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export default defineComponent({ | ||||
|     name: "Answer", | ||||
|     i18n, | ||||
|     props: { | ||||
|         calendarId: { type: Number, required: true }, | ||||
|         status: { | ||||
|             type: String as PropType< | ||||
|                 "accepted" | "declined" | "pending" | "tentative" | ||||
|             >, | ||||
|             required: true, | ||||
|         }, | ||||
|   name: "Answer", | ||||
|   i18n, | ||||
|   props: { | ||||
|     calendarId: { type: Number, required: true }, | ||||
|     status: { | ||||
|       type: String as PropType< | ||||
|         "accepted" | "declined" | "pending" | "tentative" | ||||
|       >, | ||||
|       required: true, | ||||
|     }, | ||||
|     emits: { | ||||
|         statusChanged( | ||||
|             payload: "accepted" | "declined" | "pending" | "tentative", | ||||
|         ) { | ||||
|             return true; | ||||
|         }, | ||||
|   }, | ||||
|   emits: { | ||||
|     statusChanged(payload: "accepted" | "declined" | "pending" | "tentative") { | ||||
|       return true; | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             Statuses: { | ||||
|                 ACCEPTED, | ||||
|                 DECLINED, | ||||
|                 PENDING, | ||||
|                 TENTATIVELY_ACCEPTED, | ||||
|             }, | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         changeStatus: function ( | ||||
|             newStatus: "accepted" | "declined" | "pending" | "tentative", | ||||
|         ) { | ||||
|             console.log("changeStatus", newStatus); | ||||
|             const url = `/api/1.0/calendar/calendar/${this.$props.calendarId}/answer/${newStatus}.json`; | ||||
|             window | ||||
|                 .fetch(url, { | ||||
|                     method: "POST", | ||||
|                 }) | ||||
|                 .then((r: Response) => { | ||||
|                     if (!r.ok) { | ||||
|                         console.error("could not confirm answer", newStatus); | ||||
|                         return; | ||||
|                     } | ||||
|                     console.log("answer sent", newStatus); | ||||
|                     this.$emit("statusChanged", newStatus); | ||||
|                 }); | ||||
|         }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       Statuses: { | ||||
|         ACCEPTED, | ||||
|         DECLINED, | ||||
|         PENDING, | ||||
|         TENTATIVELY_ACCEPTED, | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     changeStatus: function ( | ||||
|       newStatus: "accepted" | "declined" | "pending" | "tentative", | ||||
|     ) { | ||||
|       console.log("changeStatus", newStatus); | ||||
|       const url = `/api/1.0/calendar/calendar/${this.$props.calendarId}/answer/${newStatus}.json`; | ||||
|       window | ||||
|         .fetch(url, { | ||||
|           method: "POST", | ||||
|         }) | ||||
|         .then((r: Response) => { | ||||
|           if (!r.ok) { | ||||
|             console.error("could not confirm answer", newStatus); | ||||
|             return; | ||||
|           } | ||||
|           console.log("answer sent", newStatus); | ||||
|           this.$emit("statusChanged", newStatus); | ||||
|         }); | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -1,228 +1,177 @@ | ||||
| <template> | ||||
|     <div class="row"> | ||||
|         <div class="col-sm"> | ||||
|             <label class="form-label">{{ $t("created_availabilities") }}</label> | ||||
|             <vue-multiselect | ||||
|                 v-model="pickedLocation" | ||||
|                 :options="locations" | ||||
|                 :label="'name'" | ||||
|                 :track-by="'id'" | ||||
|                 :selectLabel="'Presser \'Entrée\' pour choisir'" | ||||
|                 :selectedLabel="'Choisir'" | ||||
|                 :deselectLabel="'Presser \'Entrée\' pour enlever'" | ||||
|                 :placeholder="'Choisir'" | ||||
|             ></vue-multiselect> | ||||
|         </div> | ||||
|   <div class="row"> | ||||
|     <div class="col-sm"> | ||||
|       <label class="form-label">{{ $t("created_availabilities") }}</label> | ||||
|       <vue-multiselect | ||||
|         v-model="pickedLocation" | ||||
|         :options="locations" | ||||
|         :label="'name'" | ||||
|         :track-by="'id'" | ||||
|         :selectLabel="'Presser \'Entrée\' pour choisir'" | ||||
|         :selectedLabel="'Choisir'" | ||||
|         :deselectLabel="'Presser \'Entrée\' pour enlever'" | ||||
|         :placeholder="'Choisir'" | ||||
|       ></vue-multiselect> | ||||
|     </div> | ||||
|     <div | ||||
|         class="display-options row justify-content-between" | ||||
|         style="margin-top: 1rem" | ||||
|     > | ||||
|         <div class="col-sm-9 col-xs-12"> | ||||
|             <div class="input-group mb-3"> | ||||
|                 <label class="input-group-text" for="slotDuration" | ||||
|                     >Durée des créneaux</label | ||||
|                 > | ||||
|                 <select | ||||
|                     v-model="slotDuration" | ||||
|                     id="slotDuration" | ||||
|                     class="form-select" | ||||
|                 > | ||||
|                     <option value="00:05:00">5 minutes</option> | ||||
|                     <option value="00:10:00">10 minutes</option> | ||||
|                     <option value="00:15:00">15 minutes</option> | ||||
|                     <option value="00:30:00">30 minutes</option> | ||||
|                     <option value="00:45:00">45 minutes</option> | ||||
|                     <option value="00:60:00">60 minutes</option> | ||||
|                 </select> | ||||
|                 <label class="input-group-text" for="slotMinTime">De</label> | ||||
|                 <select | ||||
|                     v-model="slotMinTime" | ||||
|                     id="slotMinTime" | ||||
|                     class="form-select" | ||||
|                 > | ||||
|                     <option value="00:00:00">0h</option> | ||||
|                     <option value="01:00:00">1h</option> | ||||
|                     <option value="02:00:00">2h</option> | ||||
|                     <option value="03:00:00">3h</option> | ||||
|                     <option value="04:00:00">4h</option> | ||||
|                     <option value="05:00:00">5h</option> | ||||
|                     <option value="06:00:00">6h</option> | ||||
|                     <option value="07:00:00">7h</option> | ||||
|                     <option value="08:00:00">8h</option> | ||||
|                     <option value="09:00:00">9h</option> | ||||
|                     <option value="10:00:00">10h</option> | ||||
|                     <option value="11:00:00">11h</option> | ||||
|                     <option value="12:00:00">12h</option> | ||||
|                 </select> | ||||
|                 <label class="input-group-text" for="slotMaxTime">À</label> | ||||
|                 <select | ||||
|                     v-model="slotMaxTime" | ||||
|                     id="slotMaxTime" | ||||
|                     class="form-select" | ||||
|                 > | ||||
|                     <option value="12:00:00">12h</option> | ||||
|                     <option value="13:00:00">13h</option> | ||||
|                     <option value="14:00:00">14h</option> | ||||
|                     <option value="15:00:00">15h</option> | ||||
|                     <option value="16:00:00">16h</option> | ||||
|                     <option value="17:00:00">17h</option> | ||||
|                     <option value="18:00:00">18h</option> | ||||
|                     <option value="19:00:00">19h</option> | ||||
|                     <option value="20:00:00">20h</option> | ||||
|                     <option value="21:00:00">21h</option> | ||||
|                     <option value="22:00:00">22h</option> | ||||
|                     <option value="23:00:00">23h</option> | ||||
|                     <option value="23:59:59">24h</option> | ||||
|                 </select> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="col-xs-12 col-sm-3"> | ||||
|             <div class="float-end"> | ||||
|                 <div class="form-check input-group"> | ||||
|                     <span class="input-group-text"> | ||||
|                         <input | ||||
|                             id="showHideWE" | ||||
|                             class="mt-0" | ||||
|                             type="checkbox" | ||||
|                             v-model="showWeekends" | ||||
|                         /> | ||||
|                     </span> | ||||
|                     <label | ||||
|                         for="showHideWE" | ||||
|                         class="form-check-label input-group-text" | ||||
|                         >Week-ends</label | ||||
|                     > | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|   </div> | ||||
|   <div | ||||
|     class="display-options row justify-content-between" | ||||
|     style="margin-top: 1rem" | ||||
|   > | ||||
|     <div class="col-sm-9 col-xs-12"> | ||||
|       <div class="input-group mb-3"> | ||||
|         <label class="input-group-text" for="slotDuration" | ||||
|           >Durée des créneaux</label | ||||
|         > | ||||
|         <select v-model="slotDuration" id="slotDuration" class="form-select"> | ||||
|           <option value="00:05:00">5 minutes</option> | ||||
|           <option value="00:10:00">10 minutes</option> | ||||
|           <option value="00:15:00">15 minutes</option> | ||||
|           <option value="00:30:00">30 minutes</option> | ||||
|         </select> | ||||
|         <label class="input-group-text" for="slotMinTime">De</label> | ||||
|         <select v-model="slotMinTime" id="slotMinTime" class="form-select"> | ||||
|           <option value="00:00:00">0h</option> | ||||
|           <option value="01:00:00">1h</option> | ||||
|           <option value="02:00:00">2h</option> | ||||
|           <option value="03:00:00">3h</option> | ||||
|           <option value="04:00:00">4h</option> | ||||
|           <option value="05:00:00">5h</option> | ||||
|           <option value="06:00:00">6h</option> | ||||
|           <option value="07:00:00">7h</option> | ||||
|           <option value="08:00:00">8h</option> | ||||
|           <option value="09:00:00">9h</option> | ||||
|           <option value="10:00:00">10h</option> | ||||
|           <option value="11:00:00">11h</option> | ||||
|           <option value="12:00:00">12h</option> | ||||
|         </select> | ||||
|         <label class="input-group-text" for="slotMaxTime">À</label> | ||||
|         <select v-model="slotMaxTime" id="slotMaxTime" class="form-select"> | ||||
|           <option value="12:00:00">12h</option> | ||||
|           <option value="13:00:00">13h</option> | ||||
|           <option value="14:00:00">14h</option> | ||||
|           <option value="15:00:00">15h</option> | ||||
|           <option value="16:00:00">16h</option> | ||||
|           <option value="17:00:00">17h</option> | ||||
|           <option value="18:00:00">18h</option> | ||||
|           <option value="19:00:00">19h</option> | ||||
|           <option value="20:00:00">20h</option> | ||||
|           <option value="21:00:00">21h</option> | ||||
|           <option value="22:00:00">22h</option> | ||||
|           <option value="23:00:00">23h</option> | ||||
|           <option value="23:59:59">24h</option> | ||||
|         </select> | ||||
|       </div> | ||||
|     </div> | ||||
|     <FullCalendar :options="calendarOptions" ref="calendarRef"> | ||||
|         <template v-slot:eventContent="{ event }: { event: EventApi }"> | ||||
|             <span :class="eventClasses"> | ||||
|                 <b v-if="event.extendedProps.is === 'remote'">{{ | ||||
|                     event.title | ||||
|                 }}</b> | ||||
|                 <b v-else-if="event.extendedProps.is === 'range'" | ||||
|                     >{{ formatDate(event.startStr, "time") }} - | ||||
|                     {{ formatDate(event.endStr, "time") }}: | ||||
|                     {{ event.extendedProps.locationName }}</b | ||||
|                 > | ||||
|                 <b v-else-if="event.extendedProps.is === 'local'">{{ | ||||
|                     event.title | ||||
|                 }}</b> | ||||
|                 <b v-else>no 'is'</b> | ||||
|                 <a | ||||
|                     v-if="event.extendedProps.is === 'range'" | ||||
|                     class="fa fa-fw fa-times delete" | ||||
|                     @click.prevent="onClickDelete(event)" | ||||
|                 > | ||||
|                 </a> | ||||
|             </span> | ||||
|     <div class="col-xs-12 col-sm-3"> | ||||
|       <div class="float-end"> | ||||
|         <div class="form-check input-group"> | ||||
|           <span class="input-group-text"> | ||||
|             <input | ||||
|               id="showHideWE" | ||||
|               class="mt-0" | ||||
|               type="checkbox" | ||||
|               v-model="showWeekends" | ||||
|             /> | ||||
|           </span> | ||||
|           <label for="showHideWE" class="form-check-label input-group-text" | ||||
|             >Week-ends</label | ||||
|           > | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   <FullCalendar :options="calendarOptions" ref="calendarRef"> | ||||
|     <template v-slot:eventContent="{ event }: { event: EventApi }"> | ||||
|       <span :class="eventClasses"> | ||||
|         <b v-if="event.extendedProps.is === 'remote'">{{ event.title }}</b> | ||||
|         <b v-else-if="event.extendedProps.is === 'range'" | ||||
|           >{{ formatDate(event.startStr) }} - | ||||
|           {{ event.extendedProps.locationName }}</b | ||||
|         > | ||||
|         <b v-else-if="event.extendedProps.is === 'local'">{{ event.title }}</b> | ||||
|         <b v-else>no 'is'</b> | ||||
|         <a | ||||
|           v-if="event.extendedProps.is === 'range'" | ||||
|           class="fa fa-fw fa-times delete" | ||||
|           @click.prevent="onClickDelete(event)" | ||||
|         > | ||||
|         </a> | ||||
|       </span> | ||||
|     </template> | ||||
|   </FullCalendar> | ||||
|  | ||||
|   <div id="copy-widget"> | ||||
|     <div class="container mt-2 mb-2"> | ||||
|       <div class="row justify-content-between align-items-center mb-4"> | ||||
|         <div class="col-xs-12 col-sm-3 col-md-2"> | ||||
|           <h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6> | ||||
|         </div> | ||||
|         <div class="col-xs-12 col-sm-9 col-md-2"> | ||||
|           <select v-model="dayOrWeek" id="dayOrWeek" class="form-select"> | ||||
|             <option value="day">{{ $t("from_day_to_day") }}</option> | ||||
|             <option value="week"> | ||||
|               {{ $t("from_week_to_week") }} | ||||
|             </option> | ||||
|           </select> | ||||
|         </div> | ||||
|         <template v-if="dayOrWeek === 'day'"> | ||||
|           <div class="col-xs-12 col-sm-3 col-md-3"> | ||||
|             <input class="form-control" type="date" v-model="copyFrom" /> | ||||
|           </div> | ||||
|           <div class="col-xs-12 col-sm-1 col-md-1 copy-chevron"> | ||||
|             <i class="fa fa-angle-double-right"></i> | ||||
|           </div> | ||||
|           <div class="col-xs-12 col-sm-3 col-md-3"> | ||||
|             <input class="form-control" type="date" v-model="copyTo" /> | ||||
|           </div> | ||||
|           <div class="col-xs-12 col-sm-5 col-md-1"> | ||||
|             <button class="btn btn-action float-end" @click="copyDay"> | ||||
|               {{ $t("copy_range") }} | ||||
|             </button> | ||||
|           </div> | ||||
|         </template> | ||||
|     </FullCalendar> | ||||
|  | ||||
|     <div id="copy-widget"> | ||||
|         <div class="container mt-2 mb-2"> | ||||
|             <div class="row justify-content-between align-items-center mb-4"> | ||||
|                 <div class="col-xs-12 col-sm-3 col-md-2"> | ||||
|                     <h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6> | ||||
|                 </div> | ||||
|                 <div class="col-xs-12 col-sm-9 col-md-2"> | ||||
|                     <select | ||||
|                         v-model="dayOrWeek" | ||||
|                         id="dayOrWeek" | ||||
|                         class="form-select" | ||||
|                     > | ||||
|                         <option value="day">{{ $t("from_day_to_day") }}</option> | ||||
|                         <option value="week"> | ||||
|                             {{ $t("from_week_to_week") }} | ||||
|                         </option> | ||||
|                     </select> | ||||
|                 </div> | ||||
|                 <template v-if="dayOrWeek === 'day'"> | ||||
|                     <div class="col-xs-12 col-sm-3 col-md-3"> | ||||
|                         <input | ||||
|                             class="form-control" | ||||
|                             type="date" | ||||
|                             v-model="copyFrom" | ||||
|                         /> | ||||
|                     </div> | ||||
|                     <div class="col-xs-12 col-sm-1 col-md-1 copy-chevron"> | ||||
|                         <i class="fa fa-angle-double-right"></i> | ||||
|                     </div> | ||||
|                     <div class="col-xs-12 col-sm-3 col-md-3"> | ||||
|                         <input | ||||
|                             class="form-control" | ||||
|                             type="date" | ||||
|                             v-model="copyTo" | ||||
|                         /> | ||||
|                     </div> | ||||
|                     <div class="col-xs-12 col-sm-5 col-md-1"> | ||||
|                         <button | ||||
|                             class="btn btn-action float-end" | ||||
|                             @click="copyDay" | ||||
|                         > | ||||
|                             {{ $t("copy_range") }} | ||||
|                         </button> | ||||
|                     </div> | ||||
|                 </template> | ||||
|                 <template v-else> | ||||
|                     <div class="col-xs-12 col-sm-3 col-md-3"> | ||||
|                         <select | ||||
|                             v-model="copyFromWeek" | ||||
|                             id="copyFromWeek" | ||||
|                             class="form-select" | ||||
|                         > | ||||
|                             <option | ||||
|                                 v-for="w in lastWeeks" | ||||
|                                 :value="w.value" | ||||
|                                 :key="w.value" | ||||
|                             > | ||||
|                                 {{ w.text }} | ||||
|                             </option> | ||||
|                         </select> | ||||
|                     </div> | ||||
|                     <div class="col-xs-12 col-sm-1 col-md-1 copy-chevron"> | ||||
|                         <i class="fa fa-angle-double-right"></i> | ||||
|                     </div> | ||||
|                     <div class="col-xs-12 col-sm-3 col-md-3"> | ||||
|                         <select | ||||
|                             v-model="copyToWeek" | ||||
|                             id="copyToWeek" | ||||
|                             class="form-select" | ||||
|                         > | ||||
|                             <option | ||||
|                                 v-for="w in nextWeeks" | ||||
|                                 :value="w.value" | ||||
|                                 :key="w.value" | ||||
|                             > | ||||
|                                 {{ w.text }} | ||||
|                             </option> | ||||
|                         </select> | ||||
|                     </div> | ||||
|                     <div class="col-xs-12 col-sm-5 col-md-1"> | ||||
|                         <button | ||||
|                             class="btn btn-action float-end" | ||||
|                             @click="copyWeek" | ||||
|                         > | ||||
|                             {{ $t("copy_range") }} | ||||
|                         </button> | ||||
|                     </div> | ||||
|                 </template> | ||||
|             </div> | ||||
|         </div> | ||||
|         <template v-else> | ||||
|           <div class="col-xs-12 col-sm-3 col-md-3"> | ||||
|             <select | ||||
|               v-model="copyFromWeek" | ||||
|               id="copyFromWeek" | ||||
|               class="form-select" | ||||
|             > | ||||
|               <option v-for="w in lastWeeks" :value="w.value" :key="w.value"> | ||||
|                 {{ w.text }} | ||||
|               </option> | ||||
|             </select> | ||||
|           </div> | ||||
|           <div class="col-xs-12 col-sm-1 col-md-1 copy-chevron"> | ||||
|             <i class="fa fa-angle-double-right"></i> | ||||
|           </div> | ||||
|           <div class="col-xs-12 col-sm-3 col-md-3"> | ||||
|             <select v-model="copyToWeek" id="copyToWeek" class="form-select"> | ||||
|               <option v-for="w in nextWeeks" :value="w.value" :key="w.value"> | ||||
|                 {{ w.text }} | ||||
|               </option> | ||||
|             </select> | ||||
|           </div> | ||||
|           <div class="col-xs-12 col-sm-5 col-md-1"> | ||||
|             <button class="btn btn-action float-end" @click="copyWeek"> | ||||
|               {{ $t("copy_range") }} | ||||
|             </button> | ||||
|           </div> | ||||
|         </template> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|     <!-- not directly seen, but include in a modal --> | ||||
|     <edit-location ref="editLocation"></edit-location> | ||||
|   <!-- not directly seen, but include in a modal --> | ||||
|   <edit-location ref="editLocation"></edit-location> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import type { | ||||
|     CalendarOptions, | ||||
|     DatesSetArg, | ||||
|     EventInput, | ||||
|   CalendarOptions, | ||||
|   DatesSetArg, | ||||
|   EventInput, | ||||
| } from "@fullcalendar/core"; | ||||
| import { computed, ref, onMounted } from "vue"; | ||||
| import { useStore } from "vuex"; | ||||
| @@ -230,14 +179,14 @@ import { key } from "./store"; | ||||
| import FullCalendar from "@fullcalendar/vue3"; | ||||
| import frLocale from "@fullcalendar/core/locales/fr"; | ||||
| import interactionPlugin, { | ||||
|     EventResizeDoneArg, | ||||
|   EventResizeDoneArg, | ||||
| } from "@fullcalendar/interaction"; | ||||
| import timeGridPlugin from "@fullcalendar/timegrid"; | ||||
| import { | ||||
|     EventApi, | ||||
|     DateSelectArg, | ||||
|     EventDropArg, | ||||
|     EventClickArg, | ||||
|   EventApi, | ||||
|   DateSelectArg, | ||||
|   EventDropArg, | ||||
|   EventClickArg, | ||||
| } from "@fullcalendar/core"; | ||||
| import { dateToISO, ISOToDate } from "ChillMainAssets/chill/js/date"; | ||||
| import VueMultiselect from "vue-multiselect"; | ||||
| @@ -258,113 +207,96 @@ const copyFromWeek = ref<string | null>(null); | ||||
| const copyToWeek = ref<string | null>(null); | ||||
|  | ||||
| interface Weeks { | ||||
|     value: string | null; | ||||
|     text: string; | ||||
|   value: string | null; | ||||
|   text: string; | ||||
| } | ||||
|  | ||||
| const getMonday = (week: number): Date => { | ||||
|     const lastMonday = new Date(); | ||||
|     lastMonday.setDate( | ||||
|         lastMonday.getDate() - ((lastMonday.getDay() + 6) % 7) + week * 7, | ||||
|     ); | ||||
|     return lastMonday; | ||||
|   const lastMonday = new Date(); | ||||
|   lastMonday.setDate( | ||||
|     lastMonday.getDate() - ((lastMonday.getDay() + 6) % 7) + week * 7, | ||||
|   ); | ||||
|   return lastMonday; | ||||
| }; | ||||
|  | ||||
| const dateOptions: Intl.DateTimeFormatOptions = { | ||||
|     weekday: "long", | ||||
|     year: "numeric", | ||||
|     month: "long", | ||||
|     day: "numeric", | ||||
|   weekday: "long", | ||||
|   year: "numeric", | ||||
|   month: "long", | ||||
|   day: "numeric", | ||||
| }; | ||||
|  | ||||
| const lastWeeks = computed((): Weeks[] => | ||||
|     Array.from(Array(30).keys()).map((w) => { | ||||
|         const lastMonday = getMonday(15 - w); | ||||
|         return { | ||||
|             value: dateToISO(lastMonday), | ||||
|             text: `Semaine du ${lastMonday.toLocaleDateString("fr-FR", dateOptions)}`, | ||||
|         }; | ||||
|     }), | ||||
|   Array.from(Array(30).keys()).map((w) => { | ||||
|     const lastMonday = getMonday(15 - w); | ||||
|     return { | ||||
|       value: dateToISO(lastMonday), | ||||
|       text: `Semaine du ${lastMonday.toLocaleDateString("fr-FR", dateOptions)}`, | ||||
|     }; | ||||
|   }), | ||||
| ); | ||||
|  | ||||
| const nextWeeks = computed((): Weeks[] => | ||||
|     Array.from(Array(52).keys()).map((w) => { | ||||
|         const nextMonday = getMonday(w + 1); | ||||
|         return { | ||||
|             value: dateToISO(nextMonday), | ||||
|             text: `Semaine du ${nextMonday.toLocaleDateString("fr-FR", dateOptions)}`, | ||||
|         }; | ||||
|     }), | ||||
|   Array.from(Array(52).keys()).map((w) => { | ||||
|     const nextMonday = getMonday(w + 1); | ||||
|     return { | ||||
|       value: dateToISO(nextMonday), | ||||
|       text: `Semaine du ${nextMonday.toLocaleDateString("fr-FR", dateOptions)}`, | ||||
|     }; | ||||
|   }), | ||||
| ); | ||||
|  | ||||
| const formatDate = (datetime: string, format: null | "time" = null) => { | ||||
|     const date = ISOToDate(datetime); | ||||
|     if (!date) return ""; | ||||
|  | ||||
|     if (format === "time") { | ||||
|         return date.toLocaleTimeString("fr-FR", { | ||||
|             hour: "2-digit", | ||||
|             minute: "2-digit", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // French date formatting | ||||
|     return date.toLocaleDateString("fr-FR", { | ||||
|         weekday: "short", | ||||
|         year: "numeric", | ||||
|         month: "short", | ||||
|         day: "numeric", | ||||
|         hour: "2-digit", | ||||
|         minute: "2-digit", | ||||
|     }); | ||||
| const formatDate = (datetime: string) => { | ||||
|   console.log(typeof datetime); | ||||
|   return ISOToDate(datetime); | ||||
| }; | ||||
|  | ||||
| const baseOptions = ref<CalendarOptions>({ | ||||
|     locale: frLocale, | ||||
|     plugins: [interactionPlugin, timeGridPlugin], | ||||
|     initialView: "timeGridWeek", | ||||
|     initialDate: new Date(), | ||||
|     scrollTimeReset: false, | ||||
|     selectable: true, | ||||
|     // when the dates are changes in the fullcalendar view OR when new events are added | ||||
|     datesSet: onDatesSet, | ||||
|     // when a date is selected | ||||
|     select: onDateSelect, | ||||
|     // when a event is resized | ||||
|     eventResize: onEventDropOrResize, | ||||
|     // when an event is moved | ||||
|     eventDrop: onEventDropOrResize, | ||||
|     // when an event si clicked | ||||
|     eventClick: onEventClick, | ||||
|     selectMirror: false, | ||||
|     editable: true, | ||||
|     headerToolbar: { | ||||
|         left: "prev,next today", | ||||
|         center: "title", | ||||
|         right: "timeGridWeek,timeGridDay", | ||||
|     }, | ||||
|   locale: frLocale, | ||||
|   plugins: [interactionPlugin, timeGridPlugin], | ||||
|   initialView: "timeGridWeek", | ||||
|   initialDate: new Date(), | ||||
|   scrollTimeReset: false, | ||||
|   selectable: true, | ||||
|   // when the dates are changes in the fullcalendar view OR when new events are added | ||||
|   datesSet: onDatesSet, | ||||
|   // when a date is selected | ||||
|   select: onDateSelect, | ||||
|   // when a event is resized | ||||
|   eventResize: onEventDropOrResize, | ||||
|   // when an event is moved | ||||
|   eventDrop: onEventDropOrResize, | ||||
|   // when an event si clicked | ||||
|   eventClick: onEventClick, | ||||
|   selectMirror: false, | ||||
|   editable: true, | ||||
|   headerToolbar: { | ||||
|     left: "prev,next today", | ||||
|     center: "title", | ||||
|     right: "timeGridWeek,timeGridDay", | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const ranges = computed<EventInput[]>(() => { | ||||
|     return store.state.calendarRanges.ranges; | ||||
|   return store.state.calendarRanges.ranges; | ||||
| }); | ||||
|  | ||||
| const locations = computed<Location[]>(() => { | ||||
|     return store.state.locations.locations; | ||||
|   return store.state.locations.locations; | ||||
| }); | ||||
|  | ||||
| const pickedLocation = computed<Location | null>({ | ||||
|     get(): Location | null { | ||||
|         return ( | ||||
|             store.state.locations.locationPicked || | ||||
|             store.state.locations.currentLocation | ||||
|         ); | ||||
|     }, | ||||
|     set(newLocation: Location | null): void { | ||||
|         store.commit("locations/setLocationPicked", newLocation, { | ||||
|             root: true, | ||||
|         }); | ||||
|     }, | ||||
|   get(): Location | null { | ||||
|     return ( | ||||
|       store.state.locations.locationPicked || | ||||
|       store.state.locations.currentLocation | ||||
|     ); | ||||
|   }, | ||||
|   set(newLocation: Location | null): void { | ||||
|     store.commit("locations/setLocationPicked", newLocation, { | ||||
|       root: true, | ||||
|     }); | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -393,116 +325,116 @@ const sources = computed<EventSourceInput[]>(() => { | ||||
| */ | ||||
|  | ||||
| const calendarOptions = computed((): CalendarOptions => { | ||||
|     return { | ||||
|         ...baseOptions.value, | ||||
|         weekends: showWeekends.value, | ||||
|         slotDuration: slotDuration.value, | ||||
|         events: ranges.value, | ||||
|         slotMinTime: slotMinTime.value, | ||||
|         slotMaxTime: slotMaxTime.value, | ||||
|     }; | ||||
|   return { | ||||
|     ...baseOptions.value, | ||||
|     weekends: showWeekends.value, | ||||
|     slotDuration: slotDuration.value, | ||||
|     events: ranges.value, | ||||
|     slotMinTime: slotMinTime.value, | ||||
|     slotMaxTime: slotMaxTime.value, | ||||
|   }; | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * launched when the calendar range date change | ||||
|  */ | ||||
| function onDatesSet(event: DatesSetArg): void { | ||||
|     store.dispatch("fullCalendar/setCurrentDatesView", { | ||||
|         start: event.start, | ||||
|         end: event.end, | ||||
|     }); | ||||
|   store.dispatch("fullCalendar/setCurrentDatesView", { | ||||
|     start: event.start, | ||||
|     end: event.end, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function onDateSelect(event: DateSelectArg): void { | ||||
|     if (null === pickedLocation.value) { | ||||
|         window.alert( | ||||
|             "Indiquez une localisation avant de créer une période de disponibilité.", | ||||
|         ); | ||||
|         return; | ||||
|     } | ||||
|   if (null === pickedLocation.value) { | ||||
|     window.alert( | ||||
|       "Indiquez une localisation avant de créer une période de disponibilité.", | ||||
|     ); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|     store.dispatch("calendarRanges/createRange", { | ||||
|         start: event.start, | ||||
|         end: event.end, | ||||
|         location: pickedLocation.value, | ||||
|     }); | ||||
|   store.dispatch("calendarRanges/createRange", { | ||||
|     start: event.start, | ||||
|     end: event.end, | ||||
|     location: pickedLocation.value, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * When a calendar range is deleted | ||||
|  */ | ||||
| function onClickDelete(event: EventApi): void { | ||||
|     if (event.extendedProps.is !== "range") { | ||||
|         return; | ||||
|     } | ||||
|   if (event.extendedProps.is !== "range") { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|     store.dispatch( | ||||
|         "calendarRanges/deleteRange", | ||||
|         event.extendedProps.calendarRangeId, | ||||
|     ); | ||||
|   store.dispatch( | ||||
|     "calendarRanges/deleteRange", | ||||
|     event.extendedProps.calendarRangeId, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function onEventDropOrResize(payload: EventDropArg | EventResizeDoneArg) { | ||||
|     if (payload.event.extendedProps.is !== "range") { | ||||
|         return; | ||||
|     } | ||||
|   if (payload.event.extendedProps.is !== "range") { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|     store.dispatch("calendarRanges/patchRangeTime", { | ||||
|         calendarRangeId: payload.event.extendedProps.calendarRangeId, | ||||
|         start: payload.event.start, | ||||
|         end: payload.event.end, | ||||
|     }); | ||||
|   store.dispatch("calendarRanges/patchRangeTime", { | ||||
|     calendarRangeId: payload.event.extendedProps.calendarRangeId, | ||||
|     start: payload.event.start, | ||||
|     end: payload.event.end, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function onEventClick(payload: EventClickArg): void { | ||||
|     // @ts-ignore TS does not recognize the target. But it does exists. | ||||
|     if (payload.jsEvent.target.classList.contains("delete")) { | ||||
|         return; | ||||
|     } | ||||
|     if (payload.event.extendedProps.is !== "range") { | ||||
|         return; | ||||
|     } | ||||
|   // @ts-ignore TS does not recognize the target. But it does exists. | ||||
|   if (payload.jsEvent.target.classList.contains("delete")) { | ||||
|     return; | ||||
|   } | ||||
|   if (payload.event.extendedProps.is !== "range") { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|     editLocation.value?.startEdit(payload.event); | ||||
|   editLocation.value?.startEdit(payload.event); | ||||
| } | ||||
|  | ||||
| function copyDay() { | ||||
|     if (null === copyFrom.value || null === copyTo.value) { | ||||
|         return; | ||||
|     } | ||||
|     store.dispatch("calendarRanges/copyFromDayToAnotherDay", { | ||||
|         from: ISOToDate(copyFrom.value), | ||||
|         to: ISOToDate(copyTo.value), | ||||
|     }); | ||||
|   if (null === copyFrom.value || null === copyTo.value) { | ||||
|     return; | ||||
|   } | ||||
|   store.dispatch("calendarRanges/copyFromDayToAnotherDay", { | ||||
|     from: ISOToDate(copyFrom.value), | ||||
|     to: ISOToDate(copyTo.value), | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function copyWeek() { | ||||
|     if (null === copyFromWeek.value || null === copyToWeek.value) { | ||||
|         return; | ||||
|     } | ||||
|     store.dispatch("calendarRanges/copyFromWeekToAnotherWeek", { | ||||
|         fromMonday: ISOToDate(copyFromWeek.value), | ||||
|         toMonday: ISOToDate(copyToWeek.value), | ||||
|     }); | ||||
|   if (null === copyFromWeek.value || null === copyToWeek.value) { | ||||
|     return; | ||||
|   } | ||||
|   store.dispatch("calendarRanges/copyFromWeekToAnotherWeek", { | ||||
|     fromMonday: ISOToDate(copyFromWeek.value), | ||||
|     toMonday: ISOToDate(copyToWeek.value), | ||||
|   }); | ||||
| } | ||||
|  | ||||
| onMounted(() => { | ||||
|     copyFromWeek.value = dateToISO(getMonday(0)); | ||||
|     copyToWeek.value = dateToISO(getMonday(1)); | ||||
|   copyFromWeek.value = dateToISO(getMonday(0)); | ||||
|   copyToWeek.value = dateToISO(getMonday(1)); | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| #copy-widget { | ||||
|     position: sticky; | ||||
|     bottom: 0px; | ||||
|     background-color: white; | ||||
|     z-index: 9999999999; | ||||
|     padding: 0.25rem 0 0.25rem; | ||||
|   position: sticky; | ||||
|   bottom: 0px; | ||||
|   background-color: white; | ||||
|   z-index: 9999999999; | ||||
|   padding: 0.25rem 0 0.25rem; | ||||
| } | ||||
| div.copy-chevron { | ||||
|     text-align: center; | ||||
|     font-size: x-large; | ||||
|     width: 2rem; | ||||
|   text-align: center; | ||||
|   font-size: x-large; | ||||
|   width: 2rem; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,28 +1,28 @@ | ||||
| <template> | ||||
|     <component :is="Teleport" to="body"> | ||||
|         <modal v-if="showModal" @close="closeModal"> | ||||
|             <template v-slot:header> | ||||
|                 <h3>{{ "Modifier le lieu" }}</h3> | ||||
|             </template> | ||||
|   <component :is="Teleport" to="body"> | ||||
|     <modal v-if="showModal" @close="closeModal"> | ||||
|       <template v-slot:header> | ||||
|         <h3>{{ "Modifier le lieu" }}</h3> | ||||
|       </template> | ||||
|  | ||||
|             <template v-slot:body> | ||||
|                 <div></div> | ||||
|                 <label>Localisation</label> | ||||
|                 <vue-multiselect | ||||
|                     v-model="location" | ||||
|                     :options="locations" | ||||
|                     :label="'name'" | ||||
|                     :track-by="'id'" | ||||
|                 ></vue-multiselect> | ||||
|             </template> | ||||
|       <template v-slot:body> | ||||
|         <div></div> | ||||
|         <label>Localisation</label> | ||||
|         <vue-multiselect | ||||
|           v-model="location" | ||||
|           :options="locations" | ||||
|           :label="'name'" | ||||
|           :track-by="'id'" | ||||
|         ></vue-multiselect> | ||||
|       </template> | ||||
|  | ||||
|             <template v-slot:footer> | ||||
|                 <button class="btn btn-save" @click="saveAndClose"> | ||||
|                     {{ "Enregistrer" }} | ||||
|                 </button> | ||||
|             </template> | ||||
|         </modal> | ||||
|     </component> | ||||
|       <template v-slot:footer> | ||||
|         <button class="btn btn-save" @click="saveAndClose"> | ||||
|           {{ "Enregistrer" }} | ||||
|         </button> | ||||
|       </template> | ||||
|     </modal> | ||||
|   </component> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| @@ -39,7 +39,7 @@ import VueMultiselect from "vue-multiselect"; | ||||
| import { Teleport as teleport_, TeleportProps, VNodeProps } from "vue"; | ||||
|  | ||||
| const Teleport = teleport_ as new () => { | ||||
|     $props: VNodeProps & TeleportProps; | ||||
|   $props: VNodeProps & TeleportProps; | ||||
| }; | ||||
|  | ||||
| const store = useStore(key); | ||||
| @@ -50,37 +50,37 @@ const showModal = ref(false); | ||||
| //const tele = ref<InstanceType<typeof Teleport> | null>(null); | ||||
|  | ||||
| const locations = computed<Location[]>(() => { | ||||
|     return store.state.locations.locations; | ||||
|   return store.state.locations.locations; | ||||
| }); | ||||
|  | ||||
| const startEdit = function (event: EventApi): void { | ||||
|     console.log("startEditing", event); | ||||
|     calendarRangeId.value = event.extendedProps.calendarRangeId; | ||||
|     location.value = | ||||
|         store.getters["locations/getLocationById"]( | ||||
|             event.extendedProps.locationId, | ||||
|         ) || null; | ||||
|   console.log("startEditing", event); | ||||
|   calendarRangeId.value = event.extendedProps.calendarRangeId; | ||||
|   location.value = | ||||
|     store.getters["locations/getLocationById"]( | ||||
|       event.extendedProps.locationId, | ||||
|     ) || null; | ||||
|  | ||||
|     console.log("new location value", location.value); | ||||
|     console.log("calendar range id", calendarRangeId.value); | ||||
|     showModal.value = true; | ||||
|   console.log("new location value", location.value); | ||||
|   console.log("calendar range id", calendarRangeId.value); | ||||
|   showModal.value = true; | ||||
| }; | ||||
|  | ||||
| const saveAndClose = function (e: Event): void { | ||||
|     console.log("saveEditAndClose", e); | ||||
|   console.log("saveEditAndClose", e); | ||||
|  | ||||
|     store | ||||
|         .dispatch("calendarRanges/patchRangeLocation", { | ||||
|             location: location.value, | ||||
|             calendarRangeId: calendarRangeId.value, | ||||
|         }) | ||||
|         .then((_) => { | ||||
|             showModal.value = false; | ||||
|         }); | ||||
|   store | ||||
|     .dispatch("calendarRanges/patchRangeLocation", { | ||||
|       location: location.value, | ||||
|       calendarRangeId: calendarRangeId.value, | ||||
|     }) | ||||
|     .then((_) => { | ||||
|       showModal.value = false; | ||||
|     }); | ||||
| }; | ||||
|  | ||||
| const closeModal = function (_: any): void { | ||||
|     showModal.value = false; | ||||
|   showModal.value = false; | ||||
| }; | ||||
|  | ||||
| defineExpose({ startEdit }); | ||||
|   | ||||
| @@ -1,27 +1,27 @@ | ||||
| const appMessages = { | ||||
|     fr: { | ||||
|         created_availabilities: "Lieu des plages de disponibilités créées", | ||||
|         edit_your_calendar_range: "Planifiez vos plages de disponibilités", | ||||
|         show_my_calendar: "Afficher mon calendrier", | ||||
|         show_weekends: "Afficher les week-ends", | ||||
|         copy_range: "Copier", | ||||
|         copy_range_from_to: "Copier les plages", | ||||
|         from_day_to_day: "d'un jour à l'autre", | ||||
|         from_week_to_week: "d'une semaine à l'autre", | ||||
|         copy_range_how_to: | ||||
|             "Créez les plages de disponibilités durant une journée et copiez-les facilement au jour suivant avec ce bouton. Si les week-ends sont cachés, le jour suivant un vendredi sera le lundi.", | ||||
|         new_range_to_save: "Nouvelles plages à enregistrer", | ||||
|         update_range_to_save: "Plages à modifier", | ||||
|         delete_range_to_save: "Plages à supprimer", | ||||
|         by: "Par", | ||||
|         main_user_concerned: "Utilisateur concerné", | ||||
|         dateFrom: "De", | ||||
|         dateTo: "à", | ||||
|         day: "Jour", | ||||
|         week: "Semaine", | ||||
|         month: "Mois", | ||||
|         today: "Aujourd'hui", | ||||
|     }, | ||||
|   fr: { | ||||
|     created_availabilities: "Lieu des plages de disponibilités créées", | ||||
|     edit_your_calendar_range: "Planifiez vos plages de disponibilités", | ||||
|     show_my_calendar: "Afficher mon calendrier", | ||||
|     show_weekends: "Afficher les week-ends", | ||||
|     copy_range: "Copier", | ||||
|     copy_range_from_to: "Copier les plages", | ||||
|     from_day_to_day: "d'un jour à l'autre", | ||||
|     from_week_to_week: "d'une semaine à l'autre", | ||||
|     copy_range_how_to: | ||||
|       "Créez les plages de disponibilités durant une journée et copiez-les facilement au jour suivant avec ce bouton. Si les week-ends sont cachés, le jour suivant un vendredi sera le lundi.", | ||||
|     new_range_to_save: "Nouvelles plages à enregistrer", | ||||
|     update_range_to_save: "Plages à modifier", | ||||
|     delete_range_to_save: "Plages à supprimer", | ||||
|     by: "Par", | ||||
|     main_user_concerned: "Utilisateur concerné", | ||||
|     dateFrom: "De", | ||||
|     dateTo: "à", | ||||
|     day: "Jour", | ||||
|     week: "Semaine", | ||||
|     month: "Mois", | ||||
|     today: "Aujourd'hui", | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export { appMessages }; | ||||
|   | ||||
| @@ -7,13 +7,13 @@ import App2 from "./App2.vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
|  | ||||
| futureStore().then((store) => { | ||||
|     const i18n = _createI18n(appMessages, false); | ||||
|   const i18n = _createI18n(appMessages, false); | ||||
|  | ||||
|     const app = createApp({ | ||||
|         template: `<app></app>`, | ||||
|     }) | ||||
|         .use(store, key) | ||||
|         .use(i18n) | ||||
|         .component("app", App2) | ||||
|         .mount("#myCalendar"); | ||||
|   const app = createApp({ | ||||
|     template: `<app></app>`, | ||||
|   }) | ||||
|     .use(store, key) | ||||
|     .use(i18n) | ||||
|     .component("app", App2) | ||||
|     .mount("#myCalendar"); | ||||
| }); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import me, { MeState } from "./modules/me"; | ||||
| import fullCalendar, { FullCalendarState } from "./modules/fullcalendar"; | ||||
| import calendarRanges, { CalendarRangesState } from "./modules/calendarRanges"; | ||||
| import calendarRemotes, { | ||||
|     CalendarRemotesState, | ||||
|   CalendarRemotesState, | ||||
| } from "./modules/calendarRemotes"; | ||||
| import { whoami } from "../../../../../../ChillMainBundle/Resources/public/lib/api/user"; | ||||
| import { User } from "../../../../../../ChillMainBundle/Resources/public/types"; | ||||
| @@ -15,42 +15,40 @@ import calendarLocals, { CalendarLocalsState } from "./modules/calendarLocals"; | ||||
| const debug = process.env.NODE_ENV !== "production"; | ||||
|  | ||||
| export interface State { | ||||
|     calendarRanges: CalendarRangesState; | ||||
|     calendarRemotes: CalendarRemotesState; | ||||
|     calendarLocals: CalendarLocalsState; | ||||
|     fullCalendar: FullCalendarState; | ||||
|     me: MeState; | ||||
|     locations: LocationState; | ||||
|   calendarRanges: CalendarRangesState; | ||||
|   calendarRemotes: CalendarRemotesState; | ||||
|   calendarLocals: CalendarLocalsState; | ||||
|   fullCalendar: FullCalendarState; | ||||
|   me: MeState; | ||||
|   locations: LocationState; | ||||
| } | ||||
|  | ||||
| export const key: InjectionKey<Store<State>> = Symbol(); | ||||
|  | ||||
| const futureStore = function (): Promise<Store<State>> { | ||||
|     return whoami().then((user: User) => { | ||||
|         const store = createStore<State>({ | ||||
|             strict: debug, | ||||
|             modules: { | ||||
|                 me, | ||||
|                 fullCalendar, | ||||
|                 calendarRanges, | ||||
|                 calendarRemotes, | ||||
|                 calendarLocals, | ||||
|                 locations, | ||||
|             }, | ||||
|             mutations: {}, | ||||
|         }); | ||||
|  | ||||
|         store.commit("me/setWhoAmi", user, { root: true }); | ||||
|         store | ||||
|             .dispatch("locations/getLocations", null, { root: true }) | ||||
|             .then((_) => { | ||||
|                 return store.dispatch("locations/getCurrentLocation", null, { | ||||
|                     root: true, | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         return Promise.resolve(store); | ||||
|   return whoami().then((user: User) => { | ||||
|     const store = createStore<State>({ | ||||
|       strict: debug, | ||||
|       modules: { | ||||
|         me, | ||||
|         fullCalendar, | ||||
|         calendarRanges, | ||||
|         calendarRemotes, | ||||
|         calendarLocals, | ||||
|         locations, | ||||
|       }, | ||||
|       mutations: {}, | ||||
|     }); | ||||
|  | ||||
|     store.commit("me/setWhoAmi", user, { root: true }); | ||||
|     store.dispatch("locations/getLocations", null, { root: true }).then((_) => { | ||||
|       return store.dispatch("locations/getCurrentLocation", null, { | ||||
|         root: true, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     return Promise.resolve(store); | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| export default futureStore; | ||||
|   | ||||
| @@ -8,109 +8,99 @@ import { TransportExceptionInterface } from "../../../../../../../ChillMainBundl | ||||
| import { COLORS } from "../../../Calendar/const"; | ||||
|  | ||||
| export interface CalendarLocalsState { | ||||
|     locals: EventInput[]; | ||||
|     localsLoaded: { start: number; end: number }[]; | ||||
|     localsIndex: Set<string>; | ||||
|     key: number; | ||||
|   locals: EventInput[]; | ||||
|   localsLoaded: { start: number; end: number }[]; | ||||
|   localsIndex: Set<string>; | ||||
|   key: number; | ||||
| } | ||||
|  | ||||
| type Context = ActionContext<CalendarLocalsState, State>; | ||||
|  | ||||
| export default { | ||||
|     namespaced: true, | ||||
|     state: (): CalendarLocalsState => ({ | ||||
|         locals: [], | ||||
|         localsLoaded: [], | ||||
|         localsIndex: new Set<string>(), | ||||
|         key: 0, | ||||
|     }), | ||||
|     getters: { | ||||
|         isLocalsLoaded: | ||||
|             (state: CalendarLocalsState) => | ||||
|             ({ start, end }: { start: Date; end: Date }): boolean => { | ||||
|                 for (const range of state.localsLoaded) { | ||||
|                     if ( | ||||
|                         start.getTime() === range.start && | ||||
|                         end.getTime() === range.end | ||||
|                     ) { | ||||
|                         return true; | ||||
|                     } | ||||
|                 } | ||||
|   namespaced: true, | ||||
|   state: (): CalendarLocalsState => ({ | ||||
|     locals: [], | ||||
|     localsLoaded: [], | ||||
|     localsIndex: new Set<string>(), | ||||
|     key: 0, | ||||
|   }), | ||||
|   getters: { | ||||
|     isLocalsLoaded: | ||||
|       (state: CalendarLocalsState) => | ||||
|       ({ start, end }: { start: Date; end: Date }): boolean => { | ||||
|         for (const range of state.localsLoaded) { | ||||
|           if (start.getTime() === range.start && end.getTime() === range.end) { | ||||
|             return true; | ||||
|           } | ||||
|         } | ||||
|  | ||||
|                 return false; | ||||
|             }, | ||||
|         return false; | ||||
|       }, | ||||
|   }, | ||||
|   mutations: { | ||||
|     addLocals(state: CalendarLocalsState, ranges: CalendarLight[]) { | ||||
|       console.log("addLocals", ranges); | ||||
|  | ||||
|       const toAdd = ranges | ||||
|         .map((cr) => localsToFullCalendarEvent(cr)) | ||||
|         .filter((r) => !state.localsIndex.has(r.id)); | ||||
|  | ||||
|       toAdd.forEach((r) => { | ||||
|         state.localsIndex.add(r.id); | ||||
|         state.locals.push(r); | ||||
|       }); | ||||
|       state.key = state.key + toAdd.length; | ||||
|     }, | ||||
|     mutations: { | ||||
|         addLocals(state: CalendarLocalsState, ranges: CalendarLight[]) { | ||||
|             console.log("addLocals", ranges); | ||||
|  | ||||
|             const toAdd = ranges | ||||
|                 .map((cr) => localsToFullCalendarEvent(cr)) | ||||
|                 .filter((r) => !state.localsIndex.has(r.id)); | ||||
|  | ||||
|             toAdd.forEach((r) => { | ||||
|                 state.localsIndex.add(r.id); | ||||
|                 state.locals.push(r); | ||||
|             }); | ||||
|             state.key = state.key + toAdd.length; | ||||
|         }, | ||||
|         addLoaded( | ||||
|             state: CalendarLocalsState, | ||||
|             payload: { start: Date; end: Date }, | ||||
|         ) { | ||||
|             state.localsLoaded.push({ | ||||
|                 start: payload.start.getTime(), | ||||
|                 end: payload.end.getTime(), | ||||
|             }); | ||||
|         }, | ||||
|     addLoaded(state: CalendarLocalsState, payload: { start: Date; end: Date }) { | ||||
|       state.localsLoaded.push({ | ||||
|         start: payload.start.getTime(), | ||||
|         end: payload.end.getTime(), | ||||
|       }); | ||||
|     }, | ||||
|     actions: { | ||||
|         fetchLocals( | ||||
|             ctx: Context, | ||||
|             payload: { start: Date; end: Date }, | ||||
|         ): Promise<null> { | ||||
|             const start = payload.start; | ||||
|             const end = payload.end; | ||||
|   }, | ||||
|   actions: { | ||||
|     fetchLocals( | ||||
|       ctx: Context, | ||||
|       payload: { start: Date; end: Date }, | ||||
|     ): Promise<null> { | ||||
|       const start = payload.start; | ||||
|       const end = payload.end; | ||||
|  | ||||
|             if (ctx.rootGetters["me/getMe"] === null) { | ||||
|                 return Promise.resolve(null); | ||||
|             } | ||||
|       if (ctx.rootGetters["me/getMe"] === null) { | ||||
|         return Promise.resolve(null); | ||||
|       } | ||||
|  | ||||
|             if (ctx.getters.isLocalsLoaded({ start, end })) { | ||||
|                 return Promise.resolve(ctx.getters.getRangeSource); | ||||
|             } | ||||
|       if (ctx.getters.isLocalsLoaded({ start, end })) { | ||||
|         return Promise.resolve(ctx.getters.getRangeSource); | ||||
|       } | ||||
|  | ||||
|             ctx.commit("addLoaded", { | ||||
|                 start: start, | ||||
|                 end: end, | ||||
|             }); | ||||
|       ctx.commit("addLoaded", { | ||||
|         start: start, | ||||
|         end: end, | ||||
|       }); | ||||
|  | ||||
|             return fetchCalendarLocalForUser( | ||||
|                 ctx.rootGetters["me/getMe"], | ||||
|                 start, | ||||
|                 end, | ||||
|             ) | ||||
|                 .then((remotes: CalendarLight[]) => { | ||||
|                     // to be add when reactivity problem will be solve ? | ||||
|                     //ctx.commit('addRemotes', remotes); | ||||
|                     const inputs = remotes | ||||
|                         .map((cr) => localsToFullCalendarEvent(cr)) | ||||
|                         .map((cr) => ({ | ||||
|                             ...cr, | ||||
|                             backgroundColor: COLORS[0], | ||||
|                             textColor: "black", | ||||
|                             editable: false, | ||||
|                         })); | ||||
|                     ctx.commit("calendarRanges/addExternals", inputs, { | ||||
|                         root: true, | ||||
|                     }); | ||||
|                     return Promise.resolve(null); | ||||
|                 }) | ||||
|                 .catch((e: TransportExceptionInterface) => { | ||||
|                     console.error(e); | ||||
|       return fetchCalendarLocalForUser(ctx.rootGetters["me/getMe"], start, end) | ||||
|         .then((remotes: CalendarLight[]) => { | ||||
|           // to be add when reactivity problem will be solve ? | ||||
|           //ctx.commit('addRemotes', remotes); | ||||
|           const inputs = remotes | ||||
|             .map((cr) => localsToFullCalendarEvent(cr)) | ||||
|             .map((cr) => ({ | ||||
|               ...cr, | ||||
|               backgroundColor: COLORS[0], | ||||
|               textColor: "black", | ||||
|               editable: false, | ||||
|             })); | ||||
|           ctx.commit("calendarRanges/addExternals", inputs, { | ||||
|             root: true, | ||||
|           }); | ||||
|           return Promise.resolve(null); | ||||
|         }) | ||||
|         .catch((e: TransportExceptionInterface) => { | ||||
|           console.error(e); | ||||
|  | ||||
|                     return Promise.resolve(null); | ||||
|                 }); | ||||
|         }, | ||||
|           return Promise.resolve(null); | ||||
|         }); | ||||
|     }, | ||||
|   }, | ||||
| } as Module<CalendarLocalsState, State>; | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { State } from "./../index"; | ||||
| import { ActionContext, Module } from "vuex"; | ||||
| import { | ||||
|     CalendarRange, | ||||
|     CalendarRangeCreate, | ||||
|     CalendarRangeEdit, | ||||
|     isEventInputCalendarRange, | ||||
|   CalendarRange, | ||||
|   CalendarRangeCreate, | ||||
|   CalendarRangeEdit, | ||||
|   isEventInputCalendarRange, | ||||
| } from "../../../../types"; | ||||
| import { Location } from "../../../../../../../ChillMainBundle/Resources/public/types"; | ||||
| import { fetchCalendarRangeForUser } from "../../../Calendar/api"; | ||||
| @@ -12,369 +12,332 @@ import { calendarRangeToFullCalendarEvent } from "../../../Calendar/store/utils" | ||||
| import { EventInput } from "@fullcalendar/core"; | ||||
| import { makeFetch } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods"; | ||||
| import { | ||||
|     datetimeToISO, | ||||
|     dateToISO, | ||||
|     ISOToDatetime, | ||||
|   datetimeToISO, | ||||
|   dateToISO, | ||||
|   ISOToDatetime, | ||||
| } from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date"; | ||||
| import type { EventInputCalendarRange } from "../../../../types"; | ||||
|  | ||||
| export interface CalendarRangesState { | ||||
|     ranges: (EventInput | EventInputCalendarRange)[]; | ||||
|     rangesLoaded: { start: number; end: number }[]; | ||||
|     rangesIndex: Set<string>; | ||||
|     key: number; | ||||
|   ranges: (EventInput | EventInputCalendarRange)[]; | ||||
|   rangesLoaded: { start: number; end: number }[]; | ||||
|   rangesIndex: Set<string>; | ||||
|   key: number; | ||||
| } | ||||
|  | ||||
| type Context = ActionContext<CalendarRangesState, State>; | ||||
|  | ||||
| export default { | ||||
|     namespaced: true, | ||||
|     state: (): CalendarRangesState => ({ | ||||
|         ranges: [], | ||||
|         rangesLoaded: [], | ||||
|         rangesIndex: new Set<string>(), | ||||
|         key: 0, | ||||
|     }), | ||||
|     getters: { | ||||
|         isRangeLoaded: | ||||
|             (state: CalendarRangesState) => | ||||
|             ({ start, end }: { start: Date; end: Date }): boolean => { | ||||
|                 for (const range of state.rangesLoaded) { | ||||
|                     if ( | ||||
|                         start.getTime() === range.start && | ||||
|                         end.getTime() === range.end | ||||
|                     ) { | ||||
|                         return true; | ||||
|                     } | ||||
|                 } | ||||
|   namespaced: true, | ||||
|   state: (): CalendarRangesState => ({ | ||||
|     ranges: [], | ||||
|     rangesLoaded: [], | ||||
|     rangesIndex: new Set<string>(), | ||||
|     key: 0, | ||||
|   }), | ||||
|   getters: { | ||||
|     isRangeLoaded: | ||||
|       (state: CalendarRangesState) => | ||||
|       ({ start, end }: { start: Date; end: Date }): boolean => { | ||||
|         for (const range of state.rangesLoaded) { | ||||
|           if (start.getTime() === range.start && end.getTime() === range.end) { | ||||
|             return true; | ||||
|           } | ||||
|         } | ||||
|  | ||||
|                 return false; | ||||
|             }, | ||||
|         getRangesOnDate: | ||||
|             (state: CalendarRangesState) => | ||||
|             (date: Date): EventInputCalendarRange[] => { | ||||
|                 const founds = []; | ||||
|                 const dateStr = dateToISO(date) as string; | ||||
|         return false; | ||||
|       }, | ||||
|     getRangesOnDate: | ||||
|       (state: CalendarRangesState) => | ||||
|       (date: Date): EventInputCalendarRange[] => { | ||||
|         const founds = []; | ||||
|         const dateStr = dateToISO(date) as string; | ||||
|  | ||||
|                 for (const range of state.ranges) { | ||||
|                     if ( | ||||
|                         isEventInputCalendarRange(range) && | ||||
|                         range.start.startsWith(dateStr) | ||||
|                     ) { | ||||
|                         founds.push(range); | ||||
|                     } | ||||
|                 } | ||||
|         for (const range of state.ranges) { | ||||
|           if ( | ||||
|             isEventInputCalendarRange(range) && | ||||
|             range.start.startsWith(dateStr) | ||||
|           ) { | ||||
|             founds.push(range); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|                 return founds; | ||||
|             }, | ||||
|         getRangesOnWeek: | ||||
|             (state: CalendarRangesState) => | ||||
|             (mondayDate: Date): EventInputCalendarRange[] => { | ||||
|                 const founds = []; | ||||
|                 for (const d of Array.from(Array(7).keys())) { | ||||
|                     const dateOfWeek = new Date(mondayDate); | ||||
|                     dateOfWeek.setDate(mondayDate.getDate() + d); | ||||
|                     const dateStr = dateToISO(dateOfWeek) as string; | ||||
|                     for (const range of state.ranges) { | ||||
|                         if ( | ||||
|                             isEventInputCalendarRange(range) && | ||||
|                             range.start.startsWith(dateStr) | ||||
|                         ) { | ||||
|                             founds.push(range); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|         return founds; | ||||
|       }, | ||||
|     getRangesOnWeek: | ||||
|       (state: CalendarRangesState) => | ||||
|       (mondayDate: Date): EventInputCalendarRange[] => { | ||||
|         const founds = []; | ||||
|         for (const d of Array.from(Array(7).keys())) { | ||||
|           const dateOfWeek = new Date(mondayDate); | ||||
|           dateOfWeek.setDate(mondayDate.getDate() + d); | ||||
|           const dateStr = dateToISO(dateOfWeek) as string; | ||||
|           for (const range of state.ranges) { | ||||
|             if ( | ||||
|               isEventInputCalendarRange(range) && | ||||
|               range.start.startsWith(dateStr) | ||||
|             ) { | ||||
|               founds.push(range); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|  | ||||
|                 return founds; | ||||
|             }, | ||||
|         return founds; | ||||
|       }, | ||||
|   }, | ||||
|   mutations: { | ||||
|     addRanges(state: CalendarRangesState, ranges: CalendarRange[]) { | ||||
|       const toAdd = ranges | ||||
|         .map((cr) => calendarRangeToFullCalendarEvent(cr)) | ||||
|         .map((cr) => ({ | ||||
|           ...cr, | ||||
|           backgroundColor: "white", | ||||
|           borderColor: "#3788d8", | ||||
|           textColor: "black", | ||||
|         })) | ||||
|         .filter((r) => !state.rangesIndex.has(r.id)); | ||||
|  | ||||
|       toAdd.forEach((r) => { | ||||
|         state.rangesIndex.add(r.id); | ||||
|         state.ranges.push(r); | ||||
|       }); | ||||
|       state.key = state.key + toAdd.length; | ||||
|     }, | ||||
|     mutations: { | ||||
|         addRanges(state: CalendarRangesState, ranges: CalendarRange[]) { | ||||
|             const toAdd = ranges | ||||
|                 .map((cr) => calendarRangeToFullCalendarEvent(cr)) | ||||
|                 .map((cr) => ({ | ||||
|                     ...cr, | ||||
|                     backgroundColor: "white", | ||||
|                     borderColor: "#3788d8", | ||||
|                     textColor: "black", | ||||
|                 })) | ||||
|                 .filter((r) => !state.rangesIndex.has(r.id)); | ||||
|     addExternals( | ||||
|       state: CalendarRangesState, | ||||
|       externalEvents: (EventInput & { id: string })[], | ||||
|     ) { | ||||
|       const toAdd = externalEvents.filter((r) => !state.rangesIndex.has(r.id)); | ||||
|  | ||||
|             toAdd.forEach((r) => { | ||||
|                 state.rangesIndex.add(r.id); | ||||
|                 state.ranges.push(r); | ||||
|             }); | ||||
|             state.key = state.key + toAdd.length; | ||||
|         }, | ||||
|         addExternals( | ||||
|             state: CalendarRangesState, | ||||
|             externalEvents: (EventInput & { id: string })[], | ||||
|         ) { | ||||
|             const toAdd = externalEvents.filter( | ||||
|                 (r) => !state.rangesIndex.has(r.id), | ||||
|             ); | ||||
|  | ||||
|             toAdd.forEach((r) => { | ||||
|                 state.rangesIndex.add(r.id); | ||||
|                 state.ranges.push(r); | ||||
|             }); | ||||
|             state.key = state.key + toAdd.length; | ||||
|         }, | ||||
|         addLoaded( | ||||
|             state: CalendarRangesState, | ||||
|             payload: { start: Date; end: Date }, | ||||
|         ) { | ||||
|             state.rangesLoaded.push({ | ||||
|                 start: payload.start.getTime(), | ||||
|                 end: payload.end.getTime(), | ||||
|             }); | ||||
|         }, | ||||
|         addRange(state: CalendarRangesState, payload: CalendarRange) { | ||||
|             const asEvent = calendarRangeToFullCalendarEvent(payload); | ||||
|             state.ranges.push({ | ||||
|                 ...asEvent, | ||||
|                 backgroundColor: "white", | ||||
|                 borderColor: "#3788d8", | ||||
|                 textColor: "black", | ||||
|             }); | ||||
|             state.rangesIndex.add(asEvent.id); | ||||
|             state.key = state.key + 1; | ||||
|         }, | ||||
|         removeRange(state: CalendarRangesState, calendarRangeId: number) { | ||||
|             const found = state.ranges.find( | ||||
|                 (r) => | ||||
|                     r.calendarRangeId === calendarRangeId && r.is === "range", | ||||
|             ); | ||||
|  | ||||
|             if (found !== undefined) { | ||||
|                 state.ranges = state.ranges.filter( | ||||
|                     (r) => | ||||
|                         !( | ||||
|                             r.calendarRangeId === calendarRangeId && | ||||
|                             r.is === "range" | ||||
|                         ), | ||||
|                 ); | ||||
|  | ||||
|                 if (typeof found.id === "string") { | ||||
|                     // should always be true | ||||
|                     state.rangesIndex.delete(found.id); | ||||
|                 } | ||||
|  | ||||
|                 state.key = state.key + 1; | ||||
|             } | ||||
|         }, | ||||
|         updateRange(state: CalendarRangesState, range: CalendarRange) { | ||||
|             const found = state.ranges.find( | ||||
|                 (r) => r.calendarRangeId === range.id && r.is === "range", | ||||
|             ); | ||||
|             const newEvent = calendarRangeToFullCalendarEvent(range); | ||||
|  | ||||
|             if (found !== undefined) { | ||||
|                 found.start = newEvent.start; | ||||
|                 found.end = newEvent.end; | ||||
|                 found.locationId = range.location.id; | ||||
|                 found.locationName = range.location.name; | ||||
|             } | ||||
|  | ||||
|             state.key = state.key + 1; | ||||
|         }, | ||||
|       toAdd.forEach((r) => { | ||||
|         state.rangesIndex.add(r.id); | ||||
|         state.ranges.push(r); | ||||
|       }); | ||||
|       state.key = state.key + toAdd.length; | ||||
|     }, | ||||
|     actions: { | ||||
|         fetchRanges( | ||||
|             ctx: Context, | ||||
|             payload: { start: Date; end: Date }, | ||||
|         ): Promise<null> { | ||||
|             const start = payload.start; | ||||
|             const end = payload.end; | ||||
|  | ||||
|             if (ctx.rootGetters["me/getMe"] === null) { | ||||
|                 return Promise.resolve(ctx.getters.getRangeSource); | ||||
|             } | ||||
|  | ||||
|             if (ctx.getters.isRangeLoaded({ start, end })) { | ||||
|                 return Promise.resolve(ctx.getters.getRangeSource); | ||||
|             } | ||||
|  | ||||
|             ctx.commit("addLoaded", { | ||||
|                 start: start, | ||||
|                 end: end, | ||||
|             }); | ||||
|  | ||||
|             return fetchCalendarRangeForUser( | ||||
|                 ctx.rootGetters["me/getMe"], | ||||
|                 start, | ||||
|                 end, | ||||
|             ).then((ranges: CalendarRange[]) => { | ||||
|                 ctx.commit("addRanges", ranges); | ||||
|                 return Promise.resolve(null); | ||||
|             }); | ||||
|         }, | ||||
|         createRange( | ||||
|             ctx: Context, | ||||
|             { | ||||
|                 start, | ||||
|                 end, | ||||
|                 location, | ||||
|             }: { start: Date; end: Date; location: Location }, | ||||
|         ): Promise<null> { | ||||
|             const url = `/api/1.0/calendar/calendar-range.json?`; | ||||
|  | ||||
|             if (ctx.rootState.me.me === null) { | ||||
|                 throw new Error("user is currently null"); | ||||
|             } | ||||
|  | ||||
|             const body = { | ||||
|                 user: { | ||||
|                     id: ctx.rootState.me.me.id, | ||||
|                     type: "user", | ||||
|                 }, | ||||
|                 startDate: { | ||||
|                     datetime: datetimeToISO(start), | ||||
|                 }, | ||||
|                 endDate: { | ||||
|                     datetime: datetimeToISO(end), | ||||
|                 }, | ||||
|                 location: { | ||||
|                     id: location.id, | ||||
|                     type: "location", | ||||
|                 }, | ||||
|             } as CalendarRangeCreate; | ||||
|  | ||||
|             return makeFetch<CalendarRangeCreate, CalendarRange>( | ||||
|                 "POST", | ||||
|                 url, | ||||
|                 body, | ||||
|             ) | ||||
|                 .then((newRange) => { | ||||
|                     ctx.commit("addRange", newRange); | ||||
|  | ||||
|                     return Promise.resolve(null); | ||||
|                 }) | ||||
|                 .catch((error) => { | ||||
|                     console.error(error); | ||||
|  | ||||
|                     throw error; | ||||
|                 }); | ||||
|         }, | ||||
|         deleteRange(ctx: Context, calendarRangeId: number) { | ||||
|             const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`; | ||||
|  | ||||
|             makeFetch<undefined, never>("DELETE", url).then(() => { | ||||
|                 ctx.commit("removeRange", calendarRangeId); | ||||
|             }); | ||||
|         }, | ||||
|         patchRangeTime( | ||||
|             ctx, | ||||
|             { | ||||
|                 calendarRangeId, | ||||
|                 start, | ||||
|                 end, | ||||
|             }: { calendarRangeId: number; start: Date; end: Date }, | ||||
|         ): Promise<null> { | ||||
|             const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`; | ||||
|             const body = { | ||||
|                 startDate: { | ||||
|                     datetime: datetimeToISO(start), | ||||
|                 }, | ||||
|                 endDate: { | ||||
|                     datetime: datetimeToISO(end), | ||||
|                 }, | ||||
|             } as CalendarRangeEdit; | ||||
|  | ||||
|             return makeFetch<CalendarRangeEdit, CalendarRange>( | ||||
|                 "PATCH", | ||||
|                 url, | ||||
|                 body, | ||||
|             ) | ||||
|                 .then((range) => { | ||||
|                     ctx.commit("updateRange", range); | ||||
|                     return Promise.resolve(null); | ||||
|                 }) | ||||
|                 .catch((error) => { | ||||
|                     console.error(error); | ||||
|                     return Promise.resolve(null); | ||||
|                 }); | ||||
|         }, | ||||
|         patchRangeLocation( | ||||
|             ctx, | ||||
|             { | ||||
|                 location, | ||||
|                 calendarRangeId, | ||||
|             }: { location: Location; calendarRangeId: number }, | ||||
|         ): Promise<null> { | ||||
|             const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`; | ||||
|             const body = { | ||||
|                 location: { | ||||
|                     id: location.id, | ||||
|                     type: "location", | ||||
|                 }, | ||||
|             } as CalendarRangeEdit; | ||||
|  | ||||
|             return makeFetch<CalendarRangeEdit, CalendarRange>( | ||||
|                 "PATCH", | ||||
|                 url, | ||||
|                 body, | ||||
|             ) | ||||
|                 .then((range) => { | ||||
|                     ctx.commit("updateRange", range); | ||||
|                     return Promise.resolve(null); | ||||
|                 }) | ||||
|                 .catch((error) => { | ||||
|                     console.error(error); | ||||
|                     return Promise.resolve(null); | ||||
|                 }); | ||||
|         }, | ||||
|         copyFromDayToAnotherDay( | ||||
|             ctx, | ||||
|             { from, to }: { from: Date; to: Date }, | ||||
|         ): Promise<null> { | ||||
|             const rangesToCopy: EventInputCalendarRange[] = | ||||
|                 ctx.getters["getRangesOnDate"](from); | ||||
|             const promises = []; | ||||
|  | ||||
|             for (const r of rangesToCopy) { | ||||
|                 const start = new Date(ISOToDatetime(r.start) as Date); | ||||
|                 start.setFullYear( | ||||
|                     to.getFullYear(), | ||||
|                     to.getMonth(), | ||||
|                     to.getDate(), | ||||
|                 ); | ||||
|                 const end = new Date(ISOToDatetime(r.end) as Date); | ||||
|                 end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate()); | ||||
|                 const location = ctx.rootGetters["locations/getLocationById"]( | ||||
|                     r.locationId, | ||||
|                 ); | ||||
|  | ||||
|                 promises.push( | ||||
|                     ctx.dispatch("createRange", { start, end, location }), | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             return Promise.all(promises).then(() => Promise.resolve(null)); | ||||
|         }, | ||||
|         copyFromWeekToAnotherWeek( | ||||
|             ctx: Context, | ||||
|             { fromMonday, toMonday }: { fromMonday: Date; toMonday: Date }, | ||||
|         ): Promise<null> { | ||||
|             const rangesToCopy: EventInputCalendarRange[] = | ||||
|                 ctx.getters["getRangesOnWeek"](fromMonday); | ||||
|             const promises = []; | ||||
|             const diffTime = toMonday.getTime() - fromMonday.getTime(); | ||||
|             for (const r of rangesToCopy) { | ||||
|                 const start = new Date(ISOToDatetime(r.start) as Date); | ||||
|                 const end = new Date(ISOToDatetime(r.end) as Date); | ||||
|                 start.setTime(start.getTime() + diffTime); | ||||
|                 end.setTime(end.getTime() + diffTime); | ||||
|                 const location = ctx.rootGetters["locations/getLocationById"]( | ||||
|                     r.locationId, | ||||
|                 ); | ||||
|  | ||||
|                 promises.push( | ||||
|                     ctx.dispatch("createRange", { start, end, location }), | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             return Promise.all(promises).then(() => Promise.resolve(null)); | ||||
|         }, | ||||
|     addLoaded(state: CalendarRangesState, payload: { start: Date; end: Date }) { | ||||
|       state.rangesLoaded.push({ | ||||
|         start: payload.start.getTime(), | ||||
|         end: payload.end.getTime(), | ||||
|       }); | ||||
|     }, | ||||
|     addRange(state: CalendarRangesState, payload: CalendarRange) { | ||||
|       const asEvent = calendarRangeToFullCalendarEvent(payload); | ||||
|       state.ranges.push({ | ||||
|         ...asEvent, | ||||
|         backgroundColor: "white", | ||||
|         borderColor: "#3788d8", | ||||
|         textColor: "black", | ||||
|       }); | ||||
|       state.rangesIndex.add(asEvent.id); | ||||
|       state.key = state.key + 1; | ||||
|     }, | ||||
|     removeRange(state: CalendarRangesState, calendarRangeId: number) { | ||||
|       const found = state.ranges.find( | ||||
|         (r) => r.calendarRangeId === calendarRangeId && r.is === "range", | ||||
|       ); | ||||
|  | ||||
|       if (found !== undefined) { | ||||
|         state.ranges = state.ranges.filter( | ||||
|           (r) => !(r.calendarRangeId === calendarRangeId && r.is === "range"), | ||||
|         ); | ||||
|  | ||||
|         if (typeof found.id === "string") { | ||||
|           // should always be true | ||||
|           state.rangesIndex.delete(found.id); | ||||
|         } | ||||
|  | ||||
|         state.key = state.key + 1; | ||||
|       } | ||||
|     }, | ||||
|     updateRange(state: CalendarRangesState, range: CalendarRange) { | ||||
|       const found = state.ranges.find( | ||||
|         (r) => r.calendarRangeId === range.id && r.is === "range", | ||||
|       ); | ||||
|       const newEvent = calendarRangeToFullCalendarEvent(range); | ||||
|  | ||||
|       if (found !== undefined) { | ||||
|         found.start = newEvent.start; | ||||
|         found.end = newEvent.end; | ||||
|         found.locationId = range.location.id; | ||||
|         found.locationName = range.location.name; | ||||
|       } | ||||
|  | ||||
|       state.key = state.key + 1; | ||||
|     }, | ||||
|   }, | ||||
|   actions: { | ||||
|     fetchRanges( | ||||
|       ctx: Context, | ||||
|       payload: { start: Date; end: Date }, | ||||
|     ): Promise<null> { | ||||
|       const start = payload.start; | ||||
|       const end = payload.end; | ||||
|  | ||||
|       if (ctx.rootGetters["me/getMe"] === null) { | ||||
|         return Promise.resolve(ctx.getters.getRangeSource); | ||||
|       } | ||||
|  | ||||
|       if (ctx.getters.isRangeLoaded({ start, end })) { | ||||
|         return Promise.resolve(ctx.getters.getRangeSource); | ||||
|       } | ||||
|  | ||||
|       ctx.commit("addLoaded", { | ||||
|         start: start, | ||||
|         end: end, | ||||
|       }); | ||||
|  | ||||
|       return fetchCalendarRangeForUser( | ||||
|         ctx.rootGetters["me/getMe"], | ||||
|         start, | ||||
|         end, | ||||
|       ).then((ranges: CalendarRange[]) => { | ||||
|         ctx.commit("addRanges", ranges); | ||||
|         return Promise.resolve(null); | ||||
|       }); | ||||
|     }, | ||||
|     createRange( | ||||
|       ctx: Context, | ||||
|       { start, end, location }: { start: Date; end: Date; location: Location }, | ||||
|     ): Promise<null> { | ||||
|       const url = `/api/1.0/calendar/calendar-range.json?`; | ||||
|  | ||||
|       if (ctx.rootState.me.me === null) { | ||||
|         throw new Error("user is currently null"); | ||||
|       } | ||||
|  | ||||
|       const body = { | ||||
|         user: { | ||||
|           id: ctx.rootState.me.me.id, | ||||
|           type: "user", | ||||
|         }, | ||||
|         startDate: { | ||||
|           datetime: datetimeToISO(start), | ||||
|         }, | ||||
|         endDate: { | ||||
|           datetime: datetimeToISO(end), | ||||
|         }, | ||||
|         location: { | ||||
|           id: location.id, | ||||
|           type: "location", | ||||
|         }, | ||||
|       } as CalendarRangeCreate; | ||||
|  | ||||
|       return makeFetch<CalendarRangeCreate, CalendarRange>("POST", url, body) | ||||
|         .then((newRange) => { | ||||
|           ctx.commit("addRange", newRange); | ||||
|  | ||||
|           return Promise.resolve(null); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error(error); | ||||
|  | ||||
|           throw error; | ||||
|         }); | ||||
|     }, | ||||
|     deleteRange(ctx: Context, calendarRangeId: number) { | ||||
|       const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`; | ||||
|  | ||||
|       makeFetch<undefined, never>("DELETE", url).then(() => { | ||||
|         ctx.commit("removeRange", calendarRangeId); | ||||
|       }); | ||||
|     }, | ||||
|     patchRangeTime( | ||||
|       ctx, | ||||
|       { | ||||
|         calendarRangeId, | ||||
|         start, | ||||
|         end, | ||||
|       }: { calendarRangeId: number; start: Date; end: Date }, | ||||
|     ): Promise<null> { | ||||
|       const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`; | ||||
|       const body = { | ||||
|         startDate: { | ||||
|           datetime: datetimeToISO(start), | ||||
|         }, | ||||
|         endDate: { | ||||
|           datetime: datetimeToISO(end), | ||||
|         }, | ||||
|       } as CalendarRangeEdit; | ||||
|  | ||||
|       return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body) | ||||
|         .then((range) => { | ||||
|           ctx.commit("updateRange", range); | ||||
|           return Promise.resolve(null); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error(error); | ||||
|           return Promise.resolve(null); | ||||
|         }); | ||||
|     }, | ||||
|     patchRangeLocation( | ||||
|       ctx, | ||||
|       { | ||||
|         location, | ||||
|         calendarRangeId, | ||||
|       }: { location: Location; calendarRangeId: number }, | ||||
|     ): Promise<null> { | ||||
|       const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`; | ||||
|       const body = { | ||||
|         location: { | ||||
|           id: location.id, | ||||
|           type: "location", | ||||
|         }, | ||||
|       } as CalendarRangeEdit; | ||||
|  | ||||
|       return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body) | ||||
|         .then((range) => { | ||||
|           ctx.commit("updateRange", range); | ||||
|           return Promise.resolve(null); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error(error); | ||||
|           return Promise.resolve(null); | ||||
|         }); | ||||
|     }, | ||||
|     copyFromDayToAnotherDay( | ||||
|       ctx, | ||||
|       { from, to }: { from: Date; to: Date }, | ||||
|     ): Promise<null> { | ||||
|       const rangesToCopy: EventInputCalendarRange[] = | ||||
|         ctx.getters["getRangesOnDate"](from); | ||||
|       const promises = []; | ||||
|  | ||||
|       for (const r of rangesToCopy) { | ||||
|         const start = new Date(ISOToDatetime(r.start) as Date); | ||||
|         start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate()); | ||||
|         const end = new Date(ISOToDatetime(r.end) as Date); | ||||
|         end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate()); | ||||
|         const location = ctx.rootGetters["locations/getLocationById"]( | ||||
|           r.locationId, | ||||
|         ); | ||||
|  | ||||
|         promises.push(ctx.dispatch("createRange", { start, end, location })); | ||||
|       } | ||||
|  | ||||
|       return Promise.all(promises).then(() => Promise.resolve(null)); | ||||
|     }, | ||||
|     copyFromWeekToAnotherWeek( | ||||
|       ctx: Context, | ||||
|       { fromMonday, toMonday }: { fromMonday: Date; toMonday: Date }, | ||||
|     ): Promise<null> { | ||||
|       const rangesToCopy: EventInputCalendarRange[] = | ||||
|         ctx.getters["getRangesOnWeek"](fromMonday); | ||||
|       const promises = []; | ||||
|       const diffTime = toMonday.getTime() - fromMonday.getTime(); | ||||
|       for (const r of rangesToCopy) { | ||||
|         const start = new Date(ISOToDatetime(r.start) as Date); | ||||
|         const end = new Date(ISOToDatetime(r.end) as Date); | ||||
|         start.setTime(start.getTime() + diffTime); | ||||
|         end.setTime(end.getTime() + diffTime); | ||||
|         const location = ctx.rootGetters["locations/getLocationById"]( | ||||
|           r.locationId, | ||||
|         ); | ||||
|  | ||||
|         promises.push(ctx.dispatch("createRange", { start, end, location })); | ||||
|       } | ||||
|  | ||||
|       return Promise.all(promises).then(() => Promise.resolve(null)); | ||||
|     }, | ||||
|   }, | ||||
| } as Module<CalendarRangesState, State>; | ||||
|   | ||||
| @@ -8,109 +8,102 @@ import { TransportExceptionInterface } from "../../../../../../../ChillMainBundl | ||||
| import { COLORS } from "../../../Calendar/const"; | ||||
|  | ||||
| export interface CalendarRemotesState { | ||||
|     remotes: EventInput[]; | ||||
|     remotesLoaded: { start: number; end: number }[]; | ||||
|     remotesIndex: Set<string>; | ||||
|     key: number; | ||||
|   remotes: EventInput[]; | ||||
|   remotesLoaded: { start: number; end: number }[]; | ||||
|   remotesIndex: Set<string>; | ||||
|   key: number; | ||||
| } | ||||
|  | ||||
| type Context = ActionContext<CalendarRemotesState, State>; | ||||
|  | ||||
| export default { | ||||
|     namespaced: true, | ||||
|     state: (): CalendarRemotesState => ({ | ||||
|         remotes: [], | ||||
|         remotesLoaded: [], | ||||
|         remotesIndex: new Set<string>(), | ||||
|         key: 0, | ||||
|     }), | ||||
|     getters: { | ||||
|         isRemotesLoaded: | ||||
|             (state: CalendarRemotesState) => | ||||
|             ({ start, end }: { start: Date; end: Date }): boolean => { | ||||
|                 for (const range of state.remotesLoaded) { | ||||
|                     if ( | ||||
|                         start.getTime() === range.start && | ||||
|                         end.getTime() === range.end | ||||
|                     ) { | ||||
|                         return true; | ||||
|                     } | ||||
|                 } | ||||
|   namespaced: true, | ||||
|   state: (): CalendarRemotesState => ({ | ||||
|     remotes: [], | ||||
|     remotesLoaded: [], | ||||
|     remotesIndex: new Set<string>(), | ||||
|     key: 0, | ||||
|   }), | ||||
|   getters: { | ||||
|     isRemotesLoaded: | ||||
|       (state: CalendarRemotesState) => | ||||
|       ({ start, end }: { start: Date; end: Date }): boolean => { | ||||
|         for (const range of state.remotesLoaded) { | ||||
|           if (start.getTime() === range.start && end.getTime() === range.end) { | ||||
|             return true; | ||||
|           } | ||||
|         } | ||||
|  | ||||
|                 return false; | ||||
|             }, | ||||
|         return false; | ||||
|       }, | ||||
|   }, | ||||
|   mutations: { | ||||
|     addRemotes(state: CalendarRemotesState, ranges: CalendarRemote[]) { | ||||
|       console.log("addRemotes", ranges); | ||||
|  | ||||
|       const toAdd = ranges | ||||
|         .map((cr) => remoteToFullCalendarEvent(cr)) | ||||
|         .filter((r) => !state.remotesIndex.has(r.id)); | ||||
|  | ||||
|       toAdd.forEach((r) => { | ||||
|         state.remotesIndex.add(r.id); | ||||
|         state.remotes.push(r); | ||||
|       }); | ||||
|       state.key = state.key + toAdd.length; | ||||
|     }, | ||||
|     mutations: { | ||||
|         addRemotes(state: CalendarRemotesState, ranges: CalendarRemote[]) { | ||||
|             console.log("addRemotes", ranges); | ||||
|  | ||||
|             const toAdd = ranges | ||||
|                 .map((cr) => remoteToFullCalendarEvent(cr)) | ||||
|                 .filter((r) => !state.remotesIndex.has(r.id)); | ||||
|  | ||||
|             toAdd.forEach((r) => { | ||||
|                 state.remotesIndex.add(r.id); | ||||
|                 state.remotes.push(r); | ||||
|             }); | ||||
|             state.key = state.key + toAdd.length; | ||||
|         }, | ||||
|         addLoaded( | ||||
|             state: CalendarRemotesState, | ||||
|             payload: { start: Date; end: Date }, | ||||
|         ) { | ||||
|             state.remotesLoaded.push({ | ||||
|                 start: payload.start.getTime(), | ||||
|                 end: payload.end.getTime(), | ||||
|             }); | ||||
|         }, | ||||
|     addLoaded( | ||||
|       state: CalendarRemotesState, | ||||
|       payload: { start: Date; end: Date }, | ||||
|     ) { | ||||
|       state.remotesLoaded.push({ | ||||
|         start: payload.start.getTime(), | ||||
|         end: payload.end.getTime(), | ||||
|       }); | ||||
|     }, | ||||
|     actions: { | ||||
|         fetchRemotes( | ||||
|             ctx: Context, | ||||
|             payload: { start: Date; end: Date }, | ||||
|         ): Promise<null> { | ||||
|             const start = payload.start; | ||||
|             const end = payload.end; | ||||
|   }, | ||||
|   actions: { | ||||
|     fetchRemotes( | ||||
|       ctx: Context, | ||||
|       payload: { start: Date; end: Date }, | ||||
|     ): Promise<null> { | ||||
|       const start = payload.start; | ||||
|       const end = payload.end; | ||||
|  | ||||
|             if (ctx.rootGetters["me/getMe"] === null) { | ||||
|                 return Promise.resolve(null); | ||||
|             } | ||||
|       if (ctx.rootGetters["me/getMe"] === null) { | ||||
|         return Promise.resolve(null); | ||||
|       } | ||||
|  | ||||
|             if (ctx.getters.isRemotesLoaded({ start, end })) { | ||||
|                 return Promise.resolve(ctx.getters.getRangeSource); | ||||
|             } | ||||
|       if (ctx.getters.isRemotesLoaded({ start, end })) { | ||||
|         return Promise.resolve(ctx.getters.getRangeSource); | ||||
|       } | ||||
|  | ||||
|             ctx.commit("addLoaded", { | ||||
|                 start: start, | ||||
|                 end: end, | ||||
|             }); | ||||
|       ctx.commit("addLoaded", { | ||||
|         start: start, | ||||
|         end: end, | ||||
|       }); | ||||
|  | ||||
|             return fetchCalendarRemoteForUser( | ||||
|                 ctx.rootGetters["me/getMe"], | ||||
|                 start, | ||||
|                 end, | ||||
|             ) | ||||
|                 .then((remotes: CalendarRemote[]) => { | ||||
|                     // to be add when reactivity problem will be solve ? | ||||
|                     //ctx.commit('addRemotes', remotes); | ||||
|                     const inputs = remotes | ||||
|                         .map((cr) => remoteToFullCalendarEvent(cr)) | ||||
|                         .map((cr) => ({ | ||||
|                             ...cr, | ||||
|                             backgroundColor: COLORS[0], | ||||
|                             textColor: "black", | ||||
|                             editable: false, | ||||
|                         })); | ||||
|                     ctx.commit("calendarRanges/addExternals", inputs, { | ||||
|                         root: true, | ||||
|                     }); | ||||
|                     return Promise.resolve(null); | ||||
|                 }) | ||||
|                 .catch((e: TransportExceptionInterface) => { | ||||
|                     console.error(e); | ||||
|       return fetchCalendarRemoteForUser(ctx.rootGetters["me/getMe"], start, end) | ||||
|         .then((remotes: CalendarRemote[]) => { | ||||
|           // to be add when reactivity problem will be solve ? | ||||
|           //ctx.commit('addRemotes', remotes); | ||||
|           const inputs = remotes | ||||
|             .map((cr) => remoteToFullCalendarEvent(cr)) | ||||
|             .map((cr) => ({ | ||||
|               ...cr, | ||||
|               backgroundColor: COLORS[0], | ||||
|               textColor: "black", | ||||
|               editable: false, | ||||
|             })); | ||||
|           ctx.commit("calendarRanges/addExternals", inputs, { | ||||
|             root: true, | ||||
|           }); | ||||
|           return Promise.resolve(null); | ||||
|         }) | ||||
|         .catch((e: TransportExceptionInterface) => { | ||||
|           console.error(e); | ||||
|  | ||||
|                     return Promise.resolve(null); | ||||
|                 }); | ||||
|         }, | ||||
|           return Promise.resolve(null); | ||||
|         }); | ||||
|     }, | ||||
|   }, | ||||
| } as Module<CalendarRemotesState, State>; | ||||
|   | ||||
| @@ -2,77 +2,77 @@ import { State } from "./../index"; | ||||
| import { ActionContext } from "vuex"; | ||||
|  | ||||
| export interface FullCalendarState { | ||||
|     currentView: { | ||||
|         start: Date | null; | ||||
|         end: Date | null; | ||||
|     }; | ||||
|     key: number; | ||||
|   currentView: { | ||||
|     start: Date | null; | ||||
|     end: Date | null; | ||||
|   }; | ||||
|   key: number; | ||||
| } | ||||
|  | ||||
| type Context = ActionContext<FullCalendarState, State>; | ||||
|  | ||||
| export default { | ||||
|     namespaced: true, | ||||
|     state: (): FullCalendarState => ({ | ||||
|         currentView: { | ||||
|             start: null, | ||||
|             end: null, | ||||
|         }, | ||||
|         key: 0, | ||||
|     }), | ||||
|     mutations: { | ||||
|         setCurrentDatesView: function ( | ||||
|             state: FullCalendarState, | ||||
|             payload: { start: Date; end: Date }, | ||||
|         ): void { | ||||
|             state.currentView.start = payload.start; | ||||
|             state.currentView.end = payload.end; | ||||
|         }, | ||||
|         increaseKey: function (state: FullCalendarState): void { | ||||
|             state.key = state.key + 1; | ||||
|         }, | ||||
|   namespaced: true, | ||||
|   state: (): FullCalendarState => ({ | ||||
|     currentView: { | ||||
|       start: null, | ||||
|       end: null, | ||||
|     }, | ||||
|     actions: { | ||||
|         setCurrentDatesView( | ||||
|             ctx: Context, | ||||
|             { start, end }: { start: Date | null; end: Date | null }, | ||||
|         ): Promise<null> { | ||||
|             console.log("dispatch setCurrentDatesView", { start, end }); | ||||
|  | ||||
|             if ( | ||||
|                 ctx.state.currentView.start !== start || | ||||
|                 ctx.state.currentView.end !== end | ||||
|             ) { | ||||
|                 ctx.commit("setCurrentDatesView", { start, end }); | ||||
|             } | ||||
|  | ||||
|             if (start !== null && end !== null) { | ||||
|                 return Promise.all([ | ||||
|                     ctx | ||||
|                         .dispatch( | ||||
|                             "calendarRanges/fetchRanges", | ||||
|                             { start, end }, | ||||
|                             { root: true }, | ||||
|                         ) | ||||
|                         .then((_) => Promise.resolve(null)), | ||||
|                     ctx | ||||
|                         .dispatch( | ||||
|                             "calendarRemotes/fetchRemotes", | ||||
|                             { start, end }, | ||||
|                             { root: true }, | ||||
|                         ) | ||||
|                         .then((_) => Promise.resolve(null)), | ||||
|                     ctx | ||||
|                         .dispatch( | ||||
|                             "calendarLocals/fetchLocals", | ||||
|                             { start, end }, | ||||
|                             { root: true }, | ||||
|                         ) | ||||
|                         .then((_) => Promise.resolve(null)), | ||||
|                 ]).then((_) => Promise.resolve(null)); | ||||
|             } else { | ||||
|                 return Promise.resolve(null); | ||||
|             } | ||||
|         }, | ||||
|     key: 0, | ||||
|   }), | ||||
|   mutations: { | ||||
|     setCurrentDatesView: function ( | ||||
|       state: FullCalendarState, | ||||
|       payload: { start: Date; end: Date }, | ||||
|     ): void { | ||||
|       state.currentView.start = payload.start; | ||||
|       state.currentView.end = payload.end; | ||||
|     }, | ||||
|     increaseKey: function (state: FullCalendarState): void { | ||||
|       state.key = state.key + 1; | ||||
|     }, | ||||
|   }, | ||||
|   actions: { | ||||
|     setCurrentDatesView( | ||||
|       ctx: Context, | ||||
|       { start, end }: { start: Date | null; end: Date | null }, | ||||
|     ): Promise<null> { | ||||
|       console.log("dispatch setCurrentDatesView", { start, end }); | ||||
|  | ||||
|       if ( | ||||
|         ctx.state.currentView.start !== start || | ||||
|         ctx.state.currentView.end !== end | ||||
|       ) { | ||||
|         ctx.commit("setCurrentDatesView", { start, end }); | ||||
|       } | ||||
|  | ||||
|       if (start !== null && end !== null) { | ||||
|         return Promise.all([ | ||||
|           ctx | ||||
|             .dispatch( | ||||
|               "calendarRanges/fetchRanges", | ||||
|               { start, end }, | ||||
|               { root: true }, | ||||
|             ) | ||||
|             .then((_) => Promise.resolve(null)), | ||||
|           ctx | ||||
|             .dispatch( | ||||
|               "calendarRemotes/fetchRemotes", | ||||
|               { start, end }, | ||||
|               { root: true }, | ||||
|             ) | ||||
|             .then((_) => Promise.resolve(null)), | ||||
|           ctx | ||||
|             .dispatch( | ||||
|               "calendarLocals/fetchLocals", | ||||
|               { start, end }, | ||||
|               { root: true }, | ||||
|             ) | ||||
|             .then((_) => Promise.resolve(null)), | ||||
|         ]).then((_) => Promise.resolve(null)); | ||||
|       } else { | ||||
|         return Promise.resolve(null); | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -5,61 +5,61 @@ import { getLocations } from "../../../../../../../ChillMainBundle/Resources/pub | ||||
| import { whereami } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/user"; | ||||
|  | ||||
| export interface LocationState { | ||||
|     locations: Location[]; | ||||
|     locationPicked: Location | null; | ||||
|     currentLocation: Location | null; | ||||
|   locations: Location[]; | ||||
|   locationPicked: Location | null; | ||||
|   currentLocation: Location | null; | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     namespaced: true, | ||||
|     state: (): LocationState => { | ||||
|         return { | ||||
|             locations: [], | ||||
|             locationPicked: null, | ||||
|             currentLocation: null, | ||||
|         }; | ||||
|   namespaced: true, | ||||
|   state: (): LocationState => { | ||||
|     return { | ||||
|       locations: [], | ||||
|       locationPicked: null, | ||||
|       currentLocation: null, | ||||
|     }; | ||||
|   }, | ||||
|   getters: { | ||||
|     getLocationById: | ||||
|       (state) => | ||||
|       (id: number): Location | undefined => { | ||||
|         return state.locations.find((l) => l.id === id); | ||||
|       }, | ||||
|   }, | ||||
|   mutations: { | ||||
|     setLocations(state, locations): void { | ||||
|       state.locations = locations; | ||||
|     }, | ||||
|     getters: { | ||||
|         getLocationById: | ||||
|             (state) => | ||||
|             (id: number): Location | undefined => { | ||||
|                 return state.locations.find((l) => l.id === id); | ||||
|             }, | ||||
|     }, | ||||
|     mutations: { | ||||
|         setLocations(state, locations): void { | ||||
|             state.locations = locations; | ||||
|         }, | ||||
|         setLocationPicked(state, location: Location | null): void { | ||||
|             if (null === location) { | ||||
|                 state.locationPicked = null; | ||||
|                 return; | ||||
|             } | ||||
|     setLocationPicked(state, location: Location | null): void { | ||||
|       if (null === location) { | ||||
|         state.locationPicked = null; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|             state.locationPicked = | ||||
|                 state.locations.find((l) => l.id === location.id) || null; | ||||
|         }, | ||||
|         setCurrentLocation(state, location: Location | null): void { | ||||
|             if (null === location) { | ||||
|                 state.currentLocation = null; | ||||
|                 return; | ||||
|             } | ||||
|       state.locationPicked = | ||||
|         state.locations.find((l) => l.id === location.id) || null; | ||||
|     }, | ||||
|     setCurrentLocation(state, location: Location | null): void { | ||||
|       if (null === location) { | ||||
|         state.currentLocation = null; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|             state.currentLocation = | ||||
|                 state.locations.find((l) => l.id === location.id) || null; | ||||
|         }, | ||||
|       state.currentLocation = | ||||
|         state.locations.find((l) => l.id === location.id) || null; | ||||
|     }, | ||||
|     actions: { | ||||
|         getLocations(ctx): Promise<void> { | ||||
|             return getLocations().then((locations) => { | ||||
|                 ctx.commit("setLocations", locations); | ||||
|                 return Promise.resolve(); | ||||
|             }); | ||||
|         }, | ||||
|         getCurrentLocation(ctx): Promise<void> { | ||||
|             return whereami().then((location) => { | ||||
|                 ctx.commit("setCurrentLocation", location); | ||||
|             }); | ||||
|         }, | ||||
|   }, | ||||
|   actions: { | ||||
|     getLocations(ctx): Promise<void> { | ||||
|       return getLocations().then((locations) => { | ||||
|         ctx.commit("setLocations", locations); | ||||
|         return Promise.resolve(); | ||||
|       }); | ||||
|     }, | ||||
|     getCurrentLocation(ctx): Promise<void> { | ||||
|       return whereami().then((location) => { | ||||
|         ctx.commit("setCurrentLocation", location); | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
| } as Module<LocationState, State>; | ||||
|   | ||||
| @@ -3,24 +3,24 @@ import { User } from "../../../../../../../ChillMainBundle/Resources/public/type | ||||
| import { ActionContext } from "vuex"; | ||||
|  | ||||
| export interface MeState { | ||||
|     me: User | null; | ||||
|   me: User | null; | ||||
| } | ||||
|  | ||||
| type Context = ActionContext<MeState, State>; | ||||
|  | ||||
| export default { | ||||
|     namespaced: true, | ||||
|     state: (): MeState => ({ | ||||
|         me: null, | ||||
|     }), | ||||
|     getters: { | ||||
|         getMe: function (state: MeState): User | null { | ||||
|             return state.me; | ||||
|         }, | ||||
|   namespaced: true, | ||||
|   state: (): MeState => ({ | ||||
|     me: null, | ||||
|   }), | ||||
|   getters: { | ||||
|     getMe: function (state: MeState): User | null { | ||||
|       return state.me; | ||||
|     }, | ||||
|     mutations: { | ||||
|         setWhoAmi(state: MeState, me: User) { | ||||
|             state.me = me; | ||||
|         }, | ||||
|   }, | ||||
|   mutations: { | ||||
|     setWhoAmi(state: MeState, me: User) { | ||||
|       state.me = me; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -1,51 +1,51 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <h2 class="chill-red"> | ||||
|             {{ $t("choose_your_calendar_user") }} | ||||
|         </h2> | ||||
|         <VueMultiselect | ||||
|             name="field" | ||||
|             id="calendarUserSelector" | ||||
|             v-model="value" | ||||
|             track-by="id" | ||||
|             label="value" | ||||
|             :custom-label="transName" | ||||
|             :placeholder="$t('select_user')" | ||||
|             :multiple="true" | ||||
|             :close-on-select="false" | ||||
|             :allow-empty="true" | ||||
|             :model-value="value" | ||||
|             :select-label="$t('multiselect.select_label')" | ||||
|             :deselect-label="$t('multiselect.deselect_label')" | ||||
|             :selected-label="$t('multiselect.selected_label')" | ||||
|             @select="selectUsers" | ||||
|             @remove="unSelectUsers" | ||||
|             @close="coloriseSelectedValues" | ||||
|             :options="options" | ||||
|         /> | ||||
|     </div> | ||||
|     <div class="form-check"> | ||||
|         <input | ||||
|             type="checkbox" | ||||
|             id="myCalendar" | ||||
|             class="form-check-input" | ||||
|             v-model="showMyCalendarWidget" | ||||
|         /> | ||||
|         <label class="form-check-label" for="myCalendar">{{ | ||||
|             $t("show_my_calendar") | ||||
|         }}</label> | ||||
|     </div> | ||||
|     <div class="form-check"> | ||||
|         <input | ||||
|             type="checkbox" | ||||
|             id="weekends" | ||||
|             class="form-check-input" | ||||
|             @click="toggleWeekends" | ||||
|         /> | ||||
|         <label class="form-check-label" for="weekends">{{ | ||||
|             $t("show_weekends") | ||||
|         }}</label> | ||||
|     </div> | ||||
|   <div> | ||||
|     <h2 class="chill-red"> | ||||
|       {{ $t("choose_your_calendar_user") }} | ||||
|     </h2> | ||||
|     <VueMultiselect | ||||
|       name="field" | ||||
|       id="calendarUserSelector" | ||||
|       v-model="value" | ||||
|       track-by="id" | ||||
|       label="value" | ||||
|       :custom-label="transName" | ||||
|       :placeholder="$t('select_user')" | ||||
|       :multiple="true" | ||||
|       :close-on-select="false" | ||||
|       :allow-empty="true" | ||||
|       :model-value="value" | ||||
|       :select-label="$t('multiselect.select_label')" | ||||
|       :deselect-label="$t('multiselect.deselect_label')" | ||||
|       :selected-label="$t('multiselect.selected_label')" | ||||
|       @select="selectUsers" | ||||
|       @remove="unSelectUsers" | ||||
|       @close="coloriseSelectedValues" | ||||
|       :options="options" | ||||
|     /> | ||||
|   </div> | ||||
|   <div class="form-check"> | ||||
|     <input | ||||
|       type="checkbox" | ||||
|       id="myCalendar" | ||||
|       class="form-check-input" | ||||
|       v-model="showMyCalendarWidget" | ||||
|     /> | ||||
|     <label class="form-check-label" for="myCalendar">{{ | ||||
|       $t("show_my_calendar") | ||||
|     }}</label> | ||||
|   </div> | ||||
|   <div class="form-check"> | ||||
|     <input | ||||
|       type="checkbox" | ||||
|       id="weekends" | ||||
|       class="form-check-input" | ||||
|       @click="toggleWeekends" | ||||
|     /> | ||||
|     <label class="form-check-label" for="weekends">{{ | ||||
|       $t("show_weekends") | ||||
|     }}</label> | ||||
|   </div> | ||||
| </template> | ||||
| <script> | ||||
| import { fetchCalendarRanges, fetchCalendar } from "../../_api/api"; | ||||
| @@ -53,206 +53,183 @@ import VueMultiselect from "vue-multiselect"; | ||||
| import { whoami } from "ChillPersonAssets/vuejs/AccompanyingCourse/api"; | ||||
|  | ||||
| const COLORS = [ | ||||
|     /* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */ | ||||
|     "#8dd3c7", | ||||
|     "#ffffb3", | ||||
|     "#bebada", | ||||
|     "#fb8072", | ||||
|     "#80b1d3", | ||||
|     "#fdb462", | ||||
|     "#b3de69", | ||||
|     "#fccde5", | ||||
|     "#d9d9d9", | ||||
|     "#bc80bd", | ||||
|     "#ccebc5", | ||||
|     "#ffed6f", | ||||
|   /* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */ | ||||
|   "#8dd3c7", | ||||
|   "#ffffb3", | ||||
|   "#bebada", | ||||
|   "#fb8072", | ||||
|   "#80b1d3", | ||||
|   "#fdb462", | ||||
|   "#b3de69", | ||||
|   "#fccde5", | ||||
|   "#d9d9d9", | ||||
|   "#bc80bd", | ||||
|   "#ccebc5", | ||||
|   "#ffed6f", | ||||
| ]; | ||||
|  | ||||
| export default { | ||||
|     name: "CalendarUserSelector", | ||||
|     components: { VueMultiselect }, | ||||
|     props: [ | ||||
|         "users", | ||||
|         "updateEventsSource", | ||||
|         "calendarEvents", | ||||
|         "showMyCalendar", | ||||
|         "toggleMyCalendar", | ||||
|         "toggleWeekends", | ||||
|     ], | ||||
|     data() { | ||||
|         return { | ||||
|             errorMsg: [], | ||||
|             value: [], | ||||
|             options: [], | ||||
|         }; | ||||
|   name: "CalendarUserSelector", | ||||
|   components: { VueMultiselect }, | ||||
|   props: [ | ||||
|     "users", | ||||
|     "updateEventsSource", | ||||
|     "calendarEvents", | ||||
|     "showMyCalendar", | ||||
|     "toggleMyCalendar", | ||||
|     "toggleWeekends", | ||||
|   ], | ||||
|   data() { | ||||
|     return { | ||||
|       errorMsg: [], | ||||
|       value: [], | ||||
|       options: [], | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     showMyCalendarWidget: { | ||||
|       set(value) { | ||||
|         this.toggleMyCalendar(value); | ||||
|         this.updateEventsSource(); | ||||
|       }, | ||||
|       get() { | ||||
|         return this.showMyCalendar; | ||||
|       }, | ||||
|     }, | ||||
|     computed: { | ||||
|         showMyCalendarWidget: { | ||||
|             set(value) { | ||||
|                 this.toggleMyCalendar(value); | ||||
|                 this.updateEventsSource(); | ||||
|             }, | ||||
|             get() { | ||||
|                 return this.showMyCalendar; | ||||
|             }, | ||||
|         }, | ||||
|   }, | ||||
|   methods: { | ||||
|     init() { | ||||
|       this.fetchData(); | ||||
|     }, | ||||
|     methods: { | ||||
|         init() { | ||||
|             this.fetchData(); | ||||
|         }, | ||||
|         fetchData() { | ||||
|             fetchCalendarRanges() | ||||
|                 .then( | ||||
|                     (calendarRanges) => | ||||
|                         new Promise((resolve, reject) => { | ||||
|                             let results = calendarRanges.results; | ||||
|     fetchData() { | ||||
|       fetchCalendarRanges() | ||||
|         .then( | ||||
|           (calendarRanges) => | ||||
|             new Promise((resolve, reject) => { | ||||
|               let results = calendarRanges.results; | ||||
|  | ||||
|                             let users = []; | ||||
|               let users = []; | ||||
|  | ||||
|                             results.forEach((i) => { | ||||
|                                 if (!users.some((j) => i.user.id === j.id)) { | ||||
|                                     let ratio = Math.floor( | ||||
|                                         users.length / COLORS.length, | ||||
|                                     ); | ||||
|                                     let colorIndex = | ||||
|                                         users.length - ratio * COLORS.length; | ||||
|                                     users.push({ | ||||
|                                         id: i.user.id, | ||||
|                                         username: i.user.username, | ||||
|                                         color: COLORS[colorIndex], | ||||
|                                     }); | ||||
|                                 } | ||||
|                             }); | ||||
|  | ||||
|                             let calendarEvents = []; | ||||
|                             users.forEach((u) => { | ||||
|                                 let arr = results | ||||
|                                     .filter((i) => i.user.id === u.id) | ||||
|                                     .map((i) => ({ | ||||
|                                         start: i.startDate.datetime, | ||||
|                                         end: i.endDate.datetime, | ||||
|                                         calendarRangeId: i.id, | ||||
|                                         sourceColor: u.color, | ||||
|                                         //display: 'background' // can be an option for the disponibility | ||||
|                                     })); | ||||
|                                 calendarEvents.push({ | ||||
|                                     events: arr, | ||||
|                                     color: u.color, | ||||
|                                     textColor: "#444444", | ||||
|                                     editable: false, | ||||
|                                     id: u.id, | ||||
|                                 }); | ||||
|                             }); | ||||
|  | ||||
|                             this.users.loaded = users; | ||||
|                             this.options = users; | ||||
|  | ||||
|                             this.calendarEvents.loaded = calendarEvents; | ||||
|                             whoami().then( | ||||
|                                 (me) => | ||||
|                                     new Promise((resolve, reject) => { | ||||
|                                         this.users.logged = me; | ||||
|                                         let currentUser = users.find( | ||||
|                                             (u) => u.id === me.id, | ||||
|                                         ); | ||||
|                                         this.value = currentUser; | ||||
|  | ||||
|                                         fetchCalendar(currentUser.id).then( | ||||
|                                             (calendar) => | ||||
|                                                 new Promise( | ||||
|                                                     (resolve, reject) => { | ||||
|                                                         let results = | ||||
|                                                             calendar.results; | ||||
|                                                         let events = | ||||
|                                                             results.map( | ||||
|                                                                 (i) => ({ | ||||
|                                                                     start: i | ||||
|                                                                         .startDate | ||||
|                                                                         .datetime, | ||||
|                                                                     end: i | ||||
|                                                                         .endDate | ||||
|                                                                         .datetime, | ||||
|                                                                 }), | ||||
|                                                             ); | ||||
|                                                         let calendarEventsCurrentUser = | ||||
|                                                             { | ||||
|                                                                 events: events, | ||||
|                                                                 color: "darkblue", | ||||
|                                                                 id: 1000, | ||||
|                                                                 editable: false, | ||||
|                                                             }; | ||||
|                                                         this.calendarEvents.user = | ||||
|                                                             calendarEventsCurrentUser; | ||||
|  | ||||
|                                                         this.selectUsers( | ||||
|                                                             currentUser, | ||||
|                                                         ); | ||||
|  | ||||
|                                                         resolve(); | ||||
|                                                     }, | ||||
|                                                 ), | ||||
|                                         ); | ||||
|  | ||||
|                                         resolve(); | ||||
|                                     }), | ||||
|                             ); | ||||
|  | ||||
|                             resolve(); | ||||
|                         }), | ||||
|                 ) | ||||
|                 .catch((error) => { | ||||
|                     this.errorMsg.push(error.message); | ||||
|                 }); | ||||
|         }, | ||||
|         transName(value) { | ||||
|             return `${value.username}`; | ||||
|         }, | ||||
|         coloriseSelectedValues() { | ||||
|             let tags = document.querySelectorAll( | ||||
|                 "div.multiselect__tags-wrap", | ||||
|             )[0]; | ||||
|  | ||||
|             if (tags.hasChildNodes()) { | ||||
|                 let children = tags.childNodes; | ||||
|                 for (let i = 0; i < children.length; i++) { | ||||
|                     let child = children[i]; | ||||
|                     if (child.nodeType === Node.ELEMENT_NODE) { | ||||
|                         this.users.selected.forEach((u) => { | ||||
|                             if (child.hasChildNodes()) { | ||||
|                                 if (child.firstChild.innerText == u.username) { | ||||
|                                     child.style.background = u.color; | ||||
|                                     child.firstChild.style.color = "#444444"; | ||||
|                                 } | ||||
|                             } | ||||
|                         }); | ||||
|                     } | ||||
|               results.forEach((i) => { | ||||
|                 if (!users.some((j) => i.user.id === j.id)) { | ||||
|                   let ratio = Math.floor(users.length / COLORS.length); | ||||
|                   let colorIndex = users.length - ratio * COLORS.length; | ||||
|                   users.push({ | ||||
|                     id: i.user.id, | ||||
|                     username: i.user.username, | ||||
|                     color: COLORS[colorIndex], | ||||
|                   }); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         selectEvents() { | ||||
|             let selectedUsersId = this.users.selected.map((a) => a.id); | ||||
|             this.calendarEvents.selected = this.calendarEvents.loaded.filter( | ||||
|                 (a) => selectedUsersId.includes(a.id), | ||||
|             ); | ||||
|         }, | ||||
|         selectUsers(value) { | ||||
|             this.users.selected.push(value); | ||||
|             this.coloriseSelectedValues(); | ||||
|             this.selectEvents(); | ||||
|             this.updateEventsSource(); | ||||
|         }, | ||||
|         unSelectUsers(value) { | ||||
|             this.users.selected = this.users.selected.filter( | ||||
|                 (a) => a.id != value.id, | ||||
|             ); | ||||
|             this.selectEvents(); | ||||
|             this.updateEventsSource(); | ||||
|         }, | ||||
|               }); | ||||
|  | ||||
|               let calendarEvents = []; | ||||
|               users.forEach((u) => { | ||||
|                 let arr = results | ||||
|                   .filter((i) => i.user.id === u.id) | ||||
|                   .map((i) => ({ | ||||
|                     start: i.startDate.datetime, | ||||
|                     end: i.endDate.datetime, | ||||
|                     calendarRangeId: i.id, | ||||
|                     sourceColor: u.color, | ||||
|                     //display: 'background' // can be an option for the disponibility | ||||
|                   })); | ||||
|                 calendarEvents.push({ | ||||
|                   events: arr, | ||||
|                   color: u.color, | ||||
|                   textColor: "#444444", | ||||
|                   editable: false, | ||||
|                   id: u.id, | ||||
|                 }); | ||||
|               }); | ||||
|  | ||||
|               this.users.loaded = users; | ||||
|               this.options = users; | ||||
|  | ||||
|               this.calendarEvents.loaded = calendarEvents; | ||||
|               whoami().then( | ||||
|                 (me) => | ||||
|                   new Promise((resolve, reject) => { | ||||
|                     this.users.logged = me; | ||||
|                     let currentUser = users.find((u) => u.id === me.id); | ||||
|                     this.value = currentUser; | ||||
|  | ||||
|                     fetchCalendar(currentUser.id).then( | ||||
|                       (calendar) => | ||||
|                         new Promise((resolve, reject) => { | ||||
|                           let results = calendar.results; | ||||
|                           let events = results.map((i) => ({ | ||||
|                             start: i.startDate.datetime, | ||||
|                             end: i.endDate.datetime, | ||||
|                           })); | ||||
|                           let calendarEventsCurrentUser = { | ||||
|                             events: events, | ||||
|                             color: "darkblue", | ||||
|                             id: 1000, | ||||
|                             editable: false, | ||||
|                           }; | ||||
|                           this.calendarEvents.user = calendarEventsCurrentUser; | ||||
|  | ||||
|                           this.selectUsers(currentUser); | ||||
|  | ||||
|                           resolve(); | ||||
|                         }), | ||||
|                     ); | ||||
|  | ||||
|                     resolve(); | ||||
|                   }), | ||||
|               ); | ||||
|  | ||||
|               resolve(); | ||||
|             }), | ||||
|         ) | ||||
|         .catch((error) => { | ||||
|           this.errorMsg.push(error.message); | ||||
|         }); | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.init(); | ||||
|     transName(value) { | ||||
|       return `${value.username}`; | ||||
|     }, | ||||
|     coloriseSelectedValues() { | ||||
|       let tags = document.querySelectorAll("div.multiselect__tags-wrap")[0]; | ||||
|  | ||||
|       if (tags.hasChildNodes()) { | ||||
|         let children = tags.childNodes; | ||||
|         for (let i = 0; i < children.length; i++) { | ||||
|           let child = children[i]; | ||||
|           if (child.nodeType === Node.ELEMENT_NODE) { | ||||
|             this.users.selected.forEach((u) => { | ||||
|               if (child.hasChildNodes()) { | ||||
|                 if (child.firstChild.innerText == u.username) { | ||||
|                   child.style.background = u.color; | ||||
|                   child.firstChild.style.color = "#444444"; | ||||
|                 } | ||||
|               } | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     selectEvents() { | ||||
|       let selectedUsersId = this.users.selected.map((a) => a.id); | ||||
|       this.calendarEvents.selected = this.calendarEvents.loaded.filter((a) => | ||||
|         selectedUsersId.includes(a.id), | ||||
|       ); | ||||
|     }, | ||||
|     selectUsers(value) { | ||||
|       this.users.selected.push(value); | ||||
|       this.coloriseSelectedValues(); | ||||
|       this.selectEvents(); | ||||
|       this.updateEventsSource(); | ||||
|     }, | ||||
|     unSelectUsers(value) { | ||||
|       this.users.selected = this.users.selected.filter((a) => a.id != value.id); | ||||
|       this.selectEvents(); | ||||
|       this.updateEventsSource(); | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.init(); | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -1,59 +1,54 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <template v-if="templates.length > 0"> | ||||
|             <slot name="title"> | ||||
|                 <h2>{{ $t("generate_document") }}</h2> | ||||
|             </slot> | ||||
|   <div> | ||||
|     <template v-if="templates.length > 0"> | ||||
|       <slot name="title"> | ||||
|         <h2>{{ $t("generate_document") }}</h2> | ||||
|       </slot> | ||||
|  | ||||
|             <div class="container"> | ||||
|                 <div class="row"> | ||||
|                     <div class="col-md-4"> | ||||
|                         <slot name="label"> | ||||
|                             <label>{{ $t("select_a_template") }}</label> | ||||
|                         </slot> | ||||
|                     </div> | ||||
|                     <div class="col-md-8"> | ||||
|                         <div class="input-group mb-3"> | ||||
|                             <select class="form-select" v-model="template"> | ||||
|                                 <option disabled selected value=""> | ||||
|                                     {{ $t("choose_a_template") }} | ||||
|                                 </option> | ||||
|                                 <template v-for="t in templates" :key="t.id"> | ||||
|                                     <option :value="t.id"> | ||||
|                                         {{ | ||||
|                                             localizeString(t.name) || | ||||
|                                             "Aucun nom défini" | ||||
|                                         }} | ||||
|                                     </option> | ||||
|                                 </template> | ||||
|                             </select> | ||||
|                             <a | ||||
|                                 v-if="canGenerate" | ||||
|                                 class="btn btn-update btn-sm change-icon" | ||||
|                                 :href="buildUrlGenerate" | ||||
|                                 @click.prevent=" | ||||
|                                     clickGenerate($event, buildUrlGenerate) | ||||
|                                 " | ||||
|                                 ><i class="fa fa-fw fa-cog" | ||||
|                             /></a> | ||||
|                             <a | ||||
|                                 v-else | ||||
|                                 class="btn btn-update btn-sm change-icon" | ||||
|                                 href="#" | ||||
|                                 disabled | ||||
|                                 ><i class="fa fa-fw fa-cog" | ||||
|                             /></a> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="row" v-if="hasDescription"> | ||||
|                     <div class="col-md-8 align-self-end"> | ||||
|                         <p>{{ getDescription }}</p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|       <div class="container"> | ||||
|         <div class="row"> | ||||
|           <div class="col-md-4"> | ||||
|             <slot name="label"> | ||||
|               <label>{{ $t("select_a_template") }}</label> | ||||
|             </slot> | ||||
|           </div> | ||||
|           <div class="col-md-8"> | ||||
|             <div class="input-group mb-3"> | ||||
|               <select class="form-select" v-model="template"> | ||||
|                 <option disabled selected value=""> | ||||
|                   {{ $t("choose_a_template") }} | ||||
|                 </option> | ||||
|                 <template v-for="t in templates" :key="t.id"> | ||||
|                   <option :value="t.id"> | ||||
|                     {{ localizeString(t.name) || "Aucun nom défini" }} | ||||
|                   </option> | ||||
|                 </template> | ||||
|               </select> | ||||
|               <a | ||||
|                 v-if="canGenerate" | ||||
|                 class="btn btn-update btn-sm change-icon" | ||||
|                 :href="buildUrlGenerate" | ||||
|                 @click.prevent="clickGenerate($event, buildUrlGenerate)" | ||||
|                 ><i class="fa fa-fw fa-cog" | ||||
|               /></a> | ||||
|               <a | ||||
|                 v-else | ||||
|                 class="btn btn-update btn-sm change-icon" | ||||
|                 href="#" | ||||
|                 disabled | ||||
|                 ><i class="fa fa-fw fa-cog" | ||||
|               /></a> | ||||
|             </div> | ||||
|         </template> | ||||
|     </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="row" v-if="hasDescription"> | ||||
|           <div class="col-md-8 align-self-end"> | ||||
|             <p>{{ getDescription }}</p> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </template> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| @@ -61,83 +56,83 @@ import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator"; | ||||
| import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; | ||||
|  | ||||
| export default { | ||||
|     name: "PickTemplate", | ||||
|     props: { | ||||
|         entityId: [String, Number], | ||||
|         entityClass: { | ||||
|             type: String, | ||||
|             required: false, | ||||
|         }, | ||||
|         templates: { | ||||
|             type: Array, | ||||
|             required: true, | ||||
|         }, | ||||
|         preventDefaultMoveToGenerate: { | ||||
|             type: Boolean, | ||||
|             required: false, | ||||
|             default: false, | ||||
|         }, | ||||
|   name: "PickTemplate", | ||||
|   props: { | ||||
|     entityId: [String, Number], | ||||
|     entityClass: { | ||||
|       type: String, | ||||
|       required: false, | ||||
|     }, | ||||
|     emits: ["goToGenerateDocument"], | ||||
|     data() { | ||||
|         return { | ||||
|             template: null, | ||||
|         }; | ||||
|     templates: { | ||||
|       type: Array, | ||||
|       required: true, | ||||
|     }, | ||||
|     computed: { | ||||
|         canGenerate() { | ||||
|             return this.template != null; | ||||
|         }, | ||||
|         hasDescription() { | ||||
|             if (this.template == null) { | ||||
|                 return false; | ||||
|             } | ||||
|     preventDefaultMoveToGenerate: { | ||||
|       type: Boolean, | ||||
|       required: false, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   emits: ["goToGenerateDocument"], | ||||
|   data() { | ||||
|     return { | ||||
|       template: null, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     canGenerate() { | ||||
|       return this.template != null; | ||||
|     }, | ||||
|     hasDescription() { | ||||
|       if (this.template == null) { | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|             return true; | ||||
|         }, | ||||
|         getDescription() { | ||||
|             if (null === this.template) { | ||||
|                 return ""; | ||||
|             } | ||||
|             let desc = this.templates.find((t) => t.id === this.template); | ||||
|             if (null === desc) { | ||||
|                 return ""; | ||||
|             } | ||||
|             return desc.description || ""; | ||||
|         }, | ||||
|         buildUrlGenerate() { | ||||
|             if (null === this.template) { | ||||
|                 return "#"; | ||||
|             } | ||||
|       return true; | ||||
|     }, | ||||
|     getDescription() { | ||||
|       if (null === this.template) { | ||||
|         return ""; | ||||
|       } | ||||
|       let desc = this.templates.find((t) => t.id === this.template); | ||||
|       if (null === desc) { | ||||
|         return ""; | ||||
|       } | ||||
|       return desc.description || ""; | ||||
|     }, | ||||
|     buildUrlGenerate() { | ||||
|       if (null === this.template) { | ||||
|         return "#"; | ||||
|       } | ||||
|  | ||||
|             return buildLink(this.template, this.entityId, this.entityClass); | ||||
|         }, | ||||
|       return buildLink(this.template, this.entityId, this.entityClass); | ||||
|     }, | ||||
|     methods: { | ||||
|         localizeString(str) { | ||||
|             return localizeString(str); | ||||
|         }, | ||||
|         clickGenerate(event, link) { | ||||
|             if (!this.preventDefaultMoveToGenerate) { | ||||
|                 window.location.assign(link); | ||||
|             } | ||||
|   }, | ||||
|   methods: { | ||||
|     localizeString(str) { | ||||
|       return localizeString(str); | ||||
|     }, | ||||
|     clickGenerate(event, link) { | ||||
|       if (!this.preventDefaultMoveToGenerate) { | ||||
|         window.location.assign(link); | ||||
|       } | ||||
|  | ||||
|             this.$emit("goToGenerateDocument", { | ||||
|                 event, | ||||
|                 link, | ||||
|                 template: this.template, | ||||
|             }); | ||||
|         }, | ||||
|       this.$emit("goToGenerateDocument", { | ||||
|         event, | ||||
|         link, | ||||
|         template: this.template, | ||||
|       }); | ||||
|     }, | ||||
|     i18n: { | ||||
|         messages: { | ||||
|             fr: { | ||||
|                 generate_document: "Générer un document", | ||||
|                 select_a_template: "Choisir un modèle", | ||||
|                 choose_a_template: "Choisir", | ||||
|             }, | ||||
|         }, | ||||
|   }, | ||||
|   i18n: { | ||||
|     messages: { | ||||
|       fr: { | ||||
|         generate_document: "Générer un document", | ||||
|         select_a_template: "Choisir un modèle", | ||||
|         choose_a_template: "Choisir", | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -59,7 +59,7 @@ final readonly class StoredObjectVersionApiController | ||||
|  | ||||
|         return new JsonResponse( | ||||
|             $this->serializer->serialize( | ||||
|                 new Collection(array_values($items->toArray()), $paginator), | ||||
|                 new Collection($items, $paginator), | ||||
|                 'json', | ||||
|                 [AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]] | ||||
|             ), | ||||
|   | ||||
| @@ -94,7 +94,7 @@ class StoredObject implements Document, TrackCreationInterface | ||||
|     /** | ||||
|      * @var Collection<int, StoredObjectVersion>&Selectable<int, StoredObjectVersion> | ||||
|      */ | ||||
|     #[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true)] | ||||
|     #[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true, fetch: 'EAGER')] | ||||
|     private Collection&Selectable $versions; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -1,20 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Exception; | ||||
|  | ||||
| class ConversionWithSameMimeTypeException extends \RuntimeException | ||||
| { | ||||
|     public function __construct(string $mimeType, ?\Throwable $previous = null) | ||||
|     { | ||||
|         parent::__construct("Conversion to same MIME type '{$mimeType}' is not allowed: already at the same MIME type", 0, $previous); | ||||
|     } | ||||
| } | ||||
| @@ -6,20 +6,20 @@ const algo = "AES-CBC"; | ||||
| const URL_POST = "/asyncupload/temp_url/generate/post"; | ||||
|  | ||||
| const keyDefinition = { | ||||
|     name: algo, | ||||
|     length: 256, | ||||
|   name: algo, | ||||
|   length: 256, | ||||
| }; | ||||
|  | ||||
| const createFilename = (): string => { | ||||
|     let text = ""; | ||||
|     const possible = | ||||
|         "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | ||||
|   let text = ""; | ||||
|   const possible = | ||||
|     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | ||||
|  | ||||
|     for (let i = 0; i < 7; i++) { | ||||
|         text += possible.charAt(Math.floor(Math.random() * possible.length)); | ||||
|     } | ||||
|   for (let i = 0; i < 7; i++) { | ||||
|     text += possible.charAt(Math.floor(Math.random() * possible.length)); | ||||
|   } | ||||
|  | ||||
|     return text; | ||||
|   return text; | ||||
| }; | ||||
|  | ||||
| /** | ||||
| @@ -30,59 +30,59 @@ const createFilename = (): string => { | ||||
|  * @returns {Promise<StoredObject>} A Promise that resolves to the newly created StoredObject. | ||||
|  */ | ||||
| export const fetchNewStoredObject = async (): Promise<StoredObject> => { | ||||
|     return makeFetch("POST", "/api/1.0/doc-store/stored-object/create", null); | ||||
|   return makeFetch("POST", "/api/1.0/doc-store/stored-object/create", null); | ||||
| }; | ||||
|  | ||||
| export const uploadVersion = async ( | ||||
|     uploadFile: ArrayBuffer, | ||||
|     storedObject: StoredObject, | ||||
|   uploadFile: ArrayBuffer, | ||||
|   storedObject: StoredObject, | ||||
| ): Promise<string> => { | ||||
|     const params = new URLSearchParams(); | ||||
|     params.append("expires_delay", "180"); | ||||
|     params.append("submit_delay", "180"); | ||||
|     const asyncData: PostStoreObjectSignature = await makeFetch( | ||||
|         "GET", | ||||
|         `/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/post` + | ||||
|             "?" + | ||||
|             params.toString(), | ||||
|     ); | ||||
|     const suffix = createFilename(); | ||||
|     const filename = asyncData.prefix + suffix; | ||||
|     const formData = new FormData(); | ||||
|     formData.append("redirect", asyncData.redirect); | ||||
|     formData.append("max_file_size", asyncData.max_file_size.toString()); | ||||
|     formData.append("max_file_count", asyncData.max_file_count.toString()); | ||||
|     formData.append("expires", asyncData.expires.toString()); | ||||
|     formData.append("signature", asyncData.signature); | ||||
|     formData.append(filename, new Blob([uploadFile]), suffix); | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("expires_delay", "180"); | ||||
|   params.append("submit_delay", "180"); | ||||
|   const asyncData: PostStoreObjectSignature = await makeFetch( | ||||
|     "GET", | ||||
|     `/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/post` + | ||||
|       "?" + | ||||
|       params.toString(), | ||||
|   ); | ||||
|   const suffix = createFilename(); | ||||
|   const filename = asyncData.prefix + suffix; | ||||
|   const formData = new FormData(); | ||||
|   formData.append("redirect", asyncData.redirect); | ||||
|   formData.append("max_file_size", asyncData.max_file_size.toString()); | ||||
|   formData.append("max_file_count", asyncData.max_file_count.toString()); | ||||
|   formData.append("expires", asyncData.expires.toString()); | ||||
|   formData.append("signature", asyncData.signature); | ||||
|   formData.append(filename, new Blob([uploadFile]), suffix); | ||||
|  | ||||
|     const response = await window.fetch(asyncData.url, { | ||||
|         method: "POST", | ||||
|         body: formData, | ||||
|     }); | ||||
|   const response = await window.fetch(asyncData.url, { | ||||
|     method: "POST", | ||||
|     body: formData, | ||||
|   }); | ||||
|  | ||||
|     if (!response.ok) { | ||||
|         console.error("Error while sending file to store", response); | ||||
|         throw new Error(response.statusText); | ||||
|     } | ||||
|   if (!response.ok) { | ||||
|     console.error("Error while sending file to store", response); | ||||
|     throw new Error(response.statusText); | ||||
|   } | ||||
|  | ||||
|     return Promise.resolve(filename); | ||||
|   return Promise.resolve(filename); | ||||
| }; | ||||
|  | ||||
| export const encryptFile = async ( | ||||
|     originalFile: ArrayBuffer, | ||||
|   originalFile: ArrayBuffer, | ||||
| ): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => { | ||||
|     const iv = crypto.getRandomValues(new Uint8Array(16)); | ||||
|     const key = await window.crypto.subtle.generateKey(keyDefinition, true, [ | ||||
|         "encrypt", | ||||
|         "decrypt", | ||||
|     ]); | ||||
|     const exportedKey = await window.crypto.subtle.exportKey("jwk", key); | ||||
|     const encrypted = await window.crypto.subtle.encrypt( | ||||
|         { name: algo, iv: iv }, | ||||
|         key, | ||||
|         originalFile, | ||||
|     ); | ||||
|   const iv = crypto.getRandomValues(new Uint8Array(16)); | ||||
|   const key = await window.crypto.subtle.generateKey(keyDefinition, true, [ | ||||
|     "encrypt", | ||||
|     "decrypt", | ||||
|   ]); | ||||
|   const exportedKey = await window.crypto.subtle.exportKey("jwk", key); | ||||
|   const encrypted = await window.crypto.subtle.encrypt( | ||||
|     { name: algo, iv: iv }, | ||||
|     key, | ||||
|     originalFile, | ||||
|   ); | ||||
|  | ||||
|     return Promise.resolve([encrypted, iv, exportedKey]); | ||||
|   return Promise.resolve([encrypted, iv, exportedKey]); | ||||
| }; | ||||
|   | ||||
| @@ -2,9 +2,9 @@ import { fetchResults } from "ChillMainAssets/lib/api/apiMethods"; | ||||
| import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc"; | ||||
|  | ||||
| export function fetch_generic_docs_by_accompanying_period( | ||||
|     periodId: number, | ||||
|   periodId: number, | ||||
| ): Promise<GenericDocForAccompanyingPeriod[]> { | ||||
|     return fetchResults( | ||||
|         `/api/1.0/doc-store/generic-doc/by-period/${periodId}/index`, | ||||
|     ); | ||||
|   return fetchResults( | ||||
|     `/api/1.0/doc-store/generic-doc/by-period/${periodId}/index`, | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -6,117 +6,116 @@ import { _createI18n } from "../../../../../ChillMainBundle/Resources/public/vue | ||||
| const i18n = _createI18n({}); | ||||
|  | ||||
| const startApp = ( | ||||
|     divElement: HTMLDivElement, | ||||
|     collectionEntry: null | HTMLLIElement, | ||||
|   divElement: HTMLDivElement, | ||||
|   collectionEntry: null | HTMLLIElement, | ||||
| ): void => { | ||||
|     console.log("app started", divElement); | ||||
|   console.log("app started", divElement); | ||||
|  | ||||
|     const inputTitle = collectionEntry?.querySelector("input[type='text']"); | ||||
|   const inputTitle = collectionEntry?.querySelector("input[type='text']"); | ||||
|  | ||||
|     const input_stored_object: HTMLInputElement | null = | ||||
|         divElement.querySelector("input[data-stored-object]"); | ||||
|     if (null === input_stored_object) { | ||||
|         throw new Error("input to stored object not found"); | ||||
|     } | ||||
|   const input_stored_object: HTMLInputElement | null = divElement.querySelector( | ||||
|     "input[data-stored-object]", | ||||
|   ); | ||||
|   if (null === input_stored_object) { | ||||
|     throw new Error("input to stored object not found"); | ||||
|   } | ||||
|  | ||||
|     let existingDoc: StoredObject | null = null; | ||||
|     if (input_stored_object.value !== "") { | ||||
|         existingDoc = JSON.parse(input_stored_object.value); | ||||
|     } | ||||
|     const app_container = document.createElement("div"); | ||||
|     divElement.appendChild(app_container); | ||||
|   let existingDoc: StoredObject | null = null; | ||||
|   if (input_stored_object.value !== "") { | ||||
|     existingDoc = JSON.parse(input_stored_object.value); | ||||
|   } | ||||
|   const app_container = document.createElement("div"); | ||||
|   divElement.appendChild(app_container); | ||||
|  | ||||
|     const app = createApp({ | ||||
|         template: | ||||
|             '<drop-file-widget :existingDoc="this.$data.existingDoc" :allowRemove="true" @addDocument="this.addDocument" @removeDocument="removeDocument"></drop-file-widget>', | ||||
|         data() { | ||||
|             return { | ||||
|                 existingDoc: existingDoc, | ||||
|                 inputTitle: inputTitle, | ||||
|             }; | ||||
|         }, | ||||
|         components: { | ||||
|             DropFileWidget, | ||||
|         }, | ||||
|         methods: { | ||||
|             addDocument: function ({ | ||||
|                 stored_object, | ||||
|                 stored_object_version, | ||||
|                 file_name, | ||||
|             }: { | ||||
|                 stored_object: StoredObject; | ||||
|                 stored_object_version: StoredObjectVersion; | ||||
|                 file_name: string; | ||||
|             }): void { | ||||
|                 stored_object.title = file_name; | ||||
|                 console.log("object added", stored_object); | ||||
|                 console.log("version added", stored_object_version); | ||||
|                 this.$data.existingDoc = stored_object; | ||||
|                 this.$data.existingDoc.currentVersion = stored_object_version; | ||||
|                 input_stored_object.value = JSON.stringify( | ||||
|                     this.$data.existingDoc, | ||||
|                 ); | ||||
|                 if (this.$data.inputTitle) { | ||||
|                     if (!this.$data.inputTitle?.value) { | ||||
|                         this.$data.inputTitle.value = file_name; | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             removeDocument: function (object: StoredObject): void { | ||||
|                 console.log("catch remove document", object); | ||||
|                 input_stored_object.value = ""; | ||||
|                 this.$data.existingDoc = undefined; | ||||
|                 console.log("collectionEntry", collectionEntry); | ||||
|   const app = createApp({ | ||||
|     template: | ||||
|       '<drop-file-widget :existingDoc="this.$data.existingDoc" :allowRemove="true" @addDocument="this.addDocument" @removeDocument="removeDocument"></drop-file-widget>', | ||||
|     data() { | ||||
|       return { | ||||
|         existingDoc: existingDoc, | ||||
|         inputTitle: inputTitle, | ||||
|       }; | ||||
|     }, | ||||
|     components: { | ||||
|       DropFileWidget, | ||||
|     }, | ||||
|     methods: { | ||||
|       addDocument: function ({ | ||||
|         stored_object, | ||||
|         stored_object_version, | ||||
|         file_name, | ||||
|       }: { | ||||
|         stored_object: StoredObject; | ||||
|         stored_object_version: StoredObjectVersion; | ||||
|         file_name: string; | ||||
|       }): void { | ||||
|         stored_object.title = file_name; | ||||
|         console.log("object added", stored_object); | ||||
|         console.log("version added", stored_object_version); | ||||
|         this.$data.existingDoc = stored_object; | ||||
|         this.$data.existingDoc.currentVersion = stored_object_version; | ||||
|         input_stored_object.value = JSON.stringify(this.$data.existingDoc); | ||||
|         if (this.$data.inputTitle) { | ||||
|           if (!this.$data.inputTitle?.value) { | ||||
|             this.$data.inputTitle.value = file_name; | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       removeDocument: function (object: StoredObject): void { | ||||
|         console.log("catch remove document", object); | ||||
|         input_stored_object.value = ""; | ||||
|         this.$data.existingDoc = undefined; | ||||
|         console.log("collectionEntry", collectionEntry); | ||||
|  | ||||
|                 if (null !== collectionEntry) { | ||||
|                     console.log("will remove collection"); | ||||
|                     collectionEntry.remove(); | ||||
|                 } | ||||
|             }, | ||||
|         }, | ||||
|     }); | ||||
|         if (null !== collectionEntry) { | ||||
|           console.log("will remove collection"); | ||||
|           collectionEntry.remove(); | ||||
|         } | ||||
|       }, | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|     app.use(i18n).mount(app_container); | ||||
|   app.use(i18n).mount(app_container); | ||||
| }; | ||||
| window.addEventListener("collection-add-entry", (( | ||||
|     e: CustomEvent<CollectionEventPayload>, | ||||
|   e: CustomEvent<CollectionEventPayload>, | ||||
| ) => { | ||||
|     const detail = e.detail; | ||||
|     const divElement: null | HTMLDivElement = detail.entry.querySelector( | ||||
|         "div[data-stored-object]", | ||||
|     ); | ||||
|   const detail = e.detail; | ||||
|   const divElement: null | HTMLDivElement = detail.entry.querySelector( | ||||
|     "div[data-stored-object]", | ||||
|   ); | ||||
|  | ||||
|     if (null === divElement) { | ||||
|         throw new Error("div[data-stored-object] not found"); | ||||
|     } | ||||
|   if (null === divElement) { | ||||
|     throw new Error("div[data-stored-object] not found"); | ||||
|   } | ||||
|  | ||||
|     startApp(divElement, detail.entry); | ||||
|   startApp(divElement, detail.entry); | ||||
| }) as EventListener); | ||||
|  | ||||
| window.addEventListener("DOMContentLoaded", () => { | ||||
|     const upload_inputs: NodeListOf<HTMLDivElement> = document.querySelectorAll( | ||||
|         "div[data-stored-object]", | ||||
|     ); | ||||
|   const upload_inputs: NodeListOf<HTMLDivElement> = document.querySelectorAll( | ||||
|     "div[data-stored-object]", | ||||
|   ); | ||||
|  | ||||
|     upload_inputs.forEach((input: HTMLDivElement): void => { | ||||
|         // test for a parent to check if this is a collection entry | ||||
|         let collectionEntry: null | HTMLLIElement = null; | ||||
|         const parent = input.parentElement; | ||||
|         console.log("parent", parent); | ||||
|         if (null !== parent) { | ||||
|             const grandParent = parent.parentElement; | ||||
|             console.log("grandParent", grandParent); | ||||
|             if (null !== grandParent) { | ||||
|                 if ( | ||||
|                     grandParent.tagName.toLowerCase() === "li" && | ||||
|                     grandParent.classList.contains("entry") | ||||
|                 ) { | ||||
|                     collectionEntry = grandParent as HTMLLIElement; | ||||
|                 } | ||||
|             } | ||||
|   upload_inputs.forEach((input: HTMLDivElement): void => { | ||||
|     // test for a parent to check if this is a collection entry | ||||
|     let collectionEntry: null | HTMLLIElement = null; | ||||
|     const parent = input.parentElement; | ||||
|     console.log("parent", parent); | ||||
|     if (null !== parent) { | ||||
|       const grandParent = parent.parentElement; | ||||
|       console.log("grandParent", grandParent); | ||||
|       if (null !== grandParent) { | ||||
|         if ( | ||||
|           grandParent.tagName.toLowerCase() === "li" && | ||||
|           grandParent.classList.contains("entry") | ||||
|         ) { | ||||
|           collectionEntry = grandParent as HTMLLIElement; | ||||
|         } | ||||
|         startApp(input, collectionEntry); | ||||
|     }); | ||||
|       } | ||||
|     } | ||||
|     startApp(input, collectionEntry); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| export {}; | ||||
|   | ||||
| @@ -9,26 +9,26 @@ import ToastPlugin from "vue-toast-notification"; | ||||
| const i18n = _createI18n({}); | ||||
|  | ||||
| window.addEventListener("DOMContentLoaded", function (e) { | ||||
|     document | ||||
|         .querySelectorAll<HTMLDivElement>("div[data-download-button-single]") | ||||
|         .forEach((el) => { | ||||
|             const storedObject = JSON.parse( | ||||
|                 el.dataset.storedObject as string, | ||||
|             ) as StoredObject; | ||||
|             const title = el.dataset.title as string; | ||||
|             const app = createApp({ | ||||
|                 components: { DownloadButton }, | ||||
|                 data() { | ||||
|                     return { | ||||
|                         storedObject, | ||||
|                         title, | ||||
|                         classes: { btn: true, "btn-outline-primary": true }, | ||||
|                     }; | ||||
|                 }, | ||||
|                 template: | ||||
|                     '<download-button :stored-object="storedObject" :at-version="storedObject.currentVersion" :classes="classes" :filename="title" :direct-download="true"></download-button>', | ||||
|             }); | ||||
|   document | ||||
|     .querySelectorAll<HTMLDivElement>("div[data-download-button-single]") | ||||
|     .forEach((el) => { | ||||
|       const storedObject = JSON.parse( | ||||
|         el.dataset.storedObject as string, | ||||
|       ) as StoredObject; | ||||
|       const title = el.dataset.title as string; | ||||
|       const app = createApp({ | ||||
|         components: { DownloadButton }, | ||||
|         data() { | ||||
|           return { | ||||
|             storedObject, | ||||
|             title, | ||||
|             classes: { btn: true, "btn-outline-primary": true }, | ||||
|           }; | ||||
|         }, | ||||
|         template: | ||||
|           '<download-button :stored-object="storedObject" :at-version="storedObject.currentVersion" :classes="classes" :filename="title" :direct-download="true"></download-button>', | ||||
|       }); | ||||
|  | ||||
|             app.use(i18n).use(ToastPlugin).mount(el); | ||||
|         }); | ||||
|       app.use(i18n).use(ToastPlugin).mount(el); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -8,66 +8,66 @@ import ToastPlugin from "vue-toast-notification"; | ||||
| const i18n = _createI18n({}); | ||||
|  | ||||
| window.addEventListener("DOMContentLoaded", function (e) { | ||||
|     document | ||||
|         .querySelectorAll<HTMLDivElement>("div[data-download-buttons]") | ||||
|         .forEach((el) => { | ||||
|             const app = createApp({ | ||||
|                 components: { DocumentActionButtonsGroup }, | ||||
|                 data() { | ||||
|                     const datasets = el.dataset as { | ||||
|                         filename: string; | ||||
|                         canEdit: string; | ||||
|                         storedObject: string; | ||||
|                         buttonSmall: string; | ||||
|                         davLink: string; | ||||
|                         davLinkExpiration: string; | ||||
|                     }; | ||||
|   document | ||||
|     .querySelectorAll<HTMLDivElement>("div[data-download-buttons]") | ||||
|     .forEach((el) => { | ||||
|       const app = createApp({ | ||||
|         components: { DocumentActionButtonsGroup }, | ||||
|         data() { | ||||
|           const datasets = el.dataset as { | ||||
|             filename: string; | ||||
|             canEdit: string; | ||||
|             storedObject: string; | ||||
|             buttonSmall: string; | ||||
|             davLink: string; | ||||
|             davLinkExpiration: string; | ||||
|           }; | ||||
|  | ||||
|                     const storedObject = JSON.parse( | ||||
|                             datasets.storedObject, | ||||
|                         ) as StoredObject, | ||||
|                         filename = datasets.filename, | ||||
|                         canEdit = datasets.canEdit === "1", | ||||
|                         small = datasets.buttonSmall === "1", | ||||
|                         davLink = | ||||
|                             "davLink" in datasets && datasets.davLink !== "" | ||||
|                                 ? datasets.davLink | ||||
|                                 : null, | ||||
|                         davLinkExpiration = | ||||
|                             "davLinkExpiration" in datasets | ||||
|                                 ? Number.parseInt(datasets.davLinkExpiration) | ||||
|                                 : null; | ||||
|                     return { | ||||
|                         storedObject, | ||||
|                         filename, | ||||
|                         canEdit, | ||||
|                         small, | ||||
|                         davLink, | ||||
|                         davLinkExpiration, | ||||
|                     }; | ||||
|                 }, | ||||
|                 template: | ||||
|                     '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" :dav-link="davLink" :dav-link-expiration="davLinkExpiration" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>', | ||||
|                 methods: { | ||||
|                     onStoredObjectStatusChange: function ( | ||||
|                         newStatus: StoredObjectStatusChange, | ||||
|                     ): void { | ||||
|                         this.$data.storedObject.status = newStatus.status; | ||||
|                         this.$data.storedObject.filename = newStatus.filename; | ||||
|                         this.$data.storedObject.type = newStatus.type; | ||||
|           const storedObject = JSON.parse( | ||||
|               datasets.storedObject, | ||||
|             ) as StoredObject, | ||||
|             filename = datasets.filename, | ||||
|             canEdit = datasets.canEdit === "1", | ||||
|             small = datasets.buttonSmall === "1", | ||||
|             davLink = | ||||
|               "davLink" in datasets && datasets.davLink !== "" | ||||
|                 ? datasets.davLink | ||||
|                 : null, | ||||
|             davLinkExpiration = | ||||
|               "davLinkExpiration" in datasets | ||||
|                 ? Number.parseInt(datasets.davLinkExpiration) | ||||
|                 : null; | ||||
|           return { | ||||
|             storedObject, | ||||
|             filename, | ||||
|             canEdit, | ||||
|             small, | ||||
|             davLink, | ||||
|             davLinkExpiration, | ||||
|           }; | ||||
|         }, | ||||
|         template: | ||||
|           '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" :dav-link="davLink" :dav-link-expiration="davLinkExpiration" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>', | ||||
|         methods: { | ||||
|           onStoredObjectStatusChange: function ( | ||||
|             newStatus: StoredObjectStatusChange, | ||||
|           ): void { | ||||
|             this.$data.storedObject.status = newStatus.status; | ||||
|             this.$data.storedObject.filename = newStatus.filename; | ||||
|             this.$data.storedObject.type = newStatus.type; | ||||
|  | ||||
|                         // remove eventual div which inform pending status | ||||
|                         document | ||||
|                             .querySelectorAll( | ||||
|                                 `[data-docgen-is-pending="${this.$data.storedObject.id}"]`, | ||||
|                             ) | ||||
|                             .forEach(function (el) { | ||||
|                                 el.remove(); | ||||
|                             }); | ||||
|                     }, | ||||
|                 }, | ||||
|             }); | ||||
|             // remove eventual div which inform pending status | ||||
|             document | ||||
|               .querySelectorAll( | ||||
|                 `[data-docgen-is-pending="${this.$data.storedObject.id}"]`, | ||||
|               ) | ||||
|               .forEach(function (el) { | ||||
|                 el.remove(); | ||||
|               }); | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
|  | ||||
|             app.use(i18n).use(ToastPlugin).mount(el); | ||||
|         }); | ||||
|       app.use(i18n).use(ToastPlugin).mount(el); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { DateTime } from "ChillMainAssets/types"; | ||||
| import { StoredObject } from "ChillDocStoreAssets/types/index"; | ||||
|  | ||||
| export interface GenericDocMetadata { | ||||
|     isPresent: boolean; | ||||
|   isPresent: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -15,57 +15,57 @@ export interface EmptyMetadata extends GenericDocMetadata {} | ||||
|  * Minimal Metadata for a GenericDoc with a normalizer | ||||
|  */ | ||||
| export interface BaseMetadata extends GenericDocMetadata { | ||||
|     title: string; | ||||
|   title: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * A generic doc is a document attached to a Person or an AccompanyingPeriod. | ||||
|  */ | ||||
| export interface GenericDoc { | ||||
|     type: "doc_store_generic_doc"; | ||||
|     uniqueKey: string; | ||||
|     key: string; | ||||
|     identifiers: { id: number }; | ||||
|     context: "person" | "accompanying-period"; | ||||
|     doc_date: DateTime; | ||||
|     metadata: GenericDocMetadata; | ||||
|     storedObject: StoredObject | null; | ||||
|   type: "doc_store_generic_doc"; | ||||
|   uniqueKey: string; | ||||
|   key: string; | ||||
|   identifiers: object; | ||||
|   context: "person" | "accompanying-period"; | ||||
|   doc_date: DateTime; | ||||
|   metadata: GenericDocMetadata; | ||||
|   storedObject: StoredObject | null; | ||||
| } | ||||
|  | ||||
| export interface GenericDocForAccompanyingPeriod extends GenericDoc { | ||||
|     context: "accompanying-period"; | ||||
|   context: "accompanying-period"; | ||||
| } | ||||
|  | ||||
| interface BaseMetadataWithHtml extends BaseMetadata { | ||||
|     html: string; | ||||
|   html: string; | ||||
| } | ||||
|  | ||||
| export interface GenericDocForAccompanyingCourseDocument | ||||
|     extends GenericDocForAccompanyingPeriod { | ||||
|     key: "accompanying_course_document"; | ||||
|     metadata: BaseMetadataWithHtml; | ||||
|   extends GenericDocForAccompanyingPeriod { | ||||
|   key: "accompanying_course_document"; | ||||
|   metadata: BaseMetadataWithHtml; | ||||
| } | ||||
|  | ||||
| export interface GenericDocForAccompanyingCourseActivityDocument | ||||
|     extends GenericDocForAccompanyingPeriod { | ||||
|     key: "accompanying_course_activity_document"; | ||||
|     metadata: BaseMetadataWithHtml; | ||||
|   extends GenericDocForAccompanyingPeriod { | ||||
|   key: "accompanying_course_activity_document"; | ||||
|   metadata: BaseMetadataWithHtml; | ||||
| } | ||||
|  | ||||
| export interface GenericDocForAccompanyingCourseCalendarDocument | ||||
|     extends GenericDocForAccompanyingPeriod { | ||||
|     key: "accompanying_course_calendar_document"; | ||||
|     metadata: BaseMetadataWithHtml; | ||||
|   extends GenericDocForAccompanyingPeriod { | ||||
|   key: "accompanying_course_calendar_document"; | ||||
|   metadata: BaseMetadataWithHtml; | ||||
| } | ||||
|  | ||||
| export interface GenericDocForAccompanyingCoursePersonDocument | ||||
|     extends GenericDocForAccompanyingPeriod { | ||||
|     key: "person_document"; | ||||
|     metadata: BaseMetadataWithHtml; | ||||
|   extends GenericDocForAccompanyingPeriod { | ||||
|   key: "person_document"; | ||||
|   metadata: BaseMetadataWithHtml; | ||||
| } | ||||
|  | ||||
| export interface GenericDocForAccompanyingCourseWorkEvaluationDocument | ||||
|     extends GenericDocForAccompanyingPeriod { | ||||
|     key: "accompanying_period_work_evaluation_document"; | ||||
|     metadata: BaseMetadataWithHtml; | ||||
|   extends GenericDocForAccompanyingPeriod { | ||||
|   key: "accompanying_period_work_evaluation_document"; | ||||
|   metadata: BaseMetadataWithHtml; | ||||
| } | ||||
|   | ||||
| @@ -4,73 +4,73 @@ import { SignedUrlGet } from "ChillDocStoreAssets/vuejs/StoredObjectButton/helpe | ||||
| export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending"; | ||||
|  | ||||
| export interface StoredObject { | ||||
|     id: number; | ||||
|     title: string | null; | ||||
|     uuid: string; | ||||
|     prefix: string; | ||||
|     status: StoredObjectStatus; | ||||
|     currentVersion: | ||||
|         | null | ||||
|         | StoredObjectVersionCreated | ||||
|         | StoredObjectVersionPersisted; | ||||
|     totalVersions: number; | ||||
|     datas: object; | ||||
|     /** @deprecated */ | ||||
|     creationDate: DateTime; | ||||
|     createdAt: DateTime | null; | ||||
|     createdBy: User | null; | ||||
|     _permissions: { | ||||
|         canEdit: boolean; | ||||
|         canSee: boolean; | ||||
|     }; | ||||
|     _links?: { | ||||
|         dav_link?: { | ||||
|             href: string; | ||||
|             expiration: number; | ||||
|         }; | ||||
|         downloadLink?: SignedUrlGet; | ||||
|   id: number; | ||||
|   title: string | null; | ||||
|   uuid: string; | ||||
|   prefix: string; | ||||
|   status: StoredObjectStatus; | ||||
|   currentVersion: | ||||
|     | null | ||||
|     | StoredObjectVersionCreated | ||||
|     | StoredObjectVersionPersisted; | ||||
|   totalVersions: number; | ||||
|   datas: object; | ||||
|   /** @deprecated */ | ||||
|   creationDate: DateTime; | ||||
|   createdAt: DateTime | null; | ||||
|   createdBy: User | null; | ||||
|   _permissions: { | ||||
|     canEdit: boolean; | ||||
|     canSee: boolean; | ||||
|   }; | ||||
|   _links?: { | ||||
|     dav_link?: { | ||||
|       href: string; | ||||
|       expiration: number; | ||||
|     }; | ||||
|     downloadLink?: SignedUrlGet; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export interface StoredObjectVersion { | ||||
|     /** | ||||
|      * filename of the object in the object storage | ||||
|      */ | ||||
|     filename: string; | ||||
|     iv: number[]; | ||||
|     keyInfos: JsonWebKey; | ||||
|     type: string; | ||||
|   /** | ||||
|    * filename of the object in the object storage | ||||
|    */ | ||||
|   filename: string; | ||||
|   iv: number[]; | ||||
|   keyInfos: JsonWebKey; | ||||
|   type: string; | ||||
| } | ||||
|  | ||||
| export interface StoredObjectVersionCreated extends StoredObjectVersion { | ||||
|     persisted: false; | ||||
|   persisted: false; | ||||
| } | ||||
|  | ||||
| export interface StoredObjectVersionPersisted | ||||
|     extends StoredObjectVersionCreated { | ||||
|     version: number; | ||||
|     id: number; | ||||
|     createdAt: DateTime | null; | ||||
|     createdBy: User | null; | ||||
|   extends StoredObjectVersionCreated { | ||||
|   version: number; | ||||
|   id: number; | ||||
|   createdAt: DateTime | null; | ||||
|   createdBy: User | null; | ||||
| } | ||||
|  | ||||
| export interface StoredObjectStatusChange { | ||||
|     id: number; | ||||
|     filename: string; | ||||
|     status: StoredObjectStatus; | ||||
|     type: string; | ||||
|   id: number; | ||||
|   filename: string; | ||||
|   status: StoredObjectStatus; | ||||
|   type: string; | ||||
| } | ||||
|  | ||||
| export interface StoredObjectVersionWithPointInTime | ||||
|     extends StoredObjectVersionPersisted { | ||||
|     "point-in-times": StoredObjectPointInTime[]; | ||||
|     "from-restored": StoredObjectVersionPersisted | null; | ||||
|   extends StoredObjectVersionPersisted { | ||||
|   "point-in-times": StoredObjectPointInTime[]; | ||||
|   "from-restored": StoredObjectVersionPersisted | null; | ||||
| } | ||||
|  | ||||
| export interface StoredObjectPointInTime { | ||||
|     id: number; | ||||
|     byUser: User | null; | ||||
|     reason: "keep-before-conversion" | "keep-by-user"; | ||||
|   id: number; | ||||
|   byUser: User | null; | ||||
|   reason: "keep-before-conversion" | "keep-by-user"; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -82,63 +82,63 @@ export type WopiEditButtonExecutableBeforeLeaveFunction = () => Promise<void>; | ||||
|  * Object containing information for performering a POST request to a swift object store | ||||
|  */ | ||||
| export interface PostStoreObjectSignature { | ||||
|     method: "POST"; | ||||
|     max_file_size: number; | ||||
|     max_file_count: 1; | ||||
|     expires: number; | ||||
|     submit_delay: 180; | ||||
|     redirect: string; | ||||
|     prefix: string; | ||||
|     url: string; | ||||
|     signature: string; | ||||
|   method: "POST"; | ||||
|   max_file_size: number; | ||||
|   max_file_count: 1; | ||||
|   expires: number; | ||||
|   submit_delay: 180; | ||||
|   redirect: string; | ||||
|   prefix: string; | ||||
|   url: string; | ||||
|   signature: string; | ||||
| } | ||||
|  | ||||
| export interface PDFPage { | ||||
|     index: number; | ||||
|     width: number; | ||||
|     height: number; | ||||
|   index: number; | ||||
|   width: number; | ||||
|   height: number; | ||||
| } | ||||
| export interface SignatureZone { | ||||
|     index: number | null; | ||||
|     x: number; | ||||
|     y: number; | ||||
|     width: number; | ||||
|     height: number; | ||||
|     PDFPage: PDFPage; | ||||
|   index: number | null; | ||||
|   x: number; | ||||
|   y: number; | ||||
|   width: number; | ||||
|   height: number; | ||||
|   PDFPage: PDFPage; | ||||
| } | ||||
|  | ||||
| export interface Signature { | ||||
|     id: number; | ||||
|     storedObject: StoredObject; | ||||
|     zones: SignatureZone[]; | ||||
|   id: number; | ||||
|   storedObject: StoredObject; | ||||
|   zones: SignatureZone[]; | ||||
| } | ||||
|  | ||||
| export type SignedState = | ||||
|     | "pending" | ||||
|     | "signed" | ||||
|     | "rejected" | ||||
|     | "canceled" | ||||
|     | "error"; | ||||
|   | "pending" | ||||
|   | "signed" | ||||
|   | "rejected" | ||||
|   | "canceled" | ||||
|   | "error"; | ||||
|  | ||||
| export interface CheckSignature { | ||||
|     state: SignedState; | ||||
|     storedObject: StoredObject; | ||||
|   state: SignedState; | ||||
|   storedObject: StoredObject; | ||||
| } | ||||
|  | ||||
| export type CanvasEvent = "select" | "add"; | ||||
|  | ||||
| export interface ZoomLevel { | ||||
|     id: number; | ||||
|     zoom: number; | ||||
|     label: { | ||||
|         fr?: string; | ||||
|         nl?: string; | ||||
|     }; | ||||
|   id: number; | ||||
|   zoom: number; | ||||
|   label: { | ||||
|     fr?: string; | ||||
|     nl?: string; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export interface GenericDoc { | ||||
|     type: "doc_store_generic_doc"; | ||||
|     key: string; | ||||
|     context: "person" | "accompanying-period"; | ||||
|     doc_date: DateTime; | ||||
|   type: "doc_store_generic_doc"; | ||||
|   key: string; | ||||
|   context: "person" | "accompanying-period"; | ||||
|   doc_date: DateTime; | ||||
| } | ||||
|   | ||||
| @@ -1,67 +1,65 @@ | ||||
| <template> | ||||
|     <div v-if="isButtonGroupDisplayable" class="btn-group"> | ||||
|         <button | ||||
|             :class=" | ||||
|                 Object.assign({ | ||||
|                     btn: true, | ||||
|                     'btn-outline-primary': true, | ||||
|                     'dropdown-toggle': true, | ||||
|                     'btn-sm': props.small, | ||||
|                 }) | ||||
|             " | ||||
|             type="button" | ||||
|             data-bs-toggle="dropdown" | ||||
|             aria-expanded="false" | ||||
|         > | ||||
|             Actions | ||||
|         </button> | ||||
|         <ul class="dropdown-menu"> | ||||
|             <li v-if="isEditableOnline"> | ||||
|                 <wopi-edit-button | ||||
|                     :stored-object="props.storedObject" | ||||
|                     :classes="{ 'dropdown-item': true }" | ||||
|                     :execute-before-leave="props.executeBeforeLeave" | ||||
|                 ></wopi-edit-button> | ||||
|             </li> | ||||
|             <li v-if="isEditableOnDesktop"> | ||||
|                 <desktop-edit-button | ||||
|                     :classes="{ 'dropdown-item': true }" | ||||
|                     :edit-link="props.davLink" | ||||
|                     :expiration-link="props.davLinkExpiration" | ||||
|                 ></desktop-edit-button> | ||||
|             </li> | ||||
|             <li v-if="isConvertibleToPdf"> | ||||
|                 <convert-button | ||||
|                     :stored-object="props.storedObject" | ||||
|                     :filename="filename" | ||||
|                     :classes="{ 'dropdown-item': true }" | ||||
|                 ></convert-button> | ||||
|             </li> | ||||
|             <li v-if="isDownloadable"> | ||||
|                 <download-button | ||||
|                     :stored-object="props.storedObject" | ||||
|                     :at-version="props.storedObject.currentVersion" | ||||
|                     :filename="filename" | ||||
|                     :classes="{ 'dropdown-item': true }" | ||||
|                     :display-action-string-in-button="true" | ||||
|                 ></download-button> | ||||
|             </li> | ||||
|             <li v-if="isHistoryViewable"> | ||||
|                 <history-button | ||||
|                     :stored-object="props.storedObject" | ||||
|                     :can-edit=" | ||||
|                         canEdit && props.storedObject._permissions.canEdit | ||||
|                     " | ||||
|                 ></history-button> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
|     <div v-else-if="'pending' === props.storedObject.status"> | ||||
|         <div class="btn btn-outline-info">Génération en cours</div> | ||||
|     </div> | ||||
|     <div v-else-if="'failure' === props.storedObject.status"> | ||||
|         <div class="btn btn-outline-danger">La génération a échoué</div> | ||||
|     </div> | ||||
|   <div v-if="isButtonGroupDisplayable" class="btn-group"> | ||||
|     <button | ||||
|       :class=" | ||||
|         Object.assign({ | ||||
|           btn: true, | ||||
|           'btn-outline-primary': true, | ||||
|           'dropdown-toggle': true, | ||||
|           'btn-sm': props.small, | ||||
|         }) | ||||
|       " | ||||
|       type="button" | ||||
|       data-bs-toggle="dropdown" | ||||
|       aria-expanded="false" | ||||
|     > | ||||
|       Actions | ||||
|     </button> | ||||
|     <ul class="dropdown-menu"> | ||||
|       <li v-if="isEditableOnline"> | ||||
|         <wopi-edit-button | ||||
|           :stored-object="props.storedObject" | ||||
|           :classes="{ 'dropdown-item': true }" | ||||
|           :execute-before-leave="props.executeBeforeLeave" | ||||
|         ></wopi-edit-button> | ||||
|       </li> | ||||
|       <li v-if="isEditableOnDesktop"> | ||||
|         <desktop-edit-button | ||||
|           :classes="{ 'dropdown-item': true }" | ||||
|           :edit-link="props.davLink" | ||||
|           :expiration-link="props.davLinkExpiration" | ||||
|         ></desktop-edit-button> | ||||
|       </li> | ||||
|       <li v-if="isConvertibleToPdf"> | ||||
|         <convert-button | ||||
|           :stored-object="props.storedObject" | ||||
|           :filename="filename" | ||||
|           :classes="{ 'dropdown-item': true }" | ||||
|         ></convert-button> | ||||
|       </li> | ||||
|       <li v-if="isDownloadable"> | ||||
|         <download-button | ||||
|           :stored-object="props.storedObject" | ||||
|           :at-version="props.storedObject.currentVersion" | ||||
|           :filename="filename" | ||||
|           :classes="{ 'dropdown-item': true }" | ||||
|           :display-action-string-in-button="true" | ||||
|         ></download-button> | ||||
|       </li> | ||||
|       <li v-if="isHistoryViewable"> | ||||
|         <history-button | ||||
|           :stored-object="props.storedObject" | ||||
|           :can-edit="canEdit && props.storedObject._permissions.canEdit" | ||||
|         ></history-button> | ||||
|       </li> | ||||
|     </ul> | ||||
|   </div> | ||||
|   <div v-else-if="'pending' === props.storedObject.status"> | ||||
|     <div class="btn btn-outline-info">Génération en cours</div> | ||||
|   </div> | ||||
|   <div v-else-if="'failure' === props.storedObject.status"> | ||||
|     <div class="btn btn-outline-danger">La génération a échoué</div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| @@ -70,68 +68,66 @@ import ConvertButton from "./StoredObjectButton/ConvertButton.vue"; | ||||
| import DownloadButton from "./StoredObjectButton/DownloadButton.vue"; | ||||
| import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue"; | ||||
| import { | ||||
|     is_extension_editable, | ||||
|     is_extension_viewable, | ||||
|     is_object_ready, | ||||
|   is_extension_editable, | ||||
|   is_extension_viewable, | ||||
|   is_object_ready, | ||||
| } from "./StoredObjectButton/helpers"; | ||||
| import { | ||||
|     StoredObject, | ||||
|     StoredObjectStatusChange, | ||||
|     StoredObjectVersion, | ||||
|     WopiEditButtonExecutableBeforeLeaveFunction, | ||||
|   StoredObject, | ||||
|   StoredObjectStatusChange, | ||||
|   StoredObjectVersion, | ||||
|   WopiEditButtonExecutableBeforeLeaveFunction, | ||||
| } from "../types"; | ||||
| import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue"; | ||||
| import HistoryButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton.vue"; | ||||
|  | ||||
| interface DocumentActionButtonsGroupConfig { | ||||
|     storedObject: StoredObject; | ||||
|     small?: boolean; | ||||
|     canEdit?: boolean; | ||||
|     canDownload?: boolean; | ||||
|     canConvertPdf?: boolean; | ||||
|     returnPath?: string; | ||||
|   storedObject: StoredObject; | ||||
|   small?: boolean; | ||||
|   canEdit?: boolean; | ||||
|   canDownload?: boolean; | ||||
|   canConvertPdf?: boolean; | ||||
|   returnPath?: string; | ||||
|  | ||||
|     /** | ||||
|      * Will be the filename displayed to the user when he·she download the document | ||||
|      * (the document will be saved on his disk with this name) | ||||
|      * | ||||
|      * If not set, 'document' will be used. | ||||
|      */ | ||||
|     filename?: string; | ||||
|   /** | ||||
|    * Will be the filename displayed to the user when he·she download the document | ||||
|    * (the document will be saved on his disk with this name) | ||||
|    * | ||||
|    * If not set, 'document' will be used. | ||||
|    */ | ||||
|   filename?: string; | ||||
|  | ||||
|     /** | ||||
|      * If set, will execute this function before leaving to the editor | ||||
|      */ | ||||
|     executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction; | ||||
|   /** | ||||
|    * If set, will execute this function before leaving to the editor | ||||
|    */ | ||||
|   executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction; | ||||
|  | ||||
|     /** | ||||
|      * a link to download and edit file using webdav | ||||
|      */ | ||||
|     davLink?: string; | ||||
|   /** | ||||
|    * a link to download and edit file using webdav | ||||
|    */ | ||||
|   davLink?: string; | ||||
|  | ||||
|     /** | ||||
|      * the expiration date of the download, as a unix timestamp | ||||
|      */ | ||||
|     davLinkExpiration?: number; | ||||
|   /** | ||||
|    * the expiration date of the download, as a unix timestamp | ||||
|    */ | ||||
|   davLinkExpiration?: number; | ||||
| } | ||||
|  | ||||
| const emit = | ||||
|     defineEmits< | ||||
|         ( | ||||
|             e: "onStoredObjectStatusChange", | ||||
|             newStatus: StoredObjectStatusChange, | ||||
|         ) => void | ||||
|     >(); | ||||
|   defineEmits< | ||||
|     ( | ||||
|       e: "onStoredObjectStatusChange", | ||||
|       newStatus: StoredObjectStatusChange, | ||||
|     ) => void | ||||
|   >(); | ||||
|  | ||||
| const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), { | ||||
|     small: false, | ||||
|     canEdit: true, | ||||
|     canDownload: true, | ||||
|     canConvertPdf: true, | ||||
|     returnPath: | ||||
|         window.location.pathname + | ||||
|         window.location.search + | ||||
|         window.location.hash, | ||||
|   small: false, | ||||
|   canEdit: true, | ||||
|   canDownload: true, | ||||
|   canConvertPdf: true, | ||||
|   returnPath: | ||||
|     window.location.pathname + window.location.search + window.location.hash, | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -145,93 +141,93 @@ let tryiesForReady = 0; | ||||
| const maxTryiesForReady = 120; | ||||
|  | ||||
| const isButtonGroupDisplayable = computed<boolean>(() => { | ||||
|     return ( | ||||
|         isDownloadable.value || | ||||
|         isEditableOnline.value || | ||||
|         isEditableOnDesktop.value || | ||||
|         isConvertibleToPdf.value | ||||
|     ); | ||||
|   return ( | ||||
|     isDownloadable.value || | ||||
|     isEditableOnline.value || | ||||
|     isEditableOnDesktop.value || | ||||
|     isConvertibleToPdf.value | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| const isDownloadable = computed<boolean>(() => { | ||||
|     return ( | ||||
|         props.storedObject.status === "ready" || | ||||
|         // happens when the stored object version is just added, but not persisted | ||||
|         (props.storedObject.currentVersion !== null && | ||||
|             props.storedObject.status === "empty") | ||||
|     ); | ||||
|   return ( | ||||
|     props.storedObject.status === "ready" || | ||||
|     // happens when the stored object version is just added, but not persisted | ||||
|     (props.storedObject.currentVersion !== null && | ||||
|       props.storedObject.status === "empty") | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| const isEditableOnline = computed<boolean>(() => { | ||||
|     return ( | ||||
|         props.storedObject.status === "ready" && | ||||
|         props.storedObject._permissions.canEdit && | ||||
|         props.canEdit && | ||||
|         props.storedObject.currentVersion !== null && | ||||
|         is_extension_editable(props.storedObject.currentVersion.type) && | ||||
|         props.storedObject.currentVersion.persisted !== false | ||||
|     ); | ||||
|   return ( | ||||
|     props.storedObject.status === "ready" && | ||||
|     props.storedObject._permissions.canEdit && | ||||
|     props.canEdit && | ||||
|     props.storedObject.currentVersion !== null && | ||||
|     is_extension_editable(props.storedObject.currentVersion.type) && | ||||
|     props.storedObject.currentVersion.persisted !== false | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| const isEditableOnDesktop = computed<boolean>(() => { | ||||
|     return isEditableOnline.value; | ||||
|   return isEditableOnline.value; | ||||
| }); | ||||
|  | ||||
| const isConvertibleToPdf = computed<boolean>(() => { | ||||
|     return ( | ||||
|         props.storedObject.status === "ready" && | ||||
|         props.storedObject._permissions.canSee && | ||||
|         props.canConvertPdf && | ||||
|         props.storedObject.currentVersion !== null && | ||||
|         is_extension_viewable(props.storedObject.currentVersion.type) && | ||||
|         props.storedObject.currentVersion.type !== "application/pdf" && | ||||
|         props.storedObject.currentVersion.persisted !== false | ||||
|     ); | ||||
|   return ( | ||||
|     props.storedObject.status === "ready" && | ||||
|     props.storedObject._permissions.canSee && | ||||
|     props.canConvertPdf && | ||||
|     props.storedObject.currentVersion !== null && | ||||
|     is_extension_viewable(props.storedObject.currentVersion.type) && | ||||
|     props.storedObject.currentVersion.type !== "application/pdf" && | ||||
|     props.storedObject.currentVersion.persisted !== false | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| const isHistoryViewable = computed<boolean>(() => { | ||||
|     return props.storedObject.status === "ready"; | ||||
|   return props.storedObject.status === "ready"; | ||||
| }); | ||||
|  | ||||
| const checkForReady = function (): void { | ||||
|     if ( | ||||
|         "ready" === props.storedObject.status || | ||||
|         "empty" === props.storedObject.status || | ||||
|         "failure" === props.storedObject.status || | ||||
|         // stop reloading if the page stays opened for a long time | ||||
|         tryiesForReady > maxTryiesForReady | ||||
|     ) { | ||||
|         return; | ||||
|     } | ||||
|   if ( | ||||
|     "ready" === props.storedObject.status || | ||||
|     "empty" === props.storedObject.status || | ||||
|     "failure" === props.storedObject.status || | ||||
|     // stop reloading if the page stays opened for a long time | ||||
|     tryiesForReady > maxTryiesForReady | ||||
|   ) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|     tryiesForReady = tryiesForReady + 1; | ||||
|   tryiesForReady = tryiesForReady + 1; | ||||
|  | ||||
|     setTimeout(onObjectNewStatusCallback, 5000); | ||||
|   setTimeout(onObjectNewStatusCallback, 5000); | ||||
| }; | ||||
|  | ||||
| const onObjectNewStatusCallback = async function (): Promise<void> { | ||||
|     if (props.storedObject.status === "stored_object_created") { | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
|  | ||||
|     const new_status = await is_object_ready(props.storedObject); | ||||
|     if (props.storedObject.status !== new_status.status) { | ||||
|         emit("onStoredObjectStatusChange", new_status); | ||||
|         return Promise.resolve(); | ||||
|     } else if ("failure" === new_status.status) { | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
|  | ||||
|     if ("ready" !== new_status.status) { | ||||
|         // we check for new status, unless it is ready | ||||
|         checkForReady(); | ||||
|     } | ||||
|  | ||||
|   if (props.storedObject.status === "stored_object_created") { | ||||
|     return Promise.resolve(); | ||||
|   } | ||||
|  | ||||
|   const new_status = await is_object_ready(props.storedObject); | ||||
|   if (props.storedObject.status !== new_status.status) { | ||||
|     emit("onStoredObjectStatusChange", new_status); | ||||
|     return Promise.resolve(); | ||||
|   } else if ("failure" === new_status.status) { | ||||
|     return Promise.resolve(); | ||||
|   } | ||||
|  | ||||
|   if ("ready" !== new_status.status) { | ||||
|     // we check for new status, unless it is ready | ||||
|     checkForReady(); | ||||
|   } | ||||
|  | ||||
|   return Promise.resolve(); | ||||
| }; | ||||
|  | ||||
| onMounted(() => { | ||||
|     checkForReady(); | ||||
|   checkForReady(); | ||||
| }); | ||||
| </script> | ||||
|  | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -4,36 +4,36 @@ import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n"; | ||||
| import App from "./App.vue"; | ||||
|  | ||||
| const appMessages = { | ||||
|     fr: { | ||||
|         yes: "Oui", | ||||
|         are_you_sure: "Êtes-vous sûr·e?", | ||||
|         you_are_going_to_sign: "Vous allez signer le document", | ||||
|         signature_confirmation: "Confirmation de la signature", | ||||
|         sign: "Signer", | ||||
|         choose_another_signature: "Choisir une autre zone", | ||||
|         cancel: "Annuler", | ||||
|         last_sign_zone: "Zone de signature précédente", | ||||
|         next_sign_zone: "Zone de signature suivante", | ||||
|         add_sign_zone: "Ajouter une zone de signature", | ||||
|         click_on_document: "Cliquer sur le document", | ||||
|         last_zone: "Zone précédente", | ||||
|         next_zone: "Zone suivante", | ||||
|         add_zone: "Ajouter une zone", | ||||
|         another_zone: "Autre zone", | ||||
|         electronic_signature_in_progress: "Signature électronique en cours...", | ||||
|         loading: "Chargement...", | ||||
|         remove_sign_zone: "Enlever la zone", | ||||
|         return: "Retour", | ||||
|         see_all_pages: "Voir toutes les pages", | ||||
|         all_pages: "Toutes les pages", | ||||
|     }, | ||||
|   fr: { | ||||
|     yes: "Oui", | ||||
|     are_you_sure: "Êtes-vous sûr·e?", | ||||
|     you_are_going_to_sign: "Vous allez signer le document", | ||||
|     signature_confirmation: "Confirmation de la signature", | ||||
|     sign: "Signer", | ||||
|     choose_another_signature: "Choisir une autre zone", | ||||
|     cancel: "Annuler", | ||||
|     last_sign_zone: "Zone de signature précédente", | ||||
|     next_sign_zone: "Zone de signature suivante", | ||||
|     add_sign_zone: "Ajouter une zone de signature", | ||||
|     click_on_document: "Cliquer sur le document", | ||||
|     last_zone: "Zone précédente", | ||||
|     next_zone: "Zone suivante", | ||||
|     add_zone: "Ajouter une zone", | ||||
|     another_zone: "Autre zone", | ||||
|     electronic_signature_in_progress: "Signature électronique en cours...", | ||||
|     loading: "Chargement...", | ||||
|     remove_sign_zone: "Enlever la zone", | ||||
|     return: "Retour", | ||||
|     see_all_pages: "Voir toutes les pages", | ||||
|     all_pages: "Toutes les pages", | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| const i18n = _createI18n(appMessages); | ||||
|  | ||||
| const app = createApp({ | ||||
|     template: `<app></app>`, | ||||
|   template: `<app></app>`, | ||||
| }) | ||||
|     .use(i18n) | ||||
|     .component("app", App) | ||||
|     .mount("#document-signature"); | ||||
|   .use(i18n) | ||||
|   .component("app", App) | ||||
|   .mount("#document-signature"); | ||||
|   | ||||
| @@ -1,208 +1,206 @@ | ||||
| <script setup lang="ts"> | ||||
| import { StoredObject, StoredObjectVersionCreated } from "../../types"; | ||||
| import { | ||||
|     encryptFile, | ||||
|     fetchNewStoredObject, | ||||
|     uploadVersion, | ||||
|   encryptFile, | ||||
|   fetchNewStoredObject, | ||||
|   uploadVersion, | ||||
| } from "../../js/async-upload/uploader"; | ||||
| import { computed, ref, Ref } from "vue"; | ||||
| import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue"; | ||||
|  | ||||
| interface DropFileConfig { | ||||
|     existingDoc?: StoredObject; | ||||
|   existingDoc?: StoredObject; | ||||
| } | ||||
|  | ||||
| const props = withDefaults(defineProps<DropFileConfig>(), { | ||||
|     existingDoc: null, | ||||
|   existingDoc: null, | ||||
| }); | ||||
|  | ||||
| const emit = | ||||
|     defineEmits< | ||||
|         ( | ||||
|             e: "addDocument", | ||||
|             { | ||||
|                 stored_object_version: StoredObjectVersionCreated, | ||||
|                 stored_object: StoredObject, | ||||
|                 file_name: string, | ||||
|             }, | ||||
|         ) => void | ||||
|     >(); | ||||
|   defineEmits< | ||||
|     ( | ||||
|       e: "addDocument", | ||||
|       { | ||||
|         stored_object_version: StoredObjectVersionCreated, | ||||
|         stored_object: StoredObject, | ||||
|         file_name: string, | ||||
|       }, | ||||
|     ) => void | ||||
|   >(); | ||||
|  | ||||
| const is_dragging: Ref<boolean> = ref(false); | ||||
| const uploading: Ref<boolean> = ref(false); | ||||
| const display_filename: Ref<string | null> = ref(null); | ||||
|  | ||||
| const has_existing_doc = computed<boolean>(() => { | ||||
|     return props.existingDoc !== undefined && props.existingDoc !== null; | ||||
|   return props.existingDoc !== undefined && props.existingDoc !== null; | ||||
| }); | ||||
|  | ||||
| const onDragOver = (e: Event) => { | ||||
|     e.preventDefault(); | ||||
|   e.preventDefault(); | ||||
|  | ||||
|     is_dragging.value = true; | ||||
|   is_dragging.value = true; | ||||
| }; | ||||
|  | ||||
| const onDragLeave = (e: Event) => { | ||||
|     e.preventDefault(); | ||||
|   e.preventDefault(); | ||||
|  | ||||
|     is_dragging.value = false; | ||||
|   is_dragging.value = false; | ||||
| }; | ||||
|  | ||||
| const onDrop = (e: DragEvent) => { | ||||
|     e.preventDefault(); | ||||
|   e.preventDefault(); | ||||
|  | ||||
|     const files = e.dataTransfer?.files; | ||||
|   const files = e.dataTransfer?.files; | ||||
|  | ||||
|     if (null === files || undefined === files) { | ||||
|         console.error("no files transferred", e.dataTransfer); | ||||
|         return; | ||||
|     } | ||||
|     if (files.length === 0) { | ||||
|         console.error("no files given"); | ||||
|         return; | ||||
|     } | ||||
|   if (null === files || undefined === files) { | ||||
|     console.error("no files transferred", e.dataTransfer); | ||||
|     return; | ||||
|   } | ||||
|   if (files.length === 0) { | ||||
|     console.error("no files given"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|     handleFile(files[0]); | ||||
|   handleFile(files[0]); | ||||
| }; | ||||
|  | ||||
| const onZoneClick = (e: Event) => { | ||||
|     e.stopPropagation(); | ||||
|     e.preventDefault(); | ||||
|   e.stopPropagation(); | ||||
|   e.preventDefault(); | ||||
|  | ||||
|     const input = document.createElement("input"); | ||||
|     input.type = "file"; | ||||
|     input.addEventListener("change", onFileChange); | ||||
|   const input = document.createElement("input"); | ||||
|   input.type = "file"; | ||||
|   input.addEventListener("change", onFileChange); | ||||
|  | ||||
|     input.click(); | ||||
|   input.click(); | ||||
| }; | ||||
|  | ||||
| const onFileChange = async (event: Event): Promise<void> => { | ||||
|     const input = event.target as HTMLInputElement; | ||||
|   const input = event.target as HTMLInputElement; | ||||
|  | ||||
|     if (input.files && input.files[0]) { | ||||
|         console.log("file added", input.files[0]); | ||||
|         const file = input.files[0]; | ||||
|         await handleFile(file); | ||||
|   if (input.files && input.files[0]) { | ||||
|     console.log("file added", input.files[0]); | ||||
|     const file = input.files[0]; | ||||
|     await handleFile(file); | ||||
|  | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
|     return Promise.resolve(); | ||||
|   } | ||||
|  | ||||
|     throw "No file given"; | ||||
|   throw "No file given"; | ||||
| }; | ||||
|  | ||||
| const handleFile = async (file: File): Promise<void> => { | ||||
|     uploading.value = true; | ||||
|     display_filename.value = file.name; | ||||
|     const type = file.type; | ||||
|   uploading.value = true; | ||||
|   display_filename.value = file.name; | ||||
|   const type = file.type; | ||||
|  | ||||
|     // create a stored_object if not exists | ||||
|     let stored_object; | ||||
|     if (null === props.existingDoc) { | ||||
|         stored_object = await fetchNewStoredObject(); | ||||
|     } else { | ||||
|         stored_object = props.existingDoc; | ||||
|     } | ||||
|   // create a stored_object if not exists | ||||
|   let stored_object; | ||||
|   if (null === props.existingDoc) { | ||||
|     stored_object = await fetchNewStoredObject(); | ||||
|   } else { | ||||
|     stored_object = props.existingDoc; | ||||
|   } | ||||
|  | ||||
|     const buffer = await file.arrayBuffer(); | ||||
|     const [encrypted, iv, jsonWebKey] = await encryptFile(buffer); | ||||
|     const filename = await uploadVersion(encrypted, stored_object); | ||||
|   const buffer = await file.arrayBuffer(); | ||||
|   const [encrypted, iv, jsonWebKey] = await encryptFile(buffer); | ||||
|   const filename = await uploadVersion(encrypted, stored_object); | ||||
|  | ||||
|     const stored_object_version: StoredObjectVersionCreated = { | ||||
|         filename: filename, | ||||
|         iv: Array.from(iv), | ||||
|         keyInfos: jsonWebKey, | ||||
|         type: type, | ||||
|         persisted: false, | ||||
|     }; | ||||
|   const stored_object_version: StoredObjectVersionCreated = { | ||||
|     filename: filename, | ||||
|     iv: Array.from(iv), | ||||
|     keyInfos: jsonWebKey, | ||||
|     type: type, | ||||
|     persisted: false, | ||||
|   }; | ||||
|  | ||||
|     const fileName = file.name; | ||||
|     let file_name = "Nouveau document"; | ||||
|     const file_name_split = fileName.split("."); | ||||
|     if (file_name_split.length > 1) { | ||||
|         const extension = file_name_split | ||||
|             ? file_name_split[file_name_split.length - 1] | ||||
|             : ""; | ||||
|         file_name = fileName.replace(extension, "").slice(0, -1); | ||||
|     } | ||||
|   const fileName = file.name; | ||||
|   let file_name = "Nouveau document"; | ||||
|   const file_name_split = fileName.split("."); | ||||
|   if (file_name_split.length > 1) { | ||||
|     const extension = file_name_split | ||||
|       ? file_name_split[file_name_split.length - 1] | ||||
|       : ""; | ||||
|     file_name = fileName.replace(extension, "").slice(0, -1); | ||||
|   } | ||||
|  | ||||
|     emit("addDocument", { | ||||
|         stored_object, | ||||
|         stored_object_version, | ||||
|         file_name: file_name, | ||||
|     }); | ||||
|     uploading.value = false; | ||||
|   emit("addDocument", { | ||||
|     stored_object, | ||||
|     stored_object_version, | ||||
|     file_name: file_name, | ||||
|   }); | ||||
|   uploading.value = false; | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <div class="drop-file"> | ||||
|         <div | ||||
|             v-if="!uploading" | ||||
|             :class="{ area: true, dragging: is_dragging }" | ||||
|             @click="onZoneClick" | ||||
|             @dragover="onDragOver" | ||||
|             @dragleave="onDragLeave" | ||||
|             @drop="onDrop" | ||||
|         > | ||||
|             <p v-if="has_existing_doc" class="file-icon"> | ||||
|                 <file-icon :type="props.existingDoc?.type"></file-icon> | ||||
|             </p> | ||||
|   <div class="drop-file"> | ||||
|     <div | ||||
|       v-if="!uploading" | ||||
|       :class="{ area: true, dragging: is_dragging }" | ||||
|       @click="onZoneClick" | ||||
|       @dragover="onDragOver" | ||||
|       @dragleave="onDragLeave" | ||||
|       @drop="onDrop" | ||||
|     > | ||||
|       <p v-if="has_existing_doc" class="file-icon"> | ||||
|         <file-icon :type="props.existingDoc?.type"></file-icon> | ||||
|       </p> | ||||
|  | ||||
|             <p v-if="display_filename !== null" class="display-filename"> | ||||
|                 {{ display_filename }} | ||||
|             </p> | ||||
|             <!-- todo i18n --> | ||||
|             <p v-if="has_existing_doc"> | ||||
|                 Déposez un document ou cliquez ici pour remplacer le document | ||||
|                 existant | ||||
|             </p> | ||||
|             <p v-else> | ||||
|                 Déposez un document ou cliquez ici pour ouvrir le navigateur de | ||||
|                 fichier | ||||
|             </p> | ||||
|         </div> | ||||
|         <div v-else class="waiting"> | ||||
|             <i class="fa fa-cog fa-spin fa-3x fa-fw"></i> | ||||
|             <span class="sr-only">Loading...</span> | ||||
|         </div> | ||||
|       <p v-if="display_filename !== null" class="display-filename"> | ||||
|         {{ display_filename }} | ||||
|       </p> | ||||
|       <!-- todo i18n --> | ||||
|       <p v-if="has_existing_doc"> | ||||
|         Déposez un document ou cliquez ici pour remplacer le document existant | ||||
|       </p> | ||||
|       <p v-else> | ||||
|         Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier | ||||
|       </p> | ||||
|     </div> | ||||
|     <div v-else class="waiting"> | ||||
|       <i class="fa fa-cog fa-spin fa-3x fa-fw"></i> | ||||
|       <span class="sr-only">Loading...</span> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| .drop-file { | ||||
|   width: 100%; | ||||
|  | ||||
|   .file-icon { | ||||
|     font-size: xx-large; | ||||
|   } | ||||
|  | ||||
|   .display-filename { | ||||
|     font-variant: small-caps; | ||||
|     font-weight: 200; | ||||
|   } | ||||
|  | ||||
|   & > .area, | ||||
|   & > .waiting { | ||||
|     width: 100%; | ||||
|     height: 10rem; | ||||
|  | ||||
|     .file-icon { | ||||
|         font-size: xx-large; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|  | ||||
|     p { | ||||
|       // require for display in DropFileModal | ||||
|       text-align: center; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|     .display-filename { | ||||
|         font-variant: small-caps; | ||||
|         font-weight: 200; | ||||
|     } | ||||
|  | ||||
|     & > .area, | ||||
|     & > .waiting { | ||||
|         width: 100%; | ||||
|         height: 10rem; | ||||
|  | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         justify-content: center; | ||||
|         align-items: center; | ||||
|  | ||||
|         p { | ||||
|             // require for display in DropFileModal | ||||
|             text-align: center; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     & > .area { | ||||
|         border: 4px dashed #ccc; | ||||
|  | ||||
|         &.dragging { | ||||
|             border: 4px dashed blue; | ||||
|         } | ||||
|   & > .area { | ||||
|     border: 4px dashed #ccc; | ||||
|  | ||||
|     &.dragging { | ||||
|       border: 4px dashed blue; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -4,27 +4,26 @@ import { StoredObject, StoredObjectVersion } from "../../types"; | ||||
| import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue"; | ||||
| import { computed, reactive } from "vue"; | ||||
| import { useToast } from "vue-toast-notification"; | ||||
| import { DOCUMENT_ADD, trans } from "translator"; | ||||
|  | ||||
| interface DropFileConfig { | ||||
|     allowRemove: boolean; | ||||
|     existingDoc?: StoredObject; | ||||
|   allowRemove: boolean; | ||||
|   existingDoc?: StoredObject; | ||||
| } | ||||
|  | ||||
| const props = withDefaults(defineProps<DropFileConfig>(), { | ||||
|     allowRemove: false, | ||||
|   allowRemove: false, | ||||
| }); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
|     ( | ||||
|         e: "addDocument", | ||||
|         { | ||||
|             stored_object: StoredObject, | ||||
|             stored_object_version: StoredObjectVersion, | ||||
|             file_name: string, | ||||
|         }, | ||||
|     ): void; | ||||
|     (e: "removeDocument"): void; | ||||
|   ( | ||||
|     e: "addDocument", | ||||
|     { | ||||
|       stored_object: StoredObject, | ||||
|       stored_object_version: StoredObjectVersion, | ||||
|       file_name: string, | ||||
|     }, | ||||
|   ): void; | ||||
|   (e: "removeDocument"): void; | ||||
| }>(); | ||||
|  | ||||
| const $toast = useToast(); | ||||
| @@ -34,65 +33,67 @@ const state = reactive({ showModal: false }); | ||||
| const modalClasses = { "modal-dialog-centered": true, "modal-md": true }; | ||||
|  | ||||
| const buttonState = computed<"add" | "replace">(() => { | ||||
|     if (props.existingDoc === undefined || props.existingDoc === null) { | ||||
|         return "add"; | ||||
|     } | ||||
|   if (props.existingDoc === undefined || props.existingDoc === null) { | ||||
|     return "add"; | ||||
|   } | ||||
|  | ||||
|     return "replace"; | ||||
|   return "replace"; | ||||
| }); | ||||
|  | ||||
| function onAddDocument({ | ||||
|     stored_object, | ||||
|     stored_object_version, | ||||
|     file_name, | ||||
|   stored_object, | ||||
|   stored_object_version, | ||||
|   file_name, | ||||
| }: { | ||||
|     stored_object: StoredObject; | ||||
|     stored_object_version: StoredObjectVersion; | ||||
|     file_name: string; | ||||
|   stored_object: StoredObject; | ||||
|   stored_object_version: StoredObjectVersion; | ||||
|   file_name: string; | ||||
| }): void { | ||||
|     const message = | ||||
|         buttonState.value === "add" ? "Document ajouté" : "Document remplacé"; | ||||
|     $toast.success(message); | ||||
|     emit("addDocument", { stored_object_version, stored_object, file_name }); | ||||
|     state.showModal = false; | ||||
|   const message = | ||||
|     buttonState.value === "add" ? "Document ajouté" : "Document remplacé"; | ||||
|   $toast.success(message); | ||||
|   emit("addDocument", { stored_object_version, stored_object, file_name }); | ||||
|   state.showModal = false; | ||||
| } | ||||
|  | ||||
| function onRemoveDocument(): void { | ||||
|     emit("removeDocument"); | ||||
|   emit("removeDocument"); | ||||
| } | ||||
|  | ||||
| function openModal(): void { | ||||
|     state.showModal = true; | ||||
|   state.showModal = true; | ||||
| } | ||||
|  | ||||
| function closeModal(): void { | ||||
|     state.showModal = false; | ||||
|   state.showModal = false; | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <button | ||||
|         v-if="buttonState === 'add'" | ||||
|         @click="openModal" | ||||
|         class="btn btn-create" | ||||
|     > | ||||
|         {{ trans(DOCUMENT_ADD) }} | ||||
|     </button> | ||||
|     <button v-else @click="openModal" class="btn btn-edit"></button> | ||||
|     <modal | ||||
|         v-if="state.showModal" | ||||
|         :modal-dialog-class="modalClasses" | ||||
|         @close="closeModal" | ||||
|     > | ||||
|         <template v-slot:body> | ||||
|             <drop-file-widget | ||||
|                 :existing-doc="existingDoc" | ||||
|                 :allow-remove="allowRemove" | ||||
|                 @add-document="onAddDocument" | ||||
|                 @remove-document="onRemoveDocument" | ||||
|             ></drop-file-widget> | ||||
|         </template> | ||||
|     </modal> | ||||
|   <button | ||||
|     v-if="buttonState === 'add'" | ||||
|     @click="openModal" | ||||
|     class="btn btn-create" | ||||
|   > | ||||
|     Ajouter un document | ||||
|   </button> | ||||
|   <button v-else @click="openModal" class="btn btn-edit"> | ||||
|     Remplacer le document | ||||
|   </button> | ||||
|   <modal | ||||
|     v-if="state.showModal" | ||||
|     :modal-dialog-class="modalClasses" | ||||
|     @close="closeModal" | ||||
|   > | ||||
|     <template v-slot:body> | ||||
|       <drop-file-widget | ||||
|         :existing-doc="existingDoc" | ||||
|         :allow-remove="allowRemove" | ||||
|         @add-document="onAddDocument" | ||||
|         @remove-document="onRemoveDocument" | ||||
|       ></drop-file-widget> | ||||
|     </template> | ||||
|   </modal> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"></style> | ||||
|   | ||||
| @@ -5,97 +5,97 @@ import DropFile from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFile.vue"; | ||||
| import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue"; | ||||
|  | ||||
| interface DropFileConfig { | ||||
|     allowRemove: boolean; | ||||
|     existingDoc?: StoredObject; | ||||
|   allowRemove: boolean; | ||||
|   existingDoc?: StoredObject; | ||||
| } | ||||
|  | ||||
| const props = withDefaults(defineProps<DropFileConfig>(), { | ||||
|     allowRemove: false, | ||||
|   allowRemove: false, | ||||
| }); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
|     ( | ||||
|         e: "addDocument", | ||||
|         { | ||||
|             stored_object: StoredObject, | ||||
|             stored_object_version: StoredObjectVersion, | ||||
|             file_name: string, | ||||
|         }, | ||||
|     ): void; | ||||
|     (e: "removeDocument"): void; | ||||
|   ( | ||||
|     e: "addDocument", | ||||
|     { | ||||
|       stored_object: StoredObject, | ||||
|       stored_object_version: StoredObjectVersion, | ||||
|       file_name: string, | ||||
|     }, | ||||
|   ): void; | ||||
|   (e: "removeDocument"): void; | ||||
| }>(); | ||||
|  | ||||
| const has_existing_doc = computed<boolean>(() => { | ||||
|     return props.existingDoc !== undefined && props.existingDoc !== null; | ||||
|   return props.existingDoc !== undefined && props.existingDoc !== null; | ||||
| }); | ||||
|  | ||||
| const dav_link_expiration = computed<number | undefined>(() => { | ||||
|     if (props.existingDoc === undefined || props.existingDoc === null) { | ||||
|         return undefined; | ||||
|     } | ||||
|     if (props.existingDoc.status !== "ready") { | ||||
|         return undefined; | ||||
|     } | ||||
|   if (props.existingDoc === undefined || props.existingDoc === null) { | ||||
|     return undefined; | ||||
|   } | ||||
|   if (props.existingDoc.status !== "ready") { | ||||
|     return undefined; | ||||
|   } | ||||
|  | ||||
|     return props.existingDoc._links?.dav_link?.expiration; | ||||
|   return props.existingDoc._links?.dav_link?.expiration; | ||||
| }); | ||||
|  | ||||
| const dav_link_href = computed<string | undefined>(() => { | ||||
|     if (props.existingDoc === undefined || props.existingDoc === null) { | ||||
|         return undefined; | ||||
|     } | ||||
|     if (props.existingDoc.status !== "ready") { | ||||
|         return undefined; | ||||
|     } | ||||
|   if (props.existingDoc === undefined || props.existingDoc === null) { | ||||
|     return undefined; | ||||
|   } | ||||
|   if (props.existingDoc.status !== "ready") { | ||||
|     return undefined; | ||||
|   } | ||||
|  | ||||
|     return props.existingDoc._links?.dav_link?.href; | ||||
|   return props.existingDoc._links?.dav_link?.href; | ||||
| }); | ||||
|  | ||||
| const onAddDocument = ({ | ||||
|     stored_object, | ||||
|     stored_object_version, | ||||
|     file_name, | ||||
|   stored_object, | ||||
|   stored_object_version, | ||||
|   file_name, | ||||
| }: { | ||||
|     stored_object: StoredObject; | ||||
|     stored_object_version: StoredObjectVersion; | ||||
|     file_name: string; | ||||
|   stored_object: StoredObject; | ||||
|   stored_object_version: StoredObjectVersion; | ||||
|   file_name: string; | ||||
| }): void => { | ||||
|     emit("addDocument", { stored_object, stored_object_version, file_name }); | ||||
|   emit("addDocument", { stored_object, stored_object_version, file_name }); | ||||
| }; | ||||
|  | ||||
| const onRemoveDocument = (e: Event): void => { | ||||
|     e.stopPropagation(); | ||||
|     e.preventDefault(); | ||||
|     emit("removeDocument"); | ||||
|   e.stopPropagation(); | ||||
|   e.preventDefault(); | ||||
|   emit("removeDocument"); | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <div> | ||||
|         <drop-file | ||||
|             :existingDoc="props.existingDoc" | ||||
|             @addDocument="onAddDocument" | ||||
|         ></drop-file> | ||||
|   <div> | ||||
|     <drop-file | ||||
|       :existingDoc="props.existingDoc" | ||||
|       @addDocument="onAddDocument" | ||||
|     ></drop-file> | ||||
|  | ||||
|         <ul class="record_actions"> | ||||
|             <li v-if="has_existing_doc"> | ||||
|                 <document-action-buttons-group | ||||
|                     :stored-object="props.existingDoc" | ||||
|                     :can-edit="props.existingDoc?.status === 'ready'" | ||||
|                     :can-download="true" | ||||
|                     :dav-link="dav_link_href" | ||||
|                     :dav-link-expiration="dav_link_expiration" | ||||
|                 /> | ||||
|             </li> | ||||
|             <li> | ||||
|                 <button | ||||
|                     v-if="allowRemove" | ||||
|                     class="btn btn-delete" | ||||
|                     @click="onRemoveDocument($event)" | ||||
|                 ></button> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
|     <ul class="record_actions"> | ||||
|       <li v-if="has_existing_doc"> | ||||
|         <document-action-buttons-group | ||||
|           :stored-object="props.existingDoc" | ||||
|           :can-edit="props.existingDoc?.status === 'ready'" | ||||
|           :can-download="true" | ||||
|           :dav-link="dav_link_href" | ||||
|           :dav-link-expiration="dav_link_expiration" | ||||
|         /> | ||||
|       </li> | ||||
|       <li> | ||||
|         <button | ||||
|           v-if="allowRemove" | ||||
|           class="btn btn-delete" | ||||
|           @click="onRemoveDocument($event)" | ||||
|         ></button> | ||||
|       </li> | ||||
|     </ul> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"></style> | ||||
|   | ||||
| @@ -1,46 +1,46 @@ | ||||
| <script setup lang="ts"> | ||||
| interface FileIconConfig { | ||||
|     type: string; | ||||
|   type: string; | ||||
| } | ||||
|  | ||||
| const props = defineProps<FileIconConfig>(); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <i class="fa fa-file-pdf-o" v-if="props.type === 'application/pdf'"></i> | ||||
|     <i | ||||
|         class="fa fa-file-word-o" | ||||
|         v-else-if="props.type === 'application/vnd.oasis.opendocument.text'" | ||||
|     ></i> | ||||
|     <i | ||||
|         class="fa fa-file-word-o" | ||||
|         v-else-if=" | ||||
|             props.type === | ||||
|             'application/vnd.openxmlformats-officedocument.wordprocessingml.document' | ||||
|         " | ||||
|     ></i> | ||||
|     <i | ||||
|         class="fa fa-file-word-o" | ||||
|         v-else-if="props.type === 'application/msword'" | ||||
|     ></i> | ||||
|     <i | ||||
|         class="fa fa-file-excel-o" | ||||
|         v-else-if=" | ||||
|             props.type === | ||||
|             'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' | ||||
|         " | ||||
|     ></i> | ||||
|     <i | ||||
|         class="fa fa-file-excel-o" | ||||
|         v-else-if="props.type === 'application/vnd.ms-excel'" | ||||
|     ></i> | ||||
|     <i class="fa fa-file-image-o" v-else-if="props.type === 'image/jpeg'"></i> | ||||
|     <i class="fa fa-file-image-o" v-else-if="props.type === 'image/png'"></i> | ||||
|     <i | ||||
|         class="fa fa-file-archive-o" | ||||
|         v-else-if="props.type === 'application/x-zip-compressed'" | ||||
|     ></i> | ||||
|     <i class="fa fa-file-code-o" v-else></i> | ||||
|   <i class="fa fa-file-pdf-o" v-if="props.type === 'application/pdf'"></i> | ||||
|   <i | ||||
|     class="fa fa-file-word-o" | ||||
|     v-else-if="props.type === 'application/vnd.oasis.opendocument.text'" | ||||
|   ></i> | ||||
|   <i | ||||
|     class="fa fa-file-word-o" | ||||
|     v-else-if=" | ||||
|       props.type === | ||||
|       'application/vnd.openxmlformats-officedocument.wordprocessingml.document' | ||||
|     " | ||||
|   ></i> | ||||
|   <i | ||||
|     class="fa fa-file-word-o" | ||||
|     v-else-if="props.type === 'application/msword'" | ||||
|   ></i> | ||||
|   <i | ||||
|     class="fa fa-file-excel-o" | ||||
|     v-else-if=" | ||||
|       props.type === | ||||
|       'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' | ||||
|     " | ||||
|   ></i> | ||||
|   <i | ||||
|     class="fa fa-file-excel-o" | ||||
|     v-else-if="props.type === 'application/vnd.ms-excel'" | ||||
|   ></i> | ||||
|   <i class="fa fa-file-image-o" v-else-if="props.type === 'image/jpeg'"></i> | ||||
|   <i class="fa fa-file-image-o" v-else-if="props.type === 'image/png'"></i> | ||||
|   <i | ||||
|     class="fa fa-file-archive-o" | ||||
|     v-else-if="props.type === 'application/x-zip-compressed'" | ||||
|   ></i> | ||||
|   <i class="fa fa-file-code-o" v-else></i> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"></style> | ||||
|   | ||||
| @@ -1,28 +1,28 @@ | ||||
| <template> | ||||
|     <a :class="props.classes" @click="download_and_open($event)" ref="btn"> | ||||
|         <i class="fa fa-file-pdf-o"></i> | ||||
|         Télécharger en pdf | ||||
|     </a> | ||||
|   <a :class="props.classes" @click="download_and_open($event)" ref="btn"> | ||||
|     <i class="fa fa-file-pdf-o"></i> | ||||
|     Télécharger en pdf | ||||
|   </a> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { | ||||
|     build_convert_link, | ||||
|     download_and_decrypt_doc, | ||||
|     download_doc, | ||||
|   build_convert_link, | ||||
|   download_and_decrypt_doc, | ||||
|   download_doc, | ||||
| } from "./helpers"; | ||||
| import mime from "mime"; | ||||
| import { reactive, ref } from "vue"; | ||||
| import { StoredObject } from "../../types"; | ||||
|  | ||||
| interface ConvertButtonConfig { | ||||
|     storedObject: StoredObject; | ||||
|     classes: Record<string, boolean>; | ||||
|     filename?: string; | ||||
|   storedObject: StoredObject; | ||||
|   classes: Record<string, boolean>; | ||||
|   filename?: string; | ||||
| } | ||||
|  | ||||
| interface DownloadButtonState { | ||||
|     content: null | string; | ||||
|   content: null | string; | ||||
| } | ||||
|  | ||||
| const props = defineProps<ConvertButtonConfig>(); | ||||
| @@ -30,36 +30,34 @@ const state: DownloadButtonState = reactive({ content: null }); | ||||
| const btn = ref<HTMLAnchorElement | null>(null); | ||||
|  | ||||
| async function download_and_open(event: Event): Promise<void> { | ||||
|     const button = event.target as HTMLAnchorElement; | ||||
|   const button = event.target as HTMLAnchorElement; | ||||
|  | ||||
|     if (null === state.content) { | ||||
|         event.preventDefault(); | ||||
|   if (null === state.content) { | ||||
|     event.preventDefault(); | ||||
|  | ||||
|         const raw = await download_doc( | ||||
|             build_convert_link(props.storedObject.uuid), | ||||
|         ); | ||||
|         state.content = window.URL.createObjectURL(raw); | ||||
|     const raw = await download_doc(build_convert_link(props.storedObject.uuid)); | ||||
|     state.content = window.URL.createObjectURL(raw); | ||||
|  | ||||
|         button.href = window.URL.createObjectURL(raw); | ||||
|         button.type = "application/pdf"; | ||||
|     button.href = window.URL.createObjectURL(raw); | ||||
|     button.type = "application/pdf"; | ||||
|  | ||||
|         button.download = props.filename + ".pdf" || "document.pdf"; | ||||
|     } | ||||
|     button.download = props.filename + ".pdf" || "document.pdf"; | ||||
|   } | ||||
|  | ||||
|     button.click(); | ||||
|     const reset_pending = setTimeout(reset_state, 45000); | ||||
|   button.click(); | ||||
|   const reset_pending = setTimeout(reset_state, 45000); | ||||
| } | ||||
|  | ||||
| function reset_state(): void { | ||||
|     state.content = null; | ||||
|     btn.value?.removeAttribute("download"); | ||||
|     btn.value?.removeAttribute("href"); | ||||
|     btn.value?.removeAttribute("type"); | ||||
|   state.content = null; | ||||
|   btn.value?.removeAttribute("download"); | ||||
|   btn.value?.removeAttribute("href"); | ||||
|   btn.value?.removeAttribute("type"); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| i.fa::before { | ||||
|     color: var(--bs-dropdown-link-hover-color); | ||||
|   color: var(--bs-dropdown-link-hover-color); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -3,13 +3,13 @@ import Modal from "ChillMainAssets/vuejs/_components/Modal.vue"; | ||||
| import { computed, reactive } from "vue"; | ||||
|  | ||||
| export interface DesktopEditButtonConfig { | ||||
|     editLink: null; | ||||
|     classes: Record<string, boolean>; | ||||
|     expirationLink: number | Date; | ||||
|   editLink: null; | ||||
|   classes: Record<string, boolean>; | ||||
|   expirationLink: number | Date; | ||||
| } | ||||
|  | ||||
| interface DesktopEditButtonState { | ||||
|     modalOpened: boolean; | ||||
|   modalOpened: boolean; | ||||
| } | ||||
|  | ||||
| const state: DesktopEditButtonState = reactive({ modalOpened: false }); | ||||
| @@ -17,80 +17,76 @@ const state: DesktopEditButtonState = reactive({ modalOpened: false }); | ||||
| const props = defineProps<DesktopEditButtonConfig>(); | ||||
|  | ||||
| const buildCommand = computed<string>( | ||||
|     () => "vnd.libreoffice.command:ofe|u|" + props.editLink, | ||||
|   () => "vnd.libreoffice.command:ofe|u|" + props.editLink, | ||||
| ); | ||||
|  | ||||
| const editionUntilFormatted = computed<string>(() => { | ||||
|     let d; | ||||
|   let d; | ||||
|  | ||||
|     if (props.expirationLink instanceof Date) { | ||||
|         d = props.expirationLink; | ||||
|     } else { | ||||
|         d = new Date(props.expirationLink * 1000); | ||||
|     } | ||||
|     console.log(props.expirationLink); | ||||
|   if (props.expirationLink instanceof Date) { | ||||
|     d = props.expirationLink; | ||||
|   } else { | ||||
|     d = new Date(props.expirationLink * 1000); | ||||
|   } | ||||
|   console.log(props.expirationLink); | ||||
|  | ||||
|     return new Intl.DateTimeFormat(undefined, { | ||||
|         dateStyle: "long", | ||||
|         timeStyle: "medium", | ||||
|     }).format(d); | ||||
|   return new Intl.DateTimeFormat(undefined, { | ||||
|     dateStyle: "long", | ||||
|     timeStyle: "medium", | ||||
|   }).format(d); | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <teleport to="body"> | ||||
|         <modal v-if="state.modalOpened" @close="state.modalOpened = false"> | ||||
|             <template v-slot:body> | ||||
|                 <div class="desktop-edit"> | ||||
|                     <p class="center"> | ||||
|                         Veuillez enregistrer vos modifications avant le | ||||
|                     </p> | ||||
|                     <p> | ||||
|                         <strong>{{ editionUntilFormatted }}</strong> | ||||
|                     </p> | ||||
|   <teleport to="body"> | ||||
|     <modal v-if="state.modalOpened" @close="state.modalOpened = false"> | ||||
|       <template v-slot:body> | ||||
|         <div class="desktop-edit"> | ||||
|           <p class="center">Veuillez enregistrer vos modifications avant le</p> | ||||
|           <p> | ||||
|             <strong>{{ editionUntilFormatted }}</strong> | ||||
|           </p> | ||||
|  | ||||
|                     <p> | ||||
|                         <a class="btn btn-primary" :href="buildCommand" | ||||
|                             >Ouvrir le document pour édition</a | ||||
|                         > | ||||
|                     </p> | ||||
|           <p> | ||||
|             <a class="btn btn-primary" :href="buildCommand" | ||||
|               >Ouvrir le document pour édition</a | ||||
|             > | ||||
|           </p> | ||||
|  | ||||
|                     <p> | ||||
|                         <small | ||||
|                             >Le document peut être édité uniquement en utilisant | ||||
|                             Libre Office.</small | ||||
|                         > | ||||
|                     </p> | ||||
|           <p> | ||||
|             <small | ||||
|               >Le document peut être édité uniquement en utilisant Libre | ||||
|               Office.</small | ||||
|             > | ||||
|           </p> | ||||
|  | ||||
|                     <p> | ||||
|                         <small | ||||
|                             >En cas d'échec lors de l'enregistrement, sauver le | ||||
|                             document sur le poste de travail avant de le déposer | ||||
|                             à nouveau ici.</small | ||||
|                         > | ||||
|                     </p> | ||||
|           <p> | ||||
|             <small | ||||
|               >En cas d'échec lors de l'enregistrement, sauver le document sur | ||||
|               le poste de travail avant de le déposer à nouveau ici.</small | ||||
|             > | ||||
|           </p> | ||||
|  | ||||
|                     <p> | ||||
|                         <small | ||||
|                             >Vous pouvez naviguez sur d'autres pages pendant | ||||
|                             l'édition.</small | ||||
|                         > | ||||
|                     </p> | ||||
|                 </div> | ||||
|             </template> | ||||
|         </modal> | ||||
|     </teleport> | ||||
|     <a :class="props.classes" @click="state.modalOpened = true"> | ||||
|         <i class="fa fa-desktop"></i> | ||||
|         Éditer sur le bureau | ||||
|     </a> | ||||
|           <p> | ||||
|             <small | ||||
|               >Vous pouvez naviguez sur d'autres pages pendant l'édition.</small | ||||
|             > | ||||
|           </p> | ||||
|         </div> | ||||
|       </template> | ||||
|     </modal> | ||||
|   </teleport> | ||||
|   <a :class="props.classes" @click="state.modalOpened = true"> | ||||
|     <i class="fa fa-desktop"></i> | ||||
|     Éditer sur le bureau | ||||
|   </a> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| .desktop-edit { | ||||
|     text-align: center; | ||||
|   text-align: center; | ||||
| } | ||||
| i.fa::before { | ||||
|     color: var(--bs-dropdown-link-hover-color); | ||||
|   color: var(--bs-dropdown-link-hover-color); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,26 +1,26 @@ | ||||
| <template> | ||||
|     <a | ||||
|         v-if="!state.is_ready" | ||||
|         :class="props.classes" | ||||
|         @click="download_and_open()" | ||||
|         title="Télécharger" | ||||
|     > | ||||
|         <i class="fa fa-download"></i> | ||||
|         <template v-if="displayActionStringInButton">Télécharger</template> | ||||
|     </a> | ||||
|     <a | ||||
|         v-else | ||||
|         :class="props.classes" | ||||
|         target="_blank" | ||||
|         :type="props.atVersion.type" | ||||
|         :download="buildDocumentName()" | ||||
|         :href="state.href_url" | ||||
|         ref="open_button" | ||||
|         title="Ouvrir" | ||||
|     > | ||||
|         <i class="fa fa-external-link"></i> | ||||
|         <template v-if="displayActionStringInButton">Ouvrir</template> | ||||
|     </a> | ||||
|   <a | ||||
|     v-if="!state.is_ready" | ||||
|     :class="props.classes" | ||||
|     @click="download_and_open()" | ||||
|     title="Télécharger" | ||||
|   > | ||||
|     <i class="fa fa-download"></i> | ||||
|     <template v-if="displayActionStringInButton">Télécharger</template> | ||||
|   </a> | ||||
|   <a | ||||
|     v-else | ||||
|     :class="props.classes" | ||||
|     target="_blank" | ||||
|     :type="props.atVersion.type" | ||||
|     :download="buildDocumentName()" | ||||
|     :href="state.href_url" | ||||
|     ref="open_button" | ||||
|     title="Ouvrir" | ||||
|   > | ||||
|     <i class="fa fa-external-link"></i> | ||||
|     <template v-if="displayActionStringInButton">Ouvrir</template> | ||||
|   </a> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| @@ -30,112 +30,109 @@ import mime from "mime"; | ||||
| import { StoredObject, StoredObjectVersion } from "../../types"; | ||||
|  | ||||
| interface DownloadButtonConfig { | ||||
|     storedObject: StoredObject; | ||||
|     atVersion: StoredObjectVersion; | ||||
|     classes: Record<string, boolean>; | ||||
|     filename?: string; | ||||
|     /** | ||||
|      * if true, display the action string into the button. If false, displays only | ||||
|      * the icon | ||||
|      */ | ||||
|     displayActionStringInButton?: boolean; | ||||
|     /** | ||||
|      * if true, will download directly the file on load | ||||
|      */ | ||||
|     directDownload?: boolean; | ||||
|   storedObject: StoredObject; | ||||
|   atVersion: StoredObjectVersion; | ||||
|   classes: Record<string, boolean>; | ||||
|   filename?: string; | ||||
|   /** | ||||
|    * if true, display the action string into the button. If false, displays only | ||||
|    * the icon | ||||
|    */ | ||||
|   displayActionStringInButton?: boolean; | ||||
|   /** | ||||
|    * if true, will download directly the file on load | ||||
|    */ | ||||
|   directDownload?: boolean; | ||||
| } | ||||
|  | ||||
| interface DownloadButtonState { | ||||
|     is_ready: boolean; | ||||
|     is_running: boolean; | ||||
|     href_url: string; | ||||
|   is_ready: boolean; | ||||
|   is_running: boolean; | ||||
|   href_url: string; | ||||
| } | ||||
|  | ||||
| const props = withDefaults(defineProps<DownloadButtonConfig>(), { | ||||
|     displayActionStringInButton: true, | ||||
|     directDownload: false, | ||||
|   displayActionStringInButton: true, | ||||
|   directDownload: false, | ||||
| }); | ||||
| const state: DownloadButtonState = reactive({ | ||||
|     is_ready: false, | ||||
|     is_running: false, | ||||
|     href_url: "#", | ||||
|   is_ready: false, | ||||
|   is_running: false, | ||||
|   href_url: "#", | ||||
| }); | ||||
|  | ||||
| const open_button = ref<HTMLAnchorElement | null>(null); | ||||
|  | ||||
| function buildDocumentName(): string { | ||||
|     let document_name = props.filename ?? props.storedObject.title; | ||||
|   let document_name = props.filename ?? props.storedObject.title; | ||||
|  | ||||
|     if ("" === document_name || null === document_name) { | ||||
|         document_name = "document"; | ||||
|     } | ||||
|   if ("" === document_name || null === document_name) { | ||||
|     document_name = "document"; | ||||
|   } | ||||
|  | ||||
|     const ext = mime.getExtension(props.atVersion.type); | ||||
|   const ext = mime.getExtension(props.atVersion.type); | ||||
|  | ||||
|     if (null !== ext) { | ||||
|         return document_name + "." + ext; | ||||
|     } | ||||
|   if (null !== ext) { | ||||
|     return document_name + "." + ext; | ||||
|   } | ||||
|  | ||||
|     return document_name; | ||||
|   return document_name; | ||||
| } | ||||
|  | ||||
| async function download_and_open(): Promise<void> { | ||||
|     if (state.is_running) { | ||||
|         console.log("state is running, aborting"); | ||||
|         return; | ||||
|     } | ||||
|   if (state.is_running) { | ||||
|     console.log("state is running, aborting"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|     state.is_running = true; | ||||
|   state.is_running = true; | ||||
|  | ||||
|     if (state.is_ready) { | ||||
|         console.log("state is ready. This should not happens"); | ||||
|         return; | ||||
|     } | ||||
|   if (state.is_ready) { | ||||
|     console.log("state is ready. This should not happens"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|     let raw; | ||||
|   let raw; | ||||
|  | ||||
|     try { | ||||
|         raw = await download_and_decrypt_doc( | ||||
|             props.storedObject, | ||||
|             props.atVersion, | ||||
|         ); | ||||
|     } catch (e) { | ||||
|         console.error("error while downloading and decrypting document"); | ||||
|         console.error(e); | ||||
|         throw e; | ||||
|     } | ||||
|   try { | ||||
|     raw = await download_and_decrypt_doc(props.storedObject, props.atVersion); | ||||
|   } catch (e) { | ||||
|     console.error("error while downloading and decrypting document"); | ||||
|     console.error(e); | ||||
|     throw e; | ||||
|   } | ||||
|  | ||||
|     state.href_url = window.URL.createObjectURL(raw); | ||||
|     state.is_running = false; | ||||
|     state.is_ready = true; | ||||
|   state.href_url = window.URL.createObjectURL(raw); | ||||
|   state.is_running = false; | ||||
|   state.is_ready = true; | ||||
|  | ||||
|     if (!props.directDownload) { | ||||
|         await nextTick(); | ||||
|         open_button.value?.click(); | ||||
|   if (!props.directDownload) { | ||||
|     await nextTick(); | ||||
|     open_button.value?.click(); | ||||
|  | ||||
|         console.log("open button should have been clicked"); | ||||
|         setTimeout(reset_state, 45000); | ||||
|     } | ||||
|     console.log("open button should have been clicked"); | ||||
|     setTimeout(reset_state, 45000); | ||||
|   } | ||||
| } | ||||
|  | ||||
| function reset_state(): void { | ||||
|     state.href_url = "#"; | ||||
|     state.is_ready = false; | ||||
|     state.is_running = false; | ||||
|   state.href_url = "#"; | ||||
|   state.is_ready = false; | ||||
|   state.is_running = false; | ||||
| } | ||||
|  | ||||
| onMounted(() => { | ||||
|     if (props.directDownload) { | ||||
|         download_and_open(); | ||||
|     } | ||||
|   if (props.directDownload) { | ||||
|     download_and_open(); | ||||
|   } | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| i.fa::before { | ||||
|     color: var(--bs-dropdown-link-hover-color); | ||||
|   color: var(--bs-dropdown-link-hover-color); | ||||
| } | ||||
| i.fa { | ||||
|     margin-right: 0.5rem; | ||||
|   margin-right: 0.5rem; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,20 +1,20 @@ | ||||
| <script setup lang="ts"> | ||||
| import HistoryButtonModal from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonModal.vue"; | ||||
| import { | ||||
|     StoredObject, | ||||
|     StoredObjectVersionWithPointInTime, | ||||
|   StoredObject, | ||||
|   StoredObjectVersionWithPointInTime, | ||||
| } from "./../../types"; | ||||
| import { computed, reactive, ref, useTemplateRef } from "vue"; | ||||
| import { get_versions } from "./HistoryButton/api"; | ||||
|  | ||||
| interface HistoryButtonConfig { | ||||
|     storedObject: StoredObject; | ||||
|     canEdit: boolean; | ||||
|   storedObject: StoredObject; | ||||
|   canEdit: boolean; | ||||
| } | ||||
|  | ||||
| interface HistoryButtonState { | ||||
|     versions: StoredObjectVersionWithPointInTime[]; | ||||
|     loaded: boolean; | ||||
|   versions: StoredObjectVersionWithPointInTime[]; | ||||
|   loaded: boolean; | ||||
| } | ||||
|  | ||||
| const props = defineProps<HistoryButtonConfig>(); | ||||
| @@ -22,47 +22,47 @@ const state = reactive<HistoryButtonState>({ versions: [], loaded: false }); | ||||
| const modal = useTemplateRef<typeof HistoryButtonModal>("modal"); | ||||
|  | ||||
| const download_version_and_open_modal = async function (): Promise<void> { | ||||
|     if (null !== modal.value) { | ||||
|         modal.value.open(); | ||||
|     } else { | ||||
|         console.log("modal is null"); | ||||
|     } | ||||
|   if (null !== modal.value) { | ||||
|     modal.value.open(); | ||||
|   } else { | ||||
|     console.log("modal is null"); | ||||
|   } | ||||
|  | ||||
|     if (!state.loaded) { | ||||
|         const versions = await get_versions(props.storedObject); | ||||
|   if (!state.loaded) { | ||||
|     const versions = await get_versions(props.storedObject); | ||||
|  | ||||
|         for (const version of versions) { | ||||
|             state.versions.push(version); | ||||
|         } | ||||
|         state.loaded = true; | ||||
|     for (const version of versions) { | ||||
|       state.versions.push(version); | ||||
|     } | ||||
|     state.loaded = true; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const onRestoreVersion = ({ | ||||
|     newVersion, | ||||
|   newVersion, | ||||
| }: { | ||||
|     newVersion: StoredObjectVersionWithPointInTime; | ||||
|   newVersion: StoredObjectVersionWithPointInTime; | ||||
| }) => { | ||||
|     state.versions.unshift(newVersion); | ||||
|   state.versions.unshift(newVersion); | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <a @click="download_version_and_open_modal" class="dropdown-item"> | ||||
|         <history-button-modal | ||||
|             ref="modal" | ||||
|             :versions="state.versions" | ||||
|             :stored-object="storedObject" | ||||
|             :can-edit="canEdit" | ||||
|             @restore-version="onRestoreVersion" | ||||
|         ></history-button-modal> | ||||
|         <i class="fa fa-history"></i> | ||||
|         Historique | ||||
|     </a> | ||||
|   <a @click="download_version_and_open_modal" class="dropdown-item"> | ||||
|     <history-button-modal | ||||
|       ref="modal" | ||||
|       :versions="state.versions" | ||||
|       :stored-object="storedObject" | ||||
|       :can-edit="canEdit" | ||||
|       @restore-version="onRestoreVersion" | ||||
|     ></history-button-modal> | ||||
|     <i class="fa fa-history"></i> | ||||
|     Historique | ||||
|   </a> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| i.fa::before { | ||||
|     color: var(--bs-dropdown-link-hover-color); | ||||
|   color: var(--bs-dropdown-link-hover-color); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,26 +1,26 @@ | ||||
| <script setup lang="ts"> | ||||
| import { | ||||
|     StoredObject, | ||||
|     StoredObjectVersionWithPointInTime, | ||||
|   StoredObject, | ||||
|   StoredObjectVersionWithPointInTime, | ||||
| } from "./../../../types"; | ||||
| import HistoryButtonListItem from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonListItem.vue"; | ||||
| import { computed, reactive } from "vue"; | ||||
|  | ||||
| interface HistoryButtonListConfig { | ||||
|     versions: StoredObjectVersionWithPointInTime[]; | ||||
|     storedObject: StoredObject; | ||||
|     canEdit: boolean; | ||||
|   versions: StoredObjectVersionWithPointInTime[]; | ||||
|   storedObject: StoredObject; | ||||
|   canEdit: boolean; | ||||
| } | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
|     restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]; | ||||
|   restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]; | ||||
| }>(); | ||||
|  | ||||
| interface HistoryButtonListState { | ||||
|     /** | ||||
|      * Contains the number of the newly created version when a version is restored. | ||||
|      */ | ||||
|     restored: number; | ||||
|   /** | ||||
|    * Contains the number of the newly created version when a version is restored. | ||||
|    */ | ||||
|   restored: number; | ||||
| } | ||||
|  | ||||
| const props = defineProps<HistoryButtonListConfig>(); | ||||
| @@ -28,11 +28,11 @@ const props = defineProps<HistoryButtonListConfig>(); | ||||
| const state = reactive<HistoryButtonListState>({ restored: -1 }); | ||||
|  | ||||
| const higher_version = computed<number>(() => | ||||
|     props.versions.reduce( | ||||
|         (accumulator: number, version: StoredObjectVersionWithPointInTime) => | ||||
|             Math.max(accumulator, version.version), | ||||
|         -1, | ||||
|     ), | ||||
|   props.versions.reduce( | ||||
|     (accumulator: number, version: StoredObjectVersionWithPointInTime) => | ||||
|       Math.max(accumulator, version.version), | ||||
|     -1, | ||||
|   ), | ||||
| ); | ||||
|  | ||||
| /** | ||||
| @@ -41,32 +41,32 @@ const higher_version = computed<number>(() => | ||||
|  * internally, keep track of the newly restored version | ||||
|  */ | ||||
| const onRestored = ({ | ||||
|     newVersion, | ||||
|   newVersion, | ||||
| }: { | ||||
|     newVersion: StoredObjectVersionWithPointInTime; | ||||
|   newVersion: StoredObjectVersionWithPointInTime; | ||||
| }) => { | ||||
|     state.restored = newVersion.version; | ||||
|     emit("restoreVersion", { newVersion }); | ||||
|   state.restored = newVersion.version; | ||||
|   emit("restoreVersion", { newVersion }); | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <template v-if="props.versions.length > 0"> | ||||
|         <div class="container"> | ||||
|             <template v-for="v in props.versions" :key="v.id"> | ||||
|                 <history-button-list-item | ||||
|                     :version="v" | ||||
|                     :can-edit="canEdit" | ||||
|                     :is-current="higher_version === v.version" | ||||
|                     :stored-object="storedObject" | ||||
|                     @restore-version="onRestored" | ||||
|                 ></history-button-list-item> | ||||
|             </template> | ||||
|         </div> | ||||
|     </template> | ||||
|     <template v-else> | ||||
|         <p>Chargement des versions</p> | ||||
|     </template> | ||||
|   <template v-if="props.versions.length > 0"> | ||||
|     <div class="container"> | ||||
|       <template v-for="v in props.versions" :key="v.id"> | ||||
|         <history-button-list-item | ||||
|           :version="v" | ||||
|           :can-edit="canEdit" | ||||
|           :is-current="higher_version === v.version" | ||||
|           :stored-object="storedObject" | ||||
|           @restore-version="onRestored" | ||||
|         ></history-button-list-item> | ||||
|       </template> | ||||
|     </div> | ||||
|   </template> | ||||
|   <template v-else> | ||||
|     <p>Chargement des versions</p> | ||||
|   </template> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"></style> | ||||
|   | ||||
| @@ -1,196 +1,184 @@ | ||||
| <script setup lang="ts"> | ||||
| import { | ||||
|     StoredObject, | ||||
|     StoredObjectPointInTime, | ||||
|     StoredObjectVersionWithPointInTime, | ||||
| } from "ChillDocStoreAssets/types"; | ||||
|   StoredObject, | ||||
|   StoredObjectPointInTime, | ||||
|   StoredObjectVersionWithPointInTime, | ||||
| } from "./../../../types"; | ||||
| import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue"; | ||||
| import { ISOToDatetime } from "ChillMainAssets/chill/js/date"; | ||||
| import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date"; | ||||
| import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue"; | ||||
| import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue"; | ||||
| import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue"; | ||||
| import { computed } from "vue"; | ||||
|  | ||||
| interface HistoryButtonListItemConfig { | ||||
|     version: StoredObjectVersionWithPointInTime; | ||||
|     storedObject: StoredObject; | ||||
|     canEdit: boolean; | ||||
|     isCurrent: boolean; | ||||
|   version: StoredObjectVersionWithPointInTime; | ||||
|   storedObject: StoredObject; | ||||
|   canEdit: boolean; | ||||
|   isCurrent: boolean; | ||||
| } | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
|     restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]; | ||||
|   restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]; | ||||
| }>(); | ||||
|  | ||||
| const props = defineProps<HistoryButtonListItemConfig>(); | ||||
|  | ||||
| const onRestore = ({ | ||||
|     newVersion, | ||||
|   newVersion, | ||||
| }: { | ||||
|     newVersion: StoredObjectVersionWithPointInTime; | ||||
|   newVersion: StoredObjectVersionWithPointInTime; | ||||
| }) => { | ||||
|     emit("restoreVersion", { newVersion }); | ||||
|   emit("restoreVersion", { newVersion }); | ||||
| }; | ||||
|  | ||||
| const isKeptBeforeConversion = computed<boolean>(() => { | ||||
|     if ("point-in-times" in props.version) { | ||||
|         return props.version["point-in-times"].reduce( | ||||
|             (accumulator: boolean, pit: StoredObjectPointInTime) => | ||||
|                 accumulator || "keep-before-conversion" === pit.reason, | ||||
|             false, | ||||
|         ); | ||||
|     } else { | ||||
|         return false; | ||||
|     } | ||||
|   if ("point-in-times" in props.version) { | ||||
|     return props.version["point-in-times"].reduce( | ||||
|       (accumulator: boolean, pit: StoredObjectPointInTime) => | ||||
|         accumulator || "keep-before-conversion" === pit.reason, | ||||
|       false, | ||||
|     ); | ||||
|   } else { | ||||
|     return false; | ||||
|   } | ||||
| }); | ||||
|  | ||||
| const isRestored = computed<boolean>( | ||||
|     () => props.version.version > 0 && null !== props.version["from-restored"], | ||||
|   () => props.version.version > 0 && null !== props.version["from-restored"], | ||||
| ); | ||||
|  | ||||
| const isDuplicated = computed<boolean>( | ||||
|     () => | ||||
|         props.version.version === 0 && null !== props.version["from-restored"], | ||||
|   () => props.version.version === 0 && null !== props.version["from-restored"], | ||||
| ); | ||||
|  | ||||
| const classes = computed<{ | ||||
|     row: true; | ||||
|     "row-hover": true; | ||||
|     "blinking-1": boolean; | ||||
|     "blinking-2": boolean; | ||||
|   row: true; | ||||
|   "row-hover": true; | ||||
|   "blinking-1": boolean; | ||||
|   "blinking-2": boolean; | ||||
| }>(() => ({ | ||||
|     row: true, | ||||
|     "row-hover": true, | ||||
|     "blinking-1": props.isRestored && 0 === props.version.version % 2, | ||||
|     "blinking-2": props.isRestored && 1 === props.version.version % 2, | ||||
|   row: true, | ||||
|   "row-hover": true, | ||||
|   "blinking-1": props.isRestored && 0 === props.version.version % 2, | ||||
|   "blinking-2": props.isRestored && 1 === props.version.version % 2, | ||||
| })); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <div :class="classes"> | ||||
|         <div | ||||
|             class="col-12 tags" | ||||
|             v-if=" | ||||
|                 isCurrent || | ||||
|                 isKeptBeforeConversion || | ||||
|                 isRestored || | ||||
|                 isDuplicated | ||||
|             " | ||||
|         > | ||||
|             <span class="badge bg-success" v-if="isCurrent" | ||||
|                 >Version actuelle</span | ||||
|             > | ||||
|             <span class="badge bg-info" v-if="isKeptBeforeConversion" | ||||
|                 >Conservée avant conversion dans un autre format</span | ||||
|             > | ||||
|             <span class="badge bg-info" v-if="isRestored" | ||||
|                 >Restaurée depuis la version | ||||
|                 {{ version["from-restored"]?.version + 1 }}</span | ||||
|             > | ||||
|             <span class="badge bg-info" v-if="isDuplicated" | ||||
|                 >Dupliqué depuis un autre document</span | ||||
|             > | ||||
|         </div> | ||||
|         <div class="col-12"> | ||||
|             <file-icon :type="version.type"></file-icon> | ||||
|             <span | ||||
|                 ><strong> #{{ version.version + 1 }} </strong></span | ||||
|             > | ||||
|             <template | ||||
|                 v-if="version.createdBy !== null && version.createdAt !== null" | ||||
|                 ><strong v-if="version.version == 0">créé par</strong | ||||
|                 ><strong v-else>modifié par</strong> | ||||
|                 <span class="badge-user" | ||||
|                     ><UserRenderBoxBadge | ||||
|                         :user="version.createdBy" | ||||
|                     ></UserRenderBoxBadge | ||||
|                 ></span> | ||||
|                 <strong>à</strong> | ||||
|                 {{ | ||||
|                     $d(ISOToDatetime(version.createdAt.datetime8601), "long") | ||||
|                 }}</template | ||||
|             ><template | ||||
|                 v-if="version.createdBy === null && version.createdAt !== null" | ||||
|                 ><strong v-if="version.version == 0">Créé le</strong | ||||
|                 ><strong v-else>modifié le</strong> | ||||
|                 {{ | ||||
|                     $d(ISOToDatetime(version.createdAt.datetime8601), "long") | ||||
|                 }}</template | ||||
|             > | ||||
|         </div> | ||||
|         <div class="col-12"> | ||||
|             <ul class="record_actions small slim on-version-actions"> | ||||
|                 <li v-if="canEdit && !isCurrent"> | ||||
|                     <restore-version-button | ||||
|                         :stored-object-version="props.version" | ||||
|                         @restore-version="onRestore" | ||||
|                     ></restore-version-button> | ||||
|                 </li> | ||||
|                 <li> | ||||
|                     <download-button | ||||
|                         :stored-object="storedObject" | ||||
|                         :at-version="version" | ||||
|                         :classes="{ | ||||
|                             btn: true, | ||||
|                             'btn-outline-primary': true, | ||||
|                             'btn-sm': true, | ||||
|                         }" | ||||
|                         :display-action-string-in-button="false" | ||||
|                     ></download-button> | ||||
|                 </li> | ||||
|             </ul> | ||||
|         </div> | ||||
|   <div :class="classes"> | ||||
|     <div | ||||
|       class="col-12 tags" | ||||
|       v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated" | ||||
|     > | ||||
|       <span class="badge bg-success" v-if="isCurrent">Version actuelle</span> | ||||
|       <span class="badge bg-info" v-if="isKeptBeforeConversion" | ||||
|         >Conservée avant conversion dans un autre format</span | ||||
|       > | ||||
|       <span class="badge bg-info" v-if="isRestored" | ||||
|         >Restaurée depuis la version | ||||
|         {{ version["from-restored"]?.version + 1 }}</span | ||||
|       > | ||||
|       <span class="badge bg-info" v-if="isDuplicated" | ||||
|         >Dupliqué depuis un autre document</span | ||||
|       > | ||||
|     </div> | ||||
|     <div class="col-12"> | ||||
|       <file-icon :type="version.type"></file-icon> | ||||
|       <span | ||||
|         ><strong> #{{ version.version + 1 }} </strong></span | ||||
|       > | ||||
|       <template v-if="version.createdBy !== null && version.createdAt !== null" | ||||
|         ><strong v-if="version.version == 0">créé par</strong | ||||
|         ><strong v-else>modifié par</strong> | ||||
|         <span class="badge-user" | ||||
|           ><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge | ||||
|         ></span> | ||||
|         <strong>à</strong> | ||||
|         {{ | ||||
|           $d(ISOToDatetime(version.createdAt.datetime8601), "long") | ||||
|         }}</template | ||||
|       ><template v-if="version.createdBy === null && version.createdAt !== null" | ||||
|         ><strong v-if="version.version == 0">Créé le</strong | ||||
|         ><strong v-else>modifié le</strong> | ||||
|         {{ | ||||
|           $d(ISOToDatetime(version.createdAt.datetime8601), "long") | ||||
|         }}</template | ||||
|       > | ||||
|     </div> | ||||
|     <div class="col-12"> | ||||
|       <ul class="record_actions small slim on-version-actions"> | ||||
|         <li v-if="canEdit && !isCurrent"> | ||||
|           <restore-version-button | ||||
|             :stored-object-version="props.version" | ||||
|             @restore-version="onRestore" | ||||
|           ></restore-version-button> | ||||
|         </li> | ||||
|         <li> | ||||
|           <download-button | ||||
|             :stored-object="storedObject" | ||||
|             :at-version="version" | ||||
|             :classes="{ | ||||
|               btn: true, | ||||
|               'btn-outline-primary': true, | ||||
|               'btn-sm': true, | ||||
|             }" | ||||
|             :display-action-string-in-button="false" | ||||
|           ></download-button> | ||||
|         </li> | ||||
|       </ul> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| div.tags { | ||||
|     span.badge:not(:last-child) { | ||||
|         margin-right: 0.5rem; | ||||
|     } | ||||
|   span.badge:not(:last-child) { | ||||
|     margin-right: 0.5rem; | ||||
|   } | ||||
| } | ||||
| // to make the animation restart, we have the same animation twice, | ||||
| // and alternate between both | ||||
| .blinking-1 { | ||||
|     animation-name: backgroundColorPalette-1; | ||||
|     animation-duration: 8s; | ||||
|     animation-iteration-count: 1; | ||||
|     animation-direction: normal; | ||||
|     animation-timing-function: linear; | ||||
|   animation-name: backgroundColorPalette-1; | ||||
|   animation-duration: 8s; | ||||
|   animation-iteration-count: 1; | ||||
|   animation-direction: normal; | ||||
|   animation-timing-function: linear; | ||||
| } | ||||
| @keyframes backgroundColorPalette-1 { | ||||
|     0% { | ||||
|         background: var(--bs-chill-green-dark); | ||||
|     } | ||||
|     25% { | ||||
|         background: var(--bs-chill-green); | ||||
|     } | ||||
|     65% { | ||||
|         background: var(--bs-chill-beige); | ||||
|     } | ||||
|     100% { | ||||
|         background: unset; | ||||
|     } | ||||
|   0% { | ||||
|     background: var(--bs-chill-green-dark); | ||||
|   } | ||||
|   25% { | ||||
|     background: var(--bs-chill-green); | ||||
|   } | ||||
|   65% { | ||||
|     background: var(--bs-chill-beige); | ||||
|   } | ||||
|   100% { | ||||
|     background: unset; | ||||
|   } | ||||
| } | ||||
| .blinking-2 { | ||||
|     animation-name: backgroundColorPalette-2; | ||||
|     animation-duration: 8s; | ||||
|     animation-iteration-count: 1; | ||||
|     animation-direction: normal; | ||||
|     animation-timing-function: linear; | ||||
|   animation-name: backgroundColorPalette-2; | ||||
|   animation-duration: 8s; | ||||
|   animation-iteration-count: 1; | ||||
|   animation-direction: normal; | ||||
|   animation-timing-function: linear; | ||||
| } | ||||
| @keyframes backgroundColorPalette-2 { | ||||
|     0% { | ||||
|         background: var(--bs-chill-green-dark); | ||||
|     } | ||||
|     25% { | ||||
|         background: var(--bs-chill-green); | ||||
|     } | ||||
|     65% { | ||||
|         background: var(--bs-chill-beige); | ||||
|     } | ||||
|     100% { | ||||
|         background: unset; | ||||
|     } | ||||
|   0% { | ||||
|     background: var(--bs-chill-green-dark); | ||||
|   } | ||||
|   25% { | ||||
|     background: var(--bs-chill-green); | ||||
|   } | ||||
|   65% { | ||||
|     background: var(--bs-chill-beige); | ||||
|   } | ||||
|   100% { | ||||
|     background: unset; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -3,54 +3,54 @@ import Modal from "ChillMainAssets/vuejs/_components/Modal.vue"; | ||||
| import { reactive } from "vue"; | ||||
| import HistoryButtonList from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonList.vue"; | ||||
| import { | ||||
|     StoredObject, | ||||
|     StoredObjectVersionWithPointInTime, | ||||
|   StoredObject, | ||||
|   StoredObjectVersionWithPointInTime, | ||||
| } from "./../../../types"; | ||||
|  | ||||
| interface HistoryButtonListConfig { | ||||
|     versions: StoredObjectVersionWithPointInTime[]; | ||||
|     storedObject: StoredObject; | ||||
|     canEdit: boolean; | ||||
|   versions: StoredObjectVersionWithPointInTime[]; | ||||
|   storedObject: StoredObject; | ||||
|   canEdit: boolean; | ||||
| } | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
|     restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]; | ||||
|   restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]; | ||||
| }>(); | ||||
|  | ||||
| interface HistoryButtonModalState { | ||||
|     opened: boolean; | ||||
|   opened: boolean; | ||||
| } | ||||
|  | ||||
| const props = defineProps<HistoryButtonListConfig>(); | ||||
| const state = reactive<HistoryButtonModalState>({ opened: false }); | ||||
|  | ||||
| const open = () => { | ||||
|     state.opened = true; | ||||
|   state.opened = true; | ||||
| }; | ||||
|  | ||||
| const onRestoreVersion = (payload: { | ||||
|     newVersion: StoredObjectVersionWithPointInTime; | ||||
|   newVersion: StoredObjectVersionWithPointInTime; | ||||
| }) => emit("restoreVersion", payload); | ||||
|  | ||||
| defineExpose({ open }); | ||||
| </script> | ||||
| <template> | ||||
|     <Teleport to="body"> | ||||
|         <modal v-if="state.opened" @close="state.opened = false"> | ||||
|             <template v-slot:header> | ||||
|                 <h3>Historique des versions du document</h3> | ||||
|             </template> | ||||
|             <template v-slot:body> | ||||
|                 <p>Les versions sont conservées pendant 90 jours.</p> | ||||
|                 <history-button-list | ||||
|                     :versions="props.versions" | ||||
|                     :can-edit="canEdit" | ||||
|                     :stored-object="storedObject" | ||||
|                     @restore-version="onRestoreVersion" | ||||
|                 ></history-button-list> | ||||
|             </template> | ||||
|         </modal> | ||||
|     </Teleport> | ||||
|   <Teleport to="body"> | ||||
|     <modal v-if="state.opened" @close="state.opened = false"> | ||||
|       <template v-slot:header> | ||||
|         <h3>Historique des versions du document</h3> | ||||
|       </template> | ||||
|       <template v-slot:body> | ||||
|         <p>Les versions sont conservées pendant 90 jours.</p> | ||||
|         <history-button-list | ||||
|           :versions="props.versions" | ||||
|           :can-edit="canEdit" | ||||
|           :stored-object="storedObject" | ||||
|           @restore-version="onRestoreVersion" | ||||
|         ></history-button-list> | ||||
|       </template> | ||||
|     </modal> | ||||
|   </Teleport> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"></style> | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| <script setup lang="ts"> | ||||
| import { | ||||
|     StoredObjectVersionPersisted, | ||||
|     StoredObjectVersionWithPointInTime, | ||||
|   StoredObjectVersionPersisted, | ||||
|   StoredObjectVersionWithPointInTime, | ||||
| } from "../../../types"; | ||||
| import { useToast } from "vue-toast-notification"; | ||||
| import { restore_version } from "./api"; | ||||
|  | ||||
| interface RestoreVersionButtonProps { | ||||
|     storedObjectVersion: StoredObjectVersionPersisted; | ||||
|   storedObjectVersion: StoredObjectVersionPersisted; | ||||
| } | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
|     restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]; | ||||
|   restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]; | ||||
| }>(); | ||||
|  | ||||
| const props = defineProps<RestoreVersionButtonProps>(); | ||||
| @@ -19,21 +19,21 @@ const props = defineProps<RestoreVersionButtonProps>(); | ||||
| const $toast = useToast(); | ||||
|  | ||||
| const restore_version_fn = async () => { | ||||
|     const newVersion = await restore_version(props.storedObjectVersion); | ||||
|   const newVersion = await restore_version(props.storedObjectVersion); | ||||
|  | ||||
|     $toast.success("Version restaurée"); | ||||
|     emit("restoreVersion", { newVersion }); | ||||
|   $toast.success("Version restaurée"); | ||||
|   emit("restoreVersion", { newVersion }); | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <button | ||||
|         class="btn btn-outline-action" | ||||
|         @click="restore_version_fn" | ||||
|         title="Restaurer" | ||||
|     > | ||||
|         <i class="fa fa-rotate-left"></i> Restaurer | ||||
|     </button> | ||||
|   <button | ||||
|     class="btn btn-outline-action" | ||||
|     @click="restore_version_fn" | ||||
|     title="Restaurer" | ||||
|   > | ||||
|     <i class="fa fa-rotate-left"></i> Restaurer | ||||
|   </button> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"></style> | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user