continue location dict

This commit is contained in:
faiztyanirh 2026-02-14 14:26:01 +07:00
parent 4f61832f19
commit 9cada5fbd4
8 changed files with 256 additions and 93 deletions

View File

@ -0,0 +1,12 @@
export function useDictionaryForm(formState) {
let uploadErrors = $state({});
let isChecking = $state({});
let hasErrors = $derived(
Object.values(formState.errors).some(value => value !== null)
);
return {
get hasErrors() { return hasErrors },
}
}

View File

@ -22,6 +22,45 @@ export const searchFields = [
];
export const detailSections = [
{
class: "grid grid-cols-2 gap-4 items-center",
fields: [
{ key: "SiteID", label: "Site" },
{ key: "Parent", label: "Parent" },
{ key: "LocCode", label: "Location Code" },
{ key: "LocFull", label: "Location Name" },
{ key: "LocType", label: "Location Type" },
{ key: "Description", label: "Description" },
]
},
{
title: "",
class: "grid grid-cols-1 md:grid-cols-2 gap-4",
groups: [
{
class: "space-y-3",
fields: [
{ key: "Province", label: "Province" },
{ key: "City", label: "City" },
{ key: "PostCode", label: "ZIP" },
]
},
{
class: "space-y-3",
fields: [
{ key: "Street1", label: "Street 1" },
{ key: "Street2", label: "Street 2" },
]
}
]
},
{
class: "grid grid-cols-2 gap-4 items-center",
fields: [
{ key: "GeoLocationSystem", label: "Geo Location System" },
{ key: "GeoLocationData", label: "Geo Location Data" },
]
},
];
export function locationActions(masterDetail) {

View File

@ -2,7 +2,10 @@ import { API } from "$lib/config/api";
import EraserIcon from "@lucide/svelte/icons/eraser";
import { z } from "zod";
export const locationSchema = z.object({});
export const locationSchema = z.object({
LocCode: z.string().min(1, "Required"),
LocFull: z.string().min(1, "Required"),
});
export const locationInitialForm = {
LocationID: '',
@ -23,8 +26,7 @@ export const locationInitialForm = {
export const locationDefaultErrors = {
LocCode: "Required",
LocName: "Required",
LocType: "Required",
LocFull: "Required",
};
export const locationFormFields = [
@ -37,7 +39,7 @@ export const locationFormFields = [
{
key: "SiteID",
label: "Site ID",
required: true,
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
},
@ -48,21 +50,26 @@ export const locationFormFields = [
type: "text",
validateOn: ["input"]
},
]
},
{
type: "row",
columns: [
{
key: "LocType",
label: "Location Type",
required: true,
required: false,
type: "text",
validateOn: ["input"]
},
{
key: "LocFull",
label: "Location Name",
required: false,
required: true,
type: "text",
validateOn: ["input"]
}
]
}
},
]
},
{
@ -78,25 +85,26 @@ export const locationFormFields = [
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.PROVINCE}`,
},
{
key: "City",
label: "City",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.CITY}`,
},
{
key: "ZIP",
label: "ZIP",
required: false,
type: "text",
},
{
key: "Street1",
label: "Street 1",
required: false,
type: "text",
},
]
},
{
type: "row",
columns: [
{
key: "City",
label: "City",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.CITY}`,
dependsOn: "Province",
endpointParamKey: "Parent"
},
{
key: "Street2",
label: "Street 2",
@ -105,7 +113,17 @@ export const locationFormFields = [
}
]
},
{
type: "row",
columns: [
{
key: "ZIP",
label: "ZIP",
required: false,
type: "text",
},
]
},
]
},
{

View File

@ -0,0 +1,64 @@
<script>
import { useDictionaryForm } from "$lib/components/composable/use-dictionary-form.svelte";
import FormPageContainer from "$lib/components/patient/reusable/form-page-container.svelte";
import PatientFormRenderer from "$lib/components/patient/reusable/patient-form-renderer.svelte";
import { toast } from "svelte-sonner";
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
const { formState } = masterDetail;
const helpers = useDictionaryForm(formState);
const handlers = {
clearForm: () => {
formState.reset();
}
};
const actions = formActions(handlers);
let showConfirm = $state(false);
async function handleSave() {
const result = await formState.save(masterDetail.mode);
toast('Location Created!');
masterDetail?.exitForm(true);
}
const primaryAction = $derived({
label: 'Save',
onClick: handleSave,
disabled: helpers.hasErrors || formState.isSaving.current,
loading: formState.isSaving.current
});
const secondaryActions = [];
$effect(() => {
if (masterDetail.form?.PatientID) {
formState.setForm({
...formState.form,
...masterDetail.form
});
}
});
$inspect(formState.errors)
</script>
<FormPageContainer title="Create Location" {primaryAction} {secondaryActions} {actions}>
<PatientFormRenderer
{formState}
formFields={formFields}
mode="create"
/>
</FormPageContainer>
<ReusableAlertDialog
bind:open={masterDetail.showExitConfirm}
onConfirm={masterDetail.confirmExit}
/>

View File

@ -55,7 +55,7 @@
<TopbarWrapper {actions}/>
<div class="flex-1 w-full h-full">
{#if search.searchData.length > 0}
<ReusableDataTable data={search.searchData} columns={columns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="InternalPID"/>
<ReusableDataTable data={search.searchData} columns={locationColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="LocationID"/>
{:else}
<div class="flex h-full">
<ReusableEmpty icon={MapPinXIcon} desc="Try searching from search parameters"/>

View File

@ -1 +1,91 @@
vw
<script>
import { formatUTCDate } from "$lib/utils/formatUTCDate";
import { detailSections, viewActions } from "$lib/components/dictionary/location/config/location-config";
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import MapPinXIcon from "@lucide/svelte/icons/map-pin-x";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
let location = $derived(masterDetail?.selectedItem?.data);
const handlers = {
editPatient: () => masterDetail.enterEdit("data"),
};
const actions = viewActions(handlers);
function getFieldValue(field) {
if (!location) return "-";
if (field.keys) {
return field.keys
.map(k => field.parentKey ? location[field.parentKey]?.[k] : location[k])
.filter(val => val && val.trim() !== "")
.join(" / ");
}
return field.parentKey ? location[field.parentKey]?.[field.key] : location[field.key];
}
</script>
{#snippet Fieldset({ value, label, isUTCDate = false })}
<div class="space-y-1.5">
<dt class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{label}
</dt>
<dd class="text-sm font-medium">
{#if isUTCDate}
{formatUTCDate(value)}
{:else}
{value ?? "-"}
{/if}
</dd>
</div>
{/snippet}
{#if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={masterDetail.selectedItem.data.LocFull}
{actions}
/>
<div class="flex-1 min-h-0 overflow-y-auto space-y-4">
{#each detailSections as section}
<div class="p-4">
{#if section.groups}
<div class={section.class}>
{#each section.groups as group}
<div>
<div class={group.class}>
{#each group.fields as field}
{@render Fieldset({
label: field.label,
value: getFieldValue(field),
isUTCDate: field.isUTCDate
})}
{/each}
</div>
</div>
{/each}
</div>
{:else}
<div class={section.class}>
{#each section.fields as field}
{@render Fieldset({
label: field.label,
value: getFieldValue(field),
isUTCDate: field.isUTCDate
})}
{/each}
</div>
{/if}
</div>
{/each}
</div>
</div>
{:else}
<ReusableEmpty icon={MapPinXIcon} desc="Select a location to see details"/>
{/if}

View File

@ -24,16 +24,16 @@
let showConfirm = $state(false);
function handleExit() {
const ok = masterDetail.exitForm();
if (!ok) {
showConfirm = true;
}
}
// function handleExit() {
// const ok = masterDetail.exitForm();
// if (!ok) {
// showConfirm = true;
// }
// }
function confirmDiscard() {
masterDetail.exitForm(true);
}
// function confirmDiscard() {
// masterDetail.exitForm(true);
// }
async function handleSave() {
const payload = buildPayload(formState.form);

View File

@ -9,14 +9,6 @@
let props = $props();
let loadedFields = $state(new Set());
function handleOpenSelect(field) {
if (loadedFields.has(field.key)) return;
loadedFields.add(field.key);
props.fetchOptions(field);
}
$inspect(props.searchQuery)
</script>
<div class="w-full">
@ -55,58 +47,6 @@
{/each}
</Select.Content>
</Select.Root>
<!-- === -->
<!-- <Select.Root type="single" bind:value={props.searchQuery[field.key]}
onOpenChange={(open) => {
if (open && optionsEndpoint) {
formState.fetchOptions(
{ key, optionsEndpoint, dependsOn, endpointParamKey, valueKey, labelKey },
formState.form
);
}
}}
>
<Select.Trigger class="w-full truncate">
{selectedLabel}
</Select.Trigger>
<Select.Content>
{#if formState.loadingOptions[key]}
<Select.Item disabled value="loading">Loading...</Select.Item>
{:else}
{#if !required}
<Select.Item value="">- None -</Select.Item>
{/if}
{#each selectOptions as option}
<Select.Item value={option.value}>
{option.label}
</Select.Item>
{/each}
{/if}
</Select.Content>
</Select.Root> -->
<!-- === -->
<!-- <Select.Root bind:value={props.searchQuery[field.key]}>
<Select.Trigger id={field.key}>
<Select.Value placeholder={`Select ${field.label}`} />
</Select.Trigger>
<Select.Content>
{#if props.loadingOptions[field.key]}
<Select.Item value="" disabled>
Loading...
</Select.Item>
{:else}
<Select.Item value="">
All {field.label}
</Select.Item>
{#each props.selectOptions[field.key] || [] as option}
<Select.Item value={option.value}>
{option.label}
</Select.Item>
{/each}
{/if}
</Select.Content>
</Select.Root> -->
</div>
{/if}
{/each}