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:
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user