Compare commits

..

45 Commits

Author SHA1 Message Date
eb4a33ff75 Remove limit_lines variable for comments in activity list item. Entire comment to be displayed. 2025-11-25 15:01:41 +01:00
0d32810d0d Change position and color of confirm parcours button 2025-11-24 15:13:16 +01:00
b221ad1621 Merge branch '466-set-main-user-activity' into 'master'
Associate activity's creator as a participant by default, and retro-actively append the creator to each activity

Closes #466

See merge request Chill-Projet/chill-bundles!924
2025-11-24 09:23:12 +00:00
a96e9d5377 Associate activity's creator as a participant by default, and retro-actively append the creator to each activity 2025-11-24 09:23:12 +00:00
54b73128c3 Merge branch '470-alphabetical-order-admin' into 'master'
Alphabetically order userJobs and mainLocations within user creation form

Closes #470

See merge request Chill-Projet/chill-bundles!926
2025-11-24 09:18:03 +00:00
5c0cb01fdc Alphabetically order userJobs and mainLocations within user creation form 2025-11-24 09:18:03 +00:00
26d9b55c6d Update to v4.8.1 2025-11-20 16:19:52 +01:00
add9249502 Merge branch '471-fix-inactive-user-group-api' into 'master'
Hide inactive user groups in API responses

Closes #471

See merge request Chill-Projet/chill-bundles!927
2025-11-19 15:19:25 +00:00
380d48c43a Hide inactive user groups in API responses 2025-11-19 15:19:25 +00:00
c7d7c3ac6f Add missing 'id' paramater in path 2025-11-19 13:48:35 +01:00
7eb895c0e1 Insert name of file as the document title when uploading 2025-11-19 13:33:51 +01:00
e1b91ebbfd Release v4.8.0 2025-11-17 15:11:14 +01:00
2139b53fb0 Merge branch '449-scope-picker-form-label' into 'master'
Remove the label if there is only one scope and no scope picking field is displayed.

Closes #449

See merge request Chill-Projet/chill-bundles!911
2025-11-17 10:48:16 +00:00
a43181d60d Remove the label if there is only one scope and no scope picking field is displayed. 2025-11-17 10:48:15 +00:00
04bc1c5de8 Merge branch '463-update-calendar-with-accepted-invites' into 'master'
Update calendar with accepted invites

Closes #463

See merge request Chill-Projet/chill-bundles!921
2025-11-14 14:22:59 +00:00
0a07d68b6d Merge branch '461-calendar-items-clickable' into 'master'
Resolve "Rendre le rendez-vous clicable dans la page "mes rendez-vous""

Closes #461

See merge request Chill-Projet/chill-bundles!919
2025-11-14 14:08:04 +00:00
fccd29e3c7 Resolve "Rendre le rendez-vous clicable dans la page "mes rendez-vous"" 2025-11-14 14:08:03 +00:00
274ee94196 Merge branch '420-localisation-variable' into 'master'
Ajouter une variable de localisation aux utilisateurs

Closes #420

See merge request Chill-Projet/chill-bundles!904
2025-11-14 13:52:33 +00:00
799d04142e Ajouter une variable de localisation aux utilisateurs 2025-11-14 13:52:33 +00:00
dfe8d8b0bf Merge branch 'accessibility/improve-login-page' into 'master'
Improve accessibility on the login page

See merge request Chill-Projet/chill-bundles!922
2025-11-14 10:16:08 +00:00
82f347b93a Improve accessibility on the login page 2025-11-14 10:16:08 +00:00
635efd6f1d Update calendar with accepted invites 2025-11-12 17:01:09 +01:00
869880d8f3 Revert "Display calendar items linked to person within search results"
This reverts commit f7ea7e4dbf.
2025-11-12 13:08:54 +01:00
f7ea7e4dbf Display calendar items linked to person within search results 2025-11-12 13:00:52 +01:00
0a58e05230 Update chill bundles to v4.7.0 2025-11-10 16:47:38 +01:00
68c83223dd Merge branch '455-results-objectives-display-order' into 'master'
Resolve "Action d'accompagnement - afficher les objectifs avant les résultats"

Closes #455

See merge request Chill-Projet/chill-bundles!913
2025-11-07 16:23:53 +00:00
c28bd22560 Resolve "Action d'accompagnement - afficher les objectifs avant les résultats" 2025-11-07 16:23:52 +00:00
a5ef2475fb Merge branch 'text-wrapping-badges' into 'master'
Wrap text when it is too long within badges

See merge request Chill-Projet/chill-bundles!918
2025-11-07 14:48:48 +00:00
86dd9bfb80 Wrap text when it is too long within badges 2025-11-07 15:18:02 +01:00
c28670f0fd Merge branch '457-merge-thirdparty-bug' into 'master'
Fix the fusion of thirdparty properties that are located in another schema...

Closes #457

See merge request Chill-Projet/chill-bundles!916
2025-11-07 10:50:03 +00:00
9e2c030224 Fix the fusion of thirdparty properties that are located in another schema... 2025-11-07 10:50:03 +00:00
a706c6f337 fix: set back to true suggestion of referrer when creating notification for
accompanyingPeriodWorkDocument
2025-11-06 16:18:33 +01:00
bc63b489ee Merge branch '285-cancel-calendar' into 'master'
Permettre d'annuler un rendez-vous

Closes #285

See merge request Chill-Projet/chill-bundles!775
2025-11-06 15:07:11 +00:00
a4cfc6a178 Permettre d'annuler un rendez-vous 2025-11-06 15:07:11 +00:00
f75d1da3b1 Merge branch '385-invitation-list' into 'master'
Add user invitation list page

Closes #385

See merge request Chill-Projet/chill-bundles!866
2025-11-06 12:06:15 +00:00
b8b68e5e5a Rename page title key for invitations list to align with translation standards
- Replaced hardcoded title 'My invitations list' with 'invite.list.title' translation key.
2025-11-06 13:00:38 +01:00
ae5ba67064 Update UserMenuBuilder to adjust menu labels and sort order
- Renamed 'My invitations list' to 'invite.list.title'.
- Updated the sort order for 'My calendar' from 9 to 8, to place "invitation list" just after the calendar list
2025-11-06 13:00:28 +01:00
bfe4dd3aec Merge branch 'master' into 385-invitation-list 2025-11-06 12:14:21 +01:00
74c9eb5585 Rector corrections 2025-09-30 16:23:27 +02:00
f93c7e014f Add test for MyInvitationsController.php 2025-09-30 15:45:26 +02:00
e6a799abc4 Add translation for invitation list page title 2025-09-30 15:30:38 +02:00
68a0ef7115 Reorganize templates to allow re-use of _list.html.twig within listByUser.html.twig template 2025-09-30 15:30:20 +02:00
1675c56f3d Fix order of paginator parameters passed to findBy method 2025-09-30 15:29:41 +02:00
675e8450fc WIP: switch from ACLAware to normal repository usage 2025-09-30 14:34:47 +02:00
4ffd7034d0 feat: add invitation list
- Introduced `MyInvitationsController` for managing user invitations
- Added `InviteACLAwareRepository` and its interface for handling invite data operations
- Created views for listing and displaying user-specific invitations
- Updated user menu to include "My invitations list" option
2025-09-30 14:34:47 +02:00
610 changed files with 24025 additions and 47438 deletions

View File

@@ -1,7 +0,0 @@
kind: DX
body: |
Send notifications log to dedicated channel, if it exists
time: 2025-10-27T15:00:53.309372316+01:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Feature
body: |
Upgrade import of address list to the last version of compiled addresses of belgian-best-address
time: 2024-05-30T16:00:03.440767606+02:00
custom:
Issue: ""

View File

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

View File

@@ -1,6 +0,0 @@
kind: Feature
body: Create invitation list in user menu
time: 2025-08-08T12:08:02.446361367+02:00
custom:
Issue: "385"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Feature
body: Admin interface for Motive entity
time: 2025-10-07T15:59:45.597029709+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Feature
body: Add an admin interface for Motive entity
time: 2025-10-22T11:15:52.13937955+02:00
custom:
Issue: ""
SchemaChange: Add columns or tables

View File

@@ -1,6 +0,0 @@
kind: Feature
body: Add columns for comments linked to an activity in the activity list export
time: 2025-10-29T15:25:10.493968528+01:00
custom:
Issue: "404"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: 'Fix: display also social actions linked to parents of the selected social issue'
time: 2025-10-29T12:43:55.008647232+01:00
custom:
Issue: "451"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: 'Fix: export actions and their results in csv even when action does not have any goals attached to it.'
time: 2025-10-29T14:38:36.195220844+01:00
custom:
Issue: "453"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: Fix the possibility to delete a workflow
time: 2025-11-04T13:51:08.113234488+01:00
custom:
Issue: ""
SchemaChange: Drop or rename table or columns, or enforce new constraint that must be manually fixed

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: Fix suggestion of referrer when creating notification for accompanyingPeriodWorkDocument
time: 2025-11-06T16:16:05.861813041+01:00
custom:
Issue: "428"
SchemaChange: No schema change

