diff --git a/app/components/app/patient/entry-form.vue b/app/components/app/patient/entry-form.vue
index 228c7911..46efdc31 100644
--- a/app/components/app/patient/entry-form.vue
+++ b/app/components/app/patient/entry-form.vue
@@ -3,7 +3,7 @@ import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import { Form } from '~/components/pub/ui/form'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
-import FileUpload from '~/components/pub/my-ui/form/file-upload.vue'
+import FileUpload from '~/components/pub/my-ui/form/file-field.vue'
import InputName from './_common/input-name.vue'
import RadioCommunicationBarrier from './_common/radio-communication-barrier.vue'
import RadioDisability from './_common/radio-disability.vue'
@@ -144,20 +144,22 @@ defineExpose({
diff --git a/app/components/app/patient/preview.vue b/app/components/app/patient/preview.vue
index b1439f64..03ec65fb 100644
--- a/app/components/app/patient/preview.vue
+++ b/app/components/app/patient/preview.vue
@@ -184,19 +184,6 @@ function onClick(type: string) {
{{ relationshipOptions.find((item) => item.code === relative.relationship_code)?.label || '-' }}
-
- {{ genderOptions.find((item) => item.code === relative.gender_code)?.label || '-' }}
-
-
- {{ educationOptions.find((item) => item.code === relative.education_code)?.label || '-' }}
-
-
- {{
- occupationOptions.find((item) => item.code === relative.occupation_code)?.label ||
- relative.occupation_name ||
- '-'
- }}
-
{{ relative.address || '-' }}
{{ relative.phoneNumber || '-' }}
diff --git a/app/components/content/patient/entry.vue b/app/components/content/patient/entry.vue
index 2cde4320..ce27b487 100644
--- a/app/components/content/patient/entry.vue
+++ b/app/components/content/patient/entry.vue
@@ -10,7 +10,8 @@ import { PersonAddressSchema } from '~/schemas/person-address.schema'
import { PersonContactListSchema } from '~/schemas/person-contact.schema'
import { PersonFamiliesSchema } from '~/schemas/person-family.schema'
import { ResponsiblePersonSchema } from '~/schemas/person-relative.schema'
-import { postPatient } from '~/services/patient.service'
+import { postPatient, uploadAttachment } from '~/services/patient.service'
+import { uploadCode } from '~/lib/constants'
// #region Props & Emits
const props = defineProps<{
@@ -97,13 +98,26 @@ async function sendRequest() {
try {
const result = await postPatient(formData)
- const patientData: PatientBase = result.body
+ const patientData: PatientBase = result.body.data
if (result.success) {
console.log('Patient created successfully:', patientData)
+ const createdPatientId = patientData.id
+
+ // void run uploadAttachment in background so this try-catch non blocking
+ // behavior: fire-and-forget
+ if (patient?.values.residentIdentityFile) {
+ void uploadAttachment(patient?.values.residentIdentityFile, createdPatientId, 'ktp')
+ }
+
+ if (patient?.values.familyIdentityFile) {
+ void uploadAttachment(patient?.values.familyIdentityFile, createdPatientId, 'kk')
+ }
+ // 30s
+ await new Promise((r) => setTimeout(r, 30_000))
// If has callback provided redirect to callback with patientData
if (props.callbackUrl) {
- await navigateTo(props.callbackUrl + '?patient-id=' + patientData.person_id)
+ await navigateTo(props.callbackUrl + '?patient-id=' + patientData.id)
return
}
@@ -115,7 +129,6 @@ async function sendRequest() {
}
} catch (error) {
console.error('Error creating patient:', error)
- // Handle error - show error message to user
}
}
// #endregion region
diff --git a/app/components/pub/my-ui/form/file-upload.vue b/app/components/pub/my-ui/form/file-field.vue
similarity index 79%
rename from app/components/pub/my-ui/form/file-upload.vue
rename to app/components/pub/my-ui/form/file-field.vue
index a42dc6f2..84f7b1f0 100644
--- a/app/components/pub/my-ui/form/file-upload.vue
+++ b/app/components/pub/my-ui/form/file-field.vue
@@ -28,6 +28,13 @@ const hintMsg = computed(() => {
}
return 'Gunakan file yang sesuai'
})
+
+async function onFileChange(event: Event, handleChange: (value: any) => void) {
+ const target = event.target as HTMLInputElement
+ const file = target.files?.[0]
+
+ handleChange(file)
+}
@@ -44,20 +51,21 @@ const hintMsg = computed(() => {
:errors="errors"
>
-
-
+
+ {{ hintMsg }}
- {{ hintMsg }}
diff --git a/app/lib/constants.ts b/app/lib/constants.ts
index c61b10d2..5b579e27 100644
--- a/app/lib/constants.ts
+++ b/app/lib/constants.ts
@@ -327,7 +327,10 @@ export const uploadCode: Record = {
kk: 'person-family-card',
paspor: 'person-passport',
'mcu-report': 'mcu-item-result',
-}
+} as const
+
+export type UploadCodeKey = keyof typeof uploadCode
+export type UploadCodeValue = (typeof uploadCode)[UploadCodeKey]
export const infraGroupCodes: Record = {
building: 'Bangunan',
diff --git a/app/schemas/patient.schema.ts b/app/schemas/patient.schema.ts
index f915fbf0..2af50a67 100644
--- a/app/schemas/patient.schema.ts
+++ b/app/schemas/patient.schema.ts
@@ -12,6 +12,8 @@ const IsNewBornSchema = z
})
.transform((val) => val === 'YA')
+const ACCEPTED_UPLOAD_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
+
const PatientSchema = z
.object({
// Data Diri Pasien
@@ -21,8 +23,24 @@ const PatientSchema = z
// })
// .min(16, 'NIK harus berupa angka 16 digit')
// .regex(/^\d+$/, 'NIK harus berupa angka 16 digit'),
- // identityCardFile: z.instanceof(File, { message: 'File KTP harus dipilih' }),
- // familyCardFile: z.instanceof(File, { message: 'File KK harus dipilih' }),
+ residentIdentityFile: z
+ .any()
+ .optional()
+ .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 <= 1 * 1024 * 1024, { message: 'Maksimal 1MB' }),
+
+ familyIdentityFile: z
+ .any()
+ .optional()
+ .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 <= 1 * 1024 * 1024, { message: 'Maksimal 1MB' }),
+ // .refine(f => ['image/jpeg', 'image/png'].includes(f.type), 'Hanya JPG/PNG')
// Informasi Dasar
// alias: z.string({
diff --git a/app/services/patient.service.ts b/app/services/patient.service.ts
index b28f92ab..7591f356 100644
--- a/app/services/patient.service.ts
+++ b/app/services/patient.service.ts
@@ -1,4 +1,5 @@
import { xfetch } from '~/composables/useXfetch'
+import { uploadCode, type UploadCodeKey } from '~/lib/constants'
const mainUrl = '/api/v1/patient'
@@ -77,3 +78,30 @@ export async function removePatient(id: number) {
throw new Error('Failed to delete patient')
}
}
+
+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(`${mainUrl}/${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')
+ }
+}