Compare commits

..

158 Commits

Author SHA1 Message Date
aabf62d399 Merge branch '369-duplicate-evaluation-document' into testing202509 2025-09-02 17:54:23 +02:00
6e36771349 fix changelog 2025-09-02 17:52:20 +02:00
7a82cae155 Release v4.2.0 2025-09-02 17:13:28 +02:00
d2d297a377 eslint fixes 2025-09-02 17:02:13 +02:00
0541995a60 Add icons to document action buttons and update bindings for accompanying period work IDs 2025-09-02 16:37:19 +02:00
29e054bd10 fix issues about icons to remain blank on blank
Refactor accompanying period work fetching logic to include a new filter for ignoring specific IDs, and update related components with new prop bindings.
2025-09-02 16:37:08 +02:00
da0099aafc Merge branch '369-duplicate-evaluation-document' into move-document-to-other-eval 2025-09-02 16:01:08 +02:00
3a18ea42fe Add ignore filter for accompanying period work IDs
Refactor accompanying period work fetching logic to include a new filter for ignoring specific IDs, and update related components with new prop bindings.
2025-09-02 15:52:06 +02:00
d204df0316 Merge branch '422-password-recover-layout' into 'master'
Resolve "Fix layout of password recover pages"

Closes #422

See merge request Chill-Projet/chill-bundles!869
2025-09-02 08:29:27 +00:00
juminet
82c02f442b Resolve "Fix layout of password recover pages" 2025-09-02 08:29:26 +00:00
f32a9dc7bc Merge branch '64-identifiant-personne' into 'master'
Add external identifiers for person, editable in edit form, with minimal features associated

See merge request Chill-Projet/chill-bundles!871
2025-09-01 08:05:11 +00:00
ea06a96f91 Add external identifiers for person, editable in edit form, with minimal features associated 2025-09-01 08:05:11 +00:00
76433e2512 Fix incorrect parameter name in event details link 2025-08-28 13:49:45 +02:00
1fa464b87a Fix typo in 'uncheckAll' script for centers selection 2025-08-28 13:32:43 +02:00
3b75f43e80 Update chill bundles to v4.1.0 2025-08-26 15:43:21 +02:00
a40eb95c43 Add changie for new event bundle features 2025-08-26 15:41:58 +02:00
8429c6e693 Merge branch 'improvements_event_module' into 'master'
Improvements event module

See merge request Chill-Projet/chill-bundles!825
2025-08-26 13:35:36 +00:00
6db7f6827c Update eslint baseline 2025-08-26 15:24:44 +02:00
3c60c57985 Adapt export list events to new export features 2025-08-26 15:18:08 +02:00
10aa36aae0 Set required to false for entitychoice filter field 2025-08-26 15:18:08 +02:00
eed9913a49 Allow select2 option for entityChoice filterOrderHelper 2025-08-26 15:18:08 +02:00
1a847d36a0 Fixes in template parameters + remove budget elements when removing event 2025-08-26 15:18:08 +02:00
d916962d9b Phpstan fix import Serializer instead of SerializerInterface 2025-08-26 15:18:08 +02:00
1092fc64ae Fix voter for the create event permission 2025-08-26 15:18:08 +02:00
4e99b6ecbd Allow filtering of event list by center and responsable 2025-08-26 15:18:08 +02:00
60d107b541 Create internal and external animators 2025-08-26 15:18:08 +02:00
4c3befe489 WIP change animator field to animator intern and animator extern 2025-08-26 15:18:08 +02:00
e176319775 WIP change animator field 2025-08-26 15:18:08 +02:00
5d810b4230 Add center to the show page of an event 2025-08-26 15:18:08 +02:00
52b8eea069 Fix passing of id parameter to route 2025-08-26 15:18:08 +02:00
4bebeaeaaa Fix wrong import of serializer 2025-08-26 15:18:08 +02:00
3969e12633 Fix cs and phpstan issues 2025-08-26 15:18:08 +02:00
d60312d4a2 Move styling to scss file and fix styling of participation list 2025-08-26 15:18:08 +02:00
d2454ae134 use key for column names in export 2025-08-26 15:18:08 +02:00
17c2cb1fdc Add missing translations 2025-08-26 15:18:08 +02:00
94d7a2a0bb Reverse deleting of organizationCost property on event entity to keep db data 2025-08-26 15:18:08 +02:00
aef1efc6cd Add missing translations and add eventThemeType missing config 2025-08-26 15:18:08 +02:00
dd0c662c9e Add missing description to migration 2025-08-26 15:18:08 +02:00
6b1696b62e phpstan, rector, phpcs fixes 2025-08-26 15:18:08 +02:00
c4b760c452 eslint fixes and new baseline 2025-08-26 15:18:08 +02:00
69fe2a8256 Add translations 2025-08-26 15:18:08 +02:00
8c98242896 Split budget elements in charges and resources column 2025-08-26 15:18:08 +02:00
7eecfd3882 Add new columns to export list event 2025-08-26 15:18:08 +02:00
6713658569 Add animators property to event 2025-08-26 15:18:08 +02:00
342b786106 Create export list of events 2025-08-26 15:18:08 +02:00
80a7437769 Update twig templates for display budget elements 2025-08-26 15:18:08 +02:00
8a38ce1a5c Add event budget element entity, forms and event property 2025-08-26 15:18:08 +02:00
5d94bf0556 Create an event budget kind admin entity 2025-08-26 15:18:08 +02:00
bb71e084b8 Create address on the fly field in event form 2025-08-26 15:18:08 +02:00
27f0bf28e9 Adjust templates and translations 2025-08-26 15:18:08 +02:00
383f588795 Add field in event for themes 2025-08-26 15:18:08 +02:00
e7a1ff1ac8 Add event theme property to event entity 2025-08-26 15:18:08 +02:00
adc9c47d0a Add event theme color for badge 2025-08-26 15:18:08 +02:00
e594b65d1e Create event theme admin entity 2025-08-26 15:18:08 +02:00
e60435b8cc Handle store state updates when moving documents
- Add null check to prevent error when evaluation is not part of this social work
2025-08-25 15:09:38 +02:00
ab6ab19499 feat: enhance document actions in UI
- Add edit and delete options in translation files
- Refactor `DocumentsList.vue` to group replace, delete, move, and duplicate actions in a dropdown menu
2025-08-25 15:02:26 +02:00
2a1762ea8d Remove code block in method setAccompanyingPeriodWorkEvaluation, no longer necessary 2025-08-25 15:02:26 +02:00
18ababbca9 Handle error when moving document between evaluations and display toast upon success 2025-08-25 15:02:26 +02:00
f6179cd3a3 WIP Add toast after successful move 2025-08-25 15:02:26 +02:00
ddf8da4cee php cs fixes 2025-08-25 15:02:26 +02:00
bf2181c2f1 allow changing evaluation for a document
- Remove restriction on changing evaluation in entity logic
2025-08-25 15:02:26 +02:00
d508fde8d2 enable moving documents between evaluations
- Add Vuex action and mutation for moving documents between evaluations
- Implement `moveDocumentToEvaluation` API method
- Update `DocumentsList.vue` and `FormEvaluation.vue` to handle move actions
2025-08-25 15:02:26 +02:00
14dba22181 wip: enable moving documents to evaluations
- Add `AccompanyingPeriodWorkEvaluationDocumentMoveController` for move functionality
- Update `DocumentsList.vue` to emit move event
- Adjust `FormEvaluation.vue` to handle move action
2025-08-25 15:02:26 +02:00
7dc7e77c62 WIP enable moving documents to evaluations
- Add new button and logic in `DocumentsList.vue` for moving documents
- Implement `moveDocumentToEvaluation` method in `FormEvaluation.vue`
- Ensure duplication and moving actions are mutually exclusive
- Add event for moving documents to evaluations
2025-08-25 15:02:26 +02:00
9d58904969 refactor: improve document duplication handling
- Remove unnecessary console logs
- Add null check before duplicating document to evaluation within another social work
2025-08-25 14:57:48 +02:00
4d90c7028f Store commit of document duplication only upon successful API call otherwise log error 2025-08-21 16:19:03 +02:00
3abb76d268 eslint 2025-08-21 09:55:52 +02:00
d62dd4396e Display toast upon successful duplication of evaluation document 2025-08-21 09:52:59 +02:00
59e8d9d516 Fix the access of results after API call 2025-08-20 16:03:56 +02:00
7dcb8abe38 Merge branch 'master' into 369-duplicate-evaluation-document 2025-08-20 15:28:19 +02:00
a0b2d92ba2 Fix the selection modal for acpw for merging functionality 2025-08-20 12:53:09 +02:00
7843e5dfd1 Add return types and remove unnecessary html snippet 2025-08-19 14:11:26 +02:00
32c847267b Remove dump 2025-08-19 10:20:38 +02:00
c0826bc65c Merge branch '400-add-filter-mes-actions' into 'master'
Add a filter to list for acpw where current user intervenes

Closes #400

See merge request Chill-Projet/chill-bundles!859
2025-08-18 16:26:20 +00:00
904f4e5ed9 Add a filter to list for acpw where current user intervenes 2025-08-18 16:26:20 +00:00
481f82b4c7 Merge branch '355-fusion-thirdparty' into 'master'
Resolve "Fusion des tiers"

Closes #355

See merge request Chill-Projet/chill-bundles!795
2025-08-18 15:34:48 +00:00
f5668592ca Resolve "Fusion des tiers" 2025-08-18 15:34:48 +00:00
9b353f4d1b Filter accompanying period works in evaluation selector mode
- Add filtering to show only accompanying period works with evaluations in evaluation selector mode
2025-08-13 13:19:41 +02:00
81a858f07a eslint corrections 2025-08-13 12:38:32 +02:00
6a2ee232a9 feat: enable document duplication to another evaluation
- Introduce API method for duplicating a document to a different evaluation
- Add Vuex actions and mutations to handle duplication logic to another evaluation
2025-08-13 12:35:40 +02:00
56c43a0a76 Refactor display document duplication button and add translations
- Add new translations for document duplication and replacement options
- Adjust order of list elements in `DocumentsList.vue` for better readability
2025-08-13 09:40:36 +02:00
aa085a1562 **fix:** add min and step attributes to integer field in DateIntervalType 2025-08-06 17:35:45 +02:00
2754251fdc Merge branch 'master' of https://gitlab.com/Chill-Projet/chill-bundles 2025-08-06 14:20:29 +02:00
2f6cef4238 - **fix:** move closing motive up to be coherent with display elsewhere 2025-08-06 14:20:09 +02:00
2309636eae - **fix:** adjust display logic for accompanying period dates, include closing date if period is closed. 2025-08-06 13:47:29 +02:00
56ec8fb516 Remove 'to_validate' as default for task filter 2025-08-06 09:05:39 +02:00
fe6e6e54c1 Show filters on list pages unfolded by default 2025-07-22 15:50:49 +02:00
2a09594b4a UI improvement: limit display of particapations in event list page 2025-07-22 13:26:44 +02:00
7c798e1f63 Merge branch '387-notification-user-group' into 'master'
Resolve "Notification: envoi à des groupes utilisateurs"

Closes #387

See merge request Chill-Projet/chill-bundles!842
2025-07-20 20:18:49 +00:00
ab8da4ab7a Resolve "Notification: envoi à des groupes utilisateurs" 2025-07-20 20:18:49 +00:00
5bdb2df929 Merge branch 'revert-5f016734' into 'master'
Revert "Merge branch 'ticket/supplementary-comments-on-motive' into 'master'"

See merge request Chill-Projet/chill-bundles!863
2025-07-20 18:51:51 +00:00
e3a6b60fa2 Revert "Merge branch 'ticket/supplementary-comments-on-motive' into 'master'"
This reverts merge request !855
2025-07-20 18:50:33 +00:00
5f01673404 Merge branch 'ticket/supplementary-comments-on-motive' into 'master'
Ajout de commentaires supplémentaires aux motifs

See merge request Chill-Projet/chill-bundles!855
2025-07-11 14:06:40 +00:00
63d0a52ea1 Ajout de commentaires supplémentaires aux motifs 2025-07-11 14:06:40 +00:00
837089ff5d Fix testMerge method in AccompanyingPeriodWorkMergeServiceTest.php 2025-07-10 11:33:23 +02:00
f383fab578 Fix styling 2025-07-09 15:30:39 +02:00
f3cc4a89af Update chill bundles to v4.0.2 2025-07-09 15:23:59 +02:00
703f5dc32d Transfer evaluations (and related documents) during merge 2025-07-09 15:21:42 +02:00
b870e71f77 Add translation for validation message in social action merger 2025-07-09 15:21:24 +02:00
eb724a730c remove line ux-translator 2025-04-28 10:50:37 +02:00
18f98b6795 Changie added for fusion of accompanying period works 2025-04-03 10:09:16 +02:00
d73994edd0 Adjust display of acpw tag when modal in use for the selection of an evaluation 2025-04-03 10:05:43 +02:00
70603570c8 Changie added 2025-04-03 10:03:25 +02:00
df09dd2017 Eslint fixes 2025-04-03 10:02:17 +02:00
1c87280b1e Display a toast message when document is duplicated succesfully 2025-04-03 09:56:36 +02:00
445e093a28 Emit duplication of document to an evaluation and add backend logic 2025-04-02 19:03:58 +02:00
3f91c65b30 Display evaluations in modal after selection of accompanyingPeriodWork 2025-04-02 15:36:11 +02:00
9bc3c16b58 WIP prepare modal for display of evaluations linked to accompanying period work 2025-04-02 13:52:51 +02:00
12dff82248 Re-establish normal behavior for component within twig 2025-04-02 12:44:55 +02:00
ab23a4efb5 Refactor FormEvaluation.vue component 2025-04-02 11:55:37 +02:00
204fb20475 Change behavior of AccompanyingPeriodWorkSelectorModal.vue: open modal directly 2025-04-02 11:53:21 +02:00
f430d97152 Transform duplicate button into dropdown 2025-04-01 18:45:46 +02:00
4fa4d3b65c Phpstan and cs fixes 2025-03-27 14:32:06 +01:00
bd4c34cc1d Fix eslint issues and add ts interfaces for typing 2025-03-27 14:26:43 +01:00
4cea678e93 Fix updating of manyToMany relationships 2025-03-27 13:34:16 +01:00
5e6833975b Fix handling comments and workflows on acpw 2025-03-26 20:25:54 +01:00
f523b9adb3 Fix typing errors 2025-03-26 20:25:39 +01:00
a211549432 Adjust template and add translations 2025-03-26 15:16:27 +01:00
17b1363113 Fixes after rebase + apply item styling for accompanying course work 2025-03-26 14:08:45 +01:00
3356ed8e57 Correct for loop to display accompanying period list items 2025-03-24 16:13:56 +01:00
2a7fa517ee Only show merge button if there are more than 1 works attached to the parcours 2025-03-24 16:07:47 +01:00
85781c8e14 Use item renderbox for display of accompanyingperiodwork 2025-03-19 11:04:01 +01:00
00eb435896 Add chevron icon in merge button 2025-03-19 11:04:01 +01:00
ed71cffd6a Change behavior of information exchange between backend and frontend 2025-03-19 11:03:59 +01:00
ae679e6997 Fix merge service and passing of json to vue 2025-03-19 11:03:53 +01:00
e1d308fd97 WIP create new picker for accompanying period works 2025-03-19 11:03:42 +01:00
d9acda67e3 WIP dynamic picking of accompanying period work 2025-03-19 11:03:42 +01:00
e88da74882 WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:03:41 +01:00
591c44d1a0 Create types 2025-03-19 11:03:18 +01:00
bf04b7981c Improve merge service according to specifications 2025-03-19 11:03:02 +01:00
df33eec30f WIP merge service 2025-03-19 11:03:00 +01:00
c657c98918 Styling and organization of components 2025-03-19 11:02:55 +01:00
ef5eb5b907 Open modal to select acpw 2025-03-19 11:02:27 +01:00
d683fe002d Different approach to creating acpw selector 2025-03-19 11:02:25 +01:00
555bbca59b WIP create new picker for accompanying period works 2025-03-19 11:02:22 +01:00
e9e9d5c458 WIP dynamic picking of accompanying period work 2025-03-19 11:02:22 +01:00
b1842a33ae WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:02:19 +01:00
6afeaccf24 Improve merge service according to specifications 2025-03-19 11:01:52 +01:00
fb76bac480 WIP merge service 2025-03-19 11:01:49 +01:00
6ded185289 Treat duplicate in backend and setup confirm page of merge 2025-03-19 11:00:40 +01:00
95adc29f9d WIP create new picker for accompanying period works 2025-03-19 11:00:40 +01:00
4d0c3e683f WIP dynamic picking of accompanying period work 2025-03-19 11:00:40 +01:00
018aafc773 WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:00:40 +01:00
c4aea4efc2 Create types 2025-03-19 11:00:40 +01:00
225e3ca13f Improve merge service according to specifications 2025-03-19 11:00:40 +01:00
8c1fa7956a WIP merge service 2025-03-19 11:00:40 +01:00
e253d1b276 Styling and organization of components 2025-03-19 11:00:40 +01:00
a52aac2d98 Update package.json 2025-03-19 11:00:40 +01:00
9e8cf60dd8 Open modal to select acpw 2025-03-19 11:00:40 +01:00
7682d81d50 Different approach to creating acpw selector 2025-03-19 11:00:40 +01:00
5d31ce96c1 WIP create new picker for accompanying period works 2025-03-19 11:00:40 +01:00
81ef64a246 WIP dynamic picking of accompanying period work 2025-03-19 11:00:40 +01:00
49d1f78001 WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:00:40 +01:00
0d0f3528e2 Add (temporary) types in Main and ThirdpartyBundle 2025-03-19 11:00:40 +01:00
d97d5e689a Create types 2025-03-19 11:00:40 +01:00
95d80ce13e Improve merge service according to specifications 2025-03-19 11:00:40 +01:00
668720984d WIP merge service 2025-03-19 11:00:40 +01:00
245c3fa121 First commit - changie for feature 2025-03-19 11:00:40 +01:00
589 changed files with 30830 additions and 36136 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

