Introduce edit functionality for PersonEdit and refactor OnTheFly components for improved modularity.

- Added support for editing a person entity in `PersonEdit.vue` with proper data initialization and API integration.
- Refactored `OnTheFly.vue` to dynamically render components based on the action (`show`, `edit`, etc.).
- Introduced `personToWritePerson` conversion logic for mapping `Person` to `PersonWrite` structure.
- Enhanced template conditions and loading states for `PersonEdit`.
- Updated API with `editPerson` function and adjusted related types for consistency.
This commit is contained in:
2025-10-29 15:05:08 +01:00
parent 491fd81f9b
commit e107d20bea
5 changed files with 174 additions and 82 deletions

View File

@@ -30,7 +30,7 @@
</h3>
</template>
<template #body v-if="type === 'person' && action !== 'addContact'">
<template #body v-if="type === 'person' && action === 'show'">
<on-the-fly-person
:id="id"
:type="type"
@@ -45,6 +45,15 @@
</div>
</template>
<template #body v-else-if="type === 'person' && action === 'edit'">
<PersonEdit
:id="id"
:action="'edit'"
:query="''"
ref="castEditPerson"
></PersonEdit>
</template>
<template #body v-else-if="type === 'thirdparty'">
<on-the-fly-thirdparty
:id="id"
@@ -80,17 +89,21 @@
<template #footer>
<a
v-if="action === 'show'"
:href="buildLocation(id, type)"
:title="titleMessage"
class="btn btn-show"
>{{ buttonMessage }}
</a>
<a v-else class="btn btn-save" @click="saveAction">
{{ trans(ACTION_SAVE) }}
</a>
</template>
</modal>
</teleport>
</template>
<script setup lang="ts">
import { ref, computed, defineEmits, defineProps } from "vue";
import {ref, computed, defineEmits, defineProps, useTemplateRef} from "vue";
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import OnTheFlyCreate from "./Create.vue";
import OnTheFlyPerson from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue";
@@ -100,6 +113,7 @@ import {
ACTION_SHOW,
ACTION_EDIT,
ACTION_CREATE,
ACTION_SAVE,
ONTHEFLY_CREATE_TITLE_DEFAULT,
ONTHEFLY_CREATE_TITLE_PERSON,
ONTHEFLY_CREATE_TITLE_THIRDPARTY,
@@ -116,6 +130,7 @@ import {
THIRDPARTY_ADDCONTACT,
THIRDPARTY_ADDCONTACT_TITLE,
} from "translator";
import PersonEdit from "ChillPersonAssets/vuejs/_components/OnTheFly/PersonEdit.vue";
// Types
type EntityType = "person" | "thirdparty";
@@ -153,6 +168,9 @@ const emit = defineEmits<{
(e: "saveFormOnTheFly", payload: { type: string | undefined; data: any }): void;
}>();
type castEditPersonType = InstanceType<typeof PersonEdit>;
const castEditPerson = useTemplateRef<castEditPersonType>('castEditPerson')
const modal = ref<{ showModal: boolean; modalDialogClass: string }>({
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl",
@@ -287,6 +305,15 @@ function buildLocation(id: string | number | undefined, type: EntityType | undef
return undefined;
}
async function saveAction() {
if (props.type === "person") {
const person = await castEditPerson.value?.postPerson();
if (null !== person) {
emit("saveFormOnTheFly", {type: props.type, data: person})
}
}
}
defineExpose({
openModal,
closeModal,

View File

@@ -20,15 +20,30 @@ import { StoredObject } from "ChillDocStoreAssets/types";
import { Thirdparty } from "../../../ChillThirdPartyBundle/Resources/public/types";
import { Calendar } from "../../../ChillCalendarBundle/Resources/public/types";
/**
* An alternative name, as configured locally
*/
export interface AltName {
labels: TranslatableString;
key: string;
}
export interface AltNameWrite {
export interface PersonAltNameWrite {
key: string;
value: string;
}
/**
* An altname for a person
*/
export interface PersonAltName {
label: string;
/**
* will match a key in @link{AltName}
*/
key: string;
}
export interface Person {
id: number;
type: "person";
@@ -36,7 +51,7 @@ export interface Person {
textAge: string;
firstName: string;
lastName: string;
altNames: AltName[];
altNames: PersonAltName[];
suffixText: string;
current_household_address: Address | null;
birthdate: DateTime | null;
@@ -54,6 +69,20 @@ export interface Person {
* The person id as configured by the user
*/
personId: string;
identifiers: PersonIdentifier[];
}
export interface PersonIdentifier {
id: number;
type: "person_identifier";
value: object;
definition: PersonIdentifierDefinition;
}
export interface PersonIdentifierDefinition {
id: number;
type: "person_identifier_definition";
engine: string;
}
export interface ResidentialAddress {
@@ -77,8 +106,8 @@ export interface PersonWrite {
type: "person";
firstName: string;
lastName: string;
altNames: AltNameWrite[];
// address: number | null;
altNames: PersonAltNameWrite[];
addressId: number | null;
birthdate: DateTimeWrite | null;
deathdate: DateTimeWrite | null;
phonenumber: string;

View File

@@ -1,8 +1,8 @@
import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { Center, Civility, Gender } from "ChillMainAssets/types";
import {Center, Civility, Gender, SetCenter} from "ChillMainAssets/types";
import {
AltName,
Person,
Person, PersonIdentifier,
PersonIdentifierWorker,
PersonWrite,
} from "ChillPersonAssets/types";
@@ -21,6 +21,27 @@ export const getPerson = async (id: number): Promise<Person> => {
});
};
export const personToWritePerson = (person: Person): PersonWrite => {
return {
type: "person",
firstName: person.firstName,
lastName: person.lastName,
altNames: person.altNames.map((altName) => ({key: altName.key, value: altName.label})),
addressId: null,
birthdate: null === person.birthdate ? null : {datetime: person.birthdate.datetime8601},
deathdate: null === person.deathdate ? null : {datetime: person.deathdate.datetime8601},
phonenumber: person.phonenumber,
mobilenumber: person.mobilenumber,
center: null === person.centers ? null : person.centers
.map((center): SetCenter => ({id: center.id, type: "center"}))
.find(() => true) || null,
email: person.email,
civility: null === person.civility ? null : {id: person.civility.id, type: "chill_main_civility"},
gender: null === person.gender ? null : {id: person.gender.id, type: "chill_main_gender"},
identifiers: person.identifiers.map((identifier: PersonIdentifier) => ({type: "person_identifier", definition_id: identifier.definition.id, value: identifier.value})),
}
}
export const getPersonAltNames = async (): Promise<AltName[]> =>
fetch("/api/1.0/person/config/alt_names.json").then((response) => {
if (response.ok) {
@@ -85,3 +106,11 @@ export const createPerson = async (person: PersonWrite): Promise<Person> => {
person,
);
};
export const editPerson = async (person: PersonWrite, personId: number): Promise<Person> => {
return makeFetch<PersonWrite, Person, WritePersonViolationMap>(
"PATCH",
`/api/1.0/person/person/${personId}.json`,
person,
);
}

View File

@@ -48,22 +48,4 @@ const props = withDefaults(defineProps<Props>(), {query: ""});
const person = ref<Person | null>(null);
function loadData(): void {
if (props.id === undefined || props.id === null) {
return;
}
const idNum = props.id;
if (!Number.isFinite(idNum)) {
return;
}
getPerson(idNum as number).then((p) => {
person.value = p;
});
}
onMounted(() => {
if (props.action !== "create") {
loadData();
}
});
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div v-if="action === 'create' || (action === 'edit' && dataLoaded)">
<div class="mb-3">
<div class="input-group has-validation">
<div class="form-floating">
@@ -70,48 +70,53 @@
</ul>
</div>
<div v-for="(a, i) in config.altNames" :key="a.key" class="mb-3">
<div class="input-group has-validation">
<div class="form-floating">
<input
class="form-control form-control-lg"
:id="a.key"
:name="'label_' + a.key"
value=""
@input="onAltNameInput($event, a.key)"
/>
<label :for="'label_' + a.key">{{ localizeString(a.labels) }}</label>
<template v-if="action === 'create'">
<div v-for="(a, i) in config.altNames" :key="a.key" class="mb-3">
<div class="input-group has-validation">
<div class="form-floating">
<input
class="form-control form-control-lg"
:id="a.key"
:name="'label_' + a.key"
value=""
@input="onAltNameInput($event, a.key)"
/>
<label :for="'label_' + a.key">{{ localizeString(a.labels) }}</label>
</div>
</div>
</div>
</div>
<div
v-for="worker in config.identifiers"
:key="worker.definition_id"
class="mb-3"
>
<div class="input-group has-validation">
<div class="form-floating">
<input
class="form-control form-control-lg"
:class="{'is-invalid': violations.hasViolationWithParameter('identifiers', 'definition_id', worker.definition_id.toString())}"
type="text"
:name="'worker_' + worker.definition_id"
:placeholder="localizeString(worker.label)"
@input="onIdentifierInput($event, worker.definition_id)"
/>
<label :for="'worker_' + worker.definition_id">{{
localizeString(worker.label)
}}</label>
</div>
<div
v-for="err in violations.violationTitlesWithParameter('identifiers', 'definition_id', worker.definition_id.toString())"
class="invalid-feedback was-validated-force"
>
{{ err }}
</template>
<template v-if="action === 'create'">
<div
v-for="worker in config.identifiers"
:key="worker.definition_id"
class="mb-3"
>
<div class="input-group has-validation">
<div class="form-floating">
<input
class="form-control form-control-lg"
:class="{'is-invalid': violations.hasViolationWithParameter('identifiers', 'definition_id', worker.definition_id.toString())}"
type="text"
:name="'worker_' + worker.definition_id"
:placeholder="localizeString(worker.label)"
@input="onIdentifierInput($event, worker.definition_id)"
/>
<label :for="'worker_' + worker.definition_id">{{
localizeString(worker.label)
}}</label>
</div>
<div
v-for="err in violations.violationTitlesWithParameter('identifiers', 'definition_id', worker.definition_id.toString())"
class="invalid-feedback was-validated-force"
>
{{ err }}
</div>
</div>
</div>
</div>
</template>
<div class="mb-3">
<div class="input-group has-validation">
@@ -322,11 +327,9 @@
/>
</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>
<div v-else>
</div>
</template>
@@ -335,13 +338,14 @@ import { ref, reactive, computed, onMounted } from "vue";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
import {
createPerson,
createPerson, editPerson,
getCentersForPersonCreation,
getCivilities,
getGenders,
getPerson,
getPersonAltNames,
getPersonIdentifiers,
personToWritePerson,
WritePersonViolationMap,
} from "../../_api/OnTheFly";
import {
@@ -366,15 +370,12 @@ import {
Center,
Civility,
Gender,
DateTimeWrite, ValidationExceptionInterface,
} from "ChillMainAssets/types";
import {
AltName,
Person,
PersonWrite,
PersonIdentifierWorker,
type Suggestion,
type EntitiesOrMe,
} from "ChillPersonAssets/types";
import {
isValidationException,
@@ -385,14 +386,12 @@ import {useViolationList} from "ChillMainAssets/vuejs/_composables/violationList
interface PersonEditComponentConfig {
id?: number | null;
type?: string;
action: "edit" | "create";
query: string;
}
const props = withDefaults(defineProps<PersonEditComponentConfig>(), {
id: null,
type: "TODO",
});
const emit =
@@ -407,7 +406,7 @@ const person = reactive<PersonWrite>({
firstName: "",
lastName: "",
altNames: [],
// address: null,
addressId: null,
birthdate: null,
deathdate: null,
phonenumber: "",
@@ -435,7 +434,6 @@ const config = reactive<{
const showCenters = ref(false);
const showAddressFormValue = ref(false);
const errors = ref<string[]>([]);
const addAddress = reactive({
options: {
@@ -560,9 +558,27 @@ const queryItems = computed(() => {
.filter((word) => !lastNameWords.includes(word.toLowerCase()));
});
const dataLoaded = ref<boolean>(false);
async function loadData() {
if (props.id !== undefined && props.id !== null) {
const person = await getPerson(props.id);
const p = await getPerson(props.id);
const w = personToWritePerson(p);
person.firstName = w.firstName;
person.lastName = w.lastName;
person.altNames.push(...w.altNames)
person.civility = w.civility;
person.addressId = w.addressId;
person.birthdate = w.birthdate;
person.deathdate = w.deathdate;
person.phonenumber = w.phonenumber;
person.mobilenumber = w.mobilenumber;
person.email = w.email;
person.gender = w.gender;
person.center = w.center;
person.civility = w.civility;
person.identifiers.push(...w.identifiers);
dataLoaded.value = true;
}
}
@@ -612,14 +628,22 @@ function addQueryItem(field: "lastName" | "firstName", queryItem: string) {
const violations = useViolationList<WritePersonViolationMap>();
function submitNewAddress(payload: { addressId: number }) {
// person.addressId = payload.addressId;
person.addressId = payload.addressId;
}
async function postPerson(): Promise<void> {
async function postPerson(): Promise<Person> {
try {
const createdPerson = await createPerson(person);
if (props.action === 'create') {
const createdPerson = await createPerson(person);
emit("onPersonCreated", { person: createdPerson });
emit("onPersonCreated", { person: createdPerson });
return createdPerson;
} else if (props.id !== null) {
const updatedPerson = await editPerson(person, props.id);
emit("onPersonCreated", { person: updatedPerson });
return updatedPerson;
}
} catch (e: unknown) {
if (isValidationException<WritePersonViolationMap>(e)) {
violations.setValidationException(e);
@@ -627,6 +651,7 @@ async function postPerson(): Promise<void> {
toast.error(trans(PERSON_EDIT_ERROR_WHILE_SAVING));
}
}
throw "impossible case might happen...";
}
onMounted(() => {