add feature and bug fix

patient list :
menambahkan client validation patientid boleh '.' dan '-'
fix isDeadLabel karena backend gamau ngeluarin valuenya

contact :
tambahkan guard pada handlesave
fix create contact gak nampilin detail di payload
fix edit contact updated ga nampil

occupation :
testing spinner untuk indikasi rowclick
testing close popover setelah save selesai
This commit is contained in:
faiztyanirh 2026-04-13 18:06:07 +07:00
parent ffb57539e8
commit 239147f7ec
17 changed files with 237 additions and 508 deletions

View File

@ -17,7 +17,7 @@ export function useSearch(searchFields, searchApiFunction) {
return query;
}
async function handleSearch() {
async function handleSearch(onDone) {
isLoading = true;
try {
searchData = await searchApiFunction(searchQuery);
@ -25,6 +25,7 @@ export function useSearch(searchFields, searchApiFunction) {
console.error('Search failed:', error);
} finally {
isLoading = false;
onDone?.();
}
}

View File

@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
import PencilIcon from "@lucide/svelte/icons/pencil";
import { API } from "$lib/config/api";
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
export const searchFields = [
{
@ -53,8 +54,13 @@ export const detailSections = [
}
];
export function contactActions(masterDetail) {
export function contactActions(masterDetail, handlers) {
return [
{
Icon: RefreshIcon,
label: 'Refresh Data',
onClick: handlers.refresh,
},
{
Icon: PlusIcon,
label: 'Add Contact',

View File

@ -113,10 +113,16 @@
tempDetailContact,
});
console.log(payload)
const result = await formState.save(masterDetail.mode);
const result = await formState.save(masterDetail.mode, payload);
toast('Contact Created!');
masterDetail?.exitForm(true);
if (result.status === 'success') {
toast('Contact Created!');
masterDetail?.exitForm(true);
} else {
console.error('Failed to save contact');
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save contact';
toast.error(errorMessages)
}
}
const primaryAction = $derived({

View File

@ -1,309 +0,0 @@
<script>
import { useDictionaryForm } from "$lib/components/composable/use-dictionary-form.svelte";
import FormPageContainer from "$lib/components/reusable/form/form-page-container.svelte";
import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-form-renderer.svelte";
import { toast } from "svelte-sonner";
import { untrack } from "svelte";
import { API } from "$lib/config/api";
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
import { Separator } from "$lib/components/ui/separator/index.js";
import PlusIcon from "@lucide/svelte/icons/plus";
import * as Card from "$lib/components/ui/card/index.js";
import { Badge } from "$lib/components/ui/badge/index.js";
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import { contactDetailInitialForm, contactDetailFormFields } from "$lib/components/dictionary/contact/config/contact-form-config";
import { Button } from "$lib/components/ui/button/index.js";
import { useForm } from "$lib/components/composable/use-form.svelte";
import XIcon from "@lucide/svelte/icons/x";
import Edit2Icon from "@lucide/svelte/icons/edit-2";
import { getChangedFields } from "$lib/utils/getChangedFields";
let props = $props();
const { masterDetail, formFields, formActions, schema, initialForm } = props.context;
const { formState } = masterDetail;
const detailFormState = useForm({
schema: null,
initialForm: contactDetailInitialForm,
defaultErrors: {},
mode: 'create',
modeOpt: 'default',
saveEndpoint: null,
editEndpoint: null,
});
let showDetailForm = $state(false);
let editingDetailIndex = $state(null);
let isEditingDetail = $derived(editingDetailIndex !== null);
const helpers = useDictionaryForm(formState);
let showConfirm = $state(false);
function getLabel(fieldKey, value) {
if (!detailFormState.selectOptions?.[fieldKey]) return value;
const option = detailFormState.selectOptions[fieldKey].find(opt => opt.value === value);
return option?.label || value || "-";
}
$effect(() => {
untrack(() => {
formFields.forEach(group => {
group.rows.forEach(row => {
row.columns.forEach(col => {
if (col.type === "group") {
col.columns.forEach(child => {
if (child.type === "select" && child.optionsEndpoint) {
formState.fetchOptions(child, formState.form);
}
});
} else if ((col.type === "select") && col.optionsEndpoint) {
formState.fetchOptions(col, formState.form);
}
});
});
});
});
});
$effect(() => {
untrack(() => {
contactDetailFormFields.forEach(group => {
group.rows.forEach(row => {
row.columns.forEach(col => {
if (col.type === "select" && col.optionsEndpoint) {
detailFormState.fetchOptions(col, detailFormState.form);
}
});
});
});
});
});
let editedDetails = $state(masterDetail.selectedItem?.data?.Details || []);
async function handleEdit() {
const originalDetails = masterDetail.selectedItem?.data?.Details || [];
const currentPayload = $state.snapshot({ ...formState.form, Details: editedDetails });
const originalPayload = $state.snapshot({ ...masterDetail.formSnapshot, Details: originalDetails });
console.log('Current Payload:', editedDetails);
console.log('Original Payload:', originalDetails);
// const customPayload = {
// ...formState.form,
// Details: masterDetail.selectedItem?.data?.Details || []
// };
// console.log('Custom Payload for Edit:', JSON.stringify(customPayload));
// const result = await formState.save(masterDetail.mode, customPayload);
// ***
const changedFields = getChangedFields(originalPayload, currentPayload);
console.log('Changed Fields:', JSON.stringify(changedFields));
// if (Object.keys(changedFields).length === 0) {
// toast('No changes detected');
// return;
// }
// const payload = {
// ContactID: formState.form.ContactID,
// ...changedFields
// };
// console.log('Custom Payload for Edit:', payload);
// ***
// const result = await formState.save(masterDetail.mode, payload);
// if (result.status === 'success') {
// console.log('Contact updated successfully');
// toast('Contact Updated!');
// masterDetail.exitForm(true);
// } else {
// console.error('Failed to update contact:', result.message);
// }
}
const primaryAction = $derived({
label: 'Edit',
onClick: handleEdit,
disabled: helpers.hasErrors || formState.isSaving.current,
loading: formState.isSaving.current
});
const secondaryActions = [];
const actionsDetail = [
{
Icon: PlusIcon,
label: 'Add Contact Detail',
onClick: () => addDetail(),
},
];
function addDetail() {
editingDetailIndex = null; // Mode create baru
detailFormState.reset(); // Reset form ke initialForm
detailFormState.setForm({ ...contactDetailInitialForm }); // Set form kosong
showDetailForm = true;
}
async function saveDetail() {
// Ambil current form dari detailFormState.form
const newDetail = { ...detailFormState.form };
if (isEditingDetail) {
// Mode edit: update detail yang ada
masterDetail.selectedItem.data.Details[editingDetailIndex] = newDetail;
toast('Contact Detail Updated!');
} else {
// Mode create: tambah detail baru
if (!masterDetail.selectedItem.data.Details) {
masterDetail.selectedItem.data.Details = [];
}
masterDetail.selectedItem.data.Details.push(newDetail);
toast('Contact Detail Added!');
}
// Reset form dan tutup form
detailFormState.reset();
detailFormState.setForm({ ...contactDetailInitialForm });
editingDetailIndex = null;
showDetailForm = false;
}
function editDetail(index) {
const detailToEdit = masterDetail.selectedItem.data.Details[index];
editingDetailIndex = index; // Set mode edit dengan index
detailFormState.setForm({ ...detailToEdit }); // Load data ke form
showDetailForm = true;
}
function cancelDetail() {
detailFormState.reset();
detailFormState.setForm({ ...contactDetailInitialForm });
editingDetailIndex = null;
showDetailForm = false;
}
function removeDetail(index) {
masterDetail.selectedItem.data.Details = masterDetail.selectedItem.data.Details.filter((_, i) => i !== index);
toast('Contact Detail Removed!');
// Jika sedang mengedit detail yang dihapus, reset form
if (editingDetailIndex === index) {
cancelDetail();
} else if (editingDetailIndex !== null && editingDetailIndex > index) {
// Adjust index jika mengedit detail setelah yang dihapus
editingDetailIndex--;
}
}
</script>
<FormPageContainer title="Edit Contact" {primaryAction} {secondaryActions}>
<DictionaryFormRenderer
{formState}
formFields={formFields}
mode="edit"
/>
<Separator class="my-4"/>
<div class="flex flex-col px-2 py-1 gap-2 h-fit w-full">
<TopbarWrapper
title="Contact Detail"
actions={actionsDetail}
/>
<div class="flex flex-col gap-4">
{#if showDetailForm}
<Card.Root class="w-full gap-2 2xl:gap-4 py-2 2xl:py-4">
<Card.Content class="space-y-3">
<DictionaryFormRenderer
formState={detailFormState}
formFields={contactDetailFormFields}
/>
</Card.Content>
<Card.Footer class="flex justify-end flex-end gap-2">
<Button size="sm" onclick={saveDetail}>
{isEditingDetail ? 'Update Detail' : 'Save Detail'}
</Button>
<Button size="sm" variant="outline" onclick={cancelDetail}>Cancel</Button>
</Card.Footer>
</Card.Root>
{/if}
{#each masterDetail.selectedItem?.data?.Details as contactdetail, index}
<Card.Root class="w-full gap-2 2xl:gap-4 py-2 2xl:py-4">
<Card.Header>
<div class="flex items-start justify-between">
<div class="space-y-1">
<Card.Title class="text-sm font-medium">
{contactdetail.ContactCode || "null"}
</Card.Title>
<Card.Description class="text-sm font-medium">
{contactdetail.ContactEmail || "null"}
</Card.Description>
</div>
<div class="flex items-center gap-4">
<Badge variant="outline" class="text-xs">
{getLabel('SiteID', contactdetail.SiteID)}
</Badge>
<div>
<Button
size="icon"
variant="ghost" class="size-7"
onclick={() => editDetail(index)}
>
<Edit2Icon class="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost" class="size-7"
onclick={() => removeDetail(index)}
>
<XIcon class="h-4 w-4" />
</Button>
</div>
</div>
</div>
</Card.Header>
<Card.Content class="space-y-3">
<div class="grid grid-cols-3 gap-3">
<div class="space-y-1">
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Department
</p>
<p class="text-sm font-medium">
{getLabel('Department', contactdetail.Department)}
</p>
</div>
<div class="space-y-1">
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Job Title
</p>
<p class="text-sm font-medium">
{contactdetail.JobTitle || "-"}
</p>
</div>
<div class="space-y-1">
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Occupation
</p>
<p class="text-sm font-medium">
<!-- {contactdetail.OccupationID || "-"} -->
{getLabel('OccupationID', contactdetail.OccupationID)}
</p>
</div>
</div>
</Card.Content>
</Card.Root>
{/each}
</div>
</div>
</FormPageContainer>
<ReusableAlertDialog
bind:open={masterDetail.showExitConfirm}
onConfirm={masterDetail.confirmExit}
/>

View File

@ -21,6 +21,11 @@
const { formState } = masterDetail;
let editingId = $state(null);
let idCounter = $state(0);
let tempDetailContact = $state([]);
let deletedDetailIds = $state([]);
const contactDetailFormState = useForm({
schema: contactDetailSchema,
initialForm: contactDetailInitialForm,
@ -31,158 +36,7 @@
let showConfirm = $state(false);
let editingId = $state(null);
let idCounter = $state(0);
let tempDetailContact = $state([]);
let deletedDetailIds = $state([]);
function getLabel(fieldKey, value) {
if (!contactDetailFormState.selectOptions?.[fieldKey]) return value;
const option = contactDetailFormState.selectOptions[fieldKey].find(opt => opt.value === value);
return option?.label || value || "-";
}
$effect(() => {
untrack(() => {
formFields.forEach(group => {
group.rows.forEach(row => {
row.columns.forEach(col => {
if (col.type === "group") {
col.columns.forEach(child => {
if (child.type === "select" && child.optionsEndpoint) {
formState.fetchOptions(child, formState.form);
}
});
} else if ((col.type === "select") && col.optionsEndpoint) {
formState.fetchOptions(col, formState.form);
}
});
});
});
});
});
$effect(() => {
untrack(() => {
contactDetailFormFields.forEach(group => {
group.rows.forEach(row => {
row.columns.forEach(col => {
if (col.type === "select" && col.optionsEndpoint) {
contactDetailFormState.fetchOptions(col, contactDetailFormState.form);
}
});
});
});
});
});
// let editedDetails = $state(masterDetail.selectedItem?.data?.Details || []);
function diffDetails(current, original) {
const originalMap = new Map(
original.map(item => [item.ContactDetID, item])
);
const updated = [];
for (const item of current) {
const orig = originalMap.get(item.ContactDetID);
if (!orig) continue;
const changed = Object.keys(item).some(
key => item[key] !== orig[key]
);
if (changed) updated.push(item);
}
return updated;
}
async function handleEdit() {
const currentPayload = buildContactPayload({
mainForm: formState.form,
tempDetailContact
});
const originalPayload = buildContactPayload({
mainForm: masterDetail.formSnapshot,
tempDetailContact: masterDetail.formSnapshot.Details
});
const updatedDetails = diffDetails(
currentPayload.Details,
originalPayload.Details
);
const finalPayload = {
...getChangedFields(originalPayload, currentPayload),
Details: {
created: tempDetailContact.filter(r => !r.ContactDetID),
updated: updatedDetails,
deleted: deletedDetailIds.map(id => ({
ContactDetID: id
}))
}
};
console.log(finalPayload);
// console.log('Original Payload:', JSON.stringify(originalPayload));
// console.log('Current Payload:', JSON.stringify(currentPayload));
// console.log('Changed Fields:', getChangedFields(originalPayload, currentPayload));
// console.log(originalPayload.Details);
// console.log(currentPayload.Details);
// console.log('Diff:', diffDetails(currentPayload.Details, originalPayload.Details));
// const originalDetails = masterDetail.selectedItem?.data?.Details || [];
// const currentPayload = $state.snapshot({ ...formState.form, Details: editedDetails });
// const originalPayload = $state.snapshot({ ...masterDetail.formSnapshot, Details: originalDetails });
// console.log('Current Payload:', editedDetails);
// console.log('Original Payload:', originalDetails);
// // const customPayload = {
// // ...formState.form,
// // Details: masterDetail.selectedItem?.data?.Details || []
// // };
// // console.log('Custom Payload for Edit:', JSON.stringify(customPayload));
// // const result = await formState.save(masterDetail.mode, customPayload);
// // ***
// const changedFields = getChangedFields(originalPayload, currentPayload);
// console.log('Changed Fields:', JSON.stringify(changedFields));
// // if (Object.keys(changedFields).length === 0) {
// // toast('No changes detected');
// // return;
// // }
// // const payload = {
// // ContactID: formState.form.ContactID,
// // ...changedFields
// // };
// // console.log('Custom Payload for Edit:', payload);
// // ***
// // const result = await formState.save(masterDetail.mode, payload);
// // if (result.status === 'success') {
// // console.log('Contact updated successfully');
// // toast('Contact Updated!');
// // masterDetail.exitForm(true);
// // } else {
// // console.error('Failed to update contact:', result.message);
// // }
}
const primaryAction = $derived({
label: 'Edit',
onClick: handleEdit,
disabled: helpers.hasErrors || formState.isSaving.current,
loading: formState.isSaving.current
});
const secondaryActions = [];
function snapshotForm() {
function snapshotForm() {
return untrack(() => {
const f = contactDetailFormState.form;
return {
@ -228,17 +82,6 @@
});
}
// function handleUpdateDetail() {
// tempDetailContact = tempDetailContact.map((row) =>
// row.id === editingId ?
// {
// ...row,
// ...snapshotForm()
// } : row
// );
// resetContactDetailForm();
// }
function handleUpdateDetail() {
const updated = snapshotForm();
@ -270,6 +113,144 @@
}
}
function getLabel(fieldKey, value) {
if (!contactDetailFormState.selectOptions?.[fieldKey]) return value;
const option = contactDetailFormState.selectOptions[fieldKey].find(opt => opt.value === value);
return option?.label || value || "-";
}
$effect(() => {
untrack(() => {
formFields.forEach(group => {
group.rows.forEach(row => {
row.columns.forEach(col => {
if ((col.type === "select") && col.optionsEndpoint) {
formState.fetchOptions(col, formState.form);
}
});
});
});
});
});
$effect(() => {
untrack(() => {
contactDetailFormFields.forEach(group => {
group.rows.forEach(row => {
row.columns.forEach(col => {
if ((col.type === "select") && col.optionsEndpoint) {
contactDetailFormState.fetchOptions(col, contactDetailFormState.form);
}
});
});
});
});
});
// function diffDetails(current, original) {
// const originalMap = new Map(
// original.map(item => [item.ContactDetID, item])
// );
// const updated = [];
// for (const item of current) {
// const orig = originalMap.get(item.ContactDetID);
// if (!orig) continue;
// const changed = Object.keys(item).some(
// key => item[key] !== orig[key]
// );
// if (changed) updated.push(item);
// }
// return updated;
// }
function diffDetails(currentRows, originalRows) {
const originalMap = new Map(
originalRows
.filter(item => item.ContactDetID)
.map(item => [item.ContactDetID, item])
);
const updated = [];
const detailKeys = ['SiteID', 'ContactCode', 'ContactEmail', 'OccupationID', 'JobTitle', 'Department'];
for (const item of currentRows) {
if (!item.ContactDetID) continue;
const orig = originalMap.get(item.ContactDetID);
if (!orig) continue;
const changed = detailKeys.some(
key => item[key] !== orig[key]
);
if (changed) updated.push(item);
}
return updated;
}
async function handleEdit() {
const currentPayload = buildContactPayload({
mainForm: formState.form,
tempDetailContact
});
const originalPayload = buildContactPayload({
mainForm: masterDetail.formSnapshot,
tempDetailContact: masterDetail.formSnapshot.Details ?? []
});
const originalRows = masterDetail.formSnapshot.Details ?? [];
const updatedDetails = diffDetails(tempDetailContact, originalRows);
const changedFields = getChangedFields(originalPayload, currentPayload);
const hasMainChanges = Object.keys(changedFields).length > 0;
const hasDetailChanges = updatedDetails.length > 0 || tempDetailContact.some(r => !r.ContactDetID) || deletedDetailIds.length > 0;
if (!hasMainChanges && !hasDetailChanges) {
toast('No changes detected');
return;
}
const finalPayload = {
ContactID: formState.form.ContactID,
...changedFields,
...(hasDetailChanges && {
Details: {
created: tempDetailContact.filter(r => !r.ContactDetID),
edited: updatedDetails,
deleted: deletedDetailIds
}
})
};
console.log(finalPayload);
const result = await formState.save(masterDetail.mode, finalPayload);
if (result.status === 'success') {
console.log('Contact updated successfully');
toast('Contact Updated!');
masterDetail.exitForm(true);
} else {
console.error('Failed to update contact:', result.message);
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to update contact';
toast.error(errorMessages)
}
}
const primaryAction = $derived({
label: 'Edit',
onClick: handleEdit,
disabled: helpers.hasErrors || formState.isSaving.current,
loading: formState.isSaving.current
});
const secondaryActions = [];
$effect(() => {
const mainForm = formState.form;
if (mainForm.Details && Array.isArray(mainForm.Details)) {

View File

@ -8,12 +8,15 @@
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
import UserXIcon from "@lucide/svelte/icons/user-x";
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
let props = $props();
const search = useSearch(searchFields, getContacts);
const initialForm = props.masterDetail.formState.form;
const actions = contactActions(props.masterDetail, initialForm)
const handlers = {
refresh: () => {search.handleSearch()},
};
const actions = contactActions(props.masterDetail, handlers)
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
let activeRowId = $state(null);
@ -38,10 +41,13 @@
>
<div class={`flex w-full ${props.masterDetail.isFormMode ? "flex-col justify-center h-full items-center" : "flex-col justify-start h-full"}`} >
{#if props.masterDetail.isFormMode}
<span class="flex flex-col items-center justify-center gap-4 tracking-widest font-semibold select-none">
{#each "CONTACT".split("") as c}
<span class="leading-none">{c}</span>
{/each}
<span class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "CONTACT".split("") as c}
<span class="leading-none">{c}</span>
{/each}
</div>
</span>
{/if}

View File

@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
import PencilIcon from "@lucide/svelte/icons/pencil";
import { API } from "$lib/config/api";
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
export const searchFields = [
{
@ -67,8 +68,13 @@ export const detailSections = [
},
];
export function locationActions(masterDetail) {
export function locationActions(masterDetail, handlers) {
return [
{
Icon: RefreshIcon,
label: 'Refresh Data',
onClick: handlers.refresh,
},
{
Icon: PlusIcon,
label: 'Add Location',

View File

@ -8,12 +8,15 @@
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
import MapPinXIcon from "@lucide/svelte/icons/map-pin-x";
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
let props = $props();
const search = useSearch(searchFields, getLocations);
const initialForm = props.masterDetail.formState.form;
const actions = locationActions(props.masterDetail, initialForm)
const handlers = {
refresh: () => {search.handleSearch()},
};
const actions = locationActions(props.masterDetail, handlers)
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
let activeRowId = $state(null);
@ -38,10 +41,13 @@
>
<div class={`flex w-full ${props.masterDetail.isFormMode ? "flex-col justify-center h-full items-center" : "flex-col justify-start h-full"}`} >
{#if props.masterDetail.isFormMode}
<span class="flex flex-col items-center justify-center gap-4 tracking-widest font-semibold select-none">
{#each "LOCATION".split("") as c}
<span class="leading-none">{c}</span>
{/each}
<span class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "LOCATION".split("") as c}
<span class="leading-none">{c}</span>
{/each}
</div>
</span>
{/if}

View File

@ -1,6 +1,7 @@
import PlusIcon from "@lucide/svelte/icons/plus";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
import PencilIcon from "@lucide/svelte/icons/pencil";
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
export const searchFields = [
{
@ -37,8 +38,13 @@ export const detailSections = [
},
];
export function occupationActions(masterDetail) {
export function occupationActions(masterDetail, handlers) {
return [
{
Icon: RefreshIcon,
label: 'Refresh Data',
onClick: handlers.refresh,
},
{
Icon: PlusIcon,
label: 'Add Location',

View File

@ -8,20 +8,23 @@
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
import FolderXIcon from "@lucide/svelte/icons/folder-x";
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
let props = $props();
const search = useSearch(searchFields, getOccupations);
const initialForm = props.masterDetail.formState.form;
const actions = occupationActions(props.masterDetail, initialForm)
const handlers = {
refresh: () => {search.handleSearch()},
};
const actions = occupationActions(props.masterDetail, handlers)
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
let activeRowId = $state(null);
</script>
{#snippet searchParamSnippet()}
{#snippet searchParamSnippet(close)}
<ReusableSearchParam {searchFields}
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
/>
{/snippet}
@ -38,10 +41,13 @@
>
<div class={`flex w-full ${props.masterDetail.isFormMode ? "flex-col justify-center h-full items-center" : "flex-col justify-start h-full"}`} >
{#if props.masterDetail.isFormMode}
<span class="flex flex-col items-center justify-center gap-4 tracking-widest font-semibold select-none">
{#each "OCCUPATION".split("") as c}
<span class="leading-none">{c}</span>
{/each}
<span class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "OCCUPATION".split("") as c}
<span class="leading-none">{c}</span>
{/each}
</div>
</span>
{/if}

View File

@ -4,6 +4,7 @@
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import FolderXIcon from "@lucide/svelte/icons/folder-x";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props();
@ -46,7 +47,11 @@
</div>
{/snippet}
{#if masterDetail.selectedItem}
{#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={masterDetail.selectedItem.data.OccText}

View File

@ -77,7 +77,7 @@ export const detailSections = [
{ parentKey: "Custodian", key: "PatientID", label: "Custodian ID" },
],
},
{ key: "isDeadLabel", label: "Deceased" },
{ key: "isDead", label: "Deceased", valueMap: { 0: "N", 1: "Y" } },
{ key: "CreateDate", label: "Create Date", isUTCDate: true },
{ key: "DelDate", label: "Delete Date" },
{ key: "TimeOfDeath", label: "Time of Death", isUTCDate: true },

View File

@ -3,7 +3,7 @@ import EraserIcon from "@lucide/svelte/icons/eraser";
import { z } from "zod";
export const patientSchema = z.object({
PatientID: z.string().min(1, "Required"),
PatientID: z.string().min(1, "Required").regex(/^[A-Za-z0-9.-]+$/, "Only letters, numbers, '-' and '.' are allowed"),
Sex: z.string().min(1, "Required"),
NameFirst: z.string().min(1, "Required"),
Birthdate: z.string().min(1, "Required").refine(
@ -56,7 +56,7 @@ export const patientInitialForm = {
Phone: "",
EmailAddress2: "",
MobilePhone: "",
isDead: "",
isDead: 0,
TimeOfDeath: "",
LinkTo: [],
PatCom: "",

View File

@ -30,7 +30,7 @@
showConfirm = true;
}
}
// $inspect(formState.form)
function confirmDiscard() {
masterDetail.exitForm(true);
}
@ -68,7 +68,6 @@
disabled: formState.isSaving.current
}
];
$inspect(formState.errors)
</script>
<FormPageContainer title="Create Patient" {primaryAction} {secondaryActions} {actions}>

View File

@ -42,8 +42,14 @@
.filter(val => val && val.trim() !== "")
.join(" / ");
}
let value = field.parentKey ? patient[field.parentKey]?.[field.key] : patient[field.key];
return field.parentKey ? patient[field.parentKey]?.[field.key] : patient[field.key];
if (field.valueMap && value != null) {
value = field.valueMap[value] ?? value;
}
// return field.parentKey ? patient[field.parentKey]?.[field.key] : patient[field.key];
return value;
}
</script>

View File

@ -7,6 +7,8 @@
let props = $props();
const { Icon } = props;
let open = $state(false);
</script>
<Tooltip.Provider delayDuration={100}>
@ -14,7 +16,7 @@
<Tooltip.Trigger>
{#snippet child({ props: tooltipProps })}
{#if props.popoverContent}
<Popover.Root>
<Popover.Root bind:open>
<Popover.Trigger>
{#snippet child({ props: popoverProps })}
<Button
@ -28,7 +30,8 @@
{/snippet}
</Popover.Trigger>
<Popover.Content collisionPadding={props.collisionPadding ?? 0} class={props.popoverWidth ?? "w-72"}>
{@render props.popoverContent()}
<!-- {@render props.popoverContent()} -->
{@render props.popoverContent(() => open = false)}
</Popover.Content>
</Popover.Root>
{:else}

View File

@ -20,6 +20,7 @@
modeOpt: 'default',
saveEndpoint: createContact,
editEndpoint: editContact,
idKey: 'ContactID',
}
});