View File

@@ -0,0 +1,7 @@
kind: Fixed
body: |
Associate activity's creator as a participant by default, and retro-actively append the creator to each activity
time: 2025-11-18T14:05:59.904993123+01:00
custom:
Issue: "466"
SchemaChange: Add columns or tables

View File

@@ -1,6 +0,0 @@
kind: UX
body: Change the terms 'cercle' and 'centre' to 'service', and 'territoire' respectively.
time: 2025-10-06T12:39:32.514056818+02:00
custom:
Issue: "425"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: UX
body: Improve the ux for selecting whether user wants to be notified of the final step of a workflow or all steps
time: 2025-10-29T11:08:04.077020411+01:00
custom:
Issue: "542"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: UX
body: Expand timeSpent choices for evaluation document and translate them to user locale or fallback 'fr'
time: 2025-10-30T18:09:19.373907522+01:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: UX
body: Alphabetically order userJobs and mainLocations within user creation form
time: 2025-11-19T15:37:06.393470745+01:00
custom:
Issue: "470"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: UX
body: Change position and color of confirm parcours button
time: 2025-11-24T15:11:15.960279853+01:00
custom:
Issue: "437"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: UX
body: Display entire comment for activity item within list
time: 2025-11-25T15:01:35.558013876+01:00
custom:
Issue: "424"
SchemaChange: No schema change

21
.changes/v4.7.0.md Normal file
View File

