mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-03-16 19:07:48 +00:00
Compare commits
340 Commits
master
...
ticket/64-
| Author | SHA1 | Date | |
|---|---|---|---|
|
f55dee4f8a
|
|||
| 9261bbadc3 | |||
|
|
df15c2fee7 | ||
|
e82cee2ce1
|
|||
|
d16de04d27
|
|||
|
1bbea33c4b
|
|||
|
310dd61870
|
|||
|
4ddbcf4037
|
|||
| d21d03d407 | |||
| 2f930ac138 | |||
|
6d3bcf48b5
|
|||
|
7136c682f2
|
|||
|
625e55056d
|
|||
|
6d617146b8
|
|||
| 65bbdc283d | |||
|
|
f0b3e63d66 | ||
|
14e0cba2e8
|
|||
| 41a31944b3 | |||
| 51607572de | |||
|
627e128977
|
|||
| 1fc5530707 | |||
|
|
b31778c068 | ||
|
305c6deb24
|
|||
|
d691098a21
|
|||
|
3f6bbbc1b3
|
|||
|
31e57d7507
|
|||
|
5c098a336d
|
|||
|
e107d20bea
|
|||
|
491fd81f9b
|
|||
|
8c2acbd166
|
|||
|
e291c7abec
|
|||
|
1e186fab58
|
|||
|
83a2c04537
|
|||
|
e89b33bc1a
|
|||
|
6b208e9962
|
|||
|
b0c63fab91
|
|||
|
6d4c4d2c74
|
|||
|
df6087d468
|
|||
|
ffac143ab9
|
|||
|
f1bf6023ff
|
|||
|
71e146e4f0
|
|||
|
4234377b7e
|
|||
|
1be82b3049
|
|||
|
808954df42
|
|||
|
3f8bb6c5c0
|
|||
|
23e1a0d36a
|
|||
|
a594d86346
|
|||
|
870907804b
|
|||
|
e9e6c05e3d
|
|||
|
532f2dd842
|
|||
|
d14d4d4d8f
|
|||
| a22cbe0239 | |||
| 98902bdeb8 | |||
|
592a0f3698
|
|||
|
d469eb19ad
|
|||
| 4765d4fe28 | |||
|
|
30bcb85549 | ||
|
189a9337b4
|
|||
|
c030232a73
|
|||
|
d4f9726f90
|
|||
|
8740025dbd
|
|||
|
6d8ef035ea
|
|||
|
60eab628ee
|
|||
|
1fd559b722
|
|||
|
b526e802d7
|
|||
|
60937152c3
|
|||
|
6d2e78ce55
|
|||
|
e566f60a4a
|
|||
|
c06531cddb
|
|||
| 61ca700bbe | |||
|
|
b43aeebc3c | ||
| 056e2dcc5f | |||
| e57d1ac696 | |||
|
4a1da25fee
|
|||
| 0eff1d2e79 | |||
| 3928b2cc7a | |||
|
02783e5391
|
|||
|
4f51ef81ad
|
|||
|
4637dc692c
|
|||
|
be3b9f0f56
|
|||
|
ee006f55d6
|
|||
|
13b1c45271
|
|||
|
ad2b6d63ac
|
|||
|
bfbde078b7
|
|||
|
d42a1296c4
|
|||
|
4b7e3c1601
|
|||
|
6ea9af588b
|
|||
|
0fd76d3fa8
|
|||
|
34af53130b
|
|||
|
a1fd395868
|
|||
|
b8a7cbb321
|
|||
|
6124eb9e34
|
|||
|
a5b06de92a
|
|||
|
52404956d2
|
|||
|
4207efd6bf
|
|||
|
840fde4ad4
|
|||
|
3611ea2518
|
|||
|
bbd4292cb9
|
|||
|
54f8c92240
|
|||
|
5330befc8f
|
|||
|
c19206be0c
|
|||
|
5ff374d2fa
|
|||
|
4a73aaae94
|
|||
|
ff2c567d05
|
|||
|
a734e84f28
|
|||
|
4367ed086e
|
|||
|
3227bfcd3a
|
|||
|
8d29fb260a
|
|||
|
bda0743c63
|
|||
|
d9b730627f
|
|||
|
27548ad654
|
|||
|
bec7297039
|
|||
|
852523e644
|
|||
|
c05d0aad47
|
|||
|
1c0ed9abc8
|
|||
|
9aed5cc216
|
|||
|
e4fe5bff68
|
|||
|
4c73c4d9d0
|
|||
| 38935edb93 | |||
|
|
e1ef65d4ca | ||
| ec9d0be70b | |||
|
|
0ba2cbc1e8 | ||
| e87429933a | |||
|
8e2e676e3d
|
|||
| e12ad563a3 | |||
|
|
711aa8db9b | ||
| e78d44953f | |||
| 18f67801c7 | |||
|
c815e6bc69
|
|||
| 807f2711fe | |||
|
|
cd594cd580 | ||
|
fb6b26bfb5
|
|||
|
c5cedb8bd6
|
|||
|
2665e43a61
|
|||
|
25561cdf63
|
|||
| 10b73e06e1 | |||
|
|
e7c04e34a9 | ||
| 164beee7c6 | |||
|
|
4d96eb9457 | ||
| fe2eba3b29 | |||
|
|
61d1232e31 | ||
| 6594d4f6a6 | |||
| 1a66a9e864 | |||
| 1b74c119dc | |||
| 14d88810f3 | |||
| 445a2c9358 | |||
| c8baf0a8aa | |||
| faed443a96 | |||
| bbf387d96f | |||
|
|
b7c9b60744 | ||
|
2bd303bbbe
|
|||
|
c5e6122d2c
|
|||
| 088b876e20 | |||
| 3400656d7c | |||
|
568c8be7fd
|
|||
|
538ecc42ea
|
|||
|
15d26d4b06
|
|||
|
d8bd9bd7cd
|
|||
|
dcdfba5ccd
|
|||
|
0204bdd38d
|
|||
|
392fd01b56
|
|||
| 35844f3b73 | |||
| 7506b918d7 | |||
| cfba291f2c | |||
|
|
04438c09d3 | ||
| 2a54d1b909 | |||
|
|
628eeac5e0 | ||
| a2263b3fa1 | |||
| 74796d0fb0 | |||
|
c19481e40a
|
|||
|
|
6eeb717b1a | ||
|
beb7c462da
|
|||
|
|
dbf363a9e8 | ||
|
64a2f7c9ed
|
|||
| f26d9739c8 | |||
|
afa5edc1d8
|
|||
|
42d6c9e672
|
|||
|
2b22d4cb7c
|
|||
|
c8e5d0eb37
|
|||
| 2bf8ad5d6c | |||
| 11698a52e3 | |||
| 70955573e8 | |||
|
|
3df4043eb9 | ||
|
06e8264dde
|
|||
| b451d2c4a3 | |||
| 4f93150874 | |||
| 0566ab0910 | |||
|
|
f4eeee1598 | ||
| 33cf16fc13 | |||
| 0a331aab37 | |||
| d43b739654 | |||
| c72432efae | |||
|
95975fae55
|
|||
|
95a7efa138
|
|||
|
45e193ff6d
|
|||
|
dfc146ff3f
|
|||
| b41fcf66a9 | |||
|
|
a8dd1b3548 | ||
|
2b99a480ac
|
|||
|
7633e587bb
|
|||
|
fc61dfdf3a
|
|||
|
f1a5b5c49e
|
|||
|
ec685dcd47
|
|||
|
631ae3eedd
|
|||
|
440a7837ac
|
|||
|
e0abf34784
|
|||
|
377ae9a9dc
|
|||
|
034dc30e30
|
|||
|
d615111a0f
|
|||
|
ffb756c712
|
|||
|
69daccb860
|
|||
|
16435423cf
|
|||
|
697b4ab436
|
|||
|
67d804e28e
|
|||
|
cf41fa9574
|
|||
|
b8b325f7d7
|
|||
|
e97bd8c4ef
|
|||
|
e28d7df533
|
|||
|
4b20b1bc01
|
|||
|
b15733076c
|
|||
|
25be5c9ea3
|
|||
|
b035020c6f
|
|||
|
128101dc46
|
|||
|
5f2711023e
|
|||
|
bdf2ed4bbd
|
|||
|
1df542603e
|
|||
|
80bcc68ce5
|
|||
|
154fc3e2f6
|
|||
|
e45af94c78
|
|||
|
166a6fde20
|
|||
|
631f047338
|
|||
|
a777588bb8
|
|||
|
ca78d112c2
|
|||
|
bcfd317d83
|
|||
|
348740f073
|
|||
|
0d74f0980f
|
|||
|
be19dc00db
|
|||
|
643028ffd6
|
|||
|
ac4e2e5bf2
|
|||
|
498572b96e
|
|||
|
d2a61ce69b
|
|||
|
a9c0567ee1
|
|||
|
76cec5b5a8
|
|||
|
efe8a67697
|
|||
|
26dfa9b028
|
|||
|
50025044d3
|
|||
|
e6202a2e34
|
|||
|
b863bd967d
|
|||
|
e65bcf7275
|
|||
|
e00ece4200
|
|||
|
640fd71402
|
|||
|
aae50ca290
|
|||
|
1fa483598b
|
|||
|
e4b6a468f8
|
|||
|
|
66c7758023 | ||
|
|
4750d2c24e | ||
|
|
ca05e3d979 | ||
|
|
a20f9b4f86 | ||
|
|
c73c1eb8d5 | ||
|
|
8778bb0731 | ||
|
|
c7d20eebc5 | ||
|
|
b9e130c159 | ||
|
|
3e8bc94af3 | ||
|
|
0c914c9f9f | ||
|
|
580a60c939 | ||
|
|
4996ac3b7c | ||
|
|
2a23bf19cb | ||
|
|
650d2596d9 | ||
|
|
2bdd5a329e | ||
|
78d1776733
|
|||
|
66dc603c85
|
|||
|
3a8154ecce
|
|||
|
c81828e04f
|
|||
|
|
ec17dd7de2 | ||
| 76c076a5f3 | |||
|
|
f0045edd6c | ||
|
|
d00b76ffcd | ||
|
|
8991f0ef3f | ||
|
|
d6f5eae0c9 | ||
|
|
821fce3dd8 | ||
|
|
1d33ae1e39 | ||
|
|
19af0feb57 | ||
|
|
1c09e9a692 | ||
|
|
d72e748388 | ||
|
|
ab850b7b70 | ||
|
|
3f9745d8cf | ||
|
|
473765366a | ||
|
|
6500c24a7f | ||
|
|
1d00457141 | ||
|
|
eb0bf56cff | ||
|
|
7b8cd90cf1 | ||
|
|
a27d92aba0 | ||
|
|
85bdfb9e21 | ||
|
|
4cffcf4de1 | ||
|
|
b2587a688f | ||
|
|
c9f0e9843b | ||
|
|
b40ad9e445 | ||
|
|
3e10e47e29 | ||
|
|
2a1963e993 | ||
| 34c171659b | |||
|
2d8b960d9e
|
|||
| 831ae03431 | |||
|
45828174d1
|
|||
|
ed45f14a45
|
|||
|
fa67835690
|
|||
|
b434d38091
|
|||
|
|
800a952532 | ||
|
9f355032a8
|
|||
|
0bc6e62d4d
|
|||
|
46fb1c04b5
|
|||
| 3b2c3d1464 | |||
|
|
0bd6038160 | ||
|
|
baab8e94ce | ||
|
e2deb55fdb
|
|||
|
|
2cdfb50058 | ||
|
39d701feb2
|
|||
|
613ee8b186
|
|||
|
56a1a488de
|
|||
| 3f789ad0f4 | |||
|
467bea7cde
|
|||
|
670b8eb82b
|
|||
|
a9760b323f
|
|||
|
71a3a1924a
|
|||
|
ecdc1e25bf
|
|||
|
dd37427be1
|
|||
|
c8467df1b1
|
|||
|
4c89a954fa
|
|||
|
7c1f3b114d
|
|||
|
36bc4dab24
|
|||
|
4b30d92282
|
|||
|
75fbec5489
|
|||
|
912fdd6349
|
|||
|
5832542978
|
|||
|
5c3585a1ed
|
|||
|
a2f1e20ddf
|
|||
|
4d67702a76
|
|||
| 18e442db29 | |||
|
|
deb3d92189 | ||
|
a59ea7db31
|
|||
|
a738b0cac9
|
6
.changes/unreleased/Feature-20240530-160003.yaml
Normal file
6
.changes/unreleased/Feature-20240530-160003.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Feature
|
||||
body: |
|
||||
Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||
time: 2024-05-30T16:00:03.440767606+02:00
|
||||
custom:
|
||||
Issue: ""
|
||||
6
.changes/unreleased/Feature-20240531-190242.yaml
Normal file
6
.changes/unreleased/Feature-20240531-190242.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Feature
|
||||
body: |
|
||||
Upgrade CKEditor and refactor configuration with use of typescript
|
||||
time: 2024-05-31T19:02:42.776662753+02:00
|
||||
custom:
|
||||
Issue: ""
|
||||
6
.changes/unreleased/Feature-20250808-120802.yaml
Normal file
6
.changes/unreleased/Feature-20250808-120802.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Feature
|
||||
body: Create invitation list in user menu
|
||||
time: 2025-08-08T12:08:02.446361367+02:00
|
||||
custom:
|
||||
Issue: "385"
|
||||
SchemaChange: No schema change
|
||||
6
.changes/unreleased/Feature-20251007-155945.yaml
Normal file
6
.changes/unreleased/Feature-20251007-155945.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Feature
|
||||
body: Admin interface for Motive entity
|
||||
time: 2025-10-07T15:59:45.597029709+02:00
|
||||
custom:
|
||||
Issue: ""
|
||||
SchemaChange: No schema change
|
||||
6
.changes/unreleased/Feature-20251022-111552.yaml
Normal file
6
.changes/unreleased/Feature-20251022-111552.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Feature
|
||||
body: Add an admin interface for Motive entity
|
||||
time: 2025-10-22T11:15:52.13937955+02:00
|
||||
custom:
|
||||
Issue: ""
|
||||
SchemaChange: Add columns or tables
|
||||
6
.changes/unreleased/Fixed-20251106-161605.yaml
Normal file
6
.changes/unreleased/Fixed-20251106-161605.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Fixed
|
||||
body: Fix suggestion of referrer when creating notification for accompanyingPeriodWorkDocument
|
||||
time: 2025-11-06T16:16:05.861813041+01:00
|
||||
custom:
|
||||
Issue: "428"
|
||||
SchemaChange: No schema change
|
||||
@@ -1,6 +0,0 @@
|
||||
## v4.10.0 - 2025-12-09
|
||||
### Feature
|
||||
* [MR 928](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/928) [#462](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/462) Add the future appointments for the person in search results
|
||||
### Fixed
|
||||
* Remove dependency to package @symfony/ux-translator
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
## v4.10.1 - 2025-12-11
|
||||
### Fixed
|
||||
* Fix missing translation variable in NewLocation component
|
||||
* ([#476](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/476)) Fix display of header for ByActivityNumberAggregator
|
||||
* Fix use of ByActivityNumberAggregator in combination with activity count exports
|
||||
* ([#483](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/483)) Tentatively fix usage of CTRL+C in collabora editor with chrome / edge browser
|
||||
@@ -1,9 +0,0 @@
|
||||
## v4.11.0 - 2025-12-17
|
||||
### Feature
|
||||
* ([#478](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/478)) Add filtering to admin lists: social actions, social issues, goals, results, and evaluations
|
||||
### Fixed
|
||||
* ([#466](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/466)) Fix migration query after previous fix
|
||||
|
||||
* Fix translation key/value
|
||||
|
||||
Cannot start with % and should be wrapped in "".
|
||||
@@ -1,16 +0,0 @@
|
||||
## v4.12.0 - 2026-01-15
|
||||
### Feature
|
||||
* ([#473](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/473)) Display version of chill bundles in application footer
|
||||
* Increase the delay before removing stale workflow from 90 days to 180 days.
|
||||
### Fixed
|
||||
* ([#480](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/480)) Fix the condition to display concerned persons in calendar list items.
|
||||
* ([#481](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/481)) Fix ordering of social actions: actions with a closing date in the future should be considered as 'still open'.
|
||||
* ([#477](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/477)) Fix export group by center for persons without a center in CenterAggregator.php
|
||||
* Fix the calculation of budget balance to only take into account resources and charges that are still actual
|
||||
* ([#489](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/489)) Fix desactivation date for Goals and results
|
||||
* ([#490](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/490)) Prevent sending a notification when the user signs the document himself
|
||||
* ([#491](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/491)) Fix: acc periods of which user is the referrer should not be included if when the list is filtered by center and none of the participations are part of the center
|
||||
* ([#492](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/492)) fix CommentInput: replace deprecated value binding with model-value
|
||||
* ([#493](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/493)) fix issue with stored object permissions associated with workflows (as attachment, or through a related entity)
|
||||
|
||||
BC: the constructor's signature of `\Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter` has changed.
|
||||
@@ -1,4 +0,0 @@
|
||||
## v4.12.1 - 2026-02-01
|
||||
### Fixed
|
||||
* ([#496](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/496)) Add the option to deal with duplicate address in BAN adress importer
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
## v4.13.0 - 2026-02-23
|
||||
### Feature
|
||||
* ([#500](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/500)) ([!964](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/964)) Limit the number of public download of stored object to 30 downloads
|
||||
* ([#495](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/495)) ([!967](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/967)) Send email related to notification in both html and txt format, and render quote correctly
|
||||
|
||||
### Fixed
|
||||
* ([#438](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/438)) Change wrong color of submit button "Désigner comme adresse du parcours"
|
||||
* ([#498](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/498)) For giving edit permissions on documents, take into account the workflow creator
|
||||
* Fixed mispelling of address in translations: addresse -> adresse
|
||||
* ([#499](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/499)) ([!963](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/963)) Fix: some postal code appears in the UI, although they are marked as deleted
|
||||
* ([#501](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/501)) ([!966](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/966)) Fix deprecation in the markdown rendering
|
||||
* ([#494](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/494)) ([!965](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/965)) Remove unused all-day slot display
|
||||
|
||||
### DX
|
||||
* ([!960](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/960)) Configure changie to ask for merge request number for a better tracking of changes
|
||||
@@ -1,6 +0,0 @@
|
||||
## v4.14.0 - 2026-03-09
|
||||
### Feature
|
||||
* ([#486](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/486)) ([!<no value>](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/<no value>)) Add filter and aggregator based on referrer's main center for exports of accompanying period
|
||||
### Fixed
|
||||
* ([#502](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/502)) ([!968](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/968)) Fix import of postal code: mark postal code as deleted if they are not present in the import any more
|
||||
* ([#503](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/503)) ([!969](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/969)) Add a flash message when reassigning accompanying course (reassign list)
|
||||
@@ -1,5 +0,0 @@
|
||||
## v4.14.1 - 2026-03-16
|
||||
### Security
|
||||
* ([#506](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/506)) ([!972](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/972)) Fix permission in list of activities in person context
|
||||
### DX
|
||||
* ([#504](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/504)) ([!970](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/970)) Add seeds in DataFixtures and in some tests to avoid random test failures
|
||||
@@ -7,7 +7,7 @@ versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
|
||||
kindFormat: '### {{.Kind}}'
|
||||
# Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description.
|
||||
changeFormat: >-
|
||||
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ if not (eq .Custom.MR "") }}([!{{ .Custom.MR }}](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/{{ .Custom.MR }})) {{ end }}{{ .Body }} {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }}
|
||||
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ .Body }} {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }}
|
||||
|
||||
**Schema Change**: {{ .Custom.SchemaChange }}
|
||||
{{- end -}}
|
||||
@@ -30,12 +30,6 @@ custom:
|
||||
type: int
|
||||
minInt: 1
|
||||
|
||||
- key: MR
|
||||
label: Merge request number (on chill-bundles repository) (optional)
|
||||
optional: true
|
||||
type: int
|
||||
minInt: 1
|
||||
|
||||
body:
|
||||
# allow multiline messages
|
||||
block: true
|
||||
|
||||
@@ -19,11 +19,11 @@ max_line_length = 80
|
||||
[COMMIT_EDITMSG]
|
||||
max_line_length = 0
|
||||
|
||||
[*.{js, vue, ts}]
|
||||
[*.{js,vue,ts}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[.rst]
|
||||
ident_size = 3
|
||||
ident_style = space
|
||||
[*.rst]
|
||||
indent_size = 3
|
||||
indent_style = space
|
||||
|
||||
|
||||
@@ -234,17 +234,17 @@ This must be a decision made by a human, not by an AI. Every AI task must abort
|
||||
|
||||
#### Running Tests
|
||||
|
||||
The tests are run from the project's root (not from the bundle's root).
|
||||
The tests are run from the project's root (not from the bundle's root: so, do not change the directory to any bundle directory before running tests).
|
||||
|
||||
Tests must be run using the `symfony` command:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
symfony composer exec phpunit
|
||||
|
||||
# Run a specific test file
|
||||
symfony composer exec phpunit -- path/to/TestFile.php
|
||||
|
||||
# Run a specific test method
|
||||
symfony composer exec phpunit --filter methodName path/to/TestFile.php
|
||||
symfony composer exec phpunit -- --filter methodName path/to/TestFile.php
|
||||
```
|
||||
|
||||
When writing tests, only test specific files. Do not run all tests or the full
|
||||
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
30
.vscode/launch.json
vendored
Normal file
30
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Chill Debug",
|
||||
"type": "php",
|
||||
"request": "launch",
|
||||
"port": 9000,
|
||||
"pathMappings": {
|
||||
"/var/www/html": "${workspaceFolder}"
|
||||
},
|
||||
"preLaunchTask": "symfony"
|
||||
},
|
||||
{
|
||||
"name": "Yarn Encore Dev (Watch)",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "yarn encore dev --watch",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Chill Debug + Yarn Encore Dev (Watch)",
|
||||
"configurations": ["Chill Debug", "Yarn Encore Dev (Watch)"]
|
||||
}
|
||||
]
|
||||
}
|
||||
23
.vscode/tasks.json
vendored
Normal file
23
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "symfony",
|
||||
"args": [
|
||||
"server:start",
|
||||
"--allow-http",
|
||||
"--no-tls",
|
||||
"--port=8000",
|
||||
"--allow-all-ip",
|
||||
"-d"
|
||||
],
|
||||
"label": "symfony"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "yarn",
|
||||
"args": ["encore", "dev", "--watch"],
|
||||
"label": "webpack"
|
||||
}
|
||||
]
|
||||
}
|
||||
75
CHANGELOG.md
75
CHANGELOG.md
@@ -6,81 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v4.14.1 - 2026-03-16
|
||||
### Security
|
||||
* ([#506](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/506)) ([!972](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/972)) Fix permission in list of activities in person context
|
||||
### DX
|
||||
* ([#504](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/504)) ([!970](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/970)) Add seeds in DataFixtures and in some tests to avoid random test failures
|
||||
|
||||
## v4.14.0 - 2026-03-09
|
||||
### Feature
|
||||
* ([#486](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/486)) ([!<no value>](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/<no value>)) Add filter and aggregator based on referrer's main center for exports of accompanying period
|
||||
### Fixed
|
||||
* ([#502](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/502)) ([!968](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/968)) Fix import of postal code: mark postal code as deleted if they are not present in the import any more
|
||||
* ([#503](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/503)) ([!969](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/969)) Add a flash message when reassigning accompanying course (reassign list)
|
||||
|
||||
## v4.13.0 - 2026-02-23
|
||||
### Feature
|
||||
* ([#500](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/500)) ([!964](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/964)) Limit the number of public download of stored object to 30 downloads
|
||||
* ([#495](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/495)) ([!967](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/967)) Send email related to notification in both html and txt format, and render quote correctly
|
||||
|
||||
### Fixed
|
||||
* ([#438](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/438)) Change wrong color of submit button "Désigner comme adresse du parcours"
|
||||
* ([#498](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/498)) For giving edit permissions on documents, take into account the workflow creator
|
||||
* Fixed mispelling of address in translations: addresse -> adresse
|
||||
* ([#499](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/499)) ([!963](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/963)) Fix: some postal code appears in the UI, although they are marked as deleted
|
||||
* ([#501](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/501)) ([!966](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/966)) Fix deprecation in the markdown rendering
|
||||
* ([#494](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/494)) ([!965](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/965)) Remove unused all-day slot display
|
||||
|
||||
### DX
|
||||
* ([!960](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/960)) Configure changie to ask for merge request number for a better tracking of changes
|
||||
|
||||
## v4.12.1 - 2026-02-01
|
||||
### Fixed
|
||||
* ([#496](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/496)) Add the option to deal with duplicate address in BAN adress importer
|
||||
|
||||
|
||||
## v4.12.0 - 2026-01-15
|
||||
### Feature
|
||||
* ([#473](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/473)) Display version of chill bundles in application footer
|
||||
* Increase the delay before removing stale workflow from 90 days to 180 days.
|
||||
### Fixed
|
||||
* ([#480](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/480)) Fix the condition to display concerned persons in calendar list items.
|
||||
* ([#481](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/481)) Fix ordering of social actions: actions with a closing date in the future should be considered as 'still open'.
|
||||
* ([#477](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/477)) Fix export group by center for persons without a center in CenterAggregator.php
|
||||
* Fix the calculation of budget balance to only take into account resources and charges that are still actual
|
||||
* ([#489](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/489)) Fix desactivation date for Goals and results
|
||||
* ([#490](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/490)) Prevent sending a notification when the user signs the document himself
|
||||
* ([#491](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/491)) Fix: acc periods of which user is the referrer should not be included if when the list is filtered by center and none of the participations are part of the center
|
||||
* ([#492](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/492)) fix CommentInput: replace deprecated value binding with model-value
|
||||
* ([#493](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/493)) fix issue with stored object permissions associated with workflows (as attachment, or through a related entity)
|
||||
|
||||
BC: the constructor's signature of `\Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter` has changed.
|
||||
|
||||
## v4.11.0 - 2025-12-17
|
||||
### Feature
|
||||
* ([#478](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/478)) Add filtering to admin lists: social actions, social issues, goals, results, and evaluations
|
||||
### Fixed
|
||||
* ([#466](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/466)) Fix migration query after previous fix
|
||||
|
||||
* Fix translation key/value
|
||||
|
||||
Cannot start with % and should be wrapped in "".
|
||||
|
||||
## v4.10.1 - 2025-12-11
|
||||
### Fixed
|
||||
* Fix missing translation variable in NewLocation component
|
||||
* ([#476](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/476)) Fix display of header for ByActivityNumberAggregator
|
||||
* Fix use of ByActivityNumberAggregator in combination with activity count exports
|
||||
* ([#483](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/483)) Tentatively fix usage of CTRL+C in collabora editor with chrome / edge browser
|
||||
|
||||
## v4.10.0 - 2025-12-09
|
||||
### Feature
|
||||
* [MR 928](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/928) [#462](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/462) Add the future appointments for the person in search results
|
||||
### Fixed
|
||||
* Remove dependency to package @symfony/ux-translator
|
||||
|
||||
|
||||
## v4.9.0 - 2025-12-05
|
||||
### Feature
|
||||
* ([#459](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/459)) Add a counter for invitations awaiting reply
|
||||
|
||||
@@ -54,7 +54,7 @@ Arborescence:
|
||||
- person
|
||||
- personvendee
|
||||
- household_edit_metadata
|
||||
- index.js
|
||||
- index.ts
|
||||
```
|
||||
|
||||
## Organisation des feuilles de styles
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { trans, setLocale, setLocaleFallbacks } from "./ux-translator";
|
||||
import {
|
||||
trans,
|
||||
setLocale,
|
||||
getLocale,
|
||||
setLocaleFallbacks,
|
||||
} from "./ux-translator";
|
||||
|
||||
setLocaleFallbacks({"en": "fr", "nl": "fr", "fr": "en"});
|
||||
setLocale('fr');
|
||||
setLocaleFallbacks({ en: "fr", nl: "fr", fr: "en" });
|
||||
setLocale("fr");
|
||||
|
||||
export { trans };
|
||||
export * from '../var/translations';
|
||||
export { trans, getLocale };
|
||||
export * from "../var/translations";
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"ext-openssl": "*",
|
||||
"ext-redis": "*",
|
||||
"ext-zlib": "*",
|
||||
"composer-runtime-api": "*",
|
||||
"chill-project/chill-zimbra-bundle": "@dev",
|
||||
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
|
||||
"champs-libres/wopi-lib": "dev-master@dev",
|
||||
"doctrine/data-fixtures": "^1.8",
|
||||
@@ -62,6 +62,7 @@
|
||||
"symfony/http-client": "^5.4",
|
||||
"symfony/http-foundation": "^5.4",
|
||||
"symfony/intl": "^5.4",
|
||||
"symfony/loco-translation-provider": "^6.0",
|
||||
"symfony/mailer": "^5.4",
|
||||
"symfony/messenger": "^5.4",
|
||||
"symfony/mime": "^5.4",
|
||||
@@ -83,7 +84,7 @@
|
||||
"symfony/templating": "^5.4",
|
||||
"symfony/translation": "^5.4",
|
||||
"symfony/twig-bundle": "^5.4",
|
||||
"symfony/ux-translator": "2.31.0",
|
||||
"symfony/ux-translator": "^2.22",
|
||||
"symfony/validator": "^5.4",
|
||||
"symfony/webpack-encore-bundle": "^1.11",
|
||||
"symfony/workflow": "^5.4",
|
||||
@@ -98,7 +99,7 @@
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.3",
|
||||
"fakerphp/faker": "^1.13",
|
||||
"friendsofphp/php-cs-fixer": "3.93.0",
|
||||
"friendsofphp/php-cs-fixer": "3.65.0",
|
||||
"jangregor/phpstan-prophecy": "^1.0",
|
||||
"nelmio/alice": "^3.8",
|
||||
"nikic/php-parser": "^4.15",
|
||||
@@ -118,8 +119,7 @@
|
||||
"symfony/runtime": "^5.4",
|
||||
"symfony/stopwatch": "^5.4",
|
||||
"symfony/var-dumper": "^5.4",
|
||||
"symfony/web-profiler-bundle": "^5.4",
|
||||
"symfony/loco-translation-provider": "^6.0"
|
||||
"symfony/web-profiler-bundle": "^5.4"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
@@ -142,6 +142,7 @@
|
||||
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
|
||||
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle",
|
||||
"Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src",
|
||||
"Chill\\TicketBundle\\": "src/Bundle/ChillTicketBundle/src",
|
||||
"Chill\\Utils\\Rector\\": "utils/rector/src"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -34,7 +34,9 @@ return [
|
||||
Chill\ThirdPartyBundle\ChillThirdPartyBundle::class => ['all' => true],
|
||||
Chill\BudgetBundle\ChillBudgetBundle::class => ['all' => true],
|
||||
Chill\WopiBundle\ChillWopiBundle::class => ['all' => true],
|
||||
Chill\TicketBundle\ChillTicketBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
||||
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true],
|
||||
Chill\ZimbraBundle\ChillZimbraBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
chill_doc_store:
|
||||
use_driver: openstack
|
||||
use_driver: local_storage
|
||||
local_storage:
|
||||
storage_path: '%kernel.project_dir%/var/storage'
|
||||
openstack:
|
||||
|
||||
5
config/packages/chill_ticket.yaml
Normal file
5
config/packages/chill_ticket.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
chill_ticket:
|
||||
ticket:
|
||||
person_per_ticket: one # One of "one"; "many"
|
||||
response_time_exceeded_delay: PT12H
|
||||
|
||||
@@ -14,6 +14,7 @@ doctrine_migrations:
|
||||
'Chill\Migrations\Calendar': '@ChillCalendarBundle/migrations'
|
||||
'Chill\Migrations\Budget': '@ChillBudgetBundle/migrations'
|
||||
'Chill\Migrations\Report': '@ChillReportBundle/migrations'
|
||||
'Chill\Migrations\Ticket': '@ChillTicketBundle/migrations'
|
||||
all_or_nothing:
|
||||
true
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ framework:
|
||||
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
|
||||
'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async
|
||||
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async
|
||||
'Chill\TicketBundle\Messenger\PostTicketUpdateMessage': async
|
||||
# end of routes added by chill-bundles recipes
|
||||
# Route your messages to the transports
|
||||
# 'App\Message\YourMessage': async
|
||||
|
||||
@@ -8,6 +8,5 @@ when@dev: &dev
|
||||
- 'file'
|
||||
- 'md5'
|
||||
- 'sha1'
|
||||
seed: 1234567890
|
||||
|
||||
when@test: *dev
|
||||
|
||||
2
config/routes/chill_ticket.yaml
Normal file
2
config/routes/chill_ticket.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
chill_ticket_bundle:
|
||||
resource: '@ChillTicketBundle/config/routes.yaml'
|
||||
@@ -11,6 +11,3 @@ services:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
when@dev:
|
||||
services:
|
||||
ChampsLibres\WopiLib\Contract\Service\ProofValidatorInterface: '@Chill\WopiBundle\Service\Wopi\NullProofValidator'
|
||||
|
||||
@@ -1,23 +1,90 @@
|
||||
Permission is granted to copy, distribute and/or modify this document
|
||||
under the terms of the GNU Free Documentation License, Version 1.3
|
||||
or any later version published by the Free Software Foundation;
|
||||
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
|
||||
A copy of the license is included in the section entitled "GNU
|
||||
Free Documentation License".
|
||||
# Create a new bundle {#create-new-bundle}
|
||||
|
||||
###### Create a new bundle
|
||||
:::: warning
|
||||
::: title
|
||||
Warning
|
||||
:::
|
||||
|
||||
Create your own bundle is not a trivial task.
|
||||
This part of the doc is not yet tested
|
||||
::::
|
||||
|
||||
The easiest way to achieve this is seems to be :
|
||||
## Create a new directory with Bundle class
|
||||
|
||||
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 [installation-for-development` section, and add your new repository to the composer.json file
|
||||
6. Work as :ref:`usual ](editing-code-and-commiting.md)
|
||||
``` bash
|
||||
mkdir -p src/Bundle/ChillSomeBundle/src/config
|
||||
mkdir -p src/Bundle/ChillSomeBundle/src/Controller
|
||||
```
|
||||
|
||||
This part of the doc is not yet tested
|
||||
Add a bundle file
|
||||
|
||||
TODO
|
||||
``` 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:
|
||||
|
||||
``` yaml
|
||||
chill_ticket_controller:
|
||||
resource: '@ChillTicketBundle/Controller/'
|
||||
type: annotation
|
||||
```
|
||||
|
||||
## Register the new psr-4 namespace
|
||||
|
||||
In composer.json, add the new psr4 namespace
|
||||
|
||||
``` diff
|
||||
{
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
+ "Chill\\SomeBundle\\": "src/Bundle/ChillSomeBundle/src",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Register the bundle
|
||||
|
||||
Register in the file `config/bundles.php`:
|
||||
|
||||
``` php
|
||||
Vendor\Bundle\YourBundle\YourBundle::class => ['all' => true],
|
||||
```
|
||||
|
||||
And import routes in `config/routes/chill_some_bundle.yaml`:
|
||||
|
||||
``` yaml
|
||||
chill_ticket_bundle:
|
||||
resource: '@ChillSomeBundle/config/routes.yaml'
|
||||
```
|
||||
|
||||
## Add the doctrine_migration namespace
|
||||
|
||||
Add the namespace to `config/packages/doctrine_migrations_chill.yaml`
|
||||
|
||||
``` diff
|
||||
doctrine_migrations:
|
||||
migrations_paths:
|
||||
+ 'Chill\Some\Ticket': '@ChillSomeBundle/migrations'
|
||||
```
|
||||
|
||||
## Dump autoloading
|
||||
|
||||
``` bash
|
||||
symfony composer dump-autoload
|
||||
```
|
||||
|
||||
@@ -12,8 +12,6 @@ As Chill relies on the [symfony ](http://symfony.com) framework, reading the fra
|
||||
- [Messages to users](messages-to-users.md)
|
||||
- [Pagination](pagination.md)
|
||||
- [Localisation](localisation.md)
|
||||
- [Translation directives](translation_directives.md)
|
||||
- [Translation provider](translation_provider.md)
|
||||
- [Logging](logging.md)
|
||||
- [Database migrations](migrations.md)
|
||||
- [Searching](searching.md)
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
# Translation Key Directives
|
||||
|
||||
These directives are meant to ensure better consistency across bundles, avoid duplication, and make keys more predictable.
|
||||
|
||||
## General Principles
|
||||
|
||||
1. **Use lowercase snake_case for all keys**
|
||||
|
||||
2. **Use dot-separated namespaces**
|
||||
The dot is used to reflect:
|
||||
- bundle
|
||||
- feature
|
||||
- sub-feature
|
||||
- key type
|
||||
|
||||
3. **Do not use spaces in keys**
|
||||
|
||||
4. **Avoid duplicating the same text in multiple places**
|
||||
When a translation is needed, try a search for the translation value first and see if it exists elsewhere
|
||||
|
||||
5. **If a key is used across multiple bundles, it must live in ChillMainBundle.**
|
||||
|
||||
6. **If a key is used across multiple bundles and is a generic term, it must be placed in the `common` namespace.**
|
||||
|
||||
## Key Structure
|
||||
|
||||
We use the following structure:
|
||||
|
||||
```
|
||||
<scope>.<feature>.<sub-feature>.<key-type>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `<scope>` identifies the bundle or shared context
|
||||
- `<feature>` identifies the part of the module using the translation
|
||||
- `<element>` describes the text purpose
|
||||
- `<subelement>` for a multi-level element (e.g., activity.export.person.count.description)
|
||||
|
||||
### Examples of scopes
|
||||
|
||||
- `activity` — ChillActivityBundle
|
||||
- `person` — ChillPersonBundle
|
||||
- `common` — neutral shared translation values
|
||||
|
||||
## Naming Scopes
|
||||
|
||||
### 1. Bundle-specific keys
|
||||
|
||||
For most things inside a bundle:
|
||||
|
||||
```
|
||||
activity.<feature>.<element>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
activity.form.save
|
||||
activity.list.title
|
||||
activity.entity.type
|
||||
activity.menu.activities
|
||||
activity.controller.success_created
|
||||
```
|
||||
|
||||
### 2. Shared UI elements (buttons, labels, generic text)
|
||||
|
||||
These belong in the `common` namespace in ChillMainBundle:
|
||||
|
||||
```
|
||||
common.save
|
||||
common.delete
|
||||
common.edit
|
||||
common.filter
|
||||
common.duration_time
|
||||
```
|
||||
|
||||
## Translation Workflow
|
||||
|
||||
Use the following workflow when deciding where a key belongs:
|
||||
|
||||
1. **Is this text used in more than one bundle?**
|
||||
→ Place in `main` or `common`
|
||||
|
||||
2. **Is this text generic UI (button, label, pagination, yes/no)?**
|
||||
→ Place in `common`
|
||||
|
||||
3. **Is this text specific to one bundle and one feature?**
|
||||
→ Place in `<bundle>.feature.<element>`
|
||||
|
||||
4. **Is this text related to an entity or value object?**
|
||||
→ Place in `<bundle>.entity.<entityname>.<field>`
|
||||
|
||||
5. **Is this text used in forms?**
|
||||
→ `<bundle>.form.<field>` or `<bundle>.form.<action>`
|
||||
|
||||
6. **Is this text related to exports?**
|
||||
→ `<bundle>.export.<feature>.<column>`
|
||||
|
||||
7. **Is it related to filtering, searching or parameters?**
|
||||
→ `<bundle>.filter.<name>` or
|
||||
→ `<bundle>.filter.<feature>.<field>` for nested filters
|
||||
|
||||
## Examples Based on Translations Within ChillActivityBundle
|
||||
|
||||
Below are concrete examples from `ChillActivityBundle`, refactored according to the guidelines.
|
||||
|
||||
### General activity keys
|
||||
|
||||
Instead of scattered keys like:
|
||||
|
||||
```
|
||||
Show the activity
|
||||
Edit the activity
|
||||
Activity
|
||||
Duration time
|
||||
...
|
||||
```
|
||||
|
||||
We use:
|
||||
|
||||
```
|
||||
activity.general.show
|
||||
activity.general.edit
|
||||
activity.general.title
|
||||
activity.general.duration
|
||||
activity.general.travel_time
|
||||
activity.general.attendee
|
||||
activity.general.remark
|
||||
activity.general.no_comments
|
||||
```
|
||||
|
||||
### Forms
|
||||
|
||||
Instead of keys like:
|
||||
|
||||
```
|
||||
Activity creation
|
||||
Save activity
|
||||
Reset form
|
||||
Choose a type
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```
|
||||
activity.form.title_create
|
||||
activity.form.save
|
||||
activity.form.reset
|
||||
activity.form.choose_type
|
||||
activity.form.choose_duration
|
||||
```
|
||||
|
||||
Long lists (like durations) should be grouped:
|
||||
|
||||
```
|
||||
activity.form.duration.5min
|
||||
activity.form.duration.10min
|
||||
activity.form.duration.15min
|
||||
activity.form.duration.1h
|
||||
activity.form.duration.1h30
|
||||
activity.form.duration.2h
|
||||
...
|
||||
```
|
||||
|
||||
### Entities
|
||||
|
||||
Entity fields should follow:
|
||||
|
||||
```
|
||||
activity.entity.activity.date
|
||||
activity.entity.activity.comment
|
||||
activity.entity.activity.deleted
|
||||
activity.entity.location.name
|
||||
activity.entity.location.type
|
||||
```
|
||||
|
||||
### Controller messages
|
||||
|
||||
Instead of strings as keys:
|
||||
|
||||
```
|
||||
'Success : activity created!'
|
||||
'The form is not valid. The activity has not been created !'
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```
|
||||
activity.controller.success_created
|
||||
activity.controller.error_invalid_create
|
||||
activity.controller.success_updated
|
||||
activity.controller.error_invalid_update
|
||||
```
|
||||
|
||||
### Roles
|
||||
|
||||
Access control keys should be:
|
||||
|
||||
```
|
||||
activity.role.create
|
||||
activity.role.update
|
||||
activity.role.see
|
||||
activity.role.see_details
|
||||
activity.role.delete
|
||||
activity.role.stats
|
||||
activity.role.list
|
||||
```
|
||||
|
||||
### Admin
|
||||
|
||||
```
|
||||
activity.admin.configuration
|
||||
activity.admin.types
|
||||
activity.admin.reasons
|
||||
activity.admin.reason_category
|
||||
activity.admin.presence
|
||||
```
|
||||
|
||||
### CRUD
|
||||
|
||||
```
|
||||
activity.crud.type.title_new
|
||||
activity.crud.type.title_edit
|
||||
activity.crud.presence.title_new
|
||||
```
|
||||
|
||||
### Activity Reason
|
||||
|
||||
```
|
||||
activity.reason.list
|
||||
activity.reason.create
|
||||
activity.reason.active
|
||||
activity.reason.category
|
||||
activity.reason.entity_title
|
||||
```
|
||||
|
||||
### Exports
|
||||
|
||||
Group them logically:
|
||||
|
||||
```
|
||||
activity.export.person.count.title
|
||||
activity.export.person.count.description
|
||||
activity.export.person.count.header
|
||||
|
||||
activity.export.period.sum_duration.title
|
||||
activity.export.period.sum_duration.description
|
||||
activity.export.period.sum_duration.header
|
||||
```
|
||||
|
||||
### Filters
|
||||
|
||||
Use hierarchical filters:
|
||||
|
||||
```
|
||||
activity.filter.by_reason
|
||||
activity.filter.by_type
|
||||
activity.filter.by_date
|
||||
activity.filter.by_location
|
||||
activity.filter.by_sent_received
|
||||
activity.filter.by_user
|
||||
```
|
||||
|
||||
### Aggregators
|
||||
|
||||
```
|
||||
activity.aggregator.reason.by_category
|
||||
activity.aggregator.reason.level
|
||||
activity.aggregator.user.by_scope
|
||||
activity.aggregator.user.by_job
|
||||
```
|
||||
|
||||
## Global/Shared Keys
|
||||
|
||||
Keys like the following **must not be redeclared** in each bundle:
|
||||
|
||||
- First name
|
||||
- Last name
|
||||
- Username
|
||||
- ID
|
||||
- Type
|
||||
- Duration
|
||||
- Comment
|
||||
- Date
|
||||
- Location
|
||||
- Present / Not present
|
||||
- Add / Edit / Delete / Save / Update
|
||||
|
||||
These belong in `common` or `main`:
|
||||
|
||||
```
|
||||
common.firstname
|
||||
common.lastname
|
||||
common.username
|
||||
common.id
|
||||
common.type
|
||||
common.comment
|
||||
common.date
|
||||
common.location
|
||||
common.present
|
||||
common.absent
|
||||
common.add
|
||||
common.edit
|
||||
common.delete
|
||||
common.save
|
||||
common.update
|
||||
```
|
||||
|
||||
## Naming Directives Summary
|
||||
|
||||
- **snake_case**
|
||||
- **namespaced with dots**
|
||||
- **bundle prefix for bundle-specific concepts**
|
||||
- **common or main for shared concepts**
|
||||
- **avoid free-floating keys (without namespace)**
|
||||
- **reuse common keys wherever possible**
|
||||
|
||||
## Migration Strategy (Optional)
|
||||
|
||||
To apply this structure progressively:
|
||||
|
||||
1. New keys must follow these guidelines.
|
||||
2. Existing keys may remain as-is until refactored.
|
||||
3. When refactoring:
|
||||
- Move cross-bundle keys to ChillMainBundle and possible `common` namespace.
|
||||
- Replace duplicated keys with shared ones.
|
||||
|
||||
---
|
||||
|
||||
# Avoiding Duplicate Translations
|
||||
|
||||
## 1. Use Shared Namespaces
|
||||
|
||||
Two namespaces must be used for shared translations:
|
||||
|
||||
- `common.*` — generic UI concepts (save, delete, date, name, etc.)
|
||||
|
||||
If a translation may be reused in multiple bundles, it must be placed in the `common` namespace or in ChillMainBundle.
|
||||
|
||||
## 2. Bundle-Specific Keys
|
||||
|
||||
Keys belonging only to one bundle or one feature are namespaced inside that bundle:
|
||||
|
||||
```
|
||||
activity.<feature>.<element>
|
||||
person.<feature>.<element>
|
||||
```
|
||||
|
||||
## 3. Search Before Creating
|
||||
|
||||
Before adding a new translation key, developers must:
|
||||
|
||||
1. For common translations like "enregistrer/opslaan" look in the `common` namespace.
|
||||
2. Search in Loco or translations for existing values.
|
||||
|
||||
If a suitable key exists, reuse it.
|
||||
|
||||
## 4. Only Create a New Key When Necessary
|
||||
|
||||
Create a new key only when the text is:
|
||||
|
||||
- specific to the bundle
|
||||
- specific to the feature
|
||||
- not reusable elsewhere
|
||||
|
||||
## 5. Progressive Cleanup
|
||||
|
||||
Old duplicates may remain temporarily. When updating code in an area, clean duplicate values by moving them into `common` or `main`.
|
||||
|
||||
## General Workflow
|
||||
|
||||
- **Reuse shared keys** within `common` namespace.
|
||||
- **Search before creating** new keys.
|
||||
- **Namespace bundle-specific keys** under their bundle.
|
||||
- **Refactor progressively** when touching old code.
|
||||
419
docs/source/development/translation_directives.rst
Normal file
419
docs/source/development/translation_directives.rst
Normal file
@@ -0,0 +1,419 @@
|
||||
============================================
|
||||
Directives for creating new translation keys
|
||||
============================================
|
||||
|
||||
These directives are meant to ensure better consistency across bundles, avoid duplication, and make keys more predictable.
|
||||
|
||||
|
||||
General Principles
|
||||
==================
|
||||
|
||||
1. **Use lowercase snake_case for all keys**
|
||||
|
||||
2. **Use dot-separated namespaces**
|
||||
The dot is used to reflect:
|
||||
- bundle
|
||||
- feature
|
||||
- sub-feature
|
||||
- key type
|
||||
|
||||
3. **Do not use spaces in keys**
|
||||
|
||||
4. **Avoid duplicating the same text in multiple places**
|
||||
When a translation is needed, try a search for the translation value first and see if it exists elsewhere
|
||||
|
||||
5. **If a key is used across multiple bundles, it must live in ChillMainBundle.**
|
||||
|
||||
6. **If a key is used across multiple bundles and is a generic term, it must be placed in the ``common`` namespace.**
|
||||
|
||||
|
||||
Key Structure
|
||||
=============
|
||||
|
||||
We use the following structure:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
<scope>.<feature>.<sub-feature>.<key-type>
|
||||
|
||||
Where:
|
||||
|
||||
* ``<>`` identifies the bundle or shared context
|
||||
* ``<feature>`` identifies the part of the module using the translation
|
||||
* ``<element>`` describes the text purpose
|
||||
* ``<subelement>`` for a multi-level element ( eg. activity.export.person.count.description)
|
||||
|
||||
Examples of scopes
|
||||
------------------
|
||||
|
||||
* ``activity`` — ChillActivityBundle
|
||||
* ``person`` — ChillPersonBundle
|
||||
* ``common`` — neutral shared translation values
|
||||
|
||||
|
||||
Naming Scopes
|
||||
=============
|
||||
|
||||
1. **Bundle-specific keys**
|
||||
|
||||
For most things inside a bundle:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.<feature>.<element>
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.form.save
|
||||
activity.list.title
|
||||
activity.entity.type
|
||||
activity.menu.activities
|
||||
activity.controller.success_created
|
||||
|
||||
2. **Shared UI elements (buttons, labels, generic text)**
|
||||
|
||||
These belong in the ``common`` namespace in ChillMainBundle:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
common.save
|
||||
common.delete
|
||||
common.edit
|
||||
common.filter
|
||||
common.duration_time
|
||||
|
||||
Translation workflow
|
||||
====================
|
||||
|
||||
Use the following workflow when deciding where a key belongs:
|
||||
|
||||
1. **Is this text used in more than one bundle?**
|
||||
→ Place in ``main`` or ``common``
|
||||
|
||||
2. **Is this text generic UI (button, label, pagination, yes/no)?**
|
||||
→ Place in ``common``
|
||||
|
||||
3. **Is this text specific to one bundle and one feature?**
|
||||
→ Place in ``<bundle>.feature.<element>``
|
||||
|
||||
4. **Is this text related to an entity or value object?**
|
||||
→ Place in ``<bundle>.entity.<entityname>.<field>``
|
||||
|
||||
5. **Is this text used in forms?**
|
||||
→ ``<bundle>.form.<field>`` or ``<bundle>.form.<action>``
|
||||
|
||||
6. **Is this text related to exports?**
|
||||
→ ``<bundle>.export.<feature>.<column>``
|
||||
|
||||
7. **Is it related to filtering, searching or parameters?**
|
||||
→ ``<bundle>.filter.<name>`` or
|
||||
→ ``<bundle>.filter.<feature>.<field>`` for nested filters
|
||||
|
||||
|
||||
Examples based on translations within ChillActivityBundle
|
||||
=========================================================
|
||||
|
||||
Below are concrete examples from ``ChillActivityBundle``,
|
||||
refactored according to the guidelines.
|
||||
|
||||
|
||||
General activity keys
|
||||
---------------------
|
||||
|
||||
Instead of scattered keys like::
|
||||
|
||||
Show the activity
|
||||
Edit the activity
|
||||
Activity
|
||||
Duration time
|
||||
...
|
||||
|
||||
We use:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.general.show
|
||||
activity.general.edit
|
||||
activity.general.title
|
||||
activity.general.duration
|
||||
activity.general.travel_time
|
||||
activity.general.attendee
|
||||
activity.general.remark
|
||||
activity.general.no_comments
|
||||
|
||||
|
||||
Forms
|
||||
-----
|
||||
|
||||
Instead of keys like::
|
||||
|
||||
Activity creation
|
||||
Save activity
|
||||
Reset form
|
||||
Choose a type
|
||||
|
||||
Use:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.form.title_create
|
||||
activity.form.save
|
||||
activity.form.reset
|
||||
activity.form.choose_type
|
||||
activity.form.choose_duration
|
||||
|
||||
Long lists (like durations) should be grouped:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.form.duration.5min
|
||||
activity.form.duration.10min
|
||||
activity.form.duration.15min
|
||||
activity.form.duration.1h
|
||||
activity.form.duration.1h30
|
||||
activity.form.duration.2h
|
||||
...
|
||||
|
||||
Entities
|
||||
--------
|
||||
|
||||
Entity fields should follow:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.entity.activity.date
|
||||
activity.entity.activity.comment
|
||||
activity.entity.activity.deleted
|
||||
activity.entity.location.name
|
||||
activity.entity.location.type
|
||||
|
||||
|
||||
Controller messages
|
||||
-------------------
|
||||
|
||||
Instead of strings as keys::
|
||||
|
||||
'Success : activity created!'
|
||||
'The form is not valid. The activity has not been created !'
|
||||
|
||||
Use:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.controller.success_created
|
||||
activity.controller.error_invalid_create
|
||||
activity.controller.success_updated
|
||||
activity.controller.error_invalid_update
|
||||
|
||||
|
||||
Roles
|
||||
-----
|
||||
|
||||
Access control keys should be:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.role.create
|
||||
activity.role.update
|
||||
activity.role.see
|
||||
activity.role.see_details
|
||||
activity.role.delete
|
||||
activity.role.stats
|
||||
activity.role.list
|
||||
|
||||
|
||||
Admin
|
||||
-----
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.admin.configuration
|
||||
activity.admin.types
|
||||
activity.admin.reasons
|
||||
activity.admin.reason_category
|
||||
activity.admin.presence
|
||||
|
||||
|
||||
CRUD
|
||||
----
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.crud.type.title_new
|
||||
activity.crud.type.title_edit
|
||||
activity.crud.presence.title_new
|
||||
|
||||
|
||||
Activity Reason
|
||||
---------------
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.reason.list
|
||||
activity.reason.create
|
||||
activity.reason.active
|
||||
activity.reason.category
|
||||
activity.reason.entity_title
|
||||
|
||||
|
||||
Exports
|
||||
-------
|
||||
|
||||
Group them logically:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.export.person.count.title
|
||||
activity.export.person.count.description
|
||||
activity.export.person.count.header
|
||||
|
||||
activity.export.period.sum_duration.title
|
||||
activity.export.period.sum_duration.description
|
||||
activity.export.period.sum_duration.header
|
||||
|
||||
|
||||
Filters
|
||||
-------
|
||||
|
||||
Use hierarchical filters:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.filter.by_reason
|
||||
activity.filter.by_type
|
||||
activity.filter.by_date
|
||||
activity.filter.by_location
|
||||
activity.filter.by_sent_received
|
||||
activity.filter.by_user
|
||||
|
||||
|
||||
Aggregators
|
||||
-----------
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.aggregator.reason.by_category
|
||||
activity.aggregator.reason.level
|
||||
activity.aggregator.user.by_scope
|
||||
activity.aggregator.user.by_job
|
||||
|
||||
|
||||
Global/Shared Keys
|
||||
==================
|
||||
|
||||
Keys like the following **must not be redeclared** in each bundle:
|
||||
|
||||
- First name
|
||||
- Last name
|
||||
- Username
|
||||
- ID
|
||||
- Type
|
||||
- Duration
|
||||
- Comment
|
||||
- Date
|
||||
- Location
|
||||
- Present / Not present
|
||||
- Add / Edit / Delete / Save / Update
|
||||
|
||||
These belong in ``common`` or ``main``:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
common.firstname
|
||||
common.lastname
|
||||
common.username
|
||||
common.id
|
||||
common.type
|
||||
common.comment
|
||||
common.date
|
||||
common.location
|
||||
common.present
|
||||
common.absent
|
||||
common.add
|
||||
common.edit
|
||||
common.delete
|
||||
common.save
|
||||
common.update
|
||||
|
||||
|
||||
Naming directives summary
|
||||
==========================
|
||||
|
||||
* **snake_case**
|
||||
* **namespaced with dots**
|
||||
* **bundle prefix for bundle-specific concepts**
|
||||
* **common or main for shared concepts**
|
||||
* **avoid free-floating keys (without namespace)**
|
||||
* **reuse common keys wherever possible**
|
||||
|
||||
|
||||
Migration Strategy (Optional)
|
||||
=============================
|
||||
|
||||
To apply this structure progressively:
|
||||
|
||||
1. New keys must follow these guidelines.
|
||||
2. Existing keys may remain as-is until refactored.
|
||||
3. When refactoring:
|
||||
- Move cross-bundle keys to ChillMainBundle and possible `common` namespace.
|
||||
- Replace duplicated keys with shared ones.
|
||||
|
||||
===========================================
|
||||
Avoiding duplicate translations
|
||||
===========================================
|
||||
|
||||
1. Use Shared Namespaces
|
||||
========================
|
||||
|
||||
Two namespaces must be used for shared translations:
|
||||
|
||||
* ``common.*`` — generic UI concepts (save, delete, date, name, etc.)
|
||||
|
||||
If a translation may be reused in multiple bundles, it must be placed
|
||||
in the ``common`` namespace or in ChillMainBundle.
|
||||
|
||||
2. Bundle-Specific Keys
|
||||
=======================
|
||||
|
||||
Keys belonging only to one bundle or one feature are namespaced inside that
|
||||
bundle:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.<feature>.<element>
|
||||
person.<feature>.<element>
|
||||
|
||||
3. Search Before Creating
|
||||
=========================
|
||||
|
||||
Before adding a new translation key, developers must:
|
||||
|
||||
1. For common translations like: "enregistrer/opslaan" look in the `common` namespace.
|
||||
3. Search in Loco or translations for existing values.
|
||||
|
||||
If a suitable key exists, reuse it.
|
||||
|
||||
4. Only Create a New Key When Necessary
|
||||
=======================================
|
||||
|
||||
Create a new key only when the text is:
|
||||
|
||||
* specific to the bundle
|
||||
* specific to the feature
|
||||
* not reusable elsewhere
|
||||
|
||||
6. Progressive Cleanup
|
||||
======================
|
||||
|
||||
Old duplicates may remain temporarily. When updating code in an area, clean
|
||||
duplicate values by moving them into ``common`` or ``main``.
|
||||
|
||||
General workflow
|
||||
================
|
||||
|
||||
* **Reuse shared keys** within ``common`` namespace.
|
||||
* **Search before creating** new keys.
|
||||
* **Namespace bundle-specific keys** under their bundle.
|
||||
* **Refactor progressively** when touching old code.
|
||||
@@ -1,139 +0,0 @@
|
||||
# Managing Translations Within CHILL Using Loco as a Translation Provider
|
||||
|
||||
Within CHILL we make use of Symfony's translation component together with *Loco* as an external translation provider. Using this setup centralises translations in a single online location (Loco), while still allowing developers to create and update translation keys locally in the project (YAML files).
|
||||
|
||||
## Workflow
|
||||
|
||||
We use the following workflow:
|
||||
|
||||
- Developers create translation keys in YAML files inside each bundle.
|
||||
- Keys are written in **English**.
|
||||
- Application UI defaults to **French**, with **Dutch** as an additional locale (other languages can be added in the future).
|
||||
- Loco acts as the central translation memory and synchronisation source.
|
||||
- Loco Symfony package was installed so that built-in translation commands can be used to push/pull content between Loco and the local project.
|
||||
|
||||
## Translation Directory Structure
|
||||
|
||||
Each bundle contains its own `translations` directory, for example:
|
||||
|
||||
```
|
||||
chill-bundles/
|
||||
ChillCoreBundle/
|
||||
translations/
|
||||
messages.fr.yml
|
||||
messages.nl.yml
|
||||
ChillPersonBundle/
|
||||
translations/
|
||||
messages.fr.yml
|
||||
messages.nl.yml
|
||||
...
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The translation configuration is defined in `config/packages/translation.yaml`:
|
||||
|
||||
```yaml
|
||||
framework:
|
||||
default_locale: '%env(resolve:LOCALE)%'
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- '%env(resolve:LOCALE)%'
|
||||
- 'en'
|
||||
providers:
|
||||
loco:
|
||||
dsn: '%env(LOCO_DSN)%'
|
||||
domains: [ 'messages' ]
|
||||
locales: [ 'fr', 'nl' ]
|
||||
```
|
||||
|
||||
Note:
|
||||
|
||||
- `en` is the **source locale** in Loco.
|
||||
- `fr` and `nl` are the **application locales**.
|
||||
- `domains: [messages]` means only `messages.*.yml` files are pushed.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
In `.env`:
|
||||
|
||||
```
|
||||
LOCALE=fr
|
||||
```
|
||||
|
||||
In `.env.local`:
|
||||
|
||||
```
|
||||
LOCO_DSN="loco://API_KEY@default"
|
||||
```
|
||||
|
||||
Replace `API_KEY` with the key provided by Loco.
|
||||
|
||||
## Working with Loco
|
||||
|
||||
Loco shows all translation keys under three languages:
|
||||
|
||||
- **English (source)** — keys are listed but remain "untranslated"
|
||||
- **French** — translated strings for French users
|
||||
- **Dutch** — translated strings for Dutch users
|
||||
|
||||
Note: Don't add translations directly in the English column. This column simply represents the *key*.
|
||||
|
||||
## Pushing Translations to Loco
|
||||
|
||||
You can push local translations to Loco using:
|
||||
|
||||
```bash
|
||||
symfony console translation:push loco --locales=fr --locales=nl --force
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Upload all French and Dutch translation values from `*.fr.yml` and `*.nl.yml` files
|
||||
- Ensure Loco stays in sync with local YAML files
|
||||
- Create any missing keys in Loco
|
||||
|
||||
## Pulling Translations from Loco
|
||||
|
||||
When translators update strings in Loco, developers can fetch updates with:
|
||||
|
||||
```bash
|
||||
symfony console translation:pull loco --locales=fr --locales=nl --force
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Download the latest French and Dutch translations
|
||||
- Overwrite the local YAML files with Loco's content
|
||||
- Keep everything consistent across the team
|
||||
|
||||
## Adding New Translation Keys (Developer Workflow)
|
||||
|
||||
1. Add a new key directly in the appropriate YAML file, for example:
|
||||
|
||||
```
|
||||
chill-bundles/ChillPersonBundle/translations/messages.fr.yml
|
||||
```
|
||||
|
||||
Example key:
|
||||
|
||||
```yaml
|
||||
person.form.submit: "Envoyer"
|
||||
```
|
||||
|
||||
2. Add Dutch translation as well if you can (otherwise leave empty to be translated within Loco later):
|
||||
|
||||
```yaml
|
||||
person.form.submit: "Verzenden"
|
||||
```
|
||||
|
||||
3. Run a push to send the new key to Loco:
|
||||
|
||||
```bash
|
||||
symfony console translation:push loco --locales=fr --locales=nl --force
|
||||
```
|
||||
|
||||
4. The key will now appear in Loco for translation management.
|
||||
|
||||
Note: English appears as "untranslated", because it is merely the source language.
|
||||
148
docs/source/development/translation_provider.rst
Normal file
148
docs/source/development/translation_provider.rst
Normal file
@@ -0,0 +1,148 @@
|
||||
=======================================================================
|
||||
Managing translations within CHILL using Loco as a translation provider
|
||||
=======================================================================
|
||||
|
||||
Within CHILL we make use of Symfony's translation component together with *Loco* as an external
|
||||
translation provider. Using this setup centralise translations in a single online
|
||||
location (Loco), while still allowing developers to create and update
|
||||
translation keys locally in the project (YAML files).
|
||||
|
||||
Workflow
|
||||
========
|
||||
|
||||
We use the following workflow:
|
||||
|
||||
* Developers create translation keys in YAML files inside each bundle.
|
||||
* Keys are written in **English**.
|
||||
* Application UI defaults to **French**, with **Dutch** as an additional locale (other languages can be added in the future).
|
||||
* Loco acts as the central translation memory and synchronisation source.
|
||||
* Loco Symfony package was installed so that built-in translation commands can be used to push/pull content
|
||||
between Loco and the local project.
|
||||
|
||||
|
||||
Translation directory structure
|
||||
===============================
|
||||
|
||||
Each bundle contains its own ``translations`` directory, for example::
|
||||
|
||||
chill-bundles/
|
||||
ChillCoreBundle/
|
||||
translations/
|
||||
messages.fr.yml
|
||||
messages.nl.yml
|
||||
ChillPersonBundle/
|
||||
translations/
|
||||
messages.fr.yml
|
||||
messages.nl.yml
|
||||
...
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
The translation configuration is defined in
|
||||
``config/packages/translation.yaml``::
|
||||
|
||||
framework:
|
||||
default_locale: '%env(resolve:LOCALE)%'
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- '%env(resolve:LOCALE)%'
|
||||
- 'en'
|
||||
providers:
|
||||
loco:
|
||||
dsn: '%env(LOCO_DSN)%'
|
||||
domains: [ 'messages' ]
|
||||
locales: [ 'fr', 'nl' ]
|
||||
|
||||
Note:
|
||||
|
||||
* ``en`` is the **source locale** in Loco.
|
||||
* ``fr`` and ``nl`` are the **application locales**.
|
||||
* ``domains: [messages]`` means only ``messages.*.yml`` files are pushed.
|
||||
|
||||
|
||||
Environment variables
|
||||
---------------------
|
||||
|
||||
In ``.env``::
|
||||
|
||||
LOCALE=fr
|
||||
|
||||
In ``.env.local``::
|
||||
|
||||
LOCO_DSN="loco://API_KEY@default"
|
||||
|
||||
Replace ``API_KEY`` with the key provided by Loco.
|
||||
|
||||
|
||||
Working with Loco
|
||||
=================
|
||||
|
||||
Loco shows all translation keys under three languages:
|
||||
|
||||
* **English (source)** — keys are listed but remain “untranslated”
|
||||
* **French** — translated strings for French users
|
||||
* **Dutch** — translated strings for Dutch users
|
||||
|
||||
Note: Don't add translations directly in the English column.
|
||||
This column simply represents the *key*.
|
||||
|
||||
|
||||
Pushing translations to Loco
|
||||
============================
|
||||
|
||||
You can push local translations to Loco using:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
symfony console translation:push loco --locales=fr --locales=nl --force
|
||||
|
||||
This will:
|
||||
|
||||
* Upload all French and Dutch translation values from ``*.fr.yml`` and
|
||||
``*.nl.yml`` files
|
||||
* Ensures Loco stays in sync with local YAML files
|
||||
* Creates any missing keys in Loco
|
||||
|
||||
|
||||
Pulling translations from Loco
|
||||
==============================
|
||||
|
||||
When translators update strings in Loco, developers can fetch updates with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
symfony console translation:pull loco --locales=fr --locales=nl --force
|
||||
|
||||
This will:
|
||||
|
||||
* Download the latest French and Dutch translations
|
||||
* Overwrite the local YAML files with Loco’s content
|
||||
* Keep everything consistent across the team
|
||||
|
||||
|
||||
Adding new translation keys (Developer workflow)
|
||||
================================================
|
||||
|
||||
1. Add a new key directly in the appropriate YAML file, for example::
|
||||
|
||||
chill-bundles/ChillPersonBundle/translations/messages.fr.yml
|
||||
|
||||
Example key::
|
||||
|
||||
person.form.submit: "Envoyer"
|
||||
|
||||
2. Add Dutch translation as well if you can (otherwise leave empty to be translated within Loco later)::
|
||||
|
||||
person.form.submit: "Verzenden"
|
||||
|
||||
3. Run a push to send the new key to Loco:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
symfony console translation:push loco --locales=fr --locales=nl --force
|
||||
|
||||
4. The key will now appear in Loco for translation management.
|
||||
|
||||
Note: English appears as “untranslated”, because it is merely the source language
|
||||
@@ -11,6 +11,7 @@
|
||||
"@hotwired/stimulus": "^3.0.0",
|
||||
"@luminateone/eslint-baseline": "^1.0.9",
|
||||
"@symfony/stimulus-bridge": "^3.2.0",
|
||||
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
|
||||
"@symfony/webpack-encore": "^4.1.0",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
@@ -41,6 +42,7 @@
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.13.0",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-tsc": "^3.1.3",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
@@ -80,12 +82,12 @@
|
||||
"dev": "encore dev",
|
||||
"watch": "encore dev --watch",
|
||||
"build": "encore production --progress",
|
||||
"specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml> templates/api/specs.yaml",
|
||||
"specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml src/Bundle/ChillTicketBundle/chill.api.specs.yaml> templates/api/specs.yaml",
|
||||
"specs-validate": "swagger-cli validate templates/api/specs.yaml",
|
||||
"specs-create-dir": "mkdir -p templates/api",
|
||||
"specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate",
|
||||
"version": "node --version",
|
||||
"eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
|
||||
"eslint": "eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
kind: Added
|
||||
body: Use admin delegated account for handling authentication
|
||||
time: 2026-01-22T15:32:23.932994899+01:00
|
||||
@@ -9,7 +9,7 @@
|
||||
"social worker"
|
||||
],
|
||||
"require": {
|
||||
"chill-project/chill-bundles": "^4.9.0",
|
||||
"chill-project/chill-bundles": "dev-master as v4.6.1",
|
||||
"zimbra-api/soap-api": "^3.2.2",
|
||||
"psr/http-client": "^1.0",
|
||||
"nyholm/psr7": "^1.0"
|
||||
|
||||
@@ -80,19 +80,12 @@ final readonly class CreateZimbraComponent
|
||||
$location = $calendar->getCalendar()->getLocation();
|
||||
$hasLocation = $calendar->getCalendar()->hasLocation();
|
||||
$isPrivate = $calendar->getCalendar()->getAccompanyingPeriod()?->isConfidential() ?? false;
|
||||
} elseif ($calendar instanceof Calendar) {
|
||||
} else {
|
||||
$startDate = $calendar->getStartDate();
|
||||
$endDate = $calendar->getEndDate();
|
||||
$location = $calendar->getLocation();
|
||||
$hasLocation = $calendar->hasLocation();
|
||||
$isPrivate = $calendar->getAccompanyingPeriod()?->isConfidential() ?? false;
|
||||
} else {
|
||||
// Calendar range case
|
||||
$startDate = $calendar->getStartDate();
|
||||
$endDate = $calendar->getEndDate();
|
||||
$location = $calendar->getLocation();
|
||||
$hasLocation = $calendar->hasLocation();
|
||||
$isPrivate = false;
|
||||
}
|
||||
|
||||
$comp = new InviteComponent();
|
||||
|
||||
@@ -11,84 +11,48 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
|
||||
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpClient\Psr18Client;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Zimbra\Admin\AdminApi;
|
||||
use Zimbra\Common\Enum\AccountBy;
|
||||
use Zimbra\Common\Soap\ClientFactory;
|
||||
use Zimbra\Common\Struct\AccountSelector;
|
||||
use Zimbra\Common\Struct\Header\AccountInfo;
|
||||
use Zimbra\Mail\MailApi;
|
||||
|
||||
final class SoapClientBuilder
|
||||
final readonly class SoapClientBuilder
|
||||
{
|
||||
private readonly string $username;
|
||||
private string $username;
|
||||
|
||||
private readonly string $password;
|
||||
private string $password;
|
||||
|
||||
private readonly string $url;
|
||||
private string $url;
|
||||
|
||||
private readonly string $adminUrl;
|
||||
|
||||
private readonly bool $verifyHost;
|
||||
|
||||
private readonly bool $verifyPeer;
|
||||
|
||||
private readonly bool $adminVerifyHost;
|
||||
|
||||
private readonly bool $adminVerifyPeer;
|
||||
|
||||
/**
|
||||
* Keep the cache of the tokens.
|
||||
*
|
||||
* @var array<string, array{token: string, expirationTime: \DateTimeImmutable}>
|
||||
*/
|
||||
private array $tokenCache = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly ParameterBagInterface $parameterBag,
|
||||
private readonly HttpClientInterface $client,
|
||||
private readonly ClockInterface $clock,
|
||||
) {
|
||||
public function __construct(private ParameterBagInterface $parameterBag, private HttpClientInterface $client)
|
||||
{
|
||||
$dsn = $this->parameterBag->get('chill_calendar.remote_calendar_dsn');
|
||||
$url = parse_url($dsn);
|
||||
|
||||
$this->username = urldecode($url['user']);
|
||||
$this->password = urldecode($url['pass']);
|
||||
if ('zimbra+http' === $url['scheme']) {
|
||||
$scheme = 'http';
|
||||
$scheme = 'http://';
|
||||
$port = $url['port'] ?? 80;
|
||||
} elseif ('zimbra+https' === $url['scheme']) {
|
||||
$scheme = 'https';
|
||||
$scheme = 'https://';
|
||||
$port = $url['port'] ?? 443;
|
||||
} else {
|
||||
throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Unsupported remote calendar scheme: '.$url['scheme']);
|
||||
}
|
||||
|
||||
// get attributes for adminUrl
|
||||
$query = [];
|
||||
parse_str($url['query'] ?? '', $query);
|
||||
$adminPort = $query['adminPort'] ?? '7071';
|
||||
$adminHost = $query['adminHost'] ?? $url['host'];
|
||||
$adminScheme = $query['adminScheme'] ?? $scheme;
|
||||
|
||||
$this->verifyPeer = (bool) ($query['verifyPeer'] ?? true);
|
||||
$this->verifyHost = (bool) ($query['verifyHost'] ?? true);
|
||||
$this->adminVerifyHost = (bool) ($query['adminVerifyHost'] ?? $this->verifyPeer);
|
||||
$this->adminVerifyPeer = (bool) ($query['adminVerifyPeer'] ?? $this->verifyHost);
|
||||
|
||||
$this->url = $scheme.'://'.$url['host'].':'.$port;
|
||||
$this->adminUrl = $adminScheme.'://'.$adminHost.':'.$adminPort;
|
||||
$this->url = $scheme.$url['host'].':'.$port;
|
||||
}
|
||||
|
||||
private function buildApi(): MailApi
|
||||
{
|
||||
$baseClient = $this->client->withOptions([
|
||||
'base_uri' => $location = $this->url.'/service/soap',
|
||||
'verify_host' => $this->verifyHost,
|
||||
'verify_peer' => $this->verifyPeer,
|
||||
'verify_host' => false,
|
||||
'verify_peer' => false,
|
||||
]);
|
||||
$psr18Client = new Psr18Client($baseClient);
|
||||
$api = new MailApi();
|
||||
@@ -98,36 +62,12 @@ final class SoapClientBuilder
|
||||
return $api;
|
||||
}
|
||||
|
||||
private function buildAdminApi(): AdminApi
|
||||
{
|
||||
$baseClient = $this->client->withOptions([
|
||||
'base_uri' => $location = $this->adminUrl.'/service/admin/soap',
|
||||
'verify_host' => $this->adminVerifyHost,
|
||||
'verify_peer' => $this->adminVerifyPeer,
|
||||
]);
|
||||
$psr18Client = new Psr18Client($baseClient);
|
||||
$api = new AdminApi();
|
||||
$client = ClientFactory::create($location, $psr18Client);
|
||||
$api->setClient($client);
|
||||
|
||||
return $api;
|
||||
}
|
||||
|
||||
public function getApiForAccount(string $accountName): MailApi
|
||||
{
|
||||
['token' => $token, 'expirationTime' => $expirationTime] = $this->tokenCache[$accountName]
|
||||
?? ['token' => null, 'expirationTime' => null];
|
||||
$api = $this->buildApi();
|
||||
$response = $api->authByAccountName($this->username, $this->password);
|
||||
|
||||
if (null === $token || null === $expirationTime || $expirationTime <= $this->clock->now()) {
|
||||
$adminApi = $this->buildAdminApi();
|
||||
$adminApi->auth($this->username, $this->password);
|
||||
|
||||
$delegateResponse = $adminApi->delegateAuth(new AccountSelector(AccountBy::NAME, $accountName));
|
||||
$token = $delegateResponse->getAuthToken();
|
||||
$expiration = $delegateResponse->getLifetime();
|
||||
$expirationTime = $this->clock->now()->add(new \DateInterval('PT'.$expiration.'S'));
|
||||
$this->tokenCache[$accountName] = ['token' => $token, 'expirationTime' => $expirationTime];
|
||||
}
|
||||
$token = $response->getAuthToken();
|
||||
|
||||
$apiBy = $this->buildApi();
|
||||
$apiBy->setAuthToken($token);
|
||||
|
||||
@@ -3,6 +3,7 @@ parameters:
|
||||
paths:
|
||||
- src/
|
||||
- utils/
|
||||
- packages/
|
||||
tmpDir: var/cache/phpstan
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
excludePaths:
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
<!-- temporarily removed, the time to find a fix -->
|
||||
<exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude>
|
||||
</testsuite>
|
||||
|
||||
<testsuite name="TicketBundle">
|
||||
<directory suffix="Test.php">src/Bundle/ChillTicketBundle/tests/</directory>
|
||||
</testsuite>
|
||||
<!--
|
||||
<testsuite name="ReportBundle">
|
||||
<directory suffix="Test.php">src/Bundle/ChillReportBundle/Tests/</directory>
|
||||
|
||||
8
resources/ticket_motives_import/README.md
Normal file
8
resources/ticket_motives_import/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
In this directory, you find an example of file for the command `chill:main:ticket_motives_import`.
|
||||
|
||||
This file contains a list of ticket motives to import into the system. Each entry is a dictionary with two keys: `code` and `label`. The `code` key contains the unique code for the ticket motive, and the `label` key contains the human-readable label for the ticket motive.
|
||||
|
||||
The `stored_objects` key contains the documents that will be associated with the tickets. They must be found in the same directory.
|
||||
|
||||
The command `chill:main:ticket_motives_import` uses this file to import the specified ticket motives into the system.
|
||||
|
||||
136
resources/ticket_motives_import/motives.yaml
Normal file
136
resources/ticket_motives_import/motives.yaml
Normal file
@@ -0,0 +1,136 @@
|
||||
- label:
|
||||
fr: Appel famille pour annonce de décès
|
||||
urgent: false
|
||||
supplementary_informations:
|
||||
- label:
|
||||
fr: Date du décès
|
||||
- label:
|
||||
fr: lieu du décès (domicile ou hôpital)
|
||||
- label:
|
||||
fr: nom de l’hôpital
|
||||
- label:
|
||||
fr: service concerné
|
||||
stored_objects:
|
||||
- label:
|
||||
fr: ☀️ De 07h à 21h
|
||||
filename: 2_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🌙 De 21h à 07h du matin
|
||||
filename: 3_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🗓️ Dimanches et jours fériés
|
||||
filename: 4_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 'Appel famille pour annonce absence : hospitalisation ou consultation'
|
||||
urgent: false
|
||||
supplementary_informations:
|
||||
- label:
|
||||
fr: Quel hôpital
|
||||
- label:
|
||||
fr: quel service
|
||||
- label:
|
||||
fr: pour quelles raisons
|
||||
- label:
|
||||
fr: 'consultation : date et heure'
|
||||
- label:
|
||||
fr: hospitalisation complète ou HDJ
|
||||
stored_objects:
|
||||
- label:
|
||||
fr: ☀️ De 07h à 21h
|
||||
filename: 5_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🌙 De 21h à 07h du matin
|
||||
filename: 6_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🗓️ Dimanches et jours fériés
|
||||
filename: 7_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 'Appel famille pour annonce absence : interruption de prise en charge'
|
||||
urgent: false
|
||||
supplementary_informations:
|
||||
- label:
|
||||
fr: Pour quelles raisons ? Date
|
||||
- label:
|
||||
fr: durée
|
||||
- label:
|
||||
fr: accord médical ?
|
||||
stored_objects:
|
||||
- label:
|
||||
fr: ☀️ De 07h à 21h
|
||||
filename: 8_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🌙 De 21h à 07h du matin
|
||||
filename: 9_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🗓️ Dimanches et jours fériés
|
||||
filename: 10_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 'Appel famille pour annonce absence : changement d’adresse'
|
||||
urgent: false
|
||||
supplementary_informations:
|
||||
- label:
|
||||
fr: Où
|
||||
- label:
|
||||
fr: Pourquoi ? Pour combien de temps ? Besoin d’un relais des soins ? Nouvelle adresse ?
|
||||
stored_objects:
|
||||
- label:
|
||||
fr: ☀️ De 07h à 21h
|
||||
filename: 11_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🌙 De 21h à 07h du matin
|
||||
filename: 12_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🗓️ Dimanches et jours fériés
|
||||
filename: 13_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: Appel famille pour altération de l’état général du patient
|
||||
urgent: true
|
||||
supplementary_informations:
|
||||
- label:
|
||||
fr: Recherche des symptômes
|
||||
- label:
|
||||
fr: Attentes par rapport à la demande
|
||||
stored_objects:
|
||||
- label:
|
||||
fr: ☀️ De 07h à 21h
|
||||
filename: 14_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🌙 De 21h à 07h du matin
|
||||
filename: 15_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🗓️ Dimanches et jours fériés
|
||||
filename: 16_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: Appel famille pour prise en charge de la douleur
|
||||
urgent: true
|
||||
supplementary_informations:
|
||||
- label:
|
||||
fr: Localisation douleur
|
||||
- label:
|
||||
fr: Horaire dernier passage
|
||||
- label:
|
||||
fr: Traitements en cours
|
||||
stored_objects:
|
||||
- label:
|
||||
fr: ☀️ De 07h à 21h
|
||||
filename: 17_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🌙 De 21h à 07h du matin
|
||||
filename: 18_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🗓️ Dimanches et jours fériés
|
||||
filename: 19_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: Appel famille pour information sur la date de prise en charge
|
||||
urgent: false
|
||||
supplementary_informations: []
|
||||
stored_objects:
|
||||
- label:
|
||||
fr: ☀️ De 07h à 21h
|
||||
filename: 20_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🌙 De 21h à 07h du matin
|
||||
filename: 21_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
- label:
|
||||
fr: 🗓️ Dimanches et jours fériés
|
||||
filename: 22_doc_20250402_Pelotons flux externes consolidés.pdf
|
||||
6
resources/translation_override/README.md
Normal file
6
resources/translation_override/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
In this directory, you find an example of file for the command `chill:main:override_translation`.
|
||||
|
||||
This file contains a list of translations to override in the translation catalogue. Each entry is a dictionary with two keys: `from` and `to`. The `from` key contains the original translation string, and the `to` key contains the replacement string.
|
||||
|
||||
The command `chill:main:override_translation` uses this file to generate a new translation catalogue with the specified overrides applied.
|
||||
|
||||
8
resources/translation_override/overrides.yaml
Normal file
8
resources/translation_override/overrides.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
- {from: "de l'usager", to: "du patient"}
|
||||
- {from: "l'usager", to: "le patient"}
|
||||
- {from: "L'usager", to: "Le patient"}
|
||||
- {from: "d'usagers", to: "de patients"}
|
||||
- {from: "usagers", to: "patients"}
|
||||
- {from: "Usagers", to: "Patients"}
|
||||
- {from: "usager", to: "patient"}
|
||||
- {from: "Usager", to: "Patient"}
|
||||
@@ -33,7 +33,6 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $em)
|
||||
{
|
||||
mt_srand(123456789);
|
||||
$this->faker = FakerFactory::create('fr_FR');
|
||||
}
|
||||
|
||||
@@ -49,7 +48,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
|
||||
->findAll();
|
||||
|
||||
foreach ($persons as $person) {
|
||||
$activityNbr = mt_rand(0, 3);
|
||||
$activityNbr = random_int(0, 3);
|
||||
|
||||
for ($i = 0; $i < $activityNbr; ++$i) {
|
||||
$activity = $this->newRandomActivity($person);
|
||||
@@ -74,7 +73,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
|
||||
|
||||
// ->setAttendee($this->faker->boolean())
|
||||
|
||||
for ($i = 0; mt_rand(0, 4) > $i; ++$i) {
|
||||
for ($i = 0; random_int(0, 4) > $i; ++$i) {
|
||||
$reason = $this->getRandomActivityReason();
|
||||
|
||||
if (null !== $reason) {
|
||||
|
||||
@@ -69,7 +69,7 @@ class ChillActivityExtension extends Extension implements PrependExtensionInterf
|
||||
}
|
||||
|
||||
/** (non-PHPdoc).
|
||||
* @see PrependExtensionInterface::prepend()
|
||||
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
|
||||
*/
|
||||
public function prependRoutes(ContainerBuilder $container)
|
||||
{
|
||||
|
||||
@@ -27,8 +27,7 @@ class ByActivityNumberAggregator implements AggregatorInterface
|
||||
public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
|
||||
{
|
||||
$qb
|
||||
// Use a distinct alias inside the subquery to avoid colliding with the root alias "activity"
|
||||
->addSelect('(SELECT COUNT(agg_activity.id) FROM '.Activity::class.' agg_activity WHERE agg_activity.accompanyingPeriod = acp) AS activity_by_number_aggregator')
|
||||
->addSelect('(SELECT COUNT(activity.id) FROM '.Activity::class.' activity WHERE activity.accompanyingPeriod = acp) AS activity_by_number_aggregator')
|
||||
->addGroupBy('activity_by_number_aggregator');
|
||||
}
|
||||
|
||||
@@ -66,7 +65,7 @@ class ByActivityNumberAggregator implements AggregatorInterface
|
||||
{
|
||||
return static function ($value) {
|
||||
if ('_header' === $value) {
|
||||
return 'Count activities linked to an accompanying period';
|
||||
return '';
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
|
||||
@@ -24,7 +24,6 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInt
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -341,7 +340,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
|
||||
}
|
||||
|
||||
foreach ($person->getAccompanyingPeriodParticipations() as $participation) {
|
||||
if (!$this->security->isGranted(AccompanyingPeriodVoter::SEE, $participation->getAccompanyingPeriod())) {
|
||||
if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<concerned-groups v-if="hasPerson" />
|
||||
<social-issues-acc v-if="hasSocialIssues" />
|
||||
<location v-if="hasLocation" />
|
||||
<concerned-groups v-if="hasPerson" />
|
||||
<social-issues-acc v-if="hasSocialIssues" />
|
||||
<location v-if="hasLocation" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -10,12 +10,12 @@ import SocialIssuesAcc from "./components/SocialIssuesAcc.vue";
|
||||
import Location from "./components/Location.vue";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
props: ["hasSocialIssues", "hasLocation", "hasPerson"],
|
||||
components: {
|
||||
ConcernedGroups,
|
||||
SocialIssuesAcc,
|
||||
Location,
|
||||
},
|
||||
name: "App",
|
||||
props: ["hasSocialIssues", "hasLocation", "hasPerson"],
|
||||
components: {
|
||||
ConcernedGroups,
|
||||
SocialIssuesAcc,
|
||||
Location,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,46 +1,43 @@
|
||||
<template>
|
||||
<teleport to="#add-persons" v-if="isComponentVisible">
|
||||
<div class="flex-bloc concerned-groups" :class="getContext">
|
||||
<persons-bloc
|
||||
v-for="bloc in contextPersonsBlocs"
|
||||
:key="bloc.key"
|
||||
:bloc="bloc"
|
||||
:bloc-width="getBlocWidth"
|
||||
:set-persons-in-bloc="setPersonsInBloc"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
getContext === 'accompanyingCourse' &&
|
||||
suggestedEntities.length > 0
|
||||
"
|
||||
<teleport to="#add-persons" v-if="isComponentVisible">
|
||||
<div class="flex-bloc concerned-groups" :class="getContext">
|
||||
<persons-bloc
|
||||
v-for="bloc in contextPersonsBlocs"
|
||||
:key="bloc.key"
|
||||
:bloc="bloc"
|
||||
:bloc-width="getBlocWidth"
|
||||
:set-persons-in-bloc="setPersonsInBloc"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="getContext === 'accompanyingCourse' && suggestedEntities.length > 0"
|
||||
>
|
||||
<ul class="list-suggest add-items inline">
|
||||
<li
|
||||
v-for="(p, i) in suggestedEntities"
|
||||
@click="addSuggestedEntity(p)"
|
||||
:key="`suggestedEntities-${i}`"
|
||||
>
|
||||
<ul class="list-suggest add-items inline">
|
||||
<li
|
||||
v-for="(p, i) in suggestedEntities"
|
||||
@click="addSuggestedEntity(p)"
|
||||
:key="`suggestedEntities-${i}`"
|
||||
>
|
||||
<person-text v-if="p.type === 'person'" :person="p" />
|
||||
<span v-else>{{ p.text }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<person-text v-if="p.type === 'person'" :person="p" />
|
||||
<span v-else>{{ p.text }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="record_actions">
|
||||
<li class="add-persons">
|
||||
<add-persons
|
||||
:buttonTitle="trans(ACTIVITY_ADD_PERSONS)"
|
||||
:modalTitle="trans(ACTIVITY_ADD_PERSONS)"
|
||||
v-bind:key="addPersons.key"
|
||||
v-bind:options="addPersonsOptions"
|
||||
@addNewPersons="addNewPersons"
|
||||
ref="addPersons"
|
||||
>
|
||||
</add-persons>
|
||||
</li>
|
||||
</ul>
|
||||
</teleport>
|
||||
<ul class="record_actions">
|
||||
<li class="add-persons">
|
||||
<add-persons
|
||||
:buttonTitle="trans(ACTIVITY_ADD_PERSONS)"
|
||||
:modalTitle="trans(ACTIVITY_ADD_PERSONS)"
|
||||
v-bind:key="addPersons.key"
|
||||
v-bind:options="addPersonsOptions"
|
||||
@addNewPersons="addNewPersons"
|
||||
ref="addPersons"
|
||||
>
|
||||
</add-persons>
|
||||
</li>
|
||||
</ul>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -49,208 +46,208 @@ import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
|
||||
import PersonsBloc from "./ConcernedGroups/PersonsBloc.vue";
|
||||
import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
|
||||
import {
|
||||
ACTIVITY_BLOC_PERSONS,
|
||||
ACTIVITY_BLOC_PERSONS_ASSOCIATED,
|
||||
ACTIVITY_BLOC_THIRDPARTY,
|
||||
ACTIVITY_BLOC_USERS,
|
||||
ACTIVITY_ADD_PERSONS,
|
||||
trans,
|
||||
ACTIVITY_BLOC_PERSONS,
|
||||
ACTIVITY_BLOC_PERSONS_ASSOCIATED,
|
||||
ACTIVITY_BLOC_THIRDPARTY,
|
||||
ACTIVITY_BLOC_USERS,
|
||||
ACTIVITY_ADD_PERSONS,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
export default {
|
||||
name: "ConcernedGroups",
|
||||
components: {
|
||||
AddPersons,
|
||||
PersonsBloc,
|
||||
PersonText,
|
||||
name: "ConcernedGroups",
|
||||
components: {
|
||||
AddPersons,
|
||||
PersonsBloc,
|
||||
PersonText,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
trans,
|
||||
ACTIVITY_ADD_PERSONS,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
personsBlocs: [
|
||||
{
|
||||
key: "persons",
|
||||
title: trans(ACTIVITY_BLOC_PERSONS),
|
||||
persons: [],
|
||||
included: false,
|
||||
},
|
||||
{
|
||||
key: "personsAssociated",
|
||||
title: trans(ACTIVITY_BLOC_PERSONS_ASSOCIATED),
|
||||
persons: [],
|
||||
included: window.activity
|
||||
? window.activity.activityType.personsVisible !== 0
|
||||
: true,
|
||||
},
|
||||
{
|
||||
key: "personsNotAssociated",
|
||||
title: "activity.bloc_persons_not_associated",
|
||||
persons: [],
|
||||
included: window.activity
|
||||
? window.activity.activityType.personsVisible !== 0
|
||||
: true,
|
||||
},
|
||||
{
|
||||
key: "thirdparty",
|
||||
title: trans(ACTIVITY_BLOC_THIRDPARTY),
|
||||
persons: [],
|
||||
included: window.activity
|
||||
? window.activity.activityType.thirdPartiesVisible !== 0
|
||||
: true,
|
||||
},
|
||||
{
|
||||
key: "users",
|
||||
title: trans(ACTIVITY_BLOC_USERS),
|
||||
persons: [],
|
||||
included: window.activity
|
||||
? window.activity.activityType.usersVisible !== 0
|
||||
: true,
|
||||
},
|
||||
],
|
||||
addPersons: {
|
||||
key: "activity",
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isComponentVisible() {
|
||||
return window.activity
|
||||
? window.activity.activityType.personsVisible !== 0 ||
|
||||
window.activity.activityType.thirdPartiesVisible !== 0 ||
|
||||
window.activity.activityType.usersVisible !== 0
|
||||
: true;
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
trans,
|
||||
ACTIVITY_ADD_PERSONS,
|
||||
};
|
||||
...mapState({
|
||||
persons: (state) => state.activity.persons,
|
||||
thirdParties: (state) => state.activity.thirdParties,
|
||||
users: (state) => state.activity.users,
|
||||
accompanyingCourse: (state) => state.activity.accompanyingPeriod,
|
||||
}),
|
||||
...mapGetters(["suggestedEntities"]),
|
||||
getContext() {
|
||||
return this.accompanyingCourse ? "accompanyingCourse" : "person";
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
personsBlocs: [
|
||||
{
|
||||
key: "persons",
|
||||
title: trans(ACTIVITY_BLOC_PERSONS),
|
||||
persons: [],
|
||||
included: false,
|
||||
},
|
||||
{
|
||||
key: "personsAssociated",
|
||||
title: trans(ACTIVITY_BLOC_PERSONS_ASSOCIATED),
|
||||
persons: [],
|
||||
included: window.activity
|
||||
? window.activity.activityType.personsVisible !== 0
|
||||
: true,
|
||||
},
|
||||
{
|
||||
key: "personsNotAssociated",
|
||||
title: "activity.bloc_persons_not_associated",
|
||||
persons: [],
|
||||
included: window.activity
|
||||
? window.activity.activityType.personsVisible !== 0
|
||||
: true,
|
||||
},
|
||||
{
|
||||
key: "thirdparty",
|
||||
title: trans(ACTIVITY_BLOC_THIRDPARTY),
|
||||
persons: [],
|
||||
included: window.activity
|
||||
? window.activity.activityType.thirdPartiesVisible !== 0
|
||||
: true,
|
||||
},
|
||||
{
|
||||
key: "users",
|
||||
title: trans(ACTIVITY_BLOC_USERS),
|
||||
persons: [],
|
||||
included: window.activity
|
||||
? window.activity.activityType.usersVisible !== 0
|
||||
: true,
|
||||
},
|
||||
],
|
||||
addPersons: {
|
||||
key: "activity",
|
||||
},
|
||||
};
|
||||
contextPersonsBlocs() {
|
||||
return this.personsBlocs.filter((bloc) => bloc.included !== false);
|
||||
},
|
||||
computed: {
|
||||
isComponentVisible() {
|
||||
return window.activity
|
||||
? window.activity.activityType.personsVisible !== 0 ||
|
||||
window.activity.activityType.thirdPartiesVisible !== 0 ||
|
||||
window.activity.activityType.usersVisible !== 0
|
||||
: true;
|
||||
},
|
||||
...mapState({
|
||||
persons: (state) => state.activity.persons,
|
||||
thirdParties: (state) => state.activity.thirdParties,
|
||||
users: (state) => state.activity.users,
|
||||
accompanyingCourse: (state) => state.activity.accompanyingPeriod,
|
||||
}),
|
||||
...mapGetters(["suggestedEntities"]),
|
||||
getContext() {
|
||||
return this.accompanyingCourse ? "accompanyingCourse" : "person";
|
||||
},
|
||||
contextPersonsBlocs() {
|
||||
return this.personsBlocs.filter((bloc) => bloc.included !== false);
|
||||
},
|
||||
addPersonsOptions() {
|
||||
let optionsType = [];
|
||||
if (window.activity) {
|
||||
if (window.activity.activityType.personsVisible !== 0) {
|
||||
optionsType.push("person");
|
||||
}
|
||||
if (window.activity.activityType.thirdPartiesVisible !== 0) {
|
||||
optionsType.push("thirdparty");
|
||||
}
|
||||
if (window.activity.activityType.usersVisible !== 0) {
|
||||
optionsType.push("user");
|
||||
}
|
||||
} else {
|
||||
optionsType = ["person", "thirdparty", "user"];
|
||||
}
|
||||
return {
|
||||
type: optionsType,
|
||||
priority: null,
|
||||
uniq: false,
|
||||
button: {
|
||||
size: "btn-sm",
|
||||
},
|
||||
};
|
||||
},
|
||||
getBlocWidth() {
|
||||
return Math.round(100 / this.contextPersonsBlocs.length) + "%";
|
||||
addPersonsOptions() {
|
||||
let optionsType = [];
|
||||
if (window.activity) {
|
||||
if (window.activity.activityType.personsVisible !== 0) {
|
||||
optionsType.push("person");
|
||||
}
|
||||
if (window.activity.activityType.thirdPartiesVisible !== 0) {
|
||||
optionsType.push("thirdparty");
|
||||
}
|
||||
if (window.activity.activityType.usersVisible !== 0) {
|
||||
optionsType.push("user");
|
||||
}
|
||||
} else {
|
||||
optionsType = ["person", "thirdparty", "user"];
|
||||
}
|
||||
return {
|
||||
type: optionsType,
|
||||
priority: null,
|
||||
uniq: false,
|
||||
button: {
|
||||
size: "btn-sm",
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.setPersonsInBloc();
|
||||
getBlocWidth() {
|
||||
return Math.round(100 / this.contextPersonsBlocs.length) + "%";
|
||||
},
|
||||
methods: {
|
||||
setPersonsInBloc() {
|
||||
let groups;
|
||||
if (this.accompanyingCourse) {
|
||||
groups = this.splitPersonsInGroups();
|
||||
}
|
||||
this.personsBlocs.forEach((bloc) => {
|
||||
if (this.accompanyingCourse) {
|
||||
switch (bloc.key) {
|
||||
case "personsAssociated":
|
||||
bloc.persons = groups.personsAssociated;
|
||||
bloc.included = true;
|
||||
break;
|
||||
case "personsNotAssociated":
|
||||
bloc.persons = groups.personsNotAssociated;
|
||||
bloc.included = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (bloc.key) {
|
||||
case "persons":
|
||||
bloc.persons = this.persons;
|
||||
bloc.included = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
switch (bloc.key) {
|
||||
case "thirdparty":
|
||||
bloc.persons = this.thirdParties;
|
||||
break;
|
||||
case "users":
|
||||
bloc.persons = this.users;
|
||||
break;
|
||||
}
|
||||
}, groups);
|
||||
},
|
||||
splitPersonsInGroups() {
|
||||
let personsAssociated = [];
|
||||
let personsNotAssociated = this.persons;
|
||||
let participations = this.getCourseParticipations();
|
||||
this.persons.forEach((person) => {
|
||||
participations.forEach((participation) => {
|
||||
if (person.id === participation.id) {
|
||||
//console.log(person.id);
|
||||
personsAssociated.push(person);
|
||||
personsNotAssociated = personsNotAssociated.filter(
|
||||
(p) => p !== person,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
personsAssociated: personsAssociated,
|
||||
personsNotAssociated: personsNotAssociated,
|
||||
};
|
||||
},
|
||||
getCourseParticipations() {
|
||||
let participations = [];
|
||||
this.accompanyingCourse.participations.forEach((participation) => {
|
||||
if (!participation.endDate) {
|
||||
participations.push(participation.person);
|
||||
}
|
||||
});
|
||||
return participations;
|
||||
},
|
||||
addNewPersons({ selected, modal }) {
|
||||
console.log("@@@ CLICK button addNewPersons", selected);
|
||||
selected.forEach((item) => {
|
||||
this.$store.dispatch("addPersonsInvolved", item);
|
||||
}, this);
|
||||
this.$refs.addPersons.resetSearch(); // to cast child method
|
||||
modal.showModal = false;
|
||||
this.setPersonsInBloc();
|
||||
},
|
||||
addSuggestedEntity(person) {
|
||||
this.$store.dispatch("addPersonsInvolved", {
|
||||
result: person,
|
||||
type: "person",
|
||||
});
|
||||
this.setPersonsInBloc();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setPersonsInBloc();
|
||||
},
|
||||
methods: {
|
||||
setPersonsInBloc() {
|
||||
let groups;
|
||||
if (this.accompanyingCourse) {
|
||||
groups = this.splitPersonsInGroups();
|
||||
}
|
||||
this.personsBlocs.forEach((bloc) => {
|
||||
if (this.accompanyingCourse) {
|
||||
switch (bloc.key) {
|
||||
case "personsAssociated":
|
||||
bloc.persons = groups.personsAssociated;
|
||||
bloc.included = true;
|
||||
break;
|
||||
case "personsNotAssociated":
|
||||
bloc.persons = groups.personsNotAssociated;
|
||||
bloc.included = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (bloc.key) {
|
||||
case "persons":
|
||||
bloc.persons = this.persons;
|
||||
bloc.included = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
switch (bloc.key) {
|
||||
case "thirdparty":
|
||||
bloc.persons = this.thirdParties;
|
||||
break;
|
||||
case "users":
|
||||
bloc.persons = this.users;
|
||||
break;
|
||||
}
|
||||
}, groups);
|
||||
},
|
||||
splitPersonsInGroups() {
|
||||
let personsAssociated = [];
|
||||
let personsNotAssociated = this.persons;
|
||||
let participations = this.getCourseParticipations();
|
||||
this.persons.forEach((person) => {
|
||||
participations.forEach((participation) => {
|
||||
if (person.id === participation.id) {
|
||||
//console.log(person.id);
|
||||
personsAssociated.push(person);
|
||||
personsNotAssociated = personsNotAssociated.filter(
|
||||
(p) => p !== person,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
personsAssociated: personsAssociated,
|
||||
personsNotAssociated: personsNotAssociated,
|
||||
};
|
||||
},
|
||||
getCourseParticipations() {
|
||||
let participations = [];
|
||||
this.accompanyingCourse.participations.forEach((participation) => {
|
||||
if (!participation.endDate) {
|
||||
participations.push(participation.person);
|
||||
}
|
||||
});
|
||||
return participations;
|
||||
},
|
||||
addNewPersons({ selected, modal }) {
|
||||
console.log("@@@ CLICK button addNewPersons", selected);
|
||||
selected.forEach((item) => {
|
||||
this.$store.dispatch("addPersonsInvolved", item);
|
||||
}, this);
|
||||
this.$refs.addPersons.resetSearch(); // to cast child method
|
||||
modal.showModal = false;
|
||||
this.setPersonsInBloc();
|
||||
},
|
||||
addSuggestedEntity(person) {
|
||||
this.$store.dispatch("addPersonsInvolved", {
|
||||
result: person,
|
||||
type: "person",
|
||||
});
|
||||
this.setPersonsInBloc();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<template>
|
||||
<li>
|
||||
<span :title="person.text" @click.prevent="$emit('remove', person)">
|
||||
<span class="chill_denomination">
|
||||
<person-text :person="person" :is-cut="true" />
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span :title="person.text" @click.prevent="$emit('remove', person)">
|
||||
<span class="chill_denomination">
|
||||
<person-text :person="person" :is-cut="true" />
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
|
||||
|
||||
export default {
|
||||
name: "PersonBadge",
|
||||
props: ["person"],
|
||||
components: {
|
||||
PersonText,
|
||||
},
|
||||
// computed: {
|
||||
// textCutted() {
|
||||
// let more = (this.person.text.length > 15) ?'…' : '';
|
||||
// return this.person.text.slice(0,15) + more;
|
||||
// }
|
||||
// },
|
||||
emits: ["remove"],
|
||||
name: "PersonBadge",
|
||||
props: ["person"],
|
||||
components: {
|
||||
PersonText,
|
||||
},
|
||||
// computed: {
|
||||
// textCutted() {
|
||||
// let more = (this.person.text.length > 15) ?'…' : '';
|
||||
// return this.person.text.slice(0,15) + more;
|
||||
// }
|
||||
// },
|
||||
emits: ["remove"],
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<template>
|
||||
<div class="item-bloc" :style="{ 'flex-basis': blocWidth }">
|
||||
<div class="item-row">
|
||||
<div class="item-col">
|
||||
<h4>{{ $t(bloc.title) }}</h4>
|
||||
</div>
|
||||
<div class="item-col">
|
||||
<ul class="list-suggest remove-items">
|
||||
<person-badge
|
||||
v-for="person in bloc.persons"
|
||||
:key="person.id"
|
||||
:person="person"
|
||||
@remove="removePerson"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-bloc" :style="{ 'flex-basis': blocWidth }">
|
||||
<div class="item-row">
|
||||
<div class="item-col">
|
||||
<h4>{{ $t(bloc.title) }}</h4>
|
||||
</div>
|
||||
<div class="item-col">
|
||||
<ul class="list-suggest remove-items">
|
||||
<person-badge
|
||||
v-for="person in bloc.persons"
|
||||
:key="person.id"
|
||||
:person="person"
|
||||
@remove="removePerson"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PersonBadge from "./PersonBadge.vue";
|
||||
export default {
|
||||
name: "PersonsBloc",
|
||||
components: {
|
||||
PersonBadge,
|
||||
},
|
||||
props: ["bloc", "setPersonsInBloc", "blocWidth"],
|
||||
methods: {
|
||||
removePerson(item) {
|
||||
console.log("@@ CLICK remove person: item", item);
|
||||
this.$store.dispatch("removePersonInvolved", item);
|
||||
this.setPersonsInBloc();
|
||||
},
|
||||
name: "PersonsBloc",
|
||||
components: {
|
||||
PersonBadge,
|
||||
},
|
||||
props: ["bloc", "setPersonsInBloc", "blocWidth"],
|
||||
methods: {
|
||||
removePerson(item) {
|
||||
console.log("@@ CLICK remove person: item", item);
|
||||
this.$store.dispatch("removePersonInvolved", item);
|
||||
this.setPersonsInBloc();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<template>
|
||||
<teleport to="#location">
|
||||
<div class="mb-3 row">
|
||||
<label :class="locationClassList">
|
||||
{{ trans(ACTIVITY_LOCATION) }}
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<VueMultiselect
|
||||
name="selectLocation"
|
||||
id="selectLocation"
|
||||
label="name"
|
||||
track-by="id"
|
||||
open-direction="top"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:placeholder="trans(ACTIVITY_CHOOSE_LOCATION)"
|
||||
:custom-label="customLabel"
|
||||
:select-label="trans(MULTISELECT_SELECT_LABEL)"
|
||||
:deselect-label="trans(MULTISELECT_DESELECT_LABEL)"
|
||||
:selected-label="trans(MULTISELECT_SELECTED_LABEL)"
|
||||
:options="availableLocations"
|
||||
group-values="locations"
|
||||
group-label="locationGroup"
|
||||
v-model="location"
|
||||
/>
|
||||
<new-location v-bind:available-locations="availableLocations" />
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
<teleport to="#location">
|
||||
<div class="mb-3 row">
|
||||
<label :class="locationClassList">
|
||||
{{ trans(ACTIVITY_LOCATION) }}
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<VueMultiselect
|
||||
name="selectLocation"
|
||||
id="selectLocation"
|
||||
label="name"
|
||||
track-by="id"
|
||||
open-direction="top"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:placeholder="trans(ACTIVITY_CHOOSE_LOCATION)"
|
||||
:custom-label="customLabel"
|
||||
:select-label="trans(MULTISELECT_SELECT_LABEL)"
|
||||
:deselect-label="trans(MULTISELECT_DESELECT_LABEL)"
|
||||
:selected-label="trans(MULTISELECT_SELECTED_LABEL)"
|
||||
:options="availableLocations"
|
||||
group-values="locations"
|
||||
group-label="locationGroup"
|
||||
v-model="location"
|
||||
/>
|
||||
<new-location v-bind:available-locations="availableLocations" />
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -35,60 +35,60 @@ import VueMultiselect from "vue-multiselect";
|
||||
import NewLocation from "./Location/NewLocation.vue";
|
||||
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||
import {
|
||||
trans,
|
||||
ACTIVITY_LOCATION,
|
||||
ACTIVITY_CHOOSE_LOCATION,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
MULTISELECT_DESELECT_LABEL,
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
trans,
|
||||
ACTIVITY_LOCATION,
|
||||
ACTIVITY_CHOOSE_LOCATION,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
MULTISELECT_DESELECT_LABEL,
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
} from "translator";
|
||||
|
||||
export default {
|
||||
name: "Location",
|
||||
components: {
|
||||
NewLocation,
|
||||
VueMultiselect,
|
||||
name: "Location",
|
||||
components: {
|
||||
NewLocation,
|
||||
VueMultiselect,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
trans,
|
||||
ACTIVITY_LOCATION,
|
||||
ACTIVITY_CHOOSE_LOCATION,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
MULTISELECT_DESELECT_LABEL,
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
locationClassList: `col-form-label col-sm-4 ${document.querySelector("input#chill_activitybundle_activity_location").getAttribute("required") ? "required" : ""}`,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["activity", "availableLocations"]),
|
||||
...mapGetters(["suggestedEntities"]),
|
||||
location: {
|
||||
get() {
|
||||
return this.activity.location;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.dispatch("updateLocation", value);
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
trans,
|
||||
ACTIVITY_LOCATION,
|
||||
ACTIVITY_CHOOSE_LOCATION,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
MULTISELECT_DESELECT_LABEL,
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
labelAccompanyingCourseLocation(value) {
|
||||
return `${value.address.text} (${localizeString(value.locationType.title)})`;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
locationClassList: `col-form-label col-sm-4 ${document.querySelector("input#chill_activitybundle_activity_location").getAttribute("required") ? "required" : ""}`,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["activity", "availableLocations"]),
|
||||
...mapGetters(["suggestedEntities"]),
|
||||
location: {
|
||||
get() {
|
||||
return this.activity.location;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.dispatch("updateLocation", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
labelAccompanyingCourseLocation(value) {
|
||||
return `${value.address.text} (${localizeString(value.locationType.title)})`;
|
||||
},
|
||||
customLabel(value) {
|
||||
return value.locationType
|
||||
? value.name
|
||||
? value.name === "__AccompanyingCourseLocation__"
|
||||
? this.labelAccompanyingCourseLocation(value)
|
||||
: `${value.name} (${localizeString(value.locationType.title)})`
|
||||
: localizeString(value.locationType.title)
|
||||
: "";
|
||||
},
|
||||
customLabel(value) {
|
||||
return value.locationType
|
||||
? value.name
|
||||
? value.name === "__AccompanyingCourseLocation__"
|
||||
? this.labelAccompanyingCourseLocation(value)
|
||||
: `${value.name} (${localizeString(value.locationType.title)})`
|
||||
: localizeString(value.locationType.title)
|
||||
: "";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,123 +1,114 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="btn btn-sm btn-create" @click="openModal">
|
||||
{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="btn btn-sm btn-create" @click="openModal">
|
||||
{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<teleport to="body">
|
||||
<modal
|
||||
v-if="modal.showModal"
|
||||
:modalDialogClass="modal.modalDialogClass"
|
||||
@close="modal.showModal = false"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="modal-title">
|
||||
{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
|
||||
</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<form>
|
||||
<div class="alert alert-warning" v-if="errors.length">
|
||||
<ul>
|
||||
<li v-for="(e, i) in errors" :key="i">
|
||||
{{ e }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<teleport to="body">
|
||||
<modal
|
||||
v-if="modal.showModal"
|
||||
:modalDialogClass="modal.modalDialogClass"
|
||||
@close="modal.showModal = false"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="modal-title">
|
||||
{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
|
||||
</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<form>
|
||||
<div class="alert alert-warning" v-if="errors.length">
|
||||
<ul>
|
||||
<li v-for="(e, i) in errors" :key="i">
|
||||
{{ e }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<select
|
||||
class="form-select form-select-lg"
|
||||
id="type"
|
||||
required
|
||||
v-model="selectType"
|
||||
>
|
||||
<option selected disabled value="">
|
||||
{{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }}
|
||||
</option>
|
||||
<option
|
||||
v-for="t in locationTypes"
|
||||
:value="t"
|
||||
:key="t.id"
|
||||
>
|
||||
{{ localizeString(t.title) }}
|
||||
</option>
|
||||
</select>
|
||||
<label>{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_TYPE)
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<select
|
||||
class="form-select form-select-lg"
|
||||
id="type"
|
||||
required
|
||||
v-model="selectType"
|
||||
>
|
||||
<option selected disabled value="">
|
||||
{{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }}
|
||||
</option>
|
||||
<option v-for="t in locationTypes" :value="t" :key="t.id">
|
||||
{{ localizeString(t.title) }}
|
||||
</option>
|
||||
</select>
|
||||
<label>{{ trans(ACTIVITY_LOCATION_FIELDS_TYPE) }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="name"
|
||||
v-model="inputName"
|
||||
placeholder
|
||||
/>
|
||||
<label for="name">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_NAME)
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="name"
|
||||
v-model="inputName"
|
||||
placeholder
|
||||
/>
|
||||
<label for="name">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_NAME)
|
||||
}}</label>
|
||||
</div>
|
||||
|
||||
<add-address
|
||||
:context="addAddress.context"
|
||||
:options="addAddress.options"
|
||||
:addressChangedCallback="submitNewAddress"
|
||||
v-if="showAddAddress"
|
||||
ref="addAddress"
|
||||
/>
|
||||
<add-address
|
||||
:context="addAddress.context"
|
||||
:options="addAddress.options"
|
||||
:addressChangedCallback="submitNewAddress"
|
||||
v-if="showAddAddress"
|
||||
ref="addAddress"
|
||||
/>
|
||||
|
||||
<div class="form-floating mb-3" v-if="showContactData">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="phonenumber1"
|
||||
v-model="inputPhonenumber1"
|
||||
placeholder
|
||||
/>
|
||||
<label for="phonenumber1">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER1)
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3" v-if="hasPhonenumber1">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="phonenumber2"
|
||||
v-model="inputPhonenumber2"
|
||||
placeholder
|
||||
/>
|
||||
<label for="phonenumber2">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER2)
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3" v-if="showContactData">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="email"
|
||||
v-model="inputEmail"
|
||||
placeholder
|
||||
/>
|
||||
<label for="email">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_EMAIL)
|
||||
}}</label>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button
|
||||
class="btn btn-save"
|
||||
@click.prevent="saveNewLocation"
|
||||
>
|
||||
{{ trans(SAVE) }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</teleport>
|
||||
</div>
|
||||
<div class="form-floating mb-3" v-if="showContactData">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="phonenumber1"
|
||||
v-model="inputPhonenumber1"
|
||||
placeholder
|
||||
/>
|
||||
<label for="phonenumber1">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER1)
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3" v-if="hasPhonenumber1">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="phonenumber2"
|
||||
v-model="inputPhonenumber2"
|
||||
placeholder
|
||||
/>
|
||||
<label for="phonenumber2">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER2)
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3" v-if="showContactData">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="email"
|
||||
v-model="inputEmail"
|
||||
placeholder
|
||||
/>
|
||||
<label for="email">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_EMAIL)
|
||||
}}</label>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="btn btn-save" @click.prevent="saveNewLocation">
|
||||
{{ trans(SAVE) }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -128,241 +119,236 @@ import { getLocationTypes } from "../../api";
|
||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||
import {
|
||||
SAVE,
|
||||
ACTIVITY_LOCATION_FIELDS_EMAIL,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
|
||||
ACTIVITY_LOCATION_FIELDS_NAME,
|
||||
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||
ACTIVITY_CREATE_NEW_LOCATION,
|
||||
ACTIVITY_EDIT_ADDRESS,
|
||||
ACTIVITY_CREATE_ADDRESS,
|
||||
trans,
|
||||
SAVE,
|
||||
ACTIVITY_LOCATION_FIELDS_EMAIL,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
|
||||
ACTIVITY_LOCATION_FIELDS_NAME,
|
||||
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||
ACTIVITY_CREATE_NEW_LOCATION,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
export default {
|
||||
name: "NewLocation",
|
||||
components: {
|
||||
Modal,
|
||||
AddAddress,
|
||||
name: "NewLocation",
|
||||
components: {
|
||||
Modal,
|
||||
AddAddress,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
trans,
|
||||
SAVE,
|
||||
ACTIVITY_LOCATION_FIELDS_EMAIL,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
|
||||
ACTIVITY_LOCATION_FIELDS_NAME,
|
||||
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||
ACTIVITY_CREATE_NEW_LOCATION,
|
||||
};
|
||||
},
|
||||
props: ["availableLocations"],
|
||||
data() {
|
||||
return {
|
||||
errors: [],
|
||||
selected: {
|
||||
type: null,
|
||||
name: null,
|
||||
addressId: null,
|
||||
phonenumber1: null,
|
||||
phonenumber2: null,
|
||||
email: null,
|
||||
},
|
||||
locationTypes: [],
|
||||
modal: {
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-dialog-scrollable modal-xl",
|
||||
},
|
||||
addAddress: {
|
||||
options: {
|
||||
button: {
|
||||
text: {
|
||||
create: "activity.create_address",
|
||||
edit: "activity.edit_address",
|
||||
},
|
||||
size: "btn-sm",
|
||||
},
|
||||
title: {
|
||||
create: "activity.create_address",
|
||||
edit: "activity.edit_address",
|
||||
},
|
||||
},
|
||||
context: {
|
||||
target: {
|
||||
//name, id
|
||||
},
|
||||
edit: false,
|
||||
addressId: null,
|
||||
defaults: window.addaddress,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["activity"]),
|
||||
selectType: {
|
||||
get() {
|
||||
return this.selected.type;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.type = value;
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
trans,
|
||||
SAVE,
|
||||
ACTIVITY_LOCATION_FIELDS_EMAIL,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
|
||||
ACTIVITY_LOCATION_FIELDS_NAME,
|
||||
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||
ACTIVITY_CREATE_NEW_LOCATION,
|
||||
ACTIVITY_EDIT_ADDRESS,
|
||||
ACTIVITY_CREATE_ADDRESS,
|
||||
inputName: {
|
||||
get() {
|
||||
return this.selected.name;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.name = value;
|
||||
},
|
||||
},
|
||||
inputEmail: {
|
||||
get() {
|
||||
return this.selected.email;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.email = value;
|
||||
},
|
||||
},
|
||||
inputPhonenumber1: {
|
||||
get() {
|
||||
return this.selected.phonenumber1;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.phonenumber1 = value;
|
||||
},
|
||||
},
|
||||
inputPhonenumber2: {
|
||||
get() {
|
||||
return this.selected.phonenumber2;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.phonenumber2 = value;
|
||||
},
|
||||
},
|
||||
hasPhonenumber1() {
|
||||
return (
|
||||
this.selected.phonenumber1 !== null && this.selected.phonenumber1 !== ""
|
||||
);
|
||||
},
|
||||
showAddAddress() {
|
||||
let cond = false;
|
||||
if (this.selected.type) {
|
||||
if (this.selected.type.addressRequired !== "never") {
|
||||
cond = true;
|
||||
}
|
||||
}
|
||||
return cond;
|
||||
},
|
||||
showContactData() {
|
||||
let cond = false;
|
||||
if (this.selected.type) {
|
||||
if (this.selected.type.contactData !== "never") {
|
||||
cond = true;
|
||||
}
|
||||
}
|
||||
return cond;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getLocationTypesList();
|
||||
},
|
||||
methods: {
|
||||
localizeString,
|
||||
checkForm() {
|
||||
let cond = true;
|
||||
this.errors = [];
|
||||
if (!this.selected.type) {
|
||||
this.errors.push("Type de localisation requis");
|
||||
cond = false;
|
||||
} else {
|
||||
if (
|
||||
this.selected.type.addressRequired === "required" &&
|
||||
!this.selected.addressId
|
||||
) {
|
||||
this.errors.push("Adresse requise");
|
||||
cond = false;
|
||||
}
|
||||
if (
|
||||
this.selected.type.contactData === "required" &&
|
||||
!this.selected.phonenumber1
|
||||
) {
|
||||
this.errors.push("Numéro de téléphone requis");
|
||||
cond = false;
|
||||
}
|
||||
if (
|
||||
this.selected.type.contactData === "required" &&
|
||||
!this.selected.email
|
||||
) {
|
||||
this.errors.push("Adresse email requise");
|
||||
cond = false;
|
||||
}
|
||||
}
|
||||
return cond;
|
||||
},
|
||||
getLocationTypesList() {
|
||||
getLocationTypes().then((results) => {
|
||||
this.locationTypes = results.filter(
|
||||
(t) => t.availableForUsers === true,
|
||||
);
|
||||
});
|
||||
},
|
||||
openModal() {
|
||||
this.modal.showModal = true;
|
||||
},
|
||||
saveNewLocation() {
|
||||
if (this.checkForm()) {
|
||||
let body = {
|
||||
type: "location",
|
||||
name: this.selected.name,
|
||||
locationType: {
|
||||
id: this.selected.type.id,
|
||||
type: "location-type",
|
||||
},
|
||||
phonenumber1: this.selected.phonenumber1,
|
||||
phonenumber2: this.selected.phonenumber2,
|
||||
email: this.selected.email,
|
||||
};
|
||||
},
|
||||
props: ["availableLocations"],
|
||||
data() {
|
||||
return {
|
||||
errors: [],
|
||||
selected: {
|
||||
type: null,
|
||||
name: null,
|
||||
addressId: null,
|
||||
phonenumber1: null,
|
||||
phonenumber2: null,
|
||||
email: null,
|
||||
if (this.selected.addressId) {
|
||||
body = Object.assign(body, {
|
||||
address: {
|
||||
id: this.selected.addressId,
|
||||
},
|
||||
locationTypes: [],
|
||||
modal: {
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-dialog-scrollable modal-xl",
|
||||
},
|
||||
addAddress: {
|
||||
options: {
|
||||
button: {
|
||||
text: {
|
||||
create: ACTIVITY_CREATE_ADDRESS,
|
||||
edit: ACTIVITY_EDIT_ADDRESS,
|
||||
},
|
||||
size: "btn-sm",
|
||||
},
|
||||
title: {
|
||||
create: ACTIVITY_CREATE_ADDRESS,
|
||||
edit: ACTIVITY_EDIT_ADDRESS,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
target: {
|
||||
//name, id
|
||||
},
|
||||
edit: false,
|
||||
addressId: null,
|
||||
defaults: window.addaddress,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["activity"]),
|
||||
selectType: {
|
||||
get() {
|
||||
return this.selected.type;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.type = value;
|
||||
},
|
||||
},
|
||||
inputName: {
|
||||
get() {
|
||||
return this.selected.name;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.name = value;
|
||||
},
|
||||
},
|
||||
inputEmail: {
|
||||
get() {
|
||||
return this.selected.email;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.email = value;
|
||||
},
|
||||
},
|
||||
inputPhonenumber1: {
|
||||
get() {
|
||||
return this.selected.phonenumber1;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.phonenumber1 = value;
|
||||
},
|
||||
},
|
||||
inputPhonenumber2: {
|
||||
get() {
|
||||
return this.selected.phonenumber2;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.phonenumber2 = value;
|
||||
},
|
||||
},
|
||||
hasPhonenumber1() {
|
||||
return (
|
||||
this.selected.phonenumber1 !== null &&
|
||||
this.selected.phonenumber1 !== ""
|
||||
);
|
||||
},
|
||||
showAddAddress() {
|
||||
let cond = false;
|
||||
if (this.selected.type) {
|
||||
if (this.selected.type.addressRequired !== "never") {
|
||||
cond = true;
|
||||
}
|
||||
}
|
||||
return cond;
|
||||
},
|
||||
showContactData() {
|
||||
let cond = false;
|
||||
if (this.selected.type) {
|
||||
if (this.selected.type.contactData !== "never") {
|
||||
cond = true;
|
||||
}
|
||||
}
|
||||
return cond;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getLocationTypesList();
|
||||
},
|
||||
methods: {
|
||||
localizeString,
|
||||
checkForm() {
|
||||
let cond = true;
|
||||
this.errors = [];
|
||||
if (!this.selected.type) {
|
||||
this.errors.push("Type de localisation requis");
|
||||
cond = false;
|
||||
} else {
|
||||
if (
|
||||
this.selected.type.addressRequired === "required" &&
|
||||
!this.selected.addressId
|
||||
) {
|
||||
this.errors.push("Adresse requise");
|
||||
cond = false;
|
||||
}
|
||||
if (
|
||||
this.selected.type.contactData === "required" &&
|
||||
!this.selected.phonenumber1
|
||||
) {
|
||||
this.errors.push("Numéro de téléphone requis");
|
||||
cond = false;
|
||||
}
|
||||
if (
|
||||
this.selected.type.contactData === "required" &&
|
||||
!this.selected.email
|
||||
) {
|
||||
this.errors.push("Adresse email requise");
|
||||
cond = false;
|
||||
}
|
||||
}
|
||||
return cond;
|
||||
},
|
||||
getLocationTypesList() {
|
||||
getLocationTypes().then((results) => {
|
||||
this.locationTypes = results.filter(
|
||||
(t) => t.availableForUsers === true,
|
||||
);
|
||||
});
|
||||
},
|
||||
openModal() {
|
||||
this.modal.showModal = true;
|
||||
},
|
||||
saveNewLocation() {
|
||||
if (this.checkForm()) {
|
||||
let body = {
|
||||
type: "location",
|
||||
name: this.selected.name,
|
||||
locationType: {
|
||||
id: this.selected.type.id,
|
||||
type: "location-type",
|
||||
},
|
||||
phonenumber1: this.selected.phonenumber1,
|
||||
phonenumber2: this.selected.phonenumber2,
|
||||
email: this.selected.email,
|
||||
};
|
||||
if (this.selected.addressId) {
|
||||
body = Object.assign(body, {
|
||||
address: {
|
||||
id: this.selected.addressId,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
makeFetch("POST", "/api/1.0/main/location.json", body)
|
||||
.then((response) => {
|
||||
this.$store.dispatch("addAvailableLocationGroup", {
|
||||
locationGroup: "Localisations nouvellement créées",
|
||||
locations: [response],
|
||||
});
|
||||
this.$store.dispatch("updateLocation", response);
|
||||
this.modal.showModal = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.name === "ValidationException") {
|
||||
for (let v of error.violations) {
|
||||
this.errors.push(v);
|
||||
}
|
||||
} else {
|
||||
this.errors.push("An error occurred");
|
||||
}
|
||||
});
|
||||
makeFetch("POST", "/api/1.0/main/location.json", body)
|
||||
.then((response) => {
|
||||
this.$store.dispatch("addAvailableLocationGroup", {
|
||||
locationGroup: "Localisations nouvellement créées",
|
||||
locations: [response],
|
||||
});
|
||||
this.$store.dispatch("updateLocation", response);
|
||||
this.modal.showModal = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.name === "ValidationException") {
|
||||
for (let v of error.violations) {
|
||||
this.errors.push(v);
|
||||
}
|
||||
} else {
|
||||
this.errors.push("An error occurred");
|
||||
}
|
||||
},
|
||||
submitNewAddress(payload) {
|
||||
this.selected.addressId = payload.addressId;
|
||||
this.addAddress.context.addressId = payload.addressId;
|
||||
this.addAddress.context.edit = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
submitNewAddress(payload) {
|
||||
this.selected.addressId = payload.addressId;
|
||||
this.addAddress.context.addressId = payload.addressId;
|
||||
this.addAddress.context.edit = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<template>
|
||||
<span class="inline-choice">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
v-model="selected"
|
||||
name="action"
|
||||
:id="action.id"
|
||||
:value="action"
|
||||
/>
|
||||
<label class="form-check-label" :for="action.id">
|
||||
<span class="badge bg-light text-dark" :title="action.text">{{
|
||||
action.text
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</span>
|
||||
<span class="inline-choice">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
v-model="selected"
|
||||
name="action"
|
||||
:id="action.id"
|
||||
:value="action"
|
||||
/>
|
||||
<label class="form-check-label" :for="action.id">
|
||||
<span class="badge bg-light text-dark" :title="action.text">{{
|
||||
action.text
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "CheckSocialAction",
|
||||
props: ["action", "selection"],
|
||||
emits: ["updateSelected"],
|
||||
computed: {
|
||||
selected: {
|
||||
set(value) {
|
||||
this.$emit("updateSelected", value);
|
||||
},
|
||||
get() {
|
||||
return this.selection;
|
||||
},
|
||||
},
|
||||
name: "CheckSocialAction",
|
||||
props: ["action", "selection"],
|
||||
emits: ["updateSelected"],
|
||||
computed: {
|
||||
selected: {
|
||||
set(value) {
|
||||
this.$emit("updateSelected", value);
|
||||
},
|
||||
get() {
|
||||
return this.selection;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -41,21 +41,19 @@ export default {
|
||||
@import "ChillPersonAssets/chill/scss/mixins";
|
||||
@import "ChillMainAssets/chill/scss/chill_variables";
|
||||
span.badge {
|
||||
@include badge_social($social-action-color);
|
||||
font-size: 95%;
|
||||
white-space: normal;
|
||||
@include badge_social($social-action-color);
|
||||
font-size: 95%;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
margin-right: 1em;
|
||||
text-align: left;
|
||||
max-width: 100%;margin-bottom: 5px;
|
||||
margin-right: 1em;
|
||||
text-align: left;
|
||||
line-height: 1.2em;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
top: 0;
|
||||
margin: 0 0.3em 0 -0.75em;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,36 @@
|
||||
<template>
|
||||
<span class="inline-choice">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
v-model="selected"
|
||||
name="issue"
|
||||
:id="issue.id"
|
||||
:value="issue"
|
||||
/>
|
||||
<label class="form-check-label" :for="issue.id">
|
||||
<span class="badge bg-chill-l-gray text-dark">{{
|
||||
issue.text
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</span>
|
||||
<span class="inline-choice">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
v-model="selected"
|
||||
name="issue"
|
||||
:id="issue.id"
|
||||
:value="issue"
|
||||
/>
|
||||
<label class="form-check-label" :for="issue.id">
|
||||
<span class="badge bg-chill-l-gray text-dark">{{ issue.text }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "CheckSocialIssue",
|
||||
props: ["issue", "selection"],
|
||||
emits: ["updateSelected"],
|
||||
computed: {
|
||||
selected: {
|
||||
set(value) {
|
||||
this.$emit("updateSelected", value);
|
||||
},
|
||||
get() {
|
||||
return this.selection;
|
||||
},
|
||||
},
|
||||
name: "CheckSocialIssue",
|
||||
props: ["issue", "selection"],
|
||||
emits: ["updateSelected"],
|
||||
computed: {
|
||||
selected: {
|
||||
set(value) {
|
||||
this.$emit("updateSelected", value);
|
||||
},
|
||||
get() {
|
||||
return this.selection;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -41,16 +39,14 @@ export default {
|
||||
@import "ChillPersonAssets/chill/scss/mixins";
|
||||
@import "ChillMainAssets/chill/scss/chill_variables";
|
||||
span.badge {
|
||||
@include badge_social($social-issue-color);
|
||||
font-size: 95%;
|
||||
white-space: normal;
|
||||
@include badge_social($social-issue-color);
|
||||
font-size: 95%;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
margin-right: 1em;
|
||||
text-align: left;
|
||||
max-width: 100%;margin-bottom: 5px;
|
||||
margin-right: 1em;text-align: left;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
|
||||
@@ -16,8 +16,7 @@ use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
@@ -25,10 +24,9 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
public function __construct(
|
||||
private readonly ActivityRepository $repository,
|
||||
Security $security,
|
||||
WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
|
||||
EntityWorkflowAttachmentRepository $attachmentRepository,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
) {
|
||||
parent::__construct($security, $attachmentRepository, $workflowDocumentService);
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
|
||||
@@ -39,7 +39,7 @@ final class Version20251118124241 extends AbstractMigration
|
||||
$this->addSql("COMMENT ON COLUMN activity_user.by_migration IS 'For backup purpose - can be safely deleted after a while. See migration \\Chill\\Migrations\\Activity\\Version20251118124241'");
|
||||
|
||||
$this->addSql('INSERT INTO activity_user (activity_id, user_id, by_migration)
|
||||
SELECT id, user_id, true FROM activity WHERE user_id is not null
|
||||
SELECT id, user_id, true FROM activity
|
||||
ON CONFLICT DO NOTHING');
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,7 @@ use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
class LoadAsideActivity extends Fixture implements DependentFixtureInterface
|
||||
{
|
||||
public function __construct(private readonly UserRepository $userRepository)
|
||||
{
|
||||
mt_srand(123456789);
|
||||
}
|
||||
public function __construct(private readonly UserRepository $userRepository) {}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
@@ -50,7 +47,7 @@ class LoadAsideActivity extends Fixture implements DependentFixtureInterface
|
||||
$this->getReference('aside_activity_category_0', AsideActivityCategory::class)
|
||||
)
|
||||
->setDate((new \DateTimeImmutable('today'))
|
||||
->sub(new \DateInterval('P'.\mt_rand(1, 100).'D')));
|
||||
->sub(new \DateInterval('P'.\random_int(1, 100).'D')));
|
||||
|
||||
$manager->persist($activity);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class ChillBudgetExtension extends Extension implements PrependExtensionInterfac
|
||||
}
|
||||
|
||||
/** (non-PHPdoc).
|
||||
* @see PrependExtensionInterface::prepend()
|
||||
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
|
||||
*/
|
||||
public function prependRoutes(ContainerBuilder $container)
|
||||
{
|
||||
|
||||
@@ -72,20 +72,14 @@
|
||||
|
||||
{% macro table_results(actualCharges, actualResources, results) %}
|
||||
|
||||
{% set now = date() %}
|
||||
|
||||
{% set totalCharges = 0 %}
|
||||
{% for c in actualCharges %}
|
||||
{% if c.startDate <= now and (c.endDate is null or c.endDate >= now) %}
|
||||
{% set totalCharges = totalCharges + c.amount %}
|
||||
{% endif %}
|
||||
{% set totalCharges = totalCharges + c.amount %}
|
||||
{% endfor %}
|
||||
|
||||
{% set totalResources = 0 %}
|
||||
{% for r in actualResources %}
|
||||
{% if r.startDate <= now and (r.endDate is null or r.endDate >= now) %}
|
||||
{% set totalResources = totalResources + r.amount %}
|
||||
{% endif %}
|
||||
{% set totalResources = totalResources + r.amount %}
|
||||
{% endfor %}
|
||||
|
||||
{% set result = (totalResources - totalCharges) %}
|
||||
|
||||
@@ -1,76 +1,74 @@
|
||||
import { EventInput } from "@fullcalendar/core";
|
||||
import {
|
||||
DateTime,
|
||||
Location,
|
||||
User,
|
||||
UserAssociatedInterface,
|
||||
DateTime,
|
||||
Location,
|
||||
User,
|
||||
UserAssociatedInterface,
|
||||
} from "../../../ChillMainBundle/Resources/public/types";
|
||||
import { Person } from "../../../ChillPersonBundle/Resources/public/types";
|
||||
|
||||
export interface CalendarRange {
|
||||
id: number;
|
||||
endDate: DateTime;
|
||||
startDate: DateTime;
|
||||
user: User;
|
||||
location: Location;
|
||||
createdAt: DateTime;
|
||||
createdBy: User;
|
||||
updatedAt: DateTime;
|
||||
updatedBy: User;
|
||||
id: number;
|
||||
endDate: DateTime;
|
||||
startDate: DateTime;
|
||||
user: User;
|
||||
location: Location;
|
||||
createdAt: DateTime;
|
||||
createdBy: User;
|
||||
updatedAt: DateTime;
|
||||
updatedBy: User;
|
||||
}
|
||||
|
||||
export interface CalendarRangeCreate {
|
||||
user: UserAssociatedInterface;
|
||||
startDate: DateTime;
|
||||
endDate: DateTime;
|
||||
location: Location;
|
||||
user: UserAssociatedInterface;
|
||||
startDate: DateTime;
|
||||
endDate: DateTime;
|
||||
location: Location;
|
||||
}
|
||||
|
||||
export interface CalendarRangeEdit {
|
||||
startDate?: DateTime;
|
||||
endDate?: DateTime;
|
||||
location?: Location;
|
||||
startDate?: DateTime;
|
||||
endDate?: DateTime;
|
||||
location?: Location;
|
||||
}
|
||||
|
||||
export interface Calendar {
|
||||
id: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface CalendarLight {
|
||||
id: number;
|
||||
endDate: DateTime;
|
||||
startDate: DateTime;
|
||||
mainUser: User;
|
||||
persons: Person[];
|
||||
status: "valid" | "moved" | "canceled";
|
||||
id: number;
|
||||
endDate: DateTime;
|
||||
startDate: DateTime;
|
||||
mainUser: User;
|
||||
persons: Person[];
|
||||
status: "valid" | "moved" | "canceled";
|
||||
}
|
||||
|
||||
export interface CalendarRemote {
|
||||
id: number;
|
||||
endDate: DateTime;
|
||||
startDate: DateTime;
|
||||
title: string;
|
||||
isAllDay: boolean;
|
||||
id: number;
|
||||
endDate: DateTime;
|
||||
startDate: DateTime;
|
||||
title: string;
|
||||
isAllDay: boolean;
|
||||
}
|
||||
|
||||
export type EventInputCalendarRange = EventInput & {
|
||||
id: string;
|
||||
userId: number;
|
||||
userLabel: string;
|
||||
calendarRangeId: number;
|
||||
locationId: number;
|
||||
locationName: string;
|
||||
start: string;
|
||||
end: string;
|
||||
is: "range";
|
||||
id: string;
|
||||
userId: number;
|
||||
userLabel: string;
|
||||
calendarRangeId: number;
|
||||
locationId: number;
|
||||
locationName: string;
|
||||
start: string;
|
||||
end: string;
|
||||
is: "range";
|
||||
};
|
||||
|
||||
export function isEventInputCalendarRange(
|
||||
toBeDetermined: EventInputCalendarRange | EventInput,
|
||||
toBeDetermined: EventInputCalendarRange | EventInput,
|
||||
): toBeDetermined is EventInputCalendarRange {
|
||||
return (
|
||||
typeof toBeDetermined.is === "string" && toBeDetermined.is === "range"
|
||||
);
|
||||
return typeof toBeDetermined.is === "string" && toBeDetermined.is === "range";
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
@@ -1,119 +1,105 @@
|
||||
<template>
|
||||
<div :style="style" class="calendar-active">
|
||||
<span class="badge-user">
|
||||
{{ user.text }}
|
||||
<template v-if="invite !== null">
|
||||
<i v-if="invite.status === 'accepted'" class="fa fa-check" />
|
||||
<i
|
||||
v-else-if="invite.status === 'declined'"
|
||||
class="fa fa-times"
|
||||
/>
|
||||
<i
|
||||
v-else-if="invite.status === 'pending'"
|
||||
class="fa fa-question-o"
|
||||
/>
|
||||
<i
|
||||
v-else-if="invite.status === 'tentative'"
|
||||
class="fa fa-question"
|
||||
/>
|
||||
<span v-else="">{{ invite.status }}</span>
|
||||
</template>
|
||||
</span>
|
||||
<span class="form-check-inline form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="flexSwitchCheckDefault"
|
||||
v-model="rangeShow"
|
||||
/>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="flexSwitchCheckDefault"
|
||||
title="Disponibilités"
|
||||
><i class="fa fa-calendar-check-o"
|
||||
/></label>
|
||||
</span>
|
||||
<span class="form-check-inline form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="flexSwitchCheckDefault"
|
||||
v-model="remoteShow"
|
||||
/>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="flexSwitchCheckDefault"
|
||||
title="Agenda"
|
||||
><i class="fa fa-calendar"
|
||||
/></label>
|
||||
</span>
|
||||
</div>
|
||||
<div :style="style" class="calendar-active">
|
||||
<span class="badge-user">
|
||||
{{ user.text }}
|
||||
<template v-if="invite !== null">
|
||||
<i v-if="invite.status === 'accepted'" class="fa fa-check" />
|
||||
<i v-else-if="invite.status === 'declined'" class="fa fa-times" />
|
||||
<i v-else-if="invite.status === 'pending'" class="fa fa-question-o" />
|
||||
<i v-else-if="invite.status === 'tentative'" class="fa fa-question" />
|
||||
<span v-else="">{{ invite.status }}</span>
|
||||
</template>
|
||||
</span>
|
||||
<span class="form-check-inline form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="flexSwitchCheckDefault"
|
||||
v-model="rangeShow"
|
||||
/>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="flexSwitchCheckDefault"
|
||||
title="Disponibilités"
|
||||
><i class="fa fa-calendar-check-o"
|
||||
/></label>
|
||||
</span>
|
||||
<span class="form-check-inline form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="flexSwitchCheckDefault"
|
||||
v-model="remoteShow"
|
||||
/>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="flexSwitchCheckDefault"
|
||||
title="Agenda"
|
||||
><i class="fa fa-calendar"
|
||||
/></label>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "CalendarActive",
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
invite: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
name: "CalendarActive",
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
backgroundColor: this.$store.getters.getUserData(this.user)
|
||||
.mainColor,
|
||||
};
|
||||
},
|
||||
rangeShow: {
|
||||
set(value) {
|
||||
this.$store.commit("showUserOnCalendar", {
|
||||
user: this.user,
|
||||
ranges: value,
|
||||
});
|
||||
},
|
||||
get() {
|
||||
return this.$store.getters.isRangeShownOnCalendarForUser(
|
||||
this.user,
|
||||
);
|
||||
},
|
||||
},
|
||||
remoteShow: {
|
||||
set(value) {
|
||||
this.$store.commit("showUserOnCalendar", {
|
||||
user: this.user,
|
||||
remotes: value,
|
||||
});
|
||||
},
|
||||
get() {
|
||||
return this.$store.getters.isRemoteShownOnCalendarForUser(
|
||||
this.user,
|
||||
);
|
||||
},
|
||||
},
|
||||
invite: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
backgroundColor: this.$store.getters.getUserData(this.user).mainColor,
|
||||
};
|
||||
},
|
||||
rangeShow: {
|
||||
set(value) {
|
||||
this.$store.commit("showUserOnCalendar", {
|
||||
user: this.user,
|
||||
ranges: value,
|
||||
});
|
||||
},
|
||||
get() {
|
||||
return this.$store.getters.isRangeShownOnCalendarForUser(this.user);
|
||||
},
|
||||
},
|
||||
remoteShow: {
|
||||
set(value) {
|
||||
this.$store.commit("showUserOnCalendar", {
|
||||
user: this.user,
|
||||
remotes: value,
|
||||
});
|
||||
},
|
||||
get() {
|
||||
return this.$store.getters.isRemoteShownOnCalendarForUser(this.user);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.calendar-active {
|
||||
margin: 0 0.25rem 0.25rem 0;
|
||||
padding: 0.5rem;
|
||||
margin: 0 0.25rem 0.25rem 0;
|
||||
padding: 0.5rem;
|
||||
|
||||
border-radius: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
color: var(--bs-blue);
|
||||
color: var(--bs-blue);
|
||||
|
||||
& > .badge-user {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
& > .badge-user {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,37 +14,37 @@ export { whoami } from "../../../../../ChillMainBundle/Resources/public/lib/api/
|
||||
* @return Promise
|
||||
*/
|
||||
export const fetchCalendarRangeForUser = (
|
||||
user: User,
|
||||
start: Date,
|
||||
end: Date,
|
||||
user: User,
|
||||
start: Date,
|
||||
end: Date,
|
||||
): Promise<CalendarRange[]> => {
|
||||
const uri = `/api/1.0/calendar/calendar-range-available/${user.id}.json`;
|
||||
const dateFrom = datetimeToISO(start);
|
||||
const dateTo = datetimeToISO(end);
|
||||
const uri = `/api/1.0/calendar/calendar-range-available/${user.id}.json`;
|
||||
const dateFrom = datetimeToISO(start);
|
||||
const dateTo = datetimeToISO(end);
|
||||
|
||||
return fetchResults<CalendarRange>(uri, { dateFrom, dateTo });
|
||||
return fetchResults<CalendarRange>(uri, { dateFrom, dateTo });
|
||||
};
|
||||
|
||||
export const fetchCalendarRemoteForUser = (
|
||||
user: User,
|
||||
start: Date,
|
||||
end: Date,
|
||||
user: User,
|
||||
start: Date,
|
||||
end: Date,
|
||||
): Promise<CalendarRemote[]> => {
|
||||
const uri = `/api/1.0/calendar/proxy/calendar/by-user/${user.id}/events`;
|
||||
const dateFrom = datetimeToISO(start);
|
||||
const dateTo = datetimeToISO(end);
|
||||
const uri = `/api/1.0/calendar/proxy/calendar/by-user/${user.id}/events`;
|
||||
const dateFrom = datetimeToISO(start);
|
||||
const dateTo = datetimeToISO(end);
|
||||
|
||||
return fetchResults<CalendarRemote>(uri, { dateFrom, dateTo });
|
||||
return fetchResults<CalendarRemote>(uri, { dateFrom, dateTo });
|
||||
};
|
||||
|
||||
export const fetchCalendarLocalForUser = (
|
||||
user: User,
|
||||
start: Date,
|
||||
end: Date,
|
||||
user: User,
|
||||
start: Date,
|
||||
end: Date,
|
||||
): Promise<CalendarLight[]> => {
|
||||
const uri = `/api/1.0/calendar/calendar/by-user/${user.id}.json`;
|
||||
const dateFrom = datetimeToISO(start);
|
||||
const dateTo = datetimeToISO(end);
|
||||
const uri = `/api/1.0/calendar/calendar/by-user/${user.id}.json`;
|
||||
const dateFrom = datetimeToISO(start);
|
||||
const dateTo = datetimeToISO(end);
|
||||
|
||||
return fetchResults<CalendarLight>(uri, { dateFrom, dateTo });
|
||||
return fetchResults<CalendarLight>(uri, { dateFrom, dateTo });
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
const COLORS = [
|
||||
/* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
|
||||
"#8dd3c7",
|
||||
"#ffffb3",
|
||||
"#bebada",
|
||||
"#fb8072",
|
||||
"#80b1d3",
|
||||
"#fdb462",
|
||||
"#b3de69",
|
||||
"#fccde5",
|
||||
"#d9d9d9",
|
||||
"#bc80bd",
|
||||
"#ccebc5",
|
||||
"#ffed6f",
|
||||
/* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
|
||||
"#8dd3c7",
|
||||
"#ffffb3",
|
||||
"#bebada",
|
||||
"#fb8072",
|
||||
"#80b1d3",
|
||||
"#fdb462",
|
||||
"#b3de69",
|
||||
"#fccde5",
|
||||
"#d9d9d9",
|
||||
"#bc80bd",
|
||||
"#ccebc5",
|
||||
"#ffed6f",
|
||||
];
|
||||
|
||||
export { COLORS };
|
||||
|
||||
@@ -1,117 +1,117 @@
|
||||
import { COLORS } from "../const";
|
||||
import { ISOToDatetime } from "../../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||
import {
|
||||
DateTime,
|
||||
User,
|
||||
DateTime,
|
||||
User,
|
||||
} from "../../../../../../ChillMainBundle/Resources/public/types";
|
||||
import { CalendarLight, CalendarRange, CalendarRemote } from "../../../types";
|
||||
import type { EventInputCalendarRange } from "../../../types";
|
||||
import { EventInput } from "@fullcalendar/core";
|
||||
|
||||
export interface UserData {
|
||||
user: User;
|
||||
calendarRanges: CalendarRange[];
|
||||
calendarRangesLoaded: {}[];
|
||||
remotes: CalendarRemote[];
|
||||
remotesLoaded: {}[];
|
||||
locals: CalendarRemote[];
|
||||
localsLoaded: {}[];
|
||||
mainColor: string;
|
||||
user: User;
|
||||
calendarRanges: CalendarRange[];
|
||||
calendarRangesLoaded: {}[];
|
||||
remotes: CalendarRemote[];
|
||||
remotesLoaded: {}[];
|
||||
locals: CalendarRemote[];
|
||||
localsLoaded: {}[];
|
||||
mainColor: string;
|
||||
}
|
||||
|
||||
export const addIdToValue = (string: string, id: number): string => {
|
||||
const array = string ? string.split(",") : [];
|
||||
array.push(id.toString());
|
||||
const str = array.join();
|
||||
return str;
|
||||
const array = string ? string.split(",") : [];
|
||||
array.push(id.toString());
|
||||
const str = array.join();
|
||||
return str;
|
||||
};
|
||||
|
||||
export const removeIdFromValue = (string: string, id: number) => {
|
||||
let array = string.split(",");
|
||||
array = array.filter((el) => el !== id.toString());
|
||||
const str = array.join();
|
||||
return str;
|
||||
let array = string.split(",");
|
||||
array = array.filter((el) => el !== id.toString());
|
||||
const str = array.join();
|
||||
return str;
|
||||
};
|
||||
|
||||
/*
|
||||
* Assign missing keys for the ConcernedGroups component
|
||||
*/
|
||||
export const mapEntity = (entity: EventInput): EventInput => {
|
||||
const calendar = { ...entity };
|
||||
Object.assign(calendar, { thirdParties: entity.professionals });
|
||||
const calendar = { ...entity };
|
||||
Object.assign(calendar, { thirdParties: entity.professionals });
|
||||
|
||||
if (entity.startDate !== null) {
|
||||
calendar.startDate = ISOToDatetime(entity.startDate.datetime);
|
||||
}
|
||||
if (entity.endDate !== null) {
|
||||
calendar.endDate = ISOToDatetime(entity.endDate.datetime);
|
||||
}
|
||||
if (entity.startDate !== null) {
|
||||
calendar.startDate = ISOToDatetime(entity.startDate.datetime);
|
||||
}
|
||||
if (entity.endDate !== null) {
|
||||
calendar.endDate = ISOToDatetime(entity.endDate.datetime);
|
||||
}
|
||||
|
||||
if (entity.calendarRange !== null) {
|
||||
calendar.calendarRange.calendarRangeId = entity.calendarRange.id;
|
||||
calendar.calendarRange.id = `range_${entity.calendarRange.id}`;
|
||||
}
|
||||
if (entity.calendarRange !== null) {
|
||||
calendar.calendarRange.calendarRangeId = entity.calendarRange.id;
|
||||
calendar.calendarRange.id = `range_${entity.calendarRange.id}`;
|
||||
}
|
||||
|
||||
return calendar;
|
||||
return calendar;
|
||||
};
|
||||
|
||||
export const createUserData = (user: User, colorIndex: number): UserData => {
|
||||
const colorId = colorIndex % COLORS.length;
|
||||
const colorId = colorIndex % COLORS.length;
|
||||
|
||||
return {
|
||||
user: user,
|
||||
calendarRanges: [],
|
||||
calendarRangesLoaded: [],
|
||||
remotes: [],
|
||||
remotesLoaded: [],
|
||||
locals: [],
|
||||
localsLoaded: [],
|
||||
mainColor: COLORS[colorId],
|
||||
};
|
||||
return {
|
||||
user: user,
|
||||
calendarRanges: [],
|
||||
calendarRangesLoaded: [],
|
||||
remotes: [],
|
||||
remotesLoaded: [],
|
||||
locals: [],
|
||||
localsLoaded: [],
|
||||
mainColor: COLORS[colorId],
|
||||
};
|
||||
};
|
||||
|
||||
// TODO move this function to a more global namespace, as it is also in use in MyCalendarRange app
|
||||
export const calendarRangeToFullCalendarEvent = (
|
||||
entity: CalendarRange,
|
||||
entity: CalendarRange,
|
||||
): EventInputCalendarRange => {
|
||||
return {
|
||||
id: `range_${entity.id}`,
|
||||
title: "(" + entity.user.text + ")",
|
||||
start: entity.startDate.datetime8601,
|
||||
end: entity.endDate.datetime8601,
|
||||
allDay: false,
|
||||
userId: entity.user.id,
|
||||
userLabel: entity.user.label,
|
||||
calendarRangeId: entity.id,
|
||||
locationId: entity.location.id,
|
||||
locationName: entity.location.name,
|
||||
is: "range",
|
||||
};
|
||||
return {
|
||||
id: `range_${entity.id}`,
|
||||
title: "(" + entity.user.text + ")",
|
||||
start: entity.startDate.datetime8601,
|
||||
end: entity.endDate.datetime8601,
|
||||
allDay: false,
|
||||
userId: entity.user.id,
|
||||
userLabel: entity.user.label,
|
||||
calendarRangeId: entity.id,
|
||||
locationId: entity.location.id,
|
||||
locationName: entity.location.name,
|
||||
is: "range",
|
||||
};
|
||||
};
|
||||
|
||||
export const remoteToFullCalendarEvent = (
|
||||
entity: CalendarRemote,
|
||||
entity: CalendarRemote,
|
||||
): EventInput & { id: string } => {
|
||||
return {
|
||||
id: `range_${entity.id}`,
|
||||
title: entity.title,
|
||||
start: entity.startDate.datetime8601,
|
||||
end: entity.endDate.datetime8601,
|
||||
allDay: entity.isAllDay,
|
||||
is: "remote",
|
||||
};
|
||||
return {
|
||||
id: `range_${entity.id}`,
|
||||
title: entity.title,
|
||||
start: entity.startDate.datetime8601,
|
||||
end: entity.endDate.datetime8601,
|
||||
allDay: entity.isAllDay,
|
||||
is: "remote",
|
||||
};
|
||||
};
|
||||
|
||||
export const localsToFullCalendarEvent = (
|
||||
entity: CalendarLight,
|
||||
entity: CalendarLight,
|
||||
): EventInput & { id: string; originId: number } => {
|
||||
return {
|
||||
id: `local_${entity.id}`,
|
||||
title: entity.persons.map((p) => p.text).join(", "),
|
||||
originId: entity.id,
|
||||
start: entity.startDate.datetime8601,
|
||||
end: entity.endDate.datetime8601,
|
||||
allDay: false,
|
||||
is: "local",
|
||||
};
|
||||
return {
|
||||
id: `local_${entity.id}`,
|
||||
title: entity.persons.map((p) => p.text).join(", "),
|
||||
originId: entity.id,
|
||||
start: entity.startDate.datetime8601,
|
||||
end: entity.endDate.datetime8601,
|
||||
allDay: false,
|
||||
is: "local",
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,58 +1,50 @@
|
||||
<template>
|
||||
<div class="btn-group" role="group">
|
||||
<button
|
||||
id="btnGroupDrop1"
|
||||
type="button"
|
||||
class="btn btn-misc dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
<div class="btn-group" role="group">
|
||||
<button
|
||||
id="btnGroupDrop1"
|
||||
type="button"
|
||||
class="btn btn-misc dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<template v-if="status === Statuses.PENDING">
|
||||
<span class="fa fa-hourglass"></span> {{ $t("Give_an_answer") }}
|
||||
</template>
|
||||
<template v-else-if="status === Statuses.ACCEPTED">
|
||||
<span class="fa fa-check"></span> {{ $t("Accepted") }}
|
||||
</template>
|
||||
<template v-else-if="status === Statuses.DECLINED">
|
||||
<span class="fa fa-times"></span> {{ $t("Declined") }}
|
||||
</template>
|
||||
<template v-else-if="status === Statuses.TENTATIVELY_ACCEPTED">
|
||||
<span class="fa fa-question"></span> {{ $t("Tentative") }}
|
||||
</template>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||
<li v-if="status !== Statuses.ACCEPTED">
|
||||
<a class="dropdown-item" @click="changeStatus(Statuses.ACCEPTED)"
|
||||
><i class="fa fa-check" aria-hidden="true"></i> {{ $t("Accept") }}</a
|
||||
>
|
||||
<template v-if="status === Statuses.PENDING">
|
||||
<span class="fa fa-hourglass"></span> {{ $t("Give_an_answer") }}
|
||||
</template>
|
||||
<template v-else-if="status === Statuses.ACCEPTED">
|
||||
<span class="fa fa-check"></span> {{ $t("Accepted") }}
|
||||
</template>
|
||||
<template v-else-if="status === Statuses.DECLINED">
|
||||
<span class="fa fa-times"></span> {{ $t("Declined") }}
|
||||
</template>
|
||||
<template v-else-if="status === Statuses.TENTATIVELY_ACCEPTED">
|
||||
<span class="fa fa-question"></span> {{ $t("Tentative") }}
|
||||
</template>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||
<li v-if="status !== Statuses.ACCEPTED">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="changeStatus(Statuses.ACCEPTED)"
|
||||
><i class="fa fa-check" aria-hidden="true"></i>
|
||||
{{ $t("Accept") }}</a
|
||||
>
|
||||
</li>
|
||||
<li v-if="status !== Statuses.DECLINED">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="changeStatus(Statuses.DECLINED)"
|
||||
><i class="fa fa-times" aria-hidden="true"></i>
|
||||
{{ $t("Decline") }}</a
|
||||
>
|
||||
</li>
|
||||
<li v-if="status !== Statuses.TENTATIVELY_ACCEPTED">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)"
|
||||
><i class="fa fa-question"></i>
|
||||
{{ $t("Tentatively_accept") }}</a
|
||||
>
|
||||
</li>
|
||||
<li v-if="status !== Statuses.PENDING">
|
||||
<a class="dropdown-item" @click="changeStatus(Statuses.PENDING)"
|
||||
><i class="fa fa-hourglass-o"></i>
|
||||
{{ $t("Set_pending") }}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="status !== Statuses.DECLINED">
|
||||
<a class="dropdown-item" @click="changeStatus(Statuses.DECLINED)"
|
||||
><i class="fa fa-times" aria-hidden="true"></i> {{ $t("Decline") }}</a
|
||||
>
|
||||
</li>
|
||||
<li v-if="status !== Statuses.TENTATIVELY_ACCEPTED">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)"
|
||||
><i class="fa fa-question"></i> {{ $t("Tentatively_accept") }}</a
|
||||
>
|
||||
</li>
|
||||
<li v-if="status !== Statuses.PENDING">
|
||||
<a class="dropdown-item" @click="changeStatus(Statuses.PENDING)"
|
||||
><i class="fa fa-hourglass-o"></i> {{ $t("Set_pending") }}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -64,69 +56,67 @@ const PENDING = "pending";
|
||||
const TENTATIVELY_ACCEPTED = "tentative";
|
||||
|
||||
const i18n = {
|
||||
messages: {
|
||||
fr: {
|
||||
Give_an_answer: "Répondre",
|
||||
Accepted: "Accepté",
|
||||
Declined: "Refusé",
|
||||
Tentative: "Accepté provisoirement",
|
||||
Accept: "Accepter",
|
||||
Decline: "Refuser",
|
||||
Tentatively_accept: "Accepter provisoirement",
|
||||
Set_pending: "Ne pas répondre",
|
||||
},
|
||||
messages: {
|
||||
fr: {
|
||||
Give_an_answer: "Répondre",
|
||||
Accepted: "Accepté",
|
||||
Declined: "Refusé",
|
||||
Tentative: "Accepté provisoirement",
|
||||
Accept: "Accepter",
|
||||
Decline: "Refuser",
|
||||
Tentatively_accept: "Accepter provisoirement",
|
||||
Set_pending: "Ne pas répondre",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "Answer",
|
||||
i18n,
|
||||
props: {
|
||||
calendarId: { type: Number, required: true },
|
||||
status: {
|
||||
type: String as PropType<
|
||||
"accepted" | "declined" | "pending" | "tentative"
|
||||
>,
|
||||
required: true,
|
||||
},
|
||||
name: "Answer",
|
||||
i18n,
|
||||
props: {
|
||||
calendarId: { type: Number, required: true },
|
||||
status: {
|
||||
type: String as PropType<
|
||||
"accepted" | "declined" | "pending" | "tentative"
|
||||
>,
|
||||
required: true,
|
||||
},
|
||||
emits: {
|
||||
statusChanged(
|
||||
payload: "accepted" | "declined" | "pending" | "tentative",
|
||||
) {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
statusChanged(payload: "accepted" | "declined" | "pending" | "tentative") {
|
||||
return true;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Statuses: {
|
||||
ACCEPTED,
|
||||
DECLINED,
|
||||
PENDING,
|
||||
TENTATIVELY_ACCEPTED,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
changeStatus: function (
|
||||
newStatus: "accepted" | "declined" | "pending" | "tentative",
|
||||
) {
|
||||
console.log("changeStatus", newStatus);
|
||||
const url = `/api/1.0/calendar/calendar/${this.$props.calendarId}/answer/${newStatus}.json`;
|
||||
window
|
||||
.fetch(url, {
|
||||
method: "POST",
|
||||
})
|
||||
.then((r: Response) => {
|
||||
if (!r.ok) {
|
||||
console.error("could not confirm answer", newStatus);
|
||||
return;
|
||||
}
|
||||
console.log("answer sent", newStatus);
|
||||
this.$emit("statusChanged", newStatus);
|
||||
});
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Statuses: {
|
||||
ACCEPTED,
|
||||
DECLINED,
|
||||
PENDING,
|
||||
TENTATIVELY_ACCEPTED,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
changeStatus: function (
|
||||
newStatus: "accepted" | "declined" | "pending" | "tentative",
|
||||
) {
|
||||
console.log("changeStatus", newStatus);
|
||||
const url = `/api/1.0/calendar/calendar/${this.$props.calendarId}/answer/${newStatus}.json`;
|
||||
window
|
||||
.fetch(url, {
|
||||
method: "POST",
|
||||
})
|
||||
.then((r: Response) => {
|
||||
if (!r.ok) {
|
||||
console.error("could not confirm answer", newStatus);
|
||||
return;
|
||||
}
|
||||
console.log("answer sent", newStatus);
|
||||
this.$emit("statusChanged", newStatus);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -346,7 +346,6 @@ const baseOptions = ref<CalendarOptions>({
|
||||
center: "title",
|
||||
right: "timeGridWeek,timeGridDay",
|
||||
},
|
||||
allDaySlot: false,
|
||||
});
|
||||
|
||||
const ranges = computed<EventInput[]>(() => {
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<template>
|
||||
<component :is="Teleport" to="body">
|
||||
<modal v-if="showModal" @close="closeModal">
|
||||
<template v-slot:header>
|
||||
<h3>{{ "Modifier le lieu" }}</h3>
|
||||
</template>
|
||||
<component :is="Teleport" to="body">
|
||||
<modal v-if="showModal" @close="closeModal">
|
||||
<template v-slot:header>
|
||||
<h3>{{ "Modifier le lieu" }}</h3>
|
||||
</template>
|
||||
|
||||
<template v-slot:body>
|
||||
<div></div>
|
||||
<label>Localisation</label>
|
||||
<vue-multiselect
|
||||
v-model="location"
|
||||
:options="locations"
|
||||
:label="'name'"
|
||||
:track-by="'id'"
|
||||
></vue-multiselect>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<div></div>
|
||||
<label>Localisation</label>
|
||||
<vue-multiselect
|
||||
v-model="location"
|
||||
:options="locations"
|
||||
:label="'name'"
|
||||
:track-by="'id'"
|
||||
></vue-multiselect>
|
||||
</template>
|
||||
|
||||
<template v-slot:footer>
|
||||
<button class="btn btn-save" @click="saveAndClose">
|
||||
{{ "Enregistrer" }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</component>
|
||||
<template v-slot:footer>
|
||||
<button class="btn btn-save" @click="saveAndClose">
|
||||
{{ "Enregistrer" }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -39,7 +39,7 @@ import VueMultiselect from "vue-multiselect";
|
||||
import { Teleport as teleport_, TeleportProps, VNodeProps } from "vue";
|
||||
|
||||
const Teleport = teleport_ as new () => {
|
||||
$props: VNodeProps & TeleportProps;
|
||||
$props: VNodeProps & TeleportProps;
|
||||
};
|
||||
|
||||
const store = useStore(key);
|
||||
@@ -50,37 +50,37 @@ const showModal = ref(false);
|
||||
//const tele = ref<InstanceType<typeof Teleport> | null>(null);
|
||||
|
||||
const locations = computed<Location[]>(() => {
|
||||
return store.state.locations.locations;
|
||||
return store.state.locations.locations;
|
||||
});
|
||||
|
||||
const startEdit = function (event: EventApi): void {
|
||||
console.log("startEditing", event);
|
||||
calendarRangeId.value = event.extendedProps.calendarRangeId;
|
||||
location.value =
|
||||
store.getters["locations/getLocationById"](
|
||||
event.extendedProps.locationId,
|
||||
) || null;
|
||||
console.log("startEditing", event);
|
||||
calendarRangeId.value = event.extendedProps.calendarRangeId;
|
||||
location.value =
|
||||
store.getters["locations/getLocationById"](
|
||||
event.extendedProps.locationId,
|
||||
) || null;
|
||||
|
||||
console.log("new location value", location.value);
|
||||
console.log("calendar range id", calendarRangeId.value);
|
||||
showModal.value = true;
|
||||
console.log("new location value", location.value);
|
||||
console.log("calendar range id", calendarRangeId.value);
|
||||
showModal.value = true;
|
||||
};
|
||||
|
||||
const saveAndClose = function (e: Event): void {
|
||||
console.log("saveEditAndClose", e);
|
||||
console.log("saveEditAndClose", e);
|
||||
|
||||
store
|
||||
.dispatch("calendarRanges/patchRangeLocation", {
|
||||
location: location.value,
|
||||
calendarRangeId: calendarRangeId.value,
|
||||
})
|
||||
.then((_) => {
|
||||
showModal.value = false;
|
||||
});
|
||||
store
|
||||
.dispatch("calendarRanges/patchRangeLocation", {
|
||||
location: location.value,
|
||||
calendarRangeId: calendarRangeId.value,
|
||||
})
|
||||
.then((_) => {
|
||||
showModal.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = function (_: any): void {
|
||||
showModal.value = false;
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
defineExpose({ startEdit });
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
const appMessages = {
|
||||
fr: {
|
||||
created_availabilities: "Lieu des plages de disponibilités créées",
|
||||
edit_your_calendar_range: "Planifiez vos plages de disponibilités",
|
||||
show_my_calendar: "Afficher mon calendrier",
|
||||
show_weekends: "Afficher les week-ends",
|
||||
copy_range: "Copier",
|
||||
copy_range_from_to: "Copier les plages",
|
||||
from_day_to_day: "d'un jour à l'autre",
|
||||
from_week_to_week: "d'une semaine à l'autre",
|
||||
copy_range_how_to:
|
||||
"Créez les plages de disponibilités durant une journée et copiez-les facilement au jour suivant avec ce bouton. Si les week-ends sont cachés, le jour suivant un vendredi sera le lundi.",
|
||||
new_range_to_save: "Nouvelles plages à enregistrer",
|
||||
update_range_to_save: "Plages à modifier",
|
||||
delete_range_to_save: "Plages à supprimer",
|
||||
by: "Par",
|
||||
main_user_concerned: "Utilisateur concerné",
|
||||
dateFrom: "De",
|
||||
dateTo: "à",
|
||||
day: "Jour",
|
||||
week: "Semaine",
|
||||
month: "Mois",
|
||||
today: "Aujourd'hui",
|
||||
},
|
||||
fr: {
|
||||
created_availabilities: "Lieu des plages de disponibilités créées",
|
||||
edit_your_calendar_range: "Planifiez vos plages de disponibilités",
|
||||
show_my_calendar: "Afficher mon calendrier",
|
||||
show_weekends: "Afficher les week-ends",
|
||||
copy_range: "Copier",
|
||||
copy_range_from_to: "Copier les plages",
|
||||
from_day_to_day: "d'un jour à l'autre",
|
||||
from_week_to_week: "d'une semaine à l'autre",
|
||||
copy_range_how_to:
|
||||
"Créez les plages de disponibilités durant une journée et copiez-les facilement au jour suivant avec ce bouton. Si les week-ends sont cachés, le jour suivant un vendredi sera le lundi.",
|
||||
new_range_to_save: "Nouvelles plages à enregistrer",
|
||||
update_range_to_save: "Plages à modifier",
|
||||
delete_range_to_save: "Plages à supprimer",
|
||||
by: "Par",
|
||||
main_user_concerned: "Utilisateur concerné",
|
||||
dateFrom: "De",
|
||||
dateTo: "à",
|
||||
day: "Jour",
|
||||
week: "Semaine",
|
||||
month: "Mois",
|
||||
today: "Aujourd'hui",
|
||||
},
|
||||
};
|
||||
|
||||
export { appMessages };
|
||||
|
||||
@@ -7,13 +7,13 @@ import App2 from "./App2.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
futureStore().then((store) => {
|
||||
const i18n = _createI18n(appMessages, false);
|
||||
const i18n = _createI18n(appMessages, false);
|
||||
|
||||
const app = createApp({
|
||||
template: `<app></app>`,
|
||||
})
|
||||
.use(store, key)
|
||||
.use(i18n)
|
||||
.component("app", App2)
|
||||
.mount("#myCalendar");
|
||||
const app = createApp({
|
||||
template: `<app></app>`,
|
||||
})
|
||||
.use(store, key)
|
||||
.use(i18n)
|
||||
.component("app", App2)
|
||||
.mount("#myCalendar");
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import me, { MeState } from "./modules/me";
|
||||
import fullCalendar, { FullCalendarState } from "./modules/fullcalendar";
|
||||
import calendarRanges, { CalendarRangesState } from "./modules/calendarRanges";
|
||||
import calendarRemotes, {
|
||||
CalendarRemotesState,
|
||||
CalendarRemotesState,
|
||||
} from "./modules/calendarRemotes";
|
||||
import { whoami } from "../../../../../../ChillMainBundle/Resources/public/lib/api/user";
|
||||
import { User } from "../../../../../../ChillMainBundle/Resources/public/types";
|
||||
@@ -15,42 +15,40 @@ import calendarLocals, { CalendarLocalsState } from "./modules/calendarLocals";
|
||||
const debug = process.env.NODE_ENV !== "production";
|
||||
|
||||
export interface State {
|
||||
calendarRanges: CalendarRangesState;
|
||||
calendarRemotes: CalendarRemotesState;
|
||||
calendarLocals: CalendarLocalsState;
|
||||
fullCalendar: FullCalendarState;
|
||||
me: MeState;
|
||||
locations: LocationState;
|
||||
calendarRanges: CalendarRangesState;
|
||||
calendarRemotes: CalendarRemotesState;
|
||||
calendarLocals: CalendarLocalsState;
|
||||
fullCalendar: FullCalendarState;
|
||||
me: MeState;
|
||||
locations: LocationState;
|
||||
}
|
||||
|
||||
export const key: InjectionKey<Store<State>> = Symbol();
|
||||
|
||||
const futureStore = function (): Promise<Store<State>> {
|
||||
return whoami().then((user: User) => {
|
||||
const store = createStore<State>({
|
||||
strict: debug,
|
||||
modules: {
|
||||
me,
|
||||
fullCalendar,
|
||||
calendarRanges,
|
||||
calendarRemotes,
|
||||
calendarLocals,
|
||||
locations,
|
||||
},
|
||||
mutations: {},
|
||||
});
|
||||
|
||||
store.commit("me/setWhoAmi", user, { root: true });
|
||||
store
|
||||
.dispatch("locations/getLocations", null, { root: true })
|
||||
.then((_) => {
|
||||
return store.dispatch("locations/getCurrentLocation", null, {
|
||||
root: true,
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.resolve(store);
|
||||
return whoami().then((user: User) => {
|
||||
const store = createStore<State>({
|
||||
strict: debug,
|
||||
modules: {
|
||||
me,
|
||||
fullCalendar,
|
||||
calendarRanges,
|
||||
calendarRemotes,
|
||||
calendarLocals,
|
||||
locations,
|
||||
},
|
||||
mutations: {},
|
||||
});
|
||||
|
||||
store.commit("me/setWhoAmi", user, { root: true });
|
||||
store.dispatch("locations/getLocations", null, { root: true }).then((_) => {
|
||||
return store.dispatch("locations/getCurrentLocation", null, {
|
||||
root: true,
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.resolve(store);
|
||||
});
|
||||
};
|
||||
|
||||
export default futureStore;
|
||||
|
||||
@@ -8,109 +8,99 @@ import { TransportExceptionInterface } from "../../../../../../../ChillMainBundl
|
||||
import { COLORS } from "../../../Calendar/const";
|
||||
|
||||
export interface CalendarLocalsState {
|
||||
locals: EventInput[];
|
||||
localsLoaded: { start: number; end: number }[];
|
||||
localsIndex: Set<string>;
|
||||
key: number;
|
||||
locals: EventInput[];
|
||||
localsLoaded: { start: number; end: number }[];
|
||||
localsIndex: Set<string>;
|
||||
key: number;
|
||||
}
|
||||
|
||||
type Context = ActionContext<CalendarLocalsState, State>;
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: (): CalendarLocalsState => ({
|
||||
locals: [],
|
||||
localsLoaded: [],
|
||||
localsIndex: new Set<string>(),
|
||||
key: 0,
|
||||
}),
|
||||
getters: {
|
||||
isLocalsLoaded:
|
||||
(state: CalendarLocalsState) =>
|
||||
({ start, end }: { start: Date; end: Date }): boolean => {
|
||||
for (const range of state.localsLoaded) {
|
||||
if (
|
||||
start.getTime() === range.start &&
|
||||
end.getTime() === range.end
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
namespaced: true,
|
||||
state: (): CalendarLocalsState => ({
|
||||
locals: [],
|
||||
localsLoaded: [],
|
||||
localsIndex: new Set<string>(),
|
||||
key: 0,
|
||||
}),
|
||||
getters: {
|
||||
isLocalsLoaded:
|
||||
(state: CalendarLocalsState) =>
|
||||
({ start, end }: { start: Date; end: Date }): boolean => {
|
||||
for (const range of state.localsLoaded) {
|
||||
if (start.getTime() === range.start && end.getTime() === range.end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
return false;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
addLocals(state: CalendarLocalsState, ranges: CalendarLight[]) {
|
||||
console.log("addLocals", ranges);
|
||||
|
||||
const toAdd = ranges
|
||||
.map((cr) => localsToFullCalendarEvent(cr))
|
||||
.filter((r) => !state.localsIndex.has(r.id));
|
||||
|
||||
toAdd.forEach((r) => {
|
||||
state.localsIndex.add(r.id);
|
||||
state.locals.push(r);
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
mutations: {
|
||||
addLocals(state: CalendarLocalsState, ranges: CalendarLight[]) {
|
||||
console.log("addLocals", ranges);
|
||||
|
||||
const toAdd = ranges
|
||||
.map((cr) => localsToFullCalendarEvent(cr))
|
||||
.filter((r) => !state.localsIndex.has(r.id));
|
||||
|
||||
toAdd.forEach((r) => {
|
||||
state.localsIndex.add(r.id);
|
||||
state.locals.push(r);
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
addLoaded(
|
||||
state: CalendarLocalsState,
|
||||
payload: { start: Date; end: Date },
|
||||
) {
|
||||
state.localsLoaded.push({
|
||||
start: payload.start.getTime(),
|
||||
end: payload.end.getTime(),
|
||||
});
|
||||
},
|
||||
addLoaded(state: CalendarLocalsState, payload: { start: Date; end: Date }) {
|
||||
state.localsLoaded.push({
|
||||
start: payload.start.getTime(),
|
||||
end: payload.end.getTime(),
|
||||
});
|
||||
},
|
||||
actions: {
|
||||
fetchLocals(
|
||||
ctx: Context,
|
||||
payload: { start: Date; end: Date },
|
||||
): Promise<null> {
|
||||
const start = payload.start;
|
||||
const end = payload.end;
|
||||
},
|
||||
actions: {
|
||||
fetchLocals(
|
||||
ctx: Context,
|
||||
payload: { start: Date; end: Date },
|
||||
): Promise<null> {
|
||||
const start = payload.start;
|
||||
const end = payload.end;
|
||||
|
||||
if (ctx.rootGetters["me/getMe"] === null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (ctx.rootGetters["me/getMe"] === null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (ctx.getters.isLocalsLoaded({ start, end })) {
|
||||
return Promise.resolve(ctx.getters.getRangeSource);
|
||||
}
|
||||
if (ctx.getters.isLocalsLoaded({ start, end })) {
|
||||
return Promise.resolve(ctx.getters.getRangeSource);
|
||||
}
|
||||
|
||||
ctx.commit("addLoaded", {
|
||||
start: start,
|
||||
end: end,
|
||||
});
|
||||
ctx.commit("addLoaded", {
|
||||
start: start,
|
||||
end: end,
|
||||
});
|
||||
|
||||
return fetchCalendarLocalForUser(
|
||||
ctx.rootGetters["me/getMe"],
|
||||
start,
|
||||
end,
|
||||
)
|
||||
.then((remotes: CalendarLight[]) => {
|
||||
// to be add when reactivity problem will be solve ?
|
||||
//ctx.commit('addRemotes', remotes);
|
||||
const inputs = remotes
|
||||
.map((cr) => localsToFullCalendarEvent(cr))
|
||||
.map((cr) => ({
|
||||
...cr,
|
||||
backgroundColor: COLORS[0],
|
||||
textColor: "black",
|
||||
editable: false,
|
||||
}));
|
||||
ctx.commit("calendarRanges/addExternals", inputs, {
|
||||
root: true,
|
||||
});
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((e: TransportExceptionInterface) => {
|
||||
console.error(e);
|
||||
return fetchCalendarLocalForUser(ctx.rootGetters["me/getMe"], start, end)
|
||||
.then((remotes: CalendarLight[]) => {
|
||||
// to be add when reactivity problem will be solve ?
|
||||
//ctx.commit('addRemotes', remotes);
|
||||
const inputs = remotes
|
||||
.map((cr) => localsToFullCalendarEvent(cr))
|
||||
.map((cr) => ({
|
||||
...cr,
|
||||
backgroundColor: COLORS[0],
|
||||
textColor: "black",
|
||||
editable: false,
|
||||
}));
|
||||
ctx.commit("calendarRanges/addExternals", inputs, {
|
||||
root: true,
|
||||
});
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((e: TransportExceptionInterface) => {
|
||||
console.error(e);
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
},
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
},
|
||||
},
|
||||
} as Module<CalendarLocalsState, State>;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { State } from "./../index";
|
||||
import { ActionContext, Module } from "vuex";
|
||||
import {
|
||||
CalendarRange,
|
||||
CalendarRangeCreate,
|
||||
CalendarRangeEdit,
|
||||
isEventInputCalendarRange,
|
||||
CalendarRange,
|
||||
CalendarRangeCreate,
|
||||
CalendarRangeEdit,
|
||||
isEventInputCalendarRange,
|
||||
} from "../../../../types";
|
||||
import { Location } from "../../../../../../../ChillMainBundle/Resources/public/types";
|
||||
import { fetchCalendarRangeForUser } from "../../../Calendar/api";
|
||||
@@ -12,369 +12,332 @@ import { calendarRangeToFullCalendarEvent } from "../../../Calendar/store/utils"
|
||||
import { EventInput } from "@fullcalendar/core";
|
||||
import { makeFetch } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
import {
|
||||
datetimeToISO,
|
||||
dateToISO,
|
||||
ISOToDatetime,
|
||||
datetimeToISO,
|
||||
dateToISO,
|
||||
ISOToDatetime,
|
||||
} from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||
import type { EventInputCalendarRange } from "../../../../types";
|
||||
|
||||
export interface CalendarRangesState {
|
||||
ranges: (EventInput | EventInputCalendarRange)[];
|
||||
rangesLoaded: { start: number; end: number }[];
|
||||
rangesIndex: Set<string>;
|
||||
key: number;
|
||||
ranges: (EventInput | EventInputCalendarRange)[];
|
||||
rangesLoaded: { start: number; end: number }[];
|
||||
rangesIndex: Set<string>;
|
||||
key: number;
|
||||
}
|
||||
|
||||
type Context = ActionContext<CalendarRangesState, State>;
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: (): CalendarRangesState => ({
|
||||
ranges: [],
|
||||
rangesLoaded: [],
|
||||
rangesIndex: new Set<string>(),
|
||||
key: 0,
|
||||
}),
|
||||
getters: {
|
||||
isRangeLoaded:
|
||||
(state: CalendarRangesState) =>
|
||||
({ start, end }: { start: Date; end: Date }): boolean => {
|
||||
for (const range of state.rangesLoaded) {
|
||||
if (
|
||||
start.getTime() === range.start &&
|
||||
end.getTime() === range.end
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
namespaced: true,
|
||||
state: (): CalendarRangesState => ({
|
||||
ranges: [],
|
||||
rangesLoaded: [],
|
||||
rangesIndex: new Set<string>(),
|
||||
key: 0,
|
||||
}),
|
||||
getters: {
|
||||
isRangeLoaded:
|
||||
(state: CalendarRangesState) =>
|
||||
({ start, end }: { start: Date; end: Date }): boolean => {
|
||||
for (const range of state.rangesLoaded) {
|
||||
if (start.getTime() === range.start && end.getTime() === range.end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
getRangesOnDate:
|
||||
(state: CalendarRangesState) =>
|
||||
(date: Date): EventInputCalendarRange[] => {
|
||||
const founds = [];
|
||||
const dateStr = dateToISO(date) as string;
|
||||
return false;
|
||||
},
|
||||
getRangesOnDate:
|
||||
(state: CalendarRangesState) =>
|
||||
(date: Date): EventInputCalendarRange[] => {
|
||||
const founds = [];
|
||||
const dateStr = dateToISO(date) as string;
|
||||
|
||||
for (const range of state.ranges) {
|
||||
if (
|
||||
isEventInputCalendarRange(range) &&
|
||||
range.start.startsWith(dateStr)
|
||||
) {
|
||||
founds.push(range);
|
||||
}
|
||||
}
|
||||
for (const range of state.ranges) {
|
||||
if (
|
||||
isEventInputCalendarRange(range) &&
|
||||
range.start.startsWith(dateStr)
|
||||
) {
|
||||
founds.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
return founds;
|
||||
},
|
||||
getRangesOnWeek:
|
||||
(state: CalendarRangesState) =>
|
||||
(mondayDate: Date): EventInputCalendarRange[] => {
|
||||
const founds = [];
|
||||
for (const d of Array.from(Array(7).keys())) {
|
||||
const dateOfWeek = new Date(mondayDate);
|
||||
dateOfWeek.setDate(mondayDate.getDate() + d);
|
||||
const dateStr = dateToISO(dateOfWeek) as string;
|
||||
for (const range of state.ranges) {
|
||||
if (
|
||||
isEventInputCalendarRange(range) &&
|
||||
range.start.startsWith(dateStr)
|
||||
) {
|
||||
founds.push(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
return founds;
|
||||
},
|
||||
getRangesOnWeek:
|
||||
(state: CalendarRangesState) =>
|
||||
(mondayDate: Date): EventInputCalendarRange[] => {
|
||||
const founds = [];
|
||||
for (const d of Array.from(Array(7).keys())) {
|
||||
const dateOfWeek = new Date(mondayDate);
|
||||
dateOfWeek.setDate(mondayDate.getDate() + d);
|
||||
const dateStr = dateToISO(dateOfWeek) as string;
|
||||
for (const range of state.ranges) {
|
||||
if (
|
||||
isEventInputCalendarRange(range) &&
|
||||
range.start.startsWith(dateStr)
|
||||
) {
|
||||
founds.push(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return founds;
|
||||
},
|
||||
return founds;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
addRanges(state: CalendarRangesState, ranges: CalendarRange[]) {
|
||||
const toAdd = ranges
|
||||
.map((cr) => calendarRangeToFullCalendarEvent(cr))
|
||||
.map((cr) => ({
|
||||
...cr,
|
||||
backgroundColor: "white",
|
||||
borderColor: "#3788d8",
|
||||
textColor: "black",
|
||||
}))
|
||||
.filter((r) => !state.rangesIndex.has(r.id));
|
||||
|
||||
toAdd.forEach((r) => {
|
||||
state.rangesIndex.add(r.id);
|
||||
state.ranges.push(r);
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
mutations: {
|
||||
addRanges(state: CalendarRangesState, ranges: CalendarRange[]) {
|
||||
const toAdd = ranges
|
||||
.map((cr) => calendarRangeToFullCalendarEvent(cr))
|
||||
.map((cr) => ({
|
||||
...cr,
|
||||
backgroundColor: "white",
|
||||
borderColor: "#3788d8",
|
||||
textColor: "black",
|
||||
}))
|
||||
.filter((r) => !state.rangesIndex.has(r.id));
|
||||
addExternals(
|
||||
state: CalendarRangesState,
|
||||
externalEvents: (EventInput & { id: string })[],
|
||||
) {
|
||||
const toAdd = externalEvents.filter((r) => !state.rangesIndex.has(r.id));
|
||||
|
||||
toAdd.forEach((r) => {
|
||||
state.rangesIndex.add(r.id);
|
||||
state.ranges.push(r);
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
addExternals(
|
||||
state: CalendarRangesState,
|
||||
externalEvents: (EventInput & { id: string })[],
|
||||
) {
|
||||
const toAdd = externalEvents.filter(
|
||||
(r) => !state.rangesIndex.has(r.id),
|
||||
);
|
||||
|
||||
toAdd.forEach((r) => {
|
||||
state.rangesIndex.add(r.id);
|
||||
state.ranges.push(r);
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
addLoaded(
|
||||
state: CalendarRangesState,
|
||||
payload: { start: Date; end: Date },
|
||||
) {
|
||||
state.rangesLoaded.push({
|
||||
start: payload.start.getTime(),
|
||||
end: payload.end.getTime(),
|
||||
});
|
||||
},
|
||||
addRange(state: CalendarRangesState, payload: CalendarRange) {
|
||||
const asEvent = calendarRangeToFullCalendarEvent(payload);
|
||||
state.ranges.push({
|
||||
...asEvent,
|
||||
backgroundColor: "white",
|
||||
borderColor: "#3788d8",
|
||||
textColor: "black",
|
||||
});
|
||||
state.rangesIndex.add(asEvent.id);
|
||||
state.key = state.key + 1;
|
||||
},
|
||||
removeRange(state: CalendarRangesState, calendarRangeId: number) {
|
||||
const found = state.ranges.find(
|
||||
(r) =>
|
||||
r.calendarRangeId === calendarRangeId && r.is === "range",
|
||||
);
|
||||
|
||||
if (found !== undefined) {
|
||||
state.ranges = state.ranges.filter(
|
||||
(r) =>
|
||||
!(
|
||||
r.calendarRangeId === calendarRangeId &&
|
||||
r.is === "range"
|
||||
),
|
||||
);
|
||||
|
||||
if (typeof found.id === "string") {
|
||||
// should always be true
|
||||
state.rangesIndex.delete(found.id);
|
||||
}
|
||||
|
||||
state.key = state.key + 1;
|
||||
}
|
||||
},
|
||||
updateRange(state: CalendarRangesState, range: CalendarRange) {
|
||||
const found = state.ranges.find(
|
||||
(r) => r.calendarRangeId === range.id && r.is === "range",
|
||||
);
|
||||
const newEvent = calendarRangeToFullCalendarEvent(range);
|
||||
|
||||
if (found !== undefined) {
|
||||
found.start = newEvent.start;
|
||||
found.end = newEvent.end;
|
||||
found.locationId = range.location.id;
|
||||
found.locationName = range.location.name;
|
||||
}
|
||||
|
||||
state.key = state.key + 1;
|
||||
},
|
||||
toAdd.forEach((r) => {
|
||||
state.rangesIndex.add(r.id);
|
||||
state.ranges.push(r);
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
actions: {
|
||||
fetchRanges(
|
||||
ctx: Context,
|
||||
payload: { start: Date; end: Date },
|
||||
): Promise<null> {
|
||||
const start = payload.start;
|
||||
const end = payload.end;
|
||||
|
||||
if (ctx.rootGetters["me/getMe"] === null) {
|
||||
return Promise.resolve(ctx.getters.getRangeSource);
|
||||
}
|
||||
|
||||
if (ctx.getters.isRangeLoaded({ start, end })) {
|
||||
return Promise.resolve(ctx.getters.getRangeSource);
|
||||
}
|
||||
|
||||
ctx.commit("addLoaded", {
|
||||
start: start,
|
||||
end: end,
|
||||
});
|
||||
|
||||
return fetchCalendarRangeForUser(
|
||||
ctx.rootGetters["me/getMe"],
|
||||
start,
|
||||
end,
|
||||
).then((ranges: CalendarRange[]) => {
|
||||
ctx.commit("addRanges", ranges);
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
},
|
||||
createRange(
|
||||
ctx: Context,
|
||||
{
|
||||
start,
|
||||
end,
|
||||
location,
|
||||
}: { start: Date; end: Date; location: Location },
|
||||
): Promise<null> {
|
||||
const url = `/api/1.0/calendar/calendar-range.json?`;
|
||||
|
||||
if (ctx.rootState.me.me === null) {
|
||||
throw new Error("user is currently null");
|
||||
}
|
||||
|
||||
const body = {
|
||||
user: {
|
||||
id: ctx.rootState.me.me.id,
|
||||
type: "user",
|
||||
},
|
||||
startDate: {
|
||||
datetime: datetimeToISO(start),
|
||||
},
|
||||
endDate: {
|
||||
datetime: datetimeToISO(end),
|
||||
},
|
||||
location: {
|
||||
id: location.id,
|
||||
type: "location",
|
||||
},
|
||||
} as CalendarRangeCreate;
|
||||
|
||||
return makeFetch<CalendarRangeCreate, CalendarRange>(
|
||||
"POST",
|
||||
url,
|
||||
body,
|
||||
)
|
||||
.then((newRange) => {
|
||||
ctx.commit("addRange", newRange);
|
||||
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
deleteRange(ctx: Context, calendarRangeId: number) {
|
||||
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
|
||||
|
||||
makeFetch<undefined, never>("DELETE", url).then(() => {
|
||||
ctx.commit("removeRange", calendarRangeId);
|
||||
});
|
||||
},
|
||||
patchRangeTime(
|
||||
ctx,
|
||||
{
|
||||
calendarRangeId,
|
||||
start,
|
||||
end,
|
||||
}: { calendarRangeId: number; start: Date; end: Date },
|
||||
): Promise<null> {
|
||||
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
|
||||
const body = {
|
||||
startDate: {
|
||||
datetime: datetimeToISO(start),
|
||||
},
|
||||
endDate: {
|
||||
datetime: datetimeToISO(end),
|
||||
},
|
||||
} as CalendarRangeEdit;
|
||||
|
||||
return makeFetch<CalendarRangeEdit, CalendarRange>(
|
||||
"PATCH",
|
||||
url,
|
||||
body,
|
||||
)
|
||||
.then((range) => {
|
||||
ctx.commit("updateRange", range);
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
},
|
||||
patchRangeLocation(
|
||||
ctx,
|
||||
{
|
||||
location,
|
||||
calendarRangeId,
|
||||
}: { location: Location; calendarRangeId: number },
|
||||
): Promise<null> {
|
||||
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
|
||||
const body = {
|
||||
location: {
|
||||
id: location.id,
|
||||
type: "location",
|
||||
},
|
||||
} as CalendarRangeEdit;
|
||||
|
||||
return makeFetch<CalendarRangeEdit, CalendarRange>(
|
||||
"PATCH",
|
||||
url,
|
||||
body,
|
||||
)
|
||||
.then((range) => {
|
||||
ctx.commit("updateRange", range);
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
},
|
||||
copyFromDayToAnotherDay(
|
||||
ctx,
|
||||
{ from, to }: { from: Date; to: Date },
|
||||
): Promise<null> {
|
||||
const rangesToCopy: EventInputCalendarRange[] =
|
||||
ctx.getters["getRangesOnDate"](from);
|
||||
const promises = [];
|
||||
|
||||
for (const r of rangesToCopy) {
|
||||
const start = new Date(ISOToDatetime(r.start) as Date);
|
||||
start.setFullYear(
|
||||
to.getFullYear(),
|
||||
to.getMonth(),
|
||||
to.getDate(),
|
||||
);
|
||||
const end = new Date(ISOToDatetime(r.end) as Date);
|
||||
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
||||
const location = ctx.rootGetters["locations/getLocationById"](
|
||||
r.locationId,
|
||||
);
|
||||
|
||||
promises.push(
|
||||
ctx.dispatch("createRange", { start, end, location }),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => Promise.resolve(null));
|
||||
},
|
||||
copyFromWeekToAnotherWeek(
|
||||
ctx: Context,
|
||||
{ fromMonday, toMonday }: { fromMonday: Date; toMonday: Date },
|
||||
): Promise<null> {
|
||||
const rangesToCopy: EventInputCalendarRange[] =
|
||||
ctx.getters["getRangesOnWeek"](fromMonday);
|
||||
const promises = [];
|
||||
const diffTime = toMonday.getTime() - fromMonday.getTime();
|
||||
for (const r of rangesToCopy) {
|
||||
const start = new Date(ISOToDatetime(r.start) as Date);
|
||||
const end = new Date(ISOToDatetime(r.end) as Date);
|
||||
start.setTime(start.getTime() + diffTime);
|
||||
end.setTime(end.getTime() + diffTime);
|
||||
const location = ctx.rootGetters["locations/getLocationById"](
|
||||
r.locationId,
|
||||
);
|
||||
|
||||
promises.push(
|
||||
ctx.dispatch("createRange", { start, end, location }),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => Promise.resolve(null));
|
||||
},
|
||||
addLoaded(state: CalendarRangesState, payload: { start: Date; end: Date }) {
|
||||
state.rangesLoaded.push({
|
||||
start: payload.start.getTime(),
|
||||
end: payload.end.getTime(),
|
||||
});
|
||||
},
|
||||
addRange(state: CalendarRangesState, payload: CalendarRange) {
|
||||
const asEvent = calendarRangeToFullCalendarEvent(payload);
|
||||
state.ranges.push({
|
||||
...asEvent,
|
||||
backgroundColor: "white",
|
||||
borderColor: "#3788d8",
|
||||
textColor: "black",
|
||||
});
|
||||
state.rangesIndex.add(asEvent.id);
|
||||
state.key = state.key + 1;
|
||||
},
|
||||
removeRange(state: CalendarRangesState, calendarRangeId: number) {
|
||||
const found = state.ranges.find(
|
||||
(r) => r.calendarRangeId === calendarRangeId && r.is === "range",
|
||||
);
|
||||
|
||||
if (found !== undefined) {
|
||||
state.ranges = state.ranges.filter(
|
||||
(r) => !(r.calendarRangeId === calendarRangeId && r.is === "range"),
|
||||
);
|
||||
|
||||
if (typeof found.id === "string") {
|
||||
// should always be true
|
||||
state.rangesIndex.delete(found.id);
|
||||
}
|
||||
|
||||
state.key = state.key + 1;
|
||||
}
|
||||
},
|
||||
updateRange(state: CalendarRangesState, range: CalendarRange) {
|
||||
const found = state.ranges.find(
|
||||
(r) => r.calendarRangeId === range.id && r.is === "range",
|
||||
);
|
||||
const newEvent = calendarRangeToFullCalendarEvent(range);
|
||||
|
||||
if (found !== undefined) {
|
||||
found.start = newEvent.start;
|
||||
found.end = newEvent.end;
|
||||
found.locationId = range.location.id;
|
||||
found.locationName = range.location.name;
|
||||
}
|
||||
|
||||
state.key = state.key + 1;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
fetchRanges(
|
||||
ctx: Context,
|
||||
payload: { start: Date; end: Date },
|
||||
): Promise<null> {
|
||||
const start = payload.start;
|
||||
const end = payload.end;
|
||||
|
||||
if (ctx.rootGetters["me/getMe"] === null) {
|
||||
return Promise.resolve(ctx.getters.getRangeSource);
|
||||
}
|
||||
|
||||
if (ctx.getters.isRangeLoaded({ start, end })) {
|
||||
return Promise.resolve(ctx.getters.getRangeSource);
|
||||
}
|
||||
|
||||
ctx.commit("addLoaded", {
|
||||
start: start,
|
||||
end: end,
|
||||
});
|
||||
|
||||
return fetchCalendarRangeForUser(
|
||||
ctx.rootGetters["me/getMe"],
|
||||
start,
|
||||
end,
|
||||
).then((ranges: CalendarRange[]) => {
|
||||
ctx.commit("addRanges", ranges);
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
},
|
||||
createRange(
|
||||
ctx: Context,
|
||||
{ start, end, location }: { start: Date; end: Date; location: Location },
|
||||
): Promise<null> {
|
||||
const url = `/api/1.0/calendar/calendar-range.json?`;
|
||||
|
||||
if (ctx.rootState.me.me === null) {
|
||||
throw new Error("user is currently null");
|
||||
}
|
||||
|
||||
const body = {
|
||||
user: {
|
||||
id: ctx.rootState.me.me.id,
|
||||
type: "user",
|
||||
},
|
||||
startDate: {
|
||||
datetime: datetimeToISO(start),
|
||||
},
|
||||
endDate: {
|
||||
datetime: datetimeToISO(end),
|
||||
},
|
||||
location: {
|
||||
id: location.id,
|
||||
type: "location",
|
||||
},
|
||||
} as CalendarRangeCreate;
|
||||
|
||||
return makeFetch<CalendarRangeCreate, CalendarRange>("POST", url, body)
|
||||
.then((newRange) => {
|
||||
ctx.commit("addRange", newRange);
|
||||
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
deleteRange(ctx: Context, calendarRangeId: number) {
|
||||
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
|
||||
|
||||
makeFetch<undefined, never>("DELETE", url).then(() => {
|
||||
ctx.commit("removeRange", calendarRangeId);
|
||||
});
|
||||
},
|
||||
patchRangeTime(
|
||||
ctx,
|
||||
{
|
||||
calendarRangeId,
|
||||
start,
|
||||
end,
|
||||
}: { calendarRangeId: number; start: Date; end: Date },
|
||||
): Promise<null> {
|
||||
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
|
||||
const body = {
|
||||
startDate: {
|
||||
datetime: datetimeToISO(start),
|
||||
},
|
||||
endDate: {
|
||||
datetime: datetimeToISO(end),
|
||||
},
|
||||
} as CalendarRangeEdit;
|
||||
|
||||
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body)
|
||||
.then((range) => {
|
||||
ctx.commit("updateRange", range);
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
},
|
||||
patchRangeLocation(
|
||||
ctx,
|
||||
{
|
||||
location,
|
||||
calendarRangeId,
|
||||
}: { location: Location; calendarRangeId: number },
|
||||
): Promise<null> {
|
||||
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
|
||||
const body = {
|
||||
location: {
|
||||
id: location.id,
|
||||
type: "location",
|
||||
},
|
||||
} as CalendarRangeEdit;
|
||||
|
||||
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body)
|
||||
.then((range) => {
|
||||
ctx.commit("updateRange", range);
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
},
|
||||
copyFromDayToAnotherDay(
|
||||
ctx,
|
||||
{ from, to }: { from: Date; to: Date },
|
||||
): Promise<null> {
|
||||
const rangesToCopy: EventInputCalendarRange[] =
|
||||
ctx.getters["getRangesOnDate"](from);
|
||||
const promises = [];
|
||||
|
||||
for (const r of rangesToCopy) {
|
||||
const start = new Date(ISOToDatetime(r.start) as Date);
|
||||
start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
||||
const end = new Date(ISOToDatetime(r.end) as Date);
|
||||
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
||||
const location = ctx.rootGetters["locations/getLocationById"](
|
||||
r.locationId,
|
||||
);
|
||||
|
||||
promises.push(ctx.dispatch("createRange", { start, end, location }));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => Promise.resolve(null));
|
||||
},
|
||||
copyFromWeekToAnotherWeek(
|
||||
ctx: Context,
|
||||
{ fromMonday, toMonday }: { fromMonday: Date; toMonday: Date },
|
||||
): Promise<null> {
|
||||
const rangesToCopy: EventInputCalendarRange[] =
|
||||
ctx.getters["getRangesOnWeek"](fromMonday);
|
||||
const promises = [];
|
||||
const diffTime = toMonday.getTime() - fromMonday.getTime();
|
||||
for (const r of rangesToCopy) {
|
||||
const start = new Date(ISOToDatetime(r.start) as Date);
|
||||
const end = new Date(ISOToDatetime(r.end) as Date);
|
||||
start.setTime(start.getTime() + diffTime);
|
||||
end.setTime(end.getTime() + diffTime);
|
||||
const location = ctx.rootGetters["locations/getLocationById"](
|
||||
r.locationId,
|
||||
);
|
||||
|
||||
promises.push(ctx.dispatch("createRange", { start, end, location }));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => Promise.resolve(null));
|
||||
},
|
||||
},
|
||||
} as Module<CalendarRangesState, State>;
|
||||
|
||||
@@ -8,109 +8,102 @@ import { TransportExceptionInterface } from "../../../../../../../ChillMainBundl
|
||||
import { COLORS } from "../../../Calendar/const";
|
||||
|
||||
export interface CalendarRemotesState {
|
||||
remotes: EventInput[];
|
||||
remotesLoaded: { start: number; end: number }[];
|
||||
remotesIndex: Set<string>;
|
||||
key: number;
|
||||
remotes: EventInput[];
|
||||
remotesLoaded: { start: number; end: number }[];
|
||||
remotesIndex: Set<string>;
|
||||
key: number;
|
||||
}
|
||||
|
||||
type Context = ActionContext<CalendarRemotesState, State>;
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: (): CalendarRemotesState => ({
|
||||
remotes: [],
|
||||
remotesLoaded: [],
|
||||
remotesIndex: new Set<string>(),
|
||||
key: 0,
|
||||
}),
|
||||
getters: {
|
||||
isRemotesLoaded:
|
||||
(state: CalendarRemotesState) =>
|
||||
({ start, end }: { start: Date; end: Date }): boolean => {
|
||||
for (const range of state.remotesLoaded) {
|
||||
if (
|
||||
start.getTime() === range.start &&
|
||||
end.getTime() === range.end
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
namespaced: true,
|
||||
state: (): CalendarRemotesState => ({
|
||||
remotes: [],
|
||||
remotesLoaded: [],
|
||||
remotesIndex: new Set<string>(),
|
||||
key: 0,
|
||||
}),
|
||||
getters: {
|
||||
isRemotesLoaded:
|
||||
(state: CalendarRemotesState) =>
|
||||
({ start, end }: { start: Date; end: Date }): boolean => {
|
||||
for (const range of state.remotesLoaded) {
|
||||
if (start.getTime() === range.start && end.getTime() === range.end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
return false;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
addRemotes(state: CalendarRemotesState, ranges: CalendarRemote[]) {
|
||||
console.log("addRemotes", ranges);
|
||||
|
||||
const toAdd = ranges
|
||||
.map((cr) => remoteToFullCalendarEvent(cr))
|
||||
.filter((r) => !state.remotesIndex.has(r.id));
|
||||
|
||||
toAdd.forEach((r) => {
|
||||
state.remotesIndex.add(r.id);
|
||||
state.remotes.push(r);
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
mutations: {
|
||||
addRemotes(state: CalendarRemotesState, ranges: CalendarRemote[]) {
|
||||
console.log("addRemotes", ranges);
|
||||
|
||||
const toAdd = ranges
|
||||
.map((cr) => remoteToFullCalendarEvent(cr))
|
||||
.filter((r) => !state.remotesIndex.has(r.id));
|
||||
|
||||
toAdd.forEach((r) => {
|
||||
state.remotesIndex.add(r.id);
|
||||
state.remotes.push(r);
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
addLoaded(
|
||||
state: CalendarRemotesState,
|
||||
payload: { start: Date; end: Date },
|
||||
) {
|
||||
state.remotesLoaded.push({
|
||||
start: payload.start.getTime(),
|
||||
end: payload.end.getTime(),
|
||||
});
|
||||
},
|
||||
addLoaded(
|
||||
state: CalendarRemotesState,
|
||||
payload: { start: Date; end: Date },
|
||||
) {
|
||||
state.remotesLoaded.push({
|
||||
start: payload.start.getTime(),
|
||||
end: payload.end.getTime(),
|
||||
});
|
||||
},
|
||||
actions: {
|
||||
fetchRemotes(
|
||||
ctx: Context,
|
||||
payload: { start: Date; end: Date },
|
||||
): Promise<null> {
|
||||
const start = payload.start;
|
||||
const end = payload.end;
|
||||
},
|
||||
actions: {
|
||||
fetchRemotes(
|
||||
ctx: Context,
|
||||
payload: { start: Date; end: Date },
|
||||
): Promise<null> {
|
||||
const start = payload.start;
|
||||
const end = payload.end;
|
||||
|
||||
if (ctx.rootGetters["me/getMe"] === null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (ctx.rootGetters["me/getMe"] === null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (ctx.getters.isRemotesLoaded({ start, end })) {
|
||||
return Promise.resolve(ctx.getters.getRangeSource);
|
||||
}
|
||||
if (ctx.getters.isRemotesLoaded({ start, end })) {
|
||||
return Promise.resolve(ctx.getters.getRangeSource);
|
||||
}
|
||||
|
||||
ctx.commit("addLoaded", {
|
||||
start: start,
|
||||
end: end,
|
||||
});
|
||||
ctx.commit("addLoaded", {
|
||||
start: start,
|
||||
end: end,
|
||||
});
|
||||
|
||||
return fetchCalendarRemoteForUser(
|
||||
ctx.rootGetters["me/getMe"],
|
||||
start,
|
||||
end,
|
||||
)
|
||||
.then((remotes: CalendarRemote[]) => {
|
||||
// to be add when reactivity problem will be solve ?
|
||||
//ctx.commit('addRemotes', remotes);
|
||||
const inputs = remotes
|
||||
.map((cr) => remoteToFullCalendarEvent(cr))
|
||||
.map((cr) => ({
|
||||
...cr,
|
||||
backgroundColor: COLORS[0],
|
||||
textColor: "black",
|
||||
editable: false,
|
||||
}));
|
||||
ctx.commit("calendarRanges/addExternals", inputs, {
|
||||
root: true,
|
||||
});
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((e: TransportExceptionInterface) => {
|
||||
console.error(e);
|
||||
return fetchCalendarRemoteForUser(ctx.rootGetters["me/getMe"], start, end)
|
||||
.then((remotes: CalendarRemote[]) => {
|
||||
// to be add when reactivity problem will be solve ?
|
||||
//ctx.commit('addRemotes', remotes);
|
||||
const inputs = remotes
|
||||
.map((cr) => remoteToFullCalendarEvent(cr))
|
||||
.map((cr) => ({
|
||||
...cr,
|
||||
backgroundColor: COLORS[0],
|
||||
textColor: "black",
|
||||
editable: false,
|
||||
}));
|
||||
ctx.commit("calendarRanges/addExternals", inputs, {
|
||||
root: true,
|
||||
});
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((e: TransportExceptionInterface) => {
|
||||
console.error(e);
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
},
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
},
|
||||
},
|
||||
} as Module<CalendarRemotesState, State>;
|
||||
|
||||
@@ -2,77 +2,77 @@ import { State } from "./../index";
|
||||
import { ActionContext } from "vuex";
|
||||
|
||||
export interface FullCalendarState {
|
||||
currentView: {
|
||||
start: Date | null;
|
||||
end: Date | null;
|
||||
};
|
||||
key: number;
|
||||
currentView: {
|
||||
start: Date | null;
|
||||
end: Date | null;
|
||||
};
|
||||
key: number;
|
||||
}
|
||||
|
||||
type Context = ActionContext<FullCalendarState, State>;
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: (): FullCalendarState => ({
|
||||
currentView: {
|
||||
start: null,
|
||||
end: null,
|
||||
},
|
||||
key: 0,
|
||||
}),
|
||||
mutations: {
|
||||
setCurrentDatesView: function (
|
||||
state: FullCalendarState,
|
||||
payload: { start: Date; end: Date },
|
||||
): void {
|
||||
state.currentView.start = payload.start;
|
||||
state.currentView.end = payload.end;
|
||||
},
|
||||
increaseKey: function (state: FullCalendarState): void {
|
||||
state.key = state.key + 1;
|
||||
},
|
||||
namespaced: true,
|
||||
state: (): FullCalendarState => ({
|
||||
currentView: {
|
||||
start: null,
|
||||
end: null,
|
||||
},
|
||||
actions: {
|
||||
setCurrentDatesView(
|
||||
ctx: Context,
|
||||
{ start, end }: { start: Date | null; end: Date | null },
|
||||
): Promise<null> {
|
||||
console.log("dispatch setCurrentDatesView", { start, end });
|
||||
|
||||
if (
|
||||
ctx.state.currentView.start !== start ||
|
||||
ctx.state.currentView.end !== end
|
||||
) {
|
||||
ctx.commit("setCurrentDatesView", { start, end });
|
||||
}
|
||||
|
||||
if (start !== null && end !== null) {
|
||||
return Promise.all([
|
||||
ctx
|
||||
.dispatch(
|
||||
"calendarRanges/fetchRanges",
|
||||
{ start, end },
|
||||
{ root: true },
|
||||
)
|
||||
.then((_) => Promise.resolve(null)),
|
||||
ctx
|
||||
.dispatch(
|
||||
"calendarRemotes/fetchRemotes",
|
||||
{ start, end },
|
||||
{ root: true },
|
||||
)
|
||||
.then((_) => Promise.resolve(null)),
|
||||
ctx
|
||||
.dispatch(
|
||||
"calendarLocals/fetchLocals",
|
||||
{ start, end },
|
||||
{ root: true },
|
||||
)
|
||||
.then((_) => Promise.resolve(null)),
|
||||
]).then((_) => Promise.resolve(null));
|
||||
} else {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
},
|
||||
key: 0,
|
||||
}),
|
||||
mutations: {
|
||||
setCurrentDatesView: function (
|
||||
state: FullCalendarState,
|
||||
payload: { start: Date; end: Date },
|
||||
): void {
|
||||
state.currentView.start = payload.start;
|
||||
state.currentView.end = payload.end;
|
||||
},
|
||||
increaseKey: function (state: FullCalendarState): void {
|
||||
state.key = state.key + 1;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setCurrentDatesView(
|
||||
ctx: Context,
|
||||
{ start, end }: { start: Date | null; end: Date | null },
|
||||
): Promise<null> {
|
||||
console.log("dispatch setCurrentDatesView", { start, end });
|
||||
|
||||
if (
|
||||
ctx.state.currentView.start !== start ||
|
||||
ctx.state.currentView.end !== end
|
||||
) {
|
||||
ctx.commit("setCurrentDatesView", { start, end });
|
||||
}
|
||||
|
||||
if (start !== null && end !== null) {
|
||||
return Promise.all([
|
||||
ctx
|
||||
.dispatch(
|
||||
"calendarRanges/fetchRanges",
|
||||
{ start, end },
|
||||
{ root: true },
|
||||
)
|
||||
.then((_) => Promise.resolve(null)),
|
||||
ctx
|
||||
.dispatch(
|
||||
"calendarRemotes/fetchRemotes",
|
||||
{ start, end },
|
||||
{ root: true },
|
||||
)
|
||||
.then((_) => Promise.resolve(null)),
|
||||
ctx
|
||||
.dispatch(
|
||||
"calendarLocals/fetchLocals",
|
||||
{ start, end },
|
||||
{ root: true },
|
||||
)
|
||||
.then((_) => Promise.resolve(null)),
|
||||
]).then((_) => Promise.resolve(null));
|
||||
} else {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,61 +5,61 @@ import { getLocations } from "../../../../../../../ChillMainBundle/Resources/pub
|
||||
import { whereami } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/user";
|
||||
|
||||
export interface LocationState {
|
||||
locations: Location[];
|
||||
locationPicked: Location | null;
|
||||
currentLocation: Location | null;
|
||||
locations: Location[];
|
||||
locationPicked: Location | null;
|
||||
currentLocation: Location | null;
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: (): LocationState => {
|
||||
return {
|
||||
locations: [],
|
||||
locationPicked: null,
|
||||
currentLocation: null,
|
||||
};
|
||||
namespaced: true,
|
||||
state: (): LocationState => {
|
||||
return {
|
||||
locations: [],
|
||||
locationPicked: null,
|
||||
currentLocation: null,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
getLocationById:
|
||||
(state) =>
|
||||
(id: number): Location | undefined => {
|
||||
return state.locations.find((l) => l.id === id);
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setLocations(state, locations): void {
|
||||
state.locations = locations;
|
||||
},
|
||||
getters: {
|
||||
getLocationById:
|
||||
(state) =>
|
||||
(id: number): Location | undefined => {
|
||||
return state.locations.find((l) => l.id === id);
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setLocations(state, locations): void {
|
||||
state.locations = locations;
|
||||
},
|
||||
setLocationPicked(state, location: Location | null): void {
|
||||
if (null === location) {
|
||||
state.locationPicked = null;
|
||||
return;
|
||||
}
|
||||
setLocationPicked(state, location: Location | null): void {
|
||||
if (null === location) {
|
||||
state.locationPicked = null;
|
||||
return;
|
||||
}
|
||||
|
||||
state.locationPicked =
|
||||
state.locations.find((l) => l.id === location.id) || null;
|
||||
},
|
||||
setCurrentLocation(state, location: Location | null): void {
|
||||
if (null === location) {
|
||||
state.currentLocation = null;
|
||||
return;
|
||||
}
|
||||
state.locationPicked =
|
||||
state.locations.find((l) => l.id === location.id) || null;
|
||||
},
|
||||
setCurrentLocation(state, location: Location | null): void {
|
||||
if (null === location) {
|
||||
state.currentLocation = null;
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentLocation =
|
||||
state.locations.find((l) => l.id === location.id) || null;
|
||||
},
|
||||
state.currentLocation =
|
||||
state.locations.find((l) => l.id === location.id) || null;
|
||||
},
|
||||
actions: {
|
||||
getLocations(ctx): Promise<void> {
|
||||
return getLocations().then((locations) => {
|
||||
ctx.commit("setLocations", locations);
|
||||
return Promise.resolve();
|
||||
});
|
||||
},
|
||||
getCurrentLocation(ctx): Promise<void> {
|
||||
return whereami().then((location) => {
|
||||
ctx.commit("setCurrentLocation", location);
|
||||
});
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
getLocations(ctx): Promise<void> {
|
||||
return getLocations().then((locations) => {
|
||||
ctx.commit("setLocations", locations);
|
||||
return Promise.resolve();
|
||||
});
|
||||
},
|
||||
getCurrentLocation(ctx): Promise<void> {
|
||||
return whereami().then((location) => {
|
||||
ctx.commit("setCurrentLocation", location);
|
||||
});
|
||||
},
|
||||
},
|
||||
} as Module<LocationState, State>;
|
||||
|
||||
@@ -3,24 +3,24 @@ import { User } from "../../../../../../../ChillMainBundle/Resources/public/type
|
||||
import { ActionContext } from "vuex";
|
||||
|
||||
export interface MeState {
|
||||
me: User | null;
|
||||
me: User | null;
|
||||
}
|
||||
|
||||
type Context = ActionContext<MeState, State>;
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: (): MeState => ({
|
||||
me: null,
|
||||
}),
|
||||
getters: {
|
||||
getMe: function (state: MeState): User | null {
|
||||
return state.me;
|
||||
},
|
||||
namespaced: true,
|
||||
state: (): MeState => ({
|
||||
me: null,
|
||||
}),
|
||||
getters: {
|
||||
getMe: function (state: MeState): User | null {
|
||||
return state.me;
|
||||
},
|
||||
mutations: {
|
||||
setWhoAmi(state: MeState, me: User) {
|
||||
state.me = me;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setWhoAmi(state: MeState, me: User) {
|
||||
state.me = me;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="chill-red">
|
||||
{{ $t("choose_your_calendar_user") }}
|
||||
</h2>
|
||||
<VueMultiselect
|
||||
name="field"
|
||||
id="calendarUserSelector"
|
||||
v-model="value"
|
||||
track-by="id"
|
||||
label="value"
|
||||
:custom-label="transName"
|
||||
:placeholder="$t('select_user')"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:allow-empty="true"
|
||||
:model-value="value"
|
||||
:select-label="$t('multiselect.select_label')"
|
||||
:deselect-label="$t('multiselect.deselect_label')"
|
||||
:selected-label="$t('multiselect.selected_label')"
|
||||
@select="selectUsers"
|
||||
@remove="unSelectUsers"
|
||||
@close="coloriseSelectedValues"
|
||||
:options="options"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="myCalendar"
|
||||
class="form-check-input"
|
||||
v-model="showMyCalendarWidget"
|
||||
/>
|
||||
<label class="form-check-label" for="myCalendar">{{
|
||||
$t("show_my_calendar")
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="weekends"
|
||||
class="form-check-input"
|
||||
@click="toggleWeekends"
|
||||
/>
|
||||
<label class="form-check-label" for="weekends">{{
|
||||
$t("show_weekends")
|
||||
}}</label>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="chill-red">
|
||||
{{ $t("choose_your_calendar_user") }}
|
||||
</h2>
|
||||
<VueMultiselect
|
||||
name="field"
|
||||
id="calendarUserSelector"
|
||||
v-model="value"
|
||||
track-by="id"
|
||||
label="value"
|
||||
:custom-label="transName"
|
||||
:placeholder="$t('select_user')"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:allow-empty="true"
|
||||
:model-value="value"
|
||||
:select-label="$t('multiselect.select_label')"
|
||||
:deselect-label="$t('multiselect.deselect_label')"
|
||||
:selected-label="$t('multiselect.selected_label')"
|
||||
@select="selectUsers"
|
||||
@remove="unSelectUsers"
|
||||
@close="coloriseSelectedValues"
|
||||
:options="options"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="myCalendar"
|
||||
class="form-check-input"
|
||||
v-model="showMyCalendarWidget"
|
||||
/>
|
||||
<label class="form-check-label" for="myCalendar">{{
|
||||
$t("show_my_calendar")
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="weekends"
|
||||
class="form-check-input"
|
||||
@click="toggleWeekends"
|
||||
/>
|
||||
<label class="form-check-label" for="weekends">{{
|
||||
$t("show_weekends")
|
||||
}}</label>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { fetchCalendarRanges, fetchCalendar } from "../../_api/api";
|
||||
@@ -53,206 +53,183 @@ import VueMultiselect from "vue-multiselect";
|
||||
import { whoami } from "ChillPersonAssets/vuejs/AccompanyingCourse/api";
|
||||
|
||||
const COLORS = [
|
||||
/* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
|
||||
"#8dd3c7",
|
||||
"#ffffb3",
|
||||
"#bebada",
|
||||
"#fb8072",
|
||||
"#80b1d3",
|
||||
"#fdb462",
|
||||
"#b3de69",
|
||||
"#fccde5",
|
||||
"#d9d9d9",
|
||||
"#bc80bd",
|
||||
"#ccebc5",
|
||||
"#ffed6f",
|
||||
/* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
|
||||
"#8dd3c7",
|
||||
"#ffffb3",
|
||||
"#bebada",
|
||||
"#fb8072",
|
||||
"#80b1d3",
|
||||
"#fdb462",
|
||||
"#b3de69",
|
||||
"#fccde5",
|
||||
"#d9d9d9",
|
||||
"#bc80bd",
|
||||
"#ccebc5",
|
||||
"#ffed6f",
|
||||
];
|
||||
|
||||
export default {
|
||||
name: "CalendarUserSelector",
|
||||
components: { VueMultiselect },
|
||||
props: [
|
||||
"users",
|
||||
"updateEventsSource",
|
||||
"calendarEvents",
|
||||
"showMyCalendar",
|
||||
"toggleMyCalendar",
|
||||
"toggleWeekends",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
errorMsg: [],
|
||||
value: [],
|
||||
options: [],
|
||||
};
|
||||
name: "CalendarUserSelector",
|
||||
components: { VueMultiselect },
|
||||
props: [
|
||||
"users",
|
||||
"updateEventsSource",
|
||||
"calendarEvents",
|
||||
"showMyCalendar",
|
||||
"toggleMyCalendar",
|
||||
"toggleWeekends",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
errorMsg: [],
|
||||
value: [],
|
||||
options: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showMyCalendarWidget: {
|
||||
set(value) {
|
||||
this.toggleMyCalendar(value);
|
||||
this.updateEventsSource();
|
||||
},
|
||||
get() {
|
||||
return this.showMyCalendar;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showMyCalendarWidget: {
|
||||
set(value) {
|
||||
this.toggleMyCalendar(value);
|
||||
this.updateEventsSource();
|
||||
},
|
||||
get() {
|
||||
return this.showMyCalendar;
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.fetchData();
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.fetchData();
|
||||
},
|
||||
fetchData() {
|
||||
fetchCalendarRanges()
|
||||
.then(
|
||||
(calendarRanges) =>
|
||||
new Promise((resolve, reject) => {
|
||||
let results = calendarRanges.results;
|
||||
fetchData() {
|
||||
fetchCalendarRanges()
|
||||
.then(
|
||||
(calendarRanges) =>
|
||||
new Promise((resolve, reject) => {
|
||||
let results = calendarRanges.results;
|
||||
|
||||
let users = [];
|
||||
let users = [];
|
||||
|
||||
results.forEach((i) => {
|
||||
if (!users.some((j) => i.user.id === j.id)) {
|
||||
let ratio = Math.floor(
|
||||
users.length / COLORS.length,
|
||||
);
|
||||
let colorIndex =
|
||||
users.length - ratio * COLORS.length;
|
||||
users.push({
|
||||
id: i.user.id,
|
||||
username: i.user.username,
|
||||
color: COLORS[colorIndex],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let calendarEvents = [];
|
||||
users.forEach((u) => {
|
||||
let arr = results
|
||||
.filter((i) => i.user.id === u.id)
|
||||
.map((i) => ({
|
||||
start: i.startDate.datetime,
|
||||
end: i.endDate.datetime,
|
||||
calendarRangeId: i.id,
|
||||
sourceColor: u.color,
|
||||
//display: 'background' // can be an option for the disponibility
|
||||
}));
|
||||
calendarEvents.push({
|
||||
events: arr,
|
||||
color: u.color,
|
||||
textColor: "#444444",
|
||||
editable: false,
|
||||
id: u.id,
|
||||
});
|
||||
});
|
||||
|
||||
this.users.loaded = users;
|
||||
this.options = users;
|
||||
|
||||
this.calendarEvents.loaded = calendarEvents;
|
||||
whoami().then(
|
||||
(me) =>
|
||||
new Promise((resolve, reject) => {
|
||||
this.users.logged = me;
|
||||
let currentUser = users.find(
|
||||
(u) => u.id === me.id,
|
||||
);
|
||||
this.value = currentUser;
|
||||
|
||||
fetchCalendar(currentUser.id).then(
|
||||
(calendar) =>
|
||||
new Promise(
|
||||
(resolve, reject) => {
|
||||
let results =
|
||||
calendar.results;
|
||||
let events =
|
||||
results.map(
|
||||
(i) => ({
|
||||
start: i
|
||||
.startDate
|
||||
.datetime,
|
||||
end: i
|
||||
.endDate
|
||||
.datetime,
|
||||
}),
|
||||
);
|
||||
let calendarEventsCurrentUser =
|
||||
{
|
||||
events: events,
|
||||
color: "darkblue",
|
||||
id: 1000,
|
||||
editable: false,
|
||||
};
|
||||
this.calendarEvents.user =
|
||||
calendarEventsCurrentUser;
|
||||
|
||||
this.selectUsers(
|
||||
currentUser,
|
||||
);
|
||||
|
||||
resolve();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
|
||||
resolve();
|
||||
}),
|
||||
)
|
||||
.catch((error) => {
|
||||
this.errorMsg.push(error.message);
|
||||
});
|
||||
},
|
||||
transName(value) {
|
||||
return `${value.username}`;
|
||||
},
|
||||
coloriseSelectedValues() {
|
||||
let tags = document.querySelectorAll(
|
||||
"div.multiselect__tags-wrap",
|
||||
)[0];
|
||||
|
||||
if (tags.hasChildNodes()) {
|
||||
let children = tags.childNodes;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
let child = children[i];
|
||||
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
this.users.selected.forEach((u) => {
|
||||
if (child.hasChildNodes()) {
|
||||
if (child.firstChild.innerText == u.username) {
|
||||
child.style.background = u.color;
|
||||
child.firstChild.style.color = "#444444";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
results.forEach((i) => {
|
||||
if (!users.some((j) => i.user.id === j.id)) {
|
||||
let ratio = Math.floor(users.length / COLORS.length);
|
||||
let colorIndex = users.length - ratio * COLORS.length;
|
||||
users.push({
|
||||
id: i.user.id,
|
||||
username: i.user.username,
|
||||
color: COLORS[colorIndex],
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
selectEvents() {
|
||||
let selectedUsersId = this.users.selected.map((a) => a.id);
|
||||
this.calendarEvents.selected = this.calendarEvents.loaded.filter(
|
||||
(a) => selectedUsersId.includes(a.id),
|
||||
);
|
||||
},
|
||||
selectUsers(value) {
|
||||
this.users.selected.push(value);
|
||||
this.coloriseSelectedValues();
|
||||
this.selectEvents();
|
||||
this.updateEventsSource();
|
||||
},
|
||||
unSelectUsers(value) {
|
||||
this.users.selected = this.users.selected.filter(
|
||||
(a) => a.id != value.id,
|
||||
);
|
||||
this.selectEvents();
|
||||
this.updateEventsSource();
|
||||
},
|
||||
});
|
||||
|
||||
let calendarEvents = [];
|
||||
users.forEach((u) => {
|
||||
let arr = results
|
||||
.filter((i) => i.user.id === u.id)
|
||||
.map((i) => ({
|
||||
start: i.startDate.datetime,
|
||||
end: i.endDate.datetime,
|
||||
calendarRangeId: i.id,
|
||||
sourceColor: u.color,
|
||||
//display: 'background' // can be an option for the disponibility
|
||||
}));
|
||||
calendarEvents.push({
|
||||
events: arr,
|
||||
color: u.color,
|
||||
textColor: "#444444",
|
||||
editable: false,
|
||||
id: u.id,
|
||||
});
|
||||
});
|
||||
|
||||
this.users.loaded = users;
|
||||
this.options = users;
|
||||
|
||||
this.calendarEvents.loaded = calendarEvents;
|
||||
whoami().then(
|
||||
(me) =>
|
||||
new Promise((resolve, reject) => {
|
||||
this.users.logged = me;
|
||||
let currentUser = users.find((u) => u.id === me.id);
|
||||
this.value = currentUser;
|
||||
|
||||
fetchCalendar(currentUser.id).then(
|
||||
(calendar) =>
|
||||
new Promise((resolve, reject) => {
|
||||
let results = calendar.results;
|
||||
let events = results.map((i) => ({
|
||||
start: i.startDate.datetime,
|
||||
end: i.endDate.datetime,
|
||||
}));
|
||||
let calendarEventsCurrentUser = {
|
||||
events: events,
|
||||
color: "darkblue",
|
||||
id: 1000,
|
||||
editable: false,
|
||||
};
|
||||
this.calendarEvents.user = calendarEventsCurrentUser;
|
||||
|
||||
this.selectUsers(currentUser);
|
||||
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
|
||||
resolve();
|
||||
}),
|
||||
)
|
||||
.catch((error) => {
|
||||
this.errorMsg.push(error.message);
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
transName(value) {
|
||||
return `${value.username}`;
|
||||
},
|
||||
coloriseSelectedValues() {
|
||||
let tags = document.querySelectorAll("div.multiselect__tags-wrap")[0];
|
||||
|
||||
if (tags.hasChildNodes()) {
|
||||
let children = tags.childNodes;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
let child = children[i];
|
||||
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
this.users.selected.forEach((u) => {
|
||||
if (child.hasChildNodes()) {
|
||||
if (child.firstChild.innerText == u.username) {
|
||||
child.style.background = u.color;
|
||||
child.firstChild.style.color = "#444444";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
selectEvents() {
|
||||
let selectedUsersId = this.users.selected.map((a) => a.id);
|
||||
this.calendarEvents.selected = this.calendarEvents.loaded.filter((a) =>
|
||||
selectedUsersId.includes(a.id),
|
||||
);
|
||||
},
|
||||
selectUsers(value) {
|
||||
this.users.selected.push(value);
|
||||
this.coloriseSelectedValues();
|
||||
this.selectEvents();
|
||||
this.updateEventsSource();
|
||||
},
|
||||
unSelectUsers(value) {
|
||||
this.users.selected = this.users.selected.filter((a) => a.id != value.id);
|
||||
this.selectEvents();
|
||||
this.updateEventsSource();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
{% if calendar.status == 'canceled' %}
|
||||
<del>
|
||||
{% endif %}
|
||||
{% if calendar.status == 'canceled' %}
|
||||
<del>
|
||||
{% endif %}
|
||||
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
|
||||
{{ calendar.startDate|format_datetime('short', 'short') }}
|
||||
- {{ calendar.endDate|format_datetime('short', 'short') }}
|
||||
@@ -75,7 +78,7 @@
|
||||
</div>
|
||||
|
||||
{% if calendar.comment.comment is not empty
|
||||
or calendar.persons|length > 0
|
||||
or calendar.users|length > 0
|
||||
or calendar.thirdParties|length > 0
|
||||
or calendar.users|length > 0 %}
|
||||
<div class="item-row details separator">
|
||||
@@ -237,6 +240,10 @@
|
||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_cancel', { 'id': calendar.id } ) }}"
|
||||
class="btn btn-action"><i class="bi bi-x-circle"></i> {{ 'Cancel'|trans }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_cancel', { 'id': calendar.id } ) }}"
|
||||
class="btn btn-action"><i class="bi bi-x-circle"></i> {{ 'Cancel'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('CHILL_CALENDAR_CALENDAR_DELETE', calendar) %}
|
||||
|
||||
@@ -41,7 +41,6 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
mt_srand(123456789);
|
||||
$this->fakerFr = \Faker\Factory::create('fr_FR');
|
||||
$this->fakerEn = \Faker\Factory::create('en_EN');
|
||||
$this->fakerNl = \Faker\Factory::create('nl_NL');
|
||||
@@ -105,7 +104,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface
|
||||
$manager->persist($parent);
|
||||
|
||||
// Load children
|
||||
$expected_nb_children = mt_rand(10, 50);
|
||||
$expected_nb_children = random_int(10, 50);
|
||||
|
||||
for ($i = 0; $i < $expected_nb_children; ++$i) {
|
||||
$companyName = $this->fakerFr->company;
|
||||
@@ -145,7 +144,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface
|
||||
$manager->persist($parent);
|
||||
|
||||
// Load children
|
||||
$expected_nb_children = mt_rand(10, 50);
|
||||
$expected_nb_children = random_int(10, 50);
|
||||
|
||||
for ($i = 0; $i < $expected_nb_children; ++$i) {
|
||||
$manager->persist($this->createChildOption($parent, [
|
||||
|
||||
@@ -52,7 +52,7 @@ class ChillCustomFieldsExtension extends Extension implements PrependExtensionIn
|
||||
}
|
||||
|
||||
/** (non-PHPdoc).
|
||||
* @see PrependExtensionInterface::prepend()
|
||||
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
|
||||
*/
|
||||
public function prepend(ContainerBuilder $container)
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ class ChoiceWithOtherType extends AbstractType
|
||||
private string $otherValueLabel = 'Other value';
|
||||
|
||||
/** (non-PHPdoc).
|
||||
* @see AbstractType::buildForm()
|
||||
* @see \Symfony\Component\Form\AbstractType::buildForm()
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
@@ -42,7 +42,7 @@ class ChoiceWithOtherType extends AbstractType
|
||||
}
|
||||
|
||||
/** (non-PHPdoc).
|
||||
* @see AbstractType::configureOptions()
|
||||
* @see \Symfony\Component\Form\AbstractType::configureOptions()
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ use Symfony\Component\Form\FormEvents;
|
||||
class ChoicesListType extends AbstractType
|
||||
{
|
||||
/** (non-PHPdoc).
|
||||
* @see AbstractType::buildForm()
|
||||
* @see \Symfony\Component\Form\AbstractType::buildForm()
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
|
||||
@@ -82,7 +82,7 @@ class CustomFieldProvider implements ContainerAwareInterface
|
||||
/**
|
||||
* (non-PHPdoc).
|
||||
*
|
||||
* @see ContainerAwareInterface::setContainer()
|
||||
* @see \Symfony\Component\DependencyInjection\ContainerAwareInterface::setContainer()
|
||||
*/
|
||||
public function setContainer(?ContainerInterface $container = null)
|
||||
{
|
||||
|
||||
@@ -1,59 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="templates.length > 0">
|
||||
<slot name="title">
|
||||
<h2>{{ $t("generate_document") }}</h2>
|
||||
</slot>
|
||||
<div>
|
||||
<template v-if="templates.length > 0">
|
||||
<slot name="title">
|
||||
<h2>{{ $t("generate_document") }}</h2>
|
||||
</slot>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<slot name="label">
|
||||
<label>{{ $t("select_a_template") }}</label>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="input-group mb-3">
|
||||
<select class="form-select" v-model="template">
|
||||
<option disabled selected value="">
|
||||
{{ $t("choose_a_template") }}
|
||||
</option>
|
||||
<template v-for="t in templates" :key="t.id">
|
||||
<option :value="t.id">
|
||||
{{
|
||||
localizeString(t.name) ||
|
||||
"Aucun nom défini"
|
||||
}}
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
<a
|
||||
v-if="canGenerate"
|
||||
class="btn btn-update btn-sm change-icon"
|
||||
:href="buildUrlGenerate"
|
||||
@click.prevent="
|
||||
clickGenerate($event, buildUrlGenerate)
|
||||
"
|
||||
><i class="fa fa-fw fa-cog"
|
||||
/></a>
|
||||
<a
|
||||
v-else
|
||||
class="btn btn-update btn-sm change-icon"
|
||||
href="#"
|
||||
disabled
|
||||
><i class="fa fa-fw fa-cog"
|
||||
/></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="hasDescription">
|
||||
<div class="col-md-8 align-self-end">
|
||||
<p>{{ getDescription }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<slot name="label">
|
||||
<label>{{ $t("select_a_template") }}</label>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="input-group mb-3">
|
||||
<select class="form-select" v-model="template">
|
||||
<option disabled selected value="">
|
||||
{{ $t("choose_a_template") }}
|
||||
</option>
|
||||
<template v-for="t in templates" :key="t.id">
|
||||
<option :value="t.id">
|
||||
{{ localizeString(t.name) || "Aucun nom défini" }}
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
<a
|
||||
v-if="canGenerate"
|
||||
class="btn btn-update btn-sm change-icon"
|
||||
:href="buildUrlGenerate"
|
||||
@click.prevent="clickGenerate($event, buildUrlGenerate)"
|
||||
><i class="fa fa-fw fa-cog"
|
||||
/></a>
|
||||
<a
|
||||
v-else
|
||||
class="btn btn-update btn-sm change-icon"
|
||||
href="#"
|
||||
disabled
|
||||
><i class="fa fa-fw fa-cog"
|
||||
/></a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="hasDescription">
|
||||
<div class="col-md-8 align-self-end">
|
||||
<p>{{ getDescription }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -61,83 +56,83 @@ import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator";
|
||||
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||
|
||||
export default {
|
||||
name: "PickTemplate",
|
||||
props: {
|
||||
entityId: [String, Number],
|
||||
entityClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
templates: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
preventDefaultMoveToGenerate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
name: "PickTemplate",
|
||||
props: {
|
||||
entityId: [String, Number],
|
||||
entityClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
emits: ["goToGenerateDocument"],
|
||||
data() {
|
||||
return {
|
||||
template: null,
|
||||
};
|
||||
templates: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
canGenerate() {
|
||||
return this.template != null;
|
||||
},
|
||||
hasDescription() {
|
||||
if (this.template == null) {
|
||||
return false;
|
||||
}
|
||||
preventDefaultMoveToGenerate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["goToGenerateDocument"],
|
||||
data() {
|
||||
return {
|
||||
template: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canGenerate() {
|
||||
return this.template != null;
|
||||
},
|
||||
hasDescription() {
|
||||
if (this.template == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
getDescription() {
|
||||
if (null === this.template) {
|
||||
return "";
|
||||
}
|
||||
let desc = this.templates.find((t) => t.id === this.template);
|
||||
if (null === desc) {
|
||||
return "";
|
||||
}
|
||||
return desc.description || "";
|
||||
},
|
||||
buildUrlGenerate() {
|
||||
if (null === this.template) {
|
||||
return "#";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getDescription() {
|
||||
if (null === this.template) {
|
||||
return "";
|
||||
}
|
||||
let desc = this.templates.find((t) => t.id === this.template);
|
||||
if (null === desc) {
|
||||
return "";
|
||||
}
|
||||
return desc.description || "";
|
||||
},
|
||||
buildUrlGenerate() {
|
||||
if (null === this.template) {
|
||||
return "#";
|
||||
}
|
||||
|
||||
return buildLink(this.template, this.entityId, this.entityClass);
|
||||
},
|
||||
return buildLink(this.template, this.entityId, this.entityClass);
|
||||
},
|
||||
methods: {
|
||||
localizeString(str) {
|
||||
return localizeString(str);
|
||||
},
|
||||
clickGenerate(event, link) {
|
||||
if (!this.preventDefaultMoveToGenerate) {
|
||||
window.location.assign(link);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
localizeString(str) {
|
||||
return localizeString(str);
|
||||
},
|
||||
clickGenerate(event, link) {
|
||||
if (!this.preventDefaultMoveToGenerate) {
|
||||
window.location.assign(link);
|
||||
}
|
||||
|
||||
this.$emit("goToGenerateDocument", {
|
||||
event,
|
||||
link,
|
||||
template: this.template,
|
||||
});
|
||||
},
|
||||
this.$emit("goToGenerateDocument", {
|
||||
event,
|
||||
link,
|
||||
template: this.template,
|
||||
});
|
||||
},
|
||||
i18n: {
|
||||
messages: {
|
||||
fr: {
|
||||
generate_document: "Générer un document",
|
||||
select_a_template: "Choisir un modèle",
|
||||
choose_a_template: "Choisir",
|
||||
},
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
messages: {
|
||||
fr: {
|
||||
generate_document: "Générer un document",
|
||||
select_a_template: "Choisir un modèle",
|
||||
choose_a_template: "Choisir",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
/**
|
||||
* @var Collection<int, StoredObjectVersion>&Selectable<int, StoredObjectVersion>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true)]
|
||||
#[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true, fetch: 'EAGER')]
|
||||
private Collection&Selectable $versions;
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,20 +6,20 @@ const algo = "AES-CBC";
|
||||
const URL_POST = "/asyncupload/temp_url/generate/post";
|
||||
|
||||
const keyDefinition = {
|
||||
name: algo,
|
||||
length: 256,
|
||||
name: algo,
|
||||
length: 256,
|
||||
};
|
||||
|
||||
const createFilename = (): string => {
|
||||
let text = "";
|
||||
const possible =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let text = "";
|
||||
const possible =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
for (let i = 0; i < 7; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
|
||||
return text;
|
||||
return text;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -30,59 +30,59 @@ const createFilename = (): string => {
|
||||
* @returns {Promise<StoredObject>} A Promise that resolves to the newly created StoredObject.
|
||||
*/
|
||||
export const fetchNewStoredObject = async (): Promise<StoredObject> => {
|
||||
return makeFetch("POST", "/api/1.0/doc-store/stored-object/create", null);
|
||||
return makeFetch("POST", "/api/1.0/doc-store/stored-object/create", null);
|
||||
};
|
||||
|
||||
export const uploadVersion = async (
|
||||
uploadFile: ArrayBuffer,
|
||||
storedObject: StoredObject,
|
||||
uploadFile: ArrayBuffer,
|
||||
storedObject: StoredObject,
|
||||
): Promise<string> => {
|
||||
const params = new URLSearchParams();
|
||||
params.append("expires_delay", "180");
|
||||
params.append("submit_delay", "180");
|
||||
const asyncData: PostStoreObjectSignature = await makeFetch(
|
||||
"GET",
|
||||
`/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/post` +
|
||||
"?" +
|
||||
params.toString(),
|
||||
);
|
||||
const suffix = createFilename();
|
||||
const filename = asyncData.prefix + suffix;
|
||||
const formData = new FormData();
|
||||
formData.append("redirect", asyncData.redirect);
|
||||
formData.append("max_file_size", asyncData.max_file_size.toString());
|
||||
formData.append("max_file_count", asyncData.max_file_count.toString());
|
||||
formData.append("expires", asyncData.expires.toString());
|
||||
formData.append("signature", asyncData.signature);
|
||||
formData.append(filename, new Blob([uploadFile]), suffix);
|
||||
const params = new URLSearchParams();
|
||||
params.append("expires_delay", "180");
|
||||
params.append("submit_delay", "180");
|
||||
const asyncData: PostStoreObjectSignature = await makeFetch(
|
||||
"GET",
|
||||
`/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/post` +
|
||||
"?" +
|
||||
params.toString(),
|
||||
);
|
||||
const suffix = createFilename();
|
||||
const filename = asyncData.prefix + suffix;
|
||||
const formData = new FormData();
|
||||
formData.append("redirect", asyncData.redirect);
|
||||
formData.append("max_file_size", asyncData.max_file_size.toString());
|
||||
formData.append("max_file_count", asyncData.max_file_count.toString());
|
||||
formData.append("expires", asyncData.expires.toString());
|
||||
formData.append("signature", asyncData.signature);
|
||||
formData.append(filename, new Blob([uploadFile]), suffix);
|
||||
|
||||
const response = await window.fetch(asyncData.url, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const response = await window.fetch(asyncData.url, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Error while sending file to store", response);
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
if (!response.ok) {
|
||||
console.error("Error while sending file to store", response);
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return Promise.resolve(filename);
|
||||
return Promise.resolve(filename);
|
||||
};
|
||||
|
||||
export const encryptFile = async (
|
||||
originalFile: ArrayBuffer,
|
||||
originalFile: ArrayBuffer,
|
||||
): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||
const key = await window.crypto.subtle.generateKey(keyDefinition, true, [
|
||||
"encrypt",
|
||||
"decrypt",
|
||||
]);
|
||||
const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{ name: algo, iv: iv },
|
||||
key,
|
||||
originalFile,
|
||||
);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||
const key = await window.crypto.subtle.generateKey(keyDefinition, true, [
|
||||
"encrypt",
|
||||
"decrypt",
|
||||
]);
|
||||
const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{ name: algo, iv: iv },
|
||||
key,
|
||||
originalFile,
|
||||
);
|
||||
|
||||
return Promise.resolve([encrypted, iv, exportedKey]);
|
||||
return Promise.resolve([encrypted, iv, exportedKey]);
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@ import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
|
||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||
|
||||
export function fetch_generic_docs_by_accompanying_period(
|
||||
periodId: number,
|
||||
periodId: number,
|
||||
): Promise<GenericDocForAccompanyingPeriod[]> {
|
||||
return fetchResults(
|
||||
`/api/1.0/doc-store/generic-doc/by-period/${periodId}/index`,
|
||||
);
|
||||
return fetchResults(
|
||||
`/api/1.0/doc-store/generic-doc/by-period/${periodId}/index`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,117 +6,116 @@ import { _createI18n } from "../../../../../ChillMainBundle/Resources/public/vue
|
||||
const i18n = _createI18n({});
|
||||
|
||||
const startApp = (
|
||||
divElement: HTMLDivElement,
|
||||
collectionEntry: null | HTMLLIElement,
|
||||
divElement: HTMLDivElement,
|
||||
collectionEntry: null | HTMLLIElement,
|
||||
): void => {
|
||||
console.log("app started", divElement);
|
||||
console.log("app started", divElement);
|
||||
|
||||
const inputTitle = collectionEntry?.querySelector("input[type='text']");
|
||||
const inputTitle = collectionEntry?.querySelector("input[type='text']");
|
||||
|
||||
const input_stored_object: HTMLInputElement | null =
|
||||
divElement.querySelector("input[data-stored-object]");
|
||||
if (null === input_stored_object) {
|
||||
throw new Error("input to stored object not found");
|
||||
}
|
||||
const input_stored_object: HTMLInputElement | null = divElement.querySelector(
|
||||
"input[data-stored-object]",
|
||||
);
|
||||
if (null === input_stored_object) {
|
||||
throw new Error("input to stored object not found");
|
||||
}
|
||||
|
||||
let existingDoc: StoredObject | null = null;
|
||||
if (input_stored_object.value !== "") {
|
||||
existingDoc = JSON.parse(input_stored_object.value);
|
||||
}
|
||||
const app_container = document.createElement("div");
|
||||
divElement.appendChild(app_container);
|
||||
let existingDoc: StoredObject | null = null;
|
||||
if (input_stored_object.value !== "") {
|
||||
existingDoc = JSON.parse(input_stored_object.value);
|
||||
}
|
||||
const app_container = document.createElement("div");
|
||||
divElement.appendChild(app_container);
|
||||
|
||||
const app = createApp({
|
||||
template:
|
||||
'<drop-file-widget :existingDoc="this.$data.existingDoc" :allowRemove="true" @addDocument="this.addDocument" @removeDocument="removeDocument"></drop-file-widget>',
|
||||
data() {
|
||||
return {
|
||||
existingDoc: existingDoc,
|
||||
inputTitle: inputTitle,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
DropFileWidget,
|
||||
},
|
||||
methods: {
|
||||
addDocument: function ({
|
||||
stored_object,
|
||||
stored_object_version,
|
||||
file_name,
|
||||
}: {
|
||||
stored_object: StoredObject;
|
||||
stored_object_version: StoredObjectVersion;
|
||||
file_name: string;
|
||||
}): void {
|
||||
stored_object.title = file_name;
|
||||
console.log("object added", stored_object);
|
||||
console.log("version added", stored_object_version);
|
||||
this.$data.existingDoc = stored_object;
|
||||
this.$data.existingDoc.currentVersion = stored_object_version;
|
||||
input_stored_object.value = JSON.stringify(
|
||||
this.$data.existingDoc,
|
||||
);
|
||||
if (this.$data.inputTitle) {
|
||||
if (!this.$data.inputTitle?.value) {
|
||||
this.$data.inputTitle.value = file_name;
|
||||
}
|
||||
}
|
||||
},
|
||||
removeDocument: function (object: StoredObject): void {
|
||||
console.log("catch remove document", object);
|
||||
input_stored_object.value = "";
|
||||
this.$data.existingDoc = undefined;
|
||||
console.log("collectionEntry", collectionEntry);
|
||||
const app = createApp({
|
||||
template:
|
||||
'<drop-file-widget :existingDoc="this.$data.existingDoc" :allowRemove="true" @addDocument="this.addDocument" @removeDocument="removeDocument"></drop-file-widget>',
|
||||
data() {
|
||||
return {
|
||||
existingDoc: existingDoc,
|
||||
inputTitle: inputTitle,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
DropFileWidget,
|
||||
},
|
||||
methods: {
|
||||
addDocument: function ({
|
||||
stored_object,
|
||||
stored_object_version,
|
||||
file_name,
|
||||
}: {
|
||||
stored_object: StoredObject;
|
||||
stored_object_version: StoredObjectVersion;
|
||||
file_name: string;
|
||||
}): void {
|
||||
stored_object.title = file_name;
|
||||
console.log("object added", stored_object);
|
||||
console.log("version added", stored_object_version);
|
||||
this.$data.existingDoc = stored_object;
|
||||
this.$data.existingDoc.currentVersion = stored_object_version;
|
||||
input_stored_object.value = JSON.stringify(this.$data.existingDoc);
|
||||
if (this.$data.inputTitle) {
|
||||
if (!this.$data.inputTitle?.value) {
|
||||
this.$data.inputTitle.value = file_name;
|
||||
}
|
||||
}
|
||||
},
|
||||
removeDocument: function (object: StoredObject): void {
|
||||
console.log("catch remove document", object);
|
||||
input_stored_object.value = "";
|
||||
this.$data.existingDoc = undefined;
|
||||
console.log("collectionEntry", collectionEntry);
|
||||
|
||||
if (null !== collectionEntry) {
|
||||
console.log("will remove collection");
|
||||
collectionEntry.remove();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
if (null !== collectionEntry) {
|
||||
console.log("will remove collection");
|
||||
collectionEntry.remove();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app.use(i18n).mount(app_container);
|
||||
app.use(i18n).mount(app_container);
|
||||
};
|
||||
window.addEventListener("collection-add-entry", ((
|
||||
e: CustomEvent<CollectionEventPayload>,
|
||||
e: CustomEvent<CollectionEventPayload>,
|
||||
) => {
|
||||
const detail = e.detail;
|
||||
const divElement: null | HTMLDivElement = detail.entry.querySelector(
|
||||
"div[data-stored-object]",
|
||||
);
|
||||
const detail = e.detail;
|
||||
const divElement: null | HTMLDivElement = detail.entry.querySelector(
|
||||
"div[data-stored-object]",
|
||||
);
|
||||
|
||||
if (null === divElement) {
|
||||
throw new Error("div[data-stored-object] not found");
|
||||
}
|
||||
if (null === divElement) {
|
||||
throw new Error("div[data-stored-object] not found");
|
||||
}
|
||||
|
||||
startApp(divElement, detail.entry);
|
||||
startApp(divElement, detail.entry);
|
||||
}) as EventListener);
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const upload_inputs: NodeListOf<HTMLDivElement> = document.querySelectorAll(
|
||||
"div[data-stored-object]",
|
||||
);
|
||||
const upload_inputs: NodeListOf<HTMLDivElement> = document.querySelectorAll(
|
||||
"div[data-stored-object]",
|
||||
);
|
||||
|
||||
upload_inputs.forEach((input: HTMLDivElement): void => {
|
||||
// test for a parent to check if this is a collection entry
|
||||
let collectionEntry: null | HTMLLIElement = null;
|
||||
const parent = input.parentElement;
|
||||
console.log("parent", parent);
|
||||
if (null !== parent) {
|
||||
const grandParent = parent.parentElement;
|
||||
console.log("grandParent", grandParent);
|
||||
if (null !== grandParent) {
|
||||
if (
|
||||
grandParent.tagName.toLowerCase() === "li" &&
|
||||
grandParent.classList.contains("entry")
|
||||
) {
|
||||
collectionEntry = grandParent as HTMLLIElement;
|
||||
}
|
||||
}
|
||||
upload_inputs.forEach((input: HTMLDivElement): void => {
|
||||
// test for a parent to check if this is a collection entry
|
||||
let collectionEntry: null | HTMLLIElement = null;
|
||||
const parent = input.parentElement;
|
||||
console.log("parent", parent);
|
||||
if (null !== parent) {
|
||||
const grandParent = parent.parentElement;
|
||||
console.log("grandParent", grandParent);
|
||||
if (null !== grandParent) {
|
||||
if (
|
||||
grandParent.tagName.toLowerCase() === "li" &&
|
||||
grandParent.classList.contains("entry")
|
||||
) {
|
||||
collectionEntry = grandParent as HTMLLIElement;
|
||||
}
|
||||
startApp(input, collectionEntry);
|
||||
});
|
||||
}
|
||||
}
|
||||
startApp(input, collectionEntry);
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
|
||||
@@ -9,26 +9,26 @@ import ToastPlugin from "vue-toast-notification";
|
||||
const i18n = _createI18n({});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function (e) {
|
||||
document
|
||||
.querySelectorAll<HTMLDivElement>("div[data-download-button-single]")
|
||||
.forEach((el) => {
|
||||
const storedObject = JSON.parse(
|
||||
el.dataset.storedObject as string,
|
||||
) as StoredObject;
|
||||
const title = el.dataset.title as string;
|
||||
const app = createApp({
|
||||
components: { DownloadButton },
|
||||
data() {
|
||||
return {
|
||||
storedObject,
|
||||
title,
|
||||
classes: { btn: true, "btn-outline-primary": true },
|
||||
};
|
||||
},
|
||||
template:
|
||||
'<download-button :stored-object="storedObject" :at-version="storedObject.currentVersion" :classes="classes" :filename="title" :direct-download="true"></download-button>',
|
||||
});
|
||||
document
|
||||
.querySelectorAll<HTMLDivElement>("div[data-download-button-single]")
|
||||
.forEach((el) => {
|
||||
const storedObject = JSON.parse(
|
||||
el.dataset.storedObject as string,
|
||||
) as StoredObject;
|
||||
const title = el.dataset.title as string;
|
||||
const app = createApp({
|
||||
components: { DownloadButton },
|
||||
data() {
|
||||
return {
|
||||
storedObject,
|
||||
title,
|
||||
classes: { btn: true, "btn-outline-primary": true },
|
||||
};
|
||||
},
|
||||
template:
|
||||
'<download-button :stored-object="storedObject" :at-version="storedObject.currentVersion" :classes="classes" :filename="title" :direct-download="true"></download-button>',
|
||||
});
|
||||
|
||||
app.use(i18n).use(ToastPlugin).mount(el);
|
||||
});
|
||||
app.use(i18n).use(ToastPlugin).mount(el);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,66 +8,66 @@ import ToastPlugin from "vue-toast-notification";
|
||||
const i18n = _createI18n({});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function (e) {
|
||||
document
|
||||
.querySelectorAll<HTMLDivElement>("div[data-download-buttons]")
|
||||
.forEach((el) => {
|
||||
const app = createApp({
|
||||
components: { DocumentActionButtonsGroup },
|
||||
data() {
|
||||
const datasets = el.dataset as {
|
||||
filename: string;
|
||||
canEdit: string;
|
||||
storedObject: string;
|
||||
buttonSmall: string;
|
||||
davLink: string;
|
||||
davLinkExpiration: string;
|
||||
};
|
||||
document
|
||||
.querySelectorAll<HTMLDivElement>("div[data-download-buttons]")
|
||||
.forEach((el) => {
|
||||
const app = createApp({
|
||||
components: { DocumentActionButtonsGroup },
|
||||
data() {
|
||||
const datasets = el.dataset as {
|
||||
filename: string;
|
||||
canEdit: string;
|
||||
storedObject: string;
|
||||
buttonSmall: string;
|
||||
davLink: string;
|
||||
davLinkExpiration: string;
|
||||
};
|
||||
|
||||
const storedObject = JSON.parse(
|
||||
datasets.storedObject,
|
||||
) as StoredObject,
|
||||
filename = datasets.filename,
|
||||
canEdit = datasets.canEdit === "1",
|
||||
small = datasets.buttonSmall === "1",
|
||||
davLink =
|
||||
"davLink" in datasets && datasets.davLink !== ""
|
||||
? datasets.davLink
|
||||
: null,
|
||||
davLinkExpiration =
|
||||
"davLinkExpiration" in datasets
|
||||
? Number.parseInt(datasets.davLinkExpiration)
|
||||
: null;
|
||||
return {
|
||||
storedObject,
|
||||
filename,
|
||||
canEdit,
|
||||
small,
|
||||
davLink,
|
||||
davLinkExpiration,
|
||||
};
|
||||
},
|
||||
template:
|
||||
'<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" :dav-link="davLink" :dav-link-expiration="davLinkExpiration" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>',
|
||||
methods: {
|
||||
onStoredObjectStatusChange: function (
|
||||
newStatus: StoredObjectStatusChange,
|
||||
): void {
|
||||
this.$data.storedObject.status = newStatus.status;
|
||||
this.$data.storedObject.filename = newStatus.filename;
|
||||
this.$data.storedObject.type = newStatus.type;
|
||||
const storedObject = JSON.parse(
|
||||
datasets.storedObject,
|
||||
) as StoredObject,
|
||||
filename = datasets.filename,
|
||||
canEdit = datasets.canEdit === "1",
|
||||
small = datasets.buttonSmall === "1",
|
||||
davLink =
|
||||
"davLink" in datasets && datasets.davLink !== ""
|
||||
? datasets.davLink
|
||||
: null,
|
||||
davLinkExpiration =
|
||||
"davLinkExpiration" in datasets
|
||||
? Number.parseInt(datasets.davLinkExpiration)
|
||||
: null;
|
||||
return {
|
||||
storedObject,
|
||||
filename,
|
||||
canEdit,
|
||||
small,
|
||||
davLink,
|
||||
davLinkExpiration,
|
||||
};
|
||||
},
|
||||
template:
|
||||
'<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" :dav-link="davLink" :dav-link-expiration="davLinkExpiration" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>',
|
||||
methods: {
|
||||
onStoredObjectStatusChange: function (
|
||||
newStatus: StoredObjectStatusChange,
|
||||
): void {
|
||||
this.$data.storedObject.status = newStatus.status;
|
||||
this.$data.storedObject.filename = newStatus.filename;
|
||||
this.$data.storedObject.type = newStatus.type;
|
||||
|
||||
// remove eventual div which inform pending status
|
||||
document
|
||||
.querySelectorAll(
|
||||
`[data-docgen-is-pending="${this.$data.storedObject.id}"]`,
|
||||
)
|
||||
.forEach(function (el) {
|
||||
el.remove();
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
// remove eventual div which inform pending status
|
||||
document
|
||||
.querySelectorAll(
|
||||
`[data-docgen-is-pending="${this.$data.storedObject.id}"]`,
|
||||
)
|
||||
.forEach(function (el) {
|
||||
el.remove();
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app.use(i18n).use(ToastPlugin).mount(el);
|
||||
});
|
||||
app.use(i18n).use(ToastPlugin).mount(el);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,71 +4,73 @@ import { SignedUrlGet } from "ChillDocStoreAssets/vuejs/StoredObjectButton/helpe
|
||||
export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending";
|
||||
|
||||
export interface StoredObject {
|
||||
id: number;
|
||||
title: string | null;
|
||||
uuid: string;
|
||||
prefix: string;
|
||||
status: StoredObjectStatus;
|
||||
currentVersion:
|
||||
| null
|
||||
| StoredObjectVersionCreated
|
||||
| StoredObjectVersionPersisted;
|
||||
totalVersions: number;
|
||||
datas: object;
|
||||
/** @deprecated */
|
||||
creationDate: DateTime;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
_permissions: {
|
||||
canEdit: boolean;
|
||||
canSee: boolean;
|
||||
};
|
||||
_links?: {
|
||||
dav_link?: {
|
||||
href: string;
|
||||
expiration: number;
|
||||
};
|
||||
downloadLink?: SignedUrlGet;
|
||||
id: number;
|
||||
title: string | null;
|
||||
uuid: string;
|
||||
prefix: string;
|
||||
status: StoredObjectStatus;
|
||||
currentVersion:
|
||||
| null
|
||||
| StoredObjectVersionCreated
|
||||
| StoredObjectVersionPersisted;
|
||||
totalVersions: number;
|
||||
datas: object;
|
||||
/** @deprecated */
|
||||
creationDate: DateTime;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
_permissions: {
|
||||
canEdit: boolean;
|
||||
canSee: boolean;
|
||||
};
|
||||
_links?: {
|
||||
dav_link?: {
|
||||
href: string;
|
||||
expiration: number;
|
||||
};
|
||||
downloadLink?: SignedUrlGet;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StoredObjectVersion {
|
||||
/**
|
||||
* filename of the object in the object storage
|
||||
*/
|
||||
filename: string;
|
||||
iv: number[];
|
||||
keyInfos: JsonWebKey;
|
||||
type: string;
|
||||
/**
|
||||
* filename of the object in the object storage
|
||||
*/
|
||||
filename: string;
|
||||
iv: number[];
|
||||
keyInfos: JsonWebKey;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface StoredObjectVersionCreated extends StoredObjectVersion {
|
||||
persisted: false;
|
||||
persisted: false;
|
||||
}
|
||||
|
||||
export interface StoredObjectVersionPersisted extends StoredObjectVersionCreated {
|
||||
version: number;
|
||||
id: number;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
export interface StoredObjectVersionPersisted
|
||||
extends StoredObjectVersionCreated {
|
||||
version: number;
|
||||
id: number;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
}
|
||||
|
||||
export interface StoredObjectStatusChange {
|
||||
id: number;
|
||||
filename: string;
|
||||
status: StoredObjectStatus;
|
||||
type: string;
|
||||
id: number;
|
||||
filename: string;
|
||||
status: StoredObjectStatus;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface StoredObjectVersionWithPointInTime extends StoredObjectVersionPersisted {
|
||||
"point-in-times": StoredObjectPointInTime[];
|
||||
"from-restored": StoredObjectVersionPersisted | null;
|
||||
export interface StoredObjectVersionWithPointInTime
|
||||
extends StoredObjectVersionPersisted {
|
||||
"point-in-times": StoredObjectPointInTime[];
|
||||
"from-restored": StoredObjectVersionPersisted | null;
|
||||
}
|
||||
|
||||
export interface StoredObjectPointInTime {
|
||||
id: number;
|
||||
byUser: User | null;
|
||||
reason: "keep-before-conversion" | "keep-by-user";
|
||||
id: number;
|
||||
byUser: User | null;
|
||||
reason: "keep-before-conversion" | "keep-by-user";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,63 +82,63 @@ export type WopiEditButtonExecutableBeforeLeaveFunction = () => Promise<void>;
|
||||
* Object containing information for performering a POST request to a swift object store
|
||||
*/
|
||||
export interface PostStoreObjectSignature {
|
||||
method: "POST";
|
||||
max_file_size: number;
|
||||
max_file_count: 1;
|
||||
expires: number;
|
||||
submit_delay: 180;
|
||||
redirect: string;
|
||||
prefix: string;
|
||||
url: string;
|
||||
signature: string;
|
||||
method: "POST";
|
||||
max_file_size: number;
|
||||
max_file_count: 1;
|
||||
expires: number;
|
||||
submit_delay: 180;
|
||||
redirect: string;
|
||||
prefix: string;
|
||||
url: string;
|
||||
signature: string;
|
||||
}
|
||||
|
||||
export interface PDFPage {
|
||||
index: number;
|
||||
width: number;
|
||||
height: number;
|
||||
index: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
export interface SignatureZone {
|
||||
index: number | null;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
PDFPage: PDFPage;
|
||||
index: number | null;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
PDFPage: PDFPage;
|
||||
}
|
||||
|
||||
export interface Signature {
|
||||
id: number;
|
||||
storedObject: StoredObject;
|
||||
zones: SignatureZone[];
|
||||
id: number;
|
||||
storedObject: StoredObject;
|
||||
zones: SignatureZone[];
|
||||
}
|
||||
|
||||
export type SignedState =
|
||||
| "pending"
|
||||
| "signed"
|
||||
| "rejected"
|
||||
| "canceled"
|
||||
| "error";
|
||||
| "pending"
|
||||
| "signed"
|
||||
| "rejected"
|
||||
| "canceled"
|
||||
| "error";
|
||||
|
||||
export interface CheckSignature {
|
||||
state: SignedState;
|
||||
storedObject: StoredObject;
|
||||
state: SignedState;
|
||||
storedObject: StoredObject;
|
||||
}
|
||||
|
||||
export type CanvasEvent = "select" | "add";
|
||||
|
||||
export interface ZoomLevel {
|
||||
id: number;
|
||||
zoom: number;
|
||||
label: {
|
||||
fr?: string;
|
||||
nl?: string;
|
||||
};
|
||||
id: number;
|
||||
zoom: number;
|
||||
label: {
|
||||
fr?: string;
|
||||
nl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GenericDoc {
|
||||
type: "doc_store_generic_doc";
|
||||
key: string;
|
||||
context: "person" | "accompanying-period";
|
||||
doc_date: DateTime;
|
||||
type: "doc_store_generic_doc";
|
||||
key: string;
|
||||
context: "person" | "accompanying-period";
|
||||
doc_date: DateTime;
|
||||
}
|
||||
|
||||
@@ -1,67 +1,65 @@
|
||||
<template>
|
||||
<div v-if="isButtonGroupDisplayable" class="btn-group">
|
||||
<button
|
||||
:class="
|
||||
Object.assign({
|
||||
btn: true,
|
||||
'btn-outline-primary': true,
|
||||
'dropdown-toggle': true,
|
||||
'btn-sm': props.small,
|
||||
})
|
||||
"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li v-if="isEditableOnline">
|
||||
<wopi-edit-button
|
||||
:stored-object="props.storedObject"
|
||||
:classes="{ 'dropdown-item': true }"
|
||||
:execute-before-leave="props.executeBeforeLeave"
|
||||
></wopi-edit-button>
|
||||
</li>
|
||||
<li v-if="isEditableOnDesktop">
|
||||
<desktop-edit-button
|
||||
:classes="{ 'dropdown-item': true }"
|
||||
:edit-link="props.davLink"
|
||||
:expiration-link="props.davLinkExpiration"
|
||||
></desktop-edit-button>
|
||||
</li>
|
||||
<li v-if="isConvertibleToPdf">
|
||||
<convert-button
|
||||
:stored-object="props.storedObject"
|
||||
:filename="filename"
|
||||
:classes="{ 'dropdown-item': true }"
|
||||
></convert-button>
|
||||
</li>
|
||||
<li v-if="isDownloadable">
|
||||
<download-button
|
||||
:stored-object="props.storedObject"
|
||||
:at-version="props.storedObject.currentVersion"
|
||||
:filename="filename"
|
||||
:classes="{ 'dropdown-item': true }"
|
||||
:display-action-string-in-button="true"
|
||||
></download-button>
|
||||
</li>
|
||||
<li v-if="isHistoryViewable">
|
||||
<history-button
|
||||
:stored-object="props.storedObject"
|
||||
:can-edit="
|
||||
canEdit && props.storedObject._permissions.canEdit
|
||||
"
|
||||
></history-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else-if="'pending' === props.storedObject.status">
|
||||
<div class="btn btn-outline-info">Génération en cours</div>
|
||||
</div>
|
||||
<div v-else-if="'failure' === props.storedObject.status">
|
||||
<div class="btn btn-outline-danger">La génération a échoué</div>
|
||||
</div>
|
||||
<div v-if="isButtonGroupDisplayable" class="btn-group">
|
||||
<button
|
||||
:class="
|
||||
Object.assign({
|
||||
btn: true,
|
||||
'btn-outline-primary': true,
|
||||
'dropdown-toggle': true,
|
||||
'btn-sm': props.small,
|
||||
})
|
||||
"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li v-if="isEditableOnline">
|
||||
<wopi-edit-button
|
||||
:stored-object="props.storedObject"
|
||||
:classes="{ 'dropdown-item': true }"
|
||||
:execute-before-leave="props.executeBeforeLeave"
|
||||
></wopi-edit-button>
|
||||
</li>
|
||||
<li v-if="isEditableOnDesktop">
|
||||
<desktop-edit-button
|
||||
:classes="{ 'dropdown-item': true }"
|
||||
:edit-link="props.davLink"
|
||||
:expiration-link="props.davLinkExpiration"
|
||||
></desktop-edit-button>
|
||||
</li>
|
||||
<li v-if="isConvertibleToPdf">
|
||||
<convert-button
|
||||
:stored-object="props.storedObject"
|
||||
:filename="filename"
|
||||
:classes="{ 'dropdown-item': true }"
|
||||
></convert-button>
|
||||
</li>
|
||||
<li v-if="isDownloadable">
|
||||
<download-button
|
||||
:stored-object="props.storedObject"
|
||||
:at-version="props.storedObject.currentVersion"
|
||||
:filename="filename"
|
||||
:classes="{ 'dropdown-item': true }"
|
||||
:display-action-string-in-button="true"
|
||||
></download-button>
|
||||
</li>
|
||||
<li v-if="isHistoryViewable">
|
||||
<history-button
|
||||
:stored-object="props.storedObject"
|
||||
:can-edit="canEdit && props.storedObject._permissions.canEdit"
|
||||
></history-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else-if="'pending' === props.storedObject.status">
|
||||
<div class="btn btn-outline-info">Génération en cours</div>
|
||||
</div>
|
||||
<div v-else-if="'failure' === props.storedObject.status">
|
||||
<div class="btn btn-outline-danger">La génération a échoué</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -70,68 +68,66 @@ import ConvertButton from "./StoredObjectButton/ConvertButton.vue";
|
||||
import DownloadButton from "./StoredObjectButton/DownloadButton.vue";
|
||||
import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue";
|
||||
import {
|
||||
is_extension_editable,
|
||||
is_extension_viewable,
|
||||
is_object_ready,
|
||||
is_extension_editable,
|
||||
is_extension_viewable,
|
||||
is_object_ready,
|
||||
} from "./StoredObjectButton/helpers";
|
||||
import {
|
||||
StoredObject,
|
||||
StoredObjectStatusChange,
|
||||
StoredObjectVersion,
|
||||
WopiEditButtonExecutableBeforeLeaveFunction,
|
||||
StoredObject,
|
||||
StoredObjectStatusChange,
|
||||
StoredObjectVersion,
|
||||
WopiEditButtonExecutableBeforeLeaveFunction,
|
||||
} from "../types";
|
||||
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
|
||||
import HistoryButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton.vue";
|
||||
|
||||
interface DocumentActionButtonsGroupConfig {
|
||||
storedObject: StoredObject;
|
||||
small?: boolean;
|
||||
canEdit?: boolean;
|
||||
canDownload?: boolean;
|
||||
canConvertPdf?: boolean;
|
||||
returnPath?: string;
|
||||
storedObject: StoredObject;
|
||||
small?: boolean;
|
||||
canEdit?: boolean;
|
||||
canDownload?: boolean;
|
||||
canConvertPdf?: boolean;
|
||||
returnPath?: string;
|
||||
|
||||
/**
|
||||
* Will be the filename displayed to the user when he·she download the document
|
||||
* (the document will be saved on his disk with this name)
|
||||
*
|
||||
* If not set, 'document' will be used.
|
||||
*/
|
||||
filename?: string;
|
||||
/**
|
||||
* Will be the filename displayed to the user when he·she download the document
|
||||
* (the document will be saved on his disk with this name)
|
||||
*
|
||||
* If not set, 'document' will be used.
|
||||
*/
|
||||
filename?: string;
|
||||
|
||||
/**
|
||||
* If set, will execute this function before leaving to the editor
|
||||
*/
|
||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction;
|
||||
/**
|
||||
* If set, will execute this function before leaving to the editor
|
||||
*/
|
||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction;
|
||||
|
||||
/**
|
||||
* a link to download and edit file using webdav
|
||||
*/
|
||||
davLink?: string;
|
||||
/**
|
||||
* a link to download and edit file using webdav
|
||||
*/
|
||||
davLink?: string;
|
||||
|
||||
/**
|
||||
* the expiration date of the download, as a unix timestamp
|
||||
*/
|
||||
davLinkExpiration?: number;
|
||||
/**
|
||||
* the expiration date of the download, as a unix timestamp
|
||||
*/
|
||||
davLinkExpiration?: number;
|
||||
}
|
||||
|
||||
const emit =
|
||||
defineEmits<
|
||||
(
|
||||
e: "onStoredObjectStatusChange",
|
||||
newStatus: StoredObjectStatusChange,
|
||||
) => void
|
||||
>();
|
||||
defineEmits<
|
||||
(
|
||||
e: "onStoredObjectStatusChange",
|
||||
newStatus: StoredObjectStatusChange,
|
||||
) => void
|
||||
>();
|
||||
|
||||
const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
|
||||
small: false,
|
||||
canEdit: true,
|
||||
canDownload: true,
|
||||
canConvertPdf: true,
|
||||
returnPath:
|
||||
window.location.pathname +
|
||||
window.location.search +
|
||||
window.location.hash,
|
||||
small: false,
|
||||
canEdit: true,
|
||||
canDownload: true,
|
||||
canConvertPdf: true,
|
||||
returnPath:
|
||||
window.location.pathname + window.location.search + window.location.hash,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -145,93 +141,93 @@ let tryiesForReady = 0;
|
||||
const maxTryiesForReady = 120;
|
||||
|
||||
const isButtonGroupDisplayable = computed<boolean>(() => {
|
||||
return (
|
||||
isDownloadable.value ||
|
||||
isEditableOnline.value ||
|
||||
isEditableOnDesktop.value ||
|
||||
isConvertibleToPdf.value
|
||||
);
|
||||
return (
|
||||
isDownloadable.value ||
|
||||
isEditableOnline.value ||
|
||||
isEditableOnDesktop.value ||
|
||||
isConvertibleToPdf.value
|
||||
);
|
||||
});
|
||||
|
||||
const isDownloadable = computed<boolean>(() => {
|
||||
return (
|
||||
props.storedObject.status === "ready" ||
|
||||
// happens when the stored object version is just added, but not persisted
|
||||
(props.storedObject.currentVersion !== null &&
|
||||
props.storedObject.status === "empty")
|
||||
);
|
||||
return (
|
||||
props.storedObject.status === "ready" ||
|
||||
// happens when the stored object version is just added, but not persisted
|
||||
(props.storedObject.currentVersion !== null &&
|
||||
props.storedObject.status === "empty")
|
||||
);
|
||||
});
|
||||
|
||||
const isEditableOnline = computed<boolean>(() => {
|
||||
return (
|
||||
props.storedObject.status === "ready" &&
|
||||
props.storedObject._permissions.canEdit &&
|
||||
props.canEdit &&
|
||||
props.storedObject.currentVersion !== null &&
|
||||
is_extension_editable(props.storedObject.currentVersion.type) &&
|
||||
props.storedObject.currentVersion.persisted !== false
|
||||
);
|
||||
return (
|
||||
props.storedObject.status === "ready" &&
|
||||
props.storedObject._permissions.canEdit &&
|
||||
props.canEdit &&
|
||||
props.storedObject.currentVersion !== null &&
|
||||
is_extension_editable(props.storedObject.currentVersion.type) &&
|
||||
props.storedObject.currentVersion.persisted !== false
|
||||
);
|
||||
});
|
||||
|
||||
const isEditableOnDesktop = computed<boolean>(() => {
|
||||
return isEditableOnline.value;
|
||||
return isEditableOnline.value;
|
||||
});
|
||||
|
||||
const isConvertibleToPdf = computed<boolean>(() => {
|
||||
return (
|
||||
props.storedObject.status === "ready" &&
|
||||
props.storedObject._permissions.canSee &&
|
||||
props.canConvertPdf &&
|
||||
props.storedObject.currentVersion !== null &&
|
||||
is_extension_viewable(props.storedObject.currentVersion.type) &&
|
||||
props.storedObject.currentVersion.type !== "application/pdf" &&
|
||||
props.storedObject.currentVersion.persisted !== false
|
||||
);
|
||||
return (
|
||||
props.storedObject.status === "ready" &&
|
||||
props.storedObject._permissions.canSee &&
|
||||
props.canConvertPdf &&
|
||||
props.storedObject.currentVersion !== null &&
|
||||
is_extension_viewable(props.storedObject.currentVersion.type) &&
|
||||
props.storedObject.currentVersion.type !== "application/pdf" &&
|
||||
props.storedObject.currentVersion.persisted !== false
|
||||
);
|
||||
});
|
||||
|
||||
const isHistoryViewable = computed<boolean>(() => {
|
||||
return props.storedObject.status === "ready";
|
||||
return props.storedObject.status === "ready";
|
||||
});
|
||||
|
||||
const checkForReady = function (): void {
|
||||
if (
|
||||
"ready" === props.storedObject.status ||
|
||||
"empty" === props.storedObject.status ||
|
||||
"failure" === props.storedObject.status ||
|
||||
// stop reloading if the page stays opened for a long time
|
||||
tryiesForReady > maxTryiesForReady
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
"ready" === props.storedObject.status ||
|
||||
"empty" === props.storedObject.status ||
|
||||
"failure" === props.storedObject.status ||
|
||||
// stop reloading if the page stays opened for a long time
|
||||
tryiesForReady > maxTryiesForReady
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
tryiesForReady = tryiesForReady + 1;
|
||||
tryiesForReady = tryiesForReady + 1;
|
||||
|
||||
setTimeout(onObjectNewStatusCallback, 5000);
|
||||
setTimeout(onObjectNewStatusCallback, 5000);
|
||||
};
|
||||
|
||||
const onObjectNewStatusCallback = async function (): Promise<void> {
|
||||
if (props.storedObject.status === "stored_object_created") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const new_status = await is_object_ready(props.storedObject);
|
||||
if (props.storedObject.status !== new_status.status) {
|
||||
emit("onStoredObjectStatusChange", new_status);
|
||||
return Promise.resolve();
|
||||
} else if ("failure" === new_status.status) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if ("ready" !== new_status.status) {
|
||||
// we check for new status, unless it is ready
|
||||
checkForReady();
|
||||
}
|
||||
|
||||
if (props.storedObject.status === "stored_object_created") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const new_status = await is_object_ready(props.storedObject);
|
||||
if (props.storedObject.status !== new_status.status) {
|
||||
emit("onStoredObjectStatusChange", new_status);
|
||||
return Promise.resolve();
|
||||
} else if ("failure" === new_status.status) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if ("ready" !== new_status.status) {
|
||||
// we check for new status, unless it is ready
|
||||
checkForReady();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkForReady();
|
||||
checkForReady();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user