Add support for person identifiers workflow: update PersonEdit component, API methods, and modals for identifier handling during person creation. Adjust related types for improved consistency.

This commit is contained in:
2025-09-16 10:00:29 +02:00
parent bec7297039
commit 27548ad654
6 changed files with 132 additions and 48 deletions

View File

@@ -64,6 +64,8 @@ const props = withDefaults(defineProps<CreateComponentConfig>(), {
action: "create", action: "create",
query: "", query: "",
}); });
const type = ref<CreatableEntityType | null>(null); const type = ref<CreatableEntityType | null>(null);
const radioType = computed<CreatableEntityType | null>({ const radioType = computed<CreatableEntityType | null>({
@@ -74,12 +76,14 @@ const radioType = computed<CreatableEntityType | null>({
}, },
}); });
type PersonEditComponent = InstanceType<typeof PersonEdit>;
type AnyComponentInstance = type AnyComponentInstance =
| InstanceType<typeof OnTheFlyPerson> | InstanceType<typeof OnTheFlyPerson>
| InstanceType<typeof OnTheFlyThirdparty> | InstanceType<typeof OnTheFlyThirdparty>
| null; | null;
const castPerson = ref<AnyComponentInstance>(null); const castPerson = ref<PersonEditComponent>(null);
const castThirdparty = ref<AnyComponentInstance>(null); const castThirdparty = ref<AnyComponentInstance>(null);
onMounted(() => { onMounted(() => {
@@ -100,31 +104,13 @@ const containsPerson = computed<boolean>(() => {
return props.allowedTypes.includes("person"); return props.allowedTypes.includes("person");
}); });
// Types for data structures coming from child components are not declared in TS yet. function save(): void {
// We conservatively type them as any to preserve runtime behavior while enabling TS in this component. castPerson.value.postPerson();
function castDataByType(): any {
switch (radioType.value) {
case "person":
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (castPerson.value as any)?.$data?.person;
case "thirdparty": {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let data: any = (castThirdparty.value as any)?.$data?.thirdparty;
if (data && data.address !== undefined && data.address !== null) {
data.address = { id: data.address.address_id };
} else if (data) {
data.address = null;
}
return data;
}
default:
throw Error("Invalid type of entity");
}
} }
defineExpose({ defineExpose({save});
castDataByType,
});
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>

View File

@@ -2,11 +2,25 @@
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue"; import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import Create from "ChillMainAssets/vuejs/OnTheFly/components/Create.vue"; import Create from "ChillMainAssets/vuejs/OnTheFly/components/Create.vue";
import { CreateComponentConfig } from "ChillMainAssets/types"; import { CreateComponentConfig } from "ChillMainAssets/types";
import {trans, SAVE} from "translator";
import {useTemplateRef} from "vue";
const emit = defineEmits<(e: "close") => void>(); const emit = defineEmits<(e: "close") => void>();
const props = defineProps<CreateComponentConfig>(); const props = defineProps<CreateComponentConfig>();
const modalDialogClass = { "modal-xl": true, "modal-scrollable": true }; const modalDialogClass = { "modal-xl": true, "modal-scrollable": true };
type CreateComponentType = InstanceType<typeof Create>
const create = useTemplateRef<CreateComponentType>("create");
function save(): void {
console.log('save from CreateModal');
create.value?.save();
}
defineExpose({save})
</script> </script>
<template> <template>
@@ -22,12 +36,16 @@ const modalDialogClass = { "modal-xl": true, "modal-scrollable": true };
<template #body-head> <template #body-head>
<div class="modal-body"> <div class="modal-body">
<Create <Create
ref="create"
:allowedTypes="props.allowedTypes" :allowedTypes="props.allowedTypes"
:action="props.action" :action="props.action"
:query="props.query" :query="props.query"
></Create> ></Create>
</div> </div>
</template> </template>
<template #footer>
<button class="btn btn-save" type="button" @click.prevent="save">{{ trans(SAVE) }}</button>
</template>
</modal> </modal>
</teleport> </teleport>
</template> </template>

View File

@@ -18,9 +18,14 @@ import { Calendar } from "../../../ChillCalendarBundle/Resources/public/types";
import Person from "./vuejs/_components/OnTheFly/Person.vue"; import Person from "./vuejs/_components/OnTheFly/Person.vue";
export interface AltName { export interface AltName {
label: TranslatableString; labels: TranslatableString;
key: string; key: string;
} }
export interface AltNameWrite {
key: string;
value: string;
}
export interface Person { export interface Person {
id: number; id: number;
type: "person"; type: "person";
@@ -44,14 +49,20 @@ export interface Person {
current_residential_addresses: Address[]; current_residential_addresses: Address[];
} }
export interface PersonIdentifierWrite {
type: "person_identifier";
definition_id: number;
value: object;
}
/** /**
* Person representation to create or update a Person * Person representation to create or update a Person
*/ */
export interface PersonEdit { export interface PersonWrite {
type: "person"; type: "person";
firstName: string; firstName: string;
lastName: string; lastName: string;
altNames: AltName[]; altNames: AltNameWrite[];
// address: number | null; // address: number | null;
birthdate: DateTimeCreate | null; birthdate: DateTimeCreate | null;
deathdate: DateTimeCreate | null; deathdate: DateTimeCreate | null;
@@ -61,6 +72,7 @@ export interface PersonEdit {
gender: SetGender | null; gender: SetGender | null;
center: SetCenter | null; center: SetCenter | null;
civility: SetCivility | null; civility: SetCivility | null;
identifiers: PersonIdentifierWrite[];
} }
export interface AccompanyingPeriod { export interface AccompanyingPeriod {
@@ -408,6 +420,14 @@ export interface SearchOptions {
}; };
} }
export interface PersonIdentifierWorker {
type: "person_identifier_worker";
definition_id: number;
engine: string;
label: TranslatableString;
isActive: boolean;
}
export class MakeFetchException extends Error { export class MakeFetchException extends Error {
sta: number; sta: number;
txt: string; txt: string;

View File

@@ -1,6 +1,7 @@
import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods"; import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { Center, Civility, Gender } from "ChillMainAssets/types"; import { Center, Civility, Gender } from "ChillMainAssets/types";
import { AltName, Person } from "ChillPersonAssets/types"; import {AltName, Person, PersonIdentifierWorker, PersonWrite} from "ChillPersonAssets/types";
import person from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue";
/* /*
* GET a person by id * GET a person by id
@@ -31,3 +32,10 @@ export const getGenders = async (): Promise<Gender[]> =>
export const getCentersForPersonCreation = async (): Promise<{showCenters: boolean; centers: Center[];}> => export const getCentersForPersonCreation = async (): Promise<{showCenters: boolean; centers: Center[];}> =>
makeFetch("GET", "/api/1.0/person/creation/authorized-centers", null); makeFetch("GET", "/api/1.0/person/creation/authorized-centers", null);
export const getPersonIdentifiers = async (): Promise<PersonIdentifierWorker[]> =>
fetchResults("/api/1.0/person/identifiers/workers");
export const createPerson = async (person: PersonWrite): Promise<Person> => {
return makeFetch("POST", "/api/1.0/person/person.json", person);
}

View File

@@ -35,7 +35,7 @@ const { person, isCut = false, addAge = true } = toRefs(props);
const altNameLabel = computed(() => { const altNameLabel = computed(() => {
if (!person.value.altNames) return ""; if (!person.value.altNames) return "";
return person.value.altNames.map((a: AltName) => a.label).join(""); return person.value.altNames.map((a: AltName) => a.labels).join("");
}); });
const altNameKey = computed(() => { const altNameKey = computed(() => {

View File

@@ -56,10 +56,26 @@
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
:id="a.key" :id="a.key"
:value="personAltNamesLabels[i]" :name="'label_'+a.key"
@input="onAltNameInput" value=""
@input="onAltNameInput($event, a.key)"
/> />
<label :for="a.key">{{ localizeString(a.labels) }}</label> <label :for="'label_'+a.key">{{ localizeString(a.labels) }}</label>
</div>
<div
v-for="worker in config.identifiers"
:key="worker.definition_id"
class="form-floating mb-3"
>
<input
class="form-control form-control-lg"
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>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
@@ -181,11 +197,12 @@ import { ref, reactive, computed, onMounted } from "vue";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue"; import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
import { import {
createPerson,
getCentersForPersonCreation, getCentersForPersonCreation,
getCivilities, getCivilities,
getGenders, getGenders,
getPerson, getPerson,
getPersonAltNames, getPersonAltNames, getPersonIdentifiers,
} from "../../_api/OnTheFly"; } from "../../_api/OnTheFly";
import { import {
trans, trans,
@@ -209,7 +226,14 @@ import {
Gender, Gender,
DateTimeCreate, DateTimeCreate,
} from "ChillMainAssets/types"; } from "ChillMainAssets/types";
import {AltName, PersonEdit} from "ChillPersonAssets/types"; import {
AltName,
Person,
PersonWrite,
PersonIdentifierWorker,
type Suggestion,
type EntitiesOrMe
} from "ChillPersonAssets/types";
interface PersonEditComponentConfig { interface PersonEditComponentConfig {
@@ -224,7 +248,13 @@ const props = withDefaults(defineProps<PersonEditComponentConfig>(), {
type: "TODO", type: "TODO",
}); });
const person = reactive<PersonEdit>({ const emit = defineEmits<{
(e: "onPersonCreated", payload: { person: Person }): void;
}>();
defineExpose({postPerson});
const person = reactive<PersonWrite>({
type: "person", type: "person",
firstName: "", firstName: "",
lastName: "", lastName: "",
@@ -238,6 +268,7 @@ const person = reactive<PersonEdit>({
gender: null, gender: null,
center: null, center: null,
civility: null, civility: null,
identifiers: [],
}); });
const config = reactive<{ const config = reactive<{
@@ -245,11 +276,13 @@ const config = reactive<{
civilities: Civility[]; civilities: Civility[];
centers: Center[]; centers: Center[];
genders: Gender[]; genders: Gender[];
identifiers: PersonIdentifierWorker[];
}>({ }>({
altNames: [], altNames: [],
civilities: [], civilities: [],
centers: [], centers: [],
genders: [], genders: [],
identifiers: [],
}); });
const showCenters = ref(false); const showCenters = ref(false);
@@ -350,9 +383,9 @@ const center = computed({
}, },
}); });
const personAltNamesLabels = computed(() => /**
person.altNames.map((a) => (a ? a.label : "")), * Find the query items to display for suggestion
); */
const queryItems = computed(() => { const queryItems = computed(() => {
const words: null|string[] = props.query ? props.query.split(" ") : null; const words: null|string[] = props.query ? props.query.split(" ") : null;
@@ -387,21 +420,30 @@ function checkErrors() {
async function loadData() { async function loadData() {
if (props.id !== undefined && props.id !== null) { if (props.id !== undefined && props.id !== null) {
const person = await getPerson(props.id); const person = await getPerson(props.id);
} }
} }
/*
function onAltNameInput(event: Event) { function onAltNameInput(event: Event, key: string): void {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const key = target.id; const value = target.value;
const label = target.value; const updateAltNamesKey = person.altNames.findIndex((a) => a.key === key);
const updateAltNames = person.altNames.filter((a) => a.key !== key); if (-1 === updateAltNamesKey) {
updateAltNames.push({ key, label }); person.altNames.push({key, value})
person.altNames = updateAltNames; } else {
person.altNames[updateAltNamesKey].value = value;
}
} }
*/ function onIdentifierInput(event: Event, definition_id: number): void {
const target = event.target as HTMLInputElement;
const value = target.value;
const updateIdentifierKey = person.identifiers.findIndex((w) => w.definition_id === definition_id);
if (-1 === updateIdentifierKey) {
person.identifiers.push({type: "person_identifier", definition_id, value: { content: value }})
} else {
person.identifiers[updateIdentifierKey].value = {content: value}
}
}
function addQueryItem(field: "lastName" | "firstName", queryItem: string) { function addQueryItem(field: "lastName" | "firstName", queryItem: string) {
switch (field) { switch (field) {
@@ -422,6 +464,13 @@ function submitNewAddress(payload: { addressId: number }) {
// person.addressId = payload.addressId; // person.addressId = payload.addressId;
} }
async function postPerson(): Promise<void> {
console.log('postPerson');
const createdPerson = await createPerson(person);
emit('onPersonCreated', {person: createdPerson});
}
onMounted(() => { onMounted(() => {
getPersonAltNames().then((altNames) => { getPersonAltNames().then((altNames) => {
config.altNames = altNames; config.altNames = altNames;
@@ -432,6 +481,9 @@ onMounted(() => {
getGenders().then((genders) => { getGenders().then((genders) => {
config.genders = genders; config.genders = genders;
}); });
getPersonIdentifiers().then((identifiers) => {
config.identifiers = identifiers;
});
if (props.action !== "create") { if (props.action !== "create") {
loadData(); loadData();
} else { } else {