Merge remote-tracking branch 'origin/ticket-app-master' into ticket-app-master

This commit is contained in:
Julien Fastré 2025-06-20 12:45:33 +02:00
commit dfc146ff3f
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
47 changed files with 2646 additions and 2400 deletions

30
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,30 @@
{
// 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 Normal file
View File

@ -0,0 +1,23 @@
{
"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

@ -49,6 +49,30 @@ export interface User {
// todo: mainCenter; mainJob; etc.. // todo: mainCenter; mainJob; etc..
} }
// TODO : Add missing household properties
export interface Household {
type: "household";
id: number;
}
export interface ThirdParty {
type: "thirdparty";
id: number;
firstname: string;
name: string;
email: string;
telephone: string;
telephone2: string;
address: {
address_id: number;
text: string;
postcode: {
name: string;
};
id: number;
};
}
export interface UserGroup { export interface UserGroup {
type: "user_group"; type: "user_group";
id: number; id: number;

View File

@ -10,7 +10,7 @@
v-model="radioType" v-model="radioType"
value="person" value="person"
/> />
{{ $t("onthefly.create.person") }} {{ trans(ONTHEFLY_CREATE_PERSON) }}
</label> </label>
</a> </a>
</li> </li>
@ -24,7 +24,7 @@
v-model="radioType" v-model="radioType"
value="thirdparty" value="thirdparty"
/> />
{{ $t("onthefly.create.thirdparty") }} {{ trans(ONTHEFLY_CREATE_THIRDPARTY) }}
</label> </label>
</a> </a>
</li> </li>
@ -46,64 +46,67 @@
/> />
</div> </div>
</template> </template>
<script setup>
<script> import { ref, computed, onMounted } from "vue";
import OnTheFlyPerson from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue"; import OnTheFlyPerson from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue";
import OnTheFlyThirdparty from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdParty.vue"; import OnTheFlyThirdparty from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdParty.vue";
import {
trans,
ONTHEFLY_CREATE_PERSON,
ONTHEFLY_CREATE_THIRDPARTY,
} from "translator";
export default { const props = defineProps({
name: "OnTheFlyCreate", action: String,
props: ["action", "allowedTypes", "query"], allowedTypes: Array,
components: { query: String,
OnTheFlyPerson, });
OnTheFlyThirdparty,
},
data() {
return {
type: null,
};
},
computed: {
radioType: {
set(type) {
this.type = type;
console.log("## type:", type, ", action:", this.action);
},
get() {
return this.type;
},
},
},
mounted() {
this.type =
this.allowedTypes.length === 1 &&
this.allowedTypes.includes("thirdparty")
? "thirdparty"
: "person";
},
methods: {
isActive(tab) {
return this.type === tab ? true : false;
},
castDataByType() {
switch (this.radioType) {
case "person":
return this.$refs.castPerson.$data.person;
case "thirdparty":
let data = this.$refs.castThirdparty.$data.thirdparty;
if (data.address !== undefined && data.address !== null) {
data.address = { id: data.address.address_id };
} else {
data.address = null;
}
return data; const type = ref(null);
default:
throw Error("Invalid type of entity"); const radioType = computed({
get: () => type.value,
set: (val) => {
type.value = val;
console.log("## type:", val, ", action:", props.action);
},
});
const castPerson = ref(null);
const castThirdparty = ref(null);
onMounted(() => {
type.value =
props.allowedTypes.length === 1 &&
props.allowedTypes.includes("thirdparty")
? "thirdparty"
: "person";
});
function isActive(tab) {
return type.value === tab;
}
function castDataByType() {
switch (radioType.value) {
case "person":
return castPerson.value.$data.person;
case "thirdparty":
let data = castThirdparty.value.$data.thirdparty;
if (data.address !== undefined && data.address !== null) {
data.address = { id: data.address.address_id };
} else {
data.address = null;
} }
}, return data;
}, default:
}; throw Error("Invalid type of entity");
}
}
defineExpose({
castDataByType,
});
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>

View File

@ -9,7 +9,7 @@
class="btn btn-sm" class="btn btn-sm"
target="_blank" target="_blank"
:class="classAction" :class="classAction"
:title="$t(titleAction)" :title="trans(titleAction)"
@click="openModal" @click="openModal"
> >
{{ buttonText }}<span v-if="isDead"> ()</span> {{ buttonText }}<span v-if="isDead"> ()</span>
@ -23,10 +23,10 @@
> >
<template #header> <template #header>
<h3 v-if="parent" class="modal-title"> <h3 v-if="parent" class="modal-title">
{{ $t(titleModal, { q: parent.text }) }} {{ trans(titleModal, { q: parent.text }) }}
</h3> </h3>
<h3 v-else class="modal-title"> <h3 v-else class="modal-title">
{{ $t(titleModal) }} {{ trans(titleModal) }}
</h3> </h3>
</template> </template>
@ -38,7 +38,7 @@
ref="castPerson" ref="castPerson"
/> />
<div v-if="hasResourceComment"> <div v-if="hasResourceComment">
<h3>{{ $t("onthefly.resource_comment_title") }}</h3> <h3>{{ trans(ONTHEFLY_RESOURCE_COMMENT_TITLE) }}</h3>
<blockquote class="chill-user-quote"> <blockquote class="chill-user-quote">
{{ parent.comment }} {{ parent.comment }}
</blockquote> </blockquote>
@ -53,7 +53,7 @@
ref="castThirdparty" ref="castThirdparty"
/> />
<div v-if="hasResourceComment"> <div v-if="hasResourceComment">
<h3>{{ $t("onthefly.resource_comment_title") }}</h3> <h3>{{ trans(ONTHEFLY_RESOURCE_COMMENT_TITLE) }}</h3>
<blockquote class="chill-user-quote"> <blockquote class="chill-user-quote">
{{ parent.comment }} {{ parent.comment }}
</blockquote> </blockquote>
@ -82,242 +82,273 @@
<a <a
v-if="action === 'show'" v-if="action === 'show'"
:href="buildLocation(id, type)" :href="buildLocation(id, type)"
:title="$t(titleMessage)" :title="trans(titleMessage)"
class="btn btn-show" class="btn btn-show"
>{{ $t(buttonMessage) }} >{{ trans(buttonMessage) }}
</a> </a>
<a v-else class="btn btn-save" @click="saveAction"> <a v-else class="btn btn-save" @click="saveAction">
{{ $t("action.save") }} {{ trans(ACTION_SAVE) }}
</a> </a>
</template> </template>
</modal> </modal>
</teleport> </teleport>
</template> </template>
<script setup>
<script> import { ref, computed, defineEmits, defineProps } from "vue";
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue"; import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import OnTheFlyCreate from "./Create.vue"; import OnTheFlyCreate from "./Create.vue";
import OnTheFlyPerson from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue"; import OnTheFlyPerson from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue";
import OnTheFlyThirdparty from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdParty.vue"; import OnTheFlyThirdparty from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdParty.vue";
import {
trans,
ACTION_SHOW,
ACTION_EDIT,
ACTION_CREATE,
ACTION_ADDCONTACT,
ONTHEFLY_CREATE_TITLE_DEFAULT,
ONTHEFLY_CREATE_TITLE_PERSON,
ONTHEFLY_CREATE_TITLE_THIRDPARTY,
ONTHEFLY_SHOW_PERSON,
ONTHEFLY_SHOW_THIRDPARTY,
ONTHEFLY_EDIT_PERSON,
ONTHEFLY_EDIT_THIRDPARTY,
ONTHEFLY_ADDCONTACT_TITLE,
ACTION_REDIRECT_PERSON,
ACTION_REDIRECT_THIRDPARTY,
ONTHEFLY_SHOW_FILE_PERSON,
ONTHEFLY_SHOW_FILE_THIRDPARTY,
ONTHEFLY_SHOW_FILE_DEFAULT,
ONTHEFLY_RESOURCE_COMMENT_TITLE,
ACTION_SAVE,
} from "translator";
export default { const props = defineProps({
name: "OnTheFly", type: String,
components: { id: [String, Number],
Modal, action: String,
OnTheFlyPerson, buttonText: String,
OnTheFlyThirdparty, displayBadge: Boolean,
OnTheFlyCreate, isDead: Boolean,
}, parent: Object,
props: [ allowedTypes: Array,
"type", query: String,
"id", });
"action",
"buttonText",
"displayBadge",
"isDead",
"parent",
"allowedTypes",
"query",
],
emits: ["saveFormOnTheFly"],
data() {
return {
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl",
},
};
},
computed: {
hasResourceComment() {
return (
typeof this.parent !== "undefined" &&
this.parent !== null &&
this.action === "show" &&
this.parent.type === "accompanying_period_resource" &&
this.parent.comment !== null &&
this.parent.comment !== ""
);
},
classAction() {
switch (this.action) {
case "show":
return "btn-show";
case "edit":
return "btn-update";
case "create":
return "btn-create";
case "addContact":
return "btn-tpchild";
default:
return "";
}
},
titleAction() {
switch (this.action) {
case "show":
return "action.show";
case "edit":
return "action.edit";
case "create":
return "action.create";
case "addContact":
return "action.addContact";
default:
return "";
}
},
titleCreate() {
if (typeof this.allowedTypes === "undefined") {
return "onthefly.create.title.default";
}
return this.allowedTypes.every((t) => t === "person")
? "onthefly.create.title.person"
: this.allowedTypes.every((t) => t === "thirdparty")
? "onthefly.create.title.thirdparty"
: "onthefly.create.title.default";
},
titleModal() {
switch (this.action) {
case "show":
return "onthefly.show." + this.type;
case "edit":
return "onthefly.edit." + this.type;
case "create":
return this.titleCreate;
case "addContact":
return "onthefly.addContact.title";
default:
return "";
}
},
titleMessage() {
switch (this.type) {
case "person":
return "action.redirect." + this.type;
case "thirdparty":
return "action.redirect." + this.type;
default:
return "";
}
},
buttonMessage() {
switch (this.type) {
case "person":
return "onthefly.show.file_" + this.type;
case "thirdparty":
return "onthefly.show.file_" + this.type;
}
},
isDisplayBadge() {
return this.displayBadge === true && this.buttonText !== null;
},
badgeType() {
return "entity-" + this.type + " badge-" + this.type;
},
getReturnPath() {
return `?returnPath=${window.location.pathname}${window.location.search}${window.location.hash}`;
},
},
methods: {
closeModal() {
this.modal.showModal = false;
},
openModal() {
// console.log('## OPEN ON THE FLY MODAL');
// console.log('## type:', this.type, ', action:', this.action);
this.modal.showModal = true;
this.$nextTick(function () {
//this.$refs.search.focus();
});
},
changeActionTo(action) {
this.$data.action = action;
},
saveAction() {
// console.log('saveAction button: create/edit action with', this.type);
let type = this.type,
data = {};
switch (type) {
case "person":
data = this.$refs.castPerson.$data.person;
console.log("person data are", data);
break;
case "thirdparty": const emit = defineEmits(["saveFormOnTheFly"]);
data = this.$refs.castThirdparty.$data.thirdparty;
/* never executed ? */
break;
default: const modal = ref({
if (typeof this.type === "undefined") { showModal: false,
// action=create or addContact modalDialogClass: "modal-dialog-scrollable modal-xl",
// console.log('will rewrite data'); });
if (this.action === "addContact") {
type = "thirdparty"; const castPerson = ref();
data = this.$refs.castThirdparty.$data.thirdparty; const castThirdparty = ref();
// console.log('data original', data); const castNew = ref();
data.parent = {
type: "thirdparty", const hasResourceComment = computed(() => {
id: this.parent.id, return (
}; typeof props.parent !== "undefined" &&
data.civility = props.parent !== null &&
data.civility !== null props.action === "show" &&
? { props.parent.type === "accompanying_period_resource" &&
type: "chill_main_civility", props.parent.comment !== null &&
id: data.civility.id, props.parent.comment !== ""
} );
: null; });
data.profession =
data.profession !== "" ? data.profession : ""; const classAction = computed(() => {
} else { switch (props.action) {
type = this.$refs.castNew.radioType; case "show":
data = this.$refs.castNew.castDataByType(); return "btn-show";
// console.log('type', type); case "edit":
if ( return "btn-update";
typeof data.civility !== "undefined" && case "create":
null !== data.civility return "btn-create";
) { case "addContact":
data.civility = return "btn-tpchild";
data.civility !== null default:
? { return "";
type: "chill_main_civility", }
id: data.civility.id, });
}
: null; const titleAction = computed(() => {
} switch (props.action) {
if ( case "show":
typeof data.profession !== "undefined" && return ACTION_SHOW;
"" !== data.profession case "edit":
) { return ACTION_EDIT;
data.profession = case "create":
data.profession !== "" return ACTION_CREATE;
? data.profession case "addContact":
: ""; return ACTION_ADDCONTACT;
} default:
// console.log('onthefly data', data); return "";
} }
} else { });
throw "error with object type";
const titleCreate = computed(() => {
if (typeof props.allowedTypes === "undefined") {
return ONTHEFLY_CREATE_TITLE_DEFAULT;
}
return props.allowedTypes.every((t) => t === "person")
? ONTHEFLY_CREATE_TITLE_PERSON
: props.allowedTypes.every((t) => t === "thirdparty")
? ONTHEFLY_CREATE_TITLE_THIRDPARTY
: ONTHEFLY_CREATE_TITLE_DEFAULT;
});
const titleModal = computed(() => {
switch (props.action) {
case "show":
if (props.type == "person") {
return ONTHEFLY_SHOW_PERSON;
} else if (props.type == "thirdparty") {
return ONTHEFLY_SHOW_THIRDPARTY;
}
break;
case "edit":
if (props.type == "person") {
return ONTHEFLY_EDIT_PERSON;
} else if (props.type == "thirdparty") {
return ONTHEFLY_EDIT_THIRDPARTY;
}
break;
case "create":
return titleCreate.value;
case "addContact":
return ONTHEFLY_ADDCONTACT_TITLE;
default:
break;
}
return "";
});
const titleMessage = computed(() => {
switch (props.type) {
case "person":
return ACTION_REDIRECT_PERSON;
case "thirdparty":
return ACTION_REDIRECT_THIRDPARTY;
default:
return "";
}
});
const buttonMessage = computed(() => {
switch (props.type) {
case "person":
return ONTHEFLY_SHOW_FILE_PERSON;
case "thirdparty":
return ONTHEFLY_SHOW_FILE_THIRDPARTY;
default:
return ONTHEFLY_SHOW_FILE_DEFAULT;
}
});
const isDisplayBadge = computed(() => {
return props.displayBadge === true && props.buttonText !== null;
});
const badgeType = computed(() => {
return "entity-" + props.type + " badge-" + props.type;
});
const getReturnPath = computed(() => {
return `?returnPath=${window.location.pathname}${window.location.search}${window.location.hash}`;
});
function closeModal() {
modal.value.showModal = false;
}
function openModal() {
modal.value.showModal = true;
}
function changeActionTo(action) {
console.log(action);
// Not reactive in setup, but you can emit or use a ref if needed
}
function saveAction() {
let type = props.type,
data = {};
switch (type) {
case "person":
data = castPerson.value?.$data.person;
break;
case "thirdparty":
data = castThirdparty.value?.$data.thirdparty;
break;
default:
if (typeof props.type === "undefined") {
if (props.action === "addContact") {
type = "thirdparty";
data = castThirdparty.value?.$data.thirdparty;
data.parent = {
type: "thirdparty",
id: props.parent.id,
};
data.civility =
data.civility !== null
? {
type: "chill_main_civility",
id: data.civility.id,
}
: null;
data.profession =
data.profession !== "" ? data.profession : "";
} else {
type = castNew.value.radioType;
data = castNew.value.castDataByType();
if (
typeof data.civility !== "undefined" &&
null !== data.civility
) {
data.civility =
data.civility !== null
? {
type: "chill_main_civility",
id: data.civility.id,
}
: null;
} }
if (
typeof data.profession !== "undefined" &&
"" !== data.profession
) {
data.profession =
data.profession !== "" ? data.profession : "";
}
}
} else {
throw "error with object type";
} }
// pass datas to parent }
this.$emit("saveFormOnTheFly", { type: type, data: data }); emit("saveFormOnTheFly", { type: type, data: data });
}, }
buildLocation(id, type) {
if (type === "person") { function buildLocation(id, type) {
// TODO i18n if (type === "person") {
return encodeURI( return encodeURI(`/fr/person/${id}/general${getReturnPath.value}`);
`/fr/person/${id}/general${this.getReturnPath}`, } else if (type === "thirdparty") {
); return encodeURI(`/fr/3party/3party/${id}/view${getReturnPath.value}`);
} else if (type === "thirdparty") { }
return encodeURI( }
`/fr/3party/3party/${id}/view${this.getReturnPath}`,
); defineExpose({
} openModal,
}, closeModal,
}, changeActionTo,
}; saveAction,
castPerson,
castThirdparty,
castNew,
hasResourceComment,
modal,
isDisplayBadge,
Modal,
});
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>

View File

@ -1,9 +1,28 @@
<template> <template>
<i :class="gender.icon" /> <i :class="['fa', genderClass, 'px-1']" />
</template> </template>
<script setup> <script setup>
import { computed } from "vue";
const props = defineProps({ const props = defineProps({
gender: Object, gender: {
type: Object,
required: true,
},
});
const genderClass = computed(() => {
switch (props.gender.genderTranslation) {
case "woman":
return "fa-venus";
case "man":
return "fa-mars";
case "both":
return "fa-neuter";
case "unknown":
return "fa-genderless";
default:
return "fa-genderless";
}
}); });
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<transition name="modal"> <transition name="modal">
<div class="modal-mask"> <div class="modal-mask" v-if="show">
<!-- :: styles bootstrap :: --> <!-- :: styles bootstrap :: -->
<div <div
class="modal fade show" class="modal fade show"
@ -8,7 +8,7 @@
aria-modal="true" aria-modal="true"
role="dialog" role="dialog"
> >
<div class="modal-dialog" :class="props.modalDialogClass || {}"> <div class="modal-dialog" :class="modalDialogClass || {}">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<slot name="header"></slot> <slot name="header"></slot>
@ -53,14 +53,23 @@ import { trans, MODAL_ACTION_CLOSE } from "translator";
import { defineProps } from "vue"; import { defineProps } from "vue";
export interface ModalProps { export interface ModalProps {
modalDialogClass: object | null; modalDialogClass: string;
hideFooter: boolean; hideFooter: boolean;
} }
// Define the props defineProps({
const props = withDefaults(defineProps<ModalProps>(), { modalDialogClass: {
hideFooter: false, type: String,
modalDialogClass: null, default: "",
},
hideFooter: {
type: Boolean,
default: false,
},
show: {
type: Boolean,
default: true,
},
}); });
const emits = defineEmits<{ const emits = defineEmits<{

View File

@ -129,3 +129,30 @@ filter_order:
Search: Chercher dans la liste Search: Chercher dans la liste
By date: Filtrer par date By date: Filtrer par date
search_box: Filtrer par contenu search_box: Filtrer par contenu
renderbox:
person: "Usager"
birthday:
man: "Né le"
woman: "Née le"
neutral: "Né·e le"
unknown: "Né·e le"
deathdate: "Date de décès"
household_without_address: "Le ménage de l'usager est sans adresse"
no_data: "Aucune information renseignée"
type:
thirdparty: "Tiers"
person: "Usager"
holder: "Titulaire"
years_old: >-
{n, plural,
=0 {0 an}
one {1 an}
other {# ans}
}
residential_address: "Adresse de résidence"
located_at: "réside chez"
household_number: "Ménage n°{number}"
current_members: "Membres actuels"
no_current_address: "Sans adresse actuellement"
new_household: "Nouveau ménage"
no_members_yet: "Aucun membre actuellement"

View File

@ -919,3 +919,34 @@ multiselect:
editor: editor:
switch_to_simple: Éditeur simple switch_to_simple: Éditeur simple
switch_to_complex: Éditeur riche switch_to_complex: Éditeur riche
action:
actions: Actions
show: Voir
edit: Modifier
create: Créer
remove: Enlever
delete: Supprimer
save: Enregistrer
valid: Valider
valid_and_see: Valider et voir
add: Ajouter
show_modal: Ouvrir une modale
ok: OK
cancel: Annuler
close: Fermer
back: Retour
check_all: cocher tout
reset: réinitialiser
redirect:
person: Quitter la page et ouvrir la fiche de l'usager
thirdparty: Quitter la page et voir le tiers
refresh: Rafraîchir
addContact: Ajouter un contact
nav:
next: "Suivant"
previous: "Précédent"
top: "Haut"
bottom: "Bas"

View File

@ -1,12 +1,15 @@
import { StoredObject } from "ChillDocStoreAssets/types";
import { import {
Address, Address,
Center, Center,
Civility, Civility,
DateTime, DateTime,
Household,
ThirdParty,
User, User,
UserGroup,
WorkflowAvailable, WorkflowAvailable,
} from "../../../ChillMainBundle/Resources/public/types"; } from "../../../ChillMainBundle/Resources/public/types";
import { StoredObject } from "../../../ChillDocStoreBundle/Resources/public/types";
export interface Person { export interface Person {
id: number; id: number;
@ -41,3 +44,61 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
workflows_availables: WorkflowAvailable[]; workflows_availables: WorkflowAvailable[];
workflows: object[]; workflows: object[];
} }
export type Entities = (UserGroup | User | Person | ThirdParty | Household) & {
address?: Address | null;
kind?: string;
text?: string;
profession?: string;
};
export type Result = Entities & {
parent?: Entities | null;
};
export interface Suggestion {
key: string;
relevance: number;
result: Result;
}
export interface SearchPagination {
first: number;
items_per_page: number;
next: number | null;
previous: number | null;
more: boolean;
}
export interface Search {
count: number;
pagination: SearchPagination;
results: Suggestion[];
}
export interface SearchOptions {
uniq: boolean;
type: string[];
priority: number | null;
button: {
size: string;
class: string;
type: string;
display: string;
};
}
export class MakeFetchException extends Error {
sta: number;
txt: string;
violations: unknown | null;
constructor(txt: string, sta: number, violations: unknown | null = null) {
super(txt);
this.name = "ValidationException";
this.sta = sta;
this.txt = txt;
this.violations = violations;
Object.setPrototypeOf(this, MakeFetchException.prototype);
}
}

View File

@ -1,7 +1,15 @@
import { Search, SearchOptions } from "ChillPersonAssets/types";
/* /*
* Build query string with query and options * Build query string with query and options
*/ */
const parametersToString = ({ query, options }) => { const parametersToString = ({
query,
options,
}: {
query: string;
options: SearchOptions;
}) => {
let types = ""; let types = "";
options.type.forEach(function (type) { options.type.forEach(function (type) {
types += "&type[]=" + type; types += "&type[]=" + type;
@ -16,11 +24,13 @@ const parametersToString = ({ query, options }) => {
* @query string - the query to search for * @query string - the query to search for
* @deprecated * @deprecated
*/ */
const searchPersons = ({ query, options }, signal) => { export function searchPersons(
console.err("deprecated"); { query, options }: { query: string; options: SearchOptions },
let queryStr = parametersToString({ query, options }); signal: AbortSignal,
let url = `/fr/search.json?name=person_regular&${queryStr}`; ): Promise<Search> {
let fetchOpts = { const queryStr = parametersToString({ query, options });
const url = `/fr/search.json?name=person_regular&${queryStr}`;
const fetchOpts = {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json;charset=utf-8", "Content-Type": "application/json;charset=utf-8",
@ -34,7 +44,7 @@ const searchPersons = ({ query, options }, signal) => {
} }
throw Error("Error with request resource response"); throw Error("Error with request resource response");
}); });
}; }
/* /*
* Endpoint v.2 chill_main_search_global * Endpoint v.2 chill_main_search_global
@ -43,15 +53,16 @@ const searchPersons = ({ query, options }, signal) => {
* @param query string - the query to search for * @param query string - the query to search for
* *
*/ */
const searchEntities = ({ query, options }, signal) => { export function searchEntities(
let queryStr = parametersToString({ query, options }); { query, options }: { query: string; options: SearchOptions },
let url = `/api/1.0/search.json?${queryStr}`; signal: AbortSignal,
): Promise<Search> {
const queryStr = parametersToString({ query, options });
const url = `/api/1.0/search.json?${queryStr}`;
return fetch(url, { signal }).then((response) => { return fetch(url, { signal }).then((response) => {
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 { searchPersons, searchEntities };

View File

@ -6,48 +6,39 @@
</ul> </ul>
</template> </template>
<script> <script setup>
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods.ts"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods.ts";
import { defineProps, defineEmits } from "vue";
export default { const props = defineProps({
name: "SetReferrer", suggested: {
props: { type: Array,
suggested: { required: false,
type: Array, // default: [],
required: false,
//default: [],
},
periodId: {
type: Number,
required: true,
},
}, },
data() { periodId: {
return { type: Number,
/*suggested: [ required: true,
{id: 5, text: 'Robert'}, {id: 8, text: 'Monique'},
]*/
};
}, },
emits: ["referrerSet"], });
methods: {
setReferrer: function (ref) {
const url = `/api/1.0/person/accompanying-course/${this.periodId}.json`;
const body = {
type: "accompanying_period",
user: { id: ref.id, type: ref.type },
};
return makeFetch("PATCH", url, body) const emit = defineEmits(["referrerSet"]);
.then((response) => {
this.$emit("referrerSet", ref); function setReferrer(ref) {
}) const url = `/api/1.0/person/accompanying-course/${props.periodId}.json`;
.catch((error) => { const body = {
throw error; type: "accompanying_period",
}); user: { id: ref.id, type: ref.type },
}, };
},
}; return makeFetch("PATCH", url, body)
.then(() => {
emit("referrerSet", ref);
})
.catch((error) => {
throw error;
});
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -2,21 +2,23 @@
<a <a
class="btn" class="btn"
:class="getClassButton" :class="getClassButton"
:title="$t(buttonTitle || '')" :title="buttonTitle"
@click="openModal" @click="openModal"
> >
<span v-if="displayTextButton">{{ $t(buttonTitle || "") }}</span> <span v-if="displayTextButton">{{ buttonTitle }}</span>
</a> </a>
<teleport to="body"> <teleport to="body">
<modal <modal
v-if="modal.showModal" v-if="showModal"
:modal-dialog-class="modal.modalDialogClass" @close="closeModal"
@close="modal.showModal = false" :modal-dialog-class="modalDialogClass"
:show="showModal"
:hide-footer="false"
> >
<template #header> <template #header>
<h3 class="modal-title"> <h3 class="modal-title">
{{ $t(modalTitle) }} {{ modalTitle }}
</h3> </h3>
</template> </template>
@ -24,15 +26,19 @@
<div class="modal-body"> <div class="modal-body">
<div class="search"> <div class="search">
<label class="col-form-label" style="float: right"> <label class="col-form-label" style="float: right">
{{ $tc("add_persons.suggested_counter", suggestedCounter) }} {{
trans(ADD_PERSONS_SUGGESTED_COUNTER, {
count: suggestedCounter,
})
}}
</label> </label>
<input <input
id="search-persons" id="search-persons"
name="query" name="query"
v-model="query" v-model="query"
:placeholder="$t('add_persons.search_some_persons')" :placeholder="trans(ADD_PERSONS_SEARCH_SOME_PERSONS)"
ref="search" ref="searchRef"
/> />
<i class="fa fa-search fa-lg" /> <i class="fa fa-search fa-lg" />
<i <i
@ -46,15 +52,19 @@
<div class="count"> <div class="count">
<span> <span>
<a v-if="suggestedCounter > 2" @click="selectAll"> <a v-if="suggestedCounter > 2" @click="selectAll">
{{ $t("action.check_all") }} {{ trans(ACTION_CHECK_ALL) }}
</a> </a>
<a v-if="selectedCounter > 0" @click="resetSelection"> <a v-if="selectedCounter > 0" @click="resetSelection">
<i v-if="suggestedCounter > 2"> </i> <i v-if="suggestedCounter > 2"> </i>
{{ $t("action.reset") }} {{ trans(ACTION_RESET) }}
</a> </a>
</span> </span>
<span v-if="selectedCounter > 0"> <span v-if="selectedCounter > 0">
{{ $tc("add_persons.selected_counter", selectedCounter) }} {{
trans(ADD_PERSONS_SELECTED_COUNTER, {
count: selectedCounter,
})
}}
</span> </span>
</div> </div>
</div> </div>
@ -63,7 +73,7 @@
<template #body> <template #body>
<div class="results"> <div class="results">
<person-suggestion <person-suggestion
v-for="item in this.selectedAndSuggested.slice().reverse()" v-for="item in selectedAndSuggested.slice().reverse()"
:key="itemKey(item)" :key="itemKey(item)"
:item="item" :item="item"
:search="search" :search="search"
@ -80,7 +90,7 @@
(options.type.includes('person') || (options.type.includes('person') ||
options.type.includes('thirdparty')) options.type.includes('thirdparty'))
" "
:button-text="$t('onthefly.create.button', { q: query })" :button-text="trans(ONTHEFLY_CREATE_BUTTON, { q: query })"
:allowed-types="options.type" :allowed-types="options.type"
:query="query" :query="query"
action="create" action="create"
@ -94,333 +104,335 @@
<template #footer> <template #footer>
<button <button
class="btn btn-create" class="btn btn-create"
@click.prevent="$emit('addNewPersons', { selected, modal })" @click.prevent="
() => {
$emit('addNewPersons', { selected: selectedComputed });
query = '';
closeModal();
}
"
> >
{{ $t("action.add") }} {{ trans(ACTION_ADD) }}
</button> </button>
</template> </template>
</modal> </modal>
</teleport> </teleport>
</template> </template>
<script> <script setup lang="ts">
import Modal from "ChillMainAssets/vuejs/_components/Modal"; // eslint-disable-next-line @typescript-eslint/no-unused-vars
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue"; import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import PersonSuggestion from "./AddPersons/PersonSuggestion"; import { ref, reactive, computed, nextTick, watch, shallowRef } from "vue";
import PersonSuggestion from "./AddPersons/PersonSuggestion.vue";
import { searchEntities } from "ChillPersonAssets/vuejs/_api/AddPersons"; import { searchEntities } from "ChillPersonAssets/vuejs/_api/AddPersons";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
export default { import {
name: "AddPersons", trans,
components: { ADD_PERSONS_SUGGESTED_COUNTER,
Modal, ADD_PERSONS_SEARCH_SOME_PERSONS,
PersonSuggestion, ADD_PERSONS_SELECTED_COUNTER,
OnTheFly, ONTHEFLY_CREATE_BUTTON,
}, ACTION_CHECK_ALL,
props: ["buttonTitle", "modalTitle", "options"], ACTION_RESET,
emits: ["addNewPersons"], ACTION_ADD,
data() { } from "translator";
return { import {
modal: { Suggestion,
showModal: false, Search,
modalDialogClass: "modal-dialog-scrollable modal-xl", Result as OriginalResult,
}, SearchOptions,
search: { } from "ChillPersonAssets/types";
query: "",
previousQuery: "", // Extend Result type to include optional addressId
currentSearchQueryController: null, type Result = OriginalResult & { addressId?: number };
suggested: [],
selected: [], const props = defineProps({
priorSuggestion: {}, suggested: { type: Array as () => Suggestion[], default: () => [] },
}, selected: { type: Array as () => Suggestion[], default: () => [] },
}; buttonTitle: { type: String, required: true },
}, modalTitle: { type: String, required: true },
computed: { options: { type: Object as () => SearchOptions, required: true },
query: { });
set(query) {
return this.setQuery(query); defineEmits(["addNewPersons"]);
},
get() { const showModal = ref(false);
return this.search.query; const modalDialogClass = ref("modal-dialog-scrollable modal-xl");
},
}, const modal = shallowRef({
queryLength() { showModal: false,
return this.search.query.length; modalDialogClass: "modal-dialog-scrollable modal-xl",
}, });
suggested() {
return this.search.suggested; const search = reactive({
}, query: "" as string,
suggestedCounter() { previousQuery: "" as string,
return this.search.suggested.length; currentSearchQueryController: null as AbortController | null,
}, suggested: props.suggested as Suggestion[],
selected() { selected: props.selected as Suggestion[],
return this.search.selected; priorSuggestion: {} as Partial<Suggestion>,
}, });
selectedCounter() {
return this.search.selected.length; const searchRef = ref<HTMLInputElement | null>(null);
}, const onTheFly = ref<InstanceType<typeof OnTheFly> | null>(null);
selectedAndSuggested() {
this.addPriorSuggestion(); const query = computed({
const uniqBy = (a, key) => [ get: () => search.query,
...new Map(a.map((x) => [key(x), x])).values(), set: (val) => setQuery(val),
]; });
let union = [ const queryLength = computed(() => search.query.length);
...new Set([ const suggestedCounter = computed(() => search.suggested.length);
...this.suggested.slice().reverse(), const selectedComputed = computed(() => search.selected);
...this.selected.slice().reverse(), const selectedCounter = computed(() => search.selected.length);
]),
]; const getClassButton = computed(() => {
return uniqBy(union, (k) => k.key); let size = props.options?.button?.size ?? "";
}, let type = props.options?.button?.type ?? "btn-create";
getClassButton() { return size ? size + " " + type : type;
let size = });
typeof this.options.button !== "undefined" && const displayTextButton = computed(() =>
typeof this.options.button.size !== "undefined" props.options?.button?.display !== undefined
? this.options.button.size ? props.options.button.display
: ""; : true,
let type = );
typeof this.options.button !== "undefined" &&
typeof this.options.button.type !== "undefined" const checkUniq = computed(() =>
? this.options.button.type props.options.uniq === true ? "radio" : "checkbox",
: "btn-create"; );
return size ? size + " " + type : type;
}, const priorSuggestion = computed(() => search.priorSuggestion);
displayTextButton() { const hasPriorSuggestion = computed(() => !!search.priorSuggestion.key);
return typeof this.options.button !== "undefined" &&
typeof this.options.button.display !== "undefined" const itemKey = (item: Suggestion) => item.result.type + item.result.id;
? this.options.button.display
: true; function addPriorSuggestion() {
}, if (hasPriorSuggestion.value) {
checkUniq() { // Type assertion is safe here due to the checks above
if (this.options.uniq === true) { search.suggested.unshift(priorSuggestion.value as Suggestion);
return "radio"; search.selected.unshift(priorSuggestion.value as Suggestion);
} newPriorSuggestion(null);
return "checkbox"; }
}, }
priorSuggestion() {
return this.search.priorSuggestion; const selectedAndSuggested = computed(() => {
}, addPriorSuggestion();
hasPriorSuggestion() { const uniqBy = (a: Suggestion[], key: (item: Suggestion) => string) => [
return this.search.priorSuggestion.key ? true : false; ...new Map(a.map((x) => [key(x), x])).values(),
}, ];
}, let union = [
methods: { ...new Set([
openModal() { ...search.suggested.slice().reverse(),
this.modal.showModal = true; ...search.selected.slice().reverse(),
this.$nextTick(function () { ]),
this.$refs.search.focus(); ];
return uniqBy(union, (k: Suggestion) => k.key);
});
function openModal() {
showModal.value = true;
nextTick(() => {
if (searchRef.value) searchRef.value.focus();
});
}
function closeModal() {
showModal.value = false;
}
function setQuery(q: string) {
search.query = q;
// Clear previous search if any
if (search.currentSearchQueryController) {
search.currentSearchQueryController.abort();
search.currentSearchQueryController = null;
}
if (q === "") {
loadSuggestions([]);
return;
}
// Debounce delay based on query length
const delay = q.length > 3 ? 300 : 700;
setTimeout(() => {
// Only search if query hasn't changed in the meantime
if (q !== search.query) return;
search.currentSearchQueryController = new AbortController();
searchEntities(
{ query: q, options: props.options },
search.currentSearchQueryController.signal,
)
.then((suggested: Search) => {
loadSuggestions(suggested.results);
})
.catch((error: DOMException) => {
if (error instanceof DOMException && error.name === "AbortError") {
// Request was aborted, ignore
return;
}
throw error;
}); });
}, }, delay);
setQuery(query) { }
this.search.query = query;
setTimeout( function loadSuggestions(suggestedArr: Suggestion[]) {
function () { search.suggested = suggestedArr;
if (query === "") { search.suggested.forEach((item) => {
this.loadSuggestions([]); item.key = itemKey(item);
return; });
} }
if (query === this.search.query) {
if (this.search.currentSearchQueryController !== null) {
this.search.currentSearchQueryController.abort();
}
this.search.currentSearchQueryController = new AbortController();
searchEntities(
{ query, options: this.options },
this.search.currentSearchQueryController.signal,
)
.then(
(suggested) =>
new Promise((resolve) => {
this.loadSuggestions(suggested.results);
resolve();
}),
)
.catch((error) => {
if (error instanceof DOMException) {
if (error.name === "AbortError") {
console.log("request aborted due to user continue typing");
return;
}
}
throw error; function updateSelected(value: Suggestion[]) {
}); search.selected = value;
} }
}.bind(this),
query.length > 3 ? 300 : 700,
);
},
loadSuggestions(suggested) {
this.search.suggested = suggested;
this.search.suggested.forEach(function (item) {
item.key = this.itemKey(item);
}, this);
},
updateSelected(value) {
//console.log('value', value);
this.search.selected = value;
},
resetSearch() {
this.resetSelection();
this.resetSuggestion();
},
resetSuggestion() {
this.search.query = "";
this.search.suggested = [];
},
resetSelection() {
this.search.selected = [];
},
selectAll() {
this.search.suggested.forEach(function (item) {
this.search.selected.push(item);
}, this);
},
itemKey(item) {
return item.result.type + item.result.id;
},
addPriorSuggestion() {
// console.log('prior suggestion', this.priorSuggestion);
if (this.hasPriorSuggestion) {
// console.log('addPriorSuggestion',);
this.suggested.unshift(this.priorSuggestion);
this.selected.unshift(this.priorSuggestion);
this.newPriorSuggestion(null); function resetSuggestion() {
} search.query = "";
}, search.suggested = [];
newPriorSuggestion(entity) { }
// console.log('newPriorSuggestion', entity);
if (entity !== null) { function resetSelection() {
let suggestion = { search.selected = [];
key: entity.type + entity.id, }
relevance: 0.5,
result: entity, function resetSearch() {
}; resetSelection();
this.search.priorSuggestion = suggestion; resetSuggestion();
// console.log('search priorSuggestion', this.search.priorSuggestion); }
this.addPriorSuggestion(suggestion);
} else { function selectAll() {
this.search.priorSuggestion = {}; search.suggested.forEach((item) => {
} search.selected.push(item);
}, });
saveFormOnTheFly({ type, data }) { }
console.log(
"saveFormOnTheFly from addPersons, type", function newPriorSuggestion(entity: Result | null) {
type, if (entity !== null) {
", data", let suggestion = {
key: entity.type + entity.id,
relevance: 0.5,
result: entity,
};
search.priorSuggestion = suggestion;
} else {
search.priorSuggestion = {};
}
}
async function saveFormOnTheFly({
type,
data,
}: {
type: string;
data: Result;
}) {
try {
if (type === "person") {
const responsePerson: Result = await makeFetch(
"POST",
"/api/1.0/person/person.json",
data, data,
); );
if (type === "person") { newPriorSuggestion(responsePerson);
makeFetch("POST", "/api/1.0/person/person.json", data) if (onTheFly.value) onTheFly.value.closeModal();
.then((responsePerson) => {
this.newPriorSuggestion(responsePerson);
this.$refs.onTheFly.closeModal();
if (null !== data.addressId) { if (data.addressId != null) {
const household = { const household = { type: "household" };
type: "household", const address = { id: data.addressId };
}; try {
const address = { const responseHousehold: Result = await makeFetch(
id: data.addressId, "POST",
}; "/api/1.0/person/household.json",
makeFetch("POST", "/api/1.0/person/household.json", household) household,
.then((responseHousehold) => { );
const member = { const member = {
concerned: [ concerned: [
{ {
person: { person: {
type: "person", type: "person",
id: responsePerson.id, id: responsePerson.id,
}, },
start_date: { start_date: {
// TODO: use date.ts methods (low priority) datetime: `${new Date().toISOString().split("T")[0]}T00:00:00+02:00`,
datetime: `${new Date().toISOString().split("T")[0]}T00:00:00+02:00`, },
}, holder: false,
holder: false, comment: null,
comment: null, },
}, ],
], destination: {
destination: { type: "household",
type: "household", id: responseHousehold.id,
id: responseHousehold.id, },
}, composition: null,
composition: null, };
}; await makeFetch(
return makeFetch( "POST",
"POST", "/api/1.0/person/household/members/move.json",
"/api/1.0/person/household/members/move.json", member,
member, );
) try {
.then((_response) => { const _response = await makeFetch(
makeFetch( "POST",
"POST", `/api/1.0/person/household/${responseHousehold.id}/address.json`,
`/api/1.0/person/household/${responseHousehold.id}/address.json`, address,
address, );
) console.log(_response);
.then((_response) => { } catch (error) {
console.log(_response); console.error(error);
}) }
.catch((error) => { } catch (error) {
if (error.name === "ValidationException") { console.error(error);
for (let v of error.violations) { }
this.$toast.open({ message: v });
}
} else {
this.$toast.open({ message: "An error occurred" });
}
});
})
.catch((error) => {
if (error.name === "ValidationException") {
for (let v of error.violations) {
this.$toast.open({ message: v });
}
} else {
this.$toast.open({ message: "An error occurred" });
}
});
})
.catch((error) => {
if (error.name === "ValidationException") {
for (let v of error.violations) {
this.$toast.open({ message: v });
}
} else {
this.$toast.open({ message: "An error occurred" });
}
});
}
})
.catch((error) => {
if (error.name === "ValidationException") {
for (let v of error.violations) {
this.$toast.open({ message: v });
}
} else {
this.$toast.open({ message: "An error occurred" });
}
});
} else if (type === "thirdparty") {
makeFetch("POST", "/api/1.0/thirdparty/thirdparty.json", data)
.then((response) => {
this.newPriorSuggestion(response);
this.$refs.onTheFly.closeModal();
})
.catch((error) => {
if (error.name === "ValidationException") {
for (let v of error.violations) {
this.$toast.open({ message: v });
}
} else {
this.$toast.open({ message: "An error occurred" });
}
});
} }
}, } else if (type === "thirdparty") {
const response: Result = await makeFetch(
"POST",
"/api/1.0/thirdparty/thirdparty.json",
data,
);
newPriorSuggestion(response);
if (onTheFly.value) onTheFly.value.closeModal();
}
} catch (error) {
console.error(error);
}
}
watch(
() => props.selected,
(newSelected) => {
search.selected = newSelected;
}, },
}; { deep: true },
);
watch(
() => props.suggested,
(newSuggested) => {
search.suggested = newSuggested;
},
{ deep: true },
);
watch(
() => modal,
(val) => {
showModal.value = val.value.showModal;
modalDialogClass.value = val.value.modalDialogClass;
},
{ deep: true },
);
defineExpose({
resetSearch,
showModal,
});
</script> </script>
<style lang="scss"> <style lang="scss">
@ -438,7 +450,6 @@ div.body-head {
input { input {
width: 100%; width: 100%;
padding: 1.2em 1.5em 1.2em 2.5em; padding: 1.2em 1.5em 1.2em 2.5em;
//margin: 1em 0;
} }
i { i {
position: absolute; position: absolute;

View File

@ -3,87 +3,80 @@
<label> <label>
<div> <div>
<input <input
v-bind:type="type" :type="type"
v-model="selected" v-model="selected"
name="item" name="item"
v-bind:id="item" :id="item.key"
v-bind:value="setValueByType(item, type)" :value="setValueByType(item, type)"
/> />
</div> </div>
<suggestion-person <suggestion-person
v-if="item.result.type === 'person'" v-if="item.result.type === 'person'"
v-bind:item="item" :item="item"
> ></suggestion-person>
</suggestion-person>
<suggestion-third-party <suggestion-third-party
v-if="item.result.type === 'thirdparty'" v-if="item.result.type === 'thirdparty'"
@newPriorSuggestion="newPriorSuggestion" @newPriorSuggestion="newPriorSuggestion"
v-bind:item="item" :item="item"
> ></suggestion-third-party>
</suggestion-third-party>
<suggestion-user v-if="item.result.type === 'user'" v-bind:item="item"> <suggestion-user
</suggestion-user> v-if="item.result.type === 'user'"
:item="item"
></suggestion-user>
<suggestion-user-group <suggestion-user-group
v-if="item.result.type === 'user_group'" v-if="item.result.type === 'user_group'"
v-bind:item="item" :item="item"
> ></suggestion-user-group>
></suggestion-user-group
>
<suggestion-household <suggestion-household
v-if="item.result.type === 'household'" v-if="item.result.type === 'household'"
v-bind:item="item" :item="item"
> ></suggestion-household>
</suggestion-household>
</label> </label>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import SuggestionPerson from "./TypePerson"; import { computed } from "vue";
import SuggestionThirdParty from "./TypeThirdParty";
import SuggestionUser from "./TypeUser";
import SuggestionHousehold from "./TypeHousehold";
import SuggestionUserGroup from "./TypeUserGroup";
export default { // Components
name: "PersonSuggestion", import SuggestionPerson from "./TypePerson.vue";
components: { import SuggestionThirdParty from "./TypeThirdParty.vue";
SuggestionPerson, import SuggestionUser from "./TypeUser.vue";
SuggestionThirdParty, import SuggestionHousehold from "./TypeHousehold.vue";
SuggestionUser, import SuggestionUserGroup from "./TypeUserGroup.vue";
SuggestionHousehold,
SuggestionUserGroup, // Types
}, import { Result, Suggestion } from "ChillPersonAssets/types";
props: ["item", "search", "type"],
emits: ["updateSelected", "newPriorSuggestion"], const props = defineProps<{
computed: { item: Suggestion;
selected: { search: { selected: Suggestion[] };
set(value) { type: string;
//console.log('value', value); }>();
this.$emit("updateSelected", value); const emit = defineEmits(["updateSelected", "newPriorSuggestion"]);
},
get() { // v-model for selected
return this.search.selected; const selected = computed({
}, get: () => props.search.selected,
}, set: (value) => emit("updateSelected", value),
isChecked() { });
return this.search.selected.indexOf(this.item) === -1 ? false : true;
}, const isChecked = computed(
}, () => props.search.selected.indexOf(props.item) !== -1,
methods: { );
setValueByType(value, type) {
return type === "radio" ? [value] : value; function setValueByType(value: Suggestion, type: string) {
}, return type === "radio" ? [value] : value;
newPriorSuggestion(response) { }
this.$emit("newPriorSuggestion", response);
}, function newPriorSuggestion(response: Result) {
}, emit("newPriorSuggestion", response);
}; }
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -1,26 +1,25 @@
<template> <template>
<div class="container household"> <div class="container household">
<household-render-box <HouseholdRenderBox
:household="item.result" :household="item.result"
:is-address-multiline="false" :is-address-multiline="false"
/> />
</div> </div>
<div class="right_actions"> <div class="right_actions">
<badge-entity :entity="item.result" :options="{ displayLong: true }" /> <BadgeEntity :entity="item.result" :options="{ displayLong: true }" />
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { defineProps } from "vue";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue"; import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import HouseholdRenderBox from "ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue"; import HouseholdRenderBox from "ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue";
import { Suggestion } from "ChillPersonAssets/types";
export default { interface TypeHouseholdProps {
name: "SuggestionHousehold", item: Suggestion;
components: { }
BadgeEntity,
HouseholdRenderBox, defineProps<TypeHouseholdProps>();
},
props: ["item"],
};
</script> </script>

View File

@ -4,11 +4,11 @@
<person-text :person="item.result" /> <person-text :person="item.result" />
</span> </span>
<span class="birthday" v-if="hasBirthdate"> <span class="birthday" v-if="hasBirthdate">
{{ $d(item.result.birthdate.datetime, "short") }} {{ formatDate(item.result.birthdate?.datetime, "short") }}
</span> </span>
<span class="location" v-if="hasAddress"> <span class="location" v-if="hasAddress">
{{ item.result.current_household_address.text }} - {{ item.result.current_household_address?.text }} -
{{ item.result.current_household_address.postcode.name }} {{ item.result.current_household_address?.postcode?.name }}
</span> </span>
</div> </div>
@ -18,26 +18,38 @@
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import { computed, defineProps } from "vue";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue"; import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue"; import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue"; import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
export default { function formatDate(dateString: string | undefined, format: string) {
name: "SuggestionPerson", if (!dateString) return "";
components: { // Use Intl.DateTimeFormat or your preferred formatting here
OnTheFly, const date = new Date(dateString);
BadgeEntity, if (format === "short") {
PersonText, return date.toLocaleDateString();
}, }
props: ["item"], return date.toString();
computed: { }
hasBirthdate() {
return this.item.result.birthdate !== null; const props = defineProps<{
}, item: {
hasAddress() { result: {
return this.item.result.current_household_address !== null; id: number | string;
}, birthdate: { datetime: string } | null;
}, current_household_address: {
}; text: string;
postcode: { name: string };
} | null;
// add other fields as needed
};
};
}>();
const hasBirthdate = computed(() => props.item.result.birthdate !== null);
const hasAddress = computed(
() => props.item.result.current_household_address !== null,
);
</script> </script>

View File

@ -7,13 +7,13 @@
<span class="name"> {{ item.result.text }}&nbsp; </span> <span class="name"> {{ item.result.text }}&nbsp; </span>
<span class="location"> <span class="location">
<template v-if="hasAddress"> <template v-if="hasAddress">
{{ getAddress.text }} - {{ getAddress?.text }} -
{{ getAddress.postcode.name }} {{ getAddress?.postcode?.name }}
</template> </template>
</span> </span>
</div> </div>
<div class="tpartyparent" v-if="hasParent"> <div class="tpartyparent" v-if="hasParent">
<span class="name"> > {{ item.result.parent.text }} </span> <span class="name"> &gt; {{ item.result.parent?.text }} </span>
</div> </div>
</div> </div>
@ -30,11 +30,72 @@
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import { computed, ref } from "vue";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue"; import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue"; import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { useToast } from "vue-toast-notification";
import { ThirdParty } from "ChillMainAssets/types";
import { Result, Suggestion } from "ChillPersonAssets/types";
interface TypeThirdPartyProps {
item: Suggestion;
}
const props = defineProps<TypeThirdPartyProps>();
const emit = defineEmits<(e: "newPriorSuggestion", payload: unknown) => void>();
const onTheFly = ref<InstanceType<typeof OnTheFly> | null>(null);
const toast = useToast();
const hasAddress = computed(() => {
if (props.item.result.address !== null) {
return true;
}
if (props.item.result.parent !== null) {
if (props.item.result.parent) {
return props.item.result.parent.address !== null;
}
}
return false;
});
const hasParent = computed(() => {
return props.item.result.parent !== null;
});
const getAddress = computed(() => {
if (props.item.result.address !== null) {
return props.item.result.address;
}
if (props.item.result.parent && props.item.result.parent.address !== null) {
return props.item.result.parent.address;
}
return null;
});
function saveFormOnTheFly({ data }: { data: ThirdParty }) {
makeFetch("POST", "/api/1.0/thirdparty/thirdparty.json", data)
.then((response: unknown) => {
const result = response as Result;
emit("newPriorSuggestion", result);
if (onTheFly.value) onTheFly.value.closeModal();
})
.catch((error: unknown) => {
const errorResponse = error as { name: string; violations: string[] };
if (errorResponse.name === "ValidationException") {
for (let v of errorResponse.violations) {
if (toast) toast.open({ message: v });
}
} else {
if (toast) toast.open({ message: "An error occurred" });
}
});
}
// i18n config (if needed elsewhere)
const i18n = { const i18n = {
messages: { messages: {
fr: { fr: {
@ -46,59 +107,9 @@ const i18n = {
}, },
}, },
}; };
defineExpose({
export default {
name: "SuggestionThirdParty",
components: {
OnTheFly,
BadgeEntity,
},
props: ["item"],
emits: ["newPriorSuggestion"],
i18n, i18n,
computed: { });
hasAddress() {
if (this.$props.item.result.address !== null) {
return true;
}
if (this.$props.item.result.parent !== null) {
return this.$props.item.result.parent.address !== null;
}
return false;
},
hasParent() {
return this.$props.item.result.parent !== null;
},
getAddress() {
if (this.$props.item.result.address !== null) {
return this.$props.item.result.address;
}
if (this.$props.item.result.parent.address !== null) {
return this.$props.item.result.parent.address;
}
return null;
},
},
methods: {
saveFormOnTheFly({ data }) {
makeFetch("POST", "/api/1.0/thirdparty/thirdparty.json", data)
.then((response) => {
this.$emit("newPriorSuggestion", response);
this.$refs.onTheFly.closeModal();
})
.catch((error) => {
if (error.name === "ValidationException") {
for (let v of error.violations) {
this.$toast.open({ message: v });
}
} else {
this.$toast.open({ message: "An error occurred" });
}
});
},
},
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,31 +1,31 @@
<template> <template>
<div class="container usercontainer"> <div class="container usercontainer">
<div class="user-identification"> <div class="user-identification">
<user-render-box-badge :user="item.result" /> <UserRenderBoxBadge :user="item.result" />
</div> </div>
</div> </div>
<div class="right_actions"> <div class="right_actions">
<badge-entity :entity="item.result" :options="{ displayLong: true }" /> <BadgeEntity :entity="item.result" :options="{ displayLong: true }" />
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import { computed, defineProps } from "vue";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue"; import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue"; import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
import { Suggestion } from "ChillPersonAssets/types";
export default { interface TypeUserProps {
name: "SuggestionUser", item: Suggestion;
components: { }
UserRenderBoxBadge,
BadgeEntity, const props = defineProps<TypeUserProps>();
},
props: ["item"], const hasParent = computed(() => props.item.result.parent !== null);
computed: {
hasParent() { defineExpose({
return this.$props.item.result.parent !== null; hasParent,
}, });
},
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,25 +1,7 @@
<script setup lang="ts">
import {
ResultItem,
UserGroup,
} from "../../../../../../ChillMainBundle/Resources/public/types";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
import UserGroupRenderBox from "ChillMainAssets/vuejs/_components/Entity/UserGroupRenderBox.vue";
interface TypeUserGroupProps {
item: ResultItem<UserGroup>;
}
const props = defineProps<TypeUserGroupProps>();
</script>
<template> <template>
<div class="container user-group-container"> <div class="container user-group-container">
<div class="user-group-identification"> <div class="user-group-identification">
<user-group-render-box <user-group-render-box :user-group="props.item.result as UserGroup" />
:user-group="props.item.result"
></user-group-render-box>
</div> </div>
</div> </div>
<div class="right_actions"> <div class="right_actions">
@ -27,4 +9,16 @@ const props = defineProps<TypeUserGroupProps>();
</div> </div>
</template> </template>
<script setup lang="ts">
import { UserGroup } from "../../../../../../ChillMainBundle/Resources/public/types";
import UserGroupRenderBox from "ChillMainAssets/vuejs/_components/Entity/UserGroupRenderBox.vue";
import { Suggestion } from "ChillPersonAssets/types";
interface TypeUserGroupProps {
item: Suggestion;
}
const props = defineProps<TypeUserGroupProps>();
</script>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@ -5,11 +5,11 @@
<!-- identifier --> <!-- identifier -->
<div v-if="isHouseholdNew()" class="h4"> <div v-if="isHouseholdNew()" class="h4">
<i class="fa fa-home" /> <i class="fa fa-home" />
{{ $t("new_household") }} {{ trans(RENDERBOX_NEW_HOUSEHOLD) }}
</div> </div>
<div v-else class="h4"> <div v-else class="h4">
<i class="fa fa-home" /> <i class="fa fa-home" />
{{ $t("household_number", { number: household.id }) }} {{ trans(RENDERBOX_HOUSEHOLD_NUMBER, { number: household.id }) }}
</div> </div>
</div> </div>
<div class="item-col"> <div class="item-col">
@ -18,7 +18,7 @@
<li <li
v-if="hasCurrentMembers" v-if="hasCurrentMembers"
class="members" class="members"
:title="$t('current_members')" :title="trans(RENDERBOX_CURRENT_MEMBERS)"
> >
<span <span
v-for="m in currentMembers()" v-for="m in currentMembers()"
@ -42,9 +42,9 @@
</person-render-box> </person-render-box>
</span> </span>
</li> </li>
<li v-else class="members" :title="$t('current_members')"> <li v-else class="members" :title="trans(RENDERBOX_CURRENT_MEMBERS)">
<p class="chill-no-data-statement"> <p class="chill-no-data-statement">
{{ $t("no_members_yet") }} {{ trans(RENDERBOX_NO_MEMBERS_YET) }}
</p> </p>
</li> </li>
@ -57,7 +57,7 @@
</li> </li>
<li v-else> <li v-else>
<span class="chill-no-data-statement">{{ <span class="chill-no-data-statement">{{
$t("no_current_address") trans(RENDERBOX_NO_CURRENT_ADDRESS)
}}</span> }}</span>
</li> </li>
</ul> </ul>
@ -65,89 +65,82 @@
</div> </div>
</section> </section>
</template> </template>
<script setup>
<script> import { computed } from "vue";
import PersonRenderBox from "ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue"; import PersonRenderBox from "ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue";
import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue"; import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue";
import {
trans,
RENDERBOX_NEW_HOUSEHOLD,
RENDERBOX_HOUSEHOLD_NUMBER,
RENDERBOX_CURRENT_MEMBERS,
RENDERBOX_NO_MEMBERS_YET,
RENDERBOX_NO_CURRENT_ADDRESS,
} from "translator";
const i18n = { const props = defineProps({
messages: { household: { type: Object, required: true },
fr: { isAddressMultiline: { type: Boolean, default: false },
household_number: "Ménage n°{number}", });
current_members: "Membres actuels",
no_current_address: "Sans adresse actuellement",
new_household: "Nouveau ménage",
no_members_yet: "Aucun membre actuellement",
holder: "titulaire",
},
},
};
export default { const isMultiline = computed(() =>
name: "HouseholdRenderBox", typeof props.isAddressMultiline !== "undefined"
props: ["household", "isAddressMultiline"], ? props.isAddressMultiline
components: { : false,
PersonRenderBox, );
AddressRenderBox,
},
i18n,
computed: {
isMultiline() {
return typeof this.isAddressMultiline !== "undefined"
? this.isAddressMultiline
: false;
},
},
methods: {
hasCurrentMembers() {
return this.household.current_members_id.length > 0;
},
currentMembers() {
let members = this.household.members
.filter((m) => this.household.current_members_id.includes(m.id))
.sort((a, b) => {
const orderA = a.position ? a.position.ordering : 0;
const orderB = b.position ? b.position.ordering : 0;
if (orderA < orderB) { function hasCurrentMembers() {
return -1; return props.household.current_members_id.length > 0;
} }
if (orderA > orderB) {
return 1;
}
if (a.holder && !b.holder) {
return -1;
}
if (!a.holder && b.holder) {
return 1;
}
return 0;
});
if (this.household.new_members !== undefined) { function currentMembers() {
this.household.new_members let members = props.household.members
.map((m) => { .filter((m) => props.household.current_members_id.includes(m.id))
m.is_new = true; .sort((a, b) => {
return m; const orderA = a.position ? a.position.ordering : 0;
}) const orderB = b.position ? b.position.ordering : 0;
.forEach((m) => {
members.push(m); if (orderA < orderB) {
}); return -1;
} }
if (orderA > orderB) {
return 1;
}
if (a.holder && !b.holder) {
return -1;
}
if (!a.holder && b.holder) {
return 1;
}
return 0;
});
return members; if (props.household.new_members !== undefined) {
}, props.household.new_members
currentMembersLength() { .map((m) => {
return this.household.current_members_id.length; m.is_new = true;
}, return m;
isHouseholdNew() { })
return !Number.isInteger(this.household.id); .forEach((m) => {
}, members.push(m);
hasAddress() { });
return this.household.current_address !== null; }
},
}, return members;
}; }
function currentMembersLength() {
return props.household.current_members_id.length;
}
function isHouseholdNew() {
return !Number.isInteger(props.household.id);
}
function hasAddress() {
return props.household.current_address !== null;
}
defineExpose(currentMembersLength);
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -23,7 +23,7 @@
</a> </a>
<!-- use person-text here to avoid code duplication ? TODO --> <!-- use person-text here to avoid code duplication ? TODO -->
<span class="firstname">{{ person.firstName }}</span> <span class="firstname">{{ person.firstName + " " }}</span>
<span class="lastname">{{ person.lastName }}</span> <span class="lastname">{{ person.lastName }}</span>
<span v-if="person.suffixText" class="suffixtext" <span v-if="person.suffixText" class="suffixtext"
>&nbsp;{{ person.suffixText }}</span >&nbsp;{{ person.suffixText }}</span
@ -63,13 +63,11 @@
:title="birthdate" :title="birthdate"
> >
{{ {{
$t( trans(birthdateTranslation) +
person.gender
? `renderbox.birthday.${person.gender.genderTranslation}`
: "renderbox.birthday.neutral",
) +
" " + " " +
$d(birthdate, "text") new Intl.DateTimeFormat("fr-FR", {
dateStyle: "long",
}).format(birthdate)
}} }}
</time> </time>
@ -78,7 +76,17 @@
:datetime="person.deathdate" :datetime="person.deathdate"
:title="person.deathdate" :title="person.deathdate"
> >
{{ $d(birthdate) }} - {{ $d(deathdate) }} {{
new Intl.DateTimeFormat("fr-FR", {
dateStyle: "long",
}).format(birthdate)
}}
-
{{
new Intl.DateTimeFormat("fr-FR", {
dateStyle: "long",
}).format(deathdate)
}}
</time> </time>
<time <time
@ -86,12 +94,18 @@
:datetime="person.deathdate" :datetime="person.deathdate"
:title="person.deathdate" :title="person.deathdate"
> >
{{ $t("renderbox.deathdate") + " " + deathdate }} {{
trans(RENDERBOX_DEATHDATE) +
" " +
new Intl.DateTimeFormat("fr-FR", {
dateStyle: "long",
}).format(deathdate)
}}
</time> </time>
<span v-if="options.addAge && person.birthdate" class="age">{{ <span v-if="options.addAge && person.birthdate" class="age">
$tc("renderbox.years_old", person.age) ({{ trans(RENDERBOX_YEARS_OLD, { n: person.age }) }})
}}</span> </span>
</p> </p>
</div> </div>
</div> </div>
@ -111,13 +125,13 @@
:is-multiline="isMultiline" :is-multiline="isMultiline"
/> />
<p v-else class="chill-no-data-statement"> <p v-else class="chill-no-data-statement">
{{ $t("renderbox.household_without_address") }} {{ trans(RENDERBOX_HOUSEHOLD_WITHOUT_ADDRESS) }}
</p> </p>
<a <a
v-if="options.addHouseholdLink === true" v-if="options.addHouseholdLink === true"
:href="getCurrentHouseholdUrl" :href="getCurrentHouseholdUrl"
:title=" :title="
$t('persons_associated.show_household_number', { trans(PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER, {
id: person.current_household_id, id: person.current_household_id,
}) })
" "
@ -125,14 +139,14 @@
<span class="badge rounded-pill bg-chill-beige"> <span class="badge rounded-pill bg-chill-beige">
<i <i
class="fa fa-fw fa-home" class="fa fa-fw fa-home"
/><!--{{ $t('persons_associated.show_household') }}--> /><!--{{ trans(PERSONS_ASSOCIATED_SHOW_HOUSEHOLD) }}-->
</span> </span>
</a> </a>
</li> </li>
<li v-else-if="options.addNoData"> <li v-else-if="options.addNoData">
<i class="fa fa-li fa-map-marker" /> <i class="fa fa-li fa-map-marker" />
<p class="chill-no-data-statement"> <p class="chill-no-data-statement">
{{ $t("renderbox.no_data") }} {{ trans(RENDERBOX_NO_DATA) }}
</p> </p>
</li> </li>
@ -148,9 +162,9 @@
> >
<i class="fa fa-li fa-map-marker" /> <i class="fa fa-li fa-map-marker" />
<div v-if="addr.address"> <div v-if="addr.address">
<span class="item-key" <span class="item-key">
>{{ $t("renderbox.residential_address") }}:</span {{ trans(RENDERBOX_RESIDENTIAL_ADDRESS) }}:
> </span>
<div style="margin-top: -1em"> <div style="margin-top: -1em">
<address-render-box <address-render-box
:address="addr.address" :address="addr.address"
@ -159,7 +173,7 @@
</div> </div>
</div> </div>
<div v-else-if="addr.hostPerson" class="mt-3"> <div v-else-if="addr.hostPerson" class="mt-3">
<p>{{ $t("renderbox.located_at") }}:</p> <p>{{ trans(RENDERBOX_LOCATED_AT) }}:</p>
<span class="chill-entity entity-person badge-person"> <span class="chill-entity entity-person badge-person">
<person-text <person-text
v-if="addr.hostPerson" v-if="addr.hostPerson"
@ -173,7 +187,7 @@
/> />
</div> </div>
<div v-else-if="addr.hostThirdParty" class="mt-3"> <div v-else-if="addr.hostThirdParty" class="mt-3">
<p>{{ $t("renderbox.located_at") }}:</p> <p>{{ trans(RENDERBOX_LOCATED_AT) }}:</p>
<span class="chill-entity entity-person badge-thirdparty"> <span class="chill-entity entity-person badge-thirdparty">
<third-party-text <third-party-text
v-if="addr.hostThirdParty" v-if="addr.hostThirdParty"
@ -196,32 +210,32 @@
<li v-else-if="options.addNoData"> <li v-else-if="options.addNoData">
<i class="fa fa-li fa-envelope-o" /> <i class="fa fa-li fa-envelope-o" />
<p class="chill-no-data-statement"> <p class="chill-no-data-statement">
{{ $t("renderbox.no_data") }} {{ trans(RENDERBOX_NO_DATA) }}
</p> </p>
</li> </li>
<li v-if="person.mobilenumber"> <li v-if="person.mobilenumber">
<i class="fa fa-li fa-mobile" /> <i class="fa fa-li fa-mobile" />
<a :href="'tel: ' + person.mobilenumber">{{ <a :href="'tel: ' + person.mobilenumber">
person.mobilenumber {{ person.mobilenumber }}
}}</a> </a>
</li> </li>
<li v-else-if="options.addNoData"> <li v-else-if="options.addNoData">
<i class="fa fa-li fa-mobile" /> <i class="fa fa-li fa-mobile" />
<p class="chill-no-data-statement"> <p class="chill-no-data-statement">
{{ $t("renderbox.no_data") }} {{ trans(RENDERBOX_NO_DATA) }}
</p> </p>
</li> </li>
<li v-if="person.phonenumber"> <li v-if="person.phonenumber">
<i class="fa fa-li fa-phone" /> <i class="fa fa-li fa-phone" />
<a :href="'tel: ' + person.phonenumber">{{ <a :href="'tel: ' + person.phonenumber">
person.phonenumber {{ person.phonenumber }}
}}</a> </a>
</li> </li>
<li v-else-if="options.addNoData"> <li v-else-if="options.addNoData">
<i class="fa fa-li fa-phone" /> <i class="fa fa-li fa-phone" />
<p class="chill-no-data-statement"> <p class="chill-no-data-statement">
{{ $t("renderbox.no_data") }} {{ trans(RENDERBOX_NO_DATA) }}
</p> </p>
</li> </li>
@ -240,7 +254,7 @@
<li v-else-if="options.addNoData"> <li v-else-if="options.addNoData">
<i class="fa fa-li fa-long-arrow-right" /> <i class="fa fa-li fa-long-arrow-right" />
<p class="chill-no-data-statement"> <p class="chill-no-data-statement">
{{ $t("renderbox.no_data") }} {{ trans(RENDERBOX_NO_DATA) }}
</p> </p>
</li> </li>
<slot name="custom-zone" /> <slot name="custom-zone" />
@ -262,7 +276,7 @@
<span <span
v-if="options.isHolder" v-if="options.isHolder"
class="fa-stack fa-holder" class="fa-stack fa-holder"
:title="$t('renderbox.holder')" :title="trans(RENDERBOX_HOLDER)"
> >
<i class="fa fa-circle fa-stack-1x text-success" /> <i class="fa fa-circle fa-stack-1x text-success" />
<i class="fa fa-stack-1x">T</i> <i class="fa fa-stack-1x">T</i>
@ -274,7 +288,7 @@
<span <span
v-if="options.isHolder" v-if="options.isHolder"
class="fa-stack fa-holder" class="fa-stack fa-holder"
:title="$t('renderbox.holder')" :title="trans(RENDERBOX_HOLDER)"
> >
<i class="fa fa-circle fa-stack-1x text-success" /> <i class="fa fa-circle fa-stack-1x text-success" />
<i class="fa fa-stack-1x">T</i> <i class="fa fa-stack-1x">T</i>
@ -285,131 +299,120 @@
</span> </span>
</template> </template>
<script> <script setup>
import { computed } from "vue";
import { ISOToDate } from "ChillMainAssets/chill/js/date"; import { ISOToDate } from "ChillMainAssets/chill/js/date";
import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue"; import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue";
import GenderIconRenderBox from "ChillMainAssets/vuejs/_components/Entity/GenderIconRenderBox.vue"; import GenderIconRenderBox from "ChillMainAssets/vuejs/_components/Entity/GenderIconRenderBox.vue";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue"; import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue"; import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
import ThirdPartyText from "ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyText.vue"; import ThirdPartyText from "ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyText.vue";
import {
trans,
RENDERBOX_HOLDER,
RENDERBOX_NO_DATA,
RENDERBOX_DEATHDATE,
RENDERBOX_HOUSEHOLD_WITHOUT_ADDRESS,
RENDERBOX_RESIDENTIAL_ADDRESS,
RENDERBOX_LOCATED_AT,
RENDERBOX_BIRTHDAY_MAN,
RENDERBOX_BIRTHDAY_WOMAN,
RENDERBOX_BIRTHDAY_UNKNOWN,
RENDERBOX_BIRTHDAY_NEUTRAL,
PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER,
RENDERBOX_YEARS_OLD,
} from "translator";
export default { const props = defineProps({
name: "PersonRenderBox", person: {
components: { required: true,
AddressRenderBox,
GenderIconRenderBox,
BadgeEntity,
PersonText,
ThirdPartyText,
}, },
props: { options: {
person: { type: Object,
required: true, required: false,
},
options: {
type: Object,
required: false,
},
render: {
type: String,
},
returnPath: {
type: String,
},
showResidentialAddresses: {
type: Boolean,
default: false,
},
}, },
computed: { render: {
isMultiline: function () { type: String,
if (this.options.isMultiline) {
return this.options.isMultiline;
} else {
return false;
}
},
birthdate: function () {
if (
this.person.birthdate !== null ||
this.person.birthdate === "undefined"
) {
return ISOToDate(this.person.birthdate.datetime);
} else {
return "";
}
},
deathdate: function () {
if (
this.person.deathdate !== null ||
this.person.birthdate === "undefined"
) {
return new Date(this.person.deathdate.datetime);
} else {
return "";
}
},
altNameLabel: function () {
let altNameLabel = "";
this.person.altNames.forEach(
(altName) => (altNameLabel += altName.label),
);
return altNameLabel;
},
altNameKey: function () {
let altNameKey = "";
this.person.altNames.forEach((altName) => (altNameKey += altName.key));
return altNameKey;
},
getUrl: function () {
return `/fr/person/${this.person.id}/general`;
},
getCurrentHouseholdUrl: function () {
let returnPath = this.returnPath ? `?returnPath=${this.returnPath}` : ``;
return `/fr/person/household/${this.person.current_household_id}/summary${returnPath}`;
},
}, },
}; returnPath: {
</script> type: String,
},
showResidentialAddresses: {
type: Boolean,
default: false,
},
});
<style lang="scss" scoped> const birthdateTranslation = computed(() => {
@import "ChillMainAssets/module/bootstrap/shared"; if (props.person.gender) {
@import "ChillPersonAssets/chill/scss/mixins"; const { genderTranslation } = props.person.gender;
@import "ChillMainAssets/chill/scss/chill_variables"; switch (genderTranslation) {
case "man":
.lastname:before { return RENDERBOX_BIRTHDAY_MAN;
content: " "; case "woman":
} return RENDERBOX_BIRTHDAY_WOMAN;
case "neutral":
div.flex-table { return RENDERBOX_BIRTHDAY_NEUTRAL;
div.item-bloc { case "unknown":
div.item-row { return RENDERBOX_BIRTHDAY_UNKNOWN;
div.item-col:first-child { default:
width: 33%; return RENDERBOX_BIRTHDAY_UNKNOWN;
}
@include media-breakpoint-down(sm) {
div.item-col:first-child {
width: unset;
}
}
div.item-col:last-child {
justify-content: flex-start;
}
} }
} else {
return RENDERBOX_BIRTHDAY_UNKNOWN;
} }
} });
.age { const isMultiline = computed(() => {
margin-left: 0.5em; return props.options?.isMultiline || false;
});
&:before { const birthdate = computed(() => {
content: "("; if (
props.person.birthdate !== null &&
props.person.birthdate !== undefined &&
props.person.birthdate.datetime
) {
return ISOToDate(props.person.birthdate.datetime);
} else {
return "";
} }
});
&:after { const deathdate = computed(() => {
content: ")"; if (
props.person.deathdate !== null &&
props.person.deathdate !== undefined &&
props.person.deathdate.datetime
) {
return new Date(props.person.deathdate.datetime);
} else {
return "";
} }
} });
</style>
const altNameLabel = computed(() => {
let altNameLabel = "";
(props.person.altNames || []).forEach(
(altName) => (altNameLabel += altName.label),
);
return altNameLabel;
});
const altNameKey = computed(() => {
let altNameKey = "";
(props.person.altNames || []).forEach(
(altName) => (altNameKey += altName.key),
);
return altNameKey;
});
const getUrl = computed(() => {
return `/fr/person/${props.person.id}/general`;
});
const getCurrentHouseholdUrl = computed(() => {
let returnPath = props.returnPath ? `?returnPath=${props.returnPath}` : ``;
return `/fr/person/household/${props.person.current_household_id}/summary${returnPath}`;
});
</script>

View File

@ -2,65 +2,65 @@
<span v-if="isCut">{{ cutText }}</span> <span v-if="isCut">{{ cutText }}</span>
<span v-else class="person-text"> <span v-else class="person-text">
<span class="firstname">{{ person.firstName }}</span> <span class="firstname">{{ person.firstName }}</span>
<!-- display: inline -->
<span class="lastname">&nbsp;{{ person.lastName }}</span> <span class="lastname">&nbsp;{{ person.lastName }}</span>
<span v-if="person.altNames && person.altNames.length > 0" class="altnames"> <span v-if="person.altNames && person.altNames.length > 0" class="altnames">
<!-- display: inline -->
<span :class="'altname altname-' + altNameKey" <span :class="'altname altname-' + altNameKey"
>&nbsp;({{ altNameLabel }})</span >&nbsp;({{ altNameLabel }})</span
> >
</span> </span>
<!-- display: inline -->
<span v-if="person.suffixText" class="suffixtext" <span v-if="person.suffixText" class="suffixtext"
>&nbsp;{{ person.suffixText }}</span >&nbsp;{{ person.suffixText }}</span
> >
<!-- display: inline -->
<span <span
class="age" class="age"
v-if=" v-if="addAge && person.birthdate !== null && person.deathdate === null"
this.addAge && person.birthdate !== null && person.deathdate === null >&nbsp;{{ trans(RENDERBOX_YEARS_OLD, person.age) }}</span
"
>&nbsp;{{ $tc("renderbox.years_old", person.age) }}</span
> >
<span v-else-if="this.addAge && person.deathdate !== null">&nbsp;()</span> <span v-else-if="addAge && person.deathdate !== null">&nbsp;()</span>
</span> </span>
</template> </template>
<script> <script lang="ts" setup>
export default { import { computed, toRefs } from "vue";
name: "PersonText", import { trans, RENDERBOX_YEARS_OLD } from "translator";
props: {
person: { interface AltName {
required: true, label: string;
}, key: string;
isCut: { }
type: Boolean,
required: false, interface Person {
default: false, firstName: string;
}, lastName: string;
addAge: { altNames: AltName[];
type: Boolean, suffixText?: string;
required: false, birthdate: string | null;
default: true, deathdate: string | null;
}, age: number;
}, text: string;
computed: { }
altNameLabel: function () {
let altNameLabel = ""; const props = defineProps<{
this.person.altNames.forEach( person: Person;
(altName) => (altNameLabel += altName.label), isCut?: boolean;
); addAge?: boolean;
return altNameLabel; }>();
},
altNameKey: function () { const { person, isCut = false, addAge = true } = toRefs(props);
let altNameKey = "";
this.person.altNames.forEach((altName) => (altNameKey += altName.key)); const altNameLabel = computed(() => {
return altNameKey; if (!person.value.altNames) return "";
}, return person.value.altNames.map((a: AltName) => a.label).join("");
cutText: function () { });
let more = this.person.text.length > 15 ? "…" : "";
return this.person.text.slice(0, 15) + more; const altNameKey = computed(() => {
}, if (!person.value.altNames) return "";
}, return person.value.altNames.map((a: AltName) => a.key).join("");
}; });
const cutText = computed(() => {
if (!person.value.text) return "";
const more = person.value.text.length > 15 ? "…" : "";
return person.value.text.slice(0, 15) + more;
});
</script> </script>

View File

@ -17,7 +17,7 @@
isMultiline: true, isMultiline: true,
}" }"
:show-residential-addresses="true" :show-residential-addresses="true"
></person-render-box> />
</div> </div>
</div> </div>
@ -27,10 +27,10 @@
class="form-control form-control-lg" class="form-control form-control-lg"
id="lastname" id="lastname"
v-model="lastName" v-model="lastName"
:placeholder="$t('person.lastname')" :placeholder="trans(PERSON_MESSAGES_PERSON_LASTNAME)"
@change="checkErrors" @change="checkErrors"
/> />
<label for="lastname">{{ $t("person.lastname") }}</label> <label for="lastname">{{ trans(PERSON_MESSAGES_PERSON_LASTNAME) }}</label>
</div> </div>
<div v-if="queryItems"> <div v-if="queryItems">
@ -50,10 +50,12 @@
class="form-control form-control-lg" class="form-control form-control-lg"
id="firstname" id="firstname"
v-model="firstName" v-model="firstName"
:placeholder="$t('person.firstname')" :placeholder="trans(PERSON_MESSAGES_PERSON_FIRSTNAME)"
@change="checkErrors" @change="checkErrors"
/> />
<label for="firstname">{{ $t("person.firstname") }}</label> <label for="firstname">{{
trans(PERSON_MESSAGES_PERSON_FIRSTNAME)
}}</label>
</div> </div>
<div v-if="queryItems"> <div v-if="queryItems">
@ -86,12 +88,14 @@
--> -->
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<select class="form-select form-select-lg" id="gender" v-model="gender"> <select class="form-select form-select-lg" id="gender" v-model="gender">
<option selected disabled>{{ $t("person.gender.placeholder") }}</option> <option selected disabled>
{{ trans(PERSON_MESSAGES_PERSON_GENDER_PLACEHOLDER) }}
</option>
<option v-for="g in config.genders" :value="g.id" :key="g.id"> <option v-for="g in config.genders" :value="g.id" :key="g.id">
{{ g.label }} {{ g.label }}
</option> </option>
</select> </select>
<label>{{ $t("person.gender.title") }}</label> <label>{{ trans(PERSON_MESSAGES_PERSON_GENDER_TITLE) }}</label>
</div> </div>
<div <div
@ -99,12 +103,14 @@
v-if="showCenters && config.centers.length > 1" v-if="showCenters && config.centers.length > 1"
> >
<select class="form-select form-select-lg" id="center" v-model="center"> <select class="form-select form-select-lg" id="center" v-model="center">
<option selected disabled>{{ $t("person.center.placeholder") }}</option> <option selected disabled>
{{ trans(PERSON_MESSAGES_PERSON_CENTER_PLACEHOLDER) }}
</option>
<option v-for="c in config.centers" :value="c" :key="c.id"> <option v-for="c in config.centers" :value="c" :key="c.id">
{{ c.name }} {{ c.name }}
</option> </option>
</select> </select>
<label>{{ $t("person.center.title") }}</label> <label>{{ trans(PERSON_MESSAGES_PERSON_CENTER_TITLE) }}</label>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
@ -114,64 +120,50 @@
v-model="civility" v-model="civility"
> >
<option selected disabled> <option selected disabled>
{{ $t("person.civility.placeholder") }} {{ trans(PERSON_MESSAGES_PERSON_CIVILITY_PLACEHOLDER) }}
</option> </option>
<option v-for="c in config.civilities" :value="c.id" :key="c.id"> <option v-for="c in config.civilities" :value="c.id" :key="c.id">
{{ localizeString(c.name) }} {{ localizeString(c.name) }}
</option> </option>
</select> </select>
<label>{{ $t("person.civility.title") }}</label> <label>{{ trans(PERSON_MESSAGES_PERSON_CIVILITY_TITLE) }}</label>
</div> </div>
<div class="input-group mb-3"> <div class="input-group mb-3">
<span class="input-group-text" id="birthdate" <span class="input-group-text" id="phonenumber">
><i class="fa fa-fw fa-birthday-cake"></i <i class="fa fa-fw fa-phone"></i>
></span> </span>
<input
type="date"
class="form-control form-control-lg"
id="chill_personbundle_person_birthdate"
name="chill_personbundle_person[birthdate]"
v-model="birthDate"
aria-describedby="birthdate"
/>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="phonenumber"
><i class="fa fa-fw fa-phone"></i
></span>
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
v-model="phonenumber" v-model="phonenumber"
:placeholder="$t('person.phonenumber')" :placeholder="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
:aria-label="$t('person.phonenumber')" :aria-label="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
aria-describedby="phonenumber" aria-describedby="phonenumber"
/> />
</div> </div>
<div class="input-group mb-3"> <div class="input-group mb-3">
<span class="input-group-text" id="mobilenumber" <span class="input-group-text" id="mobilenumber">
><i class="fa fa-fw fa-mobile"></i <i class="fa fa-fw fa-mobile"></i>
></span> </span>
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
v-model="mobilenumber" v-model="mobilenumber"
:placeholder="$t('person.mobilenumber')" :placeholder="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
:aria-label="$t('person.mobilenumber')" :aria-label="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
aria-describedby="mobilenumber" aria-describedby="mobilenumber"
/> />
</div> </div>
<div class="input-group mb-3"> <div class="input-group mb-3">
<span class="input-group-text" id="email" <span class="input-group-text" id="email">
><i class="fa fa-fw fa-at"></i <i class="fa fa-fw fa-at"></i>
></span> </span>
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
v-model="email" v-model="email"
:placeholder="$t('person.email')" :placeholder="trans(PERSON_MESSAGES_PERSON_EMAIL)"
:aria-label="$t('person.email')" :aria-label="trans(PERSON_MESSAGES_PERSON_EMAIL)"
aria-describedby="email" aria-describedby="email"
/> />
</div> </div>
@ -183,22 +175,21 @@
v-model="showAddressForm" v-model="showAddressForm"
name="showAddressForm" name="showAddressForm"
/> />
<label class="form-check-label">{{ <label class="form-check-label">
$t("person.address.show_address_form") {{ trans(PERSON_MESSAGES_PERSON_ADDRESS_SHOW_ADDRESS_FORM) }}
}}</label> </label>
</div> </div>
<div <div
v-if="action === 'create' && showAddressFormValue" v-if="action === 'create' && showAddressFormValue"
class="form-floating mb-3" class="form-floating mb-3"
> >
<p>{{ $t("person.address.warning") }}</p> <p>{{ trans(PERSON_MESSAGES_PERSON_ADDRESS_WARNING) }}</p>
<add-address <AddAddress
:context="addAddress.context" :context="addAddress.context"
:options="addAddress.options" :options="addAddress.options"
:addressChangedCallback="submitNewAddress" :addressChangedCallback="submitNewAddress"
ref="addAddress" ref="addAddress"
> />
</add-address>
</div> </div>
<div class="alert alert-warning" v-if="errors.length"> <div class="alert alert-warning" v-if="errors.length">
@ -208,8 +199,8 @@
</div> </div>
</div> </div>
</template> </template>
<script setup>
<script> import { ref, reactive, computed, onMounted } from "vue";
import { import {
getCentersForPersonCreation, getCentersForPersonCreation,
getCivilities, getCivilities,
@ -220,286 +211,256 @@ import {
import PersonRenderBox from "../Entity/PersonRenderBox.vue"; import PersonRenderBox from "../Entity/PersonRenderBox.vue";
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue"; import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import {
trans,
PERSON_MESSAGES_PERSON_LASTNAME,
PERSON_MESSAGES_PERSON_FIRSTNAME,
PERSON_MESSAGES_PERSON_GENDER_PLACEHOLDER,
PERSON_MESSAGES_PERSON_GENDER_TITLE,
PERSON_MESSAGES_PERSON_CENTER_PLACEHOLDER,
PERSON_MESSAGES_PERSON_CENTER_TITLE,
PERSON_MESSAGES_PERSON_CIVILITY_PLACEHOLDER,
PERSON_MESSAGES_PERSON_CIVILITY_TITLE,
PERSON_MESSAGES_PERSON_PHONENUMBER,
PERSON_MESSAGES_PERSON_MOBILENUMBER,
PERSON_MESSAGES_PERSON_EMAIL,
PERSON_MESSAGES_PERSON_ADDRESS_SHOW_ADDRESS_FORM,
PERSON_MESSAGES_PERSON_ADDRESS_WARNING,
} from "translator";
export default { const props = defineProps({
name: "OnTheFlyPerson", id: [String, Number],
props: ["id", "type", "action", "query"], type: String,
//emits: ['createAction'], action: String,
components: { query: String,
PersonRenderBox, });
AddAddress,
},
data() {
return {
person: {
type: "person",
lastName: "",
firstName: "",
altNames: [],
addressId: null,
center: null,
},
config: {
altNames: [],
civilities: [],
centers: [],
genders: [],
},
showCenters: false, // NOTE: must remains false if the form is not in create mode
showAddressFormValue: false,
addAddress: {
options: {
button: {
text: { create: "person.address.create_address" },
size: "btn-sm",
},
title: { create: "person.address.create_address" },
},
context: {
target: {}, // boilerplate for getting the address id
edit: false,
addressId: null,
defaults: window.addaddress,
},
},
errors: [],
};
},
computed: {
firstName: {
set(value) {
this.person.firstName = value;
},
get() {
return this.person.firstName;
},
},
lastName: {
set(value) {
this.person.lastName = value;
},
get() {
return this.person.lastName;
},
},
gender: {
set(value) {
this.person.gender = { id: value, type: "chill_main_gender" };
},
get() {
return this.person.gender ? this.person.gender.id : null;
},
},
civility: {
set(value) {
this.person.civility = { id: value, type: "chill_main_civility" };
},
get() {
return this.person.civility ? this.person.civility.id : null;
},
},
birthDate: {
set(value) {
if (this.person.birthdate) {
this.person.birthdate.datetime = value + "T00:00:00+0100";
} else {
this.person.birthdate = { datetime: value + "T00:00:00+0100" };
}
},
get() {
return this.person.birthdate
? this.person.birthdate.datetime.split("T")[0]
: "";
},
},
phonenumber: {
set(value) {
this.person.phonenumber = value;
},
get() {
return this.person.phonenumber;
},
},
mobilenumber: {
set(value) {
this.person.mobilenumber = value;
},
get() {
return this.person.mobilenumber;
},
},
email: {
set(value) {
this.person.email = value;
},
get() {
return this.person.email;
},
},
showAddressForm: {
set(value) {
this.showAddressFormValue = value;
},
get() {
return this.showAddressFormValue;
},
},
center: {
set(value) {
console.log("will set center", value);
this.person.center = { id: value.id, type: value.type };
},
get() {
const center = this.config.centers.find(
(c) => this.person.center !== null && this.person.center.id === c.id,
);
console.log("center get", center); const person = reactive({
type: "person",
lastName: "",
firstName: "",
altNames: [],
addressId: null,
center: null,
gender: null,
civility: null,
birthdate: null,
phonenumber: "",
mobilenumber: "",
email: "",
});
return typeof center === "undefined" ? null : center; const config = reactive({
}, altNames: [],
}, civilities: [],
genderClass() { centers: [],
switch (this.person.gender) { genders: [],
case "woman": });
return "fa-venus";
case "man": const showCenters = ref(false);
return "fa-mars"; const showAddressFormValue = ref(false);
case "both": const errors = ref([]);
return "fa-neuter";
case "unknown": const addAddress = reactive({
return "fa-genderless"; options: {
default: button: {
return "fa-genderless"; text: { create: "person.address.create_address" },
} size: "btn-sm",
},
genderTranslation() {
switch (this.person.gender.genderTranslation) {
case "woman":
return "person.gender.woman";
case "man":
return "person.gender.man";
case "neutral":
return "person.gender.neutral";
case "unknown":
return "person.gender.unknown";
default:
return "person.gender.unknown";
}
},
feminized() {
return this.person.gender === "woman" ? "e" : "";
},
personAltNamesLabels() {
return this.person.altNames.map((a) => (a ? a.label : ""));
},
queryItems() {
return this.query ? this.query.split(" ") : null;
}, },
title: { create: "person.address.create_address" },
}, },
mounted() { context: {
getPersonAltNames().then((altNames) => { target: {},
this.config.altNames = altNames; edit: false,
}); addressId: null,
getCivilities().then((civilities) => { defaults: window.addaddress,
if ("results" in civilities) { },
this.config.civilities = civilities.results; });
}
}); const firstName = computed({
getGenders().then((genders) => { get: () => person.firstName,
if ("results" in genders) { set: (value) => {
console.log("genders", genders.results); person.firstName = value;
this.config.genders = genders.results; },
} });
}); const lastName = computed({
if (this.action !== "create") { get: () => person.lastName,
this.loadData(); set: (value) => {
person.lastName = value;
},
});
const gender = computed({
get: () => (person.gender ? person.gender.id : null),
set: (value) => {
person.gender = { id: value, type: "chill_main_gender" };
},
});
const civility = computed({
get: () => (person.civility ? person.civility.id : null),
set: (value) => {
person.civility = { id: value, type: "chill_main_civility" };
},
});
const birthDate = computed({
get: () => (person.birthdate ? person.birthdate.datetime.split("T")[0] : ""),
set: (value) => {
if (person.birthdate) {
person.birthdate.datetime = value + "T00:00:00+0100";
} else { } else {
// console.log('show centers', this.showCenters); person.birthdate = { datetime: value + "T00:00:00+0100" };
getCentersForPersonCreation().then((params) => {
this.config.centers = params.centers.filter((c) => c.isActive);
this.showCenters = params.showCenters;
// console.log('centers', this.config.centers)
// console.log('show centers inside', this.showCenters);
if (this.showCenters && this.config.centers.length === 1) {
this.person.center = this.config.centers[0];
}
});
} }
}, },
methods: { });
localizeString, const phonenumber = computed({
checkErrors() { get: () => person.phonenumber,
this.errors = []; set: (value) => {
if (this.person.lastName === "") { person.phonenumber = value;
this.errors.push("Le nom ne doit pas être vide.");
}
if (this.person.firstName === "") {
this.errors.push("Le prénom ne doit pas être vide.");
}
if (!this.person.gender) {
this.errors.push("Le genre doit être renseigné");
}
if (this.showCenters && this.person.center === null) {
this.errors.push("Le centre doit être renseigné");
}
},
loadData() {
getPerson(this.id).then(
(person) =>
new Promise((resolve) => {
this.person = person;
//console.log('get person', this.person);
resolve();
}),
);
},
onAltNameInput(event) {
const key = event.target.id;
const label = event.target.value;
let updateAltNames = this.person.altNames.filter((a) => a.key !== key);
updateAltNames.push({ key: key, label: label });
this.person.altNames = updateAltNames;
},
addQueryItem(field, queryItem) {
switch (field) {
case "lastName":
this.person.lastName = this.person.lastName
? (this.person.lastName += ` ${queryItem}`)
: queryItem;
break;
case "firstName":
this.person.firstName = this.person.firstName
? (this.person.firstName += ` ${queryItem}`)
: queryItem;
break;
}
},
submitNewAddress(payload) {
this.person.addressId = payload.addressId;
},
}, },
}; });
</script> const mobilenumber = computed({
get: () => person.mobilenumber,
set: (value) => {
person.mobilenumber = value;
},
});
const email = computed({
get: () => person.email,
set: (value) => {
person.email = value;
},
});
const showAddressForm = computed({
get: () => showAddressFormValue.value,
set: (value) => {
showAddressFormValue.value = value;
},
});
const center = computed({
get: () => {
const c = config.centers.find(
(c) => person.center !== null && person.center.id === c.id,
);
return typeof c === "undefined" ? null : c;
},
set: (value) => {
person.center = { id: value.id, type: value.type };
},
});
<style lang="scss" scoped> const genderClass = computed(() => {
div.flex-table { switch (person.gender && person.gender.id) {
div.item-bloc { case "woman":
div.item-row { return "fa-venus";
div.item-col:last-child { case "man":
justify-content: flex-start; return "fa-mars";
} case "both":
return "fa-neuter";
case "unknown":
return "fa-genderless";
default:
return "fa-genderless";
}
});
const genderTranslation = computed(() => {
switch (person.gender && person.gender.genderTranslation) {
case "woman":
return PERSON_MESSAGES_PERSON_GENDER_WOMAN;
case "man":
return PERSON_MESSAGES_PERSON_GENDER_MAN;
case "neutral":
return PERSON_MESSAGES_PERSON_GENDER_NEUTRAL;
case "unknown":
return PERSON_MESSAGES_PERSON_GENDER_UNKNOWN;
default:
return PERSON_MESSAGES_PERSON_GENDER_UNKNOWN;
}
});
const feminized = computed(() =>
person.gender && person.gender.id === "woman" ? "e" : "",
);
const personAltNamesLabels = computed(() =>
person.altNames.map((a) => (a ? a.label : "")),
);
const queryItems = computed(() =>
props.query ? props.query.split(" ") : null,
);
function checkErrors() {
errors.value = [];
if (person.lastName === "") {
errors.value.push("Le nom ne doit pas être vide.");
}
if (person.firstName === "") {
errors.value.push("Le prénom ne doit pas être vide.");
}
if (!person.gender) {
errors.value.push("Le genre doit être renseigné");
}
if (showCenters.value && person.center === null) {
errors.value.push("Le centre doit être renseigné");
}
}
function loadData() {
getPerson(props.id).then((p) => {
Object.assign(person, p);
});
}
function onAltNameInput(event) {
const key = event.target.id;
const label = event.target.value;
let updateAltNames = person.altNames.filter((a) => a.key !== key);
updateAltNames.push({ key: key, label: label });
person.altNames = updateAltNames;
}
function addQueryItem(field, queryItem) {
switch (field) {
case "lastName":
person.lastName = person.lastName
? (person.lastName += ` ${queryItem}`)
: queryItem;
break;
case "firstName":
person.firstName = person.firstName
? (person.firstName += ` ${queryItem}`)
: queryItem;
break;
}
}
function submitNewAddress(payload) {
person.addressId = payload.addressId;
}
onMounted(() => {
getPersonAltNames().then((altNames) => {
config.altNames = altNames;
});
getCivilities().then((civilities) => {
if ("results" in civilities) {
config.civilities = civilities.results;
} }
});
getGenders().then((genders) => {
if ("results" in genders) {
config.genders = genders.results;
}
});
if (props.action !== "create") {
loadData();
} else {
getCentersForPersonCreation().then((params) => {
config.centers = params.centers.filter((c) => c.isActive);
showCenters.value = params.showCenters;
if (showCenters.value && config.centers.length === 1) {
person.center = config.centers[0];
}
});
} }
} });
dl {
dd { defineExpose(genderClass, genderTranslation, feminized, birthDate);
margin-left: 1em; </script>
}
}
div.form-check {
label {
margin-left: 0.5em !important;
}
}
</style>

View File

@ -198,3 +198,59 @@ accompanying_course_evaluation_document:
accompanying_period_work: accompanying_period_work:
title: Action d'accompagnement (n°{id}) - {action_title} title: Action d'accompagnement (n°{id}) - {action_title}
add_persons:
title: "Ajouter des usagers"
suggested_counter: >-
{count, plural,
=0 {Pas de résultats}
=1 {1 résultat}
other {# résultats}
}
selected_counter: >-
{count, plural,
=1 {1 sélectionné}
other {# sélectionnés}
}
search_some_persons: "Rechercher des personnes.."
item:
type_person: "Usager"
type_user: "TMS"
type_thirdparty: "Tiers professionnel"
type_household: "Ménage"
person:
firstname: "Prénom"
lastname: "Nom"
born:
man: "Né le"
woman: "Née le"
neutral: "Né·e le"
center_id: "Identifiant du centre"
center_type: "Type de centre"
center_name: "Territoire"
phonenumber: "Téléphone"
mobilenumber: "Mobile"
altnames: "Autres noms"
email: "Courriel"
gender:
title: "Genre"
placeholder: "Choisissez le genre de l'usager"
woman: "Féminin"
man: "Masculin"
neutral: "Neutre, non binaire"
unknown: "Non renseigné"
undefined: "Non renseigné"
civility:
title: "Civilité"
placeholder: "Choisissez la civilité"
address:
create_address: "Ajouter une adresse"
show_address_form: "Ajouter une adresse pour un usager non suivi et seul dans un ménage"
warning: "Un nouveau ménage va être créé. L'usager sera membre de ce ménage."
center:
placeholder: "Choisissez un centre"
title: "Centre"
error_only_one_person: "Une seule personne peut être sélectionnée !"

View File

@ -1505,3 +1505,48 @@ my_parcours_filters:
parcours_intervening: Intervenant parcours_intervening: Intervenant
is_open: Parcours ouverts is_open: Parcours ouverts
is_closed: Parcours clôturés is_closed: Parcours clôturés
person_messages:
add_persons:
title: "Ajouter des usagers"
suggested_counter: "Pas de résultats | 1 résultat | {count} résultats"
selected_counter: " 1 sélectionné | {count} sélectionnés"
search_some_persons: "Rechercher des personnes.."
item:
type_person: "Usager"
type_user: "TMS"
type_thirdparty: "Tiers professionnel"
type_household: "Ménage"
person:
firstname: "Prénom"
lastname: "Nom"
born:
man: "Né le"
woman: "Née le"
neutral: "Né·e le"
center_id: "Identifiant du centre"
center_type: "Type de centre"
center_name: "Territoire"
phonenumber: "Téléphone"
mobilenumber: "Mobile"
altnames: "Autres noms"
email: "Courriel"
gender:
title: "Genre"
placeholder: "Choisissez le genre de l'usager"
woman: "Féminin"
man: "Masculin"
neutral: "Neutre, non binaire"
unknown: "Non renseigné"
undefined: "Non renseigné"
civility:
title: "Civilité"
placeholder: "Choisissez la civilité"
address:
create_address: "Ajouter une adresse"
show_address_form: "Ajouter une adresse pour un usager non suivi et seul dans un ménage"
warning: "Un nouveau ménage va être créé. L'usager sera membre de ce ménage."
center:
placeholder: "Choisissez un centre"
title: "Centre"
error_only_one_person: "Une seule personne peut être sélectionnée !"

View File

@ -27,18 +27,18 @@
<div v-if="parent"> <div v-if="parent">
<div class="parent-info"> <div class="parent-info">
<i class="fa fa-li fa-hand-o-right" /> <i class="fa fa-li fa-hand-o-right" />
<b class="me-2">{{ $t("child_of") }}</b> <b class="me-2">{{ trans(THIRDPARTY_MESSAGES_CHILD_OF) }}</b>
<span class="chill-entity badge-thirdparty">{{ <span class="chill-entity badge-thirdparty">{{
parent.text parent.text
}}</span> }}</span>
</div> </div>
</div> </div>
<div class="form-floating mb-3" v-else-if="kind !== 'child'"> <div class="form-floating mb-3" v-else-if="kind.value !== 'child'">
<div class="form-check"> <div class="form-check">
<input <input
class="form-check-input mt-0" class="form-check-input mt-0"
type="radio" type="radio"
v-model="kind" v-model="kind.value"
value="company" value="company"
id="tpartyKindInstitution" id="tpartyKindInstitution"
/> />
@ -53,7 +53,7 @@
<input <input
class="form-check-input mt-0" class="form-check-input mt-0"
type="radio" type="radio"
v-model="kind" v-model="kind.value"
value="contact" value="contact"
id="tpartyKindContact" id="tpartyKindContact"
/> />
@ -95,7 +95,7 @@
v-model="thirdparty.civility" v-model="thirdparty.civility"
> >
<option selected disabled :value="null"> <option selected disabled :value="null">
{{ $t("thirdparty.civility") }} {{ trans(THIRDPARTY_MESSAGES_THIRDPARTY_CIVILITY) }}
</option> </option>
<option <option
v-for="civility in civilities" v-for="civility in civilities"
@ -110,8 +110,12 @@
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
v-model="thirdparty.profession" v-model="thirdparty.profession"
:placeholder="$t('thirdparty.profession')" :placeholder="
:aria-label="$t('thirdparty.profession')" trans(THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION)
"
:aria-label="
trans(THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION)
"
aria-describedby="profession" aria-describedby="profession"
/> />
</div> </div>
@ -123,10 +127,12 @@
class="form-control form-control-lg" class="form-control form-control-lg"
id="firstname" id="firstname"
v-model="thirdparty.firstname" v-model="thirdparty.firstname"
:placeholder="$t('thirdparty.firstname')" :placeholder="
trans(THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME)
"
/> />
<label for="firstname">{{ <label for="firstname">{{
$t("thirdparty.firstname") trans(THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME)
}}</label> }}</label>
</div> </div>
<div v-if="queryItems"> <div v-if="queryItems">
@ -147,10 +153,12 @@
class="form-control form-control-lg" class="form-control form-control-lg"
id="name" id="name"
v-model="thirdparty.name" v-model="thirdparty.name"
:placeholder="$t('thirdparty.lastname')" :placeholder="
trans(THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME)
"
/> />
<label for="name">{{ <label for="name">{{
$t("thirdparty.lastname") trans(THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME)
}}</label> }}</label>
</div> </div>
<div v-if="queryItems"> <div v-if="queryItems">
@ -174,9 +182,11 @@
class="form-control form-control-lg" class="form-control form-control-lg"
id="name" id="name"
v-model="thirdparty.name" v-model="thirdparty.name"
:placeholder="$t('thirdparty.name')" :placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_NAME)"
/> />
<label for="name">{{ $t("thirdparty.name") }}</label> <label for="name">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_NAME)
}}</label>
</div> </div>
<div v-if="query"> <div v-if="query">
<ul class="list-suggest add-items inline"> <ul class="list-suggest add-items inline">
@ -188,7 +198,7 @@
</div> </div>
<template v-if="thirdparty.kind !== 'child'"> <template v-if="thirdparty.kind !== 'child'">
<add-address <AddAddress
key="thirdparty" key="thirdparty"
:context="context" :context="context"
:options="addAddress.options" :options="addAddress.options"
@ -204,8 +214,8 @@
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
v-model="thirdparty.email" v-model="thirdparty.email"
:placeholder="$t('thirdparty.email')" :placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL)"
:aria-label="$t('thirdparty.email')" :aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL)"
aria-describedby="email" aria-describedby="email"
/> />
</div> </div>
@ -217,8 +227,8 @@
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
v-model="thirdparty.telephone" v-model="thirdparty.telephone"
:placeholder="$t('thirdparty.phonenumber')" :placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER)"
:aria-label="$t('thirdparty.phonenumber')" :aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER)"
aria-describedby="phonenumber" aria-describedby="phonenumber"
/> />
</div> </div>
@ -230,8 +240,10 @@
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
v-model="thirdparty.telephone2" v-model="thirdparty.telephone2"
:placeholder="$t('thirdparty.phonenumber2')" :placeholder="
:aria-label="$t('thirdparty.phonenumber2')" trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2)
"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2)"
aria-describedby="phonenumber2" aria-describedby="phonenumber2"
/> />
</div> </div>
@ -243,7 +255,7 @@
/></span> /></span>
<textarea <textarea
class="form-control form-control-lg" class="form-control form-control-lg"
:placeholder="$t('thirdparty.comment')" :placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_COMMENT)"
v-model="thirdparty.comment" v-model="thirdparty.comment"
/> />
</div> </div>
@ -251,173 +263,178 @@
</div> </div>
</template> </template>
<script> <script setup>
import { ref, reactive, computed, onMounted, getCurrentInstance } from "vue";
import ThirdPartyRenderBox from "../Entity/ThirdPartyRenderBox.vue"; import ThirdPartyRenderBox from "../Entity/ThirdPartyRenderBox.vue";
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress"; import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress";
import { getThirdparty } from "../../_api/OnTheFly"; import { getThirdparty } from "../../_api/OnTheFly";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue"; import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; import { localizeString as _localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import {
trans,
THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME,
THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME,
THIRDPARTY_MESSAGES_THIRDPARTY_NAME,
THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL,
THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER,
THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2,
THIRDPARTY_MESSAGES_THIRDPARTY_COMMENT,
THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION,
THIRDPARTY_MESSAGES_THIRDPARTY_CIVILITY,
THIRDPARTY_MESSAGES_CHILD_OF,
} from "translator";
// Props
const props = defineProps(["id", "type", "action", "query", "parent"]);
export default { // Instance for $t and $toast
name: "OnTheFlyThirdParty", const { proxy } = getCurrentInstance();
props: ["id", "type", "action", "query", "parent"],
components: { // State
ThirdPartyRenderBox, const thirdparty = reactive({
AddAddress, type: "thirdparty",
BadgeEntity, address: null,
}, kind: "company",
data() { firstname: "",
return { name: "",
//context: {}, <-- telephone: "",
thirdparty: { telephone2: "",
type: "thirdparty", civility: null,
address: null, profession: "",
kind: "company", comment: "",
firstname: "", parent: props.parent ? props.parent : undefined,
name: "", });
telephone: "", const civilities = ref([]);
telephone2: "", const addAddress = reactive({
civility: null, options: {
profession: "", openPanesInModal: true,
}, onlyButton: false,
civilities: [], button: {
addAddress: { size: "btn-sm",
options: {
openPanesInModal: true,
onlyButton: false,
button: {
size: "btn-sm",
},
title: {
create: "add_an_address_title",
edit: "edit_address",
},
},
},
};
},
computed: {
kind: {
get() {
// note: there are also default to 'institution' set in the "mounted" method
if (this.$data.thirdparty.kind !== undefined) {
return this.$data.thirdparty.kind;
} else {
return "company";
}
},
set(v) {
this.$data.thirdparty.kind = v;
},
}, },
context() { title: {
let context = { create: "add_an_address_title",
target: { edit: "edit_address",
name: this.type,
id: this.id,
},
edit: false,
addressId: null,
defaults: window.addaddress,
};
if (
!(
this.thirdparty.address === undefined ||
this.thirdparty.address === null
) &&
this.thirdparty.address.address_id !== null
) {
// to complete
context.addressId = this.thirdparty.address.address_id;
context.edit = true;
}
//this.context = context; <--
return context;
},
queryItems() {
return this.query ? this.query.split(" ") : null;
}, },
}, },
methods: { });
localizeString, const addAddressRef = ref(null);
loadData() {
return getThirdparty(this.id).then( // Kind as computed ref
(thirdparty) => const kind = computed({
new Promise((resolve) => { get() {
this.thirdparty = thirdparty; return thirdparty.kind !== undefined ? thirdparty.kind : "company";
this.thirdparty.kind = thirdparty.kind; },
if (this.action !== "show") { set(v) {
if (thirdparty.address !== null) { thirdparty.kind = v;
// bof! we force getInitialAddress because addressId not available when mounted },
this.$refs.addAddress.getInitialAddress( });
thirdparty.address.address_id,
); // Context as computed
} const context = computed(() => {
} let ctx = {
resolve(); target: {
}), name: props.type,
); id: props.id,
}, },
loadCivilities() { edit: false,
const url = `/api/1.0/main/civility.json`; addressId: null,
return makeFetch("GET", url) defaults: window.addaddress,
.then((response) => { };
this.$data.civilities = response.results; if (
return Promise.resolve(); !(thirdparty.address === undefined || thirdparty.address === null) &&
}) thirdparty.address.address_id !== null
.catch((error) => { ) {
console.log(error); ctx.addressId = thirdparty.address.address_id;
this.$toast.open({ message: error.body }); ctx.edit = true;
}); }
}, return ctx;
submitAddress(payload) { });
console.log("submitAddress", payload);
if (typeof payload.addressId !== "undefined") { // Query items
// <-- const queryItems = computed(() =>
this.context.edit = true; props.query ? props.query.split(" ") : null,
this.context.addressId = payload.addressId; // bof! use legacy and not legacy in payload );
this.thirdparty.address = payload.address; // <--
console.log("switch address to edit mode", this.context); // Methods
} function localizeString(str) {
}, return _localizeString(str);
addQueryItem(field, queryItem) { }
switch (field) {
case "name": function loadData() {
if (this.thirdparty.name) { return getThirdparty(props.id).then(
this.thirdparty.name += ` ${queryItem}`; (tp) =>
} else { new Promise((resolve) => {
this.thirdparty.name = queryItem; Object.assign(thirdparty, tp);
thirdparty.kind = tp.kind;
if (props.action !== "show") {
if (tp.address !== null && addAddressRef.value) {
addAddressRef.value.getInitialAddress(
tp.address.address_id,
);
} }
break; }
case "firstName": resolve();
this.thirdparty.firstname = queryItem; }),
break; );
}
function loadCivilities() {
const url = `/api/1.0/main/civility.json`;
return makeFetch("GET", url)
.then((response) => {
civilities.value = response.results;
return Promise.resolve();
})
.catch((error) => {
console.log(error);
proxy.$toast.open({ message: error.body });
});
}
function submitAddress(payload) {
if (typeof payload.addressId !== "undefined") {
context.value.edit = true;
context.value.addressId = payload.addressId;
thirdparty.address = payload.address;
}
}
function addQueryItem(field, queryItem) {
switch (field) {
case "name":
if (thirdparty.name) {
thirdparty.name += ` ${queryItem}`;
} else {
thirdparty.name = queryItem;
} }
}, break;
addQuery(query) { case "firstName":
this.thirdparty.name = query; thirdparty.firstname = queryItem;
}, break;
}, }
mounted() { }
let dependencies = [];
dependencies.push(this.loadCivilities()); function addQuery(query) {
if (this.action !== "create") { thirdparty.name = query;
if (this.id) { }
dependencies.push(this.loadData());
// here we can do something when all promises are resolve, with // Lifecycle
// Promise.all(dependencies).then(() => { /* do something */ }); onMounted(() => {
} let dependencies = [];
if (this.action === "addContact") { dependencies.push(loadCivilities());
this.$data.thirdparty.kind = "child"; if (props.action !== "create") {
// this.$data.thirdparty.parent = this.parent.id if (props.id) {
this.$data.thirdparty.address = null; dependencies.push(loadData());
}
} else {
this.thirdparty.kind = "company";
} }
}, if (props.action === "addContact") {
}; thirdparty.kind = "child";
thirdparty.address = null;
}
} else {
thirdparty.kind = "company";
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -155,3 +155,16 @@ Telephone2: Autre téléphone
Contact email: Courrier électronique du contact Contact email: Courrier électronique du contact
Contact address: Adresse du contact Contact address: Adresse du contact
Contact profession: Profession du contact Contact profession: Profession du contact
thirdpartyMessages:
thirdparty:
firstname: "Prénom"
lastname: "Nom"
name: "Dénomination"
email: "Courriel"
phonenumber: "Téléphone"
phonenumber2: "Autre numéro de téléphone"
comment: "Commentaire"
profession: "Qualité"
civility: "Civilité"
child_of: "Contact de: "
children: "Personnes de contact: "

View File

@ -107,3 +107,15 @@ export interface Ticket {
createdAt: DateTime | null; createdAt: DateTime | null;
updatedBy: User | null; updatedBy: User | null;
} }
export interface addNewPersons {
selected: Selected[];
modal: Modal;
}
export interface Modal {
showModal: boolean;
modalDialogClass: string;
}
export interface Selected {
result: User;
}

View File

@ -6,13 +6,13 @@
</div> </div>
<action-toolbar-component /> <action-toolbar-component />
</template> </template>
<script setup lang="ts">
<script lang="ts"> import { useToast } from "vue-toast-notification";
import { computed, defineComponent, inject, onMounted, ref } from "vue"; import { computed, onMounted } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
// Types // Types
import { Motive, Ticket } from "../../types"; import { Ticket } from "../../types";
// Components // Components
import TicketSelectorComponent from "./components/TicketSelectorComponent.vue"; import TicketSelectorComponent from "./components/TicketSelectorComponent.vue";
@ -20,43 +20,23 @@ import TicketHistoryListComponent from "./components/TicketHistoryListComponent.
import ActionToolbarComponent from "./components/ActionToolbarComponent.vue"; import ActionToolbarComponent from "./components/ActionToolbarComponent.vue";
import BannerComponent from "./components/BannerComponent.vue"; import BannerComponent from "./components/BannerComponent.vue";
export default defineComponent({ const store = useStore();
name: "App", const toast = useToast();
components: {
TicketSelectorComponent,
TicketHistoryListComponent,
ActionToolbarComponent,
BannerComponent,
},
setup() {
const store = useStore();
const toast = inject("toast") as any;
store.commit("setTicket", JSON.parse(window.initialTicket) as Ticket); store.commit("setTicket", JSON.parse(window.initialTicket) as Ticket);
const motives = computed(() => store.getters.getMotives as Motive[]); const ticket = computed(() => store.getters.getTicket as Ticket);
const ticket = computed(() => store.getters.getTicket as Ticket);
const ticketHistory = computed( const ticketHistory = computed(() => store.getters.getDistinctAddressesHistory);
() => store.getters.getDistinctAddressesHistory,
);
onMounted(async () => { onMounted(async () => {
try { try {
await store.dispatch("fetchMotives"); await store.dispatch("fetchMotives");
await store.dispatch("fetchUserGroups"); await store.dispatch("fetchUserGroups");
await store.dispatch("fetchUsers"); await store.dispatch("fetchUsers");
} catch (error) { } catch (error) {
toast.error(error); toast.error(error as string);
} }
});
return {
ticketHistory,
motives,
ticket,
};
},
}); });
</script> </script>

View File

@ -4,7 +4,7 @@
<div class="tab-content p-2"> <div class="tab-content p-2">
<div> <div>
<label class="col-form-label"> <label class="col-form-label">
{{ $t(`${activeTab}.title`) }} {{ activeTabTitle }}
</label> </label>
</div> </div>
@ -36,12 +36,20 @@
@click="activeTab = ''" @click="activeTab = ''"
class="btn btn-cancel" class="btn btn-cancel"
> >
{{ $t("ticket.cancel") }} {{
trans(
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL,
)
}}
</button> </button>
</li> </li>
<li> <li>
<button class="btn btn-save" type="submit"> <button class="btn btn-save" type="submit">
{{ $t("ticket.save") }} {{
trans(
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE,
)
}}
</button> </button>
</li> </li>
</ul> </ul>
@ -78,7 +86,7 @@
" "
> >
<i :class="actionIcons['set_motive']"></i> <i :class="actionIcons['set_motive']"></i>
{{ $t("set_motive.title") }} {{ trans(CHILL_TICKET_TICKET_SET_MOTIVE_TITLE) }}
</button> </button>
</li> </li>
<li class="nav-item p-2"> <li class="nav-item p-2">
@ -96,7 +104,7 @@
" "
> >
<i :class="actionIcons['add_comment']"></i> <i :class="actionIcons['add_comment']"></i>
{{ $t("add_comment.title") }} {{ trans(CHILL_TICKET_TICKET_ADD_COMMENT_TITLE) }}
</button> </button>
</li> </li>
<li class="nav-item p-2"> <li class="nav-item p-2">
@ -114,7 +122,7 @@
" "
> >
<i :class="actionIcons['addressees_state']"></i> <i :class="actionIcons['addressees_state']"></i>
{{ $t("add_addressee.title") }} {{ trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_TITLE) }}
</button> </button>
</li> </li>
<li class="nav-item p-2"> <li class="nav-item p-2">
@ -132,7 +140,7 @@
" "
> >
<i :class="actionIcons['set_persons']"></i> <i :class="actionIcons['set_persons']"></i>
Patients concernés {{ trans(CHILL_TICKET_TICKET_SET_PERSONS_TITLE) }}
</button> </button>
</li> </li>
@ -143,7 +151,7 @@
@click="handleClick()" @click="handleClick()"
> >
<i class="fa fa-bolt"></i> <i class="fa fa-bolt"></i>
{{ $t("ticket.close") }} {{ trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE) }}
</button> </button>
</li> </li>
</ul> </ul>
@ -151,10 +159,34 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { computed, defineComponent, inject, ref } from "vue"; import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useStore } from "vuex"; import { useStore } from "vuex";
import { useToast } from "vue-toast-notification";
// Component
import MotiveSelectorComponent from "./MotiveSelectorComponent.vue";
import AddresseeSelectorComponent from "./AddresseeSelectorComponent.vue";
import AddCommentComponent from "./AddCommentComponent.vue";
import PersonsSelectorComponent from "./PersonsSelectorComponent.vue";
// Translations
import {
trans,
CHILL_TICKET_TICKET_ADD_ADDRESSEE_TITLE,
CHILL_TICKET_TICKET_ADD_ADDRESSEE_ERROR,
CHILL_TICKET_TICKET_ADD_ADDRESSEE_SUCCESS,
CHILL_TICKET_TICKET_ADD_COMMENT_TITLE,
CHILL_TICKET_TICKET_ADD_COMMENT_ERROR,
CHILL_TICKET_TICKET_ADD_COMMENT_SUCCESS,
CHILL_TICKET_TICKET_SET_MOTIVE_TITLE,
CHILL_TICKET_TICKET_SET_MOTIVE_ERROR,
CHILL_TICKET_TICKET_SET_MOTIVE_SUCCESS,
CHILL_TICKET_TICKET_SET_PERSONS_TITLE,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE,
} from "translator";
// Types // Types
import { import {
@ -164,140 +196,118 @@ import {
} from "../../../../../../../ChillMainBundle/Resources/public/types"; } from "../../../../../../../ChillMainBundle/Resources/public/types";
import { Comment, Motive, Ticket } from "../../../types"; import { Comment, Motive, Ticket } from "../../../types";
// Component const store = useStore();
import MotiveSelectorComponent from "./MotiveSelectorComponent.vue"; const toast = useToast();
import AddresseeSelectorComponent from "./AddresseeSelectorComponent.vue";
import AddCommentComponent from "./AddCommentComponent.vue";
import PersonsSelectorComponent from "./PersonsSelectorComponent.vue";
export default defineComponent({ const activeTab = ref(
name: "ActionToolbarComponent", "" as "" | "add_comment" | "set_motive" | "add_addressee" | "set_persons",
components: { );
PersonsSelectorComponent,
AddCommentComponent,
MotiveSelectorComponent,
AddresseeSelectorComponent,
},
setup() {
const store = useStore();
const { t } = useI18n();
const toast = inject("toast") as any;
const activeTab = ref(
"" as
| ""
| "add_comment"
| "set_motive"
| "add_addressee"
| "set_persons",
);
const ticket = computed(() => store.getters.getTicket as Ticket); const activeTabTitle = computed((): string => {
const motives = computed(() => store.getters.getMotives as Motive[]); switch (activeTab.value) {
const userGroups = computed( case "add_comment":
() => store.getters.getUserGroups as UserGroup[], return trans(CHILL_TICKET_TICKET_ADD_COMMENT_TITLE);
); case "set_motive":
const users = computed(() => store.getters.getUsers as User[]); return trans(CHILL_TICKET_TICKET_SET_MOTIVE_TITLE);
case "add_addressee":
const hasReturnPath = computed((): boolean => { return trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_TITLE);
const params = new URL(document.location.toString()).searchParams; case "set_persons":
return params.has("returnPath"); return trans(CHILL_TICKET_TICKET_SET_PERSONS_TITLE);
}); default:
return "";
const returnPath = computed((): string => { }
const params = new URL(document.location.toString()).searchParams;
const returnPath = params.get("returnPath");
if (null === returnPath) {
throw new Error(
"there isn't any returnPath, please check the existence before",
);
}
return returnPath;
});
const motive = ref(
ticket.value.currentMotive
? ticket.value.currentMotive
: ({} as Motive),
);
const content = ref("" as Comment["content"]);
const addressees = ref(
ticket.value.currentAddressees as UserGroupOrUser[],
);
async function submitAction() {
try {
switch (activeTab.value) {
case "add_comment":
if (!content.value) {
toast.error(t("add_comment.error"));
} else {
await store.dispatch("createComment", {
ticketId: ticket.value.id,
content: content.value,
});
content.value = "";
activeTab.value = "";
toast.success(t("add_comment.success"));
}
break;
case "set_motive":
if (!motive.value.id) {
toast.error(t("set_motive.error"));
} else {
await store.dispatch("createMotive", {
ticketId: ticket.value.id,
motive: motive.value,
});
activeTab.value = "";
toast.success(t("set_motive.success"));
}
break;
case "add_addressee":
if (!addressees.value.length) {
toast.error(t("add_addressee.error"));
} else {
await store.dispatch("setAdressees", {
ticketId: ticket.value.id,
addressees: addressees.value,
});
activeTab.value = "";
toast.success(t("add_addressee.success"));
}
break;
}
} catch (error) {
toast.error(error);
}
}
function handleClick() {
alert("Sera disponible plus tard");
}
const closeAllActions = function () {
activeTab.value = "";
};
return {
actionIcons: ref(store.getters.getActionIcons),
activeTab,
ticket,
motives,
motive,
userGroups,
addressees,
users,
content,
submitAction,
handleClick,
hasReturnPath,
returnPath,
closeAllActions,
};
},
}); });
const ticket = computed(() => store.getters.getTicket as Ticket);
const motives = computed(() => store.getters.getMotives as Motive[]);
const userGroups = computed(() => store.getters.getUserGroups as UserGroup[]);
const users = computed(() => store.getters.getUsers as User[]);
const hasReturnPath = computed((): boolean => {
const params = new URL(document.location.toString()).searchParams;
return params.has("returnPath");
});
const returnPath = computed((): string => {
const params = new URL(document.location.toString()).searchParams;
const returnPath = params.get("returnPath");
if (null === returnPath) {
throw new Error(
"there isn't any returnPath, please check the existence before",
);
}
return returnPath;
});
const motive = ref(
ticket.value.currentMotive ? ticket.value.currentMotive : ({} as Motive),
);
const content = ref("" as Comment["content"]);
const addressees = ref(ticket.value.currentAddressees as UserGroupOrUser[]);
async function submitAction() {
try {
switch (activeTab.value) {
case "add_comment":
if (!content.value) {
toast.error(trans(CHILL_TICKET_TICKET_ADD_COMMENT_ERROR));
} else {
await store.dispatch("createComment", {
ticketId: ticket.value.id,
content: content.value,
});
content.value = "";
activeTab.value = "";
toast.success(
trans(CHILL_TICKET_TICKET_ADD_COMMENT_SUCCESS),
);
}
break;
case "set_motive":
if (!motive.value.id) {
toast.error(trans(CHILL_TICKET_TICKET_SET_MOTIVE_ERROR));
} else {
await store.dispatch("createMotive", {
ticketId: ticket.value.id,
motive: motive.value,
});
activeTab.value = "";
toast.success(
trans(CHILL_TICKET_TICKET_SET_MOTIVE_SUCCESS),
);
}
break;
case "add_addressee":
if (!addressees.value.length) {
toast.error(trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_ERROR));
} else {
await store.dispatch("setAdressees", {
ticketId: ticket.value.id,
addressees: addressees.value,
});
activeTab.value = "";
toast.success(
trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_SUCCESS),
);
}
break;
}
} catch (error) {
toast.error(error as string);
}
}
function handleClick() {
alert("Sera disponible plus tard");
}
function closeAllActions() {
activeTab.value = "";
}
const actionIcons = ref(store.getters.getActionIcons);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -6,35 +6,21 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, ref, watch } from "vue"; import { ref, watch } from "vue";
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue"; import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
export default defineComponent({ const props = defineProps<{
name: "AddCommentComponent", modelValue?: string;
props: { }>();
modelValue: {
type: String,
required: false,
},
},
components: {
CommentEditor,
},
emits: ["update:modelValue"],
setup(props, ctx) { const emit =
const content = ref(props.modelValue); defineEmits<(e: "update:modelValue", value: string | undefined) => void>();
watch(content, (content) => { const content = ref(props.modelValue);
ctx.emit("update:modelValue", content);
});
return { watch(content, (value) => {
content, emit("update:modelValue", value);
};
},
}); });
</script> </script>

View File

@ -30,8 +30,11 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { PropType, computed, defineComponent, ref } from "vue"; import { computed } from "vue";
// Components
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
// Types // Types
import { import {
@ -39,43 +42,32 @@ import {
UserGroup, UserGroup,
UserGroupOrUser, UserGroupOrUser,
} from "../../../../../../../ChillMainBundle/Resources/public/types"; } from "../../../../../../../ChillMainBundle/Resources/public/types";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
export default defineComponent({ const props = defineProps<{ addressees: UserGroupOrUser[] }>();
name: "AddresseeComponent",
components: { UserRenderBoxBadge }, const userGroups = computed(
props: { () =>
addressees: { props.addressees.filter(
type: Array as PropType<UserGroupOrUser[]>, (addressee: UserGroupOrUser) =>
required: true, addressee.type == "user_group" && addressee.excludeKey == "",
}, ) as UserGroup[],
}, );
setup(props, ctx) {
const userGroups = computed( const userGroupLevels = computed(
() => () =>
props.addressees.filter( props.addressees.filter(
(addressee) => (addressee: UserGroupOrUser) =>
addressee.type == "user_group" && addressee.type == "user_group" &&
addressee.excludeKey == "", addressee.excludeKey == "level",
) as UserGroup[], ) as UserGroup[],
); );
const userGroupLevels = computed(
() => const users = computed(
props.addressees.filter( () =>
(addressee) => props.addressees.filter(
addressee.type == "user_group" && (addressee: UserGroupOrUser) => addressee.type == "user",
addressee.excludeKey == "level", ) as User[],
) as UserGroup[], );
);
const users = computed(
() =>
props.addressees.filter(
(addressee) => addressee.type == "user",
) as User[],
);
return { userGroups, users, userGroupLevels };
},
});
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -66,9 +66,14 @@
<add-persons <add-persons
:options="addPersonsOptions" :options="addPersonsOptions"
key="add-person-ticket" key="add-person-ticket"
buttonTitle="add_addressee.user_label" :buttonTitle="
modalTitle="add_addressee.user_label" trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_USER_LABEL)
ref="addPersons" "
:modalTitle="
trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_USER_LABEL)
"
:selected="selectedValues"
:suggested="suggestedValues"
@addNewPersons="addNewEntity" @addNewPersons="addNewEntity"
/> />
<div class="p-2"> <div class="p-2">
@ -84,145 +89,120 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { PropType, computed, defineComponent, ref, watch } from "vue"; import { ref, watch, defineProps, defineEmits } from "vue";
import { useI18n } from "vue-i18n";
// Types
import { User, UserGroup, UserGroupOrUser } from "ChillMainAssets/types";
// Components // Components
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue"; import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
export default defineComponent({ // Types
name: "AddresseeSelectorComponent", import type { User, UserGroup, UserGroupOrUser } from "ChillMainAssets/types";
props: { import { SearchOptions, Suggestion } from "ChillPersonAssets/types";
modelValue: { import type { addNewPersons } from "../../../types";
type: Array as PropType<UserGroupOrUser[]>,
default: [], // Translations
required: false, import {
}, CHILL_TICKET_TICKET_ADD_ADDRESSEE_USER_LABEL,
userGroups: { trans,
type: Array as PropType<UserGroup[]>, } from "translator";
required: true,
}, const props = defineProps<{
users: { modelValue?: UserGroupOrUser[];
type: Array as PropType<User[]>, userGroups: UserGroup[];
required: true, users: User[];
}, }>();
const selectedValues = ref<Suggestion[]>([]);
const suggestedValues = ref<Suggestion[]>([]);
const emit =
defineEmits<(e: "update:modelValue", value: UserGroupOrUser[]) => void>();
const addressees = ref<UserGroupOrUser[]>([...(props.modelValue ?? [])]);
const userGroupsInit = [
...(props.modelValue ?? []).filter(
(addressee: UserGroupOrUser) => addressee.type == "user_group",
),
] as UserGroup[];
const userGroupLevel = ref<UserGroup | Record<string, never>>(
(userGroupsInit.filter(
(userGroup: UserGroup) => userGroup.excludeKey == "level",
)[0] as UserGroup) ?? {},
);
const userGroup = ref<UserGroup[]>(
userGroupsInit.filter(
(userGroup: UserGroup) => userGroup.excludeKey == "",
) as UserGroup[],
);
const users = ref<User[]>([
...(props.modelValue ?? []).filter(
(addressee: UserGroupOrUser) => addressee.type == "user",
),
] as User[]);
const addPersonsOptions = {
uniq: false,
type: ["user"],
priority: null,
button: {
size: "btn-sm",
class: "btn-submit",
}, },
components: { } as SearchOptions;
AddPersons,
},
emits: ["update:modelValue"],
setup(props, ctx) { function getUserGroupBtnColor(userGroup: UserGroup) {
const addressees = ref([...props.modelValue] as UserGroupOrUser[]); return [
const userGroups = [ `color: ${userGroup.foregroundColor};
...props.modelValue.filter( .btn-check:checked + .btn-${userGroup.id} {
(addressee) => addressee.type == "user_group", color: ${userGroup.foregroundColor};
), background-color: ${userGroup.backgroundColor};
] as UserGroup[]; }`,
];
}
const userGroupLevel = ref( function addNewEntity(datas: addNewPersons) {
userGroups.filter( const { selected } = datas;
(userGroup) => userGroup.excludeKey == "level", users.value = selected.map((selected) => selected.result);
)[0] as UserGroup | {}, addressees.value = addressees.value.filter(
); (addressee) => addressee.type === "user_group",
const userGroup = ref( );
userGroups.filter( addressees.value = [...addressees.value, ...users.value];
(userGroup) => userGroup.excludeKey == "", emit("update:modelValue", addressees.value);
) as UserGroup[], selectedValues.value = [];
); suggestedValues.value = [];
const users = ref([ }
...props.modelValue.filter((addressee) => addressee.type == "user"),
] as User[]);
const addPersons = ref();
const { t } = useI18n(); function removeUser(user: User) {
users.value.splice(users.value.indexOf(user), 1);
addressees.value = addressees.value.filter(
(addressee) => addressee.id !== user.id,
);
emit("update:modelValue", addressees.value);
}
function getUserGroupBtnColor(userGroup: UserGroup) { watch(userGroupLevel, (userGroupLevelAdd, userGroupLevelRem) => {
return [ const index = addressees.value.indexOf(userGroupLevelRem as UserGroup);
`color: ${userGroup.foregroundColor}; if (index !== -1) {
.btn-check:checked + .btn-${userGroup.id} { addressees.value.splice(index, 1);
color: ${userGroup.foregroundColor}; }
background-color: ${userGroup.backgroundColor}; addressees.value.push(userGroupLevelAdd as UserGroup);
}`, emit("update:modelValue", addressees.value);
]; });
}
function addNewEntity(datas: any) {
const { selected, modal } = datas;
users.value = selected.map((selected: any) => selected.result);
addressees.value = addressees.value.filter(
(addressee) => addressee.type === "user_group",
);
addressees.value = [...addressees.value, ...users.value];
ctx.emit("update:modelValue", addressees.value);
addPersons.value.resetSearch();
modal.showModal = false;
}
const addPersonsOptions = computed(() => { watch(userGroup, (userGroupAdd) => {
return { const userGroupLevelArr = addressees.value.filter(
uniq: false, (addressee) =>
type: ["user"], addressee.type == "user_group" && addressee.excludeKey == "level",
priority: null, ) as UserGroup[];
button: { const usersArr = addressees.value.filter(
size: "btn-sm", (addressee) => addressee.type == "user",
class: "btn-submit", ) as User[];
}, addressees.value = [...usersArr, ...userGroupLevelArr, ...userGroupAdd];
}; emit("update:modelValue", addressees.value);
});
function removeUser(user: User) {
users.value.splice(users.value.indexOf(user), 1);
addressees.value = addressees.value.filter(
(addressee) => addressee.id !== user.id,
);
ctx.emit("update:modelValue", addressees.value);
}
watch(userGroupLevel, (userGroupLevelAdd, userGroupLevelRem) => {
const index = addressees.value.indexOf(
userGroupLevelRem as UserGroup,
);
if (index !== -1) {
addressees.value.splice(index, 1);
}
addressees.value.push(userGroupLevelAdd as UserGroup);
ctx.emit("update:modelValue", addressees.value);
});
watch(userGroup, (userGroupAdd) => {
const userGroupLevel = addressees.value.filter(
(addressee) =>
addressee.type == "user_group" &&
addressee.excludeKey == "level",
) as UserGroup[];
const users = addressees.value.filter(
(addressee) => addressee.type == "user",
) as User[];
addressees.value = [...users, ...userGroupLevel, ...userGroupAdd];
ctx.emit("update:modelValue", addressees.value);
});
return {
addressees,
userGroupLevel,
userGroup,
users,
addPersons,
addPersonsOptions,
addNewEntity,
removeUser,
getUserGroupBtnColor,
customUserGroupLabel(selectedUserGroup: UserGroup) {
return selectedUserGroup.label
? selectedUserGroup.label.fr
: t("add_addresseeuser_group_label");
},
};
},
}); });
</script> </script>

View File

@ -8,7 +8,7 @@
{{ ticket.currentMotive.label.fr }} {{ ticket.currentMotive.label.fr }}
</h1> </h1>
<p class="chill-no-data-statement" v-else> <p class="chill-no-data-statement" v-else>
{{ $t("banner.no_motive") }} {{ trans(CHILL_TICKET_TICKET_BANNER_NO_MOTIVE) }}
</p> </p>
</div> </div>
@ -18,13 +18,13 @@
class="badge text-bg-chill-green text-white" class="badge text-bg-chill-green text-white"
style="font-size: 1rem" style="font-size: 1rem"
> >
{{ $t("banner.open") }} {{ trans(CHILL_TICKET_TICKET_BANNER_OPEN) }}
</span> </span>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<p class="created-at-timespan" v-if="ticket.createdAt"> <p class="created-at-timespan" v-if="ticket.createdAt">
{{ {{
$t("banner.since", { trans(CHILL_TICKET_TICKET_BANNER_SINCE, {
time: since, time: since,
}) })
}} }}
@ -39,7 +39,7 @@
<div class="row justify-content-between"> <div class="row justify-content-between">
<div class="col-md-6 col-sm-12 ps-md-5 ps-xxl-0"> <div class="col-md-6 col-sm-12 ps-md-5 ps-xxl-0">
<h3 class="text-primary"> <h3 class="text-primary">
{{ $t("banner.concerned_patient") }} {{ trans(CHILL_TICKET_TICKET_BANNER_CONCERNED_USAGER) }}
</h3> </h3>
<on-the-fly <on-the-fly
v-for="person in ticket.currentPersons" v-for="person in ticket.currentPersons"
@ -49,10 +49,13 @@
:buttonText="person.textAge" :buttonText="person.textAge"
:displayBadge="'true' === 'true'" :displayBadge="'true' === 'true'"
action="show" action="show"
CHILL_TICKET_TICKET_BANNER_CONCERNED_USAGER
></on-the-fly> ></on-the-fly>
</div> </div>
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">
<h3 class="text-primary">{{ $t("banner.speaker") }}</h3> <h3 class="text-primary">
{{ trans(CHILL_TICKET_TICKET_BANNER_SPEAKER) }}
</h3>
<addressee-component <addressee-component
:addressees="ticket.currentAddressees" :addressees="ticket.currentAddressees"
/> />
@ -69,81 +72,91 @@
} }
</style> </style>
<script lang="ts"> <script setup lang="ts">
import { PropType, computed, defineComponent, ref } from "vue"; import { ref, computed } from "vue";
import { useI18n } from "vue-i18n";
// Components // Components
import PersonRenderBox from "../../../../../../../ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue";
import AddresseeComponent from "./AddresseeComponent.vue"; import AddresseeComponent from "./AddresseeComponent.vue";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
// Types // Types
import { Ticket } from "../../../types"; import { Ticket } from "../../../types";
import { ISOToDatetime } from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date"; import { ISOToDatetime } from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
export default defineComponent({ // Translations
name: "BannerComponent", import {
props: { trans,
ticket: { CHILL_TICKET_TICKET_BANNER_NO_MOTIVE,
type: Object as PropType<Ticket>, CHILL_TICKET_TICKET_BANNER_OPEN,
required: true, CHILL_TICKET_TICKET_BANNER_SINCE,
}, CHILL_TICKET_TICKET_BANNER_CONCERNED_USAGER,
}, CHILL_TICKET_TICKET_BANNER_SPEAKER,
components: { CHILL_TICKET_TICKET_BANNER_DAYS,
OnTheFly, CHILL_TICKET_TICKET_BANNER_HOURS,
PersonRenderBox, CHILL_TICKET_TICKET_BANNER_MINUTES,
AddresseeComponent, CHILL_TICKET_TICKET_BANNER_SECONDS,
}, CHILL_TICKET_TICKET_BANNER_AND,
setup(props) { } from "translator";
const { t } = useI18n();
const today = ref(new Date());
const createdAt = ref(props.ticket.createdAt);
setInterval(function () { const props = defineProps<{
today.value = new Date(); ticket: Ticket;
}, 5000); }>();
const since = computed(() => { const today = ref(new Date());
if (null === createdAt.value) { const createdAt = ref(props.ticket.createdAt);
return "";
}
const date = ISOToDatetime(createdAt.value.datetime);
if (null === date) { setInterval(() => {
return ""; today.value = new Date();
} }, 5000);
const timeDiff = Math.abs(today.value.getTime() - date.getTime()); const since = computed(() => {
const daysDiff = Math.floor(timeDiff / (1000 * 3600 * 24)); if (createdAt.value == null) {
const hoursDiff = Math.floor( return "";
(timeDiff % (1000 * 3600 * 24)) / (1000 * 3600), }
); const date = ISOToDatetime(createdAt.value.datetime);
const minutesDiff = Math.floor(
(timeDiff % (1000 * 3600)) / (1000 * 60),
);
const secondsDiff = Math.floor((timeDiff % (1000 * 60)) / 1000);
if (daysDiff < 1 && hoursDiff < 1 && minutesDiff < 1) { if (date == null) {
return `${t("banner.seconds", { count: secondsDiff })}`; return "";
} else if (daysDiff < 1 && hoursDiff < 1) { }
return `${t("banner.minutes", { count: minutesDiff })}`;
} else if (daysDiff < 1) { const timeDiff = Math.abs(today.value.getTime() - date.getTime());
return `${t("banner.hours", { count: hoursDiff })} const daysDiff = Math.floor(timeDiff / (1000 * 3600 * 24));
${t("banner.minutes", { count: minutesDiff })}`; const hoursDiff = Math.floor(
} else { (timeDiff % (1000 * 3600 * 24)) / (1000 * 3600),
return `${t("banner.days", { count: daysDiff })}, ${t( );
"banner.hours", const minutesDiff = Math.floor((timeDiff % (1000 * 3600)) / (1000 * 60));
{ const secondsDiff = Math.floor((timeDiff % (1000 * 60)) / 1000);
count: hoursDiff,
}, // On construit la liste des parties à afficher
)} ${t("banner.minutes", { const parts: string[] = [];
count: minutesDiff, if (daysDiff > 0) {
})}`; parts.push(trans(CHILL_TICKET_TICKET_BANNER_DAYS, { count: daysDiff }));
} }
if (hoursDiff > 0 || daysDiff > 0) {
parts.push(
trans(CHILL_TICKET_TICKET_BANNER_HOURS, { count: hoursDiff }),
);
}
if (minutesDiff > 0 || hoursDiff > 0 || daysDiff > 0) {
parts.push(
trans(CHILL_TICKET_TICKET_BANNER_MINUTES, { count: minutesDiff }),
);
}
if (parts.length === 0) {
return trans(CHILL_TICKET_TICKET_BANNER_SECONDS, {
count: secondsDiff,
}); });
}
return { since }; if (parts.length > 1) {
}, const last = parts.pop();
return (
parts.join(", ") +
" " +
trans(CHILL_TICKET_TICKET_BANNER_AND) +
" " +
last
);
}
return parts[0];
}); });
</script> </script>

View File

@ -10,10 +10,10 @@
open-direction="top" open-direction="top"
:multiple="false" :multiple="false"
:searchable="true" :searchable="true"
:placeholder="$t('set_motive.label')" :placeholder="trans(CHILL_TICKET_TICKET_SET_MOTIVE_LABEL)"
:select-label="$t('multiselect.select_label')" :select-label="trans(MULTISELECT_SELECT_LABEL)"
:deselect-label="$t('multiselect.deselect_label')" :deselect-label="trans(MULTISELECT_DESELECT_LABEL)"
:selected-label="$t('multiselect.selected_label')" :selected-label="trans(MULTISELECT_SELECTED_LABEL)"
:options="motives" :options="motives"
v-model="motive" v-model="motive"
class="mb-4" class="mb-4"
@ -22,47 +22,46 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { PropType, defineComponent, ref, watch } from "vue"; import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import VueMultiselect from "vue-multiselect"; import VueMultiselect from "vue-multiselect";
// Types // Types
import { Motive } from "../../../types"; import { Motive } from "../../../types";
export default defineComponent({ // Translations
name: "MotiveSelectorComponent", import {
props: { trans,
modelValue: { CHILL_TICKET_TICKET_SET_MOTIVE_LABEL,
type: Object as PropType<Motive>, MULTISELECT_SELECT_LABEL,
required: false, MULTISELECT_DESELECT_LABEL,
}, MULTISELECT_SELECTED_LABEL,
motives: { } from "translator";
type: Object as PropType<Motive[]>,
required: true,
},
},
components: {
VueMultiselect,
},
emits: ["update:modelValue"],
setup(props, ctx) { const props = defineProps<{
const motive = ref(props.modelValue); modelValue?: Motive;
const { t } = useI18n(); motives: Motive[];
}>();
watch(motive, (motive) => { const emit =
ctx.emit("update:modelValue", motive); defineEmits<(e: "update:modelValue", value: Motive | undefined) => void>();
});
return { const motive = ref(props.modelValue);
motive,
customLabel(motive: Motive) { watch(motive, (val) => {
return motive.label ? motive.label.fr : t("set_motive.label"); emit("update:modelValue", val);
},
};
},
}); });
watch(
() => props.modelValue,
(val) => {
motive.value = val;
},
);
function customLabel(motive: Motive) {
return motive?.label?.fr ?? trans(CHILL_TICKET_TICKET_SET_MOTIVE_LABEL);
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,12 +1,68 @@
<template>
<div>
<div style="display: flex; flex-direction: column; align-items: center">
<add-persons
:options="addPersonsOptions"
key="add-person-selector"
:buttonTitle="trans(CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL)"
:modalTitle="trans(CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL)"
:selected="selectedValues"
:suggested="suggestedValues"
@addNewPersons="addNewEntity"
/>
<div class="p-2">
<ul class="list-suggest inline remove-items">
<li v-for="person in currentPersons" :key="person.id">
<span
:title="`${person.firstName} ${person.lastName}`"
@click="removePerson(person)"
>
{{ `${person.firstName} ${person.lastName}` }}
</span>
</li>
</ul>
</div>
</div>
</div>
<ul class="record_actions">
<li class="cancel">
<button
class="btn btn-cancel"
type="button"
@click="emit('closeRequested')"
>
{{ trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL) }}
</button>
</li>
<li>
<button class="btn btn-save" type="submit" @click.prevent="save">
{{ trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE) }}
</button>
</li>
</ul>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, reactive, ref } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
import { computed, inject, reactive } from "vue";
import { Ticket } from "../../../types";
import { Person } from "../../../../../../../ChillPersonBundle/Resources/public/types";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import { ToastPluginApi } from "vue-toast-notification"; import { ToastPluginApi } from "vue-toast-notification";
// Components
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
// Types
import { SearchOptions, Suggestion, Person } from "ChillPersonAssets/types";
import { Ticket } from "../../../types";
// Translations
import {
trans,
CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE,
CHILL_TICKET_TICKET_SET_PERSONS_SUCCESS,
} from "translator";
const emit = defineEmits<(e: "closeRequested") => void>(); const emit = defineEmits<(e: "closeRequested") => void>();
const store = useStore(); const store = useStore();
@ -19,9 +75,13 @@ const addPersonsOptions = {
type: ["person"], type: ["person"],
priority: null, priority: null,
button: { button: {
size: "btn-sm",
class: "btn-submit", class: "btn-submit",
}, },
}; } as SearchOptions;
const selectedValues = ref<Suggestion[]>([]);
const suggestedValues = ref<Suggestion[]>([]);
const added: Person[] = reactive([]); const added: Person[] = reactive([]);
const removed: Person[] = reactive([]); const removed: Person[] = reactive([]);
@ -50,14 +110,12 @@ const removePerson = (p: Person) => {
removed.push(p); removed.push(p);
}; };
const addNewEntity = (n: { const addNewEntity = (n: { selected: { result: Person }[] }) => {
modal: { showModal: boolean };
selected: { result: Person }[];
}) => {
n.modal.showModal = false;
for (let p of n.selected) { for (let p of n.selected) {
added.push(p.result); added.push(p.result);
} }
selectedValues.value = [];
suggestedValues.value = [];
}; };
const save = async function (): Promise<void> { const save = async function (): Promise<void> {
@ -65,64 +123,14 @@ const save = async function (): Promise<void> {
await store.dispatch("setPersons", { await store.dispatch("setPersons", {
persons: computeCurrentPersons(persons.value, added, removed), persons: computeCurrentPersons(persons.value, added, removed),
}); });
toast.success("Patients concernés sauvegardés"); toast.success(trans(CHILL_TICKET_TICKET_SET_PERSONS_SUCCESS));
} catch (e: any) { } catch (error) {
console.error("error while saving", e); toast.error((error as Error).message);
toast.error((e as Error).message);
return Promise.resolve(); return Promise.resolve();
} }
emit("closeRequested"); emit("closeRequested");
}; };
</script> </script>
<template>
<div>
<ul v-if="currentPersons.length > 0" class="person-list">
<li v-for="person in currentPersons" :key="person.id">
<on-the-fly
:type="person.type"
:id="person.id"
:buttonText="person.textAge"
:displayBadge="'true' === 'true'"
action="show"
></on-the-fly>
<button
type="button"
class="btn btn-delete remove-person"
@click="removePerson(person)"
></button>
</li>
</ul>
<p v-else class="chill-no-data-statement">Aucun patient</p>
</div>
<ul class="record_actions">
<li class="cancel">
<button
class="btn btn-cancel"
type="button"
@click="emit('closeRequested')"
>
{{ $t("ticket.cancel") }}
</button>
</li>
<li>
<add-persons
:options="addPersonsOptions"
key="add-person-ticket"
buttonTitle="set_persons.user_label"
modalTitle="set_persons.user_label"
ref="addPersons"
@addNewPersons="addNewEntity"
/>
</li>
<li>
<button class="btn btn-save" type="submit" @click.prevent="save">
{{ $t("ticket.save") }}
</button>
</li>
</ul>
</template>
<style scoped lang="scss"> <style scoped lang="scss">
ul.person-list { ul.person-list {
list-style-type: none; list-style-type: none;

View File

@ -3,31 +3,16 @@
<addressee-component :addressees="addressees" /> <addressee-component :addressees="addressees" />
</div> </div>
</template> </template>
<script setup lang="ts">
<script lang="ts">
import { PropType, defineComponent } from "vue";
// Types // Types
import { UserGroupOrUser } from "../../../../../../../ChillMainBundle/Resources/public/types"; import { UserGroupOrUser } from "../../../../../../../ChillMainBundle/Resources/public/types";
// Components // Components
import AddresseeComponent from "./AddresseeComponent.vue"; import AddresseeComponent from "./AddresseeComponent.vue";
export default defineComponent({ defineProps<{
name: "TicketHistoryAddresseeComponenvt", addressees: UserGroupOrUser[];
props: { }>();
addressees: {
type: Array as PropType<UserGroupOrUser[]>,
required: true,
},
},
components: {
AddresseeComponent,
},
setup() {
return {};
},
});
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -6,55 +6,41 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { PropType, defineComponent } from "vue";
import { marked } from "marked"; import { marked } from "marked";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
// Types // Types
import { Comment } from "../../../types"; import { Comment } from "../../../types";
export default defineComponent({ defineProps<{ commentHistory: Comment }>();
name: "TicketHistoryCommentComponent",
props: {
commentHistory: {
type: Object as PropType<Comment>,
required: true,
},
},
setup() {
const preprocess = (markdown: string): string => {
return markdown;
};
const postprocess = (html: string): string => { const preprocess = (markdown: string): string => {
DOMPurify.addHook("afterSanitizeAttributes", (node: any) => { return markdown;
if ("target" in node) { };
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener noreferrer");
}
if (
!node.hasAttribute("target") &&
(node.hasAttribute("xlink:href") ||
node.hasAttribute("href"))
) {
node.setAttribute("xlink:show", "new");
}
});
return DOMPurify.sanitize(html); const postprocess = (html: string): string => {
}; DOMPurify.addHook("afterSanitizeAttributes", (node: Element) => {
if ("target" in node) {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener noreferrer");
}
if (
!node.hasAttribute("target") &&
(node.hasAttribute("xlink:href") || node.hasAttribute("href"))
) {
node.setAttribute("xlink:show", "new");
}
});
const convertMarkdownToHtml = (markdown: string): string => { return DOMPurify.sanitize(html);
marked.use({ hooks: { postprocess, preprocess } }); };
const rawHtml = marked(markdown) as string;
return rawHtml; const convertMarkdownToHtml = (markdown: string): string => {
}; marked.use({ hooks: { postprocess, preprocess } });
return { const rawHtml = marked(markdown) as string;
convertMarkdownToHtml, return rawHtml;
}; };
},
});
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -1,4 +1,9 @@
<template>
<p>Ticket créé par {{ props.by.text }}</p>
</template>
<script setup lang="ts"> <script setup lang="ts">
// Types
import { User } from "../../../../../../../ChillMainBundle/Resources/public/types"; import { User } from "../../../../../../../ChillMainBundle/Resources/public/types";
interface TicketHistoryCreateComponentConfig { interface TicketHistoryCreateComponentConfig {
@ -8,8 +13,4 @@ interface TicketHistoryCreateComponentConfig {
const props = defineProps<TicketHistoryCreateComponentConfig>(); const props = defineProps<TicketHistoryCreateComponentConfig>();
</script> </script>
<template>
<p>Ticket créé par {{ props.by.text }}</p>
</template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@ -50,9 +50,8 @@
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts">
<script lang="ts"> import { ref } from "vue";
import { PropType, defineComponent, ref, computed } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
// Types // Types
@ -66,72 +65,41 @@ import TicketHistoryCommentComponent from "./TicketHistoryCommentComponent.vue";
import TicketHistoryAddresseeComponent from "./TicketHistoryAddresseeComponent.vue"; import TicketHistoryAddresseeComponent from "./TicketHistoryAddresseeComponent.vue";
import TicketHistoryCreateComponent from "./TicketHistoryCreateComponent.vue"; import TicketHistoryCreateComponent from "./TicketHistoryCreateComponent.vue";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue"; import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
// Utils
import { ISOToDatetime } from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date"; import { ISOToDatetime } from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
export default defineComponent({ defineProps<{ history: TicketHistoryLine[] }>();
name: "TicketHistoryListComponent",
components: {
UserRenderBoxBadge,
TicketHistoryPersonComponent,
TicketHistoryMotiveComponent,
TicketHistoryCommentComponent,
TicketHistoryAddresseeComponent,
TicketHistoryCreateComponent,
},
props: {
history: {
type: Array as PropType<TicketHistoryLine[]>,
required: true,
},
},
setup() { const store = useStore();
const store = useStore();
const explainSentence = (history: TicketHistoryLine): string => { const actionIcons = ref(store.getters.getActionIcons);
switch (history.event_type) {
case "add_comment":
return "Nouveau commentaire";
case "addressees_state":
return "Attributions";
case "persons_state":
return "Patients concernés";
case "set_motive":
return "Nouveau motifs";
case "create_ticket":
return "Ticket créé";
}
};
function formatDate(d: DateTime): string { function explainSentence(history: TicketHistoryLine): string {
const date = ISOToDatetime(d.datetime); switch (history.event_type) {
case "add_comment":
if (date === null) { return "Nouveau commentaire";
return ""; case "addressees_state":
} return "Attributions";
case "persons_state":
const month = date.toLocaleString("default", { month: "long" }); return "Usagés concernés";
return `${date.getDate()} ${month} ${date.getFullYear()}, ${date.toLocaleTimeString()}`; case "set_motive":
} return "Nouveau motifs";
case "create_ticket":
return { return "Ticket créé";
actionIcons: ref(store.getters.getActionIcons), default:
formatDate, return "";
explainSentence,
};
},
});
</script>
<style lang="scss" scoped>
div.history-header {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
& > div.description {
margin-right: auto;
} }
} }
</style>
function formatDate(d: DateTime): string {
const date = ISOToDatetime(d.datetime);
if (date === null) {
return "";
}
const month = date.toLocaleString("default", { month: "long" });
return `${date.getDate()} ${month} ${date.getFullYear()}, ${date.toLocaleTimeString()}`;
}
</script>

View File

@ -4,23 +4,11 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { PropType, defineComponent } from "vue";
// Types // Types
import { MotiveHistory } from "../../../types"; import { MotiveHistory } from "../../../types";
export default defineComponent({ defineProps<{ motiveHistory: MotiveHistory }>();
name: "TicketHistoryMotiveComponent",
props: {
motiveHistory: {
type: Object as PropType<MotiveHistory>,
required: true,
},
},
setup() {},
});
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -6,7 +6,7 @@
:type="person.type" :type="person.type"
:id="person.id" :id="person.id"
:buttonText="person.textAge" :buttonText="person.textAge"
:displayBadge="'true' === 'true'" :displayBadge="true"
action="show" action="show"
></on-the-fly> ></on-the-fly>
</li> </li>
@ -14,27 +14,13 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { PropType, defineComponent } from "vue";
// Type
import { PersonsState } from "../../../types";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue"; import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
export default defineComponent({ // Types
name: "TicketHistoryPersonComponent", import { PersonsState } from "../../../types";
props: {
personHistory: {
type: Object as PropType<PersonsState>,
required: true,
},
},
components: {
OnTheFly,
},
setup() {}, defineProps<{ personHistory: PersonsState }>();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -7,7 +7,7 @@
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
> >
{{ $t("ticket.previous_tickets") }} {{ trans(CHILL_TICKET_TICKET_PREVIOUS_TICKETS) }}
<span <span
class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-chill-green" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-chill-green"
> >
@ -19,27 +19,18 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { PropType, defineComponent } from "vue"; // Translations
import { trans, CHILL_TICKET_TICKET_PREVIOUS_TICKETS } from "translator";
// Types // Types
import { Ticket } from "../../../types"; import { Ticket } from "../../../types";
export default defineComponent({ defineProps<{ tickets: Ticket[] }>();
name: "TicketSelectorComponent",
props: { function handleClick() {
tickets: { alert("Sera disponible plus tard");
type: Object as PropType<Ticket[]>, }
required: true,
},
},
setup() {
function handleClick() {
alert("Sera disponible plus tard");
}
return { handleClick };
},
});
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -1,56 +0,0 @@
import { multiSelectMessages } from "../../../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
import { personMessages } from "../../../../../../../ChillPersonBundle/Resources/public/vuejs/_js/i18n";
const messages = {
fr: {
ticket: {
previous_tickets: "Précédents tickets",
cancel: "Annuler",
save: "Enregistrer",
close: "Fermer",
},
history: {
person: "Ouverture par appel téléphonique de ",
user: "Prise en charge par ",
},
add_comment: {
title: "Commentaire",
label: "Ajouter un commentaire",
success: "Commentaire enregistré",
content: "Ajouter un commentaire",
error: "Aucun commentaire ajouté",
},
set_motive: {
title: "Motif",
label: "Choisir un motif",
success: "Motif enregistré",
error: "Aucun motif sélectionné",
},
add_addressee: {
title: "Attribuer",
user_group_label: "Attributer à un groupe",
user_label: "Attribuer à un ou plusieurs utilisateurs",
success: "Attribution effectuée",
error: "Aucun destinataire sélectionné",
},
set_persons: {
title: "Patients concernés",
user_label: "Ajouter un patient",
},
banner: {
concerned_patient: "Patients concernés",
speaker: "Attribué à",
open: "Ouvert",
since: "Depuis {time}",
and: "et",
days: "|1 jour|{count} jours",
hours: "|1 heure et|{count} heures",
minutes: "|1 minute|{count} minutes",
seconds: "|1 seconde|{count} secondes",
no_motive: "Pas de motif",
},
},
};
Object.assign(messages.fr, multiSelectMessages.fr);
Object.assign(messages.fr, personMessages.fr);
export default messages;

View File

@ -1,13 +1,10 @@
import App from "./App.vue"; import App from "./App.vue";
import { createApp } from "vue"; import { createApp } from "vue";
import { _createI18n } from "../../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
import VueToast from "vue-toast-notification"; import VueToast from "vue-toast-notification";
import "vue-toast-notification/dist/theme-sugar.css"; import "vue-toast-notification/dist/theme-sugar.css";
import { store } from "./store"; import { store } from "./store";
import messages from "./i18n/messages";
declare global { declare global {
interface Window { interface Window {
@ -15,16 +12,11 @@ declare global {
} }
} }
const i18n = _createI18n(messages, false);
const _app = createApp({ const _app = createApp({
template: "<app></app>", template: "<app></app>",
}); });
_app.use(store) _app.use(store)
.use(i18n)
// Cant use this.$toast in components in composition API so we need to provide it
// Fix: with vue-toast-notification@^3
.use(VueToast) .use(VueToast)
.provide("toast", _app.config.globalProperties.$toast) .provide("toast", _app.config.globalProperties.$toast)
.component("app", App) .component("app", App)

View File

@ -0,0 +1,66 @@
chill_ticket:
list:
title: Tickets
filter:
to_me: Tickets qui me sont attribués
in_alert: Tickets en alerte (délai de résolution dépassé)
created_between: Créés entre
ticket:
previous_tickets: "Précédents tickets"
actions_toolbar:
cancel: "Annuler"
save: "Enregistrer"
close: "Fermer"
add_comment:
title: "Commentaire"
label: "Ajouter un commentaire"
success: "Commentaire enregistré"
content: "Ajouter un commentaire"
error: "Aucun commentaire ajouté"
set_motive:
title: "Motif"
label: "Choisir un motif"
success: "Motif enregistré"
error: "Aucun motif sélectionné"
add_addressee:
title: "Attribuer"
user_group_label: "Attributer à un groupe"
user_label: "Attribuer à un ou plusieurs utilisateurs"
success: "Attribution effectuée"
error: "Aucun destinataire sélectionné"
set_persons:
title: "Usagers concernés"
user_label: "Ajouter un usager"
success: "Usager ajouté"
error: "Aucun usager sélectionné"
banner:
concerned_usager: "Usagers concernés"
speaker: "Attribué à"
open: "Ouvert"
since: "Depuis {time}"
and: "et"
days: >-
{count, plural,
=0 {aucun jour}
=1 {1 jour}
other {# jours}
}
hours: >-
{count, plural,
=0 {aucune heure}
=1 {1 heure}
other {# heures}
}
minutes: >-
{count, plural,
=0 {aucune minute}
=1 {1 minute}
other {# minutes}
}
seconds: >-
{count, plural,
=0 {aucune seconde}
=1 {1 seconde}
other {# secondes}
}
no_motive: "Pas de motif"

View File

@ -1,9 +0,0 @@
chill_ticket:
list:
title: Tickets
filter:
to_me: Tickets qui me sont attribués
in_alert: Tickets en alerte (délai de résolution dépassé)
created_between: Créés entre