@@ -0,0 +1,6 @@
kind: Feature
body: Allow the merge of two accompanying period works
time: 2025-02-11T14:22:43.134106669+01:00
custom:
Issue: "359"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Duplication of a document to another accompanying period work evaluation
time: 2025-04-03T10:03:11.796736107+02:00
custom:
Issue: "369"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Fusion of two accompanying period works
time: 2025-04-03T10:08:57.25079018+02:00
custom:
Issue: "359"
SchemaChange: No schema change

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

@@ -0,0 +1,4 @@
## v4.0.2 - 2025-07-09
### Fixed
* Fix add missing translation
* Fix the transfer of evaluations and documents during of accompanyingperiodwork

12
.changes/v4.1.0.md Normal file
View File

@@ -0,0 +1,12 @@
## 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
**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
### UX
* Limit display of participations in event list

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

@@ -0,0 +1,10 @@
## v4.2.0 - 2025-09-02
### Feature
* ([#64](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/64)) Add external identifier for a Person
**Schema Change**: Add columns or tables
* ([#330](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/330) Allow users to choose for which notifications they want to receive an email
### Fixed
* ([#422](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/422)) Fixed html layout of pages for recovering password
* Fix typo in 'uncheckAll' script for centers selection
* Fix incorrect parameter name in event details link

View File

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

13
.env
View File

@@ -16,6 +16,9 @@ APP_ENV=prod
APP_SECRET=!ChangeMeInAppEnv! APP_SECRET=!ChangeMeInAppEnv!
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
## Wopi server for editing documents online
EDITOR_SERVER=http://collabora:9980
# must be manually set in .env.local # must be manually set in .env.local
# ADMIN_PASSWORD= # ADMIN_PASSWORD=
@@ -89,13 +92,3 @@ REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
###> symfony/ovh-cloud-notifier ### ###> symfony/ovh-cloud-notifier ###
# OVHCLOUD_DSN=ovhcloud://APPLICATION_KEY:APPLICATION_SECRET@default?consumer_key=CONSUMER_KEY&service_name=SERVICE_NAME # OVHCLOUD_DSN=ovhcloud://APPLICATION_KEY:APPLICATION_SECRET@default?consumer_key=CONSUMER_KEY&service_name=SERVICE_NAME
###< symfony/ovh-cloud-notifier ### ###< symfony/ovh-cloud-notifier ###
###> symfony/mercure-bundle ###
# See https://symfony.com/doc/current/mercure.html#configuration
# The URL of the Mercure hub, used by the app to publish updates (can be a local URL)
MERCURE_URL=https://example.com/.well-known/mercure
# The public URL of the Mercure hub, used by the browser to connect
MERCURE_PUBLIC_URL=https://example.com/.well-known/mercure
# The secret used to sign the JWTs
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
###< symfony/mercure-bundle ###

File diff suppressed because it is too large Load Diff

3
.gitignore vendored
View File

@@ -18,6 +18,9 @@ migrations/*
templates/* templates/*
translations/* translations/*
# we allow developers to add customization on their installation, without commiting it
config/packages/dev/*
###> symfony/framework-bundle ### ###> symfony/framework-bundle ###
/.env.local /.env.local
/.env.local.php /.env.local.php

View File

@@ -185,14 +185,57 @@ When we need to use a DateTime or DateTimeImmutable that need to express "now",
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities, `Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
where injection does not work when restoring an entity from database, but usually possible in services. where injection does not work when restoring an entity from database, but usually possible in services.
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
where we have full and easy control of the date.
### Testing Information ### Testing Information
The project uses PHPUnit for testing. Each bundle has its own test suite, and there's also a global test suite at the root level. The project uses PHPUnit for testing. Each bundle has its own test suite, and there's also a global test suite at the root level.
#### Use of mock in tests
##### General mocking
For creating mock, we prefer using prophecy (library phpspec/prophecy). For creating mock, we prefer using prophecy (library phpspec/prophecy).
##### Useful helpers and tips that avoid create a mock
Some notable implementations that are tests helper, and avoid to create a mock:
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
- `\Symfony\Component\HttpClient\MockHttpClient`, an implementation of `\Symfony\Contracts\HttpClient\HttpClientInterface`;
- When using `\Symfony\Component\Mailer\MailerInterface`, we can create the mock with "InMemoryTransport":
```php
use Symfony\Component\Mailer\Transport\InMemoryTransport;
use \Symfony\Component\Mailer\Mailer;
$transport = new InMemoryTransport();
$mailer = new Mailer($transport);
// After sending:
$messages = $transport->getSent(); // array of SentMessage
```
- When using `\Symfony\Contracts\EventDispatcher\EventDispatcherInterface`, we can use directly an instance of `\Symfony\Component\EventDispatcher\EventDispatcher`;
##### When we prefer not creating a mock
- When we use Doctrine Entities related to the project, we prefer not to use a mock: we instantiate them directly (unless it requires too much code to write);
##### Mocking final and readonly classes
Classes marked as final can't be mocked. To avoid that, either:
- we remove the `final` keyword from the class;
- we extract an interface from the final class.
This must be a decision made by a human, not by an AI. Every AI task must abort with an explicit message in that case.
#### Running Tests #### Running Tests
The tests are run from the project's root (not from the bundle's root).
```bash ```bash
# Run all tests # Run all tests
vendor/bin/phpunit vendor/bin/phpunit

View File

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

30
.vscode/launch.json vendored
View File

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

23
.vscode/tasks.json vendored
View File

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

View File

@@ -6,6 +6,35 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.2.0 - 2025-09-02
### Feature
* ([#64](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/64)) Add external identifier for a Person
**Schema Change**: Add columns or tables
* ([#330](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/330) Allow users to choose for which notifications they want to receive an email
### Fixed
* ([#422](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/422)) Fixed html layout of pages for recovering password
* Fix typo in 'uncheckAll' script for centers selection
* Fix incorrect parameter name in event details link
## 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
**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
### UX
* 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
## v4.0.1 - 2025-07-08 ## v4.0.1 - 2025-07-08
### Fixed ### Fixed
* Fix package.json for compilation * Fix package.json for compilation

View File

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

View File

@@ -32,9 +32,3 @@ services:
hostname: my-rabbit hostname: my-rabbit
volumes: volumes:
- ./docker/rabbitmq/data:/var/lib/rabbitmq - ./docker/rabbitmq/data:/var/lib/rabbitmq
###> symfony/mercure-bundle ###
mercure:
ports:
- "127.0.0.1:8043:443"
###< symfony/mercure-bundle ###

View File

@@ -50,36 +50,7 @@ services:
timeout: 30s timeout: 30s
retries: 3 retries: 3
###> symfony/mercure-bundle ###
mercure:
image: dunglas/mercure
restart: unless-stopped
environment:
# Uncomment the following line to disable HTTPS,
#SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
# Set the URL of your Symfony project (without trailing slash!) as value of the cors_origins directive
MERCURE_EXTRA_DIRECTIVES: |
cors_origins http://chill-bundles.wip https://chill-bundles.wip
# Comment the following line to disable the development mode
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
healthcheck:
test: [ "CMD", "curl", "-f", "https://localhost/healthz" ]
timeout: 5s
retries: 5
start_period: 60s
volumes:
- mercure_data:/data
- mercure_config:/config
###< symfony/mercure-bundle ###
volumes: volumes:
###> doctrine/doctrine-bundle ### ###> doctrine/doctrine-bundle ###
database_data: database_data:
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
###> symfony/mercure-bundle ###
mercure_data:
mercure_config:
###< symfony/mercure-bundle ###

View File

@@ -55,7 +55,6 @@
"symfony/http-foundation": "^5.4", "symfony/http-foundation": "^5.4",
"symfony/intl": "^5.4", "symfony/intl": "^5.4",
"symfony/mailer": "^5.4", "symfony/mailer": "^5.4",
"symfony/mercure-bundle": "^0.3.9",
"symfony/messenger": "^5.4", "symfony/messenger": "^5.4",
"symfony/mime": "^5.4", "symfony/mime": "^5.4",
"symfony/monolog-bundle": "^3.5", "symfony/monolog-bundle": "^3.5",
@@ -134,7 +133,6 @@
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle", "Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle", "Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle",
"Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src", "Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src",
"Chill\\TicketBundle\\": "src/Bundle/ChillTicketBundle/src",
"Chill\\Utils\\Rector\\": "utils/rector/src" "Chill\\Utils\\Rector\\": "utils/rector/src"
} }
}, },

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
mercure:
hubs:
default:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: '*'

View File

@@ -62,8 +62,10 @@ framework:
'Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage': priority 'Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage': priority
'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async 'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async
'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async 'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage': async
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority 'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async 'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async
# end of routes added by chill-bundles recipes # end of routes added by chill-bundles recipes
# Route your messages to the transports # Route your messages to the transports
# 'App\Message\YourMessage': async # 'App\Message\YourMessage': async

View File

@@ -17,8 +17,3 @@ when@dev:
defaults: defaults:
template: '@ChillMain/Dev/dev.assets.test2.html.twig' template: '@ChillMain/Dev/dev.assets.test2.html.twig'
dev_mercure:
path: /_dev/mercure
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: '@ChillMain/Dev/dev.mercure.html.twig'

View File

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

View File

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

View File

@@ -11,7 +11,6 @@
"@hotwired/stimulus": "^3.0.0", "@hotwired/stimulus": "^3.0.0",
"@luminateone/eslint-baseline": "^1.0.9", "@luminateone/eslint-baseline": "^1.0.9",
"@symfony/stimulus-bridge": "^3.2.0", "@symfony/stimulus-bridge": "^3.2.0",
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
"@symfony/webpack-encore": "^4.1.0", "@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node20": "^20.1.4", "@tsconfig/node20": "^20.1.4",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
@@ -80,12 +79,12 @@
"dev": "encore dev", "dev": "encore dev",
"watch": "encore dev --watch", "watch": "encore dev --watch",
"build": "encore production --progress", "build": "encore production --progress",
"specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml src/Bundle/ChillTicketBundle/chill.api.specs.yaml> templates/api/specs.yaml", "specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml> templates/api/specs.yaml",
"specs-validate": "swagger-cli validate templates/api/specs.yaml", "specs-validate": "swagger-cli validate templates/api/specs.yaml",
"specs-create-dir": "mkdir -p templates/api", "specs-create-dir": "mkdir -p templates/api",
"specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate", "specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate",
"version": "node --version", "version": "node --version",
"eslint": "eslint-baseline --fix \"src/**/*.{js,ts,vue}\"" "eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
}, },
"private": true "private": true
} }

View File

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

View File

@@ -1,20 +0,0 @@
{
# Désactive les redirections automatiques HTTP -> HTTPS
# auto_https off
# Désactive le port 80 par défaut
# default_bind :8080
}
localhost:8043 {
mercure {
# Publisher JWT key
publisher_jwt !ChangeThisMercureHubJWTSecretKey!
# Subscriber JWT key
subscriber_jwt !ChangeThisMercureHubJWTSecretKey!
cors_origins http://chill-bundles.wip https://chill-bundles.wip
ui
demo
}
respond "Not Found" 404
}

View File

@@ -10,7 +10,10 @@
/> />
</div> </div>
<div <div
v-if="getContext === 'accompanyingCourse' && suggestedEntities.length > 0" v-if="
getContext === 'accompanyingCourse' &&
suggestedEntities.length > 0
"
> >
<ul class="list-suggest add-items inline"> <ul class="list-suggest add-items inline">
<li <li

View File

@@ -39,11 +39,17 @@
<option selected disabled value=""> <option selected disabled value="">
{{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }} {{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }}
</option> </option>
<option v-for="t in locationTypes" :value="t" :key="t.id"> <option
v-for="t in locationTypes"
:value="t"
:key="t.id"
>
{{ localizeString(t.title) }} {{ localizeString(t.title) }}
</option> </option>
</select> </select>
<label>{{ trans(ACTIVITY_LOCATION_FIELDS_TYPE) }}</label> <label>{{
trans(ACTIVITY_LOCATION_FIELDS_TYPE)
}}</label>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
@@ -102,7 +108,10 @@
</form> </form>
</template> </template>
<template #footer> <template #footer>
<button class="btn btn-save" @click.prevent="saveNewLocation"> <button
class="btn btn-save"
@click.prevent="saveNewLocation"
>
{{ trans(SAVE) }} {{ trans(SAVE) }}
</button> </button>
</template> </template>
@@ -235,7 +244,8 @@ export default {
}, },
hasPhonenumber1() { hasPhonenumber1() {
return ( return (
this.selected.phonenumber1 !== null && this.selected.phonenumber1 !== "" this.selected.phonenumber1 !== null &&
this.selected.phonenumber1 !== ""
); );
}, },
showAddAddress() { showAddAddress() {

View File

@@ -49,7 +49,9 @@
</div> </div>
<div class="col-8"> <div class="col-8">
<div v-if="actionIsLoading === true"> <div v-if="actionIsLoading === true">
<i class="chill-green fa fa-circle-o-notch fa-spin fa-lg"></i> <i
class="chill-green fa fa-circle-o-notch fa-spin fa-lg"
></i>
</div> </div>
<span <span
@@ -62,7 +64,8 @@
<template <template
v-else-if=" v-else-if="
socialActionsList.length > 0 && socialActionsList.length > 0 &&
(socialIssuesSelected.length || socialActionsSelected.length) (socialIssuesSelected.length ||
socialActionsSelected.length)
" "
> >
<div <div
@@ -85,7 +88,9 @@
</template> </template>
<span <span
v-else-if="actionAreLoaded && socialActionsList.length === 0" v-else-if="
actionAreLoaded && socialActionsList.length === 0
"
class="inline-choice chill-no-data-statement mt-3" class="inline-choice chill-no-data-statement mt-3"
> >
{{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }} {{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }}
@@ -164,7 +169,8 @@ export default {
/* Add in list the issues already associated (if not yet listed) */ /* Add in list the issues already associated (if not yet listed) */
this.socialIssuesSelected.forEach((issue) => { this.socialIssuesSelected.forEach((issue) => {
if ( if (
this.socialIssuesList.filter((i) => i.id === issue.id).length !== 1 this.socialIssuesList.filter((i) => i.id === issue.id)
.length !== 1
) { ) {
this.$store.commit("addIssueInList", issue); this.$store.commit("addIssueInList", issue);
} }

View File

@@ -10,7 +10,9 @@
:value="issue" :value="issue"
/> />
<label class="form-check-label" :for="issue.id"> <label class="form-check-label" :for="issue.id">
<span class="badge bg-chill-l-gray text-dark">{{ issue.text }}</span> <span class="badge bg-chill-l-gray text-dark">{{
issue.text
}}</span>
</label> </label>
</div> </div>
</span> </span>

View File

@@ -68,7 +68,9 @@ export type EventInputCalendarRange = EventInput & {
export function isEventInputCalendarRange( export function isEventInputCalendarRange(
toBeDetermined: EventInputCalendarRange | EventInput, toBeDetermined: EventInputCalendarRange | EventInput,
): toBeDetermined is EventInputCalendarRange { ): toBeDetermined is EventInputCalendarRange {
return typeof toBeDetermined.is === "string" && toBeDetermined.is === "range"; return (
typeof toBeDetermined.is === "string" && toBeDetermined.is === "range"
);
} }
export {}; export {};

View File

@@ -61,14 +61,22 @@
<label class="input-group-text" for="slotDuration" <label class="input-group-text" for="slotDuration"
>Durée des créneaux</label >Durée des créneaux</label
> >
<select v-model="slotDuration" id="slotDuration" class="form-select"> <select
v-model="slotDuration"
id="slotDuration"
class="form-select"
>
<option value="00:05:00">5 minutes</option> <option value="00:05:00">5 minutes</option>
<option value="00:10:00">10 minutes</option> <option value="00:10:00">10 minutes</option>
<option value="00:15:00">15 minutes</option> <option value="00:15:00">15 minutes</option>
<option value="00:30:00">30 minutes</option> <option value="00:30:00">30 minutes</option>
</select> </select>
<label class="input-group-text" for="slotMinTime">De</label> <label class="input-group-text" for="slotMinTime">De</label>
<select v-model="slotMinTime" id="slotMinTime" class="form-select"> <select
v-model="slotMinTime"
id="slotMinTime"
class="form-select"
>
<option value="00:00:00">0h</option> <option value="00:00:00">0h</option>
<option value="01:00:00">1h</option> <option value="01:00:00">1h</option>
<option value="02:00:00">2h</option> <option value="02:00:00">2h</option>
@@ -84,7 +92,11 @@
<option value="12:00:00">12h</option> <option value="12:00:00">12h</option>
</select> </select>
<label class="input-group-text" for="slotMaxTime">À</label> <label class="input-group-text" for="slotMaxTime">À</label>
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select"> <select
v-model="slotMaxTime"
id="slotMaxTime"
class="form-select"
>
<option value="12:00:00">12h</option> <option value="12:00:00">12h</option>
<option value="13:00:00">13h</option> <option value="13:00:00">13h</option>
<option value="14:00:00">14h</option> <option value="14:00:00">14h</option>
@@ -112,7 +124,9 @@
v-model="hideWeekends" v-model="hideWeekends"
/> />
</span> </span>
<label for="showHideWE" class="form-check-label input-group-text" <label
for="showHideWE"
class="form-check-label input-group-text"
>Week-ends</label >Week-ends</label
> >
</div> </div>
@@ -128,7 +142,9 @@
<b v-else-if="arg.event.extendedProps.is === 'range'" <b v-else-if="arg.event.extendedProps.is === 'range'"
>{{ arg.timeText }} >{{ arg.timeText }}
{{ arg.event.extendedProps.locationName }} {{ arg.event.extendedProps.locationName }}
<small>{{ arg.event.extendedProps.userLabel }}</small></b <small>{{
arg.event.extendedProps.userLabel
}}</small></b
> >
<b v-else-if="arg.event.extendedProps.is === 'current'" <b v-else-if="arg.event.extendedProps.is === 'current'"
>{{ arg.timeText }} {{ $t("current_selected") }} >{{ arg.timeText }} {{ $t("current_selected") }}
@@ -136,7 +152,9 @@
<b v-else-if="arg.event.extendedProps.is === 'local'">{{ <b v-else-if="arg.event.extendedProps.is === 'local'">{{
arg.event.title arg.event.title
}}</b> }}</b>
<b v-else>{{ arg.timeText }} {{ $t("current_selected") }} </b> <b v-else
>{{ arg.timeText }} {{ $t("current_selected") }}
</b>
</span> </span>
</template> </template>
</FullCalendar> </FullCalendar>
@@ -250,7 +268,9 @@ export default {
this.$store.state.activity.endDate !== null) this.$store.state.activity.endDate !== null)
) { ) {
if ( if (
!window.confirm(this.$t("change_main_user_will_reset_event_data")) !window.confirm(
this.$t("change_main_user_will_reset_event_data"),
)
) { ) {
return; return;
} }
@@ -258,9 +278,13 @@ export default {
// add the previous user, if any, in the previous user list (in use for suggestion) // add the previous user, if any, in the previous user list (in use for suggestion)
if (null !== this.$store.getters.getMainUser) { if (null !== this.$store.getters.getMainUser) {
const suggestedUids = new Set(this.$data.previousUser.map((u) => u.id)); const suggestedUids = new Set(
this.$data.previousUser.map((u) => u.id),
);
if (!suggestedUids.has(this.$store.getters.getMainUser.id)) { if (!suggestedUids.has(this.$store.getters.getMainUser.id)) {
this.$data.previousUser.push(this.$store.getters.getMainUser); this.$data.previousUser.push(
this.$store.getters.getMainUser,
);
} }
} }
@@ -290,7 +314,8 @@ export default {
// show an alert if changing mainUser // show an alert if changing mainUser
if ( if (
(this.$store.getters.getMainUser !== null && (this.$store.getters.getMainUser !== null &&
this.$store.state.me.id !== this.$store.getters.getMainUser.id) || this.$store.state.me.id !==
this.$store.getters.getMainUser.id) ||
this.$store.getters.getMainUser === null this.$store.getters.getMainUser === null
) { ) {
if (!window.confirm(this.$t("will_change_main_user_for_me"))) { if (!window.confirm(this.$t("will_change_main_user_for_me"))) {
@@ -334,7 +359,9 @@ export default {
this.$store.getters.getMainUser.id this.$store.getters.getMainUser.id
) { ) {
if ( if (
!window.confirm(this.$t("this_calendar_range_will_change_main_user")) !window.confirm(
this.$t("this_calendar_range_will_change_main_user"),
)
) { ) {
return; return;
} }

View File

@@ -4,9 +4,18 @@
{{ user.text }} {{ user.text }}
<template v-if="invite !== null"> <template v-if="invite !== null">
<i v-if="invite.status === 'accepted'" class="fa fa-check" /> <i v-if="invite.status === 'accepted'" class="fa fa-check" />
<i v-else-if="invite.status === 'declined'" class="fa fa-times" /> <i
<i v-else-if="invite.status === 'pending'" class="fa fa-question-o" /> v-else-if="invite.status === 'declined'"
<i v-else-if="invite.status === 'tentative'" class="fa fa-question" /> class="fa fa-times"
/>
<i
v-else-if="invite.status === 'pending'"
class="fa fa-question-o"
/>
<i
v-else-if="invite.status === 'tentative'"
class="fa fa-question"
/>
<span v-else="">{{ invite.status }}</span> <span v-else="">{{ invite.status }}</span>
</template> </template>
</span> </span>
@@ -60,7 +69,8 @@ export default {
computed: { computed: {
style() { style() {
return { return {
backgroundColor: this.$store.getters.getUserData(this.user).mainColor, backgroundColor: this.$store.getters.getUserData(this.user)
.mainColor,
}; };
}, },
rangeShow: { rangeShow: {
@@ -71,7 +81,9 @@ export default {
}); });
}, },
get() { get() {
return this.$store.getters.isRangeShownOnCalendarForUser(this.user); return this.$store.getters.isRangeShownOnCalendarForUser(
this.user,
);
}, },
}, },
remoteShow: { remoteShow: {
@@ -82,7 +94,9 @@ export default {
}); });
}, },
get() { get() {
return this.$store.getters.isRemoteShownOnCalendarForUser(this.user); return this.$store.getters.isRemoteShownOnCalendarForUser(
this.user,
);
}, },
}, },
}, },

View File

@@ -22,25 +22,33 @@
</button> </button>
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1"> <ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
<li v-if="status !== Statuses.ACCEPTED"> <li v-if="status !== Statuses.ACCEPTED">
<a class="dropdown-item" @click="changeStatus(Statuses.ACCEPTED)" <a
><i class="fa fa-check" aria-hidden="true"></i> {{ $t("Accept") }}</a class="dropdown-item"
@click="changeStatus(Statuses.ACCEPTED)"
><i class="fa fa-check" aria-hidden="true"></i>
{{ $t("Accept") }}</a
> >
</li> </li>
<li v-if="status !== Statuses.DECLINED"> <li v-if="status !== Statuses.DECLINED">
<a class="dropdown-item" @click="changeStatus(Statuses.DECLINED)" <a
><i class="fa fa-times" aria-hidden="true"></i> {{ $t("Decline") }}</a class="dropdown-item"
@click="changeStatus(Statuses.DECLINED)"
><i class="fa fa-times" aria-hidden="true"></i>
{{ $t("Decline") }}</a
> >
</li> </li>
<li v-if="status !== Statuses.TENTATIVELY_ACCEPTED"> <li v-if="status !== Statuses.TENTATIVELY_ACCEPTED">
<a <a
class="dropdown-item" class="dropdown-item"
@click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)" @click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)"
><i class="fa fa-question"></i> {{ $t("Tentatively_accept") }}</a ><i class="fa fa-question"></i>
{{ $t("Tentatively_accept") }}</a
> >
</li> </li>
<li v-if="status !== Statuses.PENDING"> <li v-if="status !== Statuses.PENDING">
<a class="dropdown-item" @click="changeStatus(Statuses.PENDING)" <a class="dropdown-item" @click="changeStatus(Statuses.PENDING)"
><i class="fa fa-hourglass-o"></i> {{ $t("Set_pending") }}</a ><i class="fa fa-hourglass-o"></i>
{{ $t("Set_pending") }}</a
> >
</li> </li>
</ul> </ul>
@@ -83,7 +91,9 @@ export default defineComponent({
}, },
}, },
emits: { emits: {
statusChanged(payload: "accepted" | "declined" | "pending" | "tentative") { statusChanged(
payload: "accepted" | "declined" | "pending" | "tentative",
) {
return true; return true;
}, },
}, },

View File

@@ -23,14 +23,22 @@
<label class="input-group-text" for="slotDuration" <label class="input-group-text" for="slotDuration"
>Durée des créneaux</label >Durée des créneaux</label
> >
<select v-model="slotDuration" id="slotDuration" class="form-select"> <select
v-model="slotDuration"
id="slotDuration"
class="form-select"
>
<option value="00:05:00">5 minutes</option> <option value="00:05:00">5 minutes</option>
<option value="00:10:00">10 minutes</option> <option value="00:10:00">10 minutes</option>
<option value="00:15:00">15 minutes</option> <option value="00:15:00">15 minutes</option>
<option value="00:30:00">30 minutes</option> <option value="00:30:00">30 minutes</option>
</select> </select>
<label class="input-group-text" for="slotMinTime">De</label> <label class="input-group-text" for="slotMinTime">De</label>
<select v-model="slotMinTime" id="slotMinTime" class="form-select"> <select
v-model="slotMinTime"
id="slotMinTime"
class="form-select"
>
<option value="00:00:00">0h</option> <option value="00:00:00">0h</option>
<option value="01:00:00">1h</option> <option value="01:00:00">1h</option>
<option value="02:00:00">2h</option> <option value="02:00:00">2h</option>
@@ -46,7 +54,11 @@
<option value="12:00:00">12h</option> <option value="12:00:00">12h</option>
</select> </select>
<label class="input-group-text" for="slotMaxTime">À</label> <label class="input-group-text" for="slotMaxTime">À</label>
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select"> <select
v-model="slotMaxTime"
id="slotMaxTime"
class="form-select"
>
<option value="12:00:00">12h</option> <option value="12:00:00">12h</option>
<option value="13:00:00">13h</option> <option value="13:00:00">13h</option>
<option value="14:00:00">14h</option> <option value="14:00:00">14h</option>
@@ -74,7 +86,9 @@
v-model="showWeekends" v-model="showWeekends"
/> />
</span> </span>
<label for="showHideWE" class="form-check-label input-group-text" <label
for="showHideWE"
class="form-check-label input-group-text"
>Week-ends</label >Week-ends</label
> >
</div> </div>
@@ -84,12 +98,16 @@
<FullCalendar :options="calendarOptions" ref="calendarRef"> <FullCalendar :options="calendarOptions" ref="calendarRef">
<template v-slot:eventContent="{ event }: { event: EventApi }"> <template v-slot:eventContent="{ event }: { event: EventApi }">
<span :class="eventClasses"> <span :class="eventClasses">
<b v-if="event.extendedProps.is === 'remote'">{{ event.title }}</b> <b v-if="event.extendedProps.is === 'remote'">{{
event.title
}}</b>
<b v-else-if="event.extendedProps.is === 'range'" <b v-else-if="event.extendedProps.is === 'range'"
>{{ formatDate(event.startStr) }} - >{{ formatDate(event.startStr) }} -
{{ event.extendedProps.locationName }}</b {{ event.extendedProps.locationName }}</b
> >
<b v-else-if="event.extendedProps.is === 'local'">{{ event.title }}</b> <b v-else-if="event.extendedProps.is === 'local'">{{
event.title
}}</b>
<b v-else>no 'is'</b> <b v-else>no 'is'</b>
<a <a
v-if="event.extendedProps.is === 'range'" v-if="event.extendedProps.is === 'range'"
@@ -108,7 +126,11 @@
<h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6> <h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6>
</div> </div>
<div class="col-xs-12 col-sm-9 col-md-2"> <div class="col-xs-12 col-sm-9 col-md-2">
<select v-model="dayOrWeek" id="dayOrWeek" class="form-select"> <select
v-model="dayOrWeek"
id="dayOrWeek"
class="form-select"
>
<option value="day">{{ $t("from_day_to_day") }}</option> <option value="day">{{ $t("from_day_to_day") }}</option>
<option value="week"> <option value="week">
{{ $t("from_week_to_week") }} {{ $t("from_week_to_week") }}
@@ -117,16 +139,27 @@
</div> </div>
<template v-if="dayOrWeek === 'day'"> <template v-if="dayOrWeek === 'day'">
<div class="col-xs-12 col-sm-3 col-md-3"> <div class="col-xs-12 col-sm-3 col-md-3">
<input class="form-control" type="date" v-model="copyFrom" /> <input
class="form-control"
type="date"
v-model="copyFrom"
/>
</div> </div>
<div class="col-xs-12 col-sm-1 col-md-1 copy-chevron"> <div class="col-xs-12 col-sm-1 col-md-1 copy-chevron">
<i class="fa fa-angle-double-right"></i> <i class="fa fa-angle-double-right"></i>
</div> </div>
<div class="col-xs-12 col-sm-3 col-md-3"> <div class="col-xs-12 col-sm-3 col-md-3">
<input class="form-control" type="date" v-model="copyTo" /> <input
class="form-control"
type="date"
v-model="copyTo"
/>
</div> </div>
<div class="col-xs-12 col-sm-5 col-md-1"> <div class="col-xs-12 col-sm-5 col-md-1">
<button class="btn btn-action float-end" @click="copyDay"> <button
class="btn btn-action float-end"
@click="copyDay"
>
{{ $t("copy_range") }} {{ $t("copy_range") }}
</button> </button>
</div> </div>
@@ -138,7 +171,11 @@
id="copyFromWeek" id="copyFromWeek"
class="form-select" class="form-select"
> >
<option v-for="w in lastWeeks" :value="w.value" :key="w.value"> <option
v-for="w in lastWeeks"
:value="w.value"
:key="w.value"
>
{{ w.text }} {{ w.text }}
</option> </option>
</select> </select>
@@ -147,14 +184,25 @@
<i class="fa fa-angle-double-right"></i> <i class="fa fa-angle-double-right"></i>
</div> </div>
<div class="col-xs-12 col-sm-3 col-md-3"> <div class="col-xs-12 col-sm-3 col-md-3">
<select v-model="copyToWeek" id="copyToWeek" class="form-select"> <select
<option v-for="w in nextWeeks" :value="w.value" :key="w.value"> v-model="copyToWeek"
id="copyToWeek"
class="form-select"
>
<option
v-for="w in nextWeeks"
:value="w.value"
:key="w.value"
>
{{ w.text }} {{ w.text }}
</option> </option>
</select> </select>
</div> </div>
<div class="col-xs-12 col-sm-5 col-md-1"> <div class="col-xs-12 col-sm-5 col-md-1">
<button class="btn btn-action float-end" @click="copyWeek"> <button
class="btn btn-action float-end"
@click="copyWeek"
>
{{ $t("copy_range") }} {{ $t("copy_range") }}
</button> </button>
</div> </div>

View File

@@ -41,7 +41,9 @@ const futureStore = function (): Promise<Store<State>> {
}); });
store.commit("me/setWhoAmi", user, { root: true }); store.commit("me/setWhoAmi", user, { root: true });
store.dispatch("locations/getLocations", null, { root: true }).then((_) => { store
.dispatch("locations/getLocations", null, { root: true })
.then((_) => {
return store.dispatch("locations/getCurrentLocation", null, { return store.dispatch("locations/getCurrentLocation", null, {
root: true, root: true,
}); });

View File

@@ -29,7 +29,10 @@ export default {
(state: CalendarLocalsState) => (state: CalendarLocalsState) =>
({ start, end }: { start: Date; end: Date }): boolean => { ({ start, end }: { start: Date; end: Date }): boolean => {
for (const range of state.localsLoaded) { for (const range of state.localsLoaded) {
if (start.getTime() === range.start && end.getTime() === range.end) { if (
start.getTime() === range.start &&
end.getTime() === range.end
) {
return true; return true;
} }
} }
@@ -51,7 +54,10 @@ export default {
}); });
state.key = state.key + toAdd.length; state.key = state.key + toAdd.length;
}, },
addLoaded(state: CalendarLocalsState, payload: { start: Date; end: Date }) { addLoaded(
state: CalendarLocalsState,
payload: { start: Date; end: Date },
) {
state.localsLoaded.push({ state.localsLoaded.push({
start: payload.start.getTime(), start: payload.start.getTime(),
end: payload.end.getTime(), end: payload.end.getTime(),
@@ -79,7 +85,11 @@ export default {
end: end, end: end,
}); });
return fetchCalendarLocalForUser(ctx.rootGetters["me/getMe"], start, end) return fetchCalendarLocalForUser(
ctx.rootGetters["me/getMe"],
start,
end,
)
.then((remotes: CalendarLight[]) => { .then((remotes: CalendarLight[]) => {
// to be add when reactivity problem will be solve ? // to be add when reactivity problem will be solve ?
//ctx.commit('addRemotes', remotes); //ctx.commit('addRemotes', remotes);

View File

@@ -40,7 +40,10 @@ export default {
(state: CalendarRangesState) => (state: CalendarRangesState) =>
({ start, end }: { start: Date; end: Date }): boolean => { ({ start, end }: { start: Date; end: Date }): boolean => {
for (const range of state.rangesLoaded) { for (const range of state.rangesLoaded) {
if (start.getTime() === range.start && end.getTime() === range.end) { if (
start.getTime() === range.start &&
end.getTime() === range.end
) {
return true; return true;
} }
} }
@@ -107,7 +110,9 @@ export default {
state: CalendarRangesState, state: CalendarRangesState,
externalEvents: (EventInput & { id: string })[], externalEvents: (EventInput & { id: string })[],
) { ) {
const toAdd = externalEvents.filter((r) => !state.rangesIndex.has(r.id)); const toAdd = externalEvents.filter(
(r) => !state.rangesIndex.has(r.id),
);
toAdd.forEach((r) => { toAdd.forEach((r) => {
state.rangesIndex.add(r.id); state.rangesIndex.add(r.id);
@@ -115,7 +120,10 @@ export default {
}); });
state.key = state.key + toAdd.length; state.key = state.key + toAdd.length;
}, },
addLoaded(state: CalendarRangesState, payload: { start: Date; end: Date }) { addLoaded(
state: CalendarRangesState,
payload: { start: Date; end: Date },
) {
state.rangesLoaded.push({ state.rangesLoaded.push({
start: payload.start.getTime(), start: payload.start.getTime(),
end: payload.end.getTime(), end: payload.end.getTime(),
@@ -134,12 +142,17 @@ export default {
}, },
removeRange(state: CalendarRangesState, calendarRangeId: number) { removeRange(state: CalendarRangesState, calendarRangeId: number) {
const found = state.ranges.find( const found = state.ranges.find(
(r) => r.calendarRangeId === calendarRangeId && r.is === "range", (r) =>
r.calendarRangeId === calendarRangeId && r.is === "range",
); );
if (found !== undefined) { if (found !== undefined) {
state.ranges = state.ranges.filter( state.ranges = state.ranges.filter(
(r) => !(r.calendarRangeId === calendarRangeId && r.is === "range"), (r) =>
!(
r.calendarRangeId === calendarRangeId &&
r.is === "range"
),
); );
if (typeof found.id === "string") { if (typeof found.id === "string") {
@@ -198,7 +211,11 @@ export default {
}, },
createRange( createRange(
ctx: Context, ctx: Context,
{ start, end, location }: { start: Date; end: Date; location: Location }, {
start,
end,
location,
}: { start: Date; end: Date; location: Location },
): Promise<null> { ): Promise<null> {
const url = `/api/1.0/calendar/calendar-range.json?`; const url = `/api/1.0/calendar/calendar-range.json?`;
@@ -223,7 +240,11 @@ export default {
}, },
} as CalendarRangeCreate; } as CalendarRangeCreate;
return makeFetch<CalendarRangeCreate, CalendarRange>("POST", url, body) return makeFetch<CalendarRangeCreate, CalendarRange>(
"POST",
url,
body,
)
.then((newRange) => { .then((newRange) => {
ctx.commit("addRange", newRange); ctx.commit("addRange", newRange);
@@ -260,7 +281,11 @@ export default {
}, },
} as CalendarRangeEdit; } as CalendarRangeEdit;
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body) return makeFetch<CalendarRangeEdit, CalendarRange>(
"PATCH",
url,
body,
)
.then((range) => { .then((range) => {
ctx.commit("updateRange", range); ctx.commit("updateRange", range);
return Promise.resolve(null); return Promise.resolve(null);
@@ -285,7 +310,11 @@ export default {
}, },
} as CalendarRangeEdit; } as CalendarRangeEdit;
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body) return makeFetch<CalendarRangeEdit, CalendarRange>(
"PATCH",
url,
body,
)
.then((range) => { .then((range) => {
ctx.commit("updateRange", range); ctx.commit("updateRange", range);
return Promise.resolve(null); return Promise.resolve(null);
@@ -305,14 +334,20 @@ export default {
for (const r of rangesToCopy) { for (const r of rangesToCopy) {
const start = new Date(ISOToDatetime(r.start) as Date); const start = new Date(ISOToDatetime(r.start) as Date);
start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate()); start.setFullYear(
to.getFullYear(),
to.getMonth(),
to.getDate(),
);
const end = new Date(ISOToDatetime(r.end) as Date); const end = new Date(ISOToDatetime(r.end) as Date);
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate()); end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
const location = ctx.rootGetters["locations/getLocationById"]( const location = ctx.rootGetters["locations/getLocationById"](
r.locationId, r.locationId,
); );
promises.push(ctx.dispatch("createRange", { start, end, location })); promises.push(
ctx.dispatch("createRange", { start, end, location }),
);
} }
return Promise.all(promises).then(() => Promise.resolve(null)); return Promise.all(promises).then(() => Promise.resolve(null));
@@ -334,7 +369,9 @@ export default {
r.locationId, r.locationId,
); );
promises.push(ctx.dispatch("createRange", { start, end, location })); promises.push(
ctx.dispatch("createRange", { start, end, location }),
);
} }
return Promise.all(promises).then(() => Promise.resolve(null)); return Promise.all(promises).then(() => Promise.resolve(null));

View File

@@ -29,7 +29,10 @@ export default {
(state: CalendarRemotesState) => (state: CalendarRemotesState) =>
({ start, end }: { start: Date; end: Date }): boolean => { ({ start, end }: { start: Date; end: Date }): boolean => {
for (const range of state.remotesLoaded) { for (const range of state.remotesLoaded) {
if (start.getTime() === range.start && end.getTime() === range.end) { if (
start.getTime() === range.start &&
end.getTime() === range.end
) {
return true; return true;
} }
} }
@@ -82,7 +85,11 @@ export default {
end: end, end: end,
}); });
return fetchCalendarRemoteForUser(ctx.rootGetters["me/getMe"], start, end) return fetchCalendarRemoteForUser(
ctx.rootGetters["me/getMe"],
start,
end,
)
.then((remotes: CalendarRemote[]) => { .then((remotes: CalendarRemote[]) => {
// to be add when reactivity problem will be solve ? // to be add when reactivity problem will be solve ?
//ctx.commit('addRemotes', remotes); //ctx.commit('addRemotes', remotes);

View File

@@ -112,8 +112,11 @@ export default {
results.forEach((i) => { results.forEach((i) => {
if (!users.some((j) => i.user.id === j.id)) { if (!users.some((j) => i.user.id === j.id)) {
let ratio = Math.floor(users.length / COLORS.length); let ratio = Math.floor(
let colorIndex = users.length - ratio * COLORS.length; users.length / COLORS.length,
);
let colorIndex =
users.length - ratio * COLORS.length;
users.push({ users.push({
id: i.user.id, id: i.user.id,
username: i.user.username, username: i.user.username,
@@ -150,29 +153,45 @@ export default {
(me) => (me) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
this.users.logged = me; this.users.logged = me;
let currentUser = users.find((u) => u.id === me.id); let currentUser = users.find(
(u) => u.id === me.id,
);
this.value = currentUser; this.value = currentUser;
fetchCalendar(currentUser.id).then( fetchCalendar(currentUser.id).then(
(calendar) => (calendar) =>
new Promise((resolve, reject) => { new Promise(
let results = calendar.results; (resolve, reject) => {
let events = results.map((i) => ({ let results =
start: i.startDate.datetime, calendar.results;
end: i.endDate.datetime, let events =
})); results.map(
let calendarEventsCurrentUser = { (i) => ({
start: i
.startDate
.datetime,
end: i
.endDate
.datetime,
}),
);
let calendarEventsCurrentUser =
{
events: events, events: events,
color: "darkblue", color: "darkblue",
id: 1000, id: 1000,
editable: false, editable: false,
}; };
this.calendarEvents.user = calendarEventsCurrentUser; this.calendarEvents.user =
calendarEventsCurrentUser;
this.selectUsers(currentUser); this.selectUsers(
currentUser,
);
resolve(); resolve();
}), },
),
); );
resolve(); resolve();
@@ -190,7 +209,9 @@ export default {
return `${value.username}`; return `${value.username}`;
}, },
coloriseSelectedValues() { coloriseSelectedValues() {
let tags = document.querySelectorAll("div.multiselect__tags-wrap")[0]; let tags = document.querySelectorAll(
"div.multiselect__tags-wrap",
)[0];
if (tags.hasChildNodes()) { if (tags.hasChildNodes()) {
let children = tags.childNodes; let children = tags.childNodes;
@@ -211,8 +232,8 @@ export default {
}, },
selectEvents() { selectEvents() {
let selectedUsersId = this.users.selected.map((a) => a.id); let selectedUsersId = this.users.selected.map((a) => a.id);
this.calendarEvents.selected = this.calendarEvents.loaded.filter((a) => this.calendarEvents.selected = this.calendarEvents.loaded.filter(
selectedUsersId.includes(a.id), (a) => selectedUsersId.includes(a.id),
); );
}, },
selectUsers(value) { selectUsers(value) {
@@ -222,7 +243,9 @@ export default {
this.updateEventsSource(); this.updateEventsSource();
}, },
unSelectUsers(value) { unSelectUsers(value) {
this.users.selected = this.users.selected.filter((a) => a.id != value.id); this.users.selected = this.users.selected.filter(
(a) => a.id != value.id,
);
this.selectEvents(); this.selectEvents();
this.updateEventsSource(); this.updateEventsSource();
}, },

View File

@@ -24,7 +24,11 @@ use Doctrine\ORM\EntityManagerInterface;
class CalendarForShortMessageProvider class CalendarForShortMessageProvider
{ {
public function __construct(private readonly CalendarRepository $calendarRepository, private readonly EntityManagerInterface $em, private readonly RangeGeneratorInterface $rangeGenerator) {} public function __construct(
private readonly CalendarRepository $calendarRepository,
private readonly EntityManagerInterface $em,
private readonly RangeGeneratorInterface $rangeGenerator,
) {}
/** /**
* Generate calendars instance. * Generate calendars instance.

View File

@@ -21,7 +21,6 @@ namespace Chill\CalendarBundle\Tests\Service\ShortMessageNotification;
use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Repository\CalendarRepository; use Chill\CalendarBundle\Repository\CalendarRepository;
use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider; use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider;
use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultRangeGenerator;
use Chill\CalendarBundle\Service\ShortMessageNotification\RangeGeneratorInterface; use Chill\CalendarBundle\Service\ShortMessageNotification\RangeGeneratorInterface;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@@ -82,10 +81,16 @@ final class CalendarForShortMessageProviderTest extends TestCase
$em = $this->prophesize(EntityManagerInterface::class); $em = $this->prophesize(EntityManagerInterface::class);
$em->clear()->shouldBeCalled(); $em->clear()->shouldBeCalled();
$calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
$calendarRangeGenerator->generateRange(Argument::any())->willReturn([
'startDate' => new \DateTimeImmutable('yesterday'),
'endDate' => new \DateTimeImmutable('now'),
]);
$provider = new CalendarForShortMessageProvider( $provider = new CalendarForShortMessageProvider(
$calendarRepository->reveal(), $calendarRepository->reveal(),
$em->reveal(), $em->reveal(),
new DefaultRangeGenerator() $calendarRangeGenerator->reveal(),
); );
$calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now'))); $calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now')));
@@ -103,26 +108,32 @@ final class CalendarForShortMessageProviderTest extends TestCase
Argument::type(\DateTimeImmutable::class), Argument::type(\DateTimeImmutable::class),
Argument::type('int'), Argument::type('int'),
Argument::exact(0) Argument::exact(0)
)->will(static fn ($args) => array_fill(0, 1, new Calendar()))->shouldBeCalledTimes(1); )->will(static fn ($args) => array_fill(0, 10, new Calendar()))->shouldBeCalledTimes(1);
$calendarRepository->findByNotificationAvailable( $calendarRepository->findByNotificationAvailable(
Argument::type(\DateTimeImmutable::class), Argument::type(\DateTimeImmutable::class),
Argument::type(\DateTimeImmutable::class), Argument::type(\DateTimeImmutable::class),
Argument::type('int'), Argument::type('int'),
Argument::not(0) Argument::exact(10)
)->will(static fn ($args) => [])->shouldBeCalledTimes(1); )->will(static fn ($args) => [])->shouldBeCalledTimes(1);
$em = $this->prophesize(EntityManagerInterface::class); $em = $this->prophesize(EntityManagerInterface::class);
$em->clear()->shouldBeCalled(); $em->clear()->shouldBeCalled();
$calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
$calendarRangeGenerator->generateRange(Argument::any())->willReturn([
'startDate' => new \DateTimeImmutable('yesterday'),
'endDate' => new \DateTimeImmutable('now'),
]);
$provider = new CalendarForShortMessageProvider( $provider = new CalendarForShortMessageProvider(
$calendarRepository->reveal(), $calendarRepository->reveal(),
$em->reveal(), $em->reveal(),
new DefaultRangeGenerator() $calendarRangeGenerator->reveal(),
); );
$calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now'))); $calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now')));
$this->assertEquals(1, \count($calendars)); $this->assertEquals(10, \count($calendars));
$this->assertContainsOnly(Calendar::class, $calendars); $this->assertContainsOnly(Calendar::class, $calendars);
} }
} }

View File

@@ -0,0 +1,29 @@
<?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\CustomFieldsBundle\EntityRepository;
use Chill\CustomFieldsBundle\Entity\CustomFieldsDefaultGroup;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class CustomFieldsDefaultGroupRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, CustomFieldsDefaultGroup::class);
}
public function findOneByEntity(string $className): ?CustomFieldsDefaultGroup
{
return $this->findOneBy(['entity' => $className]);
}
}

View File

@@ -127,3 +127,7 @@ services:
factory: ["@doctrine", getRepository] factory: ["@doctrine", getRepository]
arguments: arguments:
- "Chill\\CustomFieldsBundle\\Entity\\CustomFieldLongChoice\\Option" - "Chill\\CustomFieldsBundle\\Entity\\CustomFieldLongChoice\\Option"
Chill\CustomFieldsBundle\EntityRepository\CustomFieldsDefaultGroupRepository:
autowire: true
autoconfigure: true

View File

@@ -20,7 +20,10 @@
</option> </option>
<template v-for="t in templates" :key="t.id"> <template v-for="t in templates" :key="t.id">
<option :value="t.id"> <option :value="t.id">
{{ localizeString(t.name) || "Aucun nom défini" }} {{
localizeString(t.name) ||
"Aucun nom défini"
}}
</option> </option>
</template> </template>
</select> </select>
@@ -28,7 +31,9 @@
v-if="canGenerate" v-if="canGenerate"
class="btn btn-update btn-sm change-icon" class="btn btn-update btn-sm change-icon"
:href="buildUrlGenerate" :href="buildUrlGenerate"
@click.prevent="clickGenerate($event, buildUrlGenerate)" @click.prevent="
clickGenerate($event, buildUrlGenerate)
"
><i class="fa fa-fw fa-cog" ><i class="fa fa-fw fa-cog"
/></a> /></a>
<a <a

View File

@@ -13,9 +13,8 @@ const startApp = (
const inputTitle = collectionEntry?.querySelector("input[type='text']"); const inputTitle = collectionEntry?.querySelector("input[type='text']");
const input_stored_object: HTMLInputElement | null = divElement.querySelector( const input_stored_object: HTMLInputElement | null =
"input[data-stored-object]", divElement.querySelector("input[data-stored-object]");
);
if (null === input_stored_object) { if (null === input_stored_object) {
throw new Error("input to stored object not found"); throw new Error("input to stored object not found");
} }
@@ -54,7 +53,9 @@ const startApp = (
console.log("version added", stored_object_version); console.log("version added", stored_object_version);
this.$data.existingDoc = stored_object; this.$data.existingDoc = stored_object;
this.$data.existingDoc.currentVersion = stored_object_version; this.$data.existingDoc.currentVersion = stored_object_version;
input_stored_object.value = JSON.stringify(this.$data.existingDoc); input_stored_object.value = JSON.stringify(
this.$data.existingDoc,
);
if (this.$data.inputTitle) { if (this.$data.inputTitle) {
if (!this.$data.inputTitle?.value) { if (!this.$data.inputTitle?.value) {
this.$data.inputTitle.value = file_name; this.$data.inputTitle.value = file_name;

View File

@@ -49,7 +49,9 @@
<li v-if="isHistoryViewable"> <li v-if="isHistoryViewable">
<history-button <history-button
:stored-object="props.storedObject" :stored-object="props.storedObject"
:can-edit="canEdit && props.storedObject._permissions.canEdit" :can-edit="
canEdit && props.storedObject._permissions.canEdit
"
></history-button> ></history-button>
</li> </li>
</ul> </ul>
@@ -127,7 +129,9 @@ const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
canDownload: true, canDownload: true,
canConvertPdf: true, canConvertPdf: true,
returnPath: returnPath:
window.location.pathname + window.location.search + window.location.hash, window.location.pathname +
window.location.search +
window.location.hash,
}); });
/** /**

View File

@@ -29,7 +29,9 @@
</modal> </modal>
</teleport> </teleport>
<div class="col-12 m-auto sticky-top"> <div class="col-12 m-auto sticky-top">
<div class="row justify-content-center border-bottom pdf-tools d-md-none"> <div
class="row justify-content-center border-bottom pdf-tools d-md-none"
>
<div class="col-5 text-center turn-page"> <div class="col-5 text-center turn-page">
<select <select
class="form-select form-select-sm" class="form-select form-select-sm"
@@ -90,7 +92,10 @@
v-if="signature.zones.length === 1 && signedState !== 'signed'" v-if="signature.zones.length === 1 && signedState !== 'signed'"
class="col-5 p-0 text-center turnSignature" class="col-5 p-0 text-center turnSignature"
> >
<button class="btn btn-light btn-sm" @click="goToSignatureZoneUnique"> <button
class="btn btn-light btn-sm"
@click="goToSignatureZoneUnique"
>
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }} {{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
</button> </button>
</div> </div>
@@ -145,7 +150,10 @@
:title="trans(SIGNATURES_ADD_SIGN_ZONE)" :title="trans(SIGNATURES_ADD_SIGN_ZONE)"
> >
<template v-if="canvasEvent === 'add'"> <template v-if="canvasEvent === 'add'">
<div class="spinner-border spinner-border-sm" role="status"> <div
class="spinner-border spinner-border-sm"
role="status"
>
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
</template> </template>
@@ -199,7 +207,10 @@
v-if="signature.zones.length === 1 && signedState !== 'signed'" v-if="signature.zones.length === 1 && signedState !== 'signed'"
class="col-4 d-xl-none text-center turnSignature p-0" class="col-4 d-xl-none text-center turnSignature p-0"
> >
<button class="btn btn-light btn-sm" @click="goToSignatureZoneUnique"> <button
class="btn btn-light btn-sm"
@click="goToSignatureZoneUnique"
>
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }} {{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
</button> </button>
</div> </div>
@@ -227,7 +238,10 @@
v-if="signature.zones.length === 1 && signedState !== 'signed'" v-if="signature.zones.length === 1 && signedState !== 'signed'"
class="col-4 d-none d-xl-flex p-0 text-center turnSignature" class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
> >
<button class="btn btn-light btn-sm" @click="goToSignatureZoneUnique"> <button
class="btn btn-light btn-sm"
@click="goToSignatureZoneUnique"
>
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }} {{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
</button> </button>
</div> </div>
@@ -285,7 +299,10 @@
</template> </template>
<template v-else> <template v-else>
{{ trans(SIGNATURES_CLICK_ON_DOCUMENT) }} {{ trans(SIGNATURES_CLICK_ON_DOCUMENT) }}
<div class="spinner-border spinner-border-sm" role="status"> <div
class="spinner-border spinner-border-sm"
role="status"
>
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
</template> </template>
@@ -545,8 +562,14 @@ const addCanvasEvents = () => {
); );
}); });
} else { } else {
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement; const canvas = document.querySelectorAll(
canvas.addEventListener("pointerup", (e) => canvasClick(e, canvas), false); "canvas",
)[0] as HTMLCanvasElement;
canvas.addEventListener(
"pointerup",
(e) => canvasClick(e, canvas),
false,
);
} }
}; };
@@ -582,7 +605,11 @@ const hitSignature = (
scaleYToCanvas(zone.y, canvas.height, zone.PDFPage.height) < scaleYToCanvas(zone.y, canvas.height, zone.PDFPage.height) <
xy[1] && xy[1] &&
xy[1] < xy[1] <
scaleYToCanvas(zone.height - zone.y, canvas.height, zone.PDFPage.height) + scaleYToCanvas(
zone.height - zone.y,
canvas.height,
zone.PDFPage.height,
) +
zone.PDFPage.height * zoom.value; zone.PDFPage.height * zoom.value;
const selectZone = async (z: SignatureZone, canvas: HTMLCanvasElement) => { const selectZone = async (z: SignatureZone, canvas: HTMLCanvasElement) => {
@@ -598,7 +625,8 @@ const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
signature.zones signature.zones
.filter( .filter(
(z) => (z) =>
(z.PDFPage.index + 1 === getCanvasId(canvas) && multiPage.value) || (z.PDFPage.index + 1 === getCanvasId(canvas) &&
multiPage.value) ||
(z.PDFPage.index + 1 === page.value && !multiPage.value), (z.PDFPage.index + 1 === page.value && !multiPage.value),
) )
.map((z) => { .map((z) => {

View File

@@ -153,10 +153,12 @@ const handleFile = async (file: File): Promise<void> => {
</p> </p>
<!-- todo i18n --> <!-- todo i18n -->
<p v-if="has_existing_doc"> <p v-if="has_existing_doc">
Déposez un document ou cliquez ici pour remplacer le document existant Déposez un document ou cliquez ici pour remplacer le document
existant
</p> </p>
<p v-else> <p v-else>
Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier Déposez un document ou cliquez ici pour ouvrir le navigateur de
fichier
</p> </p>
</div> </div>
<div v-else class="waiting"> <div v-else class="waiting">

View File

@@ -4,6 +4,7 @@ import { StoredObject, StoredObjectVersion } from "../../types";
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue"; import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
import { computed, reactive } from "vue"; import { computed, reactive } from "vue";
import { useToast } from "vue-toast-notification"; import { useToast } from "vue-toast-notification";
import { DOCUMENT_REPLACE, DOCUMENT_ADD, trans } from "translator";
interface DropFileConfig { interface DropFileConfig {
allowRemove: boolean; allowRemove: boolean;
@@ -75,10 +76,10 @@ function closeModal(): void {
@click="openModal" @click="openModal"
class="btn btn-create" class="btn btn-create"
> >
Ajouter un document {{ trans(DOCUMENT_ADD) }}
</button> </button>
<button v-else @click="openModal" class="btn btn-edit"> <button v-else @click="openModal" class="dropdown-item">
Remplacer le document {{ trans(DOCUMENT_REPLACE) }}
</button> </button>
<modal <modal
v-if="state.showModal" v-if="state.showModal"

View File

@@ -35,7 +35,9 @@ async function download_and_open(event: Event): Promise<void> {
if (null === state.content) { if (null === state.content) {
event.preventDefault(); event.preventDefault();
const raw = await download_doc(build_convert_link(props.storedObject.uuid)); const raw = await download_doc(
build_convert_link(props.storedObject.uuid),
);
state.content = window.URL.createObjectURL(raw); state.content = window.URL.createObjectURL(raw);
button.href = window.URL.createObjectURL(raw); button.href = window.URL.createObjectURL(raw);

View File

@@ -42,7 +42,9 @@ const editionUntilFormatted = computed<string>(() => {
<modal v-if="state.modalOpened" @close="state.modalOpened = false"> <modal v-if="state.modalOpened" @close="state.modalOpened = false">
<template v-slot:body> <template v-slot:body>
<div class="desktop-edit"> <div class="desktop-edit">
<p class="center">Veuillez enregistrer vos modifications avant le</p> <p class="center">
Veuillez enregistrer vos modifications avant le
</p>
<p> <p>
<strong>{{ editionUntilFormatted }}</strong> <strong>{{ editionUntilFormatted }}</strong>
</p> </p>
@@ -55,21 +57,23 @@ const editionUntilFormatted = computed<string>(() => {
<p> <p>
<small <small
>Le document peut être édité uniquement en utilisant Libre >Le document peut être édité uniquement en utilisant
Office.</small Libre Office.</small
> >
</p> </p>
<p> <p>
<small <small
>En cas d'échec lors de l'enregistrement, sauver le document sur >En cas d'échec lors de l'enregistrement, sauver le
le poste de travail avant de le déposer à nouveau ici.</small document sur le poste de travail avant de le déposer
à nouveau ici.</small
> >
</p> </p>
<p> <p>
<small <small
>Vous pouvez naviguez sur d'autres pages pendant l'édition.</small >Vous pouvez naviguez sur d'autres pages pendant
l'édition.</small
> >
</p> </p>
</div> </div>

View File

@@ -95,7 +95,10 @@ async function download_and_open(): Promise<void> {
let raw; let raw;
try { try {
raw = await download_and_decrypt_doc(props.storedObject, props.atVersion); raw = await download_and_decrypt_doc(
props.storedObject,
props.atVersion,
);
} catch (e) { } catch (e) {
console.error("error while downloading and decrypting document"); console.error("error while downloading and decrypting document");
console.error(e); console.error(e);

View File

@@ -49,7 +49,8 @@ const isRestored = computed<boolean>(
); );
const isDuplicated = computed<boolean>( const isDuplicated = computed<boolean>(
() => props.version.version === 0 && null !== props.version["from-restored"], () =>
props.version.version === 0 && null !== props.version["from-restored"],
); );
const classes = computed<{ const classes = computed<{
@@ -69,9 +70,16 @@ const classes = computed<{
<div :class="classes"> <div :class="classes">
<div <div
class="col-12 tags" class="col-12 tags"
v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated" v-if="
isCurrent ||
isKeptBeforeConversion ||
isRestored ||
isDuplicated
"
>
<span class="badge bg-success" v-if="isCurrent"
>Version actuelle</span
> >
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span>
<span class="badge bg-info" v-if="isKeptBeforeConversion" <span class="badge bg-info" v-if="isKeptBeforeConversion"
>Conservée avant conversion dans un autre format</span >Conservée avant conversion dans un autre format</span
> >
@@ -88,17 +96,21 @@ const classes = computed<{
<span <span
><strong>&nbsp;#{{ version.version + 1 }}&nbsp;</strong></span ><strong>&nbsp;#{{ version.version + 1 }}&nbsp;</strong></span
> >
<template v-if="version.createdBy !== null && version.createdAt !== null" <template
v-if="version.createdBy !== null && version.createdAt !== null"
><strong v-if="version.version == 0">créé par</strong ><strong v-if="version.version == 0">créé par</strong
><strong v-else>modifié par</strong> ><strong v-else>modifié par</strong>
<span class="badge-user" <span class="badge-user"
><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge ><UserRenderBoxBadge
:user="version.createdBy"
></UserRenderBoxBadge
></span> ></span>
<strong>à</strong> <strong>à</strong>
{{ {{
$d(ISOToDatetime(version.createdAt.datetime8601), "long") $d(ISOToDatetime(version.createdAt.datetime8601), "long")
}}</template }}</template
><template v-if="version.createdBy === null && version.createdAt !== null" ><template
v-if="version.createdBy === null && version.createdAt !== null"
><strong v-if="version.version == 0">Créé le</strong ><strong v-if="version.version == 0">Créé le</strong
><strong v-else>modifié le</strong> ><strong v-else>modifié le</strong>
{{ {{

View File

@@ -2,7 +2,9 @@
<a <a
:class="Object.assign(props.classes, { btn: true })" :class="Object.assign(props.classes, { btn: true })"
@click="beforeLeave($event)" @click="beforeLeave($event)"
:href="build_wopi_editor_link(props.storedObject.uuid, props.returnPath)" :href="
build_wopi_editor_link(props.storedObject.uuid, props.returnPath)
"
> >
<i class="fa fa-paragraph"></i> <i class="fa fa-paragraph"></i>
Editer en ligne Editer en ligne

View File

@@ -145,7 +145,9 @@ async function download_info_link(
function build_wopi_editor_link(uuid: string, returnPath?: string) { function build_wopi_editor_link(uuid: string, returnPath?: string) {
if (returnPath === undefined) { if (returnPath === undefined) {
returnPath = returnPath =
window.location.pathname + window.location.search + window.location.hash; window.location.pathname +
window.location.search +
window.location.hash;
} }
return ( return (
@@ -184,7 +186,10 @@ async function download_and_decrypt_doc(
) { ) {
downloadInfo = storedObject._links.downloadLink; downloadInfo = storedObject._links.downloadLink;
} else { } else {
downloadInfo = await download_info_link(storedObject, atVersionToDownload); downloadInfo = await download_info_link(
storedObject,
atVersionToDownload,
);
} }
const rawResponse = await window.fetch(downloadInfo.url); const rawResponse = await window.fetch(downloadInfo.url);
@@ -239,7 +244,10 @@ async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob> {
} }
if (storedObject.currentVersion?.type === "application/pdf") { if (storedObject.currentVersion?.type === "application/pdf") {
return download_and_decrypt_doc(storedObject, storedObject.currentVersion); return download_and_decrypt_doc(
storedObject,
storedObject.currentVersion,
);
} }
const convertLink = build_convert_link(storedObject.uuid); const convertLink = build_convert_link(storedObject.uuid);

View File

@@ -23,6 +23,8 @@ See the document: Voir le document
document: document:
Any title: Aucun titre Any title: Aucun titre
replace: Remplacer
Add: Ajouter un document
generic_doc: generic_doc:
filter: filter:

View File

@@ -0,0 +1,28 @@
<?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\EventBundle\Controller\Admin;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\Request;
class EventBudgetKindController extends CRUDController
{
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{
/* @var QueryBuilder $query */
$query->addOrderBy('e.type', 'ASC');
return parent::orderQuery($action, $query, $request, $paginator);
}
}

View File

@@ -23,11 +23,11 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\Type\PickPersonDynamicType; use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
use Chill\PersonBundle\Privacy\PrivacyEvent; use Chill\PersonBundle\Privacy\PrivacyEvent;
use Doctrine\Persistence\ManagerRegistry;
use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Csv; use PhpOffice\PhpSpreadsheet\Writer\Csv;
use PhpOffice\PhpSpreadsheet\Writer\Ods; use PhpOffice\PhpSpreadsheet\Writer\Ods;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -41,6 +41,8 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
@@ -58,7 +60,8 @@ final class EventController extends AbstractController
private readonly TranslatorInterface $translator, private readonly TranslatorInterface $translator,
private readonly PaginatorFactory $paginator, private readonly PaginatorFactory $paginator,
private readonly Security $security, private readonly Security $security,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry, private readonly ManagerRegistry $managerRegistry,
private readonly NormalizerInterface $normalizer,
) {} ) {}
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/delete', name: 'chill_event__event_delete', requirements: ['event_id' => '\d+'], methods: ['GET', 'POST', 'DELETE'])] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/delete', name: 'chill_event__event_delete', requirements: ['event_id' => '\d+'], methods: ['GET', 'POST', 'DELETE'])]
@@ -75,6 +78,7 @@ final class EventController extends AbstractController
/** @var array $participations */ /** @var array $participations */
$participations = $event->getParticipations(); $participations = $event->getParticipations();
$budgetElements = $event->getBudgetElements();
$form = $this->createDeleteForm($event_id); $form = $this->createDeleteForm($event_id);
@@ -86,6 +90,10 @@ final class EventController extends AbstractController
$em->remove($participation); $em->remove($participation);
} }
foreach ($budgetElements as $e) {
$em->remove($e);
}
$em->remove($event); $em->remove($event);
$em->flush(); $em->flush();
@@ -103,7 +111,7 @@ final class EventController extends AbstractController
} }
return $this->render('@ChillEvent/Event/confirm_delete.html.twig', [ return $this->render('@ChillEvent/Event/confirm_delete.html.twig', [
'event_id' => $event->getId(), 'id' => $event->getId(),
'delete_form' => $form->createView(), 'delete_form' => $form->createView(),
]); ]);
} }
@@ -169,6 +177,8 @@ final class EventController extends AbstractController
/** /**
* Displays a form to create a new Event entity. * Displays a form to create a new Event entity.
*
* @throws ExceptionInterface
*/ */
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/new', name: 'chill_event__event_new', methods: ['GET', 'POST'])] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/new', name: 'chill_event__event_new', methods: ['GET', 'POST'])]
public function newAction(?Center $center, Request $request): Response public function newAction(?Center $center, Request $request): Response
@@ -199,26 +209,23 @@ final class EventController extends AbstractController
$this->addFlash('success', $this->translator $this->addFlash('success', $this->translator
->trans('The event was created')); ->trans('The event was created'));
return $this->redirectToRoute('chill_event__event_show', ['event_id' => $entity->getId()]); return $this->redirectToRoute('chill_event__event_show', ['id' => $entity->getId()]);
} }
$entity_array = $this->normalizer->normalize($entity, 'json', ['groups' => 'read']);
return $this->render('@ChillEvent/Event/new.html.twig', [ return $this->render('@ChillEvent/Event/new.html.twig', [
'entity' => $entity, 'entity' => $entity,
'form' => $form->createView(), 'form' => $form->createView(),
'entity_json' => $entity_array,
]); ]);
} }
/**
* First step of new Event form.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/new/pick-center', name: 'chill_event__event_new_pickcenter', options: [null])] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/new/pick-center', name: 'chill_event__event_new_pickcenter', options: [null])]
public function newPickCenterAction(): Response public function newPickCenterAction(): Response
{ {
$role = 'CHILL_EVENT_CREATE'; $role = 'CHILL_EVENT_CREATE';
/**
* @var Center $centers
*/
$centers = $this->authorizationHelper->getReachableCenters($this->getUser(), $role); $centers = $this->authorizationHelper->getReachableCenters($this->getUser(), $role);
if (1 === \count($centers)) { if (1 === \count($centers)) {
@@ -238,7 +245,7 @@ final class EventController extends AbstractController
->add('center_id', EntityType::class, [ ->add('center_id', EntityType::class, [
'class' => Center::class, 'class' => Center::class,
'choices' => $centers, 'choices' => $centers,
'placeholder' => '', 'placeholder' => $this->translator->trans('Pick a center'),
'label' => 'To which centre should the event be associated ?', 'label' => 'To which centre should the event be associated ?',
]) ])
->add('submit', SubmitType::class, [ ->add('submit', SubmitType::class, [
@@ -251,16 +258,7 @@ final class EventController extends AbstractController
]); ]);
} }
/** #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{id}/show', name: 'chill_event__event_show')]
* Finds and displays a Event entity.
*
* @ParamConverter("event", options={"id": "event_id"})
*
* @return Response
*
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/show', name: 'chill_event__event_show')]
public function showAction(Event $event, Request $request) public function showAction(Event $event, Request $request)
{ {
if (!$event) { if (!$event) {
@@ -317,7 +315,7 @@ final class EventController extends AbstractController
$this->addFlash('success', $this->translator->trans('The event was updated')); $this->addFlash('success', $this->translator->trans('The event was updated'));
return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]); return $this->redirectToRoute('chill_event__event_show', ['id' => $event_id]);
} }
return $this->render('@ChillEvent/Event/edit.html.twig', [ return $this->render('@ChillEvent/Event/edit.html.twig', [

View File

@@ -15,11 +15,15 @@ use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\EventType; use Chill\EventBundle\Entity\EventType;
use Chill\EventBundle\Repository\EventACLAwareRepositoryInterface; use Chill\EventBundle\Repository\EventACLAwareRepositoryInterface;
use Chill\EventBundle\Repository\EventTypeRepository; use Chill\EventBundle\Repository\EventTypeRepository;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Pagination\PaginatorFactoryInterface; use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper; use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory; use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Form\Type\PickPersonDynamicType; use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormFactoryInterface;
@@ -29,17 +33,18 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment; use Twig\Environment;
final readonly class EventListController final class EventListController extends AbstractController
{ {
public function __construct( public function __construct(
private Environment $environment, private readonly Environment $environment,
private EventACLAwareRepositoryInterface $eventACLAwareRepository, private readonly EventACLAwareRepositoryInterface $eventACLAwareRepository,
private EventTypeRepository $eventTypeRepository, private readonly EventTypeRepository $eventTypeRepository,
private FilterOrderHelperFactory $filterOrderHelperFactory, private readonly FilterOrderHelperFactory $filterOrderHelperFactory,
private FormFactoryInterface $formFactory, private readonly FormFactoryInterface $formFactory,
private PaginatorFactoryInterface $paginatorFactory, private readonly PaginatorFactoryInterface $paginatorFactory,
private TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatableStringHelperInterface $translatableStringHelper,
private UrlGeneratorInterface $urlGenerator, private readonly UrlGeneratorInterface $urlGenerator,
private readonly AuthorizationHelper $authorizationHelper,
) {} ) {}
#[Route(path: '{_locale}/event/event/list', name: 'chill_event_event_list')] #[Route(path: '{_locale}/event/event/list', name: 'chill_event_event_list')]
@@ -50,6 +55,8 @@ final readonly class EventListController
'q' => (string) $filter->getQueryString(), 'q' => (string) $filter->getQueryString(),
'dates' => $filter->getDateRangeData('dates'), 'dates' => $filter->getDateRangeData('dates'),
'event_types' => $filter->getEntityChoiceData('event_types'), 'event_types' => $filter->getEntityChoiceData('event_types'),
'responsables' => $filter->getUserPickerData('responsables'),
'centers' => $filter->getEntityChoiceData('centers'),
]; ];
$total = $this->eventACLAwareRepository->countAllViewable($filterData); $total = $this->eventACLAwareRepository->countAllViewable($filterData);
$pagination = $this->paginatorFactory->create($total); $pagination = $this->paginatorFactory->create($total);
@@ -73,6 +80,7 @@ final readonly class EventListController
private function buildFilterOrder(): FilterOrderHelper private function buildFilterOrder(): FilterOrderHelper
{ {
$types = $this->eventTypeRepository->findAllActive(); $types = $this->eventTypeRepository->findAllActive();
$centers = $this->authorizationHelper->getReachableCenters($this->getUser(), EventVoter::SEE);
$builder = $this->filterOrderHelperFactory->create(__METHOD__); $builder = $this->filterOrderHelperFactory->create(__METHOD__);
$builder $builder
@@ -80,6 +88,16 @@ final readonly class EventListController
->addSearchBox(['name']) ->addSearchBox(['name'])
->addEntityChoice('event_types', 'event.filter.event_types', EventType::class, $types, [ ->addEntityChoice('event_types', 'event.filter.event_types', EventType::class, $types, [
'choice_label' => fn (EventType $e) => $this->translatableStringHelper->localize($e->getName()), 'choice_label' => fn (EventType $e) => $this->translatableStringHelper->localize($e->getName()),
'expanded' => false,
'required' => false,
'attr' => ['class' => 'select2'],
])
->addUserPicker('responsables', 'event.filter.pick_responsable', ['multiple' => true, 'required' => false])
->addEntityChoice('centers', 'event.filter.center', Center::class, $centers, [
'choice_label' => fn (Center $c) => $c->getName(),
'expanded' => false,
'required' => false,
'attr' => ['class' => 'select2'],
]); ]);
return $builder->build(); return $builder->build();

View File

@@ -0,0 +1,44 @@
<?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\EventBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
class EventThemeController extends CRUDController
{
protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface
{
if ('new' === $action) {
return parent::createFormFor($action, $entity, $formClass, ['step' => 'create']);
}
if ('edit' === $action) {
return parent::createFormFor($action, $entity, $formClass, ['step' => 'edit']);
}
throw new \LogicException('action is not supported: '.$action);
}
/**
* @param QueryBuilder|mixed $query
*/
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator): QueryBuilder
{
/* @var QueryBuilder $query */
return $query->orderBy('e.ordering', 'ASC')
->addOrderBy('e.id', 'ASC');
}
}

View File

@@ -228,7 +228,7 @@ final class ParticipationController extends AbstractController
} }
return $this->redirectToRoute('chill_event__event_show', [ return $this->redirectToRoute('chill_event__event_show', [
'event_id' => $participation->getEvent()->getId(), 'id' => $participation->getEvent()->getId(),
]); ]);
} }
@@ -242,7 +242,7 @@ final class ParticipationController extends AbstractController
/** /**
* @param int $participation_id * @param int $participation_id
*/ */
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/{participation_id}/delete', name: 'chill_event_participation_delete', requirements: ['participation_id' => '\d+'], methods: ['GET', 'DELETE'])] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/{participation_id}/delete', name: 'chill_event_participation_delete', requirements: ['participation_id' => '\d+'])]
public function deleteAction($participation_id, Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse public function deleteAction($participation_id, Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse
{ {
$em = $this->managerRegistry->getManager(); $em = $this->managerRegistry->getManager();
@@ -273,7 +273,7 @@ final class ParticipationController extends AbstractController
); );
return $this->redirectToRoute('chill_event__event_show', [ return $this->redirectToRoute('chill_event__event_show', [
'event_id' => $event->getId(), 'id' => $event->getId(),
]); ]);
} }
} }
@@ -442,7 +442,7 @@ final class ParticipationController extends AbstractController
)); ));
return $this->redirectToRoute('chill_event__event_show', [ return $this->redirectToRoute('chill_event__event_show', [
'event_id' => $participation->getEvent()->getId(), 'id' => $participation->getEvent()->getId(),
]); ]);
} }

View File

@@ -11,6 +11,12 @@ declare(strict_types=1);
namespace Chill\EventBundle\DependencyInjection; namespace Chill\EventBundle\DependencyInjection;
use Chill\EventBundle\Controller\Admin\EventBudgetKindController;
use Chill\EventBundle\Controller\EventThemeController;
use Chill\EventBundle\Entity\EventBudgetKind;
use Chill\EventBundle\Entity\EventTheme;
use Chill\EventBundle\Form\EventBudgetKindType;
use Chill\EventBundle\Form\EventThemeType;
use Chill\EventBundle\Security\EventVoter; use Chill\EventBundle\Security\EventVoter;
use Chill\EventBundle\Security\ParticipationVoter; use Chill\EventBundle\Security\ParticipationVoter;
use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\FileLocator;
@@ -26,7 +32,10 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension;
*/ */
class ChillEventExtension extends Extension implements PrependExtensionInterface class ChillEventExtension extends Extension implements PrependExtensionInterface
{ {
public function load(array $configs, ContainerBuilder $container) /**
* @throws \Exception
*/
public function load(array $configs, ContainerBuilder $container): void
{ {
$configuration = new Configuration(); $configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs); $config = $this->processConfiguration($configuration, $configs);
@@ -45,16 +54,17 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
/** (non-PHPdoc). /** (non-PHPdoc).
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend() * @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
*/ */
public function prepend(ContainerBuilder $container) public function prepend(ContainerBuilder $container): void
{ {
$this->prependAuthorization($container); $this->prependAuthorization($container);
$this->prependCruds($container);
$this->prependRoute($container); $this->prependRoute($container);
} }
/** /**
* add authorization hierarchy. * add authorization hierarchy.
*/ */
protected function prependAuthorization(ContainerBuilder $container) protected function prependAuthorization(ContainerBuilder $container): void
{ {
$container->prependExtensionConfig('security', [ $container->prependExtensionConfig('security', [
'role_hierarchy' => [ 'role_hierarchy' => [
@@ -70,7 +80,7 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
/** /**
* add route to route loader for chill. * add route to route loader for chill.
*/ */
protected function prependRoute(ContainerBuilder $container) protected function prependRoute(ContainerBuilder $container): void
{ {
// add routes for custom bundle // add routes for custom bundle
$container->prependExtensionConfig('chill_main', [ $container->prependExtensionConfig('chill_main', [
@@ -81,4 +91,54 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
], ],
]); ]);
} }
protected function prependCruds(ContainerBuilder $container): void
{
$container->prependExtensionConfig('chill_main', [
'cruds' => [
[
'class' => EventTheme::class,
'name' => 'event_theme',
'base_path' => '/admin/event/theme',
'form_class' => EventThemeType::class,
'controller' => EventThemeController::class,
'actions' => [
'index' => [
'template' => '@ChillEvent/Admin/EventTheme/index.html.twig',
'role' => 'ROLE_ADMIN',
],
'new' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/EventTheme/new.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/EventTheme/edit.html.twig',
],
],
],
[
'class' => EventBudgetKind::class,
'name' => 'event_budget_kind',
'base_path' => '/admin/event/budget',
'form_class' => EventBudgetKindType::class,
'controller' => EventBudgetKindController::class,
'actions' => [
'index' => [
'template' => '@ChillEvent/Admin/BudgetKind/index.html.twig',
'role' => 'ROLE_ADMIN',
],
'new' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/BudgetKind/new.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/BudgetKind/edit.html.twig',
],
],
],
],
]);
}
} }

View File

@@ -9,8 +9,10 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code. * the LICENSE file that was distributed with this source code.
*/ */
namespace Chill\TicketBundle; namespace Chill\EventBundle\Entity;
use Symfony\Component\HttpKernel\Bundle\Bundle; enum BudgetTypeEnum: string
{
class ChillTicketBundle extends Bundle {} case CHARGE = 'Charge';
case RESOURCE = 'Resource';
}

View File

@@ -23,10 +23,13 @@ use Chill\MainBundle\Entity\HasScopeInterface;
use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation as Serializer;
/** /**
* Class Event. * Class Event.
@@ -46,35 +49,63 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
#[ORM\ManyToOne(targetEntity: Scope::class)] #[ORM\ManyToOne(targetEntity: Scope::class)]
private ?Scope $circle = null; private ?Scope $circle = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE)] #[ORM\Column(type: Types::DATETIME_MUTABLE)]
private ?\DateTime $date = null; private ?\DateTime $date = null;
#[ORM\Id] #[ORM\Id]
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)] #[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\GeneratedValue(strategy: 'AUTO')] #[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null; private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class)] #[ORM\ManyToOne(targetEntity: User::class)]
private ?User $moderator = null; private ?User $moderator = null;
/**
* @var Collection<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[Serializer\Groups(['read'])]
#[ORM\JoinTable('chill_event_animatorsintern')]
private Collection $animatorsIntern;
/**
* @var Collection<int, ThirdParty>
*/
#[ORM\ManyToMany(targetEntity: ThirdParty::class)]
#[Serializer\Groups(['read'])]
#[ORM\JoinTable('chill_event_animatorsextern')]
private Collection $animatorsExtern;
#[Assert\NotBlank] #[Assert\NotBlank]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 150)] #[Serializer\Groups(['read'])]
#[ORM\Column(type: Types::STRING, length: 150)]
private ?string $name = null; private ?string $name = null;
/** /**
* @var Collection<int, Participation> * @var Collection<int, Participation>
*/ */
#[ORM\OneToMany(mappedBy: 'event', targetEntity: Participation::class)] #[ORM\OneToMany(mappedBy: 'event', targetEntity: Participation::class)]
#[Serializer\Groups(['read'])]
private Collection $participations; private Collection $participations;
#[Assert\NotNull] #[Assert\NotNull]
#[Serializer\Groups(['read'])]
#[ORM\ManyToOne(targetEntity: EventType::class)] #[ORM\ManyToOne(targetEntity: EventType::class)]
private ?EventType $type = null; private ?EventType $type = null;
/**
* @var Collection<int, EventTheme>
*/
#[ORM\ManyToMany(targetEntity: EventTheme::class)]
#[Serializer\Groups(['read'])]
#[ORM\JoinTable('chill_event_eventtheme')]
private Collection $themes;
#[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'comment_')] #[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'comment_')]
private CommentEmbeddable $comment; private CommentEmbeddable $comment;
#[ORM\ManyToOne(targetEntity: Location::class)] #[ORM\ManyToOne(targetEntity: Location::class)]
#[Serializer\Groups(['read'])]
#[ORM\JoinColumn(nullable: true)] #[ORM\JoinColumn(nullable: true)]
private ?Location $location = null; private ?Location $location = null;
@@ -85,7 +116,17 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
#[ORM\JoinTable('chill_event_event_documents')] #[ORM\JoinTable('chill_event_event_documents')]
private Collection $documents; private Collection $documents;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DECIMAL, precision: 10, scale: 4, nullable: true, options: ['default' => '0.0'])] /**
* @var Collection<int, EventBudgetElement>
*/
#[ORM\OneToMany(mappedBy: 'event', targetEntity: EventBudgetElement::class, cascade: ['persist'])]
#[Serializer\Groups(['read'])]
private Collection $budgetElements;
/**
* @deprecated use budgetElements instead
*/
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 4, nullable: true, options: ['default' => '0.0'])]
private string $organizationCost = '0.0'; private string $organizationCost = '0.0';
/** /**
@@ -96,6 +137,20 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
$this->participations = new ArrayCollection(); $this->participations = new ArrayCollection();
$this->documents = new ArrayCollection(); $this->documents = new ArrayCollection();
$this->comment = new CommentEmbeddable(); $this->comment = new CommentEmbeddable();
$this->themes = new ArrayCollection();
$this->budgetElements = new ArrayCollection();
$this->animatorsIntern = new ArrayCollection();
$this->animatorsExtern = new ArrayCollection();
}
public function addBudgetElement(EventBudgetElement $budgetElement)
{
if (!$this->budgetElements->contains($budgetElement)) {
$this->budgetElements[] = $budgetElement;
$budgetElement->setEvent($this);
}
return $this;
} }
/** /**
@@ -126,38 +181,79 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
return $this; return $this;
} }
/** public function getThemes(): Collection
* @return Center {
*/ return $this->themes;
public function getCenter() }
public function addTheme(EventTheme $theme): self
{
$this->themes->add($theme);
return $this;
}
public function removeTheme(EventTheme $theme): void
{
$this->themes->removeElement($theme);
}
public function getAnimatorsIntern(): Collection
{
return $this->animatorsIntern;
}
public function getAnimatorsExtern(): Collection
{
return $this->animatorsExtern;
}
public function addAnimatorsIntern(User $ai): self
{
$this->animatorsIntern->add($ai);
return $this;
}
public function removeAnimatorsIntern(User $ai): void
{
$this->animatorsIntern->removeElement($ai);
}
public function addAnimatorsExtern(ThirdParty $ae): self
{
$this->animatorsExtern->add($ae);
return $this;
}
public function removeAnimatorsExtern(ThirdParty $ae): void
{
$this->animatorsExtern->removeElement($ae);
}
public function getCenter(): Center
{ {
return $this->center; return $this->center;
} }
/** public function getCircle(): ?Scope
* @return Scope
*/
public function getCircle()
{ {
return $this->circle; return $this->circle;
} }
/** /**
* Get date. * Get date.
*
* @return \DateTime
*/ */
public function getDate() public function getDate(): ?\DateTime
{ {
return $this->date; return $this->date;
} }
/** /**
* Get id. * Get id.
*
* @return int
*/ */
public function getId() public function getId(): ?int
{ {
return $this->id; return $this->id;
} }
@@ -169,14 +265,20 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
/** /**
* Get label. * Get label.
*
* @return string
*/ */
public function getName() public function getName(): ?string
{ {
return $this->name; return $this->name;
} }
/**
* @return Collection<int, EventBudgetElement>
*/
public function getBudgetElements(): Collection
{
return $this->budgetElements;
}
/** /**
* @return Collection<int, Participation> * @return Collection<int, Participation>
*/ */
@@ -199,26 +301,26 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
/** /**
* @deprecated * @deprecated
*
* @return Scope
*/ */
public function getScope() public function getScope(): Scope
{ {
return $this->getCircle(); return $this->getCircle();
} }
/** public function getType(): ?EventType
* @return EventType
*/
public function getType()
{ {
return $this->type; return $this->type;
} }
public function removeBudgetElement(EventBudgetElement $budgetElement): void
{
$this->budgetElements->removeElement($budgetElement);
}
/** /**
* Remove participation. * Remove participation.
*/ */
public function removeParticipation(Participation $participation) public function removeParticipation(Participation $participation): void
{ {
$this->participations->removeElement($participation); $this->participations->removeElement($participation);
} }
@@ -314,11 +416,17 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
$this->documents = $documents; $this->documents = $documents;
} }
/**
* @deprecated
*/
public function getOrganizationCost(): string public function getOrganizationCost(): string
{ {
return $this->organizationCost; return $this->organizationCost;
} }
/**
* @deprecated
*/
public function setOrganizationCost(string $organizationCost): void public function setOrganizationCost(string $organizationCost): void
{ {
$this->organizationCost = $organizationCost; $this->organizationCost = $organizationCost;

View File

@@ -0,0 +1,103 @@
<?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\EventBundle\Entity;
use Chill\EventBundle\Repository\EventThemeRepository;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: EventThemeRepository::class)]
#[ORM\Table(name: 'chill_event_budget_element')]
class EventBudgetElement
{
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
#[Assert\GreaterThan(value: 0)]
#[Assert\NotNull(message: 'The amount cannot be empty')]
#[ORM\Column(name: 'amount', type: Types::DECIMAL, precision: 10, scale: 2)]
private string $amount;
#[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'comment_budget_element_')]
private ?CommentEmbeddable $comment = null;
#[ORM\ManyToOne(targetEntity: Event::class)]
private Event $event;
#[ORM\ManyToOne(targetEntity: EventBudgetKind::class, inversedBy: 'EventBudgetElement')]
#[ORM\JoinColumn]
private EventBudgetKind $kind;
/* Getters and Setters */
public function getId(): ?int
{
return $this->id;
}
public function setId(?int $id): void
{
$this->id = $id;
}
public function getAmount(): float
{
return (float) $this->amount;
}
public function getComment(): ?CommentEmbeddable
{
return $this->comment;
}
public function getEvent(): Event
{
return $this->event;
}
public function getKind(): EventBudgetKind
{
return $this->kind;
}
public function setAmount(string $amount): self
{
$this->amount = $amount;
return $this;
}
public function setComment(?CommentEmbeddable $comment = null): self
{
$this->comment = $comment;
return $this;
}
public function setEvent(Event $event): self
{
$this->event = $event;
return $this;
}
public function setKind(EventBudgetKind $kind): self
{
$this->kind = $kind;
return $this;
}
}

View File

@@ -0,0 +1,78 @@
<?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\EventBundle\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
/**
* Type of event budget element.
*/
#[ORM\Entity]
#[ORM\Table(name: 'chill_event_budget_kind')]
class EventBudgetKind
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private ?int $id = null;
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => true])]
private bool $isActive = true;
#[ORM\Column(enumType: BudgetTypeEnum::class)]
private BudgetTypeEnum $type;
#[ORM\Column(type: Types::JSON, length: 255, options: ['default' => '{}', 'jsonb' => true])]
private array $name = [];
public function getId(): ?int
{
return $this->id;
}
public function getIsActive(): bool
{
return $this->isActive;
}
public function getType(): BudgetTypeEnum
{
return $this->type;
}
public function getName(): ?array
{
return $this->name;
}
public function setIsActive(bool $isActive): self
{
$this->isActive = $isActive;
return $this;
}
public function setType(BudgetTypeEnum $type): self
{
$this->type = $type;
return $this;
}
public function setName(array $name): self
{
$this->name = $name;
return $this;
}
}

View File

@@ -0,0 +1,158 @@
<?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\EventBundle\Entity;
use Chill\EventBundle\Repository\EventThemeRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
/**
* Class EventTheme.
*/
#[ORM\HasLifecycleCallbacks]
#[ORM\Entity(repositoryClass: EventThemeRepository::class)]
#[ORM\Table(name: 'chill_event_event_theme')]
class EventTheme
{
#[ORM\Column(type: Types::BOOLEAN, nullable: false)]
private bool $isActive = true;
#[ORM\Id]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
#[ORM\Column(type: Types::JSON)]
private array $name;
/**
* @var Collection<int, EventTheme>
*/
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: EventTheme::class)]
private Collection $children;
#[ORM\ManyToOne(targetEntity: EventTheme::class, inversedBy: 'children')]
private ?EventTheme $parent = null;
#[ORM\Column(name: 'ordering', type: Types::FLOAT, options: ['default' => '0.0'])]
private float $ordering = 0.0;
/**
* Constructor.
*/
public function __construct()
{
$this->children = new ArrayCollection();
}
/**
* Get active.
*/
public function getIsActive(): bool
{
return $this->isActive;
}
/**
* Get id.
*/
public function getId(): ?int
{
return $this->id;
}
/**
* Get label.
*/
public function getName(): array
{
return $this->name;
}
public function setIsActive(bool $active): self
{
$this->isActive = $active;
return $this;
}
public function setName(array $label): self
{
$this->name = $label;
return $this;
}
public function addChild(self $child): self
{
if (!$this->children->contains($child)) {
$this->children[] = $child;
}
return $this;
}
public function removeChild(self $child): self
{
if ($this->children->removeElement($child)) {
// set the owning side to null (unless already changed)
if ($child->getParent() === $this) {
$child->setParent(null);
}
}
return $this;
}
public function getChildren(): Collection
{
return $this->children;
}
public function hasChildren(): bool
{
return 0 < $this->getChildren()->count();
}
public function hasParent(): bool
{
return null !== $this->parent;
}
public function getOrdering(): float
{
return $this->ordering;
}
public function setOrdering(float $ordering): EventTheme
{
$this->ordering = $ordering;
return $this;
}
public function getParent(): ?self
{
return $this->parent;
}
public function setParent(?self $parent): self
{
$this->parent = $parent;
$parent?->addChild($this);
return $this;
}
}

