Compare commits

..

86 Commits

Author SHA1 Message Date
0541995a60 Add icons to document action buttons and update bindings for accompanying period work IDs 2025-09-02 16:37:19 +02:00
29e054bd10 fix issues about icons to remain blank on blank
Refactor accompanying period work fetching logic to include a new filter for ignoring specific IDs, and update related components with new prop bindings.
2025-09-02 16:37:08 +02:00
da0099aafc Merge branch '369-duplicate-evaluation-document' into move-document-to-other-eval 2025-09-02 16:01:08 +02:00
3a18ea42fe Add ignore filter for accompanying period work IDs
Refactor accompanying period work fetching logic to include a new filter for ignoring specific IDs, and update related components with new prop bindings.
2025-09-02 15:52:06 +02:00
e60435b8cc Handle store state updates when moving documents
- Add null check to prevent error when evaluation is not part of this social work
2025-08-25 15:09:38 +02:00
ab6ab19499 feat: enhance document actions in UI
- Add edit and delete options in translation files
- Refactor `DocumentsList.vue` to group replace, delete, move, and duplicate actions in a dropdown menu
2025-08-25 15:02:26 +02:00
2a1762ea8d Remove code block in method setAccompanyingPeriodWorkEvaluation, no longer necessary 2025-08-25 15:02:26 +02:00
18ababbca9 Handle error when moving document between evaluations and display toast upon success 2025-08-25 15:02:26 +02:00
f6179cd3a3 WIP Add toast after successful move 2025-08-25 15:02:26 +02:00
ddf8da4cee php cs fixes 2025-08-25 15:02:26 +02:00
bf2181c2f1 allow changing evaluation for a document
- Remove restriction on changing evaluation in entity logic
2025-08-25 15:02:26 +02:00
d508fde8d2 enable moving documents between evaluations
- Add Vuex action and mutation for moving documents between evaluations
- Implement `moveDocumentToEvaluation` API method
- Update `DocumentsList.vue` and `FormEvaluation.vue` to handle move actions
2025-08-25 15:02:26 +02:00
14dba22181 wip: enable moving documents to evaluations
- Add `AccompanyingPeriodWorkEvaluationDocumentMoveController` for move functionality
- Update `DocumentsList.vue` to emit move event
- Adjust `FormEvaluation.vue` to handle move action
2025-08-25 15:02:26 +02:00
7dc7e77c62 WIP enable moving documents to evaluations
- Add new button and logic in `DocumentsList.vue` for moving documents
- Implement `moveDocumentToEvaluation` method in `FormEvaluation.vue`
- Ensure duplication and moving actions are mutually exclusive
- Add event for moving documents to evaluations
2025-08-25 15:02:26 +02:00
9d58904969 refactor: improve document duplication handling
- Remove unnecessary console logs
- Add null check before duplicating document to evaluation within another social work
2025-08-25 14:57:48 +02:00
4d90c7028f Store commit of document duplication only upon successful API call otherwise log error 2025-08-21 16:19:03 +02:00
3abb76d268 eslint 2025-08-21 09:55:52 +02:00
d62dd4396e Display toast upon successful duplication of evaluation document 2025-08-21 09:52:59 +02:00
59e8d9d516 Fix the access of results after API call 2025-08-20 16:03:56 +02:00
7dcb8abe38 Merge branch 'master' into 369-duplicate-evaluation-document 2025-08-20 15:28:19 +02:00
a0b2d92ba2 Fix the selection modal for acpw for merging functionality 2025-08-20 12:53:09 +02:00
7843e5dfd1 Add return types and remove unnecessary html snippet 2025-08-19 14:11:26 +02:00
32c847267b Remove dump 2025-08-19 10:20:38 +02:00
9b353f4d1b Filter accompanying period works in evaluation selector mode
- Add filtering to show only accompanying period works with evaluations in evaluation selector mode
2025-08-13 13:19:41 +02:00
81a858f07a eslint corrections 2025-08-13 12:38:32 +02:00
6a2ee232a9 feat: enable document duplication to another evaluation
- Introduce API method for duplicating a document to a different evaluation
- Add Vuex actions and mutations to handle duplication logic to another evaluation
2025-08-13 12:35:40 +02:00
56c43a0a76 Refactor display document duplication button and add translations
- Add new translations for document duplication and replacement options
- Adjust order of list elements in `DocumentsList.vue` for better readability
2025-08-13 09:40:36 +02:00
eb724a730c remove line ux-translator 2025-04-28 10:50:37 +02:00
18f98b6795 Changie added for fusion of accompanying period works 2025-04-03 10:09:16 +02:00
d73994edd0 Adjust display of acpw tag when modal in use for the selection of an evaluation 2025-04-03 10:05:43 +02:00
70603570c8 Changie added 2025-04-03 10:03:25 +02:00
df09dd2017 Eslint fixes 2025-04-03 10:02:17 +02:00
1c87280b1e Display a toast message when document is duplicated succesfully 2025-04-03 09:56:36 +02:00
445e093a28 Emit duplication of document to an evaluation and add backend logic 2025-04-02 19:03:58 +02:00
3f91c65b30 Display evaluations in modal after selection of accompanyingPeriodWork 2025-04-02 15:36:11 +02:00
9bc3c16b58 WIP prepare modal for display of evaluations linked to accompanying period work 2025-04-02 13:52:51 +02:00
12dff82248 Re-establish normal behavior for component within twig 2025-04-02 12:44:55 +02:00
ab23a4efb5 Refactor FormEvaluation.vue component 2025-04-02 11:55:37 +02:00
204fb20475 Change behavior of AccompanyingPeriodWorkSelectorModal.vue: open modal directly 2025-04-02 11:53:21 +02:00
f430d97152 Transform duplicate button into dropdown 2025-04-01 18:45:46 +02:00
4fa4d3b65c Phpstan and cs fixes 2025-03-27 14:32:06 +01:00
bd4c34cc1d Fix eslint issues and add ts interfaces for typing 2025-03-27 14:26:43 +01:00
4cea678e93 Fix updating of manyToMany relationships 2025-03-27 13:34:16 +01:00
5e6833975b Fix handling comments and workflows on acpw 2025-03-26 20:25:54 +01:00
f523b9adb3 Fix typing errors 2025-03-26 20:25:39 +01:00
a211549432 Adjust template and add translations 2025-03-26 15:16:27 +01:00
17b1363113 Fixes after rebase + apply item styling for accompanying course work 2025-03-26 14:08:45 +01:00
3356ed8e57 Correct for loop to display accompanying period list items 2025-03-24 16:13:56 +01:00
2a7fa517ee Only show merge button if there are more than 1 works attached to the parcours 2025-03-24 16:07:47 +01:00
85781c8e14 Use item renderbox for display of accompanyingperiodwork 2025-03-19 11:04:01 +01:00
00eb435896 Add chevron icon in merge button 2025-03-19 11:04:01 +01:00
ed71cffd6a Change behavior of information exchange between backend and frontend 2025-03-19 11:03:59 +01:00
ae679e6997 Fix merge service and passing of json to vue 2025-03-19 11:03:53 +01:00
e1d308fd97 WIP create new picker for accompanying period works 2025-03-19 11:03:42 +01:00
d9acda67e3 WIP dynamic picking of accompanying period work 2025-03-19 11:03:42 +01:00
e88da74882 WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:03:41 +01:00
591c44d1a0 Create types 2025-03-19 11:03:18 +01:00
bf04b7981c Improve merge service according to specifications 2025-03-19 11:03:02 +01:00
df33eec30f WIP merge service 2025-03-19 11:03:00 +01:00
c657c98918 Styling and organization of components 2025-03-19 11:02:55 +01:00
ef5eb5b907 Open modal to select acpw 2025-03-19 11:02:27 +01:00
d683fe002d Different approach to creating acpw selector 2025-03-19 11:02:25 +01:00
555bbca59b WIP create new picker for accompanying period works 2025-03-19 11:02:22 +01:00
e9e9d5c458 WIP dynamic picking of accompanying period work 2025-03-19 11:02:22 +01:00
b1842a33ae WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:02:19 +01:00
6afeaccf24 Improve merge service according to specifications 2025-03-19 11:01:52 +01:00
fb76bac480 WIP merge service 2025-03-19 11:01:49 +01:00
6ded185289 Treat duplicate in backend and setup confirm page of merge 2025-03-19 11:00:40 +01:00
95adc29f9d WIP create new picker for accompanying period works 2025-03-19 11:00:40 +01:00
4d0c3e683f WIP dynamic picking of accompanying period work 2025-03-19 11:00:40 +01:00
018aafc773 WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:00:40 +01:00
c4aea4efc2 Create types 2025-03-19 11:00:40 +01:00
225e3ca13f Improve merge service according to specifications 2025-03-19 11:00:40 +01:00
8c1fa7956a WIP merge service 2025-03-19 11:00:40 +01:00
e253d1b276 Styling and organization of components 2025-03-19 11:00:40 +01:00
a52aac2d98 Update package.json 2025-03-19 11:00:40 +01:00
9e8cf60dd8 Open modal to select acpw 2025-03-19 11:00:40 +01:00
7682d81d50 Different approach to creating acpw selector 2025-03-19 11:00:40 +01:00
5d31ce96c1 WIP create new picker for accompanying period works 2025-03-19 11:00:40 +01:00
81ef64a246 WIP dynamic picking of accompanying period work 2025-03-19 11:00:40 +01:00
49d1f78001 WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:00:40 +01:00
0d0f3528e2 Add (temporary) types in Main and ThirdpartyBundle 2025-03-19 11:00:40 +01:00
d97d5e689a Create types 2025-03-19 11:00:40 +01:00
95d80ce13e Improve merge service according to specifications 2025-03-19 11:00:40 +01:00
668720984d WIP merge service 2025-03-19 11:00:40 +01:00
245c3fa121 First commit - changie for feature 2025-03-19 11:00:40 +01:00
581 changed files with 25348 additions and 45630 deletions

View File

@@ -1,6 +0,0 @@
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: ""

View File

@@ -1,6 +0,0 @@
kind: Feature
body: |
Upgrade CKEditor and refactor configuration with use of typescript
time: 2024-05-31T19:02:42.776662753+02:00
custom:
Issue: ""

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Allow the merge of two accompanying period works
time: 2025-02-11T14:22:43.134106669+01:00
custom:
Issue: "359"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Duplication of a document to another accompanying period work evaluation
time: 2025-04-03T10:03:11.796736107+02:00
custom:
Issue: "369"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Fusion of two accompanying period works
time: 2025-04-03T10:08:57.25079018+02:00
custom:
Issue: "359"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Add filter to social actions list to filter out actions where current user intervenes
time: 2025-07-17T11:08:50.128269232+02:00
custom:
Issue: "400"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Show filters on list pages unfolded by default
time: 2025-07-22T15:50:39.338057044+02:00
custom:
Issue: "399"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
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

View File

@@ -0,0 +1,6 @@
kind: Fixed
body: adjust display logic for accompanying period dates, include closing date if period is closed.
time: 2025-08-06T13:46:09.241584292+02:00
custom:
Issue: "382"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Fixed
body: add min and step attributes to integer field in DateIntervalType
time: 2025-08-06T17:35:27.413787704+02:00
custom:
Issue: "384"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: UX
body: Limit display of participations in event list
time: 2025-07-22T13:26:37.500656935+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,12 +0,0 @@
## v4.1.0 - 2025-08-26
### Feature
* ([#400](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/400)) Add filter to social actions list to filter out actions where current user intervenes
* ([#399](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/399)) Show filters on list pages unfolded by default
* Expansion of event module with new fields in the creation form: thematic, internal/external animator, responsable, and budget elements. Filtering options in the event list + adapted exports
**Schema Change**: Add columns or tables
### Fixed
* ([#382](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/382)) adjust display logic for accompanying period dates, include closing date if period is closed.
* ([#384](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/384)) add min and step attributes to integer field in DateIntervalType
### UX
* Limit display of participations in event list

View File

@@ -1,10 +0,0 @@
## v4.2.0 - 2025-09-02
### Feature
* ([#64](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/64)) Add external identifier for a Person
**Schema Change**: Add columns or tables
* ([#330](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/330) Allow users to choose for which notifications they want to receive an email
### Fixed
* ([#422](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/422)) Fixed html layout of pages for recovering password
* Fix typo in 'uncheckAll' script for centers selection
* Fix incorrect parameter name in event details link

View File

@@ -1,6 +0,0 @@
## v4.2.1 - 2025-09-03
### Fixed
* Fix exports to work with DirectExportInterface
### DX
* Improve error message when a stored object cannot be written on local disk

View File

@@ -19,11 +19,11 @@ max_line_length = 80
[COMMIT_EDITMSG] [COMMIT_EDITMSG]
max_line_length = 0 max_line_length = 0
[*.{js,vue,ts}] [*.{js, vue, ts}]
indent_size = 2 indent_size = 2
indent_style = space indent_style = space
[*.rst] [.rst]
indent_size = 3 ident_size = 3
indent_style = space ident_style = space

View File

@@ -7,6 +7,14 @@
"message": "'app' is assigned a value but never used.", "message": "'app' is assigned a value but never used.",
"hash": "f8c2979921289906e3baabae31ba101ead91504f" "hash": "f8c2979921289906e3baabae31ba101ead91504f"
}, },
{
"path": "src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/index.js",
"line": 57,
"column": 23,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'event' is defined but never used.",
"hash": "cf0cf378f71403f62a6425f384ccbbdec433d1f2"
},
{ {
"path": "src/Bundle/ChillCalendarBundle/Resources/public/module/Invite/answer.js", "path": "src/Bundle/ChillCalendarBundle/Resources/public/module/Invite/answer.js",
"line": 7, "line": 7,
@@ -119,6 +127,46 @@
"message": "'payload' is defined but never used.", "message": "'payload' is defined but never used.",
"hash": "66c545917093ba30f1d6ca10ddaa676140e749bd" "hash": "66c545917093ba30f1d6ca10ddaa676140e749bd"
}, },
{
"path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue",
"line": 224,
"column": 10,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reactive' is defined but never used.",
"hash": "96ed76a9828138fb125fc36c4b55e900bbfe87c2"
},
{
"path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue",
"line": 230,
"column": 5,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'DropArg' is defined but never used.",
"hash": "bd405399a4091d65e8391404bfb0c4611816c8e0"
},
{
"path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue",
"line": 251,
"column": 9,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'t' is assigned a value but never used.",
"hash": "bc09207a496405f7a71c178e522b89aeb1f7ebd3"
},
{
"path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue",
"line": 356,
"column": 32,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'arg' is defined but never used.",
"hash": "aeae152f0669b946a1ad681dd52b0ef03393ae79"
},
{
"path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue",
"line": 434,
"column": 11,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'changedEvent' is assigned a value but never used.",
"hash": "a7a81a6bf09d00c0364e3aa8207ffad853f0547b"
},
{ {
"path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/Components/EditLocation.vue", "path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/Components/EditLocation.vue",
"line": 77, "line": 77,
@@ -351,6 +399,14 @@
"message": "'error' is defined but never used.", "message": "'error' is defined but never used.",
"hash": "e26e5e101e90d2b7ee84d6f5de8c819e52129c17" "hash": "e26e5e101e90d2b7ee84d6f5de8c819e52129c17"
}, },
{
"path": "src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts",
"line": 29,
"column": 14,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'vm' is defined but never used.",
"hash": "8e7f5e89dd72c54459cf82156389b88988f97d63"
},
{ {
"path": "src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/uploader.js", "path": "src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/uploader.js",
"line": 39, "line": 39,
@@ -559,6 +615,14 @@
"message": "'ref' is defined but never used.", "message": "'ref' is defined but never used.",
"hash": "2a27cd6d06a26e1326654c929068e3704137e24b" "hash": "2a27cd6d06a26e1326654c929068e3704137e24b"
}, },
{
"path": "src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/HistoryButton/HistoryButtonList.vue",
"line": 57,
"column": 17,
"ruleId": "vue/valid-v-for",
"message": "Custom elements in iteration require 'v-bind:key' directives.",
"hash": "cce787939524e83dd135869e13738ef332d7156c"
},
{ {
"path": "src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue", "path": "src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue",
"line": 15, "line": 15,
@@ -919,6 +983,22 @@
"message": "'_e' is defined but never used.", "message": "'_e' is defined but never used.",
"hash": "1d6448401778e8c56554020fe5abd47851ed33f3" "hash": "1d6448401778e8c56554020fe5abd47851ed33f3"
}, },
{
"path": "src/Bundle/ChillMainBundle/Resources/public/module/wopi-link/index.js",
"line": 21,
"column": 55,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'e' is defined but never used.",
"hash": "eae499e4f6e9f43a9d17f9cd917cb6d3d97be25c"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/page/export/download-export.js",
"line": 3,
"column": 55,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'e' is defined but never used.",
"hash": "088fd383e7807e484aefc9825209bc7c8942bd22"
},
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/page/homepage_widget/index.js", "path": "src/Bundle/ChillMainBundle/Resources/public/page/homepage_widget/index.js",
"line": 9, "line": 9,
@@ -1009,19 +1089,115 @@
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 516, "line": 247,
"column": 21, "column": 5,
"ruleId": "vue/no-mutating-props", "ruleId": "@typescript-eslint/no-unused-vars",
"message": "Unexpected mutation of \"context\" prop.", "message": "'postAddressToPerson' is defined but never used.",
"hash": "984c4203f2ac1e1bb65f9ce76ecd03b763cfaa83" "hash": "8a41c437cf2b5554cbbe1704cd51f3102b3d5994"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 517, "line": 248,
"column": 5,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'postAddressToHousehold' is defined but never used.",
"hash": "66dec84b2ece299daf21308e5e60d497ba442b27"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 490,
"column": 21, "column": 21,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"context\" prop.", "message": "Unexpected mutation of \"context\" prop.",
"hash": "c9fb019bc21bfa77d989ed596913b99dd653c594" "hash": "0d3f40c47974a4371072b3b9ee04b197c830162d"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 491,
"column": 21,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"context\" prop.",
"hash": "8e877b7e588c30e182f7b572bdb9685360f9cf99"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 508,
"column": 47,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "5a3e3401bc3c765d91faaf4cfde57697af1262b7"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 525,
"column": 47,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "35a741d90379574b9323279f5802193d0c98a9dc"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 553,
"column": 47,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "c23d1ddf6c0d10ae97948e74aee9c14b9320b86c"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 572,
"column": 47,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "4322e81c6ea9d9734c680633a724d5bd4fabacb2"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 803,
"column": 47,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "7928a6461b9d394c7d97f048933553936f7d8963"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 852,
"column": 47,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "e5afdb8efccb5470a08dde48f755b1268fa947b5"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 93,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "68f5e1cf5c03f9ada59c9e0afca0b74c7f3fca4b"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 101,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "50d730f6109092baff2db66adc44dc1315e2bda2"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 109,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "573e4c041ce663f28b933d7a675c2a525aba644c"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 117,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "293f845eeab515b1df4649d136c2d8219ed59c4d"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
@@ -1048,180 +1224,204 @@
"hash": "2d5a5e680ff207ad97c7e7b7d999064b561dfd8a" "hash": "2d5a5e680ff207ad97c7e7b7d999064b561dfd8a"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 149, "line": 106,
"column": 17, "column": 17,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "e4c1ecd7ae77d46ac3625c5bbe92a24d6a964db9" "hash": "d52356f2af31d0167c02330ec22d09fbfa6b2b9f"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 157,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "4dece2db87c6ce1c04ae06c088ddfe916c1c0c61"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 165,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "facc7a0f17bdf19396fae3d0de3da82e60503c0d"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 173,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "19de32c76518387218264d7c4dab914d143a9cca"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 130, "line": 114,
"column": 17, "column": 17,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "239ac02a02694d5b20ab30d4c7ce5838c51d1515" "hash": "c8e8e06f370f93bf05867e93b5f037dfa46937b1"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 138, "line": 128,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "a54f9bc6d1edfa4df93c7dd7d409cfef3fccf99e"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 152,
"column": 13, "column": 13,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "74a5f664d18f3916ea908897fcd0291cb0128f29" "hash": "9abaf71ca4b4f292b3b01e724d0a7733365e71f1"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 129,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "0b0743959778a9e3d93089b132608816ee4e6646"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 132,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "9759da7b7859b8ee8efaf74876430658ac6b6fe2"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 133,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "dba8be9a27ab74ec743b7d9e07c05d857b407dd3"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 134,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "9b1f5bce779aafc46b19d7a5d266eaa29f8f9be9"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 139,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "fe6fc4aea0994ba9da15b7c09d308842b67958cb"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 153, "line": 153,
"column": 13, "column": 55,
"ruleId": "vue/no-mutating-props", "ruleId": "@typescript-eslint/no-unused-vars",
"message": "Unexpected mutation of \"entity\" prop.", "message": "'reject' is defined but never used.",
"hash": "740ea5d793c7a34c9f352d8b333f3aa04cc80ee8" "hash": "bd0e024fcad2e3f4566f15293e3c25c840f6dd3e"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 156, "line": 154,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "af8aca18f0226a5988ed90d44d95e2d607bfb5e6"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 157,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "7bc2453017793ae20cd6c10005f941d384b59d84"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 158,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "571b4ee5f22358dd165ec59696bb3439b7c9ff6c"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 163,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "cfcb5946c86e289fc61623a794284a5a272d02e8"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 178,
"column": 37, "column": 37,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "0ec402e43cb08bf129e0737c0d2c4f6d0c7af8bd" "hash": "596c4b180b926b7829f987384328bf5636cd367a"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 196, "line": 171,
"column": 59,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "5b41d5f9b45da074fb7bbbbd45e0da501da72071"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 172,
"column": 41, "column": 41,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "ec178d33e067aac892e015002afb6f3a2ff98762" "hash": "d92b92a25043244cca809bd129633b7e024e26b4"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 214, "line": 190,
"column": 17, "column": 17,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "c0f4e5454e672b6064eb9cf6c235c6810f7bfa80" "hash": "dd9a85ea740742d620e864796f67c5bff834486d"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 215, "line": 191,
"column": 17, "column": 17,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "e3dd840d2474f9865a45822872bf9ecfb15961d7" "hash": "e3e59960d0d50709a57b336f66b586710b774892"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 216, "line": 192,
"column": 17, "column": 17,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "a32a60382b145cc7a4a7ebe01ec435b8e3103320" "hash": "fe11b0e54396511e7b3b08615a78d22fc27e2fad"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 246, "line": 222,
"column": 13, "column": 13,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "082447e5c731012f3acc282943502775dfd24797" "hash": "63c14c2150c33ec701bc4a0ff94efde69537d490"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 118, "line": 96,
"column": 20, "column": 20,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "d4fba4fe09af3c0937c0dd164928c8930c1591b5" "hash": "d2a9fdaeef0e2810f480022d4c6f99e4f76a818e"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 118, "line": 96,
"column": 20, "column": 20,
"ruleId": "vue/no-side-effects-in-computed-properties", "ruleId": "vue/no-side-effects-in-computed-properties",
"message": "Unexpected side effect in \"cities\" computed property.", "message": "Unexpected side effect in \"cities\" computed property.",
"hash": "1113a114d5aaf9f32f442916d25458541c5af35c" "hash": "dd92a60a9b1ebefeb9a90941d45326fbfa483733"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 102,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "04be01ab638ce01f568fb0216929e65e1175ca23"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 110,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "8619c8e0b63e87d09268832f90e4fba06b87e41f"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 124, "line": 124,
"column": 17, "column": 13,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "fa56a7c93583f0a9d0c2ecac10228c4f4fc1bc3a" "hash": "281f918da00635079501418b1e6b2c05b62eb4a7"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 132, "line": 125,
"column": 17, "column": 13,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "9fe87937ea67d1dae95fb3d44d4be0da2eba0905" "hash": "c131b09fa67ab1d069f1d04a54582d6b0f206153"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 126,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "3d3a2a4add64c291b8f5f1cddd90a173cd6a819d"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 131,
"column": 21,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "ed48f4988914d7897018a2e06830a97e6740b3e8"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 145,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "744f3a7610d4d6015e50e25149bceffd6c6e2763"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
@@ -1241,139 +1441,115 @@
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 148, "line": 149,
"column": 13, "column": 17,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "ab4f478fbfbc954b8dff75176dcd432f9ff28cfc" "hash": "1e7b1ad55866f708baaca72dfa4ff26d6f8e5d21"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 153, "line": 152,
"column": 21,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "1d907d149f9ddb62e32140a90efe9a74b3e71fef"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 167,
"column": 13, "column": 13,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "8aa37d2d4f011773e68838a2c88017875de563b5" "hash": "84779331536ffceec8d4a8c5ca4307310b882549"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 168, "line": 161,
"column": 13, "column": 13,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "a4827a357e52a51fa9262319114d81a130296acf" "hash": "0789999841be671a4d8ab080d6fdb679f843eb52"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 169, "line": 170,
"column": 13, "column": 51,
"ruleId": "vue/no-mutating-props", "ruleId": "@typescript-eslint/no-unused-vars",
"message": "Unexpected mutation of \"entity\" prop.", "message": "'reject' is defined but never used.",
"hash": "a4c9715664202949e3242b8d4aa4098288b46dc4" "hash": "bbb17afa114f016e2058d90aa32d2a625804f0d1"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 171, "line": 171,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "f3e9e21e433e90ec7b615b8940d43c4177372b66"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 174,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "770b7a24cc24b380e88db47d62422c8e1ece2571"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 183,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "2aef3c519a9ec6abcfe7573989d3de19d5c4c752"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 193,
"column": 33, "column": 33,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "5d1f97e4d7d9f47399d312e8b9f95ef9e3843b8c" "hash": "5fbe407ceceb37bff2ac800ceddd7942540132f1"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 190,
"column": 55,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "e2af91def877befbabef8e93deba4c58a3ee2ded"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 191,
"column": 37,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "ee8544ee45681a650ed7d4918ae979685cdd8f0f"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 210,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "5d9d2217c8c7e6571bc9f72a98ea5b370edb4968"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 211,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "6e04619b373c23c91f6c36c2aad314ac16cdb697"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 212,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "39df045639a62f64ccdb03a80e286bc3ad772587"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 213, "line": 213,
"column": 37,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "c1df874f790ef0c036bf58ae8a8db1ee173685d4"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 232,
"column": 17, "column": 17,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "476e6588a28ac9382e8b9d2e63a8babecd23bad8" "hash": "c399a43fa797a8ce61c9d96a644a39cc84a387b7"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 233, "line": 245,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "6a0c82ba72d6d87217bf33a6ad8e40a4b81bc802"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 234,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "741d5af6c7d90041c0dc1c1df2e8699b80fca69a"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 235,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "c3ffd141f58d532663875cc5c7d338ed00db2a6d"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 267,
"column": 13, "column": 13,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "2700f258396516a2fe971618fafbcdf72cdda3ab" "hash": "04337a07944caaa4819cfebcf29e1a7cbfdf248b"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CountrySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CountrySelection.vue",
"line": 94, "line": 76,
"column": 13, "column": 13,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "4be1b0592efa775092a91a1d744e16ce98bd216e" "hash": "373a2e31f110d138c66d77f1faf5dc61545c55af"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CountrySelection.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CountrySelection.vue",
"line": 99, "line": 81,
"column": 13, "column": 13,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "19b54b6d76c30249d520a296f826eda9d6eb0668" "hash": "421eb6a63224b4b1d81b216677a710c5c99ddee3"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/DatePane.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/DatePane.vue",
@@ -1393,19 +1569,19 @@
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue",
"line": 169, "line": 155,
"column": 17, "column": 17,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "dcb7b34098062760ddbb849655a5bb3ca65c36d3" "hash": "b3a822914fcb5e2fcf28efc331a45b9205002eeb"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue",
"line": 178, "line": 164,
"column": 17, "column": 17,
"ruleId": "vue/no-mutating-props", "ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.", "message": "Unexpected mutation of \"entity\" prop.",
"hash": "86b3ecf201025cac36878c5e4bf8850fb9d58cb5" "hash": "72c7d850f6cdeaf65b373a33234222f9766ee30b"
}, },
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/index.js", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/index.js",
@@ -1455,6 +1631,14 @@
"message": "'app' is assigned a value but never used.", "message": "'app' is assigned a value but never used.",
"hash": "9e6125f4fc387dc362c69cc6e3ce360eb2851f1b" "hash": "9e6125f4fc387dc362c69cc6e3ce360eb2851f1b"
}, },
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue",
"line": 60,
"column": 22,
"ruleId": "vue/require-valid-default-prop",
"message": "Type of the default value for 'suggested' prop must be a function.",
"hash": "d30212820bc2e97fa02d75dbc3a014558693f169"
},
{ {
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue", "path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue",
"line": 24, "line": 24,
@@ -1543,6 +1727,14 @@
"message": "'tags' is assigned a value but never used.", "message": "'tags' is assigned a value but never used.",
"hash": "ae9bb2e0651c118ed9efd227e88b86cc83f5d80d" "hash": "ae9bb2e0651c118ed9efd227e88b86cc83f5d80d"
}, },
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/StickyNav.vue",
"line": 116,
"column": 18,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'event' is defined but never used.",
"hash": "201f182769c6dfb87148b841e7d9b592be429669"
},
{ {
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/index.js", "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/index.js",
"line": 19, "line": 19,
@@ -1575,6 +1767,14 @@
"message": "'app' is assigned a value but never used.", "message": "'app' is assigned a value but never used.",
"hash": "aaaaa63e7a60443b8cbf8191feb9142852ebdf1c" "hash": "aaaaa63e7a60443b8cbf8191feb9142852ebdf1c"
}, },
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue",
"line": 79,
"column": 13,
"ruleId": "vue/require-v-for-key",
"message": "Elements in iteration expect to have 'v-bind:key' directives.",
"hash": "422f53925922e59655d0f71624c19af75d41628c"
},
{ {
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/index.js", "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/index.js",
"line": 12, "line": 12,
@@ -1615,6 +1815,22 @@
"message": "'evalFQDN' is assigned a value but never used.", "message": "'evalFQDN' is assigned a value but never used.",
"hash": "7fc32caafa23addddf44f3acbc5045b4523a0271" "hash": "7fc32caafa23addddf44f3acbc5045b4523a0271"
}, },
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/store.js",
"line": 611,
"column": 9,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'errors' is assigned a value but never used.",
"hash": "c41cf979fc1626c38328dbf1028800c3395496bd"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/ExportFormActionGoalResult/App.vue",
"line": 282,
"column": 7,
"ruleId": "@typescript-eslint/no-unused-expressions",
"message": "Expected an assignment or function call and instead saw an expression.",
"hash": "de3a6e2bb10a80a2bacba665be74266c7efc7d64"
},
{ {
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/ExportFormActionGoalResult/index.js", "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/ExportFormActionGoalResult/index.js",
"line": 16, "line": 16,
@@ -1631,6 +1847,38 @@
"message": "'app' is assigned a value but never used.", "message": "'app' is assigned a value but never used.",
"hash": "2f161e663689e3e4dfe2c53b0d64c91a4d2b1a60" "hash": "2f161e663689e3e4dfe2c53b0d64c91a4d2b1a60"
}, },
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue",
"line": 263,
"column": 19,
"ruleId": "vue/return-in-computed-property",
"message": "Expected to return a value in \"refreshNetwork\" computed property.",
"hash": "2c1b08a49098c83b09058cedc0a962126e91e544"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue",
"line": 270,
"column": 7,
"ruleId": "vue/no-side-effects-in-computed-properties",
"message": "Unexpected side effect in \"legendLayers\" computed property.",
"hash": "760948d2187c853f17ac9a1bd7107e883092d4f4"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue",
"line": 281,
"column": 5,
"ruleId": "vue/no-dupe-keys",
"message": "Duplicate key 'checkedLayers'. May cause name collision in script or template tag.",
"hash": "447edb461e15e3ff5c60c8ecba88131e442539aa"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue",
"line": 353,
"column": 7,
"ruleId": "@typescript-eslint/no-unused-expressions",
"message": "Expected an assignment or function call and instead saw an expression.",
"hash": "9cf656cbf1eb3d7cc0082e63adcd320b6093d14f"
},
{ {
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js", "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js",
"line": 20, "line": 20,
@@ -1639,6 +1887,22 @@
"message": "'app' is assigned a value but never used.", "message": "'app' is assigned a value but never used.",
"hash": "9e94e6412b8a44e47bfe8e66218cad09cff5bed4" "hash": "9e94e6412b8a44e47bfe8e66218cad09cff5bed4"
}, },
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AccompanyingPeriod/SetReferrer.vue",
"line": 42,
"column": 16,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'response' is defined but never used.",
"hash": "62de07b13c662e32332bb062038acee23978ea70"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue",
"line": 356,
"column": 28,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'_response' is defined but never used.",
"hash": "097e7788a2b5dea500b80b8a3cf968e57063a66a"
},
{ {
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeUserGroup.vue", "path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeUserGroup.vue",
"line": 6, "line": 6,
@@ -1654,5 +1918,45 @@
"ruleId": "@typescript-eslint/no-unused-vars", "ruleId": "@typescript-eslint/no-unused-vars",
"message": "'UserRenderBoxBadge' is defined but never used.", "message": "'UserRenderBoxBadge' is defined but never used.",
"hash": "99eba0d8633b2c9497417f4f61ec4194dbb2a96b" "hash": "99eba0d8633b2c9497417f4f61ec4194dbb2a96b"
},
{
"path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts",
"line": 4,
"column": 3,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'StoredObjectStatus' is defined but never used.",
"hash": "63f8c4572293916850d6165647774b27d4b732c6"
},
{
"path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts",
"line": 5,
"column": 3,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'StoredObjectStatusChange' is defined but never used.",
"hash": "a87c178e3eb5999bf0f46b3fa1c6da77e1be08b9"
},
{
"path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts",
"line": 30,
"column": 61,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'e' is defined but never used.",
"hash": "02953121583f4f73742a19adab099ab63df9076e"
},
{
"path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts",
"line": 31,
"column": 32,
"ruleId": "@typescript-eslint/no-explicit-any",
"message": "Unexpected any. Specify a different type.",
"hash": "af48e21a1651b6017ede882dab249c00a818a44d"
},
{
"path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts",
"line": 37,
"column": 16,
"ruleId": "@typescript-eslint/no-explicit-any",
"message": "Unexpected any. Specify a different type.",
"hash": "7513ea552a0a649ce4ab93b6cf9d40bfef4f68d9"
} }
] ]

3
.gitignore vendored
View File

