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) {
console.log(cleanEmptyStrings(formData));
// try {
// const res = await fetch(`${API.BASE_URL}${endpoint}`, {
// method: 'PATCH',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify(cleanEmptyStrings(formData))
// });
try {
const res = await fetch(`${API.BASE_URL}${endpoint}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(cleanEmptyStrings(formData))
});
// const data = await res.json();
// return data;
// } catch (err) {
// console.error('Update Error:', err.message);
// return { success: false, message: err.message || 'Network error' };
// }
const data = await res.json();
return data;
} catch (err) {
console.error('Update Error:', err.message);
return { success: false, message: err.message || 'Network error' };
}
}

View File

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

View File

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

View File

@ -5,7 +5,14 @@ export function usePatientForm(formState, patientSchema) {
let uploadErrors = $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;
try {
@ -33,8 +40,8 @@ export function usePatientForm(formState, patientSchema) {
}
function validateIdentifier() {
const identifierType = formState.form.PatIdt.IdentifierType;
const identifierValue = formState.form.PatIdt.Identifier;
const identifierType = formState.form.PatIdt?.IdentifierType;
const identifierValue = formState.form.PatIdt?.Identifier;
if (!identifierType || !identifierValue) {
formState.errors['PatIdt.Identifier'] = null;
return;

View File

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

View File

@ -67,12 +67,26 @@ export function buildEditAdmissionPayload(form, diffs = []) {
const PatVisitADT = diffs.map(diff => {
// Determine ADTCode based on discharge status
let ADTCode;
if (diff.isDischarge) {
ADTCode = "A03"; // Discharge
} else if (diff.originalisDischarge) {
ADTCode = "A13"; // Cancel discharge
// if (diff.isDischarge) {
// ADTCode = "A03"; // Discharge
// } else if (diff.originalisDischarge) {
// 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 {
ADTCode = diff.code; // Normal update
// Normal update (not related to discharge)
ADTCode = diff.code;
}
return {
@ -96,7 +110,6 @@ export function buildEditPayloadWithDiff(originalData, formData) {
// Compare & get diffs
const diffs = compareAdmissionData(cleanedOriginal, cleanedForm);
console.log(formData);
// Build payload
const payload = buildEditAdmissionPayload(cleanedForm, diffs);

View File

@ -45,8 +45,8 @@
<div class="w-full h-110">
<div class="flex gap-4 h-full">
<div class="w-1/3">
<div class="space-y-2">
<div class="flex flex-col w-1/3 h-full">
<div class="flex-1 overflow-y-auto min-h-0 space-y-2">
{#each props.searchFields as field}
{#if field.type === "text"}
<div class="space-y-2">
@ -87,9 +87,9 @@
</Button>
</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}
<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">
<Table.Root>
<Table.Header>
@ -123,18 +123,18 @@
</Table.Body>
</Table.Root>
</div>
<div class="flex justify-end gap-2 mt-4">
<Popover.Close>
<Button
size="sm"
class="cursor-pointer"
disabled={isPatientEmpty}
onclick={handleButtonClick}
>
Select Patient
</Button>
</Popover.Close>
</div>
</div>
<div class="flex justify-end gap-2 mt-4 w-full">
<Popover.Close>
<Button
size="sm"
class="cursor-pointer"
disabled={isPatientEmpty}
onclick={handleButtonClick}
>
Select Patient
</Button>
</Popover.Close>
</div>
{:else}
<div class="flex h-full">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,36 +8,45 @@
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 helpers = usePatientForm(formState, schema);
$effect(() => {
const backendData = masterDetail?.selectedItem?.patient;
if (!backendData) return;
// const backendData = masterDetail?.selectedItem?.patient;
// if (!backendData) return;
untrack(() => {
const formData = {
...backendData,
// const formData = {
// ...backendData,
PatIdt: backendData.PatIdt ?? initialForm.PatIdt,
LinkTo: backendData.LinkTo ?? [],
Custodian: backendData.Custodian ?? initialForm.Custodian,
// PatIdt: backendData.PatIdt ?? initialForm.PatIdt,
// LinkTo: backendData.LinkTo ?? [],
// Custodian: backendData.Custodian ?? initialForm.Custodian,
Sex: backendData.SexKey || backendData.Sex,
Religion: backendData.ReligionKey || backendData.Religion,
MaritalStatus: backendData.MaritalStatusKey || backendData.MaritalStatus,
Ethnic: backendData.EthnicKey || backendData.Ethnic,
Race: backendData.RaceKey || backendData.Race,
Country: backendData.CountryKey || backendData.Country,
DeathIndicator: backendData.DeathIndicatorKey || backendData.DeathIndicator,
Province: backendData.ProvinceID || backendData.Province,
City: backendData.CityID || backendData.City,
};
// Sex: backendData.SexKey || backendData.Sex,
// Religion: backendData.ReligionKey || backendData.Religion,
// MaritalStatus: backendData.MaritalStatusKey || backendData.MaritalStatus,
// Ethnic: backendData.EthnicKey || backendData.Ethnic,
// Race: backendData.RaceKey || backendData.Race,
// Country: backendData.CountryKey || backendData.Country,
// DeathIndicator: backendData.DeathIndicatorKey || backendData.DeathIndicator,
// Province: backendData.ProvinceID || backendData.Province,
// 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 => {
group.rows.forEach(row => {
@ -45,40 +54,44 @@
if (col.type === "group") {
col.columns.forEach(child => {
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) {
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(
{
key: "City",
optionsEndpoint: `${API.BASE_URL}${API.CITY}`,
dependsOn: "Province",
dependsOn: "ProvinceID",
endpointParamKey: "Parent"
},
formData
formState.form
);
}
});
});
$inspect(formState.form)
// $inspect(masterDetail?.selectedItem?.patient)
// $inspect(formState.form)
async function handleEdit() {
const result = await formState.save(masterDetail.mode);
console.log(formState.form);
// const result = await formState.save(masterDetail.mode);
if (result.status === 'success') {
console.log('Patient updated successfully');
toast('Patient Updated!');
masterDetail.exitForm();
} else {
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({
@ -100,6 +113,8 @@ $inspect(formState.form)
isChecking={helpers.isChecking}
linkToDisplay={helpers.linkToDisplay}
validateIdentifier={helpers.validateIdentifier}
validateFieldAsync={helpers.validateFieldAsync}
originalData={masterDetail.formSnapshot}
mode="edit"
/>
</FormPageContainer>
</FormPageContainer>

View File

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

View File

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

View File

@ -10,7 +10,12 @@
const masterDetail = useMasterDetail({
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: {
schema: admissionSchema,

View File

@ -20,6 +20,18 @@
modeOpt: 'cascade',
saveEndpoint: createPatient,
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,
schema: patientSchema,
initialForm: patientInitialForm,
defaultErrors: {
create: patientDefaultErrors,
edit: {}
}
// defaultErrors: {
// create: patientDefaultErrors,
// edit: {}
// }
}
</script>