Restore features after merging

This commit is contained in:
Julien Fastré 2025-07-09 17:46:16 +02:00
parent 392fd01b56
commit 0204bdd38d
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
3 changed files with 605 additions and 588 deletions

View File

@ -1,14 +1,18 @@
<template> <template>
<div class="grey-card"> <div class="grey-card">
<ul :class="listClasses" v-if="picked.length && displayPicked"> <ul :class="listClasses" v-if="picked.length && displayPicked">
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type + p.id"> <li
<span v-for="p in picked"
v-if="'me' === p" @click="removeEntity(p)"
class="chill_denomination current-user updatedBy" :key="p === 'me' ? 'me' : p.type + p.id"
>{{ trans(USER_CURRENT_USER) }}</span >
>
<span <span
v-else v-if="'me' === p"
class="chill_denomination current-user updatedBy"
>{{ trans(USER_CURRENT_USER) }}</span
>
<span
v-else
:class="getBadgeClass(p)" :class="getBadgeClass(p)"
class="chill_denomination" class="chill_denomination"
:style="getBadgeStyle(p)" :style="getBadgeStyle(p)"
@ -19,17 +23,17 @@
</ul> </ul>
<ul class="record_actions"> <ul class="record_actions">
<li v-if="isCurrentUserPicker" class="btn btn-sm btn-misc"> <li v-if="isCurrentUserPicker" class="btn btn-sm btn-misc">
<label class="flex items-center gap-2"> <label class="flex items-center gap-2">
<input <input
:checked="picked.indexOf('me') >= 0 ? true : null" :checked="isMePicked"
ref="itsMeCheckbox" ref="itsMeCheckbox"
:type="multiple ? 'checkbox' : 'radio'" :type="multiple ? 'checkbox' : 'radio'"
@change="selectItsMe" @change="selectItsMe($event as InputEvent)"
/> />
{{ trans(USER_CURRENT_USER) }} {{ trans(USER_CURRENT_USER) }}
</label> </label>
</li> </li>
<li class="add-persons"> <li class="add-persons">
<add-persons <add-persons
:options="addPersonsOptions" :options="addPersonsOptions"
:key="uniqid" :key="uniqid"
@ -42,7 +46,11 @@
</ul> </ul>
<ul class="badge-suggest add-items inline" style="float: right"> <ul class="badge-suggest add-items inline" style="float: right">
<li v-for="s in suggested" :key="s.id" @click="addNewSuggested(s)"> <li
v-for="s in suggested"
:key="s.type + s.id"
@click="addNewSuggested(s)"
>
<span :class="getBadgeClass(s)" :style="getBadgeStyle(s)"> <span :class="getBadgeClass(s)" :style="getBadgeStyle(s)">
{{ s.text }} {{ s.text }}
</span> </span>
@ -52,16 +60,28 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, defineProps, defineEmits, defineComponent } from "vue"; import {
ref,
computed,
defineProps,
defineEmits,
defineComponent,
withDefaults,
} from "vue";
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue"; import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
import { Entities, EntityType, SearchOptions } from "ChillPersonAssets/types"; import {
Entities,
EntitiesOrMe,
EntityType,
SearchOptions,
} from "ChillPersonAssets/types";
import { import {
PICK_ENTITY_MODAL_TITLE, PICK_ENTITY_MODAL_TITLE,
PICK_ENTITY_USER, PICK_ENTITY_USER,
PICK_ENTITY_USER_GROUP, PICK_ENTITY_USER_GROUP,
PICK_ENTITY_PERSON, PICK_ENTITY_PERSON,
PICK_ENTITY_THIRDPARTY, PICK_ENTITY_THIRDPARTY,
USER_CURRENT_USER, USER_CURRENT_USER,
trans, trans,
} from "translator"; } from "translator";
import { addNewEntities } from "ChillMainAssets/types"; import { addNewEntities } from "ChillMainAssets/types";
@ -71,25 +91,28 @@ defineComponent({
AddPersons, AddPersons,
}, },
}); });
const props = defineProps<{ const props = withDefaults(
multiple: boolean; defineProps<{
types: EntityType[]; multiple: boolean;
picked: Entities[]; types: EntityType[];
uniqid: string; picked: EntitiesOrMe[];
removableIfSet?: boolean; uniqid: string;
displayPicked?: boolean; removableIfSet?: boolean;
suggested?: Entities[]; displayPicked?: boolean;
label?: string; suggested?: Entities[];
isCurrentUserPicker: boolean // must default to false label?: string;
}>(); isCurrentUserPicker?: boolean;
}>(),
{ isCurrentUserPicker: false },
);
const emits = defineEmits<{ const emits = defineEmits<{
(e: "addNewEntity", payload: { entity: Entities }): void; (e: "addNewEntity", payload: { entity: EntitiesOrMe }): void;
(e: "removeEntity", payload: { entity: Entities }): void; (e: "removeEntity", payload: { entity: EntitiesOrMe }): void;
(e: "addNewEntityProcessEnded"): void; (e: "addNewEntityProcessEnded"): void;
}>(); }>();
const itsMeCheckbox = ref(null); const itsMeCheckbox = ref<null | HTMLInputElement>(null);
const addPersons = ref(); const addPersons = ref();
const addPersonsOptions = computed( const addPersonsOptions = computed(
@ -105,6 +128,8 @@ const addPersonsOptions = computed(
}) as SearchOptions, }) as SearchOptions,
); );
const isMePicked = computed<boolean>(() => props.picked.indexOf("me") >= 0);
const translatedListOfTypes = computed(() => { const translatedListOfTypes = computed(() => {
if (props.label !== undefined && props.label !== "") { if (props.label !== undefined && props.label !== "") {
return props.label; return props.label;
@ -142,10 +167,12 @@ const listClasses = computed(() => ({
inline: true, inline: true,
})); }));
const selectItsMe = (event) => const selectItsMe = (event: InputEvent) => {
event.target.checked ? addNewSuggested("me") : removeEntity("me"); const target = event.target as HTMLInputElement;
target.checked ? addNewSuggested("me") : removeEntity("me");
};
function addNewSuggested(entity: Entities) { function addNewSuggested(entity: EntitiesOrMe) {
emits("addNewEntity", { entity }); emits("addNewEntity", { entity });
} }
@ -158,32 +185,31 @@ function addNewEntity({ selected }: addNewEntities) {
emits("addNewEntityProcessEnded"); emits("addNewEntityProcessEnded");
} }
const removeEntity = (entity: EntitiesOrMe) => {
const removeEntity = (entity) => { if (!props.removableIfSet) return;
if (!props.removableIfSet) return; if (entity === "me" && itsMeCheckbox.value) {
if (entity === "me" && itsMeCheckbox.value) { itsMeCheckbox.value.checked = false;
itsMeCheckbox.value.checked = false; }
} emits("removeEntity", { entity });
emits("removeEntity", { entity });
}; };
function getBadgeClass(entities: Entities) { function getBadgeClass(entities: Entities) {
if (entities.type !== "user_group") { if (entities.type !== "user_group") {
return entities.type; return entities.type;
} }
return ""; return "";
} }
function getBadgeStyle(entities: Entities) { function getBadgeStyle(entities: Entities) {
if (entities.type === "user_group") { if (entities.type === "user_group") {
return [ return [
`ul.badge-suggest li > span { `ul.badge-suggest li > span {
color: ${entities.foregroundColor}!important; color: ${entities.foregroundColor}!important;
border-bottom-color: ${entities.backgroundColor}; border-bottom-color: ${entities.backgroundColor};
}`, }`,
]; ];
} }
return []; return [];
} }
</script> </script>
@ -291,7 +317,7 @@ ul.badge-suggest li > span.thirdparty {
border-bottom-color: rgb(198.9, 72, 98.1); border-bottom-color: rgb(198.9, 72, 98.1);
} }
.current-user { .current-user {
color: var(--bs-body-color); color: var(--bs-body-color);
background-color: var(--bs-chill-l-gray) !important; background-color: var(--bs-chill-l-gray) !important;
} }
</style> </style>

View File

@ -1,4 +1,3 @@
import { StoredObject } from "ChillDocStoreAssets/types";
import { import {
Address, Address,
Center, Center,
@ -9,8 +8,8 @@ import {
Household, Household,
WorkflowAvailable, WorkflowAvailable,
Scope, Scope,
Job, Job,
PrivateCommentEmbeddable, PrivateCommentEmbeddable,
} from "ChillMainAssets/types"; } from "ChillMainAssets/types";
import { StoredObject } from "ChillDocStoreAssets/types"; import { StoredObject } from "ChillDocStoreAssets/types";
import { Thirdparty } from "../../../ChillThirdPartyBundle/Resources/public/types"; import { Thirdparty } from "../../../ChillThirdPartyBundle/Resources/public/types";
@ -45,208 +44,208 @@ export interface Person {
} }
export interface AccompanyingPeriod { export interface AccompanyingPeriod {
id: number; id: number;
addressLocation?: Address | null; addressLocation?: Address | null;
administrativeLocation?: Location | null; administrativeLocation?: Location | null;
calendars: Calendar[]; calendars: Calendar[];
closingDate?: Date | null; closingDate?: Date | null;
closingMotive?: ClosingMotive | null; closingMotive?: ClosingMotive | null;
comments: Comment[]; comments: Comment[];
confidential: boolean; confidential: boolean;
createdAt?: Date | null; createdAt?: Date | null;
createdBy?: User | null; createdBy?: User | null;
emergency: boolean; emergency: boolean;
intensity?: "occasional" | "regular"; intensity?: "occasional" | "regular";
job?: Job | null; job?: Job | null;
locationHistories: AccompanyingPeriodLocationHistory[]; locationHistories: AccompanyingPeriodLocationHistory[];
openingDate?: Date | null; openingDate?: Date | null;
origin?: Origin | null; origin?: Origin | null;
participations: AccompanyingPeriodParticipation[]; participations: AccompanyingPeriodParticipation[];
personLocation?: Person | null; personLocation?: Person | null;
pinnedComment?: Comment | null; pinnedComment?: Comment | null;
preventUserIsChangedNotification: boolean; preventUserIsChangedNotification: boolean;
remark: string; remark: string;
requestorAnonymous: boolean; requestorAnonymous: boolean;
requestorPerson?: Person | null; requestorPerson?: Person | null;
requestorThirdParty?: Thirdparty | null; requestorThirdParty?: Thirdparty | null;
resources: AccompanyingPeriodResource[]; resources: AccompanyingPeriodResource[];
scopes: Scope[]; scopes: Scope[];
socialIssues: SocialIssue[]; socialIssues: SocialIssue[];
step?: step?:
| "CLOSED" | "CLOSED"
| "CONFIRMED" | "CONFIRMED"
| "CONFIRMED_INACTIVE_SHORT" | "CONFIRMED_INACTIVE_SHORT"
| "CONFIRMED_INACTIVE_LONG" | "CONFIRMED_INACTIVE_LONG"
| "DRAFT"; | "DRAFT";
} }
export interface AccompanyingPeriodWork { export interface AccompanyingPeriodWork {
id: number; id: number;
accompanyingPeriod?: AccompanyingPeriod; accompanyingPeriod?: AccompanyingPeriod;
accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[]; accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[];
createdAt?: string; createdAt?: string;
createdAutomatically: boolean; createdAutomatically: boolean;
createdAutomaticallyReason: string; createdAutomaticallyReason: string;
createdBy: User; createdBy: User;
endDate?: string; endDate?: string;
goals: AccompanyingPeriodWorkGoal[]; goals: AccompanyingPeriodWorkGoal[];
handlingThierParty?: Thirdparty; handlingThierParty?: Thirdparty;
note: string; note: string;
persons: Person[]; persons: Person[];
privateComment: PrivateCommentEmbeddable; privateComment: PrivateCommentEmbeddable;
referrersHistory: AccompanyingPeriodWorkReferrerHistory[]; referrersHistory: AccompanyingPeriodWorkReferrerHistory[];
results: Result[]; results: Result[];
socialAction?: SocialAction; socialAction?: SocialAction;
startDate?: string; startDate?: string;
thirdParties: Thirdparty[]; thirdParties: Thirdparty[];
updatedAt?: string; updatedAt?: string;
updatedBy: User; updatedBy: User;
version: number; version: number;
} }
export interface SocialAction { export interface SocialAction {
id: number; id: number;
parent?: SocialAction | null; parent?: SocialAction | null;
children: SocialAction[]; children: SocialAction[];
issue?: SocialIssue | null; issue?: SocialIssue | null;
ordering: number; ordering: number;
title: { title: {
fr: string; fr: string;
}; };
defaultNotificationDelay?: string | null; defaultNotificationDelay?: string | null;
desactivationDate?: string | null; desactivationDate?: string | null;
evaluations: Evaluation[]; evaluations: Evaluation[];
goals: Goal[]; goals: Goal[];
results: Result[]; results: Result[];
} }
export interface AccompanyingPeriodResource { export interface AccompanyingPeriodResource {
id: number; id: number;
accompanyingPeriod: AccompanyingPeriod; accompanyingPeriod: AccompanyingPeriod;
comment?: string | null; comment?: string | null;
person?: Person | null; person?: Person | null;
thirdParty?: Thirdparty | null; thirdParty?: Thirdparty | null;
} }
export interface Origin { export interface Origin {
id: number; id: number;
label: { label: {
fr: string; fr: string;
}; };
noActiveAfter: DateTime; noActiveAfter: DateTime;
} }
export interface ClosingMotive { export interface ClosingMotive {
id: number; id: number;
active: boolean; active: boolean;
name: { name: {
fr: string; fr: string;
}; };
ordering: number; ordering: number;
isCanceledAccompanyingPeriod: boolean; isCanceledAccompanyingPeriod: boolean;
parent?: ClosingMotive | null; parent?: ClosingMotive | null;
children: ClosingMotive[]; children: ClosingMotive[];
} }
export interface AccompanyingPeriodParticipation { export interface AccompanyingPeriodParticipation {
id: number; id: number;
startDate: DateTime; startDate: DateTime;
endDate?: DateTime | null; endDate?: DateTime | null;
accompanyingPeriod: AccompanyingPeriod; accompanyingPeriod: AccompanyingPeriod;
person: Person; person: Person;
} }
export interface AccompanyingPeriodLocationHistory { export interface AccompanyingPeriodLocationHistory {
id: number; id: number;
startDate: DateTime; startDate: DateTime;
endDate?: DateTime | null; endDate?: DateTime | null;
addressLocation?: Address | null; addressLocation?: Address | null;
period: AccompanyingPeriod; period: AccompanyingPeriod;
personLocation?: Person | null; personLocation?: Person | null;
} }
export interface SocialIssue { export interface SocialIssue {
id: number; id: number;
parent?: SocialIssue | null; parent?: SocialIssue | null;
children: SocialIssue[]; children: SocialIssue[];
socialActions?: SocialAction[] | null; socialActions?: SocialAction[] | null;
ordering: number; ordering: number;
title: { title: {
fr: string; fr: string;
}; };
desactivationDate?: string | null; desactivationDate?: string | null;
} }
export interface Goal { export interface Goal {
id: number; id: number;
results: Result[]; results: Result[];
socialActions?: SocialAction[] | null; socialActions?: SocialAction[] | null;
title: { title: {
fr: string; fr: string;
}; };
} }
export interface Result { export interface Result {
id: number; id: number;
accompanyingPeriodWorks: AccompanyingPeriodWork[]; accompanyingPeriodWorks: AccompanyingPeriodWork[];
accompanyingPeriodWorkGoals: AccompanyingPeriodWorkGoal[]; accompanyingPeriodWorkGoals: AccompanyingPeriodWorkGoal[];
goals: Goal[]; goals: Goal[];
socialActions: SocialAction[]; socialActions: SocialAction[];
title: { title: {
fr: string; fr: string;
}; };
desactivationDate?: string | null; desactivationDate?: string | null;
} }
export interface AccompanyingPeriodWorkGoal { export interface AccompanyingPeriodWorkGoal {
id: number; id: number;
accompanyingPeriodWork: AccompanyingPeriodWork; accompanyingPeriodWork: AccompanyingPeriodWork;
goal: Goal; goal: Goal;
note: string; note: string;
results: Result[]; results: Result[];
} }
export interface AccompanyingPeriodWorkEvaluation { export interface AccompanyingPeriodWorkEvaluation {
accompanyingPeriodWork: AccompanyingPeriodWork | null; accompanyingPeriodWork: AccompanyingPeriodWork | null;
comment: string; comment: string;
createdAt: DateTime | null; createdAt: DateTime | null;
createdBy: User | null; createdBy: User | null;
documents: AccompanyingPeriodWorkEvaluationDocument[]; documents: AccompanyingPeriodWorkEvaluationDocument[];
endDate: DateTime | null; endDate: DateTime | null;
evaluation: Evaluation | null; evaluation: Evaluation | null;
id: number | null; id: number | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
key: any; key: any;
maxDate: DateTime | null; maxDate: DateTime | null;
startDate: DateTime | null; startDate: DateTime | null;
updatedAt: DateTime | null; updatedAt: DateTime | null;
updatedBy: User | null; updatedBy: User | null;
warningInterval: string | null; warningInterval: string | null;
timeSpent: number | null; timeSpent: number | null;
} }
export interface Evaluation { export interface Evaluation {
id: number; id: number;
url: string; url: string;
socialActions: SocialAction[]; socialActions: SocialAction[];
title: { title: {
fr: string; fr: string;
}; };
active: boolean; active: boolean;
delay: string; delay: string;
notificationDelay: string; notificationDelay: string;
} }
export interface AccompanyingPeriodWorkReferrerHistory { export interface AccompanyingPeriodWorkReferrerHistory {
id: number; id: number;
accompanyingPeriodWork: AccompanyingPeriodWork; accompanyingPeriodWork: AccompanyingPeriodWork;
user: User; user: User;
startDate: DateTime; startDate: DateTime;
endDate: DateTime | null; endDate: DateTime | null;
createdAt: DateTime; createdAt: DateTime;
updatedAt: DateTime | null; updatedAt: DateTime | null;
createdBy: User; createdBy: User;
updatedBy: User | null; updatedBy: User | null;
} }
export interface AccompanyingPeriodWorkEvaluationDocument { export interface AccompanyingPeriodWorkEvaluationDocument {
@ -276,14 +275,16 @@ export type Entities = (UserGroup | User | Person | Thirdparty | Household) & {
profession?: string; profession?: string;
}; };
export type Result = Entities & { export type EntitiesOrMe = "me" | Entities;
export type AddPersonResult = Entities & {
parent?: Entities | null; parent?: Entities | null;
}; };
export interface Suggestion { export interface Suggestion {
key: string; key: string;
relevance: number; relevance: number;
result: Result; result: AddPersonResult;
} }
export interface SearchPagination { export interface SearchPagination {

View File

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