Compare commits

..

5 Commits

Author SHA1 Message Date
d80661e479 Release v4.10.0 2025-12-09 15:31:57 +01:00
473c5a9fa3 Remove dependency on @symfony/ux-translator from package.json
- Deleted `@symfony/ux-translator` as it is no longer needed.
- Documented the change in `.changes/unreleased` with no schema modifications.
2025-12-09 15:27:39 +01:00
acceeeaa2a Merge branch '462-display-calendar-items-for-person' into 'master'
Improve the display of upcoming calendar items within the person render box

Closes #462

See merge request Chill-Projet/chill-bundles!928
2025-12-08 12:45:01 +00:00
53b02a0ced Improve the display of upcoming calendar items within the person render box 2025-12-08 12:45:01 +00:00
387bf55b11 fix version constraint in chill-zimbra-bundle 2025-12-05 16:32:30 +00:00
560 changed files with 23128 additions and 47625 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: Create invitation list in user menu
time: 2025-08-08T12:08:02.446361367+02:00
custom:
Issue: "385"
SchemaChange: No schema change

View File

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

View File

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

View File

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

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

@@ -0,0 +1,6 @@
## v4.10.0 - 2025-12-09
### Feature
* [MR 928](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/928) [#462](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/462) Add the future appointments for the person in search results
### Fixed
* Remove dependency to package @symfony/ux-translator

View File

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

View File

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

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,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.10.0 - 2025-12-09
### Feature
* [MR 928](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/928) [#462](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/462) Add the future appointments for the person in search results
### Fixed
* Remove dependency to package @symfony/ux-translator
## v4.9.0 - 2025-12-05
### Feature
* ([#459](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/459)) Add a counter for invitations awaiting reply

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,90 +1,23 @@
# Create a new bundle {#create-new-bundle}
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".
:::: warning
::: title
Warning
:::
###### Create a new bundle
This part of the doc is not yet tested
::::
Create your own bundle is not a trivial task.
## Create a new directory with Bundle class
The easiest way to achieve this is seems to be :
``` bash
mkdir -p src/Bundle/ChillSomeBundle/src/config
mkdir -p src/Bundle/ChillSomeBundle/src/Controller
```
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 [installation-for-development` section, and add your new repository to the composer.json file
6. Work as :ref:`usual ](editing-code-and-commiting.md)
Add a bundle file
This part of the doc is not yet tested
``` 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:
``` yaml
chill_ticket_controller:
resource: '@ChillTicketBundle/Controller/'
type: annotation
```
## Register the new psr-4 namespace
In composer.json, add the new psr4 namespace
``` diff
{
"autoload": {
"psr-4": {
+ "Chill\\SomeBundle\\": "src/Bundle/ChillSomeBundle/src",
}
}
}
```
## Register the bundle
Register in the file `config/bundles.php`:
``` php
Vendor\Bundle\YourBundle\YourBundle::class => ['all' => true],
```
And import routes in `config/routes/chill_some_bundle.yaml`:
``` yaml
chill_ticket_bundle:
resource: '@ChillSomeBundle/config/routes.yaml'
```
## Add the doctrine_migration namespace
Add the namespace to `config/packages/doctrine_migrations_chill.yaml`
``` diff
doctrine_migrations:
migrations_paths:
+ 'Chill\Some\Ticket': '@ChillSomeBundle/migrations'
```
## Dump autoloading
``` bash
symfony composer dump-autoload
```
TODO

View File

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

View File

@@ -9,7 +9,7 @@
"social worker"
],
"require": {
"chill-project/chill-bundles": "dev-master as v4.6.1",
"chill-project/chill-bundles": "^4.9.0",
"zimbra-api/soap-api": "^3.2.2",
"psr/http-client": "^1.0",
"nyholm/psr7": "^1.0"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,10 +47,12 @@ span.badge {
word-wrap: break-word;
word-break: break-word;
display: inline-block;
max-width: 100%;margin-bottom: 5px;
max-width: 100%;
margin-bottom: 5px;
margin-right: 1em;
text-align: left;
line-height: 1.2em;
&::before {
position: absolute;
left: 11px;

View File

@@ -10,7 +10,9 @@
:value="issue"
/>
<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>
</div>
</span>
@@ -45,8 +47,10 @@ span.badge {
word-wrap: break-word;
word-break: break-word;
display: inline-block;
max-width: 100%;margin-bottom: 5px;
margin-right: 1em;text-align: left;
max-width: 100%;
margin-bottom: 5px;
margin-right: 1em;
text-align: left;
&::before {
position: absolute;

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,9 @@ const futureStore = function (): Promise<Store<State>> {
});
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, {
root: true,
});

View File

@@ -29,7 +29,10 @@ export default {
(state: CalendarLocalsState) =>
({ start, end }: { start: Date; end: Date }): boolean => {
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;
}
}
@@ -51,7 +54,10 @@ export default {
});
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({
start: payload.start.getTime(),
end: payload.end.getTime(),
@@ -79,7 +85,11 @@ export default {
end: end,
});
return fetchCalendarLocalForUser(ctx.rootGetters["me/getMe"], start, end)
return fetchCalendarLocalForUser(
ctx.rootGetters["me/getMe"],
start,
end,
)
.then((remotes: CalendarLight[]) => {
// to be add when reactivity problem will be solve ?
//ctx.commit('addRemotes', remotes);

View File

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

View File

@@ -29,7 +29,10 @@ export default {
(state: CalendarRemotesState) =>
({ start, end }: { start: Date; end: Date }): boolean => {
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;
}
}
@@ -82,7 +85,11 @@ export default {
end: end,
});
return fetchCalendarRemoteForUser(ctx.rootGetters["me/getMe"], start, end)
return fetchCalendarRemoteForUser(
ctx.rootGetters["me/getMe"],
start,
end,
)
.then((remotes: CalendarRemote[]) => {
// to be add when reactivity problem will be solve ?
//ctx.commit('addRemotes', remotes);

View File

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

View File

@@ -28,9 +28,6 @@
{% if calendar.status == 'canceled' %}
<del>
{% endif %}
{% if calendar.status == 'canceled' %}
<del>
{% endif %}
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
{{ calendar.startDate|format_datetime('short', 'short') }}
- {{ calendar.endDate|format_datetime('short', 'short') }}
@@ -240,10 +237,6 @@
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_cancel', { 'id': calendar.id } ) }}"
class="btn btn-action"><i class="bi bi-x-circle"></i> {{ 'Cancel'|trans }}</a>
</li>
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_cancel', { 'id': calendar.id } ) }}"
class="btn btn-action"><i class="bi bi-x-circle"></i> {{ 'Cancel'|trans }}</a>
</li>
{% endif %}
{% if is_granted('CHILL_CALENDAR_CALENDAR_DELETE', calendar) %}

View File

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

View File

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

View File

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

View File

@@ -46,8 +46,7 @@ export interface StoredObjectVersionCreated extends StoredObjectVersion {
persisted: false;
}
export interface StoredObjectVersionPersisted
extends StoredObjectVersionCreated {
export interface StoredObjectVersionPersisted extends StoredObjectVersionCreated {
version: number;
id: number;
createdAt: DateTime | null;
@@ -61,8 +60,7 @@ export interface StoredObjectStatusChange {
type: string;
}
export interface StoredObjectVersionWithPointInTime
extends StoredObjectVersionPersisted {
export interface StoredObjectVersionWithPointInTime extends StoredObjectVersionPersisted {
"point-in-times": StoredObjectPointInTime[];
"from-restored": StoredObjectVersionPersisted | null;
}

View File

@@ -49,7 +49,9 @@
<li v-if="isHistoryViewable">
<history-button
:stored-object="props.storedObject"
:can-edit="canEdit && props.storedObject._permissions.canEdit"
:can-edit="
canEdit && props.storedObject._permissions.canEdit
"
></history-button>
</li>
</ul>
@@ -127,7 +129,9 @@ const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
canDownload: true,
canConvertPdf: true,
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>
</teleport>
<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">
<select
class="form-select form-select-sm"
@@ -90,7 +92,10 @@
v-if="signature.zones.length === 1 && signedState !== 'signed'"
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) }}
</button>
</div>
@@ -145,7 +150,10 @@
:title="trans(SIGNATURES_ADD_SIGN_ZONE)"
>
<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>
</div>
</template>
@@ -199,7 +207,10 @@
v-if="signature.zones.length === 1 && signedState !== 'signed'"
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) }}
</button>
</div>
@@ -227,7 +238,10 @@
v-if="signature.zones.length === 1 && signedState !== 'signed'"
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) }}
</button>
</div>
@@ -285,7 +299,10 @@
</template>
<template v-else>
{{ 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>
</div>
</template>
@@ -545,8 +562,14 @@ const addCanvasEvents = () => {
);
});
} else {
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
canvas.addEventListener("pointerup", (e) => canvasClick(e, canvas), false);
const canvas = document.querySelectorAll(
"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) <
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;
const selectZone = async (z: SignatureZone, canvas: HTMLCanvasElement) => {
@@ -598,7 +625,8 @@ const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
signature.zones
.filter(
(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),
)
.map((z) => {

View File

@@ -153,10 +153,12 @@ const handleFile = async (file: File): Promise<void> => {
</p>
<!-- todo i18n -->
<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 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>
</div>
<div v-else class="waiting">

View File

@@ -35,7 +35,9 @@ async function download_and_open(event: Event): Promise<void> {
if (null === state.content) {
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);
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">
<template v-slot:body>
<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>
<strong>{{ editionUntilFormatted }}</strong>
</p>
@@ -55,21 +57,23 @@ const editionUntilFormatted = computed<string>(() => {
<p>
<small
>Le document peut être édité uniquement en utilisant Libre
Office.</small
>Le document peut être édité uniquement en utilisant
Libre Office.</small
>
</p>
<p>
<small
>En cas d'échec lors de l'enregistrement, sauver le document sur
le poste de travail avant de le déposer à nouveau ici.</small
>En cas d'échec lors de l'enregistrement, sauver le
document sur le poste de travail avant de le déposer
à nouveau ici.</small
>
</p>
<p>
<small
>Vous pouvez naviguez sur d'autres pages pendant l'édition.</small
>Vous pouvez naviguez sur d'autres pages pendant
l'édition.</small
>
</p>
</div>

View File

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

View File

@@ -2,7 +2,9 @@
<a
:class="Object.assign(props.classes, { btn: true })"
@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>
Editer en ligne

View File

@@ -145,7 +145,9 @@ async function download_info_link(
function build_wopi_editor_link(uuid: string, returnPath?: string) {
if (returnPath === undefined) {
returnPath =
window.location.pathname + window.location.search + window.location.hash;
window.location.pathname +
window.location.search +
window.location.hash;
}
return (
@@ -175,6 +177,8 @@ async function download_and_decrypt_doc(
throw new Error("no version associated to stored object");
}
// sometimes, the downloadInfo may be embedded into the storedObject
console.log("storedObject", storedObject);
let downloadInfo;
if (
typeof storedObject._links !== "undefined" &&
@@ -182,7 +186,10 @@ async function download_and_decrypt_doc(
) {
downloadInfo = storedObject._links.downloadLink;
} else {
downloadInfo = await download_info_link(storedObject, atVersionToDownload);
downloadInfo = await download_info_link(
storedObject,
atVersionToDownload,
);
}
const rawResponse = await window.fetch(downloadInfo.url);
@@ -237,7 +244,10 @@ async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob> {
}
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);
@@ -267,7 +277,6 @@ async function is_object_ready(
export {
build_convert_link,
build_wopi_editor_link,
download_info_link,
download_and_decrypt_doc,
download_doc,
download_doc_as_pdf,

View File

@@ -43,17 +43,11 @@ class StoredObjectVersionNormalizer implements NormalizerInterface, NormalizerAw
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, [...$context, UserNormalizer::AT_DATE => $object->getCreatedAt()]),
];
$normalizationGroups = $context[AbstractNormalizer::GROUPS] ?? [];
if (is_string($normalizationGroups)) {
$normalizationGroups = [$normalizationGroups];
}
if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $normalizationGroups, true)) {
if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
$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']]);
}

View File

@@ -19,6 +19,7 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Address;
use libphonenumber\PhoneNumber;
use Symfony\Component\Validator\Constraints as Assert;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
/**
* Immersion.
@@ -85,14 +86,14 @@ class Immersion implements \Stringable
* @Assert\NotBlank()
*/
#[ORM\Column(name: 'tuteurPhoneNumber', type: 'phone_number', nullable: true)]
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
#[PhonenumberConstraint(type: 'any')]
private ?PhoneNumber $tuteurPhoneNumber = null;
#[ORM\Column(name: 'structureAccName', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
private ?string $structureAccName = null;
#[ORM\Column(name: 'structureAccPhonenumber', type: 'phone_number', nullable: true)]
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
#[PhonenumberConstraint(type: 'any')]
private ?PhoneNumber $structureAccPhonenumber = null;
#[ORM\Column(name: 'structureAccEmail', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]

View File

@@ -14,6 +14,7 @@ namespace Chill\MainBundle;
use Chill\MainBundle\Cron\CronJobInterface;
use Chill\MainBundle\CRUD\CompilerPass\CRUDControllerCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
@@ -69,6 +70,7 @@ class ChillMainBundle extends Bundle
$container->addCompilerPass(new TimelineCompilerClass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new WidgetsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new NotificationCounterCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new MenuCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new ACLFlagsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new CRUDControllerCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
}

View File

@@ -1,115 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
use Symfony\Component\Yaml\Yaml;
class OverrideTranslationCommand extends Command
{
public function __construct(
private readonly TranslationReaderInterface $reader,
private readonly TranslationWriterInterface $writer,
) {
$this->setName('chill:main:override_translation');
parent::__construct();
}
protected function configure(): void
{
$this
->setDescription('Generate a translation catalogue with translation remplacements based on replacements provided in a YAML file.')
->addArgument('locale', InputArgument::REQUIRED, 'The locale to process (e.g. fr, en).')
->addArgument('overrides', InputArgument::REQUIRED, 'Path to the overrides YAML file (list of {from, to}).');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$locale = (string) $input->getArgument('locale');
$overridesPath = (string) $input->getArgument('overrides');
$catalogue = $this->loadCatalogue($locale);
$overrides = $this->loadOverrides($overridesPath);
$toOverrideCatalogue = new MessageCatalogue($locale);
foreach ($catalogue->getDomains() as $domain) {
// hack: we have to replace the suffix ".intl-icu" by "+intl-ic"
$domain = str_replace('.intl-icu', '+intl-icu', $domain);
foreach ($catalogue->all($domain) as $key => $translation) {
foreach ($overrides as $changes) {
$from = $changes['from'];
$to = $changes['to'];
if (is_string($translation) && str_contains($translation, $from)) {
$newTranslation = strtr($translation, [$from => $to]);
$toOverrideCatalogue->set($key, $newTranslation, $domain);
$translation = $newTranslation;
}
}
}
}
/** @var KernelInterface $kernel */
/* @phpstan-ignore-next-line */
$kernel = $this->getApplication()->getKernel();
$outputDir = rtrim($kernel->getProjectDir(), '/').'/translations';
if (!is_dir($outputDir)) {
@mkdir($outputDir, 0775, true);
}
// Writer expects the 'path' option to be a directory; it will create the proper file name
$this->writer->write($toOverrideCatalogue, 'yaml', ['path' => $outputDir]);
$output->writeln(sprintf('Override catalogue written to %s (domain: messages, locale: %s).', $outputDir, $locale));
return Command::SUCCESS;
}
/**
* @return list<array{from: string, to: string}>
*/
private function loadOverrides(string $path): array
{
return Yaml::parseFile($path);
}
private function loadCatalogue(string $locale): MessageCatalogue
{
/** @var KernelInterface $kernel */
/* @phpstan-ignore-next-line */
$kernel = $this->getApplication()->getKernel();
// collect path for translations
$transPaths = [];
foreach ($kernel->getBundles() as $bundle) {
$bundleDir = $bundle->getPath();
$transPaths[] = is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundle->getPath().'/translations';
}
$currentCatalogue = new MessageCatalogue($locale);
foreach ($transPaths as $path) {
if (is_dir($path)) {
$this->reader->read($path, $currentCatalogue);
}
}
return $currentCatalogue;
}
}

View File

@@ -14,9 +14,9 @@ namespace Chill\MainBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Repository\LocationRepository;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use Doctrine\ORM\Mapping as ORM;
use libphonenumber\PhoneNumber;
use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber as MisdPhoneNumberConstraint;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
@@ -67,12 +67,12 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
#[ORM\Column(type: 'phone_number', nullable: true)]
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::ANY])]
#[PhonenumberConstraint(type: 'any')]
private ?PhoneNumber $phonenumber1 = null;
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
#[ORM\Column(type: 'phone_number', nullable: true)]
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::ANY])]
#[PhonenumberConstraint(type: 'any')]
private ?PhoneNumber $phonenumber2 = null;
#[Serializer\Groups(['read'])]

View File

@@ -119,7 +119,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
* The user's mobile phone number.
*/
#[ORM\Column(type: 'phone_number', nullable: true)]
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
#[PhonenumberConstraint]
private ?PhoneNumber $phonenumber = null;
/**

View File

@@ -27,8 +27,6 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatableInterface;
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.
*/

View File

@@ -31,8 +31,6 @@ interface PhoneNumberHelperInterface
/**
* Return true if the validation is configured and available.
*
* @deprecated this is an internal behaviour of the helper and should not be taken into account outside of the implementation
*/
public function isPhonenumberValidationConfigured(): bool;

View File

@@ -76,24 +76,6 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
->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.
*/
@@ -122,7 +104,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
*/
public function isValidPhonenumberAny($phonenumber): bool
{
if (false === $this->isConfigured) {
if (false === $this->isPhonenumberValidationConfigured()) {
return true;
}
$validation = $this->performTwilioLookup($phonenumber);
@@ -142,7 +124,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
*/
public function isValidPhonenumberLandOrVoip($phonenumber): bool
{
if (false === $this->isConfigured) {
if (false === $this->isPhonenumberValidationConfigured()) {
return true;
}
@@ -163,7 +145,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
*/
public function isValidPhonenumberMobile($phonenumber): bool
{
if (false === $this->isConfigured) {
if (false === $this->isPhonenumberValidationConfigured()) {
return true;
}
@@ -178,7 +160,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
private function performTwilioLookup($phonenumber)
{
if (false === $this->isConfigured) {
if (false === $this->isPhonenumberValidationConfigured()) {
return null;
}

View File

@@ -166,18 +166,3 @@ export const intervalISOToDays = (str: string | null): number | null => {
return days;
};
export function getTimezoneOffsetString(date: Date, timeZone: string): string {
const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
const offsetMinutes = (utcDate.getTime() - tzDate.getTime()) / (60 * 1000);
// Inverser le signe pour avoir la convention ±HH:MM
const sign = offsetMinutes <= 0 ? "+" : "-";
const absMinutes = Math.abs(offsetMinutes);
const hours = String(Math.floor(absMinutes / 60)).padStart(2, "0");
const minutes = String(absMinutes % 60).padStart(2, "0");
return `${sign}${hours}:${minutes}`;
}

View File

@@ -54,6 +54,7 @@ $chill-theme-buttons: (
&.btn-unlink,
&.btn-action,
&.btn-edit,
&.btn-tpchild,
&.btn-wopilink,
&.btn-update {
&, &:hover {
@@ -81,6 +82,7 @@ $chill-theme-buttons: (
&.btn-remove::before,
&.btn-choose::before,
&.btn-notify::before,
&.btn-tpchild::before,
&.btn-download::before,
&.btn-search::before,
&.btn-cancel::before {
@@ -110,6 +112,7 @@ $chill-theme-buttons: (
&.btn-choose::before { content: "\f00c"; } // fa-check // f046 fa-check-square-o
&.btn-unlink::before { content: "\f127"; } // fa-chain-broken
&.btn-notify::before { content: "\f1d8"; } // fa-paper-plane
&.btn-tpchild::before { content: "\f007"; } // fa-user
&.btn-download::before { content: "\f019"; } // fa-download
&.btn-search::before { content: "\f002"; } // fa-search
}

View File

@@ -29,15 +29,11 @@ form {
label {
display: inline;
}
}
label {
display: inline;
&.required:after {
content: " *";
color: $red;
}
}
}
.col-form-label {

View File

@@ -1,26 +1,15 @@
import {
DynamicKeys,
Scope,
ValidationExceptionInterface,
ValidationProblemFromMap,
ViolationFromMap
} from "../../types";
import { Scope } from "../../types";
export type body = Record<string, boolean | string | number | null>;
export type fetchOption = Record<string, boolean | string | number | null>;
export type Primitive = string | number | boolean | null;
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> {
pagination: Pagination;
pagination: {
more: boolean;
items_per_page: number;
};
results: T[];
count: number;
}
@@ -31,115 +20,19 @@ export interface TransportExceptionInterface {
name: string;
}
export class ValidationException<
M extends Record<string, Record<string, string|number>> = Record<
string,
Record<string, string|number>
>,
>
extends Error
implements ValidationExceptionInterface<M>
{
public readonly name = "ValidationException" as const;
public readonly problems: ValidationProblemFromMap<M>;
public readonly violations: string[];
public readonly violationsList: ViolationFromMap<M>[];
public readonly titles: string[];
public readonly propertyPaths: DynamicKeys<M> & string[];
public readonly byProperty: Record<Extract<keyof M, string>, string[]>;
constructor(problem: ValidationProblemFromMap<M>) {
const message = [problem.title, problem.detail].filter(Boolean).join(" — ");
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.problems = problem;
this.violationsList = problem.violations;
this.violations = problem.violations.map(
(v) => `${v.title}: ${v.propertyPath}`,
);
this.titles = problem.violations.map((v) => v.title);
this.propertyPaths = problem.violations.map(
(v) => v.propertyPath,
) as DynamicKeys<M> & string[];
this.byProperty = problem.violations.reduce(
(acc, v) => {
const key = v.propertyPath.replace('/\[\d+\]$/', "") as Extract<keyof M, string>;
(acc[key] ||= []).push(v.title);
return acc;
},
{} as Record<Extract<keyof M, string>, string[]>,
);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ValidationException);
}
}
violationsByNormalizedProperty(property: Extract<keyof M, string>): ViolationFromMap<M>[] {
return this.violationsList.filter((v) => v.propertyPath.replace(/\[\d+\]$/, "") === property);
}
violationsByNormalizedPropertyAndParams<
P extends Extract<keyof M, string>,
K extends Extract<keyof M[P], string>
>(
property: P,
param: K,
param_value: M[P][K]
): ViolationFromMap<M>[]
{
const list = this.violationsByNormalizedProperty(property);
return list.filter(
(v): boolean =>
!!v.parameters &&
// `with_parameter in v.parameters` check indexing
param in v.parameters &&
// the cast is safe, because we have overloading that bind the types
(v.parameters as M[P])[param] === param_value
);
}
export interface ValidationExceptionInterface extends TransportExceptionInterface {
name: "ValidationException";
error: object;
violations: string[];
titles: string[];
propertyPaths: string[];
}
/**
* Check that the exception is a ValidationExceptionInterface
* @param x
*/
export function isValidationException<M extends Record<string, Record<string, string|number>>>(
x: unknown,
): x is ValidationExceptionInterface<M> {
return (
x instanceof ValidationException ||
(typeof x === "object" &&
x !== null &&
(x as any).name === "ValidationException")
);
}
export function isValidationProblem(x: unknown): x is {
type: string;
export interface ValidationErrorResponse extends TransportExceptionInterface {
violations: {
title: string;
violations: { propertyPath: string; title: string }[];
} {
if (!x || typeof x !== "object") return false;
const o = x as any;
return (
typeof o.type === "string" &&
typeof o.title === "string" &&
Array.isArray(o.violations) &&
o.violations.every(
(v: any) =>
v &&
typeof v === "object" &&
typeof v.propertyPath === "string" &&
typeof v.title === "string",
)
);
propertyPath: string;
}[];
}
export interface AccessExceptionInterface extends TransportExceptionInterface {
@@ -147,8 +40,7 @@ export interface AccessExceptionInterface extends TransportExceptionInterface {
violations: string[];
}
export interface NotFoundExceptionInterface
extends TransportExceptionInterface {
export interface NotFoundExceptionInterface extends TransportExceptionInterface {
name: "NotFoundException";
}
@@ -159,158 +51,18 @@ export interface ServerExceptionInterface extends TransportExceptionInterface {
body: string;
}
export interface ConflictHttpExceptionInterface
extends TransportExceptionInterface {
export interface ConflictHttpExceptionInterface extends TransportExceptionInterface {
name: "ConflictHttpException";
violations: string[];
}
/**
* Generic api method that can be adapted to any fetch request.
* Generic api method that can be adapted to any fetch request
*
* What this does
* - Performs a single HTTP request using fetch and returns the parsed JSON as Output.
* - Interprets common API errors and throws typed exceptions you can catch in your UI.
* - When the server returns a Symfony validation problem (HTTP 422), the error is
* rethrown as a typed ValidationException that is aware of your Violation Map (see below).
*
* Important: For GET endpoints that return lists, prefer using fetchResults, which
* handles pagination and aggregation for you.
*
* Violation Map (M): make your 422 errors strongly typed
* ------------------------------------------------------
* Symfonys validation problem+json payload looks like this (simplified):
*
* {
* "type": "https://symfony.com/errors/validation",
* "title": "Validation Failed",
* "violations": [
* {
* "propertyPath": "mobilenumber",
* "title": "This value is not a valid phone number.",
* "parameters": {
* "{{ value }}": "+33 1 02 03 04 05",
* "{{ types }}": "mobile number"
* },
* "type": "urn:uuid:..."
* }
* ]
* }
*
* The makeFetch generic type parameter M lets you describe, field by field, which
* parameters may appear for each propertyPath. Doing so gives you full type-safety when
* consuming ValidationException in your UI code.
*
* How to build M (Violation Map)
* - M is a map where each key is a server-side propertyPath (string), and the value is a
* record describing the allowed keys in the parameters object for that property.
* - Keys in parameters are the exact strings you receive from Symfony, including the
* curly-braced placeholders such as "{{ value }}", "{{ types }}", etc.
*
* Example from Person creation (WritePersonViolationMap)
* -----------------------------------------------------
* In ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.ts youll find:
*
* export type WritePersonViolationMap = {
* gender: {
* "{{ value }}": string | null;
* };
* mobilenumber: {
* "{{ types }}": string; // ex: "mobile number"
* "{{ value }}": string; // ex: "+33 1 02 03 04 05"
* };
* };
*
* This means:
* - If the server reports a violation for propertyPath "gender", the parameters object
* is expected to contain a key "{{ value }}" with a string or null value.
* - If the server reports a violation for propertyPath "mobilenumber", the parameters
* may include "{{ value }}" and "{{ types }}" as strings.
*
* How makeFetch uses M
* - When the response has status 422 and the payload matches a Symfony validation
* problem, makeFetch casts it to ValidationProblemFromMap<M> and throws a
* ValidationException<M>.
* - The ValidationException exposes helpful, pre-computed fields:
* - exception.problem: the full typed payload
* - exception.violations: ["Title: propertyPath", ...]
* - exception.titles: ["Title 1", "Title 2", ...]
* - exception.propertyPaths: ["gender", "mobilenumber", ...] (typed from M)
* - exception.byProperty: { gender: [titles...], mobilenumber: [titles...] }
*
* Typical usage patterns
* ----------------------
* 1) GET without Validation Map (no 422 expected):
*
* const centers = await makeFetch<null, { showCenters: boolean; centers: Center[] }>(
* "GET",
* "/api/1.0/person/creation/authorized-centers",
* null
* );
*
* 2) POST with body and Violation Map:
*
* type WritePersonViolationMap = {
* gender: { "{{ value }}": string | null };
* mobilenumber: { "{{ types }}": string; "{{ value }}": string };
* };
*
* try {
* const created = await makeFetch<PersonWrite, Person, WritePersonViolationMap>(
* "POST",
* "/api/1.0/person/person.json",
* personPayload
* );
* // Success path
* } catch (e) {
* if (isValidationException(e)) {
* // Fully typed:
* e.propertyPaths.includes("mobilenumber");
* const firstTitleForMobile = e.byProperty.mobilenumber?.[0];
* // You can also inspect parameter values:
* const v = e.problem.violations.find(v => v.propertyPath === "mobilenumber");
* const rawValue = v?.parameters?.["{{ value }}"]; // typed as string
* } else {
* // Other error handling (AccessException, ConflictHttpException, etc.)
* }
* }
*
* Tips to design your Violation Map
* - Use exact propertyPath strings as exposed by the API (they usually match your
* DTO field names or entity property paths used by the validator).
* - Inside each property, list only the placeholders that you actually read in the UI
* (you can always add more later). This keeps your types strict but pragmatic.
* - If a field may not include parameters at all, you can set it to an empty object {}.
* - If you dont care about parameter typing, you can omit M entirely and rely on the
* default loose typing (Record<string, Primitive>), but youll lose safety.
*
* Error taxonomy thrown by makeFetch
* - ValidationException<M> when status = 422 and payload is a validation problem.
* - AccessException when status = 403.
* - ConflictHttpException when status = 409.
* - A generic error object for other non-ok statuses.
*
* @typeParam Input - Shape of the request body you send (if any)
* @typeParam Output - Shape of the successful JSON response you expect
* @typeParam M - Violation Map describing the per-field parameters you expect
* in Symfony validation violations. See examples above.
*
* @param method The HTTP method to use (POST, GET, PUT, PATCH, DELETE)
* @param url The absolute or relative URL to call
* @param body The request payload. If null/undefined, no body is sent
* @param options Extra fetch options/headers merged into the request
*
* @returns The parsed JSON response typed as Output. For 204 No Content, resolves
* with undefined (void).
* This method is suitable make a single fetch. When performing a GET to fetch a list of elements, always consider pagination
* and use of the @link{fetchResults} method.
*/
export const makeFetch = async <
Input,
Output,
M extends Record<string, Record<string, string|number>> = Record<
string,
Record<string, string|number>
>,
>(
export const makeFetch = <Input, Output>(
method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE",
url: string,
body?: body | Input | null,
@@ -330,8 +82,7 @@ export const makeFetch = async <
if (typeof options !== "undefined") {
opts = Object.assign(opts, options);
}
return fetch(url, opts).then(async (response) => {
return fetch(url, opts).then((response) => {
if (response.status === 204) {
return Promise.resolve();
}
@@ -341,20 +92,9 @@ export const makeFetch = async <
}
if (response.status === 422) {
// Unprocessable Entity -> payload de validation Symfony
const json = await response.json().catch(() => undefined);
if (isValidationProblem(json)) {
// On ré-interprète le payload selon M (ParamMap) pour typer les violations
const problem = json as unknown as ValidationProblemFromMap<M>;
throw new ValidationException<M>(problem);
}
const err = new Error(
"Validation failed but payload is not a ValidationProblem",
);
(err as any).raw = json;
throw err;
return response.json().then((response) => {
throw ValidationException(response);
});
}
if (response.status === 403) {
@@ -419,6 +159,12 @@ function _fetchAction<T>(
throw NotFoundException(response);
}
if (response.status === 422) {
return response.json().then((response) => {
throw ValidationException(response);
});
}
if (response.status === 403) {
throw AccessException(response);
}
@@ -477,6 +223,24 @@ export const fetchScopes = (): Promise<Scope[]> => {
return fetchResults("/api/1.0/main/scope.json");
};
/**
* Error objects to be thrown
*/
const ValidationException = (
response: ValidationErrorResponse,
): ValidationExceptionInterface => {
const error = {} as ValidationExceptionInterface;
error.name = "ValidationException";
error.violations = response.violations.map(
(violation) => `${violation.title}: ${violation.propertyPath}`,
);
error.titles = response.violations.map((violation) => violation.title);
error.propertyPaths = response.violations.map(
(violation) => violation.propertyPath,
);
return error;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const AccessException = (response: Response): AccessExceptionInterface => {
const error = {} as AccessExceptionInterface;

View File

@@ -1,17 +0,0 @@
import {Gender, GenderTranslation} from "ChillMainAssets/types";
/**
* Translates a given gender object into its corresponding gender translation string.
*
* @param {Gender|null} gender - The gender object to be translated, null values are also supported
* @return {GenderTranslation} Returns the gender translation string corresponding to the provided gender,
* or "unknown" if the gender is null.
*/
export function toGenderTranslation(gender: Gender|null): GenderTranslation
{
if (null === gender) {
return "unknown";
}
return gender.genderTranslation;
}

View File

@@ -1,5 +1,4 @@
import { DateTime, TranslatableString } from "ChillMainAssets/types";
import { getLocale } from "translator";
import { TranslatableString } from "ChillMainAssets/types";
/**
* Localizes a translatable string object based on the current locale.
@@ -9,25 +8,6 @@ import { getLocale } from "translator";
* @param locale defaults to browser locale
* @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 locale = getLocale();
// Ensure url starts with a slash and does not already start with /{lang}/
const normalizedUrl = url.startsWith("/") ? url : `/${url}`;
const langPrefix = `/${locale}`;
if (normalizedUrl.startsWith(langPrefix + "/")) {
return normalizedUrl;
}
return `${langPrefix}${normalizedUrl}`;
}
export function localizeString(
translatableString: TranslatableString | null | undefined,
locale?: string,
@@ -36,7 +16,7 @@ export function localizeString(
return "";
}
const currentLocale = locale || getLocale();
const currentLocale = locale || navigator.language.split("-")[0] || "fr";
if (translatableString[currentLocale]) {
return translatableString[currentLocale];
@@ -59,47 +39,3 @@ export function localizeString(
return "";
}
const datetimeFormats: Record<
string,
Record<string, Intl.DateTimeFormatOptions>
> = {
fr: {
short: {
year: "numeric",
month: "numeric",
day: "numeric",
},
text: {
year: "numeric",
month: "long",
day: "numeric",
},
long: {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
hour12: false,
},
hoursOnly: {
hour: "numeric",
minute: "numeric",
hour12: false,
},
},
};
export function localizeDateTimeFormat(
dateTime: DateTime,
format: keyof typeof datetimeFormats.fr = "short",
): string {
const locale = getLocale();
const options =
datetimeFormats[locale]?.[format] || datetimeFormats.fr[format];
return new Intl.DateTimeFormat(locale, options).format(
new Date(dateTime.datetime),
);
}
export default datetimeFormats;

View File

@@ -126,8 +126,7 @@ function loadDynamicPicker(element) {
-1 ===
this.suggested.findIndex(
(e) => e.type === entity.type && e.id === entity.id,
) &&
"me" !== 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

@@ -3,8 +3,6 @@ import {
isGenericDocWithStoredObject,
} from "ChillDocStoreAssets/types/generic_doc";
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
import { CreatableEntityType } from "ChillPersonAssets/types";
import {ThirdpartyCompany} from "../../../ChillThirdPartyBundle/Resources/public/types";
import { Person } from "../../../ChillPersonBundle/Resources/public/types";
export interface DateTime {
@@ -12,63 +10,9 @@ export interface DateTime {
datetime8601: string;
}
/**
* A date representation to use when we create or update a date
*/
export interface DateTimeWrite {
/**
* Must be a string in format Y-m-d\TH:i:sO
*/
datetime: string;
}
export interface Civility {
type: "chill_main_civility";
id: number;
abbreviation: TranslatableString;
active: boolean;
name: TranslatableString;
}
/**
* Lightweight reference to Civility, to use in POST or PUT requests.
*/
export interface SetCivility {
type: "chill_main_civility";
id: number;
}
/**
* Gender translation.
*
* Match the GenderEnum in PHP code.
*/
export type GenderTranslation = "male" | "female" | "neutral" | "unknown";
/**
* A gender
*
* See also
*/
export interface Gender {
type: "chill_main_gender";
id: number;
label: TranslatableString;
genderTranslation: GenderTranslation;
}
/**
* Lightweight reference to a Gender, used in POST / PUT requests.
*/
export interface SetGender {
type: "chill_main_gender";
id: number;
}
export interface Household {
type: "household";
id: number;
// TODO
}
export interface Job {
@@ -83,18 +27,6 @@ export interface Center {
id: number;
type: "center";
name: string;
isActive: boolean;
}
/**
* SetCenter is a lightweight reference used in POST/PUT requests to associate an existing center with a resource.
* It links by id only and does not create or modify centers.
* Expected shape: { type: "center", id: number }.
* Requests will fail if the id is invalid, the center doesn't exist, or permissions are insufficient.
*/
export interface SetCenter {
id: number;
type: "center";
}
export interface Scope {
@@ -186,13 +118,6 @@ export interface Address {
isNoAddress: boolean;
}
/**
* Associate an existing address in write operations.
*/
export interface SetAddress {
id: number;
}
export interface AddressWithPoint extends Address {
point: Point;
}
@@ -366,156 +291,6 @@ export interface PrivateCommentEmbeddable {
comments: Record<number, string>;
}
// API Exception types
export interface TransportExceptionInterface {
name: string;
}
type IndexedKey<Base extends string> = `${Base}[${number}]`;
type BaseKeys<M> = Extract<keyof M, string>;
export type DynamicKeys<M extends Record<string, Record<string, unknown>>> =
| BaseKeys<M>
| { [K in BaseKeys<M> as IndexedKey<K>]: K }[IndexedKey<BaseKeys<M>>];
type NormalizeKey<K extends string> = K extends `${infer B}[${number}]` ? B : K;
export type ViolationFromMap<M extends Record<string, Record<string, unknown>>> = {
[K in DynamicKeys<M> & string]: { // <- note le "& string" ici
propertyPath: K;
title: string;
parameters?: M[NormalizeKey<K>];
type?: string;
}
}[DynamicKeys<M> & string];
export type ValidationProblemFromMap<
M extends Record<string, Record<string, string|number>>,
> = {
type: string;
title: string;
detail?: string;
violations: ViolationFromMap<M>[];
} & Record<string, unknown>;
export interface ValidationExceptionInterface<
M extends Record<string, Record<string, string|number>> = Record<
string,
Record<string, string|number>
>,
> extends Error {
name: "ValidationException";
/** Full server payload copy */
problems: ValidationProblemFromMap<M>;
/** A list of all violations, with property key */
violationsList: ViolationFromMap<M>[];
/** Compact list "Title: path" */
violations: string[];
/** Only titles */
titles: string[];
/** Only property paths */
propertyPaths: DynamicKeys<M> & string[];
/** Indexing by property (useful for display by field) */
byProperty: Record<Extract<keyof M, string>, string[]>;
violationsByNormalizedProperty(property: Extract<keyof M, string>): ViolationFromMap<M>[];
violationsByNormalizedPropertyAndParams<
P extends Extract<keyof M, string>,
K extends Extract<keyof M[P], string>
>(
property: P,
param: K,
param_value: M[P][K]
): ViolationFromMap<M>[];
}
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;
}
export type CreateComponentConfigGeneral = {
action: 'create';
allowedTypes: CreatableEntityType[];
query: string;
parent: null;
}
export type CreateComponentThirdPartyAddContact = {
action: 'addContact';
allowedTypes: readonly ['thirdparty'];
query: string;
parent: ThirdpartyCompany;
}
/**
* Configuration for the CreateModal and Create component
*/
export type CreateComponentConfig = CreateComponentConfigGeneral | CreateComponentThirdPartyAddContact;
/**
* Possible states for the WaitingScreen Component.
*/

View File

@@ -24,7 +24,9 @@
{{ trans(getTextTitle) }}
<span v-if="flag.loading" class="loading">
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
<span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span>
<span class="sr-only">{{
trans(ADDRESS_LOADING)
}}</span>
</span>
</h2>
</template>
@@ -88,7 +90,9 @@
{{ trans(getTextTitle) }}
<span v-if="flag.loading" class="loading">
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
<span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span>
<span class="sr-only">{{
trans(ADDRESS_LOADING)
}}</span>
</span>
</h2>
</template>
@@ -144,7 +148,10 @@
</template>
<template #action>
<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) }}
<i class="fa fa-fw fa-arrow-right" />
</button>
@@ -171,7 +178,9 @@
{{ trans(getTextTitle) }}
<span v-if="flag.loading" class="loading">
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
<span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span>
<span class="sr-only">{{
trans(ADDRESS_LOADING)
}}</span>
</span>
</h2>
</template>
@@ -385,7 +394,8 @@ export default {
getTextTitle() {
if (
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);
@@ -505,7 +515,10 @@ export default {
if (!this.context.edit) {
this.context.edit = true;
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,
);
}
}
},
@@ -632,7 +645,9 @@ export default {
? this.entity.address.confidential
: false;
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.address.country
@@ -727,7 +742,8 @@ export default {
// add the address reference, if any
if (this.entity.selected.address.addressReference !== undefined) {
newAddress = Object.assign(newAddress, {
addressReference: this.entity.selected.address.addressReference,
addressReference:
this.entity.selected.address.addressReference,
});
} else {
newAddress = Object.assign(newAddress, {
@@ -747,7 +763,10 @@ export default {
});
}
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, {
validTo: {
datetime: `${this.entity.selected.valid.to.toISOString().split("T")[0]}T00:00:00+0100`,
@@ -759,7 +778,10 @@ export default {
newPostcode = Object.assign(newPostcode, {
country: { id: this.entity.selected.country.id },
}); //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, {
newPostcode: newPostcode,
});

View File

@@ -72,7 +72,10 @@ export default {
this.entity.addressMap.zoom,
);
} else {
this.map.setView(lonLatForLeaflet(this.addressPoint.coordinates), 15);
this.map.setView(
lonLatForLeaflet(this.addressPoint.coordinates),
15,
);
}
this.map.scrollWheelZoom.disable();
@@ -102,7 +105,9 @@ export default {
},
update() {
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));
}
},

View File

@@ -55,7 +55,9 @@
:placeholder="trans(ADDRESS_BUILDING_NAME)"
v-model="buildingName"
/>
<label for="buildingName">{{ trans(ADDRESS_BUILDING_NAME) }}</label>
<label for="buildingName">{{
trans(ADDRESS_BUILDING_NAME)
}}</label>
</div>
<div class="form-floating my-1">
<input
@@ -77,7 +79,9 @@
:placeholder="trans(ADDRESS_DISTRIBUTION)"
v-model="distribution"
/>
<label for="distribution">{{ trans(ADDRESS_DISTRIBUTION) }}</label>
<label for="distribution">{{
trans(ADDRESS_DISTRIBUTION)
}}</label>
</div>
</div>
</div>

View File

@@ -57,7 +57,9 @@
:placeholder="trans(ADDRESS_STREET_NUMBER)"
v-model="streetNumber"
/>
<label for="streetNumber">{{ trans(ADDRESS_STREET_NUMBER) }}</label>
<label for="streetNumber">{{
trans(ADDRESS_STREET_NUMBER)
}}</label>
</div>
</div>
</div>
@@ -98,7 +100,9 @@ export default {
props: ["entity", "context", "updateMapCenter", "flag", "checkErrors"],
data() {
return {
value: this.context.edit ? this.entity.address.addressReference : null,
value: this.context.edit
? this.entity.address.addressReference
: null,
isLoading: false,
};
},
@@ -171,7 +175,8 @@ export default {
.then(
(addresses) =>
new Promise((resolve) => {
this.entity.loaded.addresses = addresses.results;
this.entity.loaded.addresses =
addresses.results;
this.isLoading = false;
resolve();
}),
@@ -188,7 +193,8 @@ export default {
.then(
(addresses) =>
new Promise((resolve) => {
this.entity.loaded.addresses = addresses.results;
this.entity.loaded.addresses =
addresses.results;
this.isLoading = false;
resolve();
}),

View File

@@ -158,7 +158,9 @@ export default {
},
methods: {
transName(value) {
return value.code && value.name ? `${value.name} (${value.code})` : "";
return value.code && value.name
? `${value.name} (${value.code})`
: "";
},
selectCity(value) {
console.log(value);
@@ -166,7 +168,8 @@ export default {
this.entity.selected.postcode.name = value.name;
this.entity.selected.postcode.code = value.code;
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.$emit("getReferenceAddresses", value);
@@ -187,7 +190,8 @@ export default {
.then(
(cities) =>
new Promise((resolve) => {
this.entity.loaded.cities = cities.results.filter(
this.entity.loaded.cities =
cities.results.filter(
(c) => c.origin !== 3,
); // filter out user-defined cities
this.isLoading = false;
@@ -206,7 +210,8 @@ export default {
.then(
(cities) =>
new Promise((resolve) => {
this.entity.loaded.cities = cities.results.filter(
this.entity.loaded.cities =
cities.results.filter(
(c) => c.origin !== 3,
); // filter out user-defined cities
this.isLoading = false;

View File

@@ -60,8 +60,12 @@ export default {
sortedCountries() {
const countries = this.entity.loaded.countries;
let sortedCountries = [];
sortedCountries.push(...countries.filter((c) => c.countryCode === "FR"));
sortedCountries.push(...countries.filter((c) => c.countryCode === "BE"));
sortedCountries.push(
...countries.filter((c) => c.countryCode === "FR"),
);
sortedCountries.push(
...countries.filter((c) => c.countryCode === "BE"),
);
sortedCountries.push(
...countries
.filter((c) => c.countryCode !== "FR")

View File

@@ -1,6 +1,9 @@
<template>
<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>
</div>
@@ -139,7 +142,8 @@ export default {
address["street"] = this.entity.selected.address.street
? this.entity.selected.address.street
: null;
address["streetNumber"] = this.entity.selected.address.streetNumber
address["streetNumber"] = this.entity.selected.address
.streetNumber
? this.entity.selected.address.streetNumber
: null;
address["floor"] = this.entity.selected.address.floor
@@ -154,10 +158,12 @@ export default {
address["flat"] = this.entity.selected.address.flat
? this.entity.selected.address.flat
: null;
address["buildingName"] = this.entity.selected.address.buildingName
address["buildingName"] = this.entity.selected.address
.buildingName
? this.entity.selected.address.buildingName
: null;
address["distribution"] = this.entity.selected.address.distribution
address["distribution"] = this.entity.selected.address
.distribution
? this.entity.selected.address.distribution
: null;
address["extra"] = this.entity.selected.address.extra

View File

@@ -2,7 +2,10 @@
<div class="address-form">
<!-- Not display in modal -->
<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>
</div>

View File

@@ -1,7 +1,10 @@
<template>
<div v-if="!onlyButton" class="mt-4 flex-grow-1">
<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>
</div>
@@ -11,7 +14,9 @@
<div v-if="flag.success" class="alert alert-success">
{{ trans(getSuccessText) }}
<span v-if="forceRedirect">{{ trans(ADDRESS_WAIT_REDIRECTION) }}</span>
<span v-if="forceRedirect">{{
trans(ADDRESS_WAIT_REDIRECTION)
}}</span>
</div>
<div
@@ -42,7 +47,9 @@
name="button"
:title="trans(getTextButton)"
>
<span v-if="displayTextButton">{{ trans(getTextButton) }}</span>
<span v-if="displayTextButton">{{
trans(getTextButton)
}}</span>
</button>
</template>
</action-buttons>
@@ -55,7 +62,9 @@
: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">
<template #action>
<button
@@ -66,7 +75,9 @@
name="button"
:title="trans(getTextButton)"
>
<span v-if="displayTextButton">{{ trans(getTextButton) }}</span>
<span v-if="displayTextButton">{{
trans(getTextButton)
}}</span>
</button>
</template>
</action-buttons>
@@ -88,7 +99,9 @@
name="button"
:title="trans(getTextButton)"
>
<span v-if="displayTextButton">{{ trans(getTextButton) }}</span>
<span v-if="displayTextButton">{{
trans(getTextButton)
}}</span>
</button>
</template>
</action-buttons>

View File

@@ -1,6 +1,9 @@
<template>
<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>
</div>

View File

@@ -1,152 +1,155 @@
<template>
<h2>{{ trans(MAIN_TITLE) }}</h2>
<h2>{{ $t("main_title") }}</h2>
<ul class="nav nav-tabs">
<li v-for="tab in displayedTabs" :key="tab.key" class="nav-item">
<li class="nav-item">
<a
class="nav-link"
:class="{ active: activeTab === tab.key }"
@click="selectTab(tab.key)"
:class="{ active: activeTab === 'MyCustoms' }"
@click="selectTab('MyCustoms')"
>
<i v-if="tab.icon" :class="tab.icon" />
<span v-else>{{ tab.label }}</span>
<tab-counter v-if="tab.counter" :count="tab.counter()" />
<i class="fa fa-dashboard" />
</a>
</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>
</li>
<li class="nav-item loading ms-auto py-2" v-if="loading">
<i
class="fa fa-circle-o-notch fa-spin fa-lg text-chill-gray"
:title="trans(LOADING)"
:title="$t('loading')"
/>
</li>
</ul>
<div class="my-4">
<MyCustoms v-if="activeTab === HomepageTabs.MyCustoms" />
<MyTickets v-else-if="activeTab === HomepageTabs.MyTickets" />
<MyNotifications v-else-if="activeTab === HomepageTabs.MyNotifications" />
<MyAccompanyingCourses
v-else-if="activeTab === HomepageTabs.MyAccompanyingCourses"
<my-customs v-if="activeTab === 'MyCustoms'" />
<my-works v-else-if="activeTab === 'MyWorks'" />
<my-evaluations v-else-if="activeTab === 'MyEvaluations'" />
<my-tasks v-else-if="activeTab === 'MyTasks'" />
<my-accompanying-courses
v-else-if="activeTab === 'MyAccompanyingCourses'"
/>
<MyEvaluations v-else-if="activeTab === HomepageTabs.MyEvaluations" />
<MyTasks v-else-if="activeTab === HomepageTabs.MyTasks" />
<MyWorkflows v-else-if="activeTab === HomepageTabs.MyWorkflows" />
<my-notifications v-else-if="activeTab === 'MyNotifications'" />
<my-workflows v-else-if="activeTab === 'MyWorkflows'" />
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import { useStore } from "vuex";
// Components
import MyCustoms from "./MyCustoms.vue";
import MyEvaluations from "./MyEvaluations.vue";
import MyTasks from "./MyTasks.vue";
import MyAccompanyingCourses from "./MyAccompanyingCourses.vue";
import MyNotifications from "./MyNotifications.vue";
<script>
import MyCustoms from "./MyCustoms";
import MyWorks from "./MyWorks";
import MyEvaluations from "./MyEvaluations";
import MyTasks from "./MyTasks";
import MyAccompanyingCourses from "./MyAccompanyingCourses";
import MyNotifications from "./MyNotifications";
import MyWorkflows from "./MyWorkflows.vue";
import MyTickets from "./MyTickets.vue";
import TabCounter from "./TabCounter.vue";
import TabCounter from "./TabCounter";
import { mapState } from "vuex";
// Translations
import {
MAIN_TITLE,
MY_TICKETS_TAB,
MY_EVALUATIONS_TAB,
MY_TASKS_TAB,
MY_ACCOMPANYING_COURSES_TAB,
MY_NOTIFICATIONS_TAB,
MY_WORKFLOWS_TAB,
LOADING,
trans,
} 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,
export default {
name: "App",
components: {
MyCustoms,
MyWorks,
MyEvaluations,
MyTasks,
MyWorkflows,
MyAccompanyingCourses,
MyNotifications,
TabCounter,
},
{
key: HomepageTabs.MyTickets,
label: trans(MY_TICKETS_TAB),
icon: null,
counter: () => ticketListState.value?.count,
data() {
return {
activeTab: "MyCustoms",
};
},
{
key: HomepageTabs.MyNotifications,
label: trans(MY_NOTIFICATIONS_TAB),
icon: null,
counter: () => state.value?.notifications?.count,
computed: {
...mapState(["loading"]),
// just to see all in devtool :
...mapState({
state: (state) => state,
}),
},
{
key: HomepageTabs.MyAccompanyingCourses,
label: trans(MY_ACCOMPANYING_COURSES_TAB),
icon: null,
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(() => {
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);
methods: {
selectTab(tab) {
if (tab !== "MyCustoms") {
this.$store.dispatch("getByTab", { tab: tab });
}
return tabs.filter(Boolean);
});
const activeTab = ref(Number(HomepageTabs[homepageConfig.value.defaultTab]));
const loading = computed(() => store.state.loading);
async function selectTab(tab: HomepageTabs) {
activeTab.value = tab;
}
onMounted(() => {
for (const tab of displayedTabs.value) {
if (tab.key !== HomepageTabs.MyCustoms) {
store.dispatch("getByTab", { tab: tab.key, param: "countOnly=1" });
this.activeTab = tab;
console.log(this.activeTab);
},
},
mounted() {
for (const m of [
"MyNotifications",
"MyAccompanyingCourses",
// 'MyWorks',
"MyEvaluations",
"MyTasks",
"MyWorkflows",
]) {
this.$store.dispatch("getByTab", { tab: m, param: "countOnly=1" });
}
}
});
},
};
</script>
<style scoped>

View File

@@ -2,9 +2,7 @@
<li>
<h2>{{ props.item.title }}</h2>
<time class="createdBy" datetime="{{item.startDate.datetime}}">{{
props.item?.startDate
? localizeDateTimeFormat(props.item?.startDate, "text")
: ""
$d(newsItemStartDate(), "text")
}}</time>
<div class="content" v-if="shouldTruncate(item.content)">
<div v-html="prepareContent(item.content)"></div>
@@ -27,11 +25,11 @@
</template>
<template #body>
<p class="news-date">
<time class="createdBy" datetime="{{item.startDate.datetime}}">{{
props.item?.startDate
? localizeDateTimeFormat(props.item?.startDate, "text")
: ""
}}</time>
<time
class="createdBy"
datetime="{{item.startDate.datetime}}"
>{{ $d(newsItemStartDate(), "text") }}</time
>
</p>
<div v-html="convertMarkdownToHtml(item.content)"></div>
</template>
@@ -46,7 +44,7 @@ import DOMPurify from "dompurify";
import { NewsItemType } from "../../../types";
import type { PropType } from "vue";
import { ref } from "vue";
import { localizeDateTimeFormat } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import { ISOToDatetime } from "../../../chill/js/date";
const props = defineProps({
item: {
@@ -95,7 +93,10 @@ const truncateContent = (content: string): string => {
// Truncate if amount of lines are too many
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 + "...";
}
@@ -124,7 +125,8 @@ const truncateContent = (content: string): string => {
if (linkStartIndex !== -1) {
const linkEndIndex = content.indexOf(")", linkStartIndex);
const url = content.slice(linkStartIndex + 1, linkEndIndex);
truncatedContent = truncatedContent.slice(0, linkStartIndex) + `(${url})`;
truncatedContent =
truncatedContent.slice(0, linkStartIndex) + `(${url})`;
}
truncatedContent += "...";
@@ -137,7 +139,7 @@ const preprocess = (markdown: string): string => {
};
const postprocess = (html: string): string => {
DOMPurify.addHook("afterSanitizeAttributes", (node: Element) => {
DOMPurify.addHook("afterSanitizeAttributes", (node: any) => {
if ("target" in node) {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener noreferrer");
@@ -163,6 +165,10 @@ const prepareContent = (content: string): string => {
const htmlContent = convertMarkdownToHtml(content);
return truncateContent(htmlContent);
};
const newsItemStartDate = (): null | Date => {
return ISOToDatetime(props.item?.startDate.datetime);
};
</script>
<style scoped>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,38 +1,38 @@
<template>
<div class="alert alert-light">
{{ trans(MY_TASKS_DESCRIPTION_WARNING) }}
{{ $t("my_tasks.description_warning") }}
</div>
<span v-if="noResultsAlert" class="chill-no-data-statement">
{{ trans(NO_DATA) }}
</span>
<span v-if="noResultsAlert" class="chill-no-data-statement">{{
$t("no_data")
}}</span>
<tab-table v-else>
<template #thead>
<th scope="col">
{{ trans(WARNING_DATE) }}
{{ $t("warning_date") }}
</th>
<th scope="col">
{{ trans(MAX_DATE) }}
{{ $t("max_date") }}
</th>
<th scope="col">
{{ trans(TASK) }}
{{ $t("task") }}
</th>
<th scope="col" />
</template>
<template #tbody>
<tr v-for="(t, i) in tasks.alert.results" :key="`task-alert-${i}`">
<td v-if="t.warningDate !== null">
{{ localizeDateTimeFormat(t.warningDate, "short") }}
<td v-if="null !== t.warningDate">
{{ $d(t.warningDate.datetime, "short") }}
</td>
<td v-else />
<td>
<span class="outdated">{{
localizeDateTimeFormat(t.endDate, "short")
$d(t.endDate.datetime, "short")
}}</span>
</td>
<td>{{ t.title }}</td>
<td>
<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>
</td>
</tr>
@@ -40,36 +40,39 @@
</tab-table>
<div class="alert alert-light">
{{ trans(MY_TASKS_DESCRIPTION_ALERT) }}
{{ $t("my_tasks.description_alert") }}
</div>
<span v-if="noResultsWarning" class="chill-no-data-statement">
{{ trans(NO_DATA) }}
</span>
<span v-if="noResultsWarning" class="chill-no-data-statement">{{
$t("no_data")
}}</span>
<tab-table v-else>
<template #thead>
<th scope="col">
{{ trans(WARNING_DATE) }}
{{ $t("warning_date") }}
</th>
<th scope="col">
{{ trans(MAX_DATE) }}
{{ $t("max_date") }}
</th>
<th scope="col">
{{ trans(TASK) }}
{{ $t("task") }}
</th>
<th scope="col" />
</template>
<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>
<span class="outdated">{{
localizeDateTimeFormat(t.warningDate, "short")
$d(t.warningDate.datetime, "short")
}}</span>
</td>
<td>{{ localizeDateTimeFormat(t.endDate, "short") }}</td>
<td>{{ $d(t.endDate.datetime, "short") }}</td>
<td>{{ t.title }}</td>
<td>
<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>
</td>
</tr>
@@ -77,52 +80,39 @@
</tab-table>
</template>
<script lang="ts" setup>
import { computed, ComputedRef } from "vue";
import { useStore } from "vuex";
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";
import { localizeDateTimeFormat } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
const store = useStore();
const tasks: ComputedRef<TasksState> = computed(
() => store.state.homepage.tasks,
);
const isTasksWarningLoaded = computed(() => store.getters.isTasksWarningLoaded);
const isTasksAlertLoaded = computed(() => store.getters.isTasksAlertLoaded);
const noResultsAlert = computed(() => {
if (!isTasksAlertLoaded.value) {
export default {
name: "MyTasks",
components: {
TabTable,
},
computed: {
...mapState(["tasks"]),
...mapGetters(["isTasksWarningLoaded", "isTasksAlertLoaded"]),
noResultsAlert() {
if (!this.isTasksAlertLoaded) {
return false;
} else {
return tasks.value.alert.count === 0;
return this.tasks.alert.count === 0;
}
});
const noResultsWarning = computed(() => {
if (!isTasksWarningLoaded.value) {
},
noResultsWarning() {
if (!this.isTasksWarningLoaded) {
return false;
} 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`;
}
},
},
};
</script>
<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>
<div class="alert alert-light">
{{ trans(MY_WORKFLOWS_DESCRIPTION) }}
{{ $t("my_workflows.description") }}
</div>
<my-workflows-table :workflows="workflows" />
<div class="alert alert-light">
{{ trans(MY_WORKFLOWS_DESCRIPTION_CC) }}
{{ $t("my_workflows.description_cc") }}
</div>
<my-workflows-table :workflows="workflowsCc" />
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useStore } from "vuex";
<script>
import { mapState } from "vuex";
import MyWorkflowsTable from "./MyWorkflowsTable.vue";
import {
MY_WORKFLOWS_DESCRIPTION,
MY_WORKFLOWS_DESCRIPTION_CC,
trans,
} from "translator";
const store = useStore();
const workflows = computed(() => store.state.homepage.workflows);
const workflowsCc = computed(() => store.state.homepage.workflowsCc);
export default {
name: "MyWorkflows",
components: {
MyWorkflowsTable,
},
computed: {
...mapState(["workflows", "workflowsCc"]),
},
};
</script>

