continue fixing testmap

add disable button
change search param
fix quick filter not working
fix host/client name in viewpage
change text button Edit to Save
initial multi form formSnapshot
This commit is contained in:
faiztyanirh 2026-04-15 17:32:18 +07:00
parent 1502fbd140
commit 4ed048d734
11 changed files with 294 additions and 43 deletions

View File

@ -0,0 +1,166 @@
import { useResponsive } from "./use-responsive.svelte.js";
import { useForm } from "./use-form.svelte.js";
import { tick } from "svelte";
export function useMasterDetail(options = {}) {
const { onSelect = null, formConfig = null, } = options;
let selectedItem = $state(null);
let mode = $state("view");
let isLoadingDetail = $state(false);
let formSnapshot = $state(null);
let showExitConfirm = $state(false);
const formState = useForm(formConfig);
const { isMobile } = useResponsive();
const isFormMode = $derived(mode === "create" || mode === "edit");
const showMaster = $derived(!isMobile || (mode === "view" && !selectedItem));
const showDetail = $derived(!isMobile || selectedItem || isFormMode);
const layout = $derived({
masterWidth: isMobile ? "w-full" : isFormMode ? "w-[3%]" : "w-[35%]",
detailWidth: isMobile ? "w-full" : isFormMode ? "w-[97%]" : "w-[65%]",
});
const isDirty = $derived(
JSON.stringify(formState.form) !== JSON.stringify(formSnapshot)
);
async function select(item) {
mode = "view";
if (onSelect) {
isLoadingDetail = true;
try {
const detailData = await onSelect(item);
selectedItem = detailData;
} catch (error) {
console.error("Failed to fetch detail:", error);
selectedItem = null;
} finally {
isLoadingDetail = false;
}
} else {
selectedItem = item;
}
}
async function enterCreate(initialData = null) {
mode = "create";
selectedItem = null;
formState.reset();
if (initialData) {
formState.setForm(initialData);
}
await tick();
formSnapshot = $state.snapshot(formState.form);
}
function enterEdit(param) {
if (!selectedItem) return;
mode = "edit";
const raw = param
? selectedItem[param]
: selectedItem;
if (!raw) return;
const base = $state.snapshot(raw);
const entity = formConfig.mapToForm
? formConfig.mapToForm(base)
: base;
formState.reset();
formState.setForm(entity);
Object.keys(formState.errors).forEach(key => {
formState.errors[key] = null;
});
formSnapshot = $state.snapshot(formState.form);
}
function exitForm(force = false) {
if (!force && isDirty) {
showExitConfirm = true;
return;
}
// Direct exit
mode = "view";
selectedItem = null;
formSnapshot = null;
}
function confirmExit() {
mode = "view";
selectedItem = null;
formSnapshot = null;
}
function backToList() {
selectedItem = null;
mode = "view";
}
function saveForm() {
formSnapshot = { ...form };
}
return {
get selectedItem() {
return selectedItem;
},
get mode() {
return mode;
},
get isFormMode() {
return isFormMode;
},
get isMobile() {
return isMobile;
},
get showMaster() {
return showMaster;
},
get showDetail() {
return showDetail;
},
get layout() {
return layout;
},
get formSnapshot() {
return formSnapshot;
},
// get form() {
// return form;
// },
// get isDirty() {
// return isDirty;
// },
get isLoadingDetail() {
return isLoadingDetail;
},
get formState() {
return formState;
},
get showExitConfirm() { return showExitConfirm; },
set showExitConfirm(value) { showExitConfirm = value; },
select,
enterCreate,
enterEdit,
exitForm,
confirmExit,
backToList,
saveForm,
};
}

View File