View File

@@ -0,0 +1,377 @@
<?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\EventBundle\Export\Export;
use Chill\EventBundle\Entity\BudgetTypeEnum;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\EventBudgetElementRepository;
use Chill\EventBundle\Repository\EventThemeRepository;
use Chill\EventBundle\Security\EventVoter;
use Chill\EventBundle\Templating\Entity\EventThemeRender;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\ThirdPartyBundle\Export\Helper\LabelThirdPartyHelper;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatableInterface;
/**
* Render a list of events.
*/
class ListEvents implements ListInterface, GroupedExportInterface
{
protected array $fields = [
'event_id',
'event_center',
'event_name',
'event_date',
'event_location',
'event_type',
'event_themes',
'event_moderator',
'event_animators',
'event_participants_count',
'event_budget_resources',
'event_budget_charges',
];
private readonly bool $filterStatsByCenters;
public function __construct(
protected readonly EntityManagerInterface $entityManager,
ParameterBagInterface $parameterBag,
protected readonly TranslatableStringHelperInterface $translatableStringHelper,
protected readonly EventThemeRender $eventThemeRender,
protected readonly EventThemeRepository $eventThemeRepository,
protected readonly LabelThirdPartyHelper $labelThirdPartyHelper,
protected readonly EventBudgetElementRepository $eventBudgetElementRepository,
) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
}
public function buildForm(FormBuilderInterface $builder): void
{
$builder
->add('fields', ChoiceType::class, [
'multiple' => true,
'expanded' => true,
'choices' => array_combine($this->fields, $this->fields),
'label' => 'Fields to include in export',
'constraints' => [new Callback([
'callback' => static function ($selected, ExecutionContextInterface $context) {
if (0 === \count($selected)) {
$context->buildViolation('You must select at least one element')
->atPath('fields')
->addViolation();
}
},
])],
]);
}
public function getFormDefaultData(): array
{
return [
'fields' => $this->fields,
];
}
public function getAllowedFormattersTypes(): array
{
return [FormatterInterface::TYPE_LIST];
}
public function getDescription(): string
{
return 'export.event.list.description';
}
public function getGroup(): string
{
return 'Exports of events';
}
public function getLabels($key, array $values, $data)
{
return match ($key) {
'event_id' => fn ($value) => '_header' === $value ? $key : $value,
'event_name' => fn ($value) => '_header' === $value ? $key : $value,
'event_date' => function ($value) use ($key) {
if ('_header' === $value) {
return $key;
}
if ($value instanceof \DateTime) {
return $value->format('Y-m-d');
}
$date = \DateTime::createFromFormat('Y-m-d H:i:s', $value);
return $date ? $date->format('Y-m-d') : $value;
},
'event_type' => function ($value) use ($key) {
if ('_header' === $value) {
return 'export.event.list.'.$key;
}
return $this->translatableStringHelper->localize(json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR));
},
'event_center' => fn ($value) => '_header' === $value ? $key : $value,
'event_moderator' => fn ($value) => '_header' === $value ? $key : $value,
'event_participants_count' => fn ($value) => '_header' === $value ? $key : $value,
'event_location' => fn ($value) => '_header' === $value ? $key : $value,
'event_animators' => $this->labelThirdPartyHelper->getLabelMulti($key, $values, $key),
'event_themes' => function ($value) use ($key) {
if ('_header' === $value) {
return $key;
}
if (null === $value) {
return '';
}
return implode(
'|',
array_map(
fn ($t) => $this->eventThemeRender->renderString($this->eventThemeRepository->find($t), []),
json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR)
)
);
},
'event_budget_resources' => function ($value) use ($key) {
if ('_header' === $value) {
return $key;
}
if (!$value) {
return '';
}
$ids = explode(',', $value);
$ids = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
$elements = $this->eventBudgetElementRepository->findBy(['id' => $ids]);
return implode('|', array_map(function ($element) {
$name = $this->translatableStringHelper->localize($element->getKind()->getName());
$amount = number_format($element->getAmount(), 2, '.', '');
return $name.': '.$amount;
}, $elements));
},
'event_budget_charges' => function ($value) use ($key) {
if ('_header' === $value) {
return $key;
}
if (!$value) {
return '';
}
$ids = explode(',', $value);
$ids = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
$elements = $this->eventBudgetElementRepository->findBy(['id' => $ids]);
return implode('|', array_map(function ($element) {
$name = $this->translatableStringHelper->localize($element->getKind()->getName());
$amount = number_format($element->getAmount(), 2, '.', '');
return $name.': '.$amount;
}, $elements));
},
default => fn ($value) => '_header' === $value ? $key : $value,
};
}
public function getQueryKeys(array $data): array
{
return $data['fields'];
}
public function getResult($query, $data, ExportGenerationContext $context): array
{
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
}
public function getTitle(): string|TranslatableInterface
{
return 'export.event.list.title';
}
public function getType(): string
{
return Declarations::EVENT;
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data, ExportGenerationContext $context): NativeQuery|QueryBuilder
{
$centers = array_map(static fn ($el) => $el['center'], $acl);
// Throw an error if no fields are present
if (!\array_key_exists('fields', $data)) {
throw new \InvalidArgumentException('No fields have been checked.');
}
$qb = $this->entityManager->createQueryBuilder()
->from(Event::class, 'event');
if ($this->filterStatsByCenters) {
$qb
->andWhere('event.center IN (:authorized_centers)')
->setParameter('authorized_centers', $centers);
}
// Add fields based on selection
foreach ($this->fields as $field) {
if (\in_array($field, $data['fields'], true)) {
switch ($field) {
case 'event_id':
$qb->addSelect('event.id AS event_id');
break;
case 'event_name':
$qb->addSelect('event.name AS event_name');
break;
case 'event_date':
$qb->addSelect('event.date AS event_date');
break;
case 'event_type':
if (!$this->hasJoin($qb, 'event.type')) {
$qb->leftJoin('event.type', 'type');
}
$qb->addSelect('type.name AS event_type');
break;
case 'event_center':
if (!$this->hasJoin($qb, 'event.center')) {
$qb->leftJoin('event.center', 'center');
}
$qb->addSelect('center.name AS event_center');
break;
case 'event_moderator':
if (!$this->hasJoin($qb, 'event.moderator')) {
$qb->leftJoin('event.moderator', 'user');
}
$qb->addSelect('user.username AS event_moderator');
break;
case 'event_participants_count':
$qb->addSelect('(SELECT COUNT(p.id) FROM Chill\EventBundle\Entity\Participation p WHERE p.event = event.id) AS event_participants_count');
break;
case 'event_location':
if (!$this->hasJoin($qb, 'event.location')) {
$qb->leftJoin('event.location', 'location');
}
$qb->addSelect('location.name AS event_location');
break;
case 'event_animators':
$qb->addSelect(
'(SELECT AGGREGATE(tp.id) FROM Chill\ThirdPartyBundle\Entity\ThirdParty tp WHERE tp MEMBER OF event.animators) AS event_animators'
);
break;
case 'event_themes':
$qb->addSelect(
'(SELECT AGGREGATE(t.id) FROM Chill\EventBundle\Entity\EventTheme t WHERE t MEMBER OF event.themes) AS event_themes'
);
break;
case 'event_budget_resources':
$qb->addSelect(
'(SELECT AGGREGATE(ebr.id)
FROM Chill\EventBundle\Entity\EventBudgetElement ebr
JOIN ebr.kind kr
WHERE ebr.event = event.id AND kr.type = :resource_type) AS event_budget_resources'
);
$qb->setParameter('resource_type', BudgetTypeEnum::RESOURCE->value);
break;
case 'event_budget_charges':
$qb->addSelect(
'(SELECT AGGREGATE(ebc.id)
FROM Chill\EventBundle\Entity\EventBudgetElement ebc
JOIN ebc.kind kc
WHERE ebc.event = event.id AND kc.type = :charge_type) AS event_budget_charges'
);
$qb->setParameter('charge_type', BudgetTypeEnum::CHARGE->value);
break;
}
}
}
return $qb;
}
public function requiredRole(): string
{
return EventVoter::STATS;
}
public function supportsModifiers()
{
return [Declarations::EVENT];
}
/**
* Helper method to check if a join already exists in the QueryBuilder.
*/
private function hasJoin($queryBuilder, $joinPath): bool
{
$joins = $queryBuilder->getDQLPart('join');
if (!isset($joins['event'])) {
return false;
}
foreach ($joins['event'] as $join) {
if ($join->getJoin() === $joinPath) {
return true;
}
}
return false;
}
public function normalizeFormData(array $formData): array
{
return ['fields' => $formData['fields']];
}
public function denormalizeFormData(array $formData, int $fromVersion): array
{
return ['fields' => $formData['fields']];
}
public function getNormalizationVersion(): int
{
return 1;
}
}

