bug fixing patient list & admission

This commit is contained in:
faiztyanirh 2026-02-12 17:02:40 +07:00
parent a3b8582e57
commit 4784ede1a6
17 changed files with 200 additions and 117 deletions

View File

@ -114,19 +114,19 @@ export async function create(endpoint, formData) {
export async function update(endpoint, formData) { export async function update(endpoint, formData) {
console.log(cleanEmptyStrings(formData)); console.log(cleanEmptyStrings(formData));
// try { try {
// const res = await fetch(`${API.BASE_URL}${endpoint}`, { const res = await fetch(`${API.BASE_URL}${endpoint}`, {
// method: 'PATCH', method: 'PATCH',
// headers: { headers: {
// 'Content-Type': 'application/json', 'Content-Type': 'application/json',
// }, },
// body: JSON.stringify(cleanEmptyStrings(formData)) body: JSON.stringify(cleanEmptyStrings(formData))
// }); });
// const data = await res.json(); const data = await res.json();
// return data; return data;
// } catch (err) { } catch (err) {
// console.error('Update Error:', err.message); console.error('Update Error:', err.message);
// return { success: false, message: err.message || 'Network error' }; return { success: false, message: err.message || 'Network error' };
// } }
} }

View File

@ -2,13 +2,23 @@ export function useFormState(initial) {
const form = $state(structuredClone(initial)) const form = $state(structuredClone(initial))
const isSaving = $state({ current: false }); const isSaving = $state({ current: false });
// function resetForm() {
// Object.assign(form, structuredClone(initial));
// }
function resetForm() { function resetForm() {
Object.keys(form).forEach(key => delete form[key]);
Object.assign(form, structuredClone(initial)); Object.assign(form, structuredClone(initial));
} }
// function setForm(data) {
// const snapshotData = $state.snapshot(data);
// Object.assign(form, JSON.parse(JSON.stringify(snapshotData)));
// }
function setForm(data) { function setForm(data) {
const snapshotData = $state.snapshot(data); // Object.keys(form).forEach(key => delete form[key]);
Object.assign(form, JSON.parse(JSON.stringify(snapshotData))); Object.assign(form, structuredClone(data));
} }
return { isSaving, form, resetForm, setForm } return { isSaving, form, resetForm, setForm }

View File

@ -7,6 +7,7 @@ export function useMasterDetail(options = {}) {
let selectedItem = $state(null); let selectedItem = $state(null);
let mode = $state("view"); let mode = $state("view");
let isLoadingDetail = $state(false); let isLoadingDetail = $state(false);
let formSnapshot = $state(null);
const formState = useForm(formConfig); const formState = useForm(formConfig);
@ -23,6 +24,10 @@ export function useMasterDetail(options = {}) {
detailWidth: isMobile ? "w-full" : isFormMode ? "w-[97%]" : "w-[65%]", detailWidth: isMobile ? "w-full" : isFormMode ? "w-[97%]" : "w-[65%]",
}); });
const isDirty = $derived(
JSON.stringify(formState.form) !== JSON.stringify(formSnapshot)
);
async function select(item) { async function select(item) {
mode = "view"; mode = "view";
@ -49,32 +54,46 @@ export function useMasterDetail(options = {}) {
formState.reset(); formState.reset();
if (initialData) { if (initialData) {
formState.setForm({ formState.setForm(initialData);
...formState.form,
...initialData
});
} }
} }
function enterEdit(mapToForm = null) { function enterEdit(param) {
if (!selectedItem) return; if (!selectedItem) return;
mode = "edit"; mode = "edit";
const data = mapToForm const raw = param
? mapToForm(selectedItem) ? selectedItem[param]
: selectedItem; : selectedItem;
if (!raw) return;
const base = $state.snapshot(raw);
const entity = formConfig.mapToForm
? formConfig.mapToForm(base)
: base;
formState.reset(); formState.reset();
Object.assign(formState.form, data); formState.setForm(entity);
Object.keys(formState.errors).forEach(key => { Object.keys(formState.errors).forEach(key => {
formState.errors[key] = null; formState.errors[key] = null;
}); });
formSnapshot = $state.snapshot(formState.form);
} }
function exitForm() { function exitForm() {
if (isDirty) {
const ok = confirm('You have unsaved changes. Discard them?');
if (!ok) return;
}
mode = "view"; mode = "view";
selectedItem = null; selectedItem = null;
formSnapshot = null;
} }
function backToList() { function backToList() {
@ -108,6 +127,9 @@ export function useMasterDetail(options = {}) {
get layout() { get layout() {
return layout; return layout;
}, },
get formSnapshot() {
return formSnapshot;
},
// get form() { // get form() {
// return form; // return form;
// }, // },

View File

@ -5,7 +5,14 @@ export function usePatientForm(formState, patientSchema) {
let uploadErrors = $state({}); let uploadErrors = $state({});
let isChecking = $state({}); let isChecking = $state({});
async function validateFieldAsync(field) { async function validateFieldAsync(field, mode = 'create', originalValue = null) {
if (mode === 'edit' && formState.form[field] === originalValue) {
formState.errors[field] = null;
isChecking[field] = false;
return;
}
isChecking[field] = true; isChecking[field] = true;
try { try {
@ -33,8 +40,8 @@ export function usePatientForm(formState, patientSchema) {
} }
function validateIdentifier() { function validateIdentifier() {
const identifierType = formState.form.PatIdt.IdentifierType; const identifierType = formState.form.PatIdt?.IdentifierType;
const identifierValue = formState.form.PatIdt.Identifier; const identifierValue = formState.form.PatIdt?.Identifier;
if (!identifierType || !identifierValue) { if (!identifierType || !identifierValue) {
formState.errors['PatIdt.Identifier'] = null; formState.errors['PatIdt.Identifier'] = null;
return; return;

View File

@ -133,7 +133,7 @@ export const admissionFormFields = [
}, },
{ {
key: "isDischarge", key: "isDischarge",
label: "Discharge Status", label: "Admission Status",
required: false, required: false,
type: "toggle", type: "toggle",
defaultValue: false, defaultValue: false,

View File

@ -67,12 +67,26 @@ export function buildEditAdmissionPayload(form, diffs = []) {
const PatVisitADT = diffs.map(diff => { const PatVisitADT = diffs.map(diff => {
// Determine ADTCode based on discharge status // Determine ADTCode based on discharge status
let ADTCode; let ADTCode;
if (diff.isDischarge) { // if (diff.isDischarge) {
ADTCode = "A03"; // Discharge // ADTCode = "A03"; // Discharge
} else if (diff.originalisDischarge) { // } else if (diff.originalisDischarge) {
ADTCode = "A13"; // Cancel discharge // ADTCode = "A13"; // Cancel discharge
// } else {
// ADTCode = diff.code; // Normal update
// }
if (diff.isDischarge && !diff.originalisDischarge) {
// NEW discharge (false → true)
ADTCode = "A03";
} else if (!diff.isDischarge && diff.originalisDischarge) {
// CANCEL discharge (true → false)
ADTCode = "A13";
} else if (diff.isDischarge && diff.originalisDischarge) {
// ALREADY discharged, other field changed
ADTCode = "A03"; // Keep discharge
} else { } else {
ADTCode = diff.code; // Normal update // Normal update (not related to discharge)
ADTCode = diff.code;
} }
return { return {
@ -96,7 +110,6 @@ export function buildEditPayloadWithDiff(originalData, formData) {
// Compare & get diffs // Compare & get diffs
const diffs = compareAdmissionData(cleanedOriginal, cleanedForm); const diffs = compareAdmissionData(cleanedOriginal, cleanedForm);
console.log(formData);
// Build payload // Build payload
const payload = buildEditAdmissionPayload(cleanedForm, diffs); const payload = buildEditAdmissionPayload(cleanedForm, diffs);

View File

@ -45,8 +45,8 @@
<div class="w-full h-110"> <div class="w-full h-110">
<div class="flex gap-4 h-full"> <div class="flex gap-4 h-full">
<div class="w-1/3"> <div class="flex flex-col w-1/3 h-full">
<div class="space-y-2"> <div class="flex-1 overflow-y-auto min-h-0 space-y-2">
{#each props.searchFields as field} {#each props.searchFields as field}
{#if field.type === "text"} {#if field.type === "text"}
<div class="space-y-2"> <div class="space-y-2">
@ -87,9 +87,9 @@
</Button> </Button>
</div> </div>
</div> </div>
<div class="flex w-2/3 h-full justify-center items-start"> <div class="flex flex-col w-2/3 h-full">
{#if props.search.searchData && props.search.searchData.length > 0} {#if props.search.searchData && props.search.searchData.length > 0}
<div class="flex flex-col h-full gap-2"> <div class="flex-1 overflow-y-auto min-h-0 flex flex-col gap-2">
<div class="flex-1"> <div class="flex-1">
<Table.Root> <Table.Root>
<Table.Header> <Table.Header>
@ -123,7 +123,8 @@
</Table.Body> </Table.Body>
</Table.Root> </Table.Root>
</div> </div>
<div class="flex justify-end gap-2 mt-4"> </div>
<div class="flex justify-end gap-2 mt-4 w-full">
<Popover.Close> <Popover.Close>
<Button <Button
size="sm" size="sm"
@ -135,7 +136,6 @@
</Button> </Button>
</Popover.Close> </Popover.Close>
</div> </div>
</div>
{:else} {:else}
<div class="flex h-full"> <div class="flex h-full">
<ReusableEmpty desc="Try searching from search parameters"/> <ReusableEmpty desc="Try searching from search parameters"/>

View File

@ -24,7 +24,7 @@
async function handleSave() { async function handleSave() {
const payload = buildPayload(formState.form); const payload = buildPayload(formState.form);
// const result = await formState.save(masterDetail.mode, payload); const result = await formState.save(masterDetail.mode, payload);
console.log(payload); console.log(payload);
toast('Visit Created!'); toast('Visit Created!');

View File

@ -13,39 +13,41 @@
const { formState } = masterDetail; const { formState } = masterDetail;
$effect(() => { $effect(() => {
const backendData = masterDetail?.selectedItem.data; // const backendData = masterDetail?.selectedItem.data;
if (!backendData) return; // if (!backendData) return;
untrack(() => { untrack(() => {
const formData = { // const formData = {
...backendData, // ...backendData,
}; // };
formState.setForm(formData); // formState.setForm(formData);
formFields.forEach(group => { formFields.forEach(group => {
group.rows.forEach(row => { group.rows.forEach(row => {
row.columns.forEach(col => { row.columns.forEach(col => {
if (col.type === "select" && col.optionsEndpoint) { if (col.type === "select" && col.optionsEndpoint) {
formState.fetchOptions(col, formData); formState.fetchOptions(col, formState.form);
} }
}); });
}); });
}); });
}) })
}) })
// $inspect(masterDetail.selectedItem?.data)
$inspect(formState.form) // $inspect(formState.form)
async function handleEdit() { async function handleEdit() {
// const payload = buildPayload(formState.form); // const payload = buildPayload(formState.form);
const payload = buildEditPayloadWithDiff( const customPayload = buildEditPayloadWithDiff(
masterDetail.selectedItem?.data, masterDetail.selectedItem?.data,
formState.form formState.form
); );
console.log(payload); const result = await masterDetail.formState.save(masterDetail.mode, customPayload);
// toast('Visit Updated!');
// masterDetail?.exitForm(); console.log(customPayload);
toast('Visit Updated!');
masterDetail?.exitForm();
// const result = await formState.save(); // const result = await formState.save();
// if (result.status === 'success') { // if (result.status === 'success') {

View File

@ -11,7 +11,7 @@
let visit = $derived(masterDetail?.selectedItem?.data); let visit = $derived(masterDetail?.selectedItem?.data);
const handlers = { const handlers = {
editPatient: () => masterDetail.enterEdit(), editPatient: () => masterDetail.enterEdit("data"),
}; };
const actions = viewActions(handlers); const actions = viewActions(handlers);

View File

@ -109,7 +109,7 @@ export function patientActions(masterDetail, patientInitialForm) {
{ {
Icon: PlusIcon, Icon: PlusIcon,
label: 'Add Patient', label: 'Add Patient',
onClick: () => masterDetail.enterCreate(patientInitialForm), onClick: () => masterDetail.enterCreate(),
}, },
{ {
Icon: Settings2Icon, Icon: Settings2Icon,
@ -139,7 +139,6 @@ export function viewActions(handlers){
Icon: PencilIcon, Icon: PencilIcon,
label: 'Edit Patient', label: 'Edit Patient',
onClick: handlers.editPatient, onClick: handlers.editPatient,
}, },
] ]
} }

View File

@ -203,7 +203,7 @@ export const patientFormFields = [
columns: [ columns: [
{ key: "Street_1", label: "Street 1", required: false, type: "text" }, { key: "Street_1", label: "Street 1", required: false, type: "text" },
{ {
key: "Province", key: "ProvinceID",
label: "Province", label: "Province",
required: false, required: false,
type: "select", type: "select",
@ -216,13 +216,13 @@ export const patientFormFields = [
columns: [ columns: [
{ key: "Street_2", label: "Street 2", required: false, type: "text" }, { key: "Street_2", label: "Street 2", required: false, type: "text" },
{ {
key: "City", key: "CityID",
label: "City", label: "City",
required: false, required: false,
type: "select", type: "select",
optionsEndpoint: `${API.BASE_URL}${API.CITY}`, optionsEndpoint: `${API.BASE_URL}${API.CITY}`,
dependsOn: "Province", // ← field yang jadi parent dependsOn: "ProvinceID",
endpointParamKey: "Parent" // ← query param name endpointParamKey: "Parent"
} }
] ]
}, },

View File

@ -8,36 +8,45 @@
let props = $props(); let props = $props();
const { masterDetail, formFields, formActions, schema, initialForm, defaultError } = props.context; // const { masterDetail, formFields, formActions, schema, initialForm, defaultError } = props.context;
const { masterDetail, formFields, formActions, schema, initialForm } = props.context;
const { formState } = masterDetail; const { formState } = masterDetail;
const helpers = usePatientForm(formState, schema); const helpers = usePatientForm(formState, schema);
$effect(() => { $effect(() => {
const backendData = masterDetail?.selectedItem?.patient; // const backendData = masterDetail?.selectedItem?.patient;
if (!backendData) return; // if (!backendData) return;
untrack(() => { untrack(() => {
const formData = { // const formData = {
...backendData, // ...backendData,
PatIdt: backendData.PatIdt ?? initialForm.PatIdt, // PatIdt: backendData.PatIdt ?? initialForm.PatIdt,
LinkTo: backendData.LinkTo ?? [], // LinkTo: backendData.LinkTo ?? [],
Custodian: backendData.Custodian ?? initialForm.Custodian, // Custodian: backendData.Custodian ?? initialForm.Custodian,
Sex: backendData.SexKey || backendData.Sex, // Sex: backendData.SexKey || backendData.Sex,
Religion: backendData.ReligionKey || backendData.Religion, // Religion: backendData.ReligionKey || backendData.Religion,
MaritalStatus: backendData.MaritalStatusKey || backendData.MaritalStatus, // MaritalStatus: backendData.MaritalStatusKey || backendData.MaritalStatus,
Ethnic: backendData.EthnicKey || backendData.Ethnic, // Ethnic: backendData.EthnicKey || backendData.Ethnic,
Race: backendData.RaceKey || backendData.Race, // Race: backendData.RaceKey || backendData.Race,
Country: backendData.CountryKey || backendData.Country, // Country: backendData.CountryKey || backendData.Country,
DeathIndicator: backendData.DeathIndicatorKey || backendData.DeathIndicator, // DeathIndicator: backendData.DeathIndicatorKey || backendData.DeathIndicator,
Province: backendData.ProvinceID || backendData.Province, // Province: backendData.ProvinceID || backendData.Province,
City: backendData.CityID || backendData.City, // City: backendData.CityID || backendData.City,
}; // };
formState.setForm(formData); // formState.setForm(formData);
// Ensure PatIdt is always an object to prevent binding errors
// if (!formState.form.PatIdt) {
// formState.form.PatIdt = {
// IdentifierType: "",
// Identifier: ""
// };
// }
formFields.forEach(group => { formFields.forEach(group => {
group.rows.forEach(row => { group.rows.forEach(row => {
@ -45,40 +54,44 @@
if (col.type === "group") { if (col.type === "group") {
col.columns.forEach(child => { col.columns.forEach(child => {
if (child.type === "select" && child.optionsEndpoint) { if (child.type === "select" && child.optionsEndpoint) {
formState.fetchOptions(child, formData); formState.fetchOptions(child, formState.form);
} }
}); });
} else if ((col.type === "select" || col.type === "identity") && col.optionsEndpoint) { } else if ((col.type === "select" || col.type === "identity") && col.optionsEndpoint) {
formState.fetchOptions(col, formData); formState.fetchOptions(col, formState.form);
} }
}); });
}); });
}); });
if (formData.Province && formData.City) { // if (formState.form.ProvinceID && formState.form.CityID) {
if (formState.form.Province) {
formState.fetchOptions( formState.fetchOptions(
{ {
key: "City", key: "City",
optionsEndpoint: `${API.BASE_URL}${API.CITY}`, optionsEndpoint: `${API.BASE_URL}${API.CITY}`,
dependsOn: "Province", dependsOn: "ProvinceID",
endpointParamKey: "Parent" endpointParamKey: "Parent"
}, },
formData formState.form
); );
} }
}); });
}); });
$inspect(formState.form)
async function handleEdit() {
const result = await formState.save(masterDetail.mode);
if (result.status === 'success') { // $inspect(masterDetail?.selectedItem?.patient)
console.log('Patient updated successfully'); // $inspect(formState.form)
toast('Patient Updated!'); async function handleEdit() {
masterDetail.exitForm(); console.log(formState.form);
} else { // const result = await formState.save(masterDetail.mode);
console.error('Failed to update patient:', result.message);
} // if (result.status === 'success') {
// console.log('Patient updated successfully');
// toast('Patient Updated!');
// masterDetail.exitForm();
// } else {
// console.error('Failed to update patient:', result.message);
// }
} }
const primaryAction = $derived({ const primaryAction = $derived({
@ -100,6 +113,8 @@ $inspect(formState.form)
isChecking={helpers.isChecking} isChecking={helpers.isChecking}
linkToDisplay={helpers.linkToDisplay} linkToDisplay={helpers.linkToDisplay}
validateIdentifier={helpers.validateIdentifier} validateIdentifier={helpers.validateIdentifier}
validateFieldAsync={helpers.validateFieldAsync}
originalData={masterDetail.formSnapshot}
mode="edit" mode="edit"
/> />
</FormPageContainer> </FormPageContainer>

View File

@ -15,7 +15,7 @@
orderLab: () => console.log('order lab'), orderLab: () => console.log('order lab'),
medicalRecord: () => console.log('medical record'), medicalRecord: () => console.log('medical record'),
auditPatient: () => console.log('audit patient'), auditPatient: () => console.log('audit patient'),
editPatient: () => masterDetail.enterEdit(), editPatient: () => masterDetail.enterEdit("patient"),
}; };
const actions = viewActions(handlers); const actions = viewActions(handlers);

View File

@ -23,6 +23,7 @@
linkToDisplay, linkToDisplay,
validateIdentifier, validateIdentifier,
validateFieldAsync, validateFieldAsync,
originalData = null,
mode = 'create' mode = 'create'
} = $props(); } = $props();
@ -99,7 +100,7 @@
}} }}
onblur={() => { onblur={() => {
if (validateOn?.includes("blur")) { if (validateOn?.includes("blur")) {
validateFieldAsync(key); validateFieldAsync(key, mode, originalData?.[key]);
} }
}} }}
/> />
@ -220,7 +221,7 @@
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
{:else if type === "identity"} {:else if type === "identity"}
{@const selectedLabel = formState.selectOptions[key]?.find(opt => opt.value === formState.form.PatIdt.IdentifierType)?.label || "Choose"} {@const selectedLabel = formState.selectOptions[key]?.find(opt => opt.value === formState.form.PatIdt?.IdentifierType)?.label || "Choose"}
<div class="flex items-center w-full"> <div class="flex items-center w-full">
<Select.Root type="single" bind:value={formState.form.PatIdt.IdentifierType} <Select.Root type="single" bind:value={formState.form.PatIdt.IdentifierType}
onOpenChange={(open) => { onOpenChange={(open) => {
@ -253,7 +254,7 @@
{/if} {/if}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
<Input type="text" class="rounded-l-none" disabled={!formState.form.PatIdt.IdentifierType} bind:value={formState.form.PatIdt.Identifier} oninput={validateIdentifier} /> <Input type="text" class="rounded-l-none" disabled={!formState.form.PatIdt?.IdentifierType} bind:value={formState.form.PatIdt.Identifier} oninput={validateIdentifier} />
</div> </div>
{:else if type === "custodian"} {:else if type === "custodian"}
<div class="flex items-center w-full"> <div class="flex items-center w-full">
@ -289,9 +290,6 @@
variant="outline" variant="outline"
class="w-full transition-all data-[state=on]:text-primary" class="w-full transition-all data-[state=on]:text-primary"
bind:pressed={formState.form.isDischarge} bind:pressed={formState.form.isDischarge}
onPressedChange={(pressed) => {
formState.form.ADTCode = pressed ? "A03" : "";
}}
> >
{#if formState.form.isDischarge} {#if formState.form.isDischarge}

View File

@ -10,7 +10,12 @@
const masterDetail = useMasterDetail({ const masterDetail = useMasterDetail({
onSelect: async (row) => { onSelect: async (row) => {
return await getVisit(row.PVID); const response = await getVisit(row.PVID);
if (response?.data) {
response.data.isDischarge = response.data.ADTCode === "A03";
}
return response
}, },
formConfig: { formConfig: {
schema: admissionSchema, schema: admissionSchema,

View File

@ -20,6 +20,18 @@
modeOpt: 'cascade', modeOpt: 'cascade',
saveEndpoint: createPatient, saveEndpoint: createPatient,
editEndpoint: editPatient, editEndpoint: editPatient,
mapToForm: (data) => ({
...data,
PatIdt: {
IdentifierType: data.PatIdt?.IdentifierType ?? "",
Identifier: data.PatIdt?.Identifier ?? ""
},
LinkTo: Array.isArray(data.LinkTo) ? data.LinkTo : [],
Custodian: data.Custodian ?? {
InternalPID: "",
PatientID: ""
},
})
} }
}); });
@ -29,10 +41,10 @@
formActions: getPatientFormActions, formActions: getPatientFormActions,
schema: patientSchema, schema: patientSchema,
initialForm: patientInitialForm, initialForm: patientInitialForm,
defaultErrors: { // defaultErrors: {
create: patientDefaultErrors, // create: patientDefaultErrors,
edit: {} // edit: {}
} // }
} }
</script> </script>