From 0a68dbf3a625fec9580310aca1d34ccf74ba2a31 Mon Sep 17 00:00:00 2001 From: hasyim_kai Date: Tue, 18 Nov 2025 15:31:04 +0700 Subject: [PATCH] Fix: debug after reset --- .../_common/select-doc-type.vue | 71 ++++ .../app/document-upload/entry-form.vue | 73 ++++ .../app/document-upload/list.cfg.ts | 43 ++ app/components/app/document-upload/list.vue | 31 ++ .../content/document-upload/add.vue | 128 ++++++ .../content/document-upload/edit.vue | 134 +++++++ .../content/document-upload/list.vue | 170 ++++++++ app/components/content/encounter/process.vue | 28 +- .../pub/my-ui/data/dropdown-action-dd.vue | 80 ++++ .../pub/my-ui/data/dropdown-action-dud.vue | 11 +- app/components/pub/my-ui/form/file-field.vue | 2 +- .../pub/my-ui/modal/doc-preview-dialog.vue | 29 ++ .../pub/my-ui/nav-footer/ba-dr-su.vue | 2 +- app/composables/useRBAC.ts | 15 + app/handlers/supporting-document.handler.ts | 24 ++ app/lib/constants.ts | 42 ++ app/lib/utils.ts | 57 +++ app/models/encounter-document.ts | 29 ++ app/models/encounter.ts | 5 +- .../encounter/[id]/control-letter/add.vue | 9 +- .../document-upload/[document_id]/edit.vue} | 15 +- .../encounter/[id]/document-upload/add.vue} | 27 +- .../rehab/encounter/[id]/process.vue | 6 +- app/schemas/document-upload.schema.ts | 24 ++ app/services/supporting-document.service.ts | 56 +++ public/side-menu-items/sys.json | 368 ++++++++++++++++++ 26 files changed, 1427 insertions(+), 52 deletions(-) create mode 100644 app/components/app/document-upload/_common/select-doc-type.vue create mode 100644 app/components/app/document-upload/entry-form.vue create mode 100644 app/components/app/document-upload/list.cfg.ts create mode 100644 app/components/app/document-upload/list.vue create mode 100644 app/components/content/document-upload/add.vue create mode 100644 app/components/content/document-upload/edit.vue create mode 100644 app/components/content/document-upload/list.vue create mode 100644 app/components/pub/my-ui/data/dropdown-action-dd.vue create mode 100644 app/components/pub/my-ui/modal/doc-preview-dialog.vue create mode 100644 app/handlers/supporting-document.handler.ts create mode 100644 app/models/encounter-document.ts rename app/pages/(features)/{outpation-action/chemotherapy/list.vue => rehab/encounter/[id]/document-upload/[document_id]/edit.vue} (74%) rename app/pages/(features)/{outpation-action/chemotherapy/[mode]/[id]/verification.vue => rehab/encounter/[id]/document-upload/add.vue} (53%) create mode 100644 app/schemas/document-upload.schema.ts create mode 100644 app/services/supporting-document.service.ts create mode 100644 public/side-menu-items/sys.json 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 @@ + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + \ 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