mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-10-03 20:09:42 +00:00
Compare commits
273 Commits
v4.5.0
...
ticket/64-
Author | SHA1 | Date | |
---|---|---|---|
e566f60a4a
|
|||
c06531cddb
|
|||
61ca700bbe | |||
|
b43aeebc3c | ||
056e2dcc5f | |||
e57d1ac696 | |||
4a1da25fee
|
|||
0eff1d2e79 | |||
3928b2cc7a | |||
02783e5391
|
|||
4f51ef81ad
|
|||
4637dc692c
|
|||
be3b9f0f56
|
|||
ee006f55d6
|
|||
13b1c45271
|
|||
ad2b6d63ac
|
|||
bfbde078b7
|
|||
d42a1296c4
|
|||
4b7e3c1601
|
|||
6ea9af588b
|
|||
0fd76d3fa8
|
|||
34af53130b
|
|||
a1fd395868
|
|||
b8a7cbb321
|
|||
6124eb9e34
|
|||
a5b06de92a
|
|||
52404956d2
|
|||
4207efd6bf
|
|||
840fde4ad4
|
|||
3611ea2518
|
|||
bbd4292cb9
|
|||
54f8c92240
|
|||
5330befc8f
|
|||
c19206be0c
|
|||
5ff374d2fa
|
|||
4a73aaae94
|
|||
ff2c567d05
|
|||
a734e84f28
|
|||
4367ed086e
|
|||
3227bfcd3a
|
|||
8d29fb260a
|
|||
bda0743c63
|
|||
d9b730627f
|
|||
27548ad654
|
|||
bec7297039
|
|||
852523e644
|
|||
c05d0aad47
|
|||
1c0ed9abc8
|
|||
9aed5cc216
|
|||
e4fe5bff68
|
|||
4c73c4d9d0
|
|||
38935edb93 | |||
|
e1ef65d4ca | ||
ec9d0be70b | |||
|
0ba2cbc1e8 | ||
e87429933a | |||
8e2e676e3d
|
|||
e12ad563a3 | |||
|
711aa8db9b | ||
e78d44953f | |||
18f67801c7 | |||
c815e6bc69
|
|||
807f2711fe | |||
|
cd594cd580 | ||
fb6b26bfb5
|
|||
c5cedb8bd6
|
|||
2665e43a61
|
|||
25561cdf63
|
|||
10b73e06e1 | |||
|
e7c04e34a9 | ||
164beee7c6 | |||
|
4d96eb9457 | ||
fe2eba3b29 | |||
|
61d1232e31 | ||
6594d4f6a6 | |||
1a66a9e864 | |||
1b74c119dc | |||
14d88810f3 | |||
445a2c9358 | |||
c8baf0a8aa | |||
faed443a96 | |||
bbf387d96f | |||
|
b7c9b60744 | ||
2bd303bbbe
|
|||
c5e6122d2c
|
|||
088b876e20 | |||
3400656d7c | |||
568c8be7fd
|
|||
538ecc42ea
|
|||
15d26d4b06
|
|||
d8bd9bd7cd
|
|||
dcdfba5ccd
|
|||
0204bdd38d
|
|||
392fd01b56
|
|||
35844f3b73 | |||
7506b918d7 | |||
cfba291f2c | |||
|
04438c09d3 | ||
2a54d1b909 | |||
|
628eeac5e0 | ||
a2263b3fa1 | |||
74796d0fb0 | |||
c19481e40a
|
|||
|
6eeb717b1a | ||
beb7c462da
|
|||
|
dbf363a9e8 | ||
64a2f7c9ed
|
|||
f26d9739c8 | |||
afa5edc1d8
|
|||
42d6c9e672
|
|||
2b22d4cb7c
|
|||
c8e5d0eb37
|
|||
2bf8ad5d6c | |||
11698a52e3 | |||
70955573e8 | |||
|
3df4043eb9 | ||
06e8264dde
|
|||
b451d2c4a3 | |||
4f93150874 | |||
0566ab0910 | |||
|
f4eeee1598 | ||
33cf16fc13 | |||
0a331aab37 | |||
d43b739654 | |||
c72432efae | |||
95975fae55
|
|||
95a7efa138
|
|||
45e193ff6d
|
|||
dfc146ff3f
|
|||
b41fcf66a9 | |||
|
a8dd1b3548 | ||
2b99a480ac
|
|||
7633e587bb
|
|||
fc61dfdf3a
|
|||
f1a5b5c49e
|
|||
ec685dcd47
|
|||
631ae3eedd
|
|||
440a7837ac
|
|||
e0abf34784
|
|||
377ae9a9dc
|
|||
034dc30e30
|
|||
d615111a0f
|
|||
ffb756c712
|
|||
69daccb860
|
|||
16435423cf
|
|||
697b4ab436
|
|||
67d804e28e
|
|||
cf41fa9574
|
|||
b8b325f7d7
|
|||
e97bd8c4ef
|
|||
e28d7df533
|
|||
4b20b1bc01
|
|||
b15733076c
|
|||
25be5c9ea3
|
|||
b035020c6f
|
|||
128101dc46
|
|||
5f2711023e
|
|||
bdf2ed4bbd
|
|||
1df542603e
|
|||
80bcc68ce5
|
|||
154fc3e2f6
|
|||
e45af94c78
|
|||
166a6fde20
|
|||
631f047338
|
|||
a777588bb8
|
|||
ca78d112c2
|
|||
bcfd317d83
|
|||
348740f073
|
|||
0d74f0980f
|
|||
be19dc00db
|
|||
643028ffd6
|
|||
ac4e2e5bf2
|
|||
498572b96e
|
|||
d2a61ce69b
|
|||
a9c0567ee1
|
|||
76cec5b5a8
|
|||
efe8a67697
|
|||
26dfa9b028
|
|||
50025044d3
|
|||
e6202a2e34
|
|||
b863bd967d
|
|||
e65bcf7275
|
|||
e00ece4200
|
|||
640fd71402
|
|||
aae50ca290
|
|||
1fa483598b
|
|||
e4b6a468f8
|
|||
|
66c7758023 | ||
|
4750d2c24e | ||
|
ca05e3d979 | ||
|
a20f9b4f86 | ||
|
c73c1eb8d5 | ||
|
8778bb0731 | ||
|
c7d20eebc5 | ||
|
b9e130c159 | ||
|
3e8bc94af3 | ||
|
0c914c9f9f | ||
|
580a60c939 | ||
|
4996ac3b7c | ||
|
2a23bf19cb | ||
|
650d2596d9 | ||
|
2bdd5a329e | ||
78d1776733
|
|||
66dc603c85
|
|||
3a8154ecce
|
|||
c81828e04f
|
|||
|
ec17dd7de2 | ||
76c076a5f3 | |||
|
f0045edd6c | ||
|
d00b76ffcd | ||
|
8991f0ef3f | ||
|
d6f5eae0c9 | ||
|
821fce3dd8 | ||
|
1d33ae1e39 | ||
|
19af0feb57 | ||
|
1c09e9a692 | ||
|
d72e748388 | ||
|
ab850b7b70 | ||
|
3f9745d8cf | ||
|
473765366a | ||
|
6500c24a7f | ||
|
1d00457141 | ||
|
eb0bf56cff | ||
|
7b8cd90cf1 | ||
|
a27d92aba0 | ||
|
85bdfb9e21 | ||
|
4cffcf4de1 | ||
|
b2587a688f | ||
|
c9f0e9843b | ||
|
b40ad9e445 | ||
|
3e10e47e29 | ||
|
2a1963e993 | ||
34c171659b | |||
2d8b960d9e
|
|||
831ae03431 | |||
45828174d1
|
|||
ed45f14a45
|
|||
fa67835690
|
|||
b434d38091
|
|||
|
800a952532 | ||
9f355032a8
|
|||
0bc6e62d4d
|
|||
46fb1c04b5
|
|||
3b2c3d1464 | |||
|
0bd6038160 | ||
|
baab8e94ce | ||
e2deb55fdb
|
|||
|
2cdfb50058 | ||
39d701feb2
|
|||
613ee8b186
|
|||
56a1a488de
|
|||
3f789ad0f4 | |||
467bea7cde
|
|||
670b8eb82b
|
|||
a9760b323f
|
|||
71a3a1924a
|
|||
ecdc1e25bf
|
|||
dd37427be1
|
|||
c8467df1b1
|
|||
4c89a954fa
|
|||
7c1f3b114d
|
|||
36bc4dab24
|
|||
4b30d92282
|
|||
75fbec5489
|
|||
912fdd6349
|
|||
5832542978
|
|||
5c3585a1ed
|
|||
a2f1e20ddf
|
|||
4d67702a76
|
|||
18e442db29 | |||
|
deb3d92189 | ||
a59ea7db31
|
|||
a738b0cac9
|
6
.changes/unreleased/Feature-20240530-160003.yaml
Normal file
6
.changes/unreleased/Feature-20240530-160003.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: |
|
||||||
|
Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||||
|
time: 2024-05-30T16:00:03.440767606+02:00
|
||||||
|
custom:
|
||||||
|
Issue: ""
|
6
.changes/unreleased/Feature-20240531-190242.yaml
Normal file
6
.changes/unreleased/Feature-20240531-190242.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: |
|
||||||
|
Upgrade CKEditor and refactor configuration with use of typescript
|
||||||
|
time: 2024-05-31T19:02:42.776662753+02:00
|
||||||
|
custom:
|
||||||
|
Issue: ""
|
6
.changes/unreleased/Feature-20250904-181032.yaml
Normal file
6
.changes/unreleased/Feature-20250904-181032.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: Add a command to generate a list of permissions
|
||||||
|
time: 2025-09-04T18:10:32.334524026+02:00
|
||||||
|
custom:
|
||||||
|
Issue: ""
|
||||||
|
SchemaChange: No schema change
|
@@ -1,10 +0,0 @@
|
|||||||
## v4.3.0 - 2025-09-08
|
|
||||||
### Feature
|
|
||||||
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges
|
|
||||||
* Add a command to generate a list of permissions
|
|
||||||
* ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date
|
|
||||||
|
|
||||||
**Schema Change**: Add columns or tables
|
|
||||||
### Fixed
|
|
||||||
* fix date formatting in calendar range display
|
|
||||||
* Change route URL to avoid clash with person duplicate controller method
|
|
@@ -1,8 +0,0 @@
|
|||||||
## v4.4.0 - 2025-09-11
|
|
||||||
### Feature
|
|
||||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
|
|
||||||
* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation
|
|
||||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works
|
|
||||||
### Fixed
|
|
||||||
* Fix display of 'duplicate' and 'merge' buttons in CRUD templates
|
|
||||||
* Fix saving notification preferences in user's profile
|
|
@@ -1,3 +0,0 @@
|
|||||||
## v4.4.1 - 2025-09-11
|
|
||||||
### Fixed
|
|
||||||
* fix translations in duplicate evaluation document modal and realign close modal button
|
|
@@ -1,3 +0,0 @@
|
|||||||
## v4.4.2 - 2025-09-12
|
|
||||||
### Fixed
|
|
||||||
* Fix document generation and workflow generation do not work on accompanying period work documents
|
|
@@ -1,13 +0,0 @@
|
|||||||
## v4.5.0 - 2025-10-03
|
|
||||||
### Feature
|
|
||||||
* Only allow delete of attachment on workflows that are not final
|
|
||||||
* Move up signature buttons on index workflow page for easier access
|
|
||||||
* Filter out document from attachment list if it is the same as the workflow document
|
|
||||||
* Block edition on attached document on workflow, if the workflow is finalized or sent external
|
|
||||||
* Convert workflow's attached document to pdf while sending them external
|
|
||||||
* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition
|
|
||||||
### Fixed
|
|
||||||
* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance
|
|
||||||
* Fix permissions on storedObject which are subject by a workflow
|
|
||||||
### DX
|
|
||||||
* Introduce a WaitingScreen component to display a waiting screen
|
|
@@ -23,7 +23,7 @@ max_line_length = 0
|
|||||||
indent_size = 2
|
indent_size = 2
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
||||||
[.rst]
|
[*.rst]
|
||||||
ident_size = 3
|
indent_size = 3
|
||||||
ident_style = space
|
indent_style = space
|
||||||
|
|
||||||
|
@@ -234,20 +234,16 @@ This must be a decision made by a human, not by an AI. Every AI task must abort
|
|||||||
|
|
||||||
#### Running Tests
|
#### 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
|
```bash
|
||||||
# Run all tests
|
|
||||||
vendor/bin/phpunit
|
|
||||||
|
|
||||||
# Run tests for a specific bundle
|
|
||||||
vendor/bin/phpunit --testsuite NameBundle
|
|
||||||
|
|
||||||
# Run a specific test file
|
# Run a specific test file
|
||||||
vendor/bin/phpunit path/to/TestFile.php
|
symfony composer exec phpunit -- path/to/TestFile.php
|
||||||
|
|
||||||
# Run a specific test method
|
# Run a specific test method
|
||||||
vendor/bin/phpunit --filter methodName path/to/TestFile.php
|
symfony composer exec phpunit -- --filter methodName path/to/TestFile.php
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Test Structure
|
#### Test Structure
|
||||||
|
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
30
.vscode/launch.json
vendored
Normal file
30
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Chill Debug",
|
||||||
|
"type": "php",
|
||||||
|
"request": "launch",
|
||||||
|
"port": 9000,
|
||||||
|
"pathMappings": {
|
||||||
|
"/var/www/html": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"preLaunchTask": "symfony"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Yarn Encore Dev (Watch)",
|
||||||
|
"type": "node-terminal",
|
||||||
|
"request": "launch",
|
||||||
|
"command": "yarn encore dev --watch",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Chill Debug + Yarn Encore Dev (Watch)",
|
||||||
|
"configurations": ["Chill Debug", "Yarn Encore Dev (Watch)"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
23
.vscode/tasks.json
vendored
Normal file
23
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "shell",
|
||||||
|
"command": "symfony",
|
||||||
|
"args": [
|
||||||
|
"server:start",
|
||||||
|
"--allow-http",
|
||||||
|
"--no-tls",
|
||||||
|
"--port=8000",
|
||||||
|
"--allow-all-ip",
|
||||||
|
"-d"
|
||||||
|
],
|
||||||
|
"label": "symfony"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "shell",
|
||||||
|
"command": "yarn",
|
||||||
|
"args": ["encore", "dev", "--watch"],
|
||||||
|
"label": "webpack"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
42
CHANGELOG.md
42
CHANGELOG.md
@@ -6,48 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
|||||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||||
|
|
||||||
|
|
||||||
## v4.5.0 - 2025-10-03
|
|
||||||
### Feature
|
|
||||||
* Only allow delete of attachment on workflows that are not final
|
|
||||||
* Move up signature buttons on index workflow page for easier access
|
|
||||||
* Filter out document from attachment list if it is the same as the workflow document
|
|
||||||
* Block edition on attached document on workflow, if the workflow is finalized or sent external
|
|
||||||
* Convert workflow's attached document to pdf while sending them external
|
|
||||||
* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition
|
|
||||||
### Fixed
|
|
||||||
* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance
|
|
||||||
* Fix permissions on storedObject which are subject by a workflow
|
|
||||||
### DX
|
|
||||||
* Introduce a WaitingScreen component to display a waiting screen
|
|
||||||
|
|
||||||
## v4.4.2 - 2025-09-12
|
|
||||||
### Fixed
|
|
||||||
* Fix document generation and workflow generation do not work on accompanying period work documents
|
|
||||||
|
|
||||||
## v4.4.1 - 2025-09-11
|
|
||||||
### Fixed
|
|
||||||
* fix translations in duplicate evaluation document modal and realign close modal button
|
|
||||||
|
|
||||||
## v4.4.0 - 2025-09-11
|
|
||||||
### Feature
|
|
||||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
|
|
||||||
* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation
|
|
||||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works
|
|
||||||
### Fixed
|
|
||||||
* Fix display of 'duplicate' and 'merge' buttons in CRUD templates
|
|
||||||
* Fix saving notification preferences in user's profile
|
|
||||||
|
|
||||||
## v4.3.0 - 2025-09-08
|
|
||||||
### Feature
|
|
||||||
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges
|
|
||||||
* Add a command to generate a list of permissions
|
|
||||||
* ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date
|
|
||||||
|
|
||||||
**Schema Change**: Add columns or tables
|
|
||||||
### Fixed
|
|
||||||
* fix date formatting in calendar range display
|
|
||||||
* Change route URL to avoid clash with person duplicate controller method
|
|
||||||
|
|
||||||
## v4.2.1 - 2025-09-03
|
## v4.2.1 - 2025-09-03
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fix exports to work with DirectExportInterface
|
* Fix exports to work with DirectExportInterface
|
||||||
|
@@ -54,7 +54,7 @@ Arborescence:
|
|||||||
- person
|
- person
|
||||||
- personvendee
|
- personvendee
|
||||||
- household_edit_metadata
|
- household_edit_metadata
|
||||||
- index.js
|
- index.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
## Organisation des feuilles de styles
|
## 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"});
|
setLocaleFallbacks({ en: "fr", nl: "fr", fr: "en" });
|
||||||
setLocale('fr');
|
setLocale("fr");
|
||||||
|
|
||||||
export { trans };
|
export { trans, getLocale };
|
||||||
export * from '../var/translations';
|
export * from "../var/translations";
|
||||||
|
@@ -133,6 +133,7 @@
|
|||||||
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
|
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
|
||||||
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle",
|
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle",
|
||||||
"Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src",
|
"Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src",
|
||||||
|
"Chill\\TicketBundle\\": "src/Bundle/ChillTicketBundle/src",
|
||||||
"Chill\\Utils\\Rector\\": "utils/rector/src"
|
"Chill\\Utils\\Rector\\": "utils/rector/src"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -35,6 +35,7 @@ return [
|
|||||||
Chill\ThirdPartyBundle\ChillThirdPartyBundle::class => ['all' => true],
|
Chill\ThirdPartyBundle\ChillThirdPartyBundle::class => ['all' => true],
|
||||||
Chill\BudgetBundle\ChillBudgetBundle::class => ['all' => true],
|
Chill\BudgetBundle\ChillBudgetBundle::class => ['all' => true],
|
||||||
Chill\WopiBundle\ChillWopiBundle::class => ['all' => true],
|
Chill\WopiBundle\ChillWopiBundle::class => ['all' => true],
|
||||||
|
Chill\TicketBundle\ChillTicketBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||||
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
chill_doc_store:
|
chill_doc_store:
|
||||||
use_driver: openstack
|
use_driver: local_storage
|
||||||
local_storage:
|
local_storage:
|
||||||
storage_path: '%kernel.project_dir%/var/storage'
|
storage_path: '%kernel.project_dir%/var/storage'
|
||||||
openstack:
|
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\Calendar': '@ChillCalendarBundle/migrations'
|
||||||
'Chill\Migrations\Budget': '@ChillBudgetBundle/migrations'
|
'Chill\Migrations\Budget': '@ChillBudgetBundle/migrations'
|
||||||
'Chill\Migrations\Report': '@ChillReportBundle/migrations'
|
'Chill\Migrations\Report': '@ChillReportBundle/migrations'
|
||||||
|
'Chill\Migrations\Ticket': '@ChillTicketBundle/migrations'
|
||||||
all_or_nothing:
|
all_or_nothing:
|
||||||
true
|
true
|
||||||
|
|
||||||
|
2
config/routes/chill_ticket.yaml
Normal file
2
config/routes/chill_ticket.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
chill_ticket_bundle:
|
||||||
|
resource: '@ChillTicketBundle/config/routes.yaml'
|
@@ -11,24 +11,94 @@
|
|||||||
Create a new bundle
|
Create a new bundle
|
||||||
*******************
|
*******************
|
||||||
|
|
||||||
Create your own bundle is not a trivial task.
|
|
||||||
|
|
||||||
The easiest way to achieve this is seems to be :
|
|
||||||
|
|
||||||
1. Prepare a fresh installation of the chill project, in a new directory
|
|
||||||
2. Create a new bundle in this project, in the src directory
|
|
||||||
3. Initialize a git repository **at the root bundle**, and create your initial commit.
|
|
||||||
4. Register the bundle with composer/packagist. If you do not plan to distribute your bundle with packagist, you may use a custom repository for achieve this [#f1]_
|
|
||||||
5. Move to a development installation, made as described in the :ref:`installation-for-development` section, and add your new repository to the composer.json file
|
|
||||||
6. Work as :ref:`usual <editing-code-and-commiting>`
|
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
This part of the doc is not yet tested
|
This part of the doc is not yet tested
|
||||||
|
|
||||||
TODO
|
Create a new directory with Bundle class
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
mkdir -p src/Bundle/ChillSomeBundle/src/config
|
||||||
|
mkdir -p src/Bundle/ChillSomeBundle/src/Controller
|
||||||
|
|
||||||
|
Add a bundle file
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\SomeBundle;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||||
|
|
||||||
|
class ChillSomeBundle extends Bundle {}
|
||||||
|
|
||||||
|
And a route file:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
chill_ticket_controller:
|
||||||
|
resource: '@ChillTicketBundle/Controller/'
|
||||||
|
type: annotation
|
||||||
|
|
||||||
|
Register the new psr-4 namespace
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
In composer.json, add the new psr4 namespace
|
||||||
|
|
||||||
|
.. code-block:: diff
|
||||||
|
|
||||||
|
{
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
+ "Chill\\SomeBundle\\": "src/Bundle/ChillSomeBundle/src",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.. rubric:: Footnotes
|
Register the bundle
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Register in the file :code:`config/bundles.php`:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
Vendor\Bundle\YourBundle\YourBundle::class => ['all' => true],
|
||||||
|
|
||||||
|
And import routes in :code:`config/routes/chill_some_bundle.yaml`:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
chill_ticket_bundle:
|
||||||
|
resource: '@ChillSomeBundle/config/routes.yaml'
|
||||||
|
|
||||||
|
Add the doctrine_migration namespace
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
Add the namespace to :code:`config/packages/doctrine_migrations_chill.yaml`
|
||||||
|
|
||||||
|
.. code-block:: diff
|
||||||
|
|
||||||
|
doctrine_migrations:
|
||||||
|
migrations_paths:
|
||||||
|
+ 'Chill\Some\Ticket': '@ChillSomeBundle/migrations'
|
||||||
|
|
||||||
|
Dump autoloading
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
symfony composer dump-autoload
|
||||||
|
|
||||||
.. [#f1] Be aware that we use the Affero GPL Licence, which ensure that all users must have access to derivative works done with this software.
|
|
||||||
|
@@ -79,12 +79,12 @@
|
|||||||
"dev": "encore dev",
|
"dev": "encore dev",
|
||||||
"watch": "encore dev --watch",
|
"watch": "encore dev --watch",
|
||||||
"build": "encore production --progress",
|
"build": "encore production --progress",
|
||||||
"specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml> 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-validate": "swagger-cli validate templates/api/specs.yaml",
|
||||||
"specs-create-dir": "mkdir -p templates/api",
|
"specs-create-dir": "mkdir -p templates/api",
|
||||||
"specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate",
|
"specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate",
|
||||||
"version": "node --version",
|
"version": "node --version",
|
||||||
"eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
|
"eslint": "eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
@@ -58,6 +58,10 @@
|
|||||||
<!-- temporarily removed, the time to find a fix -->
|
<!-- temporarily removed, the time to find a fix -->
|
||||||
<exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude>
|
<exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
|
|
||||||
|
<testsuite name="TicketBundle">
|
||||||
|
<directory suffix="Test.php">src/Bundle/ChillTicketBundle/tests/</directory>
|
||||||
|
</testsuite>
|
||||||
<!--
|
<!--
|
||||||
<testsuite name="ReportBundle">
|
<testsuite name="ReportBundle">
|
||||||
<directory suffix="Test.php">src/Bundle/ChillReportBundle/Tests/</directory>
|
<directory suffix="Test.php">src/Bundle/ChillReportBundle/Tests/</directory>
|
||||||
|
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"}
|
@@ -10,10 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="getContext === 'accompanyingCourse' && suggestedEntities.length > 0"
|
||||||
getContext === 'accompanyingCourse' &&
|
|
||||||
suggestedEntities.length > 0
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<ul class="list-suggest add-items inline">
|
<ul class="list-suggest add-items inline">
|
||||||
<li
|
<li
|
||||||
|
@@ -39,17 +39,11 @@
|
|||||||
<option selected disabled value="">
|
<option selected disabled value="">
|
||||||
{{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }}
|
{{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option v-for="t in locationTypes" :value="t" :key="t.id">
|
||||||
v-for="t in locationTypes"
|
|
||||||
:value="t"
|
|
||||||
:key="t.id"
|
|
||||||
>
|
|
||||||
{{ localizeString(t.title) }}
|
{{ localizeString(t.title) }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<label>{{
|
<label>{{ trans(ACTIVITY_LOCATION_FIELDS_TYPE) }}</label>
|
||||||
trans(ACTIVITY_LOCATION_FIELDS_TYPE)
|
|
||||||
}}</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
@@ -108,10 +102,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<button
|
<button class="btn btn-save" @click.prevent="saveNewLocation">
|
||||||
class="btn btn-save"
|
|
||||||
@click.prevent="saveNewLocation"
|
|
||||||
>
|
|
||||||
{{ trans(SAVE) }}
|
{{ trans(SAVE) }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -244,8 +235,7 @@ export default {
|
|||||||
},
|
},
|
||||||
hasPhonenumber1() {
|
hasPhonenumber1() {
|
||||||
return (
|
return (
|
||||||
this.selected.phonenumber1 !== null &&
|
this.selected.phonenumber1 !== null && this.selected.phonenumber1 !== ""
|
||||||
this.selected.phonenumber1 !== ""
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
showAddAddress() {
|
showAddAddress() {
|
||||||
|
@@ -49,9 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<div v-if="actionIsLoading === true">
|
<div v-if="actionIsLoading === true">
|
||||||
<i
|
<i class="chill-green fa fa-circle-o-notch fa-spin fa-lg"></i>
|
||||||
class="chill-green fa fa-circle-o-notch fa-spin fa-lg"
|
|
||||||
></i>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
@@ -64,8 +62,7 @@
|
|||||||
<template
|
<template
|
||||||
v-else-if="
|
v-else-if="
|
||||||
socialActionsList.length > 0 &&
|
socialActionsList.length > 0 &&
|
||||||
(socialIssuesSelected.length ||
|
(socialIssuesSelected.length || socialActionsSelected.length)
|
||||||
socialActionsSelected.length)
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -88,9 +85,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-else-if="
|
v-else-if="actionAreLoaded && socialActionsList.length === 0"
|
||||||
actionAreLoaded && socialActionsList.length === 0
|
|
||||||
"
|
|
||||||
class="inline-choice chill-no-data-statement mt-3"
|
class="inline-choice chill-no-data-statement mt-3"
|
||||||
>
|
>
|
||||||
{{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }}
|
{{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }}
|
||||||
@@ -169,8 +164,7 @@ export default {
|
|||||||
/* Add in list the issues already associated (if not yet listed) */
|
/* Add in list the issues already associated (if not yet listed) */
|
||||||
this.socialIssuesSelected.forEach((issue) => {
|
this.socialIssuesSelected.forEach((issue) => {
|
||||||
if (
|
if (
|
||||||
this.socialIssuesList.filter((i) => i.id === issue.id)
|
this.socialIssuesList.filter((i) => i.id === issue.id).length !== 1
|
||||||
.length !== 1
|
|
||||||
) {
|
) {
|
||||||
this.$store.commit("addIssueInList", issue);
|
this.$store.commit("addIssueInList", issue);
|
||||||
}
|
}
|
||||||
|
@@ -10,9 +10,7 @@
|
|||||||
:value="issue"
|
:value="issue"
|
||||||
/>
|
/>
|
||||||
<label class="form-check-label" :for="issue.id">
|
<label class="form-check-label" :for="issue.id">
|
||||||
<span class="badge bg-chill-l-gray text-dark">{{
|
<span class="badge bg-chill-l-gray text-dark">{{ issue.text }}</span>
|
||||||
issue.text
|
|
||||||
}}</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@@ -55,6 +55,5 @@
|
|||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
|
||||||
{% endembed %}
|
{% endembed %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -68,9 +68,7 @@ export type EventInputCalendarRange = EventInput & {
|
|||||||
export function isEventInputCalendarRange(
|
export function isEventInputCalendarRange(
|
||||||
toBeDetermined: EventInputCalendarRange | EventInput,
|
toBeDetermined: EventInputCalendarRange | EventInput,
|
||||||
): toBeDetermined is EventInputCalendarRange {
|
): toBeDetermined is EventInputCalendarRange {
|
||||||
return (
|
return typeof toBeDetermined.is === "string" && toBeDetermined.is === "range";
|
||||||
typeof toBeDetermined.is === "string" && toBeDetermined.is === "range"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
@@ -61,24 +61,14 @@
|
|||||||
<label class="input-group-text" for="slotDuration"
|
<label class="input-group-text" for="slotDuration"
|
||||||
>Durée des créneaux</label
|
>Durée des créneaux</label
|
||||||
>
|
>
|
||||||
<select
|
<select v-model="slotDuration" id="slotDuration" class="form-select">
|
||||||
v-model="slotDuration"
|
|
||||||
id="slotDuration"
|
|
||||||
class="form-select"
|
|
||||||
>
|
|
||||||
<option value="00:05:00">5 minutes</option>
|
<option value="00:05:00">5 minutes</option>
|
||||||
<option value="00:10:00">10 minutes</option>
|
<option value="00:10:00">10 minutes</option>
|
||||||
<option value="00:15:00">15 minutes</option>
|
<option value="00:15:00">15 minutes</option>
|
||||||
<option value="00:30:00">30 minutes</option>
|
<option value="00:30:00">30 minutes</option>
|
||||||
<option value="00:45:00">45 minutes</option>
|
|
||||||
<option value="00:60:00">60 minutes</option>
|
|
||||||
</select>
|
</select>
|
||||||
<label class="input-group-text" for="slotMinTime">De</label>
|
<label class="input-group-text" for="slotMinTime">De</label>
|
||||||
<select
|
<select v-model="slotMinTime" id="slotMinTime" class="form-select">
|
||||||
v-model="slotMinTime"
|
|
||||||
id="slotMinTime"
|
|
||||||
class="form-select"
|
|
||||||
>
|
|
||||||
<option value="00:00:00">0h</option>
|
<option value="00:00:00">0h</option>
|
||||||
<option value="01:00:00">1h</option>
|
<option value="01:00:00">1h</option>
|
||||||
<option value="02:00:00">2h</option>
|
<option value="02:00:00">2h</option>
|
||||||
@@ -94,11 +84,7 @@
|
|||||||
<option value="12:00:00">12h</option>
|
<option value="12:00:00">12h</option>
|
||||||
</select>
|
</select>
|
||||||
<label class="input-group-text" for="slotMaxTime">À</label>
|
<label class="input-group-text" for="slotMaxTime">À</label>
|
||||||
<select
|
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select">
|
||||||
v-model="slotMaxTime"
|
|
||||||
id="slotMaxTime"
|
|
||||||
class="form-select"
|
|
||||||
>
|
|
||||||
<option value="12:00:00">12h</option>
|
<option value="12:00:00">12h</option>
|
||||||
<option value="13:00:00">13h</option>
|
<option value="13:00:00">13h</option>
|
||||||
<option value="14:00:00">14h</option>
|
<option value="14:00:00">14h</option>
|
||||||
@@ -126,9 +112,7 @@
|
|||||||
v-model="hideWeekends"
|
v-model="hideWeekends"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<label
|
<label for="showHideWE" class="form-check-label input-group-text"
|
||||||
for="showHideWE"
|
|
||||||
class="form-check-label input-group-text"
|
|
||||||
>Week-ends</label
|
>Week-ends</label
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,9 +128,7 @@
|
|||||||
<b v-else-if="arg.event.extendedProps.is === 'range'"
|
<b v-else-if="arg.event.extendedProps.is === 'range'"
|
||||||
>{{ arg.timeText }}
|
>{{ arg.timeText }}
|
||||||
{{ arg.event.extendedProps.locationName }}
|
{{ arg.event.extendedProps.locationName }}
|
||||||
<small>{{
|
<small>{{ arg.event.extendedProps.userLabel }}</small></b
|
||||||
arg.event.extendedProps.userLabel
|
|
||||||
}}</small></b
|
|
||||||
>
|
>
|
||||||
<b v-else-if="arg.event.extendedProps.is === 'current'"
|
<b v-else-if="arg.event.extendedProps.is === 'current'"
|
||||||
>{{ arg.timeText }} {{ $t("current_selected") }}
|
>{{ arg.timeText }} {{ $t("current_selected") }}
|
||||||
@@ -154,9 +136,7 @@
|
|||||||
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
|
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
|
||||||
arg.event.title
|
arg.event.title
|
||||||
}}</b>
|
}}</b>
|
||||||
<b v-else
|
<b v-else>{{ arg.timeText }} {{ $t("current_selected") }} </b>
|
||||||
>{{ arg.timeText }} {{ $t("current_selected") }}
|
|
||||||
</b>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</FullCalendar>
|
</FullCalendar>
|
||||||
@@ -270,9 +250,7 @@ export default {
|
|||||||
this.$store.state.activity.endDate !== null)
|
this.$store.state.activity.endDate !== null)
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
!window.confirm(
|
!window.confirm(this.$t("change_main_user_will_reset_event_data"))
|
||||||
this.$t("change_main_user_will_reset_event_data"),
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -280,13 +258,9 @@ export default {
|
|||||||
|
|
||||||
// add the previous user, if any, in the previous user list (in use for suggestion)
|
// add the previous user, if any, in the previous user list (in use for suggestion)
|
||||||
if (null !== this.$store.getters.getMainUser) {
|
if (null !== this.$store.getters.getMainUser) {
|
||||||
const suggestedUids = new Set(
|
const suggestedUids = new Set(this.$data.previousUser.map((u) => u.id));
|
||||||
this.$data.previousUser.map((u) => u.id),
|
|
||||||
);
|
|
||||||
if (!suggestedUids.has(this.$store.getters.getMainUser.id)) {
|
if (!suggestedUids.has(this.$store.getters.getMainUser.id)) {
|
||||||
this.$data.previousUser.push(
|
this.$data.previousUser.push(this.$store.getters.getMainUser);
|
||||||
this.$store.getters.getMainUser,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,8 +290,7 @@ export default {
|
|||||||
// show an alert if changing mainUser
|
// show an alert if changing mainUser
|
||||||
if (
|
if (
|
||||||
(this.$store.getters.getMainUser !== null &&
|
(this.$store.getters.getMainUser !== null &&
|
||||||
this.$store.state.me.id !==
|
this.$store.state.me.id !== this.$store.getters.getMainUser.id) ||
|
||||||
this.$store.getters.getMainUser.id) ||
|
|
||||||
this.$store.getters.getMainUser === null
|
this.$store.getters.getMainUser === null
|
||||||
) {
|
) {
|
||||||
if (!window.confirm(this.$t("will_change_main_user_for_me"))) {
|
if (!window.confirm(this.$t("will_change_main_user_for_me"))) {
|
||||||
@@ -361,9 +334,7 @@ export default {
|
|||||||
this.$store.getters.getMainUser.id
|
this.$store.getters.getMainUser.id
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
!window.confirm(
|
!window.confirm(this.$t("this_calendar_range_will_change_main_user"))
|
||||||
this.$t("this_calendar_range_will_change_main_user"),
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -4,18 +4,9 @@
|
|||||||
{{ user.text }}
|
{{ user.text }}
|
||||||
<template v-if="invite !== null">
|
<template v-if="invite !== null">
|
||||||
<i v-if="invite.status === 'accepted'" class="fa fa-check" />
|
<i v-if="invite.status === 'accepted'" class="fa fa-check" />
|
||||||
<i
|
<i v-else-if="invite.status === 'declined'" class="fa fa-times" />
|
||||||
v-else-if="invite.status === 'declined'"
|
<i v-else-if="invite.status === 'pending'" class="fa fa-question-o" />
|
||||||
class="fa fa-times"
|
<i v-else-if="invite.status === 'tentative'" class="fa fa-question" />
|
||||||
/>
|
|
||||||
<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>
|
<span v-else="">{{ invite.status }}</span>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
@@ -69,8 +60,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
style() {
|
style() {
|
||||||
return {
|
return {
|
||||||
backgroundColor: this.$store.getters.getUserData(this.user)
|
backgroundColor: this.$store.getters.getUserData(this.user).mainColor,
|
||||||
.mainColor,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
rangeShow: {
|
rangeShow: {
|
||||||
@@ -81,9 +71,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters.isRangeShownOnCalendarForUser(
|
return this.$store.getters.isRangeShownOnCalendarForUser(this.user);
|
||||||
this.user,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
remoteShow: {
|
remoteShow: {
|
||||||
@@ -94,9 +82,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters.isRemoteShownOnCalendarForUser(
|
return this.$store.getters.isRemoteShownOnCalendarForUser(this.user);
|
||||||
this.user,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -22,33 +22,25 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||||
<li v-if="status !== Statuses.ACCEPTED">
|
<li v-if="status !== Statuses.ACCEPTED">
|
||||||
<a
|
<a class="dropdown-item" @click="changeStatus(Statuses.ACCEPTED)"
|
||||||
class="dropdown-item"
|
><i class="fa fa-check" aria-hidden="true"></i> {{ $t("Accept") }}</a
|
||||||
@click="changeStatus(Statuses.ACCEPTED)"
|
|
||||||
><i class="fa fa-check" aria-hidden="true"></i>
|
|
||||||
{{ $t("Accept") }}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="status !== Statuses.DECLINED">
|
<li v-if="status !== Statuses.DECLINED">
|
||||||
<a
|
<a class="dropdown-item" @click="changeStatus(Statuses.DECLINED)"
|
||||||
class="dropdown-item"
|
><i class="fa fa-times" aria-hidden="true"></i> {{ $t("Decline") }}</a
|
||||||
@click="changeStatus(Statuses.DECLINED)"
|
|
||||||
><i class="fa fa-times" aria-hidden="true"></i>
|
|
||||||
{{ $t("Decline") }}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="status !== Statuses.TENTATIVELY_ACCEPTED">
|
<li v-if="status !== Statuses.TENTATIVELY_ACCEPTED">
|
||||||
<a
|
<a
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
@click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)"
|
@click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)"
|
||||||
><i class="fa fa-question"></i>
|
><i class="fa fa-question"></i> {{ $t("Tentatively_accept") }}</a
|
||||||
{{ $t("Tentatively_accept") }}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="status !== Statuses.PENDING">
|
<li v-if="status !== Statuses.PENDING">
|
||||||
<a class="dropdown-item" @click="changeStatus(Statuses.PENDING)"
|
<a class="dropdown-item" @click="changeStatus(Statuses.PENDING)"
|
||||||
><i class="fa fa-hourglass-o"></i>
|
><i class="fa fa-hourglass-o"></i> {{ $t("Set_pending") }}</a
|
||||||
{{ $t("Set_pending") }}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -91,9 +83,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: {
|
emits: {
|
||||||
statusChanged(
|
statusChanged(payload: "accepted" | "declined" | "pending" | "tentative") {
|
||||||
payload: "accepted" | "declined" | "pending" | "tentative",
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -23,24 +23,14 @@
|
|||||||
<label class="input-group-text" for="slotDuration"
|
<label class="input-group-text" for="slotDuration"
|
||||||
>Durée des créneaux</label
|
>Durée des créneaux</label
|
||||||
>
|
>
|
||||||
<select
|
<select v-model="slotDuration" id="slotDuration" class="form-select">
|
||||||
v-model="slotDuration"
|
|
||||||
id="slotDuration"
|
|
||||||
class="form-select"
|
|
||||||
>
|
|
||||||
<option value="00:05:00">5 minutes</option>
|
<option value="00:05:00">5 minutes</option>
|
||||||
<option value="00:10:00">10 minutes</option>
|
<option value="00:10:00">10 minutes</option>
|
||||||
<option value="00:15:00">15 minutes</option>
|
<option value="00:15:00">15 minutes</option>
|
||||||
<option value="00:30:00">30 minutes</option>
|
<option value="00:30:00">30 minutes</option>
|
||||||
<option value="00:45:00">45 minutes</option>
|
|
||||||
<option value="00:60:00">60 minutes</option>
|
|
||||||
</select>
|
</select>
|
||||||
<label class="input-group-text" for="slotMinTime">De</label>
|
<label class="input-group-text" for="slotMinTime">De</label>
|
||||||
<select
|
<select v-model="slotMinTime" id="slotMinTime" class="form-select">
|
||||||
v-model="slotMinTime"
|
|
||||||
id="slotMinTime"
|
|
||||||
class="form-select"
|
|
||||||
>
|
|
||||||
<option value="00:00:00">0h</option>
|
<option value="00:00:00">0h</option>
|
||||||
<option value="01:00:00">1h</option>
|
<option value="01:00:00">1h</option>
|
||||||
<option value="02:00:00">2h</option>
|
<option value="02:00:00">2h</option>
|
||||||
@@ -56,11 +46,7 @@
|
|||||||
<option value="12:00:00">12h</option>
|
<option value="12:00:00">12h</option>
|
||||||
</select>
|
</select>
|
||||||
<label class="input-group-text" for="slotMaxTime">À</label>
|
<label class="input-group-text" for="slotMaxTime">À</label>
|
||||||
<select
|
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select">
|
||||||
v-model="slotMaxTime"
|
|
||||||
id="slotMaxTime"
|
|
||||||
class="form-select"
|
|
||||||
>
|
|
||||||
<option value="12:00:00">12h</option>
|
<option value="12:00:00">12h</option>
|
||||||
<option value="13:00:00">13h</option>
|
<option value="13:00:00">13h</option>
|
||||||
<option value="14:00:00">14h</option>
|
<option value="14:00:00">14h</option>
|
||||||
@@ -88,9 +74,7 @@
|
|||||||
v-model="showWeekends"
|
v-model="showWeekends"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<label
|
<label for="showHideWE" class="form-check-label input-group-text"
|
||||||
for="showHideWE"
|
|
||||||
class="form-check-label input-group-text"
|
|
||||||
>Week-ends</label
|
>Week-ends</label
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,17 +84,12 @@
|
|||||||
<FullCalendar :options="calendarOptions" ref="calendarRef">
|
<FullCalendar :options="calendarOptions" ref="calendarRef">
|
||||||
<template v-slot:eventContent="{ event }: { event: EventApi }">
|
<template v-slot:eventContent="{ event }: { event: EventApi }">
|
||||||
<span :class="eventClasses">
|
<span :class="eventClasses">
|
||||||
<b v-if="event.extendedProps.is === 'remote'">{{
|
<b v-if="event.extendedProps.is === 'remote'">{{ event.title }}</b>
|
||||||
event.title
|
|
||||||
}}</b>
|
|
||||||
<b v-else-if="event.extendedProps.is === 'range'"
|
<b v-else-if="event.extendedProps.is === 'range'"
|
||||||
>{{ formatDate(event.startStr, "time") }} -
|
>{{ formatDate(event.startStr) }} -
|
||||||
{{ formatDate(event.endStr, "time") }}:
|
|
||||||
{{ event.extendedProps.locationName }}</b
|
{{ event.extendedProps.locationName }}</b
|
||||||
>
|
>
|
||||||
<b v-else-if="event.extendedProps.is === 'local'">{{
|
<b v-else-if="event.extendedProps.is === 'local'">{{ event.title }}</b>
|
||||||
event.title
|
|
||||||
}}</b>
|
|
||||||
<b v-else>no 'is'</b>
|
<b v-else>no 'is'</b>
|
||||||
<a
|
<a
|
||||||
v-if="event.extendedProps.is === 'range'"
|
v-if="event.extendedProps.is === 'range'"
|
||||||
@@ -129,11 +108,7 @@
|
|||||||
<h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6>
|
<h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-9 col-md-2">
|
<div class="col-xs-12 col-sm-9 col-md-2">
|
||||||
<select
|
<select v-model="dayOrWeek" id="dayOrWeek" class="form-select">
|
||||||
v-model="dayOrWeek"
|
|
||||||
id="dayOrWeek"
|
|
||||||
class="form-select"
|
|
||||||
>
|
|
||||||
<option value="day">{{ $t("from_day_to_day") }}</option>
|
<option value="day">{{ $t("from_day_to_day") }}</option>
|
||||||
<option value="week">
|
<option value="week">
|
||||||
{{ $t("from_week_to_week") }}
|
{{ $t("from_week_to_week") }}
|
||||||
@@ -142,27 +117,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<template v-if="dayOrWeek === 'day'">
|
<template v-if="dayOrWeek === 'day'">
|
||||||
<div class="col-xs-12 col-sm-3 col-md-3">
|
<div class="col-xs-12 col-sm-3 col-md-3">
|
||||||
<input
|
<input class="form-control" type="date" v-model="copyFrom" />
|
||||||
class="form-control"
|
|
||||||
type="date"
|
|
||||||
v-model="copyFrom"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-1 col-md-1 copy-chevron">
|
<div class="col-xs-12 col-sm-1 col-md-1 copy-chevron">
|
||||||
<i class="fa fa-angle-double-right"></i>
|
<i class="fa fa-angle-double-right"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-3 col-md-3">
|
<div class="col-xs-12 col-sm-3 col-md-3">
|
||||||
<input
|
<input class="form-control" type="date" v-model="copyTo" />
|
||||||
class="form-control"
|
|
||||||
type="date"
|
|
||||||
v-model="copyTo"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-5 col-md-1">
|
<div class="col-xs-12 col-sm-5 col-md-1">
|
||||||
<button
|
<button class="btn btn-action float-end" @click="copyDay">
|
||||||
class="btn btn-action float-end"
|
|
||||||
@click="copyDay"
|
|
||||||
>
|
|
||||||
{{ $t("copy_range") }}
|
{{ $t("copy_range") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,11 +138,7 @@
|
|||||||
id="copyFromWeek"
|
id="copyFromWeek"
|
||||||
class="form-select"
|
class="form-select"
|
||||||
>
|
>
|
||||||
<option
|
<option v-for="w in lastWeeks" :value="w.value" :key="w.value">
|
||||||
v-for="w in lastWeeks"
|
|
||||||
:value="w.value"
|
|
||||||
:key="w.value"
|
|
||||||
>
|
|
||||||
{{ w.text }}
|
{{ w.text }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -187,25 +147,14 @@
|
|||||||
<i class="fa fa-angle-double-right"></i>
|
<i class="fa fa-angle-double-right"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-3 col-md-3">
|
<div class="col-xs-12 col-sm-3 col-md-3">
|
||||||
<select
|
<select v-model="copyToWeek" id="copyToWeek" class="form-select">
|
||||||
v-model="copyToWeek"
|
<option v-for="w in nextWeeks" :value="w.value" :key="w.value">
|
||||||
id="copyToWeek"
|
|
||||||
class="form-select"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="w in nextWeeks"
|
|
||||||
:value="w.value"
|
|
||||||
:key="w.value"
|
|
||||||
>
|
|
||||||
{{ w.text }}
|
{{ w.text }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-5 col-md-1">
|
<div class="col-xs-12 col-sm-5 col-md-1">
|
||||||
<button
|
<button class="btn btn-action float-end" @click="copyWeek">
|
||||||
class="btn btn-action float-end"
|
|
||||||
@click="copyWeek"
|
|
||||||
>
|
|
||||||
{{ $t("copy_range") }}
|
{{ $t("copy_range") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -297,26 +246,9 @@ const nextWeeks = computed((): Weeks[] =>
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatDate = (datetime: string, format: null | "time" = null) => {
|
const formatDate = (datetime: string) => {
|
||||||
const date = ISOToDate(datetime);
|
console.log(typeof datetime);
|
||||||
if (!date) return "";
|
return ISOToDate(datetime);
|
||||||
|
|
||||||
if (format === "time") {
|
|
||||||
return date.toLocaleTimeString("fr-FR", {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// French date formatting
|
|
||||||
return date.toLocaleDateString("fr-FR", {
|
|
||||||
weekday: "short",
|
|
||||||
year: "numeric",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseOptions = ref<CalendarOptions>({
|
const baseOptions = ref<CalendarOptions>({
|
||||||
|
@@ -41,9 +41,7 @@ const futureStore = function (): Promise<Store<State>> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
store.commit("me/setWhoAmi", user, { root: true });
|
store.commit("me/setWhoAmi", user, { root: true });
|
||||||
store
|
store.dispatch("locations/getLocations", null, { root: true }).then((_) => {
|
||||||
.dispatch("locations/getLocations", null, { root: true })
|
|
||||||
.then((_) => {
|
|
||||||
return store.dispatch("locations/getCurrentLocation", null, {
|
return store.dispatch("locations/getCurrentLocation", null, {
|
||||||
root: true,
|
root: true,
|
||||||
});
|
});
|
||||||
|
@@ -29,10 +29,7 @@ export default {
|
|||||||
(state: CalendarLocalsState) =>
|
(state: CalendarLocalsState) =>
|
||||||
({ start, end }: { start: Date; end: Date }): boolean => {
|
({ start, end }: { start: Date; end: Date }): boolean => {
|
||||||
for (const range of state.localsLoaded) {
|
for (const range of state.localsLoaded) {
|
||||||
if (
|
if (start.getTime() === range.start && end.getTime() === range.end) {
|
||||||
start.getTime() === range.start &&
|
|
||||||
end.getTime() === range.end
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,10 +51,7 @@ export default {
|
|||||||
});
|
});
|
||||||
state.key = state.key + toAdd.length;
|
state.key = state.key + toAdd.length;
|
||||||
},
|
},
|
||||||
addLoaded(
|
addLoaded(state: CalendarLocalsState, payload: { start: Date; end: Date }) {
|
||||||
state: CalendarLocalsState,
|
|
||||||
payload: { start: Date; end: Date },
|
|
||||||
) {
|
|
||||||
state.localsLoaded.push({
|
state.localsLoaded.push({
|
||||||
start: payload.start.getTime(),
|
start: payload.start.getTime(),
|
||||||
end: payload.end.getTime(),
|
end: payload.end.getTime(),
|
||||||
@@ -85,11 +79,7 @@ export default {
|
|||||||
end: end,
|
end: end,
|
||||||
});
|
});
|
||||||
|
|
||||||
return fetchCalendarLocalForUser(
|
return fetchCalendarLocalForUser(ctx.rootGetters["me/getMe"], start, end)
|
||||||
ctx.rootGetters["me/getMe"],
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
)
|
|
||||||
.then((remotes: CalendarLight[]) => {
|
.then((remotes: CalendarLight[]) => {
|
||||||
// to be add when reactivity problem will be solve ?
|
// to be add when reactivity problem will be solve ?
|
||||||
//ctx.commit('addRemotes', remotes);
|
//ctx.commit('addRemotes', remotes);
|
||||||
|
@@ -40,10 +40,7 @@ export default {
|
|||||||
(state: CalendarRangesState) =>
|
(state: CalendarRangesState) =>
|
||||||
({ start, end }: { start: Date; end: Date }): boolean => {
|
({ start, end }: { start: Date; end: Date }): boolean => {
|
||||||
for (const range of state.rangesLoaded) {
|
for (const range of state.rangesLoaded) {
|
||||||
if (
|
if (start.getTime() === range.start && end.getTime() === range.end) {
|
||||||
start.getTime() === range.start &&
|
|
||||||
end.getTime() === range.end
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,9 +107,7 @@ export default {
|
|||||||
state: CalendarRangesState,
|
state: CalendarRangesState,
|
||||||
externalEvents: (EventInput & { id: string })[],
|
externalEvents: (EventInput & { id: string })[],
|
||||||
) {
|
) {
|
||||||
const toAdd = externalEvents.filter(
|
const toAdd = externalEvents.filter((r) => !state.rangesIndex.has(r.id));
|
||||||
(r) => !state.rangesIndex.has(r.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
toAdd.forEach((r) => {
|
toAdd.forEach((r) => {
|
||||||
state.rangesIndex.add(r.id);
|
state.rangesIndex.add(r.id);
|
||||||
@@ -120,10 +115,7 @@ export default {
|
|||||||
});
|
});
|
||||||
state.key = state.key + toAdd.length;
|
state.key = state.key + toAdd.length;
|
||||||
},
|
},
|
||||||
addLoaded(
|
addLoaded(state: CalendarRangesState, payload: { start: Date; end: Date }) {
|
||||||
state: CalendarRangesState,
|
|
||||||
payload: { start: Date; end: Date },
|
|
||||||
) {
|
|
||||||
state.rangesLoaded.push({
|
state.rangesLoaded.push({
|
||||||
start: payload.start.getTime(),
|
start: payload.start.getTime(),
|
||||||
end: payload.end.getTime(),
|
end: payload.end.getTime(),
|
||||||
@@ -142,17 +134,12 @@ export default {
|
|||||||
},
|
},
|
||||||
removeRange(state: CalendarRangesState, calendarRangeId: number) {
|
removeRange(state: CalendarRangesState, calendarRangeId: number) {
|
||||||
const found = state.ranges.find(
|
const found = state.ranges.find(
|
||||||
(r) =>
|
(r) => r.calendarRangeId === calendarRangeId && r.is === "range",
|
||||||
r.calendarRangeId === calendarRangeId && r.is === "range",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (found !== undefined) {
|
if (found !== undefined) {
|
||||||
state.ranges = state.ranges.filter(
|
state.ranges = state.ranges.filter(
|
||||||
(r) =>
|
(r) => !(r.calendarRangeId === calendarRangeId && r.is === "range"),
|
||||||
!(
|
|
||||||
r.calendarRangeId === calendarRangeId &&
|
|
||||||
r.is === "range"
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (typeof found.id === "string") {
|
if (typeof found.id === "string") {
|
||||||
@@ -211,11 +198,7 @@ export default {
|
|||||||
},
|
},
|
||||||
createRange(
|
createRange(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
{
|
{ start, end, location }: { start: Date; end: Date; location: Location },
|
||||||
start,
|
|
||||||
end,
|
|
||||||
location,
|
|
||||||
}: { start: Date; end: Date; location: Location },
|
|
||||||
): Promise<null> {
|
): Promise<null> {
|
||||||
const url = `/api/1.0/calendar/calendar-range.json?`;
|
const url = `/api/1.0/calendar/calendar-range.json?`;
|
||||||
|
|
||||||
@@ -240,11 +223,7 @@ export default {
|
|||||||
},
|
},
|
||||||
} as CalendarRangeCreate;
|
} as CalendarRangeCreate;
|
||||||
|
|
||||||
return makeFetch<CalendarRangeCreate, CalendarRange>(
|
return makeFetch<CalendarRangeCreate, CalendarRange>("POST", url, body)
|
||||||
"POST",
|
|
||||||
url,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
.then((newRange) => {
|
.then((newRange) => {
|
||||||
ctx.commit("addRange", newRange);
|
ctx.commit("addRange", newRange);
|
||||||
|
|
||||||
@@ -281,11 +260,7 @@ export default {
|
|||||||
},
|
},
|
||||||
} as CalendarRangeEdit;
|
} as CalendarRangeEdit;
|
||||||
|
|
||||||
return makeFetch<CalendarRangeEdit, CalendarRange>(
|
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body)
|
||||||
"PATCH",
|
|
||||||
url,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
.then((range) => {
|
.then((range) => {
|
||||||
ctx.commit("updateRange", range);
|
ctx.commit("updateRange", range);
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
@@ -310,11 +285,7 @@ export default {
|
|||||||
},
|
},
|
||||||
} as CalendarRangeEdit;
|
} as CalendarRangeEdit;
|
||||||
|
|
||||||
return makeFetch<CalendarRangeEdit, CalendarRange>(
|
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body)
|
||||||
"PATCH",
|
|
||||||
url,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
.then((range) => {
|
.then((range) => {
|
||||||
ctx.commit("updateRange", range);
|
ctx.commit("updateRange", range);
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
@@ -334,20 +305,14 @@ export default {
|
|||||||
|
|
||||||
for (const r of rangesToCopy) {
|
for (const r of rangesToCopy) {
|
||||||
const start = new Date(ISOToDatetime(r.start) as Date);
|
const start = new Date(ISOToDatetime(r.start) as Date);
|
||||||
start.setFullYear(
|
start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
||||||
to.getFullYear(),
|
|
||||||
to.getMonth(),
|
|
||||||
to.getDate(),
|
|
||||||
);
|
|
||||||
const end = new Date(ISOToDatetime(r.end) as Date);
|
const end = new Date(ISOToDatetime(r.end) as Date);
|
||||||
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
||||||
const location = ctx.rootGetters["locations/getLocationById"](
|
const location = ctx.rootGetters["locations/getLocationById"](
|
||||||
r.locationId,
|
r.locationId,
|
||||||
);
|
);
|
||||||
|
|
||||||
promises.push(
|
promises.push(ctx.dispatch("createRange", { start, end, location }));
|
||||||
ctx.dispatch("createRange", { start, end, location }),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises).then(() => Promise.resolve(null));
|
return Promise.all(promises).then(() => Promise.resolve(null));
|
||||||
@@ -369,9 +334,7 @@ export default {
|
|||||||
r.locationId,
|
r.locationId,
|
||||||
);
|
);
|
||||||
|
|
||||||
promises.push(
|
promises.push(ctx.dispatch("createRange", { start, end, location }));
|
||||||
ctx.dispatch("createRange", { start, end, location }),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises).then(() => Promise.resolve(null));
|
return Promise.all(promises).then(() => Promise.resolve(null));
|
||||||
|
@@ -29,10 +29,7 @@ export default {
|
|||||||
(state: CalendarRemotesState) =>
|
(state: CalendarRemotesState) =>
|
||||||
({ start, end }: { start: Date; end: Date }): boolean => {
|
({ start, end }: { start: Date; end: Date }): boolean => {
|
||||||
for (const range of state.remotesLoaded) {
|
for (const range of state.remotesLoaded) {
|
||||||
if (
|
if (start.getTime() === range.start && end.getTime() === range.end) {
|
||||||
start.getTime() === range.start &&
|
|
||||||
end.getTime() === range.end
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,11 +82,7 @@ export default {
|
|||||||
end: end,
|
end: end,
|
||||||
});
|
});
|
||||||
|
|
||||||
return fetchCalendarRemoteForUser(
|
return fetchCalendarRemoteForUser(ctx.rootGetters["me/getMe"], start, end)
|
||||||
ctx.rootGetters["me/getMe"],
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
)
|
|
||||||
.then((remotes: CalendarRemote[]) => {
|
.then((remotes: CalendarRemote[]) => {
|
||||||
// to be add when reactivity problem will be solve ?
|
// to be add when reactivity problem will be solve ?
|
||||||
//ctx.commit('addRemotes', remotes);
|
//ctx.commit('addRemotes', remotes);
|
||||||
|
@@ -112,11 +112,8 @@ export default {
|
|||||||
|
|
||||||
results.forEach((i) => {
|
results.forEach((i) => {
|
||||||
if (!users.some((j) => i.user.id === j.id)) {
|
if (!users.some((j) => i.user.id === j.id)) {
|
||||||
let ratio = Math.floor(
|
let ratio = Math.floor(users.length / COLORS.length);
|
||||||
users.length / COLORS.length,
|
let colorIndex = users.length - ratio * COLORS.length;
|
||||||
);
|
|
||||||
let colorIndex =
|
|
||||||
users.length - ratio * COLORS.length;
|
|
||||||
users.push({
|
users.push({
|
||||||
id: i.user.id,
|
id: i.user.id,
|
||||||
username: i.user.username,
|
username: i.user.username,
|
||||||
@@ -153,45 +150,29 @@ export default {
|
|||||||
(me) =>
|
(me) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
this.users.logged = me;
|
this.users.logged = me;
|
||||||
let currentUser = users.find(
|
let currentUser = users.find((u) => u.id === me.id);
|
||||||
(u) => u.id === me.id,
|
|
||||||
);
|
|
||||||
this.value = currentUser;
|
this.value = currentUser;
|
||||||
|
|
||||||
fetchCalendar(currentUser.id).then(
|
fetchCalendar(currentUser.id).then(
|
||||||
(calendar) =>
|
(calendar) =>
|
||||||
new Promise(
|
new Promise((resolve, reject) => {
|
||||||
(resolve, reject) => {
|
let results = calendar.results;
|
||||||
let results =
|
let events = results.map((i) => ({
|
||||||
calendar.results;
|
start: i.startDate.datetime,
|
||||||
let events =
|
end: i.endDate.datetime,
|
||||||
results.map(
|
}));
|
||||||
(i) => ({
|
let calendarEventsCurrentUser = {
|
||||||
start: i
|
|
||||||
.startDate
|
|
||||||
.datetime,
|
|
||||||
end: i
|
|
||||||
.endDate
|
|
||||||
.datetime,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
let calendarEventsCurrentUser =
|
|
||||||
{
|
|
||||||
events: events,
|
events: events,
|
||||||
color: "darkblue",
|
color: "darkblue",
|
||||||
id: 1000,
|
id: 1000,
|
||||||
editable: false,
|
editable: false,
|
||||||
};
|
};
|
||||||
this.calendarEvents.user =
|
this.calendarEvents.user = calendarEventsCurrentUser;
|
||||||
calendarEventsCurrentUser;
|
|
||||||
|
|
||||||
this.selectUsers(
|
this.selectUsers(currentUser);
|
||||||
currentUser,
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
@@ -209,9 +190,7 @@ export default {
|
|||||||
return `${value.username}`;
|
return `${value.username}`;
|
||||||
},
|
},
|
||||||
coloriseSelectedValues() {
|
coloriseSelectedValues() {
|
||||||
let tags = document.querySelectorAll(
|
let tags = document.querySelectorAll("div.multiselect__tags-wrap")[0];
|
||||||
"div.multiselect__tags-wrap",
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
if (tags.hasChildNodes()) {
|
if (tags.hasChildNodes()) {
|
||||||
let children = tags.childNodes;
|
let children = tags.childNodes;
|
||||||
@@ -232,8 +211,8 @@ export default {
|
|||||||
},
|
},
|
||||||
selectEvents() {
|
selectEvents() {
|
||||||
let selectedUsersId = this.users.selected.map((a) => a.id);
|
let selectedUsersId = this.users.selected.map((a) => a.id);
|
||||||
this.calendarEvents.selected = this.calendarEvents.loaded.filter(
|
this.calendarEvents.selected = this.calendarEvents.loaded.filter((a) =>
|
||||||
(a) => selectedUsersId.includes(a.id),
|
selectedUsersId.includes(a.id),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
selectUsers(value) {
|
selectUsers(value) {
|
||||||
@@ -243,9 +222,7 @@ export default {
|
|||||||
this.updateEventsSource();
|
this.updateEventsSource();
|
||||||
},
|
},
|
||||||
unSelectUsers(value) {
|
unSelectUsers(value) {
|
||||||
this.users.selected = this.users.selected.filter(
|
this.users.selected = this.users.selected.filter((a) => a.id != value.id);
|
||||||
(a) => a.id != value.id,
|
|
||||||
);
|
|
||||||
this.selectEvents();
|
this.selectEvents();
|
||||||
this.updateEventsSource();
|
this.updateEventsSource();
|
||||||
},
|
},
|
||||||
|
@@ -20,10 +20,7 @@
|
|||||||
</option>
|
</option>
|
||||||
<template v-for="t in templates" :key="t.id">
|
<template v-for="t in templates" :key="t.id">
|
||||||
<option :value="t.id">
|
<option :value="t.id">
|
||||||
{{
|
{{ localizeString(t.name) || "Aucun nom défini" }}
|
||||||
localizeString(t.name) ||
|
|
||||||
"Aucun nom défini"
|
|
||||||
}}
|
|
||||||
</option>
|
</option>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
@@ -31,9 +28,7 @@
|
|||||||
v-if="canGenerate"
|
v-if="canGenerate"
|
||||||
class="btn btn-update btn-sm change-icon"
|
class="btn btn-update btn-sm change-icon"
|
||||||
:href="buildUrlGenerate"
|
:href="buildUrlGenerate"
|
||||||
@click.prevent="
|
@click.prevent="clickGenerate($event, buildUrlGenerate)"
|
||||||
clickGenerate($event, buildUrlGenerate)
|
|
||||||
"
|
|
||||||
><i class="fa fa-fw fa-cog"
|
><i class="fa fa-fw fa-cog"
|
||||||
/></a>
|
/></a>
|
||||||
<a
|
<a
|
||||||
|
@@ -94,7 +94,7 @@ class StoredObject implements Document, TrackCreationInterface
|
|||||||
/**
|
/**
|
||||||
* @var Collection<int, StoredObjectVersion>&Selectable<int, StoredObjectVersion>
|
* @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;
|
private Collection&Selectable $versions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -13,8 +13,9 @@ const startApp = (
|
|||||||
|
|
||||||
const inputTitle = collectionEntry?.querySelector("input[type='text']");
|
const inputTitle = collectionEntry?.querySelector("input[type='text']");
|
||||||
|
|
||||||
const input_stored_object: HTMLInputElement | null =
|
const input_stored_object: HTMLInputElement | null = divElement.querySelector(
|
||||||
divElement.querySelector("input[data-stored-object]");
|
"input[data-stored-object]",
|
||||||
|
);
|
||||||
if (null === input_stored_object) {
|
if (null === input_stored_object) {
|
||||||
throw new Error("input to stored object not found");
|
throw new Error("input to stored object not found");
|
||||||
}
|
}
|
||||||
@@ -53,9 +54,7 @@ const startApp = (
|
|||||||
console.log("version added", stored_object_version);
|
console.log("version added", stored_object_version);
|
||||||
this.$data.existingDoc = stored_object;
|
this.$data.existingDoc = stored_object;
|
||||||
this.$data.existingDoc.currentVersion = stored_object_version;
|
this.$data.existingDoc.currentVersion = stored_object_version;
|
||||||
input_stored_object.value = JSON.stringify(
|
input_stored_object.value = JSON.stringify(this.$data.existingDoc);
|
||||||
this.$data.existingDoc,
|
|
||||||
);
|
|
||||||
if (this.$data.inputTitle) {
|
if (this.$data.inputTitle) {
|
||||||
if (!this.$data.inputTitle?.value) {
|
if (!this.$data.inputTitle?.value) {
|
||||||
this.$data.inputTitle.value = file_name;
|
this.$data.inputTitle.value = file_name;
|
||||||
|
@@ -25,7 +25,7 @@ export interface GenericDoc {
|
|||||||
type: "doc_store_generic_doc";
|
type: "doc_store_generic_doc";
|
||||||
uniqueKey: string;
|
uniqueKey: string;
|
||||||
key: string;
|
key: string;
|
||||||
identifiers: { id: number };
|
identifiers: object;
|
||||||
context: "person" | "accompanying-period";
|
context: "person" | "accompanying-period";
|
||||||
doc_date: DateTime;
|
doc_date: DateTime;
|
||||||
metadata: GenericDocMetadata;
|
metadata: GenericDocMetadata;
|
||||||
|
@@ -49,9 +49,7 @@
|
|||||||
<li v-if="isHistoryViewable">
|
<li v-if="isHistoryViewable">
|
||||||
<history-button
|
<history-button
|
||||||
:stored-object="props.storedObject"
|
:stored-object="props.storedObject"
|
||||||
:can-edit="
|
:can-edit="canEdit && props.storedObject._permissions.canEdit"
|
||||||
canEdit && props.storedObject._permissions.canEdit
|
|
||||||
"
|
|
||||||
></history-button>
|
></history-button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -129,9 +127,7 @@ const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
|
|||||||
canDownload: true,
|
canDownload: true,
|
||||||
canConvertPdf: true,
|
canConvertPdf: true,
|
||||||
returnPath:
|
returnPath:
|
||||||
window.location.pathname +
|
window.location.pathname + window.location.search + window.location.hash,
|
||||||
window.location.search +
|
|
||||||
window.location.hash,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -29,9 +29,7 @@
|
|||||||
</modal>
|
</modal>
|
||||||
</teleport>
|
</teleport>
|
||||||
<div class="col-12 m-auto sticky-top">
|
<div class="col-12 m-auto sticky-top">
|
||||||
<div
|
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
|
||||||
class="row justify-content-center border-bottom pdf-tools d-md-none"
|
|
||||||
>
|
|
||||||
<div class="col-5 text-center turn-page">
|
<div class="col-5 text-center turn-page">
|
||||||
<select
|
<select
|
||||||
class="form-select form-select-sm"
|
class="form-select form-select-sm"
|
||||||
@@ -92,10 +90,7 @@
|
|||||||
v-if="signature.zones.length === 1 && signedState !== 'signed'"
|
v-if="signature.zones.length === 1 && signedState !== 'signed'"
|
||||||
class="col-5 p-0 text-center turnSignature"
|
class="col-5 p-0 text-center turnSignature"
|
||||||
>
|
>
|
||||||
<button
|
<button class="btn btn-light btn-sm" @click="goToSignatureZoneUnique">
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
@click="goToSignatureZoneUnique"
|
|
||||||
>
|
|
||||||
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
|
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,10 +145,7 @@
|
|||||||
:title="trans(SIGNATURES_ADD_SIGN_ZONE)"
|
:title="trans(SIGNATURES_ADD_SIGN_ZONE)"
|
||||||
>
|
>
|
||||||
<template v-if="canvasEvent === 'add'">
|
<template v-if="canvasEvent === 'add'">
|
||||||
<div
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
class="spinner-border spinner-border-sm"
|
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -207,10 +199,7 @@
|
|||||||
v-if="signature.zones.length === 1 && signedState !== 'signed'"
|
v-if="signature.zones.length === 1 && signedState !== 'signed'"
|
||||||
class="col-4 d-xl-none text-center turnSignature p-0"
|
class="col-4 d-xl-none text-center turnSignature p-0"
|
||||||
>
|
>
|
||||||
<button
|
<button class="btn btn-light btn-sm" @click="goToSignatureZoneUnique">
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
@click="goToSignatureZoneUnique"
|
|
||||||
>
|
|
||||||
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
|
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,10 +227,7 @@
|
|||||||
v-if="signature.zones.length === 1 && signedState !== 'signed'"
|
v-if="signature.zones.length === 1 && signedState !== 'signed'"
|
||||||
class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
|
class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
|
||||||
>
|
>
|
||||||
<button
|
<button class="btn btn-light btn-sm" @click="goToSignatureZoneUnique">
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
@click="goToSignatureZoneUnique"
|
|
||||||
>
|
|
||||||
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
|
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -299,10 +285,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ trans(SIGNATURES_CLICK_ON_DOCUMENT) }}
|
{{ trans(SIGNATURES_CLICK_ON_DOCUMENT) }}
|
||||||
<div
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
class="spinner-border spinner-border-sm"
|
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -562,14 +545,8 @@ const addCanvasEvents = () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const canvas = document.querySelectorAll(
|
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||||
"canvas",
|
canvas.addEventListener("pointerup", (e) => canvasClick(e, canvas), false);
|
||||||
)[0] as HTMLCanvasElement;
|
|
||||||
canvas.addEventListener(
|
|
||||||
"pointerup",
|
|
||||||
(e) => canvasClick(e, canvas),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -605,11 +582,7 @@ const hitSignature = (
|
|||||||
scaleYToCanvas(zone.y, canvas.height, zone.PDFPage.height) <
|
scaleYToCanvas(zone.y, canvas.height, zone.PDFPage.height) <
|
||||||
xy[1] &&
|
xy[1] &&
|
||||||
xy[1] <
|
xy[1] <
|
||||||
scaleYToCanvas(
|
scaleYToCanvas(zone.height - zone.y, canvas.height, zone.PDFPage.height) +
|
||||||
zone.height - zone.y,
|
|
||||||
canvas.height,
|
|
||||||
zone.PDFPage.height,
|
|
||||||
) +
|
|
||||||
zone.PDFPage.height * zoom.value;
|
zone.PDFPage.height * zoom.value;
|
||||||
|
|
||||||
const selectZone = async (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
const selectZone = async (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
||||||
@@ -625,8 +598,7 @@ const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
|||||||
signature.zones
|
signature.zones
|
||||||
.filter(
|
.filter(
|
||||||
(z) =>
|
(z) =>
|
||||||
(z.PDFPage.index + 1 === getCanvasId(canvas) &&
|
(z.PDFPage.index + 1 === getCanvasId(canvas) && multiPage.value) ||
|
||||||
multiPage.value) ||
|
|
||||||
(z.PDFPage.index + 1 === page.value && !multiPage.value),
|
(z.PDFPage.index + 1 === page.value && !multiPage.value),
|
||||||
)
|
)
|
||||||
.map((z) => {
|
.map((z) => {
|
||||||
|
@@ -153,12 +153,10 @@ const handleFile = async (file: File): Promise<void> => {
|
|||||||
</p>
|
</p>
|
||||||
<!-- todo i18n -->
|
<!-- todo i18n -->
|
||||||
<p v-if="has_existing_doc">
|
<p v-if="has_existing_doc">
|
||||||
Déposez un document ou cliquez ici pour remplacer le document
|
Déposez un document ou cliquez ici pour remplacer le document existant
|
||||||
existant
|
|
||||||
</p>
|
</p>
|
||||||
<p v-else>
|
<p v-else>
|
||||||
Déposez un document ou cliquez ici pour ouvrir le navigateur de
|
Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier
|
||||||
fichier
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="waiting">
|
<div v-else class="waiting">
|
||||||
|
@@ -4,7 +4,6 @@ import { StoredObject, StoredObjectVersion } from "../../types";
|
|||||||
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
|
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
|
||||||
import { computed, reactive } from "vue";
|
import { computed, reactive } from "vue";
|
||||||
import { useToast } from "vue-toast-notification";
|
import { useToast } from "vue-toast-notification";
|
||||||
import { DOCUMENT_ADD, trans } from "translator";
|
|
||||||
|
|
||||||
interface DropFileConfig {
|
interface DropFileConfig {
|
||||||
allowRemove: boolean;
|
allowRemove: boolean;
|
||||||
@@ -76,9 +75,11 @@ function closeModal(): void {
|
|||||||
@click="openModal"
|
@click="openModal"
|
||||||
class="btn btn-create"
|
class="btn btn-create"
|
||||||
>
|
>
|
||||||
{{ trans(DOCUMENT_ADD) }}
|
Ajouter un document
|
||||||
|
</button>
|
||||||
|
<button v-else @click="openModal" class="btn btn-edit">
|
||||||
|
Remplacer le document
|
||||||
</button>
|
</button>
|
||||||
<button v-else @click="openModal" class="btn btn-edit"></button>
|
|
||||||
<modal
|
<modal
|
||||||
v-if="state.showModal"
|
v-if="state.showModal"
|
||||||
:modal-dialog-class="modalClasses"
|
:modal-dialog-class="modalClasses"
|
||||||
|
@@ -35,9 +35,7 @@ async function download_and_open(event: Event): Promise<void> {
|
|||||||
if (null === state.content) {
|
if (null === state.content) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const raw = await download_doc(
|
const raw = await download_doc(build_convert_link(props.storedObject.uuid));
|
||||||
build_convert_link(props.storedObject.uuid),
|
|
||||||
);
|
|
||||||
state.content = window.URL.createObjectURL(raw);
|
state.content = window.URL.createObjectURL(raw);
|
||||||
|
|
||||||
button.href = window.URL.createObjectURL(raw);
|
button.href = window.URL.createObjectURL(raw);
|
||||||
|
@@ -42,9 +42,7 @@ const editionUntilFormatted = computed<string>(() => {
|
|||||||
<modal v-if="state.modalOpened" @close="state.modalOpened = false">
|
<modal v-if="state.modalOpened" @close="state.modalOpened = false">
|
||||||
<template v-slot:body>
|
<template v-slot:body>
|
||||||
<div class="desktop-edit">
|
<div class="desktop-edit">
|
||||||
<p class="center">
|
<p class="center">Veuillez enregistrer vos modifications avant le</p>
|
||||||
Veuillez enregistrer vos modifications avant le
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ editionUntilFormatted }}</strong>
|
<strong>{{ editionUntilFormatted }}</strong>
|
||||||
</p>
|
</p>
|
||||||
@@ -57,23 +55,21 @@ const editionUntilFormatted = computed<string>(() => {
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<small
|
<small
|
||||||
>Le document peut être édité uniquement en utilisant
|
>Le document peut être édité uniquement en utilisant Libre
|
||||||
Libre Office.</small
|
Office.</small
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<small
|
<small
|
||||||
>En cas d'échec lors de l'enregistrement, sauver le
|
>En cas d'échec lors de l'enregistrement, sauver le document sur
|
||||||
document sur le poste de travail avant de le déposer
|
le poste de travail avant de le déposer à nouveau ici.</small
|
||||||
à nouveau ici.</small
|
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<small
|
<small
|
||||||
>Vous pouvez naviguez sur d'autres pages pendant
|
>Vous pouvez naviguez sur d'autres pages pendant l'édition.</small
|
||||||
l'édition.</small
|
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -95,10 +95,7 @@ async function download_and_open(): Promise<void> {
|
|||||||
let raw;
|
let raw;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
raw = await download_and_decrypt_doc(
|
raw = await download_and_decrypt_doc(props.storedObject, props.atVersion);
|
||||||
props.storedObject,
|
|
||||||
props.atVersion,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("error while downloading and decrypting document");
|
console.error("error while downloading and decrypting document");
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@@ -49,8 +49,7 @@ const isRestored = computed<boolean>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isDuplicated = computed<boolean>(
|
const isDuplicated = computed<boolean>(
|
||||||
() =>
|
() => props.version.version === 0 && null !== props.version["from-restored"],
|
||||||
props.version.version === 0 && null !== props.version["from-restored"],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const classes = computed<{
|
const classes = computed<{
|
||||||
@@ -70,16 +69,9 @@ const classes = computed<{
|
|||||||
<div :class="classes">
|
<div :class="classes">
|
||||||
<div
|
<div
|
||||||
class="col-12 tags"
|
class="col-12 tags"
|
||||||
v-if="
|
v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated"
|
||||||
isCurrent ||
|
|
||||||
isKeptBeforeConversion ||
|
|
||||||
isRestored ||
|
|
||||||
isDuplicated
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<span class="badge bg-success" v-if="isCurrent"
|
|
||||||
>Version actuelle</span
|
|
||||||
>
|
>
|
||||||
|
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span>
|
||||||
<span class="badge bg-info" v-if="isKeptBeforeConversion"
|
<span class="badge bg-info" v-if="isKeptBeforeConversion"
|
||||||
>Conservée avant conversion dans un autre format</span
|
>Conservée avant conversion dans un autre format</span
|
||||||
>
|
>
|
||||||
@@ -96,21 +88,17 @@ const classes = computed<{
|
|||||||
<span
|
<span
|
||||||
><strong> #{{ version.version + 1 }} </strong></span
|
><strong> #{{ version.version + 1 }} </strong></span
|
||||||
>
|
>
|
||||||
<template
|
<template v-if="version.createdBy !== null && version.createdAt !== null"
|
||||||
v-if="version.createdBy !== null && version.createdAt !== null"
|
|
||||||
><strong v-if="version.version == 0">créé par</strong
|
><strong v-if="version.version == 0">créé par</strong
|
||||||
><strong v-else>modifié par</strong>
|
><strong v-else>modifié par</strong>
|
||||||
<span class="badge-user"
|
<span class="badge-user"
|
||||||
><UserRenderBoxBadge
|
><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge
|
||||||
:user="version.createdBy"
|
|
||||||
></UserRenderBoxBadge
|
|
||||||
></span>
|
></span>
|
||||||
<strong>à</strong>
|
<strong>à</strong>
|
||||||
{{
|
{{
|
||||||
$d(ISOToDatetime(version.createdAt.datetime8601), "long")
|
$d(ISOToDatetime(version.createdAt.datetime8601), "long")
|
||||||
}}</template
|
}}</template
|
||||||
><template
|
><template v-if="version.createdBy === null && version.createdAt !== null"
|
||||||
v-if="version.createdBy === null && version.createdAt !== null"
|
|
||||||
><strong v-if="version.version == 0">Créé le</strong
|
><strong v-if="version.version == 0">Créé le</strong
|
||||||
><strong v-else>modifié le</strong>
|
><strong v-else>modifié le</strong>
|
||||||
{{
|
{{
|
||||||
|
@@ -2,9 +2,7 @@
|
|||||||
<a
|
<a
|
||||||
:class="Object.assign(props.classes, { btn: true })"
|
:class="Object.assign(props.classes, { btn: true })"
|
||||||
@click="beforeLeave($event)"
|
@click="beforeLeave($event)"
|
||||||
:href="
|
:href="build_wopi_editor_link(props.storedObject.uuid, props.returnPath)"
|
||||||
build_wopi_editor_link(props.storedObject.uuid, props.returnPath)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<i class="fa fa-paragraph"></i>
|
<i class="fa fa-paragraph"></i>
|
||||||
Editer en ligne
|
Editer en ligne
|
||||||
|
@@ -145,9 +145,7 @@ async function download_info_link(
|
|||||||
function build_wopi_editor_link(uuid: string, returnPath?: string) {
|
function build_wopi_editor_link(uuid: string, returnPath?: string) {
|
||||||
if (returnPath === undefined) {
|
if (returnPath === undefined) {
|
||||||
returnPath =
|
returnPath =
|
||||||
window.location.pathname +
|
window.location.pathname + window.location.search + window.location.hash;
|
||||||
window.location.search +
|
|
||||||
window.location.hash;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -186,10 +184,7 @@ async function download_and_decrypt_doc(
|
|||||||
) {
|
) {
|
||||||
downloadInfo = storedObject._links.downloadLink;
|
downloadInfo = storedObject._links.downloadLink;
|
||||||
} else {
|
} else {
|
||||||
downloadInfo = await download_info_link(
|
downloadInfo = await download_info_link(storedObject, atVersionToDownload);
|
||||||
storedObject,
|
|
||||||
atVersionToDownload,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawResponse = await window.fetch(downloadInfo.url);
|
const rawResponse = await window.fetch(downloadInfo.url);
|
||||||
@@ -244,10 +239,7 @@ async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (storedObject.currentVersion?.type === "application/pdf") {
|
if (storedObject.currentVersion?.type === "application/pdf") {
|
||||||
return download_and_decrypt_doc(
|
return download_and_decrypt_doc(storedObject, storedObject.currentVersion);
|
||||||
storedObject,
|
|
||||||
storedObject.currentVersion,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertLink = build_convert_link(storedObject.uuid);
|
const convertLink = build_convert_link(storedObject.uuid);
|
||||||
|
@@ -46,16 +46,6 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
|||||||
|
|
||||||
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
||||||
{
|
{
|
||||||
// we first try to get the permission from the workflow, as attachement (this is the less intensive query)
|
|
||||||
$workflowPermissionAsAttachment = match ($attribute) {
|
|
||||||
StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($subject),
|
|
||||||
StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($subject),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (WorkflowRelatedEntityPermissionHelper::FORCE_DENIED === $workflowPermissionAsAttachment) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the related entity
|
// Retrieve the related entity
|
||||||
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
|
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
|
||||||
|
|
||||||
@@ -76,7 +66,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
|||||||
return match ($workflowPermission) {
|
return match ($workflowPermission) {
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
|
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
|
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission,
|
WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,12 +14,6 @@ namespace Chill\DocStoreBundle\Security\Authorization;
|
|||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for voting on stored object permissions.
|
|
||||||
*
|
|
||||||
* Each time a stored object is attached to a document, the voter is responsible for determining
|
|
||||||
* whether the user has the necessary permissions to access or modify the stored object.
|
|
||||||
*/
|
|
||||||
interface StoredObjectVoterInterface
|
interface StoredObjectVoterInterface
|
||||||
{
|
{
|
||||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
|
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
|
||||||
|
@@ -43,11 +43,17 @@ class StoredObjectVersionNormalizer implements NormalizerInterface, NormalizerAw
|
|||||||
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, [...$context, UserNormalizer::AT_DATE => $object->getCreatedAt()]),
|
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, [...$context, UserNormalizer::AT_DATE => $object->getCreatedAt()]),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
|
$normalizationGroups = $context[AbstractNormalizer::GROUPS] ?? [];
|
||||||
|
|
||||||
|
if (is_string($normalizationGroups)) {
|
||||||
|
$normalizationGroups = [$normalizationGroups];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $normalizationGroups, true)) {
|
||||||
$data['point-in-times'] = $this->normalizer->normalize($object->getPointInTimes(), $format, $context);
|
$data['point-in-times'] = $this->normalizer->normalize($object->getPointInTimes(), $format, $context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array(self::WITH_RESTORED_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
|
if (in_array(self::WITH_RESTORED_CONTEXT, $normalizationGroups, true)) {
|
||||||
$data['from-restored'] = $this->normalizer->normalize($object->getCreatedFrom(), $format, [AbstractNormalizer::GROUPS => ['read']]);
|
$data['from-restored'] = $this->normalizer->normalize($object->getCreatedFrom(), $format, [AbstractNormalizer::GROUPS => ['read']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -86,165 +86,9 @@ class AbstractStoredObjectVoterTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission
|
* @dataProvider dataProviderVoteOnAttribute
|
||||||
*/
|
*/
|
||||||
public function testVoteOnAttributeWithStoredObjectPermission(
|
public function testVoteOnAttribute(
|
||||||
StoredObjectRoleEnum $attribute,
|
|
||||||
bool $expected,
|
|
||||||
bool $isGrantedRegularPermission,
|
|
||||||
string $isGrantedWorkflowPermission,
|
|
||||||
string $isGrantedStoredObjectAttachment,
|
|
||||||
): void {
|
|
||||||
$storedObject = new StoredObject();
|
|
||||||
$repository = new DummyRepository($related = new \stdClass());
|
|
||||||
$token = new UsernamePasswordToken(new User(), 'dummy');
|
|
||||||
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
|
||||||
|
|
||||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
|
|
||||||
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
|
||||||
|
|
||||||
if (StoredObjectRoleEnum::SEE === $attribute) {
|
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)
|
|
||||||
->shouldBeCalled()
|
|
||||||
->willReturn($isGrantedStoredObjectAttachment);
|
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
|
|
||||||
->willReturn($isGrantedWorkflowPermission);
|
|
||||||
} elseif (StoredObjectRoleEnum::EDIT === $attribute) {
|
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)
|
|
||||||
->shouldBeCalled()
|
|
||||||
->willReturn($isGrantedStoredObjectAttachment);
|
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
|
|
||||||
->willReturn($isGrantedWorkflowPermission);
|
|
||||||
} else {
|
|
||||||
throw new \LogicException('Invalid attribute for StoredObjectVoter');
|
|
||||||
}
|
|
||||||
|
|
||||||
$storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter {
|
|
||||||
public function __construct(private $repository, $helper, $security)
|
|
||||||
{
|
|
||||||
parent::__construct($security, $helper);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
|
||||||
{
|
|
||||||
return $this->repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getClass(): string
|
|
||||||
{
|
|
||||||
return \stdClass::class;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
|
||||||
{
|
|
||||||
return 'SOME_ROLE';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function canBeAssociatedWithWorkflow(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$actual = $storedObjectVoter->voteOnAttribute($attribute, $storedObject, $token);
|
|
||||||
|
|
||||||
self::assertEquals($expected, $actual);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable
|
|
||||||
{
|
|
||||||
foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) {
|
|
||||||
yield 'Not related to any workflow nor attachment ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Force grant inverse the regular permission (workflow) ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Force grant inverse the regular permission (so) ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
|
||||||
];
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider dataProviderVoteOnAttributeWithoutStoredObjectPermission
|
|
||||||
*/
|
|
||||||
public function testVoteOnAttributeWithoutStoredObjectPermission(
|
|
||||||
StoredObjectRoleEnum $attribute,
|
StoredObjectRoleEnum $attribute,
|
||||||
bool $expected,
|
bool $expected,
|
||||||
bool $canBeAssociatedWithWorkflow,
|
bool $canBeAssociatedWithWorkflow,
|
||||||
@@ -261,10 +105,6 @@ class AbstractStoredObjectVoterTest extends TestCase
|
|||||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
||||||
|
|
||||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
|
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
|
||||||
|
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
|
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
|
|
||||||
|
|
||||||
if (null !== $isGrantedWorkflowPermissionRead) {
|
if (null !== $isGrantedWorkflowPermissionRead) {
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
|
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
|
||||||
->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled();
|
->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled();
|
||||||
@@ -283,7 +123,7 @@ class AbstractStoredObjectVoterTest extends TestCase
|
|||||||
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
|
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable
|
public static function dataProviderVoteOnAttribute(): iterable
|
||||||
{
|
{
|
||||||
// not associated on a workflow
|
// not associated on a workflow
|
||||||
yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
|
yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
|
||||||
|
@@ -23,8 +23,6 @@ See the document: Voir le document
|
|||||||
|
|
||||||
document:
|
document:
|
||||||
Any title: Aucun titre
|
Any title: Aucun titre
|
||||||
replace: Remplacer
|
|
||||||
Add: Ajouter un document
|
|
||||||
|
|
||||||
generic_doc:
|
generic_doc:
|
||||||
filter:
|
filter:
|
||||||
|
@@ -19,7 +19,6 @@ use Chill\MainBundle\Entity\User;
|
|||||||
use Chill\MainBundle\Entity\Address;
|
use Chill\MainBundle\Entity\Address;
|
||||||
use libphonenumber\PhoneNumber;
|
use libphonenumber\PhoneNumber;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immersion.
|
* Immersion.
|
||||||
@@ -86,14 +85,14 @@ class Immersion implements \Stringable
|
|||||||
* @Assert\NotBlank()
|
* @Assert\NotBlank()
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(name: 'tuteurPhoneNumber', type: 'phone_number', nullable: true)]
|
#[ORM\Column(name: 'tuteurPhoneNumber', type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint(type: 'any')]
|
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
|
||||||
private ?PhoneNumber $tuteurPhoneNumber = null;
|
private ?PhoneNumber $tuteurPhoneNumber = null;
|
||||||
|
|
||||||
#[ORM\Column(name: 'structureAccName', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
|
#[ORM\Column(name: 'structureAccName', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
|
||||||
private ?string $structureAccName = null;
|
private ?string $structureAccName = null;
|
||||||
|
|
||||||
#[ORM\Column(name: 'structureAccPhonenumber', type: 'phone_number', nullable: true)]
|
#[ORM\Column(name: 'structureAccPhonenumber', type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint(type: 'any')]
|
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
|
||||||
private ?PhoneNumber $structureAccPhonenumber = null;
|
private ?PhoneNumber $structureAccPhonenumber = null;
|
||||||
|
|
||||||
#[ORM\Column(name: 'structureAccEmail', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
|
#[ORM\Column(name: 'structureAccEmail', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
|
||||||
|
@@ -118,7 +118,7 @@
|
|||||||
|
|
||||||
{{ entity.notes|chill_print_or_message("Aucune note", 'blockquote') }}
|
{{ entity.notes|chill_print_or_message("Aucune note", 'blockquote') }}
|
||||||
{% endblock crud_content_view_details %}
|
{% endblock crud_content_view_details %}
|
||||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
|
||||||
{% block content_view_actions_back %}
|
{% block content_view_actions_back %}
|
||||||
<li class="cancel">
|
<li class="cancel">
|
||||||
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_job_report_index', { 'person': entity.person.id }) }}">
|
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_job_report_index', { 'person': entity.person.id }) }}">
|
||||||
|
@@ -46,7 +46,6 @@
|
|||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
{% endblock crud_content_view_details %}
|
{% endblock crud_content_view_details %}
|
||||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content_view_actions_back %}
|
{% block content_view_actions_back %}
|
||||||
<li class="cancel">
|
<li class="cancel">
|
||||||
|
@@ -206,8 +206,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content_view_actions_after %}
|
{% block content_view_actions_after %}
|
||||||
<li>
|
<li>
|
||||||
<a class="btn btn-misc" href="{{ chill_return_path_or('chill_crud_immersion_bilan', { 'id': entity.id, 'person_id': entity.person.id }) }}">
|
<a class="btn btn-misc" href="{{ chill_return_path_or('chill_crud_immersion_bilan', { 'id': entity.id, 'person_id': entity.person.id }) }}">
|
||||||
|
@@ -94,7 +94,6 @@
|
|||||||
|
|
||||||
|
|
||||||
{% endblock crud_content_view_details %}
|
{% endblock crud_content_view_details %}
|
||||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content_view_actions_back %}
|
{% block content_view_actions_back %}
|
||||||
<li class="cancel">
|
<li class="cancel">
|
||||||
|
@@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Action\User\UpdateProfile;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Notification\NotificationFlagManager;
|
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
|
||||||
use libphonenumber\PhoneNumber;
|
|
||||||
|
|
||||||
final class UpdateProfileCommand
|
|
||||||
{
|
|
||||||
public array $notificationFlags = [];
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
#[PhonenumberConstraint]
|
|
||||||
public ?PhoneNumber $phonenumber,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public static function create(User $user, NotificationFlagManager $flagManager): self
|
|
||||||
{
|
|
||||||
$updateProfileCommand = new self($user->getPhonenumber());
|
|
||||||
|
|
||||||
foreach ($flagManager->getAllNotificationFlagProviders() as $provider) {
|
|
||||||
$updateProfileCommand->setNotificationFlag(
|
|
||||||
$provider->getFlag(),
|
|
||||||
User::NOTIF_FLAG_IMMEDIATE_EMAIL,
|
|
||||||
$user->isNotificationSendImmediately($provider->getFlag())
|
|
||||||
);
|
|
||||||
$updateProfileCommand->setNotificationFlag(
|
|
||||||
$provider->getFlag(),
|
|
||||||
User::NOTIF_FLAG_DAILY_DIGEST,
|
|
||||||
$user->isNotificationDailyDigest($provider->getFlag())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $updateProfileCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param User::NOTIF_FLAG_IMMEDIATE_EMAIL|User::NOTIF_FLAG_DAILY_DIGEST $kind
|
|
||||||
*/
|
|
||||||
private function setNotificationFlag(string $type, string $kind, bool $value): void
|
|
||||||
{
|
|
||||||
if (!array_key_exists($type, $this->notificationFlags)) {
|
|
||||||
$this->notificationFlags[$type] = ['immediate_email' => true, 'daily_digest' => false];
|
|
||||||
}
|
|
||||||
|
|
||||||
$k = match ($kind) {
|
|
||||||
User::NOTIF_FLAG_IMMEDIATE_EMAIL => 'immediate_email',
|
|
||||||
User::NOTIF_FLAG_DAILY_DIGEST => 'daily_digest',
|
|
||||||
};
|
|
||||||
|
|
||||||
$this->notificationFlags[$type][$k] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Action\User\UpdateProfile;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
|
|
||||||
final readonly class UpdateProfileCommandHandler
|
|
||||||
{
|
|
||||||
public function updateProfile(User $user, UpdateProfileCommand $command): void
|
|
||||||
{
|
|
||||||
$user->setPhonenumber($command->phonenumber);
|
|
||||||
|
|
||||||
foreach ($command->notificationFlags as $flag => $values) {
|
|
||||||
$user->setNotificationImmediately($flag, $values['immediate_email']);
|
|
||||||
$user->setNotificationDailyDigest($flag, $values['daily_digest']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -14,7 +14,6 @@ namespace Chill\MainBundle;
|
|||||||
use Chill\MainBundle\Cron\CronJobInterface;
|
use Chill\MainBundle\Cron\CronJobInterface;
|
||||||
use Chill\MainBundle\CRUD\CompilerPass\CRUDControllerCompilerPass;
|
use Chill\MainBundle\CRUD\CompilerPass\CRUDControllerCompilerPass;
|
||||||
use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass;
|
use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass;
|
||||||
use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass;
|
|
||||||
use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass;
|
use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass;
|
||||||
use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass;
|
use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass;
|
||||||
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
|
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
|
||||||
@@ -70,7 +69,6 @@ class ChillMainBundle extends Bundle
|
|||||||
$container->addCompilerPass(new TimelineCompilerClass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
$container->addCompilerPass(new TimelineCompilerClass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||||
$container->addCompilerPass(new WidgetsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
$container->addCompilerPass(new WidgetsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||||
$container->addCompilerPass(new NotificationCounterCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
$container->addCompilerPass(new NotificationCounterCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||||
$container->addCompilerPass(new MenuCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
|
||||||
$container->addCompilerPass(new ACLFlagsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
$container->addCompilerPass(new ACLFlagsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||||
$container->addCompilerPass(new CRUDControllerCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
$container->addCompilerPass(new CRUDControllerCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\HttpKernel\KernelInterface;
|
||||||
|
use Symfony\Component\Translation\MessageCatalogue;
|
||||||
|
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
|
||||||
|
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
class OverrideTranslationCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly TranslationReaderInterface $reader,
|
||||||
|
private readonly TranslationWriterInterface $writer,
|
||||||
|
) {
|
||||||
|
$this->setName('chill:main:override_translation');
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setDescription('Generate a translation catalogue with translation remplacements based on replacements provided in a YAML file.')
|
||||||
|
->addArgument('locale', InputArgument::REQUIRED, 'The locale to process (e.g. fr, en).')
|
||||||
|
->addArgument('overrides', InputArgument::REQUIRED, 'Path to the overrides YAML file (list of {from, to}).');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$locale = (string) $input->getArgument('locale');
|
||||||
|
$overridesPath = (string) $input->getArgument('overrides');
|
||||||
|
|
||||||
|
$catalogue = $this->loadCatalogue($locale);
|
||||||
|
$overrides = $this->loadOverrides($overridesPath);
|
||||||
|
|
||||||
|
|
||||||
|
$toOverrideCatalogue = new MessageCatalogue($locale);
|
||||||
|
foreach ($catalogue->getDomains() as $domain) {
|
||||||
|
// hack: we have to replace the suffix ".intl-icu" by "+intl-ic"
|
||||||
|
$domain = str_replace('.intl-icu', '+intl-icu', $domain);
|
||||||
|
foreach ($catalogue->all($domain) as $key => $translation) {
|
||||||
|
foreach ($overrides as $changes) {
|
||||||
|
$from = $changes['from'];
|
||||||
|
$to = $changes['to'];
|
||||||
|
|
||||||
|
if (is_string($translation) && str_contains($translation, $from)) {
|
||||||
|
$newTranslation = strtr($translation, [$from => $to]);
|
||||||
|
$toOverrideCatalogue->set($key, $newTranslation, $domain);
|
||||||
|
$translation = $newTranslation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var KernelInterface $kernel */
|
||||||
|
/* @phpstan-ignore-next-line */
|
||||||
|
$kernel = $this->getApplication()->getKernel();
|
||||||
|
$outputDir = rtrim($kernel->getProjectDir(), '/').'/translations';
|
||||||
|
if (!is_dir($outputDir)) {
|
||||||
|
@mkdir($outputDir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writer expects the 'path' option to be a directory; it will create the proper file name
|
||||||
|
$this->writer->write($toOverrideCatalogue, 'yaml', ['path' => $outputDir]);
|
||||||
|
|
||||||
|
$output->writeln(sprintf('Override catalogue written to %s (domain: messages, locale: %s).', $outputDir, $locale));
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{from: string, to: string}>
|
||||||
|
*/
|
||||||
|
private function loadOverrides(string $path): array
|
||||||
|
{
|
||||||
|
return Yaml::parseFile($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadCatalogue(string $locale): MessageCatalogue
|
||||||
|
{
|
||||||
|
/** @var KernelInterface $kernel */
|
||||||
|
/* @phpstan-ignore-next-line */
|
||||||
|
$kernel = $this->getApplication()->getKernel();
|
||||||
|
|
||||||
|
// collect path for translations
|
||||||
|
$transPaths = [];
|
||||||
|
foreach ($kernel->getBundles() as $bundle) {
|
||||||
|
$bundleDir = $bundle->getPath();
|
||||||
|
$transPaths[] = is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundle->getPath().'/translations';
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentCatalogue = new MessageCatalogue($locale);
|
||||||
|
foreach ($transPaths as $path) {
|
||||||
|
if (is_dir($path)) {
|
||||||
|
$this->reader->read($path, $currentCatalogue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $currentCatalogue;
|
||||||
|
}
|
||||||
|
}
|
@@ -48,7 +48,6 @@ class AbsenceController extends AbstractController
|
|||||||
$user = $this->security->getUser();
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
$user->setAbsenceStart(null);
|
$user->setAbsenceStart(null);
|
||||||
$user->setAbsenceEnd(null);
|
|
||||||
$em = $this->managerRegistry->getManager();
|
$em = $this->managerRegistry->getManager();
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
|
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Form\UserProfileType;
|
||||||
|
use Chill\MainBundle\Security\ChillSecurity;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
final class UserProfileController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly TranslatorInterface $translator,
|
||||||
|
private readonly ChillSecurity $security,
|
||||||
|
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User profile that allows editing of phonenumber and visualization of certain data.
|
||||||
|
*/
|
||||||
|
#[Route(path: '/{_locale}/main/user/my-profile', name: 'chill_main_user_profile')]
|
||||||
|
public function __invoke(Request $request)
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted('ROLE_USER')) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
$editForm = $this->createForm(UserProfileType::class, $user);
|
||||||
|
|
||||||
|
$editForm->get('notificationFlags')->setData($user->getNotificationFlags());
|
||||||
|
|
||||||
|
$editForm->handleRequest($request);
|
||||||
|
|
||||||
|
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||||
|
$notificationFlagsData = $editForm->get('notificationFlags')->getData();
|
||||||
|
$user->setNotificationFlags($notificationFlagsData);
|
||||||
|
|
||||||
|
$em = $this->managerRegistry->getManager();
|
||||||
|
$em->flush();
|
||||||
|
$this->addFlash('success', $this->translator->trans('user.profile.Profile successfully updated!'));
|
||||||
|
|
||||||
|
return $this->redirectToRoute('chill_main_user_profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('@ChillMain/User/profile.html.twig', [
|
||||||
|
'user' => $user,
|
||||||
|
'form' => $editForm->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Controller;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
|
|
||||||
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Form\UpdateProfileType;
|
|
||||||
use Chill\MainBundle\Notification\NotificationFlagManager;
|
|
||||||
use Chill\MainBundle\Security\ChillSecurity;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Component\Form\FormFactoryInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\HttpFoundation\Session\Session;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
use Twig\Environment;
|
|
||||||
|
|
||||||
final readonly class UserUpdateProfileController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private TranslatorInterface $translator,
|
|
||||||
private ChillSecurity $security,
|
|
||||||
private EntityManagerInterface $entityManager,
|
|
||||||
private NotificationFlagManager $notificationFlagManager,
|
|
||||||
private FormFactoryInterface $formFactory,
|
|
||||||
private UrlGeneratorInterface $urlGenerator,
|
|
||||||
private Environment $twig,
|
|
||||||
private UpdateProfileCommandHandler $updateProfileCommandHandler,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User profile that allows editing of phonenumber and visualization of certain data.
|
|
||||||
*/
|
|
||||||
#[Route(path: '/{_locale}/main/user/my-profile', name: 'chill_main_user_profile')]
|
|
||||||
public function __invoke(Request $request, Session $session)
|
|
||||||
{
|
|
||||||
if (!$this->security->isGranted('ROLE_USER')) {
|
|
||||||
throw new AccessDeniedHttpException();
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $this->security->getUser();
|
|
||||||
|
|
||||||
$command = UpdateProfileCommand::create($user, $this->notificationFlagManager);
|
|
||||||
$editForm = $this->formFactory->create(UpdateProfileType::class, $command);
|
|
||||||
|
|
||||||
$editForm->handleRequest($request);
|
|
||||||
|
|
||||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
|
||||||
$this->updateProfileCommandHandler->updateProfile($user, $command);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
$session->getFlashBag()->add('success', $this->translator->trans('user.profile.Profile successfully updated!'));
|
|
||||||
|
|
||||||
return new RedirectResponse($this->urlGenerator->generate('chill_main_user_profile'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response($this->twig->render('@ChillMain/User/profile.html.twig', [
|
|
||||||
'user' => $user,
|
|
||||||
'form' => $editForm->createView(),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -11,7 +11,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Controller;
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
@@ -28,7 +27,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
|||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
|
||||||
class WorkflowApiController extends ApiController
|
class WorkflowApiController
|
||||||
{
|
{
|
||||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly PaginatorFactory $paginatorFactory, private readonly Security $security, private readonly SerializerInterface $serializer) {}
|
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly PaginatorFactory $paginatorFactory, private readonly Security $security, private readonly SerializerInterface $serializer) {}
|
||||||
|
|
||||||
|
@@ -44,7 +44,7 @@ final readonly class WorkflowSignatureStateChangeController
|
|||||||
$signature,
|
$signature,
|
||||||
$request,
|
$request,
|
||||||
EntityWorkflowStepSignatureVoter::CANCEL,
|
EntityWorkflowStepSignatureVoter::CANCEL,
|
||||||
fn (EntityWorkflowStepSignature $signature): string => $this->signatureStepStateChanger->markSignatureAsCanceled($signature),
|
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsCanceled($signature); },
|
||||||
'@ChillMain/WorkflowSignature/cancel.html.twig',
|
'@ChillMain/WorkflowSignature/cancel.html.twig',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -56,18 +56,11 @@ final readonly class WorkflowSignatureStateChangeController
|
|||||||
$signature,
|
$signature,
|
||||||
$request,
|
$request,
|
||||||
EntityWorkflowStepSignatureVoter::REJECT,
|
EntityWorkflowStepSignatureVoter::REJECT,
|
||||||
fn (EntityWorkflowStepSignature $signature): string => $this->signatureStepStateChanger->markSignatureAsRejected($signature),
|
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsRejected($signature); },
|
||||||
'@ChillMain/WorkflowSignature/reject.html.twig',
|
'@ChillMain/WorkflowSignature/reject.html.twig',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callable(EntityWorkflowStepSignature): string $markSignature
|
|
||||||
*
|
|
||||||
* @throws \Twig\Error\LoaderError
|
|
||||||
* @throws \Twig\Error\RuntimeError
|
|
||||||
* @throws \Twig\Error\SyntaxError
|
|
||||||
*/
|
|
||||||
private function markSignatureAction(
|
private function markSignatureAction(
|
||||||
EntityWorkflowStepSignature $signature,
|
EntityWorkflowStepSignature $signature,
|
||||||
Request $request,
|
Request $request,
|
||||||
@@ -86,13 +79,12 @@ final readonly class WorkflowSignatureStateChangeController
|
|||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$expectedStep = $this->entityManager->wrapInTransaction(fn () => $markSignature($signature));
|
$this->entityManager->wrapInTransaction(function () use ($signature, $markSignature) {
|
||||||
|
$markSignature($signature);
|
||||||
|
});
|
||||||
|
|
||||||
return new RedirectResponse(
|
return new RedirectResponse(
|
||||||
$this->chillUrlGenerator->forwardReturnPath(
|
$this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()])
|
||||||
'chill_main_workflow_wait',
|
|
||||||
['id' => $signature->getStep()->getEntityWorkflow()->getId(), 'expectedStep' => $expectedStep]
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Controller;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
use Twig\Environment;
|
|
||||||
|
|
||||||
final readonly class WorkflowWaitStepChangeController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private ChillUrlGeneratorInterface $chillUrlGenerator,
|
|
||||||
private Environment $twig,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
#[Route('/{_locale}/main/workflow/{id}/wait/{expectedStep}', name: 'chill_main_workflow_wait', methods: ['GET'])]
|
|
||||||
public function waitForSignatureChange(EntityWorkflow $entityWorkflow, string $expectedStep): Response
|
|
||||||
{
|
|
||||||
if ($entityWorkflow->getStep() === $expectedStep) {
|
|
||||||
return new RedirectResponse(
|
|
||||||
$this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $entityWorkflow->getId()])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
$this->twig->render('@ChillMain/Workflow/waiting.html.twig', ['workflow' => $entityWorkflow, 'expectedStep' => $expectedStep])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -30,7 +30,6 @@ use Chill\MainBundle\Controller\UserGroupAdminController;
|
|||||||
use Chill\MainBundle\Controller\UserGroupApiController;
|
use Chill\MainBundle\Controller\UserGroupApiController;
|
||||||
use Chill\MainBundle\Controller\UserJobApiController;
|
use Chill\MainBundle\Controller\UserJobApiController;
|
||||||
use Chill\MainBundle\Controller\UserJobController;
|
use Chill\MainBundle\Controller\UserJobController;
|
||||||
use Chill\MainBundle\Controller\WorkflowApiController;
|
|
||||||
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
|
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
|
||||||
use Chill\MainBundle\Doctrine\DQL\Age;
|
use Chill\MainBundle\Doctrine\DQL\Age;
|
||||||
use Chill\MainBundle\Doctrine\DQL\Extract;
|
use Chill\MainBundle\Doctrine\DQL\Extract;
|
||||||
@@ -67,7 +66,6 @@ use Chill\MainBundle\Entity\Regroupment;
|
|||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\UserGroup;
|
use Chill\MainBundle\Entity\UserGroup;
|
||||||
use Chill\MainBundle\Entity\UserJob;
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Chill\MainBundle\Form\CenterType;
|
use Chill\MainBundle\Form\CenterType;
|
||||||
use Chill\MainBundle\Form\CivilityType;
|
use Chill\MainBundle\Form\CivilityType;
|
||||||
use Chill\MainBundle\Form\CountryType;
|
use Chill\MainBundle\Form\CountryType;
|
||||||
@@ -81,7 +79,6 @@ use Chill\MainBundle\Form\UserGroupType;
|
|||||||
use Chill\MainBundle\Form\UserJobType;
|
use Chill\MainBundle\Form\UserJobType;
|
||||||
use Chill\MainBundle\Form\UserType;
|
use Chill\MainBundle\Form\UserType;
|
||||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
|
||||||
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
|
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
|
||||||
use Ramsey\Uuid\Doctrine\UuidType;
|
use Ramsey\Uuid\Doctrine\UuidType;
|
||||||
use Symfony\Component\Config\FileLocator;
|
use Symfony\Component\Config\FileLocator;
|
||||||
@@ -943,21 +940,6 @@ class ChillMainExtension extends Extension implements
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
|
||||||
'class' => EntityWorkflow::class,
|
|
||||||
'name' => 'workflow',
|
|
||||||
'base_path' => '/api/1.0/main/workflow',
|
|
||||||
'base_role' => EntityWorkflowVoter::SEE,
|
|
||||||
'controller' => WorkflowApiController::class,
|
|
||||||
'actions' => [
|
|
||||||
'_entity' => [
|
|
||||||
'methods' => [
|
|
||||||
Request::METHOD_GET => true,
|
|
||||||
Request::METHOD_HEAD => true,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@@ -14,9 +14,9 @@ namespace Chill\MainBundle\Entity;
|
|||||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||||
use Chill\MainBundle\Repository\LocationRepository;
|
use Chill\MainBundle\Repository\LocationRepository;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use libphonenumber\PhoneNumber;
|
use libphonenumber\PhoneNumber;
|
||||||
|
use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber as MisdPhoneNumberConstraint;
|
||||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||||
|
|
||||||
@@ -67,12 +67,12 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
|
|
||||||
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
|
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
|
||||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint(type: 'any')]
|
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::ANY])]
|
||||||
private ?PhoneNumber $phonenumber1 = null;
|
private ?PhoneNumber $phonenumber1 = null;
|
||||||
|
|
||||||
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
|
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
|
||||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint(type: 'any')]
|
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::ANY])]
|
||||||
private ?PhoneNumber $phonenumber2 = null;
|
private ?PhoneNumber $phonenumber2 = null;
|
||||||
|
|
||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
|
@@ -23,8 +23,6 @@ use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
|||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User.
|
* User.
|
||||||
@@ -46,8 +44,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||||
private ?\DateTimeImmutable $absenceStart = null;
|
private ?\DateTimeImmutable $absenceStart = null;
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
|
||||||
private ?\DateTimeImmutable $absenceEnd = null;
|
|
||||||
/**
|
/**
|
||||||
* Array where SAML attributes's data are stored.
|
* Array where SAML attributes's data are stored.
|
||||||
*/
|
*/
|
||||||
@@ -119,7 +115,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
* The user's mobile phone number.
|
* The user's mobile phone number.
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint]
|
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
|
||||||
private ?PhoneNumber $phonenumber = null;
|
private ?PhoneNumber $phonenumber = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,11 +156,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
return $this->absenceStart;
|
return $this->absenceStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAbsenceEnd(): ?\DateTimeImmutable
|
|
||||||
{
|
|
||||||
return $this->absenceEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get attributes.
|
* Get attributes.
|
||||||
*
|
*
|
||||||
@@ -344,13 +335,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
|
|
||||||
public function isAbsent(): bool
|
public function isAbsent(): bool
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable('now');
|
return null !== $this->getAbsenceStart() && $this->getAbsenceStart() <= new \DateTimeImmutable('now');
|
||||||
$absenceStart = $this->getAbsenceStart();
|
|
||||||
$absenceEnd = $this->getAbsenceEnd();
|
|
||||||
|
|
||||||
return null !== $absenceStart
|
|
||||||
&& $absenceStart <= $now
|
|
||||||
&& (null === $absenceEnd || $now <= $absenceEnd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -424,11 +409,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
$this->absenceStart = $absenceStart;
|
$this->absenceStart = $absenceStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setAbsenceEnd(?\DateTimeImmutable $absenceEnd): void
|
|
||||||
{
|
|
||||||
$this->absenceEnd = $absenceEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setAttributeByDomain(string $domain, string $key, $value): self
|
public function setAttributeByDomain(string $domain, string $key, $value): self
|
||||||
{
|
{
|
||||||
$this->attributes[$domain][$key] = $value;
|
$this->attributes[$domain][$key] = $value;
|
||||||
@@ -652,82 +632,46 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getNotificationFlagData(string $flag): array
|
public function getNotificationFlags(): array
|
||||||
{
|
{
|
||||||
return $this->notificationFlags[$flag] ?? [self::NOTIF_FLAG_IMMEDIATE_EMAIL];
|
return $this->notificationFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNotificationFlags(array $notificationFlags)
|
||||||
|
{
|
||||||
|
$this->notificationFlags = $notificationFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNotificationFlagData(string $flag): array
|
||||||
|
{
|
||||||
|
return $this->notificationFlags[$flag] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNotificationFlagData(string $flag, array $data): void
|
||||||
|
{
|
||||||
|
$this->notificationFlags[$flag] = $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isNotificationSendImmediately(string $type): bool
|
public function isNotificationSendImmediately(string $type): bool
|
||||||
{
|
{
|
||||||
return $this->isNotificationForElement($type, self::NOTIF_FLAG_IMMEDIATE_EMAIL);
|
if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setNotificationImmediately(string $type, bool $active): void
|
return false;
|
||||||
{
|
|
||||||
$this->setNotificationFlagElement($type, $active, self::NOTIF_FLAG_IMMEDIATE_EMAIL);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setNotificationDailyDigest(string $type, bool $active): void
|
|
||||||
{
|
|
||||||
$this->setNotificationFlagElement($type, $active, self::NOTIF_FLAG_DAILY_DIGEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param self::NOTIF_FLAG_IMMEDIATE_EMAIL|self::NOTIF_FLAG_DAILY_DIGEST $kind
|
|
||||||
*/
|
|
||||||
private function setNotificationFlagElement(string $type, bool $active, string $kind): void
|
|
||||||
{
|
|
||||||
$notificationFlags = [...$this->notificationFlags];
|
|
||||||
$changed = false;
|
|
||||||
|
|
||||||
if (!isset($notificationFlags[$type])) {
|
|
||||||
$notificationFlags[$type] = [self::NOTIF_FLAG_IMMEDIATE_EMAIL];
|
|
||||||
$changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($active) {
|
|
||||||
if (!in_array($kind, $notificationFlags[$type], true)) {
|
|
||||||
$notificationFlags[$type] = [...$notificationFlags[$type], $kind];
|
|
||||||
$changed = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (in_array($kind, $notificationFlags[$type], true)) {
|
|
||||||
$notificationFlags[$type] = array_values(
|
|
||||||
array_filter($notificationFlags[$type], static fn ($k) => $k !== $kind)
|
|
||||||
);
|
|
||||||
$changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($changed) {
|
|
||||||
$this->notificationFlags = [...$notificationFlags];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function isNotificationForElement(string $type, string $kind): bool
|
|
||||||
{
|
|
||||||
return in_array($kind, $this->getNotificationFlagData($type), true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isNotificationDailyDigest(string $type): bool
|
public function isNotificationDailyDigest(string $type): bool
|
||||||
{
|
{
|
||||||
return $this->isNotificationForElement($type, self::NOTIF_FLAG_DAILY_DIGEST);
|
if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLocale(): string
|
public function getLocale(): string
|
||||||
{
|
{
|
||||||
return 'fr';
|
return 'fr';
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Assert\Callback]
|
|
||||||
public function validateAbsenceDates(ExecutionContextInterface $context): void
|
|
||||||
{
|
|
||||||
if (null !== $this->getAbsenceEnd() && null === $this->getAbsenceStart()) {
|
|
||||||
$context->buildViolation(
|
|
||||||
'user.absence_end_requires_start'
|
|
||||||
)
|
|
||||||
->atPath('absenceEnd')
|
|
||||||
->addViolation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,8 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
// command to get the report with curl : curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a CSV List for the export.
|
* Create a CSV List for the export.
|
||||||
*/
|
*/
|
||||||
|
@@ -23,14 +23,9 @@ class AbsenceType extends AbstractType
|
|||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('absenceStart', ChillDateType::class, [
|
->add('absenceStart', ChillDateType::class, [
|
||||||
'required' => false,
|
'required' => true,
|
||||||
'input' => 'datetime_immutable',
|
'input' => 'datetime_immutable',
|
||||||
'label' => 'absence.Absence start',
|
'label' => 'absence.Absence start',
|
||||||
])
|
|
||||||
->add('absenceEnd', ChillDateType::class, [
|
|
||||||
'required' => false,
|
|
||||||
'input' => 'datetime_immutable',
|
|
||||||
'label' => 'absence.Absence end',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Form\DataMapper;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Symfony\Component\Form\DataMapperInterface;
|
||||||
|
|
||||||
|
final readonly class NotificationFlagDataMapper implements DataMapperInterface
|
||||||
|
{
|
||||||
|
public function __construct(private array $notificationFlagProviders) {}
|
||||||
|
|
||||||
|
public function mapDataToForms($viewData, $forms): void
|
||||||
|
{
|
||||||
|
if (null === $viewData) {
|
||||||
|
$viewData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$formsArray = iterator_to_array($forms);
|
||||||
|
|
||||||
|
foreach ($this->notificationFlagProviders as $flagProvider) {
|
||||||
|
$flag = $flagProvider->getFlag();
|
||||||
|
|
||||||
|
if (isset($formsArray[$flag])) {
|
||||||
|
$flagForm = $formsArray[$flag];
|
||||||
|
|
||||||
|
$immediateEmailChecked = in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $viewData[$flag] ?? [], true)
|
||||||
|
|| !array_key_exists($flag, $viewData);
|
||||||
|
$dailyEmailChecked = in_array(User::NOTIF_FLAG_DAILY_DIGEST, $viewData[$flag] ?? [], true);
|
||||||
|
|
||||||
|
if ($flagForm->has('immediate_email')) {
|
||||||
|
$flagForm->get('immediate_email')->setData($immediateEmailChecked);
|
||||||
|
}
|
||||||
|
if ($flagForm->has('daily_email')) {
|
||||||
|
$flagForm->get('daily_email')->setData($dailyEmailChecked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mapFormsToData($forms, &$viewData): void
|
||||||
|
{
|
||||||
|
$formsArray = iterator_to_array($forms);
|
||||||
|
$viewData = [];
|
||||||
|
|
||||||
|
foreach ($this->notificationFlagProviders as $flagProvider) {
|
||||||
|
$flag = $flagProvider->getFlag();
|
||||||
|
|
||||||
|
if (isset($formsArray[$flag])) {
|
||||||
|
$flagForm = $formsArray[$flag];
|
||||||
|
$viewData[$flag] = [];
|
||||||
|
|
||||||
|
if (true === $flagForm['immediate_email']->getData()) {
|
||||||
|
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true === $flagForm['daily_email']->getData()) {
|
||||||
|
$viewData[$flag][] = User::NOTIF_FLAG_DAILY_DIGEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([] === $viewData[$flag]) {
|
||||||
|
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -11,9 +11,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Form\Type;
|
namespace Chill\MainBundle\Form\Type;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper;
|
||||||
use Chill\MainBundle\Notification\NotificationFlagManager;
|
use Chill\MainBundle\Notification\NotificationFlagManager;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
@@ -28,24 +30,27 @@ class NotificationFlagsType extends AbstractType
|
|||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
|
$builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders));
|
||||||
|
|
||||||
foreach ($this->notificationFlagProviders as $flagProvider) {
|
foreach ($this->notificationFlagProviders as $flagProvider) {
|
||||||
$flag = $flagProvider->getFlag();
|
$flag = $flagProvider->getFlag();
|
||||||
$flagBuilder = $builder->create($flag, options: [
|
$builder->add($flag, FormType::class, [
|
||||||
'label' => $flagProvider->getLabel(),
|
'label' => $flagProvider->getLabel(),
|
||||||
'compound' => true,
|
'required' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$flagBuilder
|
$builder->get($flag)
|
||||||
->add('immediate_email', CheckboxType::class, [
|
->add('immediate_email', CheckboxType::class, [
|
||||||
'label' => false,
|
'label' => false,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
])
|
])
|
||||||
->add('daily_digest', CheckboxType::class, [
|
->add('daily_email', CheckboxType::class, [
|
||||||
'label' => false,
|
'label' => false,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
])
|
])
|
||||||
;
|
;
|
||||||
$builder->add($flagBuilder);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +58,6 @@ class NotificationFlagsType extends AbstractType
|
|||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'data_class' => null,
|
'data_class' => null,
|
||||||
'compound' => true,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -59,7 +59,7 @@ class UserPasswordType extends AbstractType
|
|||||||
'invalid_message' => 'The password fields must match',
|
'invalid_message' => 'The password fields must match',
|
||||||
'constraints' => [
|
'constraints' => [
|
||||||
new Length([
|
new Length([
|
||||||
'min' => 14,
|
'min' => 9,
|
||||||
'minMessage' => 'The password must be greater than {{ limit }} characters',
|
'minMessage' => 'The password must be greater than {{ limit }} characters',
|
||||||
]),
|
]),
|
||||||
new NotBlank(),
|
new NotBlank(),
|
||||||
|
@@ -11,29 +11,31 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Form;
|
namespace Chill\MainBundle\Form;
|
||||||
|
|
||||||
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
|
|
||||||
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
||||||
use Chill\MainBundle\Form\Type\NotificationFlagsType;
|
use Chill\MainBundle\Form\Type\NotificationFlagsType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
class UpdateProfileType extends AbstractType
|
class UserProfileType extends AbstractType
|
||||||
{
|
{
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('phonenumber', ChillPhoneNumberType::class, [
|
->add('phonenumber', ChillPhoneNumberType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
])
|
])
|
||||||
->add('notificationFlags', NotificationFlagsType::class)
|
->add('notificationFlags', NotificationFlagsType::class, [
|
||||||
|
'label' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver): void
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'data_class' => UpdateProfileCommand::class,
|
'data_class' => \Chill\MainBundle\Entity\User::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -105,11 +105,6 @@ class UserType extends AbstractType
|
|||||||
'required' => false,
|
'required' => false,
|
||||||
'input' => 'datetime_immutable',
|
'input' => 'datetime_immutable',
|
||||||
'label' => 'absence.Absence start',
|
'label' => 'absence.Absence start',
|
||||||
])
|
|
||||||
->add('absenceEnd', ChillDateType::class, [
|
|
||||||
'required' => false,
|
|
||||||
'input' => 'datetime_immutable',
|
|
||||||
'label' => 'absence.Absence end',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// @phpstan-ignore-next-line
|
// @phpstan-ignore-next-line
|
||||||
|
@@ -31,6 +31,8 @@ interface PhoneNumberHelperInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if the validation is configured and available.
|
* Return true if the validation is configured and available.
|
||||||
|
*
|
||||||
|
* @deprecated this is an internal behaviour of the helper and should not be taken into account outside of the implementation
|
||||||
*/
|
*/
|
||||||
public function isPhonenumberValidationConfigured(): bool;
|
public function isPhonenumberValidationConfigured(): bool;
|
||||||
|
|
||||||
|
@@ -76,6 +76,24 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']);
|
->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NumberParseException
|
||||||
|
*/
|
||||||
|
public function parse(string $phoneNumber): PhoneNumber
|
||||||
|
{
|
||||||
|
$sanitizedPhoneNumber = $phoneNumber;
|
||||||
|
|
||||||
|
if (str_starts_with($sanitizedPhoneNumber, '00')) {
|
||||||
|
$sanitizedPhoneNumber = '+'.substr($sanitizedPhoneNumber, 2, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!str_starts_with($sanitizedPhoneNumber, '+') && !str_starts_with($sanitizedPhoneNumber, '0')) {
|
||||||
|
$sanitizedPhoneNumber = '+'.$sanitizedPhoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->phoneNumberUtil->parse($sanitizedPhoneNumber, $this->config['default_carrier_code']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get type (mobile, landline, ...) for phone number.
|
* Get type (mobile, landline, ...) for phone number.
|
||||||
*/
|
*/
|
||||||
@@ -104,7 +122,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
*/
|
*/
|
||||||
public function isValidPhonenumberAny($phonenumber): bool
|
public function isValidPhonenumberAny($phonenumber): bool
|
||||||
{
|
{
|
||||||
if (false === $this->isPhonenumberValidationConfigured()) {
|
if (false === $this->isConfigured) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
$validation = $this->performTwilioLookup($phonenumber);
|
$validation = $this->performTwilioLookup($phonenumber);
|
||||||
@@ -124,7 +142,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
*/
|
*/
|
||||||
public function isValidPhonenumberLandOrVoip($phonenumber): bool
|
public function isValidPhonenumberLandOrVoip($phonenumber): bool
|
||||||
{
|
{
|
||||||
if (false === $this->isPhonenumberValidationConfigured()) {
|
if (false === $this->isConfigured) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +163,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
*/
|
*/
|
||||||
public function isValidPhonenumberMobile($phonenumber): bool
|
public function isValidPhonenumberMobile($phonenumber): bool
|
||||||
{
|
{
|
||||||
if (false === $this->isPhonenumberValidationConfigured()) {
|
if (false === $this->isConfigured) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +178,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
|
|
||||||
private function performTwilioLookup($phonenumber)
|
private function performTwilioLookup($phonenumber)
|
||||||
{
|
{
|
||||||
if (false === $this->isPhonenumberValidationConfigured()) {
|
if (false === $this->isConfigured) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -37,13 +37,8 @@ export const ISOToDate = (str: string | null): Date | null => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the string already contains time info, use it directly
|
|
||||||
if (str.includes("T") || str.includes(" ")) {
|
|
||||||
return new Date(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, parse date only
|
|
||||||
const [year, month, day] = str.split("-").map((p) => parseInt(p));
|
const [year, month, day] = str.split("-").map((p) => parseInt(p));
|
||||||
|
|
||||||
return new Date(year, month - 1, day, 0, 0, 0, 0);
|
return new Date(year, month - 1, day, 0, 0, 0, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,9 +56,7 @@ export const ISOToDatetime = (str: string | null): Date | null => {
|
|||||||
[time, timezone] = times.split(times.charAt(8)),
|
[time, timezone] = times.split(times.charAt(8)),
|
||||||
[hours, minutes, seconds] = time.split(":").map((s) => parseInt(s));
|
[hours, minutes, seconds] = time.split(":").map((s) => parseInt(s));
|
||||||
if ("0000" === timezone) {
|
if ("0000" === timezone) {
|
||||||
return new Date(
|
return new Date(Date.UTC(year, month - 1, date, hours, minutes, seconds));
|
||||||
Date.UTC(year, month - 1, date, hours, minutes, seconds),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Date(year, month - 1, date, hours, minutes, seconds);
|
return new Date(year, month - 1, date, hours, minutes, seconds);
|
||||||
@@ -74,19 +67,20 @@ export const ISOToDatetime = (str: string | null): Date | null => {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const datetimeToISO = (date: Date): string => {
|
export const datetimeToISO = (date: Date): string => {
|
||||||
const cal = [
|
let cal, time, offset;
|
||||||
|
cal = [
|
||||||
date.getFullYear(),
|
date.getFullYear(),
|
||||||
(date.getMonth() + 1).toString().padStart(2, "0"),
|
(date.getMonth() + 1).toString().padStart(2, "0"),
|
||||||
date.getDate().toString().padStart(2, "0"),
|
date.getDate().toString().padStart(2, "0"),
|
||||||
].join("-");
|
].join("-");
|
||||||
|
|
||||||
const time = [
|
time = [
|
||||||
date.getHours().toString().padStart(2, "0"),
|
date.getHours().toString().padStart(2, "0"),
|
||||||
date.getMinutes().toString().padStart(2, "0"),
|
date.getMinutes().toString().padStart(2, "0"),
|
||||||
date.getSeconds().toString().padStart(2, "0"),
|
date.getSeconds().toString().padStart(2, "0"),
|
||||||
].join(":");
|
].join(":");
|
||||||
|
|
||||||
const offset = [
|
offset = [
|
||||||
date.getTimezoneOffset() <= 0 ? "+" : "-",
|
date.getTimezoneOffset() <= 0 ? "+" : "-",
|
||||||
Math.abs(Math.floor(date.getTimezoneOffset() / 60))
|
Math.abs(Math.floor(date.getTimezoneOffset() / 60))
|
||||||
.toString()
|
.toString()
|
||||||
@@ -158,11 +152,24 @@ export const intervalISOToDays = (str: string | null): number | null => {
|
|||||||
vstring = "";
|
vstring = "";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw Error(
|
throw Error("this character should not appears: " + str.charAt(i));
|
||||||
"this character should not appears: " + str.charAt(i),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return days;
|
return days;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getTimezoneOffsetString(date: Date, timeZone: string): string {
|
||||||
|
const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
|
||||||
|
const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
|
||||||
|
const offsetMinutes = (utcDate.getTime() - tzDate.getTime()) / (60 * 1000);
|
||||||
|
|
||||||
|
// Inverser le signe pour avoir la convention ±HH:MM
|
||||||
|
const sign = offsetMinutes <= 0 ? "+" : "-";
|
||||||
|
const absMinutes = Math.abs(offsetMinutes);
|
||||||
|
const hours = String(Math.floor(absMinutes / 60)).padStart(2, "0");
|
||||||
|
const minutes = String(absMinutes % 60).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${sign}${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -1,15 +1,26 @@
|
|||||||
import { Scope } from "../../types";
|
import {
|
||||||
|
DynamicKeys,
|
||||||
|
Scope,
|
||||||
|
ValidationExceptionInterface,
|
||||||
|
ValidationProblemFromMap,
|
||||||
|
ViolationFromMap
|
||||||
|
} from "../../types";
|
||||||
|
|
||||||
export type body = Record<string, boolean | string | number | null>;
|
export type body = Record<string, boolean | string | number | null>;
|
||||||
export type fetchOption = Record<string, boolean | string | number | null>;
|
export type fetchOption = Record<string, boolean | string | number | null>;
|
||||||
|
export type Primitive = string | number | boolean | null;
|
||||||
export type Params = Record<string, number | string>;
|
export type Params = Record<string, number | string>;
|
||||||
|
|
||||||
export interface PaginationResponse<T> {
|
export interface Pagination {
|
||||||
pagination: {
|
first: number;
|
||||||
more: boolean;
|
|
||||||
items_per_page: number;
|
items_per_page: number;
|
||||||
};
|
more: boolean;
|
||||||
|
next: string | null;
|
||||||
|
previous: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationResponse<T> {
|
||||||
|
pagination: Pagination;
|
||||||
results: T[];
|
results: T[];
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
@@ -20,20 +31,115 @@ export interface TransportExceptionInterface {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidationExceptionInterface
|
export class ValidationException<
|
||||||
extends TransportExceptionInterface {
|
M extends Record<string, Record<string, string|number>> = Record<
|
||||||
name: "ValidationException";
|
string,
|
||||||
error: object;
|
Record<string, string|number>
|
||||||
violations: string[];
|
>,
|
||||||
titles: string[];
|
>
|
||||||
propertyPaths: string[];
|
extends Error
|
||||||
|
implements ValidationExceptionInterface<M>
|
||||||
|
{
|
||||||
|
public readonly name = "ValidationException" as const;
|
||||||
|
public readonly problems: ValidationProblemFromMap<M>;
|
||||||
|
public readonly violations: string[];
|
||||||
|
public readonly violationsList: ViolationFromMap<M>[];
|
||||||
|
public readonly titles: string[];
|
||||||
|
public readonly propertyPaths: DynamicKeys<M> & string[];
|
||||||
|
public readonly byProperty: Record<Extract<keyof M, string>, string[]>;
|
||||||
|
|
||||||
|
constructor(problem: ValidationProblemFromMap<M>) {
|
||||||
|
const message = [problem.title, problem.detail].filter(Boolean).join(" — ");
|
||||||
|
super(message);
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
|
||||||
|
this.problems = problem;
|
||||||
|
|
||||||
|
this.violationsList = problem.violations;
|
||||||
|
this.violations = problem.violations.map(
|
||||||
|
(v) => `${v.title}: ${v.propertyPath}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.titles = problem.violations.map((v) => v.title);
|
||||||
|
|
||||||
|
this.propertyPaths = problem.violations.map(
|
||||||
|
(v) => v.propertyPath,
|
||||||
|
) as DynamicKeys<M> & string[];
|
||||||
|
|
||||||
|
this.byProperty = problem.violations.reduce(
|
||||||
|
(acc, v) => {
|
||||||
|
const key = v.propertyPath.replace('/\[\d+\]$/', "") as Extract<keyof M, string>;
|
||||||
|
(acc[key] ||= []).push(v.title);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<Extract<keyof M, string>, string[]>,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, ValidationException);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidationErrorResponse extends TransportExceptionInterface {
|
violationsByNormalizedProperty(property: Extract<keyof M, string>): ViolationFromMap<M>[] {
|
||||||
violations: {
|
return this.violationsList.filter((v) => v.propertyPath.replace(/\[\d+\]$/, "") === property);
|
||||||
|
}
|
||||||
|
|
||||||
|
violationsByNormalizedPropertyAndParams<
|
||||||
|
P extends Extract<keyof M, string>,
|
||||||
|
K extends Extract<keyof M[P], string>
|
||||||
|
>(
|
||||||
|
property: P,
|
||||||
|
param: K,
|
||||||
|
param_value: M[P][K]
|
||||||
|
): ViolationFromMap<M>[]
|
||||||
|
{
|
||||||
|
const list = this.violationsByNormalizedProperty(property);
|
||||||
|
|
||||||
|
return list.filter(
|
||||||
|
(v): boolean =>
|
||||||
|
!!v.parameters &&
|
||||||
|
// `with_parameter in v.parameters` check indexing
|
||||||
|
param in v.parameters &&
|
||||||
|
// the cast is safe, because we have overloading that bind the types
|
||||||
|
(v.parameters as M[P])[param] === param_value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the exception is a ValidationExceptionInterface
|
||||||
|
* @param x
|
||||||
|
*/
|
||||||
|
export function isValidationException<M extends Record<string, Record<string, string|number>>>(
|
||||||
|
x: unknown,
|
||||||
|
): x is ValidationExceptionInterface<M> {
|
||||||
|
return (
|
||||||
|
x instanceof ValidationException ||
|
||||||
|
(typeof x === "object" &&
|
||||||
|
x !== null &&
|
||||||
|
(x as any).name === "ValidationException")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidationProblem(x: unknown): x is {
|
||||||
|
type: string;
|
||||||
title: string;
|
title: string;
|
||||||
propertyPath: string;
|
violations: { propertyPath: string; title: string }[];
|
||||||
}[];
|
} {
|
||||||
|
if (!x || typeof x !== "object") return false;
|
||||||
|
const o = x as any;
|
||||||
|
return (
|
||||||
|
typeof o.type === "string" &&
|
||||||
|
typeof o.title === "string" &&
|
||||||
|
Array.isArray(o.violations) &&
|
||||||
|
o.violations.every(
|
||||||
|
(v: any) =>
|
||||||
|
v &&
|
||||||
|
typeof v === "object" &&
|
||||||
|
typeof v.propertyPath === "string" &&
|
||||||
|
typeof v.title === "string",
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccessExceptionInterface extends TransportExceptionInterface {
|
export interface AccessExceptionInterface extends TransportExceptionInterface {
|
||||||
@@ -60,12 +166,151 @@ export interface ConflictHttpExceptionInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic api method that can be adapted to any fetch request
|
* Generic api method that can be adapted to any fetch request.
|
||||||
*
|
*
|
||||||
* This method is suitable make a single fetch. When performing a GET to fetch a list of elements, always consider pagination
|
* What this does
|
||||||
* and use of the @link{fetchResults} method.
|
* - Performs a single HTTP request using fetch and returns the parsed JSON as Output.
|
||||||
|
* - Interprets common API errors and throws typed exceptions you can catch in your UI.
|
||||||
|
* - When the server returns a Symfony validation problem (HTTP 422), the error is
|
||||||
|
* rethrown as a typed ValidationException that is aware of your Violation Map (see below).
|
||||||
|
*
|
||||||
|
* Important: For GET endpoints that return lists, prefer using fetchResults, which
|
||||||
|
* handles pagination and aggregation for you.
|
||||||
|
*
|
||||||
|
* Violation Map (M): make your 422 errors strongly typed
|
||||||
|
* ------------------------------------------------------
|
||||||
|
* Symfony’s validation problem+json payload looks like this (simplified):
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "type": "https://symfony.com/errors/validation",
|
||||||
|
* "title": "Validation Failed",
|
||||||
|
* "violations": [
|
||||||
|
* {
|
||||||
|
* "propertyPath": "mobilenumber",
|
||||||
|
* "title": "This value is not a valid phone number.",
|
||||||
|
* "parameters": {
|
||||||
|
* "{{ value }}": "+33 1 02 03 04 05",
|
||||||
|
* "{{ types }}": "mobile number"
|
||||||
|
* },
|
||||||
|
* "type": "urn:uuid:..."
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* The makeFetch generic type parameter M lets you describe, field by field, which
|
||||||
|
* parameters may appear for each propertyPath. Doing so gives you full type-safety when
|
||||||
|
* consuming ValidationException in your UI code.
|
||||||
|
*
|
||||||
|
* How to build M (Violation Map)
|
||||||
|
* - M is a map where each key is a server-side propertyPath (string), and the value is a
|
||||||
|
* record describing the allowed keys in the parameters object for that property.
|
||||||
|
* - Keys in parameters are the exact strings you receive from Symfony, including the
|
||||||
|
* curly-braced placeholders such as "{{ value }}", "{{ types }}", etc.
|
||||||
|
*
|
||||||
|
* Example from Person creation (WritePersonViolationMap)
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* In ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.ts you’ll find:
|
||||||
|
*
|
||||||
|
* export type WritePersonViolationMap = {
|
||||||
|
* gender: {
|
||||||
|
* "{{ value }}": string | null;
|
||||||
|
* };
|
||||||
|
* mobilenumber: {
|
||||||
|
* "{{ types }}": string; // ex: "mobile number"
|
||||||
|
* "{{ value }}": string; // ex: "+33 1 02 03 04 05"
|
||||||
|
* };
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* This means:
|
||||||
|
* - If the server reports a violation for propertyPath "gender", the parameters object
|
||||||
|
* is expected to contain a key "{{ value }}" with a string or null value.
|
||||||
|
* - If the server reports a violation for propertyPath "mobilenumber", the parameters
|
||||||
|
* may include "{{ value }}" and "{{ types }}" as strings.
|
||||||
|
*
|
||||||
|
* How makeFetch uses M
|
||||||
|
* - When the response has status 422 and the payload matches a Symfony validation
|
||||||
|
* problem, makeFetch casts it to ValidationProblemFromMap<M> and throws a
|
||||||
|
* ValidationException<M>.
|
||||||
|
* - The ValidationException exposes helpful, pre-computed fields:
|
||||||
|
* - exception.problem: the full typed payload
|
||||||
|
* - exception.violations: ["Title: propertyPath", ...]
|
||||||
|
* - exception.titles: ["Title 1", "Title 2", ...]
|
||||||
|
* - exception.propertyPaths: ["gender", "mobilenumber", ...] (typed from M)
|
||||||
|
* - exception.byProperty: { gender: [titles...], mobilenumber: [titles...] }
|
||||||
|
*
|
||||||
|
* Typical usage patterns
|
||||||
|
* ----------------------
|
||||||
|
* 1) GET without Validation Map (no 422 expected):
|
||||||
|
*
|
||||||
|
* const centers = await makeFetch<null, { showCenters: boolean; centers: Center[] }>(
|
||||||
|
* "GET",
|
||||||
|
* "/api/1.0/person/creation/authorized-centers",
|
||||||
|
* null
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* 2) POST with body and Violation Map:
|
||||||
|
*
|
||||||
|
* type WritePersonViolationMap = {
|
||||||
|
* gender: { "{{ value }}": string | null };
|
||||||
|
* mobilenumber: { "{{ types }}": string; "{{ value }}": string };
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* try {
|
||||||
|
* const created = await makeFetch<PersonWrite, Person, WritePersonViolationMap>(
|
||||||
|
* "POST",
|
||||||
|
* "/api/1.0/person/person.json",
|
||||||
|
* personPayload
|
||||||
|
* );
|
||||||
|
* // Success path
|
||||||
|
* } catch (e) {
|
||||||
|
* if (isValidationException(e)) {
|
||||||
|
* // Fully typed:
|
||||||
|
* e.propertyPaths.includes("mobilenumber");
|
||||||
|
* const firstTitleForMobile = e.byProperty.mobilenumber?.[0];
|
||||||
|
* // You can also inspect parameter values:
|
||||||
|
* const v = e.problem.violations.find(v => v.propertyPath === "mobilenumber");
|
||||||
|
* const rawValue = v?.parameters?.["{{ value }}"]; // typed as string
|
||||||
|
* } else {
|
||||||
|
* // Other error handling (AccessException, ConflictHttpException, etc.)
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Tips to design your Violation Map
|
||||||
|
* - Use exact propertyPath strings as exposed by the API (they usually match your
|
||||||
|
* DTO field names or entity property paths used by the validator).
|
||||||
|
* - Inside each property, list only the placeholders that you actually read in the UI
|
||||||
|
* (you can always add more later). This keeps your types strict but pragmatic.
|
||||||
|
* - If a field may not include parameters at all, you can set it to an empty object {}.
|
||||||
|
* - If you don’t care about parameter typing, you can omit M entirely and rely on the
|
||||||
|
* default loose typing (Record<string, Primitive>), but you’ll lose safety.
|
||||||
|
*
|
||||||
|
* Error taxonomy thrown by makeFetch
|
||||||
|
* - ValidationException<M> when status = 422 and payload is a validation problem.
|
||||||
|
* - AccessException when status = 403.
|
||||||
|
* - ConflictHttpException when status = 409.
|
||||||
|
* - A generic error object for other non-ok statuses.
|
||||||
|
*
|
||||||
|
* @typeParam Input - Shape of the request body you send (if any)
|
||||||
|
* @typeParam Output - Shape of the successful JSON response you expect
|
||||||
|
* @typeParam M - Violation Map describing the per-field parameters you expect
|
||||||
|
* in Symfony validation violations. See examples above.
|
||||||
|
*
|
||||||
|
* @param method The HTTP method to use (POST, GET, PUT, PATCH, DELETE)
|
||||||
|
* @param url The absolute or relative URL to call
|
||||||
|
* @param body The request payload. If null/undefined, no body is sent
|
||||||
|
* @param options Extra fetch options/headers merged into the request
|
||||||
|
*
|
||||||
|
* @returns The parsed JSON response typed as Output. For 204 No Content, resolves
|
||||||
|
* with undefined (void).
|
||||||
*/
|
*/
|
||||||
export const makeFetch = <Input, Output>(
|
export const makeFetch = async <
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
M extends Record<string, Record<string, string|number>> = Record<
|
||||||
|
string,
|
||||||
|
Record<string, string|number>
|
||||||
|
>,
|
||||||
|
>(
|
||||||
method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE",
|
method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE",
|
||||||
url: string,
|
url: string,
|
||||||
body?: body | Input | null,
|
body?: body | Input | null,
|
||||||
@@ -85,7 +330,8 @@ export const makeFetch = <Input, Output>(
|
|||||||
if (typeof options !== "undefined") {
|
if (typeof options !== "undefined") {
|
||||||
opts = Object.assign(opts, options);
|
opts = Object.assign(opts, options);
|
||||||
}
|
}
|
||||||
return fetch(url, opts).then((response) => {
|
|
||||||
|
return fetch(url, opts).then(async (response) => {
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@@ -95,9 +341,20 @@ export const makeFetch = <Input, Output>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 422) {
|
if (response.status === 422) {
|
||||||
return response.json().then((response) => {
|
// Unprocessable Entity -> payload de validation Symfony
|
||||||
throw ValidationException(response);
|
const json = await response.json().catch(() => undefined);
|
||||||
});
|
|
||||||
|
if (isValidationProblem(json)) {
|
||||||
|
// On ré-interprète le payload selon M (ParamMap) pour typer les violations
|
||||||
|
const problem = json as unknown as ValidationProblemFromMap<M>;
|
||||||
|
throw new ValidationException<M>(problem);
|
||||||
|
}
|
||||||
|
|
||||||
|
const err = new Error(
|
||||||
|
"Validation failed but payload is not a ValidationProblem",
|
||||||
|
);
|
||||||
|
(err as any).raw = json;
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
@@ -162,12 +419,6 @@ function _fetchAction<T>(
|
|||||||
throw NotFoundException(response);
|
throw NotFoundException(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 422) {
|
|
||||||
return response.json().then((response) => {
|
|
||||||
throw ValidationException(response);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
throw AccessException(response);
|
throw AccessException(response);
|
||||||
}
|
}
|
||||||
@@ -226,24 +477,6 @@ export const fetchScopes = (): Promise<Scope[]> => {
|
|||||||
return fetchResults("/api/1.0/main/scope.json");
|
return fetchResults("/api/1.0/main/scope.json");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Error objects to be thrown
|
|
||||||
*/
|
|
||||||
const ValidationException = (
|
|
||||||
response: ValidationErrorResponse,
|
|
||||||
): ValidationExceptionInterface => {
|
|
||||||
const error = {} as ValidationExceptionInterface;
|
|
||||||
error.name = "ValidationException";
|
|
||||||
error.violations = response.violations.map(
|
|
||||||
(violation) => `${violation.title}: ${violation.propertyPath}`,
|
|
||||||
);
|
|
||||||
error.titles = response.violations.map((violation) => violation.title);
|
|
||||||
error.propertyPaths = response.violations.map(
|
|
||||||
(violation) => violation.propertyPath,
|
|
||||||
);
|
|
||||||
return error;
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const AccessException = (response: Response): AccessExceptionInterface => {
|
const AccessException = (response: Response): AccessExceptionInterface => {
|
||||||
const error = {} as AccessExceptionInterface;
|
const error = {} as AccessExceptionInterface;
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
import {Gender, GenderTranslation} from "ChillMainAssets/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates a given gender object into its corresponding gender translation string.
|
||||||
|
*
|
||||||
|
* @param {Gender|null} gender - The gender object to be translated, null values are also supported
|
||||||
|
* @return {GenderTranslation} Returns the gender translation string corresponding to the provided gender,
|
||||||
|
* or "unknown" if the gender is null.
|
||||||
|
*/
|
||||||
|
export function toGenderTranslation(gender: Gender|null): GenderTranslation
|
||||||
|
{
|
||||||
|
if (null === gender) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
return gender.genderTranslation;
|
||||||
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
import { TranslatableString } from "ChillMainAssets/types";
|
import { DateTime, TranslatableString } from "ChillMainAssets/types";
|
||||||
|
import { getLocale } from "translator";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Localizes a translatable string object based on the current locale.
|
* Localizes a translatable string object based on the current locale.
|
||||||
@@ -8,6 +9,25 @@ import { TranslatableString } from "ChillMainAssets/types";
|
|||||||
* @param locale defaults to browser locale
|
* @param locale defaults to browser locale
|
||||||
* @returns The localized string or null if no translation is available
|
* @returns The localized string or null if no translation is available
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepends the current HTML lang code to the given URL.
|
||||||
|
* Example: If lang="fr" and url="/about", returns "/fr/about"
|
||||||
|
*
|
||||||
|
* @param url The URL to localize
|
||||||
|
* @returns The localized URL
|
||||||
|
*/
|
||||||
|
export function localizedUrl(url: string): string {
|
||||||
|
const locale = getLocale();
|
||||||
|
// Ensure url starts with a slash and does not already start with /{lang}/
|
||||||
|
const normalizedUrl = url.startsWith("/") ? url : `/${url}`;
|
||||||
|
const langPrefix = `/${locale}`;
|
||||||
|
if (normalizedUrl.startsWith(langPrefix + "/")) {
|
||||||
|
return normalizedUrl;
|
||||||
|
}
|
||||||
|
return `${langPrefix}${normalizedUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function localizeString(
|
export function localizeString(
|
||||||
translatableString: TranslatableString | null | undefined,
|
translatableString: TranslatableString | null | undefined,
|
||||||
locale?: string,
|
locale?: string,
|
||||||
@@ -16,7 +36,7 @@ export function localizeString(
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLocale = locale || navigator.language.split("-")[0] || "fr";
|
const currentLocale = locale || getLocale();
|
||||||
|
|
||||||
if (translatableString[currentLocale]) {
|
if (translatableString[currentLocale]) {
|
||||||
return translatableString[currentLocale];
|
return translatableString[currentLocale];
|
||||||
@@ -39,3 +59,47 @@ export function localizeString(
|
|||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const datetimeFormats: Record<
|
||||||
|
string,
|
||||||
|
Record<string, Intl.DateTimeFormatOptions>
|
||||||
|
> = {
|
||||||
|
fr: {
|
||||||
|
short: {
|
||||||
|
year: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
},
|
||||||
|
long: {
|
||||||
|
year: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
hour12: false,
|
||||||
|
},
|
||||||
|
hoursOnly: {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
hour12: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export function localizeDateTimeFormat(
|
||||||
|
dateTime: DateTime,
|
||||||
|
format: keyof typeof datetimeFormats.fr = "short",
|
||||||
|
): string {
|
||||||
|
const locale = getLocale();
|
||||||
|
const options =
|
||||||
|
datetimeFormats[locale]?.[format] || datetimeFormats.fr[format];
|
||||||
|
return new Intl.DateTimeFormat(locale, options).format(
|
||||||
|
new Date(dateTime.datetime),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default datetimeFormats;
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* Extracts the "returnPath" parameter from the current URL's query string and returns it.
|
|
||||||
* If the parameter is not present, returns the provided fallback path.
|
|
||||||
*
|
|
||||||
* @param {string} fallbackPath - The fallback path to use if "returnPath" is not found in the query string.
|
|
||||||
* @return {string} The "returnPath" from the query string, or the fallback path if "returnPath" is not present.
|
|
||||||
*/
|
|
||||||
export function returnPathOr(fallbackPath: string): string {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const returnPath = urlParams.get("returnPath");
|
|
||||||
|
|
||||||
return returnPath ?? fallbackPath;
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
import { EntityWorkflow } from "ChillMainAssets/types";
|
|
||||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
|
||||||
|
|
||||||
export const fetchWorkflow = async (
|
|
||||||
workflowId: number,
|
|
||||||
): Promise<EntityWorkflow> => {
|
|
||||||
try {
|
|
||||||
return await makeFetch<null, EntityWorkflow>(
|
|
||||||
"GET",
|
|
||||||
`/api/1.0/main/workflow/${workflowId}.json`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to fetch workflow ${workflowId}:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
@@ -126,7 +126,8 @@ function loadDynamicPicker(element) {
|
|||||||
-1 ===
|
-1 ===
|
||||||
this.suggested.findIndex(
|
this.suggested.findIndex(
|
||||||
(e) => e.type === entity.type && e.id === entity.id,
|
(e) => e.type === entity.type && e.id === entity.id,
|
||||||
)
|
) &&
|
||||||
|
"me" !== entity
|
||||||
) {
|
) {
|
||||||
this.suggested.push(entity);
|
this.suggested.push(entity);
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
import { createApp } from "vue";
|
|
||||||
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
|
|
||||||
import { appMessages } from "ChillMainAssets/vuejs/HomepageWidget/js/i18n";
|
|
||||||
import { store } from "ChillMainAssets/vuejs/HomepageWidget/js/store";
|
|
||||||
import App from "ChillMainAssets/vuejs/HomepageWidget/App";
|
|
||||||
|
|
||||||
const i18n = _createI18n(appMessages);
|
|
||||||
|
|
||||||
const app = createApp({
|
|
||||||
template: `<app></app>`,
|
|
||||||
})
|
|
||||||
.use(store)
|
|
||||||
.use(i18n)
|
|
||||||
.component("app", App)
|
|
||||||
.mount("#homepage_widget");
|
|
@@ -0,0 +1,12 @@
|
|||||||
|
import App from "ChillMainAssets/vuejs/HomepageWidget/App.vue";
|
||||||
|
import { createApp } from "vue";
|
||||||
|
import { store } from "ChillMainAssets/vuejs/HomepageWidget/store";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
homepage_config: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _app = createApp(App);
|
||||||
|
_app.use(store).mount("#homepage_widget");
|
@@ -1,15 +1,69 @@
|
|||||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
||||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||||
import { Person } from "../../../ChillPersonBundle/Resources/public/types";
|
import { CreatableEntityType } from "ChillPersonAssets/types";
|
||||||
|
|
||||||
export interface DateTime {
|
export interface DateTime {
|
||||||
datetime: string;
|
datetime: string;
|
||||||
datetime8601: string;
|
datetime8601: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A date representation to use when we create or update a date
|
||||||
|
*/
|
||||||
|
export interface DateTimeWrite {
|
||||||
|
/**
|
||||||
|
* Must be a string in format Y-m-d\TH:i:sO
|
||||||
|
*/
|
||||||
|
datetime: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Civility {
|
export interface Civility {
|
||||||
|
type: "chill_main_civility";
|
||||||
|
id: number;
|
||||||
|
abbreviation: TranslatableString;
|
||||||
|
active: boolean;
|
||||||
|
name: TranslatableString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight reference to Civility, to use in POST or PUT requests.
|
||||||
|
*/
|
||||||
|
export interface SetCivility {
|
||||||
|
type: "chill_main_civility";
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gender translation.
|
||||||
|
*
|
||||||
|
* Match the GenderEnum in PHP code.
|
||||||
|
*/
|
||||||
|
export type GenderTranslation = "male" | "female" | "neutral" | "unknown";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A gender
|
||||||
|
*
|
||||||
|
* See also
|
||||||
|
*/
|
||||||
|
export interface Gender {
|
||||||
|
type: "chill_main_gender";
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
genderTranslation: GenderTranslation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight reference to a Gender, used in POST / PUT requests.
|
||||||
|
*/
|
||||||
|
export interface SetGender {
|
||||||
|
type: "chill_main_gender";
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Household {
|
||||||
|
type: "household";
|
||||||
id: number;
|
id: number;
|
||||||
// TODO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Job {
|
export interface Job {
|
||||||
@@ -24,6 +78,18 @@ export interface Center {
|
|||||||
id: number;
|
id: number;
|
||||||
type: "center";
|
type: "center";
|
||||||
name: string;
|
name: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetCenter is a lightweight reference used in POST/PUT requests to associate an existing center with a resource.
|
||||||
|
* It links by id only and does not create or modify centers.
|
||||||
|
* Expected shape: { type: "center", id: number }.
|
||||||
|
* Requests will fail if the id is invalid, the center doesn't exist, or permissions are insufficient.
|
||||||
|
*/
|
||||||
|
export interface SetCenter {
|
||||||
|
id: number;
|
||||||
|
type: "center";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Scope {
|
export interface Scope {
|
||||||
@@ -203,58 +269,6 @@ export interface WorkflowAttachment {
|
|||||||
genericDoc: null | GenericDoc;
|
genericDoc: null | GenericDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Workflow {
|
|
||||||
name: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntityWorkflowStep {
|
|
||||||
type: "entity_workflow_step";
|
|
||||||
id: number;
|
|
||||||
comment: string;
|
|
||||||
currentStep: StepDefinition;
|
|
||||||
isFinal: boolean;
|
|
||||||
isFreezed: boolean;
|
|
||||||
isFinalized: boolean;
|
|
||||||
transitionPrevious: Transition | null;
|
|
||||||
transitionAfter: Transition | null;
|
|
||||||
previousId: number | null;
|
|
||||||
nextId: number | null;
|
|
||||||
transitionPreviousBy: User | null;
|
|
||||||
transitionPreviousAt: DateTime | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Transition {
|
|
||||||
name: string;
|
|
||||||
text: string;
|
|
||||||
isForward: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StepDefinition {
|
|
||||||
name: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntityWorkflow {
|
|
||||||
type: "entity_workflow";
|
|
||||||
id: number;
|
|
||||||
relatedEntityClass: string;
|
|
||||||
relatedEntityId: number;
|
|
||||||
workflow: Workflow;
|
|
||||||
currentStep: EntityWorkflowStep;
|
|
||||||
steps: EntityWorkflowStep[];
|
|
||||||
datas: WorkflowData;
|
|
||||||
title: string;
|
|
||||||
isOnHoldAtCurrentStep: boolean;
|
|
||||||
_permissions: {
|
|
||||||
CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkflowData {
|
|
||||||
persons: Person[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExportGeneration {
|
export interface ExportGeneration {
|
||||||
id: string;
|
id: string;
|
||||||
type: "export_generation";
|
type: "export_generation";
|
||||||
@@ -269,7 +283,141 @@ export interface PrivateCommentEmbeddable {
|
|||||||
comments: Record<number, string>;
|
comments: Record<number, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API Exception types
|
||||||
|
export interface TransportExceptionInterface {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexedKey<Base extends string> = `${Base}[${number}]`;
|
||||||
|
type BaseKeys<M> = Extract<keyof M, string>;
|
||||||
|
|
||||||
|
export type DynamicKeys<M extends Record<string, Record<string, unknown>>> =
|
||||||
|
| BaseKeys<M>
|
||||||
|
| { [K in BaseKeys<M> as IndexedKey<K>]: K }[IndexedKey<BaseKeys<M>>];
|
||||||
|
|
||||||
|
type NormalizeKey<K extends string> = K extends `${infer B}[${number}]` ? B : K;
|
||||||
|
|
||||||
|
export type ViolationFromMap<M extends Record<string, Record<string, unknown>>> = {
|
||||||
|
[K in DynamicKeys<M> & string]: { // <- note le "& string" ici
|
||||||
|
propertyPath: K;
|
||||||
|
title: string;
|
||||||
|
parameters?: M[NormalizeKey<K>];
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
}[DynamicKeys<M> & string];
|
||||||
|
|
||||||
|
export type ValidationProblemFromMap<
|
||||||
|
M extends Record<string, Record<string, string|number>>,
|
||||||
|
> = {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
detail?: string;
|
||||||
|
violations: ViolationFromMap<M>[];
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
|
||||||
|
export interface ValidationExceptionInterface<
|
||||||
|
M extends Record<string, Record<string, string|number>> = Record<
|
||||||
|
string,
|
||||||
|
Record<string, string|number>
|
||||||
|
>,
|
||||||
|
> extends Error {
|
||||||
|
name: "ValidationException";
|
||||||
|
/** Full server payload copy */
|
||||||
|
problems: ValidationProblemFromMap<M>;
|
||||||
|
/** A list of all violations, with property key */
|
||||||
|
violationsList: ViolationFromMap<M>[];
|
||||||
|
/** Compact list "Title: path" */
|
||||||
|
violations: string[];
|
||||||
|
/** Only titles */
|
||||||
|
titles: string[];
|
||||||
|
/** Only property paths */
|
||||||
|
propertyPaths: DynamicKeys<M> & string[];
|
||||||
|
/** Indexing by property (useful for display by field) */
|
||||||
|
byProperty: Record<Extract<keyof M, string>, string[]>;
|
||||||
|
|
||||||
|
violationsByNormalizedProperty(property: Extract<keyof M, string>): ViolationFromMap<M>[];
|
||||||
|
|
||||||
|
violationsByNormalizedPropertyAndParams<
|
||||||
|
P extends Extract<keyof M, string>,
|
||||||
|
K extends Extract<keyof M[P], string>
|
||||||
|
>(
|
||||||
|
property: P,
|
||||||
|
param: K,
|
||||||
|
param_value: M[P][K]
|
||||||
|
): ViolationFromMap<M>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessExceptionInterface extends TransportExceptionInterface {
|
||||||
|
name: "AccessException";
|
||||||
|
violations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotFoundExceptionInterface
|
||||||
|
extends TransportExceptionInterface {
|
||||||
|
name: "NotFoundException";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerExceptionInterface extends TransportExceptionInterface {
|
||||||
|
name: "ServerException";
|
||||||
|
message: string;
|
||||||
|
code: number;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConflictHttpExceptionInterface
|
||||||
|
extends TransportExceptionInterface {
|
||||||
|
name: "ConflictHttpException";
|
||||||
|
violations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiException =
|
||||||
|
| ValidationExceptionInterface
|
||||||
|
| AccessExceptionInterface
|
||||||
|
| NotFoundExceptionInterface
|
||||||
|
| ServerExceptionInterface
|
||||||
|
| ConflictHttpExceptionInterface;
|
||||||
|
|
||||||
|
export interface Modal {
|
||||||
|
showModal: boolean;
|
||||||
|
modalDialogClass: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Selected {
|
||||||
|
result: UserGroupOrUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface addNewEntities {
|
||||||
|
selected: Selected[];
|
||||||
|
modal: Modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum HomepageTabs {
|
||||||
|
MyCustoms,
|
||||||
|
MyTickets,
|
||||||
|
MyNotifications,
|
||||||
|
MyAccompanyingCourses,
|
||||||
|
MyEvaluations,
|
||||||
|
MyTasks,
|
||||||
|
MyWorkflows,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HomepageConfig {
|
||||||
|
defaultTab: HomepageTabs;
|
||||||
|
displayTabs: HomepageTabs[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabDefinition {
|
||||||
|
key: HomepageTabs;
|
||||||
|
label: string;
|
||||||
|
icon: string | null;
|
||||||
|
counter: () => number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Possible states for the WaitingScreen Component.
|
* Configuration for the CreateModal and Create component
|
||||||
*/
|
*/
|
||||||
export type WaitingScreenState = "pending" | "failure" | "stopped" | "ready";
|
export interface CreateComponentConfig {
|
||||||
|
action?: string;
|
||||||
|
allowedTypes: CreatableEntityType[];
|
||||||
|
query?: string;
|
||||||
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user