Compare commits

..

16 Commits

Author SHA1 Message Date
ab98f3a102 Release v4.4.2 2025-09-12 12:47:06 +02:00
7516e68d77 Merge branch 'fix/docgen-after-accp-work-refacto' into 'master'
Fix document generation and workflow generation do not work on accompanying period work documents

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

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

Closes #369

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

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

Closes #412

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

30
.vscode/launch.json vendored
View File

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

23
.vscode/tasks.json vendored
View File

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

View File

@@ -6,6 +6,34 @@ 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.4.2 - 2025-09-12
### Fixed
* Fix document generation and workflow generation do not work on accompanying period work documents
## v4.4.1 - 2025-09-11
### Fixed
* fix translations in duplicate evaluation document modal and realign close modal button
## v4.4.0 - 2025-09-11
### Feature
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works
### Fixed
* Fix display of 'duplicate' and 'merge' buttons in CRUD templates
* Fix saving notification preferences in user's profile
## v4.3.0 - 2025-09-08
### Feature
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges
* Add a command to generate a list of permissions
* ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date
**Schema Change**: Add columns or tables
### Fixed
* fix date formatting in calendar range display
* Change route URL to avoid clash with person duplicate controller method
## v4.2.1 - 2025-09-03 ## v4.2.1 - 2025-09-03
### Fixed ### Fixed
* Fix exports to work with DirectExportInterface * Fix exports to work with DirectExportInterface

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

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

View File

@@ -35,7 +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],
]; ];

View File

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

View File

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

View File

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

View File

@@ -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

@@ -79,12 +79,12 @@
"dev": "encore dev", "dev": "encore dev",
"watch": "encore dev --watch", "watch": "encore dev --watch",
"build": "encore production --progress", "build": "encore production --progress",
"specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml 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

@@ -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

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

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,24 @@
<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>
<option value="00:45:00">45 minutes</option>
<option value="00:60:00">60 minutes</option>
</select> </select>
<label class="input-group-text" for="slotMinTime">De</label> <label class="input-group-text" for="slotMinTime">De</label>
<select 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 +94,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 +126,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 +144,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 +154,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 +270,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 +280,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 +316,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 +361,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,24 @@
<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>
<option value="00:45:00">45 minutes</option>
<option value="00:60:00">60 minutes</option>
</select> </select>
<label class="input-group-text" for="slotMinTime">De</label> <label class="input-group-text" for="slotMinTime">De</label>
<select 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 +56,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 +88,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 +100,17 @@
<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, "time") }} -
{{ formatDate(event.endStr, "time") }}:
{{ 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 +129,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 +142,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 +174,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 +187,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>
@@ -246,9 +297,26 @@ const nextWeeks = computed((): Weeks[] =>
}), }),
); );
const formatDate = (datetime: string) => { const formatDate = (datetime: string, format: null | "time" = null) => {
console.log(typeof datetime); const date = ISOToDate(datetime);
return ISOToDate(datetime); if (!date) return "";
if (format === "time") {
return date.toLocaleTimeString("fr-FR", {
hour: "2-digit",
minute: "2-digit",
});
}
// French date formatting
return date.toLocaleDateString("fr-FR", {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}; };
const baseOptions = ref<CalendarOptions>({ const baseOptions = ref<CalendarOptions>({

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

@@ -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

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

View File

@@ -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_ADD, trans } from "translator";
interface DropFileConfig { interface DropFileConfig {
allowRemove: boolean; allowRemove: boolean;
@@ -75,11 +76,9 @@ function closeModal(): void {
@click="openModal" @click="openModal"
class="btn btn-create" class="btn btn-create"
> >
Ajouter un document {{ trans(DOCUMENT_ADD) }}
</button>
<button v-else @click="openModal" class="btn btn-edit">
Remplacer le document
</button> </button>
<button v-else @click="openModal" class="btn btn-edit"></button>
<modal <modal
v-if="state.showModal" v-if="state.showModal"
:modal-dialog-class="modalClasses" :modal-dialog-class="modalClasses"

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

@@ -43,17 +43,11 @@ class StoredObjectVersionNormalizer implements NormalizerInterface, NormalizerAw
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, [...$context, UserNormalizer::AT_DATE => $object->getCreatedAt()]), 'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, [...$context, UserNormalizer::AT_DATE => $object->getCreatedAt()]),
]; ];
$normalizationGroups = $context[AbstractNormalizer::GROUPS] ?? []; if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
if (is_string($normalizationGroups)) {
$normalizationGroups = [$normalizationGroups];
}
if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $normalizationGroups, true)) {
$data['point-in-times'] = $this->normalizer->normalize($object->getPointInTimes(), $format, $context); $data['point-in-times'] = $this->normalizer->normalize($object->getPointInTimes(), $format, $context);
} }
if (in_array(self::WITH_RESTORED_CONTEXT, $normalizationGroups, true)) { if (in_array(self::WITH_RESTORED_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
$data['from-restored'] = $this->normalizer->normalize($object->getCreatedFrom(), $format, [AbstractNormalizer::GROUPS => ['read']]); $data['from-restored'] = $this->normalizer->normalize($object->getCreatedFrom(), $format, [AbstractNormalizer::GROUPS => ['read']]);
} }

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

@@ -118,7 +118,7 @@
{{ entity.notes|chill_print_or_message("Aucune note", 'blockquote') }} {{ entity.notes|chill_print_or_message("Aucune note", 'blockquote') }}
{% endblock crud_content_view_details %} {% endblock crud_content_view_details %}
{% block content_view_actions_duplicate_link %}{% endblock %}
{% block content_view_actions_back %} {% block content_view_actions_back %}
<li class="cancel"> <li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_job_report_index', { 'person': entity.person.id }) }}"> <a class="btn btn-cancel" href="{{ chill_return_path_or('chill_job_report_index', { 'person': entity.person.id }) }}">

View File

@@ -46,6 +46,7 @@
</dd> </dd>
</dl> </dl>
{% endblock crud_content_view_details %} {% endblock crud_content_view_details %}
{% block content_view_actions_duplicate_link %}{% endblock %}
{% block content_view_actions_back %} {% block content_view_actions_back %}
<li class="cancel"> <li class="cancel">

View File

@@ -206,6 +206,8 @@
</a> </a>
</li> </li>
{% endblock %} {% endblock %}
{% block content_view_actions_duplicate_link %}{% endblock %}
{% block content_view_actions_after %} {% block content_view_actions_after %}
<li> <li>
<a class="btn btn-misc" href="{{ chill_return_path_or('chill_crud_immersion_bilan', { 'id': entity.id, 'person_id': entity.person.id }) }}"> <a class="btn btn-misc" href="{{ chill_return_path_or('chill_crud_immersion_bilan', { 'id': entity.id, 'person_id': entity.person.id }) }}">

View File

@@ -94,6 +94,7 @@
{% endblock crud_content_view_details %} {% endblock crud_content_view_details %}
{% block content_view_actions_duplicate_link %}{% endblock %}
{% block content_view_actions_back %} {% block content_view_actions_back %}
<li class="cancel"> <li class="cancel">

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Action\User\UpdateProfile;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\NotificationFlagManager;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use libphonenumber\PhoneNumber;
final class UpdateProfileCommand
{
public array $notificationFlags = [];
public function __construct(
#[PhonenumberConstraint]
public ?PhoneNumber $phonenumber,
) {}
public static function create(User $user, NotificationFlagManager $flagManager): self
{
$updateProfileCommand = new self($user->getPhonenumber());
foreach ($flagManager->getAllNotificationFlagProviders() as $provider) {
$updateProfileCommand->setNotificationFlag(
$provider->getFlag(),
User::NOTIF_FLAG_IMMEDIATE_EMAIL,
$user->isNotificationSendImmediately($provider->getFlag())
);
$updateProfileCommand->setNotificationFlag(
$provider->getFlag(),
User::NOTIF_FLAG_DAILY_DIGEST,
$user->isNotificationDailyDigest($provider->getFlag())
);
}
return $updateProfileCommand;
}
/**
* @param User::NOTIF_FLAG_IMMEDIATE_EMAIL|User::NOTIF_FLAG_DAILY_DIGEST $kind
*/
private function setNotificationFlag(string $type, string $kind, bool $value): void
{
if (!array_key_exists($type, $this->notificationFlags)) {
$this->notificationFlags[$type] = ['immediate_email' => true, 'daily_digest' => false];
}
$k = match ($kind) {
User::NOTIF_FLAG_IMMEDIATE_EMAIL => 'immediate_email',
User::NOTIF_FLAG_DAILY_DIGEST => 'daily_digest',
};
$this->notificationFlags[$type][$k] = $value;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Action\User\UpdateProfile;
use Chill\MainBundle\Entity\User;
final readonly class UpdateProfileCommandHandler
{
public function updateProfile(User $user, UpdateProfileCommand $command): void
{
$user->setPhonenumber($command->phonenumber);
foreach ($command->notificationFlags as $flag => $values) {
$user->setNotificationImmediately($flag, $values['immediate_email']);
$user->setNotificationDailyDigest($flag, $values['daily_digest']);
}
}
}

View File

@@ -48,6 +48,7 @@ class AbsenceController extends AbstractController
$user = $this->security->getUser(); $user = $this->security->getUser();
$user->setAbsenceStart(null); $user->setAbsenceStart(null);
$user->setAbsenceEnd(null);
$em = $this->managerRegistry->getManager(); $em = $this->managerRegistry->getManager();
$em->flush(); $em->flush();

View File

@@ -1,63 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Form\UserProfileType;
use Chill\MainBundle\Security\ChillSecurity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Routing\Annotation\Route;
final class UserProfileController extends AbstractController
{
public function __construct(
private readonly TranslatorInterface $translator,
private readonly ChillSecurity $security,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
) {}
/**
* User profile that allows editing of phonenumber and visualization of certain data.
*/
#[Route(path: '/{_locale}/main/user/my-profile', name: 'chill_main_user_profile')]
public function __invoke(Request $request)
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedHttpException();
}
$user = $this->security->getUser();
$editForm = $this->createForm(UserProfileType::class, $user);
$editForm->get('notificationFlags')->setData($user->getNotificationFlags());
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$notificationFlagsData = $editForm->get('notificationFlags')->getData();
$user->setNotificationFlags($notificationFlagsData);
$em = $this->managerRegistry->getManager();
$em->flush();
$this->addFlash('success', $this->translator->trans('user.profile.Profile successfully updated!'));
return $this->redirectToRoute('chill_main_user_profile');
}
return $this->render('@ChillMain/User/profile.html.twig', [
'user' => $user,
'form' => $editForm->createView(),
]);
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\UpdateProfileType;
use Chill\MainBundle\Notification\NotificationFlagManager;
use Chill\MainBundle\Security\ChillSecurity;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment;
final readonly class UserUpdateProfileController
{
public function __construct(
private TranslatorInterface $translator,
private ChillSecurity $security,
private EntityManagerInterface $entityManager,
private NotificationFlagManager $notificationFlagManager,
private FormFactoryInterface $formFactory,
private UrlGeneratorInterface $urlGenerator,
private Environment $twig,
private UpdateProfileCommandHandler $updateProfileCommandHandler,
) {}
/**
* User profile that allows editing of phonenumber and visualization of certain data.
*/
#[Route(path: '/{_locale}/main/user/my-profile', name: 'chill_main_user_profile')]
public function __invoke(Request $request, Session $session)
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedHttpException();
}
$user = $this->security->getUser();
$command = UpdateProfileCommand::create($user, $this->notificationFlagManager);
$editForm = $this->formFactory->create(UpdateProfileType::class, $command);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$this->updateProfileCommandHandler->updateProfile($user, $command);
$this->entityManager->flush();
$session->getFlashBag()->add('success', $this->translator->trans('user.profile.Profile successfully updated!'));
return new RedirectResponse($this->urlGenerator->generate('chill_main_user_profile'));
}
return new Response($this->twig->render('@ChillMain/User/profile.html.twig', [
'user' => $user,
'form' => $editForm->createView(),
]));
}
}

View File

@@ -24,6 +24,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint; use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use Symfony\Component\Validator\Constraints as Assert;
/** /**
* User. * User.
@@ -45,6 +46,8 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $absenceStart = null; private ?\DateTimeImmutable $absenceStart = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $absenceEnd = null;
/** /**
* Array where SAML attributes's data are stored. * Array where SAML attributes's data are stored.
*/ */
@@ -157,6 +160,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this->absenceStart; return $this->absenceStart;
} }
public function getAbsenceEnd(): ?\DateTimeImmutable
{
return $this->absenceEnd;
}
/** /**
* Get attributes. * Get attributes.
* *
@@ -336,7 +344,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
public function isAbsent(): bool public function isAbsent(): bool
{ {
return null !== $this->getAbsenceStart() && $this->getAbsenceStart() <= new \DateTimeImmutable('now'); $now = new \DateTimeImmutable('now');
$absenceStart = $this->getAbsenceStart();
$absenceEnd = $this->getAbsenceEnd();
return null !== $absenceStart
&& $absenceStart <= $now
&& (null === $absenceEnd || $now <= $absenceEnd);
} }
/** /**
@@ -410,6 +424,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
$this->absenceStart = $absenceStart; $this->absenceStart = $absenceStart;
} }
public function setAbsenceEnd(?\DateTimeImmutable $absenceEnd): void
{
$this->absenceEnd = $absenceEnd;
}
public function setAttributeByDomain(string $domain, string $key, $value): self public function setAttributeByDomain(string $domain, string $key, $value): self
{ {
$this->attributes[$domain][$key] = $value; $this->attributes[$domain][$key] = $value;
@@ -633,46 +652,82 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return true; return true;
} }
public function getNotificationFlags(): array private function getNotificationFlagData(string $flag): array
{ {
return $this->notificationFlags; return $this->notificationFlags[$flag] ?? [self::NOTIF_FLAG_IMMEDIATE_EMAIL];
}
public function setNotificationFlags(array $notificationFlags)
{
$this->notificationFlags = $notificationFlags;
}
public function getNotificationFlagData(string $flag): array
{
return $this->notificationFlags[$flag] ?? [];
}
public function setNotificationFlagData(string $flag, array $data): void
{
$this->notificationFlags[$flag] = $data;
} }
public function isNotificationSendImmediately(string $type): bool public function isNotificationSendImmediately(string $type): bool
{ {
if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) { return $this->isNotificationForElement($type, self::NOTIF_FLAG_IMMEDIATE_EMAIL);
return true;
} }
return false; public function setNotificationImmediately(string $type, bool $active): void
{
$this->setNotificationFlagElement($type, $active, self::NOTIF_FLAG_IMMEDIATE_EMAIL);
}
public function setNotificationDailyDigest(string $type, bool $active): void
{
$this->setNotificationFlagElement($type, $active, self::NOTIF_FLAG_DAILY_DIGEST);
}
/**
* @param self::NOTIF_FLAG_IMMEDIATE_EMAIL|self::NOTIF_FLAG_DAILY_DIGEST $kind
*/
private function setNotificationFlagElement(string $type, bool $active, string $kind): void
{
$notificationFlags = [...$this->notificationFlags];
$changed = false;
if (!isset($notificationFlags[$type])) {
$notificationFlags[$type] = [self::NOTIF_FLAG_IMMEDIATE_EMAIL];
$changed = true;
}
if ($active) {
if (!in_array($kind, $notificationFlags[$type], true)) {
$notificationFlags[$type] = [...$notificationFlags[$type], $kind];
$changed = true;
}
} else {
if (in_array($kind, $notificationFlags[$type], true)) {
$notificationFlags[$type] = array_values(
array_filter($notificationFlags[$type], static fn ($k) => $k !== $kind)
);
$changed = true;
}
}
if ($changed) {
$this->notificationFlags = [...$notificationFlags];
}
}
private function isNotificationForElement(string $type, string $kind): bool
{
return in_array($kind, $this->getNotificationFlagData($type), true);
} }
public function isNotificationDailyDigest(string $type): bool public function isNotificationDailyDigest(string $type): bool
{ {
if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) { return $this->isNotificationForElement($type, self::NOTIF_FLAG_DAILY_DIGEST);
return true;
}
return false;
} }
public function getLocale(): string public function getLocale(): string
{ {
return 'fr'; return 'fr';
} }
#[Assert\Callback]
public function validateAbsenceDates(ExecutionContextInterface $context): void
{
if (null !== $this->getAbsenceEnd() && null === $this->getAbsenceStart()) {
$context->buildViolation(
'user.absence_end_requires_start'
)
->atPath('absenceEnd')
->addViolation();
}
}
} }

