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/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..4fc55bc4 --- /dev/null +++ b/app/components/content/document-upload/list.vue @@ -0,0 +1,170 @@ + + + + + + + + + + + ID: + {{ record?.id }} + + + Nama: + {{ record?.name }} + + + + + + + + + diff --git a/app/components/content/encounter/process.vue b/app/components/content/encounter/process.vue index 23640af7..ecf44507 100644 --- a/app/components/content/encounter/process.vue +++ b/app/components/content/encounter/process.vue @@ -19,6 +19,8 @@ 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 ControlLetterList from '~/components/content/control-letter/list.vue' +import DocUploadList from '~/components/content/document-upload/list.vue' +import { genEncounter } from '~/models/encounter' const route = useRoute() const router = useRouter() @@ -32,12 +34,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 } }, @@ -63,10 +71,10 @@ const tabs: TabItem[] = [ { value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' }, { value: 'consent', label: 'General Consent' }, { 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' }, @@ -75,7 +83,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, }, }, ] @@ -91,4 +99,4 @@ const tabs: TabItem[] = [ @change-tab="activeTab = $event" /> - + \ No newline at end of file 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/data/dropdown-action-dud.vue b/app/components/pub/my-ui/data/dropdown-action-dud.vue index dfcf1ada..71979c7c 100644 --- a/app/components/pub/my-ui/data/dropdown-action-dud.vue +++ b/app/components/pub/my-ui/data/dropdown-action-dud.vue @@ -2,14 +2,9 @@ import type { LinkItem, ListItemDto } from './types' import { ActionEvents } from './types' -interface Props { +const props = defineProps<{ rec: ListItemDto - size?: 'default' | 'sm' | 'lg' -} - -const props = withDefaults(defineProps(), { - size: 'lg', -}) +}>() const recId = inject>('rec_id')! const recAction = inject>('rec_action')! @@ -63,7 +58,7 @@ function del() { 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/modal/doc-preview-dialog.vue b/app/components/pub/my-ui/modal/doc-preview-dialog.vue new file mode 100644 index 00000000..26534456 --- /dev/null +++ b/app/components/pub/my-ui/modal/doc-preview-dialog.vue @@ -0,0 +1,29 @@ + + + + + Open in Browser + + + + + + \ No newline at end of file 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 8c292758..427eab0f 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/composables/useRBAC.ts b/app/composables/useRBAC.ts index ced57e3e..6cc01d72 100644 --- a/app/composables/useRBAC.ts +++ b/app/composables/useRBAC.ts @@ -1,5 +1,12 @@ import type { Permission, RoleAccess } from '~/models/role' +export interface PageOperationPermission { + canRead: boolean + canCreate: boolean + canUpdate: boolean + canDelete: boolean +} + /** * Check if user has access to a page */ @@ -36,6 +43,13 @@ export function useRBAC() { const hasUpdateAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'U') const hasDeleteAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'D') + const getPagePermissions = (roleAccess: RoleAccess): PageOperationPermission => ({ + canRead : hasReadAccess(roleAccess), + canCreate: hasCreateAccess(roleAccess), + canUpdate: hasUpdateAccess(roleAccess), + canDelete: hasDeleteAccess(roleAccess), + }) + return { checkRole, checkPermission, @@ -44,5 +58,6 @@ export function useRBAC() { hasReadAccess, hasUpdateAccess, hasDeleteAccess, + getPagePermissions, } } 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 357d8700..e201a439 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -1,6 +1,7 @@ import type { ClassValue } from 'clsx' import { clsx } from 'clsx' import { twMerge } from 'tailwind-merge' +import { toast } from '~/components/pub/ui/toast' export interface SelectOptionType<_T = string> { value: string @@ -104,3 +105,59 @@ export function calculateAge(birthDate: Date | string | null | undefined): strin return `${years} tahun ${months} bulan` } } + + +/** + * 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/pages/(features)/rehab/encounter/[id]/control-letter/add.vue b/app/pages/(features)/rehab/encounter/[id]/control-letter/add.vue index 1070a29f..fa0b386b 100644 --- a/app/pages/(features)/rehab/encounter/[id]/control-letter/add.vue +++ b/app/pages/(features)/rehab/encounter/[id]/control-letter/add.vue @@ -16,9 +16,9 @@ useHead({ title: () => route.meta.title as string, }) -const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient'] +const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter'] -const { checkRole, hasReadAccess } = useRBAC() +const { checkRole, getPagePermissions } = useRBAC() // Check if user has access to this page const hasAccess = checkRole(roleAccess) @@ -27,14 +27,13 @@ const hasAccess = checkRole(roleAccess) // } // Define permission-based computed properties -// const canRead = hasReadAccess(roleAccess) -const canRead = true +const pagePermission = getPagePermissions(roleAccess) const callbackUrl = route.query['return-path'] as string | undefined - + diff --git a/app/pages/(features)/outpation-action/chemotherapy/list.vue b/app/pages/(features)/rehab/encounter/[id]/document-upload/[document_id]/edit.vue similarity index 74% rename from app/pages/(features)/outpation-action/chemotherapy/list.vue rename to app/pages/(features)/rehab/encounter/[id]/document-upload/[document_id]/edit.vue index a141baaa..1cf5cc7c 100644 --- a/app/pages/(features)/outpation-action/chemotherapy/list.vue +++ b/app/pages/(features)/rehab/encounter/[id]/document-upload/[document_id]/edit.vue @@ -6,7 +6,7 @@ import { PAGE_PERMISSIONS } from '~/lib/page-permission' definePageMeta({ middleware: ['rbac'], roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'], - title: 'Daftar Kempterapi', + title: 'Update Dokumen Pendukung', contentFrame: 'cf-full-width', }) @@ -16,24 +16,25 @@ useHead({ title: () => route.meta.title as string, }) -const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor'] +const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient'] 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) +// const canRead = hasReadAccess(roleAccess) +const canRead = true - + diff --git a/app/pages/(features)/outpation-action/chemotherapy/[mode]/[id]/verification.vue b/app/pages/(features)/rehab/encounter/[id]/document-upload/add.vue similarity index 53% rename from app/pages/(features)/outpation-action/chemotherapy/[mode]/[id]/verification.vue rename to app/pages/(features)/rehab/encounter/[id]/document-upload/add.vue index ef936ff2..e04220f3 100644 --- a/app/pages/(features)/outpation-action/chemotherapy/[mode]/[id]/verification.vue +++ b/app/pages/(features)/rehab/encounter/[id]/document-upload/add.vue @@ -2,46 +2,41 @@ import type { PagePermission } from '~/models/role' import Error from '~/components/pub/my-ui/error/error.vue' import { PAGE_PERMISSIONS } from '~/lib/page-permission' -import ContentChemotherapyAdminList from '~/components/content/chemotherapy/admin-list.vue' -import ContentChemotherapyVerification from '~/components/content/chemotherapy/verification.vue' definePageMeta({ middleware: ['rbac'], roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'], - title: 'Kemoterapi Admin', + title: 'Tambah Dokumen Pendukung', contentFrame: 'cf-full-width', }) const route = useRoute() useHead({ - title: () => 'Verifikasi Jadwal Pasien', + title: () => route.meta.title as string, }) -const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor'] || {} +const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient'] 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) - -const mode = computed(() => route.params.mode as string) +// const canRead = hasReadAccess(roleAccess) +const canRead = true +const callbackUrl = route.query['return-path'] as string | undefined - + - + diff --git a/app/pages/(features)/rehab/encounter/[id]/process.vue b/app/pages/(features)/rehab/encounter/[id]/process.vue index abd0efa7..e25b0e77 100644 --- a/app/pages/(features)/rehab/encounter/[id]/process.vue +++ b/app/pages/(features)/rehab/encounter/[id]/process.vue @@ -18,7 +18,7 @@ useHead({ const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter'] -const { checkRole, hasCreateAccess } = useRBAC() +const { checkRole, hasCreateAccess, getPagePermissions } = useRBAC() // Check if user has access to this page const hasAccess = checkRole(roleAccess) @@ -30,11 +30,11 @@ const hasAccess = checkRole(roleAccess) // } // Define permission-based computed properties -const canCreate = true // hasCreateAccess(roleAccess) +const pagePermission = getPagePermissions(roleAccess) - + diff --git a/app/schemas/document-upload.schema.ts b/app/schemas/document-upload.schema.ts new file mode 100644 index 00000000..1cfaeee2 --- /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().optional(), + 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/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..c26d85aa --- /dev/null +++ b/public/side-menu-items/sys.json @@ -0,0 +1,368 @@ +[ + { + "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": "Surat Kontrol", + "icon": "i-lucide-circuit-board", + "link": "/integration/bpjs/control-letter" + } + ] + }, + { + "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": "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" + } + ] + } + ] + } +]
+ ID: + {{ record?.id }} +
+ Nama: + {{ record?.name }} +