View File

@@ -0,0 +1,59 @@
<?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\EventBundle\Form;
use Chill\EventBundle\Entity\BudgetTypeEnum;
use Chill\EventBundle\Entity\EventBudgetElement;
use Chill\EventBundle\Entity\EventBudgetKind;
use Chill\EventBundle\Repository\EventBudgetKindRepository;
use Chill\MainBundle\Form\Type\CommentType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AddEventBudgetElementType extends AbstractType
{
public function __construct(private readonly EventBudgetKindRepository $kindRepository, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$charges = $this->kindRepository->findByType(BudgetTypeEnum::CHARGE->value);
$resources = $this->kindRepository->findByType(BudgetTypeEnum::RESOURCE->value);
$builder->add('kind', ChoiceType::class, [
'choices' => [
'event.budget.charges' => $charges,
'event.budget.resources' => $resources,
],
'choice_label' => fn (EventBudgetKind $kind) => $this->translatableStringHelper->localize($kind->getName()),
'choice_value' => fn (?EventBudgetKind $kind) => $kind?->getId(),
'placeholder' => 'event.budget.Select a budget element kind',
])
->add('amount', NumberType::class, [
'required' => true,
])
->add('comment', CommentType::class, [
'label' => 'Comment',
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => EventBudgetElement::class,
]);
}
}