@@ -0,0 +1,21 @@
## v4.7.0 - 2025-11-10
### Feature
* ([#385](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/385)) Create invitation list in user menu
* ([#404](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/404)) Add columns for comments linked to an activity in the activity list export
### Fixed
* ([#451](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/451)) Fix: display also social actions linked to parents of the selected social issue
* ([#453](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/453)) Fix: export actions and their results in csv even when action does not have any goals attached to it.
* Fix the possibility to delete a workflow
**Schema Change**: Drop or rename table or columns, or enforce new constraint that must be manually fixed
* ([#457](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/457)) Fix the fusion of thirdparty properties that are located in another schema than public for TO_ONE relations + add extra loop for MANY_TO_MANY relations where thirdparty is the source instead of the target
* ([#428](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/428)) Fix suggestion of referrer when creating notification for accompanyingPeriodWorkDocument
### DX
* Send notifications log to dedicated channel, if it exists
### UX
* ([#425](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/425)) Change the terms 'cercle' and 'centre' to 'service', and 'territoire' respectively.
* ([#542](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/542)) Improve the ux for selecting whether user wants to be notified of the final step of a workflow or all steps
* Expand timeSpent choices for evaluation document and translate them to user locale or fallback 'fr'
* ([#455](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/455)) Change the order of display for results and objectives in the social work/action form
* Wrap text when it is too long within badges

9
.changes/v4.8.0.md Normal file
View File

@@ -0,0 +1,9 @@
## v4.8.0 - 2025-11-17
### Feature
* ([#461](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/461)) Make a calendar item on the 'mes rendez-vous' page clickable. Clicking will navigate to the edit page of the calendar item.
### Fixed
* ([#463](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/463)) Display calendar items for which an invite was accepted on the mes rendez-vous page
* Improve accessibility on login page
### UX
* ([#449](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/449)) Remove the label if there is only one scope and no scope picking field is displayed.

6
.changes/v4.8.1.md Normal file
View File

@@ -0,0 +1,6 @@
## v4.8.1 - 2025-11-20
### Fixed
* Insert name of file as the document title when uploading
* Add missing path paramater 'id' for editing multiple participations
* ([#471](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/471)) Hide the display of inactive user groups in the api

View File

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

View File

@@ -234,17 +234,17 @@ This must be a decision made by a human, not by an AI. Every AI task must abort
#### Running Tests #### Running Tests
The tests are run from the project's root (not from the bundle's root: so, do not change the directory to any bundle directory before running tests). The tests are run from the project's root (not from the bundle's root).
Tests must be run using the `symfony` command:
```bash ```bash
# Run all tests
vendor/bin/phpunit
# Run a specific test file # Run a specific test file
symfony composer exec phpunit -- path/to/TestFile.php vendor/bin/phpunit path/to/TestFile.php
# Run a specific test method # Run a specific test method
symfony composer exec phpunit -- --filter methodName path/to/TestFile.php vendor/bin/phpunit --filter methodName path/to/TestFile.php
``` ```
When writing tests, only test specific files. Do not run all tests or the full When writing tests, only test specific files. Do not run all tests or the full

View File

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

30
.vscode/launch.json vendored
View File

@@ -1,30 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Chill Debug",
"type": "php",
"request": "launch",
"port": 9000,
"pathMappings": {
"/var/www/html": "${workspaceFolder}"
},
"preLaunchTask": "symfony"
},
{
"name": "Yarn Encore Dev (Watch)",
"type": "node-terminal",
"request": "launch",
"command": "yarn encore dev --watch",
"cwd": "${workspaceFolder}"
}
],
"compounds": [
{
"name": "Chill Debug + Yarn Encore Dev (Watch)",
"configurations": ["Chill Debug", "Yarn Encore Dev (Watch)"]
}
]
}

23
.vscode/tasks.json vendored
View File

@@ -1,23 +0,0 @@
{
"tasks": [
{
"type": "shell",
"command": "symfony",
"args": [
"server:start",
"--allow-http",
"--no-tls",
"--port=8000",
"--allow-all-ip",
"-d"
],
"label": "symfony"
},
{
"type": "shell",
"command": "yarn",
"args": ["encore", "dev", "--watch"],
"label": "webpack"
}
]
}

View File

@@ -6,6 +6,45 @@ 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.8.1 - 2025-11-20
### Fixed
* Insert name of file as the document title when uploading
* Add missing path paramater 'id' for editing multiple participations
* ([#471](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/471)) Hide the display of inactive user groups in the api
## v4.8.0 - 2025-11-17
### Feature
* ([#461](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/461)) Make a calendar item on the 'mes rendez-vous' page clickable. Clicking will navigate to the edit page of the calendar item.
### Fixed
* ([#463](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/463)) Display calendar items for which an invite was accepted on the mes rendez-vous page
* Improve accessibility on login page
### UX
* ([#449](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/449)) Remove the label if there is only one scope and no scope picking field is displayed.
## v4.7.0 - 2025-11-10
### Feature
* ([#385](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/385)) Create invitation list in user menu
* ([#404](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/404)) Add columns for comments linked to an activity in the activity list export
### Fixed
* ([#451](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/451)) Fix: display also social actions linked to parents of the selected social issue
* ([#453](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/453)) Fix: export actions and their results in csv even when action does not have any goals attached to it.
* Fix the possibility to delete a workflow
**Schema Change**: Drop or rename table or columns, or enforce new constraint that must be manually fixed
* ([#457](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/457)) Fix the fusion of thirdparty properties that are located in another schema than public for TO_ONE relations + add extra loop for MANY_TO_MANY relations where thirdparty is the source instead of the target
* ([#428](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/428)) Fix suggestion of referrer when creating notification for accompanyingPeriodWorkDocument
### DX
* Send notifications log to dedicated channel, if it exists
### UX
* ([#425](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/425)) Change the terms 'cercle' and 'centre' to 'service', and 'territoire' respectively.
* ([#542](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/542)) Improve the ux for selecting whether user wants to be notified of the final step of a workflow or all steps
* Expand timeSpent choices for evaluation document and translate them to user locale or fallback 'fr'
* ([#455](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/455)) Change the order of display for results and objectives in the social work/action form
* Wrap text when it is too long within badges
## v4.6.1 - 2025-10-27 ## v4.6.1 - 2025-10-27
### Fixed ### Fixed
* Fix export case where no 'reason' is picked within the PersonHavingActivityBetweenDateFilter.php * Fix export case where no 'reason' is picked within the PersonHavingActivityBetweenDateFilter.php
@@ -780,7 +819,7 @@ Fix color of Chill footer
- ajout d'un filtre et regroupement par usager participant sur les échanges - ajout d'un filtre et regroupement par usager participant sur les échanges
- ajout d'un regroupement: par type d'activité associé au parcours; - ajout d'un regroupement: par type d'activité associé au parcours;
- trie les filtre et regroupements par ordre alphabétique dans els exports - trie les filtre et regroupements par ordre alphabétique dans els exports
- ajout d'un paramètre qui permet de désactiver le filtre par territoire dans les exports - ajout d'un paramètre qui permet de désactiver le filtre par centre dans les exports
- correction de l'interface de date dans les filtres et regroupements "par statut du parcours à la date" - correction de l'interface de date dans les filtres et regroupements "par statut du parcours à la date"
## v2.9.2 - 2023-10-17 ## v2.9.2 - 2023-10-17
@@ -960,7 +999,7 @@ error when trying to reedit a saved export
- ajout d'un regroupement par métier des intervenants sur un parcours; - ajout d'un regroupement par métier des intervenants sur un parcours;
- ajout d'un regroupement par service des intervenants sur un parcours; - ajout d'un regroupement par service des intervenants sur un parcours;
- ajout d'un regroupement par utilisateur intervenant sur un parcours - ajout d'un regroupement par utilisateur intervenant sur un parcours
- ajout d'un regroupement "par territoire de l'usager"; - ajout d'un regroupement "par centre de l'usager";
- ajout d'un filtre "par métier intervenant sur un parcours"; - ajout d'un filtre "par métier intervenant sur un parcours";
- ajout d'un filtre "par service intervenant sur un parcours"; - ajout d'un filtre "par service intervenant sur un parcours";
- création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot); - création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot);

View File

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

View File

@@ -1,12 +1,7 @@
import { import { trans, setLocale, setLocaleFallbacks } from "./ux-translator";
trans,
setLocale,
getLocale,
setLocaleFallbacks,
} from "./ux-translator";
setLocaleFallbacks({ en: "fr", nl: "fr", fr: "en" }); setLocaleFallbacks({"en": "fr", "nl": "fr", "fr": "en"});
setLocale("fr"); setLocale('fr');
export { trans, getLocale }; export { trans };
export * from "../var/translations"; export * from '../var/translations';

View File

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

View File

@@ -34,7 +34,6 @@ return [
Chill\ThirdPartyBundle\ChillThirdPartyBundle::class => ['all' => true], Chill\ThirdPartyBundle\ChillThirdPartyBundle::class => ['all' => true],
Chill\BudgetBundle\ChillBudgetBundle::class => ['all' => true], Chill\BudgetBundle\ChillBudgetBundle::class => ['all' => true],
Chill\WopiBundle\ChillWopiBundle::class => ['all' => true], Chill\WopiBundle\ChillWopiBundle::class => ['all' => true],
Chill\TicketBundle\ChillTicketBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true], Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true], loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true],

View File

@@ -1,5 +1,5 @@
chill_main: chill_main:
available_languages: [ '%env(resolve:LOCALE)%', 'en' ] available_languages: [ '%env(resolve:LOCALE)%', 'en', 'nl' ]
available_countries: ['BE', 'FR'] available_countries: ['BE', 'FR']
top_banner: top_banner:
visible: false visible: false

View File

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

View File

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

View File

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

View File

@@ -66,7 +66,6 @@ framework:
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority 'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async 'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async 'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async
'Chill\TicketBundle\Messenger\PostTicketUpdateMessage': async
# end of routes added by chill-bundles recipes # end of routes added by chill-bundles recipes
# Route your messages to the transports # Route your messages to the transports
# 'App\Message\YourMessage': async # 'App\Message\YourMessage': async

View File

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

View File

@@ -11,94 +11,24 @@
Create a new bundle Create a new bundle
******************* *******************
Create your own bundle is not a trivial task.
The easiest way to achieve this is seems to be :
1. Prepare a fresh installation of the chill project, in a new directory
2. Create a new bundle in this project, in the src directory
3. Initialize a git repository **at the root bundle**, and create your initial commit.
4. Register the bundle with composer/packagist. If you do not plan to distribute your bundle with packagist, you may use a custom repository for achieve this [#f1]_
5. Move to a development installation, made as described in the :ref:`installation-for-development` section, and add your new repository to the composer.json file
6. Work as :ref:`usual <editing-code-and-commiting>`
.. warning:: .. warning::
This part of the doc is not yet tested This part of the doc is not yet tested
Create a new directory with Bundle class TODO
----------------------------------------
.. code-block:: bash
mkdir -p src/Bundle/ChillSomeBundle/src/config
mkdir -p src/Bundle/ChillSomeBundle/src/Controller
Add a bundle file
.. code-block:: php
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\SomeBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ChillSomeBundle extends Bundle {}
And a route file:
.. code-block:: yaml
chill_ticket_controller:
resource: '@ChillTicketBundle/Controller/'
type: annotation
Register the new psr-4 namespace
--------------------------------
In composer.json, add the new psr4 namespace
.. code-block:: diff
{
"autoload": {
"psr-4": {
+ "Chill\\SomeBundle\\": "src/Bundle/ChillSomeBundle/src",
}
}
}
Register the bundle .. rubric:: Footnotes
-------------------
Register in the file :code:`config/bundles.php`:
.. code-block:: php
Vendor\Bundle\YourBundle\YourBundle::class => ['all' => true],
And import routes in :code:`config/routes/chill_some_bundle.yaml`:
.. code-block:: yaml
chill_ticket_bundle:
resource: '@ChillSomeBundle/config/routes.yaml'
Add the doctrine_migration namespace
------------------------------------
Add the namespace to :code:`config/packages/doctrine_migrations_chill.yaml`
.. code-block:: diff
doctrine_migrations:
migrations_paths:
+ 'Chill\Some\Ticket': '@ChillSomeBundle/migrations'
Dump autoloading
----------------
.. code-block:: bash
symfony composer dump-autoload
.. [#f1] Be aware that we use the Affero GPL Licence, which ensure that all users must have access to derivative works done with this software.

View File

@@ -41,7 +41,6 @@
"typescript": "^5.6.3", "typescript": "^5.6.3",
"typescript-eslint": "^8.13.0", "typescript-eslint": "^8.13.0",
"vue-loader": "^17.0.0", "vue-loader": "^17.0.0",
"vue-tsc": "^3.1.3",
"webpack": "^5.75.0", "webpack": "^5.75.0",
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1"
}, },
@@ -81,12 +80,12 @@
"dev": "encore dev", "dev": "encore dev",
"watch": "encore dev --watch", "watch": "encore dev --watch",
"build": "encore production --progress", "build": "encore production --progress",
"specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml src/Bundle/ChillTicketBundle/chill.api.specs.yaml> templates/api/specs.yaml", "specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml> templates/api/specs.yaml",
"specs-validate": "swagger-cli validate templates/api/specs.yaml", "specs-validate": "swagger-cli validate templates/api/specs.yaml",
"specs-create-dir": "mkdir -p templates/api", "specs-create-dir": "mkdir -p templates/api",
"specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate", "specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate",
"version": "node --version", "version": "node --version",
"eslint": "eslint-baseline --fix \"src/**/*.{js,ts,vue}\"" "eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
}, },
"private": true "private": true
} }

View File

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

View File

@@ -1,8 +0,0 @@
In this directory, you find an example of file for the command `chill:main:ticket_motives_import`.
This file contains a list of ticket motives to import into the system. Each entry is a dictionary with two keys: `code` and `label`. The `code` key contains the unique code for the ticket motive, and the `label` key contains the human-readable label for the ticket motive.
The `stored_objects` key contains the documents that will be associated with the tickets. They must be found in the same directory.
The command `chill:main:ticket_motives_import` uses this file to import the specified ticket motives into the system.

View File

@@ -1,136 +0,0 @@
- label:
fr: Appel famille pour annonce de décès
urgent: false
supplementary_informations:
- label:
fr: Date du décès
- label:
fr: lieu du décès (domicile ou hôpital)
- label:
fr: nom de lhôpital
- label:
fr: service concerné
stored_objects:
- label:
fr: ☀️ De 07h à 21h
filename: 2_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🌙 De 21h à 07h du matin
filename: 3_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🗓️ Dimanches et jours fériés
filename: 4_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 'Appel famille pour annonce absence : hospitalisation ou consultation'
urgent: false
supplementary_informations:
- label:
fr: Quel hôpital
- label:
fr: quel service
- label:
fr: pour quelles raisons
- label:
fr: 'consultation : date et heure'
- label:
fr: hospitalisation complète ou HDJ
stored_objects:
- label:
fr: ☀️ De 07h à 21h
filename: 5_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🌙 De 21h à 07h du matin
filename: 6_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🗓️ Dimanches et jours fériés
filename: 7_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 'Appel famille pour annonce absence : interruption de prise en charge'
urgent: false
supplementary_informations:
- label:
fr: Pour quelles raisons ? Date
- label:
fr: durée
- label:
fr: accord médical ?
stored_objects:
- label:
fr: ☀️ De 07h à 21h
filename: 8_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🌙 De 21h à 07h du matin
filename: 9_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🗓️ Dimanches et jours fériés
filename: 10_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 'Appel famille pour annonce absence : changement dadresse'
urgent: false
supplementary_informations:
- label:
fr:
- label:
fr: Pourquoi ? Pour combien de temps ? Besoin dun relais des soins ? Nouvelle adresse ?
stored_objects:
- label:
fr: ☀️ De 07h à 21h
filename: 11_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🌙 De 21h à 07h du matin
filename: 12_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🗓️ Dimanches et jours fériés
filename: 13_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: Appel famille pour altération de létat général du patient
urgent: true
supplementary_informations:
- label:
fr: Recherche des symptômes
- label:
fr: Attentes par rapport à la demande
stored_objects:
- label:
fr: ☀️ De 07h à 21h
filename: 14_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🌙 De 21h à 07h du matin
filename: 15_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🗓️ Dimanches et jours fériés
filename: 16_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: Appel famille pour prise en charge de la douleur
urgent: true
supplementary_informations:
- label:
fr: Localisation douleur
- label:
fr: Horaire dernier passage
- label:
fr: Traitements en cours
stored_objects:
- label:
fr: ☀️ De 07h à 21h
filename: 17_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🌙 De 21h à 07h du matin
filename: 18_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🗓️ Dimanches et jours fériés
filename: 19_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: Appel famille pour information sur la date de prise en charge
urgent: false
supplementary_informations: []
stored_objects:
- label:
fr: ☀️ De 07h à 21h
filename: 20_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🌙 De 21h à 07h du matin
filename: 21_doc_20250402_Pelotons flux externes consolidés.pdf
- label:
fr: 🗓️ Dimanches et jours fériés
filename: 22_doc_20250402_Pelotons flux externes consolidés.pdf

View File

@@ -1,6 +0,0 @@
In this directory, you find an example of file for the command `chill:main:override_translation`.
This file contains a list of translations to override in the translation catalogue. Each entry is a dictionary with two keys: `from` and `to`. The `from` key contains the original translation string, and the `to` key contains the replacement string.
The command `chill:main:override_translation` uses this file to generate a new translation catalogue with the specified overrides applied.

View File

@@ -1,8 +0,0 @@
- {from: "de l'usager", to: "du patient"}
- {from: "l'usager", to: "le patient"}
- {from: "L'usager", to: "Le patient"}
- {from: "d'usagers", to: "de patients"}
- {from: "usagers", to: "patients"}
- {from: "Usagers", to: "Patients"}
- {from: "usager", to: "patient"}
- {from: "Usager", to: "Patient"}

View File

@@ -382,6 +382,7 @@ final class ActivityController extends AbstractController
$entity = new Activity(); $entity = new Activity();
$entity->setUser($this->security->getUser()); $entity->setUser($this->security->getUser());
$entity->addUser($this->security->getUser());
if ($person instanceof Person) { if ($person instanceof Person) {
$entity->setPerson($person); $entity->setPerson($person);

View File

@@ -88,8 +88,8 @@ class ActivityType extends AbstractType
if (null !== $options['data']->getPerson()) { if (null !== $options['data']->getPerson()) {
$builder->add('scope', ScopePickerType::class, [ $builder->add('scope', ScopePickerType::class, [
'center' => $options['center'],
'role' => ActivityVoter::CREATE === (string) $options['role'] ? ActivityVoter::CREATE_PERSON : (string) $options['role'], 'role' => ActivityVoter::CREATE === (string) $options['role'] ? ActivityVoter::CREATE_PERSON : (string) $options['role'],
'center' => $options['center'],
'required' => true, 'required' => true,
]); ]);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,38 @@
<template> <template>
<span class="inline-choice"> <span class="inline-choice">
<div class="form-check"> <div class="form-check">
<input <input
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
v-model="selected" v-model="selected"
name="issue" name="issue"
:id="issue.id" :id="issue.id"
:value="issue" :value="issue"
/> />
<label class="form-check-label" :for="issue.id"> <label class="form-check-label" :for="issue.id">
<span class="badge bg-chill-l-gray text-dark">{{ issue.text }}</span> <span class="badge bg-chill-l-gray text-dark">{{
</label> issue.text
</div> }}</span>
</span> </label>
</div>
</span>
</template> </template>
<script> <script>
export default { export default {
name: "CheckSocialIssue", name: "CheckSocialIssue",
props: ["issue", "selection"], props: ["issue", "selection"],
emits: ["updateSelected"], emits: ["updateSelected"],
computed: { computed: {
selected: { selected: {
set(value) { set(value) {
this.$emit("updateSelected", value); this.$emit("updateSelected", value);
}, },
get() { get() {
return this.selection; return this.selection;
}, },
},
}, },
},
}; };
</script> </script>
@@ -39,9 +41,24 @@ export default {
@import "ChillPersonAssets/chill/scss/mixins"; @import "ChillPersonAssets/chill/scss/mixins";
@import "ChillMainAssets/chill/scss/chill_variables"; @import "ChillMainAssets/chill/scss/chill_variables";
span.badge { span.badge {
@include badge_social($social-issue-color); @include badge_social($social-issue-color);
font-size: 95%; font-size: 95%;
margin-bottom: 5px; white-space: normal;
margin-right: 1em; word-wrap: break-word;
word-break: break-word;
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
margin-right: 1em;
text-align: left;
&::before {
position: absolute;
left: 11px;
top: 0;
margin: 0 0.3em 0 -0.75em;
}
position: relative;
padding-left: 1.5em;
} }
</style> </style>

View File

@@ -136,7 +136,6 @@
<div class="wl-col list"> <div class="wl-col list">
{{ activity.comment|chill_entity_render_box({ {{ activity.comment|chill_entity_render_box({
'disable_markdown': false, 'disable_markdown': false,
'limit_lines': 3,
'metadata': false, 'metadata': false,
}) }} }) }}
</div> </div>

View File

@@ -0,0 +1,50 @@
<?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\Migrations\Activity;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Migration fixing the automatic association of users to activities (exchanges).
*
* Originally, the user who created an exchange was not automatically associated
* to it (the "TMS" column), which led to incomplete data and biased statistics.
*
* This migration:
* - retroactively associates the creator of each exchange to the corresponding
* activity;
* - flags these backfilled associations with a temporary column so it is clear
* they were added by this data correction and can be safely cleaned up later.
*/
final class Version20251118124241 extends AbstractMigration
{
public function getDescription(): string
{
return 'Insert the creator of activity into the activity_user table';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE activity_user ADD COLUMN by_migration BOOL DEFAULT FALSE');
$this->addSql("COMMENT ON COLUMN activity_user.by_migration IS 'For backup purpose - can be safely deleted after a while. See migration \\Chill\\Migrations\\Activity\\Version20251118124241'");
$this->addSql('INSERT INTO activity_user (activity_id, user_id, by_migration)
SELECT id, user_id, true FROM activity
ON CONFLICT DO NOTHING');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE activity_user DROP COLUMN by_migration');
}
}

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Controller; namespace Chill\CalendarBundle\Controller;
use Chill\CalendarBundle\Repository\CalendarRepository; use Chill\CalendarBundle\Repository\CalendarRepository;
use Chill\CalendarBundle\Repository\InviteRepository;
use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Serializer\Model\Collection; use Chill\MainBundle\Serializer\Model\Collection;
@@ -23,7 +24,10 @@ use Symfony\Component\Routing\Annotation\Route;
class CalendarAPIController extends ApiController class CalendarAPIController extends ApiController
{ {
public function __construct(private readonly CalendarRepository $calendarRepository) {} public function __construct(
private readonly CalendarRepository $calendarRepository,
private readonly InviteRepository $inviteRepository,
) {}
#[Route(path: '/api/1.0/calendar/calendar/by-user/{id}.{_format}', name: 'chill_api_single_calendar_list_by-user', requirements: ['_format' => 'json'])] #[Route(path: '/api/1.0/calendar/calendar/by-user/{id}.{_format}', name: 'chill_api_single_calendar_list_by-user', requirements: ['_format' => 'json'])]
public function listByUser(User $user, Request $request, string $_format): JsonResponse public function listByUser(User $user, Request $request, string $_format): JsonResponse
@@ -52,16 +56,37 @@ class CalendarAPIController extends ApiController
throw new BadRequestHttpException('dateTo not parsable'); throw new BadRequestHttpException('dateTo not parsable');
} }
$total = $this->calendarRepository->countByUser($user, $dateFrom, $dateTo); // Get calendar items where user is the main user
$paginator = $this->getPaginatorFactory()->create($total); $ownCalendars = $this->calendarRepository->findByUser(
$ranges = $this->calendarRepository->findByUser(
$user, $user,
$dateFrom, $dateFrom,
$dateTo, $dateTo
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
); );
// Get calendar items from accepted invites
$acceptedInvites = $this->inviteRepository->findAcceptedInvitesByUserAndDateRange($user, $dateFrom, $dateTo);
$inviteCalendars = array_map(fn ($invite) => $invite->getCalendar(), $acceptedInvites);
// Merge
$allCalendars = array_merge($ownCalendars, $inviteCalendars);
$uniqueCalendars = [];
$seenIds = [];
foreach ($allCalendars as $calendar) {
$id = $calendar->getId();
if (!in_array($id, $seenIds, true)) {
$seenIds[] = $id;
$uniqueCalendars[] = $calendar;
}
}
$total = count($uniqueCalendars);
$paginator = $this->getPaginatorFactory()->create($total);
$offset = $paginator->getCurrentPageFirstItemNumber();
$limit = $paginator->getItemsPerPage();
$ranges = array_slice($uniqueCalendars, $offset, $limit);
$collection = new Collection($ranges, $paginator); $collection = new Collection($ranges, $paginator);
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['calendar:light']]); return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['calendar:light']]);

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Repository; namespace Chill\CalendarBundle\Repository;
use Chill\CalendarBundle\Entity\Invite; use Chill\CalendarBundle\Entity\Invite;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
@@ -51,6 +52,52 @@ class InviteRepository implements ObjectRepository
return $this->entityRepository->findOneBy($criteria); return $this->entityRepository->findOneBy($criteria);
} }
/**
* Find accepted invites for a user within a date range.
*
* @return array|Invite[]
*/
public function findAcceptedInvitesByUserAndDateRange(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to): array
{
return $this->buildAcceptedInviteByUserAndDateRangeQuery($user, $from, $to)
->getQuery()
->getResult();
}
/**
* Count accepted invites for a user within a date range.
*/
public function countAcceptedInvitesByUserAndDateRange(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to): int
{
return $this->buildAcceptedInviteByUserAndDateRangeQuery($user, $from, $to)
->select('COUNT(c)')
->getQuery()
->getSingleScalarResult();
}
public function buildAcceptedInviteByUserAndDateRangeQuery(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to)
{
$qb = $this->entityRepository->createQueryBuilder('i');
return $qb
->join('i.calendar', 'c')
->where(
$qb->expr()->andX(
$qb->expr()->eq('i.user', ':user'),
$qb->expr()->eq('i.status', ':status'),
$qb->expr()->gte('c.startDate', ':startDate'),
$qb->expr()->lte('c.endDate', ':endDate'),
$qb->expr()->isNull('c.cancelReason')
)
)
->setParameters([
'user' => $user,
'status' => Invite::ACCEPTED,
'startDate' => $from,
'endDate' => $to,
]);
}
public function getClassName(): string public function getClassName(): string
{ {
return Invite::class; return Invite::class;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -108,9 +108,12 @@
{{ formatDate(event.endStr, "time") }}: {{ formatDate(event.endStr, "time") }}:
{{ event.extendedProps.locationName }}</b {{ event.extendedProps.locationName }}</b
> >
<b v-else-if="event.extendedProps.is === 'local'">{{ <a
event.title :href="calendarLink(event.id)"
}}</b> v-else-if="event.extendedProps.is === 'local'"
>
<b>{{ event.title }}</b>
</a>
<b v-else>no 'is'</b> <b v-else>no 'is'</b>
<a <a
v-if="event.extendedProps.is === 'range'" v-if="event.extendedProps.is === 'range'"
@@ -486,6 +489,12 @@ function copyWeek() {
}); });
} }
const calendarLink = (calendarId: string) => {
const idStr = calendarId.match(/_(\d+)$/)?.[1];
return `/fr/calendar/calendar/${idStr}/edit`;
};
onMounted(() => { onMounted(() => {
copyFromWeek.value = dateToISO(getMonday(0)); copyFromWeek.value = dateToISO(getMonday(0));
copyToWeek.value = dateToISO(getMonday(1)); copyToWeek.value = dateToISO(getMonday(1));

View File

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

View File

@@ -1,27 +1,27 @@
const appMessages = { const appMessages = {
fr: { fr: {
created_availabilities: "Lieu des plages de disponibilités créées", created_availabilities: "Lieu des plages de disponibilités créées",
edit_your_calendar_range: "Planifiez vos plages de disponibilités", edit_your_calendar_range: "Planifiez vos plages de disponibilités",
show_my_calendar: "Afficher mon calendrier", show_my_calendar: "Afficher mon calendrier",
show_weekends: "Afficher les week-ends", show_weekends: "Afficher les week-ends",
copy_range: "Copier", copy_range: "Copier",
copy_range_from_to: "Copier les plages", copy_range_from_to: "Copier les plages",
from_day_to_day: "d'un jour à l'autre", from_day_to_day: "d'un jour à l'autre",
from_week_to_week: "d'une semaine à l'autre", from_week_to_week: "d'une semaine à l'autre",
copy_range_how_to: copy_range_how_to:
"Créez les plages de disponibilités durant une journée et copiez-les facilement au jour suivant avec ce bouton. Si les week-ends sont cachés, le jour suivant un vendredi sera le lundi.", "Créez les plages de disponibilités durant une journée et copiez-les facilement au jour suivant avec ce bouton. Si les week-ends sont cachés, le jour suivant un vendredi sera le lundi.",
new_range_to_save: "Nouvelles plages à enregistrer", new_range_to_save: "Nouvelles plages à enregistrer",
update_range_to_save: "Plages à modifier", update_range_to_save: "Plages à modifier",
delete_range_to_save: "Plages à supprimer", delete_range_to_save: "Plages à supprimer",
by: "Par", by: "Par",
main_user_concerned: "Utilisateur concerné", main_user_concerned: "Utilisateur concerné",
dateFrom: "De", dateFrom: "De",
dateTo: "à", dateTo: "à",
day: "Jour", day: "Jour",
week: "Semaine", week: "Semaine",
month: "Mois", month: "Mois",
today: "Aujourd'hui", today: "Aujourd'hui",
}, },
}; };
export { appMessages }; export { appMessages };

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import { State } from "./../index"; import { State } from "./../index";
import { ActionContext, Module } from "vuex"; import { ActionContext, Module } from "vuex";
import { import {
CalendarRange, CalendarRange,
CalendarRangeCreate, CalendarRangeCreate,
CalendarRangeEdit, CalendarRangeEdit,
isEventInputCalendarRange, isEventInputCalendarRange,
} from "../../../../types"; } from "../../../../types";
import { Location } from "../../../../../../../ChillMainBundle/Resources/public/types"; import { Location } from "../../../../../../../ChillMainBundle/Resources/public/types";
import { fetchCalendarRangeForUser } from "../../../Calendar/api"; import { fetchCalendarRangeForUser } from "../../../Calendar/api";
@@ -12,332 +12,369 @@ import { calendarRangeToFullCalendarEvent } from "../../../Calendar/store/utils"
import { EventInput } from "@fullcalendar/core"; import { EventInput } from "@fullcalendar/core";
import { makeFetch } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods"; import { makeFetch } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import { import {
datetimeToISO, datetimeToISO,
dateToISO, dateToISO,
ISOToDatetime, ISOToDatetime,
} from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date"; } from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
import type { EventInputCalendarRange } from "../../../../types"; import type { EventInputCalendarRange } from "../../../../types";
export interface CalendarRangesState { export interface CalendarRangesState {
ranges: (EventInput | EventInputCalendarRange)[]; ranges: (EventInput | EventInputCalendarRange)[];
rangesLoaded: { start: number; end: number }[]; rangesLoaded: { start: number; end: number }[];
rangesIndex: Set<string>; rangesIndex: Set<string>;
key: number; key: number;
} }
type Context = ActionContext<CalendarRangesState, State>; type Context = ActionContext<CalendarRangesState, State>;
export default { export default {
namespaced: true, namespaced: true,
state: (): CalendarRangesState => ({ state: (): CalendarRangesState => ({
ranges: [], ranges: [],
rangesLoaded: [], rangesLoaded: [],
rangesIndex: new Set<string>(), rangesIndex: new Set<string>(),
key: 0, key: 0,
}), }),
getters: { getters: {
isRangeLoaded: isRangeLoaded:
(state: CalendarRangesState) => (state: CalendarRangesState) =>
({ start, end }: { start: Date; end: Date }): boolean => { ({ start, end }: { start: Date; end: Date }): boolean => {
for (const range of state.rangesLoaded) { for (const range of state.rangesLoaded) {
if (start.getTime() === range.start && end.getTime() === range.end) { if (
return true; start.getTime() === range.start &&
} end.getTime() === range.end
} ) {
return true;
}
}
return false; return false;
}, },
getRangesOnDate: getRangesOnDate:
(state: CalendarRangesState) => (state: CalendarRangesState) =>
(date: Date): EventInputCalendarRange[] => { (date: Date): EventInputCalendarRange[] => {
const founds = []; const founds = [];
const dateStr = dateToISO(date) as string; const dateStr = dateToISO(date) as string;
for (const range of state.ranges) { for (const range of state.ranges) {
if ( if (
isEventInputCalendarRange(range) && isEventInputCalendarRange(range) &&
range.start.startsWith(dateStr) range.start.startsWith(dateStr)
) { ) {
founds.push(range); founds.push(range);
} }
} }
return founds; return founds;
}, },
getRangesOnWeek: getRangesOnWeek:
(state: CalendarRangesState) => (state: CalendarRangesState) =>
(mondayDate: Date): EventInputCalendarRange[] => { (mondayDate: Date): EventInputCalendarRange[] => {
const founds = []; const founds = [];
for (const d of Array.from(Array(7).keys())) { for (const d of Array.from(Array(7).keys())) {
const dateOfWeek = new Date(mondayDate); const dateOfWeek = new Date(mondayDate);
dateOfWeek.setDate(mondayDate.getDate() + d); dateOfWeek.setDate(mondayDate.getDate() + d);
const dateStr = dateToISO(dateOfWeek) as string; const dateStr = dateToISO(dateOfWeek) as string;
for (const range of state.ranges) { for (const range of state.ranges) {
if ( if (
isEventInputCalendarRange(range) && isEventInputCalendarRange(range) &&
range.start.startsWith(dateStr) range.start.startsWith(dateStr)
) { ) {
founds.push(range); founds.push(range);
}
}
}
return founds;
},
},
mutations: {
addRanges(state: CalendarRangesState, ranges: CalendarRange[]) {
const toAdd = ranges
.map((cr) => calendarRangeToFullCalendarEvent(cr))
.map((cr) => ({
...cr,
backgroundColor: "white",
borderColor: "#3788d8",
textColor: "black",
}))
.filter((r) => !state.rangesIndex.has(r.id));
toAdd.forEach((r) => {
state.rangesIndex.add(r.id);
state.ranges.push(r);
});
state.key = state.key + toAdd.length;
},
addExternals(
state: CalendarRangesState,
externalEvents: (EventInput & { id: string })[],
) {
const toAdd = externalEvents.filter(
(r) => !state.rangesIndex.has(r.id),
);
toAdd.forEach((r) => {
state.rangesIndex.add(r.id);
state.ranges.push(r);
});
state.key = state.key + toAdd.length;
},
addLoaded(
state: CalendarRangesState,
payload: { start: Date; end: Date },
) {
state.rangesLoaded.push({
start: payload.start.getTime(),
end: payload.end.getTime(),
});
},
addRange(state: CalendarRangesState, payload: CalendarRange) {
const asEvent = calendarRangeToFullCalendarEvent(payload);
state.ranges.push({
...asEvent,
backgroundColor: "white",
borderColor: "#3788d8",
textColor: "black",
});
state.rangesIndex.add(asEvent.id);
state.key = state.key + 1;
},
removeRange(state: CalendarRangesState, calendarRangeId: number) {
const found = state.ranges.find(
(r) =>
r.calendarRangeId === calendarRangeId && r.is === "range",
);
if (found !== undefined) {
state.ranges = state.ranges.filter(
(r) =>
!(
r.calendarRangeId === calendarRangeId &&
r.is === "range"
),
);
if (typeof found.id === "string") {
// should always be true
state.rangesIndex.delete(found.id);
}
state.key = state.key + 1;
} }
}
}
return founds;
},
},
mutations: {
addRanges(state: CalendarRangesState, ranges: CalendarRange[]) {
const toAdd = ranges
.map((cr) => calendarRangeToFullCalendarEvent(cr))
.map((cr) => ({
...cr,
backgroundColor: "white",
borderColor: "#3788d8",
textColor: "black",
}))
.filter((r) => !state.rangesIndex.has(r.id));
toAdd.forEach((r) => {
state.rangesIndex.add(r.id);
state.ranges.push(r);
});
state.key = state.key + toAdd.length;
},
addExternals(
state: CalendarRangesState,
externalEvents: (EventInput & { id: string })[],
) {
const toAdd = externalEvents.filter((r) => !state.rangesIndex.has(r.id));
toAdd.forEach((r) => {
state.rangesIndex.add(r.id);
state.ranges.push(r);
});
state.key = state.key + toAdd.length;
},
addLoaded(state: CalendarRangesState, payload: { start: Date; end: Date }) {
state.rangesLoaded.push({
start: payload.start.getTime(),
end: payload.end.getTime(),
});
},
addRange(state: CalendarRangesState, payload: CalendarRange) {
const asEvent = calendarRangeToFullCalendarEvent(payload);
state.ranges.push({
...asEvent,
backgroundColor: "white",
borderColor: "#3788d8",
textColor: "black",
});
state.rangesIndex.add(asEvent.id);
state.key = state.key + 1;
},
removeRange(state: CalendarRangesState, calendarRangeId: number) {
const found = state.ranges.find(
(r) => r.calendarRangeId === calendarRangeId && r.is === "range",
);
if (found !== undefined) {
state.ranges = state.ranges.filter(
(r) => !(r.calendarRangeId === calendarRangeId && r.is === "range"),
);
if (typeof found.id === "string") {
// should always be true
state.rangesIndex.delete(found.id);
}
state.key = state.key + 1;
}
},
updateRange(state: CalendarRangesState, range: CalendarRange) {
const found = state.ranges.find(
(r) => r.calendarRangeId === range.id && r.is === "range",
);
const newEvent = calendarRangeToFullCalendarEvent(range);
if (found !== undefined) {
found.start = newEvent.start;
found.end = newEvent.end;
found.locationId = range.location.id;
found.locationName = range.location.name;
}
state.key = state.key + 1;
},
},
actions: {
fetchRanges(
ctx: Context,
payload: { start: Date; end: Date },
): Promise<null> {
const start = payload.start;
const end = payload.end;
if (ctx.rootGetters["me/getMe"] === null) {
return Promise.resolve(ctx.getters.getRangeSource);
}
if (ctx.getters.isRangeLoaded({ start, end })) {
return Promise.resolve(ctx.getters.getRangeSource);
}
ctx.commit("addLoaded", {
start: start,
end: end,
});
return fetchCalendarRangeForUser(
ctx.rootGetters["me/getMe"],
start,
end,
).then((ranges: CalendarRange[]) => {
ctx.commit("addRanges", ranges);
return Promise.resolve(null);
});
},
createRange(
ctx: Context,
{ start, end, location }: { start: Date; end: Date; location: Location },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range.json?`;
if (ctx.rootState.me.me === null) {
throw new Error("user is currently null");
}
const body = {
user: {
id: ctx.rootState.me.me.id,
type: "user",
}, },
startDate: { updateRange(state: CalendarRangesState, range: CalendarRange) {
datetime: datetimeToISO(start), const found = state.ranges.find(
(r) => r.calendarRangeId === range.id && r.is === "range",
);
const newEvent = calendarRangeToFullCalendarEvent(range);
if (found !== undefined) {
found.start = newEvent.start;
found.end = newEvent.end;
found.locationId = range.location.id;
found.locationName = range.location.name;
}
state.key = state.key + 1;
}, },
endDate: { },
datetime: datetimeToISO(end), actions: {
fetchRanges(
ctx: Context,
payload: { start: Date; end: Date },
): Promise<null> {
const start = payload.start;
const end = payload.end;
if (ctx.rootGetters["me/getMe"] === null) {
return Promise.resolve(ctx.getters.getRangeSource);
}
if (ctx.getters.isRangeLoaded({ start, end })) {
return Promise.resolve(ctx.getters.getRangeSource);
}
ctx.commit("addLoaded", {
start: start,
end: end,
});
return fetchCalendarRangeForUser(
ctx.rootGetters["me/getMe"],
start,
end,
).then((ranges: CalendarRange[]) => {
ctx.commit("addRanges", ranges);
return Promise.resolve(null);
});
}, },
location: { createRange(
id: location.id, ctx: Context,
type: "location", {
start,
end,
location,
}: { start: Date; end: Date; location: Location },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range.json?`;
if (ctx.rootState.me.me === null) {
throw new Error("user is currently null");
}
const body = {
user: {
id: ctx.rootState.me.me.id,
type: "user",
},
startDate: {
datetime: datetimeToISO(start),
},
endDate: {
datetime: datetimeToISO(end),
},
location: {
id: location.id,
type: "location",
},
} as CalendarRangeCreate;
return makeFetch<CalendarRangeCreate, CalendarRange>(
"POST",
url,
body,
)
.then((newRange) => {
ctx.commit("addRange", newRange);
return Promise.resolve(null);
})
.catch((error) => {
console.error(error);
throw error;
});
}, },
} as CalendarRangeCreate; deleteRange(ctx: Context, calendarRangeId: number) {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
return makeFetch<CalendarRangeCreate, CalendarRange>("POST", url, body) makeFetch<undefined, never>("DELETE", url).then(() => {
.then((newRange) => { ctx.commit("removeRange", calendarRangeId);
ctx.commit("addRange", newRange); });
return Promise.resolve(null);
})
.catch((error) => {
console.error(error);
throw error;
});
},
deleteRange(ctx: Context, calendarRangeId: number) {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
makeFetch<undefined, never>("DELETE", url).then(() => {
ctx.commit("removeRange", calendarRangeId);
});
},
patchRangeTime(
ctx,
{
calendarRangeId,
start,
end,
}: { calendarRangeId: number; start: Date; end: Date },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
const body = {
startDate: {
datetime: datetimeToISO(start),
}, },
endDate: { patchRangeTime(
datetime: datetimeToISO(end), ctx,
{
calendarRangeId,
start,
end,
}: { calendarRangeId: number; start: Date; end: Date },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
const body = {
startDate: {
datetime: datetimeToISO(start),
},
endDate: {
datetime: datetimeToISO(end),
},
} as CalendarRangeEdit;
return makeFetch<CalendarRangeEdit, CalendarRange>(
"PATCH",
url,
body,
)
.then((range) => {
ctx.commit("updateRange", range);
return Promise.resolve(null);
})
.catch((error) => {
console.error(error);
return Promise.resolve(null);
});
}, },
} as CalendarRangeEdit; patchRangeLocation(
ctx,
{
location,
calendarRangeId,
}: { location: Location; calendarRangeId: number },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
const body = {
location: {
id: location.id,
type: "location",
},
} as CalendarRangeEdit;
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body) return makeFetch<CalendarRangeEdit, CalendarRange>(
.then((range) => { "PATCH",
ctx.commit("updateRange", range); url,
return Promise.resolve(null); body,
}) )
.catch((error) => { .then((range) => {
console.error(error); ctx.commit("updateRange", range);
return Promise.resolve(null); return Promise.resolve(null);
}); })
}, .catch((error) => {
patchRangeLocation( console.error(error);
ctx, return Promise.resolve(null);
{ });
location,
calendarRangeId,
}: { location: Location; calendarRangeId: number },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
const body = {
location: {
id: location.id,
type: "location",
}, },
} as CalendarRangeEdit; copyFromDayToAnotherDay(
ctx,
{ from, to }: { from: Date; to: Date },
): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] =
ctx.getters["getRangesOnDate"](from);
const promises = [];
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body) for (const r of rangesToCopy) {
.then((range) => { const start = new Date(ISOToDatetime(r.start) as Date);
ctx.commit("updateRange", range); start.setFullYear(
return Promise.resolve(null); to.getFullYear(),
}) to.getMonth(),
.catch((error) => { to.getDate(),
console.error(error); );
return Promise.resolve(null); const end = new Date(ISOToDatetime(r.end) as Date);
}); end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
const location = ctx.rootGetters["locations/getLocationById"](
r.locationId,
);
promises.push(
ctx.dispatch("createRange", { start, end, location }),
);
}
return Promise.all(promises).then(() => Promise.resolve(null));
},
copyFromWeekToAnotherWeek(
ctx: Context,
{ fromMonday, toMonday }: { fromMonday: Date; toMonday: Date },
): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] =
ctx.getters["getRangesOnWeek"](fromMonday);
const promises = [];
const diffTime = toMonday.getTime() - fromMonday.getTime();
for (const r of rangesToCopy) {
const start = new Date(ISOToDatetime(r.start) as Date);
const end = new Date(ISOToDatetime(r.end) as Date);
start.setTime(start.getTime() + diffTime);
end.setTime(end.getTime() + diffTime);
const location = ctx.rootGetters["locations/getLocationById"](
r.locationId,
);
promises.push(
ctx.dispatch("createRange", { start, end, location }),
);
}
return Promise.all(promises).then(() => Promise.resolve(null));
},
}, },
copyFromDayToAnotherDay(
ctx,
{ from, to }: { from: Date; to: Date },
): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] =
ctx.getters["getRangesOnDate"](from);
const promises = [];
for (const r of rangesToCopy) {
const start = new Date(ISOToDatetime(r.start) as Date);
start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
const end = new Date(ISOToDatetime(r.end) as Date);
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
const location = ctx.rootGetters["locations/getLocationById"](
r.locationId,
);
promises.push(ctx.dispatch("createRange", { start, end, location }));
}
return Promise.all(promises).then(() => Promise.resolve(null));
},
copyFromWeekToAnotherWeek(
ctx: Context,
{ fromMonday, toMonday }: { fromMonday: Date; toMonday: Date },
): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] =
ctx.getters["getRangesOnWeek"](fromMonday);
const promises = [];
const diffTime = toMonday.getTime() - fromMonday.getTime();
for (const r of rangesToCopy) {
const start = new Date(ISOToDatetime(r.start) as Date);
const end = new Date(ISOToDatetime(r.end) as Date);
start.setTime(start.getTime() + diffTime);
end.setTime(end.getTime() + diffTime);
const location = ctx.rootGetters["locations/getLocationById"](
r.locationId,
);
promises.push(ctx.dispatch("createRange", { start, end, location }));
}
return Promise.all(promises).then(() => Promise.resolve(null));
},
},
} as Module<CalendarRangesState, State>; } as Module<CalendarRangesState, State>;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,51 +1,51 @@
<template> <template>
<div> <div>
<h2 class="chill-red"> <h2 class="chill-red">
{{ $t("choose_your_calendar_user") }} {{ $t("choose_your_calendar_user") }}
</h2> </h2>
<VueMultiselect <VueMultiselect
name="field" name="field"
id="calendarUserSelector" id="calendarUserSelector"
v-model="value" v-model="value"
track-by="id" track-by="id"
label="value" label="value"
:custom-label="transName" :custom-label="transName"
:placeholder="$t('select_user')" :placeholder="$t('select_user')"
:multiple="true" :multiple="true"
:close-on-select="false" :close-on-select="false"
:allow-empty="true" :allow-empty="true"
:model-value="value" :model-value="value"
:select-label="$t('multiselect.select_label')" :select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')" :deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')" :selected-label="$t('multiselect.selected_label')"
@select="selectUsers" @select="selectUsers"
@remove="unSelectUsers" @remove="unSelectUsers"
@close="coloriseSelectedValues" @close="coloriseSelectedValues"
:options="options" :options="options"
/> />
</div> </div>
<div class="form-check"> <div class="form-check">
<input <input
type="checkbox" type="checkbox"
id="myCalendar" id="myCalendar"
class="form-check-input" class="form-check-input"
v-model="showMyCalendarWidget" v-model="showMyCalendarWidget"
/> />
<label class="form-check-label" for="myCalendar">{{ <label class="form-check-label" for="myCalendar">{{
$t("show_my_calendar") $t("show_my_calendar")
}}</label> }}</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input <input
type="checkbox" type="checkbox"
id="weekends" id="weekends"
class="form-check-input" class="form-check-input"
@click="toggleWeekends" @click="toggleWeekends"
/> />
<label class="form-check-label" for="weekends">{{ <label class="form-check-label" for="weekends">{{
$t("show_weekends") $t("show_weekends")
}}</label> }}</label>
</div> </div>
</template> </template>
<script> <script>
import { fetchCalendarRanges, fetchCalendar } from "../../_api/api"; import { fetchCalendarRanges, fetchCalendar } from "../../_api/api";
@@ -53,183 +53,206 @@ import VueMultiselect from "vue-multiselect";
import { whoami } from "ChillPersonAssets/vuejs/AccompanyingCourse/api"; import { whoami } from "ChillPersonAssets/vuejs/AccompanyingCourse/api";
const COLORS = [ const COLORS = [
/* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */ /* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
"#8dd3c7", "#8dd3c7",
"#ffffb3", "#ffffb3",
"#bebada", "#bebada",
"#fb8072", "#fb8072",
"#80b1d3", "#80b1d3",
"#fdb462", "#fdb462",
"#b3de69", "#b3de69",
"#fccde5", "#fccde5",
"#d9d9d9", "#d9d9d9",
"#bc80bd", "#bc80bd",
"#ccebc5", "#ccebc5",
"#ffed6f", "#ffed6f",
]; ];
export default { export default {
name: "CalendarUserSelector", name: "CalendarUserSelector",
components: { VueMultiselect }, components: { VueMultiselect },
props: [ props: [
"users", "users",
"updateEventsSource", "updateEventsSource",
"calendarEvents", "calendarEvents",
"showMyCalendar", "showMyCalendar",
"toggleMyCalendar", "toggleMyCalendar",
"toggleWeekends", "toggleWeekends",
], ],
data() { data() {
return { return {
errorMsg: [], errorMsg: [],
value: [], value: [],
options: [], options: [],
}; };
},
computed: {
showMyCalendarWidget: {
set(value) {
this.toggleMyCalendar(value);
this.updateEventsSource();
},
get() {
return this.showMyCalendar;
},
}, },
}, computed: {
methods: { showMyCalendarWidget: {
init() { set(value) {
this.fetchData(); this.toggleMyCalendar(value);
this.updateEventsSource();
},
get() {
return this.showMyCalendar;
},
},
}, },
fetchData() { methods: {
fetchCalendarRanges() init() {
.then( this.fetchData();
(calendarRanges) => },
new Promise((resolve, reject) => { fetchData() {
let results = calendarRanges.results; fetchCalendarRanges()
.then(
let users = []; (calendarRanges) =>
results.forEach((i) => {
if (!users.some((j) => i.user.id === j.id)) {
let ratio = Math.floor(users.length / COLORS.length);
let colorIndex = users.length - ratio * COLORS.length;
users.push({
id: i.user.id,
username: i.user.username,
color: COLORS[colorIndex],
});
}
});
let calendarEvents = [];
users.forEach((u) => {
let arr = results
.filter((i) => i.user.id === u.id)
.map((i) => ({
start: i.startDate.datetime,
end: i.endDate.datetime,
calendarRangeId: i.id,
sourceColor: u.color,
//display: 'background' // can be an option for the disponibility
}));
calendarEvents.push({
events: arr,
color: u.color,
textColor: "#444444",
editable: false,
id: u.id,
});
});
this.users.loaded = users;
this.options = users;
this.calendarEvents.loaded = calendarEvents;
whoami().then(
(me) =>
new Promise((resolve, reject) => {
this.users.logged = me;
let currentUser = users.find((u) => u.id === me.id);
this.value = currentUser;
fetchCalendar(currentUser.id).then(
(calendar) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
let results = calendar.results; let results = calendarRanges.results;
let events = results.map((i) => ({
start: i.startDate.datetime,
end: i.endDate.datetime,
}));
let calendarEventsCurrentUser = {
events: events,
color: "darkblue",
id: 1000,
editable: false,
};
this.calendarEvents.user = calendarEventsCurrentUser;
this.selectUsers(currentUser); let users = [];
resolve(); results.forEach((i) => {
if (!users.some((j) => i.user.id === j.id)) {
let ratio = Math.floor(
users.length / COLORS.length,
);
let colorIndex =
users.length - ratio * COLORS.length;
users.push({
id: i.user.id,
username: i.user.username,
color: COLORS[colorIndex],
});
}
});
let calendarEvents = [];
users.forEach((u) => {
let arr = results
.filter((i) => i.user.id === u.id)
.map((i) => ({
start: i.startDate.datetime,
end: i.endDate.datetime,
calendarRangeId: i.id,
sourceColor: u.color,
//display: 'background' // can be an option for the disponibility
}));
calendarEvents.push({
events: arr,
color: u.color,
textColor: "#444444",
editable: false,
id: u.id,
});
});
this.users.loaded = users;
this.options = users;
this.calendarEvents.loaded = calendarEvents;
whoami().then(
(me) =>
new Promise((resolve, reject) => {
this.users.logged = me;
let currentUser = users.find(
(u) => u.id === me.id,
);
this.value = currentUser;
fetchCalendar(currentUser.id).then(
(calendar) =>
new Promise(
(resolve, reject) => {
let results =
calendar.results;
let events =
results.map(
(i) => ({
start: i
.startDate
.datetime,
end: i
.endDate
.datetime,
}),
);
let calendarEventsCurrentUser =
{
events: events,
color: "darkblue",
id: 1000,
editable: false,
};
this.calendarEvents.user =
calendarEventsCurrentUser;
this.selectUsers(
currentUser,
);
resolve();
},
),
);
resolve();
}),
);
resolve();
}), }),
); )
.catch((error) => {
this.errorMsg.push(error.message);
});
},
transName(value) {
return `${value.username}`;
},
coloriseSelectedValues() {
let tags = document.querySelectorAll(
"div.multiselect__tags-wrap",
)[0];
resolve(); if (tags.hasChildNodes()) {
}), let children = tags.childNodes;
); for (let i = 0; i < children.length; i++) {
let child = children[i];
resolve(); if (child.nodeType === Node.ELEMENT_NODE) {
}), this.users.selected.forEach((u) => {
) if (child.hasChildNodes()) {
.catch((error) => { if (child.firstChild.innerText == u.username) {
this.errorMsg.push(error.message); child.style.background = u.color;
}); child.firstChild.style.color = "#444444";
}, }
transName(value) { }
return `${value.username}`; });
}, }
coloriseSelectedValues() {
let tags = document.querySelectorAll("div.multiselect__tags-wrap")[0];
if (tags.hasChildNodes()) {
let children = tags.childNodes;
for (let i = 0; i < children.length; i++) {
let child = children[i];
if (child.nodeType === Node.ELEMENT_NODE) {
this.users.selected.forEach((u) => {
if (child.hasChildNodes()) {
if (child.firstChild.innerText == u.username) {
child.style.background = u.color;
child.firstChild.style.color = "#444444";
} }
} }
}); },
} selectEvents() {
} let selectedUsersId = this.users.selected.map((a) => a.id);
} this.calendarEvents.selected = this.calendarEvents.loaded.filter(
(a) => selectedUsersId.includes(a.id),
);
},
selectUsers(value) {
this.users.selected.push(value);
this.coloriseSelectedValues();
this.selectEvents();
this.updateEventsSource();
},
unSelectUsers(value) {
this.users.selected = this.users.selected.filter(
(a) => a.id != value.id,
);
this.selectEvents();
this.updateEventsSource();
},
}, },
selectEvents() { mounted() {
let selectedUsersId = this.users.selected.map((a) => a.id); this.init();
this.calendarEvents.selected = this.calendarEvents.loaded.filter((a) =>
selectedUsersId.includes(a.id),
);
}, },
selectUsers(value) {
this.users.selected.push(value);
this.coloriseSelectedValues();
this.selectEvents();
this.updateEventsSource();
},
unSelectUsers(value) {
this.users.selected = this.users.selected.filter((a) => a.id != value.id);
this.selectEvents();
this.updateEventsSource();
},
},
mounted() {
this.init();
},
}; };
</script> </script>

View File

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

View File

@@ -25,6 +25,8 @@ use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
// use Symfony\Component\Translation\LocaleSwitcher;
/** /**
* @see OnGenerationFailsTest for test suite * @see OnGenerationFailsTest for test suite
*/ */
@@ -40,6 +42,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
private StoredObjectRepositoryInterface $storedObjectRepository, private StoredObjectRepositoryInterface $storedObjectRepository,
private TranslatorInterface $translator, private TranslatorInterface $translator,
private UserRepositoryInterface $userRepository, private UserRepositoryInterface $userRepository,
// private LocaleSwitcher $localeSwitcher,
) {} ) {}
public static function getSubscribedEvents() public static function getSubscribedEvents()
@@ -118,6 +121,25 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
return; return;
} }
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
/*
$this->localeSwitcher->runWithLocale($creator->getLocale(), function () use ($message, $errors, $template, $creator) {
$email = (new TemplatedEmail())
->to($message->getSendResultToEmail())
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
->context([
'errors' => $errors,
'template' => $template,
'creator' => $creator,
'stored_object_id' => $message->getDestinationStoredObjectId(),
]);
$this->mailer->send($email);
});
*/
// Current implementation:
$email = (new TemplatedEmail()) $email = (new TemplatedEmail())
->to($message->getSendResultToEmail()) ->to($message->getSendResultToEmail())
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed')) ->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))

View File

@@ -27,6 +27,8 @@ use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
// use Symfony\Component\Translation\LocaleSwitcher;
/** /**
* Handle the request of document generation. * Handle the request of document generation.
*/ */
@@ -46,6 +48,7 @@ class RequestGenerationHandler implements MessageHandlerInterface
private readonly MailerInterface $mailer, private readonly MailerInterface $mailer,
private readonly TranslatorInterface $translator, private readonly TranslatorInterface $translator,
private readonly StoredObjectManagerInterface $storedObjectManager, private readonly StoredObjectManagerInterface $storedObjectManager,
// private readonly LocaleSwitcher $localeSwitcher,
) {} ) {}
public function __invoke(RequestGenerationMessage $message) public function __invoke(RequestGenerationMessage $message)
@@ -122,6 +125,30 @@ class RequestGenerationHandler implements MessageHandlerInterface
private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void
{ {
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
// Note: This method sends emails to admin addresses, not user addresses, so locale switching may not be needed
/*
$this->localeSwitcher->runWithLocale('fr', function () use ($destinationStoredObject, $message) {
// Get the content of the document
$content = $this->storedObjectManager->read($destinationStoredObject);
$filename = $destinationStoredObject->getFilename();
$contentType = $destinationStoredObject->getType();
// Create the email with the document as an attachment
$email = (new TemplatedEmail())
->to($message->getSendResultToEmail())
->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig')
->context([
'filename' => $filename,
])
->subject($this->translator->trans('docgen.data_dump_email.subject'))
->attach($content, $filename, $contentType);
$this->mailer->send($email);
});
*/
// Current implementation:
// Get the content of the document // Get the content of the document
$content = $this->storedObjectManager->read($destinationStoredObject); $content = $this->storedObjectManager->read($destinationStoredObject);
$filename = $destinationStoredObject->getFilename(); $filename = $destinationStoredObject->getFilename();

View File

@@ -94,7 +94,7 @@ class StoredObject implements Document, TrackCreationInterface
/** /**
* @var Collection<int, StoredObjectVersion>&Selectable<int, StoredObjectVersion> * @var Collection<int, StoredObjectVersion>&Selectable<int, StoredObjectVersion>
*/ */
#[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true, fetch: 'EAGER')] #[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true)]
private Collection&Selectable $versions; private Collection&Selectable $versions;
/** /**

View File

@@ -17,7 +17,6 @@ use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\ScopePickerType; use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher; use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
@@ -30,7 +29,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class PersonDocumentType extends AbstractType class PersonDocumentType extends AbstractType
{ {
public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly ScopeResolverDispatcher $scopeResolverDispatcher, private readonly ParameterBagInterface $parameterBag, private readonly CenterResolverDispatcher $centerResolverDispatcher) {} public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly ScopeResolverDispatcher $scopeResolverDispatcher, private readonly ParameterBagInterface $parameterBag) {}
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
@@ -57,8 +56,8 @@ class PersonDocumentType extends AbstractType
if ($isScopeConcerned && $this->parameterBag->get('chill_main')['acl']['form_show_scopes']) { if ($isScopeConcerned && $this->parameterBag->get('chill_main')['acl']['form_show_scopes']) {
$builder->add('scope', ScopePickerType::class, [ $builder->add('scope', ScopePickerType::class, [
'center' => $this->centerResolverDispatcher->resolveCenter($document),
'role' => $options['role'], 'role' => $options['role'],
'subject' => $document,
]); ]);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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