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] [COMMIT_EDITMSG]
max_line_length = 0 max_line_length = 0
[*.{js,vue,ts}] [*.{js, vue, ts}]
indent_size = 2 indent_size = 2
indent_style = space indent_style = space
[*.rst] [.rst]
indent_size = 3 ident_size = 3
indent_style = space ident_style = space

View File

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

View File

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

30
.vscode/launch.json vendored
View File

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

23
.vscode/tasks.json vendored
View File

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

View File

@@ -6,6 +6,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.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 ## v4.9.0 - 2025-12-05
### Feature ### Feature
* ([#459](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/459)) Add a counter for invitations awaiting reply * ([#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 - person
- personvendee - personvendee
- household_edit_metadata - household_edit_metadata
- index.ts - index.js
``` ```
## Organisation des feuilles de styles ## Organisation des feuilles de styles

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 ###### Create a new bundle
::: title
Warning
:::
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 1. Prepare a fresh installation of the chill project, in a new directory
mkdir -p src/Bundle/ChillSomeBundle/src/config 2. Create a new bundle in this project, in the src directory
mkdir -p src/Bundle/ChillSomeBundle/src/Controller 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 TODO
<?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
```

View File

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

View File

@@ -9,7 +9,7 @@
"social worker" "social worker"
], ],
"require": { "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", "zimbra-api/soap-api": "^3.2.2",
"psr/http-client": "^1.0", "psr/http-client": "^1.0",
"nyholm/psr7": "^1.0" "nyholm/psr7": "^1.0"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,9 @@
:value="issue" :value="issue"
/> />
<label class="form-check-label" :for="issue.id"> <label class="form-check-label" :for="issue.id">
<span class="badge bg-chill-l-gray text-dark">{{ issue.text }}</span> <span class="badge bg-chill-l-gray text-dark">{{
issue.text
}}</span>
</label> </label>
</div> </div>
</span> </span>
@@ -45,8 +47,10 @@ span.badge {
word-wrap: break-word; word-wrap: break-word;
word-break: break-word; word-break: break-word;
display: inline-block; display: inline-block;
max-width: 100%;margin-bottom: 5px; max-width: 100%;
margin-right: 1em;text-align: left; margin-bottom: 5px;
margin-right: 1em;
text-align: left;
&::before { &::before {
position: absolute; position: absolute;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,9 +28,6 @@
{% if calendar.status == 'canceled' %} {% if calendar.status == 'canceled' %}
<del> <del>
{% endif %} {% endif %}
{% if calendar.status == 'canceled' %}
<del>
{% endif %}
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %} {% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
{{ calendar.startDate|format_datetime('short', 'short') }} {{ calendar.startDate|format_datetime('short', 'short') }}
- {{ calendar.endDate|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 } ) }}" <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> class="btn btn-action"><i class="bi bi-x-circle"></i> {{ 'Cancel'|trans }}</a>
</li> </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 %} {% endif %}
{% if is_granted('CHILL_CALENDAR_CALENDAR_DELETE', calendar) %} {% if is_granted('CHILL_CALENDAR_CALENDAR_DELETE', calendar) %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -145,7 +145,9 @@ async function download_info_link(
function build_wopi_editor_link(uuid: string, returnPath?: string) { function build_wopi_editor_link(uuid: string, returnPath?: string) {
if (returnPath === undefined) { if (returnPath === undefined) {
returnPath = returnPath =
window.location.pathname + window.location.search + window.location.hash; window.location.pathname +
window.location.search +
window.location.hash;
} }
return ( return (
@@ -175,6 +177,8 @@ async function download_and_decrypt_doc(
throw new Error("no version associated to stored object"); throw new Error("no version associated to stored object");
} }
// sometimes, the downloadInfo may be embedded into the storedObject
console.log("storedObject", storedObject);
let downloadInfo; let downloadInfo;
if ( if (
typeof storedObject._links !== "undefined" && typeof storedObject._links !== "undefined" &&
@@ -182,7 +186,10 @@ async function download_and_decrypt_doc(
) { ) {
downloadInfo = storedObject._links.downloadLink; downloadInfo = storedObject._links.downloadLink;
} else { } else {
downloadInfo = await download_info_link(storedObject, atVersionToDownload); downloadInfo = await download_info_link(
storedObject,
atVersionToDownload,
);
} }
const rawResponse = await window.fetch(downloadInfo.url); const rawResponse = await window.fetch(downloadInfo.url);
@@ -237,7 +244,10 @@ async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob> {
} }
if (storedObject.currentVersion?.type === "application/pdf") { if (storedObject.currentVersion?.type === "application/pdf") {
return download_and_decrypt_doc(storedObject, storedObject.currentVersion); return download_and_decrypt_doc(
storedObject,
storedObject.currentVersion,
);
} }
const convertLink = build_convert_link(storedObject.uuid); const convertLink = build_convert_link(storedObject.uuid);
@@ -267,7 +277,6 @@ async function is_object_ready(
export { export {
build_convert_link, build_convert_link,
build_wopi_editor_link, build_wopi_editor_link,
download_info_link,
download_and_decrypt_doc, download_and_decrypt_doc,
download_doc, download_doc,
download_doc_as_pdf, 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()]), 'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, [...$context, UserNormalizer::AT_DATE => $object->getCreatedAt()]),
]; ];
$normalizationGroups = $context[AbstractNormalizer::GROUPS] ?? []; if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
if (is_string($normalizationGroups)) {
$normalizationGroups = [$normalizationGroups];
}
if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $normalizationGroups, true)) {
$data['point-in-times'] = $this->normalizer->normalize($object->getPointInTimes(), $format, $context); $data['point-in-times'] = $this->normalizer->normalize($object->getPointInTimes(), $format, $context);
} }
if (in_array(self::WITH_RESTORED_CONTEXT, $normalizationGroups, true)) { if (in_array(self::WITH_RESTORED_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
$data['from-restored'] = $this->normalizer->normalize($object->getCreatedFrom(), $format, [AbstractNormalizer::GROUPS => ['read']]); $data['from-restored'] = $this->normalizer->normalize($object->getCreatedFrom(), $format, [AbstractNormalizer::GROUPS => ['read']]);
} }

View File

@@ -19,6 +19,7 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\Address;
use libphonenumber\PhoneNumber; use libphonenumber\PhoneNumber;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
/** /**
* Immersion. * Immersion.
@@ -85,14 +86,14 @@ class Immersion implements \Stringable
* @Assert\NotBlank() * @Assert\NotBlank()
*/ */
#[ORM\Column(name: 'tuteurPhoneNumber', type: 'phone_number', nullable: true)] #[ORM\Column(name: 'tuteurPhoneNumber', type: 'phone_number', nullable: true)]
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber] #[PhonenumberConstraint(type: 'any')]
private ?PhoneNumber $tuteurPhoneNumber = null; private ?PhoneNumber $tuteurPhoneNumber = null;
#[ORM\Column(name: 'structureAccName', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)] #[ORM\Column(name: 'structureAccName', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
private ?string $structureAccName = null; private ?string $structureAccName = null;
#[ORM\Column(name: 'structureAccPhonenumber', type: 'phone_number', nullable: true)] #[ORM\Column(name: 'structureAccPhonenumber', type: 'phone_number', nullable: true)]
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber] #[PhonenumberConstraint(type: 'any')]
private ?PhoneNumber $structureAccPhonenumber = null; private ?PhoneNumber $structureAccPhonenumber = null;
#[ORM\Column(name: 'structureAccEmail', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)] #[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\Cron\CronJobInterface;
use Chill\MainBundle\CRUD\CompilerPass\CRUDControllerCompilerPass; use Chill\MainBundle\CRUD\CompilerPass\CRUDControllerCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass;
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass; 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 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 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 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 ACLFlagsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new CRUDControllerCompilerPass(), \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\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Repository\LocationRepository; use Chill\MainBundle\Repository\LocationRepository;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use libphonenumber\PhoneNumber; use libphonenumber\PhoneNumber;
use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber as MisdPhoneNumberConstraint;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
@@ -67,12 +67,12 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
#[Serializer\Groups(['read', 'write', 'docgen:read'])] #[Serializer\Groups(['read', 'write', 'docgen:read'])]
#[ORM\Column(type: 'phone_number', nullable: true)] #[ORM\Column(type: 'phone_number', nullable: true)]
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::ANY])] #[PhonenumberConstraint(type: 'any')]
private ?PhoneNumber $phonenumber1 = null; private ?PhoneNumber $phonenumber1 = null;
#[Serializer\Groups(['read', 'write', 'docgen:read'])] #[Serializer\Groups(['read', 'write', 'docgen:read'])]
#[ORM\Column(type: 'phone_number', nullable: true)] #[ORM\Column(type: 'phone_number', nullable: true)]
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::ANY])] #[PhonenumberConstraint(type: 'any')]
private ?PhoneNumber $phonenumber2 = null; private ?PhoneNumber $phonenumber2 = null;
#[Serializer\Groups(['read'])] #[Serializer\Groups(['read'])]

View File

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

View File

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

View File

@@ -31,8 +31,6 @@ interface PhoneNumberHelperInterface
/** /**
* Return true if the validation is configured and available. * 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; public function isPhonenumberValidationConfigured(): bool;

View File

@@ -76,24 +76,6 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']); ->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']);
} }
/**
* @throws NumberParseException
*/
public function parse(string $phoneNumber): PhoneNumber
{
$sanitizedPhoneNumber = $phoneNumber;
if (str_starts_with($sanitizedPhoneNumber, '00')) {
$sanitizedPhoneNumber = '+'.substr($sanitizedPhoneNumber, 2, null);
}
if (!str_starts_with($sanitizedPhoneNumber, '+') && !str_starts_with($sanitizedPhoneNumber, '0')) {
$sanitizedPhoneNumber = '+'.$sanitizedPhoneNumber;
}
return $this->phoneNumberUtil->parse($sanitizedPhoneNumber, $this->config['default_carrier_code']);
}
/** /**
* Get type (mobile, landline, ...) for phone number. * Get type (mobile, landline, ...) for phone number.
*/ */
@@ -122,7 +104,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
*/ */
public function isValidPhonenumberAny($phonenumber): bool public function isValidPhonenumberAny($phonenumber): bool
{ {
if (false === $this->isConfigured) { if (false === $this->isPhonenumberValidationConfigured()) {
return true; return true;
} }
$validation = $this->performTwilioLookup($phonenumber); $validation = $this->performTwilioLookup($phonenumber);
@@ -142,7 +124,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
*/ */
public function isValidPhonenumberLandOrVoip($phonenumber): bool public function isValidPhonenumberLandOrVoip($phonenumber): bool
{ {
if (false === $this->isConfigured) { if (false === $this->isPhonenumberValidationConfigured()) {
return true; return true;
} }
@@ -163,7 +145,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
*/ */
public function isValidPhonenumberMobile($phonenumber): bool public function isValidPhonenumberMobile($phonenumber): bool
{ {
if (false === $this->isConfigured) { if (false === $this->isPhonenumberValidationConfigured()) {
return true; return true;
} }
@@ -178,7 +160,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
private function performTwilioLookup($phonenumber) private function performTwilioLookup($phonenumber)
{ {
if (false === $this->isConfigured) { if (false === $this->isPhonenumberValidationConfigured()) {
return null; return null;
} }

View File

@@ -166,18 +166,3 @@ export const intervalISOToDays = (str: string | null): number | null => {
return days; 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-unlink,
&.btn-action, &.btn-action,
&.btn-edit, &.btn-edit,
&.btn-tpchild,
&.btn-wopilink, &.btn-wopilink,
&.btn-update { &.btn-update {
&, &:hover { &, &:hover {
@@ -81,6 +82,7 @@ $chill-theme-buttons: (
&.btn-remove::before, &.btn-remove::before,
&.btn-choose::before, &.btn-choose::before,
&.btn-notify::before, &.btn-notify::before,
&.btn-tpchild::before,
&.btn-download::before, &.btn-download::before,
&.btn-search::before, &.btn-search::before,
&.btn-cancel::before { &.btn-cancel::before {
@@ -110,6 +112,7 @@ $chill-theme-buttons: (
&.btn-choose::before { content: "\f00c"; } // fa-check // f046 fa-check-square-o &.btn-choose::before { content: "\f00c"; } // fa-check // f046 fa-check-square-o
&.btn-unlink::before { content: "\f127"; } // fa-chain-broken &.btn-unlink::before { content: "\f127"; } // fa-chain-broken
&.btn-notify::before { content: "\f1d8"; } // fa-paper-plane &.btn-notify::before { content: "\f1d8"; } // fa-paper-plane
&.btn-tpchild::before { content: "\f007"; } // fa-user
&.btn-download::before { content: "\f019"; } // fa-download &.btn-download::before { content: "\f019"; } // fa-download
&.btn-search::before { content: "\f002"; } // fa-search &.btn-search::before { content: "\f002"; } // fa-search
} }

View File

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

View File

@@ -1,26 +1,15 @@
import { import { Scope } from "../../types";
DynamicKeys,
Scope,
ValidationExceptionInterface,
ValidationProblemFromMap,
ViolationFromMap
} from "../../types";
export type body = Record<string, boolean | string | number | null>; export type body = Record<string, boolean | string | number | null>;
export type fetchOption = 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 type Params = Record<string, number | string>;
export interface Pagination {
first: number;
items_per_page: number;
more: boolean;
next: string | null;
previous: string | null;
}
export interface PaginationResponse<T> { export interface PaginationResponse<T> {
pagination: Pagination; pagination: {
more: boolean;
items_per_page: number;
};
results: T[]; results: T[];
count: number; count: number;
} }
@@ -31,115 +20,19 @@ export interface TransportExceptionInterface {
name: string; name: string;
} }
export class ValidationException< export interface ValidationExceptionInterface extends TransportExceptionInterface {
M extends Record<string, Record<string, string|number>> = Record< name: "ValidationException";
string, error: object;
Record<string, string|number> violations: string[];
>, titles: string[];
> propertyPaths: string[];
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 ValidationErrorResponse extends TransportExceptionInterface {
* Check that the exception is a ValidationExceptionInterface violations: {
* @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;
title: string; title: string;
violations: { propertyPath: string; title: string }[]; propertyPath: 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",
)
);
} }
export interface AccessExceptionInterface extends TransportExceptionInterface { export interface AccessExceptionInterface extends TransportExceptionInterface {
@@ -147,8 +40,7 @@ export interface AccessExceptionInterface extends TransportExceptionInterface {
violations: string[]; violations: string[];
} }
export interface NotFoundExceptionInterface export interface NotFoundExceptionInterface extends TransportExceptionInterface {
extends TransportExceptionInterface {
name: "NotFoundException"; name: "NotFoundException";
} }
@@ -159,158 +51,18 @@ export interface ServerExceptionInterface extends TransportExceptionInterface {
body: string; body: string;
} }
export interface ConflictHttpExceptionInterface export interface ConflictHttpExceptionInterface extends TransportExceptionInterface {
extends TransportExceptionInterface {
name: "ConflictHttpException"; name: "ConflictHttpException";
violations: string[]; 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 * This method is suitable make a single fetch. When performing a GET to fetch a list of elements, always consider pagination
* - Performs a single HTTP request using fetch and returns the parsed JSON as Output. * and use of the @link{fetchResults} method.
* - 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).
*/ */
export const makeFetch = async < export const makeFetch = <Input, Output>(
Input,
Output,
M extends Record<string, Record<string, string|number>> = Record<
string,
Record<string, string|number>
>,
>(
method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE", method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE",
url: string, url: string,
body?: body | Input | null, body?: body | Input | null,
@@ -330,8 +82,7 @@ export const makeFetch = async <
if (typeof options !== "undefined") { if (typeof options !== "undefined") {
opts = Object.assign(opts, options); opts = Object.assign(opts, options);
} }
return fetch(url, opts).then((response) => {
return fetch(url, opts).then(async (response) => {
if (response.status === 204) { if (response.status === 204) {
return Promise.resolve(); return Promise.resolve();
} }
@@ -341,20 +92,9 @@ export const makeFetch = async <
} }
if (response.status === 422) { if (response.status === 422) {
// Unprocessable Entity -> payload de validation Symfony return response.json().then((response) => {
const json = await response.json().catch(() => undefined); throw ValidationException(response);
});
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;
} }
if (response.status === 403) { if (response.status === 403) {
@@ -419,6 +159,12 @@ function _fetchAction<T>(
throw NotFoundException(response); throw NotFoundException(response);
} }
if (response.status === 422) {
return response.json().then((response) => {
throw ValidationException(response);
});
}
if (response.status === 403) { if (response.status === 403) {
throw AccessException(response); throw AccessException(response);
} }
@@ -477,6 +223,24 @@ export const fetchScopes = (): Promise<Scope[]> => {
return fetchResults("/api/1.0/main/scope.json"); 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 // eslint-disable-next-line @typescript-eslint/no-unused-vars
const AccessException = (response: Response): AccessExceptionInterface => { const AccessException = (response: Response): AccessExceptionInterface => {
const error = {} as 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 { TranslatableString } from "ChillMainAssets/types";
import { getLocale } from "translator";
/** /**
* Localizes a translatable string object based on the current locale. * Localizes a translatable string object based on the current locale.
@@ -9,25 +8,6 @@ import { getLocale } from "translator";
* @param locale defaults to browser locale * @param locale defaults to browser locale
* @returns The localized string or null if no translation is available * @returns The localized string or null if no translation is available
*/ */
/**
* Prepends the current HTML lang code to the given URL.
* Example: If lang="fr" and url="/about", returns "/fr/about"
*
* @param url The URL to localize
* @returns The localized URL
*/
export function localizedUrl(url: string): string {
const 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( export function localizeString(
translatableString: TranslatableString | null | undefined, translatableString: TranslatableString | null | undefined,
locale?: string, locale?: string,
@@ -36,7 +16,7 @@ export function localizeString(
return ""; return "";
} }
const currentLocale = locale || getLocale(); const currentLocale = locale || navigator.language.split("-")[0] || "fr";
if (translatableString[currentLocale]) { if (translatableString[currentLocale]) {
return translatableString[currentLocale]; return translatableString[currentLocale];
@@ -59,47 +39,3 @@ export function localizeString(
return ""; 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 === -1 ===
this.suggested.findIndex( this.suggested.findIndex(
(e) => e.type === entity.type && e.id === entity.id, (e) => e.type === entity.type && e.id === entity.id,
) && )
"me" !== entity
) { ) {
this.suggested.push(entity); this.suggested.push(entity);
} }

View File

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

View File

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

View File

@@ -3,8 +3,6 @@ import {
isGenericDocWithStoredObject, isGenericDocWithStoredObject,
} from "ChillDocStoreAssets/types/generic_doc"; } from "ChillDocStoreAssets/types/generic_doc";
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types"; 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"; import { Person } from "../../../ChillPersonBundle/Resources/public/types";
export interface DateTime { export interface DateTime {
@@ -12,63 +10,9 @@ export interface DateTime {
datetime8601: string; 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 { 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; id: number;
// TODO
} }
export interface Job { export interface Job {
@@ -83,18 +27,6 @@ export interface Center {
id: number; id: number;
type: "center"; type: "center";
name: string; 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 { export interface Scope {
@@ -186,13 +118,6 @@ export interface Address {
isNoAddress: boolean; isNoAddress: boolean;
} }
/**
* Associate an existing address in write operations.
*/
export interface SetAddress {
id: number;
}
export interface AddressWithPoint extends Address { export interface AddressWithPoint extends Address {
point: Point; point: Point;
} }
@@ -366,156 +291,6 @@ export interface PrivateCommentEmbeddable {
comments: Record<number, string>; 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. * Possible states for the WaitingScreen Component.
*/ */

View File

@@ -24,7 +24,9 @@
{{ trans(getTextTitle) }} {{ trans(getTextTitle) }}
<span v-if="flag.loading" class="loading"> <span v-if="flag.loading" class="loading">
<i class="fa fa-circle-o-notch fa-spin fa-fw" /> <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> </span>
</h2> </h2>
</template> </template>
@@ -88,7 +90,9 @@
{{ trans(getTextTitle) }} {{ trans(getTextTitle) }}
<span v-if="flag.loading" class="loading"> <span v-if="flag.loading" class="loading">
<i class="fa fa-circle-o-notch fa-spin fa-fw" /> <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> </span>
</h2> </h2>
</template> </template>
@@ -144,7 +148,10 @@
</template> </template>
<template #action> <template #action>
<li v-if="!this.context.edit && this.useDatePane"> <li v-if="!this.context.edit && this.useDatePane">
<button class="btn btn-update change-icon" @click="closeEditPane"> <button
class="btn btn-update change-icon"
@click="closeEditPane"
>
{{ trans(NEXT) }} {{ trans(NEXT) }}
<i class="fa fa-fw fa-arrow-right" /> <i class="fa fa-fw fa-arrow-right" />
</button> </button>
@@ -171,7 +178,9 @@
{{ trans(getTextTitle) }} {{ trans(getTextTitle) }}
<span v-if="flag.loading" class="loading"> <span v-if="flag.loading" class="loading">
<i class="fa fa-circle-o-notch fa-spin fa-fw" /> <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> </span>
</h2> </h2>
</template> </template>
@@ -385,7 +394,8 @@ export default {
getTextTitle() { getTextTitle() {
if ( if (
typeof this.options.title !== "undefined" && typeof this.options.title !== "undefined" &&
(this.options.title.edit !== null || this.options.title.create !== null) (this.options.title.edit !== null ||
this.options.title.create !== null)
) { ) {
console.log("this.options.title", this.options.title); console.log("this.options.title", this.options.title);
@@ -505,7 +515,10 @@ export default {
if (!this.context.edit) { if (!this.context.edit) {
this.context.edit = true; this.context.edit = true;
this.context.addressId = params.addressId; this.context.addressId = params.addressId;
console.log("context is now edit, with address", params.addressId); console.log(
"context is now edit, with address",
params.addressId,
);
} }
} }
}, },
@@ -632,7 +645,9 @@ export default {
? this.entity.address.confidential ? this.entity.address.confidential
: false; : false;
this.entity.selected.isNoAddress = this.entity.selected.isNoAddress =
this.context.edit && this.entity.address.text === "" ? true : false; this.context.edit && this.entity.address.text === ""
? true
: false;
this.entity.selected.country = this.context.edit this.entity.selected.country = this.context.edit
? this.entity.address.country ? this.entity.address.country
@@ -727,7 +742,8 @@ export default {
// add the address reference, if any // add the address reference, if any
if (this.entity.selected.address.addressReference !== undefined) { if (this.entity.selected.address.addressReference !== undefined) {
newAddress = Object.assign(newAddress, { newAddress = Object.assign(newAddress, {
addressReference: this.entity.selected.address.addressReference, addressReference:
this.entity.selected.address.addressReference,
}); });
} else { } else {
newAddress = Object.assign(newAddress, { newAddress = Object.assign(newAddress, {
@@ -747,7 +763,10 @@ export default {
}); });
} }
if (this.validTo && null !== this.entity.selected.valid.to) { if (this.validTo && null !== this.entity.selected.valid.to) {
console.log("add validTo in fetch body", this.entity.selected.valid.to); console.log(
"add validTo in fetch body",
this.entity.selected.valid.to,
);
newAddress = Object.assign(newAddress, { newAddress = Object.assign(newAddress, {
validTo: { validTo: {
datetime: `${this.entity.selected.valid.to.toISOString().split("T")[0]}T00:00:00+0100`, datetime: `${this.entity.selected.valid.to.toISOString().split("T")[0]}T00:00:00+0100`,
@@ -759,7 +778,10 @@ export default {
newPostcode = Object.assign(newPostcode, { newPostcode = Object.assign(newPostcode, {
country: { id: this.entity.selected.country.id }, country: { id: this.entity.selected.country.id },
}); //TODO why not assign postcodeBody here = Object.assign(postcodeBody, {'origin': 3}); ? }); //TODO why not assign postcodeBody here = Object.assign(postcodeBody, {'origin': 3}); ?
console.log("writeNew postcode is true! newPostcode: ", newPostcode); console.log(
"writeNew postcode is true! newPostcode: ",
newPostcode,
);
newAddress = Object.assign(newAddress, { newAddress = Object.assign(newAddress, {
newPostcode: newPostcode, newPostcode: newPostcode,
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,10 @@
<div class="address-form"> <div class="address-form">
<!-- Not display in modal --> <!-- Not display in modal -->
<div v-if="insideModal === false" class="loading"> <div v-if="insideModal === false" class="loading">
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw" /> <i
v-if="flag.loading"
class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"
/>
<span class="sr-only">Loading...</span> <span class="sr-only">Loading...</span>
</div> </div>

View File

@@ -1,7 +1,10 @@
<template> <template>
<div v-if="!onlyButton" class="mt-4 flex-grow-1"> <div v-if="!onlyButton" class="mt-4 flex-grow-1">
<div class="loading"> <div class="loading">
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw" /> <i
v-if="flag.loading"
class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"
/>
<span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span> <span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span>
</div> </div>
@@ -11,7 +14,9 @@
<div v-if="flag.success" class="alert alert-success"> <div v-if="flag.success" class="alert alert-success">
{{ trans(getSuccessText) }} {{ trans(getSuccessText) }}
<span v-if="forceRedirect">{{ trans(ADDRESS_WAIT_REDIRECTION) }}</span> <span v-if="forceRedirect">{{
trans(ADDRESS_WAIT_REDIRECTION)
}}</span>
</div> </div>
<div <div
@@ -42,7 +47,9 @@
name="button" name="button"
:title="trans(getTextButton)" :title="trans(getTextButton)"
> >
<span v-if="displayTextButton">{{ trans(getTextButton) }}</span> <span v-if="displayTextButton">{{
trans(getTextButton)
}}</span>
</button> </button>
</template> </template>
</action-buttons> </action-buttons>
@@ -55,7 +62,9 @@
:use-date-pane="useDatePane" :use-date-pane="useDatePane"
/> />
<div v-if="this.context.target.name === 'household' || this.context.edit"> <div
v-if="this.context.target.name === 'household' || this.context.edit"
>
<action-buttons :options="this.options" :defaultz="this.defaultz"> <action-buttons :options="this.options" :defaultz="this.defaultz">
<template #action> <template #action>
<button <button
@@ -66,7 +75,9 @@
name="button" name="button"
:title="trans(getTextButton)" :title="trans(getTextButton)"
> >
<span v-if="displayTextButton">{{ trans(getTextButton) }}</span> <span v-if="displayTextButton">{{
trans(getTextButton)
}}</span>
</button> </button>
</template> </template>
</action-buttons> </action-buttons>
@@ -88,7 +99,9 @@
name="button" name="button"
:title="trans(getTextButton)" :title="trans(getTextButton)"
> >
<span v-if="displayTextButton">{{ trans(getTextButton) }}</span> <span v-if="displayTextButton">{{
trans(getTextButton)
}}</span>
</button> </button>
</template> </template>
</action-buttons> </action-buttons>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,8 +11,11 @@
</table> </table>
</template> </template>
<script lang="ts" setup> <script>
// Pas de props à définir, composant slot simple export default {
name: "TabTable",
props: [],
};
</script> </script>
<style scoped></style> <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 "es6-promise/auto";
import { Module } from "vuex"; import { createStore } from "vuex";
import { import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
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";
export interface TasksState { const debug = process.env.NODE_ENV !== "production";
warning: PaginationResponse<Warning>;
alert: PaginationResponse<Alert>;
}
export interface State { const isEmpty = (obj) => {
evaluations: PaginationResponse<Evaluation>; return (
tasks: TasksState; obj &&
accompanyingCourses: PaginationResponse<AccompanyingCourse>; Object.keys(obj).length <= 1 &&
notifications: PaginationResponse<Notification>; Object.getPrototypeOf(obj) === Object.prototype
workflows: PaginationResponse<Workflow>; );
workflowsCc: PaginationResponse<WorflowCc>; };
errorMsg: unknown[];
loading: boolean;
ticketsLoading: boolean;
}
export const moduleHomepage: Module<State, RootState> = { const store = createStore({
strict: debug,
state: { state: {
evaluations: { // works: {},
count: 0, evaluations: {},
} as PaginationResponse<Evaluation>,
tasks: { tasks: {
warning: { warning: {},
count: 0, alert: {},
} as PaginationResponse<Warning>,
alert: {
count: 0,
} as PaginationResponse<Alert>,
}, },
accompanyingCourses: { accompanyingCourses: {},
count: 0, notifications: {},
} as PaginationResponse<AccompanyingCourse>, workflows: {},
notifications: { workflowsCc: {},
count: 0,
} as PaginationResponse<Notification>,
workflows: {
count: 0,
} as PaginationResponse<Workflow>,
workflowsCc: {
count: 0,
} as PaginationResponse<WorflowCc>,
ticketsLoading: false,
errorMsg: [], errorMsg: [],
loading: false, loading: false,
}, },
getters: { getters: {
isTicketLoading: (state) => { // isWorksLoaded(state) {
return state.ticketsLoading; // return !isEmpty(state.works);
}, // },
isEvaluationsLoaded(state) { isEvaluationsLoaded(state) {
return Array.isArray(state.evaluations.results); return !isEmpty(state.evaluations);
}, },
isTasksWarningLoaded(state) { isTasksWarningLoaded(state) {
return Array.isArray(state.tasks.warning.results); return !isEmpty(state.tasks.warning);
}, },
isTasksAlertLoaded(state) { isTasksAlertLoaded(state) {
return Array.isArray(state.tasks.alert.results); return !isEmpty(state.tasks.alert);
}, },
isAccompanyingCoursesLoaded(state) { isAccompanyingCoursesLoaded(state) {
return Array.isArray(state.accompanyingCourses.results); return !isEmpty(state.accompanyingCourses);
}, },
isNotificationsLoaded(state) { isNotificationsLoaded(state) {
return Array.isArray(state.notifications.results); return !isEmpty(state.notifications);
}, },
isWorkflowsLoaded(state) { 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: { mutations: {
// addWorks(state, works) {
// //console.log('addWorks', works);
// state.works = works;
// },
addEvaluations(state, evaluations) { addEvaluations(state, evaluations) {
//console.log('addEvaluations', evaluations);
state.evaluations = evaluations; state.evaluations = evaluations;
}, },
addTasksWarning(state, tasks) { addTasksWarning(state, tasks) {
//console.log('addTasksWarning', tasks);
state.tasks.warning = tasks; state.tasks.warning = tasks;
}, },
addTasksAlert(state, tasks) { addTasksAlert(state, tasks) {
//console.log('addTasksAlert', tasks);
state.tasks.alert = tasks; state.tasks.alert = tasks;
}, },
addCourses(state, courses) { addCourses(state, courses) {
//console.log('addCourses', courses);
state.accompanyingCourses = courses; state.accompanyingCourses = courses;
}, },
addNotifications(state, notifications) { addNotifications(state, notifications) {
//console.log('addNotifications', notifications);
state.notifications = notifications; state.notifications = notifications;
}, },
addWorkflows(state, workflows) { addWorkflows(state, workflows) {
@@ -110,30 +96,30 @@ export const moduleHomepage: Module<State, RootState> = {
setLoading(state, bool) { setLoading(state, bool) {
state.loading = bool; state.loading = bool;
}, },
setTicketsLoading(state, bool) {
state.ticketsLoading = bool;
},
catchError(state, error) { catchError(state, error) {
state.errorMsg.push(error); state.errorMsg.push(error);
}, },
}, },
actions: { actions: {
async getByTab({ commit, getters, dispatch }, { tab, param }) { getByTab({ commit, getters }, { tab, param }) {
switch (tab) { switch (tab) {
case HomepageTabs.MyTickets: // case 'MyWorks':
if (!getters.isTicketsLoaded) { // if (!getters.isWorksLoaded) {
commit("setTicketsLoading", true); // commit('setLoading', true);
// Utilise l'action du module ticket_list // const url = `/api/1.0/person/accompanying-period/work/my-near-end${'?'+ param}`;
await dispatch( // makeFetch('GET', url)
"fetchTicketList", // .then((response) => {
{ byAddresseeToMe: true, byCurrentState: "open" }, // commit('addWorks', response);
{ root: true }, // commit('setLoading', false);
); // })
// .catch((error) => {
commit("setTicketsLoading", false); // commit('catchError', error);
} // throw error;
break; // })
case HomepageTabs.MyEvaluations: // ;
// }
// break;
case "MyEvaluations":
if (!getters.isEvaluationsLoaded) { if (!getters.isEvaluationsLoaded) {
commit("setLoading", true); commit("setLoading", true);
const url = `/api/1.0/person/accompanying-period/work/evaluation/my-near-end${"?" + param}`; 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; break;
case HomepageTabs.MyTasks: case "MyTasks":
if (!(getters.isTasksWarningLoaded && getters.isTasksAlertLoaded)) { if (!(getters.isTasksWarningLoaded && getters.isTasksAlertLoaded)) {
commit("setLoading", true); 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}`, 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; break;
case HomepageTabs.MyAccompanyingCourses: case "MyAccompanyingCourses":
if (!getters.isAccompanyingCoursesLoaded) { if (!getters.isAccompanyingCoursesLoaded) {
commit("setLoading", true); commit("setLoading", true);
const url = `/api/1.0/person/accompanying-course/list/by-recent-attributions${"?" + param}`; const url = `/api/1.0/person/accompanying-course/list/by-recent-attributions${"?" + param}`;
@@ -188,13 +174,13 @@ export const moduleHomepage: Module<State, RootState> = {
}); });
} }
break; break;
case HomepageTabs.MyNotifications: case "MyNotifications":
if (!getters.isNotificationsLoaded) { if (!getters.isNotificationsLoaded) {
commit("setLoading", true); commit("setLoading", true);
const url = `/api/1.0/main/notification/my/unread${"?" + param}`; const url = `/api/1.0/main/notification/my/unread${"?" + param}`;
makeFetch("GET", url) makeFetch("GET", url)
.then((response) => { .then((response) => {
console.log("notifications", response);
commit("addNotifications", response); commit("addNotifications", response);
commit("setLoading", false); commit("setLoading", false);
}) })
@@ -204,7 +190,7 @@ export const moduleHomepage: Module<State, RootState> = {
}); });
} }
break; break;
case HomepageTabs.MyWorkflows: case "MyWorkflows":
if (!getters.isWorflowsLoaded) { if (!getters.isWorflowsLoaded) {
commit("setLoading", true); commit("setLoading", true);
makeFetch("GET", "/api/1.0/main/workflow/my") 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