@@ -18,9 +18,6 @@ migrations/*
templates/* templates/*
translations/* translations/*
# we allow developers to add customization on their installation, without commiting it
config/packages/dev/*
###> symfony/framework-bundle ### ###> symfony/framework-bundle ###
/.env.local /.env.local
/.env.local.php /.env.local.php

View File

@@ -27,11 +27,11 @@ Chill is a comprehensive web application built as a set of Symfony bundles. It i
## Project Structure ## Project Structure
Note: This is a project that's existed for a long time, and throughout the years we've used multiple structures inside each bundle. When having the choice, the developers should choose the new structure. Note: This is a project which exists from a long time ago, and we found multiple structure inside each bundle. When having the choice, the developers should choose the new structure.
The project follows a standard Symfony bundle structure: The project follows a standard Symfony bundle structure:
- `/src/Bundle/`: Contains all the Chill bundles. The code is either at the root of the bundle directory, or within a `src/` directory (preferred). See psr4 mapping at the root's `composer.json`. - `/src/Bundle/`: Contains all the Chill bundles. The code is either at the root of the bundle directory, or within a `src/` directory (preferred). See psr4 mapping at the root's `composer.json`.
- each bundle comes with its own tests, either in the `Tests` directory (when the code is directly within the bundle directory (for instance `src/Bundle/ChillMainBundle/Tests`, `src/Bundle/ChillPersonBundle/Tests`)), or inside the `tests` directory, alongside the `src/` sub-directory (example: `src/Bundle/ChillWopiBundle/tests`) (this is the preferred way). - each bundle come with his own tests, either in the `Tests` directory (when the code is directly within the bundle directory (for instance `src/Bundle/ChillMainBundle/Tests`, `src/Bundle/ChillPersonBundle/Tests`)), or inside the `tests` directory, alongside to the `src/` sub-directory (example: `src/Bundle/ChillWopiBundle/tests`) (this is the preferred way).
- `/docs/`: Contains project documentation - `/docs/`: Contains project documentation
Each bundle typically has the following structure: Each bundle typically has the following structure:
@@ -46,13 +46,13 @@ Each bundle typically has the following structure:
### A special word about TicketBundle ### A special word about TicketBundle
The ticket bundle is developed using a kind of "Command" pattern. The controller fills a "Command," and a "CommandHandler" handles this command. They are saved in the `src/Bundle/ChillTicketBundle/src/Action` directory. The ticket bundle is developed using a kind of "Command" pattern. The controller fill a "Command", and a "CommandHandler" handle this command. They are savec in the `src/Bundle/ChillTicketBundle/src/Action` directory.
## Development Guidelines ## Development Guidelines
### Building and Configuration Instructions ### Building and Configuration Instructions
All the commands should be run through the `symfony` command, which will configure the required variables. All the command should be run through the `symfony` command, which will configure the required variables.
For assets, we must ensure that we use node at version `^20.0.0`. This is done using `nvm use 20`. For assets, we must ensure that we use node at version `^20.0.0`. This is done using `nvm use 20`.
@@ -87,7 +87,7 @@ For assets, we must ensure that we use node at version `^20.0.0`. This is done u
docker compose up -d docker compose up -d
``` ```
6. **Set Up the Database**: 5. **Set Up the Database**:
```bash ```bash
# Create the database # Create the database
symfony console doctrine:database:create symfony console doctrine:database:create
@@ -99,20 +99,20 @@ For assets, we must ensure that we use node at version `^20.0.0`. This is done u
symfony console doctrine:fixtures:load symfony console doctrine:fixtures:load
``` ```
7. **Build Assets**: 6. **Build Assets**:
```bash ```bash
nvm use 20 nvm use 20
yarn run encore dev yarn run encore dev
``` ```
8. **Start the Development Server**: 7. **Start the Development Server**:
```bash ```bash
symfony server:start -d symfony server:start -d
``` ```
#### Docker Setup #### Docker Setup
The project includes a Docker configuration for easier development: The project includes Docker configuration for easier development:
1. **Start Docker Services**: 1. **Start Docker Services**:
```bash ```bash
@@ -153,9 +153,9 @@ Key configuration files:
Each time a doctrine entity is created, we generate migration to adapt the database. Each time a doctrine entity is created, we generate migration to adapt the database.
The migration is created using the command `symfony console doctrine:migrations:diff --no-interaction --namespace <namespace>`, where the namespace is the relevant namespace for migration. As this is a bash script, remember to quote the `\` (`\` must become `\\` in your command). The migration are created using the command `symfony console doctrine:migrations:diff --no-interaction --namespace <namespace>`, where the namespace is the relevant namespace for migration. As this is a bash script, do not forget to quote the `\` (`\` must become `\\` in your command).
Each bundle has his own namespace for migration (always ask me to confirm that command with a list of updated / created entities so that I can confirm to you that it is ok): Each bundle has his own namespace for migration (always ask me to confirm that command, with a list of updated / created entities so that I can confirm you that it is ok):
- `Chill\Bundle\ActivityBundle` writes migrations to `Chill\Migrations\Activity`; - `Chill\Bundle\ActivityBundle` writes migrations to `Chill\Migrations\Activity`;
- `Chill\Bundle\BudgetBundle` writes migrations to `Chill\Migrations\Budget`; - `Chill\Bundle\BudgetBundle` writes migrations to `Chill\Migrations\Budget`;
@@ -183,7 +183,7 @@ Once created the, comment's classes should be removed and a description of the c
When we need to use a DateTime or DateTimeImmutable that need to express "now", we prefer the usage of When we need to use a DateTime or DateTimeImmutable that need to express "now", we prefer the usage of
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities, `Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
where injection does not work when restoring an entity from a database, but usually possible in services. where injection does not work when restoring an entity from database, but usually possible in services.
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface` In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
where we have full and easy control of the date. where we have full and easy control of the date.
@@ -198,9 +198,9 @@ The project uses PHPUnit for testing. Each bundle has its own test suite, and th
For creating mock, we prefer using prophecy (library phpspec/prophecy). For creating mock, we prefer using prophecy (library phpspec/prophecy).
##### Useful helpers and tips that avoid creating a mock ##### Useful helpers and tips that avoid create a mock
Some notable implementations that are test helpers and avoid creating a mock: Some notable implementations that are tests helper, and avoid to create a mock:
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`; - `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above); - `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
@@ -234,9 +234,15 @@ This must be a decision made by a human, not by an AI. Every AI task must abort
#### Running Tests #### Running Tests
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). The tests are run from the project's root (not from the bundle's root).
```bash ```bash
# Run all tests
vendor/bin/phpunit
# Run tests for a specific bundle
vendor/bin/phpunit --testsuite NameBundle
# Run a specific test file # Run a specific test file
vendor/bin/phpunit path/to/TestFile.php vendor/bin/phpunit path/to/TestFile.php
@@ -291,7 +297,7 @@ class TicketTest extends TestCase
#### Test Database #### Test Database
For tests that require a database, the project uses a postgresql database filled with fixtures (usage of doctrine-fixtures). You can configure a different database for testing in the `.env.test` file. For tests that require a database, the project uses postgresql database filled by fixtures (usage of doctrine-fixtures). You can configure a different database for testing in the `.env.test` file.
### Code Quality Tools ### Code Quality Tools

View File

@@ -1,4 +0,0 @@
{
"tabWidth": 2,
"useTabs": false
}

30
.vscode/launch.json vendored
View File

@@ -1,30 +0,0 @@
{
// 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
View File

@@ -1,23 +0,0 @@
{
"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"
}
]
}

View File

@@ -6,37 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.2.1 - 2025-09-03
### Fixed
* Fix exports to work with DirectExportInterface
### DX
* Improve error message when a stored object cannot be written on local disk
## v4.2.0 - 2025-09-02
### Feature
* ([#64](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/64)) Add external identifier for a Person
**Schema Change**: Add columns or tables
* ([#330](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/330) Allow users to choose for which notifications they want to receive an email
### Fixed
* ([#422](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/422)) Fixed html layout of pages for recovering password
* Fix typo in 'uncheckAll' script for centers selection
* Fix incorrect parameter name in event details link
## v4.1.0 - 2025-08-26
### Feature
* ([#400](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/400)) Add filter to social actions list to filter out actions where current user intervenes
* ([#399](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/399)) Show filters on list pages unfolded by default
* Expansion of event module with new fields in the creation form: thematic, internal/external animator, responsable, and budget elements. Filtering options in the event list + adapted exports
**Schema Change**: Add columns or tables
### Fixed
* ([#382](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/382)) adjust display logic for accompanying period dates, include closing date if period is closed.
* ([#384](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/384)) add min and step attributes to integer field in DateIntervalType
### UX
* Limit display of participations in event list
## v4.0.2 - 2025-07-09 ## v4.0.2 - 2025-07-09
### Fixed ### Fixed
* Fix add missing translation * Fix add missing translation

View File

@@ -54,7 +54,7 @@ Arborescence:
- person - person
- personvendee - personvendee
- household_edit_metadata - household_edit_metadata
- index.ts - index.js
``` ```
## Organisation des feuilles de styles ## Organisation des feuilles de styles

View File

@@ -133,7 +133,6 @@
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle", "Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle", "Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle",
"Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src", "Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src",
"Chill\\TicketBundle\\": "src/Bundle/ChillTicketBundle/src",
"Chill\\Utils\\Rector\\": "utils/rector/src" "Chill\\Utils\\Rector\\": "utils/rector/src"
} }
}, },

View File

@@ -35,7 +35,6 @@ return [
Chill\ThirdPartyBundle\ChillThirdPartyBundle::class => ['all' => true], Chill\ThirdPartyBundle\ChillThirdPartyBundle::class => ['all' => true],
Chill\BudgetBundle\ChillBudgetBundle::class => ['all' => true], Chill\BudgetBundle\ChillBudgetBundle::class => ['all' => true],
Chill\WopiBundle\ChillWopiBundle::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\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true], Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
]; ];

View File

@@ -1,5 +1,5 @@
chill_doc_store: chill_doc_store:
use_driver: local_storage use_driver: openstack
local_storage: local_storage:
storage_path: '%kernel.project_dir%/var/storage' storage_path: '%kernel.project_dir%/var/storage'
openstack: openstack:

View File

@@ -1,5 +0,0 @@
chill_ticket:
ticket:
person_per_ticket: one # One of "one"; "many"
response_time_exceeded_delay: PT12H

View File

@@ -14,7 +14,6 @@ doctrine_migrations:
'Chill\Migrations\Calendar': '@ChillCalendarBundle/migrations' 'Chill\Migrations\Calendar': '@ChillCalendarBundle/migrations'
'Chill\Migrations\Budget': '@ChillBudgetBundle/migrations' 'Chill\Migrations\Budget': '@ChillBudgetBundle/migrations'
'Chill\Migrations\Report': '@ChillReportBundle/migrations' 'Chill\Migrations\Report': '@ChillReportBundle/migrations'
'Chill\Migrations\Ticket': '@ChillTicketBundle/migrations'
all_or_nothing: all_or_nothing:
true true

View File

@@ -1,2 +0,0 @@
chill_ticket_bundle:
resource: '@ChillTicketBundle/config/routes.yaml'

View File

@@ -11,94 +11,24 @@
Create a new bundle 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:: .. warning::
This part of the doc is not yet tested This part of the doc is not yet tested
Create a new directory with Bundle class TODO
----------------------------------------
.. 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",
}
}
}
Register the bundle .. rubric:: Footnotes
-------------------
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.

View File

@@ -79,12 +79,12 @@
"dev": "encore dev", "dev": "encore dev",
"watch": "encore dev --watch", "watch": "encore dev --watch",
"build": "encore production --progress", "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 src/Bundle/ChillTicketBundle/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> templates/api/specs.yaml",
"specs-validate": "swagger-cli validate templates/api/specs.yaml", "specs-validate": "swagger-cli validate templates/api/specs.yaml",
"specs-create-dir": "mkdir -p templates/api", "specs-create-dir": "mkdir -p templates/api",
"specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate", "specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate",
"version": "node --version", "version": "node --version",
"eslint": "eslint-baseline --fix \"src/**/*.{js,ts,vue}\"" "eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
}, },
"private": true "private": true
} }

View File

@@ -58,10 +58,6 @@
<!-- temporarily removed, the time to find a fix --> <!-- temporarily removed, the time to find a fix -->
<exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude> <exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude>
</testsuite> </testsuite>
<testsuite name="TicketBundle">
<directory suffix="Test.php">src/Bundle/ChillTicketBundle/tests/</directory>
</testsuite>
<!-- <!--
<testsuite name="ReportBundle"> <testsuite name="ReportBundle">
<directory suffix="Test.php">src/Bundle/ChillReportBundle/Tests/</directory> <directory suffix="Test.php">src/Bundle/ChillReportBundle/Tests/</directory>

View File

@@ -1,7 +1,7 @@
<template> <template>
<concerned-groups v-if="hasPerson" /> <concerned-groups v-if="hasPerson" />
<social-issues-acc v-if="hasSocialIssues" /> <social-issues-acc v-if="hasSocialIssues" />
<location v-if="hasLocation" /> <location v-if="hasLocation" />
</template> </template>
<script> <script>
@@ -10,12 +10,12 @@ import SocialIssuesAcc from "./components/SocialIssuesAcc.vue";
import Location from "./components/Location.vue"; import Location from "./components/Location.vue";
export default { export default {
name: "App", name: "App",
props: ["hasSocialIssues", "hasLocation", "hasPerson"], props: ["hasSocialIssues", "hasLocation", "hasPerson"],
components: { components: {
ConcernedGroups, ConcernedGroups,
SocialIssuesAcc, SocialIssuesAcc,
Location, Location,
}, },
}; };
</script> </script>

View File