@ -3,12 +3,14 @@ import { useForm } from "./use-form.svelte.js";
import { tick } from "svelte";
export function useMasterDetail(options = {}) {
const { onSelect = null, formConfig = null, } = options;
const { onSelect = null, formConfig = null, extraState = [], } = options;
let selectedItem = $state(null);
let mode = $state("view");
let isLoadingDetail = $state(false);
let formSnapshot = $state(null);
let extraSnapshots = $state({});
let extraRegistry = $state.raw([]);
let showExitConfirm = $state(false);
const formState = useForm(formConfig);
@ -26,10 +28,30 @@ export function useMasterDetail(options = {}) {
detailWidth: isMobile ? "w-full" : isFormMode ? "w-[97%]" : "w-[65%]",
});
const isDirty = $derived(
JSON.stringify(formState.form) !== JSON.stringify(formSnapshot)
);
// $inspect(formState.form)
// const isDirty = $derived(
// JSON.stringify(formState.form) !== JSON.stringify(formSnapshot)
// );
// const isDirty = $derived(
// JSON.stringify(formState.form) !== JSON.stringify(formSnapshot) ||
// (extraRegistry.length > 0 && extraRegistry.some(({ key, get }) =>
// JSON.stringify(get()) !== JSON.stringify(extraSnapshots[key])
// ))
// );
const isDirty = $derived.by(() => {
if (JSON.stringify(formState.form) !== JSON.stringify(formSnapshot)) {
return true;
}
return extraRegistry.some((item) => {
const current = item.get();
const snapshot = extraSnapshots[item.key];
return JSON.stringify(current) !== JSON.stringify(snapshot);
});
});
$inspect(isDirty)
async function select(item) {
mode = "view";
@ -52,17 +74,27 @@ export function useMasterDetail(options = {}) {
async function enterCreate(initialData = null) {
mode = "create";
selectedItem = null;
formState.reset();
for (const { reset } of extraState) {
reset?.();
}
if (initialData) {
formState.setForm(initialData);
}
await tick();
formSnapshot = $state.snapshot(formState.form);
// extraSnapshots = extraRegistry.length > 0 ? Object.fromEntries(
// extraRegistry.map(({ key, get }) => [key, $state.snapshot(get())])
// ) : {};
const extraSnapshots = extraRegistry.length > 0
? Object.fromEntries(extraRegistry.map(item => [item.key, $state.snapshot(item.get())]))
: {};
}
function enterEdit(param) {
async function enterEdit(param) {
if (!selectedItem) return;
mode = "edit";
@ -86,6 +118,20 @@ export function useMasterDetail(options = {}) {
});
formSnapshot = $state.snapshot(formState.form);
await tick();
if (extraRegistry.length > 0) {
extraSnapshots = Object.fromEntries(
// extraRegistry.map(({ key, get }) => [key, $state.snapshot(get())])
extraRegistry.map((item) => {
const key = item.key;
const get = item.get;
return [key, $state.snapshot(get())];
})
);
}
}
function exitForm(force = false) {
@ -104,17 +150,29 @@ export function useMasterDetail(options = {}) {
mode = "view";
selectedItem = null;
formSnapshot = null;
extraSnapshots = {};
}
function backToList() {
selectedItem = null;
mode = "view";
formSnapshot = null;
extraSnapshots = {}
}
function saveForm() {
formSnapshot = { ...form };
}
function registerExtraState({ key, get, reset }) {
const exists = extraRegistry.findIndex(e => e.key === key);
if (exists !== -1) {
extraRegistry[exists] = { key, get, reset };
} else {
extraRegistry = [...extraRegistry, { key, get, reset }]
}
}
return {
get selectedItem() {
return selectedItem;
@ -143,9 +201,9 @@ export function useMasterDetail(options = {}) {
// get form() {
// return form;
// },
// get isDirty() {
// return isDirty;
// },
get isDirty() {
return isDirty;
},
get isLoadingDetail() {
return isLoadingDetail;
},
@ -162,5 +220,6 @@ export function useMasterDetail(options = {}) {
confirmExit,
backToList,
saveForm,
registerExtraState,
};
}

View File

@ -3,24 +3,21 @@ 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";
import LockKeyholeIcon from "@lucide/svelte/icons/lock-keyhole";
export const searchFields = [
{
key: "TestSiteCode",
label: "Test Code",
type: "text",
key: "HostID",
label: "Host",
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/entity_type`,
},
{
key: "TestSiteName",
label: "Test Name",
type: "text",
key: "ClientID",
label: "Client",
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/entity_type`,
},
{
key: "TestMapType",
label: "TestMap Type",
type: "text",
},
];
export const detailSections = [
@ -29,8 +26,8 @@ export const detailSections = [
fields: [
{ key: "HostTypeLabel", label: "Host Type" },
{ key: "ClientTypeLabel", label: "Client Type" },
{ key: "HostID", label: "Host ID" },
{ key: "ClientID", label: "Client ID" },
{ key: "HostName", label: "Host ID" },
{ key: "ClientName", label: "Client ID" },
{ key: "details", label: "Details", fullWidth: true },
]
},
@ -57,6 +54,11 @@ export function testMapActions(masterDetail, handlers) {
export function viewActions(handlers){
return [
{
Icon: LockKeyholeIcon,
label: 'Disable Test Map',
onClick: handlers.disableTestMap,
},
{
Icon: PencilIcon,
label: 'Edit TestMap',

View File

@ -13,6 +13,7 @@
import { Button } from "$lib/components/ui/button/index.js";
import { untrack } from "svelte";
import { API } from '$lib/config/api';
import { onMount } from "svelte";
let props = $props();
@ -241,6 +242,15 @@
}))
}))
})
onMount(() => {
masterDetail.registerExtraState({
key: 'tempMap',
get: () => tempMap,
reset: () => { tempMap = []; idCounter = 0; editingId = null; },
});
});
</script>
<FormPageContainer title="Create Test Map" {primaryAction} {secondaryActions} {actions}>

View File

@ -14,6 +14,7 @@
import { untrack } from "svelte";
import { API } from "$lib/config/api";
import { getChangedFields } from "$lib/utils/getChangedFields";
import { onMount } from "svelte";
let props = $props();
@ -215,7 +216,7 @@
}
const primaryAction = $derived({
label: 'Edit',
label: 'Save',
onClick: handleEdit,
disabled: helpers.hasErrors || formState.isSaving.current,
loading: formState.isSaving.current
@ -351,6 +352,14 @@
idCounter = maxId;
}
});
onMount(() => {
masterDetail.registerExtraState({
key: 'tempMap',
get: () => tempMap,
reset: () => { tempMap = []; idCounter = 0; editingId = null; },
});
});
</script>
<FormPageContainer title="Edit Test Map" {primaryAction} {secondaryActions}>

