mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-10-09 23:09:43 +00:00
Compare commits
88 Commits
#361-impro
...
testing202
Author | SHA1 | Date | |
---|---|---|---|
aabf62d399
|
|||
d2d297a377
|
|||
0541995a60
|
|||
29e054bd10
|
|||
da0099aafc
|
|||
3a18ea42fe
|
|||
e60435b8cc | |||
ab6ab19499 | |||
2a1762ea8d | |||
18ababbca9 | |||
f6179cd3a3 | |||
ddf8da4cee | |||
bf2181c2f1 | |||
d508fde8d2 | |||
14dba22181 | |||
7dc7e77c62 | |||
9d58904969 | |||
4d90c7028f | |||
3abb76d268 | |||
d62dd4396e | |||
59e8d9d516 | |||
7dcb8abe38 | |||
a0b2d92ba2 | |||
7843e5dfd1 | |||
32c847267b | |||
9b353f4d1b | |||
81a858f07a | |||
6a2ee232a9 | |||
56c43a0a76 | |||
eb724a730c | |||
18f98b6795 | |||
d73994edd0 | |||
70603570c8 | |||
df09dd2017 | |||
1c87280b1e | |||
445e093a28 | |||
3f91c65b30 | |||
9bc3c16b58 | |||
12dff82248 | |||
ab23a4efb5 | |||
204fb20475 | |||
f430d97152 | |||
4fa4d3b65c | |||
bd4c34cc1d | |||
4cea678e93 | |||
5e6833975b | |||
f523b9adb3 | |||
a211549432 | |||
17b1363113 | |||
3356ed8e57 | |||
2a7fa517ee | |||
85781c8e14 | |||
00eb435896 | |||
ed71cffd6a | |||
ae679e6997 | |||
e1d308fd97 | |||
d9acda67e3 | |||
e88da74882 | |||
591c44d1a0 | |||
bf04b7981c | |||
df33eec30f | |||
c657c98918 | |||
ef5eb5b907 | |||
d683fe002d | |||
555bbca59b | |||
e9e9d5c458 | |||
b1842a33ae | |||
6afeaccf24 | |||
fb76bac480 | |||
6ded185289 | |||
95adc29f9d | |||
4d0c3e683f | |||
018aafc773 | |||
c4aea4efc2 | |||
225e3ca13f | |||
8c1fa7956a | |||
e253d1b276 | |||
a52aac2d98 | |||
9e8cf60dd8 | |||
7682d81d50 | |||
5d31ce96c1 | |||
81ef64a246 | |||
49d1f78001 | |||
0d0f3528e2 | |||
d97d5e689a | |||
95d80ce13e | |||
668720984d | |||
245c3fa121 |
6
.changes/unreleased/Feature-20250211-142243.yaml
Normal file
6
.changes/unreleased/Feature-20250211-142243.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: Allow the merge of two accompanying period works
|
||||||
|
time: 2025-02-11T14:22:43.134106669+01:00
|
||||||
|
custom:
|
||||||
|
Issue: "359"
|
||||||
|
SchemaChange: No schema change
|
6
.changes/unreleased/Feature-20250403-100311.yaml
Normal file
6
.changes/unreleased/Feature-20250403-100311.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: Duplication of a document to another accompanying period work evaluation
|
||||||
|
time: 2025-04-03T10:03:11.796736107+02:00
|
||||||
|
custom:
|
||||||
|
Issue: "369"
|
||||||
|
SchemaChange: No schema change
|
6
.changes/unreleased/Feature-20250403-100857.yaml
Normal file
6
.changes/unreleased/Feature-20250403-100857.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: Fusion of two accompanying period works
|
||||||
|
time: 2025-04-03T10:08:57.25079018+02:00
|
||||||
|
custom:
|
||||||
|
Issue: "359"
|
||||||
|
SchemaChange: No schema change
|
@@ -1,6 +0,0 @@
|
|||||||
## v4.2.1 - 2025-09-03
|
|
||||||
### Fixed
|
|
||||||
* Fix exports to work with DirectExportInterface
|
|
||||||
### DX
|
|
||||||
* Improve error message when a stored object cannot be written on local disk
|
|
||||||
|
|
@@ -1,10 +0,0 @@
|
|||||||
## v4.3.0 - 2025-09-08
|
|
||||||
### Feature
|
|
||||||
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges
|
|
||||||
* Add a command to generate a list of permissions
|
|
||||||
* ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date
|
|
||||||
|
|
||||||
**Schema Change**: Add columns or tables
|
|
||||||
### Fixed
|
|
||||||
* fix date formatting in calendar range display
|
|
||||||
* Change route URL to avoid clash with person duplicate controller method
|
|
@@ -1,8 +0,0 @@
|
|||||||
## v4.4.0 - 2025-09-11
|
|
||||||
### Feature
|
|
||||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
|
|
||||||
* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation
|
|
||||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works
|
|
||||||
### Fixed
|
|
||||||
* Fix display of 'duplicate' and 'merge' buttons in CRUD templates
|
|
||||||
* Fix saving notification preferences in user's profile
|
|
@@ -1,3 +0,0 @@
|
|||||||
## v4.4.1 - 2025-09-11
|
|
||||||
### Fixed
|
|
||||||
* fix translations in duplicate evaluation document modal and realign close modal button
|
|
@@ -1,3 +0,0 @@
|
|||||||
## v4.4.2 - 2025-09-12
|
|
||||||
### Fixed
|
|
||||||
* Fix document generation and workflow generation do not work on accompanying period work documents
|
|
@@ -1,13 +0,0 @@
|
|||||||
## v4.5.0 - 2025-10-03
|
|
||||||
### Feature
|
|
||||||
* Only allow delete of attachment on workflows that are not final
|
|
||||||
* Move up signature buttons on index workflow page for easier access
|
|
||||||
* Filter out document from attachment list if it is the same as the workflow document
|
|
||||||
* Block edition on attached document on workflow, if the workflow is finalized or sent external
|
|
||||||
* Convert workflow's attached document to pdf while sending them external
|
|
||||||
* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition
|
|
||||||
### Fixed
|
|
||||||
* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance
|
|
||||||
* Fix permissions on storedObject which are subject by a workflow
|
|
||||||
### DX
|
|
||||||
* Introduce a WaitingScreen component to display a waiting screen
|
|
@@ -1,4 +0,0 @@
|
|||||||
## v4.5.1 - 2025-10-03
|
|
||||||
### Fixed
|
|
||||||
* Add missing javascript dependency
|
|
||||||
* Add exception handling for conversion of attachment on sending external, when documens are already in pdf
|
|
@@ -27,11 +27,11 @@ Chill is a comprehensive web application built as a set of Symfony bundles. It i
|
|||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
Note: This is a project that's existed for a long time, and throughout the years we've used multiple structures inside each bundle. When having the choice, the developers should choose the new structure.
|
Note: This is a project which exists from a long time ago, and we found multiple structure inside each bundle. When having the choice, the developers should choose the new structure.
|
||||||
|
|
||||||
The project follows a standard Symfony bundle structure:
|
The project follows a standard Symfony bundle structure:
|
||||||
- `/src/Bundle/`: Contains all the Chill bundles. The code is either at the root of the bundle directory, or within a `src/` directory (preferred). See psr4 mapping at the root's `composer.json`.
|
- `/src/Bundle/`: Contains all the Chill bundles. The code is either at the root of the bundle directory, or within a `src/` directory (preferred). See psr4 mapping at the root's `composer.json`.
|
||||||
- each bundle comes with its own tests, either in the `Tests` directory (when the code is directly within the bundle directory (for instance `src/Bundle/ChillMainBundle/Tests`, `src/Bundle/ChillPersonBundle/Tests`)), or inside the `tests` directory, alongside the `src/` sub-directory (example: `src/Bundle/ChillWopiBundle/tests`) (this is the preferred way).
|
- each bundle come with his own tests, either in the `Tests` directory (when the code is directly within the bundle directory (for instance `src/Bundle/ChillMainBundle/Tests`, `src/Bundle/ChillPersonBundle/Tests`)), or inside the `tests` directory, alongside to the `src/` sub-directory (example: `src/Bundle/ChillWopiBundle/tests`) (this is the preferred way).
|
||||||
- `/docs/`: Contains project documentation
|
- `/docs/`: Contains project documentation
|
||||||
|
|
||||||
Each bundle typically has the following structure:
|
Each bundle typically has the following structure:
|
||||||
@@ -46,13 +46,13 @@ Each bundle typically has the following structure:
|
|||||||
|
|
||||||
### A special word about TicketBundle
|
### A special word about TicketBundle
|
||||||
|
|
||||||
The ticket bundle is developed using a kind of "Command" pattern. The controller fills a "Command," and a "CommandHandler" handles this command. They are saved in the `src/Bundle/ChillTicketBundle/src/Action` directory.
|
The ticket bundle is developed using a kind of "Command" pattern. The controller fill a "Command", and a "CommandHandler" handle this command. They are savec in the `src/Bundle/ChillTicketBundle/src/Action` directory.
|
||||||
|
|
||||||
## Development Guidelines
|
## Development Guidelines
|
||||||
|
|
||||||
### Building and Configuration Instructions
|
### Building and Configuration Instructions
|
||||||
|
|
||||||
All the commands should be run through the `symfony` command, which will configure the required variables.
|
All the command should be run through the `symfony` command, which will configure the required variables.
|
||||||
|
|
||||||
For assets, we must ensure that we use node at version `^20.0.0`. This is done using `nvm use 20`.
|
For assets, we must ensure that we use node at version `^20.0.0`. This is done using `nvm use 20`.
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ For assets, we must ensure that we use node at version `^20.0.0`. This is done u
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Set Up the Database**:
|
5. **Set Up the Database**:
|
||||||
```bash
|
```bash
|
||||||
# Create the database
|
# Create the database
|
||||||
symfony console doctrine:database:create
|
symfony console doctrine:database:create
|
||||||
@@ -99,20 +99,20 @@ For assets, we must ensure that we use node at version `^20.0.0`. This is done u
|
|||||||
symfony console doctrine:fixtures:load
|
symfony console doctrine:fixtures:load
|
||||||
```
|
```
|
||||||
|
|
||||||
7. **Build Assets**:
|
6. **Build Assets**:
|
||||||
```bash
|
```bash
|
||||||
nvm use 20
|
nvm use 20
|
||||||
yarn run encore dev
|
yarn run encore dev
|
||||||
```
|
```
|
||||||
|
|
||||||
8. **Start the Development Server**:
|
7. **Start the Development Server**:
|
||||||
```bash
|
```bash
|
||||||
symfony server:start -d
|
symfony server:start -d
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Docker Setup
|
#### Docker Setup
|
||||||
|
|
||||||
The project includes a Docker configuration for easier development:
|
The project includes Docker configuration for easier development:
|
||||||
|
|
||||||
1. **Start Docker Services**:
|
1. **Start Docker Services**:
|
||||||
```bash
|
```bash
|
||||||
@@ -153,9 +153,9 @@ Key configuration files:
|
|||||||
|
|
||||||
Each time a doctrine entity is created, we generate migration to adapt the database.
|
Each time a doctrine entity is created, we generate migration to adapt the database.
|
||||||
|
|
||||||
The migration is created using the command `symfony console doctrine:migrations:diff --no-interaction --namespace <namespace>`, where the namespace is the relevant namespace for migration. As this is a bash script, remember to quote the `\` (`\` must become `\\` in your command).
|
The migration are created using the command `symfony console doctrine:migrations:diff --no-interaction --namespace <namespace>`, where the namespace is the relevant namespace for migration. As this is a bash script, do not forget to quote the `\` (`\` must become `\\` in your command).
|
||||||
|
|
||||||
Each bundle has his own namespace for migration (always ask me to confirm that command with a list of updated / created entities so that I can confirm to you that it is ok):
|
Each bundle has his own namespace for migration (always ask me to confirm that command, with a list of updated / created entities so that I can confirm you that it is ok):
|
||||||
|
|
||||||
- `Chill\Bundle\ActivityBundle` writes migrations to `Chill\Migrations\Activity`;
|
- `Chill\Bundle\ActivityBundle` writes migrations to `Chill\Migrations\Activity`;
|
||||||
- `Chill\Bundle\BudgetBundle` writes migrations to `Chill\Migrations\Budget`;
|
- `Chill\Bundle\BudgetBundle` writes migrations to `Chill\Migrations\Budget`;
|
||||||
@@ -183,7 +183,7 @@ Once created the, comment's classes should be removed and a description of the c
|
|||||||
|
|
||||||
When we need to use a DateTime or DateTimeImmutable that need to express "now", we prefer the usage of
|
When we need to use a DateTime or DateTimeImmutable that need to express "now", we prefer the usage of
|
||||||
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
|
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
|
||||||
where injection does not work when restoring an entity from a database, but usually possible in services.
|
where injection does not work when restoring an entity from database, but usually possible in services.
|
||||||
|
|
||||||
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
|
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
|
||||||
where we have full and easy control of the date.
|
where we have full and easy control of the date.
|
||||||
@@ -198,9 +198,9 @@ The project uses PHPUnit for testing. Each bundle has its own test suite, and th
|
|||||||
|
|
||||||
For creating mock, we prefer using prophecy (library phpspec/prophecy).
|
For creating mock, we prefer using prophecy (library phpspec/prophecy).
|
||||||
|
|
||||||
##### Useful helpers and tips that avoid creating a mock
|
##### Useful helpers and tips that avoid create a mock
|
||||||
|
|
||||||
Some notable implementations that are test helpers and avoid creating a mock:
|
Some notable implementations that are tests helper, and avoid to create a mock:
|
||||||
|
|
||||||
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
|
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
|
||||||
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
|
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
|
||||||
@@ -297,7 +297,7 @@ class TicketTest extends TestCase
|
|||||||
|
|
||||||
#### Test Database
|
#### Test Database
|
||||||
|
|
||||||
For tests that require a database, the project uses a postgresql database filled with fixtures (usage of doctrine-fixtures). You can configure a different database for testing in the `.env.test` file.
|
For tests that require a database, the project uses postgresql database filled by fixtures (usage of doctrine-fixtures). You can configure a different database for testing in the `.env.test` file.
|
||||||
|
|
||||||
### Code Quality Tools
|
### Code Quality Tools
|
||||||
|
|
||||||
|
54
CHANGELOG.md
54
CHANGELOG.md
@@ -6,60 +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.1 - 2025-10-03
|
|
||||||
### Fixed
|
|
||||||
* Add missing javascript dependency
|
|
||||||
* Add exception handling for conversion of attachment on sending external, when documens are already in pdf
|
|
||||||
|
|
||||||
## v4.5.0 - 2025-10-03
|
|
||||||
### Feature
|
|
||||||
* Only allow delete of attachment on workflows that are not final
|
|
||||||
* Move up signature buttons on index workflow page for easier access
|
|
||||||
* Filter out document from attachment list if it is the same as the workflow document
|
|
||||||
* Block edition on attached document on workflow, if the workflow is finalized or sent external
|
|
||||||
* Convert workflow's attached document to pdf while sending them external
|
|
||||||
* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition
|
|
||||||
### Fixed
|
|
||||||
* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance
|
|
||||||
* Fix permissions on storedObject which are subject by a workflow
|
|
||||||
### DX
|
|
||||||
* Introduce a WaitingScreen component to display a waiting screen
|
|
||||||
|
|
||||||
## v4.4.2 - 2025-09-12
|
|
||||||
### Fixed
|
|
||||||
* Fix document generation and workflow generation do not work on accompanying period work documents
|
|
||||||
|
|
||||||
## v4.4.1 - 2025-09-11
|
|
||||||
### Fixed
|
|
||||||
* fix translations in duplicate evaluation document modal and realign close modal button
|
|
||||||
|
|
||||||
## v4.4.0 - 2025-09-11
|
|
||||||
### Feature
|
|
||||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
|
|
||||||
* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation
|
|
||||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works
|
|
||||||
### Fixed
|
|
||||||
* Fix display of 'duplicate' and 'merge' buttons in CRUD templates
|
|
||||||
* Fix saving notification preferences in user's profile
|
|
||||||
|
|
||||||
## v4.3.0 - 2025-09-08
|
|
||||||
### Feature
|
|
||||||
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges
|
|
||||||
* Add a command to generate a list of permissions
|
|
||||||
* ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date
|
|
||||||
|
|
||||||
**Schema Change**: Add columns or tables
|
|
||||||
### Fixed
|
|
||||||
* fix date formatting in calendar range display
|
|
||||||
* Change route URL to avoid clash with person duplicate controller method
|
|
||||||
|
|
||||||
## v4.2.1 - 2025-09-03
|
|
||||||
### Fixed
|
|
||||||
* Fix exports to work with DirectExportInterface
|
|
||||||
### DX
|
|
||||||
* Improve error message when a stored object cannot be written on local disk
|
|
||||||
|
|
||||||
|
|
||||||
## v4.2.0 - 2025-09-02
|
## v4.2.0 - 2025-09-02
|
||||||
### Feature
|
### Feature
|
||||||
* ([#64](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/64)) Add external identifier for a Person
|
* ([#64](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/64)) Add external identifier for a Person
|
||||||
|
@@ -17,9 +17,3 @@ when@dev:
|
|||||||
defaults:
|
defaults:
|
||||||
template: '@ChillMain/Dev/dev.assets.test2.html.twig'
|
template: '@ChillMain/Dev/dev.assets.test2.html.twig'
|
||||||
|
|
||||||
|
|
||||||
sass_address_picker:
|
|
||||||
path: /_dev/address-picker
|
|
||||||
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
|
|
||||||
defaults:
|
|
||||||
template: '@ChillMain/Dev/dev.address-picker.html.twig'
|
|
||||||
|
@@ -45,7 +45,6 @@
|
|||||||
"webpack-cli": "^5.0.1"
|
"webpack-cli": "^5.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fragaria/address-formatter": "^6.6.1",
|
|
||||||
"@fullcalendar/core": "^6.1.4",
|
"@fullcalendar/core": "^6.1.4",
|
||||||
"@fullcalendar/daygrid": "^6.1.4",
|
"@fullcalendar/daygrid": "^6.1.4",
|
||||||
"@fullcalendar/interaction": "^6.1.4",
|
"@fullcalendar/interaction": "^6.1.4",
|
||||||
@@ -56,7 +55,6 @@
|
|||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/leaflet": "^1.9.3",
|
"@types/leaflet": "^1.9.3",
|
||||||
"@vueuse/core": "^13.9.0",
|
|
||||||
"bootstrap-icons": "^1.11.3",
|
"bootstrap-icons": "^1.11.3",
|
||||||
"dropzone": "^5.7.6",
|
"dropzone": "^5.7.6",
|
||||||
"es6-promise": "^4.2.8",
|
"es6-promise": "^4.2.8",
|
||||||
@@ -67,12 +65,10 @@
|
|||||||
"mime": "^4.0.0",
|
"mime": "^4.0.0",
|
||||||
"pdfjs-dist": "^4.3.136",
|
"pdfjs-dist": "^4.3.136",
|
||||||
"vis-network": "^9.1.0",
|
"vis-network": "^9.1.0",
|
||||||
"vue": "^3.5.x",
|
"vue": "^3.5.6",
|
||||||
"vue-i18n": "^9.1.6",
|
"vue-i18n": "^9.1.6",
|
||||||
"vue-multiselect": "3.0.0-alpha.2",
|
"vue-multiselect": "3.0.0-alpha.2",
|
||||||
"vue-toast-notification": "^3.1.2",
|
"vue-toast-notification": "^3.1.2",
|
||||||
"vue-tsc": "^3.1.0",
|
|
||||||
"vue-use-leaflet": "^0.1.7",
|
|
||||||
"vuex": "^4.0.0"
|
"vuex": "^4.0.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
@@ -55,6 +55,5 @@
|
|||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
|
||||||
{% endembed %}
|
{% endembed %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -70,8 +70,6 @@
|
|||||||
<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
|
||||||
|
@@ -32,8 +32,6 @@
|
|||||||
<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
|
||||||
@@ -104,8 +102,7 @@
|
|||||||
event.title
|
event.title
|
||||||
}}</b>
|
}}</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'">{{
|
||||||
@@ -297,26 +294,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>({
|
||||||
|
@@ -18,7 +18,6 @@ use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
|||||||
use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator;
|
use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator;
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
|
|
||||||
use Symfony\Component\Filesystem\Filesystem;
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
use Symfony\Component\Filesystem\Path;
|
use Symfony\Component\Filesystem\Path;
|
||||||
|
|
||||||
@@ -148,11 +147,16 @@ class StoredObjectManager implements StoredObjectManagerInterface
|
|||||||
public function writeContent(string $filename, string $encryptedContent): void
|
public function writeContent(string $filename, string $encryptedContent): void
|
||||||
{
|
{
|
||||||
$fullPath = $this->buildPath($filename);
|
$fullPath = $this->buildPath($filename);
|
||||||
|
$dir = Path::getDirectory($fullPath);
|
||||||
|
|
||||||
try {
|
if (!$this->filesystem->exists($dir)) {
|
||||||
$this->filesystem->dumpFile($fullPath, $encryptedContent);
|
$this->filesystem->mkdir($dir);
|
||||||
} catch (IOExceptionInterface $exception) {
|
}
|
||||||
throw StoredObjectManagerException::unableToStoreDocumentOnDisk($exception);
|
|
||||||
|
$result = file_put_contents($fullPath, $encryptedContent);
|
||||||
|
|
||||||
|
if (false === $result) {
|
||||||
|
throw StoredObjectManagerException::unableToStoreDocumentOnDisk();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Exception;
|
|
||||||
|
|
||||||
class ConversionWithSameMimeTypeException extends \RuntimeException
|
|
||||||
{
|
|
||||||
public function __construct(string $mimeType, ?\Throwable $previous = null)
|
|
||||||
{
|
|
||||||
parent::__construct("Conversion to same MIME type '{$mimeType}' is not allowed: already at the same MIME type", 0, $previous);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
||||||
|
@@ -4,7 +4,7 @@ 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";
|
import { DOCUMENT_REPLACE, DOCUMENT_ADD, trans } from "translator";
|
||||||
|
|
||||||
interface DropFileConfig {
|
interface DropFileConfig {
|
||||||
allowRemove: boolean;
|
allowRemove: boolean;
|
||||||
@@ -78,7 +78,9 @@ function closeModal(): void {
|
|||||||
>
|
>
|
||||||
{{ trans(DOCUMENT_ADD) }}
|
{{ trans(DOCUMENT_ADD) }}
|
||||||
</button>
|
</button>
|
||||||
<button v-else @click="openModal" class="btn btn-edit"></button>
|
<button v-else @click="openModal" class="dropdown-item">
|
||||||
|
{{ trans(DOCUMENT_REPLACE) }}
|
||||||
|
</button>
|
||||||
<modal
|
<modal
|
||||||
v-if="state.showModal"
|
v-if="state.showModal"
|
||||||
:modal-dialog-class="modalClasses"
|
:modal-dialog-class="modalClasses"
|
||||||
|
@@ -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;
|
||||||
|
@@ -15,7 +15,6 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
|||||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
|
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||||
use Chill\DocStoreBundle\Exception\ConversionWithSameMimeTypeException;
|
|
||||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||||
use Chill\WopiBundle\Service\WopiConverter;
|
use Chill\WopiBundle\Service\WopiConverter;
|
||||||
use Symfony\Component\Mime\MimeTypesInterface;
|
use Symfony\Component\Mime\MimeTypesInterface;
|
||||||
@@ -45,7 +44,6 @@ class StoredObjectToPdfConverter
|
|||||||
* @throws \UnexpectedValueException if the preferred mime type for the conversion is not found
|
* @throws \UnexpectedValueException if the preferred mime type for the conversion is not found
|
||||||
* @throws \RuntimeException if the conversion or storage of the new version fails
|
* @throws \RuntimeException if the conversion or storage of the new version fails
|
||||||
* @throws StoredObjectManagerException
|
* @throws StoredObjectManagerException
|
||||||
* @throws ConversionWithSameMimeTypeException if the document has already the same mime type79*
|
|
||||||
*/
|
*/
|
||||||
public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf', bool $includeConvertedContent = false): array
|
public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf', bool $includeConvertedContent = false): array
|
||||||
{
|
{
|
||||||
@@ -58,7 +56,7 @@ class StoredObjectToPdfConverter
|
|||||||
$currentVersion = $storedObject->getCurrentVersion();
|
$currentVersion = $storedObject->getCurrentVersion();
|
||||||
|
|
||||||
if ($currentVersion->getType() === $newMimeType) {
|
if ($currentVersion->getType() === $newMimeType) {
|
||||||
throw new ConversionWithSameMimeTypeException($newMimeType);
|
throw new \UnexpectedValueException('Already at the same mime type');
|
||||||
}
|
}
|
||||||
|
|
||||||
$content = $this->storedObjectManager->read($currentVersion);
|
$content = $this->storedObjectManager->read($currentVersion);
|
||||||
|
@@ -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'];
|
||||||
|
@@ -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']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,35 +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\Command;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Security\RoleDumper;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
|
|
||||||
#[AsCommand(name: 'chill:main:dump-list-permissions', description: 'Print a markdown reference of permissions (roles) grouped by title with dependencies).')]
|
|
||||||
final class DumpListPermissionsCommand extends Command
|
|
||||||
{
|
|
||||||
public function __construct(private readonly RoleDumper $roleDumper)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$markdown = $this->roleDumper->dumpAsMarkdown();
|
|
||||||
$output->writeln($markdown);
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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();
|
||||||
|
|
||||||
|
@@ -1,50 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Controller;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Repository\AddressReferenceRepositoryInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
final readonly class AddressReferenceAggregatedApiController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private Security $security,
|
|
||||||
private AddressReferenceRepositoryInterface $addressReferenceRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
#[Route(path: '/api/1.0/main/address-reference/aggregated/search')]
|
|
||||||
public function search(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
if (!$this->security->isGranted('IS_AUTHENTICATED')) {
|
|
||||||
throw new AccessDeniedHttpException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$request->query->has('q')) {
|
|
||||||
throw new BadRequestHttpException('Parameter "q" is required.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$q = trim($request->query->get('q'));
|
|
||||||
|
|
||||||
if ('' === $q) {
|
|
||||||
throw new BadRequestHttpException('Parameter "q" is required and cannot be empty.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->addressReferenceRepository->findAggregatedBySearchString($q);
|
|
||||||
|
|
||||||
return new JsonResponse(iterator_to_array($result));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -345,7 +345,7 @@ class ExportController extends AbstractController
|
|||||||
* @param array $dataExport Raw data from export step
|
* @param array $dataExport Raw data from export step
|
||||||
* @param array $dataFormatter Raw data from formatter step
|
* @param array $dataFormatter Raw data from formatter step
|
||||||
*/
|
*/
|
||||||
private function buildExportDataForNormalization(string $alias, ?array $dataCenters, array $dataExport, ?array $dataFormatter, ?SavedExport $savedExport): array
|
private function buildExportDataForNormalization(string $alias, ?array $dataCenters, array $dataExport, array $dataFormatter, ?SavedExport $savedExport): array
|
||||||
{
|
{
|
||||||
if ($this->filterStatsByCenters) {
|
if ($this->filterStatsByCenters) {
|
||||||
$formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], null);
|
$formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], null);
|
||||||
@@ -365,7 +365,7 @@ class ExportController extends AbstractController
|
|||||||
$formExport->submit($dataExport);
|
$formExport->submit($dataExport);
|
||||||
$dataExport = $formExport->getData();
|
$dataExport = $formExport->getData();
|
||||||
|
|
||||||
if (is_array($dataFormatter) && \count($dataFormatter) > 0) {
|
if (\count($dataFormatter) > 0) {
|
||||||
$formFormatter = $this->createCreateFormExport(
|
$formFormatter = $this->createCreateFormExport(
|
||||||
$alias,
|
$alias,
|
||||||
'generate_formatter',
|
'generate_formatter',
|
||||||
@@ -381,7 +381,7 @@ class ExportController extends AbstractController
|
|||||||
'export' => $dataExport['export']['export'] ?? [],
|
'export' => $dataExport['export']['export'] ?? [],
|
||||||
'filters' => $dataExport['export']['filters'] ?? [],
|
'filters' => $dataExport['export']['filters'] ?? [],
|
||||||
'aggregators' => $dataExport['export']['aggregators'] ?? [],
|
'aggregators' => $dataExport['export']['aggregators'] ?? [],
|
||||||
'pick_formatter' => ($dataExport['export']['pick_formatter'] ?? [])['alias'] ?? '',
|
'pick_formatter' => $dataExport['export']['pick_formatter']['alias'],
|
||||||
'formatter' => $dataFormatter['formatter'] ?? [],
|
'formatter' => $dataFormatter['formatter'] ?? [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,47 +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\Repository\PostalCodeForAddressReferenceRepositoryInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
final readonly class PostalCodeForAddressReferenceApiController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private PostalCodeForAddressReferenceRepositoryInterface $postalCodeForAddressReferenceRepository,
|
|
||||||
private Security $security,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
#[Route('/api/1.0/main/address-reference/postal-code/search')]
|
|
||||||
public function findPostalCodeBySearch(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
|
|
||||||
if (!$this->security->isGranted('IS_AUTHENTICATED')) {
|
|
||||||
throw new AccessDeniedHttpException();
|
|
||||||
}
|
|
||||||
|
|
||||||
$search = $request->query->get('q');
|
|
||||||
|
|
||||||
if (null === $search || '' === trim($search)) {
|
|
||||||
throw new BadRequestHttpException('No search query provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
$postalCodes = iterator_to_array($this->postalCodeForAddressReferenceRepository->findPostalCode($search));
|
|
||||||
|
|
||||||
return new JsonResponse($postalCodes, json: false);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@@ -24,7 +24,6 @@ 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 Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User.
|
* User.
|
||||||
@@ -46,8 +45,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.
|
||||||
*/
|
*/
|
||||||
@@ -160,11 +157,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 +336,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 +410,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 +633,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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@ use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
|||||||
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
|
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @phpstan-type NormalizedData array{centers: array{centers: list<int>, regroupments: list<int>}, export: array{form: array<string, mixed>, version: int}, filters: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, aggregators: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, pick_formatter?: string, formatter: array{form: array<string, mixed>, version: int}}
|
* @phpstan-type NormalizedData array{centers: array{centers: list<int>, regroupments: list<int>}, export: array{form: array<string, mixed>, version: int}, filters: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, aggregators: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, pick_formatter: string, formatter: array{form: array<string, mixed>, version: int}}
|
||||||
*/
|
*/
|
||||||
class ExportConfigNormalizer
|
class ExportConfigNormalizer
|
||||||
{
|
{
|
||||||
@@ -72,14 +72,10 @@ class ExportConfigNormalizer
|
|||||||
}
|
}
|
||||||
$serialized['aggregators'] = $aggregatorsSerialized;
|
$serialized['aggregators'] = $aggregatorsSerialized;
|
||||||
|
|
||||||
if ($export instanceof ExportInterface) {
|
|
||||||
$serialized['pick_formatter'] = $formData['pick_formatter'];
|
$serialized['pick_formatter'] = $formData['pick_formatter'];
|
||||||
$formatter = $this->exportManager->getFormatter($formData['pick_formatter']);
|
$formatter = $this->exportManager->getFormatter($formData['pick_formatter']);
|
||||||
$serialized['formatter']['form'] = $formatter->normalizeFormData($formData['formatter']);
|
$serialized['formatter']['form'] = $formatter->normalizeFormData($formData['formatter']);
|
||||||
$serialized['formatter']['version'] = $formatter->getNormalizationVersion();
|
$serialized['formatter']['version'] = $formatter->getNormalizationVersion();
|
||||||
} elseif ($export instanceof DirectExportInterface) {
|
|
||||||
$serialized['formatter'] = ['form' => [], 'version' => 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $serialized;
|
return $serialized;
|
||||||
}
|
}
|
||||||
@@ -91,12 +87,7 @@ class ExportConfigNormalizer
|
|||||||
public function denormalizeConfig(string $exportAlias, array $serializedData, bool $replaceDisabledByDefaultData = false): array
|
public function denormalizeConfig(string $exportAlias, array $serializedData, bool $replaceDisabledByDefaultData = false): array
|
||||||
{
|
{
|
||||||
$export = $this->exportManager->getExport($exportAlias);
|
$export = $this->exportManager->getExport($exportAlias);
|
||||||
|
$formater = $this->exportManager->getFormatter($serializedData['pick_formatter']);
|
||||||
if ($export instanceof ExportInterface) {
|
|
||||||
$formatter = $this->exportManager->getFormatter($serializedData['pick_formatter']);
|
|
||||||
} else {
|
|
||||||
$formatter = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$filtersConfig = [];
|
$filtersConfig = [];
|
||||||
foreach ($serializedData['filters'] as $alias => $filterData) {
|
foreach ($serializedData['filters'] as $alias => $filterData) {
|
||||||
@@ -126,8 +117,8 @@ class ExportConfigNormalizer
|
|||||||
'export' => $export->denormalizeFormData($serializedData['export']['form'], $serializedData['export']['version']),
|
'export' => $export->denormalizeFormData($serializedData['export']['form'], $serializedData['export']['version']),
|
||||||
'filters' => $filtersConfig,
|
'filters' => $filtersConfig,
|
||||||
'aggregators' => $aggregatorsConfig,
|
'aggregators' => $aggregatorsConfig,
|
||||||
'pick_formatter' => $serializedData['pick_formatter'] ?? '',
|
'pick_formatter' => $serializedData['pick_formatter'],
|
||||||
'formatter' => $formatter?->denormalizeFormData($serializedData['formatter']['form'], $serializedData['formatter']['version']),
|
'formatter' => $formater->denormalizeFormData($serializedData['formatter']['form'], $serializedData['formatter']['version']),
|
||||||
'centers' => [
|
'centers' => [
|
||||||
'centers' => array_values(array_filter(array_map(fn (int $id) => $this->centerRepository->find($id), $serializedData['centers']['centers']), fn ($item) => null !== $item)),
|
'centers' => array_values(array_filter(array_map(fn (int $id) => $this->centerRepository->find($id), $serializedData['centers']['centers']), fn ($item) => null !== $item)),
|
||||||
'regroupments' => array_values(array_filter(array_map(fn (int $id) => $this->regroupmentRepository->find($id), $serializedData['centers']['regroupments']), fn ($item) => null !== $item)),
|
'regroupments' => array_values(array_filter(array_map(fn (int $id) => $this->regroupmentRepository->find($id), $serializedData['centers']['regroupments']), fn ($item) => null !== $item)),
|
||||||
|
@@ -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
|
||||||
|
@@ -14,14 +14,13 @@ namespace Chill\MainBundle\Repository;
|
|||||||
use Chill\MainBundle\Entity\AddressReference;
|
use Chill\MainBundle\Entity\AddressReference;
|
||||||
use Chill\MainBundle\Entity\PostalCode;
|
use Chill\MainBundle\Entity\PostalCode;
|
||||||
use Chill\MainBundle\Search\SearchApiQuery;
|
use Chill\MainBundle\Search\SearchApiQuery;
|
||||||
use Doctrine\DBAL\Types\Types;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\ORM\NativeQuery;
|
|
||||||
use Doctrine\ORM\Query\ResultSetMapping;
|
use Doctrine\ORM\Query\ResultSetMapping;
|
||||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
final readonly class AddressReferenceRepository implements AddressReferenceRepositoryInterface
|
final readonly class AddressReferenceRepository implements ObjectRepository
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $entityManager;
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
@@ -66,121 +65,6 @@ final readonly class AddressReferenceRepository implements AddressReferenceRepos
|
|||||||
return $this->repository->findAll();
|
return $this->repository->findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findAggregatedBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable
|
|
||||||
{
|
|
||||||
$terms = $this->buildTermsFromSearchString($search);
|
|
||||||
if ([] === $terms) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$connection = $this->entityManager->getConnection();
|
|
||||||
$qb = $connection->createQueryBuilder();
|
|
||||||
|
|
||||||
$qb->select('row_number() OVER () AS row_number', 'var.street AS street', 'cmpc.id AS postcode_id', 'cmpc.code AS code', 'cmpc.label AS label', 'jsonb_object_agg(var.address_id, var.streetnumber ORDER BY var.row_number) AS positions')
|
|
||||||
->from('view_chill_main_address_reference', 'var')
|
|
||||||
->innerJoin('var', 'chill_main_postal_code', 'cmpc', 'cmpc.id = var.postcode_id')
|
|
||||||
->groupBy('cmpc.id', 'var.street')
|
|
||||||
->setFirstResult($firstResult)
|
|
||||||
->setMaxResults($maxResults);
|
|
||||||
|
|
||||||
$paramId = 0;
|
|
||||||
|
|
||||||
foreach ($terms as $term) {
|
|
||||||
$qb->andWhere('var.address like UNACCENT(LOWER(?))');
|
|
||||||
$qb->setParameter(++$paramId, "%{$term}%");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== $postalCode) {
|
|
||||||
$qb->andWhere('var.postcode_id = ?');
|
|
||||||
$qb->setParameter(++$paramId, $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $qb->executeQuery();
|
|
||||||
|
|
||||||
foreach ($result->iterateAssociative() as $row) {
|
|
||||||
yield [...$row, 'positions' => json_decode($row['positions'], true, 512, JSON_THROW_ON_ERROR)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return iterable<AddressReference>
|
|
||||||
*/
|
|
||||||
public function findBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable
|
|
||||||
{
|
|
||||||
$terms = $this->buildTermsFromSearchString($search);
|
|
||||||
if ([] === $terms) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$rsm = new ResultSetMappingBuilder($this->entityManager);
|
|
||||||
$rsm->addRootEntityFromClassMetadata(AddressReference::class, 'ar');
|
|
||||||
$baseSql = 'SELECT '.$rsm->generateSelectClause(['ar' => 'ar']).' FROM chill_main_address_reference ar JOIN
|
|
||||||
view_chill_main_address_reference var ON var.address_id = ar.id';
|
|
||||||
$nql = $this->buildQueryBySearchString($rsm, $baseSql, $terms, $postalCode);
|
|
||||||
|
|
||||||
$orderBy = [];
|
|
||||||
$pertinence = [];
|
|
||||||
foreach ($terms as $k => $term) {
|
|
||||||
$pertinence[] =
|
|
||||||
"(EXISTS (SELECT 1 FROM unnest(string_to_array(address, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(:order{$k})))))::int";
|
|
||||||
$pertinence[] = "(address LIKE UNACCENT(LOWER(:order{$k})))::int";
|
|
||||||
$nql->setParameter('order'.$k, $term);
|
|
||||||
}
|
|
||||||
$orderBy[] = implode(' + ', $pertinence).' ASC';
|
|
||||||
$orderBy[] = implode('row_number ASC', $orderBy);
|
|
||||||
|
|
||||||
$nql->setSQL($nql->getSQL().' ORDER BY '.implode(', ', $orderBy));
|
|
||||||
|
|
||||||
return $nql->toIterable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function countBySearchString(string $search, PostalCode|int|null $postalCode = null): int
|
|
||||||
{
|
|
||||||
$terms = $this->buildTermsFromSearchString($search);
|
|
||||||
if ([] === $terms) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$rsm = new ResultSetMappingBuilder($this->entityManager);
|
|
||||||
$rsm->addScalarResult('c', 'c', Types::INTEGER);
|
|
||||||
$nql = $this->buildQueryBySearchString($rsm, 'SELECT COUNT(var.*) AS c FROM view_chill_main_address_reference var', $terms, $postalCode);
|
|
||||||
|
|
||||||
return $nql->getSingleScalarResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildTermsFromSearchString(string $search): array
|
|
||||||
{
|
|
||||||
return array_filter(
|
|
||||||
array_map(
|
|
||||||
static fn (string $term) => trim($term),
|
|
||||||
explode(' ', $search)
|
|
||||||
),
|
|
||||||
static fn (string $term) => '' !== $term
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildQueryBySearchString(ResultSetMapping $rsm, string $select, array $terms, PostalCode|int|null $postalCode = null): NativeQuery
|
|
||||||
{
|
|
||||||
$nql = $this->entityManager->createNativeQuery('', $rsm);
|
|
||||||
|
|
||||||
$sql = $select.' WHERE ';
|
|
||||||
|
|
||||||
$wheres = [];
|
|
||||||
foreach ($terms as $k => $term) {
|
|
||||||
$wheres[] = "var.address like :w{$k}";
|
|
||||||
$nql->setParameter("w{$k}", '%'.$term.'%', Types::STRING);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== $postalCode) {
|
|
||||||
$wheres[] = 'var.postcode_id = :postalCode';
|
|
||||||
$nql->setParameter('postalCode', $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
$nql->setSQL($sql.implode(' AND ', $wheres));
|
|
||||||
|
|
||||||
return $nql;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed|null $limit
|
* @param mixed|null $limit
|
||||||
* @param mixed|null $offset
|
* @param mixed|null $offset
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Repository;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\PostalCode;
|
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
|
||||||
|
|
||||||
interface AddressReferenceRepositoryInterface extends ObjectRepository
|
|
||||||
{
|
|
||||||
public function findAggregatedBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable;
|
|
||||||
}
|
|
@@ -1,69 +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\Repository;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Connection;
|
|
||||||
|
|
||||||
final readonly class PostalCodeForAddressReferenceRepository implements PostalCodeForAddressReferenceRepositoryInterface
|
|
||||||
{
|
|
||||||
public function __construct(private Connection $connection) {}
|
|
||||||
|
|
||||||
public function findPostalCode(string $search, int $firstResult = 0, int $maxResults = 50): iterable
|
|
||||||
{
|
|
||||||
$terms = $this->buildTermsFromSearchString($search);
|
|
||||||
if ([] === $terms) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$qb = $this->connection->createQueryBuilder();
|
|
||||||
|
|
||||||
$qb->from('chill_main_postal_code', 'cmpc')
|
|
||||||
->join('cmpc', 'view_chill_main_address_reference', 'vcmar', 'vcmar.postcode_id = cmpc.id')
|
|
||||||
->join('vcmar', 'country', 'country', condition: 'cmpc.country_id = country.id')
|
|
||||||
->setFirstResult($firstResult)
|
|
||||||
->setMaxResults($maxResults)
|
|
||||||
;
|
|
||||||
|
|
||||||
$qb->select(
|
|
||||||
'DISTINCT ON (cmpc.code, cmpc.label) cmpc.id AS postcode_id',
|
|
||||||
'cmpc.code AS code',
|
|
||||||
'cmpc.label AS label',
|
|
||||||
'country.id AS country_id',
|
|
||||||
'country.countrycode AS country_code',
|
|
||||||
'country.name AS country_name'
|
|
||||||
);
|
|
||||||
|
|
||||||
$paramId = 0;
|
|
||||||
|
|
||||||
foreach ($terms as $term) {
|
|
||||||
$qb->andWhere('vcmar.address like ?');
|
|
||||||
$qb->setParameter(++$paramId, "%{$term}%");
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $qb->executeQuery();
|
|
||||||
|
|
||||||
foreach ($result->iterateAssociative() as $row) {
|
|
||||||
yield [...$row, 'country_name' => json_decode($row['country_name'], true, 512, JSON_THROW_ON_ERROR)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildTermsFromSearchString(string $search): array
|
|
||||||
{
|
|
||||||
return array_filter(
|
|
||||||
array_map(
|
|
||||||
static fn (string $term) => trim($term),
|
|
||||||
explode(' ', $search)
|
|
||||||
),
|
|
||||||
static fn (string $term) => '' !== $term
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for postal code using optimized materialized view.
|
|
||||||
*/
|
|
||||||
interface PostalCodeForAddressReferenceRepositoryInterface
|
|
||||||
{
|
|
||||||
public function findPostalCode(string $search, int $firstResult = 0, int $maxResults = 50): iterable;
|
|
||||||
}
|
|
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,19 +69,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()
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,6 +1,5 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
export interface DateTime {
|
export interface DateTime {
|
||||||
datetime: string;
|
datetime: string;
|
||||||
@@ -75,7 +74,6 @@ export interface Postcode {
|
|||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
center: Point;
|
center: Point;
|
||||||
country: Country;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Point {
|
export interface Point {
|
||||||
@@ -91,28 +89,6 @@ export interface Country {
|
|||||||
|
|
||||||
export type AddressRefStatus = "match" | "to_review" | "reviewed";
|
export type AddressRefStatus = "match" | "to_review" | "reviewed";
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface to create an address
|
|
||||||
*/
|
|
||||||
export interface AddressCreation {
|
|
||||||
confidential: boolean;
|
|
||||||
isNoAddress: boolean;
|
|
||||||
street: string;
|
|
||||||
streetNumber: string;
|
|
||||||
postcode: Postcode;
|
|
||||||
point: Point; // [number, number]; // [longitude, latitude]
|
|
||||||
addressReference: AddressReference;
|
|
||||||
validFrom: DateTime|null;
|
|
||||||
floor: string;
|
|
||||||
corridor: string;
|
|
||||||
steps: string;
|
|
||||||
flat: string;
|
|
||||||
buildingName: string;
|
|
||||||
distribution: string;
|
|
||||||
extra: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface Address {
|
export interface Address {
|
||||||
type: "address";
|
type: "address";
|
||||||
address_id: number;
|
address_id: number;
|
||||||
@@ -131,7 +107,7 @@ export interface Address {
|
|||||||
confidential: boolean;
|
confidential: boolean;
|
||||||
lines: string[];
|
lines: string[];
|
||||||
addressReference: AddressReference | null;
|
addressReference: AddressReference | null;
|
||||||
validFrom: DateTime | null; // TODO there is no null for validFrom
|
validFrom: DateTime;
|
||||||
validTo: DateTime | null;
|
validTo: DateTime | null;
|
||||||
point: Point | null;
|
point: Point | null;
|
||||||
refStatus: AddressRefStatus;
|
refStatus: AddressRefStatus;
|
||||||
@@ -226,58 +202,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";
|
||||||
@@ -291,8 +215,3 @@ export interface ExportGeneration {
|
|||||||
export interface PrivateCommentEmbeddable {
|
export interface PrivateCommentEmbeddable {
|
||||||
comments: Record<number, string>;
|
comments: Record<number, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Possible states for the WaitingScreen Component.
|
|
||||||
*/
|
|
||||||
export type WaitingScreenState = "pending" | "failure" | "stopped" | "ready";
|
|
||||||
|
@@ -1,33 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
|
||||||
import AddressPicker from "ChillMainAssets/vuejs/AddressPicker/AddressPicker.vue";
|
|
||||||
import {Ref, ref} from "vue";
|
|
||||||
|
|
||||||
const showModal: Ref<boolean> = ref(false);
|
|
||||||
|
|
||||||
const modalDialogClasses = {"modal-dialog": true, "modal-dialog-scrollable": true, "modal-xl": true};
|
|
||||||
|
|
||||||
const clickButton = () => {
|
|
||||||
showModal.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
showModal.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<modal v-if="showModal" :hide-footer="false" :modal-dialog-class="modalDialogClasses" @close="closeModal">
|
|
||||||
<template v-slot:header>TODO</template>
|
|
||||||
<template v-slot:body>
|
|
||||||
<AddressPicker></AddressPicker>
|
|
||||||
</template>
|
|
||||||
</modal>
|
|
||||||
<button class="btn btn-submit" type="button" @click="clickButton">SEARCH ADDRESS</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,170 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {Address, AddressReference} from "ChillMainAssets/types";
|
|
||||||
import SearchBar from "ChillMainAssets/vuejs/AddressPicker/Component/SearchBar.vue";
|
|
||||||
import {
|
|
||||||
AddressAggregated,
|
|
||||||
AssociatedPostalCode, fetchAddressReference,
|
|
||||||
getAddressesAggregated,
|
|
||||||
getPostalCodes,
|
|
||||||
} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
|
|
||||||
import {computed, Ref, ref} from "vue";
|
|
||||||
import AddressAggregatedList from "ChillMainAssets/vuejs/AddressPicker/Component/AddressAggregatedList.vue";
|
|
||||||
import AddressDetailsForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressDetailsForm.vue";
|
|
||||||
import AddressForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressForm.vue";
|
|
||||||
import {trans, SAVE} from "translator";
|
|
||||||
|
|
||||||
interface AddressPickerProps {
|
|
||||||
suggestions?: Address[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<AddressPickerProps>(), {
|
|
||||||
suggestions: () => [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const addresses: Ref<AddressAggregated[]> = ref([]);
|
|
||||||
const postalCodes: Ref<AssociatedPostalCode[]> = ref([]);
|
|
||||||
const searchTokens: Ref<string[]> = ref([]);
|
|
||||||
const addressReference: Ref<AddressReference|null> = ref(null);
|
|
||||||
|
|
||||||
let abortControllerSearchAddress: null | AbortController = null;
|
|
||||||
let abortControllerSearchPostalCode: null | AbortController = null;
|
|
||||||
|
|
||||||
const searchResultsClasses = computed(() => ({
|
|
||||||
"mid-size": addressReference !== null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const floor = ref<string>("");
|
|
||||||
const corridor = ref<string>("");
|
|
||||||
const steps = ref<string>("");
|
|
||||||
const flat = ref<string>("");
|
|
||||||
const buildingName = ref<string>("");
|
|
||||||
const extra = ref<string>("");
|
|
||||||
const distribution = ref<string>("");
|
|
||||||
|
|
||||||
|
|
||||||
const onSearch = async function (search: string): Promise<void> {
|
|
||||||
performSearchForAddress(search);
|
|
||||||
performSearchForPostalCode(search);
|
|
||||||
searchTokens.value = [search];
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPickPosition = async (id: string) => {
|
|
||||||
console.log('Pick Position', id);
|
|
||||||
addressReference.value = await fetchAddressReference(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const performSearchForAddress = async (search: string): Promise<void> => {
|
|
||||||
if (null !== abortControllerSearchAddress) {
|
|
||||||
abortControllerSearchAddress.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("" === search) {
|
|
||||||
addresses.value = [];
|
|
||||||
abortControllerSearchAddress = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
abortControllerSearchAddress = new AbortController();
|
|
||||||
|
|
||||||
console.log("onSearch", search);
|
|
||||||
try {
|
|
||||||
addresses.value = await getAddressesAggregated(
|
|
||||||
search,
|
|
||||||
abortControllerSearchAddress,
|
|
||||||
);
|
|
||||||
abortControllerSearchAddress = null;
|
|
||||||
|
|
||||||
// check if there is only one result
|
|
||||||
if (addresses.value.length === 1 && Object.keys(addresses.value[0].positions).length === 1) {
|
|
||||||
onPickPosition(Object.keys(addresses.value[0].positions)[0]);
|
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
|
||||||
if (e instanceof DOMException && e.name === "AbortError") {
|
|
||||||
console.log("search aborted for:", search);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const performSearchForPostalCode = async (search: string): Promise<void> => {
|
|
||||||
if (null !== abortControllerSearchPostalCode) {
|
|
||||||
abortControllerSearchPostalCode.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("" === search) {
|
|
||||||
addresses.value = [];
|
|
||||||
abortControllerSearchPostalCode = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
abortControllerSearchPostalCode = new AbortController();
|
|
||||||
|
|
||||||
console.log("onSearch", search);
|
|
||||||
try {
|
|
||||||
postalCodes.value = await getPostalCodes(
|
|
||||||
search,
|
|
||||||
abortControllerSearchPostalCode,
|
|
||||||
);
|
|
||||||
abortControllerSearchPostalCode = null;
|
|
||||||
} catch (e: unknown) {
|
|
||||||
if (e instanceof DOMException && e.name === "AbortError") {
|
|
||||||
console.log("search postal code aborted for:", search);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const save = async(): Promise<void> => {
|
|
||||||
console.log("save");
|
|
||||||
console.log("content", floor, corridor, steps, flat, buildingName, extra, distribution);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<search-bar @search="onSearch"></search-bar>
|
|
||||||
<div class="address-pick-content">
|
|
||||||
<div class="search-results" :class="searchResultsClasses">
|
|
||||||
<address-aggregated-list :addresses="addresses" :search-tokens="searchTokens" @pick-position="(id) => onPickPosition(id)"></address-aggregated-list>
|
|
||||||
</div>
|
|
||||||
<div v-if="addressReference !== null" class="address-details-form">
|
|
||||||
<address-details-form :address="addressReference"
|
|
||||||
v-model:floor="floor"
|
|
||||||
v-model:corridor="corridor"
|
|
||||||
v-model:steps="steps"
|
|
||||||
v-model:flat="flat"
|
|
||||||
v-model:building-name="buildingName"
|
|
||||||
v-model:extra="extra"
|
|
||||||
v-model:distribution="distribution"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ul class="record_actions">
|
|
||||||
<li><button class="btn btn-save">{{ trans(SAVE) }}</button></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.address-pick-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
.search-results {
|
|
||||||
&.mid-size {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.address-details-form {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,25 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {AddressAggregated} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
|
|
||||||
import AddressAggregatedListItem from "ChillMainAssets/vuejs/AddressPicker/Component/AddressAggregatedListItem.vue";
|
|
||||||
|
|
||||||
interface AddressAggregatedListProps {
|
|
||||||
addresses: AddressAggregated[];
|
|
||||||
searchTokens: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<AddressAggregatedListProps>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
pickPosition: [id: string]
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<template v-for="a in props.addresses" :key="a.row_number">
|
|
||||||
<address-aggregated-list-item :address="a" :search-tokens="props.searchTokens" @pick-position="(id) => emit('pickPosition', id)"></address-aggregated-list-item>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,82 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {AddressAggregated} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
|
|
||||||
import {computed, ref} from "vue";
|
|
||||||
|
|
||||||
interface AddressAggregatedListItemProps {
|
|
||||||
address: AddressAggregated;
|
|
||||||
searchTokens: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<AddressAggregatedListItemProps>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
pickPosition: [id: string]
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const showAllPositions = ref<boolean>(false);
|
|
||||||
const positionsToShow = computed((): Record<string, string> => {
|
|
||||||
const obj: Record<string, any> = {};
|
|
||||||
let count = 0;
|
|
||||||
for (const [id, position] of Object.entries(props.address.positions)) {
|
|
||||||
obj[id] = position;
|
|
||||||
count++;
|
|
||||||
if (count >= 10 && !showAllPositions.value) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
})
|
|
||||||
const needToShowMorePosition = computed(() => {
|
|
||||||
return Object.keys(props.address.positions).length > 10;
|
|
||||||
})
|
|
||||||
|
|
||||||
const onClickButton = (id: string) => {
|
|
||||||
console.log('onClickButton', id);
|
|
||||||
emit('pickPosition', id);
|
|
||||||
}
|
|
||||||
const displayAllPositions = () => {
|
|
||||||
showAllPositions.value = true;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="street">
|
|
||||||
<span>{{ props.address.street }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="postcode">
|
|
||||||
<span>{{ props.address.code }}</span> <span>{{ address.label }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="positions">
|
|
||||||
<ul>
|
|
||||||
<li v-for="(position, id) in positionsToShow" :key="id" >
|
|
||||||
<button type="button" @click="onClickButton(id)" >
|
|
||||||
{{ position }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li v-if="needToShowMorePosition">
|
|
||||||
<button @click="displayAllPositions">show all</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.street {
|
|
||||||
font-variant: small-caps;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.postcode {
|
|
||||||
font-variant: small-caps;
|
|
||||||
}
|
|
||||||
.positions ul {
|
|
||||||
list-style-type: none;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,44 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {AddressReference} from "ChillMainAssets/types";
|
|
||||||
import {computed, ref} from "vue";
|
|
||||||
import {addressReferenceToAddress} from "ChillMainAssets/vuejs/AddressPicker/helper";
|
|
||||||
import AddressDetailsContent from "ChillMainAssets/vuejs/_components/AddressDetails/AddressDetailsContent.vue";
|
|
||||||
import AddressForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressForm.vue";
|
|
||||||
|
|
||||||
export interface AddressDetailsFormProps {
|
|
||||||
address: AddressReference;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<AddressDetailsFormProps>();
|
|
||||||
|
|
||||||
const floor = ref<string>("");
|
|
||||||
const corridor = ref<string>("");
|
|
||||||
const steps = ref<string>("");
|
|
||||||
const flat = ref<string>("");
|
|
||||||
const buildingName = ref<string>("");
|
|
||||||
const extra = ref<string>("");
|
|
||||||
const distribution = ref<string>("");
|
|
||||||
|
|
||||||
const address = computed(() => addressReferenceToAddress(props.address));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<address-form
|
|
||||||
@update:floor="val => (floor = val)"
|
|
||||||
@update:corridor="val => (corridor = val)"
|
|
||||||
@update:steps="val => (steps = val)"
|
|
||||||
@update:flat="val => (flat = val)"
|
|
||||||
@update:building-name="val => (buildingName = val)"
|
|
||||||
@update:extra="val => (extra = val)"
|
|
||||||
@update:distribution="val => (distribution = val)"
|
|
||||||
></address-form>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<address-details-content :address="address"></address-details-content>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,112 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
ADDRESS_STREET,
|
|
||||||
ADDRESS_STREET_NUMBER,
|
|
||||||
ADDRESS_FLOOR,
|
|
||||||
ADDRESS_CORRIDOR,
|
|
||||||
ADDRESS_STEPS,
|
|
||||||
ADDRESS_FLAT,
|
|
||||||
ADDRESS_BUILDING_NAME,
|
|
||||||
ADDRESS_DISTRIBUTION,
|
|
||||||
ADDRESS_EXTRA,
|
|
||||||
ADDRESS_FILL_AN_ADDRESS,
|
|
||||||
trans,
|
|
||||||
} from "translator";
|
|
||||||
import {ref} from "vue";
|
|
||||||
|
|
||||||
const isNoAddress = ref(false);
|
|
||||||
|
|
||||||
const floor = defineModel("floor");
|
|
||||||
const corridor = defineModel("corridor");
|
|
||||||
const steps = defineModel("steps");
|
|
||||||
const flat = defineModel("flat");
|
|
||||||
const buildingName = defineModel("buildingName");
|
|
||||||
const extra = defineModel("extra");
|
|
||||||
const distribution = defineModel("distribution");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="form-floating my-1">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="floor"
|
|
||||||
:placeholder="trans(ADDRESS_FLOOR)"
|
|
||||||
v-model="floor"
|
|
||||||
/>
|
|
||||||
<label for="floor">{{ trans(ADDRESS_FLOOR) }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating my-1">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="corridor"
|
|
||||||
:placeholder="trans(ADDRESS_CORRIDOR)"
|
|
||||||
v-model="corridor"
|
|
||||||
/>
|
|
||||||
<label for="corridor">{{ trans(ADDRESS_CORRIDOR) }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating my-1">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="steps"
|
|
||||||
:placeholder="trans(ADDRESS_STEPS)"
|
|
||||||
v-model="steps"
|
|
||||||
/>
|
|
||||||
<label for="steps">{{ trans(ADDRESS_STEPS) }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating my-1">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="flat"
|
|
||||||
:placeholder="trans(ADDRESS_FLAT)"
|
|
||||||
v-model="flat"
|
|
||||||
/>
|
|
||||||
<label for="flat">{{ trans(ADDRESS_FLAT) }}</label>
|
|
||||||
</div>
|
|
||||||
<div :class="isNoAddress ? 'col-lg-12' : 'col-lg-6'">
|
|
||||||
<div class="form-floating my-1" v-if="!isNoAddress">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="buildingName"
|
|
||||||
maxlength="255"
|
|
||||||
:placeholder="trans(ADDRESS_BUILDING_NAME)"
|
|
||||||
v-model="buildingName"
|
|
||||||
/>
|
|
||||||
<label for="buildingName">{{
|
|
||||||
trans(ADDRESS_BUILDING_NAME)
|
|
||||||
}}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating my-1">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="extra"
|
|
||||||
maxlength="255"
|
|
||||||
:placeholder="trans(ADDRESS_EXTRA)"
|
|
||||||
v-model="extra"
|
|
||||||
/>
|
|
||||||
<label for="extra">{{ trans(ADDRESS_EXTRA) }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating my-1" v-if="!isNoAddress">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="distribution"
|
|
||||||
maxlength="255"
|
|
||||||
:placeholder="trans(ADDRESS_DISTRIBUTION)"
|
|
||||||
v-model="distribution"
|
|
||||||
/>
|
|
||||||
<label for="distribution">{{
|
|
||||||
trans(ADDRESS_DISTRIBUTION)
|
|
||||||
}}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,39 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ADDRESS_PICKER_SEARCH_FOR_ADDRESSES, trans } from 'translator';
|
|
||||||
const emits = defineEmits<{
|
|
||||||
search: [search: string];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
let searchTimer = 0;
|
|
||||||
let searchString: string;
|
|
||||||
|
|
||||||
const onInput = function (event: InputEvent) {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
const value = target.value;
|
|
||||||
searchString = value;
|
|
||||||
|
|
||||||
if (0 === searchTimer) {
|
|
||||||
window.clearTimeout(searchTimer);
|
|
||||||
searchTimer = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchTimer = window.setTimeout(() => {
|
|
||||||
if (value === searchString) {
|
|
||||||
emits("search", value);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<span class="input-group-text">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
|
|
||||||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<input type="search" class="form-control" @input="onInput" :placeholder="trans(ADDRESS_PICKER_SEARCH_FOR_ADDRESSES)" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
@@ -1,69 +0,0 @@
|
|||||||
import {AddressReference, TranslatableString} from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
export interface AddressAggregated {
|
|
||||||
row_number: number;
|
|
||||||
street: string;
|
|
||||||
postcode_id: number;
|
|
||||||
code: string;
|
|
||||||
label: string;
|
|
||||||
positions: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssociatedPostalCode {
|
|
||||||
postcode_id: number;
|
|
||||||
code: string;
|
|
||||||
label: string;
|
|
||||||
country_id: number;
|
|
||||||
country_code: string;
|
|
||||||
country_name: TranslatableString;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws {DOMException} when fetch is aborted, the property name is always equals to 'AbortError'
|
|
||||||
*/
|
|
||||||
export const getAddressesAggregated = async (
|
|
||||||
search: string,
|
|
||||||
abortController: AbortController,
|
|
||||||
): Promise<AddressAggregated[]> => {
|
|
||||||
const params = new URLSearchParams({ q: search.trim() });
|
|
||||||
let response = null;
|
|
||||||
|
|
||||||
response = await fetch(
|
|
||||||
`/api/1.0/main/address-reference/aggregated/search?${params}`,
|
|
||||||
{ signal: abortController.signal },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPostalCodes = async (
|
|
||||||
search: string,
|
|
||||||
abortController: AbortController,
|
|
||||||
): Promise<AssociatedPostalCode[]> => {
|
|
||||||
const params = new URLSearchParams({ q: search.trim() });
|
|
||||||
let response = null;
|
|
||||||
|
|
||||||
response = await fetch(
|
|
||||||
`/api/1.0/main/address-reference/postal-code/search?${params}`,
|
|
||||||
{ signal: abortController.signal },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchAddressReference = async (id: string): Promise<AddressReference> => {
|
|
||||||
const response = await fetch(`/api/1.0/main/address-reference/${id}.json`);
|
|
||||||
if (response.ok) {
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
}
|
|
@@ -1,21 +0,0 @@
|
|||||||
import {Address, AddressCreation, AddressReference} from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
export const addressReferenceToAddress = (reference: AddressReference): AddressCreation => {
|
|
||||||
return {
|
|
||||||
street: reference.street,
|
|
||||||
streetNumber: reference.streetNumber,
|
|
||||||
postcode: reference.postcode,
|
|
||||||
floor: "",
|
|
||||||
corridor: "",
|
|
||||||
steps: "",
|
|
||||||
flat: "",
|
|
||||||
buildingName: "",
|
|
||||||
distribution: "",
|
|
||||||
extra: "",
|
|
||||||
confidential: false,
|
|
||||||
addressReference: reference,
|
|
||||||
point: reference.point,
|
|
||||||
isNoAddress: false,
|
|
||||||
validFrom: null,
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,12 +0,0 @@
|
|||||||
import { createApp } from "vue";
|
|
||||||
import AddressButton from "ChillMainAssets/vuejs/AddressPicker/AddressButton.vue";
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
|
||||||
document
|
|
||||||
.querySelectorAll<HTMLDivElement>("div[data-address-picker]")
|
|
||||||
.forEach((elem): void => {
|
|
||||||
const app = createApp(AddressButton);
|
|
||||||
|
|
||||||
app.mount(elem);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -10,8 +10,7 @@ import { computed, onMounted, ref } from "vue";
|
|||||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||||
import { fetchExportGenerationStatus } from "ChillMainAssets/lib/api/export";
|
import { fetchExportGenerationStatus } from "ChillMainAssets/lib/api/export";
|
||||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||||
import WaitingScreen from "../_components/WaitingScreen.vue";
|
import { ExportGeneration } from "ChillMainAssets/types";
|
||||||
import { ExportGeneration, WaitingScreenState } from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
exportGenerationId: string;
|
exportGenerationId: string;
|
||||||
@@ -35,16 +34,13 @@ const storedObject = computed<null | StoredObject>(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isPending = computed<boolean>(() => status.value === "pending");
|
const isPending = computed<boolean>(() => status.value === "pending");
|
||||||
|
const isFetching = computed<boolean>(
|
||||||
|
() => tryiesForReady.value < maxTryiesForReady,
|
||||||
|
);
|
||||||
|
const isReady = computed<boolean>(() => status.value === "ready");
|
||||||
|
const isFailure = computed<boolean>(() => status.value === "failure");
|
||||||
const filename = computed<string>(() => `${props.title}-${props.createdDate}`);
|
const filename = computed<string>(() => `${props.title}-${props.createdDate}`);
|
||||||
|
|
||||||
const state = computed<WaitingScreenState>((): WaitingScreenState => {
|
|
||||||
if (status.value === "empty") {
|
|
||||||
return "pending";
|
|
||||||
}
|
|
||||||
|
|
||||||
return status.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* counter for the number of times that we check for a new status
|
* counter for the number of times that we check for a new status
|
||||||
*/
|
*/
|
||||||
@@ -89,26 +85,38 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<WaitingScreen :state="state">
|
<div id="waiting-screen">
|
||||||
<template v-slot:pending>
|
<div
|
||||||
|
v-if="isPending && isFetching"
|
||||||
|
class="alert alert-danger text-center"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
<p>
|
<p>
|
||||||
{{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
|
{{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<template v-slot:stopped>
|
<div>
|
||||||
|
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isPending && !isFetching" class="alert alert-info">
|
||||||
|
<div>
|
||||||
<p>
|
<p>
|
||||||
{{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
|
{{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</div>
|
||||||
|
</div>
|
||||||
<template v-slot:failure>
|
<div v-if="isFailure" class="alert alert-danger text-center">
|
||||||
|
<div>
|
||||||
<p>
|
<p>
|
||||||
{{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
|
{{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</div>
|
||||||
|
</div>
|
||||||
<template v-slot:ready>
|
<div v-if="isReady" class="alert alert-success text-center">
|
||||||
|
<div>
|
||||||
<p>
|
<p>
|
||||||
{{ trans(EXPORT_GENERATION_EXPORT_READY) }}
|
{{ trans(EXPORT_GENERATION_EXPORT_READY) }}
|
||||||
</p>
|
</p>
|
||||||
@@ -119,6 +127,15 @@ onMounted(() => {
|
|||||||
:filename="filename"
|
:filename="filename"
|
||||||
></document-action-buttons-group>
|
></document-action-buttons-group>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</div>
|
||||||
</WaitingScreen>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
#waiting-screen {
|
||||||
|
> .alert {
|
||||||
|
min-height: 350px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@@ -1,75 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useIntervalFn } from "@vueuse/core";
|
|
||||||
import { fetchWorkflow } from "ChillMainAssets/lib/workflow/api";
|
|
||||||
import { returnPathOr } from "ChillMainAssets/lib/return_path/returnPathHelper";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import WaitingScreen from "ChillMainAssets/vuejs/_components/WaitingScreen.vue";
|
|
||||||
import { WaitingScreenState } from "ChillMainAssets/types";
|
|
||||||
import {
|
|
||||||
trans,
|
|
||||||
WORKFLOW_WAIT_TITLE,
|
|
||||||
WORKFLOW_WAIT_ERROR_WHILE_WAITING,
|
|
||||||
WORKFLOW_WAIT_SUCCESS,
|
|
||||||
} from "translator";
|
|
||||||
|
|
||||||
interface WaitPostProcessWorkflowComponentProps {
|
|
||||||
workflowId: number;
|
|
||||||
expectedStep: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<WaitPostProcessWorkflowComponentProps>();
|
|
||||||
const counter = ref<number>(0);
|
|
||||||
const MAX_TRYIES = 50;
|
|
||||||
|
|
||||||
const state = ref<WaitingScreenState>("pending");
|
|
||||||
|
|
||||||
const { pause, resume } = useIntervalFn(
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const workflow = await fetchWorkflow(props.workflowId);
|
|
||||||
counter.value++;
|
|
||||||
if (workflow.currentStep.currentStep.name === props.expectedStep) {
|
|
||||||
window.location.assign(
|
|
||||||
returnPathOr("/fr/main/workflow" + workflow.id + "/show"),
|
|
||||||
);
|
|
||||||
resume();
|
|
||||||
state.value = "ready";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counter.value > MAX_TRYIES) {
|
|
||||||
pause();
|
|
||||||
state.value = "failure";
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
pause();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
2000,
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="container">
|
|
||||||
<WaitingScreen :state="state">
|
|
||||||
<template v-slot:pending>
|
|
||||||
<p>
|
|
||||||
{{ trans(WORKFLOW_WAIT_TITLE) }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-slot:failure>
|
|
||||||
<p>
|
|
||||||
{{ trans(WORKFLOW_WAIT_ERROR_WHILE_WAITING) }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-slot:ready>
|
|
||||||
<p>
|
|
||||||
{{ trans(WORKFLOW_WAIT_SUCCESS) }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</WaitingScreen>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
@@ -1,51 +0,0 @@
|
|||||||
import { createApp } from "vue";
|
|
||||||
import App from "./App.vue";
|
|
||||||
|
|
||||||
function mountApp(): void {
|
|
||||||
const el = document.querySelector<HTMLDivElement>(".screen-wait");
|
|
||||||
if (!el) {
|
|
||||||
console.error(
|
|
||||||
"WaitPostProcessWorkflow: mount element .screen-wait not found",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowIdAttr = el.getAttribute("data-workflow-id");
|
|
||||||
const expectedStep = el.getAttribute("data-expected-step") || "";
|
|
||||||
|
|
||||||
if (!workflowIdAttr) {
|
|
||||||
console.error(
|
|
||||||
"WaitPostProcessWorkflow: data-workflow-id attribute missing on mount element",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!expectedStep) {
|
|
||||||
console.error(
|
|
||||||
"WaitPostProcessWorkflow: data-expected-step attribute missing on mount element",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowId = Number(workflowIdAttr);
|
|
||||||
if (Number.isNaN(workflowId)) {
|
|
||||||
console.error(
|
|
||||||
"WaitPostProcessWorkflow: data-workflow-id is not a valid number:",
|
|
||||||
workflowIdAttr,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = createApp(App, {
|
|
||||||
workflowId,
|
|
||||||
expectedStep,
|
|
||||||
});
|
|
||||||
|
|
||||||
app.mount(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
|
||||||
document.addEventListener("DOMContentLoaded", mountApp);
|
|
||||||
} else {
|
|
||||||
mountApp();
|
|
||||||
}
|
|
@@ -1,11 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, useTemplateRef } from "vue";
|
import { computed, useTemplateRef } from "vue";
|
||||||
import type { EntityWorkflow, WorkflowAttachment } from "ChillMainAssets/types";
|
import type { WorkflowAttachment } from "ChillMainAssets/types";
|
||||||
import PickGenericDocModal from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue";
|
import PickGenericDocModal from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue";
|
||||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||||
import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
|
import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
|
||||||
import { GenericDoc } from "ChillDocStoreAssets/types";
|
import { GenericDoc } from "ChillDocStoreAssets/types";
|
||||||
import { fetchWorkflow } from "ChillMainAssets/lib/workflow/api";
|
|
||||||
|
|
||||||
interface AppConfig {
|
interface AppConfig {
|
||||||
workflowId: number;
|
workflowId: number;
|
||||||
@@ -35,13 +34,6 @@ const attachedGenericDoc = computed<GenericDocForAccompanyingPeriod[]>(
|
|||||||
) as GenericDocForAccompanyingPeriod[],
|
) as GenericDocForAccompanyingPeriod[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const workflow = ref<EntityWorkflow | null>(null);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
workflow.value = await fetchWorkflow(Number(props.workflowId));
|
|
||||||
console.log("workflow", workflow.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const openModal = function () {
|
const openModal = function () {
|
||||||
pickDocModal.value?.openModal();
|
pickDocModal.value?.openModal();
|
||||||
};
|
};
|
||||||
@@ -57,30 +49,20 @@ const onPickGenericDoc = ({
|
|||||||
const onRemoveAttachment = (payload: { attachment: WorkflowAttachment }) => {
|
const onRemoveAttachment = (payload: { attachment: WorkflowAttachment }) => {
|
||||||
emit("removeAttachment", payload);
|
emit("removeAttachment", payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const canEditAttachement = computed<boolean>(() => {
|
|
||||||
if (null === workflow.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return workflow.value._permissions.CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<pick-generic-doc-modal
|
<pick-generic-doc-modal
|
||||||
:workflow="workflow"
|
|
||||||
:accompanying-period-id="props.accompanyingPeriodId"
|
:accompanying-period-id="props.accompanyingPeriodId"
|
||||||
:to-remove="attachedGenericDoc"
|
:to-remove="attachedGenericDoc"
|
||||||
ref="pickDocModal"
|
ref="pickDocModal"
|
||||||
@pickGenericDoc="onPickGenericDoc"
|
@pickGenericDoc="onPickGenericDoc"
|
||||||
></pick-generic-doc-modal>
|
></pick-generic-doc-modal>
|
||||||
<attachment-list
|
<attachment-list
|
||||||
:workflow="workflow"
|
|
||||||
:attachments="props.attachments"
|
:attachments="props.attachments"
|
||||||
@removeAttachment="onRemoveAttachment"
|
@removeAttachment="onRemoveAttachment"
|
||||||
></attachment-list>
|
></attachment-list>
|
||||||
<ul v-if="canEditAttachement" class="record_actions">
|
<ul class="record_actions">
|
||||||
<li>
|
<li>
|
||||||
<button type="button" class="btn btn-create" @click="openModal">
|
<button type="button" class="btn btn-create" @click="openModal">
|
||||||
Ajouter une pièce jointe
|
Ajouter une pièce jointe
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { EntityWorkflow, WorkflowAttachment } from "ChillMainAssets/types";
|
import { WorkflowAttachment } from "ChillMainAssets/types";
|
||||||
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
|
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
|
||||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||||
|
|
||||||
interface AttachmentListProps {
|
interface AttachmentListProps {
|
||||||
attachments: WorkflowAttachment[];
|
attachments: WorkflowAttachment[];
|
||||||
workflow: EntityWorkflow | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -37,12 +36,7 @@ const props = defineProps<AttachmentListProps>();
|
|||||||
:stored-object="a.genericDoc.storedObject"
|
:stored-object="a.genericDoc.storedObject"
|
||||||
></document-action-buttons-group>
|
></document-action-buttons-group>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li>
|
||||||
v-if="
|
|
||||||
!workflow?._permissions
|
|
||||||
.CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-delete"
|
class="btn btn-delete"
|
||||||
|
@@ -6,10 +6,8 @@ import {
|
|||||||
import PickGenericDocItem from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue";
|
import PickGenericDocItem from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue";
|
||||||
import { fetch_generic_docs_by_accompanying_period } from "ChillDocStoreAssets/js/generic-doc-api";
|
import { fetch_generic_docs_by_accompanying_period } from "ChillDocStoreAssets/js/generic-doc-api";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { EntityWorkflow } from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
interface PickGenericDocProps {
|
interface PickGenericDocProps {
|
||||||
workflow: EntityWorkflow | null;
|
|
||||||
accompanyingPeriodId: number;
|
accompanyingPeriodId: number;
|
||||||
pickedList: GenericDocForAccompanyingPeriod[];
|
pickedList: GenericDocForAccompanyingPeriod[];
|
||||||
toRemove: GenericDocForAccompanyingPeriod[];
|
toRemove: GenericDocForAccompanyingPeriod[];
|
||||||
@@ -38,21 +36,9 @@ const isPicked = (genericDoc: GenericDocForAccompanyingPeriod): boolean =>
|
|||||||
) !== -1;
|
) !== -1;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const fetchedGenericDocs = await fetch_generic_docs_by_accompanying_period(
|
genericDocs.value = await fetch_generic_docs_by_accompanying_period(
|
||||||
props.accompanyingPeriodId,
|
props.accompanyingPeriodId,
|
||||||
);
|
);
|
||||||
const documentClasses = [
|
|
||||||
"Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument",
|
|
||||||
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
|
|
||||||
"Chill\\DocStoreBundle\\Entity\\PersonDocument",
|
|
||||||
];
|
|
||||||
|
|
||||||
genericDocs.value = fetchedGenericDocs.filter(
|
|
||||||
(doc) =>
|
|
||||||
!documentClasses.includes(
|
|
||||||
props.workflow?.relatedEntityClass || "",
|
|
||||||
) || props.workflow?.relatedEntityId !== doc.identifiers.id,
|
|
||||||
);
|
|
||||||
loaded.value = true;
|
loaded.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -3,10 +3,8 @@ import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
|||||||
import { computed, ref, useTemplateRef } from "vue";
|
import { computed, ref, useTemplateRef } from "vue";
|
||||||
import PickGenericDoc from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue";
|
import PickGenericDoc from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue";
|
||||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||||
import { EntityWorkflow } from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
interface PickGenericDocModalProps {
|
interface PickGenericDocModalProps {
|
||||||
workflow: EntityWorkflow | null;
|
|
||||||
accompanyingPeriodId: number;
|
accompanyingPeriodId: number;
|
||||||
toRemove: GenericDocForAccompanyingPeriod[];
|
toRemove: GenericDocForAccompanyingPeriod[];
|
||||||
}
|
}
|
||||||
@@ -82,7 +80,6 @@ defineExpose({ openModal, closeModal });
|
|||||||
</template>
|
</template>
|
||||||
<template v-slot:body>
|
<template v-slot:body>
|
||||||
<pick-generic-doc
|
<pick-generic-doc
|
||||||
:workflow="props.workflow"
|
|
||||||
:accompanying-period-id="props.accompanyingPeriodId"
|
:accompanying-period-id="props.accompanyingPeriodId"
|
||||||
:to-remove="props.toRemove"
|
:to-remove="props.toRemove"
|
||||||
:picked-list="pickeds"
|
:picked-list="pickeds"
|
||||||
|
@@ -4,27 +4,24 @@
|
|||||||
:show-button-details="false"
|
:show-button-details="false"
|
||||||
></address-render-box>
|
></address-render-box>
|
||||||
<address-details-ref-matching
|
<address-details-ref-matching
|
||||||
v-if="isAddress(props.address)"
|
|
||||||
:address="props.address"
|
:address="props.address"
|
||||||
@update-address="onUpdateAddress"
|
@update-address="onUpdateAddress"
|
||||||
></address-details-ref-matching>
|
></address-details-ref-matching>
|
||||||
<address-details-map :address="props.address"></address-details-map>
|
<address-details-map :address="props.address"></address-details-map>
|
||||||
<address-details-geographical-layers
|
<address-details-geographical-layers
|
||||||
v-if="isAddress(props.address)"
|
|
||||||
:address="props.address"
|
:address="props.address"
|
||||||
></address-details-geographical-layers>
|
></address-details-geographical-layers>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {Address, AddressCreation} from "../../../types";
|
import { Address } from "../../../types";
|
||||||
import AddressDetailsMap from "./Parts/AddressDetailsMap.vue";
|
import AddressDetailsMap from "./Parts/AddressDetailsMap.vue";
|
||||||
import AddressRenderBox from "../Entity/AddressRenderBox.vue";
|
import AddressRenderBox from "../Entity/AddressRenderBox.vue";
|
||||||
import AddressDetailsGeographicalLayers from "./Parts/AddressDetailsGeographicalLayers.vue";
|
import AddressDetailsGeographicalLayers from "./Parts/AddressDetailsGeographicalLayers.vue";
|
||||||
import AddressDetailsRefMatching from "./Parts/AddressDetailsRefMatching.vue";
|
import AddressDetailsRefMatching from "./Parts/AddressDetailsRefMatching.vue";
|
||||||
import {isAddress} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
|
|
||||||
|
|
||||||
interface AddressModalContentProps {
|
interface AddressModalContentProps {
|
||||||
address: Address|AddressCreation;
|
address: Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<AddressModalContentProps>();
|
const props = defineProps<AddressModalContentProps>();
|
||||||
|
@@ -12,91 +12,90 @@
|
|||||||
Voir sur
|
Voir sur
|
||||||
<a :href="makeUrlGoogleMap(props.address)" target="_blank"
|
<a :href="makeUrlGoogleMap(props.address)" target="_blank"
|
||||||
>Google Maps</a
|
>Google Maps</a
|
||||||
> <a
|
>
|
||||||
:href="makeUrlOsm(props.address)" target="_blank"
|
<a :href="makeUrlOsm(props.address)" target="_blank">OSM</a>
|
||||||
>OSM</a>
|
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {computed, onMounted, ref} from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import markerIconPng from "leaflet/dist/images/marker-icon.png";
|
import markerIconPng from "leaflet/dist/images/marker-icon.png";
|
||||||
import L, { LatLngExpression, LatLngTuple } from "leaflet";
|
import L, { LatLngExpression, LatLngTuple } from "leaflet";
|
||||||
import {Address, AddressCreation, Point} from "../../../../types";
|
import { Address, Point } from "../../../../types";
|
||||||
import {buildAddressLines, getAddressPoint} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
|
|
||||||
import {useLeafletDisplayLayer, useLeafletMap, useLeafletMarker, useLeafletTileLayer} from "vue-use-leaflet";
|
|
||||||
|
|
||||||
const lonLatForLeaflet = (point: Point): LatLngTuple => {
|
const lonLatForLeaflet = (point: Point): LatLngTuple => {
|
||||||
return [point.coordinates[1], point.coordinates[0]];
|
return [point.coordinates[1], point.coordinates[0]];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MapProps {
|
export interface MapProps {
|
||||||
address: Address|AddressCreation;
|
address: Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<MapProps>();
|
const props = defineProps<MapProps>();
|
||||||
|
|
||||||
const markerIcon = L.icon({
|
const map_div = ref<HTMLDivElement | null>(null);
|
||||||
iconUrl: markerIconPng,
|
let map: L.Map | null = null;
|
||||||
iconAnchor: [12, 41],
|
let marker: L.Marker | null = null;
|
||||||
});
|
|
||||||
const latLngMarker = computed((): LatLngExpression => {
|
onMounted(() => {
|
||||||
if (props.address === null || props.address.point === null) {
|
if (map_div.value === null) {
|
||||||
return [0, 0, 0];
|
// there is no map div when the address does not have any Point
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [props.address.point.coordinates[1], props.address.point.coordinates[0], 0]
|
if (props.address.point !== null) {
|
||||||
});
|
map = L.map(map_div.value);
|
||||||
|
map.setView(lonLatForLeaflet(props.address.point), 18);
|
||||||
|
|
||||||
const map_div = ref<HTMLDivElement | null>(null);
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
const map = useLeafletMap(map_div, {zoom: 18, center: latLngMarker});
|
|
||||||
const tileLayer = useLeafletTileLayer(
|
|
||||||
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
||||||
attribution:
|
attribution:
|
||||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
const markerIcon = L.icon({
|
||||||
|
iconUrl: markerIconPng,
|
||||||
|
iconAnchor: [12, 41],
|
||||||
|
});
|
||||||
|
|
||||||
|
marker = L.marker(lonLatForLeaflet(props.address.point), {
|
||||||
|
icon: markerIcon,
|
||||||
|
});
|
||||||
|
marker.addTo(map);
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
useLeafletDisplayLayer(map, tileLayer);
|
|
||||||
|
|
||||||
|
const makeUrlGoogleMap = (address: Address): string => {
|
||||||
const marker = useLeafletMarker(latLngMarker, {icon: markerIcon});
|
|
||||||
useLeafletDisplayLayer(map, marker);
|
|
||||||
|
|
||||||
|
|
||||||
const makeUrlGoogleMap = (address: Address|AddressCreation): string => {
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("api", "1");
|
params.append("api", "1");
|
||||||
const point = getAddressPoint(address);
|
if (address.point !== null && address.addressReference !== null) {
|
||||||
if (point !== null && address.addressReference !== null) {
|
|
||||||
params.append(
|
params.append(
|
||||||
"query",
|
"query",
|
||||||
`${point.coordinates[1]} ${point.coordinates[0]}`,
|
`${address.point.coordinates[1]} ${address.point.coordinates[0]}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
params.append("query", buildAddressLines(address).join(", "));
|
params.append("query", address.lines.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
return `https://www.google.com/maps/search/?${params.toString()}`;
|
return `https://www.google.com/maps/search/?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeUrlOsm = (address: Address|AddressCreation): string => {
|
const makeUrlOsm = (address: Address): string => {
|
||||||
const point = getAddressPoint(address);
|
if (address.point !== null && address.addressReference !== null) {
|
||||||
if (point !== null && address.addressReference !== null) {
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("mlat", `${point.coordinates[1]}`);
|
params.append("mlat", `${address.point.coordinates[1]}`);
|
||||||
params.append("mlon", `${point.coordinates[0]}`);
|
params.append("mlon", `${address.point.coordinates[0]}`);
|
||||||
const hashParams = new URLSearchParams();
|
const hashParams = new URLSearchParams();
|
||||||
hashParams.append(
|
hashParams.append(
|
||||||
"map",
|
"map",
|
||||||
`18/${point.coordinates[1]}/${point.coordinates[0]}`,
|
`18/${address.point.coordinates[1]}/${address.point.coordinates[0]}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return `https://www.openstreetmap.org/?${params.toString()}#${hashParams.toString()}`;
|
return `https://www.openstreetmap.org/?${params.toString()}#${hashParams.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("query", buildAddressLines(address).join(", "));
|
params.append("query", address.lines.join(", "));
|
||||||
|
|
||||||
return `https://www.openstreetmap.org/search?${params.toString()}`;
|
return `https://www.openstreetmap.org/search?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
@@ -1,46 +0,0 @@
|
|||||||
import {Address, AddressCreation, Point} from "ChillMainAssets/types";
|
|
||||||
import addressFormatter, {Input} from "@fragaria/address-formatter";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given object is of type Address by verifying the existence
|
|
||||||
* of the `lines` property and confirming that it is an array of strings.
|
|
||||||
*
|
|
||||||
* @param {AddressCreation | Address} obj - The object to check.
|
|
||||||
* @return {boolean} Returns true if the object is of type Address, otherwise false.
|
|
||||||
*/
|
|
||||||
export function isAddress(obj: AddressCreation | Address): obj is Address {
|
|
||||||
return (obj as any).lines !== undefined && Array.isArray((obj as any).lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAddressFormatterObject(address: AddressCreation): Input {
|
|
||||||
return {
|
|
||||||
city: address.postcode.name,
|
|
||||||
postcode: address.postcode.code,
|
|
||||||
countryCode: address.postcode.country.code,
|
|
||||||
street: address.street,
|
|
||||||
houseNumber: address.streetNumber,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const buildAddressLines = (address: AddressCreation|Address): string[] => {
|
|
||||||
if (isAddress(address)) {
|
|
||||||
return address.lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = addressFormatter.format(buildAddressFormatterObject(address), {output: 'array', countryCode: address.addressReference.postcode.country.code });
|
|
||||||
console.log('lines:', lines);
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const buildAddressText = (address: AddressCreation|Address): string => {
|
|
||||||
return buildAddressLines(address).join(' - ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getAddressPoint = (address: AddressCreation|Address): Point|null => {
|
|
||||||
if (isAddress(address)) {
|
|
||||||
return address.point;
|
|
||||||
}
|
|
||||||
|
|
||||||
return address.addressReference?.point;
|
|
||||||
}
|
|
@@ -4,14 +4,14 @@
|
|||||||
<div v-if="isConfidential">
|
<div v-if="isConfidential">
|
||||||
<confidential :position-btn-far="true">
|
<confidential :position-btn-far="true">
|
||||||
<template #confidential-content>
|
<template #confidential-content>
|
||||||
<div v-if="isMultiline">
|
<div v-if="isMultiline === true">
|
||||||
<p
|
<p
|
||||||
v-for="(l, i) in buildAddressLines(address)"
|
v-for="(l, i) in address.lines"
|
||||||
:key="`line-${i}`"
|
:key="`line-${i}`"
|
||||||
>
|
>
|
||||||
{{ l }}
|
{{ l }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="showButtonDetails && isAddress(address) ">
|
<p v-if="showButtonDetails">
|
||||||
<address-details-button
|
<address-details-button
|
||||||
:address_id="address.address_id"
|
:address_id="address.address_id"
|
||||||
:address_ref_status="address.refStatus"
|
:address_ref_status="address.refStatus"
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<p v-if="'' !== buildAddressText(address)" class="street">
|
<p v-if="'' !== address.text" class="street">
|
||||||
{{ buildAddressText(address) }}
|
{{ address.text }}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="null !== address.postcode"
|
v-if="null !== address.postcode"
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
{{ address.postcode.code }}
|
{{ address.postcode.code }}
|
||||||
{{ address.postcode.name }}
|
{{ address.postcode.name }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="null !== address.postcode" class="country">
|
<p v-if="null !== address.country" class="country">
|
||||||
{{ localizeString(address.postcode.country.name) }}
|
{{ localizeString(address.country.name) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -38,11 +38,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isConfidential">
|
<div v-if="!isConfidential">
|
||||||
<div v-if="isMultiline">
|
<div v-if="isMultiline === true">
|
||||||
<p v-for="(l, i) in buildAddressLines(address)" :key="`line-${i}`">
|
<p v-for="(l, i) in address.lines" :key="`line-${i}`">
|
||||||
{{ l }}
|
{{ l }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="showButtonDetails && isAddress(address) ">
|
<p v-if="showButtonDetails">
|
||||||
<address-details-button
|
<address-details-button
|
||||||
:address_id="address.address_id"
|
:address_id="address.address_id"
|
||||||
:address_ref_status="address.refStatus"
|
:address_ref_status="address.refStatus"
|
||||||
@@ -50,9 +50,9 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<p v-if="'' !== buildAddressText(address)" class="street">
|
<p v-if="address.text" class="street">
|
||||||
{{ buildAddressText(address)}}
|
{{ address.text }}
|
||||||
<template v-if="showButtonDetails && isAddress(address) ">
|
<template v-if="showButtonDetails">
|
||||||
<address-details-button
|
<address-details-button
|
||||||
:address_id="address.address_id"
|
:address_id="address.address_id"
|
||||||
:address_ref_status="address.refStatus"
|
:address_ref_status="address.refStatus"
|
||||||
@@ -63,49 +63,68 @@
|
|||||||
</div>
|
</div>
|
||||||
</component>
|
</component>
|
||||||
|
|
||||||
<div v-if="useDatePane" class="address-more">
|
<div v-if="useDatePane === true" class="address-more">
|
||||||
<div v-if="address.validFrom">
|
<div v-if="address.validFrom">
|
||||||
<span class="validFrom">
|
<span class="validFrom">
|
||||||
<b>{{ trans(ADDRESS_VALID_FROM) }}</b
|
<b>{{ trans(ADDRESS_VALID_FROM) }}</b
|
||||||
>: {{ address.validFrom?.datetime8601 }}
|
>: {{ $d(address.validFrom.date) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isAddress(address) && address.validTo !== null">
|
<div v-if="address.validTo">
|
||||||
<span class="validTo">
|
<span class="validTo">
|
||||||
<b>{{ trans(ADDRESS_VALID_TO) }}</b
|
<b>{{ trans(ADDRESS_VALID_TO) }}</b
|
||||||
>: {{ address.validTo?.datetime8601 }}
|
>: {{ $d(address.validTo.date) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { computed } from "vue";
|
|
||||||
import Confidential from "ChillMainAssets/vuejs/_components/Confidential.vue";
|
import Confidential from "ChillMainAssets/vuejs/_components/Confidential.vue";
|
||||||
import AddressDetailsButton from "ChillMainAssets/vuejs/_components/AddressDetails/AddressDetailsButton.vue";
|
import AddressDetailsButton from "ChillMainAssets/vuejs/_components/AddressDetails/AddressDetailsButton.vue";
|
||||||
import { trans, ADDRESS_VALID_FROM, ADDRESS_VALID_TO } from "translator";
|
import { trans, ADDRESS_VALID_FROM, ADDRESS_VALID_TO } from "translator";
|
||||||
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||||
import {Address, AddressCreation} from "ChillMainAssets/types";
|
|
||||||
import {isAddress, buildAddressLines, buildAddressText} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
|
|
||||||
|
|
||||||
const props = withDefaults(
|
export default {
|
||||||
defineProps<{
|
name: "AddressRenderBox",
|
||||||
address: Address|AddressCreation;
|
methods: { localizeString },
|
||||||
isMultiline?: boolean;
|
components: {
|
||||||
useDatePane?: boolean;
|
Confidential,
|
||||||
showButtonDetails?: boolean;
|
AddressDetailsButton,
|
||||||
}>(),
|
},
|
||||||
{
|
props: {
|
||||||
isMultiline: true,
|
address: {
|
||||||
useDatePane: false,
|
type: Object,
|
||||||
showButtonDetails: true,
|
},
|
||||||
}
|
isMultiline: {
|
||||||
);
|
default: true,
|
||||||
|
type: Boolean,
|
||||||
const component = computed(() => (props.isMultiline ? "div" : "span"));
|
},
|
||||||
const multiline = computed(() => (props.isMultiline ? "multiline" : ""));
|
useDatePane: {
|
||||||
const isConfidential = computed(() => Boolean(props.address?.confidential));
|
default: false,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
showButtonDetails: {
|
||||||
|
default: true,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return { trans, ADDRESS_VALID_FROM, ADDRESS_VALID_TO };
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
component() {
|
||||||
|
return this.isMultiline === true ? "div" : "span";
|
||||||
|
},
|
||||||
|
multiline() {
|
||||||
|
return this.isMultiline === true ? "multiline" : "";
|
||||||
|
},
|
||||||
|
isConfidential() {
|
||||||
|
return this.address.confidential;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@@ -84,8 +84,6 @@ const emits = defineEmits<{
|
|||||||
}
|
}
|
||||||
.modal-header .close {
|
.modal-header .close {
|
||||||
border-top-right-radius: 0.3rem;
|
border-top-right-radius: 0.3rem;
|
||||||
margin-right: 0;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* The following styles are auto-applied to elements with
|
* The following styles are auto-applied to elements with
|
||||||
|
@@ -1,62 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { WaitingScreenState } from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
state: WaitingScreenState;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="waiting-screen">
|
|
||||||
<div
|
|
||||||
v-if="props.state === 'pending' && !!$slots.pending"
|
|
||||||
class="alert alert-danger text-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<slot name="pending"></slot>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
|
|
||||||
<span class="sr-only">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="props.state === 'stopped' && !!$slots.stopped"
|
|
||||||
class="alert alert-info"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<slot name="stopped"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="props.state === 'failure' && !!$slots.failure"
|
|
||||||
class="alert alert-danger text-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<slot name="failure"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="props.state === 'ready' && !!$slots.ready"
|
|
||||||
class="alert alert-success text-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<slot name="ready"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
#waiting-screen {
|
|
||||||
> .alert {
|
|
||||||
min-height: 350px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -44,7 +44,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock content_view_actions_duplicate_link %}
|
{% endblock content_view_actions_duplicate_link %}
|
||||||
{% block content_view_actions_merge %}{% endblock %}
|
{% block content_view_actions_merge %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_thirdparty_find_duplicate',
|
||||||
|
{ 'thirdparty_id': entity.id }) }}"
|
||||||
|
title="{{ 'Merge'|trans }}"
|
||||||
|
class="btn btn-misc">
|
||||||
|
<i class="bi bi-chevron-contract"></i>
|
||||||
|
{{ 'Merge'|trans }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endblock %}
|
||||||
{% block content_view_actions_edit_link %}
|
{% block content_view_actions_edit_link %}
|
||||||
{% if chill_crud_action_exists(crud_name, 'edit') %}
|
{% if chill_crud_action_exists(crud_name, 'edit') %}
|
||||||
{% if is_granted(chill_crud_config('role', crud_name, 'edit'), entity) %}
|
{% if is_granted(chill_crud_config('role', crud_name, 'edit'), entity) %}
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
{% extends '@ChillMain/layout.html.twig' %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ encore_entry_link_tags('address_picker') }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ encore_entry_script_tags('address_picker') }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div data-address-picker="data-address-picker"></div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@@ -8,36 +8,36 @@
|
|||||||
|
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<h2>{{ 'absence.My absence'|trans }}</h2>
|
<h2>{{ 'absence.My absence'|trans }}</h2>
|
||||||
<div>
|
|
||||||
{% if user.absenceStart is not null %}
|
{% if user.absenceStart is not null %}
|
||||||
<div class="alert alert-success flash_message">{{ 'absence.You are listed as absent, as of {date, date, short}'|trans({
|
<div>
|
||||||
date: user.absenceStart
|
<p>{{ 'absence.You are listed as absent, as of'|trans }} {{ user.absenceStart|format_date('long') }}</p>
|
||||||
}) }}
|
<ul class="record_actions sticky-form-buttons">
|
||||||
{% if user.absenceEnd is not null %}
|
<li>
|
||||||
{{ 'until %date%'|trans({'%date%': user.absenceEnd|format_date('short') }) }}
|
<a href="{{ path('chill_main_user_absence_unset') }}"
|
||||||
{% endif %}
|
class="btn btn-delete">{{ 'absence.Unset absence'|trans }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-warning flash_message">{{ 'absence.No absence listed'|trans }}</div>
|
<div>
|
||||||
{% endif %}
|
<p class="chill-no-data-statement">{{ 'absence.No absence listed'|trans }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ form_start(form) }}
|
{{ form_start(form) }}
|
||||||
{{ form_row(form.absenceStart) }}
|
{{ form_row(form.absenceStart) }}
|
||||||
{{ form_row(form.absenceEnd) }}
|
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
<ul class="record_actions sticky-form-buttons">
|
||||||
<li>
|
|
||||||
<a class="btn btn-delete" title="Modifier" href="{{ path('chill_main_user_absence_unset') }}">{{ 'absence.Unset absence'|trans }}</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<button class="btn btn-save" type="submit">
|
<button class="btn btn-save" type="submit">
|
||||||
{{ 'Save'|trans }}
|
{{ 'Save'|trans }}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false">
|
aria-expanded="false">
|
||||||
<i class="bi bi-lightning-fill"></i>
|
<i class="fa fa-flash"></i>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
{% for menu in menus %}
|
{% for menu in menus %}
|
||||||
|
@@ -64,7 +64,7 @@
|
|||||||
{{ form_widget(flag.immediate_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
|
{{ form_widget(flag.immediate_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{{ form_widget(flag.daily_digest, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
|
{{ form_widget(flag.daily_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@@ -58,14 +58,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% if signatures|length > 0 %}
|
|
||||||
<section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<section class="step my-4">{% include '@ChillMain/Workflow/_attachment.html.twig' %}</section>
|
<section class="step my-4">{% include '@ChillMain/Workflow/_attachment.html.twig' %}</section>
|
||||||
|
|
||||||
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
|
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
|
||||||
{% if entity_workflow.currentStep.sends|length > 0 %}
|
{% if signatures|length > 0 %}
|
||||||
|
<section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
|
||||||
|
{% elseif entity_workflow.currentStep.sends|length > 0 %}
|
||||||
<section class="step my-4">
|
<section class="step my-4">
|
||||||
<h2>{{ 'workflow.external_views.title'|trans({'numberOfSends': entity_workflow.currentStep.sends|length }) }}</h2>
|
<h2>{{ 'workflow.external_views.title'|trans({'numberOfSends': entity_workflow.currentStep.sends|length }) }}</h2>
|
||||||
{% include '@ChillMain/Workflow/_send_views_list.html.twig' with {'sends': entity_workflow.currentStep.sends} %}
|
{% include '@ChillMain/Workflow/_send_views_list.html.twig' with {'sends': entity_workflow.currentStep.sends} %}
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
{% extends '@ChillMain/layout.html.twig' %}
|
|
||||||
|
|
||||||
{% block title %}{{ 'workflow.signature.waiting_for'|trans }}{% endblock %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ encore_entry_link_tags('page_workflow_waiting_post_process') }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ encore_entry_script_tags('page_workflow_waiting_post_process') }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>{{ block('title') }}</h1>
|
|
||||||
|
|
||||||
<div class="screen-wait" data-workflow-id="{{ workflow.id|e('html_attr') }}" data-expected-step="{{ expectedStep|e('html_attr') }}"></div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@@ -79,7 +79,7 @@
|
|||||||
<div class="d-flex flex-row mb-5 alert alert-warning" role="alert">
|
<div class="d-flex flex-row mb-5 alert alert-warning" role="alert">
|
||||||
<p class="m-2">{{'absence.You are marked as being absent'|trans }}</p>
|
<p class="m-2">{{'absence.You are marked as being absent'|trans }}</p>
|
||||||
<span class="ms-auto">
|
<span class="ms-auto">
|
||||||
<a class="btn btn-delete" title="Modifier" href="{{ path('chill_main_user_absence_unset') }}">{{ 'absence.Unset absence'|trans }}</a>
|
<a class="btn btn-remove" title="Modifier" href="{{ path('chill_main_user_absence_index') }}">{{ 'absence.Unset absence'|trans }}</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@@ -1,53 +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\Security\Authorization;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|
||||||
use Symfony\Component\Workflow\Registry;
|
|
||||||
|
|
||||||
final class EntityWorkflowAttachmentVoter extends Voter
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly Registry $registry,
|
|
||||||
) {}
|
|
||||||
public const EDIT = 'CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT';
|
|
||||||
|
|
||||||
protected function supports(string $attribute, $subject): bool
|
|
||||||
{
|
|
||||||
return $subject instanceof EntityWorkflow && self::EDIT === $attribute;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
|
|
||||||
{
|
|
||||||
if (!$subject instanceof EntityWorkflow) {
|
|
||||||
throw new \UnexpectedValueException('Subject must be an instance of EntityWorkflow');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($subject->isFinal()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$workflow = $this->registry->get($subject, $subject->getWorkflowName());
|
|
||||||
|
|
||||||
$marking = $workflow->getMarking($subject);
|
|
||||||
foreach ($marking->getPlaces() as $place => $int) {
|
|
||||||
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
|
||||||
if ($placeMetadata['isSentExternal'] ?? false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,86 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Security;
|
|
||||||
|
|
||||||
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
|
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
||||||
|
|
||||||
final readonly class RoleDumper
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private RoleProvider $roleProvider,
|
|
||||||
private RoleHierarchyInterface $roleHierarchy,
|
|
||||||
private TranslatorInterface $translator,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function dumpAsMarkdown(): string
|
|
||||||
{
|
|
||||||
$roles = $this->roleProvider->getRoles();
|
|
||||||
$rolesWithoutScopes = $this->roleProvider->getRolesWithoutScopes();
|
|
||||||
|
|
||||||
// Group roles by title
|
|
||||||
$groups = [];
|
|
||||||
foreach ($roles as $role) {
|
|
||||||
$title = $this->roleProvider->getRoleTitle($role);
|
|
||||||
$title ??= 'Other';
|
|
||||||
$groups[$title][] = $role;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort groups by title
|
|
||||||
ksort($groups, SORT_NATURAL | SORT_FLAG_CASE);
|
|
||||||
|
|
||||||
$lines = [];
|
|
||||||
foreach ($groups as $title => $roleList) {
|
|
||||||
// Sort roles by translated label for deterministic output
|
|
||||||
usort($roleList, function (string $a, string $b): int {
|
|
||||||
$ta = $this->translator->trans($a);
|
|
||||||
$tb = $this->translator->trans($b);
|
|
||||||
|
|
||||||
return strcasecmp($ta, $tb);
|
|
||||||
});
|
|
||||||
|
|
||||||
$translatedTitle = $this->translator->trans($title);
|
|
||||||
$lines[] = '## '.$translatedTitle;
|
|
||||||
|
|
||||||
foreach ($roleList as $role) {
|
|
||||||
// Translate primary role
|
|
||||||
$translatedRole = $this->translator->trans($role);
|
|
||||||
|
|
||||||
// Scope marker: (S) if needs scope, (~~S~~) if no scope required
|
|
||||||
$needsScope = !in_array($role, $rolesWithoutScopes, true);
|
|
||||||
$scopeMarker = $needsScope ? '(S)' : '(~~S~~)';
|
|
||||||
|
|
||||||
// Compute dependent roles from hierarchy (exclude itself)
|
|
||||||
$reachable = $this->roleHierarchy->getReachableRoleNames([$role]);
|
|
||||||
$dependents = array_values(array_filter($reachable, static fn (string $r): bool => $r !== $role));
|
|
||||||
|
|
||||||
// Translate dependents and sort deterministically
|
|
||||||
$translatedDependents = array_map(fn (string $r) => $this->translator->trans($r), $dependents);
|
|
||||||
sort($translatedDependents, SORT_NATURAL | SORT_FLAG_CASE);
|
|
||||||
|
|
||||||
if (count($translatedDependents) > 0) {
|
|
||||||
$lines[] = sprintf('- **%s** %s: %s', $translatedRole, $scopeMarker, implode(', ', $translatedDependents));
|
|
||||||
} else {
|
|
||||||
$lines[] = sprintf('- **%s** %s', $translatedRole, $scopeMarker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a blank line between groups
|
|
||||||
$lines[] = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim possible trailing blank line
|
|
||||||
$markdown = rtrim(implode("\n", $lines));
|
|
||||||
|
|
||||||
return $markdown."\n"; // End with newline for POSIX friendliness
|
|
||||||
}
|
|
||||||
}
|
|
@@ -52,8 +52,12 @@ class RoleProvider
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the title for each role.
|
* Get the title for each role.
|
||||||
|
*
|
||||||
|
* @param string $role
|
||||||
|
*
|
||||||
|
* @return string the title of the role
|
||||||
*/
|
*/
|
||||||
public function getRoleTitle(string $role): ?string
|
public function getRoleTitle($role)
|
||||||
{
|
{
|
||||||
$this->initializeRolesTitlesCache();
|
$this->initializeRolesTitlesCache();
|
||||||
|
|
||||||
@@ -69,7 +73,7 @@ class RoleProvider
|
|||||||
/**
|
/**
|
||||||
* initialize the array for caching role and titles.
|
* initialize the array for caching role and titles.
|
||||||
*/
|
*/
|
||||||
private function initializeRolesTitlesCache(): void
|
private function initializeRolesTitlesCache()
|
||||||
{
|
{
|
||||||
// break if already initialized
|
// break if already initialized
|
||||||
if (null !== $this->rolesTitlesCache) {
|
if (null !== $this->rolesTitlesCache) {
|
||||||
|
@@ -66,7 +66,6 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
|
|||||||
'name' => $address->getPostCode()->getName(),
|
'name' => $address->getPostCode()->getName(),
|
||||||
'code' => $address->getPostCode()->getCode(),
|
'code' => $address->getPostCode()->getCode(),
|
||||||
'center' => $address->getPostcode()->getCenter(),
|
'center' => $address->getPostcode()->getCenter(),
|
||||||
'country' => $this->normalizer->normalize($address->getPostCode()->getCountry(), $format, [AbstractNormalizer::GROUPS => ['read']]),
|
|
||||||
],
|
],
|
||||||
'country' => [
|
'country' => [
|
||||||
'id' => $address->getPostCode()->getCountry()->getId(),
|
'id' => $address->getPostCode()->getCountry()->getId(),
|
||||||
|
@@ -12,25 +12,18 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Serializer\Normalizer;
|
namespace Chill\MainBundle\Serializer\Normalizer;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowAttachmentVoter;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Symfony\Component\Workflow\Registry;
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
|
||||||
final class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||||
{
|
{
|
||||||
use NormalizerAwareTrait;
|
use NormalizerAwareTrait;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly MetadataExtractor $metadataExtractor, private readonly Registry $registry) {}
|
||||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
|
||||||
private readonly MetadataExtractor $metadataExtractor,
|
|
||||||
private readonly Registry $registry,
|
|
||||||
private readonly Security $security,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param EntityWorkflow $object
|
* @param EntityWorkflow $object
|
||||||
@@ -53,9 +46,6 @@ final class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerA
|
|||||||
'datas' => $this->normalizer->normalize($handler->getEntityData($object), $format, $context),
|
'datas' => $this->normalizer->normalize($handler->getEntityData($object), $format, $context),
|
||||||
'title' => $handler->getEntityTitle($object),
|
'title' => $handler->getEntityTitle($object),
|
||||||
'isOnHoldAtCurrentStep' => $object->isOnHoldAtCurrentStep(),
|
'isOnHoldAtCurrentStep' => $object->isOnHoldAtCurrentStep(),
|
||||||
'_permissions' => [
|
|
||||||
EntityWorkflowAttachmentVoter::EDIT => $this->security->isGranted(EntityWorkflowAttachmentVoter::EDIT, $object),
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -39,8 +39,6 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
|||||||
'label' => '',
|
'label' => '',
|
||||||
'email' => '',
|
'email' => '',
|
||||||
'isAbsent' => false,
|
'isAbsent' => false,
|
||||||
'absenceStart' => null,
|
|
||||||
'absenceEnd' => null,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock) {}
|
public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock) {}
|
||||||
@@ -79,11 +77,6 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
|||||||
['docgen:expects' => PhoneNumber::class, 'groups' => 'docgen:read']
|
['docgen:expects' => PhoneNumber::class, 'groups' => 'docgen:read']
|
||||||
);
|
);
|
||||||
|
|
||||||
$absenceDatesContext = array_merge(
|
|
||||||
$context,
|
|
||||||
['docgen:expects' => \DateTimeImmutable::class, 'groups' => 'docgen:read']
|
|
||||||
);
|
|
||||||
|
|
||||||
if (null === $object && 'docgen' === $format) {
|
if (null === $object && 'docgen' === $format) {
|
||||||
return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)];
|
return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)];
|
||||||
}
|
}
|
||||||
@@ -106,8 +99,6 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
|||||||
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
|
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
|
||||||
'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext),
|
'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext),
|
||||||
'isAbsent' => $object->isAbsent(),
|
'isAbsent' => $object->isAbsent(),
|
||||||
'absenceStart' => $this->normalizer->normalize($object->getAbsenceStart(), $format, $absenceDatesContext),
|
|
||||||
'absenceEnd' => $this->normalizer->normalize($object->getAbsenceEnd(), $format, $absenceDatesContext),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if ('docgen' === $format) {
|
if ('docgen' === $format) {
|
||||||
|
@@ -1,85 +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 Action\User\UpdateProfile;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
|
|
||||||
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use libphonenumber\PhoneNumber;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
final class UpdateProfileCommandHandlerTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testUpdateProfileWithNullPhoneAndFlags(): void
|
|
||||||
{
|
|
||||||
$user = new User();
|
|
||||||
|
|
||||||
// Pre-set some flags to opposite values to check they are updated
|
|
||||||
$flag = 'tickets';
|
|
||||||
$user->setNotificationImmediately($flag, true);
|
|
||||||
$user->setNotificationDailyDigest($flag, true);
|
|
||||||
|
|
||||||
$command = new UpdateProfileCommand(null);
|
|
||||||
$command->notificationFlags = [
|
|
||||||
$flag => [
|
|
||||||
'immediate_email' => false,
|
|
||||||
'daily_digest' => false,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
(new UpdateProfileCommandHandler())->updateProfile($user, $command);
|
|
||||||
|
|
||||||
self::assertNull($user->getPhonenumber(), 'Phone should be set to null');
|
|
||||||
self::assertFalse($user->isNotificationSendImmediately($flag));
|
|
||||||
self::assertFalse($user->isNotificationDailyDigest($flag));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testUpdateProfileWithPhoneAndMultipleFlags(): void
|
|
||||||
{
|
|
||||||
$user = new User();
|
|
||||||
|
|
||||||
$phone = new PhoneNumber();
|
|
||||||
$phone->setCountryCode(33); // France
|
|
||||||
$phone->setNationalNumber(612345678);
|
|
||||||
|
|
||||||
$command = new UpdateProfileCommand($phone);
|
|
||||||
$command->notificationFlags = [
|
|
||||||
'reports' => [
|
|
||||||
'immediate_email' => true,
|
|
||||||
'daily_digest' => false,
|
|
||||||
],
|
|
||||||
'activities' => [
|
|
||||||
'immediate_email' => false,
|
|
||||||
'daily_digest' => true,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
(new UpdateProfileCommandHandler())->updateProfile($user, $command);
|
|
||||||
|
|
||||||
// Phone assigned
|
|
||||||
self::assertInstanceOf(PhoneNumber::class, $user->getPhonenumber());
|
|
||||||
self::assertSame(33, $user->getPhonenumber()->getCountryCode());
|
|
||||||
self::assertSame('612345678', (string) $user->getPhonenumber()->getNationalNumber());
|
|
||||||
|
|
||||||
// Flags applied
|
|
||||||
self::assertTrue($user->isNotificationSendImmediately('reports'));
|
|
||||||
self::assertFalse($user->isNotificationDailyDigest('reports'));
|
|
||||||
|
|
||||||
self::assertFalse($user->isNotificationSendImmediately('activities'));
|
|
||||||
self::assertTrue($user->isNotificationDailyDigest('activities'));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,103 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Action\User\UpdateProfile;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
|
|
||||||
use Chill\MainBundle\Notification\NotificationFlagManager;
|
|
||||||
use libphonenumber\PhoneNumber;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Symfony\Component\Translation\TranslatableMessage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
final class UpdateProfileCommandTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testCreateTransfersPhonenumberAndNotificationFlags(): void
|
|
||||||
{
|
|
||||||
$user = new User();
|
|
||||||
|
|
||||||
// set a phone number
|
|
||||||
$phone = new PhoneNumber();
|
|
||||||
$phone->setCountryCode(32); // Belgium
|
|
||||||
$phone->setNationalNumber(471234567);
|
|
||||||
$user->setPhonenumber($phone);
|
|
||||||
|
|
||||||
// configure notification flags on the user via helpers
|
|
||||||
$flagA = 'foo';
|
|
||||||
$flagB = 'bar';
|
|
||||||
|
|
||||||
// For tickets: immediate true, daily false
|
|
||||||
$user->setNotificationImmediately($flagA, true);
|
|
||||||
$user->setNotificationDailyDigest($flagA, false);
|
|
||||||
|
|
||||||
// For reports: immediate false, daily true
|
|
||||||
$user->setNotificationImmediately($flagB, false);
|
|
||||||
$user->setNotificationDailyDigest($flagB, true);
|
|
||||||
|
|
||||||
// a third flag not explicitly set to validate default behavior from User
|
|
||||||
$flagC = 'foobar'; // by default immediate-email is true, daily-digest is false per User::getNotificationFlagData
|
|
||||||
|
|
||||||
$manager = $this->createNotificationFlagManager([$flagA, $flagB, $flagC]);
|
|
||||||
|
|
||||||
$command = UpdateProfileCommand::create($user, $manager);
|
|
||||||
|
|
||||||
// phone number transferred
|
|
||||||
self::assertInstanceOf(PhoneNumber::class, $command->phonenumber);
|
|
||||||
self::assertSame($phone->getCountryCode(), $command->phonenumber->getCountryCode());
|
|
||||||
self::assertSame($phone->getNationalNumber(), $command->phonenumber->getNationalNumber());
|
|
||||||
|
|
||||||
// flags transferred consistently
|
|
||||||
self::assertArrayHasKey($flagA, $command->notificationFlags);
|
|
||||||
self::assertArrayHasKey($flagB, $command->notificationFlags);
|
|
||||||
self::assertArrayHasKey($flagC, $command->notificationFlags);
|
|
||||||
|
|
||||||
self::assertSame([
|
|
||||||
'immediate_email' => true,
|
|
||||||
'daily_digest' => false,
|
|
||||||
], $command->notificationFlags[$flagA]);
|
|
||||||
|
|
||||||
self::assertSame([
|
|
||||||
'immediate_email' => false,
|
|
||||||
'daily_digest' => true,
|
|
||||||
], $command->notificationFlags[$flagB]);
|
|
||||||
|
|
||||||
// default from User::getNotificationFlagData -> immediate true, daily false
|
|
||||||
self::assertSame([
|
|
||||||
'immediate_email' => true,
|
|
||||||
'daily_digest' => false,
|
|
||||||
], $command->notificationFlags[$flagC]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createNotificationFlagManager(array $flags): NotificationFlagManager
|
|
||||||
{
|
|
||||||
$providers = array_map(fn (string $flag) => new class ($flag) implements NotificationFlagProviderInterface {
|
|
||||||
public function __construct(private readonly string $flag) {}
|
|
||||||
|
|
||||||
public function getFlag(): string
|
|
||||||
{
|
|
||||||
return $this->flag;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLabel(): TranslatableMessage
|
|
||||||
{
|
|
||||||
return new TranslatableMessage($this->flag);
|
|
||||||
}
|
|
||||||
}, $flags);
|
|
||||||
|
|
||||||
return new NotificationFlagManager($providers);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,118 +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\Tests\Controller;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Controller\AddressReferenceAggregatedApiController;
|
|
||||||
use Chill\MainBundle\Repository\AddressReferenceRepositoryInterface;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @covers \Chill\MainBundle\Controller\AddressReferenceAggregatedApiController
|
|
||||||
*/
|
|
||||||
final class AddressReferenceAggregatedApiControllerTest extends TestCase
|
|
||||||
{
|
|
||||||
use ProphecyTrait;
|
|
||||||
|
|
||||||
public function testAccessDeniedWhenNotAuthenticated(): void
|
|
||||||
{
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted('IS_AUTHENTICATED')->willReturn(false);
|
|
||||||
|
|
||||||
$repo = $this->prophesize(AddressReferenceRepositoryInterface::class);
|
|
||||||
|
|
||||||
$controller = new AddressReferenceAggregatedApiController($security->reveal(), $repo->reveal());
|
|
||||||
|
|
||||||
$request = new Request(query: ['q' => 'anything']);
|
|
||||||
|
|
||||||
$this->expectException(AccessDeniedHttpException::class);
|
|
||||||
|
|
||||||
$controller->search($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testBadRequestWhenQueryIsMissing(): void
|
|
||||||
{
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted('IS_AUTHENTICATED')->willReturn(true);
|
|
||||||
|
|
||||||
$repo = $this->prophesize(AddressReferenceRepositoryInterface::class);
|
|
||||||
|
|
||||||
$controller = new AddressReferenceAggregatedApiController($security->reveal(), $repo->reveal());
|
|
||||||
|
|
||||||
$request = new Request();
|
|
||||||
|
|
||||||
$this->expectException(BadRequestHttpException::class);
|
|
||||||
$this->expectExceptionMessage('Parameter "q" is required.');
|
|
||||||
|
|
||||||
$controller->search($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testBadRequestWhenQueryIsEmpty(): void
|
|
||||||
{
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted('IS_AUTHENTICATED')->willReturn(true);
|
|
||||||
|
|
||||||
$repo = $this->prophesize(AddressReferenceRepositoryInterface::class);
|
|
||||||
|
|
||||||
$controller = new AddressReferenceAggregatedApiController($security->reveal(), $repo->reveal());
|
|
||||||
|
|
||||||
$request = new Request(query: ['q' => ' ']);
|
|
||||||
|
|
||||||
$this->expectException(BadRequestHttpException::class);
|
|
||||||
$this->expectExceptionMessage('Parameter "q" is required and cannot be empty.');
|
|
||||||
|
|
||||||
$controller->search($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSuccessfulSearchReturnsJsonAndCallsRepositoryWithTrimmedQuery(): void
|
|
||||||
{
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted('IS_AUTHENTICATED')->willReturn(true);
|
|
||||||
|
|
||||||
$expectedQuery = 'foo';
|
|
||||||
$iterableResult = new \ArrayIterator([
|
|
||||||
[
|
|
||||||
'street' => 'Main Street',
|
|
||||||
'postcode_id' => 123,
|
|
||||||
'code' => '1000',
|
|
||||||
'label' => 'Brussels',
|
|
||||||
'positions' => ['1' => '12', '2' => '14'],
|
|
||||||
'row_number' => 1,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$repo = $this->prophesize(AddressReferenceRepositoryInterface::class);
|
|
||||||
$repo->findAggregatedBySearchString($expectedQuery)->willReturn($iterableResult)->shouldBeCalledOnce();
|
|
||||||
|
|
||||||
$controller = new AddressReferenceAggregatedApiController($security->reveal(), $repo->reveal());
|
|
||||||
|
|
||||||
// Provide spaces around to ensure trimming is applied
|
|
||||||
$request = new Request(query: ['q' => " {$expectedQuery} "]);
|
|
||||||
|
|
||||||
$response = $controller->search($request);
|
|
||||||
|
|
||||||
self::assertSame(200, $response->getStatusCode());
|
|
||||||
$data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
|
||||||
self::assertIsArray($data);
|
|
||||||
self::assertCount(1, $data);
|
|
||||||
self::assertSame('Main Street', $data[0]['street']);
|
|
||||||
self::assertSame(123, $data[0]['postcode_id']);
|
|
||||||
self::assertSame('1000', $data[0]['code']);
|
|
||||||
self::assertSame('Brussels', $data[0]['label']);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -45,7 +45,7 @@ final class UserControllerTest extends WebTestCase
|
|||||||
self::assertResponseIsSuccessful();
|
self::assertResponseIsSuccessful();
|
||||||
|
|
||||||
$username = 'Test_user'.uniqid();
|
$username = 'Test_user'.uniqid();
|
||||||
$password = 'Password_1234!';
|
$password = 'Password1234!';
|
||||||
|
|
||||||
// Fill in the form and submit it
|
// Fill in the form and submit it
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ final class UserControllerTest extends WebTestCase
|
|||||||
{
|
{
|
||||||
$client = $this->getClientAuthenticatedAsAdmin();
|
$client = $this->getClientAuthenticatedAsAdmin();
|
||||||
$crawler = $client->request('GET', "/fr/admin/user/{$userId}/edit_password");
|
$crawler = $client->request('GET', "/fr/admin/user/{$userId}/edit_password");
|
||||||
$newPassword = '1234_Password!';
|
$newPassword = '1234Password!';
|
||||||
|
|
||||||
$form = $crawler->selectButton('Changer le mot de passe')->form([
|
$form = $crawler->selectButton('Changer le mot de passe')->form([
|
||||||
'chill_mainbundle_user_password[new_password][first]' => $newPassword,
|
'chill_mainbundle_user_password[new_password][first]' => $newPassword,
|
||||||
|
@@ -96,13 +96,11 @@ final class NotificationTest extends KernelTestCase
|
|||||||
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when no notification flags are set, by default immediate email');
|
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when no notification flags are set, by default immediate email');
|
||||||
|
|
||||||
// immediate-email preference
|
// immediate-email preference
|
||||||
$user->setNotificationImmediately('test_notification_type', true);
|
$user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_IMMEDIATE_EMAIL, User::NOTIF_FLAG_DAILY_DIGEST]]);
|
||||||
$user->setNotificationDailyDigest('test_notification_type', true);
|
|
||||||
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when preferences contain immediate-email');
|
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when preferences contain immediate-email');
|
||||||
|
|
||||||
// daily-email preference
|
// daily-email preference
|
||||||
$user->setNotificationDailyDigest('test_notification_type', true);
|
$user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_DAILY_DIGEST]]);
|
||||||
$user->setNotificationImmediately('test_notification_type', false);
|
|
||||||
$this->assertFalse($user->isNotificationSendImmediately($notification->getType()), 'Should return false when preference is daily-email only');
|
$this->assertFalse($user->isNotificationSendImmediately($notification->getType()), 'Should return false when preference is daily-email only');
|
||||||
$this->assertTrue($user->isNotificationDailyDigest($notification->getType()), 'Should return true when preference is daily-email');
|
$this->assertTrue($user->isNotificationDailyDigest($notification->getType()), 'Should return true when preference is daily-email');
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user