View File

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

View File

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

View File

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

View File

@@ -11,8 +11,11 @@
</table>
</template>
<script lang="ts" setup>
// Pas de props à définir, composant slot simple
<script>
export default {
name: "TabTable",
props: [],
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,89 @@
const appMessages = {
fr: {
main_title: "Vue d'ensemble",
my_works: {
tab: "Mes actions",
description:
"Liste des actions d'accompagnement dont je suis référent et qui arrivent à échéance.",
},
my_evaluations: {
tab: "Mes évaluations",
description:
"Liste des évaluations dont je suis référent et qui arrivent à échéance.",
},
my_tasks: {
tab: "Mes tâches",
description_alert:
"Liste des tâches auxquelles je suis assigné et dont la date de rappel est dépassée.",
description_warning:
"Liste des tâches auxquelles je suis assigné et dont la date d'échéance est dépassée.",
},
my_accompanying_courses: {
tab: "Mes nouveaux parcours",
description:
"Liste des parcours d'accompagnement que l'on vient de m'attribuer depuis moins de 15 jours.",
},
my_notifications: {
tab: "Mes nouvelles notifications",
description: "Liste des notifications reçues et non lues.",
},
my_workflows: {
tab: "Mes workflows",
description: "Liste des workflows en attente d'une action.",
description_cc: "Liste des workflows dont je suis en copie.",
},
opening_date: "Date d'ouverture",
social_issues: "Problématiques sociales",
concerned_persons: "Usagers concernés",
max_date: "Date d'échéance",
warning_date: "Date de rappel",
evaluation: "Évaluation",
task: "Tâche",
Date: "Date",
From: "Expéditeur",
Subject: "Objet",
Entity: "Associé à",
Step: "Étape",
concerned_users: "Usagers concernés",
Object_workflow: "Objet du workflow",
on_hold: "En attente",
show_entity: "Voir {entity}",
the_activity: "l'échange",
the_course: "le parcours",
the_action: "l'action",
the_evaluation: "l'évaluation",
the_evaluation_document: "le document",
the_task: "la tâche",
the_workflow: "le workflow",
StartDate: "Date d'ouverture",
SocialAction: "Action d'accompagnement",
no_data: "Aucun résultats",
no_dashboard: "Pas de tableaux de bord",
counter: {
unread_notifications:
"{n} notification non lue | {n} notifications non lues",
assignated_courses:
"{n} parcours récent assigné | {n} parcours récents assignés",
assignated_actions: "{n} action assignée | {n} actions assignées",
assignated_evaluations:
"{n} évaluation assignée | {n} évaluations assignées",
alert_tasks: "{n} tâche en rappel | {n} tâches en rappel",
warning_tasks: "{n} tâche à échéance | {n} tâches à échéance",
},
emergency: "Urgent",
confidential: "Confidentiel",
automatic_notification: "Notification automatique",
widget: {
news: {
title: "Actualités",
readMore: "Lire la suite",
date: "Date",
none: "Aucune actualité",
},
},
},
};
Object.assign(appMessages.fr);
export { appMessages };

View File

@@ -1,104 +1,90 @@
import "es6-promise/auto";
import { Module } from "vuex";
import {
makeFetch,
PaginationResponse,
} from "ChillMainAssets/lib/api/apiMethods";
import {
AccompanyingCourse,
Alert,
Evaluation,
Warning,
Workflow,
WorflowCc,
Notification,
} from "ChillPersonAssets/types";
import { RootState } from "..";
import { HomepageTabs } from "ChillMainAssets/types";
import { createStore } from "vuex";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
export interface TasksState {
warning: PaginationResponse<Warning>;
alert: PaginationResponse<Alert>;
}
const debug = process.env.NODE_ENV !== "production";
export interface State {
evaluations: PaginationResponse<Evaluation>;
tasks: TasksState;
accompanyingCourses: PaginationResponse<AccompanyingCourse>;
notifications: PaginationResponse<Notification>;
workflows: PaginationResponse<Workflow>;
workflowsCc: PaginationResponse<WorflowCc>;
errorMsg: unknown[];
loading: boolean;
ticketsLoading: boolean;
}
const isEmpty = (obj) => {
return (
obj &&
Object.keys(obj).length <= 1 &&
Object.getPrototypeOf(obj) === Object.prototype
);
};
export const moduleHomepage: Module<State, RootState> = {
const store = createStore({
strict: debug,
state: {
evaluations: {
count: 0,
} as PaginationResponse<Evaluation>,
// works: {},
evaluations: {},
tasks: {
warning: {
count: 0,
} as PaginationResponse<Warning>,
alert: {
count: 0,
} as PaginationResponse<Alert>,
warning: {},
alert: {},
},
accompanyingCourses: {
count: 0,
} as PaginationResponse<AccompanyingCourse>,
notifications: {
count: 0,
} as PaginationResponse<Notification>,
workflows: {
count: 0,
} as PaginationResponse<Workflow>,
workflowsCc: {
count: 0,
} as PaginationResponse<WorflowCc>,
ticketsLoading: false,
accompanyingCourses: {},
notifications: {},
workflows: {},
workflowsCc: {},
errorMsg: [],
loading: false,
},
getters: {
isTicketLoading: (state) => {
return state.ticketsLoading;
},
// isWorksLoaded(state) {
// return !isEmpty(state.works);
// },
isEvaluationsLoaded(state) {
return Array.isArray(state.evaluations.results);
return !isEmpty(state.evaluations);
},
isTasksWarningLoaded(state) {
return Array.isArray(state.tasks.warning.results);
return !isEmpty(state.tasks.warning);
},
isTasksAlertLoaded(state) {
return Array.isArray(state.tasks.alert.results);
return !isEmpty(state.tasks.alert);
},
isAccompanyingCoursesLoaded(state) {
return Array.isArray(state.accompanyingCourses.results);
return !isEmpty(state.accompanyingCourses);
},
isNotificationsLoaded(state) {
return Array.isArray(state.notifications.results);
return !isEmpty(state.notifications);
},
isWorkflowsLoaded(state) {
return Array.isArray(state.workflows.results);
return !isEmpty(state.workflows);
},
counter(state) {
return {
// works: state.works.count,
evaluations: state.evaluations.count,
tasksWarning: state.tasks.warning.count,
tasksAlert: state.tasks.alert.count,
accompanyingCourses: state.accompanyingCourses.count,
notifications: state.notifications.count,
workflows: state.workflows.count,
};
},
},
mutations: {
// addWorks(state, works) {
// //console.log('addWorks', works);
// state.works = works;
// },
addEvaluations(state, evaluations) {
//console.log('addEvaluations', evaluations);
state.evaluations = evaluations;
},
addTasksWarning(state, tasks) {
//console.log('addTasksWarning', tasks);
state.tasks.warning = tasks;
},
addTasksAlert(state, tasks) {
//console.log('addTasksAlert', tasks);
state.tasks.alert = tasks;
},
addCourses(state, courses) {
//console.log('addCourses', courses);
state.accompanyingCourses = courses;
},
addNotifications(state, notifications) {
//console.log('addNotifications', notifications);
state.notifications = notifications;
},
addWorkflows(state, workflows) {
@@ -110,30 +96,30 @@ export const moduleHomepage: Module<State, RootState> = {
setLoading(state, bool) {
state.loading = bool;
},
setTicketsLoading(state, bool) {
state.ticketsLoading = bool;
},
catchError(state, error) {
state.errorMsg.push(error);
},
},
actions: {
async getByTab({ commit, getters, dispatch }, { tab, param }) {
getByTab({ commit, getters }, { tab, param }) {
switch (tab) {
case HomepageTabs.MyTickets:
if (!getters.isTicketsLoaded) {
commit("setTicketsLoading", true);
// Utilise l'action du module ticket_list
await dispatch(
"fetchTicketList",
{ byAddresseeToMe: true, byCurrentState: "open" },
{ root: true },
);
commit("setTicketsLoading", false);
}
break;
case HomepageTabs.MyEvaluations:
// case 'MyWorks':
// if (!getters.isWorksLoaded) {
// commit('setLoading', true);
// const url = `/api/1.0/person/accompanying-period/work/my-near-end${'?'+ param}`;
// makeFetch('GET', url)
// .then((response) => {
// commit('addWorks', response);
// commit('setLoading', false);
// })
// .catch((error) => {
// commit('catchError', error);
// throw error;
// })
// ;
// }
// break;
case "MyEvaluations":
if (!getters.isEvaluationsLoaded) {
commit("setLoading", true);
const url = `/api/1.0/person/accompanying-period/work/evaluation/my-near-end${"?" + param}`;
@@ -148,7 +134,7 @@ export const moduleHomepage: Module<State, RootState> = {
});
}
break;
case HomepageTabs.MyTasks:
case "MyTasks":
if (!(getters.isTasksWarningLoaded && getters.isTasksAlertLoaded)) {
commit("setLoading", true);
const urlWarning = `/api/1.0/task/single-task/list/my?f[q]=&f[checkboxes][status][]=warning&f[checkboxes][states][]=new&f[checkboxes][states][]=in_progress${"&" + param}`,
@@ -173,7 +159,7 @@ export const moduleHomepage: Module<State, RootState> = {
});
}
break;
case HomepageTabs.MyAccompanyingCourses:
case "MyAccompanyingCourses":
if (!getters.isAccompanyingCoursesLoaded) {
commit("setLoading", true);
const url = `/api/1.0/person/accompanying-course/list/by-recent-attributions${"?" + param}`;
@@ -188,13 +174,13 @@ export const moduleHomepage: Module<State, RootState> = {
});
}
break;
case HomepageTabs.MyNotifications:
case "MyNotifications":
if (!getters.isNotificationsLoaded) {
commit("setLoading", true);
const url = `/api/1.0/main/notification/my/unread${"?" + param}`;
makeFetch("GET", url)
.then((response) => {
console.log("notifications", response);
commit("addNotifications", response);
commit("setLoading", false);
})
@@ -204,7 +190,7 @@ export const moduleHomepage: Module<State, RootState> = {
});
}
break;
case HomepageTabs.MyWorkflows:
case "MyWorkflows":
if (!getters.isWorflowsLoaded) {
commit("setLoading", true);
makeFetch("GET", "/api/1.0/main/workflow/my")
@@ -231,4 +217,6 @@ export const moduleHomepage: Module<State, RootState> = {
}
},
},
};
});
export { store };

View File

@@ -1,55 +0,0 @@
import { createStore } from "vuex";
import { State as HomepageStates, moduleHomepage } from "./modules/homepage";
import {
State as TicketListStates,
moduleTicketList,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/ticket_list";
import {
State as TicketStates,
moduleTicket,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/ticket";
import {
State as CommentStates,
moduleComment,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/comment";
import {
moduleMotive,
State as MotiveStates,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/motive";
import {
State as AddresseeStates,
moduleAddressee,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/addressee";
import {
modulePersons,
State as PersonsState,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/persons";
import {
moduleUser,
State as UserState,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/user";
export interface RootState {
homepage: HomepageStates;
motive: MotiveStates;
ticket: TicketStates;
comment: CommentStates;
addressee: AddresseeStates;
persons: PersonsState;
ticketList: TicketListStates;
user: UserState;
}
export const store = createStore<RootState>({
modules: {
homepage: moduleHomepage,
motive: moduleMotive,
ticket: moduleTicket,
comment: moduleComment,
addressee: moduleAddressee,
person: modulePersons,
ticketList: moduleTicketList,
user: moduleUser,
},
});

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