View File

@ -6,14 +6,17 @@
import MapIcon from "@lucide/svelte/icons/map";
import * as Table from "$lib/components/ui/table/index.js";
import { Spinner } from "$lib/components/ui/spinner/index.js";
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
let testMap = $derived(masterDetail?.selectedItem?.data);
let showDisableConfirm = $state(false);
const handlers = {
disableTestMap: () => { showDisableConfirm = true; },
editTestMap: () => masterDetail.enterEdit("data"),
};
@ -129,4 +132,14 @@
</div>
{:else}
<ReusableEmpty icon={MapIcon} desc="Select a test map to see details"/>
{/if}
{/if}
<ReusableAlertDialog
bind:open={showDisableConfirm}
title="Disable Test Map?"
description="Are you sure you want to disable this test map?"
confirmText="Disable"
onConfirm={() => {
console.log('disabled!');
}}
/>

View File

@ -1,14 +1,14 @@
export const testMapColumns = [
{
accessorKey: "HostType",
id: "Host",
accessorFn: (row) => `${row.HostType} ${row.HostName}`,
header: "Host",
cell: ({ row }) => `${row.original.HostType} - ${row.original.HostName}`
},
{
accessorKey: "ClientType",
id: "Client",
accessorFn: (row) => `${row.ClientType} ${row.ClientName}`,
header: "Client",
},
{
accessorKey: "TestMapType",
header: "Type",
cell: ({ row }) => `${row.original.ClientType} - ${row.original.ClientName}`
},
];

View File

@ -19,21 +19,13 @@
let activeRowId = $state(null);
// let isPatientEmpty = $derived(!selectedPatient);
let isPatientEmpty = $derived(!tempSelectedPatient);
// function handleCheckboxChange(patient) {
// selectedPatient = patient;
// }
function handleCheckboxChange(patient) {
tempSelectedPatient = patient;
}
function handleButtonClick() {
// if (selectedPatient) {
// props.onConfirm(selectedPatient);
// }
if (tempSelectedPatient) {
selectedPatient = tempSelectedPatient;
props.onConfirm(tempSelectedPatient);

View File

@ -53,7 +53,7 @@
});
</script>
<FormPageContainer title="Create Admission" {primaryAction} {secondaryActions} {actions}>
<FormPageContainer title="Create Admission for {formState.form?.PatientID}" {primaryAction} {secondaryActions} {actions}>
<PatientFormRenderer
{formState}
formFields={formFields}

View File

@ -25,6 +25,7 @@
function confirmDiscard() {
masterDetail.exitForm(true);
}
$inspect(formState.form)
$effect(() => {
// const backendData = masterDetail?.selectedItem.data;
@ -80,7 +81,7 @@
const secondaryActions = [];
</script>
<FormPageContainer title="Edit Admission" {primaryAction} {secondaryActions}>
<FormPageContainer title="Edit Admission for {formState.form.PVID}" {primaryAction} {secondaryActions}>
<PatientFormRenderer
{formState}
formFields={formFields}

View File

@ -62,7 +62,6 @@
</script>
<div class="h-full flex flex-col relative w-full">
{#if props.searchable ?? true}
<!-- <div class="flex items-center absolute top-[-2rem]"> -->
<div class="flex items-center absolute" style="top: -{props.offset ?? 2}rem">
<Input
placeholder="Filter all columns..."