mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-29 10:05:03 +00:00
# Conflicts: # src/Bundle/ChillMainBundle/Export/Formatter/CSVFormatter.php # src/Bundle/ChillMainBundle/Export/Formatter/CSVListFormatter.php # src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php # src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/GeographicalUnitStatAggregator.php # src/Bundle/ChillPersonBundle/Resources/public/types.ts # src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue
500 lines
15 KiB
Vue
500 lines
15 KiB
Vue
<template>
|
|
<a
|
|
class="btn"
|
|
:class="getClassButton"
|
|
:title="buttonTitle"
|
|
@click="openModal"
|
|
>
|
|
<span v-if="displayTextButton">{{ buttonTitle }}</span>
|
|
</a>
|
|
|
|
<teleport to="body">
|
|
<modal
|
|
v-if="showModal"
|
|
@close="closeModal"
|
|
:modal-dialog-class="modalDialogClass"
|
|
:show="showModal"
|
|
:hide-footer="false"
|
|
>
|
|
<template #header>
|
|
<h3 class="modal-title">
|
|
{{ modalTitle }}
|
|
</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>
|
|
|
|
<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"
|
|
/>
|
|
|
|
<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>
|
|
|
|
<script setup lang="ts">
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
|
import { ref, reactive, computed, nextTick, watch, shallowRef } from "vue";
|
|
import PersonSuggestion from "./AddPersons/PersonSuggestion.vue";
|
|
import { searchEntities } from "ChillPersonAssets/vuejs/_api/AddPersons";
|
|
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,
|
|
} from "translator";
|
|
import {
|
|
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 },
|
|
});
|
|
|
|
defineEmits(["addNewPersons"]);
|
|
|
|
const showModal = ref(false);
|
|
const modalDialogClass = ref("modal-dialog-scrollable modal-xl");
|
|
|
|
const modal = shallowRef({
|
|
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>,
|
|
});
|
|
|
|
const searchRef = ref<HTMLInputElement | null>(null);
|
|
const onTheFly = ref<InstanceType<typeof OnTheFly> | null>(null);
|
|
|
|
const query = computed({
|
|
get: () => search.query,
|
|
set: (val) => setQuery(val),
|
|
});
|
|
const queryLength = computed(() => search.query.length);
|
|
const suggestedCounter = computed(() => search.suggested.length);
|
|
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;
|
|
});
|
|
const displayTextButton = computed(() =>
|
|
props.options?.button?.display !== undefined
|
|
? props.options.button.display
|
|
: true,
|
|
);
|
|
|
|
const checkUniq = computed(() =>
|
|
props.options.uniq === true ? "radio" : "checkbox",
|
|
);
|
|
|
|
const priorSuggestion = computed(() => search.priorSuggestion);
|
|
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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
function loadSuggestions(suggestedArr: Suggestion[]) {
|
|
search.suggested = suggestedArr;
|
|
search.suggested.forEach((item) => {
|
|
item.key = itemKey(item);
|
|
});
|
|
}
|
|
|
|
function updateSelected(value: Suggestion[]) {
|
|
search.selected = value;
|
|
}
|
|
|
|
function resetSuggestion() {
|
|
search.query = "";
|
|
search.suggested = [];
|
|
}
|
|
|
|
function resetSelection() {
|
|
search.selected = [];
|
|
}
|
|
|
|
function resetSearch() {
|
|
resetSelection();
|
|
resetSuggestion();
|
|
}
|
|
|
|
function selectAll() {
|
|
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 = {};
|
|
}
|
|
}
|
|
|
|
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,
|
|
);
|
|
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();
|
|
}
|
|
} 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>
|
|
|
|
<style lang="scss">
|
|
li.add-persons {
|
|
a {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
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;
|
|
//margin: 1em 0;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
.create-button > a {
|
|
margin-top: 0.5em;
|
|
margin-left: 2.6em;
|
|
}
|
|
</style>
|