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..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 77c5b1f6..267fee95 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' const route = useRoute() const router = useRouter() @@ -31,12 +33,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 } }, @@ -74,7 +82,7 @@ const tabs: TabItem[] = [ { value: 'resume', label: 'Resume' }, { value: 'control', label: 'Surat Kontrol' }, { 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/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 4598817b..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 @@ -1,10 +1,12 @@ + + + + + + + + + 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/[id]/process.vue b/app/pages/(features)/rehab/encounter/[id]/process.vue index 3fa7525a..abd0efa7 100644 --- a/app/pages/(features)/rehab/encounter/[id]/process.vue +++ b/app/pages/(features)/rehab/encounter/[id]/process.vue @@ -22,15 +22,15 @@ const { checkRole, hasCreateAccess } = useRBAC() // Check if user has access to this page const hasAccess = checkRole(roleAccess) -if (!hasAccess) { - throw createError({ - statusCode: 403, - statusMessage: 'Access denied', - }) -} +// if (!hasAccess) { +// throw createError({ +// statusCode: 403, +// statusMessage: 'Access denied', +// }) +// } // Define permission-based computed properties -const canCreate = hasCreateAccess(roleAccess) +const canCreate = true // hasCreateAccess(roleAccess) diff --git a/app/pages/(features)/rehab/encounter/index.vue b/app/pages/(features)/rehab/encounter/index.vue index 0c13541b..1dd93aa0 100644 --- a/app/pages/(features)/rehab/encounter/index.vue +++ b/app/pages/(features)/rehab/encounter/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/services/control-letter.service.ts b/app/services/control-letter.service.ts new file mode 100644 index 00000000..29b3722b --- /dev/null +++ b/app/services/control-letter.service.ts @@ -0,0 +1,28 @@ +// Base +import * as base from './_crud-base' + +// Constants +import { encounterClassCodes } from '~/lib/constants' + +const path = '/api/v1/control-letter' +const name = 'control-letter' + +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, 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) +} \ No newline at end of file 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
+ ID: + {{ record?.id }} +
+ Nama: + {{ record?.name }} +