Revert "Merge branch 'ticket/supplementary-comments-on-motive' into 'master'"

This reverts merge request !855
This commit is contained in:
2025-07-20 18:50:33 +00:00
parent 5f01673404
commit e3a6b60fa2
392 changed files with 24023 additions and 35435 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

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

File diff suppressed because it is too large Load Diff

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -79,12 +79,12 @@
"dev": "encore dev", "dev": "encore dev",
"watch": "encore dev --watch", "watch": "encore dev --watch",
"build": "encore production --progress", "build": "encore production --progress",
"specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml src/Bundle/ChillTicketBundle/chill.api.specs.yaml> templates/api/specs.yaml", "specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml> templates/api/specs.yaml",
"specs-validate": "swagger-cli validate templates/api/specs.yaml", "specs-validate": "swagger-cli validate templates/api/specs.yaml",
"specs-create-dir": "mkdir -p templates/api", "specs-create-dir": "mkdir -p templates/api",
"specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate", "specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate",
"version": "node --version", "version": "node --version",
"eslint": "eslint-baseline --fix \"src/**/*.{js,ts,vue}\"" "eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
}, },
"private": true "private": true
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import { State } from "./../index"; import { State } from "./../index";
import { ActionContext, Module } from "vuex"; import { ActionContext, Module } from "vuex";
import { import {
CalendarRange, CalendarRange,
CalendarRangeCreate, CalendarRangeCreate,
CalendarRangeEdit, CalendarRangeEdit,
isEventInputCalendarRange, isEventInputCalendarRange,
} from "../../../../types"; } from "../../../../types";
import { Location } from "../../../../../../../ChillMainBundle/Resources/public/types"; import { Location } from "../../../../../../../ChillMainBundle/Resources/public/types";
import { fetchCalendarRangeForUser } from "../../../Calendar/api"; import { fetchCalendarRangeForUser } from "../../../Calendar/api";
@@ -12,332 +12,369 @@ import { calendarRangeToFullCalendarEvent } from "../../../Calendar/store/utils"
import { EventInput } from "@fullcalendar/core"; import { EventInput } from "@fullcalendar/core";
import { makeFetch } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods"; import { makeFetch } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import { import {
datetimeToISO, datetimeToISO,
dateToISO, dateToISO,
ISOToDatetime, ISOToDatetime,
} from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date"; } from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
import type { EventInputCalendarRange } from "../../../../types"; import type { EventInputCalendarRange } from "../../../../types";
export interface CalendarRangesState { export interface CalendarRangesState {
ranges: (EventInput | EventInputCalendarRange)[]; ranges: (EventInput | EventInputCalendarRange)[];
rangesLoaded: { start: number; end: number }[]; rangesLoaded: { start: number; end: number }[];
rangesIndex: Set<string>; rangesIndex: Set<string>;
key: number; key: number;
} }
type Context = ActionContext<CalendarRangesState, State>; type Context = ActionContext<CalendarRangesState, State>;
export default { export default {
namespaced: true, namespaced: true,
state: (): CalendarRangesState => ({ state: (): CalendarRangesState => ({
ranges: [], ranges: [],
rangesLoaded: [], rangesLoaded: [],
rangesIndex: new Set<string>(), rangesIndex: new Set<string>(),
key: 0, key: 0,
}), }),
getters: { getters: {
isRangeLoaded: isRangeLoaded:
(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 (
return true; start.getTime() === range.start &&
} end.getTime() === range.end
} ) {
return true;
}
}
return false; return false;
}, },
getRangesOnDate: getRangesOnDate:
(state: CalendarRangesState) => (state: CalendarRangesState) =>
(date: Date): EventInputCalendarRange[] => { (date: Date): EventInputCalendarRange[] => {
const founds = []; const founds = [];
const dateStr = dateToISO(date) as string; const dateStr = dateToISO(date) as string;
for (const range of state.ranges) { for (const range of state.ranges) {
if ( if (
isEventInputCalendarRange(range) && isEventInputCalendarRange(range) &&
range.start.startsWith(dateStr) range.start.startsWith(dateStr)
) { ) {
founds.push(range); founds.push(range);
} }
} }
return founds; return founds;
}, },
getRangesOnWeek: getRangesOnWeek:
(state: CalendarRangesState) => (state: CalendarRangesState) =>
(mondayDate: Date): EventInputCalendarRange[] => { (mondayDate: Date): EventInputCalendarRange[] => {
const founds = []; const founds = [];
for (const d of Array.from(Array(7).keys())) { for (const d of Array.from(Array(7).keys())) {
const dateOfWeek = new Date(mondayDate); const dateOfWeek = new Date(mondayDate);
dateOfWeek.setDate(mondayDate.getDate() + d); dateOfWeek.setDate(mondayDate.getDate() + d);
const dateStr = dateToISO(dateOfWeek) as string; const dateStr = dateToISO(dateOfWeek) as string;
for (const range of state.ranges) { for (const range of state.ranges) {
if ( if (
isEventInputCalendarRange(range) && isEventInputCalendarRange(range) &&
range.start.startsWith(dateStr) range.start.startsWith(dateStr)
) { ) {
founds.push(range); founds.push(range);
}
}
}
return founds;
},
},
mutations: {
addRanges(state: CalendarRangesState, ranges: CalendarRange[]) {
const toAdd = ranges
.map((cr) => calendarRangeToFullCalendarEvent(cr))
.map((cr) => ({
...cr,
backgroundColor: "white",
borderColor: "#3788d8",
textColor: "black",
}))
.filter((r) => !state.rangesIndex.has(r.id));
toAdd.forEach((r) => {
state.rangesIndex.add(r.id);
state.ranges.push(r);
});
state.key = state.key + toAdd.length;
},
addExternals(
state: CalendarRangesState,
externalEvents: (EventInput & { id: string })[],
) {
const toAdd = externalEvents.filter(
(r) => !state.rangesIndex.has(r.id),
);
toAdd.forEach((r) => {
state.rangesIndex.add(r.id);
state.ranges.push(r);
});
state.key = state.key + toAdd.length;
},
addLoaded(
state: CalendarRangesState,
payload: { start: Date; end: Date },
) {
state.rangesLoaded.push({
start: payload.start.getTime(),
end: payload.end.getTime(),
});
},
addRange(state: CalendarRangesState, payload: CalendarRange) {
const asEvent = calendarRangeToFullCalendarEvent(payload);
state.ranges.push({
...asEvent,
backgroundColor: "white",
borderColor: "#3788d8",
textColor: "black",
});
state.rangesIndex.add(asEvent.id);
state.key = state.key + 1;
},
removeRange(state: CalendarRangesState, calendarRangeId: number) {
const found = state.ranges.find(
(r) =>
r.calendarRangeId === calendarRangeId && r.is === "range",
);
if (found !== undefined) {
state.ranges = state.ranges.filter(
(r) =>
!(
r.calendarRangeId === calendarRangeId &&
r.is === "range"
),
);
if (typeof found.id === "string") {
// should always be true
state.rangesIndex.delete(found.id);
}
state.key = state.key + 1;
} }
}
}
return founds;
},
},
mutations: {
addRanges(state: CalendarRangesState, ranges: CalendarRange[]) {
const toAdd = ranges
.map((cr) => calendarRangeToFullCalendarEvent(cr))
.map((cr) => ({
...cr,
backgroundColor: "white",
borderColor: "#3788d8",
textColor: "black",
}))
.filter((r) => !state.rangesIndex.has(r.id));
toAdd.forEach((r) => {
state.rangesIndex.add(r.id);
state.ranges.push(r);
});
state.key = state.key + toAdd.length;
},
addExternals(
state: CalendarRangesState,
externalEvents: (EventInput & { id: string })[],
) {
const toAdd = externalEvents.filter((r) => !state.rangesIndex.has(r.id));
toAdd.forEach((r) => {
state.rangesIndex.add(r.id);
state.ranges.push(r);
});
state.key = state.key + toAdd.length;
},
addLoaded(state: CalendarRangesState, payload: { start: Date; end: Date }) {
state.rangesLoaded.push({
start: payload.start.getTime(),
end: payload.end.getTime(),
});
},
addRange(state: CalendarRangesState, payload: CalendarRange) {
const asEvent = calendarRangeToFullCalendarEvent(payload);
state.ranges.push({
...asEvent,
backgroundColor: "white",
borderColor: "#3788d8",
textColor: "black",
});
state.rangesIndex.add(asEvent.id);
state.key = state.key + 1;
},
removeRange(state: CalendarRangesState, calendarRangeId: number) {
const found = state.ranges.find(
(r) => r.calendarRangeId === calendarRangeId && r.is === "range",
);
if (found !== undefined) {
state.ranges = state.ranges.filter(
(r) => !(r.calendarRangeId === calendarRangeId && r.is === "range"),
);
if (typeof found.id === "string") {
// should always be true
state.rangesIndex.delete(found.id);
}
state.key = state.key + 1;
}
},
updateRange(state: CalendarRangesState, range: CalendarRange) {
const found = state.ranges.find(
(r) => r.calendarRangeId === range.id && r.is === "range",
);
const newEvent = calendarRangeToFullCalendarEvent(range);
if (found !== undefined) {
found.start = newEvent.start;
found.end = newEvent.end;
found.locationId = range.location.id;
found.locationName = range.location.name;
}
state.key = state.key + 1;
},
},
actions: {
fetchRanges(
ctx: Context,
payload: { start: Date; end: Date },
): Promise<null> {
const start = payload.start;
const end = payload.end;
if (ctx.rootGetters["me/getMe"] === null) {
return Promise.resolve(ctx.getters.getRangeSource);
}
if (ctx.getters.isRangeLoaded({ start, end })) {
return Promise.resolve(ctx.getters.getRangeSource);
}
ctx.commit("addLoaded", {
start: start,
end: end,
});
return fetchCalendarRangeForUser(
ctx.rootGetters["me/getMe"],
start,
end,
).then((ranges: CalendarRange[]) => {
ctx.commit("addRanges", ranges);
return Promise.resolve(null);
});
},
createRange(
ctx: Context,
{ start, end, location }: { start: Date; end: Date; location: Location },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range.json?`;
if (ctx.rootState.me.me === null) {
throw new Error("user is currently null");
}
const body = {
user: {
id: ctx.rootState.me.me.id,
type: "user",
}, },
startDate: { updateRange(state: CalendarRangesState, range: CalendarRange) {
datetime: datetimeToISO(start), const found = state.ranges.find(
(r) => r.calendarRangeId === range.id && r.is === "range",
);
const newEvent = calendarRangeToFullCalendarEvent(range);
if (found !== undefined) {
found.start = newEvent.start;
found.end = newEvent.end;
found.locationId = range.location.id;
found.locationName = range.location.name;
}
state.key = state.key + 1;
}, },
endDate: { },
datetime: datetimeToISO(end), actions: {
fetchRanges(
ctx: Context,
payload: { start: Date; end: Date },
): Promise<null> {
const start = payload.start;
const end = payload.end;
if (ctx.rootGetters["me/getMe"] === null) {
return Promise.resolve(ctx.getters.getRangeSource);
}
if (ctx.getters.isRangeLoaded({ start, end })) {
return Promise.resolve(ctx.getters.getRangeSource);
}
ctx.commit("addLoaded", {
start: start,
end: end,
});
return fetchCalendarRangeForUser(
ctx.rootGetters["me/getMe"],
start,
end,
).then((ranges: CalendarRange[]) => {
ctx.commit("addRanges", ranges);
return Promise.resolve(null);
});
}, },
location: { createRange(
id: location.id, ctx: Context,
type: "location", {
start,
end,
location,
}: { start: Date; end: Date; location: Location },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range.json?`;
if (ctx.rootState.me.me === null) {
throw new Error("user is currently null");
}
const body = {
user: {
id: ctx.rootState.me.me.id,
type: "user",
},
startDate: {
datetime: datetimeToISO(start),
},
endDate: {
datetime: datetimeToISO(end),
},
location: {
id: location.id,
type: "location",
},
} as CalendarRangeCreate;
return makeFetch<CalendarRangeCreate, CalendarRange>(
"POST",
url,
body,
)
.then((newRange) => {
ctx.commit("addRange", newRange);
return Promise.resolve(null);
})
.catch((error) => {
console.error(error);
throw error;
});
}, },
} as CalendarRangeCreate; deleteRange(ctx: Context, calendarRangeId: number) {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
return makeFetch<CalendarRangeCreate, CalendarRange>("POST", url, body) makeFetch<undefined, never>("DELETE", url).then(() => {
.then((newRange) => { ctx.commit("removeRange", calendarRangeId);
ctx.commit("addRange", newRange); });
return Promise.resolve(null);
})
.catch((error) => {
console.error(error);
throw error;
});
},
deleteRange(ctx: Context, calendarRangeId: number) {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
makeFetch<undefined, never>("DELETE", url).then(() => {
ctx.commit("removeRange", calendarRangeId);
});
},
patchRangeTime(
ctx,
{
calendarRangeId,
start,
end,
}: { calendarRangeId: number; start: Date; end: Date },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
const body = {
startDate: {
datetime: datetimeToISO(start),
}, },
endDate: { patchRangeTime(
datetime: datetimeToISO(end), ctx,
{
calendarRangeId,
start,
end,
}: { calendarRangeId: number; start: Date; end: Date },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
const body = {
startDate: {
datetime: datetimeToISO(start),
},
endDate: {
datetime: datetimeToISO(end),
},
} as CalendarRangeEdit;
return makeFetch<CalendarRangeEdit, CalendarRange>(
"PATCH",
url,
body,
)
.then((range) => {
ctx.commit("updateRange", range);
return Promise.resolve(null);
})
.catch((error) => {
console.error(error);
return Promise.resolve(null);
});
}, },
} as CalendarRangeEdit; patchRangeLocation(
ctx,
{
location,
calendarRangeId,
}: { location: Location; calendarRangeId: number },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
const body = {
location: {
id: location.id,
type: "location",
},
} as CalendarRangeEdit;
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body) return makeFetch<CalendarRangeEdit, CalendarRange>(
.then((range) => { "PATCH",
ctx.commit("updateRange", range); url,
return Promise.resolve(null); body,
}) )
.catch((error) => { .then((range) => {
console.error(error); ctx.commit("updateRange", range);
return Promise.resolve(null); return Promise.resolve(null);
}); })
}, .catch((error) => {
patchRangeLocation( console.error(error);
ctx, return Promise.resolve(null);
{ });
location,
calendarRangeId,
}: { location: Location; calendarRangeId: number },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
const body = {
location: {
id: location.id,
type: "location",
}, },
} as CalendarRangeEdit; copyFromDayToAnotherDay(
ctx,
{ from, to }: { from: Date; to: Date },
): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] =
ctx.getters["getRangesOnDate"](from);
const promises = [];
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body) for (const r of rangesToCopy) {
.then((range) => { const start = new Date(ISOToDatetime(r.start) as Date);
ctx.commit("updateRange", range); start.setFullYear(
return Promise.resolve(null); to.getFullYear(),
}) to.getMonth(),
.catch((error) => { to.getDate(),
console.error(error); );
return Promise.resolve(null); const end = new Date(ISOToDatetime(r.end) as Date);
}); end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
const location = ctx.rootGetters["locations/getLocationById"](
r.locationId,
);
promises.push(
ctx.dispatch("createRange", { start, end, location }),
);
}
return Promise.all(promises).then(() => Promise.resolve(null));
},
copyFromWeekToAnotherWeek(
ctx: Context,
{ fromMonday, toMonday }: { fromMonday: Date; toMonday: Date },
): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] =
ctx.getters["getRangesOnWeek"](fromMonday);
const promises = [];
const diffTime = toMonday.getTime() - fromMonday.getTime();
for (const r of rangesToCopy) {
const start = new Date(ISOToDatetime(r.start) as Date);
const end = new Date(ISOToDatetime(r.end) as Date);
start.setTime(start.getTime() + diffTime);
end.setTime(end.getTime() + diffTime);
const location = ctx.rootGetters["locations/getLocationById"](
r.locationId,
);
promises.push(
ctx.dispatch("createRange", { start, end, location }),
);
}
return Promise.all(promises).then(() => Promise.resolve(null));
},
}, },
copyFromDayToAnotherDay(
ctx,
{ from, to }: { from: Date; to: Date },
): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] =
ctx.getters["getRangesOnDate"](from);
const promises = [];
for (const r of rangesToCopy) {
const start = new Date(ISOToDatetime(r.start) as Date);
start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
const end = new Date(ISOToDatetime(r.end) as Date);
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
const location = ctx.rootGetters["locations/getLocationById"](
r.locationId,
);
promises.push(ctx.dispatch("createRange", { start, end, location }));
}
return Promise.all(promises).then(() => Promise.resolve(null));
},
copyFromWeekToAnotherWeek(
ctx: Context,
{ fromMonday, toMonday }: { fromMonday: Date; toMonday: Date },
): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] =
ctx.getters["getRangesOnWeek"](fromMonday);
const promises = [];
const diffTime = toMonday.getTime() - fromMonday.getTime();
for (const r of rangesToCopy) {
const start = new Date(ISOToDatetime(r.start) as Date);
const end = new Date(ISOToDatetime(r.end) as Date);
start.setTime(start.getTime() + diffTime);
end.setTime(end.getTime() + diffTime);
const location = ctx.rootGetters["locations/getLocationById"](
r.locationId,
);
promises.push(ctx.dispatch("createRange", { start, end, location }));
}
return Promise.all(promises).then(() => Promise.resolve(null));
},
},
} as Module<CalendarRangesState, State>; } as Module<CalendarRangesState, State>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,13 +3,13 @@ import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import { computed, reactive } from "vue"; import { computed, reactive } from "vue";
export interface DesktopEditButtonConfig { export interface DesktopEditButtonConfig {
editLink: null; editLink: null;
classes: Record<string, boolean>; classes: Record<string, boolean>;
expirationLink: number | Date; expirationLink: number | Date;
} }
interface DesktopEditButtonState { interface DesktopEditButtonState {
modalOpened: boolean; modalOpened: boolean;
} }
const state: DesktopEditButtonState = reactive({ modalOpened: false }); const state: DesktopEditButtonState = reactive({ modalOpened: false });
@@ -17,76 +17,80 @@ const state: DesktopEditButtonState = reactive({ modalOpened: false });
const props = defineProps<DesktopEditButtonConfig>(); const props = defineProps<DesktopEditButtonConfig>();
const buildCommand = computed<string>( const buildCommand = computed<string>(
() => "vnd.libreoffice.command:ofe|u|" + props.editLink, () => "vnd.libreoffice.command:ofe|u|" + props.editLink,
); );
const editionUntilFormatted = computed<string>(() => { const editionUntilFormatted = computed<string>(() => {
let d; let d;
if (props.expirationLink instanceof Date) { if (props.expirationLink instanceof Date) {
d = props.expirationLink; d = props.expirationLink;
} else { } else {
d = new Date(props.expirationLink * 1000); d = new Date(props.expirationLink * 1000);
} }
console.log(props.expirationLink); console.log(props.expirationLink);
return new Intl.DateTimeFormat(undefined, { return new Intl.DateTimeFormat(undefined, {
dateStyle: "long", dateStyle: "long",
timeStyle: "medium", timeStyle: "medium",
}).format(d); }).format(d);
}); });
</script> </script>
<template> <template>
<teleport to="body"> <teleport to="body">
<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">
<p> Veuillez enregistrer vos modifications avant le
<strong>{{ editionUntilFormatted }}</strong> </p>
</p> <p>
<strong>{{ editionUntilFormatted }}</strong>
</p>
<p> <p>
<a class="btn btn-primary" :href="buildCommand" <a class="btn btn-primary" :href="buildCommand"
>Ouvrir le document pour édition</a >Ouvrir le document pour édition</a
> >
</p> </p>
<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> >
</div> </p>
</template> </div>
</modal> </template>
</teleport> </modal>
<a :class="props.classes" @click="state.modalOpened = true"> </teleport>
<i class="fa fa-desktop"></i> <a :class="props.classes" @click="state.modalOpened = true">
Éditer sur le bureau <i class="fa fa-desktop"></i>
</a> Éditer sur le bureau
</a>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.desktop-edit { .desktop-edit {
text-align: center; text-align: center;
} }
i.fa::before { i.fa::before {
color: var(--bs-dropdown-link-hover-color); color: var(--bs-dropdown-link-hover-color);
} }
</style> </style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -613,14 +613,4 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this; return $this;
} }
/**
* Check if the current object is an instance of User.
*
* @return bool returns true if the current object is an instance of User, false otherwise
*/
public function isUser(): bool
{
return true;
}
} }

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

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