View File

@@ -27,8 +27,6 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
// command to get the report with curl : curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff
/** /**
* Create a CSV List for the export. * Create a CSV List for the export.
*/ */

View File

@@ -23,9 +23,14 @@ class AbsenceType extends AbstractType
{ {
$builder $builder
->add('absenceStart', ChillDateType::class, [ ->add('absenceStart', ChillDateType::class, [
'required' => true, 'required' => false,
'input' => 'datetime_immutable', 'input' => 'datetime_immutable',
'label' => 'absence.Absence start', 'label' => 'absence.Absence start',
])
->add('absenceEnd', ChillDateType::class, [
'required' => false,
'input' => 'datetime_immutable',
'label' => 'absence.Absence end',
]); ]);
} }

View File

@@ -1,75 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form\DataMapper;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Form\DataMapperInterface;
final readonly class NotificationFlagDataMapper implements DataMapperInterface
{
public function __construct(private array $notificationFlagProviders) {}
public function mapDataToForms($viewData, $forms): void
{
if (null === $viewData) {
$viewData = [];
}
$formsArray = iterator_to_array($forms);
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
if (isset($formsArray[$flag])) {
$flagForm = $formsArray[$flag];
$immediateEmailChecked = in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $viewData[$flag] ?? [], true)
|| !array_key_exists($flag, $viewData);
$dailyEmailChecked = in_array(User::NOTIF_FLAG_DAILY_DIGEST, $viewData[$flag] ?? [], true);
if ($flagForm->has('immediate_email')) {
$flagForm->get('immediate_email')->setData($immediateEmailChecked);
}
if ($flagForm->has('daily_email')) {
$flagForm->get('daily_email')->setData($dailyEmailChecked);
}
}
}
}
public function mapFormsToData($forms, &$viewData): void
{
$formsArray = iterator_to_array($forms);
$viewData = [];
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
if (isset($formsArray[$flag])) {
$flagForm = $formsArray[$flag];
$viewData[$flag] = [];
if (true === $flagForm['immediate_email']->getData()) {
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
}
if (true === $flagForm['daily_email']->getData()) {
$viewData[$flag][] = User::NOTIF_FLAG_DAILY_DIGEST;
}
if ([] === $viewData[$flag]) {
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
}
}
}
}
}

View File

@@ -11,11 +11,9 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type; namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper;
use Chill\MainBundle\Notification\NotificationFlagManager; use Chill\MainBundle\Notification\NotificationFlagManager;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -30,27 +28,24 @@ class NotificationFlagsType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders));
foreach ($this->notificationFlagProviders as $flagProvider) { foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag(); $flag = $flagProvider->getFlag();
$builder->add($flag, FormType::class, [ $flagBuilder = $builder->create($flag, options: [
'label' => $flagProvider->getLabel(), 'label' => $flagProvider->getLabel(),
'required' => false, 'compound' => true,
]); ]);
$builder->get($flag) $flagBuilder
->add('immediate_email', CheckboxType::class, [ ->add('immediate_email', CheckboxType::class, [
'label' => false, 'label' => false,
'required' => false, 'required' => false,
'mapped' => false,
]) ])
->add('daily_email', CheckboxType::class, [ ->add('daily_digest', CheckboxType::class, [
'label' => false, 'label' => false,
'required' => false, 'required' => false,
'mapped' => false,
]) ])
; ;
$builder->add($flagBuilder);
} }
} }
@@ -58,6 +53,7 @@ class NotificationFlagsType extends AbstractType
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'data_class' => null, 'data_class' => null,
'compound' => true,
]); ]);
} }
} }

View File

@@ -11,31 +11,29 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form; namespace Chill\MainBundle\Form;
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType; use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Form\Type\NotificationFlagsType; use Chill\MainBundle\Form\Type\NotificationFlagsType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
class UserProfileType extends AbstractType class UpdateProfileType extends AbstractType
{ {
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder $builder
->add('phonenumber', ChillPhoneNumberType::class, [ ->add('phonenumber', ChillPhoneNumberType::class, [
'required' => false, 'required' => false,
]) ])
->add('notificationFlags', NotificationFlagsType::class, [ ->add('notificationFlags', NotificationFlagsType::class)
'label' => false,
'mapped' => false,
])
; ;
} }
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver): void
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'data_class' => \Chill\MainBundle\Entity\User::class, 'data_class' => UpdateProfileCommand::class,
]); ]);
} }
} }

View File

@@ -105,6 +105,11 @@ class UserType extends AbstractType
'required' => false, 'required' => false,
'input' => 'datetime_immutable', 'input' => 'datetime_immutable',
'label' => 'absence.Absence start', 'label' => 'absence.Absence start',
])
->add('absenceEnd', ChillDateType::class, [
'required' => false,
'input' => 'datetime_immutable',
'label' => 'absence.Absence end',
]); ]);
// @phpstan-ignore-next-line // @phpstan-ignore-next-line

View File

@@ -76,24 +76,6 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']); ->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']);
} }
/**
* @throws NumberParseException
*/
public function parse(string $phoneNumber): PhoneNumber
{
$sanitizedPhoneNumber = $phoneNumber;
if (str_starts_with($sanitizedPhoneNumber, '00')) {
$sanitizedPhoneNumber = '+'.substr($sanitizedPhoneNumber, 2, null);
}
if (!str_starts_with($sanitizedPhoneNumber, '+') && !str_starts_with($sanitizedPhoneNumber, '0')) {
$sanitizedPhoneNumber = '+'.$sanitizedPhoneNumber;
}
return $this->phoneNumberUtil->parse($sanitizedPhoneNumber, $this->config['default_carrier_code']);
}
/** /**
* Get type (mobile, landline, ...) for phone number. * Get type (mobile, landline, ...) for phone number.
*/ */

View File