@@ -1,43 +1,46 @@
<template> <template>
<teleport to="#add-persons" v-if="isComponentVisible"> <teleport to="#add-persons" v-if="isComponentVisible">
<div class="flex-bloc concerned-groups" :class="getContext"> <div class="flex-bloc concerned-groups" :class="getContext">
<persons-bloc <persons-bloc
v-for="bloc in contextPersonsBlocs" v-for="bloc in contextPersonsBlocs"
:key="bloc.key" :key="bloc.key"
:bloc="bloc" :bloc="bloc"
:bloc-width="getBlocWidth" :bloc-width="getBlocWidth"
:set-persons-in-bloc="setPersonsInBloc" :set-persons-in-bloc="setPersonsInBloc"
/> />
</div> </div>
<div <div
v-if="getContext === 'accompanyingCourse' && suggestedEntities.length > 0" v-if="
> getContext === 'accompanyingCourse' &&
<ul class="list-suggest add-items inline"> suggestedEntities.length > 0
<li "
v-for="(p, i) in suggestedEntities"
@click="addSuggestedEntity(p)"
:key="`suggestedEntities-${i}`"
> >
<person-text v-if="p.type === 'person'" :person="p" /> <ul class="list-suggest add-items inline">
<span v-else>{{ p.text }}</span> <li
</li> v-for="(p, i) in suggestedEntities"
</ul> @click="addSuggestedEntity(p)"
</div> :key="`suggestedEntities-${i}`"
>
<person-text v-if="p.type === 'person'" :person="p" />
<span v-else>{{ p.text }}</span>
</li>
</ul>
</div>
<ul class="record_actions"> <ul class="record_actions">
<li class="add-persons"> <li class="add-persons">
<add-persons <add-persons
:buttonTitle="trans(ACTIVITY_ADD_PERSONS)" :buttonTitle="trans(ACTIVITY_ADD_PERSONS)"
:modalTitle="trans(ACTIVITY_ADD_PERSONS)" :modalTitle="trans(ACTIVITY_ADD_PERSONS)"
v-bind:key="addPersons.key" v-bind:key="addPersons.key"
v-bind:options="addPersonsOptions" v-bind:options="addPersonsOptions"
@addNewPersons="addNewPersons" @addNewPersons="addNewPersons"
ref="addPersons" ref="addPersons"
> >
</add-persons> </add-persons>
</li> </li>
</ul> </ul>
</teleport> </teleport>
</template> </template>
<script> <script>
@@ -46,208 +49,208 @@ import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
import PersonsBloc from "./ConcernedGroups/PersonsBloc.vue"; import PersonsBloc from "./ConcernedGroups/PersonsBloc.vue";
import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue"; import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
import { import {
ACTIVITY_BLOC_PERSONS, ACTIVITY_BLOC_PERSONS,
ACTIVITY_BLOC_PERSONS_ASSOCIATED, ACTIVITY_BLOC_PERSONS_ASSOCIATED,
ACTIVITY_BLOC_THIRDPARTY, ACTIVITY_BLOC_THIRDPARTY,
ACTIVITY_BLOC_USERS, ACTIVITY_BLOC_USERS,
ACTIVITY_ADD_PERSONS, ACTIVITY_ADD_PERSONS,
trans, trans,
} from "translator"; } from "translator";
export default { export default {
name: "ConcernedGroups", name: "ConcernedGroups",
components: { components: {
AddPersons, AddPersons,
PersonsBloc, PersonsBloc,
PersonText, PersonText,
}, },
setup() { setup() {
return { return {
trans, trans,
ACTIVITY_ADD_PERSONS, ACTIVITY_ADD_PERSONS,
}; };
}, },
data() { data() {
return { return {
personsBlocs: [ personsBlocs: [
{ {
key: "persons", key: "persons",
title: trans(ACTIVITY_BLOC_PERSONS), title: trans(ACTIVITY_BLOC_PERSONS),
persons: [], persons: [],
included: false, 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;
}, },
{ ...mapState({
key: "personsAssociated", persons: (state) => state.activity.persons,
title: trans(ACTIVITY_BLOC_PERSONS_ASSOCIATED), thirdParties: (state) => state.activity.thirdParties,
persons: [], users: (state) => state.activity.users,
included: window.activity accompanyingCourse: (state) => state.activity.accompanyingPeriod,
? window.activity.activityType.personsVisible !== 0 }),
: true, ...mapGetters(["suggestedEntities"]),
getContext() {
return this.accompanyingCourse ? "accompanyingCourse" : "person";
}, },
{ contextPersonsBlocs() {
key: "personsNotAssociated", return this.personsBlocs.filter((bloc) => bloc.included !== false);
title: "activity.bloc_persons_not_associated",
persons: [],
included: window.activity
? window.activity.activityType.personsVisible !== 0
: true,
}, },
{ addPersonsOptions() {
key: "thirdparty", let optionsType = [];
title: trans(ACTIVITY_BLOC_THIRDPARTY), if (window.activity) {
persons: [], if (window.activity.activityType.personsVisible !== 0) {
included: window.activity optionsType.push("person");
? window.activity.activityType.thirdPartiesVisible !== 0 }
: true, 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() {
key: "users", return Math.round(100 / this.contextPersonsBlocs.length) + "%";
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;
}, },
...mapState({ mounted() {
persons: (state) => state.activity.persons, this.setPersonsInBloc();
thirdParties: (state) => state.activity.thirdParties,
users: (state) => state.activity.users,
accompanyingCourse: (state) => state.activity.accompanyingPeriod,
}),
...mapGetters(["suggestedEntities"]),
getContext() {
return this.accompanyingCourse ? "accompanyingCourse" : "person";
}, },
contextPersonsBlocs() { methods: {
return this.personsBlocs.filter((bloc) => bloc.included !== false); setPersonsInBloc() {
}, let groups;
addPersonsOptions() { if (this.accompanyingCourse) {
let optionsType = []; groups = this.splitPersonsInGroups();
if (window.activity) { }
if (window.activity.activityType.personsVisible !== 0) { this.personsBlocs.forEach((bloc) => {
optionsType.push("person"); if (this.accompanyingCourse) {
} switch (bloc.key) {
if (window.activity.activityType.thirdPartiesVisible !== 0) { case "personsAssociated":
optionsType.push("thirdparty"); bloc.persons = groups.personsAssociated;
} bloc.included = true;
if (window.activity.activityType.usersVisible !== 0) { break;
optionsType.push("user"); case "personsNotAssociated":
} bloc.persons = groups.personsNotAssociated;
} else { bloc.included = true;
optionsType = ["person", "thirdparty", "user"]; break;
} }
return { } else {
type: optionsType, switch (bloc.key) {
priority: null, case "persons":
uniq: false, bloc.persons = this.persons;
button: { bloc.included = true;
size: "btn-sm", 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();
}, },
};
}, },
getBlocWidth() {
return Math.round(100 / this.contextPersonsBlocs.length) + "%";
},
},
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> </script>

View File

@@ -1,29 +1,29 @@
<template> <template>
<li> <li>
<span :title="person.text" @click.prevent="$emit('remove', person)"> <span :title="person.text" @click.prevent="$emit('remove', person)">
<span class="chill_denomination"> <span class="chill_denomination">
<person-text :person="person" :is-cut="true" /> <person-text :person="person" :is-cut="true" />
</span> </span>
</span> </span>
</li> </li>
</template> </template>
<script> <script>
import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue"; import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
export default { export default {
name: "PersonBadge", name: "PersonBadge",
props: ["person"], props: ["person"],
components: { components: {
PersonText, PersonText,
}, },
// computed: { // computed: {
// textCutted() { // textCutted() {
// let more = (this.person.text.length > 15) ?'…' : ''; // let more = (this.person.text.length > 15) ?'…' : '';
// return this.person.text.slice(0,15) + more; // return this.person.text.slice(0,15) + more;
// } // }
// }, // },
emits: ["remove"], emits: ["remove"],
}; };
</script> </script>

View File

@@ -1,38 +1,38 @@
<template> <template>
<div class="item-bloc" :style="{ 'flex-basis': blocWidth }"> <div class="item-bloc" :style="{ 'flex-basis': blocWidth }">
<div class="item-row"> <div class="item-row">
<div class="item-col"> <div class="item-col">
<h4>{{ $t(bloc.title) }}</h4> <h4>{{ $t(bloc.title) }}</h4>
</div> </div>
<div class="item-col"> <div class="item-col">
<ul class="list-suggest remove-items"> <ul class="list-suggest remove-items">
<person-badge <person-badge
v-for="person in bloc.persons" v-for="person in bloc.persons"
:key="person.id" :key="person.id"
:person="person" :person="person"
@remove="removePerson" @remove="removePerson"
/> />
</ul> </ul>
</div> </div>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
import PersonBadge from "./PersonBadge.vue"; import PersonBadge from "./PersonBadge.vue";
export default { export default {
name: "PersonsBloc", name: "PersonsBloc",
components: { components: {
PersonBadge, PersonBadge,
}, },
props: ["bloc", "setPersonsInBloc", "blocWidth"], props: ["bloc", "setPersonsInBloc", "blocWidth"],
methods: { methods: {
removePerson(item) { removePerson(item) {
console.log("@@ CLICK remove person: item", item); console.log("@@ CLICK remove person: item", item);
this.$store.dispatch("removePersonInvolved", item); this.$store.dispatch("removePersonInvolved", item);
this.setPersonsInBloc(); this.setPersonsInBloc();
},
}, },
},
}; };
</script> </script>

View File

@@ -1,32 +1,32 @@
<template> <template>
<teleport to="#location"> <teleport to="#location">
<div class="mb-3 row"> <div class="mb-3 row">
<label :class="locationClassList"> <label :class="locationClassList">
{{ trans(ACTIVITY_LOCATION) }} {{ trans(ACTIVITY_LOCATION) }}
</label> </label>
<div class="col-sm-8"> <div class="col-sm-8">
<VueMultiselect <VueMultiselect
name="selectLocation" name="selectLocation"
id="selectLocation" id="selectLocation"
label="name" label="name"
track-by="id" track-by="id"
open-direction="top" open-direction="top"
:multiple="false" :multiple="false"
:searchable="true" :searchable="true"
:placeholder="trans(ACTIVITY_CHOOSE_LOCATION)" :placeholder="trans(ACTIVITY_CHOOSE_LOCATION)"
:custom-label="customLabel" :custom-label="customLabel"
:select-label="trans(MULTISELECT_SELECT_LABEL)" :select-label="trans(MULTISELECT_SELECT_LABEL)"
:deselect-label="trans(MULTISELECT_DESELECT_LABEL)" :deselect-label="trans(MULTISELECT_DESELECT_LABEL)"
:selected-label="trans(MULTISELECT_SELECTED_LABEL)" :selected-label="trans(MULTISELECT_SELECTED_LABEL)"
:options="availableLocations" :options="availableLocations"
group-values="locations" group-values="locations"
group-label="locationGroup" group-label="locationGroup"
v-model="location" v-model="location"
/> />
<new-location v-bind:available-locations="availableLocations" /> <new-location v-bind:available-locations="availableLocations" />
</div> </div>
</div> </div>
</teleport> </teleport>
</template> </template>
<script> <script>
@@ -35,60 +35,60 @@ import VueMultiselect from "vue-multiselect";
import NewLocation from "./Location/NewLocation.vue"; import NewLocation from "./Location/NewLocation.vue";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import { import {
trans, trans,
ACTIVITY_LOCATION, ACTIVITY_LOCATION,
ACTIVITY_CHOOSE_LOCATION, ACTIVITY_CHOOSE_LOCATION,
MULTISELECT_SELECT_LABEL, MULTISELECT_SELECT_LABEL,
MULTISELECT_DESELECT_LABEL, MULTISELECT_DESELECT_LABEL,
MULTISELECT_SELECTED_LABEL, MULTISELECT_SELECTED_LABEL,
} from "translator"; } from "translator";
export default { export default {
name: "Location", name: "Location",
components: { components: {
NewLocation, NewLocation,
VueMultiselect, 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() {
methods: { return {
labelAccompanyingCourseLocation(value) { trans,
return `${value.address.text} (${localizeString(value.locationType.title)})`; ACTIVITY_LOCATION,
ACTIVITY_CHOOSE_LOCATION,
MULTISELECT_SELECT_LABEL,
MULTISELECT_DESELECT_LABEL,
MULTISELECT_SELECTED_LABEL,
};
}, },
customLabel(value) { data() {
return value.locationType return {
? value.name locationClassList: `col-form-label col-sm-4 ${document.querySelector("input#chill_activitybundle_activity_location").getAttribute("required") ? "required" : ""}`,
? value.name === "__AccompanyingCourseLocation__" };
? this.labelAccompanyingCourseLocation(value) },
: `${value.name} (${localizeString(value.locationType.title)})` computed: {
: localizeString(value.locationType.title) ...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)
: "";
},
}, },
},
}; };
</script> </script>

View File

@@ -1,114 +1,123 @@
<template> <template>
<div> <div>
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
<a class="btn btn-sm btn-create" @click="openModal"> <a class="btn btn-sm btn-create" @click="openModal">
{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }} {{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
</a> </a>
</li> </li>
</ul> </ul>
<teleport to="body"> <teleport to="body">
<modal <modal
v-if="modal.showModal" v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass" :modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false" @close="modal.showModal = false"
> >
<template #header> <template #header>
<h3 class="modal-title"> <h3 class="modal-title">
{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }} {{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
</h3> </h3>
</template> </template>
<template #body> <template #body>
<form> <form>
<div class="alert alert-warning" v-if="errors.length"> <div class="alert alert-warning" v-if="errors.length">
<ul> <ul>
<li v-for="(e, i) in errors" :key="i"> <li v-for="(e, i) in errors" :key="i">
{{ e }} {{ e }}
</li> </li>
</ul> </ul>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<select <select
class="form-select form-select-lg" class="form-select form-select-lg"
id="type" id="type"
required required
v-model="selectType" v-model="selectType"
> >
<option selected disabled value=""> <option selected disabled value="">
{{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }} {{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }}
</option> </option>
<option v-for="t in locationTypes" :value="t" :key="t.id"> <option
{{ localizeString(t.title) }} v-for="t in locationTypes"
</option> :value="t"
</select> :key="t.id"
<label>{{ trans(ACTIVITY_LOCATION_FIELDS_TYPE) }}</label> >
</div> {{ localizeString(t.title) }}
</option>
</select>
<label>{{
trans(ACTIVITY_LOCATION_FIELDS_TYPE)
}}</label>
</div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
id="name" id="name"
v-model="inputName" v-model="inputName"
placeholder placeholder
/> />
<label for="name">{{ <label for="name">{{
trans(ACTIVITY_LOCATION_FIELDS_NAME) trans(ACTIVITY_LOCATION_FIELDS_NAME)
}}</label> }}</label>
</div> </div>
<add-address <add-address
:context="addAddress.context" :context="addAddress.context"
:options="addAddress.options" :options="addAddress.options"
:addressChangedCallback="submitNewAddress" :addressChangedCallback="submitNewAddress"
v-if="showAddAddress" v-if="showAddAddress"
ref="addAddress" ref="addAddress"
/> />
<div class="form-floating mb-3" v-if="showContactData"> <div class="form-floating mb-3" v-if="showContactData">
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
id="phonenumber1" id="phonenumber1"
v-model="inputPhonenumber1" v-model="inputPhonenumber1"
placeholder placeholder
/> />
<label for="phonenumber1">{{ <label for="phonenumber1">{{
trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER1) trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER1)
}}</label> }}</label>
</div> </div>
<div class="form-floating mb-3" v-if="hasPhonenumber1"> <div class="form-floating mb-3" v-if="hasPhonenumber1">
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
id="phonenumber2" id="phonenumber2"
v-model="inputPhonenumber2" v-model="inputPhonenumber2"
placeholder placeholder
/> />
<label for="phonenumber2">{{ <label for="phonenumber2">{{
trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER2) trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER2)
}}</label> }}</label>
</div> </div>
<div class="form-floating mb-3" v-if="showContactData"> <div class="form-floating mb-3" v-if="showContactData">
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
id="email" id="email"
v-model="inputEmail" v-model="inputEmail"
placeholder placeholder
/> />
<label for="email">{{ <label for="email">{{
trans(ACTIVITY_LOCATION_FIELDS_EMAIL) trans(ACTIVITY_LOCATION_FIELDS_EMAIL)
}}</label> }}</label>
</div> </div>
</form> </form>
</template> </template>
<template #footer> <template #footer>
<button class="btn btn-save" @click.prevent="saveNewLocation"> <button
{{ trans(SAVE) }} class="btn btn-save"
</button> @click.prevent="saveNewLocation"
</template> >
</modal> {{ trans(SAVE) }}
</teleport> </button>
</div> </template>
</modal>
</teleport>
</div>
</template> </template>
<script> <script>
@@ -119,236 +128,237 @@ import { getLocationTypes } from "../../api";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import { import {
SAVE, SAVE,
ACTIVITY_LOCATION_FIELDS_EMAIL, ACTIVITY_LOCATION_FIELDS_EMAIL,
ACTIVITY_LOCATION_FIELDS_PHONENUMBER1, ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2, ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
ACTIVITY_LOCATION_FIELDS_NAME, ACTIVITY_LOCATION_FIELDS_NAME,
ACTIVITY_LOCATION_FIELDS_TYPE, ACTIVITY_LOCATION_FIELDS_TYPE,
ACTIVITY_CHOOSE_LOCATION_TYPE, ACTIVITY_CHOOSE_LOCATION_TYPE,
ACTIVITY_CREATE_NEW_LOCATION, ACTIVITY_CREATE_NEW_LOCATION,
trans, trans,
} from "translator"; } from "translator";
export default { export default {
name: "NewLocation", name: "NewLocation",
components: { components: {
Modal, Modal,
AddAddress, 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;
},
}, },
inputName: { setup() {
get() { return {
return this.selected.name; trans,
}, SAVE,
set(value) { ACTIVITY_LOCATION_FIELDS_EMAIL,
this.selected.name = value; ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
}, ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
}, ACTIVITY_LOCATION_FIELDS_NAME,
inputEmail: { ACTIVITY_LOCATION_FIELDS_TYPE,
get() { ACTIVITY_CHOOSE_LOCATION_TYPE,
return this.selected.email; ACTIVITY_CREATE_NEW_LOCATION,
},
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, { props: ["availableLocations"],
address: { data() {
id: this.selected.addressId, return {
errors: [],
selected: {
type: null,
name: null,
addressId: null,
phonenumber1: null,
phonenumber2: null,
email: null,
}, },
}); locationTypes: [],
} modal: {
showModal: false,
makeFetch("POST", "/api/1.0/main/location.json", body) modalDialogClass: "modal-dialog-scrollable modal-xl",
.then((response) => { },
this.$store.dispatch("addAvailableLocationGroup", { addAddress: {
locationGroup: "Localisations nouvellement créées", options: {
locations: [response], button: {
}); text: {
this.$store.dispatch("updateLocation", response); create: "activity.create_address",
this.modal.showModal = false; edit: "activity.edit_address",
}) },
.catch((error) => { size: "btn-sm",
if (error.name === "ValidationException") { },
for (let v of error.violations) { title: {
this.errors.push(v); create: "activity.create_address",
} edit: "activity.edit_address",
} else { },
this.errors.push("An error occurred"); },
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;
},
}, },
submitNewAddress(payload) { mounted() {
this.selected.addressId = payload.addressId; this.getLocationTypesList();
this.addAddress.context.addressId = payload.addressId; },
this.addAddress.context.edit = true; 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");
}
});
}
},
submitNewAddress(payload) {
this.selected.addressId = payload.addressId;
this.addAddress.context.addressId = payload.addressId;
this.addAddress.context.edit = true;
},
}, },
},
}; };
</script> </script>

View File

@@ -1,98 +1,103 @@
<template> <template>
<teleport to="#social-issues-acc"> <teleport to="#social-issues-acc">
<div class="mb-3 row"> <div class="mb-3 row">
<div class="col-4"> <div class="col-4">
<label :class="socialIssuesClassList">{{ <label :class="socialIssuesClassList">{{
trans(ACTIVITY_SOCIAL_ISSUES) trans(ACTIVITY_SOCIAL_ISSUES)
}}</label> }}</label>
</div> </div>
<div class="col-8"> <div class="col-8">
<check-social-issue <check-social-issue
v-for="issue in socialIssuesList" v-for="issue in socialIssuesList"
:key="issue.id" :key="issue.id"
:issue="issue" :issue="issue"
:selection="socialIssuesSelected" :selection="socialIssuesSelected"
@updateSelected="updateIssuesSelected" @updateSelected="updateIssuesSelected"
> >
</check-social-issue> </check-social-issue>
<div class="my-3"> <div class="my-3">
<VueMultiselect <VueMultiselect
name="otherIssues" name="otherIssues"
label="text" label="text"
track-by="id" track-by="id"
open-direction="bottom" open-direction="bottom"
:close-on-select="true" :close-on-select="true"
:preserve-search="false" :preserve-search="false"
:reset-after="true" :reset-after="true"
:hide-selected="true" :hide-selected="true"
:taggable="false" :taggable="false"
:multiple="false" :multiple="false"
:searchable="true" :searchable="true"
:allow-empty="true" :allow-empty="true"
:show-labels="false" :show-labels="false"
:loading="issueIsLoading" :loading="issueIsLoading"
:placeholder="trans(ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE)" :placeholder="trans(ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE)"
:options="socialIssuesOther" :options="socialIssuesOther"
@select="addIssueInList" @select="addIssueInList"
> >
</VueMultiselect> </VueMultiselect>
</div> </div>
</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>
<span <div class="mb-3 row">
v-else-if="socialIssuesSelected.length === 0" <div class="col-4">
class="inline-choice chill-no-data-statement mt-3" <label :class="socialActionsClassList">{{
> trans(ACTIVITY_SOCIAL_ACTIONS)
{{ trans(ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE) }} }}</label>
</span> </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>
<template <span
v-else-if=" v-else-if="socialIssuesSelected.length === 0"
socialActionsList.length > 0 && class="inline-choice chill-no-data-statement mt-3"
(socialIssuesSelected.length || socialActionsSelected.length) >
" {{ trans(ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE) }}
> </span>
<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 <template
v-else-if="actionAreLoaded && socialActionsList.length === 0" v-else-if="
class="inline-choice chill-no-data-statement mt-3" socialActionsList.length > 0 &&
> (socialIssuesSelected.length ||
{{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }} socialActionsSelected.length)
</span> "
</div> >
</div> <div
</teleport> 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>
</template> </template>
<script> <script>
@@ -101,153 +106,154 @@ import CheckSocialIssue from "./SocialIssuesAcc/CheckSocialIssue.vue";
import CheckSocialAction from "./SocialIssuesAcc/CheckSocialAction.vue"; import CheckSocialAction from "./SocialIssuesAcc/CheckSocialAction.vue";
import { getSocialIssues, getSocialActionByIssue } from "../api.js"; import { getSocialIssues, getSocialActionByIssue } from "../api.js";
import { import {
ACTIVITY_SOCIAL_ACTION_LIST_EMPTY, ACTIVITY_SOCIAL_ACTION_LIST_EMPTY,
ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE, ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE,
ACTIVITY_SOCIAL_ACTIONS, ACTIVITY_SOCIAL_ACTIONS,
ACTIVITY_SOCIAL_ISSUES, ACTIVITY_SOCIAL_ISSUES,
ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE, ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE,
trans, trans,
} from "translator"; } from "translator";
export default { export default {
name: "SocialIssuesAcc", name: "SocialIssuesAcc",
components: { components: {
CheckSocialIssue, CheckSocialIssue,
CheckSocialAction, CheckSocialAction,
VueMultiselect, 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;
}, },
socialIssuesSelected() { setup() {
return this.$store.state.activity.socialIssues; return {
trans,
ACTIVITY_SOCIAL_ACTION_LIST_EMPTY,
ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE,
ACTIVITY_SOCIAL_ACTIONS,
ACTIVITY_SOCIAL_ISSUES,
ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE,
};
}, },
socialIssuesOther() { data() {
return this.$store.state.socialIssuesOther; 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" : ""}`,
};
}, },
socialActionsList() { computed: {
return this.$store.getters.socialActionsListSorted; 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;
},
}, },
socialActionsSelected() { mounted() {
return this.$store.state.activity.socialActions; /* 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: {
mounted() { /* When choosing an issue in multiselect, add it in checkboxes (as selected),
/* 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 remove it from multiselect, and add socialActions concerned
*/ */
addIssueInList(value) { addIssueInList(value) {
//console.log('addIssueInList', value); //console.log('addIssueInList', value);
this.$store.commit("addIssueInList", value); this.$store.commit("addIssueInList", value);
this.$store.commit("removeIssueInOther", value); this.$store.commit("removeIssueInOther", value);
this.$store.dispatch("addIssueSelected", value); this.$store.dispatch("addIssueSelected", value);
this.updateActionsList(); this.updateActionsList();
}, },
/* Update value for selected issues checkboxes /* Update value for selected issues checkboxes
*/ */
updateIssuesSelected(issues) { updateIssuesSelected(issues) {
//console.log('updateIssuesSelected', issues); //console.log('updateIssuesSelected', issues);
this.$store.dispatch("updateIssuesSelected", issues); this.$store.dispatch("updateIssuesSelected", issues);
this.updateActionsList(); this.updateActionsList();
}, },
/* Update value for selected actions checkboxes /* Update value for selected actions checkboxes
*/ */
updateActionsSelected(actions) { updateActionsSelected(actions) {
//console.log('updateActionsSelected', actions); //console.log('updateActionsSelected', actions);
this.$store.dispatch("updateActionsSelected", actions); this.$store.dispatch("updateActionsSelected", actions);
}, },
/* Add socialActions concerned: after reset, loop on each issue selected /* Add socialActions concerned: after reset, loop on each issue selected
to get social actions concerned to get social actions concerned
*/ */
updateActionsList() { updateActionsList() {
this.resetActionsList(); this.resetActionsList();
this.socialIssuesSelected.forEach((item) => { this.socialIssuesSelected.forEach((item) => {
this.actionIsLoading = true; this.actionIsLoading = true;
getSocialActionByIssue(item.id).then( getSocialActionByIssue(item.id).then(
(actions) => (actions) =>
new Promise((resolve) => { new Promise((resolve) => {
actions.results.forEach((action) => { actions.results.forEach((action) => {
this.$store.commit("addActionInList", action); this.$store.commit("addActionInList", action);
}, this); }, this);
this.$store.commit("filterList", "actions"); this.$store.commit("filterList", "actions");
this.actionIsLoading = false; this.actionIsLoading = false;
this.actionAreLoaded = true; this.actionAreLoaded = true;
resolve(); resolve();
}), }),
); );
}, this); }, 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);
},
}, },
/* 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> </script>
@@ -257,18 +263,18 @@ export default {
@import "ChillMainAssets/chill/scss/chill_variables"; @import "ChillMainAssets/chill/scss/chill_variables";
span.multiselect__single { span.multiselect__single {
display: none !important; display: none !important;
} }
#actionsList { #actionsList {
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 1rem; padding: 1rem;
margin: 0.5rem; margin: 0.5rem;
background-color: whitesmoke; background-color: whitesmoke;
} }
span.badge { span.badge {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@include badge_social($social-issue-color); @include badge_social($social-issue-color);
} }
</style> </style>

View File

@@ -1,38 +1,38 @@
<template> <template>
<span class="inline-choice"> <span class="inline-choice">
<div class="form-check"> <div class="form-check">
<input <input
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
v-model="selected" v-model="selected"
name="action" name="action"
:id="action.id" :id="action.id"
:value="action" :value="action"
/> />
<label class="form-check-label" :for="action.id"> <label class="form-check-label" :for="action.id">
<span class="badge bg-light text-dark" :title="action.text">{{ <span class="badge bg-light text-dark" :title="action.text">{{
action.text action.text
}}</span> }}</span>
</label> </label>
</div> </div>
</span> </span>
</template> </template>
<script> <script>
export default { export default {
name: "CheckSocialAction", name: "CheckSocialAction",
props: ["action", "selection"], props: ["action", "selection"],
emits: ["updateSelected"], emits: ["updateSelected"],
computed: { computed: {
selected: { selected: {
set(value) { set(value) {
this.$emit("updateSelected", value); this.$emit("updateSelected", value);
}, },
get() { get() {
return this.selection; return this.selection;
}, },
},
}, },
},
}; };
</script> </script>
@@ -41,13 +41,13 @@ export default {
@import "ChillPersonAssets/chill/scss/mixins"; @import "ChillPersonAssets/chill/scss/mixins";
@import "ChillMainAssets/chill/scss/chill_variables"; @import "ChillMainAssets/chill/scss/chill_variables";
span.badge { span.badge {
@include badge_social($social-action-color); @include badge_social($social-action-color);
font-size: 95%; font-size: 95%;
margin-bottom: 5px; margin-bottom: 5px;
margin-right: 1em; margin-right: 1em;
max-width: 100%; /* Adjust as needed */ max-width: 100%; /* Adjust as needed */
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
</style> </style>

View File

@@ -1,36 +1,38 @@
<template> <template>
<span class="inline-choice"> <span class="inline-choice">
<div class="form-check"> <div class="form-check">
<input <input
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
v-model="selected" v-model="selected"
name="issue" name="issue"
:id="issue.id" :id="issue.id"
:value="issue" :value="issue"
/> />
<label class="form-check-label" :for="issue.id"> <label class="form-check-label" :for="issue.id">
<span class="badge bg-chill-l-gray text-dark">{{ issue.text }}</span> <span class="badge bg-chill-l-gray text-dark">{{
</label> issue.text
</div> }}</span>
</span> </label>
</div>
</span>
</template> </template>
<script> <script>
export default { export default {
name: "CheckSocialIssue", name: "CheckSocialIssue",
props: ["issue", "selection"], props: ["issue", "selection"],
emits: ["updateSelected"], emits: ["updateSelected"],
computed: { computed: {
selected: { selected: {
set(value) { set(value) {
this.$emit("updateSelected", value); this.$emit("updateSelected", value);
}, },
get() { get() {
return this.selection; return this.selection;
}, },
},
}, },
},
}; };
</script> </script>
@@ -39,9 +41,9 @@ export default {
@import "ChillPersonAssets/chill/scss/mixins"; @import "ChillPersonAssets/chill/scss/mixins";
@import "ChillMainAssets/chill/scss/chill_variables"; @import "ChillMainAssets/chill/scss/chill_variables";
span.badge { span.badge {
@include badge_social($social-issue-color); @include badge_social($social-issue-color);
font-size: 95%; font-size: 95%;
margin-bottom: 5px; margin-bottom: 5px;
margin-right: 1em; margin-right: 1em;
} }
</style> </style>

View File

@@ -1,74 +1,76 @@
import { EventInput } from "@fullcalendar/core"; import { EventInput } from "@fullcalendar/core";
import { import {
DateTime, DateTime,
Location, Location,
User, User,
UserAssociatedInterface, UserAssociatedInterface,
} from "../../../ChillMainBundle/Resources/public/types"; } from "../../../ChillMainBundle/Resources/public/types";
import { Person } from "../../../ChillPersonBundle/Resources/public/types"; import { Person } from "../../../ChillPersonBundle/Resources/public/types";
export interface CalendarRange { export interface CalendarRange {
id: number; id: number;
endDate: DateTime; endDate: DateTime;
startDate: DateTime; startDate: DateTime;
user: User; user: User;
location: Location; location: Location;
createdAt: DateTime; createdAt: DateTime;
createdBy: User; createdBy: User;
updatedAt: DateTime; updatedAt: DateTime;
updatedBy: User; updatedBy: User;
} }
export interface CalendarRangeCreate { export interface CalendarRangeCreate {
user: UserAssociatedInterface; user: UserAssociatedInterface;
startDate: DateTime; startDate: DateTime;
endDate: DateTime; endDate: DateTime;
location: Location; location: Location;
} }
export interface CalendarRangeEdit { export interface CalendarRangeEdit {
startDate?: DateTime; startDate?: DateTime;
endDate?: DateTime; endDate?: DateTime;
location?: Location; location?: Location;
} }
export interface Calendar { export interface Calendar {
id: number; id: number;
} }
export interface CalendarLight { export interface CalendarLight {
id: number; id: number;
endDate: DateTime; endDate: DateTime;
startDate: DateTime; startDate: DateTime;
mainUser: User; mainUser: User;
persons: Person[]; persons: Person[];
status: "valid" | "moved" | "canceled"; status: "valid" | "moved" | "canceled";
} }
export interface CalendarRemote { export interface CalendarRemote {
id: number; id: number;
endDate: DateTime; endDate: DateTime;
startDate: DateTime; startDate: DateTime;
title: string; title: string;
isAllDay: boolean; isAllDay: boolean;
} }
export type EventInputCalendarRange = EventInput & { export type EventInputCalendarRange = EventInput & {
id: string; id: string;
userId: number; userId: number;
userLabel: string; userLabel: string;
calendarRangeId: number; calendarRangeId: number;
locationId: number; locationId: number;
locationName: string; locationName: string;
start: string; start: string;
end: string; end: string;
is: "range"; is: "range";
}; };
export function isEventInputCalendarRange( export function isEventInputCalendarRange(
toBeDetermined: EventInputCalendarRange | EventInput, toBeDetermined: EventInputCalendarRange | EventInput,
): toBeDetermined is EventInputCalendarRange { ): toBeDetermined is EventInputCalendarRange {
return typeof toBeDetermined.is === "string" && toBeDetermined.is === "range"; return (
typeof toBeDetermined.is === "string" && toBeDetermined.is === "range"
);
} }
export {}; export {};

View File

@@ -1,146 +1,164 @@
<template> <template>
<teleport to="#mainUser"> <teleport to="#mainUser">
<h2 class="chill-red">Utilisateur principal</h2> <h2 class="chill-red">Utilisateur principal</h2>
<div> <div>
<div> <div>
<div v-if="null !== this.$store.getters.getMainUser"> <div v-if="null !== this.$store.getters.getMainUser">
<calendar-active :user="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>
</div> </div>
<pick-entity </teleport>
: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"> <teleport to="#schedule">
<div class="row mb-3" v-if="activity.startDate !== null"> <div class="row mb-3" v-if="activity.startDate !== null">
<label class="col-form-label col-sm-4">Date</label> <label class="col-form-label col-sm-4">Date</label>
<div class="col-sm-8"> <div class="col-sm-8">
{{ $d(activity.startDate, "long") }} - {{ $d(activity.startDate, "long") }} -
{{ $d(activity.endDate, "hoursOnly") }} {{ $d(activity.endDate, "hoursOnly") }}
<span v-if="activity.calendarRange === null" <span v-if="activity.calendarRange === null"
>(Pas de plage de disponibilité sélectionnée)</span >(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"
> >
<span v-else>(Une plage de disponibilité sélectionnée)</span> <div class="col-sm-9 col-xs-12">
</div> <div class="input-group mb-3">
</div> <label class="input-group-text" for="slotDuration"
</teleport> >Durée des créneaux</label
>
<location /> <select
v-model="slotDuration"
<teleport to="#fullCalendar"> id="slotDuration"
<div class="calendar-actives"> class="form-select"
<template v-for="u in getActiveUsers" :key="u.id"> >
<calendar-active <option value="00:05:00">5 minutes</option>
:user="u" <option value="00:10:00">10 minutes</option>
:invite="this.$store.getters.getInviteForUser(u)" <option value="00:15:00">15 minutes</option>
/> <option value="00:30:00">30 minutes</option>
</template> </select>
</div> <label class="input-group-text" for="slotMinTime">De</label>
<div <select
class="display-options row justify-content-between" v-model="slotMinTime"
style="margin-top: 1rem" id="slotMinTime"
> class="form-select"
<div class="col-sm-9 col-xs-12"> >
<div class="input-group mb-3"> <option value="00:00:00">0h</option>
<label class="input-group-text" for="slotDuration" <option value="01:00:00">1h</option>
>Durée des créneaux</label <option value="02:00:00">2h</option>
> <option value="03:00:00">3h</option>
<select v-model="slotDuration" id="slotDuration" class="form-select"> <option value="04:00:00">4h</option>
<option value="00:05:00">5 minutes</option> <option value="05:00:00">5h</option>
<option value="00:10:00">10 minutes</option> <option value="06:00:00">6h</option>
<option value="00:15:00">15 minutes</option> <option value="07:00:00">7h</option>
<option value="00:30:00">30 minutes</option> <option value="08:00:00">8h</option>
</select> <option value="09:00:00">9h</option>
<label class="input-group-text" for="slotMinTime">De</label> <option value="10:00:00">10h</option>
<select v-model="slotMinTime" id="slotMinTime" class="form-select"> <option value="11:00:00">11h</option>
<option value="00:00:00">0h</option> <option value="12:00:00">12h</option>
<option value="01:00:00">1h</option> </select>
<option value="02:00:00">2h</option> <label class="input-group-text" for="slotMaxTime">À</label>
<option value="03:00:00">3h</option> <select
<option value="04:00:00">4h</option> v-model="slotMaxTime"
<option value="05:00:00">5h</option> id="slotMaxTime"
<option value="06:00:00">6h</option> class="form-select"
<option value="07:00:00">7h</option> >
<option value="08:00:00">8h</option> <option value="12:00:00">12h</option>
<option value="09:00:00">9h</option> <option value="13:00:00">13h</option>
<option value="10:00:00">10h</option> <option value="14:00:00">14h</option>
<option value="11:00:00">11h</option> <option value="15:00:00">15h</option>
<option value="12:00:00">12h</option> <option value="16:00:00">16h</option>
</select> <option value="17:00:00">17h</option>
<label class="input-group-text" for="slotMaxTime">À</label> <option value="18:00:00">18h</option>
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select"> <option value="19:00:00">19h</option>
<option value="12:00:00">12h</option> <option value="20:00:00">20h</option>
<option value="13:00:00">13h</option> <option value="21:00:00">21h</option>
<option value="14:00:00">14h</option> <option value="22:00:00">22h</option>
<option value="15:00:00">15h</option> <option value="23:00:00">23h</option>
<option value="16:00:00">16h</option> <option value="23:59:59">24h</option>
<option value="17:00:00">17h</option> </select>
<option value="18:00:00">18h</option> </div>
<option value="19:00:00">19h</option> </div>
<option value="20:00:00">20h</option> <div class="col-sm-3 col-xs-12">
<option value="21:00:00">21h</option> <div class="float-end">
<option value="22:00:00">22h</option> <div class="form-check input-group">
<option value="23:00:00">23h</option> <span class="input-group-text">
<option value="23:59:59">24h</option> <input
</select> 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> </div>
</div> <FullCalendar ref="fullCalendar" :options="calendarOptions">
<div class="col-sm-3 col-xs-12"> <template #eventContent="arg">
<div class="float-end"> <span>
<div class="form-check input-group"> <b v-if="arg.event.extendedProps.is === 'remote'">{{
<span class="input-group-text"> arg.event.title
<input }}</b>
id="showHideWE" <b v-else-if="arg.event.extendedProps.is === 'range'"
class="mt-0" >{{ arg.timeText }}
type="checkbox" {{ arg.event.extendedProps.locationName }}
v-model="hideWeekends" <small>{{
/> arg.event.extendedProps.userLabel
</span> }}</small></b
<label for="showHideWE" class="form-check-label input-group-text" >
>Week-ends</label <b v-else-if="arg.event.extendedProps.is === 'current'"
> >{{ arg.timeText }} {{ $t("current_selected") }}
</div> </b>
</div> <b v-else-if="arg.event.extendedProps.is === 'local'">{{
</div> arg.event.title
</div> }}</b>
<FullCalendar ref="fullCalendar" :options="calendarOptions"> <b v-else
<template #eventContent="arg"> >{{ arg.timeText }} {{ $t("current_selected") }}
<span> </b>
<b v-if="arg.event.extendedProps.is === 'remote'">{{ </span>
arg.event.title </template>
}}</b> </FullCalendar>
<b v-else-if="arg.event.extendedProps.is === 'range'" </teleport>
>{{ 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> </template>
<script> <script>
@@ -157,210 +175,219 @@ import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
import { mapGetters, mapState } from "vuex"; import { mapGetters, mapState } from "vuex";
export default { export default {
name: "App", name: "App",
components: { components: {
ConcernedGroups, ConcernedGroups,
Location, Location,
FullCalendar, FullCalendar,
CalendarActive, CalendarActive,
PickEntity, 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;
}, },
calendarOptions() { data() {
return { return {
locale: frLocale, errorMsg: [],
plugins: [ showMyCalendar: false,
dayGridPlugin, slotDuration: "00:05:00",
interactionPlugin, slotMinTime: "09:00:00",
timeGridPlugin, slotMaxTime: "18:00:00",
dayGridPlugin, hideWeekEnds: true,
listPlugin, previousUser: [],
], };
initialView: "timeGridWeek", },
initialDate: this.$store.getters.getInitialDate, computed: {
eventSources: this.events, ...mapGetters(["getMainUser"]),
selectable: true, ...mapState(["activity"]),
slotMinTime: this.slotMinTime, events() {
slotMaxTime: this.slotMaxTime, return this.$store.getters.getEventSources;
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: { calendarOptions() {
timeGrid: { return {
slotEventOverlap: false, locale: frLocale,
slotDuration: this.slotDuration, 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() { methods: {
const users = []; setMainUser({ entity }) {
for (const id of this.$store.state.currentView.users.keys()) { const user = entity;
users.push(this.$store.getters.getUserDataById(id).user); console.log("setMainUser APP", entity);
}
return users; 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,
});
},
}, },
suggestedUsers() {
const suggested = [];
this.$data.previousUser.forEach((u) => {
if (u.id !== this.$store.getters.getMainUser.id) {
suggested.push(u);
}
});
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> </script>
<style> <style>
.calendar-actives { .calendar-actives {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
} }
.display-options { .display-options {
margin-top: 1rem; margin-top: 1rem;
} }
/* for events which are range */ /* for events which are range */
.fc-event.isrange { .fc-event.isrange {
border-width: 3px; border-width: 3px;
} }
</style> </style>

View File

@@ -1,105 +1,119 @@
<template> <template>
<div :style="style" class="calendar-active"> <div :style="style" class="calendar-active">
<span class="badge-user"> <span class="badge-user">
{{ user.text }} {{ user.text }}
<template v-if="invite !== null"> <template v-if="invite !== null">
<i v-if="invite.status === 'accepted'" class="fa fa-check" /> <i v-if="invite.status === 'accepted'" class="fa fa-check" />
<i v-else-if="invite.status === 'declined'" class="fa fa-times" /> <i
<i v-else-if="invite.status === 'pending'" class="fa fa-question-o" /> v-else-if="invite.status === 'declined'"
<i v-else-if="invite.status === 'tentative'" class="fa fa-question" /> class="fa fa-times"
<span v-else="">{{ invite.status }}</span> />
</template> <i
</span> v-else-if="invite.status === 'pending'"
<span class="form-check-inline form-switch"> class="fa fa-question-o"
<input />
class="form-check-input" <i
type="checkbox" v-else-if="invite.status === 'tentative'"
id="flexSwitchCheckDefault" class="fa fa-question"
v-model="rangeShow" />
/> <span v-else="">{{ invite.status }}</span>
&nbsp;<label </template>
class="form-check-label" </span>
for="flexSwitchCheckDefault" <span class="form-check-inline form-switch">
title="Disponibilités" <input
><i class="fa fa-calendar-check-o" class="form-check-input"
/></label> type="checkbox"
</span> id="flexSwitchCheckDefault"
<span class="form-check-inline form-switch"> v-model="rangeShow"
<input />
class="form-check-input" &nbsp;<label
type="checkbox" class="form-check-label"
id="flexSwitchCheckDefault" for="flexSwitchCheckDefault"
v-model="remoteShow" title="Disponibilités"
/> ><i class="fa fa-calendar-check-o"
&nbsp;<label /></label>
class="form-check-label" </span>
for="flexSwitchCheckDefault" <span class="form-check-inline form-switch">
title="Agenda" <input
><i class="fa fa-calendar" class="form-check-input"
/></label> type="checkbox"
</span> id="flexSwitchCheckDefault"
</div> v-model="remoteShow"
/>
&nbsp;<label
class="form-check-label"
for="flexSwitchCheckDefault"
title="Agenda"
><i class="fa fa-calendar"
/></label>
</span>
</div>
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import { mapGetters } from "vuex";
export default { export default {
name: "CalendarActive", name: "CalendarActive",
props: { props: {
user: { user: {
type: Object, type: Object,
required: true, required: true,
},
invite: {
type: Object,
required: false,
default: null,
},
}, },
invite: { computed: {
type: Object, style() {
required: false, return {
default: null, 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,
);
},
},
}, },
},
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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.calendar-active { .calendar-active {
margin: 0 0.25rem 0.25rem 0; margin: 0 0.25rem 0.25rem 0;
padding: 0.5rem; padding: 0.5rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--bs-blue); color: var(--bs-blue);
& > .badge-user { & > .badge-user {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
} }
</style> </style>

View File

@@ -14,37 +14,37 @@ export { whoami } from "../../../../../ChillMainBundle/Resources/public/lib/api/
* @return Promise * @return Promise
*/ */
export const fetchCalendarRangeForUser = ( export const fetchCalendarRangeForUser = (
user: User, user: User,
start: Date, start: Date,
end: Date, end: Date,
): Promise<CalendarRange[]> => { ): Promise<CalendarRange[]> => {
const uri = `/api/1.0/calendar/calendar-range-available/${user.id}.json`; const uri = `/api/1.0/calendar/calendar-range-available/${user.id}.json`;
const dateFrom = datetimeToISO(start); const dateFrom = datetimeToISO(start);
const dateTo = datetimeToISO(end); const dateTo = datetimeToISO(end);
return fetchResults<CalendarRange>(uri, { dateFrom, dateTo }); return fetchResults<CalendarRange>(uri, { dateFrom, dateTo });
}; };
export const fetchCalendarRemoteForUser = ( export const fetchCalendarRemoteForUser = (
user: User, user: User,
start: Date, start: Date,
end: Date, end: Date,
): Promise<CalendarRemote[]> => { ): Promise<CalendarRemote[]> => {
const uri = `/api/1.0/calendar/proxy/calendar/by-user/${user.id}/events`; const uri = `/api/1.0/calendar/proxy/calendar/by-user/${user.id}/events`;
const dateFrom = datetimeToISO(start); const dateFrom = datetimeToISO(start);
const dateTo = datetimeToISO(end); const dateTo = datetimeToISO(end);
return fetchResults<CalendarRemote>(uri, { dateFrom, dateTo }); return fetchResults<CalendarRemote>(uri, { dateFrom, dateTo });
}; };
export const fetchCalendarLocalForUser = ( export const fetchCalendarLocalForUser = (
user: User, user: User,
start: Date, start: Date,
end: Date, end: Date,
): Promise<CalendarLight[]> => { ): Promise<CalendarLight[]> => {
const uri = `/api/1.0/calendar/calendar/by-user/${user.id}.json`; const uri = `/api/1.0/calendar/calendar/by-user/${user.id}.json`;
const dateFrom = datetimeToISO(start); const dateFrom = datetimeToISO(start);
const dateTo = datetimeToISO(end); const dateTo = datetimeToISO(end);
return fetchResults<CalendarLight>(uri, { dateFrom, dateTo }); return fetchResults<CalendarLight>(uri, { dateFrom, dateTo });
}; };

View File

@@ -1,17 +1,17 @@
const COLORS = [ const COLORS = [
/* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */ /* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
"#8dd3c7", "#8dd3c7",
"#ffffb3", "#ffffb3",
"#bebada", "#bebada",
"#fb8072", "#fb8072",
"#80b1d3", "#80b1d3",
"#fdb462", "#fdb462",
"#b3de69", "#b3de69",
"#fccde5", "#fccde5",
"#d9d9d9", "#d9d9d9",
"#bc80bd", "#bc80bd",
"#ccebc5", "#ccebc5",
"#ffed6f", "#ffed6f",
]; ];
export { COLORS }; export { COLORS };

View File

@@ -1,117 +1,117 @@
import { COLORS } from "../const"; import { COLORS } from "../const";
import { ISOToDatetime } from "../../../../../../ChillMainBundle/Resources/public/chill/js/date"; import { ISOToDatetime } from "../../../../../../ChillMainBundle/Resources/public/chill/js/date";
import { import {
DateTime, DateTime,
User, User,
} from "../../../../../../ChillMainBundle/Resources/public/types"; } from "../../../../../../ChillMainBundle/Resources/public/types";
import { CalendarLight, CalendarRange, CalendarRemote } from "../../../types"; import { CalendarLight, CalendarRange, CalendarRemote } from "../../../types";
import type { EventInputCalendarRange } from "../../../types"; import type { EventInputCalendarRange } from "../../../types";
import { EventInput } from "@fullcalendar/core"; import { EventInput } from "@fullcalendar/core";
export interface UserData { export interface UserData {
user: User; user: User;
calendarRanges: CalendarRange[]; calendarRanges: CalendarRange[];
calendarRangesLoaded: {}[]; calendarRangesLoaded: {}[];
remotes: CalendarRemote[]; remotes: CalendarRemote[];
remotesLoaded: {}[]; remotesLoaded: {}[];
locals: CalendarRemote[]; locals: CalendarRemote[];
localsLoaded: {}[]; localsLoaded: {}[];
mainColor: string; mainColor: string;
} }
export const addIdToValue = (string: string, id: number): string => { export const addIdToValue = (string: string, id: number): string => {
const array = string ? string.split(",") : []; const array = string ? string.split(",") : [];
array.push(id.toString()); array.push(id.toString());
const str = array.join(); const str = array.join();
return str; return str;
}; };
export const removeIdFromValue = (string: string, id: number) => { export const removeIdFromValue = (string: string, id: number) => {
let array = string.split(","); let array = string.split(",");
array = array.filter((el) => el !== id.toString()); array = array.filter((el) => el !== id.toString());
const str = array.join(); const str = array.join();
return str; return str;
}; };
/* /*
* Assign missing keys for the ConcernedGroups component * Assign missing keys for the ConcernedGroups component
*/ */
export const mapEntity = (entity: EventInput): EventInput => { export const mapEntity = (entity: EventInput): EventInput => {
const calendar = { ...entity }; const calendar = { ...entity };
Object.assign(calendar, { thirdParties: entity.professionals }); Object.assign(calendar, { thirdParties: entity.professionals });
if (entity.startDate !== null) { if (entity.startDate !== null) {
calendar.startDate = ISOToDatetime(entity.startDate.datetime); calendar.startDate = ISOToDatetime(entity.startDate.datetime);
} }
if (entity.endDate !== null) { if (entity.endDate !== null) {
calendar.endDate = ISOToDatetime(entity.endDate.datetime); calendar.endDate = ISOToDatetime(entity.endDate.datetime);
} }
if (entity.calendarRange !== null) { if (entity.calendarRange !== null) {
calendar.calendarRange.calendarRangeId = entity.calendarRange.id; calendar.calendarRange.calendarRangeId = entity.calendarRange.id;
calendar.calendarRange.id = `range_${entity.calendarRange.id}`; calendar.calendarRange.id = `range_${entity.calendarRange.id}`;
} }
return calendar; return calendar;
}; };
export const createUserData = (user: User, colorIndex: number): UserData => { export const createUserData = (user: User, colorIndex: number): UserData => {
const colorId = colorIndex % COLORS.length; const colorId = colorIndex % COLORS.length;
return { return {
user: user, user: user,
calendarRanges: [], calendarRanges: [],
calendarRangesLoaded: [], calendarRangesLoaded: [],
remotes: [], remotes: [],
remotesLoaded: [], remotesLoaded: [],
locals: [], locals: [],
localsLoaded: [], localsLoaded: [],
mainColor: COLORS[colorId], mainColor: COLORS[colorId],
}; };
}; };
// TODO move this function to a more global namespace, as it is also in use in MyCalendarRange app // TODO move this function to a more global namespace, as it is also in use in MyCalendarRange app
export const calendarRangeToFullCalendarEvent = ( export const calendarRangeToFullCalendarEvent = (
entity: CalendarRange, entity: CalendarRange,
): EventInputCalendarRange => { ): EventInputCalendarRange => {
return { return {
id: `range_${entity.id}`, id: `range_${entity.id}`,
title: "(" + entity.user.text + ")", title: "(" + entity.user.text + ")",
start: entity.startDate.datetime8601, start: entity.startDate.datetime8601,
end: entity.endDate.datetime8601, end: entity.endDate.datetime8601,
allDay: false, allDay: false,
userId: entity.user.id, userId: entity.user.id,
userLabel: entity.user.label, userLabel: entity.user.label,
calendarRangeId: entity.id, calendarRangeId: entity.id,
locationId: entity.location.id, locationId: entity.location.id,
locationName: entity.location.name, locationName: entity.location.name,
is: "range", is: "range",
}; };
}; };
export const remoteToFullCalendarEvent = ( export const remoteToFullCalendarEvent = (
entity: CalendarRemote, entity: CalendarRemote,
): EventInput & { id: string } => { ): EventInput & { id: string } => {
return { return {
id: `range_${entity.id}`, id: `range_${entity.id}`,
title: entity.title, title: entity.title,
start: entity.startDate.datetime8601, start: entity.startDate.datetime8601,
end: entity.endDate.datetime8601, end: entity.endDate.datetime8601,
allDay: entity.isAllDay, allDay: entity.isAllDay,
is: "remote", is: "remote",
}; };
}; };
export const localsToFullCalendarEvent = ( export const localsToFullCalendarEvent = (
entity: CalendarLight, entity: CalendarLight,
): EventInput & { id: string; originId: number } => { ): EventInput & { id: string; originId: number } => {
return { return {
id: `local_${entity.id}`, id: `local_${entity.id}`,
title: entity.persons.map((p) => p.text).join(", "), title: entity.persons.map((p) => p.text).join(", "),
originId: entity.id, originId: entity.id,
start: entity.startDate.datetime8601, start: entity.startDate.datetime8601,
end: entity.endDate.datetime8601, end: entity.endDate.datetime8601,
allDay: false, allDay: false,
is: "local", is: "local",
}; };
}; };

View File

@@ -1,50 +1,58 @@
<template> <template>
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button <button
id="btnGroupDrop1" id="btnGroupDrop1"
type="button" type="button"
class="btn btn-misc dropdown-toggle" class="btn btn-misc dropdown-toggle"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-expanded="false" 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
> >
</li> <template v-if="status === Statuses.PENDING">
<li v-if="status !== Statuses.DECLINED"> <span class="fa fa-hourglass"></span> {{ $t("Give_an_answer") }}
<a class="dropdown-item" @click="changeStatus(Statuses.DECLINED)" </template>
><i class="fa fa-times" aria-hidden="true"></i> {{ $t("Decline") }}</a <template v-else-if="status === Statuses.ACCEPTED">
> <span class="fa fa-check"></span> {{ $t("Accepted") }}
</li> </template>
<li v-if="status !== Statuses.TENTATIVELY_ACCEPTED"> <template v-else-if="status === Statuses.DECLINED">
<a <span class="fa fa-times"></span> {{ $t("Declined") }}
class="dropdown-item" </template>
@click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)" <template v-else-if="status === Statuses.TENTATIVELY_ACCEPTED">
><i class="fa fa-question"></i> {{ $t("Tentatively_accept") }}</a <span class="fa fa-question"></span> {{ $t("Tentative") }}
> </template>
</li> </button>
<li v-if="status !== Statuses.PENDING"> <ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
<a class="dropdown-item" @click="changeStatus(Statuses.PENDING)" <li v-if="status !== Statuses.ACCEPTED">
><i class="fa fa-hourglass-o"></i> {{ $t("Set_pending") }}</a <a
> class="dropdown-item"
</li> @click="changeStatus(Statuses.ACCEPTED)"
</ul> ><i class="fa fa-check" aria-hidden="true"></i>
</div> {{ $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>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -56,67 +64,69 @@ const PENDING = "pending";
const TENTATIVELY_ACCEPTED = "tentative"; const TENTATIVELY_ACCEPTED = "tentative";
const i18n = { const i18n = {
messages: { messages: {
fr: { fr: {
Give_an_answer: "Répondre", Give_an_answer: "Répondre",
Accepted: "Accepté", Accepted: "Accepté",
Declined: "Refusé", Declined: "Refusé",
Tentative: "Accepté provisoirement", Tentative: "Accepté provisoirement",
Accept: "Accepter", Accept: "Accepter",
Decline: "Refuser", Decline: "Refuser",
Tentatively_accept: "Accepter provisoirement", Tentatively_accept: "Accepter provisoirement",
Set_pending: "Ne pas répondre", Set_pending: "Ne pas répondre",
},
}, },
},
}; };
export default defineComponent({ export default defineComponent({
name: "Answer", name: "Answer",
i18n, i18n,
props: { props: {
calendarId: { type: Number, required: true }, calendarId: { type: Number, required: true },
status: { status: {
type: String as PropType< type: String as PropType<
"accepted" | "declined" | "pending" | "tentative" "accepted" | "declined" | "pending" | "tentative"
>, >,
required: true, required: true,
},
}, },
}, emits: {
emits: { statusChanged(
statusChanged(payload: "accepted" | "declined" | "pending" | "tentative") { payload: "accepted" | "declined" | "pending" | "tentative",
return true; ) {
return true;
},
}, },
}, data() {
data() { return {
return { Statuses: {
Statuses: { ACCEPTED,
ACCEPTED, DECLINED,
DECLINED, PENDING,
PENDING, TENTATIVELY_ACCEPTED,
TENTATIVELY_ACCEPTED, },
}, };
}; },
}, methods: {
methods: { changeStatus: function (
changeStatus: function ( newStatus: "accepted" | "declined" | "pending" | "tentative",
newStatus: "accepted" | "declined" | "pending" | "tentative", ) {
) { console.log("changeStatus", newStatus);
console.log("changeStatus", newStatus); const url = `/api/1.0/calendar/calendar/${this.$props.calendarId}/answer/${newStatus}.json`;
const url = `/api/1.0/calendar/calendar/${this.$props.calendarId}/answer/${newStatus}.json`; window
window .fetch(url, {
.fetch(url, { method: "POST",
method: "POST", })
}) .then((r: Response) => {
.then((r: Response) => { if (!r.ok) {
if (!r.ok) { console.error("could not confirm answer", newStatus);
console.error("could not confirm answer", newStatus); return;
return; }
} console.log("answer sent", newStatus);
console.log("answer sent", newStatus); this.$emit("statusChanged", newStatus);
this.$emit("statusChanged", newStatus); });
}); },
}, },
},
}); });
</script> </script>

View File

@@ -1,177 +1,225 @@
<template> <template>
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
<label class="form-label">{{ $t("created_availabilities") }}</label> <label class="form-label">{{ $t("created_availabilities") }}</label>
<vue-multiselect <vue-multiselect
v-model="pickedLocation" v-model="pickedLocation"
:options="locations" :options="locations"
:label="'name'" :label="'name'"
:track-by="'id'" :track-by="'id'"
:selectLabel="'Presser \'Entrée\' pour choisir'" :selectLabel="'Presser \'Entrée\' pour choisir'"
:selectedLabel="'Choisir'" :selectedLabel="'Choisir'"
:deselectLabel="'Presser \'Entrée\' pour enlever'" :deselectLabel="'Presser \'Entrée\' pour enlever'"
:placeholder="'Choisir'" :placeholder="'Choisir'"
></vue-multiselect> ></vue-multiselect>
</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>
<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>
</div> <div
<FullCalendar :options="calendarOptions" ref="calendarRef"> class="display-options row justify-content-between"
<template v-slot:eventContent="{ event }: { event: EventApi }"> style="margin-top: 1rem"
<span :class="eventClasses"> >
<b v-if="event.extendedProps.is === 'remote'">{{ event.title }}</b> <div class="col-sm-9 col-xs-12">
<b v-else-if="event.extendedProps.is === 'range'" <div class="input-group mb-3">
>{{ formatDate(event.startStr) }} - <label class="input-group-text" for="slotDuration"
{{ event.extendedProps.locationName }}</b >Durée des créneaux</label
> >
<b v-else-if="event.extendedProps.is === 'local'">{{ event.title }}</b> <select
<b v-else>no 'is'</b> v-model="slotDuration"
<a id="slotDuration"
v-if="event.extendedProps.is === 'range'" class="form-select"
class="fa fa-fw fa-times delete" >
@click.prevent="onClickDelete(event)" <option value="00:05:00">5 minutes</option>
> <option value="00:10:00">10 minutes</option>
</a> <option value="00:15:00">15 minutes</option>
</span> <option value="00:30:00">30 minutes</option>
</template> </select>
</FullCalendar> <label class="input-group-text" for="slotMinTime">De</label>
<select
<div id="copy-widget"> v-model="slotMinTime"
<div class="container mt-2 mb-2"> id="slotMinTime"
<div class="row justify-content-between align-items-center mb-4"> class="form-select"
<div class="col-xs-12 col-sm-3 col-md-2"> >
<h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6> <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>
<div class="col-xs-12 col-sm-9 col-md-2"> <div class="col-xs-12 col-sm-3">
<select v-model="dayOrWeek" id="dayOrWeek" class="form-select"> <div class="float-end">
<option value="day">{{ $t("from_day_to_day") }}</option> <div class="form-check input-group">
<option value="week"> <span class="input-group-text">
{{ $t("from_week_to_week") }} <input
</option> id="showHideWE"
</select> 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>
<template v-if="dayOrWeek === 'day'"> </div>
<div class="col-xs-12 col-sm-3 col-md-3"> <FullCalendar :options="calendarOptions" ref="calendarRef">
<input class="form-control" type="date" v-model="copyFrom" /> <template v-slot:eventContent="{ event }: { event: EventApi }">
</div> <span :class="eventClasses">
<div class="col-xs-12 col-sm-1 col-md-1 copy-chevron"> <b v-if="event.extendedProps.is === 'remote'">{{
<i class="fa fa-angle-double-right"></i> event.title
</div> }}</b>
<div class="col-xs-12 col-sm-3 col-md-3"> <b v-else-if="event.extendedProps.is === 'range'"
<input class="form-control" type="date" v-model="copyTo" /> >{{ formatDate(event.startStr) }} -
</div> {{ event.extendedProps.locationName }}</b
<div class="col-xs-12 col-sm-5 col-md-1"> >
<button class="btn btn-action float-end" @click="copyDay"> <b v-else-if="event.extendedProps.is === 'local'">{{
{{ $t("copy_range") }} event.title
</button> }}</b>
</div> <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> </template>
<template v-else> </FullCalendar>
<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 --> <div id="copy-widget">
<edit-location ref="editLocation"></edit-location> <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>
</div>
<!-- not directly seen, but include in a modal -->
<edit-location ref="editLocation"></edit-location>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { import type {
CalendarOptions, CalendarOptions,
DatesSetArg, DatesSetArg,
EventInput, EventInput,
} from "@fullcalendar/core"; } from "@fullcalendar/core";
import { computed, ref, onMounted } from "vue"; import { computed, ref, onMounted } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
@@ -179,14 +227,14 @@ import { key } from "./store";
import FullCalendar from "@fullcalendar/vue3"; import FullCalendar from "@fullcalendar/vue3";
import frLocale from "@fullcalendar/core/locales/fr"; import frLocale from "@fullcalendar/core/locales/fr";
import interactionPlugin, { import interactionPlugin, {
EventResizeDoneArg, EventResizeDoneArg,
} from "@fullcalendar/interaction"; } from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid"; import timeGridPlugin from "@fullcalendar/timegrid";
import { import {
EventApi, EventApi,
DateSelectArg, DateSelectArg,
EventDropArg, EventDropArg,
EventClickArg, EventClickArg,
} from "@fullcalendar/core"; } from "@fullcalendar/core";
import { dateToISO, ISOToDate } from "ChillMainAssets/chill/js/date"; import { dateToISO, ISOToDate } from "ChillMainAssets/chill/js/date";
import VueMultiselect from "vue-multiselect"; import VueMultiselect from "vue-multiselect";
@@ -207,96 +255,96 @@ const copyFromWeek = ref<string | null>(null);
const copyToWeek = ref<string | null>(null); const copyToWeek = ref<string | null>(null);
interface Weeks { interface Weeks {
value: string | null; value: string | null;
text: string; text: string;
} }
const getMonday = (week: number): Date => { const getMonday = (week: number): Date => {
const lastMonday = new Date(); const lastMonday = new Date();
lastMonday.setDate( lastMonday.setDate(
lastMonday.getDate() - ((lastMonday.getDay() + 6) % 7) + week * 7, lastMonday.getDate() - ((lastMonday.getDay() + 6) % 7) + week * 7,
); );
return lastMonday; return lastMonday;
}; };
const dateOptions: Intl.DateTimeFormatOptions = { const dateOptions: Intl.DateTimeFormatOptions = {
weekday: "long", weekday: "long",
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
}; };
const lastWeeks = computed((): Weeks[] => const lastWeeks = computed((): Weeks[] =>
Array.from(Array(30).keys()).map((w) => { Array.from(Array(30).keys()).map((w) => {
const lastMonday = getMonday(15 - w); const lastMonday = getMonday(15 - w);
return { return {
value: dateToISO(lastMonday), value: dateToISO(lastMonday),
text: `Semaine du ${lastMonday.toLocaleDateString("fr-FR", dateOptions)}`, text: `Semaine du ${lastMonday.toLocaleDateString("fr-FR", dateOptions)}`,
}; };
}), }),
); );
const nextWeeks = computed((): Weeks[] => const nextWeeks = computed((): Weeks[] =>
Array.from(Array(52).keys()).map((w) => { Array.from(Array(52).keys()).map((w) => {
const nextMonday = getMonday(w + 1); const nextMonday = getMonday(w + 1);
return { return {
value: dateToISO(nextMonday), value: dateToISO(nextMonday),
text: `Semaine du ${nextMonday.toLocaleDateString("fr-FR", dateOptions)}`, text: `Semaine du ${nextMonday.toLocaleDateString("fr-FR", dateOptions)}`,
}; };
}), }),
); );
const formatDate = (datetime: string) => { const formatDate = (datetime: string) => {
console.log(typeof datetime); console.log(typeof datetime);
return ISOToDate(datetime); return ISOToDate(datetime);
}; };
const baseOptions = ref<CalendarOptions>({ const baseOptions = ref<CalendarOptions>({
locale: frLocale, locale: frLocale,
plugins: [interactionPlugin, timeGridPlugin], plugins: [interactionPlugin, timeGridPlugin],
initialView: "timeGridWeek", initialView: "timeGridWeek",
initialDate: new Date(), initialDate: new Date(),
scrollTimeReset: false, scrollTimeReset: false,
selectable: true, selectable: true,
// when the dates are changes in the fullcalendar view OR when new events are added // when the dates are changes in the fullcalendar view OR when new events are added
datesSet: onDatesSet, datesSet: onDatesSet,
// when a date is selected // when a date is selected
select: onDateSelect, select: onDateSelect,
// when a event is resized // when a event is resized
eventResize: onEventDropOrResize, eventResize: onEventDropOrResize,
// when an event is moved // when an event is moved
eventDrop: onEventDropOrResize, eventDrop: onEventDropOrResize,
// when an event si clicked // when an event si clicked
eventClick: onEventClick, eventClick: onEventClick,
selectMirror: false, selectMirror: false,
editable: true, editable: true,
headerToolbar: { headerToolbar: {
left: "prev,next today", left: "prev,next today",
center: "title", center: "title",
right: "timeGridWeek,timeGridDay", right: "timeGridWeek,timeGridDay",
}, },
}); });
const ranges = computed<EventInput[]>(() => { const ranges = computed<EventInput[]>(() => {
return store.state.calendarRanges.ranges; return store.state.calendarRanges.ranges;
}); });
const locations = computed<Location[]>(() => { const locations = computed<Location[]>(() => {
return store.state.locations.locations; return store.state.locations.locations;
}); });
const pickedLocation = computed<Location | null>({ const pickedLocation = computed<Location | null>({
get(): Location | null { get(): Location | null {
return ( return (
store.state.locations.locationPicked || store.state.locations.locationPicked ||
store.state.locations.currentLocation store.state.locations.currentLocation
); );
}, },
set(newLocation: Location | null): void { set(newLocation: Location | null): void {
store.commit("locations/setLocationPicked", newLocation, { store.commit("locations/setLocationPicked", newLocation, {
root: true, root: true,
}); });
}, },
}); });
/** /**
@@ -325,116 +373,116 @@ const sources = computed<EventSourceInput[]>(() => {
*/ */
const calendarOptions = computed((): CalendarOptions => { const calendarOptions = computed((): CalendarOptions => {
return { return {
...baseOptions.value, ...baseOptions.value,
weekends: showWeekends.value, weekends: showWeekends.value,
slotDuration: slotDuration.value, slotDuration: slotDuration.value,
events: ranges.value, events: ranges.value,
slotMinTime: slotMinTime.value, slotMinTime: slotMinTime.value,
slotMaxTime: slotMaxTime.value, slotMaxTime: slotMaxTime.value,
}; };
}); });
/** /**
* launched when the calendar range date change * launched when the calendar range date change
*/ */
function onDatesSet(event: DatesSetArg): void { function onDatesSet(event: DatesSetArg): void {
store.dispatch("fullCalendar/setCurrentDatesView", { store.dispatch("fullCalendar/setCurrentDatesView", {
start: event.start, start: event.start,
end: event.end, end: event.end,
}); });
} }
function onDateSelect(event: DateSelectArg): void { function onDateSelect(event: DateSelectArg): void {
if (null === pickedLocation.value) { if (null === pickedLocation.value) {
window.alert( window.alert(
"Indiquez une localisation avant de créer une période de disponibilité.", "Indiquez une localisation avant de créer une période de disponibilité.",
); );
return; return;
} }
store.dispatch("calendarRanges/createRange", { store.dispatch("calendarRanges/createRange", {
start: event.start, start: event.start,
end: event.end, end: event.end,
location: pickedLocation.value, location: pickedLocation.value,
}); });
} }
/** /**
* When a calendar range is deleted * When a calendar range is deleted
*/ */
function onClickDelete(event: EventApi): void { function onClickDelete(event: EventApi): void {
if (event.extendedProps.is !== "range") { if (event.extendedProps.is !== "range") {
return; return;
} }
store.dispatch( store.dispatch(
"calendarRanges/deleteRange", "calendarRanges/deleteRange",
event.extendedProps.calendarRangeId, event.extendedProps.calendarRangeId,
); );
} }
function onEventDropOrResize(payload: EventDropArg | EventResizeDoneArg) { function onEventDropOrResize(payload: EventDropArg | EventResizeDoneArg) {
if (payload.event.extendedProps.is !== "range") { if (payload.event.extendedProps.is !== "range") {
return; return;
} }
store.dispatch("calendarRanges/patchRangeTime", { store.dispatch("calendarRanges/patchRangeTime", {
calendarRangeId: payload.event.extendedProps.calendarRangeId, calendarRangeId: payload.event.extendedProps.calendarRangeId,
start: payload.event.start, start: payload.event.start,
end: payload.event.end, end: payload.event.end,
}); });
} }
function onEventClick(payload: EventClickArg): void { function onEventClick(payload: EventClickArg): void {
// @ts-ignore TS does not recognize the target. But it does exists. // @ts-ignore TS does not recognize the target. But it does exists.
if (payload.jsEvent.target.classList.contains("delete")) { if (payload.jsEvent.target.classList.contains("delete")) {
return; return;
} }
if (payload.event.extendedProps.is !== "range") { if (payload.event.extendedProps.is !== "range") {
return; return;
} }
editLocation.value?.startEdit(payload.event); editLocation.value?.startEdit(payload.event);
} }
function copyDay() { function copyDay() {
if (null === copyFrom.value || null === copyTo.value) { if (null === copyFrom.value || null === copyTo.value) {
return; return;
} }
store.dispatch("calendarRanges/copyFromDayToAnotherDay", { store.dispatch("calendarRanges/copyFromDayToAnotherDay", {
from: ISOToDate(copyFrom.value), from: ISOToDate(copyFrom.value),
to: ISOToDate(copyTo.value), to: ISOToDate(copyTo.value),
}); });
} }
function copyWeek() { function copyWeek() {
if (null === copyFromWeek.value || null === copyToWeek.value) { if (null === copyFromWeek.value || null === copyToWeek.value) {
return; return;
} }
store.dispatch("calendarRanges/copyFromWeekToAnotherWeek", { store.dispatch("calendarRanges/copyFromWeekToAnotherWeek", {
fromMonday: ISOToDate(copyFromWeek.value), fromMonday: ISOToDate(copyFromWeek.value),
toMonday: ISOToDate(copyToWeek.value), toMonday: ISOToDate(copyToWeek.value),
}); });
} }
onMounted(() => { onMounted(() => {
copyFromWeek.value = dateToISO(getMonday(0)); copyFromWeek.value = dateToISO(getMonday(0));
copyToWeek.value = dateToISO(getMonday(1)); copyToWeek.value = dateToISO(getMonday(1));
}); });
</script> </script>
<style scoped> <style scoped>
#copy-widget { #copy-widget {
position: sticky; position: sticky;
bottom: 0px; bottom: 0px;
background-color: white; background-color: white;
z-index: 9999999999; z-index: 9999999999;
padding: 0.25rem 0 0.25rem; padding: 0.25rem 0 0.25rem;
} }
div.copy-chevron { div.copy-chevron {
text-align: center; text-align: center;
font-size: x-large; font-size: x-large;
width: 2rem; width: 2rem;
} }
</style> </style>

View File

@@ -1,28 +1,28 @@
<template> <template>
<component :is="Teleport" to="body"> <component :is="Teleport" to="body">
<modal v-if="showModal" @close="closeModal"> <modal v-if="showModal" @close="closeModal">
<template v-slot:header> <template v-slot:header>
<h3>{{ "Modifier le lieu" }}</h3> <h3>{{ "Modifier le lieu" }}</h3>
</template> </template>
<template v-slot:body> <template v-slot:body>
<div></div> <div></div>
<label>Localisation</label> <label>Localisation</label>
<vue-multiselect <vue-multiselect
v-model="location" v-model="location"
:options="locations" :options="locations"
:label="'name'" :label="'name'"
:track-by="'id'" :track-by="'id'"
></vue-multiselect> ></vue-multiselect>
</template> </template>
<template v-slot:footer> <template v-slot:footer>
<button class="btn btn-save" @click="saveAndClose"> <button class="btn btn-save" @click="saveAndClose">
{{ "Enregistrer" }} {{ "Enregistrer" }}
</button> </button>
</template> </template>
</modal> </modal>
</component> </component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -39,7 +39,7 @@ import VueMultiselect from "vue-multiselect";
import { Teleport as teleport_, TeleportProps, VNodeProps } from "vue"; import { Teleport as teleport_, TeleportProps, VNodeProps } from "vue";
const Teleport = teleport_ as new () => { const Teleport = teleport_ as new () => {
$props: VNodeProps & TeleportProps; $props: VNodeProps & TeleportProps;
}; };
const store = useStore(key); const store = useStore(key);
@@ -50,37 +50,37 @@ const showModal = ref(false);
//const tele = ref<InstanceType<typeof Teleport> | null>(null); //const tele = ref<InstanceType<typeof Teleport> | null>(null);
const locations = computed<Location[]>(() => { const locations = computed<Location[]>(() => {
return store.state.locations.locations; return store.state.locations.locations;
}); });
const startEdit = function (event: EventApi): void { const startEdit = function (event: EventApi): void {
console.log("startEditing", event); console.log("startEditing", event);
calendarRangeId.value = event.extendedProps.calendarRangeId; calendarRangeId.value = event.extendedProps.calendarRangeId;
location.value = location.value =
store.getters["locations/getLocationById"]( store.getters["locations/getLocationById"](
event.extendedProps.locationId, event.extendedProps.locationId,
) || null; ) || null;
console.log("new location value", location.value); console.log("new location value", location.value);
console.log("calendar range id", calendarRangeId.value); console.log("calendar range id", calendarRangeId.value);
showModal.value = true; showModal.value = true;
}; };
const saveAndClose = function (e: Event): void { const saveAndClose = function (e: Event): void {
console.log("saveEditAndClose", e); console.log("saveEditAndClose", e);
store store
.dispatch("calendarRanges/patchRangeLocation", { .dispatch("calendarRanges/patchRangeLocation", {
location: location.value, location: location.value,
calendarRangeId: calendarRangeId.value, calendarRangeId: calendarRangeId.value,
}) })
.then((_) => { .then((_) => {
showModal.value = false; showModal.value = false;
}); });
}; };
const closeModal = function (_: any): void { const closeModal = function (_: any): void {
showModal.value = false; showModal.value = false;
}; };
defineExpose({ startEdit }); defineExpose({ startEdit });

View File

@@ -1,27 +1,27 @@
const appMessages = { const appMessages = {
fr: { fr: {
created_availabilities: "Lieu des plages de disponibilités créées", created_availabilities: "Lieu des plages de disponibilités créées",
edit_your_calendar_range: "Planifiez vos plages de disponibilités", edit_your_calendar_range: "Planifiez vos plages de disponibilités",
show_my_calendar: "Afficher mon calendrier", show_my_calendar: "Afficher mon calendrier",
show_weekends: "Afficher les week-ends", show_weekends: "Afficher les week-ends",
copy_range: "Copier", copy_range: "Copier",
copy_range_from_to: "Copier les plages", copy_range_from_to: "Copier les plages",
from_day_to_day: "d'un jour à l'autre", from_day_to_day: "d'un jour à l'autre",
from_week_to_week: "d'une semaine à l'autre", from_week_to_week: "d'une semaine à l'autre",
copy_range_how_to: 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.", "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", new_range_to_save: "Nouvelles plages à enregistrer",
update_range_to_save: "Plages à modifier", update_range_to_save: "Plages à modifier",
delete_range_to_save: "Plages à supprimer", delete_range_to_save: "Plages à supprimer",
by: "Par", by: "Par",
main_user_concerned: "Utilisateur concerné", main_user_concerned: "Utilisateur concerné",
dateFrom: "De", dateFrom: "De",
dateTo: "à", dateTo: "à",
day: "Jour", day: "Jour",
week: "Semaine", week: "Semaine",
month: "Mois", month: "Mois",
today: "Aujourd'hui", today: "Aujourd'hui",
}, },
}; };
export { appMessages }; export { appMessages };

View File

@@ -7,13 +7,13 @@ import App2 from "./App2.vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
futureStore().then((store) => { futureStore().then((store) => {
const i18n = _createI18n(appMessages, false); const i18n = _createI18n(appMessages, false);
const app = createApp({ const app = createApp({
template: `<app></app>`, template: `<app></app>`,
}) })
.use(store, key) .use(store, key)
.use(i18n) .use(i18n)
.component("app", App2) .component("app", App2)
.mount("#myCalendar"); .mount("#myCalendar");
}); });

View File

@@ -5,7 +5,7 @@ import me, { MeState } from "./modules/me";
import fullCalendar, { FullCalendarState } from "./modules/fullcalendar"; import fullCalendar, { FullCalendarState } from "./modules/fullcalendar";
import calendarRanges, { CalendarRangesState } from "./modules/calendarRanges"; import calendarRanges, { CalendarRangesState } from "./modules/calendarRanges";
import calendarRemotes, { import calendarRemotes, {
CalendarRemotesState, CalendarRemotesState,
} from "./modules/calendarRemotes"; } from "./modules/calendarRemotes";
import { whoami } from "../../../../../../ChillMainBundle/Resources/public/lib/api/user"; import { whoami } from "../../../../../../ChillMainBundle/Resources/public/lib/api/user";
import { User } from "../../../../../../ChillMainBundle/Resources/public/types"; import { User } from "../../../../../../ChillMainBundle/Resources/public/types";
@@ -15,40 +15,42 @@ import calendarLocals, { CalendarLocalsState } from "./modules/calendarLocals";
const debug = process.env.NODE_ENV !== "production"; const debug = process.env.NODE_ENV !== "production";
export interface State { export interface State {
calendarRanges: CalendarRangesState; calendarRanges: CalendarRangesState;
calendarRemotes: CalendarRemotesState; calendarRemotes: CalendarRemotesState;
calendarLocals: CalendarLocalsState; calendarLocals: CalendarLocalsState;
fullCalendar: FullCalendarState; fullCalendar: FullCalendarState;
me: MeState; me: MeState;
locations: LocationState; locations: LocationState;
} }
export const key: InjectionKey<Store<State>> = Symbol(); export const key: InjectionKey<Store<State>> = Symbol();
const futureStore = function (): Promise<Store<State>> { const futureStore = function (): Promise<Store<State>> {
return whoami().then((user: User) => { return whoami().then((user: User) => {
const store = createStore<State>({ const store = createStore<State>({
strict: debug, strict: debug,
modules: { modules: {
me, me,
fullCalendar, fullCalendar,
calendarRanges, calendarRanges,
calendarRemotes, calendarRemotes,
calendarLocals, calendarLocals,
locations, locations,
}, },
mutations: {}, mutations: {},
}); });
store.commit("me/setWhoAmi", user, { root: true }); store.commit("me/setWhoAmi", user, { root: true });
store.dispatch("locations/getLocations", null, { root: true }).then((_) => { store
return store.dispatch("locations/getCurrentLocation", null, { .dispatch("locations/getLocations", null, { root: true })
root: true, .then((_) => {
}); return store.dispatch("locations/getCurrentLocation", null, {
}); root: true,
});
});
return Promise.resolve(store); return Promise.resolve(store);
}); });
}; };
export default futureStore; export default futureStore;

View File

@@ -8,99 +8,109 @@ import { TransportExceptionInterface } from "../../../../../../../ChillMainBundl
import { COLORS } from "../../../Calendar/const"; import { COLORS } from "../../../Calendar/const";
export interface CalendarLocalsState { export interface CalendarLocalsState {
locals: EventInput[]; locals: EventInput[];
localsLoaded: { start: number; end: number }[]; localsLoaded: { start: number; end: number }[];
localsIndex: Set<string>; localsIndex: Set<string>;
key: number; key: number;
} }
type Context = ActionContext<CalendarLocalsState, State>; type Context = ActionContext<CalendarLocalsState, State>;
export default { export default {
namespaced: true, namespaced: true,
state: (): CalendarLocalsState => ({ state: (): CalendarLocalsState => ({
locals: [], locals: [],
localsLoaded: [], localsLoaded: [],
localsIndex: new Set<string>(), localsIndex: new Set<string>(),
key: 0, key: 0,
}), }),
getters: { getters: {
isLocalsLoaded: isLocalsLoaded:
(state: CalendarLocalsState) => (state: CalendarLocalsState) =>
({ start, end }: { start: Date; end: Date }): boolean => { ({ start, end }: { start: Date; end: Date }): boolean => {
for (const range of state.localsLoaded) { for (const range of state.localsLoaded) {
if (start.getTime() === range.start && end.getTime() === range.end) { if (
return true; 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;
}, },
addLoaded(state: CalendarLocalsState, payload: { start: Date; end: Date }) { mutations: {
state.localsLoaded.push({ addLocals(state: CalendarLocalsState, ranges: CalendarLight[]) {
start: payload.start.getTime(), console.log("addLocals", ranges);
end: payload.end.getTime(),
}); 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(),
});
},
}, },
}, actions: {
actions: { fetchLocals(
fetchLocals( ctx: Context,
ctx: Context, payload: { start: Date; end: Date },
payload: { start: Date; end: Date }, ): Promise<null> {
): Promise<null> { const start = payload.start;
const start = payload.start; const end = payload.end;
const end = payload.end;
if (ctx.rootGetters["me/getMe"] === null) { if (ctx.rootGetters["me/getMe"] === null) {
return Promise.resolve(null); return Promise.resolve(null);
} }
if (ctx.getters.isLocalsLoaded({ start, end })) { if (ctx.getters.isLocalsLoaded({ start, end })) {
return Promise.resolve(ctx.getters.getRangeSource); return Promise.resolve(ctx.getters.getRangeSource);
} }
ctx.commit("addLoaded", { ctx.commit("addLoaded", {
start: start, start: start,
end: end, end: end,
}); });
return fetchCalendarLocalForUser(ctx.rootGetters["me/getMe"], start, end) return fetchCalendarLocalForUser(
.then((remotes: CalendarLight[]) => { ctx.rootGetters["me/getMe"],
// to be add when reactivity problem will be solve ? start,
//ctx.commit('addRemotes', remotes); end,
const inputs = remotes )
.map((cr) => localsToFullCalendarEvent(cr)) .then((remotes: CalendarLight[]) => {
.map((cr) => ({ // to be add when reactivity problem will be solve ?
...cr, //ctx.commit('addRemotes', remotes);
backgroundColor: COLORS[0], const inputs = remotes
textColor: "black", .map((cr) => localsToFullCalendarEvent(cr))
editable: false, .map((cr) => ({
})); ...cr,
ctx.commit("calendarRanges/addExternals", inputs, { backgroundColor: COLORS[0],
root: true, textColor: "black",
}); editable: false,
return Promise.resolve(null); }));
}) ctx.commit("calendarRanges/addExternals", inputs, {
.catch((e: TransportExceptionInterface) => { root: true,
console.error(e); });
return Promise.resolve(null);
})
.catch((e: TransportExceptionInterface) => {
console.error(e);
return Promise.resolve(null); return Promise.resolve(null);
}); });
},
}, },
},
} as Module<CalendarLocalsState, State>; } as Module<CalendarLocalsState, State>;

View File

@@ -1,10 +1,10 @@
import { State } from "./../index"; import { State } from "./../index";
import { ActionContext, Module } from "vuex"; import { ActionContext, Module } from "vuex";
import { import {
CalendarRange, CalendarRange,
CalendarRangeCreate, CalendarRangeCreate,
CalendarRangeEdit, CalendarRangeEdit,
isEventInputCalendarRange, isEventInputCalendarRange,
} from "../../../../types"; } from "../../../../types";
import { Location } from "../../../../../../../ChillMainBundle/Resources/public/types"; import { Location } from "../../../../../../../ChillMainBundle/Resources/public/types";
import { fetchCalendarRangeForUser } from "../../../Calendar/api"; import { fetchCalendarRangeForUser } from "../../../Calendar/api";
@@ -12,332 +12,369 @@ import { calendarRangeToFullCalendarEvent } from "../../../Calendar/store/utils"
import { EventInput } from "@fullcalendar/core"; import { EventInput } from "@fullcalendar/core";
import { makeFetch } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods"; import { makeFetch } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import { import {
datetimeToISO, datetimeToISO,
dateToISO, dateToISO,
ISOToDatetime, ISOToDatetime,
} from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date"; } from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
import type { EventInputCalendarRange } from "../../../../types"; import type { EventInputCalendarRange } from "../../../../types";
export interface CalendarRangesState { export interface CalendarRangesState {
ranges: (EventInput | EventInputCalendarRange)[]; ranges: (EventInput | EventInputCalendarRange)[];
rangesLoaded: { start: number; end: number }[]; rangesLoaded: { start: number; end: number }[];
rangesIndex: Set<string>; rangesIndex: Set<string>;
key: number; key: number;
} }
type Context = ActionContext<CalendarRangesState, State>; type Context = ActionContext<CalendarRangesState, State>;
export default { export default {
namespaced: true, namespaced: true,
state: (): CalendarRangesState => ({ state: (): CalendarRangesState => ({
ranges: [], ranges: [],
rangesLoaded: [], rangesLoaded: [],
rangesIndex: new Set<string>(), rangesIndex: new Set<string>(),
key: 0, key: 0,
}), }),
getters: { getters: {
isRangeLoaded: isRangeLoaded:
(state: CalendarRangesState) => (state: CalendarRangesState) =>
({ start, end }: { start: Date; end: Date }): boolean => { ({ start, end }: { start: Date; end: Date }): boolean => {
for (const range of state.rangesLoaded) { for (const range of state.rangesLoaded) {
if (start.getTime() === range.start && end.getTime() === range.end) { if (
return true; start.getTime() === range.start &&
} end.getTime() === range.end
} ) {
return true;
}
}
return false; return false;
}, },
getRangesOnDate: getRangesOnDate:
(state: CalendarRangesState) => (state: CalendarRangesState) =>
(date: Date): EventInputCalendarRange[] => { (date: Date): EventInputCalendarRange[] => {
const founds = []; const founds = [];
const dateStr = dateToISO(date) as string; const dateStr = dateToISO(date) as string;
for (const range of state.ranges) { for (const range of state.ranges) {
if ( if (
isEventInputCalendarRange(range) && isEventInputCalendarRange(range) &&
range.start.startsWith(dateStr) range.start.startsWith(dateStr)
) { ) {
founds.push(range); founds.push(range);
} }
} }
return founds; return founds;
}, },
getRangesOnWeek: getRangesOnWeek:
(state: CalendarRangesState) => (state: CalendarRangesState) =>
(mondayDate: Date): EventInputCalendarRange[] => { (mondayDate: Date): EventInputCalendarRange[] => {
const founds = []; const founds = [];
for (const d of Array.from(Array(7).keys())) { for (const d of Array.from(Array(7).keys())) {
const dateOfWeek = new Date(mondayDate); const dateOfWeek = new Date(mondayDate);
dateOfWeek.setDate(mondayDate.getDate() + d); dateOfWeek.setDate(mondayDate.getDate() + d);
const dateStr = dateToISO(dateOfWeek) as string; const dateStr = dateToISO(dateOfWeek) as string;
for (const range of state.ranges) { for (const range of state.ranges) {
if ( if (
isEventInputCalendarRange(range) && isEventInputCalendarRange(range) &&
range.start.startsWith(dateStr) range.start.startsWith(dateStr)
) { ) {
founds.push(range); founds.push(range);
}
}
}
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;
},
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;
} }
}
}
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;
},
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;
},
},
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: { updateRange(state: CalendarRangesState, range: CalendarRange) {
datetime: datetimeToISO(start), 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;
}, },
endDate: { },
datetime: datetimeToISO(end), 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);
});
}, },
location: { createRange(
id: location.id, ctx: Context,
type: "location", {
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;
});
}, },
} as CalendarRangeCreate; deleteRange(ctx: Context, calendarRangeId: number) {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
return makeFetch<CalendarRangeCreate, CalendarRange>("POST", url, body) makeFetch<undefined, never>("DELETE", url).then(() => {
.then((newRange) => { ctx.commit("removeRange", calendarRangeId);
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: { patchRangeTime(
datetime: datetimeToISO(end), 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);
});
}, },
} as CalendarRangeEdit; 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) return makeFetch<CalendarRangeEdit, CalendarRange>(
.then((range) => { "PATCH",
ctx.commit("updateRange", range); url,
return Promise.resolve(null); body,
}) )
.catch((error) => { .then((range) => {
console.error(error); ctx.commit("updateRange", range);
return Promise.resolve(null); return Promise.resolve(null);
}); })
}, .catch((error) => {
patchRangeLocation( console.error(error);
ctx, return Promise.resolve(null);
{ });
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; copyFromDayToAnotherDay(
ctx,
{ from, to }: { from: Date; to: Date },
): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] =
ctx.getters["getRangesOnDate"](from);
const promises = [];
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body) for (const r of rangesToCopy) {
.then((range) => { const start = new Date(ISOToDatetime(r.start) as Date);
ctx.commit("updateRange", range); start.setFullYear(
return Promise.resolve(null); to.getFullYear(),
}) to.getMonth(),
.catch((error) => { to.getDate(),
console.error(error); );
return Promise.resolve(null); 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));
},
}, },
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>; } as Module<CalendarRangesState, State>;

View File

@@ -8,102 +8,109 @@ import { TransportExceptionInterface } from "../../../../../../../ChillMainBundl
import { COLORS } from "../../../Calendar/const"; import { COLORS } from "../../../Calendar/const";
export interface CalendarRemotesState { export interface CalendarRemotesState {
remotes: EventInput[]; remotes: EventInput[];
remotesLoaded: { start: number; end: number }[]; remotesLoaded: { start: number; end: number }[];
remotesIndex: Set<string>; remotesIndex: Set<string>;
key: number; key: number;
} }
type Context = ActionContext<CalendarRemotesState, State>; type Context = ActionContext<CalendarRemotesState, State>;
export default { export default {
namespaced: true, namespaced: true,
state: (): CalendarRemotesState => ({ state: (): CalendarRemotesState => ({
remotes: [], remotes: [],
remotesLoaded: [], remotesLoaded: [],
remotesIndex: new Set<string>(), remotesIndex: new Set<string>(),
key: 0, key: 0,
}), }),
getters: { getters: {
isRemotesLoaded: isRemotesLoaded:
(state: CalendarRemotesState) => (state: CalendarRemotesState) =>
({ start, end }: { start: Date; end: Date }): boolean => { ({ start, end }: { start: Date; end: Date }): boolean => {
for (const range of state.remotesLoaded) { for (const range of state.remotesLoaded) {
if (start.getTime() === range.start && end.getTime() === range.end) { if (
return true; 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;
}, },
addLoaded( mutations: {
state: CalendarRemotesState, addRemotes(state: CalendarRemotesState, ranges: CalendarRemote[]) {
payload: { start: Date; end: Date }, console.log("addRemotes", ranges);
) {
state.remotesLoaded.push({ const toAdd = ranges
start: payload.start.getTime(), .map((cr) => remoteToFullCalendarEvent(cr))
end: payload.end.getTime(), .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(),
});
},
}, },
}, actions: {
actions: { fetchRemotes(
fetchRemotes( ctx: Context,
ctx: Context, payload: { start: Date; end: Date },
payload: { start: Date; end: Date }, ): Promise<null> {
): Promise<null> { const start = payload.start;
const start = payload.start; const end = payload.end;
const end = payload.end;
if (ctx.rootGetters["me/getMe"] === null) { if (ctx.rootGetters["me/getMe"] === null) {
return Promise.resolve(null); return Promise.resolve(null);
} }
if (ctx.getters.isRemotesLoaded({ start, end })) { if (ctx.getters.isRemotesLoaded({ start, end })) {
return Promise.resolve(ctx.getters.getRangeSource); return Promise.resolve(ctx.getters.getRangeSource);
} }
ctx.commit("addLoaded", { ctx.commit("addLoaded", {
start: start, start: start,
end: end, end: end,
}); });
return fetchCalendarRemoteForUser(ctx.rootGetters["me/getMe"], start, end) return fetchCalendarRemoteForUser(
.then((remotes: CalendarRemote[]) => { ctx.rootGetters["me/getMe"],
// to be add when reactivity problem will be solve ? start,
//ctx.commit('addRemotes', remotes); end,
const inputs = remotes )
.map((cr) => remoteToFullCalendarEvent(cr)) .then((remotes: CalendarRemote[]) => {
.map((cr) => ({ // to be add when reactivity problem will be solve ?
...cr, //ctx.commit('addRemotes', remotes);
backgroundColor: COLORS[0], const inputs = remotes
textColor: "black", .map((cr) => remoteToFullCalendarEvent(cr))
editable: false, .map((cr) => ({
})); ...cr,
ctx.commit("calendarRanges/addExternals", inputs, { backgroundColor: COLORS[0],
root: true, textColor: "black",
}); editable: false,
return Promise.resolve(null); }));
}) ctx.commit("calendarRanges/addExternals", inputs, {
.catch((e: TransportExceptionInterface) => { root: true,
console.error(e); });
return Promise.resolve(null);
})
.catch((e: TransportExceptionInterface) => {
console.error(e);
return Promise.resolve(null); return Promise.resolve(null);
}); });
},
}, },
},
} as Module<CalendarRemotesState, State>; } as Module<CalendarRemotesState, State>;

View File

@@ -2,77 +2,77 @@ import { State } from "./../index";
import { ActionContext } from "vuex"; import { ActionContext } from "vuex";
export interface FullCalendarState { export interface FullCalendarState {
currentView: { currentView: {
start: Date | null; start: Date | null;
end: Date | null; end: Date | null;
}; };
key: number; key: number;
} }
type Context = ActionContext<FullCalendarState, State>; type Context = ActionContext<FullCalendarState, State>;
export default { export default {
namespaced: true, namespaced: true,
state: (): FullCalendarState => ({ state: (): FullCalendarState => ({
currentView: { currentView: {
start: null, start: null,
end: 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;
},
}, },
key: 0, actions: {
}), setCurrentDatesView(
mutations: { ctx: Context,
setCurrentDatesView: function ( { start, end }: { start: Date | null; end: Date | null },
state: FullCalendarState, ): Promise<null> {
payload: { start: Date; end: Date }, console.log("dispatch setCurrentDatesView", { start, end });
): 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 ( if (
ctx.state.currentView.start !== start || ctx.state.currentView.start !== start ||
ctx.state.currentView.end !== end ctx.state.currentView.end !== end
) { ) {
ctx.commit("setCurrentDatesView", { start, end }); ctx.commit("setCurrentDatesView", { start, end });
} }
if (start !== null && end !== null) { if (start !== null && end !== null) {
return Promise.all([ return Promise.all([
ctx ctx
.dispatch( .dispatch(
"calendarRanges/fetchRanges", "calendarRanges/fetchRanges",
{ start, end }, { start, end },
{ root: true }, { root: true },
) )
.then((_) => Promise.resolve(null)), .then((_) => Promise.resolve(null)),
ctx ctx
.dispatch( .dispatch(
"calendarRemotes/fetchRemotes", "calendarRemotes/fetchRemotes",
{ start, end }, { start, end },
{ root: true }, { root: true },
) )
.then((_) => Promise.resolve(null)), .then((_) => Promise.resolve(null)),
ctx ctx
.dispatch( .dispatch(
"calendarLocals/fetchLocals", "calendarLocals/fetchLocals",
{ start, end }, { start, end },
{ root: true }, { root: true },
) )
.then((_) => Promise.resolve(null)), .then((_) => Promise.resolve(null)),
]).then((_) => Promise.resolve(null)); ]).then((_) => Promise.resolve(null));
} else { } else {
return Promise.resolve(null); return Promise.resolve(null);
} }
},
}, },
},
}; };

View File

@@ -5,61 +5,61 @@ import { getLocations } from "../../../../../../../ChillMainBundle/Resources/pub
import { whereami } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/user"; import { whereami } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/user";
export interface LocationState { export interface LocationState {
locations: Location[]; locations: Location[];
locationPicked: Location | null; locationPicked: Location | null;
currentLocation: Location | null; currentLocation: Location | null;
} }
export default { export default {
namespaced: true, namespaced: true,
state: (): LocationState => { state: (): LocationState => {
return { return {
locations: [], locations: [],
locationPicked: null, locationPicked: null,
currentLocation: 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;
}, },
setLocationPicked(state, location: Location | null): void { getters: {
if (null === location) { getLocationById:
state.locationPicked = null; (state) =>
return; (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;
}
state.locationPicked = state.locationPicked =
state.locations.find((l) => l.id === location.id) || null; state.locations.find((l) => l.id === location.id) || null;
}, },
setCurrentLocation(state, location: Location | null): void { setCurrentLocation(state, location: Location | null): void {
if (null === location) { if (null === location) {
state.currentLocation = null; state.currentLocation = null;
return; return;
} }
state.currentLocation = state.currentLocation =
state.locations.find((l) => l.id === location.id) || null; state.locations.find((l) => l.id === location.id) || null;
},
}, },
}, actions: {
actions: { getLocations(ctx): Promise<void> {
getLocations(ctx): Promise<void> { return getLocations().then((locations) => {
return getLocations().then((locations) => { ctx.commit("setLocations", locations);
ctx.commit("setLocations", locations); return Promise.resolve();
return Promise.resolve(); });
}); },
getCurrentLocation(ctx): Promise<void> {
return whereami().then((location) => {
ctx.commit("setCurrentLocation", location);
});
},
}, },
getCurrentLocation(ctx): Promise<void> {
return whereami().then((location) => {
ctx.commit("setCurrentLocation", location);
});
},
},
} as Module<LocationState, State>; } as Module<LocationState, State>;

View File

@@ -3,24 +3,24 @@ import { User } from "../../../../../../../ChillMainBundle/Resources/public/type
import { ActionContext } from "vuex"; import { ActionContext } from "vuex";
export interface MeState { export interface MeState {
me: User | null; me: User | null;
} }
type Context = ActionContext<MeState, State>; type Context = ActionContext<MeState, State>;
export default { export default {
namespaced: true, namespaced: true,
state: (): MeState => ({ state: (): MeState => ({
me: null, me: null,
}), }),
getters: { getters: {
getMe: function (state: MeState): User | null { getMe: function (state: MeState): User | null {
return state.me; return state.me;
},
}, },
}, mutations: {
mutations: { setWhoAmi(state: MeState, me: User) {
setWhoAmi(state: MeState, me: User) { state.me = me;
state.me = me; },
}, },
},
}; };

View File

@@ -1,51 +1,51 @@
<template> <template>
<div> <div>
<h2 class="chill-red"> <h2 class="chill-red">
{{ $t("choose_your_calendar_user") }} {{ $t("choose_your_calendar_user") }}
</h2> </h2>
<VueMultiselect <VueMultiselect
name="field" name="field"
id="calendarUserSelector" id="calendarUserSelector"
v-model="value" v-model="value"
track-by="id" track-by="id"
label="value" label="value"
:custom-label="transName" :custom-label="transName"
:placeholder="$t('select_user')" :placeholder="$t('select_user')"
:multiple="true" :multiple="true"
:close-on-select="false" :close-on-select="false"
:allow-empty="true" :allow-empty="true"
:model-value="value" :model-value="value"
:select-label="$t('multiselect.select_label')" :select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')" :deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')" :selected-label="$t('multiselect.selected_label')"
@select="selectUsers" @select="selectUsers"
@remove="unSelectUsers" @remove="unSelectUsers"
@close="coloriseSelectedValues" @close="coloriseSelectedValues"
:options="options" :options="options"
/> />
</div> </div>
<div class="form-check"> <div class="form-check">
<input <input
type="checkbox" type="checkbox"
id="myCalendar" id="myCalendar"
class="form-check-input" class="form-check-input"
v-model="showMyCalendarWidget" v-model="showMyCalendarWidget"
/> />
<label class="form-check-label" for="myCalendar">{{ <label class="form-check-label" for="myCalendar">{{
$t("show_my_calendar") $t("show_my_calendar")
}}</label> }}</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input <input
type="checkbox" type="checkbox"
id="weekends" id="weekends"
class="form-check-input" class="form-check-input"
@click="toggleWeekends" @click="toggleWeekends"
/> />
<label class="form-check-label" for="weekends">{{ <label class="form-check-label" for="weekends">{{
$t("show_weekends") $t("show_weekends")
}}</label> }}</label>
</div> </div>
</template> </template>
<script> <script>
import { fetchCalendarRanges, fetchCalendar } from "../../_api/api"; import { fetchCalendarRanges, fetchCalendar } from "../../_api/api";
@@ -53,183 +53,206 @@ import VueMultiselect from "vue-multiselect";
import { whoami } from "ChillPersonAssets/vuejs/AccompanyingCourse/api"; import { whoami } from "ChillPersonAssets/vuejs/AccompanyingCourse/api";
const COLORS = [ const COLORS = [
/* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */ /* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
"#8dd3c7", "#8dd3c7",
"#ffffb3", "#ffffb3",
"#bebada", "#bebada",
"#fb8072", "#fb8072",
"#80b1d3", "#80b1d3",
"#fdb462", "#fdb462",
"#b3de69", "#b3de69",
"#fccde5", "#fccde5",
"#d9d9d9", "#d9d9d9",
"#bc80bd", "#bc80bd",
"#ccebc5", "#ccebc5",
"#ffed6f", "#ffed6f",
]; ];
export default { export default {
name: "CalendarUserSelector", name: "CalendarUserSelector",
components: { VueMultiselect }, components: { VueMultiselect },
props: [ props: [
"users", "users",
"updateEventsSource", "updateEventsSource",
"calendarEvents", "calendarEvents",
"showMyCalendar", "showMyCalendar",
"toggleMyCalendar", "toggleMyCalendar",
"toggleWeekends", "toggleWeekends",
], ],
data() { data() {
return { return {
errorMsg: [], errorMsg: [],
value: [], value: [],
options: [], options: [],
}; };
},
computed: {
showMyCalendarWidget: {
set(value) {
this.toggleMyCalendar(value);
this.updateEventsSource();
},
get() {
return this.showMyCalendar;
},
}, },
}, computed: {
methods: { showMyCalendarWidget: {
init() { set(value) {
this.fetchData(); this.toggleMyCalendar(value);
this.updateEventsSource();
},
get() {
return this.showMyCalendar;
},
},
}, },
fetchData() { methods: {
fetchCalendarRanges() init() {
.then( this.fetchData();
(calendarRanges) => },
new Promise((resolve, reject) => { fetchData() {
let results = calendarRanges.results; fetchCalendarRanges()
.then(
let users = []; (calendarRanges) =>
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) => { new Promise((resolve, reject) => {
let results = calendar.results; let results = calendarRanges.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); let users = [];
resolve(); 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];
resolve(); if (tags.hasChildNodes()) {
}), let children = tags.childNodes;
); for (let i = 0; i < children.length; i++) {
let child = children[i];
resolve(); if (child.nodeType === Node.ELEMENT_NODE) {
}), this.users.selected.forEach((u) => {
) if (child.hasChildNodes()) {
.catch((error) => { if (child.firstChild.innerText == u.username) {
this.errorMsg.push(error.message); child.style.background = u.color;
}); child.firstChild.style.color = "#444444";
}, }
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();
},
}, },
selectEvents() { mounted() {
let selectedUsersId = this.users.selected.map((a) => a.id); this.init();
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> </script>

View File

@@ -1,29 +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\CustomFieldsBundle\EntityRepository;
use Chill\CustomFieldsBundle\Entity\CustomFieldsDefaultGroup;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class CustomFieldsDefaultGroupRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, CustomFieldsDefaultGroup::class);
}
public function findOneByEntity(string $className): ?CustomFieldsDefaultGroup
{
return $this->findOneBy(['entity' => $className]);
}
}

View File

@@ -127,7 +127,3 @@ services:
factory: ["@doctrine", getRepository] factory: ["@doctrine", getRepository]
arguments: arguments:
- "Chill\\CustomFieldsBundle\\Entity\\CustomFieldLongChoice\\Option" - "Chill\\CustomFieldsBundle\\Entity\\CustomFieldLongChoice\\Option"
Chill\CustomFieldsBundle\EntityRepository\CustomFieldsDefaultGroupRepository:
autowire: true
autoconfigure: true

View File

@@ -1,54 +1,59 @@
<template> <template>
<div> <div>
<template v-if="templates.length > 0"> <template v-if="templates.length > 0">
<slot name="title"> <slot name="title">
<h2>{{ $t("generate_document") }}</h2> <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> </slot>
</div>
<div class="col-md-8"> <div class="container">
<div class="input-group mb-3"> <div class="row">
<select class="form-select" v-model="template"> <div class="col-md-4">
<option disabled selected value=""> <slot name="label">
{{ $t("choose_a_template") }} <label>{{ $t("select_a_template") }}</label>
</option> </slot>
<template v-for="t in templates" :key="t.id"> </div>
<option :value="t.id"> <div class="col-md-8">
{{ localizeString(t.name) || "Aucun nom défini" }} <div class="input-group mb-3">
</option> <select class="form-select" v-model="template">
</template> <option disabled selected value="">
</select> {{ $t("choose_a_template") }}
<a </option>
v-if="canGenerate" <template v-for="t in templates" :key="t.id">
class="btn btn-update btn-sm change-icon" <option :value="t.id">
:href="buildUrlGenerate" {{
@click.prevent="clickGenerate($event, buildUrlGenerate)" localizeString(t.name) ||
><i class="fa fa-fw fa-cog" "Aucun nom défini"
/></a> }}
<a </option>
v-else </template>
class="btn btn-update btn-sm change-icon" </select>
href="#" <a
disabled v-if="canGenerate"
><i class="fa fa-fw fa-cog" class="btn btn-update btn-sm change-icon"
/></a> :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> </div>
</div> </template>
</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> </template>
<script> <script>
@@ -56,83 +61,83 @@ import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
export default { export default {
name: "PickTemplate", name: "PickTemplate",
props: { props: {
entityId: [String, Number], entityId: [String, Number],
entityClass: { entityClass: {
type: String, type: String,
required: false, required: false,
},
templates: {
type: Array,
required: true,
},
preventDefaultMoveToGenerate: {
type: Boolean,
required: false,
default: false,
},
}, },
templates: { emits: ["goToGenerateDocument"],
type: Array, data() {
required: true, return {
template: null,
};
}, },
preventDefaultMoveToGenerate: { computed: {
type: Boolean, canGenerate() {
required: false, return this.template != null;
default: false, },
}, hasDescription() {
}, if (this.template == null) {
emits: ["goToGenerateDocument"], return false;
data() { }
return {
template: null,
};
},
computed: {
canGenerate() {
return this.template != null;
},
hasDescription() {
if (this.template == null) {
return false;
}
return true; return true;
}, },
getDescription() { getDescription() {
if (null === this.template) { if (null === this.template) {
return ""; return "";
} }
let desc = this.templates.find((t) => t.id === this.template); let desc = this.templates.find((t) => t.id === this.template);
if (null === desc) { if (null === desc) {
return ""; return "";
} }
return desc.description || ""; return desc.description || "";
}, },
buildUrlGenerate() { buildUrlGenerate() {
if (null === this.template) { if (null === this.template) {
return "#"; return "#";
} }
return buildLink(this.template, this.entityId, this.entityClass); return buildLink(this.template, this.entityId, this.entityClass);
},
}, },
}, methods: {
methods: { localizeString(str) {
localizeString(str) { return localizeString(str);
return localizeString(str); },
}, clickGenerate(event, link) {
clickGenerate(event, link) { if (!this.preventDefaultMoveToGenerate) {
if (!this.preventDefaultMoveToGenerate) { window.location.assign(link);
window.location.assign(link); }
}
this.$emit("goToGenerateDocument", { this.$emit("goToGenerateDocument", {
event, event,
link, link,
template: this.template, template: this.template,
}); });
},
}, },
}, i18n: {
i18n: { messages: {
messages: { fr: {
fr: { generate_document: "Générer un document",
generate_document: "Générer un document", select_a_template: "Choisir un modèle",
select_a_template: "Choisir un modèle", choose_a_template: "Choisir",
choose_a_template: "Choisir", },
}, },
}, },
},
}; };
</script> </script>

View File

@@ -18,7 +18,6 @@ use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator; use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path; use Symfony\Component\Filesystem\Path;
@@ -148,11 +147,16 @@ class StoredObjectManager implements StoredObjectManagerInterface
public function writeContent(string $filename, string $encryptedContent): void public function writeContent(string $filename, string $encryptedContent): void
{ {
$fullPath = $this->buildPath($filename); $fullPath = $this->buildPath($filename);
$dir = Path::getDirectory($fullPath);
try { if (!$this->filesystem->exists($dir)) {
$this->filesystem->dumpFile($fullPath, $encryptedContent); $this->filesystem->mkdir($dir);
} catch (IOExceptionInterface $exception) { }
throw StoredObjectManagerException::unableToStoreDocumentOnDisk($exception);
$result = file_put_contents($fullPath, $encryptedContent);
if (false === $result) {
throw StoredObjectManagerException::unableToStoreDocumentOnDisk();
} }
} }

View File

@@ -6,20 +6,20 @@ const algo = "AES-CBC";
const URL_POST = "/asyncupload/temp_url/generate/post"; const URL_POST = "/asyncupload/temp_url/generate/post";
const keyDefinition = { const keyDefinition = {
name: algo, name: algo,
length: 256, length: 256,
}; };
const createFilename = (): string => { const createFilename = (): string => {
let text = ""; let text = "";
const possible = const possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length)); 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. * @returns {Promise<StoredObject>} A Promise that resolves to the newly created StoredObject.
*/ */
export const fetchNewStoredObject = async (): Promise<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 ( export const uploadVersion = async (
uploadFile: ArrayBuffer, uploadFile: ArrayBuffer,
storedObject: StoredObject, storedObject: StoredObject,
): Promise<string> => { ): Promise<string> => {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append("expires_delay", "180"); params.append("expires_delay", "180");
params.append("submit_delay", "180"); params.append("submit_delay", "180");
const asyncData: PostStoreObjectSignature = await makeFetch( const asyncData: PostStoreObjectSignature = await makeFetch(
"GET", "GET",
`/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/post` + `/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/post` +
"?" + "?" +
params.toString(), params.toString(),
); );
const suffix = createFilename(); const suffix = createFilename();
const filename = asyncData.prefix + suffix; const filename = asyncData.prefix + suffix;
const formData = new FormData(); const formData = new FormData();
formData.append("redirect", asyncData.redirect); formData.append("redirect", asyncData.redirect);
formData.append("max_file_size", asyncData.max_file_size.toString()); formData.append("max_file_size", asyncData.max_file_size.toString());
formData.append("max_file_count", asyncData.max_file_count.toString()); formData.append("max_file_count", asyncData.max_file_count.toString());
formData.append("expires", asyncData.expires.toString()); formData.append("expires", asyncData.expires.toString());
formData.append("signature", asyncData.signature); formData.append("signature", asyncData.signature);
formData.append(filename, new Blob([uploadFile]), suffix); formData.append(filename, new Blob([uploadFile]), suffix);
const response = await window.fetch(asyncData.url, { const response = await window.fetch(asyncData.url, {
method: "POST", method: "POST",
body: formData, body: formData,
}); });
if (!response.ok) { if (!response.ok) {
console.error("Error while sending file to store", response); console.error("Error while sending file to store", response);
throw new Error(response.statusText); throw new Error(response.statusText);
} }
return Promise.resolve(filename); return Promise.resolve(filename);
}; };
export const encryptFile = async ( export const encryptFile = async (
originalFile: ArrayBuffer, originalFile: ArrayBuffer,
): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => { ): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => {
const iv = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(16));
const key = await window.crypto.subtle.generateKey(keyDefinition, true, [ const key = await window.crypto.subtle.generateKey(keyDefinition, true, [
"encrypt", "encrypt",
"decrypt", "decrypt",
]); ]);
const exportedKey = await window.crypto.subtle.exportKey("jwk", key); const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
const encrypted = await window.crypto.subtle.encrypt( const encrypted = await window.crypto.subtle.encrypt(
{ name: algo, iv: iv }, { name: algo, iv: iv },
key, key,
originalFile, originalFile,
); );
return Promise.resolve([encrypted, iv, exportedKey]); return Promise.resolve([encrypted, iv, exportedKey]);
}; };

View File

@@ -2,9 +2,9 @@ import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc"; import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
export function fetch_generic_docs_by_accompanying_period( export function fetch_generic_docs_by_accompanying_period(
periodId: number, periodId: number,
): Promise<GenericDocForAccompanyingPeriod[]> { ): Promise<GenericDocForAccompanyingPeriod[]> {
return fetchResults( return fetchResults(
`/api/1.0/doc-store/generic-doc/by-period/${periodId}/index`, `/api/1.0/doc-store/generic-doc/by-period/${periodId}/index`,
); );
} }

View File

@@ -6,116 +6,117 @@ import { _createI18n } from "../../../../../ChillMainBundle/Resources/public/vue
const i18n = _createI18n({}); const i18n = _createI18n({});
const startApp = ( const startApp = (
divElement: HTMLDivElement, divElement: HTMLDivElement,
collectionEntry: null | HTMLLIElement, collectionEntry: null | HTMLLIElement,
): void => { ): 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( const input_stored_object: HTMLInputElement | null =
"input[data-stored-object]", divElement.querySelector("input[data-stored-object]");
); if (null === input_stored_object) {
if (null === input_stored_object) { throw new Error("input to stored object not found");
throw new Error("input to stored object not found"); }
}
let existingDoc: StoredObject | null = null; let existingDoc: StoredObject | null = null;
if (input_stored_object.value !== "") { if (input_stored_object.value !== "") {
existingDoc = JSON.parse(input_stored_object.value); existingDoc = JSON.parse(input_stored_object.value);
} }
const app_container = document.createElement("div"); const app_container = document.createElement("div");
divElement.appendChild(app_container); divElement.appendChild(app_container);
const app = createApp({ const app = createApp({
template: template:
'<drop-file-widget :existingDoc="this.$data.existingDoc" :allowRemove="true" @addDocument="this.addDocument" @removeDocument="removeDocument"></drop-file-widget>', '<drop-file-widget :existingDoc="this.$data.existingDoc" :allowRemove="true" @addDocument="this.addDocument" @removeDocument="removeDocument"></drop-file-widget>',
data() { data() {
return { return {
existingDoc: existingDoc, existingDoc: existingDoc,
inputTitle: inputTitle, inputTitle: inputTitle,
}; };
}, },
components: { components: {
DropFileWidget, DropFileWidget,
}, },
methods: { methods: {
addDocument: function ({ addDocument: function ({
stored_object, stored_object,
stored_object_version, stored_object_version,
file_name, file_name,
}: { }: {
stored_object: StoredObject; stored_object: StoredObject;
stored_object_version: StoredObjectVersion; stored_object_version: StoredObjectVersion;
file_name: string; file_name: string;
}): void { }): void {
stored_object.title = file_name; stored_object.title = file_name;
console.log("object added", stored_object); console.log("object added", stored_object);
console.log("version added", stored_object_version); console.log("version added", stored_object_version);
this.$data.existingDoc = stored_object; this.$data.existingDoc = stored_object;
this.$data.existingDoc.currentVersion = stored_object_version; this.$data.existingDoc.currentVersion = stored_object_version;
input_stored_object.value = JSON.stringify(this.$data.existingDoc); input_stored_object.value = JSON.stringify(
if (this.$data.inputTitle) { this.$data.existingDoc,
if (!this.$data.inputTitle?.value) { );
this.$data.inputTitle.value = file_name; 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 = ""; removeDocument: function (object: StoredObject): void {
this.$data.existingDoc = undefined; console.log("catch remove document", object);
console.log("collectionEntry", collectionEntry); input_stored_object.value = "";
this.$data.existingDoc = undefined;
console.log("collectionEntry", collectionEntry);
if (null !== collectionEntry) { if (null !== collectionEntry) {
console.log("will remove collection"); console.log("will remove collection");
collectionEntry.remove(); collectionEntry.remove();
} }
}, },
}, },
}); });
app.use(i18n).mount(app_container); app.use(i18n).mount(app_container);
}; };
window.addEventListener("collection-add-entry", (( window.addEventListener("collection-add-entry", ((
e: CustomEvent<CollectionEventPayload>, e: CustomEvent<CollectionEventPayload>,
) => { ) => {
const detail = e.detail; const detail = e.detail;
const divElement: null | HTMLDivElement = detail.entry.querySelector( const divElement: null | HTMLDivElement = detail.entry.querySelector(
"div[data-stored-object]", "div[data-stored-object]",
); );
if (null === divElement) { if (null === divElement) {
throw new Error("div[data-stored-object] not found"); throw new Error("div[data-stored-object] not found");
} }
startApp(divElement, detail.entry); startApp(divElement, detail.entry);
}) as EventListener); }) as EventListener);
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
const upload_inputs: NodeListOf<HTMLDivElement> = document.querySelectorAll( const upload_inputs: NodeListOf<HTMLDivElement> = document.querySelectorAll(
"div[data-stored-object]", "div[data-stored-object]",
); );
upload_inputs.forEach((input: HTMLDivElement): void => { upload_inputs.forEach((input: HTMLDivElement): void => {
// test for a parent to check if this is a collection entry // test for a parent to check if this is a collection entry
let collectionEntry: null | HTMLLIElement = null; let collectionEntry: null | HTMLLIElement = null;
const parent = input.parentElement; const parent = input.parentElement;
console.log("parent", parent); console.log("parent", parent);
if (null !== parent) { if (null !== parent) {
const grandParent = parent.parentElement; const grandParent = parent.parentElement;
console.log("grandParent", grandParent); console.log("grandParent", grandParent);
if (null !== grandParent) { if (null !== grandParent) {
if ( if (
grandParent.tagName.toLowerCase() === "li" && grandParent.tagName.toLowerCase() === "li" &&
grandParent.classList.contains("entry") grandParent.classList.contains("entry")
) { ) {
collectionEntry = grandParent as HTMLLIElement; collectionEntry = grandParent as HTMLLIElement;
}
}
} }
} startApp(input, collectionEntry);
} });
startApp(input, collectionEntry);
});
}); });
export {}; export {};

View File

@@ -9,26 +9,26 @@ import ToastPlugin from "vue-toast-notification";
const i18n = _createI18n({}); const i18n = _createI18n({});
window.addEventListener("DOMContentLoaded", function (e) { window.addEventListener("DOMContentLoaded", function (e) {
document document
.querySelectorAll<HTMLDivElement>("div[data-download-button-single]") .querySelectorAll<HTMLDivElement>("div[data-download-button-single]")
.forEach((el) => { .forEach((el) => {
const storedObject = JSON.parse( const storedObject = JSON.parse(
el.dataset.storedObject as string, el.dataset.storedObject as string,
) as StoredObject; ) as StoredObject;
const title = el.dataset.title as string; const title = el.dataset.title as string;
const app = createApp({ const app = createApp({
components: { DownloadButton }, components: { DownloadButton },
data() { data() {
return { return {
storedObject, storedObject,
title, title,
classes: { btn: true, "btn-outline-primary": true }, classes: { btn: true, "btn-outline-primary": true },
}; };
}, },
template: template:
'<download-button :stored-object="storedObject" :at-version="storedObject.currentVersion" :classes="classes" :filename="title" :direct-download="true"></download-button>', '<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);
}); });
}); });

View File

@@ -8,66 +8,66 @@ import ToastPlugin from "vue-toast-notification";
const i18n = _createI18n({}); const i18n = _createI18n({});
window.addEventListener("DOMContentLoaded", function (e) { window.addEventListener("DOMContentLoaded", function (e) {
document document
.querySelectorAll<HTMLDivElement>("div[data-download-buttons]") .querySelectorAll<HTMLDivElement>("div[data-download-buttons]")
.forEach((el) => { .forEach((el) => {
const app = createApp({ const app = createApp({
components: { DocumentActionButtonsGroup }, components: { DocumentActionButtonsGroup },
data() { data() {
const datasets = el.dataset as { const datasets = el.dataset as {
filename: string; filename: string;
canEdit: string; canEdit: string;
storedObject: string; storedObject: string;
buttonSmall: string; buttonSmall: string;
davLink: string; davLink: string;
davLinkExpiration: string; davLinkExpiration: string;
}; };
const storedObject = JSON.parse( const storedObject = JSON.parse(
datasets.storedObject, datasets.storedObject,
) as StoredObject, ) as StoredObject,
filename = datasets.filename, filename = datasets.filename,
canEdit = datasets.canEdit === "1", canEdit = datasets.canEdit === "1",
small = datasets.buttonSmall === "1", small = datasets.buttonSmall === "1",
davLink = davLink =
"davLink" in datasets && datasets.davLink !== "" "davLink" in datasets && datasets.davLink !== ""
? datasets.davLink ? datasets.davLink
: null, : null,
davLinkExpiration = davLinkExpiration =
"davLinkExpiration" in datasets "davLinkExpiration" in datasets
? Number.parseInt(datasets.davLinkExpiration) ? Number.parseInt(datasets.davLinkExpiration)
: null; : null;
return { return {
storedObject, storedObject,
filename, filename,
canEdit, canEdit,
small, small,
davLink, davLink,
davLinkExpiration, davLinkExpiration,
}; };
}, },
template: 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>', '<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: { methods: {
onStoredObjectStatusChange: function ( onStoredObjectStatusChange: function (
newStatus: StoredObjectStatusChange, newStatus: StoredObjectStatusChange,
): void { ): void {
this.$data.storedObject.status = newStatus.status; this.$data.storedObject.status = newStatus.status;
this.$data.storedObject.filename = newStatus.filename; this.$data.storedObject.filename = newStatus.filename;
this.$data.storedObject.type = newStatus.type; this.$data.storedObject.type = newStatus.type;
// remove eventual div which inform pending status // remove eventual div which inform pending status
document document
.querySelectorAll( .querySelectorAll(
`[data-docgen-is-pending="${this.$data.storedObject.id}"]`, `[data-docgen-is-pending="${this.$data.storedObject.id}"]`,
) )
.forEach(function (el) { .forEach(function (el) {
el.remove(); el.remove();
}); });
}, },
}, },
}); });
app.use(i18n).use(ToastPlugin).mount(el); app.use(i18n).use(ToastPlugin).mount(el);
}); });
}); });

View File

@@ -2,7 +2,7 @@ import { DateTime } from "ChillMainAssets/types";
import { StoredObject } from "ChillDocStoreAssets/types/index"; import { StoredObject } from "ChillDocStoreAssets/types/index";
export interface GenericDocMetadata { export interface GenericDocMetadata {
isPresent: boolean; isPresent: boolean;
} }
/** /**
@@ -15,57 +15,57 @@ export interface EmptyMetadata extends GenericDocMetadata {}
* Minimal Metadata for a GenericDoc with a normalizer * Minimal Metadata for a GenericDoc with a normalizer
*/ */
export interface BaseMetadata extends GenericDocMetadata { export interface BaseMetadata extends GenericDocMetadata {
title: string; title: string;
} }
/** /**
* A generic doc is a document attached to a Person or an AccompanyingPeriod. * A generic doc is a document attached to a Person or an AccompanyingPeriod.
*/ */
export interface GenericDoc { export interface GenericDoc {
type: "doc_store_generic_doc"; type: "doc_store_generic_doc";
uniqueKey: string; uniqueKey: string;
key: string; key: string;
identifiers: object; identifiers: object;
context: "person" | "accompanying-period"; context: "person" | "accompanying-period";
doc_date: DateTime; doc_date: DateTime;
metadata: GenericDocMetadata; metadata: GenericDocMetadata;
storedObject: StoredObject | null; storedObject: StoredObject | null;
} }
export interface GenericDocForAccompanyingPeriod extends GenericDoc { export interface GenericDocForAccompanyingPeriod extends GenericDoc {
context: "accompanying-period"; context: "accompanying-period";
} }
interface BaseMetadataWithHtml extends BaseMetadata { interface BaseMetadataWithHtml extends BaseMetadata {
html: string; html: string;
} }
export interface GenericDocForAccompanyingCourseDocument export interface GenericDocForAccompanyingCourseDocument
extends GenericDocForAccompanyingPeriod { extends GenericDocForAccompanyingPeriod {
key: "accompanying_course_document"; key: "accompanying_course_document";
metadata: BaseMetadataWithHtml; metadata: BaseMetadataWithHtml;
} }
export interface GenericDocForAccompanyingCourseActivityDocument export interface GenericDocForAccompanyingCourseActivityDocument
extends GenericDocForAccompanyingPeriod { extends GenericDocForAccompanyingPeriod {
key: "accompanying_course_activity_document"; key: "accompanying_course_activity_document";
metadata: BaseMetadataWithHtml; metadata: BaseMetadataWithHtml;
} }
export interface GenericDocForAccompanyingCourseCalendarDocument export interface GenericDocForAccompanyingCourseCalendarDocument
extends GenericDocForAccompanyingPeriod { extends GenericDocForAccompanyingPeriod {
key: "accompanying_course_calendar_document"; key: "accompanying_course_calendar_document";
metadata: BaseMetadataWithHtml; metadata: BaseMetadataWithHtml;
} }
export interface GenericDocForAccompanyingCoursePersonDocument export interface GenericDocForAccompanyingCoursePersonDocument
extends GenericDocForAccompanyingPeriod { extends GenericDocForAccompanyingPeriod {
key: "person_document"; key: "person_document";
metadata: BaseMetadataWithHtml; metadata: BaseMetadataWithHtml;
} }
export interface GenericDocForAccompanyingCourseWorkEvaluationDocument export interface GenericDocForAccompanyingCourseWorkEvaluationDocument
extends GenericDocForAccompanyingPeriod { extends GenericDocForAccompanyingPeriod {
key: "accompanying_period_work_evaluation_document"; key: "accompanying_period_work_evaluation_document";
metadata: BaseMetadataWithHtml; metadata: BaseMetadataWithHtml;
} }

View File

@@ -4,73 +4,73 @@ import { SignedUrlGet } from "ChillDocStoreAssets/vuejs/StoredObjectButton/helpe
export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending"; export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending";
export interface StoredObject { export interface StoredObject {
id: number; id: number;
title: string | null; title: string | null;
uuid: string; uuid: string;
prefix: string; prefix: string;
status: StoredObjectStatus; status: StoredObjectStatus;
currentVersion: currentVersion:
| null | null
| StoredObjectVersionCreated | StoredObjectVersionCreated
| StoredObjectVersionPersisted; | StoredObjectVersionPersisted;
totalVersions: number; totalVersions: number;
datas: object; datas: object;
/** @deprecated */ /** @deprecated */
creationDate: DateTime; creationDate: DateTime;
createdAt: DateTime | null; createdAt: DateTime | null;
createdBy: User | null; createdBy: User | null;
_permissions: { _permissions: {
canEdit: boolean; canEdit: boolean;
canSee: boolean; canSee: boolean;
}; };
_links?: { _links?: {
dav_link?: { dav_link?: {
href: string; href: string;
expiration: number; expiration: number;
};
downloadLink?: SignedUrlGet;
}; };
downloadLink?: SignedUrlGet;
};
} }
export interface StoredObjectVersion { export interface StoredObjectVersion {
/** /**
* filename of the object in the object storage * filename of the object in the object storage
*/ */
filename: string; filename: string;
iv: number[]; iv: number[];
keyInfos: JsonWebKey; keyInfos: JsonWebKey;
type: string; type: string;
} }
export interface StoredObjectVersionCreated extends StoredObjectVersion { export interface StoredObjectVersionCreated extends StoredObjectVersion {
persisted: false; persisted: false;
} }
export interface StoredObjectVersionPersisted export interface StoredObjectVersionPersisted
extends StoredObjectVersionCreated { extends StoredObjectVersionCreated {
version: number; version: number;
id: number; id: number;
createdAt: DateTime | null; createdAt: DateTime | null;
createdBy: User | null; createdBy: User | null;
} }
export interface StoredObjectStatusChange { export interface StoredObjectStatusChange {
id: number; id: number;
filename: string; filename: string;
status: StoredObjectStatus; status: StoredObjectStatus;
type: string; type: string;
} }
export interface StoredObjectVersionWithPointInTime export interface StoredObjectVersionWithPointInTime
extends StoredObjectVersionPersisted { extends StoredObjectVersionPersisted {
"point-in-times": StoredObjectPointInTime[]; "point-in-times": StoredObjectPointInTime[];
"from-restored": StoredObjectVersionPersisted | null; "from-restored": StoredObjectVersionPersisted | null;
} }
export interface StoredObjectPointInTime { export interface StoredObjectPointInTime {
id: number; id: number;
byUser: User | null; byUser: User | null;
reason: "keep-before-conversion" | "keep-by-user"; 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 * Object containing information for performering a POST request to a swift object store
*/ */
export interface PostStoreObjectSignature { export interface PostStoreObjectSignature {
method: "POST"; method: "POST";
max_file_size: number; max_file_size: number;
max_file_count: 1; max_file_count: 1;
expires: number; expires: number;
submit_delay: 180; submit_delay: 180;
redirect: string; redirect: string;
prefix: string; prefix: string;
url: string; url: string;
signature: string; signature: string;
} }
export interface PDFPage { export interface PDFPage {
index: number; index: number;
width: number; width: number;
height: number; height: number;
} }
export interface SignatureZone { export interface SignatureZone {
index: number | null; index: number | null;
x: number; x: number;
y: number; y: number;
width: number; width: number;
height: number; height: number;
PDFPage: PDFPage; PDFPage: PDFPage;
} }
export interface Signature { export interface Signature {
id: number; id: number;
storedObject: StoredObject; storedObject: StoredObject;
zones: SignatureZone[]; zones: SignatureZone[];
} }
export type SignedState = export type SignedState =
| "pending" | "pending"
| "signed" | "signed"
| "rejected" | "rejected"
| "canceled" | "canceled"
| "error"; | "error";
export interface CheckSignature { export interface CheckSignature {
state: SignedState; state: SignedState;
storedObject: StoredObject; storedObject: StoredObject;
} }
export type CanvasEvent = "select" | "add"; export type CanvasEvent = "select" | "add";
export interface ZoomLevel { export interface ZoomLevel {
id: number; id: number;
zoom: number; zoom: number;
label: { label: {
fr?: string; fr?: string;
nl?: string; nl?: string;
}; };
} }
export interface GenericDoc { export interface GenericDoc {
type: "doc_store_generic_doc"; type: "doc_store_generic_doc";
key: string; key: string;
context: "person" | "accompanying-period"; context: "person" | "accompanying-period";
doc_date: DateTime; doc_date: DateTime;
} }

View File

@@ -1,65 +1,67 @@
<template> <template>
<div v-if="isButtonGroupDisplayable" class="btn-group"> <div v-if="isButtonGroupDisplayable" class="btn-group">
<button <button
:class=" :class="
Object.assign({ Object.assign({
btn: true, btn: true,
'btn-outline-primary': true, 'btn-outline-primary': true,
'dropdown-toggle': true, 'dropdown-toggle': true,
'btn-sm': props.small, 'btn-sm': props.small,
}) })
" "
type="button" type="button"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
> >
Actions Actions
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li v-if="isEditableOnline"> <li v-if="isEditableOnline">
<wopi-edit-button <wopi-edit-button
:stored-object="props.storedObject" :stored-object="props.storedObject"
:classes="{ 'dropdown-item': true }" :classes="{ 'dropdown-item': true }"
:execute-before-leave="props.executeBeforeLeave" :execute-before-leave="props.executeBeforeLeave"
></wopi-edit-button> ></wopi-edit-button>
</li> </li>
<li v-if="isEditableOnDesktop"> <li v-if="isEditableOnDesktop">
<desktop-edit-button <desktop-edit-button
:classes="{ 'dropdown-item': true }" :classes="{ 'dropdown-item': true }"
:edit-link="props.davLink" :edit-link="props.davLink"
:expiration-link="props.davLinkExpiration" :expiration-link="props.davLinkExpiration"
></desktop-edit-button> ></desktop-edit-button>
</li> </li>
<li v-if="isConvertibleToPdf"> <li v-if="isConvertibleToPdf">
<convert-button <convert-button
:stored-object="props.storedObject" :stored-object="props.storedObject"
:filename="filename" :filename="filename"
:classes="{ 'dropdown-item': true }" :classes="{ 'dropdown-item': true }"
></convert-button> ></convert-button>
</li> </li>
<li v-if="isDownloadable"> <li v-if="isDownloadable">
<download-button <download-button
:stored-object="props.storedObject" :stored-object="props.storedObject"
:at-version="props.storedObject.currentVersion" :at-version="props.storedObject.currentVersion"
:filename="filename" :filename="filename"
:classes="{ 'dropdown-item': true }" :classes="{ 'dropdown-item': true }"
:display-action-string-in-button="true" :display-action-string-in-button="true"
></download-button> ></download-button>
</li> </li>
<li v-if="isHistoryViewable"> <li v-if="isHistoryViewable">
<history-button <history-button
:stored-object="props.storedObject" :stored-object="props.storedObject"
:can-edit="canEdit && props.storedObject._permissions.canEdit" :can-edit="
></history-button> canEdit && props.storedObject._permissions.canEdit
</li> "
</ul> ></history-button>
</div> </li>
<div v-else-if="'pending' === props.storedObject.status"> </ul>
<div class="btn btn-outline-info">Génération en cours</div> </div>
</div> <div v-else-if="'pending' === props.storedObject.status">
<div v-else-if="'failure' === props.storedObject.status"> <div class="btn btn-outline-info">Génération en cours</div>
<div class="btn btn-outline-danger">La génération a échoué</div> </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> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -68,66 +70,68 @@ import ConvertButton from "./StoredObjectButton/ConvertButton.vue";
import DownloadButton from "./StoredObjectButton/DownloadButton.vue"; import DownloadButton from "./StoredObjectButton/DownloadButton.vue";
import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue"; import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue";
import { import {
is_extension_editable, is_extension_editable,
is_extension_viewable, is_extension_viewable,
is_object_ready, is_object_ready,
} from "./StoredObjectButton/helpers"; } from "./StoredObjectButton/helpers";
import { import {
StoredObject, StoredObject,
StoredObjectStatusChange, StoredObjectStatusChange,
StoredObjectVersion, StoredObjectVersion,
WopiEditButtonExecutableBeforeLeaveFunction, WopiEditButtonExecutableBeforeLeaveFunction,
} from "../types"; } from "../types";
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue"; import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
import HistoryButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton.vue"; import HistoryButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton.vue";
interface DocumentActionButtonsGroupConfig { interface DocumentActionButtonsGroupConfig {
storedObject: StoredObject; storedObject: StoredObject;
small?: boolean; small?: boolean;
canEdit?: boolean; canEdit?: boolean;
canDownload?: boolean; canDownload?: boolean;
canConvertPdf?: boolean; canConvertPdf?: boolean;
returnPath?: string; returnPath?: string;
/** /**
* Will be the filename displayed to the user when he·she download the document * 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) * (the document will be saved on his disk with this name)
* *
* If not set, 'document' will be used. * If not set, 'document' will be used.
*/ */
filename?: string; filename?: string;
/** /**
* If set, will execute this function before leaving to the editor * If set, will execute this function before leaving to the editor
*/ */
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction; executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction;
/** /**
* a link to download and edit file using webdav * a link to download and edit file using webdav
*/ */
davLink?: string; davLink?: string;
/** /**
* the expiration date of the download, as a unix timestamp * the expiration date of the download, as a unix timestamp
*/ */
davLinkExpiration?: number; davLinkExpiration?: number;
} }
const emit = const emit =
defineEmits< defineEmits<
( (
e: "onStoredObjectStatusChange", e: "onStoredObjectStatusChange",
newStatus: StoredObjectStatusChange, newStatus: StoredObjectStatusChange,
) => void ) => void
>(); >();
const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), { const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
small: false, small: false,
canEdit: true, canEdit: true,
canDownload: true, canDownload: true,
canConvertPdf: true, canConvertPdf: true,
returnPath: returnPath:
window.location.pathname + window.location.search + window.location.hash, window.location.pathname +
window.location.search +
window.location.hash,
}); });
/** /**
@@ -141,93 +145,93 @@ let tryiesForReady = 0;
const maxTryiesForReady = 120; const maxTryiesForReady = 120;
const isButtonGroupDisplayable = computed<boolean>(() => { const isButtonGroupDisplayable = computed<boolean>(() => {
return ( return (
isDownloadable.value || isDownloadable.value ||
isEditableOnline.value || isEditableOnline.value ||
isEditableOnDesktop.value || isEditableOnDesktop.value ||
isConvertibleToPdf.value isConvertibleToPdf.value
); );
}); });
const isDownloadable = computed<boolean>(() => { const isDownloadable = computed<boolean>(() => {
return ( return (
props.storedObject.status === "ready" || props.storedObject.status === "ready" ||
// happens when the stored object version is just added, but not persisted // happens when the stored object version is just added, but not persisted
(props.storedObject.currentVersion !== null && (props.storedObject.currentVersion !== null &&
props.storedObject.status === "empty") props.storedObject.status === "empty")
); );
}); });
const isEditableOnline = computed<boolean>(() => { const isEditableOnline = computed<boolean>(() => {
return ( return (
props.storedObject.status === "ready" && props.storedObject.status === "ready" &&
props.storedObject._permissions.canEdit && props.storedObject._permissions.canEdit &&
props.canEdit && props.canEdit &&
props.storedObject.currentVersion !== null && props.storedObject.currentVersion !== null &&
is_extension_editable(props.storedObject.currentVersion.type) && is_extension_editable(props.storedObject.currentVersion.type) &&
props.storedObject.currentVersion.persisted !== false props.storedObject.currentVersion.persisted !== false
); );
}); });
const isEditableOnDesktop = computed<boolean>(() => { const isEditableOnDesktop = computed<boolean>(() => {
return isEditableOnline.value; return isEditableOnline.value;
}); });
const isConvertibleToPdf = computed<boolean>(() => { const isConvertibleToPdf = computed<boolean>(() => {
return ( return (
props.storedObject.status === "ready" && props.storedObject.status === "ready" &&
props.storedObject._permissions.canSee && props.storedObject._permissions.canSee &&
props.canConvertPdf && props.canConvertPdf &&
props.storedObject.currentVersion !== null && props.storedObject.currentVersion !== null &&
is_extension_viewable(props.storedObject.currentVersion.type) && is_extension_viewable(props.storedObject.currentVersion.type) &&
props.storedObject.currentVersion.type !== "application/pdf" && props.storedObject.currentVersion.type !== "application/pdf" &&
props.storedObject.currentVersion.persisted !== false props.storedObject.currentVersion.persisted !== false
); );
}); });
const isHistoryViewable = computed<boolean>(() => { const isHistoryViewable = computed<boolean>(() => {
return props.storedObject.status === "ready"; return props.storedObject.status === "ready";
}); });
const checkForReady = function (): void { const checkForReady = function (): void {
if ( if (
"ready" === props.storedObject.status || "ready" === props.storedObject.status ||
"empty" === props.storedObject.status || "empty" === props.storedObject.status ||
"failure" === props.storedObject.status || "failure" === props.storedObject.status ||
// stop reloading if the page stays opened for a long time // stop reloading if the page stays opened for a long time
tryiesForReady > maxTryiesForReady tryiesForReady > maxTryiesForReady
) { ) {
return; return;
} }
tryiesForReady = tryiesForReady + 1; tryiesForReady = tryiesForReady + 1;
setTimeout(onObjectNewStatusCallback, 5000); setTimeout(onObjectNewStatusCallback, 5000);
}; };
const onObjectNewStatusCallback = async function (): Promise<void> { const onObjectNewStatusCallback = async function (): Promise<void> {
if (props.storedObject.status === "stored_object_created") { if (props.storedObject.status === "stored_object_created") {
return Promise.resolve(); return Promise.resolve();
} }
const new_status = await is_object_ready(props.storedObject); const new_status = await is_object_ready(props.storedObject);
if (props.storedObject.status !== new_status.status) { if (props.storedObject.status !== new_status.status) {
emit("onStoredObjectStatusChange", new_status); emit("onStoredObjectStatusChange", new_status);
return Promise.resolve(); return Promise.resolve();
} else if ("failure" === new_status.status) { } else if ("failure" === new_status.status) {
return Promise.resolve(); return Promise.resolve();
} }
if ("ready" !== new_status.status) { if ("ready" !== new_status.status) {
// we check for new status, unless it is ready // we check for new status, unless it is ready
checkForReady(); checkForReady();
} }
return Promise.resolve(); return Promise.resolve();
}; };
onMounted(() => { onMounted(() => {
checkForReady(); checkForReady();
}); });
</script> </script>

View File

@@ -4,36 +4,36 @@ import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
import App from "./App.vue"; import App from "./App.vue";
const appMessages = { const appMessages = {
fr: { fr: {
yes: "Oui", yes: "Oui",
are_you_sure: "Êtes-vous sûr·e?", are_you_sure: "Êtes-vous sûr·e?",
you_are_going_to_sign: "Vous allez signer le document", you_are_going_to_sign: "Vous allez signer le document",
signature_confirmation: "Confirmation de la signature", signature_confirmation: "Confirmation de la signature",
sign: "Signer", sign: "Signer",
choose_another_signature: "Choisir une autre zone", choose_another_signature: "Choisir une autre zone",
cancel: "Annuler", cancel: "Annuler",
last_sign_zone: "Zone de signature précédente", last_sign_zone: "Zone de signature précédente",
next_sign_zone: "Zone de signature suivante", next_sign_zone: "Zone de signature suivante",
add_sign_zone: "Ajouter une zone de signature", add_sign_zone: "Ajouter une zone de signature",
click_on_document: "Cliquer sur le document", click_on_document: "Cliquer sur le document",
last_zone: "Zone précédente", last_zone: "Zone précédente",
next_zone: "Zone suivante", next_zone: "Zone suivante",
add_zone: "Ajouter une zone", add_zone: "Ajouter une zone",
another_zone: "Autre zone", another_zone: "Autre zone",
electronic_signature_in_progress: "Signature électronique en cours...", electronic_signature_in_progress: "Signature électronique en cours...",
loading: "Chargement...", loading: "Chargement...",
remove_sign_zone: "Enlever la zone", remove_sign_zone: "Enlever la zone",
return: "Retour", return: "Retour",
see_all_pages: "Voir toutes les pages", see_all_pages: "Voir toutes les pages",
all_pages: "Toutes les pages", all_pages: "Toutes les pages",
}, },
}; };
const i18n = _createI18n(appMessages); const i18n = _createI18n(appMessages);
const app = createApp({ const app = createApp({
template: `<app></app>`, template: `<app></app>`,
}) })
.use(i18n) .use(i18n)
.component("app", App) .component("app", App)
.mount("#document-signature"); .mount("#document-signature");

View File

@@ -1,206 +1,208 @@
<script setup lang="ts"> <script setup lang="ts">
import { StoredObject, StoredObjectVersionCreated } from "../../types"; import { StoredObject, StoredObjectVersionCreated } from "../../types";
import { import {
encryptFile, encryptFile,
fetchNewStoredObject, fetchNewStoredObject,
uploadVersion, uploadVersion,
} from "../../js/async-upload/uploader"; } from "../../js/async-upload/uploader";
import { computed, ref, Ref } from "vue"; import { computed, ref, Ref } from "vue";
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue"; import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
interface DropFileConfig { interface DropFileConfig {
existingDoc?: StoredObject; existingDoc?: StoredObject;
} }
const props = withDefaults(defineProps<DropFileConfig>(), { const props = withDefaults(defineProps<DropFileConfig>(), {
existingDoc: null, existingDoc: null,
}); });
const emit = const emit =
defineEmits< defineEmits<
( (
e: "addDocument", e: "addDocument",
{ {
stored_object_version: StoredObjectVersionCreated, stored_object_version: StoredObjectVersionCreated,
stored_object: StoredObject, stored_object: StoredObject,
file_name: string, file_name: string,
}, },
) => void ) => void
>(); >();
const is_dragging: Ref<boolean> = ref(false); const is_dragging: Ref<boolean> = ref(false);
const uploading: Ref<boolean> = ref(false); const uploading: Ref<boolean> = ref(false);
const display_filename: Ref<string | null> = ref(null); const display_filename: Ref<string | null> = ref(null);
const has_existing_doc = computed<boolean>(() => { const has_existing_doc = computed<boolean>(() => {
return props.existingDoc !== undefined && props.existingDoc !== null; return props.existingDoc !== undefined && props.existingDoc !== null;
}); });
const onDragOver = (e: Event) => { const onDragOver = (e: Event) => {
e.preventDefault(); e.preventDefault();
is_dragging.value = true; is_dragging.value = true;
}; };
const onDragLeave = (e: Event) => { const onDragLeave = (e: Event) => {
e.preventDefault(); e.preventDefault();
is_dragging.value = false; is_dragging.value = false;
}; };
const onDrop = (e: DragEvent) => { const onDrop = (e: DragEvent) => {
e.preventDefault(); e.preventDefault();
const files = e.dataTransfer?.files; const files = e.dataTransfer?.files;
if (null === files || undefined === files) { if (null === files || undefined === files) {
console.error("no files transferred", e.dataTransfer); console.error("no files transferred", e.dataTransfer);
return; return;
} }
if (files.length === 0) { if (files.length === 0) {
console.error("no files given"); console.error("no files given");
return; return;
} }
handleFile(files[0]); handleFile(files[0]);
}; };
const onZoneClick = (e: Event) => { const onZoneClick = (e: Event) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
const input = document.createElement("input"); const input = document.createElement("input");
input.type = "file"; input.type = "file";
input.addEventListener("change", onFileChange); input.addEventListener("change", onFileChange);
input.click(); input.click();
}; };
const onFileChange = async (event: Event): Promise<void> => { 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]) { if (input.files && input.files[0]) {
console.log("file added", input.files[0]); console.log("file added", input.files[0]);
const file = input.files[0]; const file = input.files[0];
await handleFile(file); await handleFile(file);
return Promise.resolve(); return Promise.resolve();
} }
throw "No file given"; throw "No file given";
}; };
const handleFile = async (file: File): Promise<void> => { const handleFile = async (file: File): Promise<void> => {
uploading.value = true; uploading.value = true;
display_filename.value = file.name; display_filename.value = file.name;
const type = file.type; const type = file.type;
// create a stored_object if not exists // create a stored_object if not exists
let stored_object; let stored_object;
if (null === props.existingDoc) { if (null === props.existingDoc) {
stored_object = await fetchNewStoredObject(); stored_object = await fetchNewStoredObject();
} else { } else {
stored_object = props.existingDoc; stored_object = props.existingDoc;
} }
const buffer = await file.arrayBuffer(); const buffer = await file.arrayBuffer();
const [encrypted, iv, jsonWebKey] = await encryptFile(buffer); const [encrypted, iv, jsonWebKey] = await encryptFile(buffer);
const filename = await uploadVersion(encrypted, stored_object); const filename = await uploadVersion(encrypted, stored_object);
const stored_object_version: StoredObjectVersionCreated = { const stored_object_version: StoredObjectVersionCreated = {
filename: filename, filename: filename,
iv: Array.from(iv), iv: Array.from(iv),
keyInfos: jsonWebKey, keyInfos: jsonWebKey,
type: type, type: type,
persisted: false, persisted: false,
}; };
const fileName = file.name; const fileName = file.name;
let file_name = "Nouveau document"; let file_name = "Nouveau document";
const file_name_split = fileName.split("."); const file_name_split = fileName.split(".");
if (file_name_split.length > 1) { if (file_name_split.length > 1) {
const extension = file_name_split const extension = file_name_split
? file_name_split[file_name_split.length - 1] ? file_name_split[file_name_split.length - 1]
: ""; : "";
file_name = fileName.replace(extension, "").slice(0, -1); file_name = fileName.replace(extension, "").slice(0, -1);
} }
emit("addDocument", { emit("addDocument", {
stored_object, stored_object,
stored_object_version, stored_object_version,
file_name: file_name, file_name: file_name,
}); });
uploading.value = false; uploading.value = false;
}; };
</script> </script>
<template> <template>
<div class="drop-file"> <div class="drop-file">
<div <div
v-if="!uploading" v-if="!uploading"
:class="{ area: true, dragging: is_dragging }" :class="{ area: true, dragging: is_dragging }"
@click="onZoneClick" @click="onZoneClick"
@dragover="onDragOver" @dragover="onDragOver"
@dragleave="onDragLeave" @dragleave="onDragLeave"
@drop="onDrop" @drop="onDrop"
> >
<p v-if="has_existing_doc" class="file-icon"> <p v-if="has_existing_doc" class="file-icon">
<file-icon :type="props.existingDoc?.type"></file-icon> <file-icon :type="props.existingDoc?.type"></file-icon>
</p> </p>
<p v-if="display_filename !== null" class="display-filename"> <p v-if="display_filename !== null" class="display-filename">
{{ display_filename }} {{ display_filename }}
</p> </p>
<!-- todo i18n --> <!-- todo i18n -->
<p v-if="has_existing_doc"> <p v-if="has_existing_doc">
Déposez un document ou cliquez ici pour remplacer le document existant Déposez un document ou cliquez ici pour remplacer le document
</p> existant
<p v-else> </p>
Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier <p v-else>
</p> 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> </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> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.drop-file { .drop-file {
width: 100%;
.file-icon {
font-size: xx-large;
}
.display-filename {
font-variant: small-caps;
font-weight: 200;
}
& > .area,
& > .waiting {
width: 100%; width: 100%;
height: 10rem;
display: flex; .file-icon {
flex-direction: column; font-size: xx-large;
justify-content: center;
align-items: center;
p {
// require for display in DropFileModal
text-align: center;
} }
}
& > .area { .display-filename {
border: 4px dashed #ccc; font-variant: small-caps;
font-weight: 200;
&.dragging { }
border: 4px dashed blue;
& > .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;
}
} }
}
} }
</style> </style>

View File

@@ -4,26 +4,27 @@ import { StoredObject, StoredObjectVersion } from "../../types";
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue"; import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
import { computed, reactive } from "vue"; import { computed, reactive } from "vue";
import { useToast } from "vue-toast-notification"; import { useToast } from "vue-toast-notification";
import { DOCUMENT_REPLACE, DOCUMENT_ADD, trans } from "translator";
interface DropFileConfig { interface DropFileConfig {
allowRemove: boolean; allowRemove: boolean;
existingDoc?: StoredObject; existingDoc?: StoredObject;
} }
const props = withDefaults(defineProps<DropFileConfig>(), { const props = withDefaults(defineProps<DropFileConfig>(), {
allowRemove: false, allowRemove: false,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
( (
e: "addDocument", e: "addDocument",
{ {
stored_object: StoredObject, stored_object: StoredObject,
stored_object_version: StoredObjectVersion, stored_object_version: StoredObjectVersion,
file_name: string, file_name: string,
}, },
): void; ): void;
(e: "removeDocument"): void; (e: "removeDocument"): void;
}>(); }>();
const $toast = useToast(); const $toast = useToast();
@@ -33,67 +34,67 @@ const state = reactive({ showModal: false });
const modalClasses = { "modal-dialog-centered": true, "modal-md": true }; const modalClasses = { "modal-dialog-centered": true, "modal-md": true };
const buttonState = computed<"add" | "replace">(() => { const buttonState = computed<"add" | "replace">(() => {
if (props.existingDoc === undefined || props.existingDoc === null) { if (props.existingDoc === undefined || props.existingDoc === null) {
return "add"; return "add";
} }
return "replace"; return "replace";
}); });
function onAddDocument({ function onAddDocument({
stored_object, stored_object,
stored_object_version, stored_object_version,
file_name, file_name,
}: { }: {
stored_object: StoredObject; stored_object: StoredObject;
stored_object_version: StoredObjectVersion; stored_object_version: StoredObjectVersion;
file_name: string; file_name: string;
}): void { }): void {
const message = const message =
buttonState.value === "add" ? "Document ajouté" : "Document remplacé"; buttonState.value === "add" ? "Document ajouté" : "Document remplacé";
$toast.success(message); $toast.success(message);
emit("addDocument", { stored_object_version, stored_object, file_name }); emit("addDocument", { stored_object_version, stored_object, file_name });
state.showModal = false; state.showModal = false;
} }
function onRemoveDocument(): void { function onRemoveDocument(): void {
emit("removeDocument"); emit("removeDocument");
} }
function openModal(): void { function openModal(): void {
state.showModal = true; state.showModal = true;
} }
function closeModal(): void { function closeModal(): void {
state.showModal = false; state.showModal = false;
} }
</script> </script>
<template> <template>
<button <button
v-if="buttonState === 'add'" v-if="buttonState === 'add'"
@click="openModal" @click="openModal"
class="btn btn-create" class="btn btn-create"
> >
Ajouter un document {{ trans(DOCUMENT_ADD) }}
</button> </button>
<button v-else @click="openModal" class="btn btn-edit"> <button v-else @click="openModal" class="dropdown-item">
Remplacer le document {{ trans(DOCUMENT_REPLACE) }}
</button> </button>
<modal <modal
v-if="state.showModal" v-if="state.showModal"
:modal-dialog-class="modalClasses" :modal-dialog-class="modalClasses"
@close="closeModal" @close="closeModal"
> >
<template v-slot:body> <template v-slot:body>
<drop-file-widget <drop-file-widget
:existing-doc="existingDoc" :existing-doc="existingDoc"
:allow-remove="allowRemove" :allow-remove="allowRemove"
@add-document="onAddDocument" @add-document="onAddDocument"
@remove-document="onRemoveDocument" @remove-document="onRemoveDocument"
></drop-file-widget> ></drop-file-widget>
</template> </template>
</modal> </modal>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@@ -5,97 +5,97 @@ import DropFile from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFile.vue";
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue"; import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
interface DropFileConfig { interface DropFileConfig {
allowRemove: boolean; allowRemove: boolean;
existingDoc?: StoredObject; existingDoc?: StoredObject;
} }
const props = withDefaults(defineProps<DropFileConfig>(), { const props = withDefaults(defineProps<DropFileConfig>(), {
allowRemove: false, allowRemove: false,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
( (
e: "addDocument", e: "addDocument",
{ {
stored_object: StoredObject, stored_object: StoredObject,
stored_object_version: StoredObjectVersion, stored_object_version: StoredObjectVersion,
file_name: string, file_name: string,
}, },
): void; ): void;
(e: "removeDocument"): void; (e: "removeDocument"): void;
}>(); }>();
const has_existing_doc = computed<boolean>(() => { 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>(() => { const dav_link_expiration = computed<number | undefined>(() => {
if (props.existingDoc === undefined || props.existingDoc === null) { if (props.existingDoc === undefined || props.existingDoc === null) {
return undefined; return undefined;
} }
if (props.existingDoc.status !== "ready") { if (props.existingDoc.status !== "ready") {
return undefined; return undefined;
} }
return props.existingDoc._links?.dav_link?.expiration; return props.existingDoc._links?.dav_link?.expiration;
}); });
const dav_link_href = computed<string | undefined>(() => { const dav_link_href = computed<string | undefined>(() => {
if (props.existingDoc === undefined || props.existingDoc === null) { if (props.existingDoc === undefined || props.existingDoc === null) {
return undefined; return undefined;
} }
if (props.existingDoc.status !== "ready") { if (props.existingDoc.status !== "ready") {
return undefined; return undefined;
} }
return props.existingDoc._links?.dav_link?.href; return props.existingDoc._links?.dav_link?.href;
}); });
const onAddDocument = ({ const onAddDocument = ({
stored_object, stored_object,
stored_object_version, stored_object_version,
file_name, file_name,
}: { }: {
stored_object: StoredObject; stored_object: StoredObject;
stored_object_version: StoredObjectVersion; stored_object_version: StoredObjectVersion;
file_name: string; file_name: string;
}): void => { }): void => {
emit("addDocument", { stored_object, stored_object_version, file_name }); emit("addDocument", { stored_object, stored_object_version, file_name });
}; };
const onRemoveDocument = (e: Event): void => { const onRemoveDocument = (e: Event): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
emit("removeDocument"); emit("removeDocument");
}; };
</script> </script>
<template> <template>
<div> <div>
<drop-file <drop-file
:existingDoc="props.existingDoc" :existingDoc="props.existingDoc"
@addDocument="onAddDocument" @addDocument="onAddDocument"
></drop-file> ></drop-file>
<ul class="record_actions"> <ul class="record_actions">
<li v-if="has_existing_doc"> <li v-if="has_existing_doc">
<document-action-buttons-group <document-action-buttons-group
:stored-object="props.existingDoc" :stored-object="props.existingDoc"
:can-edit="props.existingDoc?.status === 'ready'" :can-edit="props.existingDoc?.status === 'ready'"
:can-download="true" :can-download="true"
:dav-link="dav_link_href" :dav-link="dav_link_href"
:dav-link-expiration="dav_link_expiration" :dav-link-expiration="dav_link_expiration"
/> />
</li> </li>
<li> <li>
<button <button
v-if="allowRemove" v-if="allowRemove"
class="btn btn-delete" class="btn btn-delete"
@click="onRemoveDocument($event)" @click="onRemoveDocument($event)"
></button> ></button>
</li> </li>
</ul> </ul>
</div> </div>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@@ -1,46 +1,46 @@
<script setup lang="ts"> <script setup lang="ts">
interface FileIconConfig { interface FileIconConfig {
type: string; type: string;
} }
const props = defineProps<FileIconConfig>(); const props = defineProps<FileIconConfig>();
</script> </script>
<template> <template>
<i class="fa fa-file-pdf-o" v-if="props.type === 'application/pdf'"></i> <i class="fa fa-file-pdf-o" v-if="props.type === 'application/pdf'"></i>
<i <i
class="fa fa-file-word-o" class="fa fa-file-word-o"
v-else-if="props.type === 'application/vnd.oasis.opendocument.text'" v-else-if="props.type === 'application/vnd.oasis.opendocument.text'"
></i> ></i>
<i <i
class="fa fa-file-word-o" class="fa fa-file-word-o"
v-else-if=" v-else-if="
props.type === props.type ===
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
" "
></i> ></i>
<i <i
class="fa fa-file-word-o" class="fa fa-file-word-o"
v-else-if="props.type === 'application/msword'" v-else-if="props.type === 'application/msword'"
></i> ></i>
<i <i
class="fa fa-file-excel-o" class="fa fa-file-excel-o"
v-else-if=" v-else-if="
props.type === props.type ===
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
" "
></i> ></i>
<i <i
class="fa fa-file-excel-o" class="fa fa-file-excel-o"
v-else-if="props.type === 'application/vnd.ms-excel'" v-else-if="props.type === 'application/vnd.ms-excel'"
></i> ></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/jpeg'"></i>
<i class="fa fa-file-image-o" v-else-if="props.type === 'image/png'"></i> <i class="fa fa-file-image-o" v-else-if="props.type === 'image/png'"></i>
<i <i
class="fa fa-file-archive-o" class="fa fa-file-archive-o"
v-else-if="props.type === 'application/x-zip-compressed'" v-else-if="props.type === 'application/x-zip-compressed'"
></i> ></i>
<i class="fa fa-file-code-o" v-else></i> <i class="fa fa-file-code-o" v-else></i>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@@ -1,28 +1,28 @@
<template> <template>
<a :class="props.classes" @click="download_and_open($event)" ref="btn"> <a :class="props.classes" @click="download_and_open($event)" ref="btn">
<i class="fa fa-file-pdf-o"></i> <i class="fa fa-file-pdf-o"></i>
Télécharger en pdf Télécharger en pdf
</a> </a>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import {
build_convert_link, build_convert_link,
download_and_decrypt_doc, download_and_decrypt_doc,
download_doc, download_doc,
} from "./helpers"; } from "./helpers";
import mime from "mime"; import mime from "mime";
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import { StoredObject } from "../../types"; import { StoredObject } from "../../types";
interface ConvertButtonConfig { interface ConvertButtonConfig {
storedObject: StoredObject; storedObject: StoredObject;
classes: Record<string, boolean>; classes: Record<string, boolean>;
filename?: string; filename?: string;
} }
interface DownloadButtonState { interface DownloadButtonState {
content: null | string; content: null | string;
} }
const props = defineProps<ConvertButtonConfig>(); const props = defineProps<ConvertButtonConfig>();
@@ -30,34 +30,36 @@ const state: DownloadButtonState = reactive({ content: null });
const btn = ref<HTMLAnchorElement | null>(null); const btn = ref<HTMLAnchorElement | null>(null);
async function download_and_open(event: Event): Promise<void> { 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) { if (null === state.content) {
event.preventDefault(); event.preventDefault();
const raw = await download_doc(build_convert_link(props.storedObject.uuid)); const raw = await download_doc(
state.content = window.URL.createObjectURL(raw); build_convert_link(props.storedObject.uuid),
);
state.content = window.URL.createObjectURL(raw);
button.href = window.URL.createObjectURL(raw); button.href = window.URL.createObjectURL(raw);
button.type = "application/pdf"; button.type = "application/pdf";
button.download = props.filename + ".pdf" || "document.pdf"; button.download = props.filename + ".pdf" || "document.pdf";
} }
button.click(); button.click();
const reset_pending = setTimeout(reset_state, 45000); const reset_pending = setTimeout(reset_state, 45000);
} }
function reset_state(): void { function reset_state(): void {
state.content = null; state.content = null;
btn.value?.removeAttribute("download"); btn.value?.removeAttribute("download");
btn.value?.removeAttribute("href"); btn.value?.removeAttribute("href");
btn.value?.removeAttribute("type"); btn.value?.removeAttribute("type");
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
i.fa::before { i.fa::before {
color: var(--bs-dropdown-link-hover-color); color: var(--bs-dropdown-link-hover-color);
} }
</style> </style>

View File

@@ -3,13 +3,13 @@ import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import { computed, reactive } from "vue"; import { computed, reactive } from "vue";
export interface DesktopEditButtonConfig { export interface DesktopEditButtonConfig {
editLink: null; editLink: null;
classes: Record<string, boolean>; classes: Record<string, boolean>;
expirationLink: number | Date; expirationLink: number | Date;
} }
interface DesktopEditButtonState { interface DesktopEditButtonState {
modalOpened: boolean; modalOpened: boolean;
} }
const state: DesktopEditButtonState = reactive({ modalOpened: false }); const state: DesktopEditButtonState = reactive({ modalOpened: false });
@@ -17,76 +17,80 @@ const state: DesktopEditButtonState = reactive({ modalOpened: false });
const props = defineProps<DesktopEditButtonConfig>(); const props = defineProps<DesktopEditButtonConfig>();
const buildCommand = computed<string>( const buildCommand = computed<string>(
() => "vnd.libreoffice.command:ofe|u|" + props.editLink, () => "vnd.libreoffice.command:ofe|u|" + props.editLink,
); );
const editionUntilFormatted = computed<string>(() => { const editionUntilFormatted = computed<string>(() => {
let d; let d;
if (props.expirationLink instanceof Date) { if (props.expirationLink instanceof Date) {
d = props.expirationLink; d = props.expirationLink;
} else { } else {
d = new Date(props.expirationLink * 1000); d = new Date(props.expirationLink * 1000);
} }
console.log(props.expirationLink); console.log(props.expirationLink);
return new Intl.DateTimeFormat(undefined, { return new Intl.DateTimeFormat(undefined, {
dateStyle: "long", dateStyle: "long",
timeStyle: "medium", timeStyle: "medium",
}).format(d); }).format(d);
}); });
</script> </script>
<template> <template>
<teleport to="body"> <teleport to="body">
<modal v-if="state.modalOpened" @close="state.modalOpened = false"> <modal v-if="state.modalOpened" @close="state.modalOpened = false">
<template v-slot:body> <template v-slot:body>
<div class="desktop-edit"> <div class="desktop-edit">
<p class="center">Veuillez enregistrer vos modifications avant le</p> <p class="center">
<p> Veuillez enregistrer vos modifications avant le
<strong>{{ editionUntilFormatted }}</strong> </p>
</p> <p>
<strong>{{ editionUntilFormatted }}</strong>
</p>
<p> <p>
<a class="btn btn-primary" :href="buildCommand" <a class="btn btn-primary" :href="buildCommand"
>Ouvrir le document pour édition</a >Ouvrir le document pour édition</a
> >
</p> </p>
<p> <p>
<small <small
>Le document peut être édité uniquement en utilisant Libre >Le document peut être édité uniquement en utilisant
Office.</small Libre Office.</small
> >
</p> </p>
<p> <p>
<small <small
>En cas d'échec lors de l'enregistrement, sauver le document sur >En cas d'échec lors de l'enregistrement, sauver le
le poste de travail avant de le déposer à nouveau ici.</small document sur le poste de travail avant de le déposer
> à nouveau ici.</small
</p> >
</p>
<p> <p>
<small <small
>Vous pouvez naviguez sur d'autres pages pendant l'édition.</small >Vous pouvez naviguez sur d'autres pages pendant
> l'édition.</small
</p> >
</div> </p>
</template> </div>
</modal> </template>
</teleport> </modal>
<a :class="props.classes" @click="state.modalOpened = true"> </teleport>
<i class="fa fa-desktop"></i> <a :class="props.classes" @click="state.modalOpened = true">
Éditer sur le bureau <i class="fa fa-desktop"></i>
</a> Éditer sur le bureau
</a>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.desktop-edit { .desktop-edit {
text-align: center; text-align: center;
} }
i.fa::before { i.fa::before {
color: var(--bs-dropdown-link-hover-color); color: var(--bs-dropdown-link-hover-color);
} }
</style> </style>

View File

@@ -1,26 +1,26 @@
<template> <template>
<a <a
v-if="!state.is_ready" v-if="!state.is_ready"
:class="props.classes" :class="props.classes"
@click="download_and_open()" @click="download_and_open()"
title="T&#233;l&#233;charger" title="T&#233;l&#233;charger"
> >
<i class="fa fa-download"></i> <i class="fa fa-download"></i>
<template v-if="displayActionStringInButton">Télécharger</template> <template v-if="displayActionStringInButton">Télécharger</template>
</a> </a>
<a <a
v-else v-else
:class="props.classes" :class="props.classes"
target="_blank" target="_blank"
:type="props.atVersion.type" :type="props.atVersion.type"
:download="buildDocumentName()" :download="buildDocumentName()"
:href="state.href_url" :href="state.href_url"
ref="open_button" ref="open_button"
title="Ouvrir" title="Ouvrir"
> >
<i class="fa fa-external-link"></i> <i class="fa fa-external-link"></i>
<template v-if="displayActionStringInButton">Ouvrir</template> <template v-if="displayActionStringInButton">Ouvrir</template>
</a> </a>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -30,109 +30,112 @@ import mime from "mime";
import { StoredObject, StoredObjectVersion } from "../../types"; import { StoredObject, StoredObjectVersion } from "../../types";
interface DownloadButtonConfig { interface DownloadButtonConfig {
storedObject: StoredObject; storedObject: StoredObject;
atVersion: StoredObjectVersion; atVersion: StoredObjectVersion;
classes: Record<string, boolean>; classes: Record<string, boolean>;
filename?: string; filename?: string;
/** /**
* if true, display the action string into the button. If false, displays only * if true, display the action string into the button. If false, displays only
* the icon * the icon
*/ */
displayActionStringInButton?: boolean; displayActionStringInButton?: boolean;
/** /**
* if true, will download directly the file on load * if true, will download directly the file on load
*/ */
directDownload?: boolean; directDownload?: boolean;
} }
interface DownloadButtonState { interface DownloadButtonState {
is_ready: boolean; is_ready: boolean;
is_running: boolean; is_running: boolean;
href_url: string; href_url: string;
} }
const props = withDefaults(defineProps<DownloadButtonConfig>(), { const props = withDefaults(defineProps<DownloadButtonConfig>(), {
displayActionStringInButton: true, displayActionStringInButton: true,
directDownload: false, directDownload: false,
}); });
const state: DownloadButtonState = reactive({ const state: DownloadButtonState = reactive({
is_ready: false, is_ready: false,
is_running: false, is_running: false,
href_url: "#", href_url: "#",
}); });
const open_button = ref<HTMLAnchorElement | null>(null); const open_button = ref<HTMLAnchorElement | null>(null);
function buildDocumentName(): string { 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) { if ("" === document_name || null === document_name) {
document_name = "document"; document_name = "document";
} }
const ext = mime.getExtension(props.atVersion.type); const ext = mime.getExtension(props.atVersion.type);
if (null !== ext) { if (null !== ext) {
return document_name + "." + ext; return document_name + "." + ext;
} }
return document_name; return document_name;
} }
async function download_and_open(): Promise<void> { async function download_and_open(): Promise<void> {
if (state.is_running) { if (state.is_running) {
console.log("state is running, aborting"); console.log("state is running, aborting");
return; return;
} }
state.is_running = true; state.is_running = true;
if (state.is_ready) { if (state.is_ready) {
console.log("state is ready. This should not happens"); console.log("state is ready. This should not happens");
return; return;
} }
let raw; let raw;
try { try {
raw = await download_and_decrypt_doc(props.storedObject, props.atVersion); raw = await download_and_decrypt_doc(
} catch (e) { props.storedObject,
console.error("error while downloading and decrypting document"); props.atVersion,
console.error(e); );
throw e; } catch (e) {
} console.error("error while downloading and decrypting document");
console.error(e);
throw e;
}
state.href_url = window.URL.createObjectURL(raw); state.href_url = window.URL.createObjectURL(raw);
state.is_running = false; state.is_running = false;
state.is_ready = true; state.is_ready = true;
if (!props.directDownload) { if (!props.directDownload) {
await nextTick(); await nextTick();
open_button.value?.click(); open_button.value?.click();
console.log("open button should have been clicked"); console.log("open button should have been clicked");
setTimeout(reset_state, 45000); setTimeout(reset_state, 45000);
} }
} }
function reset_state(): void { function reset_state(): void {
state.href_url = "#"; state.href_url = "#";
state.is_ready = false; state.is_ready = false;
state.is_running = false; state.is_running = false;
} }
onMounted(() => { onMounted(() => {
if (props.directDownload) { if (props.directDownload) {
download_and_open(); download_and_open();
} }
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
i.fa::before { i.fa::before {
color: var(--bs-dropdown-link-hover-color); color: var(--bs-dropdown-link-hover-color);
} }
i.fa { i.fa {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
</style> </style>

View File

@@ -1,20 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import HistoryButtonModal from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonModal.vue"; import HistoryButtonModal from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonModal.vue";
import { import {
StoredObject, StoredObject,
StoredObjectVersionWithPointInTime, StoredObjectVersionWithPointInTime,
} from "./../../types"; } from "./../../types";
import { computed, reactive, ref, useTemplateRef } from "vue"; import { computed, reactive, ref, useTemplateRef } from "vue";
import { get_versions } from "./HistoryButton/api"; import { get_versions } from "./HistoryButton/api";
interface HistoryButtonConfig { interface HistoryButtonConfig {
storedObject: StoredObject; storedObject: StoredObject;
canEdit: boolean; canEdit: boolean;
} }
interface HistoryButtonState { interface HistoryButtonState {
versions: StoredObjectVersionWithPointInTime[]; versions: StoredObjectVersionWithPointInTime[];
loaded: boolean; loaded: boolean;
} }
const props = defineProps<HistoryButtonConfig>(); const props = defineProps<HistoryButtonConfig>();
@@ -22,47 +22,47 @@ const state = reactive<HistoryButtonState>({ versions: [], loaded: false });
const modal = useTemplateRef<typeof HistoryButtonModal>("modal"); const modal = useTemplateRef<typeof HistoryButtonModal>("modal");
const download_version_and_open_modal = async function (): Promise<void> { const download_version_and_open_modal = async function (): Promise<void> {
if (null !== modal.value) { if (null !== modal.value) {
modal.value.open(); modal.value.open();
} else { } else {
console.log("modal is null"); console.log("modal is null");
} }
if (!state.loaded) { if (!state.loaded) {
const versions = await get_versions(props.storedObject); const versions = await get_versions(props.storedObject);
for (const version of versions) { for (const version of versions) {
state.versions.push(version); state.versions.push(version);
}
state.loaded = true;
} }
state.loaded = true;
}
}; };
const onRestoreVersion = ({ const onRestoreVersion = ({
newVersion, newVersion,
}: { }: {
newVersion: StoredObjectVersionWithPointInTime; newVersion: StoredObjectVersionWithPointInTime;
}) => { }) => {
state.versions.unshift(newVersion); state.versions.unshift(newVersion);
}; };
</script> </script>
<template> <template>
<a @click="download_version_and_open_modal" class="dropdown-item"> <a @click="download_version_and_open_modal" class="dropdown-item">
<history-button-modal <history-button-modal
ref="modal" ref="modal"
:versions="state.versions" :versions="state.versions"
:stored-object="storedObject" :stored-object="storedObject"
:can-edit="canEdit" :can-edit="canEdit"
@restore-version="onRestoreVersion" @restore-version="onRestoreVersion"
></history-button-modal> ></history-button-modal>
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
Historique Historique
</a> </a>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
i.fa::before { i.fa::before {
color: var(--bs-dropdown-link-hover-color); color: var(--bs-dropdown-link-hover-color);
} }
</style> </style>

View File

@@ -1,26 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
StoredObject, StoredObject,
StoredObjectVersionWithPointInTime, StoredObjectVersionWithPointInTime,
} from "./../../../types"; } from "./../../../types";
import HistoryButtonListItem from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonListItem.vue"; import HistoryButtonListItem from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonListItem.vue";
import { computed, reactive } from "vue"; import { computed, reactive } from "vue";
interface HistoryButtonListConfig { interface HistoryButtonListConfig {
versions: StoredObjectVersionWithPointInTime[]; versions: StoredObjectVersionWithPointInTime[];
storedObject: StoredObject; storedObject: StoredObject;
canEdit: boolean; canEdit: boolean;
} }
const emit = defineEmits<{ const emit = defineEmits<{
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]; restoreVersion: [newVersion: StoredObjectVersionWithPointInTime];
}>(); }>();
interface HistoryButtonListState { interface HistoryButtonListState {
/** /**
* Contains the number of the newly created version when a version is restored. * Contains the number of the newly created version when a version is restored.
*/ */
restored: number; restored: number;
} }
const props = defineProps<HistoryButtonListConfig>(); const props = defineProps<HistoryButtonListConfig>();
@@ -28,11 +28,11 @@ const props = defineProps<HistoryButtonListConfig>();
const state = reactive<HistoryButtonListState>({ restored: -1 }); const state = reactive<HistoryButtonListState>({ restored: -1 });
const higher_version = computed<number>(() => const higher_version = computed<number>(() =>
props.versions.reduce( props.versions.reduce(
(accumulator: number, version: StoredObjectVersionWithPointInTime) => (accumulator: number, version: StoredObjectVersionWithPointInTime) =>
Math.max(accumulator, version.version), Math.max(accumulator, version.version),
-1, -1,
), ),
); );
/** /**
@@ -41,32 +41,32 @@ const higher_version = computed<number>(() =>
* internally, keep track of the newly restored version * internally, keep track of the newly restored version
*/ */
const onRestored = ({ const onRestored = ({
newVersion, newVersion,
}: { }: {
newVersion: StoredObjectVersionWithPointInTime; newVersion: StoredObjectVersionWithPointInTime;
}) => { }) => {
state.restored = newVersion.version; state.restored = newVersion.version;
emit("restoreVersion", { newVersion }); emit("restoreVersion", { newVersion });
}; };
</script> </script>
<template> <template>
<template v-if="props.versions.length > 0"> <template v-if="props.versions.length > 0">
<div class="container"> <div class="container">
<template v-for="v in props.versions" :key="v.id"> <template v-for="v in props.versions" :key="v.id">
<history-button-list-item <history-button-list-item
:version="v" :version="v"
:can-edit="canEdit" :can-edit="canEdit"
:is-current="higher_version === v.version" :is-current="higher_version === v.version"
:stored-object="storedObject" :stored-object="storedObject"
@restore-version="onRestored" @restore-version="onRestored"
></history-button-list-item> ></history-button-list-item>
</template> </template>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<p>Chargement des versions</p> <p>Chargement des versions</p>
</template> </template>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
StoredObject, StoredObject,
StoredObjectPointInTime, StoredObjectPointInTime,
StoredObjectVersionWithPointInTime, StoredObjectVersionWithPointInTime,
} from "./../../../types"; } from "./../../../types";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue"; import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date"; import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
@@ -12,173 +12,185 @@ import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/Downloa
import { computed } from "vue"; import { computed } from "vue";
interface HistoryButtonListItemConfig { interface HistoryButtonListItemConfig {
version: StoredObjectVersionWithPointInTime; version: StoredObjectVersionWithPointInTime;
storedObject: StoredObject; storedObject: StoredObject;
canEdit: boolean; canEdit: boolean;
isCurrent: boolean; isCurrent: boolean;
} }
const emit = defineEmits<{ const emit = defineEmits<{
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]; restoreVersion: [newVersion: StoredObjectVersionWithPointInTime];
}>(); }>();
const props = defineProps<HistoryButtonListItemConfig>(); const props = defineProps<HistoryButtonListItemConfig>();
const onRestore = ({ const onRestore = ({
newVersion, newVersion,
}: { }: {
newVersion: StoredObjectVersionWithPointInTime; newVersion: StoredObjectVersionWithPointInTime;
}) => { }) => {
emit("restoreVersion", { newVersion }); emit("restoreVersion", { newVersion });
}; };
const isKeptBeforeConversion = computed<boolean>(() => { const isKeptBeforeConversion = computed<boolean>(() => {
if ("point-in-times" in props.version) { if ("point-in-times" in props.version) {
return props.version["point-in-times"].reduce( return props.version["point-in-times"].reduce(
(accumulator: boolean, pit: StoredObjectPointInTime) => (accumulator: boolean, pit: StoredObjectPointInTime) =>
accumulator || "keep-before-conversion" === pit.reason, accumulator || "keep-before-conversion" === pit.reason,
false, false,
); );
} else { } else {
return false; return false;
} }
}); });
const isRestored = computed<boolean>( 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>( 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<{ const classes = computed<{
row: true; row: true;
"row-hover": true; "row-hover": true;
"blinking-1": boolean; "blinking-1": boolean;
"blinking-2": boolean; "blinking-2": boolean;
}>(() => ({ }>(() => ({
row: true, row: true,
"row-hover": true, "row-hover": true,
"blinking-1": props.isRestored && 0 === props.version.version % 2, "blinking-1": props.isRestored && 0 === props.version.version % 2,
"blinking-2": props.isRestored && 1 === props.version.version % 2, "blinking-2": props.isRestored && 1 === props.version.version % 2,
})); }));
</script> </script>
<template> <template>
<div :class="classes"> <div :class="classes">
<div <div
class="col-12 tags" class="col-12 tags"
v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated" v-if="
> isCurrent ||
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span> isKeptBeforeConversion ||
<span class="badge bg-info" v-if="isKeptBeforeConversion" isRestored ||
>Conservée avant conversion dans un autre format</span isDuplicated
> "
<span class="badge bg-info" v-if="isRestored" >
>Restaurée depuis la version <span class="badge bg-success" v-if="isCurrent"
{{ version["from-restored"]?.version + 1 }}</span >Version actuelle</span
> >
<span class="badge bg-info" v-if="isDuplicated" <span class="badge bg-info" v-if="isKeptBeforeConversion"
>Dupliqué depuis un autre document</span >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>&nbsp;#{{ version.version + 1 }}&nbsp;</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> </div>
<div class="col-12">
<file-icon :type="version.type"></file-icon>
<span
><strong>&nbsp;#{{ version.version + 1 }}&nbsp;</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> </template>
<style scoped lang="scss"> <style scoped lang="scss">
div.tags { div.tags {
span.badge:not(:last-child) { span.badge:not(:last-child) {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
} }
// to make the animation restart, we have the same animation twice, // to make the animation restart, we have the same animation twice,
// and alternate between both // and alternate between both
.blinking-1 { .blinking-1 {
animation-name: backgroundColorPalette-1; animation-name: backgroundColorPalette-1;
animation-duration: 8s; animation-duration: 8s;
animation-iteration-count: 1; animation-iteration-count: 1;
animation-direction: normal; animation-direction: normal;
animation-timing-function: linear; animation-timing-function: linear;
} }
@keyframes backgroundColorPalette-1 { @keyframes backgroundColorPalette-1 {
0% { 0% {
background: var(--bs-chill-green-dark); background: var(--bs-chill-green-dark);
} }
25% { 25% {
background: var(--bs-chill-green); background: var(--bs-chill-green);
} }
65% { 65% {
background: var(--bs-chill-beige); background: var(--bs-chill-beige);
} }
100% { 100% {
background: unset; background: unset;
} }
} }
.blinking-2 { .blinking-2 {
animation-name: backgroundColorPalette-2; animation-name: backgroundColorPalette-2;
animation-duration: 8s; animation-duration: 8s;
animation-iteration-count: 1; animation-iteration-count: 1;
animation-direction: normal; animation-direction: normal;
animation-timing-function: linear; animation-timing-function: linear;
} }
@keyframes backgroundColorPalette-2 { @keyframes backgroundColorPalette-2 {
0% { 0% {
background: var(--bs-chill-green-dark); background: var(--bs-chill-green-dark);
} }
25% { 25% {
background: var(--bs-chill-green); background: var(--bs-chill-green);
} }
65% { 65% {
background: var(--bs-chill-beige); background: var(--bs-chill-beige);
} }
100% { 100% {
background: unset; background: unset;
} }
} }
</style> </style>

View File

@@ -3,54 +3,54 @@ import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import { reactive } from "vue"; import { reactive } from "vue";
import HistoryButtonList from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonList.vue"; import HistoryButtonList from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonList.vue";
import { import {
StoredObject, StoredObject,
StoredObjectVersionWithPointInTime, StoredObjectVersionWithPointInTime,
} from "./../../../types"; } from "./../../../types";
interface HistoryButtonListConfig { interface HistoryButtonListConfig {
versions: StoredObjectVersionWithPointInTime[]; versions: StoredObjectVersionWithPointInTime[];
storedObject: StoredObject; storedObject: StoredObject;
canEdit: boolean; canEdit: boolean;
} }
const emit = defineEmits<{ const emit = defineEmits<{
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]; restoreVersion: [newVersion: StoredObjectVersionWithPointInTime];
}>(); }>();
interface HistoryButtonModalState { interface HistoryButtonModalState {
opened: boolean; opened: boolean;
} }
const props = defineProps<HistoryButtonListConfig>(); const props = defineProps<HistoryButtonListConfig>();
const state = reactive<HistoryButtonModalState>({ opened: false }); const state = reactive<HistoryButtonModalState>({ opened: false });
const open = () => { const open = () => {
state.opened = true; state.opened = true;
}; };
const onRestoreVersion = (payload: { const onRestoreVersion = (payload: {
newVersion: StoredObjectVersionWithPointInTime; newVersion: StoredObjectVersionWithPointInTime;
}) => emit("restoreVersion", payload); }) => emit("restoreVersion", payload);
defineExpose({ open }); defineExpose({ open });
</script> </script>
<template> <template>
<Teleport to="body"> <Teleport to="body">
<modal v-if="state.opened" @close="state.opened = false"> <modal v-if="state.opened" @close="state.opened = false">
<template v-slot:header> <template v-slot:header>
<h3>Historique des versions du document</h3> <h3>Historique des versions du document</h3>
</template> </template>
<template v-slot:body> <template v-slot:body>
<p>Les versions sont conservées pendant 90 jours.</p> <p>Les versions sont conservées pendant 90 jours.</p>
<history-button-list <history-button-list
:versions="props.versions" :versions="props.versions"
:can-edit="canEdit" :can-edit="canEdit"
:stored-object="storedObject" :stored-object="storedObject"
@restore-version="onRestoreVersion" @restore-version="onRestoreVersion"
></history-button-list> ></history-button-list>
</template> </template>
</modal> </modal>
</Teleport> </Teleport>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@@ -1,17 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
StoredObjectVersionPersisted, StoredObjectVersionPersisted,
StoredObjectVersionWithPointInTime, StoredObjectVersionWithPointInTime,
} from "../../../types"; } from "../../../types";
import { useToast } from "vue-toast-notification"; import { useToast } from "vue-toast-notification";
import { restore_version } from "./api"; import { restore_version } from "./api";
interface RestoreVersionButtonProps { interface RestoreVersionButtonProps {
storedObjectVersion: StoredObjectVersionPersisted; storedObjectVersion: StoredObjectVersionPersisted;
} }
const emit = defineEmits<{ const emit = defineEmits<{
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]; restoreVersion: [newVersion: StoredObjectVersionWithPointInTime];
}>(); }>();
const props = defineProps<RestoreVersionButtonProps>(); const props = defineProps<RestoreVersionButtonProps>();
@@ -19,21 +19,21 @@ const props = defineProps<RestoreVersionButtonProps>();
const $toast = useToast(); const $toast = useToast();
const restore_version_fn = async () => { const restore_version_fn = async () => {
const newVersion = await restore_version(props.storedObjectVersion); const newVersion = await restore_version(props.storedObjectVersion);
$toast.success("Version restaurée"); $toast.success("Version restaurée");
emit("restoreVersion", { newVersion }); emit("restoreVersion", { newVersion });
}; };
</script> </script>
<template> <template>
<button <button
class="btn btn-outline-action" class="btn btn-outline-action"
@click="restore_version_fn" @click="restore_version_fn"
title="Restaurer" title="Restaurer"
> >
<i class="fa fa-rotate-left"></i> Restaurer <i class="fa fa-rotate-left"></i> Restaurer
</button> </button>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@@ -1,33 +1,33 @@
import { import {
StoredObject, StoredObject,
StoredObjectVersionPersisted, StoredObjectVersionPersisted,
StoredObjectVersionWithPointInTime, StoredObjectVersionWithPointInTime,
} from "../../../types"; } from "../../../types";
import { import {
fetchResults, fetchResults,
makeFetch, makeFetch,
} from "../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods"; } from "../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
export const get_versions = async ( export const get_versions = async (
storedObject: StoredObject, storedObject: StoredObject,
): Promise<StoredObjectVersionWithPointInTime[]> => { ): Promise<StoredObjectVersionWithPointInTime[]> => {
const versions = await fetchResults<StoredObjectVersionWithPointInTime>( const versions = await fetchResults<StoredObjectVersionWithPointInTime>(
`/api/1.0/doc-store/stored-object/${storedObject.uuid}/versions`, `/api/1.0/doc-store/stored-object/${storedObject.uuid}/versions`,
); );
return versions.sort( return versions.sort(
( (
a: StoredObjectVersionWithPointInTime, a: StoredObjectVersionWithPointInTime,
b: StoredObjectVersionWithPointInTime, b: StoredObjectVersionWithPointInTime,
) => b.version - a.version, ) => b.version - a.version,
); );
}; };
export const restore_version = async ( export const restore_version = async (
version: StoredObjectVersionPersisted, version: StoredObjectVersionPersisted,
): Promise<StoredObjectVersionWithPointInTime> => { ): Promise<StoredObjectVersionWithPointInTime> => {
return await makeFetch<null, StoredObjectVersionWithPointInTime>( return await makeFetch<null, StoredObjectVersionWithPointInTime>(
"POST", "POST",
`/api/1.0/doc-store/stored-object/restore-from-version/${version.id}`, `/api/1.0/doc-store/stored-object/restore-from-version/${version.id}`,
); );
}; };

View File

@@ -1,27 +1,29 @@
<template> <template>
<a <a
:class="Object.assign(props.classes, { btn: true })" :class="Object.assign(props.classes, { btn: true })"
@click="beforeLeave($event)" @click="beforeLeave($event)"
:href="build_wopi_editor_link(props.storedObject.uuid, props.returnPath)" :href="
> build_wopi_editor_link(props.storedObject.uuid, props.returnPath)
<i class="fa fa-paragraph"></i> "
Editer en ligne >
</a> <i class="fa fa-paragraph"></i>
Editer en ligne
</a>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import WopiEditButton from "./WopiEditButton.vue"; import WopiEditButton from "./WopiEditButton.vue";
import { build_wopi_editor_link } from "./helpers"; import { build_wopi_editor_link } from "./helpers";
import { import {
StoredObject, StoredObject,
WopiEditButtonExecutableBeforeLeaveFunction, WopiEditButtonExecutableBeforeLeaveFunction,
} from "../../types"; } from "../../types";
interface WopiEditButtonConfig { interface WopiEditButtonConfig {
storedObject: StoredObject; storedObject: StoredObject;
returnPath?: string; returnPath?: string;
classes: Record<string, boolean>; classes: Record<string, boolean>;
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction; executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction;
} }
const props = defineProps<WopiEditButtonConfig>(); const props = defineProps<WopiEditButtonConfig>();
@@ -29,24 +31,24 @@ const props = defineProps<WopiEditButtonConfig>();
let executed = false; let executed = false;
async function beforeLeave(event: Event): Promise<true> { async function beforeLeave(event: Event): Promise<true> {
if (props.executeBeforeLeave === undefined || executed === true) { if (props.executeBeforeLeave === undefined || executed === true) {
return Promise.resolve(true);
}
event.preventDefault();
await props.executeBeforeLeave();
executed = true;
const link = event.target as HTMLAnchorElement;
link.click();
return Promise.resolve(true); return Promise.resolve(true);
}
event.preventDefault();
await props.executeBeforeLeave();
executed = true;
const link = event.target as HTMLAnchorElement;
link.click();
return Promise.resolve(true);
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
i.fa::before { i.fa::before {
color: var(--bs-dropdown-link-hover-color); color: var(--bs-dropdown-link-hover-color);
} }
</style> </style>

View File

@@ -1,230 +1,235 @@
import { import {
StoredObject, StoredObject,
StoredObjectStatus, StoredObjectStatus,
StoredObjectStatusChange, StoredObjectStatusChange,
StoredObjectVersion, StoredObjectVersion,
} from "../../types"; } from "../../types";
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods"; import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
const MIMES_EDIT = new Set([ const MIMES_EDIT = new Set([
"application/vnd.ms-powerpoint", "application/vnd.ms-powerpoint",
"application/vnd.ms-excel", "application/vnd.ms-excel",
"application/vnd.oasis.opendocument.text", "application/vnd.oasis.opendocument.text",
"application/vnd.oasis.opendocument.text-flat-xml", "application/vnd.oasis.opendocument.text-flat-xml",
"application/vnd.oasis.opendocument.spreadsheet", "application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.oasis.opendocument.spreadsheet-flat-xml", "application/vnd.oasis.opendocument.spreadsheet-flat-xml",
"application/vnd.oasis.opendocument.presentation", "application/vnd.oasis.opendocument.presentation",
"application/vnd.oasis.opendocument.presentation-flat-xml", "application/vnd.oasis.opendocument.presentation-flat-xml",
"application/vnd.oasis.opendocument.graphics", "application/vnd.oasis.opendocument.graphics",
"application/vnd.oasis.opendocument.graphics-flat-xml", "application/vnd.oasis.opendocument.graphics-flat-xml",
"application/vnd.oasis.opendocument.chart", "application/vnd.oasis.opendocument.chart",
"application/msword", "application/msword",
"application/vnd.ms-excel", "application/vnd.ms-excel",
"application/vnd.ms-powerpoint", "application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-word.document.macroEnabled.12", "application/vnd.ms-word.document.macroEnabled.12",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel.sheet.binary.macroEnabled.12", "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
"application/vnd.ms-excel.sheet.macroEnabled.12", "application/vnd.ms-excel.sheet.macroEnabled.12",
"application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.ms-powerpoint.presentation.macroEnabled.12", "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
"application/x-dif-document", "application/x-dif-document",
"text/spreadsheet", "text/spreadsheet",
"text/csv", "text/csv",
"application/x-dbase", "application/x-dbase",
"text/rtf", "text/rtf",
"text/plain", "text/plain",
"application/vnd.openxmlformats-officedocument.presentationml.slideshow", "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
]); ]);
const MIMES_VIEW = new Set([ const MIMES_VIEW = new Set([
...MIMES_EDIT, ...MIMES_EDIT,
[ [
"image/svg+xml", "image/svg+xml",
"application/vnd.sun.xml.writer", "application/vnd.sun.xml.writer",
"application/vnd.sun.xml.calc", "application/vnd.sun.xml.calc",
"application/vnd.sun.xml.impress", "application/vnd.sun.xml.impress",
"application/vnd.sun.xml.draw", "application/vnd.sun.xml.draw",
"application/vnd.sun.xml.writer.global", "application/vnd.sun.xml.writer.global",
"application/vnd.sun.xml.writer.template", "application/vnd.sun.xml.writer.template",
"application/vnd.sun.xml.calc.template", "application/vnd.sun.xml.calc.template",
"application/vnd.sun.xml.impress.template", "application/vnd.sun.xml.impress.template",
"application/vnd.sun.xml.draw.template", "application/vnd.sun.xml.draw.template",
"application/vnd.oasis.opendocument.text-master", "application/vnd.oasis.opendocument.text-master",
"application/vnd.oasis.opendocument.text-template", "application/vnd.oasis.opendocument.text-template",
"application/vnd.oasis.opendocument.text-master-template", "application/vnd.oasis.opendocument.text-master-template",
"application/vnd.oasis.opendocument.spreadsheet-template", "application/vnd.oasis.opendocument.spreadsheet-template",
"application/vnd.oasis.opendocument.presentation-template", "application/vnd.oasis.opendocument.presentation-template",
"application/vnd.oasis.opendocument.graphics-template", "application/vnd.oasis.opendocument.graphics-template",
"application/vnd.ms-word.template.macroEnabled.12", "application/vnd.ms-word.template.macroEnabled.12",
"application/vnd.openxmlformats-officedocument.spreadsheetml.template", "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
"application/vnd.ms-excel.template.macroEnabled.12", "application/vnd.ms-excel.template.macroEnabled.12",
"application/vnd.openxmlformats-officedocument.presentationml.template", "application/vnd.openxmlformats-officedocument.presentationml.template",
"application/vnd.ms-powerpoint.template.macroEnabled.12", "application/vnd.ms-powerpoint.template.macroEnabled.12",
"application/vnd.wordperfect", "application/vnd.wordperfect",
"application/x-aportisdoc", "application/x-aportisdoc",
"application/x-hwp", "application/x-hwp",
"application/vnd.ms-works", "application/vnd.ms-works",
"application/x-mswrite", "application/x-mswrite",
"application/vnd.lotus-1-2-3", "application/vnd.lotus-1-2-3",
"image/cgm", "image/cgm",
"image/vnd.dxf", "image/vnd.dxf",
"image/x-emf", "image/x-emf",
"image/x-wmf", "image/x-wmf",
"application/coreldraw", "application/coreldraw",
"application/vnd.visio2013", "application/vnd.visio2013",
"application/vnd.visio", "application/vnd.visio",
"application/vnd.ms-visio.drawing", "application/vnd.ms-visio.drawing",
"application/x-mspublisher", "application/x-mspublisher",
"application/x-sony-bbeb", "application/x-sony-bbeb",
"application/x-gnumeric", "application/x-gnumeric",
"application/macwriteii", "application/macwriteii",
"application/x-iwork-numbers-sffnumbers", "application/x-iwork-numbers-sffnumbers",
"application/vnd.oasis.opendocument.text-web", "application/vnd.oasis.opendocument.text-web",
"application/x-pagemaker", "application/x-pagemaker",
"application/x-fictionbook+xml", "application/x-fictionbook+xml",
"application/clarisworks", "application/clarisworks",
"image/x-wpg", "image/x-wpg",
"application/x-iwork-pages-sffpages", "application/x-iwork-pages-sffpages",
"application/x-iwork-keynote-sffkey", "application/x-iwork-keynote-sffkey",
"application/x-abiword", "application/x-abiword",
"image/x-freehand", "image/x-freehand",
"application/vnd.sun.xml.chart", "application/vnd.sun.xml.chart",
"application/x-t602", "application/x-t602",
"image/bmp", "image/bmp",
"image/png", "image/png",
"image/gif", "image/gif",
"image/tiff", "image/tiff",
"image/jpg", "image/jpg",
"image/jpeg", "image/jpeg",
"application/pdf", "application/pdf",
], ],
]); ]);
export interface SignedUrlGet { export interface SignedUrlGet {
method: "GET" | "HEAD"; method: "GET" | "HEAD";
url: string; url: string;
expires: number; expires: number;
object_name: string; object_name: string;
} }
function is_extension_editable(mimeType: string): boolean { function is_extension_editable(mimeType: string): boolean {
return MIMES_EDIT.has(mimeType); return MIMES_EDIT.has(mimeType);
} }
function is_extension_viewable(mimeType: string): boolean { function is_extension_viewable(mimeType: string): boolean {
return MIMES_VIEW.has(mimeType); return MIMES_VIEW.has(mimeType);
} }
function build_convert_link(uuid: string) { function build_convert_link(uuid: string) {
return `/chill/wopi/convert/${uuid}`; return `/chill/wopi/convert/${uuid}`;
} }
function build_download_info_link( function build_download_info_link(
storedObject: StoredObject, storedObject: StoredObject,
atVersion: null | StoredObjectVersion, atVersion: null | StoredObjectVersion,
): string { ): string {
const url = `/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/get`; const url = `/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/get`;
if (null !== atVersion) { if (null !== atVersion) {
const params = new URLSearchParams({ version: atVersion.filename }); const params = new URLSearchParams({ version: atVersion.filename });
return url + "?" + params.toString(); return url + "?" + params.toString();
} }
return url; return url;
} }
async function download_info_link( async function download_info_link(
storedObject: StoredObject, storedObject: StoredObject,
atVersion: null | StoredObjectVersion, atVersion: null | StoredObjectVersion,
): Promise<SignedUrlGet> { ): Promise<SignedUrlGet> {
return makeFetch("GET", build_download_info_link(storedObject, atVersion)); return makeFetch("GET", build_download_info_link(storedObject, atVersion));
} }
function build_wopi_editor_link(uuid: string, returnPath?: string) { function build_wopi_editor_link(uuid: string, returnPath?: string) {
if (returnPath === undefined) { if (returnPath === undefined) {
returnPath = returnPath =
window.location.pathname + window.location.search + window.location.hash; window.location.pathname +
} window.location.search +
window.location.hash;
}
return ( return (
`/chill/wopi/edit/${uuid}?returnPath=` + encodeURIComponent(returnPath) `/chill/wopi/edit/${uuid}?returnPath=` + encodeURIComponent(returnPath)
); );
} }
function download_doc(url: string): Promise<Blob> { function download_doc(url: string): Promise<Blob> {
return window.fetch(url).then((r) => { return window.fetch(url).then((r) => {
if (r.ok) { if (r.ok) {
return r.blob(); return r.blob();
} }
throw new Error("Could not download document"); throw new Error("Could not download document");
}); });
} }
async function download_and_decrypt_doc( async function download_and_decrypt_doc(
storedObject: StoredObject, storedObject: StoredObject,
atVersion: null | StoredObjectVersion, atVersion: null | StoredObjectVersion,
): Promise<Blob> { ): Promise<Blob> {
const algo = "AES-CBC"; const algo = "AES-CBC";
const atVersionToDownload = atVersion ?? storedObject.currentVersion; const atVersionToDownload = atVersion ?? storedObject.currentVersion;
if (null === atVersionToDownload) { if (null === atVersionToDownload) {
throw new Error("no version associated to stored object"); throw new Error("no version associated to stored object");
} }
// sometimes, the downloadInfo may be embedded into the storedObject // sometimes, the downloadInfo may be embedded into the storedObject
console.log("storedObject", storedObject); console.log("storedObject", storedObject);
let downloadInfo; let downloadInfo;
if ( if (
typeof storedObject._links !== "undefined" && typeof storedObject._links !== "undefined" &&
typeof storedObject._links.downloadLink !== "undefined" typeof storedObject._links.downloadLink !== "undefined"
) { ) {
downloadInfo = storedObject._links.downloadLink; downloadInfo = storedObject._links.downloadLink;
} else { } else {
downloadInfo = await download_info_link(storedObject, atVersionToDownload); downloadInfo = await download_info_link(
} storedObject,
atVersionToDownload,
);
}
const rawResponse = await window.fetch(downloadInfo.url); const rawResponse = await window.fetch(downloadInfo.url);
if (!rawResponse.ok) { if (!rawResponse.ok) {
throw new Error( throw new Error(
"error while downloading raw file " + "error while downloading raw file " +
rawResponse.status + rawResponse.status +
" " + " " +
rawResponse.statusText, rawResponse.statusText,
); );
} }
if (atVersionToDownload.iv.length === 0) { if (atVersionToDownload.iv.length === 0) {
return rawResponse.blob(); return rawResponse.blob();
} }
const rawBuffer = await rawResponse.arrayBuffer(); const rawBuffer = await rawResponse.arrayBuffer();
try { try {
const key = await window.crypto.subtle.importKey( const key = await window.crypto.subtle.importKey(
"jwk", "jwk",
atVersionToDownload.keyInfos, atVersionToDownload.keyInfos,
{ name: algo }, { name: algo },
false, false,
["decrypt"], ["decrypt"],
); );
const iv = Uint8Array.from(atVersionToDownload.iv); const iv = Uint8Array.from(atVersionToDownload.iv);
const decrypted = await window.crypto.subtle.decrypt( const decrypted = await window.crypto.subtle.decrypt(
{ name: algo, iv: iv }, { name: algo, iv: iv },
key, key,
rawBuffer, rawBuffer,
); );
return Promise.resolve(new Blob([decrypted])); return Promise.resolve(new Blob([decrypted]));
} catch (e) { } catch (e) {
console.error("encounter error while keys and decrypt operations"); console.error("encounter error while keys and decrypt operations");
console.error(e); console.error(e);
throw e; throw e;
} }
} }
/** /**
@@ -234,45 +239,48 @@ async function download_and_decrypt_doc(
* storage. * storage.
*/ */
async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob> { async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob> {
if (null === storedObject.currentVersion) { if (null === storedObject.currentVersion) {
throw new Error("the stored object does not count any version"); throw new Error("the stored object does not count any version");
} }
if (storedObject.currentVersion?.type === "application/pdf") { if (storedObject.currentVersion?.type === "application/pdf") {
return download_and_decrypt_doc(storedObject, storedObject.currentVersion); return download_and_decrypt_doc(
} storedObject,
storedObject.currentVersion,
);
}
const convertLink = build_convert_link(storedObject.uuid); const convertLink = build_convert_link(storedObject.uuid);
const response = await fetch(convertLink); const response = await fetch(convertLink);
if (!response.ok) { if (!response.ok) {
throw new Error("Could not convert the document: " + response.status); throw new Error("Could not convert the document: " + response.status);
} }
return response.blob(); return response.blob();
} }
async function is_object_ready( async function is_object_ready(
storedObject: StoredObject, storedObject: StoredObject,
): Promise<StoredObjectStatusChange> { ): Promise<StoredObjectStatusChange> {
const new_status_response = await window.fetch( const new_status_response = await window.fetch(
`/api/1.0/doc-store/stored-object/${storedObject.uuid}/is-ready`, `/api/1.0/doc-store/stored-object/${storedObject.uuid}/is-ready`,
); );
if (!new_status_response.ok) { if (!new_status_response.ok) {
throw new Error("could not fetch the new status"); throw new Error("could not fetch the new status");
} }
return await new_status_response.json(); return await new_status_response.json();
} }
export { export {
build_convert_link, build_convert_link,
build_wopi_editor_link, build_wopi_editor_link,
download_and_decrypt_doc, download_and_decrypt_doc,
download_doc, download_doc,
download_doc_as_pdf, download_doc_as_pdf,
is_extension_editable, is_extension_editable,
is_extension_viewable, is_extension_viewable,
is_object_ready, is_object_ready,
}; };

View File

@@ -43,17 +43,11 @@ class StoredObjectVersionNormalizer implements NormalizerInterface, NormalizerAw
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, [...$context, UserNormalizer::AT_DATE => $object->getCreatedAt()]), 'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, [...$context, UserNormalizer::AT_DATE => $object->getCreatedAt()]),
]; ];
$normalizationGroups = $context[AbstractNormalizer::GROUPS] ?? []; if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
if (is_string($normalizationGroups)) {
$normalizationGroups = [$normalizationGroups];
}
if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $normalizationGroups, true)) {
$data['point-in-times'] = $this->normalizer->normalize($object->getPointInTimes(), $format, $context); $data['point-in-times'] = $this->normalizer->normalize($object->getPointInTimes(), $format, $context);
} }
if (in_array(self::WITH_RESTORED_CONTEXT, $normalizationGroups, true)) { if (in_array(self::WITH_RESTORED_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
$data['from-restored'] = $this->normalizer->normalize($object->getCreatedFrom(), $format, [AbstractNormalizer::GROUPS => ['read']]); $data['from-restored'] = $this->normalizer->normalize($object->getCreatedFrom(), $format, [AbstractNormalizer::GROUPS => ['read']]);
} }

View File

@@ -23,6 +23,8 @@ See the document: Voir le document
document: document:
Any title: Aucun titre Any title: Aucun titre
replace: Remplacer
Add: Ajouter un document
generic_doc: generic_doc:
filter: filter:

View File

@@ -1,28 +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\EventBundle\Controller\Admin;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\Request;
class EventBudgetKindController extends CRUDController
{
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{
/* @var QueryBuilder $query */
$query->addOrderBy('e.type', 'ASC');
return parent::orderQuery($action, $query, $request, $paginator);
}
}

View File

@@ -23,11 +23,11 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\Type\PickPersonDynamicType; use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
use Chill\PersonBundle\Privacy\PrivacyEvent; use Chill\PersonBundle\Privacy\PrivacyEvent;
use Doctrine\Persistence\ManagerRegistry;
use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Csv; use PhpOffice\PhpSpreadsheet\Writer\Csv;
use PhpOffice\PhpSpreadsheet\Writer\Ods; use PhpOffice\PhpSpreadsheet\Writer\Ods;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -41,8 +41,6 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
@@ -60,8 +58,7 @@ final class EventController extends AbstractController
private readonly TranslatorInterface $translator, private readonly TranslatorInterface $translator,
private readonly PaginatorFactory $paginator, private readonly PaginatorFactory $paginator,
private readonly Security $security, private readonly Security $security,
private readonly ManagerRegistry $managerRegistry, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
private readonly NormalizerInterface $normalizer,
) {} ) {}
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/delete', name: 'chill_event__event_delete', requirements: ['event_id' => '\d+'], methods: ['GET', 'POST', 'DELETE'])] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/delete', name: 'chill_event__event_delete', requirements: ['event_id' => '\d+'], methods: ['GET', 'POST', 'DELETE'])]
@@ -78,7 +75,6 @@ final class EventController extends AbstractController
/** @var array $participations */ /** @var array $participations */
$participations = $event->getParticipations(); $participations = $event->getParticipations();
$budgetElements = $event->getBudgetElements();
$form = $this->createDeleteForm($event_id); $form = $this->createDeleteForm($event_id);
@@ -90,10 +86,6 @@ final class EventController extends AbstractController
$em->remove($participation); $em->remove($participation);
} }
foreach ($budgetElements as $e) {
$em->remove($e);
}
$em->remove($event); $em->remove($event);
$em->flush(); $em->flush();
@@ -111,7 +103,7 @@ final class EventController extends AbstractController
} }
return $this->render('@ChillEvent/Event/confirm_delete.html.twig', [ return $this->render('@ChillEvent/Event/confirm_delete.html.twig', [
'id' => $event->getId(), 'event_id' => $event->getId(),
'delete_form' => $form->createView(), 'delete_form' => $form->createView(),
]); ]);
} }
@@ -177,8 +169,6 @@ final class EventController extends AbstractController
/** /**
* Displays a form to create a new Event entity. * Displays a form to create a new Event entity.
*
* @throws ExceptionInterface
*/ */
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/new', name: 'chill_event__event_new', methods: ['GET', 'POST'])] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/new', name: 'chill_event__event_new', methods: ['GET', 'POST'])]
public function newAction(?Center $center, Request $request): Response public function newAction(?Center $center, Request $request): Response
@@ -209,23 +199,26 @@ final class EventController extends AbstractController
$this->addFlash('success', $this->translator $this->addFlash('success', $this->translator
->trans('The event was created')); ->trans('The event was created'));
return $this->redirectToRoute('chill_event__event_show', ['id' => $entity->getId()]); return $this->redirectToRoute('chill_event__event_show', ['event_id' => $entity->getId()]);
} }
$entity_array = $this->normalizer->normalize($entity, 'json', ['groups' => 'read']);
return $this->render('@ChillEvent/Event/new.html.twig', [ return $this->render('@ChillEvent/Event/new.html.twig', [
'entity' => $entity, 'entity' => $entity,
'form' => $form->createView(), 'form' => $form->createView(),
'entity_json' => $entity_array,
]); ]);
} }
/**
* First step of new Event form.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/new/pick-center', name: 'chill_event__event_new_pickcenter', options: [null])] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/new/pick-center', name: 'chill_event__event_new_pickcenter', options: [null])]
public function newPickCenterAction(): Response public function newPickCenterAction(): Response
{ {
$role = 'CHILL_EVENT_CREATE'; $role = 'CHILL_EVENT_CREATE';
/**
* @var Center $centers
*/
$centers = $this->authorizationHelper->getReachableCenters($this->getUser(), $role); $centers = $this->authorizationHelper->getReachableCenters($this->getUser(), $role);
if (1 === \count($centers)) { if (1 === \count($centers)) {
@@ -245,7 +238,7 @@ final class EventController extends AbstractController
->add('center_id', EntityType::class, [ ->add('center_id', EntityType::class, [
'class' => Center::class, 'class' => Center::class,
'choices' => $centers, 'choices' => $centers,
'placeholder' => $this->translator->trans('Pick a center'), 'placeholder' => '',
'label' => 'To which centre should the event be associated ?', 'label' => 'To which centre should the event be associated ?',
]) ])
->add('submit', SubmitType::class, [ ->add('submit', SubmitType::class, [
@@ -258,7 +251,16 @@ final class EventController extends AbstractController
]); ]);
} }
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{id}/show', name: 'chill_event__event_show')] /**
* Finds and displays a Event entity.
*
* @ParamConverter("event", options={"id": "event_id"})
*
* @return Response
*
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/show', name: 'chill_event__event_show')]
public function showAction(Event $event, Request $request) public function showAction(Event $event, Request $request)
{ {
if (!$event) { if (!$event) {
@@ -315,7 +317,7 @@ final class EventController extends AbstractController
$this->addFlash('success', $this->translator->trans('The event was updated')); $this->addFlash('success', $this->translator->trans('The event was updated'));
return $this->redirectToRoute('chill_event__event_show', ['id' => $event_id]); return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]);
} }
return $this->render('@ChillEvent/Event/edit.html.twig', [ return $this->render('@ChillEvent/Event/edit.html.twig', [

View File

@@ -15,15 +15,11 @@ use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\EventType; use Chill\EventBundle\Entity\EventType;
use Chill\EventBundle\Repository\EventACLAwareRepositoryInterface; use Chill\EventBundle\Repository\EventACLAwareRepositoryInterface;
use Chill\EventBundle\Repository\EventTypeRepository; use Chill\EventBundle\Repository\EventTypeRepository;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Pagination\PaginatorFactoryInterface; use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper; use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory; use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Form\Type\PickPersonDynamicType; use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormFactoryInterface;
@@ -33,18 +29,17 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment; use Twig\Environment;
final class EventListController extends AbstractController final readonly class EventListController
{ {
public function __construct( public function __construct(
private readonly Environment $environment, private Environment $environment,
private readonly EventACLAwareRepositoryInterface $eventACLAwareRepository, private EventACLAwareRepositoryInterface $eventACLAwareRepository,
private readonly EventTypeRepository $eventTypeRepository, private EventTypeRepository $eventTypeRepository,
private readonly FilterOrderHelperFactory $filterOrderHelperFactory, private FilterOrderHelperFactory $filterOrderHelperFactory,
private readonly FormFactoryInterface $formFactory, private FormFactoryInterface $formFactory,
private readonly PaginatorFactoryInterface $paginatorFactory, private PaginatorFactoryInterface $paginatorFactory,
private readonly TranslatableStringHelperInterface $translatableStringHelper, private TranslatableStringHelperInterface $translatableStringHelper,
private readonly UrlGeneratorInterface $urlGenerator, private UrlGeneratorInterface $urlGenerator,
private readonly AuthorizationHelper $authorizationHelper,
) {} ) {}
#[Route(path: '{_locale}/event/event/list', name: 'chill_event_event_list')] #[Route(path: '{_locale}/event/event/list', name: 'chill_event_event_list')]
@@ -55,8 +50,6 @@ final class EventListController extends AbstractController
'q' => (string) $filter->getQueryString(), 'q' => (string) $filter->getQueryString(),
'dates' => $filter->getDateRangeData('dates'), 'dates' => $filter->getDateRangeData('dates'),
'event_types' => $filter->getEntityChoiceData('event_types'), 'event_types' => $filter->getEntityChoiceData('event_types'),
'responsables' => $filter->getUserPickerData('responsables'),
'centers' => $filter->getEntityChoiceData('centers'),
]; ];
$total = $this->eventACLAwareRepository->countAllViewable($filterData); $total = $this->eventACLAwareRepository->countAllViewable($filterData);
$pagination = $this->paginatorFactory->create($total); $pagination = $this->paginatorFactory->create($total);
@@ -80,7 +73,6 @@ final class EventListController extends AbstractController
private function buildFilterOrder(): FilterOrderHelper private function buildFilterOrder(): FilterOrderHelper
{ {
$types = $this->eventTypeRepository->findAllActive(); $types = $this->eventTypeRepository->findAllActive();
$centers = $this->authorizationHelper->getReachableCenters($this->getUser(), EventVoter::SEE);
$builder = $this->filterOrderHelperFactory->create(__METHOD__); $builder = $this->filterOrderHelperFactory->create(__METHOD__);
$builder $builder
@@ -88,16 +80,6 @@ final class EventListController extends AbstractController
->addSearchBox(['name']) ->addSearchBox(['name'])
->addEntityChoice('event_types', 'event.filter.event_types', EventType::class, $types, [ ->addEntityChoice('event_types', 'event.filter.event_types', EventType::class, $types, [
'choice_label' => fn (EventType $e) => $this->translatableStringHelper->localize($e->getName()), 'choice_label' => fn (EventType $e) => $this->translatableStringHelper->localize($e->getName()),
'expanded' => false,
'required' => false,
'attr' => ['class' => 'select2'],
])
->addUserPicker('responsables', 'event.filter.pick_responsable', ['multiple' => true, 'required' => false])
->addEntityChoice('centers', 'event.filter.center', Center::class, $centers, [
'choice_label' => fn (Center $c) => $c->getName(),
'expanded' => false,
'required' => false,
'attr' => ['class' => 'select2'],
]); ]);
return $builder->build(); return $builder->build();

View File

@@ -1,44 +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\EventBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
class EventThemeController extends CRUDController
{
protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface
{
if ('new' === $action) {
return parent::createFormFor($action, $entity, $formClass, ['step' => 'create']);
}
if ('edit' === $action) {
return parent::createFormFor($action, $entity, $formClass, ['step' => 'edit']);
}
throw new \LogicException('action is not supported: '.$action);
}
/**
* @param QueryBuilder|mixed $query
*/
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator): QueryBuilder
{
/* @var QueryBuilder $query */
return $query->orderBy('e.ordering', 'ASC')
->addOrderBy('e.id', 'ASC');
}
}

View File

@@ -228,7 +228,7 @@ final class ParticipationController extends AbstractController
} }
return $this->redirectToRoute('chill_event__event_show', [ return $this->redirectToRoute('chill_event__event_show', [
'id' => $participation->getEvent()->getId(), 'event_id' => $participation->getEvent()->getId(),
]); ]);
} }
@@ -242,7 +242,7 @@ final class ParticipationController extends AbstractController
/** /**
* @param int $participation_id * @param int $participation_id
*/ */
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/{participation_id}/delete', name: 'chill_event_participation_delete', requirements: ['participation_id' => '\d+'])] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/{participation_id}/delete', name: 'chill_event_participation_delete', requirements: ['participation_id' => '\d+'], methods: ['GET', 'DELETE'])]
public function deleteAction($participation_id, Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse public function deleteAction($participation_id, Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse
{ {
$em = $this->managerRegistry->getManager(); $em = $this->managerRegistry->getManager();
@@ -273,7 +273,7 @@ final class ParticipationController extends AbstractController
); );
return $this->redirectToRoute('chill_event__event_show', [ return $this->redirectToRoute('chill_event__event_show', [
'id' => $event->getId(), 'event_id' => $event->getId(),
]); ]);
} }
} }
@@ -442,7 +442,7 @@ final class ParticipationController extends AbstractController
)); ));
return $this->redirectToRoute('chill_event__event_show', [ return $this->redirectToRoute('chill_event__event_show', [
'id' => $participation->getEvent()->getId(), 'event_id' => $participation->getEvent()->getId(),
]); ]);
} }