View File

@@ -13,15 +13,15 @@
* *
*/ */
export const dateToISO = (date: Date | null): string | null => { export const dateToISO = (date: Date | null): string | null => {
if (null === date) { if (null === date) {
return null; return null;
} }
return [ return [
date.getFullYear(), date.getFullYear(),
(date.getMonth() + 1).toString().padStart(2, "0"), (date.getMonth() + 1).toString().padStart(2, "0"),
date.getDate().toString().padStart(2, "0"), date.getDate().toString().padStart(2, "0"),
].join("-"); ].join("-");
}; };
/** /**
@@ -30,16 +30,16 @@ export const dateToISO = (date: Date | null): string | null => {
* **Experimental** * **Experimental**
*/ */
export const ISOToDate = (str: string | null): Date | null => { export const ISOToDate = (str: string | null): Date | null => {
if (null === str) { if (null === str) {
return null; return null;
} }
if ("" === str.trim()) { if ("" === str.trim()) {
return null; return null;
} }
const [year, month, day] = str.split("-").map((p) => parseInt(p)); const [year, month, day] = str.split("-").map((p) => parseInt(p));
return new Date(year, month - 1, day, 0, 0, 0, 0); return new Date(year, month - 1, day, 0, 0, 0, 0);
}; };
/** /**
@@ -47,19 +47,21 @@ export const ISOToDate = (str: string | null): Date | null => {
* *
*/ */
export const ISOToDatetime = (str: string | null): Date | null => { export const ISOToDatetime = (str: string | null): Date | null => {
if (null === str) { if (null === str) {
return null; return null;
} }
const [cal, times] = str.split("T"), const [cal, times] = str.split("T"),
[year, month, date] = cal.split("-").map((s) => parseInt(s)), [year, month, date] = cal.split("-").map((s) => parseInt(s)),
[time, timezone] = times.split(times.charAt(8)), [time, timezone] = times.split(times.charAt(8)),
[hours, minutes, seconds] = time.split(":").map((s) => parseInt(s)); [hours, minutes, seconds] = time.split(":").map((s) => parseInt(s));
if ("0000" === timezone) { if ("0000" === timezone) {
return new Date(Date.UTC(year, month - 1, date, hours, minutes, seconds)); return new Date(
} Date.UTC(year, month - 1, date, hours, minutes, seconds),
);
}
return new Date(year, month - 1, date, hours, minutes, seconds); return new Date(year, month - 1, date, hours, minutes, seconds);
}; };
/** /**
@@ -67,94 +69,96 @@ export const ISOToDatetime = (str: string | null): Date | null => {
* *
*/ */
export const datetimeToISO = (date: Date): string => { export const datetimeToISO = (date: Date): string => {
let cal, time, offset; let cal, time, offset;
cal = [ cal = [
date.getFullYear(), date.getFullYear(),
(date.getMonth() + 1).toString().padStart(2, "0"), (date.getMonth() + 1).toString().padStart(2, "0"),
date.getDate().toString().padStart(2, "0"), date.getDate().toString().padStart(2, "0"),
].join("-"); ].join("-");
time = [ time = [
date.getHours().toString().padStart(2, "0"), date.getHours().toString().padStart(2, "0"),
date.getMinutes().toString().padStart(2, "0"), date.getMinutes().toString().padStart(2, "0"),
date.getSeconds().toString().padStart(2, "0"), date.getSeconds().toString().padStart(2, "0"),
].join(":"); ].join(":");
offset = [ offset = [
date.getTimezoneOffset() <= 0 ? "+" : "-", date.getTimezoneOffset() <= 0 ? "+" : "-",
Math.abs(Math.floor(date.getTimezoneOffset() / 60)) Math.abs(Math.floor(date.getTimezoneOffset() / 60))
.toString() .toString()
.padStart(2, "0"), .padStart(2, "0"),
":", ":",
Math.abs(date.getTimezoneOffset() % 60) Math.abs(date.getTimezoneOffset() % 60)
.toString() .toString()
.padStart(2, "0"), .padStart(2, "0"),
].join(""); ].join("");
const x = cal + "T" + time + offset; const x = cal + "T" + time + offset;
return x; return x;
}; };
export const intervalDaysToISO = (days: number | string | null): string => { export const intervalDaysToISO = (days: number | string | null): string => {
if (null === days) { if (null === days) {
return "P0D"; return "P0D";
} }
return `P${days}D`; return `P${days}D`;
}; };
export const intervalISOToDays = (str: string | null): number | null => { export const intervalISOToDays = (str: string | null): number | null => {
if (null === str) { if (null === str) {
return null; return null;
}
if ("" === str.trim()) {
return null;
}
let days = 0;
let isDate = true;
let vstring = "";
for (let i = 0; i < str.length; i = i + 1) {
if (!isDate) {
continue;
} }
switch (str.charAt(i)) {
case "P":
isDate = true;
break;
case "T":
isDate = false;
break;
case "0":
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
case "8":
case "9":
vstring = vstring + str.charAt(i);
break;
case "Y":
days = days + Number.parseInt(vstring) * 365;
vstring = "";
break;
case "M":
days = days + Number.parseInt(vstring) * 30;
vstring = "";
break;
case "D":
days = days + Number.parseInt(vstring);
vstring = "";
break;
default:
throw Error("this character should not appears: " + str.charAt(i));
}
}
return days; if ("" === str.trim()) {
return null;
}
let days = 0;
let isDate = true;
let vstring = "";
for (let i = 0; i < str.length; i = i + 1) {
if (!isDate) {
continue;
}
switch (str.charAt(i)) {
case "P":
isDate = true;
break;
case "T":
isDate = false;
break;
case "0":
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
case "8":
case "9":
vstring = vstring + str.charAt(i);
break;
case "Y":
days = days + Number.parseInt(vstring) * 365;
vstring = "";
break;
case "M":
days = days + Number.parseInt(vstring) * 30;
vstring = "";
break;
case "D":
days = days + Number.parseInt(vstring);
vstring = "";
break;
default:
throw Error(
"this character should not appears: " + str.charAt(i),
);
}
}
return days;
}; };

View File

@@ -1,61 +1,61 @@
import { import {
Address, Address,
GeographicalUnitLayer, GeographicalUnitLayer,
SimpleGeographicalUnit, SimpleGeographicalUnit,
} from "../../types"; } from "../../types";
import { fetchResults, makeFetch } from "./apiMethods"; import { fetchResults, makeFetch } from "./apiMethods";
export const getAddressById = async (address_id: number): Promise<Address> => { export const getAddressById = async (address_id: number): Promise<Address> => {
const url = `/api/1.0/main/address/${address_id}.json`; const url = `/api/1.0/main/address/${address_id}.json`;
const response = await fetch(url); const response = await fetch(url);
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
throw Error("Error with request resource response"); throw Error("Error with request resource response");
}; };
export const getGeographicalUnitsByAddress = async ( export const getGeographicalUnitsByAddress = async (
address: Address, address: Address,
): Promise<SimpleGeographicalUnit[]> => { ): Promise<SimpleGeographicalUnit[]> => {
return fetchResults<SimpleGeographicalUnit>( return fetchResults<SimpleGeographicalUnit>(
`/api/1.0/main/geographical-unit/by-address/${address.address_id}.json`, `/api/1.0/main/geographical-unit/by-address/${address.address_id}.json`,
); );
}; };
export const getAllGeographicalUnitLayers = async (): Promise< export const getAllGeographicalUnitLayers = async (): Promise<
GeographicalUnitLayer[] GeographicalUnitLayer[]
> => { > => {
return fetchResults<GeographicalUnitLayer>( return fetchResults<GeographicalUnitLayer>(
`/api/1.0/main/geographical-unit-layer.json`, `/api/1.0/main/geographical-unit-layer.json`,
); );
}; };
export const syncAddressWithReference = async ( export const syncAddressWithReference = async (
address: Address, address: Address,
): Promise<Address> => { ): Promise<Address> => {
return makeFetch<null, Address>( return makeFetch<null, Address>(
"POST", "POST",
`/api/1.0/main/address/reference-match/${address.address_id}/sync-with-reference`, `/api/1.0/main/address/reference-match/${address.address_id}/sync-with-reference`,
); );
}; };
export const markAddressReviewed = async ( export const markAddressReviewed = async (
address: Address, address: Address,
): Promise<Address> => { ): Promise<Address> => {
return makeFetch<null, Address>( return makeFetch<null, Address>(
"POST", "POST",
`/api/1.0/main/address/reference-match/${address.address_id}/set/reviewed`, `/api/1.0/main/address/reference-match/${address.address_id}/set/reviewed`,
); );
}; };
export const markAddressToReview = async ( export const markAddressToReview = async (
address: Address, address: Address,
): Promise<Address> => { ): Promise<Address> => {
return makeFetch<null, Address>( return makeFetch<null, Address>(
"POST", "POST",
`/api/1.0/main/address/reference-match/${address.address_id}/set/to_review`, `/api/1.0/main/address/reference-match/${address.address_id}/set/to_review`,
); );
}; };

View File