View File

@@ -0,0 +1,53 @@
<?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\EventBundle\Form;
use Chill\EventBundle\Entity\BudgetTypeEnum;
use Chill\EventBundle\Entity\EventBudgetKind;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class EventBudgetKindType extends AbstractType
{
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TranslatableStringFormType::class, [
'label' => 'Title',
])
->add('type', EnumType::class, [
'class' => BudgetTypeEnum::class,
'choice_label' => fn (BudgetTypeEnum $type): string => $this->translator->trans($type->value),
'expanded' => true,
'multiple' => false,
'mapped' => true,
'label' => 'event.admin.Select budget type',
])
->add('isActive', CheckboxType::class, [
'label' => 'Actif ?',
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('class', EventBudgetKind::class);
}
}

View File

@@ -0,0 +1,67 @@
<?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\EventBundle\Form;
use Chill\EventBundle\Entity\EventTheme;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\AbstractType;
class EventThemeType extends AbstractType
{
public function __construct(protected TranslatableStringHelperInterface $translatableStringHelper) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TranslatableStringFormType::class, [
'label' => 'Nom',
]);
if ('create' === $options['step']) {
$builder
->add('parent', EntityType::class, [
'class' => EventTheme::class,
'required' => false,
'choice_label' => fn (EventTheme $theme): ?string => $this->translatableStringHelper->localize($theme->getName()),
'mapped' => 'create' == $options['step'],
]);
}
$builder
->add('ordering', NumberType::class, [
'required' => true,
'scale' => 6,
])
->add('isActive', ChoiceType::class, [
'choices' => [
'Yes' => true,
'No' => false,
],
'expanded' => true,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => EventTheme::class,
]);
$resolver->setRequired('step')
->setAllowedValues('step', ['create', 'edit']);
}
}

