diff --git a/app/components/app/document-upload/_common/select-doc-type.vue b/app/components/app/document-upload/_common/select-doc-type.vue new file mode 100644 index 00000000..70f78a7b --- /dev/null +++ b/app/components/app/document-upload/_common/select-doc-type.vue @@ -0,0 +1,71 @@ + + + + + + {{ label }} + + + + + + + + + + + + + diff --git a/app/components/app/document-upload/entry-form.vue b/app/components/app/document-upload/entry-form.vue new file mode 100644 index 00000000..f97a5161 --- /dev/null +++ b/app/components/app/document-upload/entry-form.vue @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + diff --git a/app/components/app/document-upload/list.cfg.ts b/app/components/app/document-upload/list.cfg.ts new file mode 100644 index 00000000..979c916d --- /dev/null +++ b/app/components/app/document-upload/list.cfg.ts @@ -0,0 +1,43 @@ +import type { Config } from '~/components/pub/my-ui/data-table' +import { defineAsyncComponent } from 'vue' +import { docTypeCode, docTypeLabel, type docTypeCodeKey } from '~/lib/constants' + +const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dd.vue')) + +export const config: Config = { + cols: [{}, {}, {}, {width: 50},], + + headers: [ + [ + { label: 'Nama Dokumen' }, + { label: 'Tipe Dokumen' }, + { label: 'Petugas Upload' }, + { label: 'Action' }, + ], + ], + + keys: ['fileName', 'type_code', 'employee.name', 'action'], + + delKeyNames: [ + + ], + + parses: { + type_code: (v: unknown) => { + return docTypeLabel[v?.type_code as docTypeCodeKey] + }, + }, + + components: { + action(rec, idx) { + return { + idx, + rec: rec as object, + component: action, + } + }, + }, + + htmls: { + }, +} diff --git a/app/components/app/document-upload/list.vue b/app/components/app/document-upload/list.vue new file mode 100644 index 00000000..8274e752 --- /dev/null +++ b/app/components/app/document-upload/list.vue @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/app/components/app/medicine-form/entry-form.vue b/app/components/app/medicine-form/entry-form.vue new file mode 100644 index 00000000..fb26631e --- /dev/null +++ b/app/components/app/medicine-form/entry-form.vue @@ -0,0 +1,119 @@ + + + + + + + Kode + + + + + + Nama + + + + + + + + Kembali + + + Simpan + + + + diff --git a/app/components/app/medicine-form/list-cfg.ts b/app/components/app/medicine-form/list-cfg.ts new file mode 100644 index 00000000..5b66812a --- /dev/null +++ b/app/components/app/medicine-form/list-cfg.ts @@ -0,0 +1,38 @@ +import type { Config, RecComponent } from '~/components/pub/my-ui/data-table' +import { defineAsyncComponent } from 'vue' + +const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue')) + +export const config: Config = { + cols: [{}, {}, { width: 50 }], + + headers: [ + [ + { label: 'Kode' }, + { label: 'Nama' }, + { label: 'Aksi' }, + ], + ], + + keys: ['code', 'name', 'action'], + + delKeyNames: [ + { key: 'code', label: 'Kode' }, + { key: 'name', label: 'Nama' }, + ], + + parses: {}, + + components: { + action(rec, idx) { + const res: RecComponent = { + idx, + rec: rec as object, + component: action, + } + return res + }, + }, + + htmls: {}, +} diff --git a/app/components/app/medicine-form/list.vue b/app/components/app/medicine-form/list.vue new file mode 100644 index 00000000..e4544c2f --- /dev/null +++ b/app/components/app/medicine-form/list.vue @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/app/components/app/medicine/entry-form.vue b/app/components/app/medicine/entry-form.vue index 42989fcb..af4df34f 100644 --- a/app/components/app/medicine/entry-form.vue +++ b/app/components/app/medicine/entry-form.vue @@ -18,6 +18,7 @@ interface Props { isReadonly?: boolean medicineGroups?: { value: string; label: string }[] medicineMethods?: { value: string; label: string }[] + medicineForms?: { value: string; label: string }[] uoms?: { value: string; label: string }[] } @@ -36,6 +37,7 @@ const { defineField, errors, meta } = useForm({ name: '', medicineGroup_code: '', medicineMethod_code: '', + medicineForm_code: '', uom_code: '', stock: 0, }, @@ -45,6 +47,7 @@ const [code, codeAttrs] = defineField('code') const [name, nameAttrs] = defineField('name') const [medicineGroup_code, medicineGroupAttrs] = defineField('medicineGroup_code') const [medicineMethod_code, medicineMethodAttrs] = defineField('medicineMethod_code') +const [medicineForm_code, medicineFormAttrs] = defineField('medicineForm_code') const [uom_code, uomAttrs] = defineField('uom_code') const [stock, stockAttrs] = defineField('stock') @@ -53,6 +56,7 @@ if (props.values) { if (props.values.name !== undefined) name.value = props.values.name if (props.values.medicineGroup_code !== undefined) medicineGroup_code.value = props.values.medicineGroup_code if (props.values.medicineMethod_code !== undefined) medicineMethod_code.value = props.values.medicineMethod_code + if (props.values.medicineForm_code !== undefined) medicineForm_code.value = props.values.medicineForm_code if (props.values.uom_code !== undefined) uom_code.value = props.values.uom_code if (props.values.stock !== undefined) stock.value = props.values.stock } @@ -62,6 +66,7 @@ const resetForm = () => { name.value = '' medicineGroup_code.value = '' medicineMethod_code.value = '' + medicineForm_code.value = '', uom_code.value = '' stock.value = 0 } @@ -72,6 +77,7 @@ function onSubmitForm() { name: name.value || '', medicineGroup_code: medicineGroup_code.value || '', medicineMethod_code: medicineMethod_code.value || '', + medicineForm_code: medicineForm_code.value || '', uom_code: uom_code.value || '', stock: stock.value || 0, } @@ -138,6 +144,20 @@ function onCancelForm() { /> + + Sediaan Obat + + + + Satuan diff --git a/app/components/app/medicine/list-cfg.ts b/app/components/app/medicine/list-cfg.ts index 059022c8..5d1740f8 100644 --- a/app/components/app/medicine/list-cfg.ts +++ b/app/components/app/medicine/list-cfg.ts @@ -15,12 +15,13 @@ export const config: Config = { { label: 'Golongan' }, { label: 'Metode Pemberian' }, { label: 'Satuan' }, + { label: 'Sediaan' }, { label: 'Stok' }, { label: 'Aksi' }, ], ], - keys: ['code', 'name', 'group', 'method', 'unit', 'stock', 'action'], + keys: ['code', 'name', 'group', 'method', 'unit', 'form', 'stock', 'action'], delKeyNames: [ { key: 'code', label: 'Kode' }, @@ -37,6 +38,9 @@ export const config: Config = { unit: (rec: unknown): unknown => { return (rec as SmallDetailDto).uom?.name || '-' }, + form: (rec: unknown): unknown => { + return (rec as SmallDetailDto).medicineForm?.name || '-' + }, }, components: { diff --git a/app/components/content/document-upload/add.vue b/app/components/content/document-upload/add.vue new file mode 100644 index 00000000..7d42f4f6 --- /dev/null +++ b/app/components/content/document-upload/add.vue @@ -0,0 +1,128 @@ + + + + + Upload Dokumen + + + + + + + + + + + diff --git a/app/components/content/document-upload/edit.vue b/app/components/content/document-upload/edit.vue new file mode 100644 index 00000000..c4033fb2 --- /dev/null +++ b/app/components/content/document-upload/edit.vue @@ -0,0 +1,134 @@ + + + + + Upload Dokumen + + + + + + + + + + + diff --git a/app/components/content/document-upload/list.vue b/app/components/content/document-upload/list.vue new file mode 100644 index 00000000..2ddbb77c --- /dev/null +++ b/app/components/content/document-upload/list.vue @@ -0,0 +1,143 @@ + + + + + + + + + + + ID: + {{ record?.id }} + + + Nama: + {{ record?.name }} + + + + + diff --git a/app/components/content/encounter/process.vue b/app/components/content/encounter/process.vue index 42d8b746..c2351fca 100644 --- a/app/components/content/encounter/process.vue +++ b/app/components/content/encounter/process.vue @@ -18,6 +18,8 @@ import Prescription from '~/components/content/prescription/main.vue' import CpLabOrder from '~/components/content/cp-lab-order/main.vue' import Radiology from '~/components/content/radiology-order/main.vue' import Consultation from '~/components/content/consultation/list.vue' +import DocUploadList from '~/components/content/document-upload/list.vue' +import { genEncounter } from '~/models/encounter' import GeneralConsentList from '~/components/content/general-consent/entry.vue' import ResumeList from '~/components/content/resume/list.vue' import ControlLetterList from '~/components/content/control-letter/list.vue' @@ -34,12 +36,18 @@ const activeTab = computed({ }) const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0 -const dataRes = await getDetail(id, { - includes: - 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person', +const data = ref(genEncounter()) + +async function fetchDetail() { + const res = await getDetail(id, { + includes: 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person,EncounterDocuments', + }) + if(res.body?.data) data.value = res.body?.data +} + +onMounted(() => { + fetchDetail() }) -const dataResBody = dataRes.body ?? null -const data = dataResBody?.data ?? null const tabs: TabItem[] = [ { value: 'status', label: 'Status Masuk/Keluar', component: Status, props: { encounter: data } }, @@ -65,10 +73,10 @@ const tabs: TabItem[] = [ { value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' }, { value: 'consent', label: 'General Consent', component: GeneralConsentList, props: { encounter: data } }, { value: 'patient-note', label: 'CPRJ' }, - { value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.id } }, + { value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.value.id } }, { value: 'device', label: 'Order Alkes' }, - { value: 'mcu-radiology', label: 'Order Radiologi', component: Radiology, props: { encounter_id: data.id } }, - { value: 'mcu-lab-cp', label: 'Order Lab PK', component: CpLabOrder, props: { encounter_id: data.id } }, + { value: 'mcu-radiology', label: 'Order Radiologi', component: Radiology, props: { encounter_id: data.value.id } }, + { value: 'mcu-lab-cp', label: 'Order Lab PK', component: CpLabOrder, props: { encounter_id: data.value.id } }, { value: 'mcu-lab-micro', label: 'Order Lab Mikro' }, { value: 'mcu-lab-pa', label: 'Order Lab PA' }, { value: 'medical-action', label: 'Order Ruang Tindakan' }, @@ -79,7 +87,7 @@ const tabs: TabItem[] = [ { value: 'resume', label: 'Resume' }, { value: 'control', label: 'Surat Kontrol', component: ControlLetterList, props: { encounter: data } }, { value: 'screening', label: 'Skrinning MPP' }, - { value: 'supporting-document', label: 'Upload Dokumen Pendukung' }, + { value: 'supporting-document', label: 'Upload Dokumen Pendukung', component: DocUploadList, props: { encounter: data }, }, ] diff --git a/app/components/content/equipment/list.vue b/app/components/content/equipment/list.vue index 19e5d913..0db6034c 100644 --- a/app/components/content/equipment/list.vue +++ b/app/components/content/equipment/list.vue @@ -110,6 +110,7 @@ watch([recId, recAction], () => { getCurrentMaterialDetail(recId.value) title.value = 'Edit Perlengkapan' isReadonly.value = false + isFormEntryDialogOpen.value = true break case ActionEvents.showConfirmDelete: isRecordConfirmationOpen.value = true @@ -158,7 +159,7 @@ onMounted(async () => { @submit=" (values: MaterialFormData, resetForm: any) => { if (recId > 0) { - handleActionEdit(recId, values, getEquipmentList, resetForm, toast) + handleActionEdit(recItem.code, values, getEquipmentList, resetForm, toast) return } handleActionSave(values, getEquipmentList, resetForm, toast) @@ -173,7 +174,7 @@ onMounted(async () => { v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem" - @confirm="() => handleActionRemove(recId, getEquipmentList, toast)" + @confirm="() => handleActionRemove(recItem.code, getEquipmentList, toast)" @cancel="" > diff --git a/app/components/content/medicine-form/list.vue b/app/components/content/medicine-form/list.vue new file mode 100644 index 00000000..1ca9eefe --- /dev/null +++ b/app/components/content/medicine-form/list.vue @@ -0,0 +1,193 @@ + + + + + + + + { + onResetState() + isFormEntryDialogOpen = value + } + " + > + , resetForm: () => void) => { + if (recId > 0) { + handleActionEdit(recItem.code, values, getMedicineFormList, resetForm, toast) + return + } + handleActionSave(values, getMedicineFormList, resetForm, toast) + } + " + @cancel="handleCancelForm" + /> + + + + handleActionRemove(recItem.code, getMedicineFormList, toast)" + @cancel="" + > + + + + ID: + {{ record?.id }} + + + Nama: + {{ record.name }} + + + Kode: + {{ record.code }} + + + + + diff --git a/app/components/content/medicine-group/list.vue b/app/components/content/medicine-group/list.vue index 80faf1ab..6c695d72 100644 --- a/app/components/content/medicine-group/list.vue +++ b/app/components/content/medicine-group/list.vue @@ -108,6 +108,7 @@ watch([recId, recAction], () => { getCurrentMedicineGroupDetail(recId.value) title.value = 'Edit Kelompok Obat' isReadonly.value = false + isFormEntryDialogOpen.value = true break case ActionEvents.showConfirmDelete: isRecordConfirmationOpen.value = true @@ -154,7 +155,7 @@ onMounted(async () => { @submit=" (values: BaseFormData | Record, resetForm: () => void) => { if (recId > 0) { - handleActionEdit(recId, values, getMedicineGroupList, resetForm, toast) + handleActionEdit(recItem.code, values, getMedicineGroupList, resetForm, toast) return } handleActionSave(values, getMedicineGroupList, resetForm, toast) @@ -169,7 +170,7 @@ onMounted(async () => { v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem" - @confirm="() => handleActionRemove(recId, getMedicineGroupList, toast)" + @confirm="() => handleActionRemove(recItem.code, getMedicineGroupList, toast)" @cancel="" > diff --git a/app/components/content/medicine-method/list.vue b/app/components/content/medicine-method/list.vue index 9e0e5d01..2c8fb8c6 100644 --- a/app/components/content/medicine-method/list.vue +++ b/app/components/content/medicine-method/list.vue @@ -108,6 +108,7 @@ watch([recId, recAction], () => { getCurrentMedicineMethodDetail(recId.value) title.value = 'Edit Metode Obat' isReadonly.value = false + isFormEntryDialogOpen.value = true break case ActionEvents.showConfirmDelete: isRecordConfirmationOpen.value = true @@ -154,7 +155,7 @@ onMounted(async () => { @submit=" (values: BaseFormData | Record, resetForm: () => void) => { if (recId > 0) { - handleActionEdit(recId, values, getMedicineMethodList, resetForm, toast) + handleActionEdit(recItem.code, values, getMedicineMethodList, resetForm, toast) return } handleActionSave(values, getMedicineMethodList, resetForm, toast) @@ -169,7 +170,7 @@ onMounted(async () => { v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem" - @confirm="() => handleActionRemove(recId, getMedicineMethodList, toast)" + @confirm="() => handleActionRemove(recItem.code, getMedicineMethodList, toast)" @cancel="" > diff --git a/app/components/content/medicine/list.vue b/app/components/content/medicine/list.vue index 27470776..bfbf8750 100644 --- a/app/components/content/medicine/list.vue +++ b/app/components/content/medicine/list.vue @@ -37,10 +37,12 @@ import { import { getList, getDetail } from '~/services/medicine.service' import { getValueLabelList as getMedicineGroupList } from '~/services/medicine-group.service' import { getValueLabelList as getMedicineMethodList } from '~/services/medicine-method.service' +import { getValueLabelList as getMedicineFormList } from '~/services/medicine-form.service' import { getValueLabelList as getUomList } from '~/services/uom.service' const medicineGroups = ref<{ value: string; label: string }[]>([]) const medicineMethods = ref<{ value: string; label: string }[]>([]) +const medicineForms = ref<{ value: string; label: string }[]>([]) const uoms = ref<{ value: string; label: string }[]>([]) const title = ref('') @@ -59,7 +61,7 @@ const { sort: 'createdAt:asc', 'page-number': params['page-number'] || 0, 'page-size': params['page-size'] || 10, - includes: 'medicineGroup,medicineMethod,uom', + includes: 'medicineGroup,medicineMethod,medicineForm,uom', }) return { success: result.success || false, body: result.body || {} } }, @@ -116,6 +118,7 @@ watch([recId, recAction], () => { case ActionEvents.showEdit: getCurrentMedicineDetail(recId.value) title.value = 'Edit Obat' + isFormEntryDialogOpen.value = true isReadonly.value = false break case ActionEvents.showConfirmDelete: @@ -127,6 +130,7 @@ watch([recId, recAction], () => { onMounted(async () => { medicineGroups.value = await getMedicineGroupList({ sort: 'createdAt:asc', 'page-size': 100 }) medicineMethods.value = await getMedicineMethodList({ sort: 'createdAt:asc', 'page-size': 100 }) + medicineForms.value = await getMedicineFormList({ sort: 'createdAt:asc', 'page-size': 100 }) uoms.value = await getUomList({ sort: 'createdAt:asc', 'page-size': 100 }) await getMedicineList() }) @@ -163,13 +167,14 @@ onMounted(async () => { :values="recItem" :medicineGroups="medicineGroups" :medicineMethods="medicineMethods" + :medicineForms="medicineForms" :uoms="uoms" :is-loading="isProcessing" :is-readonly="isReadonly" @submit=" (values: MedicineFormData | Record, resetForm: () => void) => { if (recId > 0) { - handleActionEdit(recId, values, getMedicineList, resetForm, toast) + handleActionEdit(recItem.code, values, getMedicineList, resetForm, toast) return } handleActionSave(values, getMedicineList, resetForm, toast) @@ -184,7 +189,7 @@ onMounted(async () => { v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem" - @confirm="() => handleActionRemove(recId, getMedicineList, toast)" + @confirm="() => handleActionRemove(recItem.code, getMedicineList, toast)" @cancel="" > diff --git a/app/components/content/tools/list.vue b/app/components/content/tools/list.vue index da22a976..4f50199f 100644 --- a/app/components/content/tools/list.vue +++ b/app/components/content/tools/list.vue @@ -163,7 +163,7 @@ onMounted(async () => { @submit=" (values: DeviceFormData, resetForm: any) => { if (recId > 0) { - handleActionEdit(recId, values, getToolsList, resetForm, toast) + handleActionEdit(recItem.code, values, getToolsList, resetForm, toast) return } handleActionSave(values, getToolsList, resetForm, toast) @@ -178,7 +178,7 @@ onMounted(async () => { v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem" - @confirm="() => handleActionRemove(recId, getToolsList, toast)" + @confirm="() => handleActionRemove(recItem.code, getToolsList, toast)" @cancel="" > diff --git a/app/components/pub/my-ui/data/dropdown-action-dd.vue b/app/components/pub/my-ui/data/dropdown-action-dd.vue new file mode 100644 index 00000000..a6a99c9a --- /dev/null +++ b/app/components/pub/my-ui/data/dropdown-action-dd.vue @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + {{ item.label }} + + + + + + diff --git a/app/components/pub/my-ui/form/file-field.vue b/app/components/pub/my-ui/form/file-field.vue index bc6a86c9..31885a6f 100644 --- a/app/components/pub/my-ui/form/file-field.vue +++ b/app/components/pub/my-ui/form/file-field.vue @@ -62,7 +62,7 @@ async function onFileChange(event: Event, handleChange: (value: any) => void) { @change="onFileChange($event, handleChange)" type="file" :disabled="isDisabled" - v-bind="componentField" + v-bind="{ onBlur: componentField.onBlur }" :placeholder="placeholder" :class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')" /> diff --git a/app/components/pub/my-ui/nav-footer/ba-dr-su.vue b/app/components/pub/my-ui/nav-footer/ba-dr-su.vue index a84c8edb..11d6e9e3 100644 --- a/app/components/pub/my-ui/nav-footer/ba-dr-su.vue +++ b/app/components/pub/my-ui/nav-footer/ba-dr-su.vue @@ -43,4 +43,4 @@ function onClick(type: ClickType) { - + \ No newline at end of file diff --git a/app/handlers/medicine-form.handler.ts b/app/handlers/medicine-form.handler.ts new file mode 100644 index 00000000..6fcadb4c --- /dev/null +++ b/app/handlers/medicine-form.handler.ts @@ -0,0 +1,21 @@ +import { createCrudHandler } from '~/handlers/_handler' +import { create, update, remove } from '~/services/medicine-form.service' + +export const { + recId, + recAction, + recItem, + isReadonly, + isProcessing, + isFormEntryDialogOpen, + isRecordConfirmationOpen, + onResetState, + handleActionSave, + handleActionEdit, + handleActionRemove, + handleCancelForm, +} = createCrudHandler({ + post: create, + patch: update, + remove: remove, +}) diff --git a/app/handlers/supporting-document.handler.ts b/app/handlers/supporting-document.handler.ts new file mode 100644 index 00000000..70b29612 --- /dev/null +++ b/app/handlers/supporting-document.handler.ts @@ -0,0 +1,24 @@ +// Handlers +import { genCrudHandler } from '~/handlers/_handler' + +// Services +import { create, update, remove } from '~/services/supporting-document.service' + +export const { + recId, + recAction, + recItem, + isReadonly, + isProcessing, + isFormEntryDialogOpen, + isRecordConfirmationOpen, + onResetState, + handleActionSave, + handleActionEdit, + handleActionRemove, + handleCancelForm, +} = genCrudHandler({ + create, + update, + remove, +}) diff --git a/app/lib/constants.ts b/app/lib/constants.ts index 3a52b22e..48fb5c8c 100644 --- a/app/lib/constants.ts +++ b/app/lib/constants.ts @@ -383,3 +383,45 @@ export const medicalActionTypeCode: Record = { } as const export type medicalActionTypeCodeKey = keyof typeof medicalActionTypeCode + +export const encounterDocTypeCode: Record = { + "person-resident-number": 'person-resident-number', + "person-driving-license": 'person-driving-license', + "person-passport": 'person-passport', + "person-family-card": 'person-family-card', + "mcu-item-result": 'mcu-item-result', + "vclaim-sep": 'vclaim-sep', + "vclaim-sipp": 'vclaim-sipp', +} as const +export type encounterDocTypeCodeKey = keyof typeof encounterDocTypeCode +export const encounterDocOpt: { label: string; value: encounterDocTypeCodeKey }[] = [ + { label: 'KTP', value: 'person-resident-number' }, + { label: 'SIM', value: 'person-driving-license' }, + { label: 'Passport', value: 'person-passport' }, + { label: 'Kartu Keluarga', value: 'person-family-card' }, + { label: 'Hasil MCU', value: 'mcu-item-result' }, + { label: 'Klaim SEP', value: 'vclaim-sep' }, + { label: 'Klaim SIPP', value: 'vclaim-sipp' }, +] + + +export const docTypeCode = { + "encounter-patient": 'encounter-patient', + "encounter-support": 'encounter-support', + "encounter-other": 'encounter-other', + "vclaim-sep": 'vclaim-sep', + "vclaim-sipp": 'vclaim-sipp', +} as const +export const docTypeLabel = { + "encounter-patient": 'Data Pasien', + "encounter-support": 'Data Penunjang', + "encounter-other": 'Lain - Lain', + "vclaim-sep": 'SEP', + "vclaim-sipp": 'SIPP', +} as const +export type docTypeCodeKey = keyof typeof docTypeCode +export const supportingDocOpt = [ + { label: 'Data Pasien', value: 'encounter-patient' }, + { label: 'Data Penunjang', value: 'encounter-support' }, + { label: 'Lain - Lain', value: 'encounter-other' }, +] diff --git a/app/lib/utils.ts b/app/lib/utils.ts index 64248caf..67f3d04b 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -106,10 +106,58 @@ export function calculateAge(birthDate: Date | string | null | undefined): strin } } + +/** + * Converts a plain JavaScript object (including File objects) into a FormData instance. + * @param {object} data - The object to convert (e.g., form values). + * @returns {FormData} The new FormData object suitable for API submission. + */ +export function toFormData(data: Record): FormData { + const formData = new FormData(); + + for (const key in data) { + if (Object.prototype.hasOwnProperty.call(data, key)) { + const value = data[key]; + + // Handle File objects, Blobs, or standard JSON values + if (value !== null && value !== undefined) { + // Check if the value is a File/Blob instance + if (value instanceof File || value instanceof Blob) { + // Append the file directly + formData.append(key, value); + } else if (typeof value === 'object') { + // Handle nested objects/arrays by stringifying them (optional, depends on API) + // Note: Most APIs expect nested data to be handled separately or passed as JSON string + // For simplicity, we stringify non-File objects. + formData.append(key, JSON.stringify(value)); + } else { + // Append standard string, number, or boolean values + formData.append(key, value); + } + } + } + } + + return formData; +} + +export function printFormData(formData: FormData) { + console.log("--- FormData Contents ---"); + // Use the entries() iterator to loop through key/value pairs + for (const [key, value] of formData.entries()) { + if (value instanceof File) { + console.log(`Key: ${key}, Value: [File: ${value.name}, Type: ${value.type}, Size: ${value.size} bytes]`); + } else { + console.log(`Key: ${key}, Value: "${value}"`); + } + } + console.log("-------------------------"); +} + export function unauthorizedToast() { toast({ title: 'Unauthorized', description: 'You are not authorized to perform this action.', variant: 'destructive', }) -} \ No newline at end of file +} diff --git a/app/models/encounter-document.ts b/app/models/encounter-document.ts new file mode 100644 index 00000000..5a98ccd5 --- /dev/null +++ b/app/models/encounter-document.ts @@ -0,0 +1,29 @@ +import { type Base, genBase } from "./_base" +import { docTypeLabel, } from '~/lib/constants' +import { genEmployee, type Employee } from "./employee" +import { genEncounter, type Encounter } from "./encounter" + +export interface EncounterDocument extends Base { + encounter_id: number + encounter?: Encounter + upload_employee_id: number + employee?: Employee + type_code: string + name: string + filePath: string + fileName: string +} + +export function genEncounterDocument(): EncounterDocument { + return { + ...genBase(), + encounter_id: 2, + encounter: genEncounter(), + upload_employee_id: 0, + employee: genEmployee(), + type_code: docTypeLabel["encounter-patient"], + name: 'example', + filePath: 'https://bing.com', + fileName: 'example', + } +} diff --git a/app/models/encounter.ts b/app/models/encounter.ts index fb2c0b04..55fbdfa4 100644 --- a/app/models/encounter.ts +++ b/app/models/encounter.ts @@ -1,6 +1,7 @@ import type { DeathCause } from "./death-cause" import { type Doctor, genDoctor } from "./doctor" import { genEmployee, type Employee } from "./employee" +import type { EncounterDocument } from "./encounter-document" import type { InternalReference } from "./internal-reference" import { type Patient, genPatient } from "./patient" import type { Specialist } from "./specialist" @@ -37,6 +38,7 @@ export interface Encounter { internalReferences?: InternalReference[] deathCause?: DeathCause status_code: string + encounterDocuments: EncounterDocument[] } export function genEncounter(): Encounter { @@ -54,7 +56,8 @@ export function genEncounter(): Encounter { appointment_doctor_id: 0, appointment_doctor: genDoctor(), medicalDischargeEducation: '', - status_code: '' + status_code: '', + encounterDocuments: [], } } diff --git a/app/models/medicine-form.ts b/app/models/medicine-form.ts new file mode 100644 index 00000000..8ca21a2b --- /dev/null +++ b/app/models/medicine-form.ts @@ -0,0 +1,38 @@ +import { type Base, genBase } from "./_base" + +export interface MedicineForm extends Base { + name: string + code: string +} + +export interface CreateDto { + name: string + code: string +} + +export interface GetListDto { + page: number + size: number + name?: string + code?: string +} + +export interface GetDetailDto { + id?: string +} + +export interface UpdateDto extends CreateDto { + id?: number +} + +export interface DeleteDto { + id?: string +} + +export function genMedicine(): MedicineForm { + return { + ...genBase(), + name: 'name', + code: 'code', + } +} diff --git a/app/pages/(features)/rehab/encounter/[id]/document-upload/[document_id]/edit.vue b/app/pages/(features)/rehab/encounter/[id]/document-upload/[document_id]/edit.vue new file mode 100644 index 00000000..8a228d55 --- /dev/null +++ b/app/pages/(features)/rehab/encounter/[id]/document-upload/[document_id]/edit.vue @@ -0,0 +1,41 @@ + + + + + + + + + + diff --git a/app/pages/(features)/rehab/encounter/[id]/document-upload/add.vue b/app/pages/(features)/rehab/encounter/[id]/document-upload/add.vue new file mode 100644 index 00000000..94805d12 --- /dev/null +++ b/app/pages/(features)/rehab/encounter/[id]/document-upload/add.vue @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/app/pages/(features)/rehab/encounter/index.vue b/app/pages/(features)/rehab/encounter/index.vue index f50ad954..1dd93aa0 100644 --- a/app/pages/(features)/rehab/encounter/index.vue +++ b/app/pages/(features)/rehab/encounter/index.vue @@ -22,9 +22,9 @@ const { checkRole, hasReadAccess } = useRBAC() // Check if user has access to this page const hasAccess = checkRole(roleAccess) -if (!hasAccess) { - // navigateTo('/403') -} +// if (!hasAccess) { +// navigateTo('/403') +// } // Define permission-based computed properties const canRead = true // hasReadAccess(roleAccess) diff --git a/app/pages/(features)/tools-equipment-src/medicine-form/index.vue b/app/pages/(features)/tools-equipment-src/medicine-form/index.vue new file mode 100644 index 00000000..120df6ea --- /dev/null +++ b/app/pages/(features)/tools-equipment-src/medicine-form/index.vue @@ -0,0 +1,38 @@ + + + + + + + + diff --git a/app/pages/(features)/tools-equipment-src/medicine/index.vue b/app/pages/(features)/tools-equipment-src/medicine/index.vue index 2be85f63..33f16618 100644 --- a/app/pages/(features)/tools-equipment-src/medicine/index.vue +++ b/app/pages/(features)/tools-equipment-src/medicine/index.vue @@ -22,12 +22,12 @@ const { checkRole, hasReadAccess } = useRBAC() // Check if user has access to this page const hasAccess = checkRole(roleAccess) -if (!hasAccess) { - navigateTo('/403') -} +// if (!hasAccess) { +// navigateTo('/403') +// } // Define permission-based computed properties -const canRead = hasReadAccess(roleAccess) +const canRead = true // hasReadAccess(roleAccess) diff --git a/app/schemas/document-upload.schema.ts b/app/schemas/document-upload.schema.ts new file mode 100644 index 00000000..7f7de622 --- /dev/null +++ b/app/schemas/document-upload.schema.ts @@ -0,0 +1,24 @@ +import { z } from 'zod' + +const ACCEPTED_UPLOAD_TYPES = ['image/jpeg', 'image/png', 'application/pdf'] +const MAX_SIZE_BYTES = 1 * 1024 * 1024 // 1MB + +const DocumentUploadSchema = z.object({ + entityType_code: z.string().default('encounter'), + ref_id: z.number(), + upload_employee_id: z.number(), + name: z.string({ required_error: 'Mohon isi', }), + type_code: z.string({ required_error: 'Mohon isi', }), + content: z.custom() + .refine((f) => f, { message: 'File tidak boleh kosong' }) + .refine((f) => !f || f instanceof File, { message: 'Harus berupa file yang valid' }) + .refine((f) => !f || ACCEPTED_UPLOAD_TYPES.includes(f.type), { + message: 'Format file harus JPG, PNG, atau PDF', + }) + .refine((f) => !f || f.size <= MAX_SIZE_BYTES, { message: 'Maksimal 1MB' }), +}) + +type DocumentUploadFormData = z.infer + +export { DocumentUploadSchema } +export type { DocumentUploadFormData } diff --git a/app/schemas/medicine.schema.ts b/app/schemas/medicine.schema.ts index 44113777..6443be36 100644 --- a/app/schemas/medicine.schema.ts +++ b/app/schemas/medicine.schema.ts @@ -5,6 +5,7 @@ export const MedicineSchema = z.object({ name: z.string({ required_error: 'Nama harus diisi' }).min(1, 'Nama minimal 1 karakter'), medicineGroup_code: z.string({ required_error: 'Kelompok obat harus diisi' }).min(1, 'Kelompok obat harus diisi'), medicineMethod_code: z.string({ required_error: 'Metode pemberian harus diisi' }).min(1, 'Metode pemberian harus diisi'), + medicineForm_code: z.string({ required_error: 'Sediaan Obat harus diisi' }).min(1, 'Sediaan Obat harus diisi'), uom_code: z.string({ required_error: 'Satuan harus diisi' }).min(1, 'Satuan harus diisi'), infra_id: z.number().nullable().optional(), stock: z.preprocess((val) => Number(val), z.number({ invalid_type_error: 'Stok harus berupa angka' }).min(1, 'Stok harus lebih besar dari 0')), diff --git a/app/services/medicine-form.service.ts b/app/services/medicine-form.service.ts new file mode 100644 index 00000000..21874f5c --- /dev/null +++ b/app/services/medicine-form.service.ts @@ -0,0 +1,41 @@ +// Base +import * as base from './_crud-base' + +// Types +import type { MedicineForm } from '~/models/medicine-form' + +const path = '/api/v1/medicine-form' +const name = 'medicine-form' + +export function create(data: any) { + return base.create(path, data, name) +} + +export function getList(params: any = null) { + return base.getList(path, params, name) +} + +export function getDetail(id: number | string) { + return base.getDetail(path, id, name) +} + +export function update(id: number | string, data: any) { + return base.update(path, id, data, name) +} + +export function remove(id: number | string) { + return base.remove(path, id, name) +} + +export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> { + let data: { value: string; label: string }[] = [] + const result = await getList(params) + if (result.success) { + const resultData = result.body?.data || [] + data = resultData.map((item: MedicineForm) => ({ + value: item.code, + label: item.name, + })) + } + return data +} diff --git a/app/services/supporting-document.service.ts b/app/services/supporting-document.service.ts new file mode 100644 index 00000000..46eaffa9 --- /dev/null +++ b/app/services/supporting-document.service.ts @@ -0,0 +1,56 @@ +// Base +import * as base from './_crud-base' + +// Constants +import { encounterClassCodes, uploadCode, type UploadCodeKey } from '~/lib/constants' + +const path = '/api/v1/encounter-document' +const create_path = '/api/v1/upload' +const name = 'encounter-document' + +export function create(data: any) { + return base.create(create_path, data, name) +} + +export function getList(params: any = null) { + return base.getList(path, params, name) +} + +export function getDetail(id: number | string, params?: any) { + return base.getDetail(path, id, name, params) +} + +export function update(id: number | string, data: any) { + return base.update(path, id, data, name) +} + +export function remove(id: number | string) { + return base.remove(path, id, name) +} + +export async function uploadAttachment(file: File, userId: number, key: UploadCodeKey) { + try { + const resolvedKey = uploadCode[key] + if (!resolvedKey) { + throw new Error(`Invalid upload code key: ${key}`) + } + + // siapkan form-data body + const formData = new FormData() + formData.append('code', resolvedKey) + formData.append('content', file) + + // kirim via xfetch + const resp = await xfetch(`${path}/${userId}/upload`, 'POST', formData) + + // struktur hasil sama seperti patchPatient + const result: any = {} + result.success = resp.success + result.body = (resp.body as Record) || {} + + return result + } catch (error) { + console.error('Error uploading attachment:', error) + throw new Error('Failed to upload attachment') + } +} \ No newline at end of file diff --git a/public/side-menu-items/sys.json b/public/side-menu-items/sys.json new file mode 100644 index 00000000..700cc4b7 --- /dev/null +++ b/public/side-menu-items/sys.json @@ -0,0 +1,367 @@ +[ + { + "heading": "Menu Utama", + "items": [ + { + "title": "Dashboard", + "icon": "i-lucide-home", + "link": "/" + }, + { + "title": "Rawat Jalan", + "icon": "i-lucide-stethoscope", + "children": [ + { + "title": "Antrian Pendaftaran", + "link": "/outpatient/registration-queue" + }, + { + "title": "Antrian Poliklinik", + "link": "/outpatient/polyclinic-queue" + }, + { + "title": "Kunjungan", + "link": "/outpatient/encounter" + }, + { + "title": "Konsultasi", + "link": "/outpatient/consultation" + } + ] + }, + { + "title": "IGD", + "icon": "i-lucide-zap", + "children": [ + { + "title": "Triase", + "link": "/emergency/triage" + }, + { + "title": "Kunjungan", + "link": "/emergency/encounter" + }, + { + "title": "Konsultasi", + "link": "/emergency/consultation" + } + ] + }, + { + "title": "Rehab Medik", + "icon": "i-lucide-bike", + "children": [ + { + "title": "Antrean Pendaftaran", + "link": "/rehab/registration-queue" + }, + { + "title": "Antrean Poliklinik", + "link": "/rehab/polyclinic-queue" + }, + { + "title": "Kunjungan", + "link": "/rehab/encounter" + }, + { + "title": "Konsultasi", + "link": "/rehab/consultation" + } + ] + }, + { + "title": "Rawat Inap", + "icon": "i-lucide-building-2", + "children": [ + { + "title": "Permintaan", + "link": "/inpatient/request" + }, + { + "title": "Kunjungan", + "link": "/inpatient/encounter" + }, + { + "title": "Konsultasi", + "link": "/inpatient/consultation" + } + ] + }, + { + "title": "Obat - Order", + "icon": "i-lucide-briefcase-medical", + "children": [ + { + "title": "Permintaan", + "link": "/medication/order" + }, + { + "title": "Standing Order", + "link": "/medication/standing-order" + } + ] + }, + { + "title": "Lab - Order", + "icon": "i-lucide-microscope", + "link": "/pc-lab-order" + }, + { + "title": "Lab Mikro - Order", + "icon": "i-lucide-microscope", + "link": "/micro-lab-order" + }, + { + "title": "Lab PA - Order", + "icon": "i-lucide-microscope", + "link": "/pa-lab-order" + }, + { + "title": "Radiologi - Order", + "icon": "i-lucide-radio", + "link": "/radiology-order" + }, + { + "title": "Gizi", + "icon": "i-lucide-egg-fried", + "link": "/nutrition-order" + }, + { + "title": "Pembayaran", + "icon": "i-lucide-banknote-arrow-up", + "link": "/payment" + } + ] + }, + { + "heading": "Ruang Tindakan Rajal", + "items": [ + { + "title": "Kemoterapi", + "icon": "i-lucide-droplets", + "link": "/outpation-action/cemotherapy" + }, + { + "title": "Hemofilia", + "icon": "i-lucide-droplet-off", + "link": "/outpation-action/hemophilia" + } + ] + }, + { + "heading": "Ruang Tindakan Anak", + "items": [ + { + "title": "Thalasemi", + "icon": "i-lucide-baby", + "link": "/children-action/thalasemia" + }, + { + "title": "Echocardiography", + "icon": "i-lucide-baby", + "link": "/children-action/echocardiography" + }, + { + "title": "Spirometri", + "icon": "i-lucide-baby", + "link": "/children-action/spirometry" + } + ] + }, + { + "heading": "Client", + "items": [ + { + "title": "Pasien", + "icon": "i-lucide-users", + "link": "/client/patient" + }, + { + "title": "Rekam Medis", + "icon": "i-lucide-file-text", + "link": "/client/medical-record" + } + ] + }, + { + "heading": "Integrasi", + "items": [ + { + "title": "BPJS", + "icon": "i-lucide-circuit-board", + "children": [ + { + "title": "SEP", + "icon": "i-lucide-circuit-board", + "link": "/integration/bpjs/sep" + }, + { + "title": "Peserta", + "icon": "i-lucide-circuit-board", + "link": "/integration/bpjs/member" + } + ] + }, + { + "title": "SATUSEHAT", + "icon": "i-lucide-database", + "link": "/integration/satusehat" + } + ] + }, + { + "heading": "Source", + "items": [ + { + "title": "Peralatan dan Perlengkapan", + "icon": "i-lucide-layout-dashboard", + "children": [ + { + "title": "Obat", + "link": "/tools-equipment-src/medicine" + }, + { + "title": "Peralatan", + "link": "/tools-equipment-src/tools" + }, + { + "title": "Perlengkapan (BMHP)", + "link": "/tools-equipment-src/equipment" + }, + { + "title": "Metode Obat", + "link": "/tools-equipment-src/medicine-method" + }, + { + "title": "Jenis Obat", + "link": "/tools-equipment-src/medicine-type" + }, + { + "title": "Sediaan Obat", + "link": "/tools-equipment-src/medicine-form" + } + ] + }, + { + "title": "Pengguna", + "icon": "i-lucide-user", + "children": [ + { + "title": "Pegawai", + "link": "/human-src/employee" + }, + { + "title": "PPDS", + "link": "/human-src/specialist-intern" + } + ] + }, + { + "title": "Pemeriksaan Penunjang", + "icon": "i-lucide-layout-list", + "children": [ + { + "title": "Checkup", + "link": "/mcu-src/mcu" + }, + { + "title": "Prosedur", + "link": "/mcu-src/procedure" + }, + { + "title": "Diagnosis", + "link": "/mcu-src/diagnose" + }, + { + "title": "Medical Action", + "link": "/mcu-src/medical-action" + } + ] + }, + { + "title": "Layanan", + "icon": "i-lucide-layout-list", + "children": [ + { + "title": "Counter", + "link": "/service-src/counter" + }, + { + "title": "Public Screen (Big Screen)", + "link": "/service-src/public-screen" + }, + { + "title": "Kasur", + "link": "/service-src/bed" + }, + { + "title": "Kamar", + "link": "/service-src/chamber" + }, + { + "title": "Ruang", + "link": "/service-src/room" + }, + { + "title": "Depo", + "link": "/service-src/warehouse" + }, + { + "title": "Lantai", + "link": "/service-src/floor" + }, + { + "title": "Gedung", + "link": "/service-src/building" + } + ] + }, + { + "title": "Organisasi", + "icon": "i-lucide-network", + "children": [ + { + "title": "Divisi", + "link": "/org-src/division" + }, + { + "title": "Instalasi", + "link": "/org-src/installation" + }, + { + "title": "Unit", + "link": "/org-src/unit" + }, + { + "title": "Spesialis", + "link": "/org-src/specialist" + }, + { + "title": "Sub Spesialis", + "link": "/org-src/subspecialist" + } + ] + }, + { + "title": "Umum", + "icon": "i-lucide-airplay", + "children": [ + { + "title": "Uom", + "link": "/common/uom" + } + ] + }, + { + "title": "Keuangan", + "icon": "i-lucide-airplay", + "children": [ + { + "title": "Item & Pricing", + "link": "/common/item" + } + ] + } + ] + } +] diff --git a/public/side-menu-items/system.json b/public/side-menu-items/system.json index 5bf62f36..31890951 100644 --- a/public/side-menu-items/system.json +++ b/public/side-menu-items/system.json @@ -240,6 +240,10 @@ { "title": "Jenis Obat", "link": "/tools-equipment-src/medicine-type" + }, + { + "title": "Sediaan Obat", + "link": "/tools-equipment-src/medicine-form" } ] },
+ ID: + {{ record?.id }} +
+ Nama: + {{ record?.name }} +
+ Nama: + {{ record.name }} +
+ Kode: + {{ record.code }} +