@@ -6,57 +6,57 @@ export type fetchOption = Record<string, boolean | string | number | null>;
export type Params = Record<string, number | string>; export type Params = Record<string, number | string>;
export interface PaginationResponse<T> { export interface PaginationResponse<T> {
pagination: { pagination: {
more: boolean; more: boolean;
items_per_page: number; items_per_page: number;
}; };
results: T[]; results: T[];
count: number; count: number;
} }
export type FetchParams = Record<string, string | number | null>; export type FetchParams = Record<string, string | number | null>;
export interface TransportExceptionInterface { export interface TransportExceptionInterface {
name: string; name: string;
} }
export interface ValidationExceptionInterface export interface ValidationExceptionInterface
extends TransportExceptionInterface { extends TransportExceptionInterface {
name: "ValidationException"; name: "ValidationException";
error: object; error: object;
violations: string[]; violations: string[];
titles: string[]; titles: string[];
propertyPaths: string[]; propertyPaths: string[];
} }
export interface ValidationErrorResponse extends TransportExceptionInterface { export interface ValidationErrorResponse extends TransportExceptionInterface {
violations: { violations: {
title: string; title: string;
propertyPath: string; propertyPath: string;
}[]; }[];
} }
export interface AccessExceptionInterface extends TransportExceptionInterface { export interface AccessExceptionInterface extends TransportExceptionInterface {
name: "AccessException"; name: "AccessException";
violations: string[]; violations: string[];
} }
export interface NotFoundExceptionInterface export interface NotFoundExceptionInterface
extends TransportExceptionInterface { extends TransportExceptionInterface {
name: "NotFoundException"; name: "NotFoundException";
} }
export interface ServerExceptionInterface extends TransportExceptionInterface { export interface ServerExceptionInterface extends TransportExceptionInterface {
name: "ServerException"; name: "ServerException";
message: string; message: string;
code: number; code: number;
body: string; body: string;
} }
export interface ConflictHttpExceptionInterface export interface ConflictHttpExceptionInterface
extends TransportExceptionInterface { extends TransportExceptionInterface {
name: "ConflictHttpException"; name: "ConflictHttpException";
violations: string[]; violations: string[];
} }
/** /**
@@ -66,223 +66,223 @@ export interface ConflictHttpExceptionInterface
* and use of the @link{fetchResults} method. * and use of the @link{fetchResults} method.
*/ */
export const makeFetch = <Input, Output>( export const makeFetch = <Input, Output>(
method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE", method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE",
url: string, url: string,
body?: body | Input | null, body?: body | Input | null,
options?: FetchParams, options?: FetchParams,
): Promise<Output> => { ): Promise<Output> => {
let opts = { let opts = {
method: method, method: method,
headers: { headers: {
"Content-Type": "application/json;charset=utf-8", "Content-Type": "application/json;charset=utf-8",
}, },
};
if (body !== null && typeof body !== "undefined") {
Object.assign(opts, { body: JSON.stringify(body) });
}
if (typeof options !== "undefined") {
opts = Object.assign(opts, options);
}
return fetch(url, opts).then((response) => {
if (response.status === 204) {
return Promise.resolve();
}
if (response.ok) {
return response.json();
}
if (response.status === 422) {
return response.json().then((response) => {
throw ValidationException(response);
});
}
if (response.status === 403) {
throw AccessException(response);
}
if (response.status === 409) {
throw ConflictHttpException(response);
}
throw {
name: "Exception",
sta: response.status,
txt: response.statusText,
err: new Error(),
violations: response.body,
}; };
});
if (body !== null && typeof body !== "undefined") {
Object.assign(opts, { body: JSON.stringify(body) });
}
if (typeof options !== "undefined") {
opts = Object.assign(opts, options);
}
return fetch(url, opts).then((response) => {
if (response.status === 204) {
return Promise.resolve();
}
if (response.ok) {
return response.json();
}
if (response.status === 422) {
return response.json().then((response) => {
throw ValidationException(response);
});
}
if (response.status === 403) {
throw AccessException(response);
}
if (response.status === 409) {
throw ConflictHttpException(response);
}
throw {
name: "Exception",
sta: response.status,
txt: response.statusText,
err: new Error(),
violations: response.body,
};
});
}; };
/** /**
* Fetch results with certain parameters * Fetch results with certain parameters
*/ */
function _fetchAction<T>( function _fetchAction<T>(
page: number, page: number,
uri: string, uri: string,
params?: FetchParams, params?: FetchParams,
): Promise<PaginationResponse<T>> { ): Promise<PaginationResponse<T>> {
const item_per_page = 50; const item_per_page = 50;
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
searchParams.append("item_per_page", item_per_page.toString()); searchParams.append("item_per_page", item_per_page.toString());
searchParams.append("page", page.toString()); searchParams.append("page", page.toString());
if (params !== undefined) { if (params !== undefined) {
Object.keys(params).forEach((key) => { Object.keys(params).forEach((key) => {
const v = params[key]; const v = params[key];
if (typeof v === "string") { if (typeof v === "string") {
searchParams.append(key, v); searchParams.append(key, v);
} else if (typeof v === "number") { } else if (typeof v === "number") {
searchParams.append(key, v.toString()); searchParams.append(key, v.toString());
} else if (v === null) { } else if (v === null) {
searchParams.append(key, ""); searchParams.append(key, "");
} }
});
}
const url = uri + "?" + searchParams.toString();
return fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
})
.then((response) => {
if (response.ok) {
return response.json();
}
if (response.status === 404) {
throw NotFoundException(response);
}
if (response.status === 422) {
return response.json().then((response) => {
throw ValidationException(response);
}); });
} }
if (response.status === 403) { const url = uri + "?" + searchParams.toString();
throw AccessException(response);
}
if (response.status >= 500) { return fetch(url, {
return response.text().then((body) => { method: "GET",
throw ServerException(response.status, body); headers: {
}); "Content-Type": "application/json;charset=utf-8",
} },
throw new Error("other network error");
}) })
.catch( .then((response) => {
( if (response.ok) {
reason: return response.json();
| NotFoundExceptionInterface }
| ServerExceptionInterface
| ValidationExceptionInterface if (response.status === 404) {
| TransportExceptionInterface, throw NotFoundException(response);
) => { }
console.error(reason);
throw reason; if (response.status === 422) {
}, return response.json().then((response) => {
); throw ValidationException(response);
});
}
if (response.status === 403) {
throw AccessException(response);
}
if (response.status >= 500) {
return response.text().then((body) => {
throw ServerException(response.status, body);
});
}
throw new Error("other network error");
})
.catch(
(
reason:
| NotFoundExceptionInterface
| ServerExceptionInterface
| ValidationExceptionInterface
| TransportExceptionInterface,
) => {
console.error(reason);
throw reason;
},
);
} }
export const fetchResults = async <T>( export const fetchResults = async <T>(
uri: string, uri: string,
params?: FetchParams, params?: FetchParams,
): Promise<T[]> => { ): Promise<T[]> => {
const promises: Promise<T[]>[] = []; const promises: Promise<T[]>[] = [];
let page = 1; let page = 1;
const firstData: PaginationResponse<T> = (await _fetchAction( const firstData: PaginationResponse<T> = (await _fetchAction(
page, page,
uri, uri,
params, params,
)) as PaginationResponse<T>; )) as PaginationResponse<T>;
promises.push(Promise.resolve(firstData.results)); promises.push(Promise.resolve(firstData.results));
if (firstData.pagination.more) { if (firstData.pagination.more) {
do { do {
page = ++page; page = ++page;
promises.push( promises.push(
_fetchAction<T>(page, uri, params).then((r) => _fetchAction<T>(page, uri, params).then((r) =>
Promise.resolve(r.results), Promise.resolve(r.results),
), ),
); );
} while (page * firstData.pagination.items_per_page < firstData.count); } while (page * firstData.pagination.items_per_page < firstData.count);
} }
return Promise.all(promises).then((values) => values.flat()); return Promise.all(promises).then((values) => values.flat());
}; };
export const fetchScopes = (): Promise<Scope[]> => { 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 * Error objects to be thrown
*/ */
const ValidationException = ( const ValidationException = (
response: ValidationErrorResponse, response: ValidationErrorResponse,
): ValidationExceptionInterface => { ): ValidationExceptionInterface => {
const error = {} as ValidationExceptionInterface; const error = {} as ValidationExceptionInterface;
error.name = "ValidationException"; error.name = "ValidationException";
error.violations = response.violations.map( error.violations = response.violations.map(
(violation) => `${violation.title}: ${violation.propertyPath}`, (violation) => `${violation.title}: ${violation.propertyPath}`,
); );
error.titles = response.violations.map((violation) => violation.title); error.titles = response.violations.map((violation) => violation.title);
error.propertyPaths = response.violations.map( error.propertyPaths = response.violations.map(
(violation) => violation.propertyPath, (violation) => violation.propertyPath,
); );
return error; 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;
error.name = "AccessException"; error.name = "AccessException";
error.violations = ["You are not allowed to perform this action"]; error.violations = ["You are not allowed to perform this action"];
return error; return error;
}; };
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const NotFoundException = (response: Response): NotFoundExceptionInterface => { const NotFoundException = (response: Response): NotFoundExceptionInterface => {
const error = {} as NotFoundExceptionInterface; const error = {} as NotFoundExceptionInterface;
error.name = "NotFoundException"; error.name = "NotFoundException";
return error; return error;
}; };
const ServerException = ( const ServerException = (
code: number, code: number,
body: string, body: string,
): ServerExceptionInterface => { ): ServerExceptionInterface => {
const error = {} as ServerExceptionInterface; const error = {} as ServerExceptionInterface;
error.name = "ServerException"; error.name = "ServerException";
error.code = code; error.code = code;
error.body = body; error.body = body;
return error; return error;
}; };
const ConflictHttpException = ( const ConflictHttpException = (
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
response: Response, response: Response,
): ConflictHttpExceptionInterface => { ): ConflictHttpExceptionInterface => {
const error = {} as ConflictHttpExceptionInterface; const error = {} as ConflictHttpExceptionInterface;
error.name = "ConflictHttpException"; error.name = "ConflictHttpException";
error.violations = [ error.violations = [
"Sorry, but someone else has already changed this entity. Please refresh the page and apply the changes again", "Sorry, but someone else has already changed this entity. Please refresh the page and apply the changes again",
]; ];
return error; return error;
}; };

View File

@@ -2,17 +2,17 @@ import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { ExportGeneration } from "ChillMainAssets/types"; import { ExportGeneration } from "ChillMainAssets/types";
export const fetchExportGenerationStatus = async ( export const fetchExportGenerationStatus = async (
exportGenerationId: string, exportGenerationId: string,
): Promise<ExportGeneration> => ): Promise<ExportGeneration> =>
makeFetch( makeFetch(
"GET", "GET",
`/api/1.0/main/export-generation/${exportGenerationId}/object`, `/api/1.0/main/export-generation/${exportGenerationId}/object`,
); );
export const generateFromSavedExport = async ( export const generateFromSavedExport = async (
savedExportUuid: string, savedExportUuid: string,
): Promise<ExportGeneration> => ): Promise<ExportGeneration> =>
makeFetch( makeFetch(
"POST", "POST",
`/api/1.0/main/export/export-generation/create-from-saved-export/${savedExportUuid}`, `/api/1.0/main/export/export-generation/create-from-saved-export/${savedExportUuid}`,
); );

View File

@@ -2,7 +2,7 @@ import { fetchResults } from "./apiMethods";
import { Location, LocationType } from "../../types"; import { Location, LocationType } from "../../types";
export const getLocations = (): Promise<Location[]> => export const getLocations = (): Promise<Location[]> =>
fetchResults("/api/1.0/main/location.json"); fetchResults("/api/1.0/main/location.json");
export const getLocationTypes = (): Promise<LocationType[]> => export const getLocationTypes = (): Promise<LocationType[]> =>
fetchResults("/api/1.0/main/location-type.json"); fetchResults("/api/1.0/main/location-type.json");

View File

@@ -1,3 +1,3 @@
export function buildReturnPath(location: Location): string { export function buildReturnPath(location: Location): string {
return location.pathname + location.search; return location.pathname + location.search;
} }

View File

@@ -2,23 +2,23 @@ import { User } from "../../types";
import { makeFetch } from "./apiMethods"; import { makeFetch } from "./apiMethods";
export const whoami = (): Promise<User> => { export const whoami = (): Promise<User> => {
const url = `/api/1.0/main/whoami.json`; const url = `/api/1.0/main/whoami.json`;
return fetch(url).then((response) => { return fetch(url).then((response) => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
throw { throw {
msg: "Error while getting whoami.", msg: "Error while getting whoami.",
sta: response.status, sta: response.status,
txt: response.statusText, txt: response.statusText,
err: new Error(), err: new Error(),
body: response.body, body: response.body,
}; };
}); });
}; };
export const whereami = (): Promise<Location | null> => { export const whereami = (): Promise<Location | null> => {
const url = `/api/1.0/main/user-current-location.json`; const url = `/api/1.0/main/user-current-location.json`;
return makeFetch<null, Location | null>("GET", url); return makeFetch<null, Location | null>("GET", url);
}; };

View File

@@ -1,22 +1,22 @@
const buildLinkCreate = function ( const buildLinkCreate = function (
relatedEntityClass: string, relatedEntityClass: string,
relatedEntityId: number, relatedEntityId: number,
to: number | null, to: number | null,
returnPath: string | null, returnPath: string | null,
): string { ): string {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append("entityClass", relatedEntityClass); params.append("entityClass", relatedEntityClass);
params.append("entityId", relatedEntityId.toString()); params.append("entityId", relatedEntityId.toString());
if (null !== to) { if (null !== to) {
params.append("tos[0]", to.toString()); params.append("tos[0]", to.toString());
} }
if (null !== returnPath) { if (null !== returnPath) {
params.append("returnPath", returnPath); params.append("returnPath", returnPath);
} }
return `/fr/notification/create?${params.toString()}`; return `/fr/notification/create?${params.toString()}`;
}; };
export { buildLinkCreate }; export { buildLinkCreate };

View File

@@ -10,17 +10,17 @@
* @throws {Error} If the related entity ID is undefined. * @throws {Error} If the related entity ID is undefined.
*/ */
export const buildLinkCreate = ( export const buildLinkCreate = (
workflowName: string, workflowName: string,
relatedEntityClass: string, relatedEntityClass: string,
relatedEntityId: number | undefined, relatedEntityId: number | undefined,
): string => { ): string => {
if (typeof relatedEntityId === "undefined") { if (typeof relatedEntityId === "undefined") {
throw new Error("the related entity id is not set"); throw new Error("the related entity id is not set");
} }
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set("entityClass", relatedEntityClass); params.set("entityClass", relatedEntityClass);
params.set("entityId", relatedEntityId.toString(10)); params.set("entityId", relatedEntityId.toString(10));
params.set("workflow", workflowName); params.set("workflow", workflowName);
return `/fr/main/workflow/create?` + params.toString(); return `/fr/main/workflow/create?` + params.toString();
}; };

View File

