Merge pull request #228 from dikstub-rssa/feat/patient-63-adjustment
Feat/patient 63 adjustment
This commit is contained in:
@@ -1,140 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import { differenceInDays, differenceInMonths, differenceInYears, parseISO } from 'date-fns'
|
||||
import { Input } from '~/components/pub/ui/input'
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
|
||||
const props = defineProps<{
|
||||
fieldName?: string
|
||||
label?: string
|
||||
placeholder?: string
|
||||
errors?: FormErrors
|
||||
class?: string
|
||||
selectClass?: string
|
||||
fieldGroupClass?: string
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
fieldName = 'birthDate',
|
||||
label = 'Tanggal Lahir',
|
||||
placeholder = 'Pilih tanggal lahir',
|
||||
errors,
|
||||
class: containerClass,
|
||||
fieldGroupClass,
|
||||
labelClass,
|
||||
} = props
|
||||
|
||||
// Reactive variables for age calculation
|
||||
const patientAge = ref<string>('Masukkan tanggal lahir')
|
||||
|
||||
// Function to calculate age with years, months, and days
|
||||
function calculateAge(birthDate: string | Date | undefined): string {
|
||||
if (!birthDate) {
|
||||
return 'Masukkan tanggal lahir'
|
||||
}
|
||||
|
||||
try {
|
||||
let dateObj: Date
|
||||
|
||||
if (typeof birthDate === 'string') {
|
||||
dateObj = parseISO(birthDate)
|
||||
} else {
|
||||
dateObj = birthDate
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
|
||||
// Calculate years, months, and days
|
||||
const totalYears = differenceInYears(today, dateObj)
|
||||
|
||||
// Calculate remaining months after years
|
||||
const yearsPassed = new Date(dateObj)
|
||||
yearsPassed.setFullYear(yearsPassed.getFullYear() + totalYears)
|
||||
const remainingMonths = differenceInMonths(today, yearsPassed)
|
||||
|
||||
// Calculate remaining days after years and months
|
||||
const monthsPassed = new Date(yearsPassed)
|
||||
monthsPassed.setMonth(monthsPassed.getMonth() + remainingMonths)
|
||||
const remainingDays = differenceInDays(today, monthsPassed)
|
||||
|
||||
// Format the result
|
||||
const parts = []
|
||||
if (totalYears > 0) parts.push(`${totalYears} Tahun`)
|
||||
if (remainingMonths > 0) parts.push(`${remainingMonths} Bulan`)
|
||||
if (remainingDays > 0) parts.push(`${remainingDays} Hari`)
|
||||
|
||||
return parts.length > 0 ? parts.join(' ') : '0 Hari'
|
||||
} catch {
|
||||
return 'Masukkan tanggal lahir'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||
<DE.Label
|
||||
:label-for="fieldName"
|
||||
:class="cn('select-field-label', labelClass)"
|
||||
:is-required="isRequired"
|
||||
>
|
||||
{{ label }}
|
||||
</DE.Label>
|
||||
<DE.Field
|
||||
:id="fieldName"
|
||||
:errors="errors"
|
||||
:class="cn('select-field-wrapper')"
|
||||
>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
:name="fieldName"
|
||||
>
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="birthDate"
|
||||
type="date"
|
||||
min="1900-01-01"
|
||||
:max="new Date().toISOString().split('T')[0]"
|
||||
v-bind="componentField"
|
||||
:placeholder="placeholder"
|
||||
@update:model-value="
|
||||
(value: string | number) => {
|
||||
const dateStr = typeof value === 'number' ? String(value) : value
|
||||
patientAge = calculateAge(dateStr)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
<DE.Cell>
|
||||
<DE.Label label-for="patientAge">Usia</DE.Label>
|
||||
<DE.Field id="patientAge">
|
||||
<FormField name="patientAge">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
:value="patientAge"
|
||||
disabled
|
||||
readonly
|
||||
placeholder="Masukkan tanggal lahir"
|
||||
:class="
|
||||
cn(
|
||||
'cursor-not-allowed bg-gray-50 focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0',
|
||||
)
|
||||
"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
</template>
|
||||
@@ -1,64 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import { useForm } from 'vee-validate'
|
||||
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-field.vue'
|
||||
import InputName from './_common/input-name.vue'
|
||||
import RadioCommunicationBarrier from './_common/radio-communication-barrier.vue'
|
||||
import RadioDisability from './_common/radio-disability.vue'
|
||||
import SelectGender from './_common/select-gender.vue'
|
||||
import RadioNationality from './_common/radio-nationality.vue'
|
||||
import RadioNewborn from './_common/radio-newborn.vue'
|
||||
import SelectBirthPlace from '~/components/app/person/_common/select-birth-place.vue'
|
||||
import SelectDisability from './_common/select-disability.vue'
|
||||
import SelectDob from './_common/select-dob.vue'
|
||||
import SelectEducation from './_common/select-education.vue'
|
||||
import SelectEthnicity from './_common/select-ethnicity.vue'
|
||||
import SelectJob from './_common/select-job.vue'
|
||||
import SelectLanguage from './_common/select-lang.vue'
|
||||
import SelectMaritalStatus from './_common/select-marital-status.vue'
|
||||
import SelectReligion from './_common/select-religion.vue'
|
||||
import Separator from '~/components/pub/ui/separator/Separator.vue'
|
||||
|
||||
// types
|
||||
import { type PatientFormData, PatientSchema } from '~/schemas/patient.schema'
|
||||
|
||||
// utils
|
||||
import { calculateAge } from '~/models/person'
|
||||
|
||||
// components
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
import { InputBase, FileField as FileUpload } from '~/components/pub/my-ui/form'
|
||||
import { SelectBirthPlace } from '~/components/app/person/fields'
|
||||
import {
|
||||
InputName,
|
||||
RadioCommunicationBarrier,
|
||||
RadioDisability,
|
||||
SelectGender,
|
||||
RadioNationality,
|
||||
RadioNewborn,
|
||||
SelectDisability,
|
||||
SelectDob,
|
||||
SelectEducation,
|
||||
SelectEthnicity,
|
||||
SelectJob,
|
||||
SelectLang as SelectLanguage,
|
||||
SelectMaritalStatus,
|
||||
SelectReligion,
|
||||
} from './fields'
|
||||
|
||||
const props = defineProps<{
|
||||
schema: any
|
||||
initialValues?: any
|
||||
errors?: FormErrors
|
||||
}>()
|
||||
interface FormData extends PatientFormData {
|
||||
_calculatedAge: string | Date
|
||||
}
|
||||
|
||||
const formSchema = toTypedSchema(props.schema)
|
||||
const formRef = ref()
|
||||
// Type untuk initial values (sebelum transform schema)
|
||||
interface PatientFormInput {
|
||||
identityNumber?: string
|
||||
drivingLicenseNumber?: string
|
||||
passportNumber?: string
|
||||
fullName?: string
|
||||
isNewBorn?: string
|
||||
gender?: string
|
||||
birthPlace?: string
|
||||
birthDate?: string
|
||||
education?: string
|
||||
job?: string
|
||||
maritalStatus?: string
|
||||
nationality?: string
|
||||
ethnicity?: string
|
||||
language?: string
|
||||
religion?: string
|
||||
communicationBarrier?: string
|
||||
disability?: string
|
||||
disabilityType?: string
|
||||
note?: string
|
||||
residentIdentityFile?: File
|
||||
familyIdentityFile?: File
|
||||
}
|
||||
|
||||
interface Props {
|
||||
isReadonly: boolean
|
||||
initialValues?: PatientFormInput
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const formSchema = toTypedSchema(PatientSchema)
|
||||
|
||||
const { values, resetForm, setValues, setFieldValue, validate, setFieldError } = useForm<FormData>({
|
||||
name: 'patientForm',
|
||||
validationSchema: formSchema,
|
||||
initialValues: (props.initialValues ?? {}) as any,
|
||||
validateOnMount: false,
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
validate: () => formRef.value?.validate(),
|
||||
resetForm: () => formRef.value?.resetForm(),
|
||||
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
|
||||
values: computed(() => formRef.value?.values),
|
||||
validate,
|
||||
resetForm,
|
||||
setValues,
|
||||
values,
|
||||
})
|
||||
|
||||
watch(
|
||||
() => values.birthDate,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
setFieldValue('_calculatedAge', calculateAge(newValue))
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => values.disability,
|
||||
(newValue) => {
|
||||
if (newValue === 'no') {
|
||||
setFieldValue('disabilityType', undefined)
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
ref="formRef"
|
||||
v-slot="{ values }"
|
||||
as=""
|
||||
keep-values
|
||||
:validation-schema="formSchema"
|
||||
:validate-on-mount="false"
|
||||
validation-mode="onSubmit"
|
||||
:initial-values="initialValues ? initialValues : {}"
|
||||
>
|
||||
<p class="text-sm 2xl:text-base mb-2 2xl:mb-3 font-semibold">Data Diri Pasien</p>
|
||||
<DE.Block :col-count="4" :cell-flex="false">
|
||||
<form @submit.prevent="">
|
||||
<p class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base">Data Diri Pasien</p>
|
||||
<DE.Block
|
||||
:col-count="4"
|
||||
:cell-flex="false"
|
||||
>
|
||||
<InputBase
|
||||
field-name="identityNumber"
|
||||
label="No. KTP"
|
||||
placeholder="Masukkan NIK"
|
||||
:errors="errors"
|
||||
numeric-only
|
||||
:is-disabled="isReadonly"
|
||||
:max-length="16"
|
||||
/>
|
||||
<InputBase
|
||||
field-name="drivingLicenseNumber"
|
||||
@@ -66,146 +123,155 @@ defineExpose({
|
||||
placeholder="Masukkan nomor SIM"
|
||||
numeric-only
|
||||
:max-length="20"
|
||||
:errors="errors"
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<InputBase
|
||||
field-name="passportNumber"
|
||||
label="No. Paspor"
|
||||
placeholder="Masukkan nomor paspor"
|
||||
:max-length="20"
|
||||
:errors="errors"
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<InputName
|
||||
field-name-alias="alias"
|
||||
field-name-input="fullName"
|
||||
label-for-input="Nama Lengkap"
|
||||
placeholder="Masukkan nama lengkap pasien"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<RadioNewborn
|
||||
field-name="isNewBorn"
|
||||
label="Pasien Bayi"
|
||||
placeholder="Pilih status pasien"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<SelectGender
|
||||
field-name="gender"
|
||||
label="Jenis Kelamin"
|
||||
placeholder="Pilih jenis kelamin"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<SelectBirthPlace
|
||||
field-name="birthPlace"
|
||||
label="Tempat Lahir"
|
||||
placeholder="Pilih tempat lahir"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<SelectDob
|
||||
field-name="birthDate"
|
||||
label="Tanggal Lahir"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<InputBase
|
||||
field-name="_calculatedAge"
|
||||
label="Usia"
|
||||
placeholder="-"
|
||||
numeric-only
|
||||
is-disabled
|
||||
/>
|
||||
<SelectEducation
|
||||
field-name="education"
|
||||
label="Pendidikan"
|
||||
placeholder="Pilih pendidikan"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<SelectJob
|
||||
field-name="job"
|
||||
label="Pekerjaan"
|
||||
placeholder="Pilih pekerjaan"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<SelectMaritalStatus
|
||||
field-name="maritalStatus"
|
||||
label="Status Perkawinan"
|
||||
placeholder="Pilih status Perkawinan"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<DE.Cell />
|
||||
<RadioNationality
|
||||
field-name="nationality"
|
||||
label="Kebangsaan"
|
||||
placeholder="Pilih kebangsaan"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<SelectEthnicity
|
||||
field-name="ethnicity"
|
||||
label="Suku"
|
||||
placeholder="Pilih suku bangsa"
|
||||
:errors="errors"
|
||||
:is-disabled="values.nationality !== 'WNI'"
|
||||
:is-disabled="isReadonly || values.nationality !== 'WNI'"
|
||||
/>
|
||||
<SelectLanguage
|
||||
field-name="language"
|
||||
label="Bahasa"
|
||||
placeholder="Pilih preferensi bahasa"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<SelectReligion
|
||||
field-name="religion"
|
||||
label="Agama"
|
||||
placeholder="Pilih agama"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<RadioCommunicationBarrier
|
||||
field-name="communicationBarrier"
|
||||
label="Hambatan Berkomunikasi"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<RadioDisability
|
||||
field-name="disability"
|
||||
label="Disabilitas"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<SelectDisability
|
||||
label="Jenis Disabilitas"
|
||||
field-name="disabilityType"
|
||||
:errors="errors"
|
||||
:is-disabled="values.disability !== 'YA'"
|
||||
:is-required="values.disability === 'YA'"
|
||||
:is-disabled="isReadonly || values.disability !== 'yes'"
|
||||
:is-required="values.disability === 'yes'"
|
||||
/>
|
||||
<InputBase
|
||||
field-name="note"
|
||||
label="Kepercayaan"
|
||||
placeholder="Contoh: tidak ingin diperiksa oleh dokter laki-laki"
|
||||
:errors="errors"
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
</DE.Block>
|
||||
|
||||
<div class="h-6"></div>
|
||||
|
||||
<p class="text-sm 2xl:text-base mb-2 2xl:mb-3 font-semibold">Dokumen Identitas</p>
|
||||
<DE.Block :col-count="2" :cell-flex="false">
|
||||
<p class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base">Dokumen Identitas</p>
|
||||
<DE.Block
|
||||
:col-count="2"
|
||||
:cell-flex="false"
|
||||
>
|
||||
<FileUpload
|
||||
field-name="identityCardFile"
|
||||
label="Dokumen KTP"
|
||||
placeholder="Unggah scan dokumen KTP"
|
||||
:errors="errors"
|
||||
:accept="['pdf', 'jpg', 'png']"
|
||||
:max-size-mb="1"
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<FileUpload
|
||||
field-name="familyCardFile"
|
||||
label="Dokumen KK"
|
||||
placeholder="Unggah scan dokumen KK"
|
||||
:errors="errors"
|
||||
:accept="['pdf', 'jpg', 'png']"
|
||||
:max-size-mb="1"
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
</DE.Block>
|
||||
</Form>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export { default as InputFile } from './input-file.vue'
|
||||
export { default as InputName } from './input-name.vue'
|
||||
export { default as RadioCommunicationBarrier } from './radio-communication-barrier.vue'
|
||||
export { default as RadioDisability } from './radio-disability.vue'
|
||||
export { default as RadioGender } from './radio-gender.vue'
|
||||
export { default as RadioNationality } from './radio-nationality.vue'
|
||||
export { default as RadioNewborn } from './radio-newborn.vue'
|
||||
export { default as SelectDisability } from './select-disability.vue'
|
||||
export { default as SelectDob } from './select-dob.vue'
|
||||
export { default as SelectEducation } from './select-education.vue'
|
||||
export { default as SelectEthnicity } from './select-ethnicity.vue'
|
||||
export { default as SelectGender } from './select-gender.vue'
|
||||
export { default as SelectJob } from './select-job.vue'
|
||||
export { default as SelectLang } from './select-lang.vue'
|
||||
export { default as SelectMaritalStatus } from './select-marital-status.vue'
|
||||
export { default as SelectReligion } from './select-religion.vue'
|
||||
+2
@@ -17,6 +17,7 @@ defineProps<{
|
||||
labelClass?: string
|
||||
maxLength?: number
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -40,6 +41,7 @@ defineProps<{
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
:disabled="isDisabled"
|
||||
v-bind="componentField"
|
||||
type="text"
|
||||
:placeholder="placeholder"
|
||||
+10
-6
@@ -15,6 +15,7 @@ const props = defineProps<{
|
||||
radioItemClass?: string
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
@@ -28,8 +29,8 @@ const {
|
||||
} = props
|
||||
|
||||
const genderOptions = [
|
||||
{ label: 'Ya', value: 'YA' },
|
||||
{ label: 'Tidak', value: 'TIDAK' },
|
||||
{ label: 'Ya', value: 'yes' },
|
||||
{ label: 'Tidak', value: 'no' },
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -46,7 +47,7 @@ const genderOptions = [
|
||||
:id="fieldName"
|
||||
:errors="errors"
|
||||
class="pt-0.5"
|
||||
>
|
||||
>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
:name="fieldName"
|
||||
@@ -55,6 +56,7 @@ const genderOptions = [
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
v-bind="componentField"
|
||||
:disabled="isDisabled"
|
||||
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
|
||||
>
|
||||
<div
|
||||
@@ -63,20 +65,22 @@ const genderOptions = [
|
||||
:class="cn('flex min-w-fit items-center space-x-2', radioItemClass)"
|
||||
>
|
||||
<RadioGroupItem
|
||||
:disabled="isDisabled"
|
||||
:id="`${fieldName}-${index}`"
|
||||
:value="option.value"
|
||||
:class="
|
||||
cn(
|
||||
'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',
|
||||
'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,
|
||||
)
|
||||
"
|
||||
/>
|
||||
<RadioLabel
|
||||
:for="`${fieldName}-${index}`"
|
||||
:for="isDisabled ? undefined : `${fieldName}-${index}`"
|
||||
:class="
|
||||
cn(
|
||||
'cursor-pointer select-none font-normal text-xs leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
||||
'select-none text-xs !font-normal leading-none transition-colors sm:text-sm',
|
||||
isDisabled ? 'cursor-not-allowed opacity-70' : 'cursor-pointer hover:text-primary',
|
||||
labelClass,
|
||||
)
|
||||
"
|
||||
+11
-7
@@ -15,6 +15,7 @@ const props = defineProps<{
|
||||
radioItemClass?: string
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
@@ -27,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>
|
||||
|
||||
@@ -55,28 +56,31 @@ const dissabilityOptions = [
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
v-bind="componentField"
|
||||
:disabled="isDisabled"
|
||||
: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)"
|
||||
>
|
||||
<RadioGroupItem
|
||||
:disabled="isDisabled"
|
||||
:id="`${fieldName}-${index}`"
|
||||
:value="option.value"
|
||||
:class="
|
||||
cn(
|
||||
'relative h-4 w-4 rounded-full border-1 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,
|
||||
)
|
||||
"
|
||||
/>
|
||||
<RadioLabel
|
||||
:for="`${fieldName}-${index}`"
|
||||
:for="isDisabled ? undefined : `${fieldName}-${index}`"
|
||||
:class="
|
||||
cn(
|
||||
'cursor-pointer select-none text-xs !font-normal leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
||||
'select-none text-xs !font-normal leading-none transition-colors',
|
||||
isDisabled ? 'cursor-not-allowed opacity-70' : 'cursor-pointer hover:text-primary',
|
||||
labelClass,
|
||||
)
|
||||
"
|
||||
+9
-3
@@ -15,6 +15,7 @@ const props = defineProps<{
|
||||
radioItemClass?: string
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
@@ -55,6 +56,7 @@ const nationalityOptions = [
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
v-bind="componentField"
|
||||
:disabled="isDisabled"
|
||||
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
|
||||
>
|
||||
<div
|
||||
@@ -63,20 +65,24 @@ const nationalityOptions = [
|
||||
:class="cn('flex min-w-fit items-center space-x-2', radioItemClass)"
|
||||
>
|
||||
<RadioGroupItem
|
||||
:disabled="isDisabled"
|
||||
:id="`${fieldName}-${index}`"
|
||||
:value="option.value"
|
||||
:class="
|
||||
cn(
|
||||
'relative h-4 w-4 rounded-full border-1 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',
|
||||
'peer relative h-4 w-4 rounded-full border-1 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,
|
||||
)
|
||||
"
|
||||
/>
|
||||
<RadioLabel
|
||||
:for="`${fieldName}-${index}`"
|
||||
:for="isDisabled ? undefined : `${fieldName}-${index}`"
|
||||
:class="
|
||||
cn(
|
||||
'cursor-pointer select-none text-xs !font-normal leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
||||
'select-none text-xs !font-normal leading-none transition-colors sm:text-sm',
|
||||
isDisabled
|
||||
? 'cursor-not-allowed opacity-70'
|
||||
: 'cursor-pointer hover:text-primary',
|
||||
labelClass,
|
||||
)
|
||||
"
|
||||
+13
-6
@@ -15,6 +15,7 @@ const props = defineProps<{
|
||||
radioItemClass?: string
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
@@ -28,13 +29,16 @@ const {
|
||||
} = props
|
||||
|
||||
const newbornOptions = [
|
||||
{ label: 'Ya', value: 'YA' },
|
||||
{ label: 'Tidak', value: 'TIDAK' },
|
||||
{ label: 'Ya', value: 'yes' },
|
||||
{ label: 'Tidak', value: 'no' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DE.Cell :class="cn('radio-group-field', containerClass)" :col-span="2">
|
||||
<DE.Cell
|
||||
:class="cn('radio-group-field', containerClass)"
|
||||
:col-span="2"
|
||||
>
|
||||
<DE.Label
|
||||
:label-for="fieldName"
|
||||
:is-required="isRequired"
|
||||
@@ -52,6 +56,7 @@ const newbornOptions = [
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
:disabled="isDisabled"
|
||||
v-bind="componentField"
|
||||
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
|
||||
>
|
||||
@@ -61,20 +66,22 @@ const newbornOptions = [
|
||||
:class="cn('flex min-w-fit items-center space-x-2 pt-1', radioItemClass)"
|
||||
>
|
||||
<RadioGroupItem
|
||||
:disabled="isDisabled"
|
||||
:id="`${fieldName}-${index}`"
|
||||
:value="option.value"
|
||||
:class="
|
||||
cn(
|
||||
'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',
|
||||
'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,
|
||||
)
|
||||
"
|
||||
/>
|
||||
<RadioLabel
|
||||
:for="`${fieldName}-${index}`"
|
||||
:for="isDisabled ? undefined : `${fieldName}-${index}`"
|
||||
:class="
|
||||
cn(
|
||||
'cursor-pointer select-none text-xs font-normal leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
||||
'select-none text-xs !font-normal leading-none transition-colors',
|
||||
isDisabled ? 'cursor-not-allowed opacity-70' : 'cursor-pointer hover:text-primary',
|
||||
labelClass,
|
||||
)
|
||||
"
|
||||
+7
-10
@@ -5,6 +5,9 @@ import { cn } from '~/lib/utils'
|
||||
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
|
||||
import { disabilityCodes } from '~/lib/constants'
|
||||
import { mapToComboboxOptList } from '~/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
fieldName?: string
|
||||
label?: string
|
||||
@@ -26,16 +29,10 @@ const {
|
||||
fieldGroupClass,
|
||||
} = props
|
||||
|
||||
const disabilityOptions = [
|
||||
{ label: 'Tuna Daksa', value: 'daksa' },
|
||||
{ label: 'Tuna Netra', value: 'netra' },
|
||||
{ label: 'Tuna Rungu', value: 'rungu' },
|
||||
{ label: 'Tuna Wicara', value: 'wicara' },
|
||||
{ label: 'Tuna Rungu-Wicara', value: 'rungu_wicara' },
|
||||
{ label: 'Tuna Grahita', value: 'grahita' },
|
||||
{ label: 'Tuna Laras', value: 'laras' },
|
||||
{ label: 'Lainnya', value: 'other', priority: -100 },
|
||||
]
|
||||
const disabilityOptions = mapToComboboxOptList(disabilityCodes).map(({ label, value }) => ({
|
||||
label,
|
||||
value,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { calculateAge } from '~/models/person'
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
// componenets
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
import { Input } from '~/components/pub/ui/input'
|
||||
|
||||
const props = defineProps<{
|
||||
fieldName?: string
|
||||
label?: string
|
||||
placeholder?: string
|
||||
class?: string
|
||||
selectClass?: string
|
||||
fieldGroupClass?: string
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
fieldName = 'birthDate',
|
||||
label = 'Tanggal Lahir',
|
||||
placeholder = 'Pilih tanggal lahir',
|
||||
class: containerClass,
|
||||
fieldGroupClass,
|
||||
labelClass,
|
||||
} = props
|
||||
|
||||
// Reactive variables for age calculation
|
||||
const patientAge = ref<string>('-')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||
<DE.Label
|
||||
:label-for="fieldName"
|
||||
:class="cn('select-field-label', labelClass)"
|
||||
:is-required="isRequired"
|
||||
>
|
||||
{{ label }}
|
||||
</DE.Label>
|
||||
<DE.Field
|
||||
:id="fieldName"
|
||||
:class="cn('select-field-wrapper')"
|
||||
>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
:name="fieldName"
|
||||
>
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
:disabled="isDisabled"
|
||||
id="birthDate"
|
||||
type="date"
|
||||
min="1900-01-01"
|
||||
:max="new Date().toISOString().split('T')[0]"
|
||||
v-bind="componentField"
|
||||
:placeholder="placeholder"
|
||||
@update:model-value="
|
||||
(value: string | number) => {
|
||||
const dateStr = typeof value === 'number' ? String(value) : value
|
||||
patientAge = calculateAge(dateStr)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
</template>
|
||||
+1
-1
@@ -21,7 +21,6 @@ const props = defineProps<{
|
||||
|
||||
const {
|
||||
fieldName = 'education',
|
||||
label = 'Pendidikan',
|
||||
placeholder = 'Pilih pendidikan terakhir',
|
||||
errors,
|
||||
class: containerClass,
|
||||
@@ -47,6 +46,7 @@ const educationOptions = [
|
||||
<template>
|
||||
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||
<DE.Label
|
||||
v-if="label"
|
||||
:label-for="fieldName"
|
||||
:class="cn('select-field-label', labelClass)"
|
||||
:is-required="isRequired && !isDisabled"
|
||||
+9
-4
@@ -16,11 +16,11 @@ const props = defineProps<{
|
||||
fieldGroupClass?: string
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
fieldName = 'job',
|
||||
label = 'Pekerjaan',
|
||||
placeholder = 'Pilih pekerjaan',
|
||||
errors,
|
||||
class: containerClass,
|
||||
@@ -29,15 +29,19 @@ 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>
|
||||
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||
<DE.Label
|
||||
v-if="label"
|
||||
:label-for="fieldName"
|
||||
:class="cn('select-field-label', labelClass)"
|
||||
:is-required="isRequired"
|
||||
:is-required="isRequired && !isDisabled"
|
||||
>
|
||||
{{ label }}
|
||||
</DE.Label>
|
||||
@@ -53,8 +57,9 @@ const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
:id="fieldName"
|
||||
v-bind="componentField"
|
||||
:is-disabled="isDisabled"
|
||||
:id="fieldName"
|
||||
:items="jobOptions"
|
||||
:placeholder="placeholder"
|
||||
search-placeholder="Cari..."
|
||||
+2
@@ -15,6 +15,7 @@ const props = defineProps<{
|
||||
fieldGroupClass?: string
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
@@ -60,6 +61,7 @@ const langOptions = [
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select
|
||||
:is-disabled="isDisabled"
|
||||
:id="fieldName"
|
||||
v-bind="componentField"
|
||||
:items="langOptions"
|
||||
+10
-9
@@ -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'
|
||||
|
||||
@@ -14,6 +16,7 @@ const props = defineProps<{
|
||||
fieldGroupClass?: string
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
@@ -26,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>
|
||||
@@ -56,8 +56,9 @@ const maritalStatusOptions = [
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select
|
||||
:id="fieldName"
|
||||
v-bind="componentField"
|
||||
:is-disabled="isDisabled"
|
||||
:id="fieldName"
|
||||
:items="maritalStatusOptions"
|
||||
:placeholder="placeholder"
|
||||
:preserve-order="true"
|
||||
+13
-7
@@ -16,6 +16,7 @@ const props = defineProps<{
|
||||
fieldGroupClass?: string
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
@@ -34,13 +35,17 @@ const extendOptions = [
|
||||
{ label: 'Kepercayaan Lain', value: 'other', priority: -1 },
|
||||
]
|
||||
|
||||
const religionOptions = [
|
||||
...mapToComboboxOptList(religionCodes).map(({ label, value }) => ({
|
||||
label,
|
||||
value,
|
||||
})),
|
||||
...extendOptions,
|
||||
]
|
||||
const religionOptions = Array.from(
|
||||
new Map(
|
||||
[
|
||||
...mapToComboboxOptList(religionCodes).map(({ label, value }) => ({
|
||||
label,
|
||||
value,
|
||||
})),
|
||||
...extendOptions,
|
||||
].map((item) => [item.value, item]),
|
||||
).values(),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -64,6 +69,7 @@ const religionOptions = [
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select
|
||||
:is-disabled="isDisabled"
|
||||
:id="fieldName"
|
||||
v-bind="componentField"
|
||||
:items="religionOptions"
|
||||
@@ -1,11 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { Patient } from '~/models/patient'
|
||||
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
|
||||
import DetailSection from '~/components/pub/my-ui/form/view/detail-section.vue'
|
||||
import { formatAddress } from '~/models/person-address'
|
||||
import { format } from 'date-fns'
|
||||
import { formatInTimeZone } from 'date-fns-tz'
|
||||
import { id } from 'date-fns/locale'
|
||||
|
||||
// types
|
||||
import type { Patient } from '~/models/patient'
|
||||
import type { ClickType } from '~/components/pub/my-ui/nav-footer'
|
||||
|
||||
// helper
|
||||
import { formatAddress } from '~/models/person-address'
|
||||
import {
|
||||
addressLocationTypeCode,
|
||||
disabilityCodes,
|
||||
educationCodes,
|
||||
genderCodes,
|
||||
occupationCodes,
|
||||
@@ -14,6 +20,15 @@ import {
|
||||
religionCodes,
|
||||
} from '~/lib/constants'
|
||||
import { mapToComboboxOptList } from '~/lib/utils'
|
||||
import { calculateAge } from '~/models/person'
|
||||
|
||||
// components
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '~/components/pub/ui/accordion'
|
||||
import { Fragment } from '~/components/pub/my-ui/form/'
|
||||
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
|
||||
import DetailSection from '~/components/pub/my-ui/form/view/detail-section.vue'
|
||||
import { Skeleton } from '~/components/pub/ui/skeleton'
|
||||
import { toZoned } from '@internationalized/date'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = defineProps<{
|
||||
@@ -21,7 +36,8 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', type: string): void
|
||||
(e: 'back'): void
|
||||
(e: 'edit'): void
|
||||
}>()
|
||||
|
||||
// #endregion
|
||||
@@ -33,6 +49,7 @@ const educationOptions = mapToComboboxOptList(educationCodes)
|
||||
const occupationOptions = mapToComboboxOptList(occupationCodes)
|
||||
const relationshipOptions = mapToComboboxOptList(relationshipCodes)
|
||||
const personContactTypeOptions = mapToComboboxOptList(personContactTypes)
|
||||
const disabilityOptions = mapToComboboxOptList(disabilityCodes)
|
||||
|
||||
// Computed addresses from nested data
|
||||
const domicileAddress = computed(() => {
|
||||
@@ -51,14 +68,7 @@ const patientAge = computed(() => {
|
||||
if (!props.patient.person.birthDate) {
|
||||
return '-'
|
||||
}
|
||||
const birthDate = new Date(props.patient.person.birthDate)
|
||||
const today = new Date()
|
||||
let age = today.getFullYear() - birthDate.getFullYear()
|
||||
const monthDiff = today.getMonth() - birthDate.getMonth()
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
||||
age--
|
||||
}
|
||||
return age
|
||||
return calculateAge(props.patient.person.birthDate)
|
||||
})
|
||||
// #endregion
|
||||
|
||||
@@ -70,8 +80,9 @@ const patientAge = computed(() => {
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
function onClick(type: string) {
|
||||
emit('click', type)
|
||||
function onNavigate(type: ClickType) {
|
||||
if (type == 'back') emit('back')
|
||||
if (type == 'edit') emit('edit')
|
||||
}
|
||||
// #endregion
|
||||
|
||||
@@ -80,120 +91,217 @@ function onClick(type: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DetailSection title="Data Pasien">
|
||||
<DetailRow label="Nomor">{{ patient.number || '-' }}</DetailRow>
|
||||
<DetailRow label="Nama Lengkap">{{ patient.person.name || '-' }}</DetailRow>
|
||||
<DetailRow label="Tempat, tanggal lahir">
|
||||
{{ patient.person.birthRegency?.name || '-' }},
|
||||
{{ patient.person.birthDate ? new Date(patient.person.birthDate).toLocaleDateString('id-ID') : '-' }}
|
||||
</DetailRow>
|
||||
<DetailRow label="Usia">{{ patientAge || '-' }} Tahun</DetailRow>
|
||||
<DetailRow label="Tanggal Daftar">
|
||||
{{ patient.person.createdAt ? new Date(patient.person.createdAt).toLocaleDateString('id-ID') : '-' }}
|
||||
</DetailRow>
|
||||
<DetailRow label="Jenis Kelamin">
|
||||
{{ genderOptions.find((item) => item.code === patient.person.gender_code)?.label || '-' }}
|
||||
</DetailRow>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
class="w-full"
|
||||
collapsible
|
||||
:defaultValue="['item-patient', 'item-document', 'item-address', 'item-contact', 'item-parents', 'item-relative']"
|
||||
>
|
||||
<Fragment
|
||||
v-slot="{ section }"
|
||||
title="Data Pasien"
|
||||
>
|
||||
<AccordionItem value="item-patient">
|
||||
<AccordionTrigger>{{ section }}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<DetailRow label="Nomor">{{ patient.number || '-' }}</DetailRow>
|
||||
<DetailRow label="Nama Lengkap">{{ patient.person.name || '-' }}</DetailRow>
|
||||
<DetailRow label="Tempat, tanggal lahir">
|
||||
{{ patient.person.birthRegency?.name || '-' }},
|
||||
{{
|
||||
patient.person.birthDate
|
||||
? format(new Date(patient.person.birthDate), 'dd MMMM yyyy', { locale: id })
|
||||
: '-'
|
||||
}}
|
||||
</DetailRow>
|
||||
<DetailRow label="Usia">{{ patientAge || '-' }}</DetailRow>
|
||||
<DetailRow label="Tanggal Daftar">
|
||||
{{
|
||||
patient.person.createdAt
|
||||
? formatInTimeZone(new Date(patient.person.createdAt), 'Asia/Jakarta', "dd MMMM yyyy, HH:mm:ss 'WIB'", {
|
||||
locale: id,
|
||||
})
|
||||
: '-'
|
||||
}}
|
||||
</DetailRow>
|
||||
<DetailRow label="Jenis Kelamin">
|
||||
{{ genderOptions.find((item) => item.code === patient.person.gender_code)?.label || '-' }}
|
||||
</DetailRow>
|
||||
|
||||
<DetailRow label="NIK">{{ patient.person.residentIdentityNumber || '-' }}</DetailRow>
|
||||
<DetailRow label="No. SIM">{{ patient.person.drivingLicenseNumber || '-' }}</DetailRow>
|
||||
<DetailRow label="No. Paspor">{{ patient.person.passportNumber || '-' }}</DetailRow>
|
||||
<DetailRow label="NIK">{{ patient.person.residentIdentityNumber || '-' }}</DetailRow>
|
||||
<DetailRow label="No. SIM">{{ patient.person.drivingLicenseNumber || '-' }}</DetailRow>
|
||||
<DetailRow label="No. Paspor">{{ patient.person.passportNumber || '-' }}</DetailRow>
|
||||
|
||||
<DetailRow label="Agama">
|
||||
{{ religionOptions.find((item) => item.code === patient.person.religion_code)?.label || '-' }}
|
||||
</DetailRow>
|
||||
<DetailRow label="Suku">{{ patient.person.ethnic?.name || '-' }}</DetailRow>
|
||||
<DetailRow label="Bahasa">{{ patient.person.language?.name || '-' }}</DetailRow>
|
||||
<DetailRow label="Pendidikan">
|
||||
{{ educationOptions.find((item) => item.code === patient.person.education_code)?.label || '-' }}
|
||||
</DetailRow>
|
||||
<DetailRow label="Pekerjaan">
|
||||
{{
|
||||
occupationOptions.find((item) => item.code === patient.person.occupation_code)?.label ||
|
||||
patient.person.occupation_name ||
|
||||
'-'
|
||||
}}
|
||||
</DetailRow>
|
||||
</DetailSection>
|
||||
<DetailRow label="Agama">
|
||||
{{ religionOptions.find((item) => item.code === patient.person.religion_code)?.label || '-' }}
|
||||
</DetailRow>
|
||||
<DetailRow label="Suku">{{ patient.person.ethnic?.name || '-' }}</DetailRow>
|
||||
<DetailRow label="Bahasa">{{ patient.person.language?.name || '-' }}</DetailRow>
|
||||
<DetailRow label="Pendidikan">
|
||||
{{ educationOptions.find((item) => item.code === patient.person.education_code)?.label || '-' }}
|
||||
</DetailRow>
|
||||
<DetailRow label="Pekerjaan">
|
||||
{{
|
||||
occupationOptions.find((item) => item.code === patient.person.occupation_code)?.label ||
|
||||
patient.person.occupation_name ||
|
||||
'-'
|
||||
}}
|
||||
</DetailRow>
|
||||
<DetailRow label="Pasien Bayi">{{ patient.newBornStatus ? 'Ya' : 'Tidak' }}</DetailRow>
|
||||
<DetailRow label="Hambatan Komunikasi">
|
||||
{{ patient.person.communicationIssueStatus ? 'Ya' : 'Tidak' }}
|
||||
</DetailRow>
|
||||
<DetailRow label="Disabilitas">
|
||||
{{ disabilityOptions.find((item) => item.code === patient.person.disability)?.label || 'Tidak' }}
|
||||
</DetailRow>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Fragment>
|
||||
|
||||
<DetailSection title="Alamat">
|
||||
<DetailRow :label="addressLocationTypeCode.domicile || 'Alamat Domisili'">{{ domicileAddress || '-' }}</DetailRow>
|
||||
<DetailRow :label="addressLocationTypeCode.identity || 'Alamat KTP'">{{ identityAddress || '-' }}</DetailRow>
|
||||
</DetailSection>
|
||||
<DetailSection title="Kontak">
|
||||
<template v-if="patient.person.contacts && patient.person.contacts.length > 0">
|
||||
<template
|
||||
v-for="contactType in personContactTypeOptions"
|
||||
:key="contactType.code"
|
||||
>
|
||||
<DetailRow :label="contactType.label">
|
||||
{{ patient.person.contacts.find((item) => item.type_code === contactType.code)?.value || '-' }}
|
||||
</DetailRow>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<DetailRow label="Kontak">-</DetailRow>
|
||||
</template>
|
||||
</DetailSection>
|
||||
<DetailSection title="Orang Tua">
|
||||
<template v-if="patient.person.relatives && patient.person.relatives.filter((rel) => !rel.responsible).length > 0">
|
||||
<template
|
||||
v-for="(relative, index) in patient.person.relatives.filter((rel) => !rel.responsible)"
|
||||
:key="relative.id"
|
||||
>
|
||||
<div
|
||||
v-if="index > 0"
|
||||
class="mt-3 border-t border-gray-200 pt-3"
|
||||
></div>
|
||||
<DetailRow label="Nama">{{ relative.name || '-' }}</DetailRow>
|
||||
<DetailRow label="Hubungan">
|
||||
{{ relationshipOptions.find((item) => item.code === relative.relationship_code)?.label || '-' }}
|
||||
</DetailRow>
|
||||
<!-- <DetailRow label="Jenis Kelamin">
|
||||
<Fragment
|
||||
v-slot="{ section }"
|
||||
title="Dokumen"
|
||||
>
|
||||
<AccordionItem value="item-document">
|
||||
<AccordionTrigger>{{ section }}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm text-muted-foreground">Dokumen KTP</span>
|
||||
<Skeleton class="h-32 w-64 rounded-md" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm text-muted-foreground">Dokumen KK</span>
|
||||
<Skeleton class="h-32 w-64 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Fragment>
|
||||
|
||||
<Fragment
|
||||
v-slot="{ section }"
|
||||
title="Alamat"
|
||||
>
|
||||
<AccordionItem value="item-address">
|
||||
<AccordionTrigger>{{ section }}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<DetailRow :label="addressLocationTypeCode.domicile || 'Alamat Domisili'">
|
||||
{{ domicileAddress || '-' }}
|
||||
</DetailRow>
|
||||
<DetailRow :label="addressLocationTypeCode.identity || 'Alamat KTP'">
|
||||
{{ identityAddress || '-' }}
|
||||
</DetailRow>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Fragment>
|
||||
|
||||
<Fragment
|
||||
v-slot="{ section }"
|
||||
title="Kontak"
|
||||
>
|
||||
<AccordionItem value="item-contact">
|
||||
<AccordionTrigger>{{ section }}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<template v-if="patient.person.contacts && patient.person.contacts.length > 0">
|
||||
<template
|
||||
v-for="contactType in personContactTypeOptions"
|
||||
:key="contactType.code"
|
||||
>
|
||||
<DetailRow :label="contactType.label">
|
||||
{{ patient.person.contacts.find((item) => item.type_code === contactType.code)?.value || '-' }}
|
||||
</DetailRow>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<DetailRow label="Kontak">-</DetailRow>
|
||||
</template>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Fragment>
|
||||
|
||||
<Fragment
|
||||
v-slot="{ section }"
|
||||
title="Data Orang Tua"
|
||||
>
|
||||
<AccordionItem value="item-parents">
|
||||
<AccordionTrigger>{{ section }}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<template
|
||||
v-if="patient.person.relatives && patient.person.relatives.filter((rel) => !rel.responsible).length > 0"
|
||||
>
|
||||
<template
|
||||
v-for="(relative, index) in patient.person.relatives.filter((rel) => !rel.responsible)"
|
||||
:key="relative.id"
|
||||
>
|
||||
<div
|
||||
v-if="index > 0"
|
||||
class="mt-3 border-t border-gray-200 pt-3"
|
||||
></div>
|
||||
<DetailRow label="Nama">{{ relative.name || '-' }}</DetailRow>
|
||||
<DetailRow label="Hubungan">
|
||||
{{ relationshipOptions.find((item) => item.code === relative.relationship_code)?.label || '-' }}
|
||||
</DetailRow>
|
||||
<!-- <DetailRow label="Jenis Kelamin">
|
||||
{{ genderOptions.find((item) => item.code === relative.gender_code)?.label || '-' }}
|
||||
</DetailRow> -->
|
||||
<DetailRow label="Pendidikan">
|
||||
{{ educationOptions.find((item) => item.code === relative.education_code)?.label || '-' }}
|
||||
</DetailRow>
|
||||
<DetailRow label="Pekerjaan">
|
||||
{{
|
||||
occupationOptions.find((item) => item.code === relative.occupation_code)?.label ||
|
||||
relative.occupation_name ||
|
||||
'-'
|
||||
}}
|
||||
</DetailRow>
|
||||
<!-- <DetailRow label="Alamat">{{ relative.address || '-' }}</DetailRow> -->
|
||||
<!-- <DetailRow label="Nomor HP">{{ relative.phoneNumber || '-' }}</DetailRow> -->
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<DetailRow label="Orang Tua">-</DetailRow>
|
||||
</template>
|
||||
</DetailSection>
|
||||
<DetailSection title="Penanggung Jawab">
|
||||
<template v-if="patient.person.relatives && patient.person.relatives.filter((rel) => rel.responsible).length > 0">
|
||||
<template
|
||||
v-for="(relative, index) in patient.person.relatives.filter((rel) => rel.responsible)"
|
||||
:key="relative.id"
|
||||
>
|
||||
<div
|
||||
v-if="index > 0"
|
||||
class="mt-3 border-t border-gray-200 pt-3"
|
||||
></div>
|
||||
<DetailRow label="Nama">{{ relative.name || '-' }}</DetailRow>
|
||||
<DetailRow label="Hubungan">
|
||||
{{ relationshipOptions.find((item) => item.code === relative.relationship_code)?.label || '-' }}
|
||||
</DetailRow>
|
||||
<DetailRow label="Alamat">{{ relative.address || '-' }}</DetailRow>
|
||||
<DetailRow label="Nomor HP">{{ relative.phoneNumber || '-' }}</DetailRow>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<DetailRow label="Penanggung Jawab">-</DetailRow>
|
||||
</template>
|
||||
</DetailSection>
|
||||
<div class="border-t-1 my-2 flex justify-end border-t-slate-300 py-2">
|
||||
<PubMyUiNavFooterBaEd @click="onClick" />
|
||||
<DetailRow label="Pendidikan">
|
||||
{{ educationOptions.find((item) => item.code === relative.education_code)?.label || '-' }}
|
||||
</DetailRow>
|
||||
<!-- <DetailRow label="Pekerjaan">
|
||||
{{
|
||||
occupationOptions.find((item) => item.code === relative.occupation_code)?.label ||
|
||||
relative.occupation_name ||
|
||||
'-'
|
||||
}}
|
||||
</DetailRow> -->
|
||||
<!-- <DetailRow label="Alamat">{{ relative.address || '-' }}</DetailRow> -->
|
||||
<!-- <DetailRow label="Nomor HP">{{ relative.phoneNumber || '-' }}</DetailRow> -->
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<DetailRow label="Orang Tua">-</DetailRow>
|
||||
</template>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Fragment>
|
||||
|
||||
<Fragment
|
||||
v-slot="{ section }"
|
||||
title="Data Penanggung Jawab"
|
||||
>
|
||||
<AccordionItem value="item-relative">
|
||||
<AccordionTrigger>{{ section }}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<template
|
||||
v-if="patient.person.relatives && patient.person.relatives.filter((rel) => rel.responsible).length > 0"
|
||||
>
|
||||
<template
|
||||
v-for="(relative, index) in patient.person.relatives.filter((rel) => rel.responsible)"
|
||||
:key="relative.id"
|
||||
>
|
||||
<div
|
||||
v-if="index > 0"
|
||||
class="mt-3 border-t border-gray-200 pt-3"
|
||||
></div>
|
||||
<DetailRow label="Nama">{{ relative.name || '-' }}</DetailRow>
|
||||
<DetailRow label="Hubungan">
|
||||
{{ relationshipOptions.find((item) => item.code === relative.relationship_code)?.label || '-' }}
|
||||
</DetailRow>
|
||||
<DetailRow label="Alamat">{{ relative.address || '-' }}</DetailRow>
|
||||
<DetailRow label="Nomor HP">{{ relative.phoneNumber || '-' }}</DetailRow>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<DetailRow label="Penanggung Jawab">-</DetailRow>
|
||||
</template>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Fragment>
|
||||
</Accordion>
|
||||
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<PubMyUiNavFooterBaEd @click="onNavigate" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,40 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import Block from '~/components/pub/my-ui/form/block.vue'
|
||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||
|
||||
// schemas
|
||||
import {
|
||||
type PersonAddressRelativeFormData,
|
||||
PersonAddressRelativeSchema,
|
||||
} from '~/schemas/person-address-relative.schema'
|
||||
|
||||
// components
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
import { Label as RadioLabel } from '~/components/pub/ui/label'
|
||||
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
||||
import SelectDistrict from './_common/select-district.vue'
|
||||
import SelectPostal from './_common/select-postal.vue'
|
||||
import SelectProvince from './_common/select-province.vue'
|
||||
import SelectRegency from './_common/select-regency.vue'
|
||||
import SelectVillage from './_common/select-village.vue'
|
||||
import { Form } from '~/components/pub/ui/form'
|
||||
import { SelectDistrict, SelectPostal, SelectProvince, SelectRegency, SelectVillage } from './fields'
|
||||
import { InputBase } from '~/components/pub/my-ui/form'
|
||||
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
interface FormData extends PersonAddressRelativeFormData {}
|
||||
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
title: string
|
||||
isReadonly: boolean
|
||||
conf?: {
|
||||
withAddressName?: boolean
|
||||
}
|
||||
schema: any
|
||||
initialValues?: any
|
||||
errors?: FormErrors
|
||||
}>()
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const formSchema = toTypedSchema(props.schema)
|
||||
const formRef = ref()
|
||||
const formSchema = toTypedSchema(PersonAddressRelativeSchema)
|
||||
|
||||
const { values, resetForm, setFieldValue, setValues, validate, setFieldError } = useForm<FormData>({
|
||||
name: 'encounterActionReportForm',
|
||||
validationSchema: formSchema,
|
||||
initialValues: props.initialValues ? props.initialValues : { isSameAddress: '1', locationType_code: 'identity' },
|
||||
validateOnMount: false,
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
validate: () => formRef.value?.validate(),
|
||||
resetForm: () => formRef.value?.resetForm(),
|
||||
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
|
||||
values: computed(() => formRef.value?.values),
|
||||
validate,
|
||||
resetForm,
|
||||
setValues,
|
||||
values,
|
||||
})
|
||||
|
||||
// Watchers untuk cascading reset
|
||||
@@ -53,16 +59,22 @@ const fieldStates: Record<string, { dependsOn?: string; placeholder: string }> =
|
||||
|
||||
// Computed untuk konversi boolean ke string untuk radio group
|
||||
const isSameAddressString = computed(() => {
|
||||
const value = formRef.value?.values?.isSameAddress
|
||||
const value = values.isSameAddress
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? '1' : '0'
|
||||
}
|
||||
return value || '1'
|
||||
})
|
||||
|
||||
// Computed untuk cek apakah alamat sama (menangani boolean dan string)
|
||||
const isSameAddress = computed(() => {
|
||||
const value = values.isSameAddress as boolean | string | undefined
|
||||
return value === true || value === '1'
|
||||
})
|
||||
// #region Function Helper
|
||||
function getFieldState(field: string) {
|
||||
const state = fieldStates[field]
|
||||
const isSame = formRef.value?.values?.isSameAddress === true || formRef.value?.values?.isSameAddress === '1'
|
||||
const isSame = isSameAddress.value
|
||||
|
||||
// Jika alamat sama, semua field kecuali provinsi disabled
|
||||
if (['address', 'rt', 'rw'].includes(field) && isSame) {
|
||||
@@ -71,7 +83,7 @@ function getFieldState(field: string) {
|
||||
|
||||
// Untuk field yang tergantung pada field lain
|
||||
if (state?.dependsOn) {
|
||||
const dependencyValue = formRef.value?.values?.[state.dependsOn]
|
||||
const dependencyValue = values[state.dependsOn as keyof FormData]
|
||||
const isDisabledByDependency = !dependencyValue
|
||||
|
||||
// Jika isSame, semua field location disabled
|
||||
@@ -98,14 +110,14 @@ function getFieldState(field: string) {
|
||||
|
||||
// Watch province_code changes
|
||||
watch(
|
||||
() => formRef.value?.values?.province_code,
|
||||
() => values.province_code,
|
||||
(newValue, oldValue) => {
|
||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||
if (isResetting || newValue === oldValue) return
|
||||
|
||||
if (oldValue && newValue !== oldValue) {
|
||||
isResetting = true
|
||||
|
||||
formRef.value.setValues(
|
||||
setValues(
|
||||
{
|
||||
regency_code: undefined,
|
||||
district_code: undefined,
|
||||
@@ -124,14 +136,14 @@ watch(
|
||||
|
||||
// Watch regency_code changes
|
||||
watch(
|
||||
() => formRef.value?.values?.regency_code,
|
||||
() => values.regency_code,
|
||||
(newValue, oldValue) => {
|
||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||
if (isResetting || newValue === oldValue) return
|
||||
|
||||
if (oldValue && newValue !== oldValue) {
|
||||
isResetting = true
|
||||
|
||||
formRef.value.setValues(
|
||||
setValues(
|
||||
{
|
||||
district_code: undefined,
|
||||
village_code: undefined,
|
||||
@@ -149,14 +161,14 @@ watch(
|
||||
|
||||
// Watch district_code changes
|
||||
watch(
|
||||
() => formRef.value?.values?.district_code,
|
||||
() => values.district_code,
|
||||
(newValue, oldValue) => {
|
||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||
if (isResetting || newValue === oldValue) return
|
||||
|
||||
if (oldValue && newValue !== oldValue) {
|
||||
isResetting = true
|
||||
|
||||
formRef.value.setValues(
|
||||
setValues(
|
||||
{
|
||||
village_code: undefined,
|
||||
postalRegion_code: undefined,
|
||||
@@ -173,14 +185,14 @@ watch(
|
||||
|
||||
// Watch village_code changes
|
||||
watch(
|
||||
() => formRef.value?.values?.village_code,
|
||||
() => values.village_code,
|
||||
(newValue, oldValue) => {
|
||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||
if (isResetting || newValue === oldValue) return
|
||||
|
||||
if (oldValue && newValue !== oldValue) {
|
||||
isResetting = true
|
||||
|
||||
formRef.value.setValues(
|
||||
setValues(
|
||||
{
|
||||
postalRegion_code: undefined,
|
||||
},
|
||||
@@ -196,19 +208,19 @@ watch(
|
||||
|
||||
// Watch isSameAddress changes untuk trigger validasi
|
||||
watch(
|
||||
() => formRef.value?.values?.isSameAddress,
|
||||
() => values.isSameAddress,
|
||||
(newValue, oldValue) => {
|
||||
if (!formRef.value || newValue === oldValue) return
|
||||
if (isResetting || newValue === oldValue) return
|
||||
|
||||
// Konversi ke boolean untuk perbandingan yang konsisten
|
||||
const newBool = newValue === true || newValue === '1'
|
||||
const oldBool = oldValue === true || oldValue === '1'
|
||||
const newBool = newValue === true || newValue === ('1' as unknown)
|
||||
const oldBool = oldValue === true || oldValue === ('1' as unknown)
|
||||
|
||||
// Ketika berubah dari true ke false, clear empty strings dan trigger validasi
|
||||
if (oldBool && !newBool) {
|
||||
nextTick(() => {
|
||||
// Set empty strings ke undefined untuk trigger required validation
|
||||
const currentValues = formRef.value.values
|
||||
const currentValues = values
|
||||
const updatedValues = { ...currentValues }
|
||||
|
||||
// Convert empty strings to undefined untuk field yang sekarang required
|
||||
@@ -220,11 +232,11 @@ watch(
|
||||
if (updatedValues.address === '') updatedValues.address = undefined
|
||||
|
||||
// Update values dan trigger validasi
|
||||
formRef.value.setValues(updatedValues, false)
|
||||
setValues(updatedValues, false)
|
||||
|
||||
// Trigger validasi untuk menampilkan error
|
||||
setTimeout(() => {
|
||||
formRef.value?.validate()
|
||||
validate()
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
@@ -233,14 +245,14 @@ watch(
|
||||
if (!oldBool && newBool) {
|
||||
nextTick(() => {
|
||||
// Clear error messages untuk field yang tidak lagi required
|
||||
formRef.value?.setFieldError('province_code', undefined)
|
||||
formRef.value?.setFieldError('regency_code', undefined)
|
||||
formRef.value?.setFieldError('district_code', undefined)
|
||||
formRef.value?.setFieldError('village_code', undefined)
|
||||
formRef.value?.setFieldError('postalRegion_code', undefined)
|
||||
formRef.value?.setFieldError('address', undefined)
|
||||
formRef.value?.setFieldError('rt', undefined)
|
||||
formRef.value?.setFieldError('rw', undefined)
|
||||
setFieldError('province_code', undefined)
|
||||
setFieldError('regency_code', undefined)
|
||||
setFieldError('district_code', undefined)
|
||||
setFieldError('village_code', undefined)
|
||||
setFieldError('postalRegion_code', undefined)
|
||||
setFieldError('address', undefined)
|
||||
setFieldError('rt', undefined)
|
||||
setFieldError('rw', undefined)
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -249,20 +261,11 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
ref="formRef"
|
||||
v-slot="{ values }"
|
||||
as=""
|
||||
keep-values
|
||||
:validation-schema="formSchema"
|
||||
:validate-on-mount="false"
|
||||
validation-mode="onSubmit"
|
||||
:initial-values="initialValues ?? { isSameAddress: '1', locationType_code: 'identity' }"
|
||||
>
|
||||
<form @submit-prevent="">
|
||||
<div>
|
||||
<p
|
||||
v-if="props.title"
|
||||
class="text-sm 2xl:text-base mb-2 2xl:mb-3 font-semibold"
|
||||
class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base"
|
||||
>
|
||||
{{ props.title }}
|
||||
</p>
|
||||
@@ -278,7 +281,10 @@ watch(
|
||||
value="identity"
|
||||
/>
|
||||
</FormField>
|
||||
<DE.Block :col-count="4" :cell-flex="false">
|
||||
<DE.Block
|
||||
:col-count="4"
|
||||
:cell-flex="false"
|
||||
>
|
||||
<DE.Cell :col-span="4">
|
||||
<DE.Label
|
||||
size="fit"
|
||||
@@ -287,10 +293,7 @@ watch(
|
||||
>
|
||||
Apakah alamat KTP sama dengan alamat sekarang?
|
||||
</DE.Label>
|
||||
<DE.Field
|
||||
id="isSameAddress"
|
||||
:errors="errors"
|
||||
>
|
||||
<DE.Field id="isSameAddress">
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="isSameAddress"
|
||||
@@ -311,6 +314,7 @@ watch(
|
||||
class="flex min-w-fit items-center space-x-2"
|
||||
>
|
||||
<RadioGroupItem
|
||||
:disabled="isReadonly"
|
||||
:id="`isSameAddress-${index}`"
|
||||
:value="option.value"
|
||||
class="relative h-4 w-4 rounded-full border-2 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"
|
||||
@@ -332,52 +336,49 @@ watch(
|
||||
<SelectProvince
|
||||
field-name="province_code"
|
||||
placeholder="Pilih"
|
||||
:is-disabled="values.isSameAddress === true || values.isSameAddress === '1'"
|
||||
:is-required="values.isSameAddress !== true && values.isSameAddress !== '1'"
|
||||
:is-disabled="isReadonly || isSameAddress"
|
||||
:is-required="!isSameAddress"
|
||||
/>
|
||||
<SelectRegency
|
||||
field-name="regency_code"
|
||||
:province-code="values.province_code"
|
||||
:is-disabled="getFieldState('regency_code').disabled"
|
||||
:is-required="values.isSameAddress !== true && values.isSameAddress !== '1'"
|
||||
:is-disabled="isReadonly || getFieldState('regency_code').disabled"
|
||||
:is-required="!isSameAddress"
|
||||
/>
|
||||
<SelectDistrict
|
||||
field-name="district_code"
|
||||
:regency-code="values.regency_code"
|
||||
:is-disabled="getFieldState('district_code').disabled"
|
||||
:is-required="values.isSameAddress !== true && values.isSameAddress !== '1'"
|
||||
:is-disabled="isReadonly || getFieldState('district_code').disabled"
|
||||
:is-required="!isSameAddress"
|
||||
/>
|
||||
<SelectVillage
|
||||
field-name="village_code"
|
||||
:district-code="values.district_code"
|
||||
:is-disabled="getFieldState('village_code').disabled"
|
||||
:is-required="values.isSameAddress !== true && values.isSameAddress !== '1'"
|
||||
:is-disabled="isReadonly || getFieldState('village_code').disabled"
|
||||
:is-required="!isSameAddress"
|
||||
/>
|
||||
<InputBase
|
||||
field-name="address"
|
||||
label="Alamat"
|
||||
:placeholder="getFieldState('address').placeholder"
|
||||
:is-disabled="getFieldState('address').disabled"
|
||||
:errors="errors"
|
||||
:is-required="values.isSameAddress !== true && values.isSameAddress !== '1'"
|
||||
:is-disabled="isReadonly || getFieldState('address').disabled"
|
||||
:is-required="!isSameAddress"
|
||||
:col-span="2"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<InputBase
|
||||
field-name="rt"
|
||||
label="RT"
|
||||
:errors="errors"
|
||||
numeric-only
|
||||
:max-length="2"
|
||||
:placeholder="getFieldState('rt').placeholder"
|
||||
:is-disabled="getFieldState('rt').disabled"
|
||||
:is-disabled="isReadonly || getFieldState('rt').disabled"
|
||||
/>
|
||||
<InputBase
|
||||
field-name="rw"
|
||||
label="RW"
|
||||
:placeholder="getFieldState('rw').placeholder"
|
||||
:is-disabled="getFieldState('rw').disabled"
|
||||
:errors="errors"
|
||||
:is-disabled="isReadonly || getFieldState('rw').disabled"
|
||||
:max-length="2"
|
||||
numeric-only
|
||||
/>
|
||||
@@ -386,9 +387,8 @@ watch(
|
||||
field-name="postalRegion_code"
|
||||
:village-code="values.village_code"
|
||||
:placeholder="getFieldState('postalRegion_code').placeholder"
|
||||
:is-disabled="getFieldState('postalRegion_code').disabled || !values.village_code"
|
||||
:is-disabled="isReadonly || getFieldState('postalRegion_code').disabled || !values.village_code"
|
||||
/>
|
||||
</DE.Block>
|
||||
|
||||
</Form>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -1,37 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||
import SelectDistrict from './_common/select-district.vue'
|
||||
import SelectPostal from './_common/select-postal.vue'
|
||||
import SelectProvince from './_common/select-province.vue'
|
||||
import SelectRegency from './_common/select-regency.vue'
|
||||
import SelectVillage from './_common/select-village.vue'
|
||||
|
||||
// types
|
||||
import { type PersonAddressFormData, PersonAddressSchema } from '~/schemas/person-address.schema'
|
||||
|
||||
// components
|
||||
import { Form } from '~/components/pub/ui/form'
|
||||
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||
import { SelectDistrict, SelectPostal, SelectProvince, SelectRegency, SelectVillage } from './fields'
|
||||
|
||||
const props = defineProps<{
|
||||
interface FormData extends PersonAddressFormData {}
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
isReadonly: boolean
|
||||
conf?: {
|
||||
withAddressName?: boolean
|
||||
}
|
||||
schema: any
|
||||
initialValues?: any
|
||||
errors?: FormErrors
|
||||
}>()
|
||||
}
|
||||
|
||||
const formSchema = toTypedSchema(props.schema)
|
||||
const formRef = ref()
|
||||
const props = defineProps<Props>()
|
||||
const formSchema = toTypedSchema(PersonAddressSchema)
|
||||
|
||||
const { values, resetForm, setValues, validate } = useForm<FormData>({
|
||||
name: 'patientForm',
|
||||
validationSchema: formSchema,
|
||||
initialValues: {
|
||||
locationType_code: 'domicile',
|
||||
...props.initialValues,
|
||||
},
|
||||
validateOnMount: false,
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
validate: () => formRef.value?.validate(),
|
||||
resetForm: () => formRef.value?.resetForm(),
|
||||
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
|
||||
values: computed(() => formRef.value?.values),
|
||||
validate,
|
||||
resetForm,
|
||||
setValues,
|
||||
values,
|
||||
})
|
||||
|
||||
// Watchers untuk cascading reset
|
||||
@@ -40,22 +48,22 @@ let isResetting = false
|
||||
// #region Watch provinceCode changes
|
||||
|
||||
watch(
|
||||
() => formRef.value?.values?.provinceCode,
|
||||
() => values.province_code,
|
||||
(newValue, oldValue) => {
|
||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||
if (isResetting || !values || newValue === oldValue) return
|
||||
|
||||
if (oldValue && newValue !== oldValue) {
|
||||
isResetting = true
|
||||
|
||||
// Delay reset untuk memberikan waktu composable menyelesaikan request
|
||||
setTimeout(() => {
|
||||
if (formRef.value) {
|
||||
formRef.value.setValues(
|
||||
if (values) {
|
||||
setValues(
|
||||
{
|
||||
regencyId: undefined,
|
||||
districtId: undefined,
|
||||
villageId: undefined,
|
||||
zipCode: undefined,
|
||||
regency_code: undefined,
|
||||
district_code: undefined,
|
||||
village_code: undefined,
|
||||
postalRegion_code: undefined,
|
||||
},
|
||||
false,
|
||||
)
|
||||
@@ -71,21 +79,21 @@ watch(
|
||||
|
||||
// Watch regencyId changes
|
||||
watch(
|
||||
() => formRef.value?.values?.regencyId,
|
||||
() => values.regency_code,
|
||||
(newValue, oldValue) => {
|
||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||
if (isResetting || !values || newValue === oldValue) return
|
||||
|
||||
if (oldValue && newValue !== oldValue) {
|
||||
isResetting = true
|
||||
|
||||
// Delay reset untuk memberikan waktu composable menyelesaikan request
|
||||
setTimeout(() => {
|
||||
if (formRef.value) {
|
||||
formRef.value.setValues(
|
||||
if (values) {
|
||||
setValues(
|
||||
{
|
||||
districtId: undefined,
|
||||
villageId: undefined,
|
||||
zipCode: undefined,
|
||||
district_code: undefined,
|
||||
village_code: undefined,
|
||||
postalRegion_code: undefined,
|
||||
},
|
||||
false,
|
||||
)
|
||||
@@ -101,20 +109,20 @@ watch(
|
||||
|
||||
// Watch districtId changes
|
||||
watch(
|
||||
() => formRef.value?.values?.districtId,
|
||||
() => values.district_code,
|
||||
(newValue, oldValue) => {
|
||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||
if (isResetting || !values || newValue === oldValue) return
|
||||
|
||||
if (oldValue && newValue !== oldValue) {
|
||||
isResetting = true
|
||||
|
||||
// Delay reset untuk memberikan waktu composable menyelesaikan request
|
||||
setTimeout(() => {
|
||||
if (formRef.value) {
|
||||
formRef.value.setValues(
|
||||
if (values) {
|
||||
setValues(
|
||||
{
|
||||
villageId: undefined,
|
||||
zipCode: undefined,
|
||||
village_code: undefined,
|
||||
postalRegion_code: undefined,
|
||||
},
|
||||
false,
|
||||
)
|
||||
@@ -130,16 +138,16 @@ watch(
|
||||
|
||||
// Watch villageId changes
|
||||
watch(
|
||||
() => formRef.value?.values?.villageId,
|
||||
() => values.village_code,
|
||||
(newValue, oldValue) => {
|
||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||
if (isResetting || !values || newValue === oldValue) return
|
||||
|
||||
if (oldValue && newValue !== oldValue) {
|
||||
isResetting = true
|
||||
|
||||
formRef.value.setValues(
|
||||
setValues(
|
||||
{
|
||||
zipCode: undefined,
|
||||
postalRegion_code: undefined,
|
||||
},
|
||||
false,
|
||||
)
|
||||
@@ -154,22 +162,11 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
ref="formRef"
|
||||
v-slot="{ values }"
|
||||
as=""
|
||||
keep-values
|
||||
:validation-schema="formSchema"
|
||||
:validate-on-mount="false"
|
||||
validation-mode="onSubmit"
|
||||
:initial-values="
|
||||
initialValues ? { locationType_code: 'domicile', ...initialValues } : { locationType_code: 'domicile' }
|
||||
"
|
||||
>
|
||||
<form>
|
||||
<div>
|
||||
<p
|
||||
v-if="props.title"
|
||||
class="text-sm 2xl:text-base mb-2 2xl:mb-3 font-semibold"
|
||||
class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base"
|
||||
>
|
||||
{{ props.title }}
|
||||
</p>
|
||||
@@ -185,33 +182,40 @@ watch(
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<DE.Block :col-count="4" :cell-flex="false">
|
||||
<DE.Block
|
||||
:col-count="4"
|
||||
:cell-flex="false"
|
||||
>
|
||||
<SelectProvince
|
||||
field-name="province_code"
|
||||
placeholder="Pilih"
|
||||
:is-disabled="isReadonly"
|
||||
is-required
|
||||
/>
|
||||
<SelectRegency
|
||||
field-name="regency_code"
|
||||
:province-code="values.province_code"
|
||||
:is-disabled="isReadonly"
|
||||
is-required
|
||||
/>
|
||||
<SelectDistrict
|
||||
field-name="district_code"
|
||||
:regency-code="values.regency_code"
|
||||
:is-disabled="isReadonly"
|
||||
is-required
|
||||
/>
|
||||
<SelectVillage
|
||||
field-name="village_code"
|
||||
:district-code="values.district_code"
|
||||
:is-disabled="isReadonly"
|
||||
is-required
|
||||
/>
|
||||
<InputBase
|
||||
field-name="address"
|
||||
label="Alamat"
|
||||
placeholder="Masukkan alamat"
|
||||
:errors="errors"
|
||||
is-required
|
||||
:is-disabled="isReadonly"
|
||||
:col-span="2"
|
||||
/>
|
||||
<DE.Cell class="flex-row gap-2">
|
||||
@@ -220,26 +224,26 @@ watch(
|
||||
field-name="rt"
|
||||
label="RT"
|
||||
placeholder="01"
|
||||
:errors="errors"
|
||||
numeric-only
|
||||
:max-length="2"
|
||||
/>
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<InputBase
|
||||
field-name="rw"
|
||||
label="RW"
|
||||
placeholder="02"
|
||||
:errors="errors"
|
||||
:max-length="2"
|
||||
numeric-only
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
</div>
|
||||
</DE.Cell>
|
||||
<SelectPostal
|
||||
field-name="postalRegion_code"
|
||||
placeholder="Pilih kelurahan dahulu"
|
||||
:village-code="values.village_code"
|
||||
:is-disabled="!values.village_code"
|
||||
/>
|
||||
field-name="postalRegion_code"
|
||||
placeholder="Pilih kelurahan dahulu"
|
||||
:village-code="values.village_code"
|
||||
:is-disabled="!values.village_code"
|
||||
/>
|
||||
</DE.Block>
|
||||
</Form>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export { default as RadioResidence } from './radio-residence.vue'
|
||||
export { default as SelectDistrict } from './select-district.vue'
|
||||
export { default as SelectPostal } from './select-postal.vue'
|
||||
export { default as SelectProvince } from './select-province.vue'
|
||||
export { default as SelectRegency } from './select-regency.vue'
|
||||
export { default as SelectVillage } from './select-village.vue'
|
||||
@@ -1,41 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import { useForm, FieldArray } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { FieldArray } from 'vee-validate'
|
||||
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||
import SelectContactType from './_common/select-contact-type.vue'
|
||||
import { Form } from '~/components/pub/ui/form'
|
||||
|
||||
const props = defineProps<{
|
||||
// type
|
||||
import { type PersonContactFormData, PersonContactListSchema } from '~/schemas/person-contact.schema'
|
||||
|
||||
// components
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
import { SelectContactType } from './fields'
|
||||
import { ButtonAction, InputBase } from '~/components/pub/my-ui/form'
|
||||
|
||||
interface FormData extends PersonContactFormData {}
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
schema: any
|
||||
contactLimit: number
|
||||
isReadonly: boolean
|
||||
contactLimit?: number
|
||||
initialValues?: any
|
||||
errors?: FormErrors
|
||||
}>()
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const { contactLimit = 5 } = props
|
||||
const formSchema = toTypedSchema(props.schema)
|
||||
const formRef = ref()
|
||||
|
||||
const formSchema = toTypedSchema(PersonContactListSchema)
|
||||
const { values, resetForm, setValues, validate } = useForm<FormData>({
|
||||
name: 'personContactForm',
|
||||
validationSchema: formSchema,
|
||||
initialValues: props.initialValues ? props.initialValues : {},
|
||||
validateOnMount: false,
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
validate: () => formRef.value?.validate(),
|
||||
resetForm: () => formRef.value?.resetForm(),
|
||||
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
|
||||
values: computed(() => formRef.value?.values),
|
||||
validate,
|
||||
resetForm,
|
||||
setValues,
|
||||
values,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
ref="formRef"
|
||||
as=""
|
||||
keep-values
|
||||
:validation-schema="formSchema"
|
||||
:validate-on-mount="false"
|
||||
validation-mode="onSubmit"
|
||||
:initial-values="initialValues || { contacts: [{ contactType: '', contactNumber: '' }] }"
|
||||
>
|
||||
<form @submit.prevent>
|
||||
<div>
|
||||
<p
|
||||
v-if="props.title"
|
||||
@@ -44,68 +48,59 @@ defineExpose({
|
||||
{{ props.title || 'Kontak Pasien' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<FieldArray
|
||||
v-slot="{ fields, push, remove }"
|
||||
name="contacts"
|
||||
>
|
||||
<div
|
||||
<div class="mb-5 space-y-4">
|
||||
<FieldArray
|
||||
v-slot="{ fields, push, remove }"
|
||||
name="contacts"
|
||||
>
|
||||
<template v-if="fields.length === 0">
|
||||
{{ push({ relation: '', name: '', address: '', phone: '' }) }}
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<DE.Block
|
||||
v-for="(field, idx) in fields"
|
||||
:key="field.key"
|
||||
class="flex-row gap-2 md:flex"
|
||||
:col-count="5"
|
||||
:cell-flex="false"
|
||||
>
|
||||
<div class="min-w-0 flex-[2]">
|
||||
<SelectContactType
|
||||
:field-name="`contacts[${idx}].contactType`"
|
||||
:label="`Kontak ${idx + 1}`"
|
||||
:is-required="idx === 0"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-[1.5]">
|
||||
<InputBase
|
||||
:field-name="`contacts[${idx}].contactNumber`"
|
||||
placeholder="081234567890"
|
||||
label="No"
|
||||
numeric-only
|
||||
:max-length="15"
|
||||
:is-required="idx === 0"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 self-start md:pt-8">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="text-red-600 hover:border-red-400 hover:bg-red-50 hover:text-red-700 dark:border-red-400 dark:text-red-400 dark:hover:border-red-300 dark:hover:bg-red-900/20"
|
||||
:class="{ invisible: idx === 0 }"
|
||||
@click="remove(idx)"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-trash-2"
|
||||
class="h-5 w-5"
|
||||
<SelectContactType
|
||||
:label="idx === 0 ? 'Jenis Kontak' : undefined"
|
||||
:field-name="`contacts[${idx}].contactType`"
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<InputBase
|
||||
:label="idx === 0 ? 'Nomor Kontak' : undefined"
|
||||
:field-name="`contacts[${idx}].contactNumber`"
|
||||
placeholder="081234567890"
|
||||
:max-length="15"
|
||||
numeric-only
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<DE.Cell class="flex items-start justify-start">
|
||||
<DE.Field :class="idx === 0 ? 'mt-[30px]' : 'mt-0'">
|
||||
<ButtonAction
|
||||
v-if="idx !== 0"
|
||||
:disabled="isReadonly"
|
||||
preset="delete"
|
||||
:title="`Hapus Kontak ${idx + 1}`"
|
||||
icon-only
|
||||
@click="remove(idx)"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
</DE.Block>
|
||||
</div>
|
||||
|
||||
<div class="self-center pt-3">
|
||||
<Button
|
||||
:disabled="fields.length >= contactLimit"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="push({ contactType: '', contactNumber: '' })"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-plus"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
Tambah Kontak
|
||||
</Button>
|
||||
</div>
|
||||
</FieldArray>
|
||||
</div>
|
||||
<ButtonAction
|
||||
preset="add"
|
||||
label="Tambah Kontak"
|
||||
title="Tambah Kontak ke Daftar Kontak"
|
||||
:disabled="isReadonly || fields.length >= contactLimit"
|
||||
:full-width-mobile="true"
|
||||
class="mt-4"
|
||||
@click="push({ contactType: '', contactNumber: '' })"
|
||||
/>
|
||||
</FieldArray>
|
||||
</div>
|
||||
</Form>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SelectContactType } from './select-contact-type.vue'
|
||||
+21
-18
@@ -1,14 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
fieldName: string
|
||||
label: string
|
||||
label?: string
|
||||
placeholder?: string
|
||||
errors?: FormErrors
|
||||
class?: string
|
||||
@@ -17,11 +16,12 @@ const props = defineProps<{
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
colSpan?: number
|
||||
}>()
|
||||
|
||||
const { fieldName = 'phoneNumber', errors, class: containerClass, selectClass, fieldGroupClass, labelClass } = props
|
||||
|
||||
const contactOptions = [
|
||||
const opts = [
|
||||
{ label: 'Nomor HP', value: 'phoneNumber' },
|
||||
{ label: 'Nomor Telepon Kantor', value: 'officePhoneNumber' },
|
||||
{ label: 'Nomor Telepon Rumah', value: 'homePhoneNumber' },
|
||||
@@ -29,17 +29,20 @@ const contactOptions = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||
<Label
|
||||
<DE.Cell
|
||||
:col-span="colSpan"
|
||||
:class="cn('select-field-group', fieldGroupClass, containerClass)"
|
||||
>
|
||||
<DE.Label
|
||||
v-if="label"
|
||||
:label-for="fieldName"
|
||||
:class="cn('select-field-label', labelClass)"
|
||||
:is-required="isRequired && !isDisabled"
|
||||
:is-required="isRequired"
|
||||
>
|
||||
{{ label }}
|
||||
</Label>
|
||||
<Field
|
||||
</DE.Label>
|
||||
<DE.Field
|
||||
:id="fieldName"
|
||||
:errors="errors"
|
||||
:class="cn('select-field-wrapper')"
|
||||
>
|
||||
<FormField
|
||||
@@ -49,15 +52,15 @@ const contactOptions = [
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select
|
||||
:disabled="isDisabled"
|
||||
:id="fieldName"
|
||||
v-bind="componentField"
|
||||
:is-disabled="isDisabled"
|
||||
:items="contactOptions"
|
||||
:placeholder="placeholder || 'Pilih jenis kontak'"
|
||||
:items="opts"
|
||||
:placeholder="placeholder"
|
||||
:preserve-order="true"
|
||||
:class="
|
||||
cn(
|
||||
'min-w-[190px] text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
|
||||
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
|
||||
selectClass,
|
||||
)
|
||||
"
|
||||
@@ -66,6 +69,6 @@ const contactOptions = [
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
</template>
|
||||
@@ -1,46 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import { useForm, FieldArray } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { FieldArray } from 'vee-validate'
|
||||
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||
import SelectContactRelation from './_common/select-relations.vue'
|
||||
import { Form } from '~/components/pub/ui/form'
|
||||
|
||||
const props = defineProps<{
|
||||
// schemas
|
||||
import { type PersonRelativeFormData, ResponsiblePersonRelativeSchema } from '~/schemas/person-relative.schema'
|
||||
|
||||
// components
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
import { SelectRelations } from './fields'
|
||||
import { ButtonAction, InputBase } from '~/components/pub/my-ui/form'
|
||||
|
||||
interface FormData extends PersonRelativeFormData {}
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
schema: any
|
||||
isReadonly: boolean
|
||||
initialValues?: any
|
||||
errors?: FormErrors
|
||||
}>()
|
||||
contactLimit?: number
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const formSchema = toTypedSchema(props.schema)
|
||||
const formRef = ref()
|
||||
const formSchema = toTypedSchema(ResponsiblePersonRelativeSchema)
|
||||
|
||||
const { values, resetForm, setValues, validate } = useForm<FormData>({
|
||||
name: 'personRelativeForm',
|
||||
validationSchema: formSchema,
|
||||
initialValues: props.initialValues ? props.initialValues : {},
|
||||
validateOnMount: false,
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
validate: () => formRef.value?.validate(),
|
||||
resetForm: () => formRef.value?.resetForm(),
|
||||
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
|
||||
values: computed(() => formRef.value?.values),
|
||||
validate,
|
||||
resetForm,
|
||||
setValues,
|
||||
values: values,
|
||||
})
|
||||
|
||||
const { title = 'Kontak Pasien', contactLimit = 5 } = props
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
ref="formRef"
|
||||
as=""
|
||||
keep-values
|
||||
:validation-schema="formSchema"
|
||||
:validate-on-mount="false"
|
||||
validation-mode="onSubmit"
|
||||
:initial-values="initialValues || { contacts: [{ relation: '', name: '', address: '', phone: '' }] }"
|
||||
>
|
||||
<form @submit.prevent>
|
||||
<div>
|
||||
<p
|
||||
v-if="props.title"
|
||||
class="text-md mb-2 mt-1 font-semibold"
|
||||
>
|
||||
{{ props.title || 'Kontak Pasien' }}
|
||||
<p class="text-md mb-2 mt-1 font-semibold">
|
||||
{{ title }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-5 space-y-4">
|
||||
@@ -48,108 +51,67 @@ defineExpose({
|
||||
v-slot="{ fields, push, remove }"
|
||||
name="contacts"
|
||||
>
|
||||
<template v-if="fields.length === 0">
|
||||
{{ push({ relation: '', name: '', address: '', phone: '' }) }}
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
<DE.Block
|
||||
v-for="(field, idx) in fields"
|
||||
:key="field.key"
|
||||
class="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700 dark:bg-gray-800/50"
|
||||
:col-count="5"
|
||||
:cell-flex="false"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300">Penanggung Jawab {{ idx + 1 }}</h4>
|
||||
<Button
|
||||
v-if="idx !== 0"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="text-red-600 hover:border-red-400 hover:bg-red-50 hover:text-red-700 dark:border-red-400 dark:text-red-400 dark:hover:border-red-300 dark:hover:bg-red-900/20"
|
||||
@click="remove(idx)"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-trash-2"
|
||||
class="h-4 w-4"
|
||||
<SelectRelations
|
||||
:label="idx === 0 ? 'Hubungan dengan Pasien' : undefined"
|
||||
:field-name="`contacts[${idx}].relation`"
|
||||
placeholder="Pilih"
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<InputBase
|
||||
:label="idx === 0 ? 'Nama' : undefined"
|
||||
:field-name="`contacts[${idx}].name`"
|
||||
placeholder="Masukkan Nama"
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<InputBase
|
||||
:label="idx === 0 ? 'Alamat' : undefined"
|
||||
:field-name="`contacts[${idx}].address`"
|
||||
placeholder="Masukkan Alamat"
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<InputBase
|
||||
:label="idx === 0 ? 'Nomor HP' : undefined"
|
||||
:field-name="`contacts[${idx}].phone`"
|
||||
placeholder="081234567890"
|
||||
:max-length="15"
|
||||
numeric-only
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
<DE.Cell class="flex items-start justify-start">
|
||||
<DE.Field :class="idx === 0 ? 'mt-[30px]' : 'mt-0'">
|
||||
<ButtonAction
|
||||
v-if="idx !== 0"
|
||||
:disabled="isReadonly"
|
||||
preset="delete"
|
||||
:title="`Hapus Kontak ${idx + 1}`"
|
||||
icon-only
|
||||
@click="remove(idx)"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
is-required
|
||||
>
|
||||
Hubungan dengan Pasien
|
||||
</Label>
|
||||
<SelectContactRelation
|
||||
:field-name="`contacts[${idx}].relation`"
|
||||
placeholder="Pilih"
|
||||
:errors="errors"
|
||||
field-group-class="mb-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
is-required
|
||||
>
|
||||
Nama
|
||||
</Label>
|
||||
<InputBase
|
||||
label=""
|
||||
:field-name="`contacts[${idx}].name`"
|
||||
placeholder="Masukkan Nama"
|
||||
:errors="errors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
is-required
|
||||
>
|
||||
Alamat
|
||||
</Label>
|
||||
<InputBase
|
||||
:field-name="`contacts[${idx}].address`"
|
||||
label=""
|
||||
placeholder="Masukkan Alamat"
|
||||
:errors="errors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
is-required
|
||||
>
|
||||
Nomor HP
|
||||
</Label>
|
||||
<InputBase
|
||||
:field-name="`contacts[${idx}].phone`"
|
||||
label=""
|
||||
placeholder="081234567890"
|
||||
:max-length="15"
|
||||
numeric-only
|
||||
:errors="errors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
</DE.Block>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="w-full rounded-md border border-primary bg-white px-4 py-2 text-primary hover:border-primary hover:bg-primary hover:text-white sm:w-auto sm:text-sm"
|
||||
<ButtonAction
|
||||
preset="add"
|
||||
label="Tambah Penanggung Jawab"
|
||||
title="Tambah Penanggung Jawab"
|
||||
:disabled="fields.length >= contactLimit || isReadonly"
|
||||
:full-width-mobile="true"
|
||||
class="mt-4"
|
||||
@click="push({ relation: '', name: '', address: '', phone: '' })"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-plus"
|
||||
class="mr-2 h-4 w-4 align-middle transition-colors"
|
||||
/>
|
||||
Tambah Penanggung Jawab
|
||||
</Button>
|
||||
/>
|
||||
</FieldArray>
|
||||
</div>
|
||||
</Form>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SelectRelations } from './select-relations.vue'
|
||||
+26
-15
@@ -1,36 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||
import { cn } from '~/lib/utils'
|
||||
import { relationshipCodes } from '~/lib/constants'
|
||||
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
fieldName: string
|
||||
label?: string
|
||||
placeholder?: string
|
||||
errors?: FormErrors
|
||||
class?: string
|
||||
selectClass?: string
|
||||
fieldGroupClass?: string
|
||||
labelClass?: string
|
||||
isDisabled?: boolean
|
||||
isRequired?: boolean
|
||||
colSpan?: number
|
||||
}>()
|
||||
|
||||
const { fieldName = 'phoneNumber', errors, class: containerClass, selectClass, fieldGroupClass } = props
|
||||
const { class: containerClass, selectClass, fieldGroupClass } = props
|
||||
|
||||
const emergencyContactOptions = Object.entries(relationshipCodes).map(([value, label]) => ({
|
||||
const opts = Object.entries(relationshipCodes).map(([value, label]) => ({
|
||||
label,
|
||||
value,
|
||||
...(value === 'other' && { priority: -1 })
|
||||
...(value === 'other' && { priority: -1 }),
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||
<Field
|
||||
<DE.Cell
|
||||
:col-span="colSpan"
|
||||
:class="cn('select-field-group', fieldGroupClass, containerClass)"
|
||||
>
|
||||
<DE.Label
|
||||
v-if="label"
|
||||
:label-for="fieldName"
|
||||
:class="cn('select-field-label', labelClass)"
|
||||
:is-required="isRequired"
|
||||
>
|
||||
{{ label }}
|
||||
</DE.Label>
|
||||
<DE.Field
|
||||
:id="fieldName"
|
||||
:errors="errors"
|
||||
:class="cn('select-field-wrapper')"
|
||||
>
|
||||
<FormField
|
||||
@@ -40,10 +51,10 @@ const emergencyContactOptions = Object.entries(relationshipCodes).map(([value, l
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select
|
||||
:disabled="isDisabled"
|
||||
:id="fieldName"
|
||||
v-bind="componentField"
|
||||
:is-disabled="isDisabled"
|
||||
:items="emergencyContactOptions"
|
||||
:items="opts"
|
||||
:placeholder="placeholder"
|
||||
:preserve-order="true"
|
||||
:class="
|
||||
@@ -57,6 +68,6 @@ const emergencyContactOptions = Object.entries(relationshipCodes).map(([value, l
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
</template>
|
||||
@@ -1,167 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import type { PersonFamilyFormData as FamilyData } from '~/schemas/person-family.schema'
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import { type PersonRelativeFormData, ResponsiblePersonRelativeSchema } from '~/schemas/person-relative.schema'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { FieldArray } from 'vee-validate'
|
||||
import SelectEducation from '~/components/app/patient/_common/select-education.vue'
|
||||
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||
import RadioParentsInput from './_common/radio-parents-input.vue'
|
||||
import { Form } from '~/components/pub/ui/form'
|
||||
import { useForm, FieldArray } from 'vee-validate'
|
||||
|
||||
const props = defineProps<{
|
||||
// component
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
import { InputBase } from '~/components/pub/my-ui/form'
|
||||
import { RadioParentsInput } from './fields'
|
||||
import { SelectJob, SelectEducation } from '~/components/app/patient/fields'
|
||||
import { SelectRelations } from '~/components/app/person-relative/fields'
|
||||
|
||||
interface FormData extends PersonRelativeFormData {}
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
schema: any
|
||||
initialValues?: any
|
||||
errors?: FormErrors
|
||||
}>()
|
||||
isReadonly: boolean
|
||||
initialValues?: FormData
|
||||
}
|
||||
|
||||
const formSchema = toTypedSchema(props.schema)
|
||||
const formRef = ref()
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Watcher untuk mengatur families ketika shareFamilyData berubah
|
||||
watch(
|
||||
() => formRef.value?.values?.shareFamilyData,
|
||||
(newValue) => {
|
||||
if (newValue === '1' && formRef.value) {
|
||||
// Ketika memilih "Ya", pastikan ada data families untuk mother dan father
|
||||
const currentFamilies = formRef.value.values?.families || []
|
||||
if (currentFamilies.length === 0) {
|
||||
formRef.value.setFieldValue('families', [
|
||||
{ relation: 'mother', name: undefined, education: undefined, occupation: undefined },
|
||||
{ relation: 'father', name: undefined, education: undefined, occupation: undefined },
|
||||
])
|
||||
}
|
||||
} else if (newValue === '0' && formRef.value) {
|
||||
// Ketika memilih "Tidak", kosongkan families
|
||||
formRef.value.setFieldValue('families', [])
|
||||
}
|
||||
},
|
||||
{ immediate: false },
|
||||
)
|
||||
const formSchema = toTypedSchema(ResponsiblePersonRelativeSchema)
|
||||
const isFamilyFormDisabled = ref(true)
|
||||
|
||||
const { values, resetForm, setValues, validate, setFieldValue } = useForm<FormData>({
|
||||
name: 'familyParentsForm',
|
||||
validationSchema: formSchema,
|
||||
initialValues: props.initialValues ? props.initialValues : {},
|
||||
validateOnMount: false,
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
validate: () => formRef.value?.validate(),
|
||||
resetForm: () => formRef.value?.resetForm(),
|
||||
values: computed(() => formRef.value?.values),
|
||||
validate,
|
||||
resetForm,
|
||||
setValues,
|
||||
values,
|
||||
})
|
||||
|
||||
watch(
|
||||
() => values._shareFamilyData,
|
||||
(newValue) => {
|
||||
if (!newValue) return
|
||||
|
||||
if (newValue === 'yes') {
|
||||
isFamilyFormDisabled.value = false
|
||||
|
||||
const fam = values?.families || []
|
||||
|
||||
const needsReset = fam.length !== 2 || fam[0]?.relation !== 'mother' || fam[1]?.relation !== 'father'
|
||||
|
||||
if (needsReset) {
|
||||
setFieldValue('families', [
|
||||
{ relation: 'mother', name: '', education_code: '', occupation_code: '' },
|
||||
{ relation: 'father', name: '', education_code: '', occupation_code: '' },
|
||||
])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
isFamilyFormDisabled.value = true
|
||||
|
||||
setFieldValue('families', [])
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
ref="formRef"
|
||||
v-slot="{ values }"
|
||||
as=""
|
||||
keep-values
|
||||
:validation-schema="formSchema"
|
||||
:validate-on-mount="false"
|
||||
validation-mode="onSubmit"
|
||||
:initial-values="
|
||||
initialValues || {
|
||||
shareFamilyData: '0',
|
||||
families: [],
|
||||
}
|
||||
"
|
||||
>
|
||||
<form @submit.prevent>
|
||||
<div>
|
||||
<p
|
||||
v-if="props.title"
|
||||
class="text-sm 2xl:text-base mb-2 2xl:mb-3 font-semibold"
|
||||
class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base"
|
||||
>
|
||||
{{ props.title }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||
<!-- Radio Button Section - Full Width -->
|
||||
<div class="mb-6">
|
||||
<RadioParentsInput field-name="shareFamilyData" />
|
||||
<div class="mb-5 pb-3 text-lg xl:text-xl">
|
||||
<div>
|
||||
<RadioParentsInput
|
||||
field-name="_shareFamilyData"
|
||||
:is-disabled="isReadonly"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Form Fields Section - Only show when "Ya" is selected -->
|
||||
<div
|
||||
v-if="values.shareFamilyData === '1'"
|
||||
class="space-y-6"
|
||||
>
|
||||
<div :key="values._shareFamilyData">
|
||||
<FieldArray
|
||||
v-slot="{ fields }"
|
||||
v-slot="{ fields, push, remove }"
|
||||
name="families"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<template
|
||||
<template v-if="fields.length === 0">
|
||||
{{ push({ relation: 'mother', name: '', address: '', education_code: '' }) }}
|
||||
{{ push({ relation: 'father', name: '', address: '', education_code: '' }) }}
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<DE.Block
|
||||
v-for="(field, idx) in fields"
|
||||
:key="field.key"
|
||||
:col-count="5"
|
||||
:cell-flex="false"
|
||||
>
|
||||
<div
|
||||
class="space-y-4 rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700 dark:bg-gray-800/50"
|
||||
>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ (values.families as FamilyData[])?.[idx]?.relation === 'mother' ? 'Data Ibu' : 'Data Ayah' }}
|
||||
</h4>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<InputBase
|
||||
:field-name="`families[${idx}].name`"
|
||||
:label="
|
||||
(values.families as FamilyData[])?.[idx]?.relation === 'mother'
|
||||
? 'Nama Ibu Kandung'
|
||||
: (values.families as FamilyData[])?.[idx]?.relation === 'father'
|
||||
? 'Nama Ayah Kandung'
|
||||
: 'Nama Keluarga'
|
||||
"
|
||||
:placeholder="
|
||||
(values.families as FamilyData[])?.[idx]?.relation === 'mother'
|
||||
? 'Masukkan nama ibu pasien'
|
||||
: (values.families as FamilyData[])?.[idx]?.relation === 'father'
|
||||
? 'Masukkan nama ayah pasien'
|
||||
: 'Masukkan nama'
|
||||
"
|
||||
:errors="errors"
|
||||
is-required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SelectEducation
|
||||
:field-name="`families[${idx}].education`"
|
||||
:label="
|
||||
(values.families as FamilyData[])?.[idx]?.relation === 'mother'
|
||||
? 'Pendidikan Ibu'
|
||||
: (values.families as FamilyData[])?.[idx]?.relation === 'father'
|
||||
? 'Pendidikan Ayah'
|
||||
: 'Pendidikan'
|
||||
"
|
||||
placeholder="Pilih"
|
||||
:errors="errors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputBase
|
||||
:field-name="`families[${idx}].occupation`"
|
||||
:label="
|
||||
(values.families as FamilyData[])?.[idx]?.relation === 'mother'
|
||||
? 'Pekerjaan Ibu'
|
||||
: (values.families as FamilyData[])?.[idx]?.relation === 'father'
|
||||
? 'Pekerjaan Ayah'
|
||||
: 'Pekerjaan'
|
||||
"
|
||||
:placeholder="
|
||||
(values.families as FamilyData[])?.[idx]?.relation === 'mother'
|
||||
? 'Masukkan pekerjaan ibu'
|
||||
: (values.families as FamilyData[])?.[idx]?.relation === 'father'
|
||||
? 'Masukkan pekerjaan ayah'
|
||||
: 'Masukkan pekerjaan'
|
||||
"
|
||||
:errors="errors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<SelectRelations
|
||||
:label="idx === 0 ? 'Hubungan dengan Pasien' : undefined"
|
||||
:field-name="`families[${idx}].relation`"
|
||||
placeholder="Pilih"
|
||||
is-disabled
|
||||
/>
|
||||
<InputBase
|
||||
:label="idx === 0 ? 'Nama' : undefined"
|
||||
:field-name="`families[${idx}].name`"
|
||||
placeholder="Masukkan Nama"
|
||||
:is-disabled="isReadonly || values._shareFamilyData === 'no'"
|
||||
/>
|
||||
<!-- <SelectJob
|
||||
:label="idx === 0 ? 'Pekerjaan' : undefined"
|
||||
:field-name="`families[${idx}].occupation_code`"
|
||||
placeholder="Pilih pekerjaan"
|
||||
is-required
|
||||
:is-disabled="isReadonly || values.shareFamilyData === 'no'"
|
||||
/> -->
|
||||
<SelectEducation
|
||||
:label="idx === 0 ? 'Pendidikan' : undefined"
|
||||
:field-name="`families[${idx}].education_code`"
|
||||
placeholder="Pilih pendidikan"
|
||||
is-required
|
||||
:is-disabled="isReadonly || values._shareFamilyData === 'no'"
|
||||
/>
|
||||
</DE.Block>
|
||||
</div>
|
||||
</FieldArray>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as RadioParentsInput } from './radio-parents-input.vue'
|
||||
export { default as SelectBirthPlace } from './select-birth-place.vue'
|
||||
+9
-5
@@ -16,6 +16,7 @@ const props = defineProps<{
|
||||
radioItemClass?: string
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
@@ -28,8 +29,8 @@ const {
|
||||
} = props
|
||||
|
||||
const residenceOptions = [
|
||||
{ label: 'Ya', value: '1' },
|
||||
{ label: 'Tidak', value: '0' },
|
||||
{ label: 'Ya', value: 'yes' },
|
||||
{ label: 'Tidak', value: 'no' },
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -55,6 +56,7 @@ const residenceOptions = [
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
v-bind="componentField"
|
||||
:disabled="isDisabled"
|
||||
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
|
||||
>
|
||||
<div
|
||||
@@ -63,20 +65,22 @@ const residenceOptions = [
|
||||
:class="cn('flex min-w-fit items-center space-x-2', radioItemClass)"
|
||||
>
|
||||
<RadioGroupItem
|
||||
:disabled="isDisabled"
|
||||
:id="`${fieldName}-${index}`"
|
||||
:value="option.value"
|
||||
:class="
|
||||
cn(
|
||||
'relative h-4 w-4 rounded-full border-2 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',
|
||||
'peer relative h-4 w-4 rounded-full border-2 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,
|
||||
)
|
||||
"
|
||||
/>
|
||||
<RadioLabel
|
||||
:for="`${fieldName}-${index}`"
|
||||
:for="isDisabled ? undefined : `${fieldName}-${index}`"
|
||||
:class="
|
||||
cn(
|
||||
'cursor-pointer select-none text-xs font-medium leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
||||
'select-none text-xs font-medium leading-none transition-colors sm:text-sm',
|
||||
isDisabled ? 'cursor-not-allowed opacity-70' : 'cursor-pointer hover:text-primary',
|
||||
labelClass,
|
||||
)
|
||||
"
|
||||
+13
-6
@@ -1,9 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
@@ -52,11 +49,21 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||
<DE.Label :label-for="fieldName" :is-required="isRequired">
|
||||
<DE.Label
|
||||
:label-for="fieldName"
|
||||
:is-required="isRequired"
|
||||
>
|
||||
Tempat Lahir
|
||||
</DE.Label>
|
||||
<DE.Field :id="fieldName" :errors="errors" :class="cn('select-field-wrapper')">
|
||||
<FormField v-slot="{ componentField }" :name="fieldName">
|
||||
<DE.Field
|
||||
:id="fieldName"
|
||||
:errors="errors"
|
||||
:class="cn('select-field-wrapper')"
|
||||
>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
:name="fieldName"
|
||||
>
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
@@ -1,9 +0,0 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||
<Icon name="i-lucide-user" class="me-2" />
|
||||
<span class="font-semibold">Tambah</span> Pasien
|
||||
</div>
|
||||
<AppPatientEntryForm />
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { withBase } from '~/models/_base'
|
||||
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import type { Patient } from '~/models/patient'
|
||||
import type { PatientEntity } from '~/models/patient'
|
||||
import type { Person } from '~/models/person'
|
||||
|
||||
// Components
|
||||
@@ -18,7 +18,7 @@ const props = defineProps<{
|
||||
|
||||
// #region State & Computed
|
||||
const patient = ref(
|
||||
withBase<Patient>({
|
||||
withBase<PatientEntity>({
|
||||
person: {} as Person,
|
||||
personAddresses: [],
|
||||
personContacts: [],
|
||||
@@ -47,19 +47,18 @@ onMounted(async () => {
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
function handleAction(type: string) {
|
||||
switch (type) {
|
||||
case 'edit':
|
||||
// TODO: Handle edit action
|
||||
console.log('editing data')
|
||||
break
|
||||
|
||||
case 'cancel':
|
||||
navigateTo({
|
||||
name: 'client-patient',
|
||||
})
|
||||
break
|
||||
}
|
||||
async function onBack() {
|
||||
await navigateTo({
|
||||
name: 'client-patient',
|
||||
})
|
||||
}
|
||||
async function onEdit() {
|
||||
await navigateTo({
|
||||
name: 'client-patient-id-edit',
|
||||
params: {
|
||||
id: props.patientId,
|
||||
},
|
||||
})
|
||||
}
|
||||
// #endregion
|
||||
|
||||
@@ -68,13 +67,19 @@ function handleAction(type: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
/>
|
||||
<div
|
||||
v-if="patient"
|
||||
:key="patient.id"
|
||||
>
|
||||
<Header
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
/>
|
||||
|
||||
<AppPatientPreview
|
||||
:patient="patient"
|
||||
@click="handleAction"
|
||||
/>
|
||||
<AppPatientPreview
|
||||
:patient="patient"
|
||||
@back="onBack"
|
||||
@edit="onEdit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Patient, genPatientProps } from '~/models/patient'
|
||||
import type { ExposedForm } from '~/types/form'
|
||||
import type { PatientBase } from '~/models/patient'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import { genPatient } from '~/models/patient'
|
||||
import { PatientSchema } from '~/schemas/patient.schema'
|
||||
import { PersonAddressRelativeSchema } from '~/schemas/person-address-relative.schema'
|
||||
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 { uploadAttachment } from '~/services/patient.service'
|
||||
|
||||
import {
|
||||
// for form entry
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/patient.handler'
|
||||
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = defineProps<{
|
||||
callbackUrl?: string
|
||||
}>()
|
||||
|
||||
const residentIdentityFile = ref<File>()
|
||||
const familyCardFile = ref<File>()
|
||||
|
||||
// form related state
|
||||
const personAddressForm = ref<ExposedForm<any> | null>(null)
|
||||
const personAddressRelativeForm = ref<ExposedForm<any> | null>(null)
|
||||
const personContactForm = ref<ExposedForm<any> | null>(null)
|
||||
const personEmergencyContactRelative = ref<ExposedForm<any> | null>(null)
|
||||
const personFamilyForm = ref<ExposedForm<any> | null>(null)
|
||||
const personPatientForm = ref<ExposedForm<any> | null>(null)
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(() => {
|
||||
// Initial synchronization when forms are mounted and isSameAddress is true by default
|
||||
nextTick(() => {
|
||||
const isSameAddress = personAddressRelativeForm.value?.values?.isSameAddress
|
||||
if (
|
||||
(isSameAddress === true || isSameAddress === '1') &&
|
||||
personAddressForm.value?.values &&
|
||||
personAddressRelativeForm.value
|
||||
) {
|
||||
const currentAddressValues = personAddressForm.value.values
|
||||
if (Object.keys(currentAddressValues).length > 0) {
|
||||
personAddressRelativeForm.value.setValues(
|
||||
{
|
||||
...personAddressRelativeForm.value.values,
|
||||
province_code: currentAddressValues.province_code || undefined,
|
||||
regency_code: currentAddressValues.regency_code || undefined,
|
||||
district_code: currentAddressValues.district_code || undefined,
|
||||
village_code: currentAddressValues.village_code || undefined,
|
||||
postalRegion_code: currentAddressValues.postalRegion_code || undefined,
|
||||
address: currentAddressValues.address || undefined,
|
||||
rt: currentAddressValues.rt || undefined,
|
||||
rw: currentAddressValues.rw || undefined,
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
async function composeFormData(): Promise<Patient> {
|
||||
const [patient, address, addressRelative, families, contacts, emergencyContact] = await Promise.all([
|
||||
personPatientForm.value?.validate(),
|
||||
personAddressForm.value?.validate(),
|
||||
personAddressRelativeForm.value?.validate(),
|
||||
personFamilyForm.value?.validate(),
|
||||
personContactForm.value?.validate(),
|
||||
personEmergencyContactRelative.value?.validate(),
|
||||
])
|
||||
|
||||
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
|
||||
// for example: dropdown not selected
|
||||
if (!allValid) return Promise.reject('Form validation failed')
|
||||
|
||||
const formDataRequest: genPatientProps = {
|
||||
patient: patient?.values,
|
||||
residentAddress: address?.values,
|
||||
cardAddress: addressRelative?.values,
|
||||
familyData: families?.values,
|
||||
contacts: contacts?.values,
|
||||
responsible: emergencyContact?.values,
|
||||
}
|
||||
|
||||
const formData = genPatient(formDataRequest)
|
||||
|
||||
if (patient?.values.residentIdentityFile) {
|
||||
residentIdentityFile.value = patient?.values.residentIdentityFile
|
||||
}
|
||||
|
||||
if (patient?.values.familyIdentityFile) {
|
||||
familyCardFile.value = patient?.values.familyIdentityFile
|
||||
}
|
||||
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
const patient: Patient = await composeFormData()
|
||||
let createdPatientId = 0
|
||||
|
||||
const response = await handleActionSave(
|
||||
patient,
|
||||
() => {},
|
||||
() => {},
|
||||
toast,
|
||||
)
|
||||
|
||||
const data = (response?.body?.data ?? null) as PatientBase | null
|
||||
if (!data) return
|
||||
createdPatientId = data.id
|
||||
|
||||
if (residentIdentityFile.value) {
|
||||
void uploadAttachment(residentIdentityFile.value, createdPatientId, 'ktp')
|
||||
}
|
||||
if (familyCardFile.value) {
|
||||
void uploadAttachment(familyCardFile.value, createdPatientId, 'kk')
|
||||
}
|
||||
|
||||
// If has callback provided redirect to callback with patientData
|
||||
if (props.callbackUrl) {
|
||||
await navigateTo(props.callbackUrl + '?patient-id=' + patient.id)
|
||||
return
|
||||
}
|
||||
|
||||
// Navigate to patient list or show success message
|
||||
await navigateTo('/client/patient')
|
||||
return
|
||||
}
|
||||
|
||||
if (eventType === 'cancel') {
|
||||
if (props.callbackUrl) {
|
||||
await navigateTo(props.callbackUrl)
|
||||
return
|
||||
}
|
||||
|
||||
await navigateTo({
|
||||
name: 'client-patient',
|
||||
})
|
||||
// handleCancelForm()
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// Watcher untuk sinkronisasi initial ketika kedua form sudah ready
|
||||
watch(
|
||||
[() => personAddressForm.value, () => personAddressRelativeForm.value],
|
||||
([addressForm, relativeForm]) => {
|
||||
if (addressForm && relativeForm) {
|
||||
// Trigger initial sync jika isSameAddress adalah true
|
||||
nextTick(() => {
|
||||
const isSameAddress = relativeForm.values?.isSameAddress
|
||||
if ((isSameAddress === true || isSameAddress === '1') && addressForm.values) {
|
||||
const currentAddressValues = addressForm.values
|
||||
if (Object.keys(currentAddressValues).length > 0) {
|
||||
relativeForm.setValues(
|
||||
{
|
||||
...relativeForm.values,
|
||||
province_code: currentAddressValues.province_code || undefined,
|
||||
regency_code: currentAddressValues.regency_code || undefined,
|
||||
district_code: currentAddressValues.district_code || undefined,
|
||||
village_code: currentAddressValues.village_code || undefined,
|
||||
postalRegion_code: currentAddressValues.postalRegion_code || undefined,
|
||||
address: currentAddressValues.address || undefined,
|
||||
rt: currentAddressValues.rt || undefined,
|
||||
rw: currentAddressValues.rw || undefined,
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// Watcher untuk sinkronisasi alamat ketika isSameAddress = true
|
||||
watch(
|
||||
() => personAddressForm.value?.values,
|
||||
(newAddressValues) => {
|
||||
// Cek apakah alamat KTP harus sama dengan alamat sekarang
|
||||
const isSameAddress = personAddressRelativeForm.value?.values?.isSameAddress
|
||||
|
||||
if ((isSameAddress === true || isSameAddress === '1') && newAddressValues && personAddressRelativeForm.value) {
|
||||
// Sinkronkan semua field alamat dari alamat sekarang ke alamat KTP
|
||||
personAddressRelativeForm.value.setValues(
|
||||
{
|
||||
...personAddressRelativeForm.value.values,
|
||||
province_code: newAddressValues.province_code || undefined,
|
||||
regency_code: newAddressValues.regency_code || undefined,
|
||||
district_code: newAddressValues.district_code || undefined,
|
||||
village_code: newAddressValues.village_code || undefined,
|
||||
postalRegion_code: newAddressValues.postalRegion_code || undefined,
|
||||
address: newAddressValues.address || undefined,
|
||||
rt: newAddressValues.rt || undefined,
|
||||
rw: newAddressValues.rw || undefined,
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// Watcher untuk memantau perubahan isSameAddress
|
||||
watch(
|
||||
() => personAddressRelativeForm.value?.values?.isSameAddress,
|
||||
(isSameAddress) => {
|
||||
if (
|
||||
(isSameAddress === true || isSameAddress === '1') &&
|
||||
personAddressForm.value?.values &&
|
||||
personAddressRelativeForm.value?.values
|
||||
) {
|
||||
// Ketika isSameAddress diubah menjadi true, copy alamat sekarang ke alamat KTP
|
||||
const currentAddressValues = personAddressForm.value.values
|
||||
personAddressRelativeForm.value.setValues(
|
||||
{
|
||||
...personAddressRelativeForm.value.values,
|
||||
province_code: currentAddressValues.province_code || undefined,
|
||||
regency_code: currentAddressValues.regency_code || undefined,
|
||||
district_code: currentAddressValues.district_code || undefined,
|
||||
village_code: currentAddressValues.village_code || undefined,
|
||||
postalRegion_code: currentAddressValues.postalRegion_code || undefined,
|
||||
address: currentAddressValues.address || undefined,
|
||||
rt: currentAddressValues.rt || undefined,
|
||||
rw: currentAddressValues.rw || undefined,
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Tambah Pasien</div>
|
||||
<AppPatientEntryForm
|
||||
ref="personPatientForm"
|
||||
:schema="PatientSchema"
|
||||
/>
|
||||
<div class="h-6"></div>
|
||||
<AppPersonAddressEntryForm
|
||||
ref="personAddressForm"
|
||||
title="Alamat Sekarang"
|
||||
:schema="PersonAddressSchema"
|
||||
/>
|
||||
<div class="h-6"></div>
|
||||
<AppPersonAddressEntryFormRelative
|
||||
ref="personAddressRelativeForm"
|
||||
title="Alamat KTP"
|
||||
:schema="PersonAddressRelativeSchema"
|
||||
/>
|
||||
<div class="h-6"></div>
|
||||
<AppPersonFamilyParentsForm
|
||||
ref="personFamilyForm"
|
||||
title="Identitas Orang Tua"
|
||||
:schema="PersonFamiliesSchema"
|
||||
/>
|
||||
<div class="h-6"></div>
|
||||
<AppPersonContactEntryForm
|
||||
ref="personContactForm"
|
||||
title="Kontak Pasien"
|
||||
:contact-limit="10"
|
||||
:schema="PersonContactListSchema"
|
||||
/>
|
||||
<AppPersonRelativeEntryForm
|
||||
ref="personEmergencyContactRelative"
|
||||
title="Penanggung Jawab"
|
||||
:schema="ResponsiblePersonSchema"
|
||||
/>
|
||||
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action @click="handleActionClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* component style */
|
||||
</style>
|
||||
@@ -0,0 +1,490 @@
|
||||
<script setup lang="ts">
|
||||
// type
|
||||
import { withBase } from '~/models/_base'
|
||||
import type { Person } from '~/models/person'
|
||||
import type { PersonAddress } from '~/models/person-address'
|
||||
import type { PersonContact } from '~/models/person-contact'
|
||||
import type { PersonRelative } from '~/models/person-relative'
|
||||
import type { ExposedForm } from '~/types/form'
|
||||
import type { PatientEntity, PatientBase, Patient, genPatientProps } from '~/models/patient'
|
||||
import { genPatientEntity } from '~/models/patient'
|
||||
|
||||
// components
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import AppPatientEntryForm from '~/components/app/patient/entry-form.vue'
|
||||
import AppPersonAddressEntryForm from '~/components/app/person-address/entry-form.vue'
|
||||
import AppPersonAddressEntryFormRelative from '~/components/app/person-address/entry-form-relative.vue'
|
||||
import AppPersonFamilyParentsForm from '~/components/app/person/family-parents-form.vue'
|
||||
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'
|
||||
|
||||
import { isReadonly, isProcessing, handleActionSave, handleActionEdit } from '~/handlers/patient.handler'
|
||||
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
// reverse mapping untuk contact type (backend → UI)
|
||||
const reverseContactTypeMapping: Record<string, string> = {
|
||||
'm-phone': 'phoneNumber',
|
||||
phone: 'homePhoneNumber',
|
||||
email: 'email',
|
||||
fax: 'fax',
|
||||
}
|
||||
|
||||
// #region Props & Emits
|
||||
const props = defineProps<{
|
||||
callbackUrl?: string
|
||||
mode: 'add' | 'edit'
|
||||
patientId?: string | number
|
||||
}>()
|
||||
|
||||
const residentIdentityFile = ref<File>()
|
||||
const familyCardFile = ref<File>()
|
||||
|
||||
// form related state
|
||||
const personAddressForm = ref<ExposedForm<any> | null>(null)
|
||||
const personAddressRelativeForm = ref<ExposedForm<any> | null>(null)
|
||||
const personContactForm = ref<ExposedForm<any> | null>(null)
|
||||
const personEmergencyContactRelative = ref<ExposedForm<any> | null>(null)
|
||||
const personFamilyForm = ref<ExposedForm<any> | null>(null)
|
||||
const personPatientForm = ref<ExposedForm<any> | null>(null)
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const patientDetail = ref(
|
||||
withBase<PatientEntity>({
|
||||
person: {} as Person,
|
||||
personAddresses: [],
|
||||
personContacts: [],
|
||||
personRelatives: [],
|
||||
}),
|
||||
)
|
||||
|
||||
// Key untuk memaksa re-render form saat data berubah
|
||||
const formKey = ref(0)
|
||||
|
||||
// Computed: unwrap patient data untuk form patient
|
||||
const patientFormInitialValues = computed(() => {
|
||||
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 || undefined,
|
||||
drivingLicenseNumber: person.drivingLicenseNumber || undefined,
|
||||
passportNumber: person.passportNumber || undefined,
|
||||
fullName: person.name || '',
|
||||
isNewBorn: p.newBornStatus ? 'yes' : 'no',
|
||||
gender: person.gender_code || '',
|
||||
birthPlace: person.birthRegency_code || '',
|
||||
birthDate: birthDate,
|
||||
education: person.education_code || '',
|
||||
job: person.occupation_code || '',
|
||||
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(() => {
|
||||
if (!patientDetail.value.person?.addresses) return {}
|
||||
|
||||
const addresses = patientDetail.value.person?.addresses
|
||||
const domicileAddress = addresses.find((a: PersonAddress) => a.locationType_code === 'domicile')
|
||||
if (!domicileAddress) return undefined
|
||||
|
||||
// extract kode wilayah dari preload data
|
||||
const village = domicileAddress.postalRegion?.village
|
||||
const district = village?.district
|
||||
const regency = district?.regency
|
||||
const province = regency?.province
|
||||
|
||||
return {
|
||||
id: domicileAddress.id || 0,
|
||||
locationType_code: 'domicile',
|
||||
province_code: province?.code || '',
|
||||
regency_code: regency?.code || '',
|
||||
district_code: district?.code || '',
|
||||
village_code: village?.code || domicileAddress.village_code || '',
|
||||
postalRegion_code: domicileAddress.postalRegion_code || '',
|
||||
address: domicileAddress.address || '',
|
||||
rt: domicileAddress.rt || '',
|
||||
rw: domicileAddress.rw || '',
|
||||
}
|
||||
})
|
||||
|
||||
// Computed: unwrap alamat KTP (identity)
|
||||
const addressRelativeFormInitialValues = computed(() => {
|
||||
if (!patientDetail.value.person?.addresses) return {}
|
||||
|
||||
const addresses = patientDetail.value.person?.addresses
|
||||
const domicileAddress = addresses.find((a: PersonAddress) => a.locationType_code === 'domicile')!
|
||||
const identityAddress = addresses.find((a: PersonAddress) => a.locationType_code === 'identity')!
|
||||
|
||||
// extract kode wilayah dari preload data
|
||||
const village = identityAddress.postalRegion?.village
|
||||
const district = village?.district
|
||||
const regency = district?.regency
|
||||
const province = regency?.province
|
||||
|
||||
const isSame = domicileAddress.village_code === identityAddress.village_code ? '1' : '0'
|
||||
|
||||
return {
|
||||
isSameAddress: isSame,
|
||||
id: identityAddress.id || 0,
|
||||
locationType_code: 'identity',
|
||||
province_code: province?.code || '',
|
||||
regency_code: regency?.code || '',
|
||||
district_code: district?.code || '',
|
||||
village_code: village?.code || identityAddress.village_code || '',
|
||||
postalRegion_code: identityAddress.postalRegion_code || '',
|
||||
address: identityAddress.address || '',
|
||||
rt: identityAddress.rt || '',
|
||||
rw: identityAddress.rw || '',
|
||||
}
|
||||
})
|
||||
|
||||
// Computed: unwrap data orang tua
|
||||
const familyFormInitialValues = computed(() => {
|
||||
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'),
|
||||
)
|
||||
|
||||
if (parents.length === 0) {
|
||||
return {
|
||||
_shareFamilyData: 'no' as const,
|
||||
families: [],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
_shareFamilyData: 'yes' as const,
|
||||
families: parents.map((parent: PersonRelative) => ({
|
||||
id: parent.id || 0,
|
||||
relation: parent.relationship_code || '',
|
||||
name: parent.name || '',
|
||||
occupation_code: parent.occupation_code || '',
|
||||
education_code: parent.education_code || '',
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
// Computed: unwrap kontak pasien
|
||||
const contactFormInitialValues = computed(() => {
|
||||
const contacts = patientDetail.value.person?.contacts || patientDetail.value.personContacts || []
|
||||
if (contacts.length === 0) return undefined
|
||||
|
||||
return {
|
||||
contacts: contacts.map((contact: PersonContact) => ({
|
||||
id: contact.id || 0,
|
||||
contactType: reverseContactTypeMapping[contact.type_code] || contact.type_code || '',
|
||||
contactNumber: contact.value || '',
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
// Computed: unwrap penanggung jawab
|
||||
const responsibleFormInitialValues = computed(() => {
|
||||
const relatives = patientDetail.value.person?.relatives || patientDetail.value.personRelatives || []
|
||||
const responsibles = relatives.filter((r: PersonRelative) => r.responsible === true)
|
||||
|
||||
if (responsibles.length === 0) return undefined
|
||||
|
||||
return {
|
||||
contacts: responsibles.map((r: PersonRelative) => ({
|
||||
id: r.id || 0,
|
||||
relation: r.relationship_code || '',
|
||||
name: r.name || '',
|
||||
address: r.address || '',
|
||||
phone: r.phoneNumber || '',
|
||||
})),
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(async () => {
|
||||
// if edit mode, fetch patient detail
|
||||
if (props.mode === 'edit' && props.patientId) {
|
||||
await loadInitData(props.patientId)
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
async function composeFormData(): Promise<PatientEntity> {
|
||||
const [patient, address, addressRelative, families, contacts, emergencyContact] = await Promise.all([
|
||||
personPatientForm.value?.validate(),
|
||||
personAddressForm.value?.validate(),
|
||||
personAddressRelativeForm.value?.validate(),
|
||||
personFamilyForm.value?.validate(),
|
||||
personContactForm.value?.validate(),
|
||||
personEmergencyContactRelative.value?.validate(),
|
||||
])
|
||||
|
||||
const results = [patient, address, addressRelative, families, contacts, emergencyContact]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
|
||||
console.log(results)
|
||||
// exit, if form errors happend during validation
|
||||
// for example: dropdown not selected
|
||||
if (!allValid) return Promise.reject('Form validation failed')
|
||||
|
||||
const formDataRequest: genPatientProps = {
|
||||
patient: {
|
||||
...patient?.values,
|
||||
|
||||
// casting comp. val to backend well known reflect value
|
||||
ethnic: patient?.values.nationality === 'WNI' ? patient?.values.ethnic : null,
|
||||
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,
|
||||
contacts: contacts?.values,
|
||||
responsible: emergencyContact?.values,
|
||||
}
|
||||
|
||||
const formData = genPatientEntity(formDataRequest, patientDetail.value)
|
||||
|
||||
if (patient?.values.residentIdentityFile) {
|
||||
residentIdentityFile.value = patient?.values.residentIdentityFile
|
||||
}
|
||||
|
||||
if (patient?.values.familyIdentityFile) {
|
||||
familyCardFile.value = patient?.values.familyIdentityFile
|
||||
}
|
||||
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function loadInitData(id: number | string) {
|
||||
isProcessing.value = true
|
||||
try {
|
||||
const response = await getPatientDetail(id as number)
|
||||
if (response.success) {
|
||||
patientDetail.value = response.body.data || {}
|
||||
// Increment key untuk memaksa re-render form dengan data baru
|
||||
formKey.value++
|
||||
}
|
||||
|
||||
isProcessing.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Terjadi kesalahan saat mengambil data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleActionClick(eventType: string) {
|
||||
try {
|
||||
if (eventType === 'submit') {
|
||||
const patient: Patient = await composeFormData()
|
||||
|
||||
let createdPatientId = 0
|
||||
let response: any
|
||||
// return
|
||||
if (props.mode === 'edit' && props.patientId) {
|
||||
response = await handleActionEdit(
|
||||
patientDetail.value.id,
|
||||
patient,
|
||||
() => {},
|
||||
() => {},
|
||||
toast,
|
||||
)
|
||||
} else {
|
||||
response = await handleActionSave(
|
||||
patient,
|
||||
() => {},
|
||||
() => {},
|
||||
toast,
|
||||
)
|
||||
}
|
||||
|
||||
const data = (response?.body?.data ?? null) as PatientBase | null
|
||||
if (!data) return
|
||||
createdPatientId = data.id
|
||||
|
||||
if (residentIdentityFile.value) {
|
||||
void uploadAttachment(residentIdentityFile.value, createdPatientId, 'ktp')
|
||||
}
|
||||
if (familyCardFile.value) {
|
||||
void uploadAttachment(familyCardFile.value, createdPatientId, 'kk')
|
||||
}
|
||||
|
||||
// If has callback provided redirect to callback with patientData
|
||||
if (props.callbackUrl && props.callbackUrl.length > 0) {
|
||||
await navigateTo(props.callbackUrl + '?patient-id=' + createdPatientId)
|
||||
return
|
||||
}
|
||||
|
||||
// Navigate to patient list or show success message
|
||||
await navigateTo('/client/patient')
|
||||
return
|
||||
}
|
||||
|
||||
if (eventType === 'cancel') {
|
||||
if (props.callbackUrl) {
|
||||
await navigateTo(props.callbackUrl)
|
||||
return
|
||||
}
|
||||
|
||||
// handleCancelForm()
|
||||
}
|
||||
if (eventType === 'back') {
|
||||
await navigateTo({
|
||||
name: 'client-patient',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
// Show error toast to user
|
||||
if (typeof error === 'string') {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error,
|
||||
variant: 'destructive',
|
||||
})
|
||||
} else if (error instanceof Error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message || 'Terjadi kesalahan saat menyimpan data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} else {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Terjadi kesalahan saat menyimpan data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// Helper: Cek apakah isSameAddress aktif
|
||||
const isSameAddressActive = () => {
|
||||
const val = personAddressRelativeForm.value?.values?.isSameAddress
|
||||
return val === true || val === '1'
|
||||
}
|
||||
|
||||
// Helper: Sinkronkan alamat sekarang ke alamat KTP
|
||||
const syncAddressToRelative = () => {
|
||||
const source = personAddressForm.value?.values
|
||||
const target = personAddressRelativeForm.value
|
||||
|
||||
if (!source || !target) return
|
||||
|
||||
const addressFields = [
|
||||
'province_code',
|
||||
'regency_code',
|
||||
'district_code',
|
||||
'village_code',
|
||||
'postalRegion_code',
|
||||
'address',
|
||||
'rt',
|
||||
'rw',
|
||||
] as const
|
||||
const syncedValues = Object.fromEntries(addressFields.map((key) => [key, source[key] || undefined]))
|
||||
|
||||
target.setValues({ ...target.values, ...syncedValues }, false)
|
||||
}
|
||||
|
||||
// Watcher: Sinkronisasi saat nilai alamat berubah atau isSameAddress diaktifkan
|
||||
watch(
|
||||
[() => personAddressForm.value?.values, () => personAddressRelativeForm.value?.values?.isSameAddress],
|
||||
() => {
|
||||
if (isSameAddressActive()) {
|
||||
syncAddressToRelative()
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">
|
||||
{{ mode === 'edit' ? 'Edit Pasien' : 'Tambah Pasien' }}
|
||||
</div>
|
||||
<AppPatientEntryForm
|
||||
:key="`patient-${formKey}`"
|
||||
ref="personPatientForm"
|
||||
:is-readonly="isProcessing || isReadonly"
|
||||
:initial-values="patientFormInitialValues"
|
||||
/>
|
||||
<div class="h-6"></div>
|
||||
<AppPersonAddressEntryForm
|
||||
:key="`address-${formKey}`"
|
||||
ref="personAddressForm"
|
||||
title="Alamat Sekarang"
|
||||
:is-readonly="isProcessing || isReadonly"
|
||||
:initial-values="addressFormInitialValues"
|
||||
/>
|
||||
<div class="h-6"></div>
|
||||
<AppPersonAddressEntryFormRelative
|
||||
:key="`address-rel-${formKey}`"
|
||||
ref="personAddressRelativeForm"
|
||||
title="Alamat KTP"
|
||||
:is-readonly="isProcessing || isReadonly"
|
||||
:initial-values="addressRelativeFormInitialValues"
|
||||
/>
|
||||
<div class="h-6"></div>
|
||||
<AppPersonFamilyParentsForm
|
||||
:key="`family-${formKey}`"
|
||||
ref="personFamilyForm"
|
||||
title="Identitas Orang Tua"
|
||||
:is-readonly="isProcessing || isReadonly"
|
||||
:initial-values="familyFormInitialValues"
|
||||
/>
|
||||
<div class="h-6"></div>
|
||||
<AppPersonContactEntryForm
|
||||
:key="`contact-${formKey}`"
|
||||
ref="personContactForm"
|
||||
title="Kontak Pasien"
|
||||
:is-readonly="isProcessing || isReadonly"
|
||||
:initial-values="contactFormInitialValues"
|
||||
/>
|
||||
<AppPersonRelativeEntryForm
|
||||
:key="`responsible-${formKey}`"
|
||||
ref="personEmergencyContactRelative"
|
||||
title="Penanggung Jawab"
|
||||
:is-readonly="isProcessing || isReadonly"
|
||||
:initial-values="responsibleFormInitialValues"
|
||||
/>
|
||||
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action @click="handleActionClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* component style */
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@ import { Calendar, Hospital, UserCheck, UsersRound } from 'lucide-vue-next'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/header.vue'
|
||||
import SummaryCard from '~/components/pub/my-ui/summary-card/summary-card.vue'
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
|
||||
@@ -46,6 +46,7 @@ const headerPrep: HeaderPrep = {
|
||||
label: 'Tambah',
|
||||
onClick: () => navigateTo('/client/patient/add'),
|
||||
},
|
||||
refSearchNav: refSearchNav,
|
||||
}
|
||||
|
||||
// Disable dulu, ayahab kalo diminta
|
||||
@@ -137,18 +138,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:
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PersonAddressRelativeSchema } from '~/schemas/person-address-relative.s
|
||||
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 { ResponsiblePersonRelativeSchema } from '~/schemas/person-relative.schema'
|
||||
import { uploadAttachment } from '~/services/patient.service'
|
||||
import { getDetail, update } from '~/services/surgery-report.service'
|
||||
import type { SurgeryReport } from '~/models/surgery-report'
|
||||
@@ -28,17 +28,18 @@ import { handleActionEdit } from '~/handlers/surgery-report.handler'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
callbackUrl?: string
|
||||
record_id: number
|
||||
}>(), {
|
||||
|
||||
})
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
encounter_id: number
|
||||
callbackUrl?: string
|
||||
record_id: number
|
||||
}>(),
|
||||
{},
|
||||
)
|
||||
|
||||
// form related state
|
||||
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
|
||||
fetchFn: (params) => getList({ ...params, includes: 'specialist,subspecialist,doctor-employee-person', }),
|
||||
fetchFn: (params) => getList({ ...params, includes: 'specialist,subspecialist,doctor-employee-person' }),
|
||||
entityName: 'surgery-report',
|
||||
})
|
||||
// #endregion
|
||||
@@ -56,7 +57,7 @@ const selectedOperativeAction = ref<any>(null)
|
||||
onMounted(async () => {
|
||||
const result = await getDetail(props.record_id)
|
||||
if (result.success) {
|
||||
const responseData = {...result.body.data, date: formatDateYyyyMmDd(result.body.data.date)}
|
||||
const responseData = { ...result.body.data, date: formatDateYyyyMmDd(result.body.data.date) }
|
||||
surgeryReport.value = responseData
|
||||
inputForm.value?.setValues(responseData)
|
||||
}
|
||||
@@ -72,17 +73,15 @@ async function handleConfirmAdd() {
|
||||
const response = await handleActionEdit(
|
||||
props.record_id,
|
||||
await composeFormData(),
|
||||
() => { },
|
||||
() => { },
|
||||
() => {},
|
||||
() => {},
|
||||
toast,
|
||||
)
|
||||
goBack()
|
||||
}
|
||||
|
||||
async function composeFormData(): Promise<SurgeryReport> {
|
||||
const [input,] = await Promise.all([
|
||||
inputForm.value?.validate(),
|
||||
])
|
||||
const [input] = await Promise.all([inputForm.value?.validate()])
|
||||
|
||||
const results = [input]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
@@ -129,9 +128,12 @@ function handleCancelAdd() {
|
||||
:schema="SurgeryReportSchema"
|
||||
:operative-action-list="[]"
|
||||
/>
|
||||
|
||||
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action :enable-draft="false" @click="handleActionClick" />
|
||||
<Action
|
||||
:enable-draft="false"
|
||||
@click="handleActionClick"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Confirmation
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1 +1 @@
|
||||
export type ClickType = 'back' | 'draft' | 'submit'
|
||||
export type ClickType = 'back' | 'draft' | 'submit' | 'edit'
|
||||
|
||||
@@ -165,16 +165,20 @@ export function genCrudHandler<T = any>(crud: {
|
||||
refresh: () => void,
|
||||
reset: () => void,
|
||||
toast: ToastFn,
|
||||
) {
|
||||
): Promise<any | null> {
|
||||
isProcessing.value = true
|
||||
|
||||
let successResponse: any = null
|
||||
|
||||
await handleAsyncAction<[number | string, any], any>({
|
||||
action: crud.update,
|
||||
args: [id, values],
|
||||
toast,
|
||||
successMessage: 'Data berhasil diubah',
|
||||
errorMessage: 'Gagal mengubah data',
|
||||
onSuccess: () => {
|
||||
onSuccess: (result) => {
|
||||
isFormEntryDialogOpen.value = false
|
||||
successResponse = result
|
||||
if (refresh) refresh()
|
||||
},
|
||||
onFinally: (isSuccess: boolean) => {
|
||||
@@ -182,6 +186,8 @@ export function genCrudHandler<T = any>(crud: {
|
||||
isProcessing.value = false
|
||||
},
|
||||
})
|
||||
|
||||
return successResponse
|
||||
}
|
||||
|
||||
async function handleActionRemove(id: number | string, refresh: () => void, toast: ToastFn) {
|
||||
|
||||
+63
-41
@@ -74,18 +74,18 @@ export const bigTimeUnitCodes: Record<string, string> = {
|
||||
}
|
||||
|
||||
export const dischargeMethodCodes: Record<string, string> = {
|
||||
home: "Pulang",
|
||||
"home-request": "Pulang Atas Permintaan Sendiri",
|
||||
"consul-back": "Konsultasi Balik / Lanjutan",
|
||||
"consul-poly": "Konsultasi Poliklinik Lain",
|
||||
"consul-executive": "Konsultasi Antar Dokter Eksekutif",
|
||||
"consul-ch-day": "Konsultasi Hari Lain",
|
||||
emergency: "Rujuk IGD",
|
||||
"emergency-covid": "Rujuk IGD Covid",
|
||||
inpatient: "Rujuk Rawat Inap",
|
||||
external: "Rujuk Faskes Lain",
|
||||
death: "Meninggal",
|
||||
"death-on-arrival": "Meninggal Saat Tiba"
|
||||
home: 'Pulang',
|
||||
'home-request': 'Pulang Atas Permintaan Sendiri',
|
||||
'consul-back': 'Konsultasi Balik / Lanjutan',
|
||||
'consul-poly': 'Konsultasi Poliklinik Lain',
|
||||
'consul-executive': 'Konsultasi Antar Dokter Eksekutif',
|
||||
'consul-ch-day': 'Konsultasi Hari Lain',
|
||||
emergency: 'Rujuk IGD',
|
||||
'emergency-covid': 'Rujuk IGD Covid',
|
||||
inpatient: 'Rujuk Rawat Inap',
|
||||
external: 'Rujuk Faskes Lain',
|
||||
death: 'Meninggal',
|
||||
'death-on-arrival': 'Meninggal Saat Tiba',
|
||||
}
|
||||
|
||||
export const genderCodes: Record<string, string> = {
|
||||
@@ -387,13 +387,13 @@ export const medicalActionTypeCode: Record<string, string> = {
|
||||
export type medicalActionTypeCodeKey = keyof typeof medicalActionTypeCode
|
||||
|
||||
export const encounterDocTypeCode: Record<string, string> = {
|
||||
"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',
|
||||
'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 }[] = [
|
||||
@@ -406,20 +406,19 @@ export const encounterDocOpt: { label: string; value: encounterDocTypeCodeKey }[
|
||||
{ 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',
|
||||
'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',
|
||||
'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 = [
|
||||
@@ -428,8 +427,7 @@ export const supportingDocOpt = [
|
||||
{ label: 'Lain - Lain', value: 'encounter-other' },
|
||||
]
|
||||
|
||||
|
||||
export type SurgeryType = "kecil" | "sedang" | "besar" | "khusus"
|
||||
export type SurgeryType = 'kecil' | 'sedang' | 'besar' | 'khusus'
|
||||
export const SurgeryTypeOptList: { label: string; value: SurgeryType }[] = [
|
||||
{ label: 'Kecil', value: 'kecil' },
|
||||
{ label: 'Sedang', value: 'sedang' },
|
||||
@@ -437,14 +435,14 @@ export const SurgeryTypeOptList: { label: string; value: SurgeryType }[] = [
|
||||
{ label: 'Khusus', value: 'khusus' },
|
||||
]
|
||||
|
||||
export type BillingCodeType = "general" | "regional" | "local"
|
||||
export type BillingCodeType = 'general' | 'regional' | 'local'
|
||||
export const BillingCodeTypeOptList: { label: string; value: BillingCodeType }[] = [
|
||||
{ label: 'General', value: 'general' },
|
||||
{ label: 'Regional', value: 'regional' },
|
||||
{ label: 'Local', value: 'local' },
|
||||
]
|
||||
|
||||
export type SurgerySystemType = "cito" | "urgent" | "efektif" | "khusus"
|
||||
export type SurgerySystemType = 'cito' | 'urgent' | 'efektif' | 'khusus'
|
||||
export const SurgerySystemTypeOptList: { label: string; value: SurgerySystemType }[] = [
|
||||
{ label: 'Cito', value: 'cito' },
|
||||
{ label: 'Urgent', value: 'urgent' },
|
||||
@@ -452,7 +450,7 @@ export const SurgerySystemTypeOptList: { label: string; value: SurgerySystemType
|
||||
{ label: 'Khusus', value: 'khusus' },
|
||||
]
|
||||
|
||||
export type DissectionType = "bersih" | "bersih terkontaminasi" | "terkontaminasi kotor" | "kotor"
|
||||
export type DissectionType = 'bersih' | 'bersih terkontaminasi' | 'terkontaminasi kotor' | 'kotor'
|
||||
export const DissectionTypeOptList: { label: string; value: DissectionType }[] = [
|
||||
{ label: 'Bersih', value: 'bersih' },
|
||||
{ label: 'Bersih terkontaminasi', value: 'bersih terkontaminasi' },
|
||||
@@ -460,19 +458,25 @@ export const DissectionTypeOptList: { label: string; value: DissectionType }[] =
|
||||
{ label: 'Kotor', value: 'kotor' },
|
||||
]
|
||||
|
||||
export type SurgeryOrderType = "satu" | "ulangan"
|
||||
export type SurgeryOrderType = 'satu' | 'ulangan'
|
||||
export const SurgeryOrderTypeOptList: { label: string; value: SurgeryOrderType }[] = [
|
||||
{ label: 'Satu', value: 'satu' },
|
||||
{ label: 'Ulangan', value: 'ulangan' },
|
||||
]
|
||||
|
||||
export type BirthDescriptionType = "lahir hidup" | "lahir mati"
|
||||
export type BirthDescriptionType = 'lahir hidup' | 'lahir mati'
|
||||
export const BirthDescriptionTypeOptList: { label: string; value: BirthDescriptionType }[] = [
|
||||
{ label: 'Lahir Hidup', value: 'lahir hidup' },
|
||||
{ label: 'Lahir Mati', value: 'lahir mati' },
|
||||
]
|
||||
|
||||
export type BirthPlaceDescriptionType = "rssa" | "bidan luar" | "dokter luar" | "dukun bayi" | "puskesmas" | "paramedis luar"
|
||||
export type BirthPlaceDescriptionType =
|
||||
| 'rssa'
|
||||
| 'bidan luar'
|
||||
| 'dokter luar'
|
||||
| 'dukun bayi'
|
||||
| 'puskesmas'
|
||||
| 'paramedis luar'
|
||||
export const BirthPlaceDescriptionTypeOptList: { label: string; value: BirthPlaceDescriptionType }[] = [
|
||||
{ label: 'RSSA', value: 'rssa' },
|
||||
{ label: 'Bidan luar', value: 'bidan luar' },
|
||||
@@ -482,7 +486,7 @@ export const BirthPlaceDescriptionTypeOptList: { label: string; value: BirthPlac
|
||||
{ label: 'Paramedis luar', value: 'paramedis luar' },
|
||||
]
|
||||
|
||||
export type SpecimenType = "pa" | "mikrobiologi" | "laborat" | "tidak perlu"
|
||||
export type SpecimenType = 'pa' | 'mikrobiologi' | 'laborat' | 'tidak perlu'
|
||||
export const SpecimenTypeOptList: { label: string; value: SpecimenType }[] = [
|
||||
{ label: 'PA', value: 'pa' },
|
||||
{ label: 'Mikrobiologi', value: 'mikrobiologi' },
|
||||
@@ -490,7 +494,15 @@ export const SpecimenTypeOptList: { label: string; value: SpecimenType }[] = [
|
||||
{ label: 'Tidak perlu', value: 'tidak perlu' },
|
||||
]
|
||||
|
||||
export type PrbProgramType = "ashma" | "diabetes mellitus" | "hipertensi" | "penyakit jantung" | "ppok" | "schizopherenia" | "stroke" | "systemic lupus erythematosus"
|
||||
export type PrbProgramType =
|
||||
| 'ashma'
|
||||
| 'diabetes mellitus'
|
||||
| 'hipertensi'
|
||||
| 'penyakit jantung'
|
||||
| 'ppok'
|
||||
| 'schizopherenia'
|
||||
| 'stroke'
|
||||
| 'systemic lupus erythematosus'
|
||||
export const PrbProgramTypeOptList: { label: string; value: PrbProgramType }[] = [
|
||||
{ label: 'ASHMA', value: 'ashma' },
|
||||
{ label: 'Diabetes Mellitus', value: 'diabetes mellitus' },
|
||||
@@ -500,4 +512,14 @@ export const PrbProgramTypeOptList: { label: string; value: PrbProgramType }[] =
|
||||
{ label: 'Schizopherenia', value: 'schizopherenia' },
|
||||
{ label: 'Stroke', value: 'stroke' },
|
||||
{ label: 'Systemic Lupus Erythematosus', value: 'systemic lupus erythematosus' },
|
||||
]
|
||||
]
|
||||
export const disabilityCodes: Record<string, string> = {
|
||||
daksa: 'Tuna Daksa',
|
||||
netra: 'Tuna Netra',
|
||||
rungu: 'Tuna Rungu',
|
||||
wicara: 'Tuna Wicara',
|
||||
rungu_wicara: 'Tuna Rungu-Wicara',
|
||||
grahita: 'Tuna Grahita',
|
||||
laras: 'Tuna Laras',
|
||||
other: 'Lainnya',
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RoleAccess } from '~/models/role'
|
||||
import type { RoleAccesses } from '~/models/role'
|
||||
|
||||
export const PAGE_PERMISSIONS = {
|
||||
'/client/patient': {
|
||||
@@ -63,4 +63,4 @@ export const PAGE_PERMISSIONS = {
|
||||
'emp|pay': ['R'],
|
||||
'emp|mng': ['R'],
|
||||
},
|
||||
} as const satisfies Record<string, RoleAccess>
|
||||
} as const satisfies Record<string, RoleAccesses>
|
||||
|
||||
+1
-3
@@ -19,9 +19,7 @@ export interface TreeItem {
|
||||
|
||||
export function genBase(): Base {
|
||||
return {
|
||||
// -1 buat mock data
|
||||
// backend harusnya non-negative/ > 0 (untuk auto increment constraint) jadi harusnya aman ya
|
||||
id: -1,
|
||||
id: 0,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
}
|
||||
|
||||
+37
-32
@@ -1,12 +1,11 @@
|
||||
import { type Base, genBase } from './_base'
|
||||
import { type Person, genPerson } from './person'
|
||||
|
||||
import type { PatientFormData } from '~/schemas/patient.schema'
|
||||
import type { PersonAddressFormData } from '~/schemas/person-address.schema'
|
||||
import type { PersonAddressRelativeFormData } from '~/schemas/person-address-relative.schema'
|
||||
import type { PersonFamiliesFormData } from '~/schemas/person-family.schema'
|
||||
import type { PersonContactFormData } from '~/schemas/person-contact.schema'
|
||||
import type { PersonRelativeFormData } from '~/schemas/person-relative.schema'
|
||||
|
||||
import { genPerson, type Person } from './person'
|
||||
import type { PersonAddress } from './person-address'
|
||||
import type { PersonContact } from './person-contact'
|
||||
import type { PersonRelative } from './person-relative'
|
||||
@@ -14,11 +13,11 @@ import type { PersonRelative } from './person-relative'
|
||||
import { contactTypeMapping } from '~/lib/constants'
|
||||
|
||||
export interface PatientBase extends Base {
|
||||
person_id?: number
|
||||
newBornStatus?: boolean
|
||||
person_id?: number | null
|
||||
newBornStatus?: boolean | string
|
||||
registeredAt?: Date | string | null
|
||||
status_code?: string
|
||||
number?: string
|
||||
status_code?: string | null
|
||||
number?: string | null
|
||||
}
|
||||
|
||||
export interface PatientEntity extends PatientBase {
|
||||
@@ -32,15 +31,16 @@ export interface genPatientProps {
|
||||
patient: PatientFormData
|
||||
residentAddress: PersonAddressFormData
|
||||
cardAddress: PersonAddressRelativeFormData
|
||||
familyData: PersonFamiliesFormData
|
||||
familyData: PersonRelativeFormData
|
||||
contacts: PersonContactFormData
|
||||
responsible: PersonRelativeFormData
|
||||
}
|
||||
|
||||
export function genPatientEntity(props: genPatientProps): PatientEntity {
|
||||
export function genPatientEntity(props: genPatientProps, patientData: PatientEntity | null): PatientEntity {
|
||||
const { patient, residentAddress, cardAddress, familyData, contacts, responsible } = props
|
||||
|
||||
const addresses: PersonAddress[] = [{ ...genBase(), person_id: 0, ...residentAddress }]
|
||||
// const val = toRaw(patientData)
|
||||
const addresses: PersonAddress[] = [{ ...genBase(), person_id: patientData?.person?.id || 0, ...residentAddress }]
|
||||
const familiesContact: PersonRelative[] = []
|
||||
const personContacts: PersonContact[] = []
|
||||
|
||||
@@ -49,15 +49,17 @@ export function genPatientEntity(props: genPatientProps): PatientEntity {
|
||||
addresses.push({
|
||||
...genBase(),
|
||||
...residentAddress,
|
||||
id: cardAddress.id || 0,
|
||||
person_id: 0,
|
||||
locationType_code: cardAddress.locationType_code || 'identity'
|
||||
locationType_code: cardAddress.locationType_code || 'identity',
|
||||
})
|
||||
} else {
|
||||
// jika alamat berbeda, tambahkan alamat relatif
|
||||
// Pastikan semua field yang diperlukan ada
|
||||
const relativeAddress = {
|
||||
...genBase(),
|
||||
person_id: 0,
|
||||
id: cardAddress.id || 0,
|
||||
person_id: patientData?.person?.id || 0,
|
||||
locationType_code: cardAddress.locationType_code || 'identity',
|
||||
address: cardAddress.address || '',
|
||||
province_code: cardAddress.province_code || '',
|
||||
@@ -72,15 +74,15 @@ export function genPatientEntity(props: genPatientProps): PatientEntity {
|
||||
}
|
||||
|
||||
// add data orang tua
|
||||
if (familyData.shareFamilyData === '1') {
|
||||
if (familyData._shareFamilyData === 'yes' && familyData.families && familyData.families.length > 0) {
|
||||
for (const family of familyData.families) {
|
||||
familiesContact.push({
|
||||
id: 0,
|
||||
id: family.id || 0,
|
||||
relationship_code: family.relation,
|
||||
name: family.name,
|
||||
education_code: family.education,
|
||||
occupation_name: family.occupation,
|
||||
occupation_code: family.occupation,
|
||||
education_code: family.education_code,
|
||||
occupation_name: family.occupation_name,
|
||||
occupation_code: family.occupation_code,
|
||||
responsible: false,
|
||||
})
|
||||
}
|
||||
@@ -94,7 +96,8 @@ export function genPatientEntity(props: genPatientProps): PatientEntity {
|
||||
|
||||
personContacts.push({
|
||||
...genBase(),
|
||||
person_id: 0,
|
||||
id: contact.id || 0,
|
||||
person_id: patientData?.person?.id || 0,
|
||||
type_code: mappedContactType || '',
|
||||
value: contact.contactNumber,
|
||||
})
|
||||
@@ -102,10 +105,10 @@ export function genPatientEntity(props: genPatientProps): PatientEntity {
|
||||
}
|
||||
|
||||
// add penanggung jawab
|
||||
if (responsible) {
|
||||
if (responsible.contacts && responsible.contacts.length > 0) {
|
||||
for (const contact of responsible.contacts) {
|
||||
familiesContact.push({
|
||||
id: 0,
|
||||
id: contact.id || 0,
|
||||
relationship_code: contact.relation,
|
||||
name: contact.name,
|
||||
address: contact.address,
|
||||
@@ -117,15 +120,15 @@ export function genPatientEntity(props: genPatientProps): PatientEntity {
|
||||
|
||||
return {
|
||||
person: {
|
||||
id: 0,
|
||||
id: patientData?.person?.id || 0,
|
||||
name: patient.fullName,
|
||||
// alias: patient.alias,
|
||||
birthDate: patient.birthDate,
|
||||
birthRegency_code: patient.birthPlace,
|
||||
gender_code: patient.gender,
|
||||
residentIdentityNumber: patient.identityNumber,
|
||||
passportNumber: patient.passportNumber,
|
||||
drivingLicenseNumber: patient.drivingLicenseNumber,
|
||||
residentIdentityNumber: patient.identityNumber || null,
|
||||
passportNumber: patient.passportNumber || null,
|
||||
drivingLicenseNumber: patient.drivingLicenseNumber || null,
|
||||
religion_code: patient.religion,
|
||||
education_code: patient.education,
|
||||
occupation_code: patient.job,
|
||||
@@ -139,6 +142,8 @@ export function genPatientEntity(props: genPatientProps): PatientEntity {
|
||||
// passportFileUrl: patient.passportFileUrl,
|
||||
// drivingLicenseFileUrl: patient.drivingLicenseFileUrl,
|
||||
// familyIdentityFileUrl: patient.familyIdentityFileUrl,
|
||||
maritalStatus_code: patient.maritalStatus,
|
||||
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
@@ -149,9 +154,9 @@ export function genPatientEntity(props: genPatientProps): PatientEntity {
|
||||
registeredAt: new Date(),
|
||||
status_code: 'active',
|
||||
newBornStatus: patient.isNewBorn,
|
||||
person_id: 0,
|
||||
id: 0,
|
||||
number: '',
|
||||
person_id: patientData?.person?.id || null,
|
||||
id: patientData?.id || 0,
|
||||
number: patientData?.number || null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
@@ -160,12 +165,12 @@ export function genPatientEntity(props: genPatientProps): PatientEntity {
|
||||
|
||||
// New model
|
||||
export interface Patient extends Base {
|
||||
person_id?: number
|
||||
person_id?: number | null
|
||||
person: Person
|
||||
newBornStatus?: boolean
|
||||
newBornStatus?: boolean | string
|
||||
registeredAt?: Date | string | null
|
||||
status_code?: string
|
||||
number?: string
|
||||
status_code?: string | null
|
||||
number?: string | null
|
||||
}
|
||||
|
||||
export function genPatient(): Patient {
|
||||
@@ -178,4 +183,4 @@ export function genPatient(): Patient {
|
||||
newBornStatus: false,
|
||||
person: genPerson(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+67
-23
@@ -1,3 +1,5 @@
|
||||
import { differenceInDays, differenceInMonths, differenceInYears, parseISO } from 'date-fns'
|
||||
|
||||
import { type Base, genBase } from './_base'
|
||||
import type { PersonAddress } from './person-address'
|
||||
import type { PersonContact } from './person-contact'
|
||||
@@ -9,27 +11,28 @@ import type { Regency } from './regency'
|
||||
export interface Person extends Base {
|
||||
name: string
|
||||
// alias?: string
|
||||
frontTitle?: string
|
||||
endTitle?: string
|
||||
frontTitle?: string | null
|
||||
endTitle?: string | null
|
||||
birthDate?: string
|
||||
birthRegency_code?: string
|
||||
gender_code?: string
|
||||
residentIdentityNumber?: string
|
||||
passportNumber?: string
|
||||
drivingLicenseNumber?: string
|
||||
religion_code?: string
|
||||
education_code?: string
|
||||
occupation_code?: string
|
||||
occupation_name?: string
|
||||
ethnic_code?: string
|
||||
language_code?: string
|
||||
nationality?: string
|
||||
communicationIssueStatus?: boolean
|
||||
disability?: string
|
||||
residentIdentityFileUrl?: string
|
||||
passportFileUrl?: string
|
||||
drivingLicenseFileUrl?: string
|
||||
familyIdentityFileUrl?: string
|
||||
birthRegency_code?: string | null
|
||||
gender_code?: string | null
|
||||
residentIdentityNumber?: string | null
|
||||
passportNumber?: string | null
|
||||
drivingLicenseNumber?: string | null
|
||||
religion_code?: string | null
|
||||
education_code?: string | null
|
||||
occupation_code?: string | null
|
||||
occupation_name?: string | null
|
||||
ethnic_code?: string | null
|
||||
language_code?: string | null
|
||||
nationality?: string | null
|
||||
communicationIssueStatus?: boolean | string
|
||||
disability?: string | null
|
||||
residentIdentityFileUrl?: string | null
|
||||
passportFileUrl?: string | null
|
||||
drivingLicenseFileUrl?: string | null
|
||||
familyIdentityFileUrl?: string | null
|
||||
maritalStatus_code?: string
|
||||
|
||||
// preload data for detail patient
|
||||
birthRegency?: Regency | null
|
||||
@@ -43,9 +46,9 @@ export interface Person extends Base {
|
||||
export function genPerson(): Person {
|
||||
return {
|
||||
...genBase(),
|
||||
frontTitle: '[MOCK] dr. ',
|
||||
name: 'Agus Iwan Setiawan',
|
||||
endTitle: 'Sp.Bo',
|
||||
frontTitle: '',
|
||||
name: '',
|
||||
endTitle: '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,3 +58,44 @@ export function parseName(person: Person): string {
|
||||
|
||||
return fullName
|
||||
}
|
||||
|
||||
export function calculateAge(birthDate: string | Date | undefined): string {
|
||||
if (!birthDate) {
|
||||
return 'Masukkan tanggal lahir'
|
||||
}
|
||||
|
||||
try {
|
||||
let dateObj: Date
|
||||
|
||||
if (typeof birthDate === 'string') {
|
||||
dateObj = parseISO(birthDate)
|
||||
} else {
|
||||
dateObj = birthDate
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
|
||||
// Calculate years, months, and days
|
||||
const totalYears = differenceInYears(today, dateObj)
|
||||
|
||||
// Calculate remaining months after years
|
||||
const yearsPassed = new Date(dateObj)
|
||||
yearsPassed.setFullYear(yearsPassed.getFullYear() + totalYears)
|
||||
const remainingMonths = differenceInMonths(today, yearsPassed)
|
||||
|
||||
// Calculate remaining days after years and months
|
||||
const monthsPassed = new Date(yearsPassed)
|
||||
monthsPassed.setMonth(monthsPassed.getMonth() + remainingMonths)
|
||||
const remainingDays = differenceInDays(today, monthsPassed)
|
||||
|
||||
// Format the result
|
||||
const parts = []
|
||||
if (totalYears > 0) parts.push(`${totalYears} Tahun`)
|
||||
if (remainingMonths > 0) parts.push(`${remainingMonths} Bulan`)
|
||||
if (remainingDays > 0) parts.push(`${remainingDays} Hari`)
|
||||
|
||||
return parts.length > 0 ? parts.join(' ') : '0 Hari'
|
||||
} catch {
|
||||
return 'Masukkan tanggal lahir'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
// import type { PagePermission } from '~/models/role'
|
||||
import type { RoleAccesses } from '~/models/role'
|
||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||
// import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||
|
||||
definePageMeta({
|
||||
// middleware: ['rbac'],
|
||||
middleware: ['rbac'],
|
||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||
title: 'Daftar Divisi',
|
||||
title: 'Edit Pasien',
|
||||
contentFrame: 'cf-full-width',
|
||||
})
|
||||
|
||||
@@ -16,24 +16,28 @@ useHead({
|
||||
title: () => route.meta.title as string,
|
||||
})
|
||||
|
||||
// const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||
const roleAccess: RoleAccesses = PAGE_PERMISSIONS['/client/patient']
|
||||
|
||||
// const { checkRole, hasReadAccess } = useRBAC()
|
||||
const { checkRole, hasReadAccess } = useRBAC()
|
||||
|
||||
// // Check if user has access to this page
|
||||
// const hasAccess = checkRole(roleAccess)
|
||||
// if (!hasAccess) {
|
||||
// navigateTo('/403')
|
||||
// }
|
||||
// Check if user has access to this page
|
||||
const hasAccess = checkRole(roleAccess)
|
||||
if (!hasAccess) {
|
||||
navigateTo('/403')
|
||||
}
|
||||
|
||||
// Define permission-based computed properties
|
||||
// const canRead = hasReadAccess(roleAccess)
|
||||
const canRead = true
|
||||
const canRead = hasReadAccess(roleAccess)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="canRead">Edit patient id: {{ route.params.id }}</div>
|
||||
<div v-if="canRead">
|
||||
<ContentPatientForm
|
||||
:mode="'edit'"
|
||||
:patient-id="route.params.id as string"
|
||||
/>
|
||||
</div>
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { PagePermission } from '~/models/role'
|
||||
import type { RoleAccesses } from '~/models/role'
|
||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||
|
||||
@@ -16,7 +16,7 @@ useHead({
|
||||
title: () => route.meta.title as string,
|
||||
})
|
||||
|
||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||
const roleAccess: RoleAccesses = PAGE_PERMISSIONS['/client/patient']
|
||||
|
||||
const { checkRole, hasReadAccess } = useRBAC()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { PagePermission } from '~/models/role'
|
||||
import type { RoleAccesses } from '~/models/role'
|
||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||
import { permissions } from '~/const/page-permission/client'
|
||||
|
||||
@@ -16,7 +16,7 @@ useHead({
|
||||
title: () => route.meta.title as string,
|
||||
})
|
||||
|
||||
const roleAccess: PagePermission = permissions['/client/patient/add']
|
||||
const roleAccess: RoleAccesses = permissions['/client/patient/add'] ?? {}
|
||||
|
||||
const { checkRole, hasReadAccess } = useRBAC()
|
||||
|
||||
@@ -34,7 +34,10 @@ const callbackUrl = route.query['return-path'] as string | undefined
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="canRead">
|
||||
<ContentPatientEntry :callback-url="callbackUrl" />
|
||||
<ContentPatientForm
|
||||
:mode="'add'"
|
||||
:callback-url="callbackUrl"
|
||||
/>
|
||||
</div>
|
||||
<Error
|
||||
v-else
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { PagePermission } from '~/models/role'
|
||||
import type { RoleAccesses } from '~/models/role'
|
||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||
|
||||
@@ -16,8 +16,8 @@ useHead({
|
||||
title: () => route.meta.title as string,
|
||||
})
|
||||
|
||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||
|
||||
const roleAccess: RoleAccesses = PAGE_PERMISSIONS['/client/patient']
|
||||
console.log(roleAccess)
|
||||
const { checkRole, hasReadAccess } = useRBAC()
|
||||
|
||||
// Check if user has access to this page
|
||||
|
||||
@@ -1,28 +1,11 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const CommunicationBarrierSchema = z
|
||||
.enum(['YA', 'TIDAK'], {
|
||||
required_error: 'Mohon lengkapi Status Hambatan Berkomunikasi',
|
||||
})
|
||||
.transform((val) => val === 'YA')
|
||||
|
||||
const IsNewBornSchema = z
|
||||
.enum(['YA', 'TIDAK'], {
|
||||
required_error: 'Mohon lengkapi status pasien',
|
||||
})
|
||||
.transform((val) => val === 'YA')
|
||||
|
||||
const ACCEPTED_UPLOAD_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
|
||||
|
||||
const PatientSchema = z
|
||||
.object({
|
||||
// Data Diri Pasien
|
||||
identityNumber: z.string().optional(),
|
||||
// .string({
|
||||
// required_error: 'Mohon lengkapi NIK',
|
||||
// })
|
||||
// .min(16, 'NIK harus berupa angka 16 digit')
|
||||
// .regex(/^\d+$/, 'NIK harus berupa angka 16 digit'),
|
||||
residentIdentityFile: z
|
||||
.any()
|
||||
.optional()
|
||||
@@ -40,12 +23,7 @@ const PatientSchema = z
|
||||
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({
|
||||
// required_error: 'Mohon pilih sapaan',
|
||||
// }),
|
||||
fullName: z.string({
|
||||
required_error: 'Mohon lengkapi Nama',
|
||||
}),
|
||||
@@ -54,35 +32,15 @@ const PatientSchema = z
|
||||
required_error: 'Mohon lengkapi Tempat Lahir',
|
||||
})
|
||||
.min(1, 'Mohon lengkapi Tempat Lahir'),
|
||||
birthDate: z
|
||||
.string({
|
||||
required_error: 'Mohon lengkapi Tanggal Lahir',
|
||||
})
|
||||
.refine(
|
||||
(date) => {
|
||||
// Jika kosong, return false untuk required validation
|
||||
if (!date || date.trim() === '') return false
|
||||
|
||||
// Jika ada isi, validasi format tanggal
|
||||
try {
|
||||
const dateObj = new Date(date)
|
||||
// Cek apakah tanggal valid dan tahun >= 1900
|
||||
return !isNaN(dateObj.getTime()) && dateObj.getFullYear() >= 1900
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
message: 'Mohon lengkapi Tanggal Lahir dengan format yang valid',
|
||||
},
|
||||
)
|
||||
.transform((dateStr) => new Date(dateStr).toISOString()),
|
||||
birthDate: z.string({
|
||||
required_error: 'Mohon lengkapi Tanggal Lahir',
|
||||
}),
|
||||
|
||||
// Jenis Kelamin & Status
|
||||
gender: z.string({
|
||||
required_error: 'Pilih Jenis Kelamin',
|
||||
}),
|
||||
maritalStatus: z.enum(['TIDAK_DIKETAHUI', 'BELUM_KAWIN', 'KAWIN', 'CERAI_HIDUP', 'CERAI_MATI'], {
|
||||
maritalStatus: z.enum(['S', 'M', 'D', 'W'], {
|
||||
required_error: 'Pilih Status Perkawinan',
|
||||
}),
|
||||
|
||||
@@ -95,7 +53,9 @@ const PatientSchema = z
|
||||
nationality: z.string({
|
||||
required_error: 'Pilih Kebangsaan',
|
||||
}),
|
||||
isNewBorn: IsNewBornSchema,
|
||||
isNewBorn: z.union([z.boolean(), z.string()], {
|
||||
required_error: 'Mohon lengkapi Status Disabilitas',
|
||||
}),
|
||||
language: z.string({
|
||||
required_error: 'Mohon pilih Preferensi Bahasa',
|
||||
}),
|
||||
@@ -112,65 +72,53 @@ const PatientSchema = z
|
||||
ethnicity: z.string().optional(),
|
||||
|
||||
// Disabilitas
|
||||
disability: z.enum(['YA', 'TIDAK'], {
|
||||
disability: z.string({
|
||||
required_error: 'Mohon lengkapi Status Disabilitas',
|
||||
}),
|
||||
disabilityType: z.string().optional(),
|
||||
|
||||
// Informasi Kontak
|
||||
passportNumber: z.string().optional(),
|
||||
communicationBarrier: CommunicationBarrierSchema,
|
||||
communicationBarrier: z.union([z.boolean(), z.string()], {
|
||||
required_error: 'Mohon lengkapi Status Hambatan Berkomunikasi',
|
||||
}),
|
||||
|
||||
note: z.string().optional(),
|
||||
drivingLicenseNumber: z.string().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// Jika disability = 'TIDAK', maka disabilityType harus kosong atau undefined
|
||||
if (data.disability === 'TIDAK') {
|
||||
return !data.disabilityType || data.disabilityType.trim() === ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: "Jenis Disabilitas harus kosong jika Status Disabilitas = 'TIDAK'",
|
||||
path: ['disabilityType'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// Jika disability = 'YA', maka disabilityType wajib diisi
|
||||
if (data.disability === 'YA') {
|
||||
if (data.disability === 'yes') {
|
||||
return !!data.disabilityType?.trim()
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: 'Mohon pilih Jenis Disabilitas',
|
||||
message: 'Mohon pilih jenis Disabilitas',
|
||||
path: ['disabilityType'],
|
||||
},
|
||||
)
|
||||
// .refine((data) => {
|
||||
// // Jika nationality = 'WNA', maka passportNumber wajib diisi
|
||||
// if (data.nationality === 'WNA') {
|
||||
// return !!data.passportNumber?.trim()
|
||||
// }
|
||||
// return true
|
||||
// }, {
|
||||
// message: 'Nomor Paspor wajib diisi untuk Warga Negara Asing',
|
||||
// path: ['passportNumber'],
|
||||
// })
|
||||
.transform((data) => {
|
||||
// Transform untuk backend: hanya kirim disabilityType sesuai kondisi
|
||||
return {
|
||||
...data,
|
||||
// Jika disability = 'YA', kirim disabilityType
|
||||
// Jika disability = 'TIDAK', kirim null untuk disabilityType
|
||||
disabilityType: data.disability === 'YA' ? data.disabilityType : null,
|
||||
// Hapus field disability karena yang dikirim ke backend adalah disabilityType
|
||||
disability: undefined,
|
||||
}
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.nationality === 'WNI') {
|
||||
const nik = data.identityNumber?.trim()
|
||||
return !!nik && nik.length === 16 && /^\d+$/.test(nik)
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: 'NIK harus berupa angka 16 digit',
|
||||
path: ['identityNumber'],
|
||||
},
|
||||
)
|
||||
// .transform((data) => {
|
||||
// return {
|
||||
// ...data,
|
||||
// // newBornStatus: data.isNewBorn === 'yes',
|
||||
// // communicationBarrier: data.communicationBarrier === 'yes',
|
||||
// // disability: data.disability ? data.disabilityType : null,
|
||||
// }
|
||||
// })
|
||||
|
||||
type PatientFormData = z.infer<typeof PatientSchema>
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const PersonAddressSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
|
||||
locationType_code: z.string({
|
||||
required_error: 'Mohon pilih jenis alamat',
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const PersonContactBaseSchema = z.object({
|
||||
contactType: z.string().min(1, 'Mohon pilih tipe kontak'),
|
||||
contactNumber: z.string().min(8, 'Nomor minimal 8 digit'),
|
||||
id: z.number().optional(),
|
||||
|
||||
contactType: z.string({ required_error: 'Mohon pilih tipe kontak' }).min(1, 'Mohon pilih tipe kontak'),
|
||||
contactNumber: z.string({ required_error: 'Nomor kontak harus diisi' }).min(8, 'Nomor minimal 8 digit'),
|
||||
})
|
||||
|
||||
const PersonContactListSchema = z.object({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const PersonFamilySchema = z.object({
|
||||
id: z.number().optional(),
|
||||
relation: z.enum(['mother', 'father', 'guardian', 'emergency_contact']),
|
||||
name: z
|
||||
.string({
|
||||
@@ -26,8 +27,19 @@ const PersonFamiliesSchema = z.discriminatedUnion('shareFamilyData', [
|
||||
}),
|
||||
])
|
||||
|
||||
interface PersonFamiliesFormData {
|
||||
shareFamilyData: '0' | '1'
|
||||
families: {
|
||||
id?: number
|
||||
relation: 'mother' | 'father' | 'guardian' | 'emergency_contact'
|
||||
name: string
|
||||
education: string
|
||||
occupation?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
type PersonFamilyFormData = z.infer<typeof PersonFamilySchema>
|
||||
type PersonFamiliesFormData = z.infer<typeof PersonFamiliesSchema>
|
||||
// type PersonFamiliesFormData = z.infer<typeof PersonFamiliesSchema>
|
||||
|
||||
export { PersonFamilySchema, PersonFamiliesSchema }
|
||||
export type { PersonFamilyFormData, PersonFamiliesFormData }
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const ResponsibleContactPersonSchema = z.object({
|
||||
const PersonRelativeSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
|
||||
relation: z.string({ required_error: 'Pilih jenis Penanggung Jawab' }).min(1, 'Pilih jenis Penanggung Jawab'),
|
||||
name: z.string({ required_error: 'Mohon lengkapi Nama' }).min(3, 'Mohon lengkapi Nama'),
|
||||
address: z.string({ required_error: 'Mohon lengkapi Alamat' }).min(3, 'Mohon lengkapi Alamat'),
|
||||
name: z.string({ required_error: 'Mohon lengkapi Nama' }).optional(),
|
||||
address: z.string({ required_error: 'Mohon lengkapi Alamat' }).optional(),
|
||||
phone: z
|
||||
.string({ required_error: 'Mohon lengkapi Nomor HP' })
|
||||
.min(8, 'Nomor HP minimal 10 digit')
|
||||
.max(15, 'Nomor HP maksimal 15 digit'),
|
||||
.max(15, 'Nomor HP maksimal 15 digit')
|
||||
.optional(),
|
||||
occupation_name: z.string().optional(),
|
||||
occupation_code: z.string().optional(),
|
||||
education_code: z.string().optional(),
|
||||
})
|
||||
|
||||
const ResponsiblePersonSchema = z.object({
|
||||
contacts: z.array(ResponsibleContactPersonSchema).min(1, 'Minimal harus ada 1 penanggung jawab'),
|
||||
const ResponsiblePersonRelativeSchema = z.object({
|
||||
contacts: z.array(PersonRelativeSchema).optional(),
|
||||
families: z.array(PersonRelativeSchema).optional(),
|
||||
_shareFamilyData: z.enum(['yes', 'no']).optional(),
|
||||
})
|
||||
|
||||
type PersonRelativeFormData = z.infer<typeof ResponsiblePersonSchema>
|
||||
type PersonRelativeFormData = z.infer<typeof ResponsiblePersonRelativeSchema>
|
||||
|
||||
export { ResponsiblePersonSchema }
|
||||
export { ResponsiblePersonRelativeSchema }
|
||||
export type { PersonRelativeFormData }
|
||||
|
||||
@@ -13,6 +13,11 @@ export async function getPatients(params: any = null) {
|
||||
searchParams.append(key, params[key])
|
||||
}
|
||||
}
|
||||
if (params.search) {
|
||||
url += `/search/${params.search}`
|
||||
searchParams.delete('search')
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString()
|
||||
if (queryString) url += `?${queryString}`
|
||||
}
|
||||
@@ -42,7 +47,8 @@ export async function getPatientDetail(id: number) {
|
||||
|
||||
export async function getPatientByIdentifier(search: string) {
|
||||
try {
|
||||
const urlPath = search.length === 16 ? `by-resident-identity/search=${encodeURIComponent(search)}` : `/search/${search}`
|
||||
const urlPath =
|
||||
search.length === 16 ? `by-resident-identity/search=${encodeURIComponent(search)}` : `/search/${search}`
|
||||
const url = `${mainUrl}/${urlPath}`
|
||||
const resp = await xfetch(url, 'GET')
|
||||
const result: any = {}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@unovis/ts": "^1.5.1",
|
||||
"@unovis/vue": "^1.5.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"embla-carousel": "^8.5.2",
|
||||
"embla-carousel-vue": "^8.5.2",
|
||||
"file-saver": "^2.0.5",
|
||||
|
||||
Generated
+6196
-7677
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user