Compare commits

..

32 Commits

Author SHA1 Message Date
8bc46d4af3 Add changie 2025-10-06 13:38:18 +02:00
09aa8bc829 Translations: change 'centre' into 'territoire' throughout application 2025-10-06 12:38:27 +02:00
6cc6cf3a71 Translations: change 'cercle' into 'service' throughout application 2025-10-06 12:25:37 +02:00
bc2fbee5c6 Fix: notification edit template
form field addressesEmail removed
2025-10-06 12:14:00 +02:00
ebd10ca522 Merge branch 'fix/history-of-versions-stored-object' into 'master'
Fix the rendering of storedObject's history

See merge request Chill-Projet/chill-bundles!893
2025-10-03 20:47:06 +00:00
d3a31be412 Fix re-ordering of StoredObjectVersion in the list of versions
As some intermediate versions are remove, this may lead to situation where the indexes are not continous. In that case, the array is not a list, and is rendered as an array with numeric indexes, instead of a list of elements. The HistoryListItem component fails to render.

- Ensured proper handling of removed versions by using `array_values` to reindex items.
- Added test case to validate the result after removing a version.
- Asserted the results are a proper list in the API response.
2025-10-03 22:40:59 +02:00
d159a82f88 Update import paths in HistoryButtonListItem.vue to use aliases
- Changed types import to use `ChillDocStoreAssets/types`.
- Updated `ISOToDatetime` import to use `ChillMainAssets/chill/js/date`.
2025-10-03 22:20:51 +02:00
c2d9c73fd4 Release v4.5.1 2025-10-03 14:11:41 +02:00
0d6d15fcf7 Merge branch 'fix/conversion-exception' into 'master'
Introduce `ConversionWithSameMimeTypeException` for improved error handling in document conversion.

See merge request Chill-Projet/chill-bundles!892
2025-10-03 12:10:24 +00:00
f9ad96c78b Introduce ConversionWithSameMimeTypeException for improved error handling in document conversion.
- Added the `ConversionWithSameMimeTypeException` to handle cases where document conversion is requested for the same MIME type.
- Updated `StoredObjectToPdfConverter` to throw the new exception when encountering such cases.
- Enhanced error logging in `PostSendExternalMessageHandler` to capture these specific conversion errors.
2025-10-03 13:57:06 +02:00
fcc9529a20 Add missing javascript dependency in package.json 2025-10-03 13:56:20 +02:00
955cb817c4 Release v4.5.0 2025-10-03 12:09:17 +02:00
823f9546b9 Merge branch '421-signature-fixes' into 'master'
Signature fixes

Closes #421

See merge request Chill-Projet/chill-bundles!887
2025-10-03 09:49:34 +00:00
be39fa16e7 Signature fixes 2025-10-03 09:49:33 +00:00
c8bb7575e7 Merge branch '426-increase_nb_chars_to_14_chill_password' into 'master'
#426 Increased the number of required characters when setting a new password in Chill

Closes #426

See merge request Chill-Projet/chill-bundles!883
2025-09-19 07:03:51 +00:00
juminet
80a3734171 #426 Increased the number of required characters when setting a new password in Chill 2025-09-19 07:03:51 +00:00
ab98f3a102 Release v4.4.2 2025-09-12 12:47:06 +02:00
7516e68d77 Merge branch 'fix/docgen-after-accp-work-refacto' into 'master'
Fix document generation and workflow generation do not work on accompanying period work documents

See merge request Chill-Projet/chill-bundles!880
2025-09-12 10:42:34 +00:00
7b60b7a8af Fix document generation and workflow generation do not work on accompanying period work documents 2025-09-12 10:42:34 +00:00
d984dec7db Release v4.4.1 2025-09-11 16:26:51 +02:00
46a4dedab8 Merge branch 'missing_commit_duplicate_evaluation' into 'master'
Fix translations and close button modal for duplicate evaluation document

See merge request Chill-Projet/chill-bundles!878
2025-09-11 14:21:05 +00:00
db98519e65 Fix translations and close button modal for duplicate evaluation document 2025-09-11 14:21:05 +00:00
c39637180a Release v4.4.0 2025-09-11 13:04:50 +02:00
15f9409bc8 Merge branch '369-duplicate-evaluation-document' into 'master'
Resolve "Dupliquer une document d'une évaluation vers une autre" + "Déplacer un document vers une autre évaluation"

Closes #369

See merge request Chill-Projet/chill-bundles!813
2025-09-11 11:01:16 +00:00
5b90d23367 Resolve "Dupliquer une document d'une évaluation vers une autre" + "Déplacer un document vers une autre évaluation" 2025-09-11 11:01:16 +00:00
c48625d1cd Merge branch 'bug/1607-the-user-preferences-for-notification-in-profile-are-not-shown-correctly' into 'master'
Resolve "user notification preferences are not displayed correctly"

See merge request Chill-Projet/chill-bundles!877
2025-09-10 16:28:45 +00:00
1195b54a68 Resolve "user notification preferences are not displayed correctly" 2025-09-10 16:28:45 +00:00
2a280b814f Refactor view templates: relocate 'merge' action block and standardize 'duplicate link' block handling 2025-09-09 17:36:46 +02:00
230c758255 Update bundles to v4.3.0 2025-09-08 16:05:09 +02:00
eafda987ae Merge branch '412-absence-enddate' into 'master'
Resolve "Absence user: add end date"

Closes #412

See merge request Chill-Projet/chill-bundles!865
2025-09-08 13:47:14 +00:00
7db8a371fc Resolve "Absence user: add end date" 2025-09-08 13:47:14 +00:00
0d0649dd31 Change route URL to avoid clash with person duplicate controller method 2025-09-08 14:51:54 +02:00
562 changed files with 27047 additions and 42833 deletions

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
kind: Feature
body: Add a command to generate a list of permissions
time: 2025-09-04T18:10:32.334524026+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Fixed
body: Fix the rendering of list of StoredObjectVersions, where there are kept version (before converting to pdf) and intermediate versions deleted
time: 2025-10-03T22:40:44.685474863+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Fixed
body: 'Notification: fix editing of sent notification by removing form.addressesEmails, a field that no longer exists'
time: 2025-10-06T12:13:15.45905994+02:00
custom:
Issue: "434"
SchemaChange: No schema change

View File

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

@@ -16,5 +16,5 @@
- ajout d'un filtre et regroupement par usager participant sur les échanges
- ajout d'un regroupement: par type d'activité associé au parcours;
- 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 centre dans les exports
- ajout d'un paramètre qui permet de désactiver le filtre par territoire dans les exports
- correction de l'interface de date dans les filtres et regroupements "par statut du parcours à la date"

View File

@@ -29,7 +29,7 @@
- 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 utilisateur intervenant sur un parcours
- ajout d'un regroupement "par centre de l'usager";
- ajout d'un regroupement "par territoire de l'usager";
- ajout d'un filtre "par métier 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);

10
.changes/v4.3.0.md Normal file
View File

@@ -0,0 +1,10 @@
## v4.3.0 - 2025-09-08
### Feature
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges
* Add a command to generate a list of permissions
* ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date
**Schema Change**: Add columns or tables
### Fixed
* fix date formatting in calendar range display
* Change route URL to avoid clash with person duplicate controller method

8
.changes/v4.4.0.md Normal file
View File

@@ -0,0 +1,8 @@
## v4.4.0 - 2025-09-11
### Feature
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works
### Fixed
* Fix display of 'duplicate' and 'merge' buttons in CRUD templates
* Fix saving notification preferences in user's profile

3
.changes/v4.4.1.md Normal file
View File

@@ -0,0 +1,3 @@
## v4.4.1 - 2025-09-11
### Fixed
* fix translations in duplicate evaluation document modal and realign close modal button

3
.changes/v4.4.2.md Normal file
View File

@@ -0,0 +1,3 @@
## v4.4.2 - 2025-09-12
### Fixed
* Fix document generation and workflow generation do not work on accompanying period work documents

13
.changes/v4.5.0.md Normal file
View File

@@ -0,0 +1,13 @@
## v4.5.0 - 2025-10-03
### Feature
* Only allow delete of attachment on workflows that are not final
* Move up signature buttons on index workflow page for easier access
* Filter out document from attachment list if it is the same as the workflow document
* Block edition on attached document on workflow, if the workflow is finalized or sent external
* Convert workflow's attached document to pdf while sending them external
* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition
### Fixed
* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance
* Fix permissions on storedObject which are subject by a workflow
### DX
* Introduce a WaitingScreen component to display a waiting screen

4
.changes/v4.5.1.md Normal file
View File

@@ -0,0 +1,4 @@
## v4.5.1 - 2025-10-03
### Fixed
* Add missing javascript dependency
* Add exception handling for conversion of attachment on sending external, when documens are already in pdf

View File

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

View File