@@ -9,33 +9,33 @@ import { TranslatableString } from "ChillMainAssets/types";
* @returns The localized string or null if no translation is available * @returns The localized string or null if no translation is available
*/ */
export function localizeString( export function localizeString(
translatableString: TranslatableString | null | undefined, translatableString: TranslatableString | null | undefined,
locale?: string, locale?: string,
): string { ): string {
if (!translatableString || Object.keys(translatableString).length === 0) { if (!translatableString || Object.keys(translatableString).length === 0) {
return ""; return "";
}
const currentLocale = locale || navigator.language.split("-")[0] || "fr";
if (translatableString[currentLocale]) {
return translatableString[currentLocale];
}
// Define fallback locales
const fallbackLocales: string[] = ["fr", "en"];
for (const fallbackLocale of fallbackLocales) {
if (translatableString[fallbackLocale]) {
return translatableString[fallbackLocale];
} }
}
// No fallback translation found, use the first available const currentLocale = locale || navigator.language.split("-")[0] || "fr";
const availableLocales = Object.keys(translatableString);
if (availableLocales.length > 0) {
return translatableString[availableLocales[0]];
}
return ""; if (translatableString[currentLocale]) {
return translatableString[currentLocale];
}
// Define fallback locales
const fallbackLocales: string[] = ["fr", "en"];
for (const fallbackLocale of fallbackLocales) {
if (translatableString[fallbackLocale]) {
return translatableString[fallbackLocale];
}
}
// No fallback translation found, use the first available
const availableLocales = Object.keys(translatableString);
if (availableLocales.length > 0) {
return translatableString[availableLocales[0]];
}
return "";
} }

View File

@@ -3,20 +3,20 @@ import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/gener
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
export const find_attachments_by_workflow = async ( export const find_attachments_by_workflow = async (
workflowId: number, workflowId: number,
): Promise<WorkflowAttachment[]> => ): Promise<WorkflowAttachment[]> =>
makeFetch("GET", `/api/1.0/main/workflow/${workflowId}/attachment`); makeFetch("GET", `/api/1.0/main/workflow/${workflowId}/attachment`);
export const create_attachment = async ( export const create_attachment = async (
workflowId: number, workflowId: number,
genericDoc: GenericDocForAccompanyingPeriod, genericDoc: GenericDocForAccompanyingPeriod,
): Promise<WorkflowAttachment> => ): Promise<WorkflowAttachment> =>
makeFetch("POST", `/api/1.0/main/workflow/${workflowId}/attachment`, { makeFetch("POST", `/api/1.0/main/workflow/${workflowId}/attachment`, {
relatedGenericDocKey: genericDoc.key, relatedGenericDocKey: genericDoc.key,
relatedGenericDocIdentifiers: genericDoc.identifiers, relatedGenericDocIdentifiers: genericDoc.identifiers,
}); });
export const delete_attachment = async ( export const delete_attachment = async (
attachment: WorkflowAttachment, attachment: WorkflowAttachment,
): Promise<void> => ): Promise<void> =>
makeFetch("DELETE", `/api/1.0/main/workflow/attachment/${attachment.id}`); makeFetch("DELETE", `/api/1.0/main/workflow/attachment/${attachment.id}`);

View File

@@ -7,43 +7,43 @@ import { Address } from "../../types";
const i18n = _createI18n({}); const i18n = _createI18n({});
document document
.querySelectorAll<HTMLSpanElement>("span[data-address-details]") .querySelectorAll<HTMLSpanElement>("span[data-address-details]")
.forEach((el) => { .forEach((el) => {
const dataset = el.dataset as { const dataset = el.dataset as {
addressId: string; addressId: string;
addressRefStatus: string; addressRefStatus: string;
};
const app = createApp({
components: { AddressDetailsButton },
data() {
return {
addressId: Number.parseInt(dataset.addressId),
addressRefStatus: dataset.addressRefStatus,
}; };
},
template:
'<address-details-button :address_id="addressId" :address_ref_status="addressRefStatus" @update-address="onUpdateAddress"></address-details-button>',
methods: {
onUpdateAddress: (address: Address): void => {
if (
address.refStatus === "to_review" ||
address.refStatus === "reviewed"
) {
// in this two case, the address content do not change
return;
}
if (
window.confirm(
"L'adresse a été modifiée. Vous pouvez continuer votre travail. Cependant, pour afficher les données immédiatement, veuillez recharger la page. \n\n Voulez-vous recharger la page immédiatement ?",
)
) {
window.location.reload();
}
},
},
});
app.use(i18n); const app = createApp({
app.mount(el); components: { AddressDetailsButton },
}); data() {
return {
addressId: Number.parseInt(dataset.addressId),
addressRefStatus: dataset.addressRefStatus,
};
},
template:
'<address-details-button :address_id="addressId" :address_ref_status="addressRefStatus" @update-address="onUpdateAddress"></address-details-button>',
methods: {
onUpdateAddress: (address: Address): void => {
if (
address.refStatus === "to_review" ||
address.refStatus === "reviewed"
) {
// in this two case, the address content do not change
return;
}
if (
window.confirm(
"L'adresse a été modifiée. Vous pouvez continuer votre travail. Cependant, pour afficher les données immédiatement, veuillez recharger la page. \n\n Voulez-vous recharger la page immédiatement ?",
)
) {
window.location.reload();
}
},
},
});
app.use(i18n);
app.mount(el);
});

View File

@@ -1,16 +1,16 @@
import { import {
Essentials, Essentials,
Bold, Bold,
Italic, Italic,
Paragraph, Paragraph,
Markdown, Markdown,
BlockQuote, BlockQuote,
Heading, Heading,
Link, Link,
List, List,
Emoji, Emoji,
Mention, Mention,
Fullscreen, Fullscreen,
} from "ckeditor5"; } from "ckeditor5";
import coreTranslations from "ckeditor5/translations/fr.js"; import coreTranslations from "ckeditor5/translations/fr.js";
@@ -19,41 +19,41 @@ import "ckeditor5/ckeditor5.css";
import "./index.scss"; import "./index.scss";
export default { export default {
plugins: [ plugins: [
Essentials, Essentials,
Markdown, Markdown,
Bold, Bold,
Italic, Italic,
BlockQuote, BlockQuote,
Heading, Heading,
Link, Link,
List, List,
Paragraph, Paragraph,
// both Emoji and Mention are required for Emoji feature // both Emoji and Mention are required for Emoji feature
Emoji, Emoji,
Mention, Mention,
// to enable fullscreen // to enable fullscreen
Fullscreen, Fullscreen,
],
toolbar: {
items: [
"heading",
"|",
"bold",
"italic",
"link",
"bulletedList",
"numberedList",
"blockQuote",
"|",
"emoji",
"|",
"undo",
"redo",
"|",
"fullscreen",
], ],
}, toolbar: {
translations: [coreTranslations], items: [
licenseKey: "GPL", "heading",
"|",
"bold",
"italic",
"link",
"bulletedList",
"numberedList",
"blockQuote",
"|",
"emoji",
"|",
"undo",
"redo",
"|",
"fullscreen",
],
},
translations: [coreTranslations],
licenseKey: "GPL",
}; };

View File

@@ -2,31 +2,31 @@ import { createApp } from "vue";
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue"; import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
const ckeditorFields: NodeListOf<HTMLTextAreaElement> = const ckeditorFields: NodeListOf<HTMLTextAreaElement> =
document.querySelectorAll("textarea[ckeditor]"); document.querySelectorAll("textarea[ckeditor]");
ckeditorFields.forEach((field: HTMLTextAreaElement): void => { ckeditorFields.forEach((field: HTMLTextAreaElement): void => {
const content = field.value; const content = field.value;
const div = document.createElement("div"); const div = document.createElement("div");
if (field.parentNode !== null) { if (field.parentNode !== null) {
field.parentNode.insertBefore(div, field); field.parentNode.insertBefore(div, field);
} else { } else {
throw "parent is null"; throw "parent is null";
} }
createApp({ createApp({
components: { CommentEditor }, components: { CommentEditor },
template: `<comment-editor v-model="content" @update:modelValue="handleInput"></comment-editor>`, template: `<comment-editor v-model="content" @update:modelValue="handleInput"></comment-editor>`,
data() { data() {
return { return {
content, content,
}; };
}, },
methods: { methods: {
handleInput() { handleInput() {
field.value = this.content; field.value = this.content;
}, },
}, },
}).mount(div); }).mount(div);
field.style.display = "none"; field.style.display = "none";
}); });

View File

@@ -31,157 +31,157 @@
import "./collection.scss"; import "./collection.scss";
declare global { declare global {
interface GlobalEventHandlersEventMap { interface GlobalEventHandlersEventMap {
"show-hide-show": CustomEvent<{ "show-hide-show": CustomEvent<{
id: number; id: number;
froms: HTMLElement[]; froms: HTMLElement[];
container: HTMLElement; container: HTMLElement;
}>; }>;
} }
} }
export class CollectionEventPayload { export class CollectionEventPayload {
collection: HTMLUListElement; collection: HTMLUListElement;
entry: HTMLLIElement; entry: HTMLLIElement;
constructor(collection: HTMLUListElement, entry: HTMLLIElement) { constructor(collection: HTMLUListElement, entry: HTMLLIElement) {
this.collection = collection; this.collection = collection;
this.entry = entry; this.entry = entry;
} }
} }
export const handleAdd = (button: any): void => { export const handleAdd = (button: any): void => {
const form_name = button.dataset.collectionAddTarget, const form_name = button.dataset.collectionAddTarget,
prototype = button.dataset.formPrototype, prototype = button.dataset.formPrototype,
collection: HTMLUListElement | null = document.querySelector( collection: HTMLUListElement | null = document.querySelector(
'ul[data-collection-name="' + form_name + '"]', 'ul[data-collection-name="' + form_name + '"]',
); );
if (collection === null) { if (collection === null) {
return; return;
}
const empty_explain: HTMLLIElement | null = collection.querySelector(
"li[data-collection-empty-explain]",
),
entry = document.createElement("li"),
counter = collection.querySelectorAll("li.entry").length, // Updated counter logic
content = prototype.replace(/__name__/g, counter.toString()),
event = new CustomEvent("collection-add-entry", {
detail: new CollectionEventPayload(collection, entry),
});
console.log(counter);
console.log(content);
entry.innerHTML = content;
entry.classList.add("entry");
if ("collectionRegular" in collection.dataset) {
initializeRemove(collection, entry);
if (empty_explain !== null) {
empty_explain.remove();
} }
}
collection.appendChild(entry); const empty_explain: HTMLLIElement | null = collection.querySelector(
collection.dispatchEvent(event); "li[data-collection-empty-explain]",
window.dispatchEvent(event); ),
entry = document.createElement("li"),
counter = collection.querySelectorAll("li.entry").length, // Updated counter logic
content = prototype.replace(/__name__/g, counter.toString()),
event = new CustomEvent("collection-add-entry", {
detail: new CollectionEventPayload(collection, entry),
});
console.log(counter);
console.log(content);
entry.innerHTML = content;
entry.classList.add("entry");
if ("collectionRegular" in collection.dataset) {
initializeRemove(collection, entry);
if (empty_explain !== null) {
empty_explain.remove();
}
}
collection.appendChild(entry);
collection.dispatchEvent(event);
window.dispatchEvent(event);
}; };
const initializeRemove = ( const initializeRemove = (
collection: HTMLUListElement, collection: HTMLUListElement,
entry: HTMLLIElement, entry: HTMLLIElement,
): void => { ): void => {
const button = buildRemoveButton(collection, entry); const button = buildRemoveButton(collection, entry);
if (null === button) { if (null === button) {
return; return;
} }
entry.appendChild(button); entry.appendChild(button);
}; };
export const buildRemoveButton = ( export const buildRemoveButton = (
collection: HTMLUListElement, collection: HTMLUListElement,
entry: HTMLLIElement, entry: HTMLLIElement,
): HTMLButtonElement | null => { ): HTMLButtonElement | null => {
const button = document.createElement("button"), const button = document.createElement("button"),
isPersisted = entry.dataset.collectionIsPersisted || "", isPersisted = entry.dataset.collectionIsPersisted || "",
content = collection.dataset.collectionButtonRemoveLabel || "", content = collection.dataset.collectionButtonRemoveLabel || "",
allowDelete = collection.dataset.collectionAllowDelete || "", allowDelete = collection.dataset.collectionAllowDelete || "",
event = new CustomEvent("collection-remove-entry", { event = new CustomEvent("collection-remove-entry", {
detail: new CollectionEventPayload(collection, entry), detail: new CollectionEventPayload(collection, entry),
});
if (allowDelete === "0" && isPersisted === "1") {
return null;
}
button.classList.add("btn", "btn-delete", "remove-entry");
button.textContent = content;
button.addEventListener("click", (e: Event) => {
e.preventDefault();
entry.remove();
collection.dispatchEvent(event);
window.dispatchEvent(event);
}); });
if (allowDelete === "0" && isPersisted === "1") { return button;
return null;
}
button.classList.add("btn", "btn-delete", "remove-entry");
button.textContent = content;
button.addEventListener("click", (e: Event) => {
e.preventDefault();
entry.remove();
collection.dispatchEvent(event);
window.dispatchEvent(event);
});
return button;
}; };
const collectionsInit = new Set<string>(); const collectionsInit = new Set<string>();
const buttonsInit = new Set<string>(); const buttonsInit = new Set<string>();
const initialize = function (target: Document | Element): void { const initialize = function (target: Document | Element): void {
const addButtons: NodeListOf<HTMLButtonElement> = document.querySelectorAll( const addButtons: NodeListOf<HTMLButtonElement> = document.querySelectorAll(
"button[data-collection-add-target]", "button[data-collection-add-target]",
), ),
collections: NodeListOf<HTMLUListElement> = document.querySelectorAll( collections: NodeListOf<HTMLUListElement> = document.querySelectorAll(
"ul[data-collection-regular]", "ul[data-collection-regular]",
); );
for (let i = 0; i < addButtons.length; i++) { for (let i = 0; i < addButtons.length; i++) {
const addButton = addButtons[i]; const addButton = addButtons[i];
const uniqid = addButton.dataset.uniqid as string; const uniqid = addButton.dataset.uniqid as string;
if (buttonsInit.has(uniqid)) { if (buttonsInit.has(uniqid)) {
continue; continue;
}
buttonsInit.add(uniqid);
addButton.addEventListener("click", (e: Event) => {
e.preventDefault();
handleAdd(e.target);
});
} }
buttonsInit.add(uniqid); for (let i = 0; i < collections.length; i++) {
addButton.addEventListener("click", (e: Event) => { const collection = collections[i];
e.preventDefault(); const uniqid = collection.dataset.uniqid as string;
handleAdd(e.target); if (collectionsInit.has(uniqid)) {
}); continue;
} }
for (let i = 0; i < collections.length; i++) { collectionsInit.add(uniqid);
const collection = collections[i]; const entries: NodeListOf<HTMLLIElement> =
const uniqid = collection.dataset.uniqid as string; collection.querySelectorAll(":scope > li");
if (collectionsInit.has(uniqid)) { for (let j = 0; j < entries.length; j++) {
continue; if (entries[j].dataset.collectionEmptyExplain === "1") {
continue;
}
initializeRemove(collections[i], entries[j]);
}
} }
collectionsInit.add(uniqid);
const entries: NodeListOf<HTMLLIElement> =
collection.querySelectorAll(":scope > li");
for (let j = 0; j < entries.length; j++) {
if (entries[j].dataset.collectionEmptyExplain === "1") {
continue;
}
initializeRemove(collections[i], entries[j]);
}
}
}; };
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
initialize(document); initialize(document);
}); });
window.addEventListener( window.addEventListener(
"show-hide-show", "show-hide-show",
( (
event: CustomEvent<{ event: CustomEvent<{
id: number; id: number;
container: HTMLElement; container: HTMLElement;
froms: HTMLElement[]; froms: HTMLElement[];
}>, }>,
) => { ) => {
const container = event.detail.container as HTMLElement; const container = event.detail.container as HTMLElement;
initialize(container); initialize(container);
}, },
); );

