form cleanup

feat(patient): add edit functionality to patient form

- Modify genPatientEntity to accept existing patient data for updates
- Add handleActionEdit handler for edit mode
- Update form to handle both create and edit modes
- Rename patient ref to patientDetail for clarity

refactor(patient): update marital status codes and job options mapping

- Change marital status enum values to standardized codes (S, M, D, W)
- Simplify job options and marital status options mapping using mapToComboboxOptList
- Add error handling in patient data loading

ajust styling text based on combobox

wip: edit patient redirect

refactor(models): update type definitions and form field handling

- Add field-name prop to SelectDob component for better form handling
- Update Person and Patient interfaces to use null for optional fields
- Add maritalStatus_code field to Person interface
- Improve type safety by replacing undefined with null for optional fields

fix casting radio str to boolean and parsing date error
This commit is contained in:
Khafid Prayoga
2025-12-08 20:43:30 +07:00
parent e967ee1cf0
commit 910b641750
13 changed files with 175 additions and 194 deletions
+7 -6
View File
@@ -34,7 +34,7 @@ interface PatientFormInput {
drivingLicenseNumber?: string
passportNumber?: string
fullName?: string
isNewBorn?: 'YA' | 'TIDAK'
isNewBorn?: string
gender?: string
birthPlace?: string
birthDate?: string
@@ -45,8 +45,8 @@ interface PatientFormInput {
ethnicity?: string
language?: string
religion?: string
communicationBarrier?: 'YA' | 'TIDAK'
disability?: 'YA' | 'TIDAK'
communicationBarrier?: string
disability?: string
disabilityType?: string
note?: string
residentIdentityFile?: File
@@ -61,7 +61,7 @@ interface Props {
const props = defineProps<Props>()
const formSchema = toTypedSchema(PatientSchema)
const { values, resetForm, setValues, validate } = useForm<FormData>({
const { values, resetForm, setValues, setFieldValue, validate } = useForm<FormData>({
name: 'patientForm',
validationSchema: formSchema,
initialValues: (props.initialValues ?? {}) as any,
@@ -135,6 +135,7 @@ defineExpose({
:is-disabled="isReadonly"
/>
<SelectDob
field-name="birthDate"
label="Tanggal Lahir"
is-required
:is-disabled="isReadonly"
@@ -203,8 +204,8 @@ defineExpose({
<SelectDisability
label="Jenis Disabilitas"
field-name="disabilityType"
:is-disabled="isReadonly || values.disability !== 'YA'"
:is-required="values.disability === 'YA'"
:is-disabled="isReadonly || values.disability !== 'yes'"
:is-required="values.disability === 'yes'"
/>
<InputBase
field-name="note"
@@ -29,8 +29,8 @@ const {
} = props
const genderOptions = [
{ label: 'Ya', value: 'YA' },
{ label: 'Tidak', value: 'TIDAK' },
{ label: 'Ya', value: 'yes' },
{ label: 'Tidak', value: 'no' },
]
</script>
@@ -47,7 +47,7 @@ const genderOptions = [
:id="fieldName"
:errors="errors"
class="pt-0.5"
>
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
@@ -80,9 +80,7 @@ const genderOptions = [
:class="
cn(
'select-none text-xs !font-normal leading-none transition-colors sm:text-sm',
isDisabled
? 'cursor-not-allowed opacity-70'
: 'cursor-pointer hover:text-primary',
isDisabled ? 'cursor-not-allowed opacity-70' : 'cursor-pointer hover:text-primary',
labelClass,
)
"
@@ -28,9 +28,9 @@ const {
labelClass,
} = props
const dissabilityOptions = [
{ label: 'Ya', value: 'YA' },
{ label: 'Tidak', value: 'TIDAK' },
const disabilityOptions = [
{ label: 'Ya', value: 'yes' },
{ label: 'Tidak', value: 'no' },
]
</script>
@@ -60,7 +60,7 @@ const dissabilityOptions = [
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
>
<div
v-for="(option, index) in dissabilityOptions"
v-for="(option, index) in disabilityOptions"
:key="option.value"
:class="cn('flex min-w-fit items-center space-x-2', radioItemClass)"
>
@@ -70,7 +70,7 @@ const dissabilityOptions = [
:value="option.value"
:class="
cn(
'peer border-1 relative h-4 w-4 rounded-full border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
'border-1 peer relative h-4 w-4 rounded-full border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
containerClass,
)
"
@@ -80,9 +80,7 @@ const dissabilityOptions = [
:class="
cn(
'select-none text-xs !font-normal leading-none transition-colors',
isDisabled
? 'cursor-not-allowed opacity-70'
: 'cursor-pointer hover:text-primary',
isDisabled ? 'cursor-not-allowed opacity-70' : 'cursor-pointer hover:text-primary',
labelClass,
)
"
@@ -29,8 +29,8 @@ const {
} = props
const newbornOptions = [
{ label: 'Ya', value: 'YA' },
{ label: 'Tidak', value: 'TIDAK' },
{ label: 'Ya', value: 'yes' },
{ label: 'Tidak', value: 'no' },
]
</script>
@@ -81,9 +81,7 @@ const newbornOptions = [
:class="
cn(
'select-none text-xs !font-normal leading-none transition-colors',
isDisabled
? 'cursor-not-allowed opacity-70'
: 'cursor-pointer hover:text-primary',
isDisabled ? 'cursor-not-allowed opacity-70' : 'cursor-pointer hover:text-primary',
labelClass,
)
"
@@ -30,7 +30,10 @@ const {
} = props
// Generate job options from constants, sama seperti pola genderCodes
const jobOptions = mapToComboboxOptList(occupationCodes)
const jobOptions = mapToComboboxOptList(occupationCodes).map(({ label, value }) => ({
label,
value,
}))
</script>
<template>
@@ -1,7 +1,9 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import { maritalStatusCodes } from '~/const/key-val/person'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
@@ -27,13 +29,10 @@ const {
placeholder = 'Pilih',
} = props
const maritalStatusOptions = [
{ label: 'Tidak Diketahui', value: 'TIDAK_DIKETAHUI' },
{ label: 'Belum Kawin', value: 'BELUM_KAWIN' },
{ label: 'Kawin', value: 'KAWIN' },
{ label: 'Cerai Hidup', value: 'CERAI_HIDUP' },
{ label: 'Cerai Mati', value: 'CERAI_MATI' },
]
const maritalStatusOptions = mapToComboboxOptList(maritalStatusCodes).map(({ label, value }) => ({
label,
value,
}))
</script>
<template>
@@ -53,7 +53,6 @@ async function onBack() {
})
}
async function onEdit() {
console.log(props.patientId)
await navigateTo({
name: 'client-patient-id-edit',
params: {
+70 -32
View File
@@ -18,6 +18,10 @@ import AppPersonFamilyParentsForm from '~/components/app/person/family-parents-f
import AppPersonContactEntryForm from '~/components/app/person-contact/entry-form.vue'
import AppPersonRelativeEntryForm from '~/components/app/person-relative/entry-form.vue'
// helper
import { format, parseISO } from 'date-fns'
import { id as localeID } from 'date-fns/locale'
// services
import { getPatientDetail, uploadAttachment } from '~/services/patient.service'
@@ -28,6 +32,7 @@ import {
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleCancelForm,
} from '~/handlers/patient.handler'
@@ -62,7 +67,7 @@ const personPatientForm = ref<ExposedForm<any> | null>(null)
// #endregion
// #region State & Computed
const patient = ref(
const patientDetail = ref(
withBase<PatientEntity>({
person: {} as Person,
personAddresses: [],
@@ -76,36 +81,40 @@ const formKey = ref(0)
// Computed: unwrap patient data untuk form patient
const patientFormInitialValues = computed(() => {
const p = patient.value
const p = patientDetail.value
const person = p.person
if (!person || !person.name) return undefined
const hasDisability = String(person?.disability).length > 0
const date = parseISO(person.birthDate!)
const birthDate = format(date, 'yyyy-MM-dd', { locale: localeID })
return {
identityNumber: person.residentIdentityNumber || '',
drivingLicenseNumber: person.drivingLicenseNumber || '',
passportNumber: person.passportNumber || '',
identityNumber: person.residentIdentityNumber || undefined,
drivingLicenseNumber: person.drivingLicenseNumber || undefined,
passportNumber: person.passportNumber || undefined,
fullName: person.name || '',
isNewBorn: (p.newBornStatus ? 'YA' : 'TIDAK') as 'YA' | 'TIDAK',
isNewBorn: p.newBornStatus ? 'yes' : 'no',
gender: person.gender_code || '',
birthPlace: person.birthRegency_code || '',
birthDate: person.birthDate ? new Date(person.birthDate).toISOString().split('T')[0] : '',
birthDate: birthDate,
education: person.education_code || '',
job: person.occupation_code || '',
maritalStatus: '', // perlu mapping jika ada field ini
nationality: person.nationality || 'WNI',
ethnicity: person.ethnic_code || '',
language: person.language_code || '',
religion: person.religion_code || '',
communicationBarrier: (person.communicationIssueStatus ? 'YA' : 'TIDAK') as 'YA' | 'TIDAK',
disability: (person.disability ? 'YA' : 'TIDAK') as 'YA' | 'TIDAK',
disabilityType: person.disability || '',
maritalStatus: person?.maritalStatus_code,
nationality: person?.nationality || 'WNI',
ethnicity: person?.ethnic_code || '',
language: person?.language_code || '',
religion: person?.religion_code || '',
communicationBarrier: person?.communicationIssueStatus ? 'yes' : 'no',
disability: hasDisability ? 'yes' : 'no',
disabilityType: hasDisability ? String(person?.disability) || '' : '',
note: '',
}
})
// Computed: unwrap alamat domisili (alamat sekarang)
const addressFormInitialValues = computed(() => {
const addresses = patient.value.person?.addresses || patient.value.personAddresses || []
const addresses = patientDetail.value.person?.addresses || patientDetail.value.personAddresses || []
const domicileAddress = addresses.find((a: PersonAddress) => a.locationType_code === 'domicile')
if (!domicileAddress) return undefined
@@ -130,7 +139,7 @@ const addressFormInitialValues = computed(() => {
// Computed: unwrap alamat KTP (identity)
const addressRelativeFormInitialValues = computed(() => {
const addresses = patient.value.person?.addresses || patient.value.personAddresses || []
const addresses = patientDetail.value.person?.addresses || patientDetail.value.personAddresses || []
const domicileAddress = addresses.find((a: PersonAddress) => a.locationType_code === 'domicile')
const identityAddress = addresses.find((a: PersonAddress) => a.locationType_code === 'identity')
@@ -177,7 +186,7 @@ const addressRelativeFormInitialValues = computed(() => {
// Computed: unwrap data orang tua
const familyFormInitialValues = computed(() => {
const relatives = patient.value.person?.relatives || patient.value.personRelatives || []
const relatives = patientDetail.value.person?.relatives || patientDetail.value.personRelatives || []
const parents = relatives.filter(
(r: PersonRelative) => !r.responsible && (r.relationship_code === 'mother' || r.relationship_code === 'father'),
)
@@ -202,7 +211,7 @@ const familyFormInitialValues = computed(() => {
// Computed: unwrap kontak pasien
const contactFormInitialValues = computed(() => {
const contacts = patient.value.person?.contacts || patient.value.personContacts || []
const contacts = patientDetail.value.person?.contacts || patientDetail.value.personContacts || []
if (contacts.length === 0) return undefined
return {
@@ -215,7 +224,7 @@ const contactFormInitialValues = computed(() => {
// Computed: unwrap penanggung jawab
const responsibleFormInitialValues = computed(() => {
const relatives = patient.value.person?.relatives || patient.value.personRelatives || []
const relatives = patientDetail.value.person?.relatives || patientDetail.value.personRelatives || []
const responsibles = relatives.filter((r: PersonRelative) => r.responsible === true)
if (responsibles.length === 0) return undefined
@@ -252,7 +261,6 @@ async function composeFormData(): Promise<PatientEntity> {
])
const results = [patient, address, addressRelative, families, contacts, emergencyContact]
console.log(results)
const allValid = results.every((r) => r?.valid)
// exit, if form errors happend during validation
@@ -260,7 +268,15 @@ async function composeFormData(): Promise<PatientEntity> {
if (!allValid) return Promise.reject('Form validation failed')
const formDataRequest: genPatientProps = {
patient: patient?.values,
patient: {
...patient?.values,
// casting comp. val to backend well known reflect value
birthDate: parseISO(patient?.values.birthDate || ''),
isNewBorn: patient?.values.isNewBorn === 'yes',
communicationBarrier: patient?.values.communicationBarrier === 'yes',
disability: patient?.values.disability === 'yes' ? patient?.values.disabilityType : '',
},
residentAddress: address?.values,
cardAddress: addressRelative?.values,
familyData: families?.values,
@@ -268,7 +284,7 @@ async function composeFormData(): Promise<PatientEntity> {
responsible: emergencyContact?.values,
}
const formData = genPatientEntity(formDataRequest)
const formData = genPatientEntity(formDataRequest, patientDetail.value)
if (patient?.values.residentIdentityFile) {
residentIdentityFile.value = patient?.values.residentIdentityFile
@@ -288,12 +304,19 @@ async function loadInitData(id: number | string) {
try {
const response = await getPatientDetail(id as number)
if (response.success) {
patient.value = response.body.data || {}
patientDetail.value = response.body.data || {}
// Increment key untuk memaksa re-render form dengan data baru
formKey.value++
}
} finally {
isProcessing.value = false
} catch (error) {
console.error(error)
toast({
title: 'Error',
description: 'Terjadi kesalahan saat mengambil data',
variant: 'destructive',
})
}
}
@@ -301,14 +324,29 @@ async function handleActionClick(eventType: string) {
try {
if (eventType === 'submit') {
const patient: Patient = await composeFormData()
let createdPatientId = 0
const response = await handleActionSave(
patient,
() => {},
() => {},
toast,
)
let createdPatientId = 0
let response: any
// If edit mode, update patient
if (props.mode === 'edit' && props.patientId) {
response = await handleActionEdit(
patientDetail.value.id,
patient,
() => {},
() => {},
toast,
)
}
// If create mode, create patient
else {
response = await handleActionSave(
patient,
() => {},
() => {},
toast,
)
}
const data = (response?.body?.data ?? null) as PatientBase | null
if (!data) return
+9 -5
View File
@@ -137,18 +137,22 @@ provide('table_data_loader', isLoading)
// #endregion
// #region Watchers
watch([recId, recAction], () => {
switch (recAction.value) {
watch([recId, recAction], ([newId, newAction]) => {
switch (newAction) {
case ActionEvents.showDetail:
navigateTo({
name: 'client-patient-id',
params: { id: recId.value },
params: { id: newId },
})
break
case ActionEvents.showEdit:
// TODO: Handle edit action
// isFormEntryDialogOpen.value = true
navigateTo({
name: 'client-patient-id-edit',
params: {
id: newId,
},
})
break
case ActionEvents.showConfirmDelete:
+1 -1
View File
@@ -122,7 +122,7 @@ watch(
<SelectTrigger
:class="
cn(
'rounded-md focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white',
'h-8 rounded-md font-normal focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white md:text-xs 2xl:h-9 2xl:text-sm',
{
'cursor-not-allowed bg-gray-100 opacity-50': isDisabled,
'bg-white text-black dark:bg-gray-800 dark:text-white': !isDisabled,