continue patient list & admission

This commit is contained in:
faiztyanirh 2026-02-11 16:43:34 +07:00
parent cc22abb033
commit a3b8582e57
18 changed files with 317 additions and 144 deletions

View File

@ -95,7 +95,6 @@ export async function searchWithPath(endpoint, searchQuery) {
}
export async function create(endpoint, formData) {
console.log(cleanEmptyStrings(formData));
try {
const res = await fetch(`${API.BASE_URL}${endpoint}`, {
method: 'POST',
@ -115,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,6 +2,10 @@
import ChartPieIcon from "@lucide/svelte/icons/chart-pie";
import FrameIcon from "@lucide/svelte/icons/frame";
import LifeBuoyIcon from "@lucide/svelte/icons/life-buoy";
import ChartColumnIcon from "@lucide/svelte/icons/chart-column";
import UserIcon from "@lucide/svelte/icons/user";
import ReceiptTextIcon from "@lucide/svelte/icons/receipt-text";
import BookOpenIcon from "@lucide/svelte/icons/book-open";
import MapIcon from "@lucide/svelte/icons/map";
import SendIcon from "@lucide/svelte/icons/send";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
@ -17,12 +21,12 @@
{
title: "Dashboard",
url: "/",
icon: LifeBuoyIcon,
icon: ChartColumnIcon,
},
{
title: "Patient",
url: "/patient",
icon: LifeBuoyIcon,
icon: UserIcon,
submenus: [
{
title: "Patient List",
@ -37,7 +41,7 @@
{
title: "Order",
url: "/order",
icon: LifeBuoyIcon,
icon: ReceiptTextIcon,
submenus: [
{
title: "Test Order",
@ -50,7 +54,7 @@
{
title: "Admission",
url: "/admission",
icon: LifeBuoyIcon,
icon: BookOpenIcon,
submenus: [
{
title: "Contact",
@ -69,7 +73,7 @@
{
title: "Value",
url: "#",
icon: LifeBuoyIcon,
icon: BookOpenIcon,
submenus: [
{
title: "Value Set Def",
@ -84,7 +88,7 @@
{
title: "Sample",
url: "#",
icon: LifeBuoyIcon,
icon: BookOpenIcon,
submenus: [
{
title: "Container",
@ -95,7 +99,7 @@
{
title: "Organization",
url: "#",
icon: LifeBuoyIcon,
icon: BookOpenIcon,
submenus: [
{
title: "Account",
@ -122,7 +126,7 @@
{
title: "Test",
url: "#",
icon: LifeBuoyIcon,
icon: BookOpenIcon,
submenus: [
{
title: "Test Site",

View File

@ -7,12 +7,13 @@ export function useForm({schema, initialForm, defaultErrors, mode, modeOpt, save
const val = useFormValidation(schema, state.form, defaultErrors, mode);
const options = useFormOptions(modeOpt);
async function save() {
async function save(currentMode, customPayload = null) {
state.isSaving.current = true
try {
const payload = { ...state.form };
const result = mode === 'edit' ? await editEndpoint(payload) : await saveEndpoint(payload)
// const payload = { ...state.form };
const payload = customPayload || { ...state.form };
const result = currentMode === 'edit' ? await editEndpoint(payload) : await saveEndpoint(payload);
return result;
} catch (error) {
console.error('Save failed', error);

View File

@ -42,21 +42,20 @@ export function useMasterDetail(options = {}) {
}
}
function enterCreate(initialData = null) {
mode = "create";
selectedItem = null;
function enterCreate(initialData = null) {
mode = "create";
selectedItem = null;
formState.reset();
formState.reset();
if (initialData) {
formState.setForm({
...formState.form,
...initialData
});
if (initialData) {
formState.setForm({
...formState.form,
...initialData
});
}
}
}
function enterEdit(mapToForm = null) {
if (!selectedItem) return;
mode = "edit";
@ -74,9 +73,6 @@ function enterCreate(initialData = null) {
}
function exitForm() {
// const confirmed = confirm('You have unsaved changes. Are you sure you want to exit?');
// if (!confirmed) return;
mode = "view";
selectedItem = null;
}
@ -87,12 +83,10 @@ function enterCreate(initialData = null) {
}
function saveForm() {
// Commit changes (mark as saved)
formSnapshot = { ...form };
}
return {
// State
get selectedItem() {
return selectedItem;
},
@ -127,7 +121,6 @@ function enterCreate(initialData = null) {
return formState;
},
// Actions
select,
enterCreate,
enterEdit,

View File

@ -41,7 +41,7 @@ export const searchFields = [
export const detailSections = [
{
title: "", // No title for top row
title: "",
class: "grid grid-cols-1 md:grid-cols-2 gap-4",
groups: [
{
@ -84,10 +84,6 @@ export function admissionActions(masterDetail, selectedPatient) {
{
Icon: PlusIcon,
label: 'Add Visit',
// onClick: masterDetail.enterCreate({
// PatientID: selectedPatient?.PatientID,
// InternalPID: selectedPatient?.InternalPID
// }),
onClick: () => masterDetail.enterCreate({
PatientID: selectedPatient?.PatientID,
InternalPID: selectedPatient?.InternalPID
@ -108,7 +104,6 @@ export function viewActions(handlers){
Icon: PencilIcon,
label: 'Edit Patient',
onClick: handlers.editPatient,
},
]
}

View File

@ -18,6 +18,7 @@ export const admissionInitialForm = {
RefDoc: "",
AdmDoc: "",
CnsDoc: "",
isDischarge: false
};
export const admissionDefaultErrors = {};
@ -131,7 +132,7 @@ export const admissionFormFields = [
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/service_classes`,
},
{
key: "Discharge",
key: "isDischarge",
label: "Discharge Status",
required: false,
type: "toggle",

View File

@ -0,0 +1,105 @@
import { cleanEmptyStrings } from "$lib/utils/cleanEmptyStrings";
const ADT_CODE_MAP = {
LocationID: "A02",
AttDoc: "A54",
RefDoc: "A08",
AdmDoc: "A08",
CnsDoc: "A61",
isDischarge: "A03",
};
export function compareAdmissionData(oldData, newData) {
let diffs = [];
let current = { ...oldData };
let sequence = 1;
for (const key in ADT_CODE_MAP) {
// Skip if field tidak ada atau tidak berubah
if (!Object.hasOwn(oldData, key) || oldData[key] === newData[key]) {
continue;
}
// Track discharge status
const originalisDischarge = !!oldData.isDischarge;
const newisDischarge = !!newData.isDischarge;
// Update current state
current[key] = newData[key];
// Build diff entry
const filtered = Object.fromEntries(
Object.keys(ADT_CODE_MAP).map((k) => [k, current[k]])
);
diffs.push({
sequence: sequence++,
code: ADT_CODE_MAP[key],
originalisDischarge,
isDischarge: newisDischarge,
...filtered,
});
}
return diffs;
}
export function buildEditAdmissionPayload(form, diffs = []) {
const base = {
InternalPVID: form.InternalPVID,
PVID: form.PVID,
EpisodeID: form.EpisodeID,
InternalPID: form.InternalPID,
PatDiag: {
DiagCode: form.DiagCode || null,
Diagnosis: form.Diagnosis || null,
},
};
// No changes, return empty PatVisitADT
if (!diffs.length) {
return {
...base,
PatVisitADT: [],
};
}
// Map diffs ke PatVisitADT entries
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
} else {
ADTCode = diff.code; // Normal update
}
return {
sequence: diff.sequence,
ADTCode,
LocationID: diff.LocationID || null,
AttDoc: diff.AttDoc || null,
RefDoc: diff.RefDoc || null,
AdmDoc: diff.AdmDoc || null,
CnsDoc: diff.CnsDoc || null,
};
});
return { ...base, PatVisitADT };
}
export function buildEditPayloadWithDiff(originalData, formData) {
// Clean data
const cleanedForm = cleanEmptyStrings(formData);
const cleanedOriginal = cleanEmptyStrings(originalData);
// Compare & get diffs
const diffs = compareAdmissionData(cleanedOriginal, cleanedForm);
console.log(formData);
// Build payload
const payload = buildEditAdmissionPayload(cleanedForm, diffs);
return cleanEmptyStrings(payload);
}

View File

@ -0,0 +1,62 @@
<script>
import { useForm } from "$lib/components/composable/use-form.svelte";
import { admissionSchema, admissionInitialForm, admissionDefaultErrors, admissionFormFields, getAdmissionFormActions, buildPayload } from "$lib/components/patient/admission/config/admission-form-config";
import { createAdmission } from "$lib/components/patient/admission/api/patient-admission-api";
import { usePatientForm } from "$lib/components/composable/use-patient-form.svelte";
import FormPageContainer from "$lib/components/patient/reusable/form-page-container.svelte";
import PatientFormRenderer from "$lib/components/patient/reusable/patient-form-renderer.svelte";
let props = $props();
let formState = useForm({
schema: admissionSchema,
initialForm: admissionInitialForm,
defaultErrors: admissionDefaultErrors,
mode: 'create',
modeOpt: 'default',
saveEndpoint: createAdmission,
editEndpoint: null,
});
const helpers = usePatientForm(formState, admissionSchema);
const handlers = {
clearForm: () => {
formState.reset();
}
};
const actions = getAdmissionFormActions(handlers);
async function handleSave() {
const payload = buildPayload(formState.form);
console.log(payload);
}
const primaryAction = $derived({
label: 'Save',
onClick: handleSave,
disabled: helpers.hasErrors || formState.isSaving.current,
loading: formState.isSaving.current
});
const secondaryActions = [];
$effect(() => {
if (props.masterDetail.form?.PatientID) {
formState.setForm({
...formState.form,
...props.masterDetail.form
});
}
});
</script>
<FormPageContainer title="Create Admission" {primaryAction} {secondaryActions} {actions}>
<PatientFormRenderer
{formState}
formFields={admissionFormFields}
mode="create"
/>
</FormPageContainer>

View File

@ -1,24 +1,17 @@
<script>
import { useForm } from "$lib/components/composable/use-form.svelte";
import { admissionSchema, admissionInitialForm, admissionDefaultErrors, admissionFormFields, getAdmissionFormActions, buildPayload } from "$lib/components/patient/admission/config/admission-form-config";
import { createAdmission } from "$lib/components/patient/admission/api/patient-admission-api";
import { usePatientForm } from "$lib/components/composable/use-patient-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 { buildPayload } from "$lib/components/patient/admission/config/admission-form-config";
import { toast } from "svelte-sonner";
let props = $props();
let formState = useForm({
schema: admissionSchema,
initialForm: admissionInitialForm,
defaultErrors: admissionDefaultErrors,
mode: 'create',
modeOpt: 'default',
saveEndpoint: createAdmission,
editEndpoint: null,
});
const { masterDetail, formFields, formActions, schema } = props.context;
const helpers = usePatientForm(formState, admissionSchema);
const { formState } = masterDetail;
const helpers = usePatientForm(formState, schema);
const handlers = {
clearForm: () => {
@ -26,12 +19,16 @@
}
};
const actions = getAdmissionFormActions(handlers);
const actions = formActions(handlers);
async function handleSave() {
const payload = buildPayload(formState.form);
// const result = await formState.save(masterDetail.mode, payload);
console.log(payload);
toast('Visit Created!');
masterDetail?.exitForm();
}
const primaryAction = $derived({
@ -44,10 +41,10 @@
const secondaryActions = [];
$effect(() => {
if (props.masterDetail.form?.PatientID) {
if (masterDetail.form?.PatientID) {
formState.setForm({
...formState.form,
...props.masterDetail.form
...masterDetail.form
});
}
});
@ -56,7 +53,7 @@
<FormPageContainer title="Create Admission" {primaryAction} {secondaryActions} {actions}>
<PatientFormRenderer
{formState}
formFields={admissionFormFields}
formFields={formFields}
mode="create"
/>
</FormPageContainer>

View File

@ -1,39 +1,51 @@
<script>
import { useForm } from "$lib/components/composable/use-form.svelte";
import { admissionSchema, admissionInitialForm, admissionDefaultErrors, admissionFormFields, getAdmissionFormActions } from "$lib/components/patient/admission/config/admission-form-config";
import { editAdmission } from "$lib/components/patient/admission/api/patient-admission-api";
import { usePatientForm } from "$lib/components/composable/use-patient-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 { untrack } from "svelte";
import { buildPayload } from "$lib/components/patient/admission/config/admission-form-config";
import { toast } from "svelte-sonner";
import { buildEditPayloadWithDiff } from "$lib/components/patient/admission/config/admission-payload";
let props = $props();
const { masterDetail, formFields, formActions, schema, initialForm, defaultError } = props.context;
let formState = useForm({
schema: admissionSchema,
initialForm: admissionInitialForm,
defaultErrors: {},
mode: 'edit',
modeOpt: 'default',
saveEndpoint: null,
editEndpoint: editAdmission,
});
const { formState } = masterDetail;
$effect(() => {
const backendData = props.masterDetail?.selectedItem.data;
const backendData = masterDetail?.selectedItem.data;
if (!backendData) return;
console.log(backendData);
untrack(() => {
const formData = {
...backendData,
};
formState.setForm(formData);
formFields.forEach(group => {
group.rows.forEach(row => {
row.columns.forEach(col => {
if (col.type === "select" && col.optionsEndpoint) {
formState.fetchOptions(col, formData);
}
});
});
});
})
})
$inspect(formState.form)
async function handleEdit() {
console.log('object');
// const payload = buildPayload(formState.form);
const payload = buildEditPayloadWithDiff(
masterDetail.selectedItem?.data,
formState.form
);
console.log(payload);
// toast('Visit Updated!');
// masterDetail?.exitForm();
// const result = await formState.save();
// if (result.status === 'success') {
@ -55,7 +67,7 @@
<FormPageContainer title="Edit Admission" {primaryAction} {secondaryActions}>
<PatientFormRenderer
{formState}
formFields={admissionFormFields}
formFields={formFields}
mode="edit"
/>
</FormPageContainer>

View File

@ -5,11 +5,12 @@
import { searchFields, admissionActions } from "$lib/components/patient/admission/config/admission-config";
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableSearchParam from "$lib/components/reusable/reusable-search-param.svelte";
import SearchParamModal from "../modal/search-param-modal.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
import SearchParamModal from "../modal/search-param-modal.svelte";
let props = $props();
let selectedPID = $state(null);
let selectedPatient = $state(null);
let tableData = $state([]);

View File

@ -6,10 +6,12 @@
let props = $props();
let visit = $derived(props.masterDetail?.selectedItem?.data);
const { masterDetail, formFields, formActions, schema } = props.context;
let visit = $derived(masterDetail?.selectedItem?.data);
const handlers = {
editPatient: () => props.masterDetail.enterEdit(),
editPatient: () => masterDetail.enterEdit(),
};
const actions = viewActions(handlers);
@ -44,10 +46,10 @@
</div>
{/snippet}
{#if props.masterDetail.selectedItem}
{#if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={props.masterDetail.selectedItem.data.PVID}
title={masterDetail.selectedItem.data.PVID}
{actions}
/>
<div class="flex-1 min-h-0 overflow-y-auto space-y-4">
@ -86,27 +88,4 @@
</div>
{:else}
<ReusableEmpty desc="Select a visit to see details"/>
{/if}
<!-- {#if props.masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper title={props.masterDetail.selectedItem.data.PVID} {actions} />
<div class="flex-1 min-h-0 overflow-y-auto space-y-4">
{#each detailSections as section}
<div class="p-4">
<div class={section.class}>
{#each section.fields as field}
{@render Fieldset({
label: field.label,
value: getFieldValue(field),
isUTCDate: field.isUTCDate,
})}
{/each}
</div>
</div>
{/each}
</div>
</div>
{:else}
<ReusableEmpty desc="Select a visit to see details"/>
{/if} -->
{/if}

View File

@ -5,6 +5,7 @@
import { toast } from "svelte-sonner";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
const { formState } = masterDetail;
@ -20,7 +21,7 @@
const actions = formActions(handlers);
async function handleSave() {
const result = await formState.save();
const result = await formState.save(masterDetail.mode);
if (result.status === 'success') {
toast('Patient Created!');

View File

@ -7,12 +7,11 @@
import { API } from "$lib/config/api";
let props = $props();
const { masterDetail, formFields, formActions, schema, initialForm } = props.context;
const { masterDetail, formFields, formActions, schema, initialForm, defaultError } = props.context;
const { formState } = masterDetail;
console.log(formState);
const helpers = usePatientForm(formState, schema);
$effect(() => {
@ -69,12 +68,13 @@
}
});
});
$inspect(formState.form)
async function handleEdit() {
const result = await formState.save();
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);

View File

@ -6,6 +6,7 @@
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
let patient = $derived(masterDetail?.selectedItem?.patient);

View File

@ -1,7 +1,7 @@
<script>
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import * as Select from "$lib/components/ui/select/index.js";
import * as ToggleGroup from "$lib/components/ui/toggle-group/index.js";
import { Toggle } from "$lib/components/ui/toggle/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import { Label } from "$lib/components/ui/label/index.js";
@ -230,8 +230,8 @@
}}
onValueChange={(val) => {
formState.form.PatIdt = {
IdentifierType: val,
Identifier:''
IdentifierType: val,
Identifier:''
};
}}
>
@ -284,23 +284,23 @@
</div>
{:else if type === "toggle"}
<div class="flex items-center w-full">
<ToggleGroup.Root variant="outline" type="single" class="w-full" bind:value={formState.form[key]} >
<ToggleGroup.Item
value="yes"
aria-label="Toggle Yes"
class="flex gap-2 px-4 w-1/2 transition-all data-[state=on]:bg-primary/10 data-[state=on]:text-primary data-[state=on]:border-primary/30 data-[state=on]:*:[svg]:stroke-[6px]"
>
<CheckIcon class="h-4 w-4" />
</ToggleGroup.Item>
<ToggleGroup.Item
value="no"
aria-label="Toggle No"
class="flex gap-2 px-4 w-1/2 transition-all data-[state=on]:bg-primary/10 data-[state=on]:text-primary data-[state=on]:border-primary/30 data-[state=on]:*:[svg]:stroke-[6px]"
>
<XIcon class="h-4 w-4" />
</ToggleGroup.Item>
</ToggleGroup.Root>
<Toggle
aria-label="Toggle discharge"
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}
<XIcon class="mr-2 h-4 w-4" />
{:else}
<CheckIcon class="mr-2 h-4 w-4" />
{/if}
{formState.form.isDischarge ? "Discharged" : "Active"}
</Toggle>
</div>
{:else}
<Input

View File

@ -1,17 +1,39 @@
<script>
import { Separator } from "$lib/components/ui/separator/index.js";
import { useMasterDetail } from "$lib/components/composable/use-master-detail.svelte";
import { getVisit } from "$lib/components/patient/admission/api/patient-admission-api";
import { getVisit, createAdmission, editAdmission } from "$lib/components/patient/admission/api/patient-admission-api";
import MasterPage from "$lib/components/patient/admission/page/master-page.svelte";
import ViewPage from "$lib/components/patient/admission/page/view-page.svelte";
import CreatePage from "$lib/components/patient/admission/page/create-page.svelte";
import EditPage from "$lib/components/patient/admission/page/edit-page.svelte";
import { admissionSchema, admissionInitialForm, admissionDefaultErrors, admissionFormFields, getAdmissionFormActions, buildPayload } from "$lib/components/patient/admission/config/admission-form-config";
const masterDetail = useMasterDetail({
onSelect: async (row) => {
return await getVisit(row.PVID);
},
formConfig: {
schema: admissionSchema,
initialForm: admissionInitialForm,
defaultErrors: admissionDefaultErrors,
mode: 'create',
modeOpt: 'default',
saveEndpoint: createAdmission,
editEndpoint: editAdmission,
}
});
const pageContext = {
masterDetail,
formFields: admissionFormFields,
formActions: getAdmissionFormActions,
schema: admissionSchema,
initialForm: admissionInitialForm,
defaultErrors: {
create: admissionDefaultErrors,
edit: {}
}
}
</script>
<div class="flex w-full h-full overflow-hidden">
@ -22,11 +44,11 @@
{#if masterDetail.showDetail}
<main class={`${masterDetail.isMobile ? 'w-full' : masterDetail.isFormMode ? 'w-[97%] flex flex-col items-start' : 'w-[65%]'} h-full overflow-y-auto flex flex-col items-center transition-all duration-300`}>
{#if masterDetail.mode === "view"}
<ViewPage {masterDetail}/>
<ViewPage context={pageContext}/>
{:else if masterDetail.mode === "create"}
<CreatePage {masterDetail}/>
<CreatePage context={pageContext}/>
{:else if masterDetail.mode === "edit"}
<EditPage {masterDetail}/>
<EditPage context={pageContext}/>
{/if}
</main>
{/if}

View File

@ -30,7 +30,7 @@
schema: patientSchema,
initialForm: patientInitialForm,
defaultErrors: {
create: patientDefaultErrors,
create: patientDefaultErrors,
edit: {}
}
}