View File

@@ -5,39 +5,39 @@ import NotificationReadAllToggle from "../../vuejs/_components/Notification/Noti
const i18n = _createI18n({}); const i18n = _createI18n({});
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const elements = document.querySelectorAll(".notification_all_read"); const elements = document.querySelectorAll(".notification_all_read");
elements.forEach((element) => { elements.forEach((element) => {
console.log("launch"); console.log("launch");
createApp({ createApp({
template: `<notification-read-all-toggle @markAsRead="markAsRead" @markAsUnRead="markAsUnread"></notification-read-all-toggle>`, template: `<notification-read-all-toggle @markAsRead="markAsRead" @markAsUnRead="markAsUnread"></notification-read-all-toggle>`,
components: { components: {
NotificationReadAllToggle, NotificationReadAllToggle,
}, },
methods: { methods: {
markAsRead(id: number) { markAsRead(id: number) {
const el = document.querySelector<HTMLDivElement>( const el = document.querySelector<HTMLDivElement>(
`div.notification-status[data-notification-id="${id}"]`, `div.notification-status[data-notification-id="${id}"]`,
); );
if (el === null) { if (el === null) {
return; return;
} }
el.classList.add("read"); el.classList.add("read");
el.classList.remove("unread"); el.classList.remove("unread");
}, },
markAsUnread(id: number) { markAsUnread(id: number) {
const el = document.querySelector<HTMLDivElement>( const el = document.querySelector<HTMLDivElement>(
`div.notification-status[data-notification-id="${id}"]`, `div.notification-status[data-notification-id="${id}"]`,
); );
if (el === null) { if (el === null) {
return; return;
} }
el.classList.remove("read"); el.classList.remove("read");
el.classList.add("unread"); el.classList.add("unread");
}, },
}, },
}) })
.use(i18n) .use(i18n)
.mount(element); .mount(element);
}); });
}); });

View File

