Afficher les patients suggérés et ajouter un sélecteur urgent/non urgent

This commit is contained in:
Boris Waaub
2025-07-04 07:45:33 +00:00
committed by Julien Fastré
parent 06e8264dde
commit 3df4043eb9
24 changed files with 1539 additions and 1121 deletions

View File

@@ -11,10 +11,10 @@
<teleport to="body">
<modal
v-if="showModal"
@close="closeModal"
@close="closeModal"
:modal-dialog-class="modalDialogClass"
:show="showModal"
:hide-footer="false"
:hide-footer="false"
>
<template #header>
<h3 class="modal-title">
@@ -22,101 +22,110 @@
</h3>
</template>
<template #body-head>
<div class="modal-body">
<div class="search">
<label class="col-form-label" style="float: right">
{{
trans(ADD_PERSONS_SUGGESTED_COUNTER, {
count: suggestedCounter,
})
}}
</label>
<template #body-head>
<div class="modal-body">
<div class="search">
<label class="col-form-label" style="float: right">
{{
trans(ADD_PERSONS_SUGGESTED_COUNTER, {
count: suggestedCounter,
})
}}
</label>
<input
id="search-persons"
name="query"
v-model="query"
:placeholder="trans(ADD_PERSONS_SEARCH_SOME_PERSONS)"
ref="searchRef"
/>
<i class="fa fa-search fa-lg" />
<i
class="fa fa-times"
v-if="queryLength >= 3"
@click="resetSuggestion"
/>
</div>
</div>
<div class="modal-body" v-if="checkUniq === 'checkbox'">
<div class="count">
<span>
<a v-if="suggestedCounter > 2" @click="selectAll">
{{ trans(ACTION_CHECK_ALL) }}
</a>
<a v-if="selectedCounter > 0" @click="resetSelection">
<i v-if="suggestedCounter > 2"> </i>
{{ trans(ACTION_RESET) }}
</a>
</span>
<span v-if="selectedCounter > 0">
{{
trans(ADD_PERSONS_SELECTED_COUNTER, {
count: selectedCounter,
})
}}
</span>
</div>
</div>
</template>
<input
id="search-persons"
name="query"
v-model="query"
:placeholder="
trans(ADD_PERSONS_SEARCH_SOME_PERSONS)
"
ref="searchRef"
/>
<i class="fa fa-search fa-lg" />
<i
class="fa fa-times"
v-if="queryLength >= 3"
@click="resetSuggestion"
/>
</div>
</div>
<div class="modal-body" v-if="checkUniq === 'checkbox'">
<div class="count">
<span>
<a v-if="suggestedCounter > 2" @click="selectAll">
{{ trans(ACTION_CHECK_ALL) }}
</a>
<a
v-if="selectedCounter > 0"
@click="resetSelection"
>
<i v-if="suggestedCounter > 2"> </i>
{{ trans(ACTION_RESET) }}
</a>
</span>
<span v-if="selectedCounter > 0">
{{
trans(ADD_PERSONS_SELECTED_COUNTER, {
count: selectedCounter,
})
}}
</span>
</div>
</div>
</template>
<template #body>
<div class="results">
<person-suggestion
v-for="item in selectedAndSuggested.slice().reverse()"
:key="itemKey(item)"
:item="item"
:search="search"
:type="checkUniq"
@save-form-on-the-fly="saveFormOnTheFly"
@new-prior-suggestion="newPriorSuggestion"
@update-selected="updateSelected"
/>
<template #body>
<div class="results">
<person-suggestion
v-for="item in selectedAndSuggested.slice().reverse()"
:key="itemKey(item)"
:item="item"
:search="search"
:type="checkUniq"
@save-form-on-the-fly="saveFormOnTheFly"
@new-prior-suggestion="newPriorSuggestion"
@update-selected="updateSelected"
/>
<div class="create-button">
<on-the-fly
v-if="
queryLength >= 3 &&
(options.type.includes('person') ||
options.type.includes('thirdparty'))
"
:button-text="trans(ONTHEFLY_CREATE_BUTTON, { q: query })"
:allowed-types="options.type"
:query="query"
action="create"
@save-form-on-the-fly="saveFormOnTheFly"
ref="onTheFly"
/>
</div>
</div>
</template>
<div class="create-button">
<on-the-fly
v-if="
queryLength >= 3 &&
(options.type.includes('person') ||
options.type.includes('thirdparty'))
"
:button-text="
trans(ONTHEFLY_CREATE_BUTTON, { q: query })
"
:allowed-types="options.type"
:query="query"
action="create"
@save-form-on-the-fly="saveFormOnTheFly"
ref="onTheFly"
/>
</div>
</div>
</template>
<template #footer>
<button
class="btn btn-create"
@click.prevent="
() => {
$emit('addNewPersons', { selected: selectedComputed });
query = '';
closeModal();
}
"
>
{{ trans(ACTION_ADD) }}
</button>
</template>
</modal>
</teleport>
<template #footer>
<button
class="btn btn-create"
@click.prevent="
() => {
$emit('addNewPersons', {
selected: selectedComputed,
});
query = '';
closeModal();
}
"
>
{{ trans(ACTION_ADD) }}
</button>
</template>
</modal>
</teleport>
</template>
<script setup lang="ts">
@@ -130,31 +139,31 @@ import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import {
trans,
ADD_PERSONS_SUGGESTED_COUNTER,
ADD_PERSONS_SEARCH_SOME_PERSONS,
ADD_PERSONS_SELECTED_COUNTER,
ONTHEFLY_CREATE_BUTTON,
ACTION_CHECK_ALL,
ACTION_RESET,
ACTION_ADD,
trans,
ADD_PERSONS_SUGGESTED_COUNTER,
ADD_PERSONS_SEARCH_SOME_PERSONS,
ADD_PERSONS_SELECTED_COUNTER,
ONTHEFLY_CREATE_BUTTON,
ACTION_CHECK_ALL,
ACTION_RESET,
ACTION_ADD,
} from "translator";
import {
Suggestion,
Search,
Result as OriginalResult,
SearchOptions,
Suggestion,
Search,
Result as OriginalResult,
SearchOptions,
} from "ChillPersonAssets/types";
// Extend Result type to include optional addressId
type Result = OriginalResult & { addressId?: number };
const props = defineProps({
suggested: { type: Array as () => Suggestion[], default: () => [] },
selected: { type: Array as () => Suggestion[], default: () => [] },
buttonTitle: { type: String, required: true },
modalTitle: { type: String, required: true },
options: { type: Object as () => SearchOptions, required: true },
suggested: { type: Array as () => Suggestion[], default: () => [] },
selected: { type: Array as () => Suggestion[], default: () => [] },
buttonTitle: { type: String, required: true },
modalTitle: { type: String, required: true },
options: { type: Object as () => SearchOptions, required: true },
});
defineEmits(["addNewPersons"]);
@@ -163,25 +172,25 @@ const showModal = ref(false);
const modalDialogClass = ref("modal-dialog-scrollable modal-xl");
const modal = shallowRef({
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl",
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl",
});
const search = reactive({
query: "" as string,
previousQuery: "" as string,
currentSearchQueryController: null as AbortController | null,
suggested: props.suggested as Suggestion[],
selected: props.selected as Suggestion[],
priorSuggestion: {} as Partial<Suggestion>,
query: "" as string,
previousQuery: "" as string,
currentSearchQueryController: null as AbortController | null,
suggested: props.suggested as Suggestion[],
selected: props.selected as Suggestion[],
priorSuggestion: {} as Partial<Suggestion>,
});
const searchRef = ref<HTMLInputElement | null>(null);
const onTheFly = ref<InstanceType<typeof OnTheFly> | null>(null);
const query = computed({
get: () => search.query,
set: (val) => setQuery(val),
get: () => search.query,
set: (val) => setQuery(val),
});
const queryLength = computed(() => search.query.length);
const suggestedCounter = computed(() => search.suggested.length);
@@ -189,18 +198,18 @@ const selectedComputed = computed(() => search.selected);
const selectedCounter = computed(() => search.selected.length);
const getClassButton = computed(() => {
let size = props.options?.button?.size ?? "";
let type = props.options?.button?.type ?? "btn-create";
return size ? size + " " + type : type;
let size = props.options?.button?.size ?? "";
let type = props.options?.button?.type ?? "btn-create";
return size ? size + " " + type : type;
});
const displayTextButton = computed(() =>
props.options?.button?.display !== undefined
? props.options.button.display
: true,
props.options?.button?.display !== undefined
? props.options.button.display
: true,
);
const checkUniq = computed(() =>
props.options.uniq === true ? "radio" : "checkbox",
props.options.uniq === true ? "radio" : "checkbox",
);
const priorSuggestion = computed(() => search.priorSuggestion);
@@ -209,229 +218,232 @@ const hasPriorSuggestion = computed(() => !!search.priorSuggestion.key);
const itemKey = (item: Suggestion) => item.result.type + item.result.id;
function addPriorSuggestion() {
if (hasPriorSuggestion.value) {
// Type assertion is safe here due to the checks above
search.suggested.unshift(priorSuggestion.value as Suggestion);
search.selected.unshift(priorSuggestion.value as Suggestion);
newPriorSuggestion(null);
}
if (hasPriorSuggestion.value) {
// Type assertion is safe here due to the checks above
search.suggested.unshift(priorSuggestion.value as Suggestion);
search.selected.unshift(priorSuggestion.value as Suggestion);
newPriorSuggestion(null);
}
}
const selectedAndSuggested = computed(() => {
addPriorSuggestion();
const uniqBy = (a: Suggestion[], key: (item: Suggestion) => string) => [
...new Map(a.map((x) => [key(x), x])).values(),
];
let union = [
...new Set([
...search.suggested.slice().reverse(),
...search.selected.slice().reverse(),
]),
];
return uniqBy(union, (k: Suggestion) => k.key);
addPriorSuggestion();
const uniqBy = (a: Suggestion[], key: (item: Suggestion) => string) => [
...new Map(a.map((x) => [key(x), x])).values(),
];
let union = [
...new Set([
...search.suggested.slice().reverse(),
...search.selected.slice().reverse(),
]),
];
return uniqBy(union, (k: Suggestion) => k.key);
});
function openModal() {
showModal.value = true;
nextTick(() => {
if (searchRef.value) searchRef.value.focus();
});
showModal.value = true;
nextTick(() => {
if (searchRef.value) searchRef.value.focus();
});
}
function closeModal() {
showModal.value = false;
showModal.value = false;
}
function setQuery(q: string) {
search.query = q;
search.query = q;
// Clear previous search if any
if (search.currentSearchQueryController) {
search.currentSearchQueryController.abort();
search.currentSearchQueryController = null;
}
// Clear previous search if any
if (search.currentSearchQueryController) {
search.currentSearchQueryController.abort();
search.currentSearchQueryController = null;
}
if (q === "") {
loadSuggestions([]);
return;
}
if (q === "") {
loadSuggestions([]);
return;
}
// Debounce delay based on query length
const delay = q.length > 3 ? 300 : 700;
// 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;
setTimeout(() => {
// Only search if query hasn't changed in the meantime
if (q !== search.query) return;
search.currentSearchQueryController = new AbortController();
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);
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);
}
function loadSuggestions(suggestedArr: Suggestion[]) {
search.suggested = suggestedArr;
search.suggested.forEach((item) => {
item.key = itemKey(item);
});
search.suggested = suggestedArr;
search.suggested.forEach((item) => {
item.key = itemKey(item);
});
}
function updateSelected(value: Suggestion[]) {
search.selected = value;
search.selected = value;
}
function resetSuggestion() {
search.query = "";
search.suggested = [];
search.query = "";
search.suggested = [];
}
function resetSelection() {
search.selected = [];
search.selected = [];
}
function resetSearch() {
resetSelection();
resetSuggestion();
resetSelection();
resetSuggestion();
}
function selectAll() {
search.suggested.forEach((item) => {
search.selected.push(item);
});
search.suggested.forEach((item) => {
search.selected.push(item);
});
}
function newPriorSuggestion(entity: Result | null) {
if (entity !== null) {
let suggestion = {
key: entity.type + entity.id,
relevance: 0.5,
result: entity,
};
search.priorSuggestion = suggestion;
} else {
search.priorSuggestion = {};
}
if (entity !== null) {
let suggestion = {
key: entity.type + entity.id,
relevance: 0.5,
result: entity,
};
search.priorSuggestion = suggestion;
} else {
search.priorSuggestion = {};
}
}
async function saveFormOnTheFly({
type,
data,
type,
data,
}: {
type: string;
data: Result;
type: string;
data: Result;
}) {
try {
if (type === "person") {
const responsePerson: Result = await makeFetch(
"POST",
"/api/1.0/person/person.json",
data,
);
newPriorSuggestion(responsePerson);
if (onTheFly.value) onTheFly.value.closeModal();
if (data.addressId != null) {
const household = { type: "household" };
const address = { id: data.addressId };
try {
const responseHousehold: Result = await makeFetch(
"POST",
"/api/1.0/person/household.json",
household,
);
const member = {
concerned: [
{
person: {
type: "person",
id: responsePerson.id,
},
start_date: {
datetime: `${new Date().toISOString().split("T")[0]}T00:00:00+02:00`,
},
holder: false,
comment: null,
},
],
destination: {
type: "household",
id: responseHousehold.id,
},
composition: null,
};
await makeFetch(
"POST",
"/api/1.0/person/household/members/move.json",
member,
);
try {
const _response = await makeFetch(
"POST",
`/api/1.0/person/household/${responseHousehold.id}/address.json`,
address,
try {
if (type === "person") {
const responsePerson: Result = await makeFetch(
"POST",
"/api/1.0/person/person.json",
data,
);
console.log(_response);
} catch (error) {
console.error(error);
}
} catch (error) {
console.error(error);
newPriorSuggestion(responsePerson);
if (onTheFly.value) onTheFly.value.closeModal();
if (data.addressId != null) {
const household = { type: "household" };
const address = { id: data.addressId };
try {
const responseHousehold: Result = await makeFetch(
"POST",
"/api/1.0/person/household.json",
household,
);
const member = {
concerned: [
{
person: {
type: "person",
id: responsePerson.id,
},
start_date: {
datetime: `${new Date().toISOString().split("T")[0]}T00:00:00+02:00`,
},
holder: false,
comment: null,
},
],
destination: {
type: "household",
id: responseHousehold.id,
},
composition: null,
};
await makeFetch(
"POST",
"/api/1.0/person/household/members/move.json",
member,
);
try {
const _response = await makeFetch(
"POST",
`/api/1.0/person/household/${responseHousehold.id}/address.json`,
address,
);
console.log(_response);
} catch (error) {
console.error(error);
}
} catch (error) {
console.error(error);
}
}
} 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();
}
}
} 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);
}
} catch (error) {
console.error(error);
}
}
watch(
() => props.selected,
(newSelected) => {
search.selected = newSelected;
},
{ deep: true },
() => props.selected,
(newSelected) => {
search.selected = newSelected;
},
{ deep: true },
);
watch(
() => props.suggested,
(newSuggested) => {
search.suggested = newSuggested;
},
{ deep: true },
() => props.suggested,
(newSuggested) => {
search.suggested = newSuggested;
},
{ deep: true },
);
watch(
() => modal,
(val) => {
showModal.value = val.value.showModal;
modalDialogClass.value = val.value.modalDialogClass;
},
{ deep: true },
() => modal,
(val) => {
showModal.value = val.value.showModal;
modalDialogClass.value = val.value.modalDialogClass;
},
{ deep: true },
);
defineExpose({
resetSearch,
showModal,
resetSearch,
showModal,
});
</script>
@@ -442,42 +454,42 @@ li.add-persons {
}
}
div.body-head {
overflow-y: unset;
div.modal-body:first-child {
margin: auto 4em;
div.search {
position: relative;
input {
width: 100%;
padding: 1.2em 1.5em 1.2em 2.5em;
}
i {
position: absolute;
opacity: 0.5;
padding: 0.65em 0;
top: 50%;
}
i.fa-search {
left: 0.5em;
}
i.fa-times {
right: 1em;
padding: 0.75em 0;
cursor: pointer;
}
overflow-y: unset;
div.modal-body:first-child {
margin: auto 4em;
div.search {
position: relative;
input {
width: 100%;
padding: 1.2em 1.5em 1.2em 2.5em;
}
i {
position: absolute;
opacity: 0.5;
padding: 0.65em 0;
top: 50%;
}
i.fa-search {
left: 0.5em;
}
i.fa-times {
right: 1em;
padding: 0.75em 0;
cursor: pointer;
}
}
}
}
div.modal-body:last-child {
padding-bottom: 0;
}
div.count {
margin: -0.5em 0 0.7em;
display: flex;
justify-content: space-between;
a {
cursor: pointer;
div.modal-body:last-child {
padding-bottom: 0;
}
div.count {
margin: -0.5em 0 0.7em;
display: flex;
justify-content: space-between;
a {
cursor: pointer;
}
}
}
}
.create-button > a {
margin-top: 0.5em;