Refactor person creation workflow: Introduce PersonEdit component and integrate it across Create, Person.vue, and modals for improved modularity. Update type definitions and API methods for consistency.

This commit is contained in:
2025-09-12 23:51:52 +02:00
parent 1c0ed9abc8
commit c05d0aad47
12 changed files with 692 additions and 624 deletions

View File

@@ -1,15 +1,35 @@
import {GenericDoc} from "ChillDocStoreAssets/types/generic_doc";
import {StoredObject, StoredObjectStatus} from "ChillDocStoreAssets/types";
import {CreatableEntityType} from "ChillPersonAssets/types";
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
import { CreatableEntityType } from "ChillPersonAssets/types";
export interface DateTime {
datetime: string;
datetime8601: string;
}
/**
* A date representation to use when we create an instance
*/
export interface DateTimeCreate {
/**
* Must be a string in format Y-m-d\TH:i:sO
*/
datetime: string;
}
export interface Civility {
type: "chill_main_civility";
id: number;
// TODO
abbreviation: TranslatableString;
active: boolean;
name: TranslatableString;
}
export interface Gender {
type: "chill_main_gender";
id: number;
label: string;
genderTranslation: string;
}
export interface Household {
@@ -306,7 +326,7 @@ export interface TabDefinition {
* Configuration for the CreateModal and Create component
*/
export interface CreateComponentConfig {
action?: string;
allowedTypes: CreatableEntityType[];
query?: string;
action?: string;
allowedTypes: CreatableEntityType[];
query?: string;
}

View File

@@ -1,6 +1,6 @@
<template>
<ul class="nav nav-tabs">
<li v-if="allowedTypes.includes('person')" class="nav-item">
<li v-if="containsPerson" class="nav-item">
<a class="nav-link" :class="{ active: isActive('person') }">
<label for="person">
<input
@@ -14,7 +14,7 @@
</label>
</a>
</li>
<li v-if="allowedTypes.includes('thirdparty')" class="nav-item">
<li v-if="containsThirdParty" class="nav-item">
<a class="nav-link" :class="{ active: isActive('thirdparty') }">
<label for="thirdparty">
<input
@@ -31,9 +31,9 @@
</ul>
<div class="my-4">
<on-the-fly-person
<PersonEdit
v-if="type === 'person'"
:action="action"
action="create"
:query="query"
ref="castPerson"
/>
@@ -47,17 +47,26 @@
</div>
</template>
<script setup lang="ts">
import {computed, onMounted, ref} from "vue";
import { computed, onMounted, ref } from "vue";
import OnTheFlyPerson from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue";
import OnTheFlyThirdparty from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdParty.vue";
import {ONTHEFLY_CREATE_PERSON, ONTHEFLY_CREATE_THIRDPARTY, trans,} from "translator";
import {CreatableEntityType} from "ChillPersonAssets/types";
import {CreateComponentConfig} from "ChillMainAssets/types";
import {
ONTHEFLY_CREATE_PERSON,
ONTHEFLY_CREATE_THIRDPARTY,
trans,
} from "translator";
import { CreatableEntityType } from "ChillPersonAssets/types";
import { CreateComponentConfig } from "ChillMainAssets/types";
import PersonEdit from "ChillPersonAssets/vuejs/_components/OnTheFly/PersonEdit.vue";
const props = defineProps<CreateComponentConfig>();
const type = ref<CreatableEntityType| null>(null);
const props = withDefaults(defineProps<CreateComponentConfig>(), {
allowedTypes: ["person", "thirdparty"],
action: "create",
query: "",
});
const type = ref<CreatableEntityType | null>(null);
const radioType = computed<CreatableEntityType| null>({
const radioType = computed<CreatableEntityType | null>({
get: () => type.value,
set: (val: CreatableEntityType | null) => {
type.value = val;
@@ -65,7 +74,10 @@ const radioType = computed<CreatableEntityType| null>({
},
});
type AnyComponentInstance = InstanceType<typeof OnTheFlyPerson> | InstanceType<typeof OnTheFlyThirdparty> | null;
type AnyComponentInstance =
| InstanceType<typeof OnTheFlyPerson>
| InstanceType<typeof OnTheFlyThirdparty>
| null;
const castPerson = ref<AnyComponentInstance>(null);
const castThirdparty = ref<AnyComponentInstance>(null);
@@ -81,6 +93,14 @@ function isActive(tab: CreatableEntityType) {
return type.value === tab;
}
const containsThirdParty = computed<boolean>(() =>
props.allowedTypes.includes("thirdparty"),
);
const containsPerson = computed<boolean>(() => {
console.log(props.allowedTypes);
return props.allowedTypes.includes("person");
});
// Types for data structures coming from child components are not declared in TS yet.
// We conservatively type them as any to preserve runtime behavior while enabling TS in this component.
function castDataByType(): any {

View File

@@ -1,9 +1,12 @@
<script setup lang="ts">
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import Create from "ChillMainAssets/vuejs/OnTheFly/components/Create.vue";
import {CreateComponentConfig} from "ChillMainAssets/types";
import { CreateComponentConfig } from "ChillMainAssets/types";
const emit = defineEmits<(e: "close") => void>();
const props = defineProps<CreateComponentConfig>();
const modalDialogClass = { "modal-xl": true, "modal-scrollable": true };
</script>
<template>
@@ -18,15 +21,15 @@ const props = defineProps<CreateComponentConfig>();
</template>
<template #body-head>
<div class="modal-body">
<Create :allowed-types="props.allowed-types" :action="props.action" :query="props.query"></Create>
<Create
:allowedTypes="props.allowedTypes"
:action="props.action"
:query="props.query"
></Create>
</div>
</template>
</modal>
</teleport>
</modal>
</teleport>
</template>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -76,7 +76,8 @@ import {
Entities,
EntitiesOrMe,
EntityType,
SearchOptions, Suggestion,
SearchOptions,
Suggestion,
} from "ChillPersonAssets/types";
import {
PICK_ENTITY_MODAL_TITLE,
@@ -183,7 +184,7 @@ function addNewSuggested(entity: EntitiesOrMe) {
emits("addNewEntity", { entity });
}
function addNewEntity({ selected }: { selected: Suggestion[]}) {
function addNewEntity({ selected }: { selected: Suggestion[] }) {
Object.values(selected).forEach((item) => {
emits("addNewEntity", { entity: item.result });
});

View File

@@ -52,23 +52,15 @@ import { trans, MODAL_ACTION_CLOSE } from "translator";
import { defineProps } from "vue";
export interface ModalProps {
modalDialogClass: string;
hideFooter: boolean;
modalDialogClass?: string | Record<string, boolean>;
hideFooter?: boolean;
show?: boolean;
}
defineProps({
modalDialogClass: {
type: String,
default: "",
},
hideFooter: {
type: Boolean,
default: false,
},
show: {
type: Boolean,
default: true,
},
const props = withDefaults(defineProps<ModalProps>(), {
modalDialogClass: "",
hideFooter: false,
show: true,
});
const emits = defineEmits<{

View File

@@ -10,6 +10,7 @@ import {
Scope,
Job,
PrivateCommentEmbeddable,
TranslatableString,
} from "ChillMainAssets/types";
import { StoredObject } from "ChillDocStoreAssets/types";
import { Thirdparty } from "../../../ChillThirdPartyBundle/Resources/public/types";
@@ -17,7 +18,7 @@ import { Calendar } from "../../../ChillCalendarBundle/Resources/public/types";
import Person from "./vuejs/_components/OnTheFly/Person.vue";
export interface AltName {
label: string;
label: TranslatableString;
key: string;
}
export interface Person {
@@ -331,15 +332,13 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
/**
* Entity types that a user can create
*/
export type CreatableEntityType =
| "person"
| "thirdparty";
export type CreatableEntityType = "person" | "thirdparty";
/**
* Entities that can be search and selected by a user
*/
export type EntityType = CreatableEntityType
export type EntityType =
| CreatableEntityType
| "user_group"
| "user"
| "household";

View File

@@ -1,88 +0,0 @@
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
/*
* GET a person by id
*/
const getPerson = (id) => {
const url = `/api/1.0/person/person/${id}.json`;
return fetch(url).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
const getPersonAltNames = () =>
fetch("/api/1.0/person/config/alt_names.json").then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
const getCivilities = () =>
fetch("/api/1.0/main/civility.json").then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
const getGenders = () => makeFetch("GET", "/api/1.0/main/gender.json");
// .then(response => {
// console.log(response)
// if (response.ok) { return response.json(); }
// throw Error('Error with request resource response');
// });
const getCentersForPersonCreation = () =>
makeFetch("GET", "/api/1.0/person/creation/authorized-centers", null);
/*
* POST a new person
*/
const postPerson = (body) => {
const url = `/api/1.0/person/person.json`;
return fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify(body),
}).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
/*
* PATCH an existing person
*/
const patchPerson = (id, body) => {
const url = `/api/1.0/person/person/${id}.json`;
return fetch(url, {
method: "PATCH",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify(body),
}).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
export {
getCentersForPersonCreation,
getPerson,
getPersonAltNames,
getCivilities,
getGenders,
postPerson,
patchPerson,
};

View File

@@ -0,0 +1,33 @@
import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { Center, Civility, Gender } from "ChillMainAssets/types";
import { AltName, Person } from "ChillPersonAssets/types";
/*
* GET a person by id
*/
export const getPerson = async (id: number): Promise<Person> => {
const url = `/api/1.0/person/person/${id}.json`;
return fetch(url).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
export const getPersonAltNames = async (): Promise<AltName[]> =>
fetch("/api/1.0/person/config/alt_names.json").then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
export const getCivilities = async (): Promise<Civility[]> =>
fetchResults("/api/1.0/main/civility.json");
export const getGenders = async (): Promise<Gender[]> =>
fetchResults("/api/1.0/main/gender.json");
export const getCentersForPersonCreation = async (): Promise<Center[]> =>
makeFetch("GET", "/api/1.0/person/creation/authorized-centers", null);

View File

@@ -3,7 +3,7 @@
class="btn"
:class="getClassButton"
:title="buttonTitle"
@click="openModal"
@click="openModalChoose"
>
<span v-if="displayTextButton">{{ buttonTitle }}</span>
</a>
@@ -16,22 +16,28 @@
:selected="selected"
:modal-dialog-class="'modal-dialog-scrollable modal-xl'"
:allow-create="props.allowCreate"
@close="closeModal"
@addNewPersons="payload => emit('addNewPersons', payload)"
@close="closeModalChoose"
@addNewPersons="(payload) => emit('addNewPersons', payload)"
@onAskForCreate="onAskForCreate"
/>
<CreateModal
v-if="creatableEntityTypes.length > 0 && showModalCreate"
:allowed-types="creatableEntityTypes"
></CreateModal>
@close="closeModalCreate"
></CreateModal>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import PersonChooseModal from './AddPersons/PersonChooseModal.vue';
import type {Suggestion, SearchOptions, CreatableEntityType, EntityType} from 'ChillPersonAssets/types';
import {marked} from "marked";
import { ref, computed } from "vue";
import PersonChooseModal from "./AddPersons/PersonChooseModal.vue";
import type {
Suggestion,
SearchOptions,
CreatableEntityType,
EntityType,
} from "ChillPersonAssets/types";
import { marked } from "marked";
import options = marked.options;
import CreateModal from "ChillMainAssets/vuejs/OnTheFly/components/CreateModal.vue";
@@ -42,7 +48,7 @@ interface AddPersonsConfig {
modalTitle: string;
options: SearchOptions;
allowCreate?: boolean;
types?: EntityType|undefined;
types?: EntityType | undefined;
}
const props = withDefaults(defineProps<AddPersonsConfig>(), {
@@ -52,43 +58,54 @@ const props = withDefaults(defineProps<AddPersonsConfig>(), {
types: () => undefined,
});
const emit = defineEmits<{
(e: 'addNewPersons', payload: { selected: Suggestion[] }): void;
}>();
const emit =
defineEmits<
(e: "addNewPersons", payload: { selected: Suggestion[] }) => void
>();
const showModalChoose = ref(false);
const showModalCreate = ref(false);
const getClassButton = computed(() => {
const size = props.options?.button?.size ?? '';
const type = props.options?.button?.type ?? 'btn-create';
const size = props.options?.button?.size ?? "";
const 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 creatableEntityTypes = computed<CreatableEntityType[]>(() => {
if (typeof props.options.type !== 'undefined') {
return props.options.type.filter((e: EntityType) => e === 'thirdparty' || e === 'person');
if (typeof props.options.type !== "undefined") {
return props.options.type.filter(
(e: EntityType) => e === "thirdparty" || e === "person",
);
}
return props.type.filter((e: EntityType) => e === 'thirdparty' || e === 'person');
})
return props.types.filter(
(e: EntityType) => e === "thirdparty" || e === "person",
);
});
function onAskForCreate({query}: {query: string}) {
console.log('onAskForCreate', query);
function onAskForCreate({ query }: { query: string }) {
console.log("onAskForCreate", query);
showModalChoose.value = false;
showModalCreate.value = true;
}
function openModal() {
function openModalChoose() {
showModalChoose.value = true;
}
function closeModal() {
function closeModalChoose() {
showModalChoose.value = false;
}
function closeModalCreate() {
showModalCreate.value = false;
}
</script>
<style lang="scss" scoped>

View File

@@ -14,7 +14,9 @@
<div class="search">
<label class="col-form-label" style="float: right">
{{
trans(ADD_PERSONS_SUGGESTED_COUNTER, { count: suggestedCounter })
trans(ADD_PERSONS_SUGGESTED_COUNTER, {
count: suggestedCounter,
})
}}
</label>
@@ -26,7 +28,11 @@
ref="searchRef"
/>
<i class="fa fa-search fa-lg" />
<i class="fa fa-times" v-if="queryLength >= 3" @click="resetSuggestion" />
<i
class="fa fa-times"
v-if="queryLength >= 3"
@click="resetSuggestion"
/>
</div>
</div>
@@ -42,7 +48,9 @@
</a>
</span>
<span v-if="selectedCounter > 0">
{{ trans(ADD_PERSONS_SELECTED_COUNTER, { count: selectedCounter }) }}
{{
trans(ADD_PERSONS_SELECTED_COUNTER, { count: selectedCounter })
}}
</span>
</div>
</div>
@@ -60,11 +68,11 @@
@update-selected="updateSelected"
/>
<div v-if="props.allowCreate && query.length > 0" class="create-button">
<button
type="button"
@click="emit('onAskForCreate', {query })"
>
<div
v-if="props.allowCreate && query.length > 0"
class="create-button"
>
<button type="button" @click="emit('onAskForCreate', { query })">
{{ trans(ONTHEFLY_CREATE_BUTTON, { q: query }) }}
</button>
<!--
@@ -100,12 +108,12 @@
</template>
<script setup lang="ts">
import {ref, reactive, computed, nextTick, watch, onMounted} from 'vue';
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
import PersonSuggestion from './PersonSuggestion.vue';
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import { searchEntities } from 'ChillPersonAssets/vuejs/_api/AddPersons';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
import { ref, reactive, computed, nextTick, watch, onMounted } from "vue";
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import PersonSuggestion from "./PersonSuggestion.vue";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import { searchEntities } from "ChillPersonAssets/vuejs/_api/AddPersons";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import {
trans,
@@ -116,14 +124,15 @@ import {
ACTION_CHECK_ALL,
ACTION_RESET,
ACTION_ADD,
} from 'translator';
} from "translator";
import type {
Suggestion,
Search,
AddPersonResult as OriginalResult,
SearchOptions, EntitiesOrMe,
} from 'ChillPersonAssets/types';
SearchOptions,
EntitiesOrMe,
} from "ChillPersonAssets/types";
type Result = OriginalResult & { addressId?: number };
@@ -139,16 +148,16 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
suggested: () => [],
selected: () => [],
modalDialogClass: 'modal-dialog-scrollable modal-xl',
modalDialogClass: "modal-dialog-scrollable modal-xl",
allowCreate: () => true,
});
const emit = defineEmits<{
(e: 'close'): void;
(e: "close"): void;
/** @deprecated use 'onPickEntities' */
(e: 'addNewPersons', payload: { selected: Suggestion[] }): void;
(e: 'onPickEntities', payload: { selected: EntitiesOrMe[] }): void;
(e: 'onAskForCreate', payload: { query: string }): void;
(e: "addNewPersons", payload: { selected: Suggestion[] }): void;
(e: "onPickEntities", payload: { selected: EntitiesOrMe[] }): void;
(e: "onAskForCreate", payload: { query: string }): void;
}>();
const searchRef = ref<HTMLInputElement | null>(null);
@@ -160,8 +169,8 @@ onMounted(() => {
});
const search = reactive({
query: '' as string,
previousQuery: '' as string,
query: "" as string,
previousQuery: "" as string,
currentSearchQueryController: null as AbortController | null,
suggested: (props.suggested ?? []) as Suggestion[],
selected: (props.selected ?? []) as Suggestion[],
@@ -173,7 +182,7 @@ watch(
(newSelected) => {
search.selected = newSelected ? [...newSelected] : [];
},
{ deep: true }
{ deep: true },
);
watch(
@@ -181,7 +190,7 @@ watch(
(newSuggested) => {
search.suggested = newSuggested ? [...newSuggested] : [];
},
{ deep: true }
{ deep: true },
);
const query = computed({
@@ -193,7 +202,9 @@ const suggestedCounter = computed(() => search.suggested.length);
const selectedComputed = computed<Suggestion[]>(() => search.selected);
const selectedCounter = computed(() => search.selected.length);
const checkUniq = computed(() => (props.options.uniq === true ? 'radio' : 'checkbox'));
const checkUniq = computed(() =>
props.options.uniq === true ? "radio" : "checkbox",
);
const priorSuggestion = computed(() => search.priorSuggestion);
const hasPriorSuggestion = computed(() => !!search.priorSuggestion.key);
@@ -230,7 +241,7 @@ function setQuery(q: string) {
search.currentSearchQueryController = null;
}
if (q === '') {
if (q === "") {
loadSuggestions([]);
return;
}
@@ -250,7 +261,7 @@ function setQuery(q: string) {
loadSuggestions(suggested.results);
})
.catch((error: DOMException) => {
if (error instanceof DOMException && error.name === 'AbortError') {
if (error instanceof DOMException && error.name === "AbortError") {
return;
}
throw error;
@@ -270,7 +281,7 @@ function updateSelected(value: Suggestion[]) {
}
function resetSuggestion() {
search.query = '';
search.query = "";
search.suggested = [];
}
@@ -306,10 +317,12 @@ function newPriorSuggestion(entity: Result | null) {
* Triggered when the user clicks on the "add" button.
*/
function pickEntities(): void {
emit('addNewPersons', { selected: search.selected });
emit('onPickEntities', {selected: search.selected.map((s: Suggestion) => s.result )})
search.query = '';
emit('close');
emit("addNewPersons", { selected: search.selected });
emit("onPickEntities", {
selected: search.selected.map((s: Suggestion) => s.result),
});
search.query = "";
emit("close");
}
/*

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="action === 'show'">
<div v-if="action === 'show' && person !== null">
<div class="flex-table">
<person-render-box
render="bloc"
@@ -22,445 +22,48 @@
</div>
<div v-else-if="action === 'edit' || action === 'create'">
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="lastname"
v-model="lastName"
:placeholder="trans(PERSON_MESSAGES_PERSON_LASTNAME)"
@change="checkErrors"
/>
<label for="lastname">{{ trans(PERSON_MESSAGES_PERSON_LASTNAME) }}</label>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li
v-for="(qi, i) in queryItems"
:key="i"
@click="addQueryItem('lastName', qi)"
>
<span class="person-text">{{ qi }}</span>
</li>
</ul>
</div>
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="firstname"
v-model="firstName"
:placeholder="trans(PERSON_MESSAGES_PERSON_FIRSTNAME)"
@change="checkErrors"
/>
<label for="firstname">{{
trans(PERSON_MESSAGES_PERSON_FIRSTNAME)
}}</label>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li
v-for="(qi, i) in queryItems"
:key="i"
@click="addQueryItem('firstName', qi)"
>
<span class="person-text">{{ qi }}</span>
</li>
</ul>
</div>
<div
v-for="(a, i) in config.altNames"
:key="a.key"
class="form-floating mb-3"
>
<input
class="form-control form-control-lg"
:id="a.key"
:value="personAltNamesLabels[i]"
@input="onAltNameInput"
/>
<label :for="a.key">{{ localizeString(a.labels) }}</label>
</div>
<!-- TODO fix placeholder if undefined
-->
<div class="form-floating mb-3">
<select class="form-select form-select-lg" id="gender" v-model="gender">
<option selected disabled>
{{ trans(PERSON_MESSAGES_PERSON_GENDER_PLACEHOLDER) }}
</option>
<option v-for="g in config.genders" :value="g.id" :key="g.id">
{{ g.label }}
</option>
</select>
<label>{{ trans(PERSON_MESSAGES_PERSON_GENDER_TITLE) }}</label>
</div>
<div
class="form-floating mb-3"
v-if="showCenters && config.centers.length > 1"
>
<select class="form-select form-select-lg" id="center" v-model="center">
<option selected disabled>
{{ trans(PERSON_MESSAGES_PERSON_CENTER_PLACEHOLDER) }}
</option>
<option v-for="c in config.centers" :value="c" :key="c.id">
{{ c.name }}
</option>
</select>
<label>{{ trans(PERSON_MESSAGES_PERSON_CENTER_TITLE) }}</label>
</div>
<div class="form-floating mb-3">
<select
class="form-select form-select-lg"
id="civility"
v-model="civility"
>
<option selected disabled>
{{ trans(PERSON_MESSAGES_PERSON_CIVILITY_PLACEHOLDER) }}
</option>
<option v-for="c in config.civilities" :value="c.id" :key="c.id">
{{ localizeString(c.name) }}
</option>
</select>
<label>{{ trans(PERSON_MESSAGES_PERSON_CIVILITY_TITLE) }}</label>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="phonenumber">
<i class="fa fa-fw fa-phone"></i>
</span>
<input
class="form-control form-control-lg"
v-model="phonenumber"
:placeholder="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
:aria-label="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
aria-describedby="phonenumber"
/>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="mobilenumber">
<i class="fa fa-fw fa-mobile"></i>
</span>
<input
class="form-control form-control-lg"
v-model="mobilenumber"
:placeholder="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
:aria-label="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
aria-describedby="mobilenumber"
/>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="email">
<i class="fa fa-fw fa-at"></i>
</span>
<input
class="form-control form-control-lg"
v-model="email"
:placeholder="trans(PERSON_MESSAGES_PERSON_EMAIL)"
:aria-label="trans(PERSON_MESSAGES_PERSON_EMAIL)"
aria-describedby="email"
/>
</div>
<div v-if="action === 'create'" class="input-group mb-3 form-check">
<input
class="form-check-input"
type="checkbox"
v-model="showAddressForm"
name="showAddressForm"
/>
<label class="form-check-label">
{{ trans(PERSON_MESSAGES_PERSON_ADDRESS_SHOW_ADDRESS_FORM) }}
</label>
</div>
<div
v-if="action === 'create' && showAddressFormValue"
class="form-floating mb-3"
>
<p>{{ trans(PERSON_MESSAGES_PERSON_ADDRESS_WARNING) }}</p>
<AddAddress
:context="addAddress.context"
:options="addAddress.options"
:addressChangedCallback="submitNewAddress"
ref="addAddress"
/>
</div>
<div class="alert alert-warning" v-if="errors.length">
<ul>
<li v-for="(e, i) in errors" :key="i">{{ e }}</li>
</ul>
</div>
<PersonEdit
:id="props.id"
:type="props.type"
:action="props.action"
:query="props.query"
/>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from "vue";
import {
getCentersForPersonCreation,
getCivilities,
getGenders,
getPerson,
getPersonAltNames,
} from "../../_api/OnTheFly";
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { getPerson } from "../../_api/OnTheFly";
import PersonRenderBox from "../Entity/PersonRenderBox.vue";
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
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";
import PersonEdit from "./PersonEdit.vue";
import type { Person } from "ChillPersonAssets/types";
const props = defineProps({
id: [String, Number],
type: String,
action: String,
query: String,
});
const person = reactive({
type: "person",
lastName: "",
firstName: "",
altNames: [],
addressId: null,
center: null,
gender: null,
civility: null,
birthdate: null,
phonenumber: "",
mobilenumber: "",
email: "",
});
const config = reactive({
altNames: [],
civilities: [],
centers: [],
genders: [],
});
const showCenters = ref(false);
const showAddressFormValue = ref(false);
const errors = ref([]);
const addAddress = reactive({
options: {
button: {
text: { create: "person.address.create_address" },
size: "btn-sm",
},
title: { create: "person.address.create_address" },
},
context: {
target: {},
edit: false,
addressId: null,
defaults: window.addaddress,
},
});
const firstName = computed({
get: () => person.firstName,
set: (value) => {
person.firstName = value;
},
});
const lastName = computed({
get: () => person.lastName,
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 {
person.birthdate = { datetime: value + "T00:00:00+0100" };
}
},
});
const phonenumber = computed({
get: () => person.phonenumber,
set: (value) => {
person.phonenumber = value;
},
});
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 };
},
});
const genderClass = computed(() => {
switch (person.gender && person.gender.id) {
case "woman":
return "fa-venus";
case "man":
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é");
}
interface Props {
id: string | number;
type?: string;
action: "show" | "edit" | "create";
query?: string;
}
function loadData() {
getPerson(props.id).then((p) => {
Object.assign(person, p);
const props = defineProps<Props>();
const person = ref<Person | null>(null);
function loadData(): void {
if (props.id === undefined || props.id === null) {
return;
}
const idNum = typeof props.id === "string" ? Number(props.id) : props.id;
if (!Number.isFinite(idNum)) {
return;
}
getPerson(idNum as number).then((p) => {
person.value = 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];
}
});
}
});
defineExpose(genderClass, genderTranslation, feminized, birthDate);
</script>

View File

@@ -0,0 +1,455 @@
<template>
<div>
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="lastname"
v-model="lastName"
:placeholder="trans(PERSON_MESSAGES_PERSON_LASTNAME)"
@change="checkErrors"
/>
<label for="lastname">{{ trans(PERSON_MESSAGES_PERSON_LASTNAME) }}</label>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li
v-for="(qi, i) in queryItems"
:key="i"
@click="addQueryItem('lastName', qi)"
>
<span class="person-text">{{ qi }}</span>
</li>
</ul>
</div>
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="firstname"
v-model="firstName"
:placeholder="trans(PERSON_MESSAGES_PERSON_FIRSTNAME)"
@change="checkErrors"
/>
<label for="firstname">{{
trans(PERSON_MESSAGES_PERSON_FIRSTNAME)
}}</label>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li
v-for="(qi, i) in queryItems"
:key="i"
@click="addQueryItem('firstName', qi)"
>
<span class="person-text">{{ qi }}</span>
</li>
</ul>
</div>
<div
v-for="(a, i) in config.altNames"
:key="a.key"
class="form-floating mb-3"
>
<input
class="form-control form-control-lg"
:id="a.key"
:value="personAltNamesLabels[i]"
@input="onAltNameInput"
/>
<label :for="a.key">{{ localizeString(a.labels) }}</label>
</div>
<div class="form-floating mb-3">
<select class="form-select form-select-lg" id="gender" v-model="gender">
<option selected disabled>
{{ trans(PERSON_MESSAGES_PERSON_GENDER_PLACEHOLDER) }}
</option>
<option v-for="g in config.genders" :value="g.id" :key="g.id">
{{ g.label }}
</option>
</select>
<label>{{ trans(PERSON_MESSAGES_PERSON_GENDER_TITLE) }}</label>
</div>
<div
class="form-floating mb-3"
v-if="showCenters && config.centers.length > 1"
>
<select class="form-select form-select-lg" id="center" v-model="center">
<option selected disabled>
{{ trans(PERSON_MESSAGES_PERSON_CENTER_PLACEHOLDER) }}
</option>
<option v-for="c in config.centers" :value="c" :key="c.id">
{{ c.name }}
</option>
</select>
<label>{{ trans(PERSON_MESSAGES_PERSON_CENTER_TITLE) }}</label>
</div>
<div class="form-floating mb-3">
<select
class="form-select form-select-lg"
id="civility"
v-model="civility"
>
<option selected disabled>
{{ trans(PERSON_MESSAGES_PERSON_CIVILITY_PLACEHOLDER) }}
</option>
<option v-for="c in config.civilities" :value="c.id" :key="c.id">
{{ localizeString(c.name) }}
</option>
</select>
<label>{{ trans(PERSON_MESSAGES_PERSON_CIVILITY_TITLE) }}</label>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="phonenumber">
<i class="fa fa-fw fa-phone"></i>
</span>
<input
class="form-control form-control-lg"
v-model="phonenumber"
:placeholder="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
:aria-label="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
aria-describedby="phonenumber"
/>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="mobilenumber">
<i class="fa fa-fw fa-mobile"></i>
</span>
<input
class="form-control form-control-lg"
v-model="mobilenumber"
:placeholder="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
:aria-label="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
aria-describedby="mobilenumber"
/>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="email">
<i class="fa fa-fw fa-at"></i>
</span>
<input
class="form-control form-control-lg"
v-model="email"
:placeholder="trans(PERSON_MESSAGES_PERSON_EMAIL)"
:aria-label="trans(PERSON_MESSAGES_PERSON_EMAIL)"
aria-describedby="email"
/>
</div>
<div v-if="action === 'create'" class="input-group mb-3 form-check">
<input
class="form-check-input"
type="checkbox"
v-model="showAddressForm"
name="showAddressForm"
/>
<label class="form-check-label">
{{ trans(PERSON_MESSAGES_PERSON_ADDRESS_SHOW_ADDRESS_FORM) }}
</label>
</div>
<div
v-if="action === 'create' && showAddressFormValue"
class="form-floating mb-3"
>
<p>{{ trans(PERSON_MESSAGES_PERSON_ADDRESS_WARNING) }}</p>
<AddAddress
:context="addAddress.context"
:options="addAddress.options"
:addressChangedCallback="submitNewAddress"
ref="addAddress"
/>
</div>
<div class="alert alert-warning" v-if="errors.length">
<ul>
<li v-for="(e, i) in errors" :key="i">{{ e }}</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from "vue";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
import {
getCentersForPersonCreation,
getCivilities,
getGenders,
getPerson,
getPersonAltNames,
} from "../../_api/OnTheFly";
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";
import {
Center,
Civility,
Gender,
DateTimeCreate,
} from "ChillMainAssets/types";
import { AltName } from "ChillPersonAssets/types";
interface PersonState {
type: "person";
lastName: string;
firstName: string;
altNames: { key: string; label: string }[];
addressId: number | null;
center: { id: number; type: string } | null;
gender: { id: string; type: string } | null;
civility: { id: number; type: string } | null;
birthdate: DateTimeCreate | null;
phonenumber: string;
mobilenumber: string;
email: string;
}
interface PersonEditComponentConfig {
id?: number | null;
type?: string;
action: "edit" | "create";
query: string;
}
const props = withDefaults(defineProps<PersonEditComponentConfig>(), {
id: null,
type: "TODO",
});
const person = reactive<PersonState>({
type: "person",
lastName: "",
firstName: "",
altNames: [],
addressId: null,
center: null,
gender: null,
civility: null,
birthdate: null,
phonenumber: "",
mobilenumber: "",
email: "",
});
const config = reactive<{
altNames: AltName[];
civilities: Civility[];
centers: Center[];
genders: Gender[];
}>({
altNames: [],
civilities: [],
centers: [],
genders: [],
});
const showCenters = ref(false);
const showAddressFormValue = ref(false);
const errors = ref<string[]>([]);
const addAddress = reactive({
options: {
button: {
text: { create: "person.address.create_address" },
size: "btn-sm",
},
title: { create: "person.address.create_address" },
},
context: {
target: {},
edit: false,
addressId: null as number | null,
defaults: (window as any).addaddress,
},
});
const firstName = computed({
get: () => person.firstName,
set: (value: string) => {
person.firstName = value;
},
});
const lastName = computed({
get: () => person.lastName,
set: (value: string) => {
person.lastName = value;
},
});
const gender = computed({
get: () => (person.gender ? person.gender.id : null),
set: (value: string | null) => {
person.gender = value ? { id: value, type: "chill_main_gender" } : null;
},
});
const civility = computed({
get: () => (person.civility ? person.civility.id : null),
set: (value: number | null) => {
person.civility =
value !== null ? { id: value, type: "chill_main_civility" } : null;
},
});
const birthDate = computed({
get: () => (person.birthdate ? person.birthdate.datetime.split("T")[0] : ""),
set: (value: string) => {
if (person.birthdate) {
person.birthdate.datetime = value + "T00:00:00+0100";
} else {
person.birthdate = { datetime: value + "T00:00:00+0100" };
}
},
});
const phonenumber = computed({
get: () => person.phonenumber,
set: (value: string) => {
person.phonenumber = value;
},
});
const mobilenumber = computed({
get: () => person.mobilenumber,
set: (value: string) => {
person.mobilenumber = value;
},
});
const email = computed({
get: () => person.email,
set: (value: string) => {
person.email = value;
},
});
const showAddressForm = computed({
get: () => showAddressFormValue.value,
set: (value: boolean) => {
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: Center | null) => {
if (value) {
person.center = {
id: value.id,
type: (value as any).type ?? "chill_main_center",
};
} else {
person.center = null;
}
},
});
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() {
if (props.id !== undefined && props.id !== null) {
getPerson(props.id as any).then((p: any) => {
Object.assign(person, p);
});
}
}
function onAltNameInput(event: Event) {
const target = event.target as HTMLInputElement;
const key = target.id;
const label = target.value;
const updateAltNames = person.altNames.filter((a) => a.key !== key);
updateAltNames.push({ key, label });
person.altNames = updateAltNames;
}
function addQueryItem(field: "lastName" | "firstName", queryItem: string) {
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: { addressId: number }) {
person.addressId = payload.addressId;
}
onMounted(() => {
getPersonAltNames().then((altNames: any) => {
config.altNames = altNames;
});
getCivilities().then((civilities: any) => {
if ("results" in civilities) {
config.civilities = civilities.results;
}
});
getGenders().then((genders: any) => {
if ("results" in genders) {
config.genders = genders.results;
}
});
if (props.action !== "create") {
loadData();
} else {
getCentersForPersonCreation().then((params: any) => {
config.centers = params.centers.filter((c: any) => c.isActive);
showCenters.value = params.showCenters;
if (showCenters.value && config.centers.length === 1) {
// if there is only one center, preselect it
person.center = {
id: config.centers[0].id,
type: (config.centers[0] as any).type ?? "chill_main_center",
};
}
});
}
});
</script>