@@ -124,10 +124,9 @@ function loadDynamicPicker(element) {
removeEntity({ entity }) { removeEntity({ entity }) {
if ( if (
-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

@@ -2,279 +2,216 @@ import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types"; import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
export interface DateTime { export interface DateTime {
datetime: string; datetime: string;
datetime8601: string; datetime8601: string;
} }
export interface Civility { export interface Civility {
id: number; id: number;
// TODO // TODO
}
export interface Household {
type: "household";
id: number;
} }
export interface Job { export interface Job {
id: number; id: number;
type: "user_job"; type: "user_job";
label: { label: {
fr: string; // could have other key. How to do that in ts ? fr: string; // could have other key. How to do that in ts ?
}; };
} }
export interface Center { export interface Center {
id: number; id: number;
type: "center"; type: "center";
name: string; name: string;
} }
export interface Scope { export interface Scope {
id: number; id: number;
type: "scope"; type: "scope";
name: { name: {
fr: string; fr: string;
}; };
} }
export interface ResultItem<T> { export interface ResultItem<T> {
result: T; result: T;
relevance: number; relevance: number;
} }
export interface User { export interface User {
type: "user"; type: "user";
id: number; id: number;
username: string; username: string;
text: string; text: string;
text_without_absence: string; text_without_absence: string;
email: string; email: string;
user_job: Job; user_job: Job;
label: string; label: string;
// todo: mainCenter; mainJob; etc.. // todo: mainCenter; mainJob; etc..
} }
export interface UserGroup { export interface UserGroup {
type: "user_group"; type: "user_group";
id: number; id: number;
label: TranslatableString; label: TranslatableString;
backgroundColor: string; backgroundColor: string;
foregroundColor: string; foregroundColor: string;
excludeKey: string; excludeKey: string;
text: string; text: string;
} }
export type UserGroupOrUser = User | UserGroup; export type UserGroupOrUser = User | UserGroup;
export interface UserAssociatedInterface { export interface UserAssociatedInterface {
type: "user"; type: "user";
id: number; id: number;
} }
export type TranslatableString = Record<string, string>; export type TranslatableString = Record<string, string>;
export interface Postcode { export interface Postcode {
id: number; id: number;
name: string; name: string;
code: string; code: string;
center: Point; center: Point;
} }
export interface Point { export interface Point {
type: "Point"; type: "Point";
coordinates: [lat: number, lon: number]; coordinates: [lat: number, lon: number];
} }
export interface Country { export interface Country {
id: number; id: number;
name: TranslatableString; name: TranslatableString;
code: string; code: string;
} }
export type AddressRefStatus = "match" | "to_review" | "reviewed"; export type AddressRefStatus = "match" | "to_review" | "reviewed";
export interface Address { export interface Address {
type: "address"; type: "address";
address_id: number; address_id: number;
text: string; text: string;
street: string; street: string;
streetNumber: string; streetNumber: string;
postcode: Postcode; postcode: Postcode;
country: Country; country: Country;
floor: string | null; floor: string | null;
corridor: string | null; corridor: string | null;
steps: string | null; steps: string | null;
flat: string | null; flat: string | null;
buildingName: string | null; buildingName: string | null;
distribution: string | null; distribution: string | null;
extra: string | null; extra: string | null;
confidential: boolean; confidential: boolean;
lines: string[]; lines: string[];
addressReference: AddressReference | null; addressReference: AddressReference | null;
validFrom: DateTime; validFrom: DateTime;
validTo: DateTime | null; validTo: DateTime | null;
point: Point | null; point: Point | null;
refStatus: AddressRefStatus; refStatus: AddressRefStatus;
isNoAddress: boolean; isNoAddress: boolean;
} }
export interface AddressWithPoint extends Address { export interface AddressWithPoint extends Address {
point: Point; point: Point;
} }
export interface AddressReference { export interface AddressReference {
id: number; id: number;
createdAt: DateTime | null; createdAt: DateTime | null;
deletedAt: DateTime | null; deletedAt: DateTime | null;
municipalityCode: string; municipalityCode: string;
point: Point; point: Point;
postcode: Postcode; postcode: Postcode;
refId: string; refId: string;
source: string; source: string;
street: string; street: string;
streetNumber: string; streetNumber: string;
updatedAt: DateTime | null; updatedAt: DateTime | null;
} }
export interface SimpleGeographicalUnit { export interface SimpleGeographicalUnit {
id: number; id: number;
layerId: number; layerId: number;
unitName: string; unitName: string;
unitRefId: string; unitRefId: string;
} }
export interface GeographicalUnitLayer { export interface GeographicalUnitLayer {
id: number; id: number;
name: TranslatableString; name: TranslatableString;
refId: string; refId: string;
} }
export interface Location { export interface Location {
type: "location"; type: "location";
id: number; id: number;
active: boolean; active: boolean;
address: Address | null; address: Address | null;
availableForUsers: boolean; availableForUsers: boolean;
createdAt: DateTime | null; createdAt: DateTime | null;
createdBy: User | null; createdBy: User | null;
updatedAt: DateTime | null; updatedAt: DateTime | null;
updatedBy: User | null; updatedBy: User | null;
email: string | null; email: string | null;
name: string; name: string;
phonenumber1: string | null; phonenumber1: string | null;
phonenumber2: string | null; phonenumber2: string | null;
locationType: LocationType; locationType: LocationType;
} }
export interface LocationAssociated { export interface LocationAssociated {
type: "location"; type: "location";
id: number; id: number;
} }
export interface LocationType { export interface LocationType {
type: "location-type"; type: "location-type";
id: number; id: number;
active: boolean; active: boolean;
addressRequired: "optional" | "required"; addressRequired: "optional" | "required";
availableForUsers: boolean; availableForUsers: boolean;
editableByUsers: boolean; editableByUsers: boolean;
contactData: "optional" | "required"; contactData: "optional" | "required";
title: TranslatableString; title: TranslatableString;
} }
export interface NewsItemType { export interface NewsItemType {
id: number; id: number;
title: string; title: string;
content: string; content: string;
startDate: DateTime; startDate: DateTime;
endDate: DateTime | null; endDate: DateTime | null;
} }
export interface WorkflowAvailable { export interface WorkflowAvailable {
name: string; name: string;
text: string; text: string;
} }
export interface WorkflowAttachment { export interface WorkflowAttachment {
id: number; id: number;
relatedGenericDocKey: string; relatedGenericDocKey: string;
relatedGenericDocIdentifiers: object; relatedGenericDocIdentifiers: object;
createdAt: DateTime | null; createdAt: DateTime | null;
createdBy: User | null; createdBy: User | null;
updatedAt: DateTime | null; updatedAt: DateTime | null;
updatedBy: User | null; updatedBy: User | null;
genericDoc: null | GenericDoc; genericDoc: null | GenericDoc;
} }
export interface ExportGeneration { export interface ExportGeneration {
id: string; id: string;
type: "export_generation"; type: "export_generation";
exportAlias: string; exportAlias: string;
createdBy: User | null; createdBy: User | null;
createdAt: DateTime | null; createdAt: DateTime | null;
status: StoredObjectStatus; status: StoredObjectStatus;
storedObject: StoredObject; storedObject: StoredObject;
} }
export interface PrivateCommentEmbeddable { export interface PrivateCommentEmbeddable {
comments: Record<number, string>; comments: Record<number, string>;
}
// API Exception types
export interface TransportExceptionInterface {
name: string;
}
export interface ValidationExceptionInterface
extends TransportExceptionInterface {
name: "ValidationException";
error: object;
violations: string[];
titles: string[];
propertyPaths: string[];
}
export interface AccessExceptionInterface extends TransportExceptionInterface {
name: "AccessException";
violations: string[];
}
export interface NotFoundExceptionInterface
extends TransportExceptionInterface {
name: "NotFoundException";
}
export interface ServerExceptionInterface extends TransportExceptionInterface {
name: "ServerException";
message: string;
code: number;
body: string;
}
export interface ConflictHttpExceptionInterface
extends TransportExceptionInterface {
name: "ConflictHttpException";
violations: string[];
}
export type ApiException =
| ValidationExceptionInterface
| AccessExceptionInterface
| NotFoundExceptionInterface
| ServerExceptionInterface
| ConflictHttpExceptionInterface;
export interface Modal {
showModal: boolean;
modalDialogClass: string;
}
export interface Selected {
result: UserGroupOrUser;
}
export interface addNewEntities {
selected: Selected[];
modal: Modal;
} }

View File

@@ -1,130 +1,130 @@
<template> <template>
<add-address <add-address
:key="key" :key="key"
:context="context" :context="context"
:options="options" :options="options"
:address-changed-callback="submitAddress" :address-changed-callback="submitAddress"
ref="addAddress" ref="addAddress"
/> />
</template> </template>
<script> <script>
import AddAddress from "./components/AddAddress.vue"; import AddAddress from "./components/AddAddress.vue";
import { import {
postAddressToHousehold, postAddressToHousehold,
postAddressToPerson, postAddressToPerson,
} from "ChillPersonAssets/vuejs/_api/AddAddress"; } from "ChillPersonAssets/vuejs/_api/AddAddress";
export default { export default {
name: "App", name: "App",
components: { components: {
AddAddress, AddAddress,
},
props: ["addAddress", "callback"],
emits: ["addressEdited", "addressCreated"],
computed: {
context() {
return this.addAddress.context;
}, },
options() { props: ["addAddress", "callback"],
return this.addAddress.options; emits: ["addressEdited", "addressCreated"],
computed: {
context() {
return this.addAddress.context;
},
options() {
return this.addAddress.options;
},
key() {
return this.context.edit
? "address_" + this.context.addressId
: this.context.target.name + "_" + this.context.target.id;
},
}, },
key() { mounted() {
return this.context.edit //console.log('AddAddress: data context', this.context);
? "address_" + this.context.addressId //console.log('AddAddress: data options', this.options);
: this.context.target.name + "_" + this.context.target.id;
}, },
}, methods: {
mounted() { displayErrors() {
//console.log('AddAddress: data context', this.context); return this.$refs.addAddress.errorMsg;
//console.log('AddAddress: data options', this.options); },
}, submitAddress(payload) {
methods: { console.log("@@@ click on Submit Address Button", payload);
displayErrors() {
return this.$refs.addAddress.errorMsg; // Existing address
if (this.context.edit) {
// address is already linked, just finish !
this.$refs.addAddress.afterLastPaneAction({});
this.$emit("addressEdited", payload);
// New created address
} else {
this.postAddressTo(payload);
}
},
/*
* Post new created address to targetEntity
*/
postAddressTo(payload) {
this.$emit("addressCreated", payload);
console.log(
"postAddress",
payload.addressId,
"To",
payload.target,
payload.targetId,
);
switch (payload.target) {
case "household":
postAddressToHousehold(payload.targetId, payload.addressId)
.then(
(address) =>
new Promise((resolve, reject) => {
console.log("..household address", address);
this.$refs.addAddress.flag.loading = false;
this.$refs.addAddress.flag.success = true;
// finish
this.$refs.addAddress.afterLastPaneAction({
addressId: address.address_id,
});
resolve();
}),
)
.catch((error) => {
this.$refs.addAddress.errorMsg.push(error);
this.$refs.addAddress.flag.loading = false;
});
break;
case "person":
postAddressToPerson(payload.targetId, payload.addressId)
.then(
(address) =>
new Promise((resolve, reject) => {
console.log("..person address", address);
this.$refs.addAddress.flag.loading = false;
this.$refs.addAddress.flag.success = true;
// finish
this.$refs.addAddress.afterLastPaneAction({
addressId: address.address_id,
});
resolve();
}),
)
.catch((error) => {
this.$refs.addAddress.errorMsg.push(error);
this.$refs.addAddress.flag.loading = false;
});
break;
case "thirdparty":
console.log("TODO write postAddressToThirdparty");
break;
default:
this.$refs.addAddress.errorMsg.push(
"That entity is not managed by address !",
);
}
},
}, },
submitAddress(payload) {
console.log("@@@ click on Submit Address Button", payload);
// Existing address
if (this.context.edit) {
// address is already linked, just finish !
this.$refs.addAddress.afterLastPaneAction({});
this.$emit("addressEdited", payload);
// New created address
} else {
this.postAddressTo(payload);
}
},
/*
* Post new created address to targetEntity
*/
postAddressTo(payload) {
this.$emit("addressCreated", payload);
console.log(
"postAddress",
payload.addressId,
"To",
payload.target,
payload.targetId,
);
switch (payload.target) {
case "household":
postAddressToHousehold(payload.targetId, payload.addressId)
.then(
(address) =>
new Promise((resolve, reject) => {
console.log("..household address", address);
this.$refs.addAddress.flag.loading = false;
this.$refs.addAddress.flag.success = true;
// finish
this.$refs.addAddress.afterLastPaneAction({
addressId: address.address_id,
});
resolve();
}),
)
.catch((error) => {
this.$refs.addAddress.errorMsg.push(error);
this.$refs.addAddress.flag.loading = false;
});
break;
case "person":
postAddressToPerson(payload.targetId, payload.addressId)
.then(
(address) =>
new Promise((resolve, reject) => {
console.log("..person address", address);
this.$refs.addAddress.flag.loading = false;
this.$refs.addAddress.flag.success = true;
// finish
this.$refs.addAddress.afterLastPaneAction({
addressId: address.address_id,
});
resolve();
}),
)
.catch((error) => {
this.$refs.addAddress.errorMsg.push(error);
this.$refs.addAddress.flag.loading = false;
});
break;
case "thirdparty":
console.log("TODO write postAddressToThirdparty");
break;
default:
this.$refs.addAddress.errorMsg.push(
"That entity is not managed by address !",
);
}
},
},
}; };
</script> </script>

View File

@@ -1,32 +1,32 @@
<template> <template>
<ul <ul
class="record_actions" class="record_actions"
v-if="!options.onlyButton" v-if="!options.onlyButton"
:class="{ 'sticky-form-buttons': isStickyForm }" :class="{ 'sticky-form-buttons': isStickyForm }"
> >
<li v-if="isStickyForm" class="cancel"> <li v-if="isStickyForm" class="cancel">
<slot name="before" /> <slot name="before" />
</li> </li>
<li> <li>
<slot name="action" /> <slot name="action" />
</li> </li>
<li v-if="isStickyForm"> <li v-if="isStickyForm">
<slot name="after" /> <slot name="after" />
</li> </li>
</ul> </ul>
<slot v-else name="action" /> <slot v-else name="action" />
</template> </template>
<script> <script>
export default { export default {
name: "ActionButtons", name: "ActionButtons",
props: ["options", "defaultz"], props: ["options", "defaultz"],
computed: { computed: {
isStickyForm() { isStickyForm() {
return typeof this.options.stickyActions !== "undefined" return typeof this.options.stickyActions !== "undefined"
? this.options.stickyActions ? this.options.stickyActions
: this.defaultz.stickyActions; : this.defaultz.stickyActions;
},
}, },
},
}; };
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div id="address_map" /> <div id="address_map" />
</template> </template>
<script> <script>
@@ -8,107 +8,112 @@ import markerIconPng from "leaflet/dist/images/marker-icon.png";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
const lonLatForLeaflet = (coordinates) => { const lonLatForLeaflet = (coordinates) => {
return [coordinates[1], coordinates[0]]; return [coordinates[1], coordinates[0]];
}; };
export default { export default {
name: "AddressMap", name: "AddressMap",
props: ["entity"], props: ["entity"],
data() { data() {
return { return {
map: null, map: null,
marker: null, marker: null,
}; };
},
computed: {
center() {
return this.entity.addressMap.center;
}, },
hasAddressPoint() { computed: {
if (Object.keys(this.entity.address).length === 0) { center() {
return false; return this.entity.addressMap.center;
} },
if (null !== this.entity.address.addressReference) { hasAddressPoint() {
return true; if (Object.keys(this.entity.address).length === 0) {
} return false;
if ( }
null !== this.entity.address.postcode && if (null !== this.entity.address.addressReference) {
null !== this.entity.address.postcode.center return true;
) { }
return true; if (
} null !== this.entity.address.postcode &&
return false; null !== this.entity.address.postcode.center
) {
return true;
}
return false;
},
/**
*
* @returns {coordinates: [float, float], type: "Point"}
*/
addressPoint() {
if (Object.keys(this.entity.address).length === 0) {
return null;
}
if (null !== this.entity.address.addressReference) {
return this.entity.address.addressReference.point;
}
if (
null !== this.entity.address.postcode &&
null !== this.entity.address.postcode.center
) {
return this.entity.address.postcode.center;
}
return null;
},
}, },
/** methods: {
* init() {
* @returns {coordinates: [float, float], type: "Point"} this.map = L.map("address_map");
*/
addressPoint() {
if (Object.keys(this.entity.address).length === 0) {
return null;
}
if (null !== this.entity.address.addressReference) { if (!this.hasAddressPoint) {
return this.entity.address.addressReference.point; this.map.setView(
} lonLatForLeaflet(this.entity.addressMap.center),
this.entity.addressMap.zoom,
);
} else {
this.map.setView(
lonLatForLeaflet(this.addressPoint.coordinates),
15,
);
}
if ( this.map.scrollWheelZoom.disable();
null !== this.entity.address.postcode &&
null !== this.entity.address.postcode.center
) {
return this.entity.address.postcode.center;
}
return null; L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(this.map);
const markerIcon = L.icon({
iconUrl: markerIconPng,
iconAnchor: [12, 41],
});
if (!this.hasAddressPoint) {
this.marker = L.marker(
lonLatForLeaflet(this.entity.addressMap.center),
{ icon: markerIcon },
);
} else {
this.marker = L.marker(
lonLatForLeaflet(this.addressPoint.coordinates),
{ icon: markerIcon },
);
}
this.marker.addTo(this.map);
},
update() {
if (this.marker && this.entity.addressMap.center) {
this.marker.setLatLng(
lonLatForLeaflet(this.entity.addressMap.center),
);
this.map.panTo(lonLatForLeaflet(this.entity.addressMap.center));
}
},
}, },
}, mounted() {
methods: { this.init();
init() {
this.map = L.map("address_map");
if (!this.hasAddressPoint) {
this.map.setView(
lonLatForLeaflet(this.entity.addressMap.center),
this.entity.addressMap.zoom,
);
} else {
this.map.setView(lonLatForLeaflet(this.addressPoint.coordinates), 15);
}
this.map.scrollWheelZoom.disable();
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(this.map);
const markerIcon = L.icon({
iconUrl: markerIconPng,
iconAnchor: [12, 41],
});
if (!this.hasAddressPoint) {
this.marker = L.marker(
lonLatForLeaflet(this.entity.addressMap.center),
{ icon: markerIcon },
);
} else {
this.marker = L.marker(
lonLatForLeaflet(this.addressPoint.coordinates),
{ icon: markerIcon },
);
}
this.marker.addTo(this.map);
}, },
update() {
if (this.marker && this.entity.addressMap.center) {
this.marker.setLatLng(lonLatForLeaflet(this.entity.addressMap.center));
this.map.panTo(lonLatForLeaflet(this.entity.addressMap.center));
}
},
},
mounted() {
this.init();
},
}; };
</script> </script>

View File

@@ -1,149 +1,149 @@
<template> <template>
<h4 class="h3"> <h4 class="h3">
{{ $t("fill_an_address") }} {{ $t("fill_an_address") }}
</h4> </h4>
<div class="row my-3"> <div class="row my-3">
<div class="col-lg-6" v-if="!isNoAddress"> <div class="col-lg-6" v-if="!isNoAddress">
<div class="form-floating my-1"> <div class="form-floating my-1">
<input <input
class="form-control" class="form-control"
type="text" type="text"
name="floor" name="floor"
:placeholder="$t('floor')" :placeholder="$t('floor')"
v-model="floor" v-model="floor"
/> />
<label for="floor">{{ $t("floor") }}</label> <label for="floor">{{ $t("floor") }}</label>
</div> </div>
<div class="form-floating my-1"> <div class="form-floating my-1">
<input <input
class="form-control" class="form-control"
type="text" type="text"
name="corridor" name="corridor"
:placeholder="$t('corridor')" :placeholder="$t('corridor')"
v-model="corridor" v-model="corridor"
/> />
<label for="corridor">{{ $t("corridor") }}</label> <label for="corridor">{{ $t("corridor") }}</label>
</div> </div>
<div class="form-floating my-1"> <div class="form-floating my-1">
<input <input
class="form-control" class="form-control"
type="text" type="text"
name="steps" name="steps"
:placeholder="$t('steps')" :placeholder="$t('steps')"
v-model="steps" v-model="steps"
/> />
<label for="steps">{{ $t("steps") }}</label> <label for="steps">{{ $t("steps") }}</label>
</div> </div>
<div class="form-floating my-1"> <div class="form-floating my-1">
<input <input
class="form-control" class="form-control"
type="text" type="text"
name="flat" name="flat"
:placeholder="$t('flat')" :placeholder="$t('flat')"
v-model="flat" v-model="flat"
/> />
<label for="flat">{{ $t("flat") }}</label> <label for="flat">{{ $t("flat") }}</label>
</div> </div>
</div>
<div :class="isNoAddress ? 'col-lg-12' : 'col-lg-6'">
<div class="form-floating my-1" v-if="!isNoAddress">
<input
class="form-control"
type="text"
name="buildingName"
maxlength="255"
:placeholder="$t('buildingName')"
v-model="buildingName"
/>
<label for="buildingName">{{ $t("buildingName") }}</label>
</div>
<div class="form-floating my-1">
<input
class="form-control"
type="text"
name="extra"
maxlength="255"
:placeholder="$t('extra')"
v-model="extra"
/>
<label for="extra">{{ $t("extra") }}</label>
</div>
<div class="form-floating my-1" v-if="!isNoAddress">
<input
class="form-control"
type="text"
name="distribution"
maxlength="255"
:placeholder="$t('distribution')"
v-model="distribution"
/>
<label for="distribution">{{ $t("distribution") }}</label>
</div>
</div>
</div> </div>
<div :class="isNoAddress ? 'col-lg-12' : 'col-lg-6'">
<div class="form-floating my-1" v-if="!isNoAddress">
<input
class="form-control"
type="text"
name="buildingName"
maxlength="255"
:placeholder="$t('buildingName')"
v-model="buildingName"
/>
<label for="buildingName">{{ $t("buildingName") }}</label>
</div>
<div class="form-floating my-1">
<input
class="form-control"
type="text"
name="extra"
maxlength="255"
:placeholder="$t('extra')"
v-model="extra"
/>
<label for="extra">{{ $t("extra") }}</label>
</div>
<div class="form-floating my-1" v-if="!isNoAddress">
<input
class="form-control"
type="text"
name="distribution"
maxlength="255"
:placeholder="$t('distribution')"
v-model="distribution"
/>
<label for="distribution">{{ $t("distribution") }}</label>
</div>
</div>
</div>
</template> </template>
<script> <script>
export default { export default {
name: "AddressMore", name: "AddressMore",
props: ["entity", "isNoAddress"], props: ["entity", "isNoAddress"],
computed: { computed: {
floor: { floor: {
set(value) { set(value) {
this.entity.selected.address.floor = value; this.entity.selected.address.floor = value;
}, },
get() { get() {
return this.entity.selected.address.floor; return this.entity.selected.address.floor;
}, },
},
corridor: {
set(value) {
this.entity.selected.address.corridor = value;
},
get() {
return this.entity.selected.address.corridor;
},
},
steps: {
set(value) {
this.entity.selected.address.steps = value;
},
get() {
return this.entity.selected.address.steps;
},
},
flat: {
set(value) {
this.entity.selected.address.flat = value;
},
get() {
return this.entity.selected.address.flat;
},
},
buildingName: {
set(value) {
this.entity.selected.address.buildingName = value;
},
get() {
return this.entity.selected.address.buildingName;
},
},
extra: {
set(value) {
this.entity.selected.address.extra = value;
},
get() {
return this.entity.selected.address.extra;
},
},
distribution: {
set(value) {
this.entity.selected.address.distribution = value;
},
get() {
return this.entity.selected.address.distribution;
},
},
}, },
corridor: {
set(value) {
this.entity.selected.address.corridor = value;
},
get() {
return this.entity.selected.address.corridor;
},
},
steps: {
set(value) {
this.entity.selected.address.steps = value;
},
get() {
return this.entity.selected.address.steps;
},
},
flat: {
set(value) {
this.entity.selected.address.flat = value;
},
get() {
return this.entity.selected.address.flat;
},
},
buildingName: {
set(value) {
this.entity.selected.address.buildingName = value;
},
get() {
return this.entity.selected.address.buildingName;
},
},
extra: {
set(value) {
this.entity.selected.address.extra = value;
},
get() {
return this.entity.selected.address.extra;
},
},
distribution: {
set(value) {
this.entity.selected.address.distribution = value;
},
get() {
return this.entity.selected.address.distribution;
},
},
},
}; };
</script> </script>

View File

@@ -1,223 +1,227 @@
<template> <template>
<div class="my-1"> <div class="my-1">
<label class="col-form-label" for="addressSelector">{{ <label class="col-form-label" for="addressSelector">{{
$t("address") $t("address")
}}</label> }}</label>
<VueMultiselect <VueMultiselect
id="addressSelector" id="addressSelector"
v-model="value" v-model="value"
:placeholder="$t('select_address')" :placeholder="$t('select_address')"
:tag-placeholder="$t('create_address')" :tag-placeholder="$t('create_address')"
:select-label="$t('multiselect.select_label')" :select-label="$t('multiselect.select_label')"
:deselect-label="$t('create_address')" :deselect-label="$t('create_address')"
:selected-label="$t('multiselect.selected_label')" :selected-label="$t('multiselect.selected_label')"
@search-change="listenInputSearch" @search-change="listenInputSearch"
:internal-search="false" :internal-search="false"
ref="addressSelector" ref="addressSelector"
@select="selectAddress" @select="selectAddress"
@remove="remove" @remove="remove"
name="field" name="field"
track-by="id" track-by="id"
label="value" label="value"
:custom-label="transName" :custom-label="transName"
:taggable="true" :taggable="true"
:multiple="false" :multiple="false"
@tag="addAddress" @tag="addAddress"
:loading="isLoading" :loading="isLoading"
:options="addresses" :options="addresses"
/> />
</div> </div>
<div <div
class="custom-address row g-1" class="custom-address row g-1"
v-if=" v-if="
writeNewAddress || writeNewAddress ||
writeNewPostalCode || writeNewPostalCode ||
(isEnteredCustomAddress && !isAddressSelectorOpen) (isEnteredCustomAddress && !isAddressSelectorOpen)
" "
> >
<div class="col-10"> <div class="col-10">
<div class="form-floating"> <div class="form-floating">
<input <input
class="form-control" class="form-control"
type="text" type="text"
name="street" name="street"
:placeholder="$t('street')" :placeholder="$t('street')"
v-model="street" v-model="street"
/> />
<label for="street">{{ $t("street") }}</label> <label for="street">{{ $t("street") }}</label>
</div> </div>
</div>
<div class="col-2">
<div class="form-floating">
<input
class="form-control"
type="text"
name="streetNumber"
:placeholder="$t('streetNumber')"
v-model="streetNumber"
/>
<label for="streetNumber">{{ $t("streetNumber") }}</label>
</div>
</div>
</div> </div>
<div class="col-2">
<div class="form-floating">
<input
class="form-control"
type="text"
name="streetNumber"
:placeholder="$t('streetNumber')"
v-model="streetNumber"
/>
<label for="streetNumber">{{ $t("streetNumber") }}</label>
</div>
</div>
</div>
</template> </template>
<script> <script>
import VueMultiselect from "vue-multiselect"; import VueMultiselect from "vue-multiselect";
import { import {
searchReferenceAddresses, searchReferenceAddresses,
fetchReferenceAddresses, fetchReferenceAddresses,
} from "../../api.js"; } from "../../api.js";
export default { export default {
name: "AddressSelection", name: "AddressSelection",
components: { VueMultiselect }, components: { VueMultiselect },
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
isLoading: false, ? this.entity.address.addressReference
}; : null,
}, isLoading: false,
computed: { };
writeNewAddress() {
return this.entity.selected.writeNew.address;
}, },
writeNewPostalCode() { computed: {
return this.entity.selected.writeNew.postCode; writeNewAddress() {
return this.entity.selected.writeNew.address;
},
writeNewPostalCode() {
return this.entity.selected.writeNew.postCode;
},
isAddressSelectorOpen() {
return this.$refs.addressSelector.$data.isOpen;
},
isEnteredCustomAddress() {
return (
this.$data.value !== null &&
typeof this.$data.value.text !== "undefined"
);
},
addresses() {
return this.entity.loaded.addresses;
},
street: {
set(value) {
this.entity.selected.address.street = value;
},
get() {
return this.entity.selected.address.street;
},
},
streetNumber: {
set(value) {
this.entity.selected.address.streetNumber = value;
},
get() {
return this.entity.selected.address.streetNumber;
},
},
}, },
isAddressSelectorOpen() { methods: {
return this.$refs.addressSelector.$data.isOpen; transName(value) {
}, return value.streetNumber === undefined
isEnteredCustomAddress() { ? value.street
return ( : `${value.streetNumber}, ${value.street}`;
this.$data.value !== null && },
typeof this.$data.value.text !== "undefined" selectAddress(value) {
); this.entity.selected.address = value;
}, this.entity.selected.address.addressReference = {
addresses() { id: value.id,
return this.entity.loaded.addresses; };
}, this.entity.selected.address.street = value.street;
street: { this.entity.selected.address.streetNumber = value.streetNumber;
set(value) { this.entity.selected.writeNew.address = false;
this.entity.selected.address.street = value; this.updateMapCenter(value.point);
}, this.checkErrors();
get() { },
return this.entity.selected.address.street; remove() {
}, this.entity.selected.address = {};
}, this.checkErrors();
streetNumber: { },
set(value) { listenInputSearch(query) {
this.entity.selected.address.streetNumber = value; //console.log('listenInputSearch', query, this.isAddressSelectorOpen);
}, if (
get() { !this.entity.selected.writeNew.postcode &&
return this.entity.selected.address.streetNumber; "id" in this.entity.selected.city
}, ) {
}, if (query.length > 2) {
}, this.isLoading = true;
methods: { searchReferenceAddresses(query, this.entity.selected.city)
transName(value) { .then(
return value.streetNumber === undefined (addresses) =>
? value.street new Promise((resolve, reject) => {
: `${value.streetNumber}, ${value.street}`; this.entity.loaded.addresses =
}, addresses.results;
selectAddress(value) { this.isLoading = false;
this.entity.selected.address = value; resolve();
this.entity.selected.address.addressReference = { }),
id: value.id, )
}; .catch((error) => {
this.entity.selected.address.street = value.street; console.log(error); //TODO better error handling
this.entity.selected.address.streetNumber = value.streetNumber; this.isLoading = false;
this.entity.selected.writeNew.address = false; });
this.updateMapCenter(value.point); } else {
this.checkErrors(); if (query.length === 0) {
}, // Fetch all cities when suppressing the query
remove() { this.isLoading = true;
this.entity.selected.address = {}; fetchReferenceAddresses(this.entity.selected.city)
this.checkErrors(); .then(
}, (addresses) =>
listenInputSearch(query) { new Promise((resolve, reject) => {
//console.log('listenInputSearch', query, this.isAddressSelectorOpen); this.entity.loaded.addresses =
if ( addresses.results;
!this.entity.selected.writeNew.postcode && this.isLoading = false;
"id" in this.entity.selected.city resolve();
) { }),
if (query.length > 2) { )
this.isLoading = true; .catch((error) => {
searchReferenceAddresses(query, this.entity.selected.city) console.log(error);
.then( this.isLoading = false;
(addresses) => });
new Promise((resolve, reject) => { }
this.entity.loaded.addresses = addresses.results; }
this.isLoading = false; }
resolve();
}),
)
.catch((error) => {
console.log(error); //TODO better error handling
this.isLoading = false;
});
} else {
if (query.length === 0) {
// Fetch all cities when suppressing the query
this.isLoading = true;
fetchReferenceAddresses(this.entity.selected.city)
.then(
(addresses) =>
new Promise((resolve, reject) => {
this.entity.loaded.addresses = addresses.results;
this.isLoading = false;
resolve();
}),
)
.catch((error) => {
console.log(error);
this.isLoading = false;
});
}
}
}
if (this.isAddressSelectorOpen) { if (this.isAddressSelectorOpen) {
this.$data.value = { text: query }; this.$data.value = { text: query };
} else if (this.isEnteredCustomAddress) { } else if (this.isEnteredCustomAddress) {
let addr = this.splitAddress(this.$data.value.text); let addr = this.splitAddress(this.$data.value.text);
this.entity.selected.address.street = addr.street; this.entity.selected.address.street = addr.street;
this.entity.selected.address.streetNumber = addr.number; this.entity.selected.address.streetNumber = addr.number;
this.entity.selected.writeNew.address = true; this.entity.selected.writeNew.address = true;
this.checkErrors(); this.checkErrors();
} }
},
splitAddress(address) {
let substr = address.split(",").map((s) => s.trim());
if (substr.length === 1) {
substr = address.split(" ");
}
let decimal = [];
substr.forEach((s, i) => {
decimal[i] = /^\d+$/.test(s);
});
if (decimal[0] === true) {
return {
number: substr.shift(),
street: substr.join(" "),
};
} else if (decimal[decimal.length - 1] === true) {
return {
number: substr.pop(),
street: substr.join(" "),
};
}
return {
number: "",
street: substr.join(" "),
};
},
addAddress() {
this.entity.selected.writeNew.address = true;
},
}, },
splitAddress(address) {
let substr = address.split(",").map((s) => s.trim());
if (substr.length === 1) {
substr = address.split(" ");
}
let decimal = [];
substr.forEach((s, i) => {
decimal[i] = /^\d+$/.test(s);
});
if (decimal[0] === true) {
return {
number: substr.shift(),
street: substr.join(" "),
};
} else if (decimal[decimal.length - 1] === true) {
return {
number: substr.pop(),
street: substr.join(" "),
};
}
return {
number: "",
street: substr.join(" "),
};
},
addAddress() {
this.entity.selected.writeNew.address = true;
},
},
}; };
</script> </script>