View File

@@ -13,25 +13,39 @@ namespace Chill\EventBundle\Form;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Form\StoredObjectType; use Chill\DocStoreBundle\Form\StoredObjectType;
use Chill\EventBundle\Entity\BudgetTypeEnum;
use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Form\Type\PickEventThemeType;
use Chill\EventBundle\Form\Type\PickEventTypeType; use Chill\EventBundle\Form\Type\PickEventTypeType;
use Chill\EventBundle\Repository\EventBudgetKindRepository;
use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Form\DataTransformer\IdToLocationDataTransformer;
use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillDateTimeType; use Chill\MainBundle\Form\Type\ChillDateTimeType;
use Chill\MainBundle\Form\Type\CommentType; use Chill\MainBundle\Form\Type\CommentType;
use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Form\Type\PickUserLocationType;
use Chill\MainBundle\Form\Type\ScopePickerType; use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class EventType extends AbstractType class EventType extends AbstractType
{ {
public function buildForm(FormBuilderInterface $builder, array $options) public function __construct(
private readonly IdToLocationDataTransformer $idToLocationDataTransformer,
private readonly EventBudgetKindRepository $eventBudgetKindRepository,
private readonly TranslatorInterface $translator,
) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$chargeKinds = $this->eventBudgetKindRepository->findByType(BudgetTypeEnum::CHARGE->value);
$resourceKinds = $this->eventBudgetKindRepository->findByType(BudgetTypeEnum::RESOURCE->value);
$builder $builder
->add('name', TextType::class, [ ->add('name', TextType::class, [
'required' => true, 'required' => true,
@@ -49,11 +63,28 @@ class EventType extends AbstractType
'class' => '', 'class' => '',
], ],
]) ])
->add('themes', PickEventThemeType::class, [
'multiple' => true,
])
->add('moderator', PickUserDynamicType::class, [ ->add('moderator', PickUserDynamicType::class, [
'label' => 'Pick a moderator', 'label' => 'Pick a moderator',
]) ])
->add('location', PickUserLocationType::class, [ ->add('animatorsIntern', PickUserDynamicType::class, [
'label' => 'event.fields.location', 'multiple' => true,
'label' => $this->translator->trans('event.fields.internal animators'),
'required' => false,
])
->add('animatorsExtern', PickThirdpartyDynamicType::class, [
'multiple' => true,
'label' => $this->translator->trans('event.fields.external animators'),
'required' => false,
])
->add('budgetElements', ChillCollectionType::class, [
'entry_type' => AddEventBudgetElementType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
]) ])
->add('comment', CommentType::class, [ ->add('comment', CommentType::class, [
'label' => 'Comment', 'label' => 'Comment',
@@ -69,11 +100,11 @@ class EventType extends AbstractType
'delete_empty' => fn (StoredObject $storedObject): bool => '' === $storedObject->getFilename(), 'delete_empty' => fn (StoredObject $storedObject): bool => '' === $storedObject->getFilename(),
'button_remove_label' => 'event.form.remove_document', 'button_remove_label' => 'event.form.remove_document',
'button_add_label' => 'event.form.add_document', 'button_add_label' => 'event.form.add_document',
])
->add('organizationCost', MoneyType::class, [
'label' => 'event.fields.organizationCost',
'help' => 'event.form.organisationCost_help',
]); ]);
$builder->add('location', HiddenType::class)
->get('location')
->addModelTransformer($this->idToLocationDataTransformer);
} }
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
@@ -87,11 +118,9 @@ class EventType extends AbstractType
->setAllowedTypes('role', 'string'); ->setAllowedTypes('role', 'string');
} }
/** public function getBlockPrefix(): string
* @return string
*/
public function getBlockPrefix()
{ {
return 'chill_eventbundle_event'; // as the js shares some hardcoded items from the activity bundle, we have to rewrite block prefix
return 'chill_activitybundle_activity';
} }
} }