View File

@@ -11,12 +11,6 @@ declare(strict_types=1);
namespace Chill\EventBundle\DependencyInjection; namespace Chill\EventBundle\DependencyInjection;
use Chill\EventBundle\Controller\Admin\EventBudgetKindController;
use Chill\EventBundle\Controller\EventThemeController;
use Chill\EventBundle\Entity\EventBudgetKind;
use Chill\EventBundle\Entity\EventTheme;
use Chill\EventBundle\Form\EventBudgetKindType;
use Chill\EventBundle\Form\EventThemeType;
use Chill\EventBundle\Security\EventVoter; use Chill\EventBundle\Security\EventVoter;
use Chill\EventBundle\Security\ParticipationVoter; use Chill\EventBundle\Security\ParticipationVoter;
use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\FileLocator;
@@ -32,10 +26,7 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension;
*/ */
class ChillEventExtension extends Extension implements PrependExtensionInterface class ChillEventExtension extends Extension implements PrependExtensionInterface
{ {
/** public function load(array $configs, ContainerBuilder $container)
* @throws \Exception
*/
public function load(array $configs, ContainerBuilder $container): void
{ {
$configuration = new Configuration(); $configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs); $config = $this->processConfiguration($configuration, $configs);
@@ -54,17 +45,16 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
/** (non-PHPdoc). /** (non-PHPdoc).
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend() * @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
*/ */
public function prepend(ContainerBuilder $container): void public function prepend(ContainerBuilder $container)
{ {
$this->prependAuthorization($container); $this->prependAuthorization($container);
$this->prependCruds($container);
$this->prependRoute($container); $this->prependRoute($container);
} }
/** /**
* add authorization hierarchy. * add authorization hierarchy.
*/ */
protected function prependAuthorization(ContainerBuilder $container): void protected function prependAuthorization(ContainerBuilder $container)
{ {
$container->prependExtensionConfig('security', [ $container->prependExtensionConfig('security', [
'role_hierarchy' => [ 'role_hierarchy' => [
@@ -80,7 +70,7 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
/** /**
* add route to route loader for chill. * add route to route loader for chill.
*/ */
protected function prependRoute(ContainerBuilder $container): void protected function prependRoute(ContainerBuilder $container)
{ {
// add routes for custom bundle // add routes for custom bundle
$container->prependExtensionConfig('chill_main', [ $container->prependExtensionConfig('chill_main', [
@@ -91,54 +81,4 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
], ],
]); ]);
} }
protected function prependCruds(ContainerBuilder $container): void
{
$container->prependExtensionConfig('chill_main', [
'cruds' => [
[
'class' => EventTheme::class,
'name' => 'event_theme',
'base_path' => '/admin/event/theme',
'form_class' => EventThemeType::class,
'controller' => EventThemeController::class,
'actions' => [
'index' => [
'template' => '@ChillEvent/Admin/EventTheme/index.html.twig',
'role' => 'ROLE_ADMIN',
],
'new' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/EventTheme/new.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/EventTheme/edit.html.twig',
],
],
],
[
'class' => EventBudgetKind::class,
'name' => 'event_budget_kind',
'base_path' => '/admin/event/budget',
'form_class' => EventBudgetKindType::class,
'controller' => EventBudgetKindController::class,
'actions' => [
'index' => [
'template' => '@ChillEvent/Admin/BudgetKind/index.html.twig',
'role' => 'ROLE_ADMIN',
],
'new' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/BudgetKind/new.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/BudgetKind/edit.html.twig',
],
],
],
],
]);
}
} }

View File

@@ -1,18 +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\EventBundle\Entity;
enum BudgetTypeEnum: string
{
case CHARGE = 'Charge';
case RESOURCE = 'Resource';
}

View File

@@ -23,13 +23,10 @@ use Chill\MainBundle\Entity\HasScopeInterface;
use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation as Serializer;
/** /**
* Class Event. * Class Event.
@@ -49,63 +46,35 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
#[ORM\ManyToOne(targetEntity: Scope::class)] #[ORM\ManyToOne(targetEntity: Scope::class)]
private ?Scope $circle = null; private ?Scope $circle = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE)]
private ?\DateTime $date = null; private ?\DateTime $date = null;
#[ORM\Id] #[ORM\Id]
#[ORM\Column(name: 'id', type: Types::INTEGER)] #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
#[ORM\GeneratedValue(strategy: 'AUTO')] #[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null; private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class)] #[ORM\ManyToOne(targetEntity: User::class)]
private ?User $moderator = null; private ?User $moderator = null;
/**
* @var Collection<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[Serializer\Groups(['read'])]
#[ORM\JoinTable('chill_event_animatorsintern')]
private Collection $animatorsIntern;
/**
* @var Collection<int, ThirdParty>
*/
#[ORM\ManyToMany(targetEntity: ThirdParty::class)]
#[Serializer\Groups(['read'])]
#[ORM\JoinTable('chill_event_animatorsextern')]
private Collection $animatorsExtern;
#[Assert\NotBlank] #[Assert\NotBlank]
#[Serializer\Groups(['read'])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 150)]
#[ORM\Column(type: Types::STRING, length: 150)]
private ?string $name = null; private ?string $name = null;
/** /**
* @var Collection<int, Participation> * @var Collection<int, Participation>
*/ */
#[ORM\OneToMany(mappedBy: 'event', targetEntity: Participation::class)] #[ORM\OneToMany(mappedBy: 'event', targetEntity: Participation::class)]
#[Serializer\Groups(['read'])]
private Collection $participations; private Collection $participations;
#[Assert\NotNull] #[Assert\NotNull]
#[Serializer\Groups(['read'])]
#[ORM\ManyToOne(targetEntity: EventType::class)] #[ORM\ManyToOne(targetEntity: EventType::class)]
private ?EventType $type = null; private ?EventType $type = null;
/**
* @var Collection<int, EventTheme>
*/
#[ORM\ManyToMany(targetEntity: EventTheme::class)]
#[Serializer\Groups(['read'])]
#[ORM\JoinTable('chill_event_eventtheme')]
private Collection $themes;
#[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'comment_')] #[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'comment_')]
private CommentEmbeddable $comment; private CommentEmbeddable $comment;
#[ORM\ManyToOne(targetEntity: Location::class)] #[ORM\ManyToOne(targetEntity: Location::class)]
#[Serializer\Groups(['read'])]
#[ORM\JoinColumn(nullable: true)] #[ORM\JoinColumn(nullable: true)]
private ?Location $location = null; private ?Location $location = null;
@@ -116,17 +85,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
#[ORM\JoinTable('chill_event_event_documents')] #[ORM\JoinTable('chill_event_event_documents')]
private Collection $documents; private Collection $documents;
/** #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DECIMAL, precision: 10, scale: 4, nullable: true, options: ['default' => '0.0'])]
* @var Collection<int, EventBudgetElement>
*/
#[ORM\OneToMany(mappedBy: 'event', targetEntity: EventBudgetElement::class, cascade: ['persist'])]
#[Serializer\Groups(['read'])]
private Collection $budgetElements;
/**
* @deprecated use budgetElements instead
*/
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 4, nullable: true, options: ['default' => '0.0'])]
private string $organizationCost = '0.0'; private string $organizationCost = '0.0';
/** /**
@@ -137,20 +96,6 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
$this->participations = new ArrayCollection(); $this->participations = new ArrayCollection();
$this->documents = new ArrayCollection(); $this->documents = new ArrayCollection();
$this->comment = new CommentEmbeddable(); $this->comment = new CommentEmbeddable();
$this->themes = new ArrayCollection();
$this->budgetElements = new ArrayCollection();
$this->animatorsIntern = new ArrayCollection();
$this->animatorsExtern = new ArrayCollection();
}
public function addBudgetElement(EventBudgetElement $budgetElement)
{
if (!$this->budgetElements->contains($budgetElement)) {
$this->budgetElements[] = $budgetElement;
$budgetElement->setEvent($this);
}
return $this;
} }
/** /**
@@ -181,79 +126,38 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
return $this; return $this;
} }
public function getThemes(): Collection /**
{ * @return Center
return $this->themes; */
} public function getCenter()
public function addTheme(EventTheme $theme): self
{
$this->themes->add($theme);
return $this;
}
public function removeTheme(EventTheme $theme): void
{
$this->themes->removeElement($theme);
}
public function getAnimatorsIntern(): Collection
{
return $this->animatorsIntern;
}
public function getAnimatorsExtern(): Collection
{
return $this->animatorsExtern;
}
public function addAnimatorsIntern(User $ai): self
{
$this->animatorsIntern->add($ai);
return $this;
}
public function removeAnimatorsIntern(User $ai): void
{
$this->animatorsIntern->removeElement($ai);
}
public function addAnimatorsExtern(ThirdParty $ae): self
{
$this->animatorsExtern->add($ae);
return $this;
}
public function removeAnimatorsExtern(ThirdParty $ae): void
{
$this->animatorsExtern->removeElement($ae);
}
public function getCenter(): Center
{ {
return $this->center; return $this->center;
} }
public function getCircle(): ?Scope /**
* @return Scope
*/
public function getCircle()
{ {
return $this->circle; return $this->circle;
} }
/** /**
* Get date. * Get date.
*
* @return \DateTime
*/ */
public function getDate(): ?\DateTime public function getDate()
{ {
return $this->date; return $this->date;
} }
/** /**
* Get id. * Get id.
*
* @return int
*/ */
public function getId(): ?int public function getId()
{ {
return $this->id; return $this->id;
} }
@@ -265,20 +169,14 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
/** /**
* Get label. * Get label.
*
* @return string
*/ */
public function getName(): ?string public function getName()
{ {
return $this->name; return $this->name;
} }
/**
* @return Collection<int, EventBudgetElement>
*/
public function getBudgetElements(): Collection
{
return $this->budgetElements;
}
/** /**
* @return Collection<int, Participation> * @return Collection<int, Participation>
*/ */
@@ -301,26 +199,26 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
/** /**
* @deprecated * @deprecated
*
* @return Scope
*/ */
public function getScope(): Scope public function getScope()
{ {
return $this->getCircle(); return $this->getCircle();
} }
public function getType(): ?EventType /**
* @return EventType
*/
public function getType()
{ {
return $this->type; return $this->type;
} }
public function removeBudgetElement(EventBudgetElement $budgetElement): void
{
$this->budgetElements->removeElement($budgetElement);
}
/** /**
* Remove participation. * Remove participation.
*/ */
public function removeParticipation(Participation $participation): void public function removeParticipation(Participation $participation)
{ {
$this->participations->removeElement($participation); $this->participations->removeElement($participation);
} }
@@ -416,17 +314,11 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
$this->documents = $documents; $this->documents = $documents;
} }
/**
* @deprecated
*/
public function getOrganizationCost(): string public function getOrganizationCost(): string
{ {
return $this->organizationCost; return $this->organizationCost;
} }
/**
* @deprecated
*/
public function setOrganizationCost(string $organizationCost): void public function setOrganizationCost(string $organizationCost): void
{ {
$this->organizationCost = $organizationCost; $this->organizationCost = $organizationCost;

View File

@@ -1,103 +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\EventBundle\Entity;
use Chill\EventBundle\Repository\EventThemeRepository;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: EventThemeRepository::class)]
#[ORM\Table(name: 'chill_event_budget_element')]
class EventBudgetElement
{
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
#[Assert\GreaterThan(value: 0)]
#[Assert\NotNull(message: 'The amount cannot be empty')]
#[ORM\Column(name: 'amount', type: Types::DECIMAL, precision: 10, scale: 2)]
private string $amount;
#[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'comment_budget_element_')]
private ?CommentEmbeddable $comment = null;
#[ORM\ManyToOne(targetEntity: Event::class)]
private Event $event;
#[ORM\ManyToOne(targetEntity: EventBudgetKind::class, inversedBy: 'EventBudgetElement')]
#[ORM\JoinColumn]
private EventBudgetKind $kind;
/* Getters and Setters */
public function getId(): ?int
{
return $this->id;
}
public function setId(?int $id): void
{
$this->id = $id;
}
public function getAmount(): float
{
return (float) $this->amount;
}
public function getComment(): ?CommentEmbeddable
{
return $this->comment;
}
public function getEvent(): Event
{
return $this->event;
}
public function getKind(): EventBudgetKind
{
return $this->kind;
}
public function setAmount(string $amount): self
{
$this->amount = $amount;
return $this;
}
public function setComment(?CommentEmbeddable $comment = null): self
{
$this->comment = $comment;
return $this;
}
public function setEvent(Event $event): self
{
$this->event = $event;
return $this;
}
public function setKind(EventBudgetKind $kind): self
{
$this->kind = $kind;
return $this;
}
}

Some files were not shown because too many files have changed in this diff Show More