@@ -234,9 +234,15 @@ This must be a decision made by a human, not by an AI. Every AI task must abort
#### Running Tests
The tests are run from the project's root (not from the bundle's root: so, do not change the directory to any bundle directory before running tests).
The tests are run from the project's root (not from the bundle's root).
```bash
# Run all tests
vendor/bin/phpunit
# Run tests for a specific bundle
vendor/bin/phpunit --testsuite NameBundle
# Run a specific test file
vendor/bin/phpunit path/to/TestFile.php

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,12 +6,59 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.5.1 - 2025-10-03
### Fixed
* Add missing javascript dependency
* Add exception handling for conversion of attachment on sending external, when documens are already in pdf
## v4.5.0 - 2025-10-03
### Feature
* Only allow delete of attachment on workflows that are not final
* Move up signature buttons on index workflow page for easier access
* Filter out document from attachment list if it is the same as the workflow document
* Block edition on attached document on workflow, if the workflow is finalized or sent external
* Convert workflow's attached document to pdf while sending them external
* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition
### Fixed
* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance
* Fix permissions on storedObject which are subject by a workflow
### DX
* Introduce a WaitingScreen component to display a waiting screen
## v4.4.2 - 2025-09-12
### Fixed
* Fix document generation and workflow generation do not work on accompanying period work documents
## v4.4.1 - 2025-09-11
### Fixed
* fix translations in duplicate evaluation document modal and realign close modal button
## v4.4.0 - 2025-09-11
### Feature
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works
### Fixed
* Fix display of 'duplicate' and 'merge' buttons in CRUD templates
* Fix saving notification preferences in user's profile
## v4.3.0 - 2025-09-08
### Feature
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges
* Add a command to generate a list of permissions
* ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date
**Schema Change**: Add columns or tables
### Fixed
* fix date formatting in calendar range display
* Change route URL to avoid clash with person duplicate controller method
## v4.2.1 - 2025-09-03
### Fixed
* Fix exports to work with DirectExportInterface
* Fix exports to work with DirectExportInterface
### DX
* Improve error message when a stored object cannot be written on local disk
## v4.2.0 - 2025-09-02
### Feature
@@ -26,26 +73,26 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.1.0 - 2025-08-26
### Feature
* ([#400](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/400)) Add filter to social actions list to filter out actions where current user intervenes
* ([#399](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/399)) Show filters on list pages unfolded by default
* Expansion of event module with new fields in the creation form: thematic, internal/external animator, responsable, and budget elements. Filtering options in the event list + adapted exports
* ([#400](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/400)) Add filter to social actions list to filter out actions where current user intervenes
* ([#399](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/399)) Show filters on list pages unfolded by default
* Expansion of event module with new fields in the creation form: thematic, internal/external animator, responsable, and budget elements. Filtering options in the event list + adapted exports
**Schema Change**: Add columns or tables
### Fixed
* ([#382](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/382)) adjust display logic for accompanying period dates, include closing date if period is closed.
* ([#384](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/384)) add min and step attributes to integer field in DateIntervalType
* ([#382](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/382)) adjust display logic for accompanying period dates, include closing date if period is closed.
* ([#384](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/384)) add min and step attributes to integer field in DateIntervalType
### UX
* Limit display of participations in event list
* Limit display of participations in event list
## v4.0.2 - 2025-07-09
### Fixed
* Fix add missing translation
* Fix the transfer of evaluations and documents during of accompanyingperiodwork
* Fix add missing translation
* Fix the transfer of evaluations and documents during of accompanyingperiodwork
## v4.0.1 - 2025-07-08
### Fixed
* Fix package.json for compilation
## v4.0.0 - 2025-07-08
### Feature
@@ -124,30 +171,30 @@ framework:
## v3.12.1 - 2025-06-30
### Fixed
* Fix loading of the list of documents
* Fix loading of the list of documents
## v3.12.0 - 2025-06-30
### Feature
* ([#377](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/377)) Add the document file name to the document title when a user upload a document, unless there is already a document title.
* Add desactivation date for social action and issue csv export
* Add Emoji and Fullscreen feature to ckeditor configuration
* ([#321](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/321)) Create editor which allow us to toggle between rich and simple text editor
* Do not remove workflow which are automatically canceled after staling for more than 30 days
* ([#377](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/377)) Add the document file name to the document title when a user upload a document, unless there is already a document title.
* Add desactivation date for social action and issue csv export
* Add Emoji and Fullscreen feature to ckeditor configuration
* ([#321](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/321)) Create editor which allow us to toggle between rich and simple text editor
* Do not remove workflow which are automatically canceled after staling for more than 30 days
### Fixed
* ([#376](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/376)) trying to prevent bug of typeerror in doc-history + improved display of document history
* ([#381](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/381)) Display previous participation in acc course work even if the person has left the acc course
* ([#372](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/372)) Fix display of text in calendar events
* Add missing translation for user_group.no_user_groups
* Fix admin entity edit actions for event admin entities and activity reason (category) entities
* ([#376](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/376)) trying to prevent bug of typeerror in doc-history + improved display of document history
* ([#381](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/381)) Display previous participation in acc course work even if the person has left the acc course
* ([#372](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/372)) Fix display of text in calendar events
* Add missing translation for user_group.no_user_groups
* Fix admin entity edit actions for event admin entities and activity reason (category) entities
* ([#392](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/392)) Allow null and cast as string to setContent method for NewsItem
* ([#393](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/393)) Doc Generation: the "dump only" method send the document as an email attachment.
* ([#393](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/393)) Doc Generation: the "dump only" method send the document as an email attachment.
### DX
* ([#352](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/352)) Remove dead code for wopi-link module
* Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)
* ([#352](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/352)) Remove dead code for wopi-link module
* Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)
### UX
* ([#374](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/374)) Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
* Improve labeling of fields in person resource creation form
* ([#374](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/374)) Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
* Improve labeling of fields in person resource creation form
## v3.11.0 - 2025-04-17
### Feature
@@ -171,11 +218,11 @@ framework:
## v3.10.3 - 2025-03-18
### DX
* Eslint fixes
* Eslint fixes
## v3.10.2 - 2025-03-17
### Fixed
* Replace a ts-expect-error with a ts-ignore
* Replace a ts-expect-error with a ts-ignore
## v3.10.1 - 2025-03-17
### DX
@@ -183,37 +230,37 @@ framework:
## v3.10.0 - 2025-03-17
### Feature
* ([#363](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/363)) Display social actions grouped per social issue within activity form
* ([#363](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/363)) Display social actions grouped per social issue within activity form
### Fixed
* ([#362](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/362)) Fix Dependency Injection, which prevented to save the CalendarRange
* ([#368](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/368)) fix search query for user groups
* ([#362](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/362)) Fix Dependency Injection, which prevented to save the CalendarRange
* ([#368](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/368)) fix search query for user groups
## v3.9.2 - 2025-02-27
### Fixed
* Use fetchResults method to fetch all social issues instead of only the first page
* Use fetchResults method to fetch all social issues instead of only the first page
## v3.9.1 - 2025-02-27
### Fixed
* Fix post/patch request with missing 'type' property for gender
* Fix post/patch request with missing 'type' property for gender
## v3.9.0 - 2025-02-27
### Feature
* ([#349](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/349)) Suggest all referrers within actions of the accompanying period when creating an activity
* ([#343](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/343)) Add possibility to export a csv with all social issues and social actions
* ([#360](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/360)) Restore document to previous kept version when a workflow is canceled
* ([#341](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/341)) Add a list of third parties from within the admin (csv download)
* ([#349](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/349)) Suggest all referrers within actions of the accompanying period when creating an activity
* ([#343](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/343)) Add possibility to export a csv with all social issues and social actions
* ([#360](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/360)) Restore document to previous kept version when a workflow is canceled
* ([#341](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/341)) Add a list of third parties from within the admin (csv download)
### Fixed
* fix generation of document with accompanying period context, and list of activities and works
* fix generation of document with accompanying period context, and list of activities and works
### DX
* ([#333](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/333)) Create an unique source of trust for translations
* ([#333](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/333)) Create an unique source of trust for translations
## v3.8.2 - 2025-02-10
### Fixed
* ([#358](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/358)) Remove "filter" button on list of documents in the workflow's "add attachement" modal
* ([#358](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/358)) Remove "filter" button on list of documents in the workflow's "add attachement" modal
## v3.8.1 - 2025-02-05
### Fixed
* Fix household link in the parcours banner
* Fix household link in the parcours banner
## v3.8.0 - 2025-02-03
### Feature
@@ -229,7 +276,7 @@ framework:
## v3.7.1 - 2025-01-21
### Fixed
* Fix legacy configuration processor for notifier component
* Fix legacy configuration processor for notifier component
## v3.7.0 - 2025-01-21
### Feature
@@ -296,33 +343,33 @@ chill_main:
## v3.6.0 - 2025-01-16
### Feature
* Importer for addresses does not fails when the postal code is not found with some addresses, and compute a recap list of all addresses that could not be imported. This recap list can be send by email.
* Importer for addresses does not fails when the postal code is not found with some addresses, and compute a recap list of all addresses that could not be imported. This recap list can be send by email.
* ([#346](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/346)) Create a driver for storing documents on disk (instead of openstack object store)
* Add address importer from french Base d'Adresse Nationale (BAN)
* ([#343](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/343)) Add csv export for social issues and social actions
* Add address importer from french Base d'Adresse Nationale (BAN)
* ([#343](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/343)) Add csv export for social issues and social actions
### Fixed
* Export: fix missing alias in activity between certain dates filter. Condition added for alias.
* Export: fix missing alias in activity between certain dates filter. Condition added for alias.
## v3.5.3 - 2025-01-07
### Fixed
* Fix the EntityToJsonTransformer to return an empty array if the value is ""
* Fix the EntityToJsonTransformer to return an empty array if the value is ""
## v3.5.2 - 2024-12-19
### Fixed
* ([#345](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/345)) Export: activity filtering of users that were associated to an activity between certain dates. Results contained activities that were not within the specified date range"
* ([#345](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/345)) Export: activity filtering of users that were associated to an activity between certain dates. Results contained activities that were not within the specified date range"
## v3.5.1 - 2024-12-16
### Fixed
* Filiation: fix the display of the gender label in the graph
* Wrap handling of PdfSignedMessage into transactions
* Filiation: fix the display of the gender label in the graph
* Wrap handling of PdfSignedMessage into transactions
## v3.5.0 - 2024-12-09
### Feature
* ([#318](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/318)) Show all the pages of the documents in the signature app
* ([#318](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/318)) Show all the pages of the documents in the signature app
### Fixed
* Wrap the signature's change state into a transaction, to avoid race conditions
* Fix display of gender label
* Wrap the signature's change state into a transaction, to avoid race conditions
* Fix display of gender label
## v3.4.3 - 2024-12-05
### Fixed
@@ -331,76 +378,76 @@ chill_main:
## v3.4.2 - 2024-12-05
### Fixed
* ([#329](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/329)) Fix the serialization of gender for the generation of documents
* ([#337](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/337)) Enforce unique contraint on activity storedobject
* ([#329](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/329)) Fix the serialization of gender for the generation of documents
* ([#337](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/337)) Enforce unique contraint on activity storedobject
### DX
* ([#310](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/310)) Clean migrations, to reduce the number of bloated migration when running diff on schema
* ([#310](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/310)) Clean migrations, to reduce the number of bloated migration when running diff on schema
## v3.4.1 - 2024-11-22
### Fixed
* Set the workflow's title to notification content and subject
* Set the workflow's title to notification content and subject
## v3.4.0 - 2024-11-20
### Feature
* ([#314](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/314)) Admin: improve document type admin form with a select field for related class.
Admin: Allow administrator to assign multiple group centers in one go to a user.
Admin: Allow administrator to assign multiple group centers in one go to a user.
## v3.3.0 - 2024-11-20
### Feature
* Electronic signature
Implementation of the electronic signature for documents within chill.
* ([#286](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/286)) The behavoir of the voters for stored objects is adjusted so as to limit edit and delete possibilities to users related to the activity, social action or workflow entity.
* ([#288](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/288)) Metadata form added for person signatures
* Add a signature step in workflow, which allow to apply an electronic signature on documents
* Keep an history of each version of a stored object.
* Add a "send external" step in workflow, which allow to send stored objects and other elements to remote people, by sending them a public url
Implementation of the electronic signature for documents within chill.
* ([#286](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/286)) The behavoir of the voters for stored objects is adjusted so as to limit edit and delete possibilities to users related to the activity, social action or workflow entity.
* ([#288](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/288)) Metadata form added for person signatures
* Add a signature step in workflow, which allow to apply an electronic signature on documents
* Keep an history of each version of a stored object.
* Add a "send external" step in workflow, which allow to send stored objects and other elements to remote people, by sending them a public url
### Fixed
* Adjust household list export to include households even if their address is NULL
* Remove validation of date string on deathDate
* Adjust household list export to include households even if their address is NULL
* Remove validation of date string on deathDate
## v3.2.4 - 2024-11-06
### Fixed
* Fix compilation of chill assets
* Fix compilation of chill assets
## v3.2.3 - 2024-11-05
### Fixed
* ([#315](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/315)) Fix display of accompanying period work referrers. Only current referrers should be displayed.
Fix color of Chill footer
Fix color of Chill footer
## v3.2.2 - 2024-10-31
### Fixed
* Fix gender translation for unknown
* Fix gender translation for unknown
## v3.2.1 - 2024-10-31
### Fixed
* Add the possibility of unknown to the gender entity
* Fix the fusion of person doubles by excluding accompanyingPeriod work entities to be deleted. They are moved instead.
* Add the possibility of unknown to the gender entity
* Fix the fusion of person doubles by excluding accompanyingPeriod work entities to be deleted. They are moved instead.
## v3.2.0 - 2024-10-30
### Feature
* Introduce a gender entity
* Introduce a gender entity
## v3.1.1 - 2024-10-01
### Fixed
* ([#308](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/308)) Show only the current referrer in the page "show" for an accompanying period workf
* ([#308](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/308)) Show only the current referrer in the page "show" for an accompanying period workf
* ([#309](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/309)) Correctly compute the grouping by referrer aggregator
* Fixed typing of custom field long choice and custom field group
* Fixed typing of custom field long choice and custom field group
## v3.1.0 - 2024-08-30
### Feature
* Add export aggregator to aggregate activities by household + filter persons that are not part of an accompanyingperiod during a certain timeframe.
* Add export aggregator to aggregate activities by household + filter persons that are not part of an accompanyingperiod during a certain timeframe.
## v3.0.0 - 2024-08-26
### Fixed
* Fix delete action for accompanying periods in draft state
* Fix connection to azure when making an calendar event in chill
* CollectionType js fixes for remove button and adding multiple entries
* Fix delete action for accompanying periods in draft state
* Fix connection to azure when making an calendar event in chill
* CollectionType js fixes for remove button and adding multiple entries
## v2.24.0 - 2024-09-11
### Feature
* ([#306](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/306)) When a document is converted or downloaded in the browser, this document is removed from the browser memory after 45s. Future click on the button re-download the document.
* ([#306](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/306)) When a document is converted or downloaded in the browser, this document is removed from the browser memory after 45s. Future click on the button re-download the document.
## v2.23.0 - 2024-07-23 & 2024-07-19
### Feature
@@ -435,13 +482,13 @@ Fix color of Chill footer
## v2.22.2 - 2024-07-03
### Fixed
* Remove scope required for event participation stats
* Remove scope required for event participation stats
## v2.22.1 - 2024-07-01
### Fixed
* Remove debug word
* Remove debug word
### DX
* Add a command for reading official address DB from Luxembourg and update chill addresses
* Add a command for reading official address DB from Luxembourg and update chill addresses
## v2.22.0 - 2024-06-25
### Feature
@@ -484,7 +531,7 @@ Fix color of Chill footer
## v2.20.1 - 2024-06-05
### Fixed
* Do not allow StoredObjectCreated for edit and convert buttons
* Do not allow StoredObjectCreated for edit and convert buttons
## v2.20.0 - 2024-06-05
### Fixed
@@ -531,96 +578,96 @@ Fix color of Chill footer
## v2.18.2 - 2024-04-12
### Fixed
* ([#250](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/250)) Postal codes import : fix the source URL and the keys to handle each record
* ([#250](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/250)) Postal codes import : fix the source URL and the keys to handle each record
## v2.18.1 - 2024-03-26
### Fixed
* Fix layout issue in document generation for admin (minor)
* Fix layout issue in document generation for admin (minor)
## v2.18.0 - 2024-03-26
### Feature
* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation
* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation
### Fixed
* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job
* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job
## v2.17.0 - 2024-03-19
### Feature
* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates
* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course
* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields
* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates
* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course
* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields
* ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage
### Fixed
* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill
* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period
* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill
* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period
## v2.16.3 - 2024-02-26
### Fixed
* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'
* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'
### UX
* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters
* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters
## v2.16.2 - 2024-02-21
### Fixed
* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template
* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template
## v2.16.1 - 2024-02-09
### Fixed
* Force bootstrap version to avoid error in builds with newer version
* Force bootstrap version to avoid error in builds with newer version
## v2.16.0 - 2024-02-08
### Feature
* ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span
* ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids
* ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this
* ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating
* ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address
* ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason
* ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work
* Modernize the event bundle, with some new fields and multiple improvements
* ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span
* ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids
* ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this
* ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating
* ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address
* ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason
* ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work
* Modernize the event bundle, with some new fields and multiple improvements
### Fixed
* ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method
* ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form
* ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method
* ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form
### UX
* ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin.
* ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin.
## v2.15.2 - 2024-01-11
### Fixed
* Fix the id_seq used when creating a new accompanying period participation during fusion of two person files
* Fix the id_seq used when creating a new accompanying period participation during fusion of two person files
### DX
* Set placeholder to False for expanded EntityType form fields where required is set to False.
* Set placeholder to False for expanded EntityType form fields where required is set to False.
## v2.15.1 - 2023-12-20
### Fixed
* Fix the household export query to exclude accompanying periods that are in draft state.
* Fix the household export query to exclude accompanying periods that are in draft state.
### DX
* ([#167](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/167)) Fixed readthedocs compilation by updating readthedocs config file and requirements for Sphinx
* ([#167](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/167)) Fixed readthedocs compilation by updating readthedocs config file and requirements for Sphinx
## v2.15.0 - 2023-12-11
### Feature
* ([#191](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/191)) Add export "number of household associate with an exchange"
* ([#235](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/235)) Export: add dates on the filter "filter course by activity type"
* ([#191](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/191)) Add export "number of household associate with an exchange"
* ([#235](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/235)) Export: add dates on the filter "filter course by activity type"
### Fixed
* ([#214](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/214)) Fix error when posting an empty comment on an accompanying period.
* ([#233](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/233)) Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
* ([#214](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/214)) Fix error when posting an empty comment on an accompanying period.
* ([#233](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/233)) Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
* ([#234](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/234)) Fix "filter aside activity by date"
* ([#228](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/228)) Fix export of activity for people created before the introduction of the createdAt column on person (during v1)
* ([#246](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/246)) Do not show activities, evaluations and social work when associated to a confidential accompanying period, except for the users which are allowed to see them
* ([#228](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/228)) Fix export of activity for people created before the introduction of the createdAt column on person (during v1)
* ([#246](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/246)) Do not show activities, evaluations and social work when associated to a confidential accompanying period, except for the users which are allowed to see them
## v2.14.1 - 2023-11-29
### Fixed
* Export: fix list person with custom fields
* ([#100](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/100)) Add a paginator to budget elements (resource and charge types) in the admin
* Fix error in ListEvaluation when "handling agents" are alone
* Export: fix list person with custom fields
* ([#100](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/100)) Add a paginator to budget elements (resource and charge types) in the admin
* Fix error in ListEvaluation when "handling agents" are alone
## v2.14.0 - 2023-11-24
### Feature
* ([#161](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/161)) Export: in filter "Filter accompanying period work (social action) by type, goal and result", order the items alphabetically or with the defined order
* ([#161](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/161)) Export: in filter "Filter accompanying period work (social action) by type, goal and result", order the items alphabetically or with the defined order
### Fixed
* ([#141](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/141)) Export: on filter "action by type goals, and results", restore the fields when editing a saved export
* ([#219](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/219)) Export: fix the list of accompanying period work, when the "calc date" is null
* ([#222](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/222)) Fix rendering of custom fields
* Fix various errors in custom fields administration
* ([#141](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/141)) Export: on filter "action by type goals, and results", restore the fields when editing a saved export
* ([#219](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/219)) Export: fix the list of accompanying period work, when the "calc date" is null
* ([#222](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/222)) Fix rendering of custom fields
* Fix various errors in custom fields administration
## v2.13.0 - 2023-11-21
### Feature
@@ -634,7 +681,7 @@ Fix color of Chill footer
## v2.12.1 - 2023-11-16
### Fixed
* ([#208](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/208)) Export: fix loading of form for "filter action by type, goal and result"
* ([#208](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/208)) Export: fix loading of form for "filter action by type, goal and result"
## v2.12.0 - 2023-11-15
### Feature
@@ -665,36 +712,36 @@ Fix color of Chill footer
## v2.11.0 - 2023-11-07
### Feature
* ([#194](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/194)) Export: add a filter "filter activity by creator job"
* ([#194](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/194)) Export: add a filter "filter activity by creator job"
### Fixed
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix "group accompanying period by geographical unit": take into account the accompanying periods when the period is not located within an unit
* Fix "group activity by creator job" aggregator
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix "group accompanying period by geographical unit": take into account the accompanying periods when the period is not located within an unit
* Fix "group activity by creator job" aggregator
## v2.10.6 - 2023-11-07
### Fixed
* ([#182](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/182)) Fix merging of double person files. Adjustement relationship sql statement
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix aggregator by geographical unit on person: avoid inconsistencies
* ([#182](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/182)) Fix merging of double person files. Adjustement relationship sql statement
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix aggregator by geographical unit on person: avoid inconsistencies
## v2.10.5 - 2023-11-05
### Fixed
* ([#183](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/183)) Fix "problem during download" on some filters, which used a wrong data type
* ([#184](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/184)) Fix filter "activity by date"
* ([#183](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/183)) Fix "problem during download" on some filters, which used a wrong data type
* ([#184](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/184)) Fix filter "activity by date"
## v2.10.4 - 2023-10-26
### Fixed
* Fix null value constraint errors when merging relationships in doubles
* Fix null value constraint errors when merging relationships in doubles
## v2.10.3 - 2023-10-26
### Fixed
* ([#175](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/175)) Replace old method of getting translator with injection of translatorInterface
* ([#175](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/175)) Replace old method of getting translator with injection of translatorInterface
## v2.10.2 - 2023-10-26
### Fixed
* ([#175](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/175)) Use injection of translator instead of ->get().
* ([#175](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/175)) Use injection of translator instead of ->get().
## v2.10.1 - 2023-10-24
### Fixed
* Fix export controller when generating an export without any data in session
* Fix export controller when generating an export without any data in session
## v2.10.0 - 2023-10-24
### Feature
@@ -714,16 +761,16 @@ Fix color of Chill footer
- ajout d'un filtre et regroupement par usager participant sur les échanges
- ajout d'un regroupement: par type d'activité associé au parcours;
- 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 centre dans les exports
- ajout d'un paramètre qui permet de désactiver le filtre par territoire dans les exports
- correction de l'interface de date dans les filtres et regroupements "par statut du parcours à la date"
## v2.9.2 - 2023-10-17
### Fixed
* Fix possible null values in string's entities
* Fix possible null values in string's entities
## v2.9.1 - 2023-10-17
### Fixed
* Fix the handling of activity form when editing or creating an activity in an accompanying period with multiple centers
* Fix the handling of activity form when editing or creating an activity in an accompanying period with multiple centers
## v2.9.0 - 2023-10-17
### Feature
@@ -771,57 +818,57 @@ But if you do not need this any more, you must ensure that the configuration key
## v2.7.0 - 2023-09-27
### Feature
* ([#155](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/155)) The regulation list load accompanying periods by exact postal code (address associated with postal code), and not by the content of the postal code (postal code with same code's string)
* ([#155](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/155)) The regulation list load accompanying periods by exact postal code (address associated with postal code), and not by the content of the postal code (postal code with same code's string)
### Fixed
* ([#142](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/142)) Fix the label of filter ActivityTypeFilter to a more obvious one
* ([#140](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/140)) [export] Fix association of filter "filter location by type" which did not appears on "list of activities"
* ([#142](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/142)) Fix the label of filter ActivityTypeFilter to a more obvious one
* ([#140](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/140)) [export] Fix association of filter "filter location by type" which did not appears on "list of activities"
## v2.6.3 - 2023-09-19
### Fixed
* Remove id property from document
mappedsuperclass
* Remove id property from document
mappedsuperclass
## v2.6.2 - 2023-09-18
### Fixed
* Fix doctrine mapping of AbstractTaskPlaceEvent and SingleTaskPlaceEvent: id property moved.
* Fix doctrine mapping of AbstractTaskPlaceEvent and SingleTaskPlaceEvent: id property moved.
## v2.6.1 - 2023-09-14
### Fixed
* Filter out active centers in exports, which uses a different PickCenterType.
* Filter out active centers in exports, which uses a different PickCenterType.
## v2.6.0 - 2023-09-14
### Feature
* ([#133](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/133)) Add locations in Aside Activity. By default, suggest user location, otherwise a select with all locations.
* ([#133](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/133)) Adapt Aside Activity exports: display location, filter by location, group by location
* Use the CRUD controller for center entity + add the isActive property to be able to mask instances of Center that are no longer in use.
* ([#133](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/133)) Add locations in Aside Activity. By default, suggest user location, otherwise a select with all locations.
* ([#133](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/133)) Adapt Aside Activity exports: display location, filter by location, group by location
* Use the CRUD controller for center entity + add the isActive property to be able to mask instances of Center that are no longer in use.
### Fixed
* ([#107](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/107)) reinstate the fusion of duplicate persons
* Missing translation in Work Actions exports
* Reimplement the mission type filter on tasks, only for instances that have a config parameter indicating true for this.
* ([#135](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/135)) Corrects a typing error in 2 filters, which caused an
* ([#107](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/107)) reinstate the fusion of duplicate persons
* Missing translation in Work Actions exports
* Reimplement the mission type filter on tasks, only for instances that have a config parameter indicating true for this.
* ([#135](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/135)) Corrects a typing error in 2 filters, which caused an
error when trying to reedit a saved export
* ([#136](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/136)) [household] when moving a person to a sharing position to a not-sharing position on the same household on the same date, remove the previous household membership on the same household. This fix duplicate member.
* ([#136](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/136)) [household] when moving a person to a sharing position to a not-sharing position on the same household on the same date, remove the previous household membership on the same household. This fix duplicate member.
* Add missing translation for comment field placeholder in repositionning household editor.
* Do not send an email to creator twice when adding a comment to a notification
* ([#107](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/107)) Fix gestion doublon functionality to work with chill bundles v2
* Do not send an email to creator twice when adding a comment to a notification
* ([#107](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/107)) Fix gestion doublon functionality to work with chill bundles v2
### UX
* Uniformize badge-person in household banner (background, size)
## v2.5.3 - 2023-07-20
### Fixed
* ([#132](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/132)) Rendez-vous documents created would appear in all documents lists of all persons with an accompanying period. Or statements are now added to the where clause to filter out documents that come from unrelated accompanying period/ or person rendez-vous.
* ([#132](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/132)) Rendez-vous documents created would appear in all documents lists of all persons with an accompanying period. Or statements are now added to the where clause to filter out documents that come from unrelated accompanying period/ or person rendez-vous.
## v2.5.2 - 2023-07-15
### Fixed
* [Collate Address] when updating address point, do not use the point's address reference if the similarity is below the requirement for associating the address reference and the address (it uses the postcode's center instead)
* [Collate Address] when updating address point, do not use the point's address reference if the similarity is below the requirement for associating the address reference and the address (it uses the postcode's center instead)
## v2.5.1 - 2023-07-14
### Fixed
* [collate addresses] block collating addresses to another address reference where the address reference is already the best match
* [collate addresses] block collating addresses to another address reference where the address reference is already the best match
## v2.5.0 - 2023-07-14
### Feature
@@ -894,7 +941,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 service des intervenants sur un parcours;
- ajout d'un regroupement par utilisateur intervenant sur un parcours
- ajout d'un regroupement "par centre de l'usager";
- ajout d'un regroupement "par territoire de l'usager";
- ajout d'un filtre "par métier 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);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
chill_doc_store:
use_driver: local_storage
use_driver: openstack
local_storage:
storage_path: '%kernel.project_dir%/var/storage'
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\Budget': '@ChillBudgetBundle/migrations'
'Chill\Migrations\Report': '@ChillReportBundle/migrations'
'Chill\Migrations\Ticket': '@ChillTicketBundle/migrations'
all_or_nothing:
true

View File

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

View File

@@ -23,8 +23,8 @@ class "Document" {
- text description
- ArrayCollection_DocumentCategory categories
- varchar_150 content #link to openstack
- Center center
- Cercle cercle
- Territoire territoire
- Service service
- User user
- DateTime date # Creation date
}

View File

@@ -11,94 +11,24 @@
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::
This part of the doc is not yet tested
Create a new directory with Bundle class
----------------------------------------
.. code-block:: bash
mkdir -p src/Bundle/ChillSomeBundle/src/config
mkdir -p src/Bundle/ChillSomeBundle/src/Controller
Add a bundle file
.. code-block:: php
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\SomeBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ChillSomeBundle extends Bundle {}
And a route file:
.. code-block:: yaml
chill_ticket_controller:
resource: '@ChillTicketBundle/Controller/'
type: annotation
Register the new psr-4 namespace
--------------------------------
In composer.json, add the new psr4 namespace
.. code-block:: diff
{
"autoload": {
"psr-4": {
+ "Chill\\SomeBundle\\": "src/Bundle/ChillSomeBundle/src",
}
}
}
TODO
Register the bundle
-------------------
Register in the file :code:`config/bundles.php`:
.. code-block:: php
Vendor\Bundle\YourBundle\YourBundle::class => ['all' => true],
And import routes in :code:`config/routes/chill_some_bundle.yaml`:
.. code-block:: yaml
chill_ticket_bundle:
resource: '@ChillSomeBundle/config/routes.yaml'
Add the doctrine_migration namespace
------------------------------------
Add the namespace to :code:`config/packages/doctrine_migrations_chill.yaml`
.. code-block:: diff
doctrine_migrations:
migrations_paths:
+ 'Chill\Some\Ticket': '@ChillSomeBundle/migrations'
Dump autoloading
----------------
.. code-block:: bash
symfony composer dump-autoload
.. rubric:: Footnotes
.. [#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

@@ -38,7 +38,7 @@ Certaines données sont historisées:
- les référents d'un parcours;
- les statuts d'un parcours;
- la liaison entre les centres et les usagers;
- la liaison entre les territoires et les usagers;
- etc.
Dans ces cas-là, Chill crée généralement deux colonnes, qui sont habituellement nommées :code:`startDate` et :code:`endDate`. Lorsque la colonne :code:`endDate` est à :code:`NULL`, cela signifie que la période n'est pas "fermée". La colonne :code:`startDate` n'est pas nullable.

View File

@@ -1,6 +1,6 @@
order,table_schema,table_name,commentaire
1,chill_3party,party_category,Catégorie de tiers
2,chill_3party,party_center,Association entre les tiers et les centres (déprécié)
2,chill_3party,party_center,Association entre les tiers et les territoires (déprécié)
3,chill_3party,party_profession,Profession du tiers (déprécié)
4,chill_3party,third_party,Tiers
5,chill_3party,thirdparty_category,association tiers - catégories
@@ -54,7 +54,7 @@ order,table_schema,table_name,commentaire
53,public,activitytpresence,Présence aux échanges
54,public,activitytype,Types d'échanges
55,public,activitytypecategory,Catégories de types d'échanges
56,public,centers,"Centres (territoires, agences, etc.)"
56,public,centers,"Territoires (territoires, agences, etc.)"
57,public,chill_activity_activity_chill_person_socialaction,
58,public,chill_activity_activity_chill_person_socialissue
59,public,chill_docgen_template,Gabarits de documents
@@ -111,7 +111,7 @@ order,table_schema,table_name,commentaire
110,public,chill_person_marital_status,Etats civils
111,public,chill_person_not_duplicate,
112,public,chill_person_person,Usagers
113,public,chill_person_person_center_history,Historique des centres d'un usagers
113,public,chill_person_person_center_history,Historique des territoires d'un usagers
114,public,chill_person_persons_to_addresses,Déprécié
115,public,chill_person_phone,Numéros d etéléphone supplémentaires d'un usager
116,public,chill_person_relations,Types de relations de filiation
@@ -142,7 +142,7 @@ order,table_schema,table_name,commentaire
141,public,permission_groups
142,public,permissionsgroup_rolescope
143,public,persons_spoken_languages
144,public,regroupment,Regroupement de centres
144,public,regroupment,Regroupement de territoires
145,public,regroupment_center,
146,public,role_scopes,
147,public,scopes,Services
Can't render this file because it has a wrong number of fields in line 28.

View File

@@ -55,6 +55,7 @@
"@tsconfig/node20": "^20.1.4",
"@types/dompurify": "^3.0.5",
"@types/leaflet": "^1.9.3",
"@vueuse/core": "^13.9.0",
"bootstrap-icons": "^1.11.3",
"dropzone": "^5.7.6",
"es6-promise": "^4.2.8",
@@ -79,12 +80,12 @@
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress",
"specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml 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-create-dir": "mkdir -p templates/api",
"specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate",
"version": "node --version",
"eslint": "eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
"eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
},
"private": true
}

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ Attendee: Présence de l'usager
attendee: présence de l'usager
list_reasons: liste des sujets
user_username: nom de l'utilisateur
circle_name: nom du cercle
circle_name: nom du service
Remark: Commentaire
No comments: Aucun commentaire
Add a new activity: Ajouter une nouvel échange
@@ -20,7 +20,7 @@ not present: absent
Delete: Supprimer
Update: Mettre à jour
Update activity: Modifier l'échange
Scope: Cercle
Scope: Service
Activity data: Données de l'échange
Activity location: Localisation de l'échange
No reason associated: Aucun sujet
@@ -398,7 +398,7 @@ export:
sent received: Envoyé ou reçu
emergency: Urgence
accompanying course id: Identifiant du parcours
course circles: Cercles du parcours
course circles: Services du parcours
travelTime: Durée de déplacement
durationTime: Durée
id: Identifiant

View File

@@ -55,5 +55,6 @@
</dl>
{% endblock %}
{% block content_view_actions_duplicate_link %}{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -177,7 +177,7 @@ export:
agent_id: Utilisateur
creator_id: Créateur
main_scope: Service principal de l'utilisateur
main_center: Centre principal de l'utilisateur
main_center: Territoire principal de l'utilisateur
aside_activity_type: Catégorie d'activité annexe
date: Date
duration: Durée

View File

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

View File

@@ -1,146 +1,166 @@
<template>
<teleport to="#mainUser">
<h2 class="chill-red">Utilisateur principal</h2>
<div>
<div>
<div v-if="null !== this.$store.getters.getMainUser">
<calendar-active :user="this.$store.getters.getMainUser" />
<teleport to="#mainUser">
<h2 class="chill-red">Utilisateur principal</h2>
<div>
<div>
<div v-if="null !== this.$store.getters.getMainUser">
<calendar-active :user="this.$store.getters.getMainUser" />
</div>
<pick-entity
:multiple="false"
:types="['user']"
:uniqid="'main_user_calendar'"
:picked="
null !== this.$store.getters.getMainUser
? [this.$store.getters.getMainUser]
: []
"
:removable-if-set="false"
:display-picked="false"
:suggested="this.suggestedUsers"
:label="'main_user'"
@add-new-entity="setMainUser"
/>
</div>
</div>
<pick-entity
:multiple="false"
:types="['user']"
:uniqid="'main_user_calendar'"
:picked="
null !== this.$store.getters.getMainUser
? [this.$store.getters.getMainUser]
: []
"
:removable-if-set="false"
:display-picked="false"
:suggested="this.suggestedUsers"
:label="'main_user'"
@add-new-entity="setMainUser"
/>
</div>
</div>
</teleport>
</teleport>
<concerned-groups />
<concerned-groups />
<teleport to="#schedule">
<div class="row mb-3" v-if="activity.startDate !== null">
<label class="col-form-label col-sm-4">Date</label>
<div class="col-sm-8">
{{ $d(activity.startDate, "long") }} -
{{ $d(activity.endDate, "hoursOnly") }}
<span v-if="activity.calendarRange === null"
>(Pas de plage de disponibilité sélectionnée)</span
<teleport to="#schedule">
<div class="row mb-3" v-if="activity.startDate !== null">
<label class="col-form-label col-sm-4">Date</label>
<div class="col-sm-8">
{{ $d(activity.startDate, "long") }} -
{{ $d(activity.endDate, "hoursOnly") }}
<span v-if="activity.calendarRange === null"
>(Pas de plage de disponibilité sélectionnée)</span
>
<span v-else>(Une plage de disponibilité sélectionnée)</span>
</div>
</div>
</teleport>
<location />
<teleport to="#fullCalendar">
<div class="calendar-actives">
<template v-for="u in getActiveUsers" :key="u.id">
<calendar-active
:user="u"
:invite="this.$store.getters.getInviteForUser(u)"
/>
</template>
</div>
<div
class="display-options row justify-content-between"
style="margin-top: 1rem"
>
<span v-else>(Une plage de disponibilité sélectionnée)</span>
</div>
</div>
</teleport>
<location />
<teleport to="#fullCalendar">
<div class="calendar-actives">
<template v-for="u in getActiveUsers" :key="u.id">
<calendar-active
:user="u"
:invite="this.$store.getters.getInviteForUser(u)"
/>
</template>
</div>
<div
class="display-options row justify-content-between"
style="margin-top: 1rem"
>
<div class="col-sm-9 col-xs-12">
<div class="input-group mb-3">
<label class="input-group-text" for="slotDuration"
>Durée des créneaux</label
>
<select v-model="slotDuration" id="slotDuration" class="form-select">
<option value="00:05:00">5 minutes</option>
<option value="00:10:00">10 minutes</option>
<option value="00:15:00">15 minutes</option>
<option value="00:30:00">30 minutes</option>
</select>
<label class="input-group-text" for="slotMinTime">De</label>
<select v-model="slotMinTime" id="slotMinTime" class="form-select">
<option value="00:00:00">0h</option>
<option value="01:00:00">1h</option>
<option value="02:00:00">2h</option>
<option value="03:00:00">3h</option>
<option value="04:00:00">4h</option>
<option value="05:00:00">5h</option>
<option value="06:00:00">6h</option>
<option value="07:00:00">7h</option>
<option value="08:00:00">8h</option>
<option value="09:00:00">9h</option>
<option value="10:00:00">10h</option>
<option value="11:00:00">11h</option>
<option value="12:00:00">12h</option>
</select>
<label class="input-group-text" for="slotMaxTime">À</label>
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select">
<option value="12:00:00">12h</option>
<option value="13:00:00">13h</option>
<option value="14:00:00">14h</option>
<option value="15:00:00">15h</option>
<option value="16:00:00">16h</option>
<option value="17:00:00">17h</option>
<option value="18:00:00">18h</option>
<option value="19:00:00">19h</option>
<option value="20:00:00">20h</option>
<option value="21:00:00">21h</option>
<option value="22:00:00">22h</option>
<option value="23:00:00">23h</option>
<option value="23:59:59">24h</option>
</select>
<div class="col-sm-9 col-xs-12">
<div class="input-group mb-3">
<label class="input-group-text" for="slotDuration"
>Durée des créneaux</label
>
<select
v-model="slotDuration"
id="slotDuration"
class="form-select"
>
<option value="00:05:00">5 minutes</option>
<option value="00:10:00">10 minutes</option>
<option value="00:15:00">15 minutes</option>
<option value="00:30:00">30 minutes</option>
<option value="00:45:00">45 minutes</option>
<option value="00:60:00">60 minutes</option>
</select>
<label class="input-group-text" for="slotMinTime">De</label>
<select
v-model="slotMinTime"
id="slotMinTime"
class="form-select"
>
<option value="00:00:00">0h</option>
<option value="01:00:00">1h</option>
<option value="02:00:00">2h</option>
<option value="03:00:00">3h</option>
<option value="04:00:00">4h</option>
<option value="05:00:00">5h</option>
<option value="06:00:00">6h</option>
<option value="07:00:00">7h</option>
<option value="08:00:00">8h</option>
<option value="09:00:00">9h</option>
<option value="10:00:00">10h</option>
<option value="11:00:00">11h</option>
<option value="12:00:00">12h</option>
</select>
<label class="input-group-text" for="slotMaxTime">À</label>
<select
v-model="slotMaxTime"
id="slotMaxTime"
class="form-select"
>
<option value="12:00:00">12h</option>
<option value="13:00:00">13h</option>
<option value="14:00:00">14h</option>
<option value="15:00:00">15h</option>
<option value="16:00:00">16h</option>
<option value="17:00:00">17h</option>
<option value="18:00:00">18h</option>
<option value="19:00:00">19h</option>
<option value="20:00:00">20h</option>
<option value="21:00:00">21h</option>
<option value="22:00:00">22h</option>
<option value="23:00:00">23h</option>
<option value="23:59:59">24h</option>
</select>
</div>
</div>
<div class="col-sm-3 col-xs-12">
<div class="float-end">
<div class="form-check input-group">
<span class="input-group-text">
<input
id="showHideWE"
class="mt-0"
type="checkbox"
v-model="hideWeekends"
/>
</span>
<label
for="showHideWE"
class="form-check-label input-group-text"
>Week-ends</label
>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-3 col-xs-12">
<div class="float-end">
<div class="form-check input-group">
<span class="input-group-text">
<input
id="showHideWE"
class="mt-0"
type="checkbox"
v-model="hideWeekends"
/>
</span>
<label for="showHideWE" class="form-check-label input-group-text"
>Week-ends</label
>
</div>
</div>
</div>
</div>
<FullCalendar ref="fullCalendar" :options="calendarOptions">
<template #eventContent="arg">
<span>
<b v-if="arg.event.extendedProps.is === 'remote'">{{
arg.event.title
}}</b>
<b v-else-if="arg.event.extendedProps.is === 'range'"
>{{ arg.timeText }}
{{ arg.event.extendedProps.locationName }}
<small>{{ arg.event.extendedProps.userLabel }}</small></b
>
<b v-else-if="arg.event.extendedProps.is === 'current'"
>{{ arg.timeText }} {{ $t("current_selected") }}
</b>
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
arg.event.title
}}</b>
<b v-else>{{ arg.timeText }} {{ $t("current_selected") }} </b>
</span>
</template>
</FullCalendar>
</teleport>
<FullCalendar ref="fullCalendar" :options="calendarOptions">
<template #eventContent="arg">
<span>
<b v-if="arg.event.extendedProps.is === 'remote'">{{
arg.event.title
}}</b>
<b v-else-if="arg.event.extendedProps.is === 'range'"
>{{ arg.timeText }}
{{ arg.event.extendedProps.locationName }}
<small>{{
arg.event.extendedProps.userLabel
}}</small></b
>
<b v-else-if="arg.event.extendedProps.is === 'current'"
>{{ arg.timeText }} {{ $t("current_selected") }}
</b>
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
arg.event.title
}}</b>
<b v-else
>{{ arg.timeText }} {{ $t("current_selected") }}
</b>
</span>
</template>
</FullCalendar>
</teleport>
</template>
<script>
@@ -157,210 +177,219 @@ import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
import { mapGetters, mapState } from "vuex";
export default {
name: "App",
components: {
ConcernedGroups,
Location,
FullCalendar,
CalendarActive,
PickEntity,
},
data() {
return {
errorMsg: [],
showMyCalendar: false,
slotDuration: "00:05:00",
slotMinTime: "09:00:00",
slotMaxTime: "18:00:00",
hideWeekEnds: true,
previousUser: [],
};
},
computed: {
...mapGetters(["getMainUser"]),
...mapState(["activity"]),
events() {
return this.$store.getters.getEventSources;
name: "App",
components: {
ConcernedGroups,
Location,
FullCalendar,
CalendarActive,
PickEntity,
},
calendarOptions() {
return {
locale: frLocale,
plugins: [
dayGridPlugin,
interactionPlugin,
timeGridPlugin,
dayGridPlugin,
listPlugin,
],
initialView: "timeGridWeek",
initialDate: this.$store.getters.getInitialDate,
eventSources: this.events,
selectable: true,
slotMinTime: this.slotMinTime,
slotMaxTime: this.slotMaxTime,
scrollTimeReset: false,
datesSet: this.onDatesSet,
select: this.onDateSelect,
eventChange: this.onEventChange,
eventClick: this.onEventClick,
selectMirror: true,
editable: true,
weekends: !this.hideWeekEnds,
headerToolbar: {
left: "prev,next today",
center: "title",
right: "timeGridWeek,timeGridDay,listWeek",
data() {
return {
errorMsg: [],
showMyCalendar: false,
slotDuration: "00:05:00",
slotMinTime: "09:00:00",
slotMaxTime: "18:00:00",
hideWeekEnds: true,
previousUser: [],
};
},
computed: {
...mapGetters(["getMainUser"]),
...mapState(["activity"]),
events() {
return this.$store.getters.getEventSources;
},
views: {
timeGrid: {
slotEventOverlap: false,
slotDuration: this.slotDuration,
},
calendarOptions() {
return {
locale: frLocale,
plugins: [
dayGridPlugin,
interactionPlugin,
timeGridPlugin,
dayGridPlugin,
listPlugin,
],
initialView: "timeGridWeek",
initialDate: this.$store.getters.getInitialDate,
eventSources: this.events,
selectable: true,
slotMinTime: this.slotMinTime,
slotMaxTime: this.slotMaxTime,
scrollTimeReset: false,
datesSet: this.onDatesSet,
select: this.onDateSelect,
eventChange: this.onEventChange,
eventClick: this.onEventClick,
selectMirror: true,
editable: true,
weekends: !this.hideWeekEnds,
headerToolbar: {
left: "prev,next today",
center: "title",
right: "timeGridWeek,timeGridDay,listWeek",
},
views: {
timeGrid: {
slotEventOverlap: false,
slotDuration: this.slotDuration,
},
},
};
},
getActiveUsers() {
const users = [];
for (const id of this.$store.state.currentView.users.keys()) {
users.push(this.$store.getters.getUserDataById(id).user);
}
return users;
},
suggestedUsers() {
const suggested = [];
this.$data.previousUser.forEach((u) => {
if (u.id !== this.$store.getters.getMainUser.id) {
suggested.push(u);
}
});
return suggested;
},
};
},
getActiveUsers() {
const users = [];
for (const id of this.$store.state.currentView.users.keys()) {
users.push(this.$store.getters.getUserDataById(id).user);
}
return users;
methods: {
setMainUser({ entity }) {
const user = entity;
console.log("setMainUser APP", entity);
if (
user.id !== this.$store.getters.getMainUser &&
(this.$store.state.activity.calendarRange !== null ||
this.$store.state.activity.startDate !== null ||
this.$store.state.activity.endDate !== null)
) {
if (
!window.confirm(
this.$t("change_main_user_will_reset_event_data"),
)
) {
return;
}
}
// add the previous user, if any, in the previous user list (in use for suggestion)
if (null !== this.$store.getters.getMainUser) {
const suggestedUids = new Set(
this.$data.previousUser.map((u) => u.id),
);
if (!suggestedUids.has(this.$store.getters.getMainUser.id)) {
this.$data.previousUser.push(
this.$store.getters.getMainUser,
);
}
}
this.$store.dispatch("setMainUser", user);
this.$store.commit("showUserOnCalendar", {
user,
ranges: true,
remotes: true,
});
},
removeMainUser(user) {
console.log("removeMainUser APP", user);
window.alert(this.$t("main_user_is_mandatory"));
return;
},
onDatesSet(event) {
console.log("onDatesSet", event);
this.$store.dispatch("setCurrentDatesView", {
start: event.start,
end: event.end,
});
},
onDateSelect(payload) {
console.log("onDateSelect", payload);
// show an alert if changing mainUser
if (
(this.$store.getters.getMainUser !== null &&
this.$store.state.me.id !==
this.$store.getters.getMainUser.id) ||
this.$store.getters.getMainUser === null
) {
if (!window.confirm(this.$t("will_change_main_user_for_me"))) {
return;
} else {
this.$store.commit("showUserOnCalendar", {
user: this.$store.state.me,
remotes: true,
ranges: true,
});
}
}
this.$store.dispatch("setEventTimes", {
start: payload.start,
end: payload.end,
});
},
onEventChange(payload) {
console.log("onEventChange", payload);
if (this.$store.state.activity.calendarRange !== null) {
throw new Error(
"not allowed to edit a calendar associated with a calendar range",
);
}
this.$store.dispatch("setEventTimes", {
start: payload.event.start,
end: payload.event.end,
});
},
onEventClick(payload) {
if (payload.event.extendedProps.is !== "range") {
// do nothing when clicking on remote
return;
}
// show an alert if changing mainUser
if (
this.$store.getters.getMainUser !== null &&
payload.event.extendedProps.userId !==
this.$store.getters.getMainUser.id
) {
if (
!window.confirm(
this.$t("this_calendar_range_will_change_main_user"),
)
) {
return;
}
}
this.$store.dispatch("associateCalendarToRange", {
range: payload.event,
});
},
},
suggestedUsers() {
const suggested = [];
this.$data.previousUser.forEach((u) => {
if (u.id !== this.$store.getters.getMainUser.id) {
suggested.push(u);
}
});
return suggested;
},
},
methods: {
setMainUser({ entity }) {
const user = entity;
console.log("setMainUser APP", entity);
if (
user.id !== this.$store.getters.getMainUser &&
(this.$store.state.activity.calendarRange !== null ||
this.$store.state.activity.startDate !== null ||
this.$store.state.activity.endDate !== null)
) {
if (
!window.confirm(this.$t("change_main_user_will_reset_event_data"))
) {
return;
}
}
// add the previous user, if any, in the previous user list (in use for suggestion)
if (null !== this.$store.getters.getMainUser) {
const suggestedUids = new Set(this.$data.previousUser.map((u) => u.id));
if (!suggestedUids.has(this.$store.getters.getMainUser.id)) {
this.$data.previousUser.push(this.$store.getters.getMainUser);
}
}
this.$store.dispatch("setMainUser", user);
this.$store.commit("showUserOnCalendar", {
user,
ranges: true,
remotes: true,
});
},
removeMainUser(user) {
console.log("removeMainUser APP", user);
window.alert(this.$t("main_user_is_mandatory"));
return;
},
onDatesSet(event) {
console.log("onDatesSet", event);
this.$store.dispatch("setCurrentDatesView", {
start: event.start,
end: event.end,
});
},
onDateSelect(payload) {
console.log("onDateSelect", payload);
// show an alert if changing mainUser
if (
(this.$store.getters.getMainUser !== null &&
this.$store.state.me.id !== this.$store.getters.getMainUser.id) ||
this.$store.getters.getMainUser === null
) {
if (!window.confirm(this.$t("will_change_main_user_for_me"))) {
return;
} else {
this.$store.commit("showUserOnCalendar", {
user: this.$store.state.me,
remotes: true,
ranges: true,
});
}
}
this.$store.dispatch("setEventTimes", {
start: payload.start,
end: payload.end,
});
},
onEventChange(payload) {
console.log("onEventChange", payload);
if (this.$store.state.activity.calendarRange !== null) {
throw new Error(
"not allowed to edit a calendar associated with a calendar range",
);
}
this.$store.dispatch("setEventTimes", {
start: payload.event.start,
end: payload.event.end,
});
},
onEventClick(payload) {
if (payload.event.extendedProps.is !== "range") {
// do nothing when clicking on remote
return;
}
// show an alert if changing mainUser
if (
this.$store.getters.getMainUser !== null &&
payload.event.extendedProps.userId !==
this.$store.getters.getMainUser.id
) {
if (
!window.confirm(this.$t("this_calendar_range_will_change_main_user"))
) {
return;
}
}
this.$store.dispatch("associateCalendarToRange", {
range: payload.event,
});
},
},
};
</script>
<style>
.calendar-actives {
display: flex;
flex-direction: row;
flex-wrap: wrap;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.display-options {
margin-top: 1rem;
margin-top: 1rem;
}
/* for events which are range */
.fc-event.isrange {
border-width: 3px;
border-width: 3px;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ final readonly class StoredObjectVersionApiController
return new JsonResponse(
$this->serializer->serialize(
new Collection($items, $paginator),
new Collection(array_values($items->toArray()), $paginator),
'json',
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
),

View File

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

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Exception;
class ConversionWithSameMimeTypeException extends \RuntimeException
{
public function __construct(string $mimeType, ?\Throwable $previous = null)
{
parent::__construct("Conversion to same MIME type '{$mimeType}' is not allowed: already at the same MIME type", 0, $previous);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,184 +1,196 @@
<script setup lang="ts">
import {
StoredObject,
StoredObjectPointInTime,
StoredObjectVersionWithPointInTime,
} from "./../../../types";
StoredObject,
StoredObjectPointInTime,
StoredObjectVersionWithPointInTime,
} from "ChillDocStoreAssets/types";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
import { computed } from "vue";
interface HistoryButtonListItemConfig {
version: StoredObjectVersionWithPointInTime;
storedObject: StoredObject;
canEdit: boolean;
isCurrent: boolean;
version: StoredObjectVersionWithPointInTime;
storedObject: StoredObject;
canEdit: boolean;
isCurrent: boolean;
}
const emit = defineEmits<{
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime];
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime];
}>();
const props = defineProps<HistoryButtonListItemConfig>();
const onRestore = ({
newVersion,
newVersion,
}: {
newVersion: StoredObjectVersionWithPointInTime;
newVersion: StoredObjectVersionWithPointInTime;
}) => {
emit("restoreVersion", { newVersion });
emit("restoreVersion", { newVersion });
};
const isKeptBeforeConversion = computed<boolean>(() => {
if ("point-in-times" in props.version) {
return props.version["point-in-times"].reduce(
(accumulator: boolean, pit: StoredObjectPointInTime) =>
accumulator || "keep-before-conversion" === pit.reason,
false,
);
} else {
return false;
}
if ("point-in-times" in props.version) {
return props.version["point-in-times"].reduce(
(accumulator: boolean, pit: StoredObjectPointInTime) =>
accumulator || "keep-before-conversion" === pit.reason,
false,
);
} else {
return false;
}
});
const isRestored = computed<boolean>(
() => props.version.version > 0 && null !== props.version["from-restored"],
() => props.version.version > 0 && null !== props.version["from-restored"],
);
const isDuplicated = computed<boolean>(
() => props.version.version === 0 && null !== props.version["from-restored"],
() =>
props.version.version === 0 && null !== props.version["from-restored"],
);
const classes = computed<{
row: true;
"row-hover": true;
"blinking-1": boolean;
"blinking-2": boolean;
row: true;
"row-hover": true;
"blinking-1": boolean;
"blinking-2": boolean;
}>(() => ({
row: true,
"row-hover": true,
"blinking-1": props.isRestored && 0 === props.version.version % 2,
"blinking-2": props.isRestored && 1 === props.version.version % 2,
row: true,
"row-hover": true,
"blinking-1": props.isRestored && 0 === props.version.version % 2,
"blinking-2": props.isRestored && 1 === props.version.version % 2,
}));
</script>
<template>
<div :class="classes">
<div
class="col-12 tags"
v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated"
>
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span>
<span class="badge bg-info" v-if="isKeptBeforeConversion"
>Conservée avant conversion dans un autre format</span
>
<span class="badge bg-info" v-if="isRestored"
>Restaurée depuis la version
{{ version["from-restored"]?.version + 1 }}</span
>
<span class="badge bg-info" v-if="isDuplicated"
>Dupliqué depuis un autre document</span
>
<div :class="classes">
<div
class="col-12 tags"
v-if="
isCurrent ||
isKeptBeforeConversion ||
isRestored ||
isDuplicated
"
>
<span class="badge bg-success" v-if="isCurrent"
>Version actuelle</span
>
<span class="badge bg-info" v-if="isKeptBeforeConversion"
>Conservée avant conversion dans un autre format</span
>
<span class="badge bg-info" v-if="isRestored"
>Restaurée depuis la version
{{ version["from-restored"]?.version + 1 }}</span
>
<span class="badge bg-info" v-if="isDuplicated"
>Dupliqué depuis un autre document</span
>
</div>
<div class="col-12">
<file-icon :type="version.type"></file-icon>
<span
><strong>&nbsp;#{{ version.version + 1 }}&nbsp;</strong></span
>
<template
v-if="version.createdBy !== null && version.createdAt !== null"
><strong v-if="version.version == 0">créé par</strong
><strong v-else>modifié par</strong>
<span class="badge-user"
><UserRenderBoxBadge
:user="version.createdBy"
></UserRenderBoxBadge
></span>
<strong>à</strong>
{{
$d(ISOToDatetime(version.createdAt.datetime8601), "long")
}}</template
><template
v-if="version.createdBy === null && version.createdAt !== null"
><strong v-if="version.version == 0">Créé le</strong
><strong v-else>modifié le</strong>
{{
$d(ISOToDatetime(version.createdAt.datetime8601), "long")
}}</template
>
</div>
<div class="col-12">
<ul class="record_actions small slim on-version-actions">
<li v-if="canEdit && !isCurrent">
<restore-version-button
:stored-object-version="props.version"
@restore-version="onRestore"
></restore-version-button>
</li>
<li>
<download-button
:stored-object="storedObject"
:at-version="version"
:classes="{
btn: true,
'btn-outline-primary': true,
'btn-sm': true,
}"
:display-action-string-in-button="false"
></download-button>
</li>
</ul>
</div>
</div>
<div class="col-12">
<file-icon :type="version.type"></file-icon>
<span
><strong>&nbsp;#{{ version.version + 1 }}&nbsp;</strong></span
>
<template v-if="version.createdBy !== null && version.createdAt !== null"
><strong v-if="version.version == 0">créé par</strong
><strong v-else>modifié par</strong>
<span class="badge-user"
><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge
></span>
<strong>à</strong>
{{
$d(ISOToDatetime(version.createdAt.datetime8601), "long")
}}</template
><template v-if="version.createdBy === null && version.createdAt !== null"
><strong v-if="version.version == 0">Créé le</strong
><strong v-else>modifié le</strong>
{{
$d(ISOToDatetime(version.createdAt.datetime8601), "long")
}}</template
>
</div>
<div class="col-12">
<ul class="record_actions small slim on-version-actions">
<li v-if="canEdit && !isCurrent">
<restore-version-button
:stored-object-version="props.version"
@restore-version="onRestore"
></restore-version-button>
</li>
<li>
<download-button
:stored-object="storedObject"
:at-version="version"
:classes="{
btn: true,
'btn-outline-primary': true,
'btn-sm': true,
}"
:display-action-string-in-button="false"
></download-button>
</li>
</ul>
</div>
</div>
</template>
<style scoped lang="scss">
div.tags {
span.badge:not(:last-child) {
margin-right: 0.5rem;
}
span.badge:not(:last-child) {
margin-right: 0.5rem;
}
}
// to make the animation restart, we have the same animation twice,
// and alternate between both
.blinking-1 {
animation-name: backgroundColorPalette-1;
animation-duration: 8s;
animation-iteration-count: 1;
animation-direction: normal;
animation-timing-function: linear;
animation-name: backgroundColorPalette-1;
animation-duration: 8s;
animation-iteration-count: 1;
animation-direction: normal;
animation-timing-function: linear;
}
@keyframes backgroundColorPalette-1 {
0% {
background: var(--bs-chill-green-dark);
}
25% {
background: var(--bs-chill-green);
}
65% {
background: var(--bs-chill-beige);
}
100% {
background: unset;
}
0% {
background: var(--bs-chill-green-dark);
}
25% {
background: var(--bs-chill-green);
}
65% {
background: var(--bs-chill-beige);
}
100% {
background: unset;
}
}
.blinking-2 {
animation-name: backgroundColorPalette-2;
animation-duration: 8s;
animation-iteration-count: 1;
animation-direction: normal;
animation-timing-function: linear;
animation-name: backgroundColorPalette-2;
animation-duration: 8s;
animation-iteration-count: 1;
animation-direction: normal;
animation-timing-function: linear;
}
@keyframes backgroundColorPalette-2 {
0% {
background: var(--bs-chill-green-dark);
}
25% {
background: var(--bs-chill-green);
}
65% {
background: var(--bs-chill-beige);
}
100% {
background: unset;
}
0% {
background: var(--bs-chill-green-dark);
}
25% {
background: var(--bs-chill-green);
}
65% {
background: var(--bs-chill-beige);
}
100% {
background: unset;
}
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,6 +46,16 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
{
// we first try to get the permission from the workflow, as attachement (this is the less intensive query)
$workflowPermissionAsAttachment = match ($attribute) {
StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($subject),
StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($subject),
};
if (WorkflowRelatedEntityPermissionHelper::FORCE_DENIED === $workflowPermissionAsAttachment) {
return false;
}
// Retrieve the related entity
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
@@ -66,7 +76,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
return match ($workflowPermission) {
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission,
WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission,
};
}
}

View File

@@ -14,6 +14,12 @@ namespace Chill\DocStoreBundle\Security\Authorization;
use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
/**
* Interface for voting on stored object permissions.
*
* Each time a stored object is attached to a document, the voter is responsible for determining
* whether the user has the necessary permissions to access or modify the stored object.
*/
interface StoredObjectVoterInterface
{
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;

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