import { error, fail, redirect } from '@sveltejs/kit'; import { createApiClient, extractErrorCode } from '$lib/shared/api.server'; import { getErrorMessage } from '$lib/shared/errors'; import type { DatePrecision } from '$lib/shared/utils/documentDate'; import { normalizePersonType, validatePersonFields, resolveValidationMessage } from '$lib/person/person-validation'; export async function load({ params, fetch, locals }) { const canWrite = (locals.user as { groups?: { permissions: string[] }[] } | undefined)?.groups?.some((g) => g.permissions.includes('WRITE_ALL') ) ?? false; if (!canWrite) throw error(403, 'Forbidden'); const { id } = params; const api = createApiClient(fetch); const [result, aliasesResult, relsResult, inferredResult] = await Promise.all([ api.GET('/api/persons/{id}', { params: { path: { id } } }), api.GET('/api/persons/{id}/aliases', { params: { path: { id } } }), api.GET('/api/persons/{id}/relationships', { params: { path: { id } } }), api.GET('/api/persons/{id}/inferred-relationships', { params: { path: { id } } }) ]); if (!result.response.ok) { throw error(result.response.status, getErrorMessage(extractErrorCode(result.error))); } const person = result.data!; const personType = normalizePersonType(person.personType); return { person: { ...person, personType }, aliases: aliasesResult.data ?? [], relationships: relsResult.data ?? [], inferredRelationships: inferredResult.data ?? [] }; } export const actions = { update: async ({ request, params, fetch }) => { const formData = await request.formData(); const personType = normalizePersonType(formData.get('personType')?.toString()); const title = formData.get('title')?.toString().trim() || undefined; const firstName = formData.get('firstName')?.toString().trim(); const lastName = formData.get('lastName')?.toString().trim(); const alias = formData.get('alias')?.toString().trim() || undefined; const notes = formData.get('notes')?.toString().trim() || undefined; // Empty date input → omit date AND precision: the backend normalises the // absent pair to null/UNKNOWN, and a lone precision would fail the // coherence check (INVALID_DATE_PRECISION). const birthDate = formData.get('birthDate')?.toString().trim() || undefined; const birthDatePrecision = birthDate ? (formData.get('birthDatePrecision')?.toString() as DatePrecision) : undefined; const deathDate = formData.get('deathDate')?.toString().trim() || undefined; const deathDatePrecision = deathDate ? (formData.get('deathDatePrecision')?.toString() as DatePrecision) : undefined; // Must NOT use the conditional-spread idiom for generation: G 0 is a // valid family-tree-root value. The key always travels in the body so // an explicit clear (empty option) reaches the backend as null. const generationRaw = formData.get('generation'); const generation = generationRaw == null || generationRaw.toString() === '' ? null : Number(generationRaw); const validationKey = validatePersonFields(personType, firstName, lastName); if (validationKey) { return fail(400, { updateError: resolveValidationMessage(validationKey) }); } const api = createApiClient(fetch); const result = await api.PUT('/api/persons/{id}', { params: { path: { id: params.id } }, body: { personType, ...(title ? { title } : {}), ...(firstName ? { firstName } : {}), lastName, ...(alias ? { alias } : {}), ...(notes ? { notes } : {}), ...(birthDate ? { birthDate, birthDatePrecision } : {}), ...(deathDate ? { deathDate, deathDatePrecision } : {}), generation } }); if (!result.response.ok) { return fail(result.response.status, { updateError: getErrorMessage(extractErrorCode(result.error)) }); } throw redirect(303, `/persons/${params.id}`); }, discard: async ({ params }) => { throw redirect(303, `/persons/${params.id}`); }, merge: async ({ request, params, fetch }) => { const formData = await request.formData(); const targetPersonId = formData.get('targetPersonId')?.toString(); if (!targetPersonId) { return fail(400, { mergeError: 'Bitte eine Zielperson auswählen.' }); } const api = createApiClient(fetch); const result = await api.POST('/api/persons/{id}/merge', { params: { path: { id: params.id } }, body: { targetPersonId } }); if (!result.response.ok) { return fail(result.response.status, { mergeError: getErrorMessage(extractErrorCode(result.error)) }); } throw redirect(303, `/persons/${targetPersonId}`); }, addAlias: async ({ request, params, fetch }) => { const formData = await request.formData(); const lastName = formData.get('lastName')?.toString().trim(); const firstName = formData.get('firstName')?.toString().trim() || undefined; const type = formData.get('type')?.toString(); if (!lastName) { return fail(400, { aliasError: 'Nachname ist ein Pflichtfeld.' }); } if (!type) { return fail(400, { aliasError: 'Art ist ein Pflichtfeld.' }); } const api = createApiClient(fetch); const result = await api.POST('/api/persons/{id}/aliases', { params: { path: { id: params.id } }, body: { lastName, firstName, type: type as 'BIRTH' | 'WIDOWED' | 'DIVORCED' | 'OTHER' } }); if (!result.response.ok) { return fail(result.response.status, { aliasError: getErrorMessage(extractErrorCode(result.error)) }); } return { aliasSuccess: true }; }, removeAlias: async ({ request, params, fetch }) => { const formData = await request.formData(); const aliasId = formData.get('aliasId')?.toString(); if (!aliasId) { return fail(400, { aliasError: 'Alias ID fehlt.' }); } const api = createApiClient(fetch); const result = await api.DELETE('/api/persons/{id}/aliases/{aliasId}', { params: { path: { id: params.id, aliasId } } }); if (!result.response.ok) { return fail(result.response.status, { aliasError: getErrorMessage(extractErrorCode(result.error)) }); } return { aliasSuccess: true }; }, toggleFamilyMember: async ({ request, params, fetch }) => { const formData = await request.formData(); const value = formData.get('familyMember')?.toString() === 'true'; const api = createApiClient(fetch); const result = await api.PATCH('/api/persons/{id}/family-member', { params: { path: { id: params.id } }, body: { familyMember: value } }); if (!result.response.ok) { return fail(result.response.status, { relationshipError: getErrorMessage(extractErrorCode(result.error)) }); } return { relationshipSuccess: true }; }, addRelationship: async ({ request, params, fetch }) => { const formData = await request.formData(); const relatedPersonId = formData.get('relatedPersonId')?.toString(); const relationType = formData.get('relationType')?.toString(); const fromYearRaw = formData.get('fromYear')?.toString().trim(); const toYearRaw = formData.get('toYear')?.toString().trim(); const notes = formData.get('notes')?.toString().trim() || undefined; if (!relatedPersonId || !relationType) { return fail(400, { relationshipError: getErrorMessage('VALIDATION_ERROR') }); } if (relatedPersonId === params.id) { return fail(400, { relationshipError: getErrorMessage('VALIDATION_ERROR') }); } const fromYear = fromYearRaw ? parseInt(fromYearRaw, 10) : undefined; const toYear = toYearRaw ? parseInt(toYearRaw, 10) : undefined; if ( fromYear !== undefined && toYear !== undefined && !Number.isNaN(fromYear) && !Number.isNaN(toYear) && toYear < fromYear ) { return fail(400, { relationshipError: getErrorMessage('VALIDATION_ERROR') }); } const api = createApiClient(fetch); const result = await api.POST('/api/persons/{id}/relationships', { params: { path: { id: params.id } }, body: { relatedPersonId, relationType, ...(fromYear !== undefined && !Number.isNaN(fromYear) ? { fromYear } : {}), ...(toYear !== undefined && !Number.isNaN(toYear) ? { toYear } : {}), ...(notes ? { notes } : {}) } }); if (!result.response.ok) { return fail(result.response.status, { relationshipError: getErrorMessage(extractErrorCode(result.error)) }); } return { relationshipSuccess: true }; }, deleteRelationship: async ({ request, params, fetch }) => { const formData = await request.formData(); const relId = formData.get('relId')?.toString(); if (!relId) { return fail(400, { relationshipError: getErrorMessage('VALIDATION_ERROR') }); } const api = createApiClient(fetch); const result = await api.DELETE('/api/persons/{id}/relationships/{relId}', { params: { path: { id: params.id, relId } } }); if (!result.response.ok) { return fail(result.response.status, { relationshipError: getErrorMessage(extractErrorCode(result.error)) }); } return { relationshipSuccess: true }; } };