mirror of
https://github.com/vcscsvcscs/GenerationsHeritage.git
synced 2025-08-12 13:59:08 +02:00
restructure profile edit
This commit is contained in:
@@ -1,22 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { life_events, unknown, until } from '$lib/paraglide/messages';
|
||||
import {
|
||||
add_life_event,
|
||||
description,
|
||||
life_events,
|
||||
unknown,
|
||||
until
|
||||
} from '$lib/paraglide/messages';
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
|
||||
export let draftPerson: any;
|
||||
export let person_life_events: components['schemas']['PersonProperties']['life_events'];
|
||||
export let editorMode = false;
|
||||
export let onChange: (field: keyof components['schemas']['PersonProperties'], value: any) => void;
|
||||
|
||||
function updateEvent(index: number, key: 'from' | 'to' | 'description', value: string) {
|
||||
if (!person_life_events) return;
|
||||
|
||||
person_life_events = person_life_events.map((event, i) =>
|
||||
i === index ? { ...event, [key]: value } : event
|
||||
);
|
||||
|
||||
onChange('life_events', person_life_events);
|
||||
}
|
||||
|
||||
function addEvent() {
|
||||
const newEvent = { from: '', to: '', description: '' };
|
||||
person_life_events = [...(person_life_events ?? []), newEvent];
|
||||
onChange('life_events', person_life_events);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if draftPerson.life_events?.length}
|
||||
{#if person_life_events?.length}
|
||||
<div class="divider">{life_events()}</div>
|
||||
<ul class="timeline timeline-snap-start timeline-vertical">
|
||||
{#each draftPerson.life_events as event}
|
||||
{#each person_life_events as event, index}
|
||||
<li>
|
||||
<div class="timeline-start">{event.from ?? unknown()}</div>
|
||||
<div class="timeline-start">
|
||||
{#if editorMode}
|
||||
<input
|
||||
type="text"
|
||||
class="input input-xs input-bordered"
|
||||
value={event.from ?? ''}
|
||||
on:input={(e) => updateEvent(index, 'from', e.currentTarget.value)}
|
||||
placeholder={unknown().toLowerCase()}
|
||||
/>
|
||||
{:else}
|
||||
{event.from ?? unknown().toLowerCase()}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="timeline-middle">
|
||||
<div class="badge badge-primary"></div>
|
||||
</div>
|
||||
<div class="timeline-end">
|
||||
<p>{event.description}</p>
|
||||
{#if event.to}
|
||||
<p class="text-sm opacity-50">{until()} {event.to}</p>
|
||||
|
||||
<div class="timeline-end space-y-1">
|
||||
{#if editorMode}
|
||||
<textarea
|
||||
class="textarea textarea-xs textarea-bordered w-full"
|
||||
value={event.description ?? ''}
|
||||
on:input={(e) => updateEvent(index, 'description', e.currentTarget.value)}
|
||||
placeholder={description()}
|
||||
></textarea>
|
||||
{:else}
|
||||
<p>{event.description}</p>
|
||||
{/if}
|
||||
|
||||
{#if event.to || editorMode}
|
||||
<p class="text-sm opacity-50">
|
||||
{until()}
|
||||
{#if editorMode}
|
||||
<input
|
||||
type="text"
|
||||
class="input input-xs input-bordered ml-1"
|
||||
value={event.to ?? ''}
|
||||
on:input={(e) => updateEvent(index, 'to', e.currentTarget.value)}
|
||||
placeholder={unknown().toLowerCase()}
|
||||
/>
|
||||
{:else}
|
||||
{event.to ?? unknown().toLowerCase()}
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<hr />
|
||||
@@ -24,3 +86,11 @@
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
{#if editorMode}
|
||||
<div class="mt-4 flex justify-center">
|
||||
<button class="btn btn-primary btn-sm" on:click={addEvent}>
|
||||
{add_life_event()}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
@@ -2,27 +2,27 @@
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
import { video, photos, upload } from '$lib/paraglide/messages';
|
||||
|
||||
export let draftPerson: components['schemas']['PersonProperties'];
|
||||
export let editorMode = false;
|
||||
export let person: components['schemas']['PersonProperties'];
|
||||
export let editorMode = false;
|
||||
</script>
|
||||
|
||||
{#if editorMode}
|
||||
<button class="btn bg-neutral text-neutral-content btn-xs" on:click={() => {}}>
|
||||
{upload()}
|
||||
</button>
|
||||
<button class="btn bg-neutral text-neutral-content btn-xs" on:click={() => {}}>
|
||||
{upload()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if draftPerson.photos?.length || draftPerson.videos?.length}
|
||||
{#if person.photos?.length || person.videos?.length}
|
||||
<div class="divider">{photos()} & {video()}</div>
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
{#each draftPerson.photos ?? [] as picture}
|
||||
{#each person.photos ?? [] as picture}
|
||||
<img
|
||||
src={picture.url}
|
||||
alt={picture.description ?? photos()}
|
||||
class="h-32 w-full rounded-lg object-cover shadow-md"
|
||||
/>
|
||||
{/each}
|
||||
{#each draftPerson.videos ?? [] as video}
|
||||
{#each person.videos ?? [] as video}
|
||||
<video src={video.url} controls class="h-32 w-full rounded-lg shadow-md">
|
||||
<track kind="captions" src={video.description} srcLang="en" default />
|
||||
<track kind="descriptions" src={video.description} srcLang="en" default />
|
||||
|
@@ -7,17 +7,24 @@
|
||||
import LifeEventsTimeline from './LifeEventsTimeline.svelte';
|
||||
import OtherDetails from './OtherDetails.svelte';
|
||||
import type { components } from '$lib/api/api.gen.js';
|
||||
import { life_events } from '$lib/paraglide/messages';
|
||||
let {
|
||||
closeModal = () => {},
|
||||
person = {}
|
||||
}: { closeModal: () => void; person: components['schemas']['PersonProperties'] } = $props();
|
||||
|
||||
let editorMode = $state(false);
|
||||
let draftPerson = $state({});
|
||||
let draftPerson = $state({} as components['schemas']['PersonProperties']);
|
||||
|
||||
draftPerson = person;
|
||||
editorMode = false;
|
||||
|
||||
function handleDraftPersonChange(
|
||||
field: keyof components['schemas']['PersonProperties'],
|
||||
value: any
|
||||
) {
|
||||
draftPerson[field] = value;
|
||||
}
|
||||
|
||||
function close() {
|
||||
closeModal();
|
||||
editorMode = false;
|
||||
@@ -32,7 +39,6 @@
|
||||
// Save logic here
|
||||
editorMode = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="modal modal-open" transition:fade>
|
||||
@@ -41,10 +47,13 @@
|
||||
<ModalButtons {editorMode} onClose={close} onSave={save} onToggleEdit={toggleEdit} />
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
|
||||
<ProfileHeader {draftPerson} {editorMode} />
|
||||
<MediaGallery {draftPerson} />
|
||||
<LifeEventsTimeline {draftPerson} />
|
||||
<OtherDetails {draftPerson} {editorMode} />
|
||||
<ProfileHeader {person} {editorMode} onChange={handleDraftPersonChange} />
|
||||
<MediaGallery {person} />
|
||||
<LifeEventsTimeline
|
||||
person_life_events={person.life_events}
|
||||
{editorMode}
|
||||
onChange={handleDraftPersonChange}
|
||||
/>
|
||||
<OtherDetails {person} {editorMode} onChange={handleDraftPersonChange} />
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,14 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { callMessageFunction } from '$lib/i18n';
|
||||
import type { MessageKeys } from '$lib/i18n';
|
||||
export let draftPerson: any;
|
||||
export let editorMode = false;
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
|
||||
export let person: components['schemas']['PersonProperties'];
|
||||
export let editorMode = false;
|
||||
export let onChange: (field: keyof components['schemas']['PersonProperties'], value: any) => void;
|
||||
const skipFields = [
|
||||
'id',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'born',
|
||||
'died',
|
||||
'middle_name',
|
||||
'biological_sex',
|
||||
'email',
|
||||
'limit',
|
||||
@@ -17,25 +21,74 @@
|
||||
'profile_picture',
|
||||
'photos',
|
||||
'videos',
|
||||
'life_events'
|
||||
'life_events',
|
||||
'residence',
|
||||
'medications',
|
||||
'medical_conditions',
|
||||
'languages',
|
||||
'notes',
|
||||
'audios',
|
||||
'google_id'
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{#each Object.entries(draftPerson) as [key, value]}
|
||||
{#if !skipFields.includes(key) && value !== undefined && value !== null}
|
||||
{#each Object.entries(person) as [key, value]}
|
||||
{#if !skipFields.includes(key) && ((value !== undefined && value !== null) || editorMode)}
|
||||
<div>
|
||||
<p>
|
||||
<strong>{callMessageFunction(key as MessageKeys) || key}:</strong>
|
||||
<label class="label font-semibold"
|
||||
>{callMessageFunction(key as MessageKeys) || key}:
|
||||
{#if editorMode}
|
||||
<textarea
|
||||
bind:value={draftPerson[key]}
|
||||
class="textarea textarea-bordered textarea-sm w-full"
|
||||
></textarea>
|
||||
{#if typeof value === 'string'}
|
||||
{#if value.length > 100}
|
||||
<textarea
|
||||
bind:value={person[key as keyof components['schemas']['PersonProperties']]}
|
||||
class="textarea textarea-bordered textarea-sm w-full"
|
||||
oninput={(e) =>
|
||||
onChange(
|
||||
key as keyof components['schemas']['PersonProperties'],
|
||||
String(person[key as keyof components['schemas']['PersonProperties']])
|
||||
)}
|
||||
></textarea>
|
||||
{:else}
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full"
|
||||
bind:value={person[key as keyof components['schemas']['PersonProperties']]}
|
||||
oninput={() =>
|
||||
onChange(
|
||||
key as keyof components['schemas']['PersonProperties'],
|
||||
String(person[key as keyof components['schemas']['PersonProperties']])
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
{:else if typeof value === 'boolean'}
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-primary"
|
||||
bind:value={person[key as keyof components['schemas']['PersonProperties']]}
|
||||
onchange={(e) =>
|
||||
onChange(
|
||||
key as keyof components['schemas']['PersonProperties'],
|
||||
Boolean(person[key as keyof components['schemas']['PersonProperties']])
|
||||
)}
|
||||
/>
|
||||
{:else if typeof value === 'number'}
|
||||
<input
|
||||
type="number"
|
||||
class="input input-bordered input-sm w-full"
|
||||
bind:value={person[key as keyof components['schemas']['PersonProperties']]}
|
||||
oninput={(e) =>
|
||||
onChange(
|
||||
key as keyof components['schemas']['PersonProperties'],
|
||||
Number(person[key as keyof components['schemas']['PersonProperties']])
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
{JSON.stringify(value) ?? '-'}
|
||||
<p>{value ?? '-'}</p>
|
||||
{/if}
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
@@ -1,18 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { death } from './../paraglide/messages/en.js';
|
||||
import { onMount } from 'svelte';
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
import { male,female,intersex,other,change_profile_picture, biological_sex, born,died, email, first_name, id, last_name, middle_name, mothers_first_name, mothers_last_name, profile_picture } from '$lib/paraglide/messages';
|
||||
import { callMessageFunction } from '$lib/i18n';
|
||||
import type { MessageKeys } from '$lib/i18n';
|
||||
import { onMount } from 'svelte';
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
import {
|
||||
male,
|
||||
female,
|
||||
intersex,
|
||||
other,
|
||||
change_profile_picture,
|
||||
biological_sex,
|
||||
born,
|
||||
died,
|
||||
email,
|
||||
first_name,
|
||||
id,
|
||||
last_name,
|
||||
middle_name,
|
||||
mothers_first_name,
|
||||
mothers_last_name,
|
||||
profile_picture
|
||||
} from '$lib/paraglide/messages';
|
||||
import { callMessageFunction } from '$lib/i18n';
|
||||
import type { MessageKeys } from '$lib/i18n';
|
||||
|
||||
export let draftPerson: components['schemas']['PersonProperties'] & {
|
||||
id?: string,
|
||||
};
|
||||
export let editorMode = false;
|
||||
let birth_date: HTMLInputElement;
|
||||
let death_date: HTMLInputElement;
|
||||
onMount(() => {
|
||||
export let person: components['schemas']['PersonProperties'] & {
|
||||
id?: string;
|
||||
};
|
||||
export let editorMode = false;
|
||||
export let onChange: (field: keyof components['schemas']['PersonProperties'], value: any) => void;
|
||||
|
||||
let birth_date: HTMLInputElement;
|
||||
let death_date: HTMLInputElement;
|
||||
onMount(() => {
|
||||
if (birth_date) {
|
||||
import('pikaday').then(({ default: Pikaday }) => {
|
||||
const picker = new Pikaday({
|
||||
@@ -20,21 +38,23 @@
|
||||
minDate: new Date(1900, 0, 1),
|
||||
field: birth_date,
|
||||
onSelect: function (date) {
|
||||
birth_date.value = date.toISOString();
|
||||
birth_date.value = date.toISOString().split('T')[0];
|
||||
onChange('born', date.toISOString().split('T')[0]);
|
||||
}
|
||||
});
|
||||
// Clean up when component unmounts
|
||||
return () => picker.destroy();
|
||||
});
|
||||
}
|
||||
if (death_date) {
|
||||
if (death_date) {
|
||||
import('pikaday').then(({ default: Pikaday }) => {
|
||||
const picker = new Pikaday({
|
||||
format: 'YYYY-MM-DD',
|
||||
minDate: new Date(1900, 0, 1),
|
||||
field: death_date,
|
||||
onSelect: function (date) {
|
||||
death_date.value = date.toISOString();
|
||||
death_date.value = date.toISOString().split('T')[0];
|
||||
onChange('died', date.toISOString().split('T')[0]);
|
||||
}
|
||||
});
|
||||
// Clean up when component unmounts
|
||||
@@ -43,62 +63,107 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<div class="flex-shrink-0 flex flex-col items-center gap-2">
|
||||
<img src={draftPerson.profile_picture||'https://cdn-icons-png.flaticon.com/512/10628/10628885.png'} alt={profile_picture()} class="rounded-lg shadow-md w-48 h-48 object-cover" />
|
||||
{#if editorMode}
|
||||
<button class="btn bg-neutral text-neutral-content btn-xs" on:click={() => {}}>
|
||||
{change_profile_picture()}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p><strong>{first_name()}:</strong> {#if editorMode}<input bind:value={draftPerson.first_name} class="input input-sm input-bordered w-full" />{:else}{draftPerson.first_name ?? '-'}{/if}</p>
|
||||
<p><strong>{last_name()}:</strong> {#if editorMode}<input bind:value={draftPerson.last_name} class="input input-sm input-bordered w-full" />{:else}{draftPerson.last_name ?? '-'}{/if}</p>
|
||||
<p><strong>{middle_name()}:</strong> {#if editorMode}<input bind:value={draftPerson.middle_name} class="input input-sm input-bordered w-full" />{:else}{draftPerson.middle_name ?? '-'}{/if}</p>
|
||||
<p><strong>{born()}:</strong>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full pika-single"
|
||||
id="birth_date"
|
||||
bind:this={birth_date}
|
||||
bind:value={draftPerson.born}/>
|
||||
</p>
|
||||
<p><strong>{died()}:</strong>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full pika-single"
|
||||
id="death_date"
|
||||
placeholder={died()}
|
||||
bind:this={death_date}
|
||||
bind:value={draftPerson.died}
|
||||
/>
|
||||
</p>
|
||||
<p><strong>{biological_sex()}:</strong>
|
||||
{#if editorMode}
|
||||
<select
|
||||
name="biological_sex"
|
||||
class="select select-bordered w-full select-sm"
|
||||
id="biological_sex"
|
||||
bind:value={draftPerson.biological_sex}
|
||||
placeholder={biological_sex()}
|
||||
>
|
||||
<option value="male">{male()} </option>
|
||||
<option value="female">{female()} </option>
|
||||
<option value="intersex">{intersex()} </option>
|
||||
<option value="other">{other()} </option>
|
||||
</select>
|
||||
{:else}{callMessageFunction(draftPerson.biological_sex as MessageKeys) ?? '-'}{/if}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>{email()}:</strong> {#if editorMode}<input bind:value={draftPerson.email} class="input input-sm input-bordered w-full" />{:else}{draftPerson.email ?? '-'}{/if}</p>
|
||||
<p><strong>{mothers_first_name()}:</strong> {#if editorMode}<input bind:value={draftPerson.mothers_first_name} class="input input-sm input-bordered w-full" />{:else}{draftPerson.mothers_first_name ?? '-'}{/if}</p>
|
||||
<p><strong>{mothers_last_name()}:</strong> {#if editorMode}<input bind:value={draftPerson.mothers_last_name} class="input input-sm input-bordered w-full" />{:else}{draftPerson.mothers_last_name ?? '-'}{/if}</p>
|
||||
<p><strong> {id()}:</strong>{draftPerson.id ?? '-'}</p>
|
||||
<p><strong> Limit:</strong>{draftPerson.limit ?? '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6 md:flex-row">
|
||||
<div class="flex flex-shrink-0 flex-col items-center gap-2">
|
||||
<img
|
||||
src={person.profile_picture || 'https://cdn-icons-png.flaticon.com/512/10628/10628885.png'}
|
||||
alt={profile_picture()}
|
||||
class="h-48 w-48 rounded-lg object-cover shadow-md"
|
||||
/>
|
||||
{#if editorMode}
|
||||
<button class="btn bg-neutral text-neutral-content btn-xs" on:click={() => {}}>
|
||||
{change_profile_picture()}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid flex-1 grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<p>
|
||||
<strong>{first_name()}:</strong>
|
||||
{#if editorMode}<input
|
||||
bind:value={person.first_name}
|
||||
class="input input-sm input-bordered w-full"
|
||||
/>{:else}{person.first_name ?? '-'}{/if}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{last_name()}:</strong>
|
||||
{#if editorMode}<input
|
||||
bind:value={person.last_name}
|
||||
class="input input-sm input-bordered w-full"
|
||||
/>{:else}{person.last_name ?? '-'}{/if}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{middle_name()}:</strong>
|
||||
{#if editorMode}<input
|
||||
bind:value={person.middle_name}
|
||||
class="input input-sm input-bordered w-full"
|
||||
/>{:else}{person.middle_name ?? '-'}{/if}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{born()}:</strong>
|
||||
{#if editorMode}<input
|
||||
type="text"
|
||||
class="pika-single w-full"
|
||||
id="birth_date"
|
||||
bind:this={birth_date}
|
||||
bind:value={person.born}
|
||||
/>
|
||||
{:else}{person.born ?? '-'}{/if}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{died()}:</strong>
|
||||
{#if editorMode}<input
|
||||
type="text"
|
||||
class="pika-single w-full"
|
||||
id="death_date"
|
||||
placeholder={died()}
|
||||
bind:this={death_date}
|
||||
bind:value={person.died}
|
||||
/>{:else}{person.died ?? '-'}{/if}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{biological_sex()}:</strong>
|
||||
{#if editorMode}
|
||||
<select
|
||||
name="biological_sex"
|
||||
class="select select-bordered select-sm w-full"
|
||||
id="biological_sex"
|
||||
bind:value={person.biological_sex}
|
||||
placeholder={biological_sex()}
|
||||
>
|
||||
<option value="male">{male()} </option>
|
||||
<option value="female">{female()} </option>
|
||||
<option value="intersex">{intersex()} </option>
|
||||
<option value="other">{other()} </option>
|
||||
</select>
|
||||
{:else}{callMessageFunction(person.biological_sex as MessageKeys) ?? '-'}{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<strong>{email()}:</strong>
|
||||
{#if editorMode}<input
|
||||
bind:value={person.email}
|
||||
class="input input-sm input-bordered w-full"
|
||||
/>{:else}{person.email ?? '-'}{/if}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{mothers_first_name()}:</strong>
|
||||
{#if editorMode}<input
|
||||
bind:value={person.mothers_first_name}
|
||||
class="input input-sm input-bordered w-full"
|
||||
/>{:else}{person.mothers_first_name ?? '-'}{/if}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{mothers_last_name()}:</strong>
|
||||
{#if editorMode}<input
|
||||
bind:value={person.mothers_last_name}
|
||||
class="input input-sm input-bordered w-full"
|
||||
/>{:else}{person.mothers_last_name ?? '-'}{/if}
|
||||
</p>
|
||||
<p><strong> {id()}:</strong>{person.id ?? '-'}</p>
|
||||
<p><strong> Limit:</strong>{person.limit ?? '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import {
|
||||
register,
|
||||
create,
|
||||
close,
|
||||
born,
|
||||
mothers_first_name,
|
||||
@@ -13,42 +13,134 @@
|
||||
male,
|
||||
female,
|
||||
other,
|
||||
intersex
|
||||
intersex,
|
||||
create_relationship_and_person,
|
||||
child,
|
||||
sibling,
|
||||
parent,
|
||||
spouse,
|
||||
relation,
|
||||
relation_type,
|
||||
notes,
|
||||
until,
|
||||
optional_field,
|
||||
from_time
|
||||
} from '$lib/paraglide/messages';
|
||||
import { onMount } from 'svelte';
|
||||
import type { components } from '$lib/api/api.gen.js';
|
||||
import { validatePersonRegistration, validateFamilyRelationship } from './validate_fields';
|
||||
import type { Node, Edge } from '@xyflow/svelte';
|
||||
|
||||
let {
|
||||
closeModal = () => {},
|
||||
relationship = null,
|
||||
}: { closeModal : ()=>void,relationship: number | null } = $props();
|
||||
onCreation = (nodes: Array<Node> | null, edges: Array<Edge> | null) => {},
|
||||
onOnlyPersonCreation = (person: components['schemas']['Person']) => {},
|
||||
relationshipStartID = null
|
||||
}: {
|
||||
closeModal: () => void;
|
||||
onCreation: (newNodes: Array<Node> | null, newEdges: Array<Edge> | null) => void;
|
||||
onOnlyPersonCreation: (person: components['schemas']['Person']) => void | undefined;
|
||||
relationshipStartID: number | null;
|
||||
} = $props();
|
||||
|
||||
let birth_date: HTMLInputElement;
|
||||
let draftRelationship: components['schemas']['FamilyRelationship'] & {type: string} | null = {} as components['schemas']['FamilyRelationship'] & {type: string} | null;
|
||||
let draftPerson :components['schemas']['PersonRegistration'] = {} as components['schemas']['PersonRegistration'];
|
||||
let relationship_from_time: HTMLInputElement;
|
||||
let relationship_until: HTMLInputElement;
|
||||
|
||||
let draftRelationship: (components['schemas']['FamilyRelationship'] & { type: string }) | null =
|
||||
$state({} as components['schemas']['FamilyRelationship'] & { type: string });
|
||||
let draftPerson: components['schemas']['PersonRegistration'] = $state(
|
||||
{} as components['schemas']['PersonRegistration']
|
||||
);
|
||||
let error: string | undefined | null = $state();
|
||||
|
||||
function onClose() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
async function create(event: SubmitEvent) {
|
||||
async function onCreate(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
error = validatePersonRegistration(draftPerson);
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (relationship !== null && draftRelationship !== null) {
|
||||
error = validateFamilyRelationship(draftRelationship);
|
||||
if (error) {
|
||||
if (relationshipStartID !== null) {
|
||||
if (draftRelationship !== null) {
|
||||
error = validateFamilyRelationship(draftRelationship);
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let requestBody = {
|
||||
relationship: draftRelationship,
|
||||
type: draftRelationship!.type,
|
||||
person: draftPerson
|
||||
} as {
|
||||
person: components['schemas']['PersonRegistration'];
|
||||
type?: 'child' | 'parent' | 'spouse' | 'sibling';
|
||||
relationship: components['schemas']['FamilyRelationship'];
|
||||
};
|
||||
let response = await fetch(`/api/person_and_relationship/${relationshipStartID}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestBody),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
error = 'Error creating person and relationship';
|
||||
return;
|
||||
}
|
||||
let data = (await response.json()) as {
|
||||
person?: components['schemas']['Person'];
|
||||
relationships?: components['schemas']['Relationship'][];
|
||||
};
|
||||
if (onCreation !== undefined) {
|
||||
let edges: Array<Edge> = [];
|
||||
data.relationships?.map((relationship) =>
|
||||
edges.push({
|
||||
id: String(relationship.id),
|
||||
source: String(relationship.start),
|
||||
target: String(relationship.end),
|
||||
data: {
|
||||
...relationship.properties
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let newNode = {
|
||||
id: String(data.person?.Id),
|
||||
data: {
|
||||
...data.person?.Props
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
type: 'personNode'
|
||||
} as Node;
|
||||
onCreation([newNode], edges);
|
||||
closeModal();
|
||||
}
|
||||
} else {
|
||||
let requestBody = draftPerson as components['schemas']['PersonRegistration'];
|
||||
let response = await fetch(`/api/person`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestBody),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
error = 'Error creating person';
|
||||
return;
|
||||
}
|
||||
|
||||
if (onOnlyPersonCreation !== undefined) {
|
||||
onOnlyPersonCreation(await response.json());
|
||||
}
|
||||
}
|
||||
|
||||
const url = `/api/person`;
|
||||
const response = await fetch(url);
|
||||
result = await response.text();
|
||||
closeModal();
|
||||
}
|
||||
|
||||
@@ -63,7 +155,44 @@
|
||||
birth_date.placeholder = '';
|
||||
},
|
||||
onSelect: function (date) {
|
||||
birth_date.value = date.toISOString();
|
||||
birth_date.value = date.toISOString().split('T')[0];
|
||||
draftPerson.born = date.toISOString().split('T')[0];
|
||||
}
|
||||
});
|
||||
// Clean up when component unmounts
|
||||
return () => picker.destroy();
|
||||
});
|
||||
}
|
||||
if (relationship_from_time) {
|
||||
import('pikaday').then(({ default: Pikaday }) => {
|
||||
const picker = new Pikaday({
|
||||
format: 'YYYY-MM-DD',
|
||||
minDate: new Date(1900, 0, 1),
|
||||
field: relationship_from_time,
|
||||
onOpen: function () {
|
||||
relationship_from_time.placeholder = '';
|
||||
},
|
||||
onSelect: function (date) {
|
||||
relationship_from_time.value = date.toISOString().split('T')[0];
|
||||
draftRelationship.from = date.toISOString().split('T')[0];
|
||||
}
|
||||
});
|
||||
// Clean up when component unmounts
|
||||
return () => picker.destroy();
|
||||
});
|
||||
}
|
||||
if (relationship_until) {
|
||||
import('pikaday').then(({ default: Pikaday }) => {
|
||||
const picker = new Pikaday({
|
||||
format: 'YYYY-MM-DD',
|
||||
minDate: new Date(1900, 0, 1),
|
||||
field: relationship_until,
|
||||
onOpen: function () {
|
||||
relationship_until.placeholder = '';
|
||||
},
|
||||
onSelect: function (date) {
|
||||
relationship_until.value = date.toISOString().split('T')[0];
|
||||
draftRelationship.to = date.toISOString().split('T')[0];
|
||||
}
|
||||
});
|
||||
// Clean up when component unmounts
|
||||
@@ -74,17 +203,25 @@
|
||||
</script>
|
||||
|
||||
<div class="modal modal-open" transition:fade>
|
||||
<div class="modal-box max-h-screen w-full max-w-5xl overflow-y-auto">
|
||||
<div class="bg-base-100 sticky top-0 z-10">
|
||||
<button class="btn btn-error btn-sm" onclick={onClose}>
|
||||
{close()}
|
||||
</button>
|
||||
<div class="divider"></div>
|
||||
<div
|
||||
class="modal-box flex max-h-screen w-full max-w-5xl flex-col items-center justify-center overflow-y-auto"
|
||||
>
|
||||
<div class="flex w-full max-w-5xl items-center justify-between p-2">
|
||||
<h3 class="text-left text-lg font-bold">{create_relationship_and_person()}</h3>
|
||||
<div>
|
||||
<button class="btn btn-error btn-sm" onclick={onClose}>
|
||||
{close()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form onsubmit={create}>
|
||||
<fieldset class="fieldset">
|
||||
<div class="divider"></div>
|
||||
|
||||
<form onsubmit={onCreate} class="w-full">
|
||||
<fieldset
|
||||
class="fieldset grid w-full grid-cols-1 items-center gap-y-4 md:grid-cols-2 md:gap-x-6"
|
||||
>
|
||||
{#if error}
|
||||
<div role="alert" class="alert alert-error">
|
||||
<div role="alert" class="alert alert-error col-span-full">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
@@ -101,74 +238,144 @@
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if relationship !== undefined}
|
||||
<input type="hidden" name="relationship" value={relationship} />
|
||||
{#if relationshipStartID !== undefined}
|
||||
<input type="hidden" name="relationshipStartID" value={relationshipStartID} />
|
||||
<div class="flex flex-col">
|
||||
<label class="label" for="relationship_type">{relation_type()}</label>
|
||||
<select
|
||||
name="relationship_type"
|
||||
class="select select-bordered"
|
||||
id="relationship_type"
|
||||
bind:value={draftRelationship.type}
|
||||
>
|
||||
<option value="child">{child()}</option>
|
||||
<option value="parent">{parent()}</option>
|
||||
<option value="sibling">{sibling()}</option>
|
||||
<option value="spouse">{spouse()}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label class="label" for="relationship_notes"
|
||||
>{relation() + ' ' + notes().toLowerCase()}:</label
|
||||
>
|
||||
<textarea
|
||||
name="relationship_notes"
|
||||
class="textarea"
|
||||
bind:value={draftRelationship.notes}
|
||||
placeholder={notes().toLowerCase() + ' ' + optional_field().toLowerCase()}
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label class="label" for="from_time">{from_time()}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="from_time"
|
||||
id="from_time"
|
||||
class="input input-bordered validator pika-single"
|
||||
placeholder={optional_field()}
|
||||
bind:this={relationship_from_time}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label class="label" for="until">{until()}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="until"
|
||||
id="until"
|
||||
class="input input-bordered validator pika-single"
|
||||
placeholder={optional_field()}
|
||||
bind:this={relationship_until}
|
||||
/>
|
||||
</div>
|
||||
<div class="divider margin-t-2 col-span-full"></div>
|
||||
{/if}
|
||||
<label class="fieldset-label" for="email">{email()}</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
class="input"
|
||||
placeholder={email()}
|
||||
bind:value={draftPerson.email}
|
||||
/>
|
||||
<label class="fieldset-label" for="first_name">{first_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
name="first_name"
|
||||
id="first_name"
|
||||
placeholder={first_name()}
|
||||
/>
|
||||
<label class="fieldset-label" for="last_name">{last_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
name="last_name"
|
||||
id="last_name"
|
||||
placeholder={last_name()}
|
||||
/>
|
||||
<label class="fieldset-label" for="birth_date">{born()}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input pika-single"
|
||||
id="birth_date"
|
||||
placeholder={born()}
|
||||
bind:value={draftPerson.born}
|
||||
bind:this={birth_date}
|
||||
/>
|
||||
<label class="fieldset-label" for="biological_sex">{biological_sex()}</label>
|
||||
<select
|
||||
name="biological_sex"
|
||||
class="select select-bordered w-full max-w-xs"
|
||||
id="biological_sex"
|
||||
placeholder={biological_sex()}
|
||||
bind:value={draftPerson.biological_sex}
|
||||
>
|
||||
<option value="male">{male()} </option>
|
||||
<option value="female">{female()} </option>
|
||||
<option value="intersex">{intersex()} </option>
|
||||
<option value="other">{other()} </option>
|
||||
</select>
|
||||
<label class="fieldset-label" for="mothers_last_name">{mothers_last_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
name="mothers_last_name"
|
||||
id="mothers_last_name"
|
||||
placeholder={mothers_last_name()}
|
||||
bind:value={draftPerson.mothers_last_name}
|
||||
/>
|
||||
<label class="fieldset-label" for="mothers_first_name">{mothers_first_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
name="mothers_first_name"
|
||||
id="mothers_first_name"
|
||||
placeholder={mothers_first_name()}
|
||||
bind:value={draftPerson.mothers_first_name}
|
||||
/>
|
||||
<button class="btn btn-neutral mt-4">{register()}</button>
|
||||
|
||||
<!-- Inputs -->
|
||||
<div class="flex flex-col">
|
||||
<label class="label" for="first_name">{first_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
class="input input-bordered"
|
||||
placeholder={first_name()}
|
||||
bind:value={draftPerson.first_name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label class="label" for="last_name">{last_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
class="input input-bordered"
|
||||
placeholder={last_name()}
|
||||
bind:value={draftPerson.last_name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label class="label" for="email">{email()}</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
class="input input-bordered validator"
|
||||
placeholder={email() + ' ' + optional_field().toLowerCase()}
|
||||
bind:value={draftPerson.email}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label class="label" for="birth_date">{born()}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="birth_date"
|
||||
class="input input-bordered validator pika-single"
|
||||
placeholder={born()}
|
||||
bind:this={birth_date}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label class="label" for="biological_sex">{biological_sex()}</label>
|
||||
<select
|
||||
name="biological_sex"
|
||||
class="select select-bordered"
|
||||
id="biological_sex"
|
||||
bind:value={draftPerson.biological_sex}
|
||||
>
|
||||
<option value="male">{male()}</option>
|
||||
<option value="female">{female()}</option>
|
||||
<option value="intersex">{intersex()}</option>
|
||||
<option value="other">{other()}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label class="label" for="mothers_last_name">{mothers_last_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="mothers_last_name"
|
||||
class="input input-bordered"
|
||||
placeholder={mothers_last_name()}
|
||||
bind:value={draftPerson.mothers_last_name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label class="label" for="mothers_first_name">{mothers_first_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="mothers_first_name"
|
||||
class="input input-bordered"
|
||||
placeholder={mothers_first_name()}
|
||||
bind:value={draftPerson.mothers_first_name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Submit button spans full width -->
|
||||
<div class="col-span-full mt-4 flex justify-center">
|
||||
<button type="submit" class="btn btn-neutral mt-4">{create()}</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -6,14 +6,14 @@ export function validatePersonRegistration(
|
||||
): string | null {
|
||||
if (!data.first_name || data.first_name.trim() === '') {
|
||||
return missing_field({
|
||||
field: first_name(),
|
||||
});
|
||||
field: first_name()
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.last_name || data.last_name.trim() === '') {
|
||||
return missing_field({
|
||||
field: last_name(),
|
||||
});
|
||||
field: last_name()
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -37,14 +37,14 @@ export function validatePersonRegistration(
|
||||
|
||||
if (!data.mothers_first_name || data.mothers_first_name.trim() === '') {
|
||||
return missing_field({
|
||||
field: mothers_first_name(),
|
||||
});
|
||||
field: mothers_first_name()
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.mothers_last_name || data.mothers_last_name.trim() === '') {
|
||||
return missing_field({
|
||||
field: 'Mother\'s last name',
|
||||
});
|
||||
field: "Mother's last name"
|
||||
});
|
||||
}
|
||||
|
||||
return null; // No errors
|
||||
|
40
apps/app/src/lib/profile/editors/EditableField.svelte
Normal file
40
apps/app/src/lib/profile/editors/EditableField.svelte
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
|
||||
export let key: keyof components['schemas']['PersonProperties'];
|
||||
export let value: any;
|
||||
export let editorMode = false;
|
||||
export let onChange: (field: keyof components['schemas']['PersonProperties'], value: any) => void;
|
||||
let numberField: HTMLInputElement;
|
||||
let textField: HTMLTextAreaElement;
|
||||
let checkboxField: HTMLInputElement;
|
||||
</script>
|
||||
|
||||
{#if editorMode}
|
||||
{#if typeof value === 'boolean'}
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
checked={value}
|
||||
bind:this={checkboxField}
|
||||
oninput={() => onChange(key, checkboxField.value === 'true')}
|
||||
/>
|
||||
{:else if typeof value === 'number'}
|
||||
<input
|
||||
type="number"
|
||||
class="input input-bordered input-sm w-full"
|
||||
{value}
|
||||
bind:this={numberField}
|
||||
oninput={() => onChange(key, Number(numberField.value))}
|
||||
/>
|
||||
{:else}
|
||||
<textarea
|
||||
class="textarea textarea-bordered textarea-sm w-full"
|
||||
{value}
|
||||
oninput={() => onChange(key, textField.value)}
|
||||
bind:this={textField}
|
||||
></textarea>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="text-sm text-gray-700">{value ?? '-'}</p>
|
||||
{/if}
|
Reference in New Issue
Block a user