mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-28 07:05:53 +07:00
continue location dict
This commit is contained in:
parent
4f61832f19
commit
9cada5fbd4
12
src/lib/components/composable/use-dictionary-form.svelte.js
Normal file
12
src/lib/components/composable/use-dictionary-form.svelte.js
Normal 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 },
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,45 @@ export const searchFields = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const detailSections = [
|
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) {
|
export function locationActions(masterDetail) {
|
||||||
|
|||||||
@ -2,7 +2,10 @@ import { API } from "$lib/config/api";
|
|||||||
import EraserIcon from "@lucide/svelte/icons/eraser";
|
import EraserIcon from "@lucide/svelte/icons/eraser";
|
||||||
import { z } from "zod";
|
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 = {
|
export const locationInitialForm = {
|
||||||
LocationID: '',
|
LocationID: '',
|
||||||
@ -23,8 +26,7 @@ export const locationInitialForm = {
|
|||||||
|
|
||||||
export const locationDefaultErrors = {
|
export const locationDefaultErrors = {
|
||||||
LocCode: "Required",
|
LocCode: "Required",
|
||||||
LocName: "Required",
|
LocFull: "Required",
|
||||||
LocType: "Required",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const locationFormFields = [
|
export const locationFormFields = [
|
||||||
@ -37,7 +39,7 @@ export const locationFormFields = [
|
|||||||
{
|
{
|
||||||
key: "SiteID",
|
key: "SiteID",
|
||||||
label: "Site ID",
|
label: "Site ID",
|
||||||
required: true,
|
required: false,
|
||||||
type: "select",
|
type: "select",
|
||||||
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
|
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
|
||||||
},
|
},
|
||||||
@ -48,21 +50,26 @@ export const locationFormFields = [
|
|||||||
type: "text",
|
type: "text",
|
||||||
validateOn: ["input"]
|
validateOn: ["input"]
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
{
|
{
|
||||||
key: "LocType",
|
key: "LocType",
|
||||||
label: "Location Type",
|
label: "Location Type",
|
||||||
required: true,
|
required: false,
|
||||||
type: "text",
|
type: "text",
|
||||||
validateOn: ["input"]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "LocFull",
|
key: "LocFull",
|
||||||
label: "Location Name",
|
label: "Location Name",
|
||||||
required: false,
|
required: true,
|
||||||
type: "text",
|
type: "text",
|
||||||
|
validateOn: ["input"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -78,25 +85,26 @@ export const locationFormFields = [
|
|||||||
type: "select",
|
type: "select",
|
||||||
optionsEndpoint: `${API.BASE_URL}${API.PROVINCE}`,
|
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",
|
key: "Street1",
|
||||||
label: "Street 1",
|
label: "Street 1",
|
||||||
required: false,
|
required: false,
|
||||||
type: "text",
|
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",
|
key: "Street2",
|
||||||
label: "Street 2",
|
label: "Street 2",
|
||||||
@ -105,7 +113,17 @@ export const locationFormFields = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "ZIP",
|
||||||
|
label: "ZIP",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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}
|
||||||
|
/>
|
||||||
@ -55,7 +55,7 @@
|
|||||||
<TopbarWrapper {actions}/>
|
<TopbarWrapper {actions}/>
|
||||||
<div class="flex-1 w-full h-full">
|
<div class="flex-1 w-full h-full">
|
||||||
{#if search.searchData.length > 0}
|
{#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}
|
{:else}
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<ReusableEmpty icon={MapPinXIcon} desc="Try searching from search parameters"/>
|
<ReusableEmpty icon={MapPinXIcon} desc="Try searching from search parameters"/>
|
||||||
|
|||||||
@ -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}
|
||||||
@ -24,16 +24,16 @@
|
|||||||
|
|
||||||
let showConfirm = $state(false);
|
let showConfirm = $state(false);
|
||||||
|
|
||||||
function handleExit() {
|
// function handleExit() {
|
||||||
const ok = masterDetail.exitForm();
|
// const ok = masterDetail.exitForm();
|
||||||
if (!ok) {
|
// if (!ok) {
|
||||||
showConfirm = true;
|
// showConfirm = true;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
function confirmDiscard() {
|
// function confirmDiscard() {
|
||||||
masterDetail.exitForm(true);
|
// masterDetail.exitForm(true);
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
const payload = buildPayload(formState.form);
|
const payload = buildPayload(formState.form);
|
||||||
|
|||||||
@ -9,14 +9,6 @@
|
|||||||
let props = $props();
|
let props = $props();
|
||||||
|
|
||||||
let loadedFields = $state(new Set());
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
@ -55,58 +47,6 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user