View File

@@ -1,60 +1,60 @@
<template> <template>
<div class="my-1"> <div class="my-1">
<label class="col-form-label">{{ $t("city") }}</label> <label class="col-form-label">{{ $t("city") }}</label>
<VueMultiselect <VueMultiselect
id="citySelector" id="citySelector"
v-model="value" v-model="value"
@search-change="listenInputSearch" @search-change="listenInputSearch"
ref="citySelector" ref="citySelector"
@select="selectCity" @select="selectCity"
@remove="remove" @remove="remove"
name="field" name="field"
track-by="id" track-by="id"
label="value" label="value"
:custom-label="transName" :custom-label="transName"
:placeholder="$t('select_city')" :placeholder="$t('select_city')"
:select-label="$t('multiselect.select_label')" :select-label="$t('multiselect.select_label')"
:deselect-label="$t('create_postal_code')" :deselect-label="$t('create_postal_code')"
:selected-label="$t('multiselect.selected_label')" :selected-label="$t('multiselect.selected_label')"
:taggable="true" :taggable="true"
:multiple="false" :multiple="false"
:internal-search="false" :internal-search="false"
@tag="addPostcode" @tag="addPostcode"
:tag-placeholder="$t('create_postal_code')" :tag-placeholder="$t('create_postal_code')"
:loading="isLoading" :loading="isLoading"
:options="cities" :options="cities"
/> />
</div> </div>
<div <div
class="custom-postcode row g-1" class="custom-postcode row g-1"
v-if="writeNewPostcode || (isEnteredCustomCity && !isCitySelectorOpen)" v-if="writeNewPostcode || (isEnteredCustomCity && !isCitySelectorOpen)"
> >
<div class="col-4"> <div class="col-4">
<div class="form-floating"> <div class="form-floating">
<input <input
class="form-control" class="form-control"
type="text" type="text"
id="code" id="code"
:placeholder="$t('postalCode_code')" :placeholder="$t('postalCode_code')"
v-model="code" v-model="code"
/> />
<label for="code">{{ $t("postalCode_code") }}</label> <label for="code">{{ $t("postalCode_code") }}</label>
</div> </div>
</div>
<div class="col-8">
<div class="form-floating">
<input
class="form-control"
type="text"
id="name"
:placeholder="$t('postalCode_name')"
v-model="name"
/>
<label for="name">{{ $t("postalCode_name") }}</label>
</div>
</div>
</div> </div>
<div class="col-8">
<div class="form-floating">
<input
class="form-control"
type="text"
id="name"
:placeholder="$t('postalCode_name')"
v-model="name"
/>
<label for="name">{{ $t("postalCode_name") }}</label>
</div>
</div>
</div>
</template> </template>
<script> <script>
@@ -62,185 +62,190 @@ import VueMultiselect from "vue-multiselect";
import { searchCities, fetchCities } from "../../api.js"; import { searchCities, fetchCities } from "../../api.js";
export default { export default {
name: "CitySelection", name: "CitySelection",
components: { VueMultiselect }, components: { VueMultiselect },
props: [ props: [
"entity", "entity",
"context", "context",
"focusOnAddress", "focusOnAddress",
"updateMapCenter", "updateMapCenter",
"flag", "flag",
"checkErrors", "checkErrors",
], ],
emits: ["getReferenceAddresses"], emits: ["getReferenceAddresses"],
data() { data() {
return { return {
value: this.context.edit ? this.entity.address.postcode : null, value: this.context.edit ? this.entity.address.postcode : null,
isLoading: false, isLoading: false,
}; };
},
computed: {
writeNewPostcode() {
return this.entity.selected.writeNew.postcode;
}, },
isCitySelectorOpen() { computed: {
return this.$refs.citySelector.$data.isOpen; writeNewPostcode() {
return this.entity.selected.writeNew.postcode;
},
isCitySelectorOpen() {
return this.$refs.citySelector.$data.isOpen;
},
isEnteredCustomCity() {
return (
this.$data.value !== null &&
typeof this.$data.value.text !== "undefined"
);
},
cities() {
return this.entity.loaded.cities.sort(
(a, b) => Number(a.code) - Number(b.code) || a.name > b.name,
);
},
name: {
set(value) {
this.entity.selected.postcode.name = value;
},
get() {
return this.entity.selected.postcode.name;
},
},
code: {
set(value) {
this.entity.selected.postcode.code = value;
},
get() {
return this.entity.selected.postcode.code;
},
},
}, },
isEnteredCustomCity() { mounted() {
return ( console.log(
this.$data.value !== null && "writeNew.postcode",
typeof this.$data.value.text !== "undefined" this.entity.selected.writeNew.postcode,
); "in mounted",
}, );
cities() { if (this.context.edit) {
return this.entity.loaded.cities.sort( this.entity.selected.city = this.value;
(a, b) => Number(a.code) - Number(b.code) || a.name > b.name, this.entity.selected.postcode.name = this.value.name;
); this.entity.selected.postcode.code = this.value.code;
}, this.$emit("getReferenceAddresses", this.value);
name: { if (typeof this.value.center !== "undefined") {
set(value) { this.updateMapCenter(this.value.center);
this.entity.selected.postcode.name = value; if (this.value.center.coordinates) {
}, this.entity.selected.postcode.coordinates =
get() { this.value.center.coordinates;
return this.entity.selected.postcode.name; }
}, }
},
code: {
set(value) {
this.entity.selected.postcode.code = value;
},
get() {
return this.entity.selected.postcode.code;
},
},
},
mounted() {
console.log(
"writeNew.postcode",
this.entity.selected.writeNew.postcode,
"in mounted",
);
if (this.context.edit) {
this.entity.selected.city = this.value;
this.entity.selected.postcode.name = this.value.name;
this.entity.selected.postcode.code = this.value.code;
this.$emit("getReferenceAddresses", this.value);
if (typeof this.value.center !== "undefined") {
this.updateMapCenter(this.value.center);
if (this.value.center.coordinates) {
this.entity.selected.postcode.coordinates =
this.value.center.coordinates;
} }
}
}
},
methods: {
transName(value) {
return value.code && value.name ? `${value.name} (${value.code})` : "";
}, },
selectCity(value) { methods: {
console.log(value); transName(value) {
this.entity.selected.city = value; return value.code && value.name
this.entity.selected.postcode.name = value.name; ? `${value.name} (${value.code})`
this.entity.selected.postcode.code = value.code; : "";
if (value.center) { },
this.entity.selected.postcode.coordinates = value.center.coordinates; selectCity(value) {
} console.log(value);
this.entity.selected.writeNew.postcode = false; this.entity.selected.city = value;
this.$emit("getReferenceAddresses", value); this.entity.selected.postcode.name = value.name;
this.focusOnAddress(); this.entity.selected.postcode.code = value.code;
if (value.center) { if (value.center) {
this.updateMapCenter(value.center); this.entity.selected.postcode.coordinates =
} value.center.coordinates;
this.checkErrors(); }
}, this.entity.selected.writeNew.postcode = false;
remove() { this.$emit("getReferenceAddresses", value);
this.entity.selected.city = {}; this.focusOnAddress();
this.checkErrors(); if (value.center) {
}, this.updateMapCenter(value.center);
listenInputSearch(query) { }
if (query.length > 2) { this.checkErrors();
this.isLoading = true; },
searchCities(query, this.entity.selected.country) remove() {
.then( this.entity.selected.city = {};
(cities) => this.checkErrors();
new Promise((resolve, reject) => { },
this.entity.loaded.cities = cities.results.filter( listenInputSearch(query) {
(c) => c.origin !== 3, if (query.length > 2) {
); // filter out user-defined cities this.isLoading = true;
this.isLoading = false; searchCities(query, this.entity.selected.country)
resolve(); .then(
}), (cities) =>
) new Promise((resolve, reject) => {
.catch((error) => { this.entity.loaded.cities =
console.log(error); //TODO better error handling cities.results.filter(
this.isLoading = false; (c) => c.origin !== 3,
}); ); // filter out user-defined cities
} else { this.isLoading = false;
if (query.length === 0) { resolve();
// Fetch all cities when suppressing the query }),
this.isLoading = true; )
fetchCities(this.entity.selected.country) .catch((error) => {
.then( console.log(error); //TODO better error handling
(cities) => this.isLoading = false;
new Promise((resolve, reject) => { });
this.entity.loaded.cities = cities.results.filter( } else {
(c) => c.origin !== 3, if (query.length === 0) {
); // filter out user-defined cities // Fetch all cities when suppressing the query
this.isLoading = false; this.isLoading = true;
resolve(); fetchCities(this.entity.selected.country)
}), .then(
) (cities) =>
.catch((error) => { new Promise((resolve, reject) => {
console.log(error); this.entity.loaded.cities =
this.isLoading = false; cities.results.filter(
(c) => c.origin !== 3,
); // filter out user-defined cities
this.isLoading = false;
resolve();
}),
)
.catch((error) => {
console.log(error);
this.isLoading = false;
});
}
}
if (this.isCitySelectorOpen) {
this.$data.value = { text: query };
} else if (this.isEnteredCustomCity) {
let city = this.splitCity(this.$data.value.text);
this.$refs.citySelector.currentOptionLabel = "";
this.entity.selected.city = city;
this.entity.selected.postcode.name = city.name;
this.entity.selected.postcode.code = city.code;
this.entity.selected.writeNew.postcode = true;
console.log("writeNew.postcode true, in listenInputSearch");
}
},
splitCity(city) {
let substr = city.split("-").map((s) => s.trim());
if (substr.length === 1) {
substr = city.split(" ");
}
//console.log('substr', substr);
let decimal = [];
substr.forEach((s, i) => {
decimal[i] = /^\d+$/.test(s);
}); });
} if (decimal[0] === true) {
} return {
if (this.isCitySelectorOpen) { code: substr.shift(),
this.$data.value = { text: query }; name: substr.join(" "),
} else if (this.isEnteredCustomCity) { };
let city = this.splitCity(this.$data.value.text); } else if (decimal[decimal.length - 1] === true) {
this.$refs.citySelector.currentOptionLabel = ""; return {
this.entity.selected.city = city; code: substr.pop(),
this.entity.selected.postcode.name = city.name; name: substr.join(" "),
this.entity.selected.postcode.code = city.code; };
this.entity.selected.writeNew.postcode = true; }
console.log("writeNew.postcode true, in listenInputSearch"); return {
} code: "",
name: substr.join(" "),
};
},
addPostcode() {
console.log("addPostcode: pass here ?? never, it seems");
this.entity.selected.writeNew.postcode = true;
console.log("writeNew.postcode true, in addPostcode");
},
}, },
splitCity(city) {
let substr = city.split("-").map((s) => s.trim());
if (substr.length === 1) {
substr = city.split(" ");
}
//console.log('substr', substr);
let decimal = [];
substr.forEach((s, i) => {
decimal[i] = /^\d+$/.test(s);
});
if (decimal[0] === true) {
return {
code: substr.shift(),
name: substr.join(" "),
};
} else if (decimal[decimal.length - 1] === true) {
return {
code: substr.pop(),
name: substr.join(" "),
};
}
return {
code: "",
name: substr.join(" "),
};
},
addPostcode() {
console.log("addPostcode: pass here ?? never, it seems");
this.entity.selected.writeNew.postcode = true;
console.log("writeNew.postcode true, in addPostcode");
},
},
}; };
</script> </script>

View File

@@ -1,23 +1,23 @@
<template> <template>
<div class="my-1"> <div class="my-1">
<label class="col-form-label" for="countrySelect">{{ <label class="col-form-label" for="countrySelect">{{
$t("country") $t("country")
}}</label> }}</label>
<VueMultiselect <VueMultiselect
id="countrySelect" id="countrySelect"
label="name" label="name"
track-by="id" track-by="id"
:custom-label="transName" :custom-label="transName"
:placeholder="$t('select_country')" :placeholder="$t('select_country')"
:options="sortedCountries" :options="sortedCountries"
v-model="value" v-model="value"
:select-label="$t('multiselect.select_label')" :select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')" :deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')" :selected-label="$t('multiselect.selected_label')"
@select="selectCountry" @select="selectCountry"
@remove="remove" @remove="remove"
/> />
</div> </div>
</template> </template>
<script> <script>
@@ -25,59 +25,63 @@ import VueMultiselect from "vue-multiselect";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
export default { export default {
name: "CountrySelection", name: "CountrySelection",
components: { VueMultiselect }, components: { VueMultiselect },
props: ["context", "entity", "flag", "checkErrors"], props: ["context", "entity", "flag", "checkErrors"],
emits: ["getCities"], emits: ["getCities"],
data() { data() {
return { return {
value: this.selectCountryByCode( value: this.selectCountryByCode(
this.context.edit this.context.edit
? this.entity.selected.country.code ? this.entity.selected.country.code
: this.context.defaults.default_country, : this.context.defaults.default_country,
), ),
}; };
},
computed: {
sortedCountries() {
const countries = this.entity.loaded.countries;
let sortedCountries = [];
sortedCountries.push(...countries.filter((c) => c.countryCode === "FR"));
sortedCountries.push(...countries.filter((c) => c.countryCode === "BE"));
sortedCountries.push(
...countries
.filter((c) => c.countryCode !== "FR")
.filter((c) => c.countryCode !== "BE"),
);
return sortedCountries;
}, },
}, computed: {
mounted() { sortedCountries() {
console.log("country selection mounted", this.value); const countries = this.entity.loaded.countries;
if (this.value !== undefined) { let sortedCountries = [];
this.selectCountry(this.value); sortedCountries.push(
} ...countries.filter((c) => c.countryCode === "FR"),
}, );
methods: { sortedCountries.push(
selectCountryByCode(countryCode) { ...countries.filter((c) => c.countryCode === "BE"),
return this.entity.loaded.countries.filter( );
(c) => c.countryCode === countryCode, sortedCountries.push(
)[0]; ...countries
.filter((c) => c.countryCode !== "FR")
.filter((c) => c.countryCode !== "BE"),
);
return sortedCountries;
},
}, },
transName({ name }) { mounted() {
return localizeString(name); console.log("country selection mounted", this.value);
if (this.value !== undefined) {
this.selectCountry(this.value);
}
}, },
selectCountry(value) { methods: {
//console.log('select country', value); selectCountryByCode(countryCode) {
this.entity.selected.country = value; return this.entity.loaded.countries.filter(
this.$emit("getCities", value); (c) => c.countryCode === countryCode,
this.checkErrors(); )[0];
},
transName({ name }) {
return localizeString(name);
},
selectCountry(value) {
//console.log('select country', value);
this.entity.selected.country = value;
this.$emit("getCities", value);
this.checkErrors();
},
remove() {
this.entity.selected.country = null;
this.checkErrors();
},
}, },
remove() {
this.entity.selected.country = null;
this.checkErrors();
},
},
}; };
</script> </script>