@@ -37,8 +37,13 @@ export const ISOToDate = (str: string | null): Date | null => {
return null; return null;
} }
const [year, month, day] = str.split("-").map((p) => parseInt(p)); // If the string already contains time info, use it directly
if (str.includes("T") || str.includes(" ")) {
return new Date(str);
}
// Otherwise, parse date only
const [year, month, day] = str.split("-").map((p) => parseInt(p));
return new Date(year, month - 1, day, 0, 0, 0, 0); return new Date(year, month - 1, day, 0, 0, 0, 0);
}; };
@@ -56,7 +61,9 @@ export const ISOToDatetime = (str: string | null): Date | null => {
[time, timezone] = times.split(times.charAt(8)), [time, timezone] = times.split(times.charAt(8)),
[hours, minutes, seconds] = time.split(":").map((s) => parseInt(s)); [hours, minutes, seconds] = time.split(":").map((s) => parseInt(s));
if ("0000" === timezone) { if ("0000" === timezone) {
return new Date(Date.UTC(year, month - 1, date, hours, minutes, seconds)); return new Date(
Date.UTC(year, month - 1, date, hours, minutes, seconds),
);
} }
return new Date(year, month - 1, date, hours, minutes, seconds); return new Date(year, month - 1, date, hours, minutes, seconds);
@@ -67,20 +74,19 @@ export const ISOToDatetime = (str: string | null): Date | null => {
* *
*/ */
export const datetimeToISO = (date: Date): string => { export const datetimeToISO = (date: Date): string => {
let cal, time, offset; const cal = [
cal = [
date.getFullYear(), date.getFullYear(),
(date.getMonth() + 1).toString().padStart(2, "0"), (date.getMonth() + 1).toString().padStart(2, "0"),
date.getDate().toString().padStart(2, "0"), date.getDate().toString().padStart(2, "0"),
].join("-"); ].join("-");
time = [ const time = [
date.getHours().toString().padStart(2, "0"), date.getHours().toString().padStart(2, "0"),
date.getMinutes().toString().padStart(2, "0"), date.getMinutes().toString().padStart(2, "0"),
date.getSeconds().toString().padStart(2, "0"), date.getSeconds().toString().padStart(2, "0"),
].join(":"); ].join(":");
offset = [ const offset = [
date.getTimezoneOffset() <= 0 ? "+" : "-", date.getTimezoneOffset() <= 0 ? "+" : "-",
Math.abs(Math.floor(date.getTimezoneOffset() / 60)) Math.abs(Math.floor(date.getTimezoneOffset() / 60))
.toString() .toString()
@@ -152,7 +158,9 @@ export const intervalISOToDays = (str: string | null): number | null => {
vstring = ""; vstring = "";
break; break;
default: default:
throw Error("this character should not appears: " + str.charAt(i)); throw Error(
"this character should not appears: " + str.charAt(i),
);
} }
} }

View File

@@ -5,16 +5,11 @@ export type fetchOption = Record<string, boolean | string | number | null>;
export type Params = Record<string, number | string>; export type Params = Record<string, number | string>;
export interface Pagination {
first: number;
items_per_page: number;
more: boolean;
next: string | null;
previous: string | null;
}
export interface PaginationResponse<T> { export interface PaginationResponse<T> {
pagination: Pagination; pagination: {
more: boolean;
items_per_page: number;
};
results: T[]; results: T[];
count: number; count: number;
} }

View File

@@ -8,26 +8,6 @@ import { TranslatableString } from "ChillMainAssets/types";
* @param locale defaults to browser locale * @param locale defaults to browser locale
* @returns The localized string or null if no translation is available * @returns The localized string or null if no translation is available
*/ */
/**
* Prepends the current HTML lang code to the given URL.
* Example: If lang="fr" and url="/about", returns "/fr/about"
*
* @param url The URL to localize
* @returns The localized URL
*/
export function localizedUrl(url: string): string {
const lang =
document.documentElement.lang || navigator.language.split("-")[0] || "fr";
// Ensure url starts with a slash and does not already start with /{lang}/
const normalizedUrl = url.startsWith("/") ? url : `/${url}`;
const langPrefix = `/${lang}`;
if (normalizedUrl.startsWith(langPrefix + "/")) {
return normalizedUrl;
}
return `${langPrefix}${normalizedUrl}`;
}
export function localizeString( export function localizeString(
translatableString: TranslatableString | null | undefined, translatableString: TranslatableString | null | undefined,
locale?: string, locale?: string,

View File

@@ -126,8 +126,7 @@ function loadDynamicPicker(element) {
-1 === -1 ===
this.suggested.findIndex( this.suggested.findIndex(
(e) => e.type === entity.type && e.id === entity.id, (e) => e.type === entity.type && e.id === entity.id,
) && )
"me" !== entity
) { ) {
this.suggested.push(entity); this.suggested.push(entity);
} }

View File

@@ -0,0 +1,15 @@
import { createApp } from "vue";
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
import { appMessages } from "ChillMainAssets/vuejs/HomepageWidget/js/i18n";
import { store } from "ChillMainAssets/vuejs/HomepageWidget/js/store";
import App from "ChillMainAssets/vuejs/HomepageWidget/App";
const i18n = _createI18n(appMessages);
const app = createApp({
template: `<app></app>`,
})
.use(store)
.use(i18n)
.component("app", App)
.mount("#homepage_widget");

View File

@@ -1,12 +0,0 @@
import App from "ChillMainAssets/vuejs/HomepageWidget/App.vue";
import { createApp } from "vue";
import { store } from "ChillMainAssets/vuejs/HomepageWidget/store";
declare global {
interface Window {
homepage_config: string;
}
}
const _app = createApp(App);
_app.use(store).mount("#homepage_widget");

View File

@@ -11,11 +11,6 @@ export interface Civility {
// TODO // TODO
} }
export interface Household {
type: "household";
id: number;
}
export interface Job { export interface Job {
id: number; id: number;
type: "user_job"; type: "user_job";
@@ -220,83 +215,3 @@ export interface ExportGeneration {
export interface PrivateCommentEmbeddable { export interface PrivateCommentEmbeddable {
comments: Record<number, string>; comments: Record<number, string>;
} }
// API Exception types
export interface TransportExceptionInterface {
name: string;
}
export interface ValidationExceptionInterface
extends TransportExceptionInterface {
name: "ValidationException";
error: object;
violations: string[];
titles: string[];
propertyPaths: string[];
}
export interface AccessExceptionInterface extends TransportExceptionInterface {
name: "AccessException";
violations: string[];
}
export interface NotFoundExceptionInterface
extends TransportExceptionInterface {
name: "NotFoundException";
}
export interface ServerExceptionInterface extends TransportExceptionInterface {
name: "ServerException";
message: string;
code: number;
body: string;
}
export interface ConflictHttpExceptionInterface
extends TransportExceptionInterface {
name: "ConflictHttpException";
violations: string[];
}
export type ApiException =
| ValidationExceptionInterface
| AccessExceptionInterface
| NotFoundExceptionInterface
| ServerExceptionInterface
| ConflictHttpExceptionInterface;
export interface Modal {
showModal: boolean;
modalDialogClass: string;
}
export interface Selected {
result: UserGroupOrUser;
}
export interface addNewEntities {
selected: Selected[];
modal: Modal;
}
export enum HomepageTabs {
MyCustoms,
MyTickets,
MyNotifications,
MyAccompanyingCourses,
MyEvaluations,
MyTasks,
MyWorkflows,
}
export interface HomepageConfig {
defaultTab: HomepageTabs;
displayTabs: HomepageTabs[];
}
export interface TabDefinition {
key: HomepageTabs;
label: string;
icon: string | null;
counter: () => number;
}

View File

@@ -148,7 +148,10 @@
</template> </template>
<template #action> <template #action>
<li v-if="!this.context.edit && this.useDatePane"> <li v-if="!this.context.edit && this.useDatePane">
<button class="btn btn-update change-icon" @click="closeEditPane"> <button
class="btn btn-update change-icon"
@click="closeEditPane"
>
{{ trans(NEXT) }} {{ trans(NEXT) }}
<i class="fa fa-fw fa-arrow-right" /> <i class="fa fa-fw fa-arrow-right" />
</button> </button>
@@ -274,7 +277,8 @@ export default {
PREVIOUS, PREVIOUS,
NEXT, NEXT,
}; };
},props: ["context", "options", "addressChangedCallback"], },
props: ["context", "options", "addressChangedCallback"],
components: { components: {
Modal, Modal,
ShowPane, ShowPane,
@@ -390,7 +394,8 @@ export default {
getTextTitle() { getTextTitle() {
if ( if (
typeof this.options.title !== "undefined" && typeof this.options.title !== "undefined" &&
(this.options.title.edit !== null || this.options.title.create !== null) (this.options.title.edit !== null ||
this.options.title.create !== null)
) { ) {
console.log("this.options.title", this.options.title); console.log("this.options.title", this.options.title);
@@ -510,7 +515,10 @@ export default {
if (!this.context.edit) { if (!this.context.edit) {
this.context.edit = true; this.context.edit = true;
this.context.addressId = params.addressId; this.context.addressId = params.addressId;
console.log("context is now edit, with address", params.addressId); console.log(
"context is now edit, with address",
params.addressId,
);
} }
} }
}, },
@@ -637,7 +645,9 @@ export default {
? this.entity.address.confidential ? this.entity.address.confidential
: false; : false;
this.entity.selected.isNoAddress = this.entity.selected.isNoAddress =
this.context.edit && this.entity.address.text === "" ? true : false; this.context.edit && this.entity.address.text === ""
? true
: false;
this.entity.selected.country = this.context.edit this.entity.selected.country = this.context.edit
? this.entity.address.country ? this.entity.address.country
@@ -732,7 +742,8 @@ export default {
// add the address reference, if any // add the address reference, if any
if (this.entity.selected.address.addressReference !== undefined) { if (this.entity.selected.address.addressReference !== undefined) {
newAddress = Object.assign(newAddress, { newAddress = Object.assign(newAddress, {
addressReference: this.entity.selected.address.addressReference, addressReference:
this.entity.selected.address.addressReference,
}); });
} else { } else {
newAddress = Object.assign(newAddress, { newAddress = Object.assign(newAddress, {
@@ -752,7 +763,10 @@ export default {
}); });
} }
if (this.validTo && null !== this.entity.selected.valid.to) { if (this.validTo && null !== this.entity.selected.valid.to) {
console.log("add validTo in fetch body", this.entity.selected.valid.to); console.log(
"add validTo in fetch body",
this.entity.selected.valid.to,
);
newAddress = Object.assign(newAddress, { newAddress = Object.assign(newAddress, {
validTo: { validTo: {
datetime: `${this.entity.selected.valid.to.toISOString().split("T")[0]}T00:00:00+0100`, datetime: `${this.entity.selected.valid.to.toISOString().split("T")[0]}T00:00:00+0100`,
@@ -764,7 +778,10 @@ export default {
newPostcode = Object.assign(newPostcode, { newPostcode = Object.assign(newPostcode, {
country: { id: this.entity.selected.country.id }, country: { id: this.entity.selected.country.id },
}); //TODO why not assign postcodeBody here = Object.assign(postcodeBody, {'origin': 3}); ? }); //TODO why not assign postcodeBody here = Object.assign(postcodeBody, {'origin': 3}); ?
console.log("writeNew postcode is true! newPostcode: ", newPostcode); console.log(
"writeNew postcode is true! newPostcode: ",
newPostcode,
);
newAddress = Object.assign(newAddress, { newAddress = Object.assign(newAddress, {
newPostcode: newPostcode, newPostcode: newPostcode,
}); });

View File

@@ -72,7 +72,10 @@ export default {
this.entity.addressMap.zoom, this.entity.addressMap.zoom,
); );
} else { } else {
this.map.setView(lonLatForLeaflet(this.addressPoint.coordinates), 15); this.map.setView(
lonLatForLeaflet(this.addressPoint.coordinates),
15,
);
} }
this.map.scrollWheelZoom.disable(); this.map.scrollWheelZoom.disable();
@@ -102,7 +105,9 @@ export default {
}, },
update() { update() {
if (this.marker && this.entity.addressMap.center) { if (this.marker && this.entity.addressMap.center) {
this.marker.setLatLng(lonLatForLeaflet(this.entity.addressMap.center)); this.marker.setLatLng(
lonLatForLeaflet(this.entity.addressMap.center),
);
this.map.panTo(lonLatForLeaflet(this.entity.addressMap.center)); this.map.panTo(lonLatForLeaflet(this.entity.addressMap.center));
} }
}, },

View File

@@ -117,7 +117,8 @@ export default {
ADDRESS_FILL_AN_ADDRESS, ADDRESS_FILL_AN_ADDRESS,
trans, trans,
}; };
},props: ["entity", "isNoAddress"], },
props: ["entity", "isNoAddress"],
computed: { computed: {
floor: { floor: {
set(value) { set(value) {

View File

@@ -96,10 +96,13 @@ export default {
ADDRESS_CREATE_ADDRESS, ADDRESS_CREATE_ADDRESS,
trans, trans,
}; };
},props: ["entity", "context", "updateMapCenter", "flag", "checkErrors"], },
props: ["entity", "context", "updateMapCenter", "flag", "checkErrors"],
data() { data() {
return { return {
value: this.context.edit ? this.entity.address.addressReference : null, value: this.context.edit
? this.entity.address.addressReference
: null,
isLoading: false, isLoading: false,
}; };
}, },
@@ -172,7 +175,8 @@ export default {
.then( .then(
(addresses) => (addresses) =>
new Promise((resolve) => { new Promise((resolve) => {
this.entity.loaded.addresses = addresses.results; this.entity.loaded.addresses =
addresses.results;
this.isLoading = false; this.isLoading = false;
resolve(); resolve();
}), }),
@@ -189,7 +193,8 @@ export default {
.then( .then(
(addresses) => (addresses) =>
new Promise((resolve) => { new Promise((resolve) => {
this.entity.loaded.addresses = addresses.results; this.entity.loaded.addresses =
addresses.results;
this.isLoading = false; this.isLoading = false;
resolve(); resolve();
}), }),

View File

@@ -85,7 +85,8 @@ export default {
ADDRESS_CREATE_POSTAL_CODE, ADDRESS_CREATE_POSTAL_CODE,
trans, trans,
}; };
},props: [ },
props: [
"entity", "entity",
"context", "context",
"focusOnAddress", "focusOnAddress",
@@ -157,7 +158,9 @@ export default {
}, },
methods: { methods: {
transName(value) { transName(value) {
return value.code && value.name ? `${value.name} (${value.code})` : ""; return value.code && value.name
? `${value.name} (${value.code})`
: "";
}, },
selectCity(value) { selectCity(value) {
console.log(value); console.log(value);
@@ -165,7 +168,8 @@ export default {
this.entity.selected.postcode.name = value.name; this.entity.selected.postcode.name = value.name;
this.entity.selected.postcode.code = value.code; this.entity.selected.postcode.code = value.code;
if (value.center) { if (value.center) {
this.entity.selected.postcode.coordinates = value.center.coordinates; this.entity.selected.postcode.coordinates =
value.center.coordinates;
} }
this.entity.selected.writeNew.postcode = false; this.entity.selected.writeNew.postcode = false;
this.$emit("getReferenceAddresses", value); this.$emit("getReferenceAddresses", value);
@@ -186,7 +190,8 @@ export default {
.then( .then(
(cities) => (cities) =>
new Promise((resolve) => { new Promise((resolve) => {
this.entity.loaded.cities = cities.results.filter( this.entity.loaded.cities =
cities.results.filter(
(c) => c.origin !== 3, (c) => c.origin !== 3,
); // filter out user-defined cities ); // filter out user-defined cities
this.isLoading = false; this.isLoading = false;
@@ -205,7 +210,8 @@ export default {
.then( .then(
(cities) => (cities) =>
new Promise((resolve) => { new Promise((resolve) => {
this.entity.loaded.cities = cities.results.filter( this.entity.loaded.cities =
cities.results.filter(
(c) => c.origin !== 3, (c) => c.origin !== 3,
); // filter out user-defined cities ); // filter out user-defined cities
this.isLoading = false; this.isLoading = false;

View File

@@ -44,7 +44,8 @@ export default {
ADDRESS_SELECT_COUNTRY, ADDRESS_SELECT_COUNTRY,
trans, trans,
}; };
},props: ["context", "entity", "flag", "checkErrors"], },
props: ["context", "entity", "flag", "checkErrors"],
emits: ["getCities"], emits: ["getCities"],
data() { data() {
return { return {
@@ -59,8 +60,12 @@ export default {
sortedCountries() { sortedCountries() {
const countries = this.entity.loaded.countries; const countries = this.entity.loaded.countries;
let sortedCountries = []; let sortedCountries = [];
sortedCountries.push(...countries.filter((c) => c.countryCode === "FR")); sortedCountries.push(
sortedCountries.push(...countries.filter((c) => c.countryCode === "BE")); ...countries.filter((c) => c.countryCode === "FR"),
);
sortedCountries.push(
...countries.filter((c) => c.countryCode === "BE"),
);
sortedCountries.push( sortedCountries.push(
...countries ...countries
.filter((c) => c.countryCode !== "FR") .filter((c) => c.countryCode !== "FR")

View File

@@ -1,6 +1,9 @@
<template> <template>
<div v-if="insideModal === false" class="loading"> <div v-if="insideModal === false" class="loading">
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw" /> <i
v-if="flag.loading"
class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"
/>
<span class="sr-only">{{ $t("loading") }}</span> <span class="sr-only">{{ $t("loading") }}</span>
</div> </div>
@@ -139,7 +142,8 @@ export default {
address["street"] = this.entity.selected.address.street address["street"] = this.entity.selected.address.street
? this.entity.selected.address.street ? this.entity.selected.address.street
: null; : null;
address["streetNumber"] = this.entity.selected.address.streetNumber address["streetNumber"] = this.entity.selected.address
.streetNumber
? this.entity.selected.address.streetNumber ? this.entity.selected.address.streetNumber
: null; : null;
address["floor"] = this.entity.selected.address.floor address["floor"] = this.entity.selected.address.floor
@@ -154,10 +158,12 @@ export default {
address["flat"] = this.entity.selected.address.flat address["flat"] = this.entity.selected.address.flat
? this.entity.selected.address.flat ? this.entity.selected.address.flat
: null; : null;
address["buildingName"] = this.entity.selected.address.buildingName address["buildingName"] = this.entity.selected.address
.buildingName
? this.entity.selected.address.buildingName ? this.entity.selected.address.buildingName
: null; : null;
address["distribution"] = this.entity.selected.address.distribution address["distribution"] = this.entity.selected.address
.distribution
? this.entity.selected.address.distribution ? this.entity.selected.address.distribution
: null; : null;
address["extra"] = this.entity.selected.address.extra address["extra"] = this.entity.selected.address.extra

View File

@@ -2,7 +2,10 @@
<div class="address-form"> <div class="address-form">
<!-- Not display in modal --> <!-- Not display in modal -->
<div v-if="insideModal === false" class="loading"> <div v-if="insideModal === false" class="loading">
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw" /> <i
v-if="flag.loading"
class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"
/>
<span class="sr-only">Loading...</span> <span class="sr-only">Loading...</span>
</div> </div>
@@ -129,7 +132,8 @@ export default {
ADDRESS_IS_CONFIDENTIAL, ADDRESS_IS_CONFIDENTIAL,
ADDRESS_IS_NO_ADDRESS, ADDRESS_IS_NO_ADDRESS,
}; };
},props: [ },
props: [
"context", "context",
"options", "options",
"defaultz", "defaultz",

View File

@@ -1,7 +1,10 @@
<template> <template>
<div v-if="!onlyButton" class="mt-4 flex-grow-1"> <div v-if="!onlyButton" class="mt-4 flex-grow-1">
<div class="loading"> <div class="loading">
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw" /> <i
v-if="flag.loading"
class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"
/>
<span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span> <span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span>
</div> </div>
@@ -44,7 +47,9 @@
name="button" name="button"
:title="trans(getTextButton)" :title="trans(getTextButton)"
> >
<span v-if="displayTextButton">{{ trans(getTextButton) }}</span> <span v-if="displayTextButton">{{
trans(getTextButton)
}}</span>
</button> </button>
</template> </template>
</action-buttons> </action-buttons>
@@ -57,7 +62,9 @@
:use-date-pane="useDatePane" :use-date-pane="useDatePane"
/> />
<div v-if="this.context.target.name === 'household' || this.context.edit"> <div
v-if="this.context.target.name === 'household' || this.context.edit"
>
<action-buttons :options="this.options" :defaultz="this.defaultz"> <action-buttons :options="this.options" :defaultz="this.defaultz">
<template #action> <template #action>
<button <button
@@ -68,7 +75,9 @@
name="button" name="button"
:title="trans(getTextButton)" :title="trans(getTextButton)"
> >
<span v-if="displayTextButton">{{ trans(getTextButton) }}</span> <span v-if="displayTextButton">{{
trans(getTextButton)
}}</span>
</button> </button>
</template> </template>
</action-buttons> </action-buttons>
@@ -90,7 +99,9 @@
name="button" name="button"
:title="trans(getTextButton)" :title="trans(getTextButton)"
> >
<span v-if="displayTextButton">{{ trans(getTextButton) }}</span> <span v-if="displayTextButton">{{
trans(getTextButton)
}}</span>
</button> </button>
</template> </template>
</action-buttons> </action-buttons>
@@ -113,7 +124,8 @@ import {
export default { export default {
name: "ShowPane", name: "ShowPane",
methods: {},components: { methods: {},
components: {
AddressRenderBox, AddressRenderBox,
ActionButtons, ActionButtons,
}, },
@@ -128,7 +140,8 @@ export default {
ADDRESS_ADDRESS_NEW_SUCCESS, ADDRESS_ADDRESS_NEW_SUCCESS,
ADDRESS_ADDRESS_EDIT_SUCCESS, ADDRESS_ADDRESS_EDIT_SUCCESS,
}; };
},props: [ },
props: [
"context", "context",
"defaultz", "defaultz",
"options", "options",
@@ -179,7 +192,9 @@ export default {
: this.defaultz.button.text.create; : this.defaultz.button.text.create;
}, },
getSuccessText() { getSuccessText() {
return this.context.edit ? ADDRESS_ADDRESS_EDIT_SUCCESS : ADDRESS_ADDRESS_NEW_SUCCESS; return this.context.edit
? ADDRESS_ADDRESS_EDIT_SUCCESS
: ADDRESS_ADDRESS_NEW_SUCCESS;
}, },
onlyButton() { onlyButton() {
return typeof this.options.onlyButton !== "undefined" return typeof this.options.onlyButton !== "undefined"

View File

@@ -1,6 +1,9 @@
<template> <template>
<div v-if="insideModal === false" class="loading"> <div v-if="insideModal === false" class="loading">
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw" /> <i
v-if="flag.loading"
class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"
/>
<span class="sr-only">{{ $t("loading") }}</span> <span class="sr-only">{{ $t("loading") }}</span>
</div> </div>

View File

@@ -86,7 +86,10 @@ onMounted(() => {
<template> <template>
<div id="waiting-screen"> <div id="waiting-screen">
<div v-if="isPending && isFetching" class="alert alert-danger text-center"> <div
v-if="isPending && isFetching"
class="alert alert-danger text-center"
>
<div> <div>
<p> <p>
{{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }} {{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}

View File

@@ -1,156 +1,155 @@
<template> <template>
<h2>{{ trans(MAIN_TITLE) }}</h2> <h2>{{ $t("main_title") }}</h2>
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li v-for="tab in displayedTabs" :key="tab.key" class="nav-item"> <li class="nav-item">
<a <a
class="nav-link" class="nav-link"
:class="{ active: activeTab === tab.key }" :class="{ active: activeTab === 'MyCustoms' }"
@click="selectTab(tab.key)" @click="selectTab('MyCustoms')"
> >
<i v-if="tab.icon" :class="tab.icon" /> <i class="fa fa-dashboard" />
<span v-else>{{ tab.label }}</span> </a>
<tab-counter v-if="tab.counter" :count="tab.counter()" /> </li>
<li class="nav-item">
<a
class="nav-link"
:class="{ active: activeTab === 'MyNotifications' }"
@click="selectTab('MyNotifications')"
>
{{ $t("my_notifications.tab") }}
<tab-counter :count="state.notifications.count" />
</a>
</li>
<li class="nav-item">
<a
class="nav-link"
:class="{ active: activeTab === 'MyAccompanyingCourses' }"
@click="selectTab('MyAccompanyingCourses')"
>
{{ $t("my_accompanying_courses.tab") }}
</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyWorks'}"
@click="selectTab('MyWorks')">
{{ $t('my_works.tab') }}
<tab-counter :count="state.works.count"></tab-counter>
</a>
</li> -->
<li class="nav-item">
<a
class="nav-link"
:class="{ active: activeTab === 'MyEvaluations' }"
@click="selectTab('MyEvaluations')"
>
{{ $t("my_evaluations.tab") }}
<tab-counter :count="state.evaluations.count" />
</a>
</li>
<li class="nav-item">
<a
class="nav-link"
:class="{ active: activeTab === 'MyTasks' }"
@click="selectTab('MyTasks')"
>
{{ $t("my_tasks.tab") }}
<tab-counter
:count="state.tasks.warning.count + state.tasks.alert.count"
/>
</a>
</li>
<li class="nav-item">
<a
class="nav-link"
:class="{ active: activeTab === 'MyWorkflows' }"
@click="selectTab('MyWorkflows')"
>
{{ $t("my_workflows.tab") }}
<tab-counter
:count="state.workflows.count + state.workflowsCc.count"
/>
</a> </a>
</li> </li>
<li class="nav-item loading ms-auto py-2" v-if="loading"> <li class="nav-item loading ms-auto py-2" v-if="loading">
<i <i
class="fa fa-circle-o-notch fa-spin fa-lg text-chill-gray" class="fa fa-circle-o-notch fa-spin fa-lg text-chill-gray"
:title="trans(LOADING)" :title="$t('loading')"
/> />
</li> </li>
</ul> </ul>
<div class="my-4"> <div class="my-4">
<MyCustoms v-if="activeTab === HomepageTabs.MyCustoms" /> <my-customs v-if="activeTab === 'MyCustoms'" />
<MyTickets v-else-if="activeTab === HomepageTabs.MyTickets" /> <my-works v-else-if="activeTab === 'MyWorks'" />
<MyNotifications v-else-if="activeTab === HomepageTabs.MyNotifications" /> <my-evaluations v-else-if="activeTab === 'MyEvaluations'" />
<MyAccompanyingCourses <my-tasks v-else-if="activeTab === 'MyTasks'" />
v-else-if="activeTab === HomepageTabs.MyAccompanyingCourses" <my-accompanying-courses
v-else-if="activeTab === 'MyAccompanyingCourses'"
/> />
<MyEvaluations v-else-if="activeTab === HomepageTabs.MyEvaluations" /> <my-notifications v-else-if="activeTab === 'MyNotifications'" />
<MyTasks v-else-if="activeTab === HomepageTabs.MyTasks" /> <my-workflows v-else-if="activeTab === 'MyWorkflows'" />
<MyWorkflows v-else-if="activeTab === HomepageTabs.MyWorkflows" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script>
import { ref, computed, onMounted } from "vue"; import MyCustoms from "./MyCustoms";
import { useStore } from "vuex"; import MyWorks from "./MyWorks";
import MyEvaluations from "./MyEvaluations";
// Components import MyTasks from "./MyTasks";
import MyCustoms from "./MyCustoms.vue"; import MyAccompanyingCourses from "./MyAccompanyingCourses";
import MyEvaluations from "./MyEvaluations.vue"; import MyNotifications from "./MyNotifications";
import MyTasks from "./MyTasks.vue";
import MyAccompanyingCourses from "./MyAccompanyingCourses.vue";
import MyNotifications from "./MyNotifications.vue";
import MyWorkflows from "./MyWorkflows.vue"; import MyWorkflows from "./MyWorkflows.vue";
import MyTickets from "./MyTickets.vue"; import TabCounter from "./TabCounter";
import TabCounter from "./TabCounter.vue"; import { mapState } from "vuex";
// Translations export default {
import { name: "App",
MAIN_TITLE, components: {
MY_TICKETS_TAB, MyCustoms,
MY_EVALUATIONS_TAB, MyWorks,
MY_TASKS_TAB, MyEvaluations,
MY_ACCOMPANYING_COURSES_TAB, MyTasks,
MY_NOTIFICATIONS_TAB, MyWorkflows,
MY_WORKFLOWS_TAB, MyAccompanyingCourses,
LOADING, MyNotifications,
trans, TabCounter,
} from "translator";
// Types
import {
HomepageConfig,
HomepageTabs,
TabDefinition,
} from "ChillMainAssets/types";
const store = useStore();
defineExpose({ HomepageTabs });
const homepageConfig = ref<HomepageConfig>(JSON.parse(window.homepage_config));
const state = computed(() => store.state.homepage);
const ticketListState = computed(() => store.state.ticketList);
const tabDefinitions: TabDefinition[] = [
{
key: HomepageTabs.MyCustoms,
label: "",
icon: "fa fa-dashboard",
counter: () => 0,
}, },
{ data() {
key: HomepageTabs.MyTickets, return {
label: trans(MY_TICKETS_TAB), activeTab: "MyCustoms",
icon: null, };
counter: () => ticketListState.value?.count,
}, },
{ computed: {
key: HomepageTabs.MyNotifications, ...mapState(["loading"]),
label: trans(MY_NOTIFICATIONS_TAB), // just to see all in devtool :
icon: null, ...mapState({
counter: () => state.value?.notifications?.count, state: (state) => state,
}),
}, },
{ methods: {
key: HomepageTabs.MyAccompanyingCourses, selectTab(tab) {
label: trans(MY_ACCOMPANYING_COURSES_TAB), if (tab !== "MyCustoms") {
icon: null, this.$store.dispatch("getByTab", { tab: tab });
counter: () => state.value?.accompanyingCourses?.count,
},
{
key: HomepageTabs.MyEvaluations,
label: trans(MY_EVALUATIONS_TAB),
icon: null,
counter: () => state.value?.evaluations?.count,
},
{
key: HomepageTabs.MyTasks,
label: trans(MY_TASKS_TAB),
icon: null,
counter: () =>
state.value?.tasks?.warning?.count + state.value?.tasks?.alert?.count,
},
{
key: HomepageTabs.MyWorkflows,
label: trans(MY_WORKFLOWS_TAB),
icon: null,
counter: () =>
state.value?.workflows?.count + state.value?.workflowsCc?.count,
},
];
const displayedTabs = computed(() => {
// Always show MyCustoms first if present
const tabs = [] as TabDefinition[];
for (const tabEnum of homepageConfig.value.displayTabs) {
const def = tabDefinitions.find(
(t) => t.key === Number(HomepageTabs[tabEnum]),
);
if (def) tabs.push(def);
} }
return tabs.filter(Boolean); this.activeTab = tab;
}); console.log(this.activeTab);
},
const activeTab = ref(Number(HomepageTabs[homepageConfig.value.defaultTab])); },
mounted() {
const loading = computed(() => store.state.loading); for (const m of [
"MyNotifications",
function selectTab(tab: HomepageTabs) { "MyAccompanyingCourses",
if (tab !== HomepageTabs.MyCustoms) { // 'MyWorks',
store.dispatch("getByTab", { tab: tab }); "MyEvaluations",
"MyTasks",
"MyWorkflows",
]) {
this.$store.dispatch("getByTab", { tab: m, param: "countOnly=1" });
} }
activeTab.value = tab; },
} };
onMounted(() => {
for (const tab of displayedTabs.value) {
if (tab.key !== HomepageTabs.MyCustoms) {
store.dispatch("getByTab", { tab: tab.key, param: "countOnly=1" });
}
}
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -25,9 +25,11 @@
</template> </template>
<template #body> <template #body>
<p class="news-date"> <p class="news-date">
<time class="createdBy" datetime="{{item.startDate.datetime}}">{{ <time
$d(newsItemStartDate(), "text") class="createdBy"
}}</time> datetime="{{item.startDate.datetime}}"
>{{ $d(newsItemStartDate(), "text") }}</time
>
</p> </p>
<div v-html="convertMarkdownToHtml(item.content)"></div> <div v-html="convertMarkdownToHtml(item.content)"></div>
</template> </template>
@@ -91,7 +93,10 @@ const truncateContent = (content: string): string => {
// Truncate if amount of lines are too many // Truncate if amount of lines are too many
if (lines.length > props.maxLines && content.length < props.maxLength) { if (lines.length > props.maxLines && content.length < props.maxLength) {
const truncatedContent = lines.slice(0, props.maxLines).join("\n").trim(); const truncatedContent = lines
.slice(0, props.maxLines)
.join("\n")
.trim();
return truncatedContent + "..."; return truncatedContent + "...";
} }
@@ -120,7 +125,8 @@ const truncateContent = (content: string): string => {
if (linkStartIndex !== -1) { if (linkStartIndex !== -1) {
const linkEndIndex = content.indexOf(")", linkStartIndex); const linkEndIndex = content.indexOf(")", linkStartIndex);
const url = content.slice(linkStartIndex + 1, linkEndIndex); const url = content.slice(linkStartIndex + 1, linkEndIndex);
truncatedContent = truncatedContent.slice(0, linkStartIndex) + `(${url})`; truncatedContent =
truncatedContent.slice(0, linkStartIndex) + `(${url})`;
} }
truncatedContent += "..."; truncatedContent += "...";

View File

@@ -1,40 +1,47 @@
<template> <template>
<div class="alert alert-light"> <div class="alert alert-light">
{{ trans(MY_ACCOMPANYING_COURSES_DESCRIPTION) }} {{ $t("my_accompanying_courses.description") }}
</div> </div>
<span v-if="noResults" class="chill-no-data-statement"> <span v-if="noResults" class="chill-no-data-statement">{{
{{ trans(NO_DATA) }} $t("no_data")
</span> }}</span>
<tab-table v-else> <tab-table v-else>
<template #thead> <template #thead>
<th scope="col"> <th scope="col">
{{ trans(OPENING_DATE) }} {{ $t("opening_date") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(SOCIAL_ISSUES) }} {{ $t("social_issues") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(CONCERNED_PERSONS) }} {{ $t("concerned_persons") }}
</th> </th>
<th scope="col" /> <th scope="col" />
<th scope="col" /> <th scope="col" />
</template> </template>
<template #tbody> <template #tbody>
<tr v-for="(c, i) in accompanyingCourses.results" :key="`course-${i}`"> <tr
<td>{{ $d(new Date(c.openingDate.datetime), "short") }}</td> v-for="(c, i) in accompanyingCourses.results"
:key="`course-${i}`"
>
<td>{{ $d(c.openingDate.datetime, "short") }}</td>
<td> <td>
<span <span
v-for="(issue, index) in c.socialIssues" v-for="(i, index) in c.socialIssues"
:key="index" :key="index"
class="chill-entity entity-social-issue" class="chill-entity entity-social-issue"
> >
<span class="badge bg-chill-l-gray text-dark"> <span class="badge bg-chill-l-gray text-dark">
{{ localizeString(issue.title) }} {{ localizeString(i.title) }}
</span> </span>
</span> </span>
</td> </td>
<td> <td>
<span v-for="p in c.participations" class="me-1" :key="p.person.id"> <span
v-for="p in c.participations"
class="me-1"
:key="p.person.id"
>
<on-the-fly <on-the-fly
:type="p.person.type" :type="p.person.type"
:id="p.person.id" :id="p.person.id"
@@ -45,16 +52,20 @@
</span> </span>
</td> </td>
<td> <td>
<span v-if="c.emergency" class="badge rounded-pill bg-danger me-1">{{ <span
trans(EMERGENCY) v-if="c.emergency"
}}</span> class="badge rounded-pill bg-danger me-1"
<span v-if="c.confidential" class="badge rounded-pill bg-danger">{{ >{{ $t("emergency") }}</span
trans(CONFIDENTIAL) >
}}</span> <span
v-if="c.confidential"
class="badge rounded-pill bg-danger"
>{{ $t("confidential") }}</span
>
</td> </td>
<td> <td>
<a class="btn btn-sm btn-show" :href="getUrl(c)"> <a class="btn btn-sm btn-show" :href="getUrl(c)">
{{ trans(SHOW_ENTITY, { entity: trans(THE_COURSE) }) }} {{ $t("show_entity", { entity: $t("the_course") }) }}
</a> </a>
</td> </td>
</tr> </tr>
@@ -62,45 +73,36 @@
</tab-table> </tab-table>
</template> </template>
<script lang="ts" setup> <script>
import { computed, ComputedRef } from "vue"; import { mapState, mapGetters } from "vuex";
import { useStore } from "vuex"; import TabTable from "./TabTable";
import TabTable from "./TabTable.vue"; import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import { AccompanyingCourse } from "ChillPersonAssets/types";
import { PaginationResponse } from "ChillMainAssets/lib/api/apiMethods";
import {
MY_ACCOMPANYING_COURSES_DESCRIPTION,
OPENING_DATE,
SOCIAL_ISSUES,
CONCERNED_PERSONS,
SHOW_ENTITY,
THE_COURSE,
NO_DATA,
EMERGENCY,
CONFIDENTIAL,
trans,
} from "translator";
const store = useStore();
const accompanyingCourses: ComputedRef<PaginationResponse<AccompanyingCourse>> = export default {
computed(() => store.state.homepage.accompanyingCourses); name: "MyAccompanyingCourses",
const isAccompanyingCoursesLoaded = computed( components: {
() => store.getters.isAccompanyingCoursesLoaded, TabTable,
); OnTheFly,
},
const noResults = computed(() => { computed: {
if (!isAccompanyingCoursesLoaded.value) { ...mapState(["accompanyingCourses"]),
...mapGetters(["isAccompanyingCoursesLoaded"]),
noResults() {
if (!this.isAccompanyingCoursesLoaded) {
return false; return false;
} else { } else {
return accompanyingCourses.value.count === 0; return this.accompanyingCourses.count === 0;
} }
}); },
},
function getUrl(c: { id: number }): string { methods: {
localizeString,
getUrl(c) {
return `/fr/parcours/${c.id}`; return `/fr/parcours/${c.id}`;
} },
},
};
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,72 +1,95 @@
<template> <template>
<span v-if="noResults" class="chill-no-data-statement"> <span v-if="noResults" class="chill-no-data-statement">{{
{{ trans(NO_DASHBOARD) }} $t("no_dashboard")
</span> }}</span>
<div v-else id="dashboards" class="container g-3"> <div v-else id="dashboards" class="container g-3">
<div class="row"> <div class="row">
<div class="mbloc col-xs-12 col-sm-4"> <div class="mbloc col-xs-12 col-sm-4">
<div class="custom1"> <div class="custom1">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li v-if="(counter.value?.notifications || 0) > 0"> <li v-if="counter.notifications > 0">
<span :class="counterClass"> <i18n-t
{{ keypath="counter.unread_notifications"
trans(COUNTER_UNREAD_NOTIFICATIONS, { tag="span"
n: counter.value?.notifications || 0, :class="counterClass"
}) :plural="counter.notifications"
}} >
</span> <template #n>
<span>{{ counter.notifications }}</span>
</template>
</i18n-t>
</li> </li>
<li v-if="(counter.value?.accompanyingCourses || 0) > 0"> <li v-if="counter.accompanyingCourses > 0">
<span :class="counterClass"> <i18n-t
{{ keypath="counter.assignated_courses"
trans(COUNTER_ASSIGNATED_COURSES, { tag="span"
n: counter.value?.accompanyingCourses || 0, :class="counterClass"
}) :plural="counter.accompanyingCourses"
}} >
</span> <template #n>
<span>{{
counter.accompanyingCourses
}}</span>
</template>
</i18n-t>
</li> </li>
<li v-if="(counter.value?.works || 0) > 0"> <li v-if="counter.works > 0">
<span :class="counterClass"> <i18n-t
{{ keypath="counter.assignated_actions"
trans(COUNTER_ASSIGNATED_ACTIONS, { tag="span"
n: counter.value?.works || 0, :class="counterClass"
}) :plural="counter.works"
}} >
</span> <template #n>
<span>{{ counter.works }}</span>
</template>
</i18n-t>
</li> </li>
<li v-if="(counter.value?.evaluations || 0) > 0"> <li v-if="counter.evaluations > 0">
<span :class="counterClass"> <i18n-t
{{ keypath="counter.assignated_evaluations"
trans(COUNTER_ASSIGNATED_EVALUATIONS, { tag="span"
n: counter.value?.evaluations || 0, :class="counterClass"
}) :plural="counter.evaluations"
}} >
</span> <template #n>
<span>{{ counter.evaluations }}</span>
</template>
</i18n-t>
</li> </li>
<li v-if="(counter.value?.tasksAlert || 0) > 0"> <li v-if="counter.tasksAlert > 0">
<span :class="counterClass"> <i18n-t
{{ keypath="counter.alert_tasks"
trans(COUNTER_ALERT_TASKS, { tag="span"
n: counter.value?.tasksAlert || 0, :class="counterClass"
}) :plural="counter.tasksAlert"
}} >
</span> <template #n>
<span>{{ counter.tasksAlert }}</span>
</template>
</i18n-t>
</li> </li>
<li v-if="(counter.value?.tasksWarning || 0) > 0"> <li v-if="counter.tasksWarning > 0">
<span :class="counterClass"> <i18n-t
{{ keypath="counter.warning_tasks"
trans(COUNTER_WARNING_TASKS, { tag="span"
n: counter.value?.tasksWarning || 0, :class="counterClass"
}) :plural="counter.tasksWarning"
}} >
</span> <template #n>
<span>{{ counter.tasksWarning }}</span>
</template>
</i18n-t>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<template v-if="hasDashboardItems"> <template v-if="this.hasDashboardItems">
<template v-for="(dashboardItem, index) in dashboardItems" :key="index"> <template
v-for="(dashboardItem, index) in this.dashboardItems"
:key="index"
>
<div <div
class="mbloc col-xs-12 col-sm-8 news" class="mbloc col-xs-12 col-sm-8 news"
v-if="dashboardItem.type === 'news'" v-if="dashboardItem.type === 'news'"
@@ -79,53 +102,44 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script>
import { ref, computed, onMounted } from "vue"; import { mapGetters } from "vuex";
import { useStore } from "vuex";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import News from "./DashboardWidgets/News.vue"; import News from "./DashboardWidgets/News.vue";
import {
NO_DASHBOARD,
COUNTER_UNREAD_NOTIFICATIONS,
COUNTER_ASSIGNATED_COURSES,
COUNTER_ASSIGNATED_ACTIONS,
COUNTER_ASSIGNATED_EVALUATIONS,
COUNTER_ALERT_TASKS,
COUNTER_WARNING_TASKS,
trans,
} from "translator";
interface MyCustom { export default {
id: number; name: "MyCustoms",
type: string; components: {
metadata: Record<string, unknown>; News,
userId: number; },
position: string; data() {
} return {
counterClass: {
const store = useStore(); counter: true, //hack to pass class 'counter' in i18n-t
},
const counter = computed(() => store.getters.counter); dashboardItems: [],
masonry: null,
const counterClass = { counter: true }; };
},
const dashboardItems = ref<MyCustom[]>([]); computed: {
...mapGetters(["counter"]),
const noResults = computed(() => false); noResults() {
return false;
const hasDashboardItems = computed(() => dashboardItems.value.length > 0); },
hasDashboardItems() {
onMounted(async () => { return this.dashboardItems.length > 0;
try { },
const response: MyCustom[] = await makeFetch( },
"GET", mounted() {
"/api/1.0/main/dashboard-config-item.json", makeFetch("GET", "/api/1.0/main/dashboard-config-item.json")
); .then((response) => {
dashboardItems.value = response; this.dashboardItems = response;
} catch (error) { })
.catch((error) => {
throw error; throw error;
} });
}); },
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,50 +1,50 @@
<template> <template>
<div class="accompanying-course-work"> <div class="accompanying-course-work">
<div class="alert alert-light"> <div class="alert alert-light">
{{ trans(MY_EVALUATIONS_DESCRIPTION) }} {{ $t("my_evaluations.description") }}
</div> </div>
<span v-if="noResults" class="chill-no-data-statement"> <span v-if="noResults" class="chill-no-data-statement">{{
{{ trans(NO_DATA) }} $t("no_data")
</span> }}</span>
<tab-table v-else> <tab-table v-else>
<template #thead> <template #thead>
<th scope="col"> <th scope="col">
{{ trans(MAX_DATE) }} {{ $t("max_date") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(EVALUATION) }} {{ $t("evaluation") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(SOCIAL_ACTION) }} {{ $t("SocialAction") }}
</th> </th>
<th scope="col" /> <th scope="col" />
</template> </template>
<template #tbody> <template #tbody>
<tr v-for="(e, i) in evaluations.results" :key="`evaluation-${i}`"> <tr
v-for="(e, i) in evaluations.results"
:key="`evaluation-${i}`"
>
<td>{{ $d(e.maxDate.datetime, "short") }}</td>
<td> <td>
{{ {{ localizeString(e.evaluation.title) }}
e.maxDate?.datetime
? $d(new Date(e.maxDate.datetime), "short")
: ""
}}
</td>
<td>
{{ localizeString(e.evaluation?.title ?? null) }}
</td> </td>
<td> <td>
<span class="chill-entity entity-social-issue"> <span class="chill-entity entity-social-issue">
<span class="badge bg-chill-l-gray text-dark"> <span class="badge bg-chill-l-gray text-dark">
{{ e.accompanyingPeriodWork?.socialAction?.issue?.text ?? "" }} {{
e.accompanyingPeriodWork.socialAction.issue
.text
}}
</span> </span>
</span> </span>
<h4 class="badge-title"> <h4 class="badge-title">
<span class="title_label" /> <span class="title_label" />
<span class="title_action"> <span class="title_action">
{{ e.accompanyingPeriodWork?.socialAction?.text ?? "" }} {{ e.accompanyingPeriodWork.socialAction.text }}
</span> </span>
</h4> </h4>
<span <span
v-for="person in e.accompanyingPeriodWork?.persons ?? []" v-for="person in e.accompanyingPeriodWork.persons"
class="me-1" class="me-1"
:key="person.id" :key="person.id"
> >
@@ -58,25 +58,30 @@
</span> </span>
</td> </td>
<td> <td>
<div class="btn-group-vertical" role="group" aria-label="Actions"> <div
class="btn-group-vertical"
role="group"
aria-label="Actions"
>
<a class="btn btn-sm btn-show" :href="getUrl(e)"> <a class="btn btn-sm btn-show" :href="getUrl(e)">
{{ {{
trans(SHOW_ENTITY, { $t("show_entity", {
entity: trans(THE_EVALUATION), entity: $t("the_evaluation"),
}) })
}} }}
</a> </a>
<a <a
class="btn btn-sm btn-show" class="btn btn-sm btn-show"
:href="getUrl(e.accompanyingPeriodWork.accompanyingPeriod!)" :href="
v-if=" getUrl(
e.accompanyingPeriodWork && e.accompanyingPeriodWork
e.accompanyingPeriodWork.accompanyingPeriod .accompanyingPeriod,
)
" "
> >
{{ {{
trans(SHOW_ENTITY, { $t("show_entity", {
entity: trans(THE_COURSE), entity: $t("the_course"),
}) })
}} }}
</a> </a>
@@ -88,60 +93,36 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script>
import { computed } from "vue"; import { mapState, mapGetters } from "vuex";
import { useStore } from "vuex"; import TabTable from "./TabTable";
import TabTable from "./TabTable.vue"; import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import { localizeString } from "../../lib/localizationHelper/localizationHelper"; import { localizeString } from "../../lib/localizationHelper/localizationHelper";
const store = useStore(); export default {
name: "MyEvaluations",
import type { ComputedRef } from "vue"; components: {
import { TabTable,
AccompanyingPeriod, OnTheFly,
AccompanyingPeriodWorkEvaluation, },
AccompanyingPeriodWork, computed: {
} from "ChillPersonAssets/types"; ...mapState(["evaluations"]),
import { PaginationResponse } from "ChillMainAssets/lib/api/apiMethods"; ...mapGetters(["isEvaluationsLoaded"]),
import { noResults() {
MY_EVALUATIONS_DESCRIPTION, if (!this.isEvaluationsLoaded) {
MAX_DATE,
EVALUATION,
SHOW_ENTITY,
THE_COURSE,
THE_EVALUATION,
SOCIAL_ACTION,
NO_DATA,
trans,
} from "translator";
const evaluations: ComputedRef<
PaginationResponse<AccompanyingPeriodWorkEvaluation>
> = computed(() => store.state.homepage.evaluations);
const isEvaluationsLoaded = computed(() => store.getters.isEvaluationsLoaded);
const noResults = computed(() => {
if (!isEvaluationsLoaded.value) {
return false; return false;
} else { } else {
return evaluations.value.count === 0; return this.evaluations.count === 0;
} }
}); },
},
function getUrl( methods: {
e: localizeString,
| AccompanyingPeriodWorkEvaluation getUrl(e) {
| AccompanyingPeriod
| AccompanyingPeriodWork,
): string {
if (!e) {
throw "entity is undefined";
}
if ("type" in e && typeof e.type === "string") {
switch (e.type) { switch (e.type) {
case "accompanying_period_work_evaluation": case "accompanying_period_work_evaluation":
return `/fr/person/accompanying-period/work/${e.accompanyingPeriodWork?.id}/edit#evaluations`; let anchor = "#evaluations";
return `/fr/person/accompanying-period/work/${e.accompanyingPeriodWork.id}/edit${anchor}`;
case "accompanying_period_work": case "accompanying_period_work":
return `/fr/person/accompanying-period/work/${e.id}/edit`; return `/fr/person/accompanying-period/work/${e.id}/edit`;
case "accompanying_period": case "accompanying_period":
@@ -149,9 +130,9 @@ function getUrl(
default: default:
throw "entity type unknown"; throw "entity type unknown";
} }
} },
return ""; },
} };
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,26 +1,26 @@
<template> <template>
<div class="alert alert-light"> <div class="alert alert-light">
{{ trans(MY_NOTIFICATIONS_DESCRIPTION) }} {{ $t("my_notifications.description") }}
</div> </div>
<span v-if="noResults" class="chill-no-data-statement"> <span v-if="noResults" class="chill-no-data-statement">{{
{{ trans(NO_DATA) }} $t("no_data")
</span> }}</span>
<tab-table v-else> <tab-table v-else>
<template #thead> <template #thead>
<th scope="col"> <th scope="col">
{{ trans(DATE) }} {{ $t("Date") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(SUBJECT) }} {{ $t("Subject") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(FROM) }} {{ $t("From") }}
</th> </th>
<th scope="col" /> <th scope="col" />
</template> </template>
<template #tbody> <template #tbody>
<tr v-for="(n, i) in notifications.results" :key="`notify-${i}`"> <tr v-for="(n, i) in notifications.results" :key="`notify-${i}`">
<td>{{ $d(new Date(n.date.datetime), "long") }}</td> <td>{{ $d(n.date.datetime, "long") }}</td>
<td> <td>
<span class="unread"> <span class="unread">
<i class="fa fa-envelope-o" /> <i class="fa fa-envelope-o" />
@@ -31,11 +31,11 @@
{{ n.sender.text }} {{ n.sender.text }}
</td> </td>
<td v-else> <td v-else>
{{ trans(AUTOMATIC_NOTIFICATION) }} {{ $t("automatic_notification") }}
</td> </td>
<td> <td>
<a class="btn btn-sm btn-show" :href="getEntityUrl(n)"> <a class="btn btn-sm btn-show" :href="getEntityUrl(n)">
{{ trans(SHOW_ENTITY, { entity: getEntityName(n) }) }} {{ $t("show_entity", { entity: getEntityName(n) }) }}
</a> </a>
</td> </td>
</tr> </tr>
@@ -43,67 +43,48 @@
</tab-table> </tab-table>
</template> </template>
<script lang="ts" setup> <script>
import { computed, ComputedRef } from "vue"; import { mapState, mapGetters } from "vuex";
import { useStore } from "vuex"; import TabTable from "./TabTable";
import TabTable from "./TabTable.vue"; import { appMessages } from "ChillMainAssets/vuejs/HomepageWidget/js/i18n";
import { Notification } from "ChillPersonAssets/types";
import { export default {
MY_NOTIFICATIONS_DESCRIPTION, name: "MyNotifications",
DATE, components: {
FROM, TabTable,
SUBJECT, },
SHOW_ENTITY, computed: {
THE_ACTIVITY, ...mapState(["notifications"]),
THE_COURSE, ...mapGetters(["isNotificationsLoaded"]),
THE_ACTION, noResults() {
THE_EVALUATION_DOCUMENT, if (!this.isNotificationsLoaded) {
THE_WORKFLOW,
NO_DATA,
AUTOMATIC_NOTIFICATION,
trans,
} from "translator";
import { PaginationResponse } from "ChillMainAssets/lib/api/apiMethods";
const store = useStore();
const notifications: ComputedRef<PaginationResponse<Notification>> = computed(
() => store.state.homepage.notifications,
);
const isNotificationsLoaded = computed(
() => store.getters.isNotificationsLoaded,
);
const noResults = computed(() => {
if (!isNotificationsLoaded.value) {
return false; return false;
} else { } else {
return notifications.value.count === 0; return this.notifications.count === 0;
} }
}); },
},
function getNotificationUrl(n: Notification): string { methods: {
getNotificationUrl(n) {
return `/fr/notification/${n.id}/show`; return `/fr/notification/${n.id}/show`;
} },
getEntityName(n) {
function getEntityName(n: Notification): string {
switch (n.relatedEntityClass) { switch (n.relatedEntityClass) {
case "Chill\\ActivityBundle\\Entity\\Activity": case "Chill\\ActivityBundle\\Entity\\Activity":
return trans(THE_ACTIVITY); return appMessages.fr.the_activity;
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod": case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod":
return trans(THE_COURSE); return appMessages.fr.the_course;
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork": case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork":
return trans(THE_ACTION); return appMessages.fr.the_action;
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument": case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument":
return trans(THE_EVALUATION_DOCUMENT); return appMessages.fr.the_evaluation_document;
case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow": case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow":
return trans(THE_WORKFLOW); return appMessages.fr.the_workflow;
default: default:
throw "notification type unknown"; throw "notification type unknown";
} }
} },
getEntityUrl(n) {
function getEntityUrl(n: Notification): string {
switch (n.relatedEntityClass) { switch (n.relatedEntityClass) {
case "Chill\\ActivityBundle\\Entity\\Activity": case "Chill\\ActivityBundle\\Entity\\Activity":
return `/fr/activity/${n.relatedEntityId}/show`; return `/fr/activity/${n.relatedEntityId}/show`;
@@ -118,7 +99,9 @@ function getEntityUrl(n: Notification): string {
default: default:
throw "notification type unknown"; throw "notification type unknown";
} }
} },
},
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,38 +1,38 @@
<template> <template>
<div class="alert alert-light"> <div class="alert alert-light">
{{ trans(MY_TASKS_DESCRIPTION_WARNING) }} {{ $t("my_tasks.description_warning") }}
</div> </div>
<span v-if="noResultsAlert" class="chill-no-data-statement"> <span v-if="noResultsAlert" class="chill-no-data-statement">{{
{{ trans(NO_DATA) }} $t("no_data")
</span> }}</span>
<tab-table v-else> <tab-table v-else>
<template #thead> <template #thead>
<th scope="col"> <th scope="col">
{{ trans(WARNING_DATE) }} {{ $t("warning_date") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(MAX_DATE) }} {{ $t("max_date") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(TASK) }} {{ $t("task") }}
</th> </th>
<th scope="col" /> <th scope="col" />
</template> </template>
<template #tbody> <template #tbody>
<tr v-for="(t, i) in tasks.alert.results" :key="`task-alert-${i}`"> <tr v-for="(t, i) in tasks.alert.results" :key="`task-alert-${i}`">
<td v-if="t.warningDate !== null"> <td v-if="null !== t.warningDate">
{{ $d(new Date(t.warningDate.datetime), "short") }} {{ $d(t.warningDate.datetime, "short") }}
</td> </td>
<td v-else /> <td v-else />
<td> <td>
<span class="outdated">{{ <span class="outdated">{{
$d(new Date(t.endDate.datetime), "short") $d(t.endDate.datetime, "short")
}}</span> }}</span>
</td> </td>
<td>{{ t.title }}</td> <td>{{ t.title }}</td>
<td> <td>
<a class="btn btn-sm btn-show" :href="getUrl(t)"> <a class="btn btn-sm btn-show" :href="getUrl(t)">
{{ trans(SHOW_ENTITY, { entity: trans(THE_TASK) }) }} {{ $t("show_entity", { entity: $t("the_task") }) }}
</a> </a>
</td> </td>
</tr> </tr>
@@ -40,36 +40,39 @@
</tab-table> </tab-table>
<div class="alert alert-light"> <div class="alert alert-light">
{{ trans(MY_TASKS_DESCRIPTION_ALERT) }} {{ $t("my_tasks.description_alert") }}
</div> </div>
<span v-if="noResultsWarning" class="chill-no-data-statement"> <span v-if="noResultsWarning" class="chill-no-data-statement">{{
{{ trans(NO_DATA) }} $t("no_data")
</span> }}</span>
<tab-table v-else> <tab-table v-else>
<template #thead> <template #thead>
<th scope="col"> <th scope="col">
{{ trans(WARNING_DATE) }} {{ $t("warning_date") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(MAX_DATE) }} {{ $t("max_date") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(TASK) }} {{ $t("task") }}
</th> </th>
<th scope="col" /> <th scope="col" />
</template> </template>
<template #tbody> <template #tbody>
<tr v-for="(t, i) in tasks.warning.results" :key="`task-warning-${i}`"> <tr
v-for="(t, i) in tasks.warning.results"
:key="`task-warning-${i}`"
>
<td> <td>
<span class="outdated">{{ <span class="outdated">{{
$d(new Date(t.warningDate.datetime), "short") $d(t.warningDate.datetime, "short")
}}</span> }}</span>
</td> </td>
<td>{{ $d(new Date(t.endDate.datetime), "short") }}</td> <td>{{ $d(t.endDate.datetime, "short") }}</td>
<td>{{ t.title }}</td> <td>{{ t.title }}</td>
<td> <td>
<a class="btn btn-sm btn-show" :href="getUrl(t)"> <a class="btn btn-sm btn-show" :href="getUrl(t)">
{{ trans(SHOW_ENTITY, { entity: trans(THE_TASK) }) }} {{ $t("show_entity", { entity: $t("the_task") }) }}
</a> </a>
</td> </td>
</tr> </tr>
@@ -77,51 +80,39 @@
</tab-table> </tab-table>
</template> </template>
<script lang="ts" setup> <script>
import { computed, ComputedRef } from "vue"; import { mapState, mapGetters } from "vuex";
import { useStore } from "vuex"; import TabTable from "./TabTable";
import TabTable from "./TabTable.vue";
import {
MY_TASKS_DESCRIPTION_ALERT,
MY_TASKS_DESCRIPTION_WARNING,
MAX_DATE,
WARNING_DATE,
TASK,
SHOW_ENTITY,
THE_TASK,
NO_DATA,
trans,
} from "translator";
import { TasksState } from "./store/modules/homepage";
import { Alert, Warning } from "ChillPersonAssets/types";
const store = useStore(); export default {
name: "MyTasks",
const tasks: ComputedRef<TasksState> = computed( components: {
() => store.state.homepage.tasks, TabTable,
); },
const isTasksWarningLoaded = computed(() => store.getters.isTasksWarningLoaded); computed: {
const isTasksAlertLoaded = computed(() => store.getters.isTasksAlertLoaded); ...mapState(["tasks"]),
...mapGetters(["isTasksWarningLoaded", "isTasksAlertLoaded"]),
const noResultsAlert = computed(() => { noResultsAlert() {
if (!isTasksAlertLoaded.value) { if (!this.isTasksAlertLoaded) {
return false; return false;
} else { } else {
return tasks.value.alert.count === 0; return this.tasks.alert.count === 0;
} }
}); },
noResultsWarning() {
const noResultsWarning = computed(() => { if (!this.isTasksWarningLoaded) {
if (!isTasksWarningLoaded.value) {
return false; return false;
} else { } else {
return tasks.value.warning.count === 0; return this.tasks.warning.count === 0;
} }
}); },
},
function getUrl(t: Warning | Alert): string { methods: {
getUrl(t) {
return `/fr/task/single-task/${t.id}/show`; return `/fr/task/single-task/${t.id}/show`;
} },
},
};
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,58 +0,0 @@
<template>
<div class="col-12">
<!-- Loading state -->
<div
v-if="isTicketLoading"
class="d-flex justify-content-center align-items-center"
style="height: 200px"
>
<div class="text-center">
<div class="spinner-border mb-3" role="status">
<span class="visually-hidden">{{
trans(CHILL_TICKET_LIST_LOADING_TICKET)
}}</span>
</div>
<div class="text-muted">
{{ trans(CHILL_TICKET_LIST_LOADING_TICKET) }}
</div>
</div>
</div>
<!-- Ticket list -->
<ticket-list-component
v-else
:tickets="ticketList"
title=""
:hasMoreTickets="pagination.next !== null"
@fetchNextPage="fetchNextPage"
/>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useStore } from "vuex";
// Components
import TicketListComponent from "../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketList/components/TicketListComponent.vue";
// Types
import { Pagination } from "ChillMainAssets/lib/api/apiMethods";
import { TicketSimple } from "src/Bundle/ChillTicketBundle/src/Resources/public/types";
// Translation
import { CHILL_TICKET_LIST_LOADING_TICKET, trans } from "translator";
const store = useStore();
const pagination = computed(() => store.getters.getPagination as Pagination);
const ticketList = computed(
() => store.getters.getTicketList as TicketSimple[],
);
const isTicketLoading = computed(() => store.getters.isTicketLoading);
const fetchNextPage = async () => {
if (pagination.value.next) {
await store.dispatch("fetchTicketListByUrl", pagination.value.next);
}
};
</script>

View File

@@ -1,27 +1,26 @@
<template> <template>
<div class="alert alert-light"> <div class="alert alert-light">
{{ trans(MY_WORKFLOWS_DESCRIPTION) }} {{ $t("my_workflows.description") }}
</div> </div>
<my-workflows-table :workflows="workflows" /> <my-workflows-table :workflows="workflows" />
<div class="alert alert-light"> <div class="alert alert-light">
{{ trans(MY_WORKFLOWS_DESCRIPTION_CC) }} {{ $t("my_workflows.description_cc") }}
</div> </div>
<my-workflows-table :workflows="workflowsCc" /> <my-workflows-table :workflows="workflowsCc" />
</template> </template>
<script lang="ts" setup> <script>
import { computed } from "vue"; import { mapState } from "vuex";
import { useStore } from "vuex";
import MyWorkflowsTable from "./MyWorkflowsTable.vue"; import MyWorkflowsTable from "./MyWorkflowsTable.vue";
import {
MY_WORKFLOWS_DESCRIPTION,
MY_WORKFLOWS_DESCRIPTION_CC,
trans,
} from "translator";
const store = useStore(); export default {
name: "MyWorkflows",
const workflows = computed(() => store.state.homepage.workflows); components: {
const workflowsCc = computed(() => store.state.homepage.workflowsCc); MyWorkflowsTable,
},
computed: {
...mapState(["workflows", "workflowsCc"]),
},
};
</script> </script>

View File

@@ -1,17 +1,17 @@
<template> <template>
<span v-if="hasNoResults" class="chill-no-data-statement"> <span v-if="hasNoResults(workflows)" class="chill-no-data-statement">{{
{{ trans(NO_DATA) }} $t("no_data")
</span> }}</span>
<tab-table v-else> <tab-table v-else>
<template #thead> <template #thead>
<th scope="col"> <th scope="col">
{{ trans(OBJECT_WORKFLOW) }} {{ $t("Object_workflow") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(STEP) }} {{ $t("Step") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(CONCERNED_USERS) }} {{ $t("concerned_users") }}
</th> </th>
<th scope="col" /> <th scope="col" />
</template> </template>
@@ -23,13 +23,15 @@
<td> <td>
<div class="workflow"> <div class="workflow">
<div class="breadcrumb"> <div class="breadcrumb">
<i class="fa fa-circle me-1 text-chill-yellow mx-2" /> <i
class="fa fa-circle me-1 text-chill-yellow mx-2"
/>
<span class="mx-2">{{ getStep(w) }}</span> <span class="mx-2">{{ getStep(w) }}</span>
</div> </div>
<span <span
v-if="w.isOnHoldAtCurrentStep" v-if="w.isOnHoldAtCurrentStep"
class="badge bg-success rounded-pill" class="badge bg-success rounded-pill"
>{{ trans(ON_HOLD) }}</span >{{ $t("on_hold") }}</span
> >
</div> </div>
</td> </td>
@@ -46,7 +48,7 @@
</td> </td>
<td> <td>
<a class="btn btn-sm btn-show" :href="getUrl(w)"> <a class="btn btn-sm btn-show" :href="getUrl(w)">
{{ trans(SHOW_ENTITY, { entity: trans(THE_WORKFLOW) }) }} {{ $t("show_entity", { entity: $t("the_workflow") }) }}
</a> </a>
</td> </td>
</tr> </tr>
@@ -54,48 +56,38 @@
</tab-table> </tab-table>
</template> </template>
<script lang="ts" setup> <script>
import { computed } from "vue"; import { mapGetters } from "vuex";
import { useStore } from "vuex"; import TabTable from "./TabTable";
import TabTable from "./TabTable.vue"; import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import { Workflow } from "ChillPersonAssets/types";
import {
CONCERNED_USERS,
OBJECT_WORKFLOW,
ON_HOLD,
SHOW_ENTITY,
THE_WORKFLOW,
NO_DATA,
STEP,
trans,
} from "translator";
import { PaginationResponse } from "ChillMainAssets/lib/api/apiMethods";
const props = defineProps<{ export default {
workflows: PaginationResponse<Workflow>; name: "MyWorkflows",
}>(); components: {
TabTable,
const store = useStore(); OnTheFly,
},
const isWorkflowsLoaded = computed(() => store.getters.isWorkflowsLoaded); props: ["workflows"],
computed: {
const hasNoResults = computed(() => { ...mapGetters(["isWorkflowsLoaded"]),
if (!isWorkflowsLoaded.value) { },
methods: {
hasNoResults(workflows) {
if (!this.isWorkflowsLoaded) {
return false; return false;
} else { } else {
return props.workflows.count === 0; return workflows.count === 0;
} }
}); },
getUrl(w) {
function getUrl(w: Workflow): string {
return `/fr/main/workflow/${w.id}/show`; return `/fr/main/workflow/${w.id}/show`;
} },
getStep(w) {
function getStep(w: Workflow): string {
const lastStep = w.steps.length - 1; const lastStep = w.steps.length - 1;
return w.steps[lastStep].currentStep.text; return w.steps[lastStep].currentStep.text;
} },
},
};
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,26 +1,27 @@
// CURRENTLY NOT IN USE
<template> <template>
<div class="accompanying-course-work"> <div class="accompanying-course-work">
<div class="alert alert-light"> <div class="alert alert-light">
{{ trans(MY_WORKS_DESCRIPTION) }} {{ $t("my_works.description") }}
</div> </div>
<span v-if="noResults" class="chill-no-data-statement"> <span v-if="noResults" class="chill-no-data-statement">{{
{{ trans(NO_DATA) }} $t("no_data")
</span> }}</span>
<tab-table v-else> <tab-table v-else>
<template #thead> <template #thead>
<th scope="col"> <th scope="col">
{{ trans(START_DATE) }} {{ $t("StartDate") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(SOCIAL_ACTION) }} {{ $t("SocialAction") }}
</th> </th>
<th scope="col"> <th scope="col">
{{ trans(CONCERNED_PERSONS) }} {{ $t("concerned_persons") }}
</th> </th>
<th scope="col" /> <th scope="col" />
</template> </template>
<template #tbody> <template #tbody>
<tr v-for="(w, i) in works.value.results" :key="`works-${i}`"> <tr v-for="(w, i) in works.results" :key="`works-${i}`">
<td>{{ $d(w.startDate.datetime, "short") }}</td> <td>{{ $d(w.startDate.datetime, "short") }}</td>
<td> <td>
<span class="chill-entity entity-social-issue"> <span class="chill-entity entity-social-issue">
@@ -36,7 +37,11 @@
</h4> </h4>
</td> </td>
<td> <td>
<span v-for="person in w.persons" class="me-1" :key="person.id"> <span
v-for="person in w.persons"
class="me-1"
:key="person.id"
>
<on-the-fly <on-the-fly
:type="person.type" :type="person.type"
:id="person.id" :id="person.id"
@@ -47,11 +52,15 @@
</span> </span>
</td> </td>
<td> <td>
<div class="btn-group-vertical" role="group" aria-label="Actions"> <div
class="btn-group-vertical"
role="group"
aria-label="Actions"
>
<a class="btn btn-sm btn-update" :href="getUrl(w)"> <a class="btn btn-sm btn-update" :href="getUrl(w)">
{{ {{
trans(SHOW_ENTITY, { $t("show_entity", {
entity: trans(THE_ACTION), entity: $t("the_action"),
}) })
}} }}
</a> </a>
@@ -60,8 +69,8 @@
:href="getUrl(w.accompanyingPeriod)" :href="getUrl(w.accompanyingPeriod)"
> >
{{ {{
trans(SHOW_ENTITY, { $t("show_entity", {
entity: trans(THE_COURSE), entity: $t("the_course"),
}) })
}} }}
</a> </a>
@@ -73,38 +82,30 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script>
import { computed } from "vue"; import { mapState, mapGetters } from "vuex";
import { useStore } from "vuex"; import TabTable from "./TabTable";
import TabTable from "./TabTable.vue"; import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import {
MY_WORKS_DESCRIPTION,
CONCERNED_PERSONS,
SHOW_ENTITY,
THE_COURSE,
THE_ACTION,
SOCIAL_ACTION,
START_DATE,
NO_DATA,
trans,
} from "translator";
import { Workflow } from "ChillPersonAssets/types";
const store = useStore(); export default {
name: "MyWorks",
const works = computed(() => store.state.homepage.works); components: {
const isWorksLoaded = computed(() => store.getters.isWorksLoaded); TabTable,
OnTheFly,
const noResults = computed(() => { },
if (!isWorksLoaded.value) { computed: {
...mapState(["works"]),
...mapGetters(["isWorksLoaded"]),
noResults() {
if (!this.isWorksLoaded) {
return false; return false;
} else { } else {
return works.value.count === 0; return this.works.count === 0;
} }
}); },
},
function getUrl(e: Workflow): string { methods: {
getUrl(e) {
switch (e.type) { switch (e.type) {
case "accompanying_period_work": case "accompanying_period_work":
return `/fr/person/accompanying-period/work/${e.id}/edit`; return `/fr/person/accompanying-period/work/${e.id}/edit`;
@@ -113,7 +114,9 @@ function getUrl(e: Workflow): string {
default: default:
throw "entity type unknown"; throw "entity type unknown";
} }
} },
},
};
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,17 +1,17 @@
<template> <template>
<span v-if="isCounterAvailable" class="badge rounded-pill bg-danger mx-2"> <span v-if="isCounterAvailable" class="badge rounded-pill bg-danger">
{{ count }} {{ count }}
</span> </span>
</template> </template>
<script lang="ts" setup> <script>
import { computed } from "vue"; export default {
name: "TabCounter",
const props = defineProps<{ props: ["count"],
count: number; computed: {
}>(); isCounterAvailable() {
return typeof this.count !== "undefined" && this.count > 0;
const isCounterAvailable = computed( },
() => typeof props.count !== "undefined" && props.count > 0, },
); };
</script> </script>

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