View File

@@ -0,0 +1,45 @@
<?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\EventBundle\Form\Type;
use Chill\EventBundle\Entity\EventTheme;
use Chill\EventBundle\Repository\EventThemeRepository;
use Chill\EventBundle\Templating\Entity\EventThemeRender;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PickEventThemeType extends AbstractType
{
public function __construct(private readonly EventThemeRender $eventThemeRender, private readonly EventThemeRepository $eventThemeRepository) {}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults([
'class' => EventTheme::class,
'choices' => $this->eventThemeRepository->findByActiveOrdered(),
'choice_label' => fn (EventTheme $et) => $this->eventThemeRender->renderString($et, []),
'placeholder' => 'event.form.Select one or more themes',
'required' => true,
'attr' => ['class' => 'select2'],
'label' => 'event.theme.label',
'multiple' => false,
])
->setAllowedTypes('multiple', ['bool']);
}
public function getParent(): string
{
return EntityType::class;
}
}

View File

@@ -23,15 +23,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/ */
class PickEventTypeType extends AbstractType class PickEventTypeType extends AbstractType
{ {
/** public function __construct(protected TranslatableStringHelper $translatableStringHelper) {}
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
public function __construct(TranslatableStringHelper $helper)
{
$this->translatableStringHelper = $helper;
}
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {

View File

@@ -17,17 +17,9 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class AdminMenuBuilder implements LocalMenuBuilderInterface class AdminMenuBuilder implements LocalMenuBuilderInterface
{ {
/** public function __construct(protected AuthorizationCheckerInterface $authorizationChecker) {}
* @var AuthorizationCheckerInterface
*/
protected $authorizationChecker;
public function __construct(AuthorizationCheckerInterface $authorizationChecker) public function buildMenu($menuId, MenuItem $menu, array $parameters): void
{
$this->authorizationChecker = $authorizationChecker;
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{ {
if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) { if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) {
return; return;
@@ -52,6 +44,14 @@ class AdminMenuBuilder implements LocalMenuBuilderInterface
$menu->addChild('Role', [ $menu->addChild('Role', [
'route' => 'chill_event_admin_role', 'route' => 'chill_event_admin_role',
])->setExtras(['order' => 6530]); ])->setExtras(['order' => 6530]);
$menu->addChild('event.theme.label', [
'route' => 'chill_crud_event_theme_index',
])->setExtras(['order' => 6540]);
$menu->addChild('event.budget.label', [
'route' => 'chill_crud_event_budget_kind_index',
])->setExtras(['order' => 6550]);
} }
public static function getMenuIds(): array public static function getMenuIds(): array

View File

@@ -88,6 +88,16 @@ final readonly class EventACLAwareRepository implements EventACLAwareRepositoryI
$qb->andWhere('event.type IN (:event_types)'); $qb->andWhere('event.type IN (:event_types)');
$qb->setParameter('event_types', $filters['event_types']); $qb->setParameter('event_types', $filters['event_types']);
} }
if (0 < count($filters['centers'] ?? [])) {
$qb->andWhere('event.center IN (:centers)');
$qb->setParameter('centers', $filters['centers']);
}
if (0 < count($filters['responsables'] ?? [])) {
$qb->andWhere('event.moderator IN (:responsables)');
$qb->setParameter('responsables', $filters['responsables']);
}
} }
public function buildQueryByAllViewable(array $filters): QueryBuilder public function buildQueryByAllViewable(array $filters): QueryBuilder