View File

@@ -1,66 +1,69 @@
<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
<span class="sr-only">{{ $t("loading") }}</span> v-if="flag.loading"
</div> class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"
<div v-if="errorMsg && errorMsg.length > 0" class="alert alert-danger">
{{ errorMsg }}
</div>
<address-render-box :address="selectedAddress" />
<div class="row">
<div v-if="showDateFrom" class="col-lg-6 address-valid date-since">
<h3>{{ $t(getValidFromDateText) }}</h3>
<div class="input-group mb-3">
<span class="input-group-text" id="validFrom"
><i class="fa fa-fw fa-calendar"
/></span>
<input
type="date"
class="form-control form-control-lg"
name="validFrom"
:placeholder="$t(getValidFromDateText)"
v-model="validFrom"
aria-describedby="validFrom"
/> />
</div> <span class="sr-only">{{ $t("loading") }}</span>
</div> </div>
<div v-if="showDateTo" class="col-lg-6 address-valid date-until"> <div v-if="errorMsg && errorMsg.length > 0" class="alert alert-danger">
<h3>{{ $t(getValidToDateText) }}</h3> {{ errorMsg }}
<div class="input-group mb-3">
<span class="input-group-text" id="validTo"
><i class="fa fa-fw fa-calendar"
/></span>
<input
type="date"
class="form-control form-control-lg"
name="validTo"
:placeholder="$t(getValidToDateText)"
v-model="validTo"
aria-describedby="validTo"
/>
</div>
</div> </div>
</div>
<action-buttons <address-render-box :address="selectedAddress" />
v-if="insideModal === false"
:options="this.options" <div class="row">
:defaultz="this.defaultz" <div v-if="showDateFrom" class="col-lg-6 address-valid date-since">
> <h3>{{ $t(getValidFromDateText) }}</h3>
<template #before> <div class="input-group mb-3">
<slot name="before" /> <span class="input-group-text" id="validFrom"
</template> ><i class="fa fa-fw fa-calendar"
<template #action> /></span>
<slot name="action" /> <input
</template> type="date"
<template #after> class="form-control form-control-lg"
<slot name="after" /> name="validFrom"
</template> :placeholder="$t(getValidFromDateText)"
</action-buttons> v-model="validFrom"
aria-describedby="validFrom"
/>
</div>
</div>
<div v-if="showDateTo" class="col-lg-6 address-valid date-until">
<h3>{{ $t(getValidToDateText) }}</h3>
<div class="input-group mb-3">
<span class="input-group-text" id="validTo"
><i class="fa fa-fw fa-calendar"
/></span>
<input
type="date"
class="form-control form-control-lg"
name="validTo"
:placeholder="$t(getValidToDateText)"
v-model="validTo"
aria-describedby="validTo"
/>
</div>
</div>
</div>
<action-buttons
v-if="insideModal === false"
:options="this.options"
:defaultz="this.defaultz"
>
<template #before>
<slot name="before" />
</template>
<template #action>
<slot name="action" />
</template>
<template #after>
<slot name="after" />
</template>
</action-buttons>
</template> </template>
<script> <script>
@@ -69,113 +72,116 @@ import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRe
import ActionButtons from "./ActionButtons.vue"; import ActionButtons from "./ActionButtons.vue";
export default { export default {
name: "DatePane", name: "DatePane",
components: { components: {
AddressRenderBox, AddressRenderBox,
ActionButtons, ActionButtons,
},
props: [
"context",
"options",
"defaultz",
"flag",
"entity",
"errorMsg",
"insideModal",
],
computed: {
address() {
return this.entity.address;
}, },
validFrom: { props: [
set(value) { "context",
this.entity.selected.valid.from = ISOToDate(value); "options",
}, "defaultz",
get() { "flag",
return dateToISO(this.entity.selected.valid.from); "entity",
}, "errorMsg",
}, "insideModal",
validTo: { ],
set(value) { computed: {
this.entity.selected.valid.to = ISOToDate(value); address() {
}, return this.entity.address;
get() { },
return dateToISO(this.entity.selected.valid.to); validFrom: {
}, set(value) {
}, this.entity.selected.valid.from = ISOToDate(value);
getValidFromDateText() { },
return this.context.target.name === "household" get() {
? "move_date" return dateToISO(this.entity.selected.valid.from);
: "validFrom"; },
}, },
getValidToDateText() { validTo: {
return "validTo"; set(value) {
}, this.entity.selected.valid.to = ISOToDate(value);
showDateFrom() { },
return !this.context.edit && this.options.useDate.validFrom; get() {
}, return dateToISO(this.entity.selected.valid.to);
showDateTo() { },
return !this.context.edit && this.options.useDate.validTo; },
}, getValidFromDateText() {
selectedAddress() { return this.context.target.name === "household"
let address = {}; ? "move_date"
: "validFrom";
},
getValidToDateText() {
return "validTo";
},
showDateFrom() {
return !this.context.edit && this.options.useDate.validFrom;
},
showDateTo() {
return !this.context.edit && this.options.useDate.validTo;
},
selectedAddress() {
let address = {};
address["country"] = this.entity.selected.country address["country"] = this.entity.selected.country
? this.entity.selected.country ? this.entity.selected.country
: null; : null;
address["postcode"] = this.entity.selected.postcode address["postcode"] = this.entity.selected.postcode
? this.entity.selected.postcode ? this.entity.selected.postcode
: null; : null;
if (this.entity.selected.address) { if (this.entity.selected.address) {
let number = this.entity.selected.address.streetNumber let number = this.entity.selected.address.streetNumber
? this.entity.selected.address.streetNumber ? this.entity.selected.address.streetNumber
: null; : null;
let street = this.entity.selected.address.street let street = this.entity.selected.address.street
? this.entity.selected.address.street ? this.entity.selected.address.street
: null; : null;
address["text"] = number + ", " + street; address["text"] = number + ", " + street;
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
? this.entity.selected.address.streetNumber .streetNumber
: null; ? this.entity.selected.address.streetNumber
address["floor"] = this.entity.selected.address.floor : null;
? this.entity.selected.address.floor address["floor"] = this.entity.selected.address.floor
: null; ? this.entity.selected.address.floor
address["corridor"] = this.entity.selected.address.corridor : null;
? this.entity.selected.address.corridor address["corridor"] = this.entity.selected.address.corridor
: null; ? this.entity.selected.address.corridor
address["steps"] = this.entity.selected.address.steps : null;
? this.entity.selected.address.steps address["steps"] = this.entity.selected.address.steps
: null; ? this.entity.selected.address.steps
address["flat"] = this.entity.selected.address.flat : null;
? this.entity.selected.address.flat address["flat"] = this.entity.selected.address.flat
: null; ? this.entity.selected.address.flat
address["buildingName"] = this.entity.selected.address.buildingName : null;
? this.entity.selected.address.buildingName address["buildingName"] = this.entity.selected.address
: null; .buildingName
address["distribution"] = this.entity.selected.address.distribution ? this.entity.selected.address.buildingName
? this.entity.selected.address.distribution : null;
: null; address["distribution"] = this.entity.selected.address
address["extra"] = this.entity.selected.address.extra .distribution
? this.entity.selected.address.extra ? this.entity.selected.address.distribution
: null; : null;
} address["extra"] = this.entity.selected.address.extra
? this.entity.selected.address.extra
: null;
}
if (this.entity.selected.valid) { if (this.entity.selected.valid) {
address["validFrom"] = this.entity.selected.valid.from address["validFrom"] = this.entity.selected.valid.from
? dateToISO(this.entity.selected.valid.from) ? dateToISO(this.entity.selected.valid.from)
: null; : null;
address["validTo"] = this.entity.selected.valid.to address["validTo"] = this.entity.selected.valid.to
? dateToISO(this.entity.selected.valid.to) ? dateToISO(this.entity.selected.valid.to)
: null; : null;
} }
return address; return address;
},
}, },
},
}; };
</script> </script>

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