View File

@@ -9,19 +9,19 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code. * the LICENSE file that was distributed with this source code.
*/ */
namespace Chill\TicketBundle\Repository; namespace Chill\EventBundle\Repository;
use Chill\TicketBundle\Entity\Motive; use Chill\EventBundle\Entity\EventBudgetElement;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
/** /**
* @template-extends ServiceEntityRepository<Motive> * @extends ServiceEntityRepository<EventBudgetElement>
*/ */
class MotiveRepository extends ServiceEntityRepository class EventBudgetElementRepository extends ServiceEntityRepository
{ {
public function __construct(ManagerRegistry $registry) public function __construct(ManagerRegistry $registry)
{ {
parent::__construct($registry, Motive::class); parent::__construct($registry, EventBudgetElement::class);
} }
} }

View File

@@ -0,0 +1,46 @@
<?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\EventBundle\Repository;
use Chill\EventBundle\Entity\EventBudgetKind;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<EventBudgetKind>
*/
class EventBudgetKindRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, EventBudgetKind::class);
}
public function findByActive(): array
{
return $this->createQueryBuilder('e')
->select('e')
->where('e.active = True')
->getQuery()
->getResult();
}
public function findByType(string $type): array
{
return $this->createQueryBuilder('e')
->select('e')
->where('e.type = :type')
->setParameter('type', $type)
->getQuery()
->getResult();
}
}

View File

@@ -0,0 +1,37 @@
<?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\EventBundle\Repository;
use Chill\EventBundle\Entity\EventTheme;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<EventTheme>
*/
class EventThemeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, EventTheme::class);
}
public function findByActiveOrdered(): array
{
return $this->createQueryBuilder('t')
->select('t')
->where('t.isActive = True')
->orderBy('t.ordering', 'ASC')
->getQuery()
->getResult();
}
}

View File

@@ -55,3 +55,13 @@ form#export_tableur {
-webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
} }
.participation-list {
flex-wrap: wrap;
}
.participations-wrapper {
background-color: whitesmoke;
padding: 1rem;
margin-bottom: .5rem;
}

View File

@@ -0,0 +1,14 @@
<template>
<location />
</template>
<script>
import Location from "ChillActivityAssets/vuejs/Activity/components/Location.vue";
export default {
name: "App",
components: {
Location,
},
};
</script>

View File

@@ -0,0 +1,6 @@
import { createApp } from "vue";
import App from "./App.vue";
import store from "./store";
createApp(App).use(store).mount("#event");

View File

@@ -0,0 +1,76 @@
import "es6-promise/auto";
import { createStore } from "vuex";
import prepareLocations from "ChillActivityAssets/vuejs/Activity/store.locations";
import { whoami } from "ChillMainAssets/lib/api/user";
import { postLocation } from "ChillActivityAssets/vuejs/Activity/api";
const debug = process.env.NODE_ENV !== "production";
const store = createStore({
strict: debug,
state: {
activity: window.entity, // activity is the event entity in this case (re-using component from activity bundle)
currentEvent: null,
availableLocations: [],
me: null,
},
getters: {},
actions: {
addAvailableLocationGroup({ commit }, payload) {
commit("addAvailableLocationGroup", payload);
},
updateLocation({ commit }, value) {
// console.log("### action: updateLocation", value);
let hiddenLocation = document.getElementById(
"chill_activitybundle_activity_location",
);
if (value.onthefly) {
const body = {
type: "location",
name:
value.name === "__AccompanyingCourseLocation__" ? null : value.name,
locationType: {
id: value.locationType.id,
type: "location-type",
},
};
if (value.address.id) {
Object.assign(body, {
address: {
id: value.address.id,
},
});
}
postLocation(body)
.then((location) => (hiddenLocation.value = location.id))
.catch((err) => {
console.log(err.message);
});
} else {
hiddenLocation.value = value.id;
}
commit("updateLocation", value);
},
},
mutations: {
setWhoAmiI(state, me) {
state.me = me;
},
addAvailableLocationGroup(state, group) {
state.availableLocations.push(group);
},
updateLocation(state, value) {
// console.log("### mutation: updateLocation", value);
state.activity.location = value;
},
},
});
whoami().then((me) => {
store.commit("setWhoAmiI", me);
});
prepareLocations(store);
export default store;

View File

@@ -0,0 +1,12 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_edit_title.html.twig') %}
{% endblock %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
{% block content_form_actions_view %}{% endblock %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -0,0 +1,51 @@
{% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
{% block title %}{{ 'event.admin.title.Event budget element list'|trans }}{% endblock title %}
{% block admin_content %}
<h1>{{ 'event.admin.title.Event budget element list'|trans }}</h1>
<table class="records_list table table-bordered border-dark">
<thead>
<tr>
<th>{{ 'Name'|trans }}</th>
<th>{{ 'Type'|trans }}</th>
<th>{{ 'Active'|trans }}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for entity in entities %}
<tr>
<td>{{ entity.name|localize_translatable_string }}</td>
<td>{{ entity.type.value|trans }}</td>
<td style="text-align:center;">
{%- if entity.isActive -%}
<i class="fa fa-check-square-o"></i>
{%- else -%}
<i class="fa fa-square-o"></i>
{%- endif -%}
</td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_crud_event_budget_kind_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ chill_pagination(paginator) }}
<ul class="record_actions sticky-form-buttons">
<li>
<a href="{{ path('chill_crud_event_budget_kind_new') }}" class="btn btn-create">
{{ 'event.admin.new.Create a new budget kind'|trans }}
</a>
</li>
</ul>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_new_content.html.twig' %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_edit_title.html.twig') %}
{% endblock %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
{% block crud_content_form_rows %}
{{ form_row(form.name) }}
<div class="mb-3 row">
<label class="col-form-label col-sm-4">
{{ 'Parent'|trans }}
</label>
<div class="col-sm-8">
{{ entity.parent|chill_entity_render_box }}
</div>
</div>
{{ form_row(form.ordering) }}
{{ form_row(form.isActive) }}
{% endblock crud_content_form_rows %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock admin_content %}

View File

@@ -0,0 +1,45 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_index.html.twig' %}
{% block table_entities_thead_tr %}
<th>{{ 'Id'|trans }}</th>
<th>{{ 'Title'|trans }}</th>
<th>{{ 'Ordering'|trans }}</th>
<th>{{ 'active'|trans }}</th>
<th>&nbsp;</th>
{% endblock %}
{% block table_entities_tbody %}
{% for entity in entities %}
<tr>
<td>{{ entity.id }}</td>
<td>
{{ entity|chill_entity_render_box }}
</td>
<td>{{ entity.ordering }}</td>
<td style="text-align:center;">
{%- if entity.isActive -%}
<i class="fa fa-check-square-o"></i>
{%- else -%}
<i class="fa fa-square-o"></i>
{%- endif -%}
</td>
<td>
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_crud_event_theme_edit', { 'id': entity.id }) }}" class="btn btn-edit"></a>
</li>
</ul>
</td>
</tr>
{% endfor %}
{% endblock %}
{% block actions_before %}
<li class='cancel'>
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans}}</a>
</li>
{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_new_content.html.twig' %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock admin_content %}

View File

@@ -0,0 +1,13 @@
{% set reversed_parents = parents|reverse %}
<span class="chill-entity entity-event-theme">
<span class="badge bg-chill-l-gray text-dark">
{%- for p in reversed_parents %}
<span class="parent-{{ loop.revindex0 }}">
{{ p.name|localize_translatable_string }}{{ options['default.separator'] }}
</span>
{%- endfor -%}
<span class="child">
{{ eventTheme.name|localize_translatable_string }}
</span>
</span>
</span>

View File

@@ -12,7 +12,7 @@
'title' : 'Delete event'|trans, 'title' : 'Delete event'|trans,
'confirm_question' : 'Are you sure you want to remove that event ?'|trans, 'confirm_question' : 'Are you sure you want to remove that event ?'|trans,
'cancel_route' : activeRouteKey, 'cancel_route' : activeRouteKey,
'cancel_parameters' : { 'event_id' : event_id }, 'cancel_parameters' : { 'id' : id },
'form' : delete_form 'form' : delete_form
} }
) }} ) }}

View File

@@ -17,10 +17,12 @@
{{ form_row(edit_form.date) }} {{ form_row(edit_form.date) }}
{{ form_row(edit_form.type, { label: "Event type" }) }} {{ form_row(edit_form.type, { label: "Event type" }) }}
{{ form_row(edit_form.themes) }}
{{ form_row(edit_form.moderator) }} {{ form_row(edit_form.moderator) }}
{{ form_row(edit_form.animatorsIntern) }}
{{ form_row(edit_form.animatorsExtern) }}
{{ form_row(edit_form.location) }} {{ form_row(edit_form.location) }}
{{ form_row(edit_form.organizationCost) }} {{ form_row(edit_form.budgetElements) }}
{{ form_row(edit_form.comment) }} {{ form_row(edit_form.comment) }}
{{ form_row(edit_form.documents) }} {{ form_row(edit_form.documents) }}

View File

@@ -34,7 +34,7 @@
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
{# {% if is_granted('CHILL_EVENT_SEE_DETAILS', event) %} #} {# {% if is_granted('CHILL_EVENT_SEE_DETAILS', event) %} #}
<a href="{{ path('chill_event__event_show', { 'event_id' : event.id } ) }}" class="btn btn-view"> <a href="{{ path('chill_event__event_show', { 'id' : event.id } ) }}" class="btn btn-view">
{{ 'See'|trans }} {{ 'See'|trans }}
</a> </a>
{# {% endif %} #} {# {% endif %} #}

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