feat(patient): address integration to backend apis

feat(patient): add newborn status field and validation

- Add radio button component for newborn status selection
- Update patient schema with newborn status validation
- Remove deprecated alias field from person model
- Refactor disability type handling in patient schema

fix(patient): correct address comparison logic and schema

Update the patient address comparison to use boolean instead of string '1' and modify the schema to transform the string value to boolean. This ensures consistent type usage throughout the application.

feat(models): add village and district model interfaces

Add new model interfaces for Village and District with their respective generator functions. These models will be used to handle administrative division data in the application.

feat(address): implement dynamic province selection with caching

- Add province service for CRUD operations
- Create useProvinces composable with caching and loading states
- Update select-province component to use dynamic data
- Export SelectItem interface for type consistency
- Improve combobox styling and accessibility

feat(address-form): implement dynamic regency selection with caching

- Add new regency service for CRUD operations
- Create useRegencies composable with caching and loading states
- Update SelectRegency component to use dynamic data based on province selection
- Improve placeholder and disabled state handling

feat(address-form): implement dynamic district selection

- Add district service for CRUD operations
- Create useDistricts composable with caching and loading states
- Update SelectDistrict component to use dynamic data
- Remove hardcoded district options and implement regency-based filtering

feat(address-form): implement dynamic village selection with caching

- Add village service for CRUD operations
- Create useVillages composable with caching and loading states
- Update SelectVillage component to fetch villages based on district
- Remove hardcoded village options in favor of API-driven data

feat(address-form): improve address selection with debouncing and request deduplication

- Add debouncing to prevent rapid API calls when selecting addresses
- Implement request deduplication to avoid duplicate API calls
- Add delayed form reset to ensure proper composable cleanup
- Add isUserAction flag to force refresh when user changes selection
This commit is contained in:
Khafid Prayoga
2025-10-08 12:22:31 +07:00
parent 3eb9dde21d
commit 55239606af
27 changed files with 1153 additions and 200 deletions
@@ -3,15 +3,12 @@ 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 { Input } from '~/components/pub/ui/input'
import { cn } from '~/lib/utils'
defineProps<{
fieldNameAlias: string
fieldNameInput: string
placeholder: string
labelForAlias: string
labelForInput: string
errors?: FormErrors
class?: string
@@ -21,54 +18,9 @@ defineProps<{
maxLength?: number
isRequired?: boolean
}>()
const aliasOptions = [
{ label: 'An', value: 'an' },
{ label: 'By.Ny', value: 'byny' },
{ label: 'Nn', value: 'nn' },
{ label: 'Ny', value: 'ny' },
{ label: 'Tn', value: 'tn' },
]
</script>
<template>
<FieldGroup>
<Label
:label-for="fieldNameAlias"
:is-required="isRequired"
>
{{ labelForAlias }}
</Label>
<Field
:id="fieldNameAlias"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
:name="fieldNameAlias"
>
<FormItem>
<FormControl>
<Select
:id="fieldNameAlias"
:preserve-order="false"
v-bind="componentField"
:auto-width="true"
:items="aliasOptions"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label
:label-for="fieldNameInput"
@@ -0,0 +1,93 @@
<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 { Label as RadioLabel } from '~/components/pub/ui/label'
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
import { cn } from '~/lib/utils'
const props = defineProps<{
fieldName?: string
label?: string
errors?: FormErrors
class?: string
radioGroupClass?: string
radioItemClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'isNewBorn',
label = 'Status Pasien',
errors,
class: containerClass,
radioGroupClass,
radioItemClass,
labelClass,
} = props
const newbornOptions = [
{ label: 'Ya', value: 'YA' },
{ label: 'Tidak', value: 'TIDAK' },
]
</script>
<template>
<FieldGroup :class="cn('radio-group-field', containerClass)">
<Label
:label-for="fieldName"
:is-required="isRequired"
>
{{ label }}
</Label>
<Field
:id="fieldName"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<RadioGroup
v-bind="componentField"
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
>
<div
v-for="(option, index) in newbornOptions"
:key="option.value"
:class="cn('flex min-w-fit items-center space-x-2', radioItemClass)"
>
<RadioGroupItem
: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',
containerClass,
)
"
/>
<RadioLabel
:for="`${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',
labelClass,
)
"
>
{{ option.label }}
</RadioLabel>
</div>
</RadioGroup>
</FormControl>
<FormMessage class="ml-0 mt-1" />
</FormItem>
</FormField>
</Field>
</FieldGroup>
</template>
+11 -5
View File
@@ -4,11 +4,12 @@ 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 InputFile from './_common/input-file.vue'
import InputPatientName from './_common/input-patient-name.vue'
import InputName from './_common/input-name.vue'
import RadioCommunicationBarrier from './_common/radio-communication-barrier.vue'
import RadioDisability from './_common/radio-disability.vue'
import RadioGender from './_common/radio-gender.vue'
import RadioNationality from './_common/radio-nationality.vue'
import RadioNewborn from './_common/radio-newborn.vue'
import SelectDisability from './_common/select-disability.vue'
import SelectDob from './_common/select-dob.vue'
import SelectEducation from './_common/select-education.vue'
@@ -48,16 +49,21 @@ defineExpose({
>
<div class="mb-3 border-b border-b-slate-300">
<p class="text-md mt-1 font-semibold">Data Diri Pasien</p>
<div class="grid grid-cols-1 md:grid-cols-[150px_1fr]">
<InputPatientName
field-name-alias="alias"
<div class="grid grid-cols-1 md:grid-cols-[1fr_1fr]">
<InputName
field-name-input="fullName"
label-for-alias="Alias"
label-for-input="Nama Lengkap"
placeholder="Masukkan nama lengkap pasien"
:errors="errors"
is-required
/>
<RadioNewborn
field-name="isNewBorn"
label="Pasien Bayi"
placeholder="Pilih status pasien"
:errors="errors"
is-required
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-3">
<InputBase
@@ -7,7 +7,8 @@ import Label from '~/components/pub/my-ui/form/label.vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
fieldName: string
fieldName?: string
regencyCode?: string
isDisabled?: boolean
placeholder?: string
errors?: FormErrors
@@ -17,13 +18,30 @@ const props = defineProps<{
isRequired?: boolean
}>()
const { placeholder = 'Pilih Kecamatan', errors, class: containerClass, selectClass, fieldGroupClass } = props
const {
fieldName = 'districtId',
placeholder = 'Pilih kecamatan',
errors,
class: containerClass,
fieldGroupClass,
} = props
const districtOptions = [
{ label: 'Kecamatan Lowokwaru', value: '18' },
{ label: 'Kecamatan Pakis', value: '33' },
{ label: 'Kecamatan Blimbing', value: '35' },
]
// Gunakan composable untuk mengelola data districts
const regencyCodeRef = toRef(props, 'regencyCode')
const { districtOptions, isLoading, error } = useDistricts(regencyCodeRef)
// Computed untuk menentukan placeholder berdasarkan state
const dynamicPlaceholder = computed(() => {
if (!props.regencyCode) return 'Pilih kabupaten/kota dahulu'
if (isLoading.value) return 'Memuat data kecamatan...'
if (error.value) return 'Gagal memuat data'
return placeholder
})
// Computed untuk menentukan apakah field disabled
const isFieldDisabled = computed(() => {
return props.isDisabled || !props.regencyCode || isLoading.value || !!error.value
})
</script>
<template>
@@ -49,9 +67,9 @@ const districtOptions = [
:id="fieldName"
v-bind="componentField"
:items="districtOptions"
:placeholder="placeholder"
:is-disabled="isDisabled"
search-placeholder="Cari..."
:placeholder="dynamicPlaceholder"
:is-disabled="isFieldDisabled"
search-placeholder="Cari kecamatan..."
empty-message="Kecamatan tidak ditemukan"
/>
</FormControl>
@@ -7,7 +7,7 @@ import Label from '~/components/pub/my-ui/form/label.vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
fieldName: string
fieldName?: string
isDisabled?: boolean
placeholder?: string
errors?: FormErrors
@@ -18,18 +18,27 @@ const props = defineProps<{
}>()
const {
fieldName = 'provinceId',
fieldName = 'provinceCode',
placeholder = 'Pilih provinsi',
errors,
class: containerClass,
fieldGroupClass,
} = props
const provinceList = [
{ label: 'Jawa Barat', value: '18' },
{ label: 'Jawa Tengah', value: '33' },
{ label: 'Jawa Timur', value: '35' },
]
// Gunakan composable untuk mengelola data provinces
const { provinceOptions, isLoading, error } = useProvinces()
// Computed untuk menentukan placeholder berdasarkan state
const dynamicPlaceholder = computed(() => {
if (isLoading.value) return 'Memuat data provinsi...'
if (error.value) return 'Gagal memuat data'
return placeholder
})
// Computed untuk menentukan apakah field disabled
const isFieldDisabled = computed(() => {
return props.isDisabled || isLoading.value || !!error.value
})
</script>
<template>
@@ -54,11 +63,11 @@ const provinceList = [
<Combobox
:id="fieldName"
v-bind="componentField"
:items="provinceList"
:placeholder="placeholder"
:is-disabled="isDisabled"
search-placeholder="Cari..."
empty-message="Kecamatan tidak ditemukan"
:items="provinceOptions"
:placeholder="dynamicPlaceholder"
:is-disabled="isFieldDisabled"
search-placeholder="Cari provinsi..."
empty-message="Provinsi tidak ditemukan"
/>
</FormControl>
<FormMessage />
@@ -7,7 +7,8 @@ import Label from '~/components/pub/my-ui/form/label.vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
fieldName: string
fieldName?: string
provinceCode?: string
isDisabled?: boolean
placeholder?: string
errors?: FormErrors
@@ -17,15 +18,30 @@ const props = defineProps<{
isRequired?: boolean
}>()
const { placeholder = 'Pilih kabupaten/kota', errors, class: containerClass, selectClass, fieldGroupClass } = props
const {
fieldName = 'regencyId',
placeholder = 'Pilih kabupaten/kota',
errors,
class: containerClass,
fieldGroupClass,
} = props
const regencyOptions = [
{ label: 'Kab. Sidoarjo', value: '32' },
{ label: 'Kab. Malang', value: '35' },
{ label: 'Kab. Mojokerto', value: '31' },
{ label: 'Kab. Lamongan', value: '30' },
{ label: 'Kota Malang', value: '18' },
]
// Gunakan composable untuk mengelola data regencies
const provinceCodeRef = toRef(props, 'provinceCode')
const { regencyOptions, isLoading, error } = useRegencies(provinceCodeRef)
// Computed untuk menentukan placeholder berdasarkan state
const dynamicPlaceholder = computed(() => {
if (!props.provinceCode) return 'Pilih provinsi dahulu'
if (isLoading.value) return 'Memuat data kabupaten/kota...'
if (error.value) return 'Gagal memuat data'
return placeholder
})
// Computed untuk menentukan apakah field disabled
const isFieldDisabled = computed(() => {
return props.isDisabled || !props.provinceCode || isLoading.value || !!error.value
})
</script>
<template>
@@ -51,9 +67,9 @@ const regencyOptions = [
:id="fieldName"
v-bind="componentField"
:items="regencyOptions"
:placeholder="placeholder"
:is-disabled="isDisabled"
search-placeholder="Cari..."
:placeholder="dynamicPlaceholder"
:is-disabled="isFieldDisabled"
search-placeholder="Cari kabupaten/kota..."
empty-message="Kabupaten/kota tidak ditemukan"
/>
</FormControl>
@@ -7,7 +7,8 @@ import Label from '~/components/pub/my-ui/form/label.vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
fieldName: string
fieldName?: string
districtCode?: string
isDisabled?: boolean
placeholder?: string
errors?: FormErrors
@@ -17,14 +18,30 @@ const props = defineProps<{
isRequired?: boolean
}>()
const { placeholder = 'Pilih Kelurahan', errors, class: containerClass, selectClass, fieldGroupClass } = props
const {
fieldName = 'villageId',
placeholder = 'Pilih kelurahan',
errors,
class: containerClass,
fieldGroupClass,
} = props
const villageOptions = [
{ label: 'Lowokwaru', value: '18' },
{ label: 'Dinoyo', value: '33' },
{ label: 'Blimbing', value: '35' },
{ label: 'Sawojajar', value: '36' },
]
// Gunakan composable untuk mengelola data villages
const districtCodeRef = toRef(props, 'districtCode')
const { villageOptions, isLoading, error } = useVillages(districtCodeRef)
// Computed untuk menentukan placeholder berdasarkan state
const dynamicPlaceholder = computed(() => {
if (!props.districtCode) return 'Pilih kecamatan dahulu'
if (isLoading.value) return 'Memuat data kelurahan...'
if (error.value) return 'Gagal memuat data'
return placeholder
})
// Computed untuk menentukan apakah field disabled
const isFieldDisabled = computed(() => {
return props.isDisabled || !props.districtCode || isLoading.value || !!error.value
})
</script>
<template>
@@ -50,9 +67,9 @@ const villageOptions = [
:id="fieldName"
v-bind="componentField"
:items="villageOptions"
:placeholder="placeholder"
:is-disabled="isDisabled"
search-placeholder="Cari..."
:placeholder="dynamicPlaceholder"
:is-disabled="isFieldDisabled"
search-placeholder="Cari kelurahan..."
empty-message="Kelurahan tidak ditemukan"
/>
</FormControl>
@@ -39,7 +39,7 @@ let isResetting = false
// Field dependency map for placeholder
const fieldStates: Record<string, { dependsOn?: string; placeholder: string }> = {
regencyId: { dependsOn: 'provinceId', placeholder: 'Pilih provinsi dahulu' },
regencyId: { dependsOn: 'provinceCode', placeholder: 'Pilih provinsi dahulu' },
districtId: { dependsOn: 'regencyId', placeholder: 'Pilih kabupaten/kota dahulu' },
villageId: { dependsOn: 'districtId', placeholder: 'Pilih kecamatan dahulu' },
zipCode: { dependsOn: 'villageId', placeholder: 'Pilih kelurahan dahulu' },
@@ -84,9 +84,9 @@ function getFieldState(field: string) {
// #region watch
// Watch provinceId changes
// Watch provinceCode changes
watch(
() => formRef.value?.values?.provinceId,
() => formRef.value?.values?.provinceCode,
(newValue, oldValue) => {
if (isResetting || !formRef.value || newValue === oldValue) return
@@ -196,7 +196,7 @@ watch(
const updatedValues = { ...currentValues }
// Convert empty strings to undefined untuk field yang sekarang required
if (updatedValues.provinceId === '') updatedValues.provinceId = undefined
if (updatedValues.provinceCode === '') updatedValues.provinceCode = undefined
if (updatedValues.regencyId === '') updatedValues.regencyId = undefined
if (updatedValues.districtId === '') updatedValues.districtId = undefined
if (updatedValues.villageId === '') updatedValues.villageId = undefined
@@ -217,7 +217,7 @@ watch(
if (oldValue === '0' && newValue === '1') {
nextTick(() => {
// Clear error messages untuk field yang tidak lagi required
formRef.value?.setFieldError('provinceId', undefined)
formRef.value?.setFieldError('provinceCode', undefined)
formRef.value?.setFieldError('regencyId', undefined)
formRef.value?.setFieldError('districtId', undefined)
formRef.value?.setFieldError('villageId', undefined)
@@ -287,7 +287,7 @@ watch(
<div class="flex-row gap-2 md:flex">
<div class="min-w-0 flex-1">
<SelectProvince
field-name="provinceId"
field-name="provinceCode"
placeholder="Pilih"
:is-disabled="values.isSameAddress === '1'"
:is-required="values.isSameAddress !== '1'"
@@ -297,8 +297,8 @@ watch(
<div class="min-w-0 flex-1">
<SelectRegency
field-name="regencyId"
:placeholder="getFieldState('regencyId').placeholder"
:is-disabled="getFieldState('regencyId').disabled || !values.provinceId"
:province-code="values.provinceCode"
:is-disabled="getFieldState('regencyId').disabled"
:is-required="values.isSameAddress !== '1'"
/>
</div>
@@ -308,16 +308,16 @@ watch(
<div class="min-w-0 flex-1">
<SelectDistrict
field-name="districtId"
:placeholder="getFieldState('districtId').placeholder"
:is-disabled="getFieldState('districtId').disabled || !values.regencyId"
:regency-code="values.regencyId"
:is-disabled="getFieldState('districtId').disabled"
:is-required="values.isSameAddress !== '1'"
/>
</div>
<div class="min-w-0 flex-1">
<SelectVillage
field-name="villageId"
:placeholder="getFieldState('villageId').placeholder"
:is-disabled="getFieldState('villageId').disabled || !values.districtId"
:district-code="values.districtId"
:is-disabled="getFieldState('villageId').disabled"
:is-required="values.isSameAddress !== '1'"
/>
</div>
@@ -35,29 +35,34 @@ defineExpose({
// Watchers untuk cascading reset
let isResetting = false
// #region Watch provinceId changes
// #region Watch provinceCode changes
watch(
() => formRef.value?.values?.provinceId,
() => formRef.value?.values?.provinceCode,
(newValue, oldValue) => {
if (isResetting || !formRef.value || newValue === oldValue) return
if (oldValue && newValue !== oldValue) {
isResetting = true
formRef.value.setValues(
{
regencyId: undefined,
districtId: undefined,
villageId: undefined,
zipCode: undefined,
},
false,
)
// Delay reset untuk memberikan waktu composable menyelesaikan request
setTimeout(() => {
if (formRef.value) {
formRef.value.setValues(
{
regencyId: undefined,
districtId: undefined,
villageId: undefined,
zipCode: undefined,
},
false,
)
}
nextTick(() => {
isResetting = false
})
nextTick(() => {
isResetting = false
})
}, 150) // Delay 150ms, lebih dari debounce composable (100ms)
}
},
)
@@ -71,18 +76,23 @@ watch(
if (oldValue && newValue !== oldValue) {
isResetting = true
formRef.value.setValues(
{
districtId: undefined,
villageId: undefined,
zipCode: undefined,
},
false,
)
// Delay reset untuk memberikan waktu composable menyelesaikan request
setTimeout(() => {
if (formRef.value) {
formRef.value.setValues(
{
districtId: undefined,
villageId: undefined,
zipCode: undefined,
},
false,
)
}
nextTick(() => {
isResetting = false
})
nextTick(() => {
isResetting = false
})
}, 150)
}
},
)
@@ -96,17 +106,22 @@ watch(
if (oldValue && newValue !== oldValue) {
isResetting = true
formRef.value.setValues(
{
villageId: undefined,
zipCode: undefined,
},
false,
)
// Delay reset untuk memberikan waktu composable menyelesaikan request
setTimeout(() => {
if (formRef.value) {
formRef.value.setValues(
{
villageId: undefined,
zipCode: undefined,
},
false,
)
}
nextTick(() => {
isResetting = false
})
nextTick(() => {
isResetting = false
})
}, 150)
}
},
)
@@ -189,7 +204,7 @@ watch(
<div class="flex-row gap-2 md:flex">
<div class="min-w-0 flex-1">
<SelectProvince
field-name="provinceId"
field-name="provinceCode"
placeholder="Pilih"
is-required
/>
@@ -198,8 +213,7 @@ watch(
<div class="min-w-0 flex-1">
<SelectRegency
field-name="regencyId"
placeholder="Pilih provinsi dahulu"
:is-disabled="!values.provinceId"
:province-code="values.provinceCode"
is-required
/>
</div>
@@ -209,16 +223,14 @@ watch(
<div class="min-w-0 flex-1">
<SelectDistrict
field-name="districtId"
placeholder="Pilih kabupaten/kota dahulu"
:is-disabled="!values.regencyId"
:regency-code="values.regencyId"
is-required
/>
</div>
<div class="min-w-0 flex-1">
<SelectVillage
field-name="villageId"
placeholder="Pilih kecamatan dahulu"
:is-disabled="!values.districtId"
:district-code="values.districtId"
is-required
/>
</div>
+2 -2
View File
@@ -94,7 +94,7 @@ watch(
personAddressRelativeForm.value.setValues(
{
...personAddressRelativeForm.value.values,
provinceId: newAddressValues.provinceId || '',
provinceCode: newAddressValues.provinceCode || '',
regencyId: newAddressValues.regencyId || '',
districtId: newAddressValues.districtId || '',
villageId: newAddressValues.villageId || '',
@@ -120,7 +120,7 @@ watch(
personAddressRelativeForm.value.setValues(
{
...personAddressRelativeForm.value.values,
provinceId: currentAddressValues.provinceId || '',
provinceCode: currentAddressValues.provinceCode || '',
regencyId: currentAddressValues.regencyId || '',
districtId: currentAddressValues.districtId || '',
villageId: currentAddressValues.villageId || '',
+19 -20
View File
@@ -1,17 +1,11 @@
<script setup lang="ts">
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
interface Item {
value: string
label: string
code?: string
priority?: number
}
const props = defineProps<{
id: string
modelValue?: string
items: Item[]
items: SelectItem[]
placeholder?: string
searchPlaceholder?: string
emptyMessage?: string
@@ -55,7 +49,7 @@ const searchableItems = computed(() => {
})
})
function onSelect(item: Item) {
function onSelect(item: SelectItem) {
emit('update:modelValue', item.value)
open.value = false
}
@@ -74,10 +68,11 @@ function onSelect(item: Item) {
:aria-describedby="`${props.id}-search`"
:class="
cn(
'w-full justify-between border text-sm font-normal rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white',
'w-full justify-between rounded-md border px-3 py-2 text-sm font-normal focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white',
{
'cursor-not-allowed bg-gray-100 opacity-50 border-gray-300 text-gray-500': props.isDisabled,
'bg-white text-black dark:bg-gray-800 dark:text-white dark:border-gray-600 border-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700': !props.isDisabled,
'cursor-not-allowed border-gray-300 bg-gray-100 text-gray-500 opacity-50': props.isDisabled,
'border-gray-400 bg-white text-black hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700':
!props.isDisabled,
'text-gray-400 dark:text-gray-500': !modelValue && !props.isDisabled,
},
props.class,
@@ -87,22 +82,26 @@ function onSelect(item: Item) {
{{ displayText }}
<Icon
name="i-lucide-chevrons-up-down"
:class="cn('ml-2 h-4 w-4 shrink-0', {
'opacity-30': props.isDisabled,
'opacity-50 text-gray-500 dark:text-gray-300': !props.isDisabled
})"
:class="
cn('ml-2 h-4 w-4 shrink-0', {
'opacity-30': props.isDisabled,
'text-gray-500 opacity-50 dark:text-gray-300': !props.isDisabled,
})
"
/>
</Button>
</PopoverTrigger>
<PopoverContent class="w-[var(--radix-popover-trigger-width)] p-0 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
<PopoverContent
class="w-[var(--radix-popover-trigger-width)] border border-gray-200 bg-white p-0 dark:border-gray-700 dark:bg-gray-800"
>
<Command class="bg-white dark:bg-gray-800">
<CommandInput
:id="`${props.id}-search`"
class="h-9 bg-white dark:bg-gray-800 text-black dark:text-white border-0 border-b border-gray-200 dark:border-gray-700 focus:ring-0"
class="h-9 border-0 border-b border-gray-200 bg-white text-black focus:ring-0 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
:placeholder="searchPlaceholder || 'Cari...'"
:aria-label="`Cari ${displayText}`"
/>
<CommandEmpty class="text-gray-500 dark:text-gray-400 py-6 text-center text-sm">
<CommandEmpty class="py-6 text-center text-sm text-gray-500 dark:text-gray-400">
{{ emptyMessage || 'Item tidak ditemukan.' }}
</CommandEmpty>
<CommandList
@@ -118,7 +117,7 @@ function onSelect(item: Item) {
:class="
cn(
'flex w-full cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 text-sm',
'focus:outline-none text-black dark:text-white',
'text-black focus:outline-none dark:text-white',
'hover:bg-primary hover:text-white focus:bg-primary focus:text-white',
'data-[highlighted]:bg-primary data-[highlighted]:text-white',
)
+2 -2
View File
@@ -10,7 +10,7 @@ import SelectTrigger from '~/components/pub/ui/select/SelectTrigger.vue'
import SelectValue from '~/components/pub/ui/select/SelectValue.vue'
import { cn } from '~/lib/utils'
interface Item {
export interface SelectItem {
value: string
label: string
code?: string
@@ -19,7 +19,7 @@ interface Item {
const props = defineProps<{
modelValue?: string
items: Item[]
items: SelectItem[]
placeholder?: string
label?: string
separator?: boolean
+180
View File
@@ -0,0 +1,180 @@
import { ref, computed, watch } from 'vue'
import { refDebounced } from '@vueuse/core'
import type { District } from '~/models/district'
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
import { toTitleCase } from '~/lib/utils'
import * as districtService from '~/services/district.service'
// Global cache untuk districts berdasarkan regency code
const districtsCache = ref<Map<string, District[]>>(new Map())
const loadingStates = ref<Map<string, boolean>>(new Map())
const errorStates = ref<Map<string, string | null>>(new Map())
export function useDistricts(regencyCode: Ref<string | undefined> | string | undefined) {
// Convert regencyCode ke ref jika bukan ref
const regencyCodeRef = typeof regencyCode === 'string' || regencyCode === undefined ? ref(regencyCode) : regencyCode
// Computed untuk mendapatkan districts berdasarkan regency code
const districts = computed(() => {
const code = regencyCodeRef.value
if (!code) return []
return districtsCache.value.get(code) || []
})
// Computed untuk loading state
const isLoading = computed(() => {
const code = regencyCodeRef.value
if (!code) return false
return loadingStates.value.get(code) || false
})
// Computed untuk error state
const error = computed(() => {
const code = regencyCodeRef.value
if (!code) return null
return errorStates.value.get(code) || null
})
// Computed untuk format SelectItem
const districtOptions = computed<SelectItem[]>(() => {
return districts.value.map((district) => ({
label: toTitleCase(district.name),
value: district.code,
searchValue: `${district.code} ${district.name}`.trim(),
}))
})
// Function untuk fetch districts berdasarkan regency code
async function fetchDistricts(regencyCodeParam?: string, forceRefresh = false, isUserAction = false) {
const code = regencyCodeParam || regencyCodeRef.value
if (!code) return
// Jika user action atau force refresh, selalu fetch
// Jika bukan user action dan sudah ada cache, skip
if (!isUserAction && !forceRefresh && districtsCache.value.has(code)) {
return
}
// Jika sedang loading, skip untuk mencegah duplicate calls
if (loadingStates.value.get(code)) {
return
}
// Tambahan: Cek apakah ada pending request untuk code yang sama
const pendingKey = `pending_${code}`
if (loadingStates.value.get(pendingKey)) {
return
}
loadingStates.value.set(pendingKey, true)
loadingStates.value.set(code, true)
errorStates.value.set(code, null)
try {
const response = await districtService.getList({
'page-size': '50',
sort: 'name:asc',
regency_code: code,
})
if (response.success) {
const districtsData = response.body.data || []
districtsCache.value.set(code, districtsData)
} else {
errorStates.value.set(code, 'Gagal memuat data kecamatan')
console.error('Failed to fetch districts:', response)
}
} catch (err) {
errorStates.value.set(code, 'Terjadi kesalahan saat memuat data kecamatan')
console.error('Error fetching districts:', err)
} finally {
loadingStates.value.set(code, false)
loadingStates.value.delete(pendingKey)
}
}
// Function untuk mencari district berdasarkan code
function getDistrictByCode(code: string): District | undefined {
const regencyCode = regencyCodeRef.value
if (!regencyCode) return undefined
const districtsForRegency = districtsCache.value.get(regencyCode) || []
return districtsForRegency.find((district) => district.code === code)
}
// Function untuk mencari district berdasarkan name
function getDistrictByName(name: string): District | undefined {
const regencyCode = regencyCodeRef.value
if (!regencyCode) return undefined
const districtsForRegency = districtsCache.value.get(regencyCode) || []
return districtsForRegency.find((district) => district.name.toLowerCase() === name.toLowerCase())
}
// Function untuk clear cache regency tertentu
function clearCache(regencyCodeParam?: string) {
const code = regencyCodeParam || regencyCodeRef.value
if (code) {
districtsCache.value.delete(code)
loadingStates.value.delete(code)
errorStates.value.delete(code)
}
}
// Function untuk clear semua cache
function clearAllCache() {
districtsCache.value.clear()
loadingStates.value.clear()
errorStates.value.clear()
}
// Function untuk refresh data
function refreshDistricts(regencyCodeParam?: string) {
const code = regencyCodeParam || regencyCodeRef.value
if (code) {
return fetchDistricts(code, true)
}
}
// Debounced regency code untuk mencegah multiple calls
const debouncedRegencyCode = refDebounced(regencyCodeRef, 100)
// Watch perubahan regency code untuk auto fetch
watch(
debouncedRegencyCode,
(newCode, oldCode) => {
if (newCode && newCode !== oldCode) {
// Jika ada oldCode berarti user action (ganti pilihan)
const isUserAction = !!oldCode
fetchDistricts(newCode, false, isUserAction)
}
},
{ immediate: true },
)
return {
// Data
districts: readonly(districts),
districtOptions,
// State
isLoading: readonly(isLoading),
error: readonly(error),
// Methods
fetchDistricts,
refreshDistricts,
getDistrictByCode,
getDistrictByName,
clearCache,
clearAllCache,
}
}
// Export untuk direct access ke cached data (jika diperlukan)
export const useDistrictsCache = () => ({
districtsCache: readonly(districtsCache),
loadingStates: readonly(loadingStates),
errorStates: readonly(errorStates),
})
+113
View File
@@ -0,0 +1,113 @@
import { ref, computed } from 'vue'
import type { Province } from '~/models/province'
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
import { toTitleCase } from '~/lib/utils'
import * as provinceService from '~/services/province.service'
// Global state untuk caching
const provincesCache = ref<Province[]>([])
const isLoading = ref(false)
const isInitialized = ref(false)
const error = ref<string | null>(null)
export function useProvinces() {
// Computed untuk format SelectItem
const provinceOptions = computed<SelectItem[]>(() => {
return provincesCache.value.map(province => ({
label: toTitleCase(province.name),
value: province.code,
// code: province.code,
searchValue: `${province.code} ${province.name}`.trim() // Untuk search internal combobox
}))
})
// Function untuk fetch data provinces
async function fetchProvinces(forceRefresh = false) {
// Jika sudah ada data dan tidak force refresh, skip
if (isInitialized.value && !forceRefresh) {
return
}
// Jika sedang loading, skip untuk mencegah duplicate calls
if (isLoading.value) {
return
}
isLoading.value = true
error.value = null
try {
const response = await provinceService.getList({
'page-no-limit': '1',
'sort': 'name:asc'
})
if (response.success) {
provincesCache.value = response.body.data || []
isInitialized.value = true
} else {
error.value = 'Gagal memuat data provinsi'
console.error('Failed to fetch provinces:', response)
}
} catch (err) {
error.value = 'Terjadi kesalahan saat memuat data provinsi'
console.error('Error fetching provinces:', err)
} finally {
isLoading.value = false
}
}
// Function untuk mencari province berdasarkan code
function getProvinceByCode(code: string): Province | undefined {
return provincesCache.value.find(province => province.code === code)
}
// Function untuk mencari province berdasarkan name
function getProvinceByName(name: string): Province | undefined {
return provincesCache.value.find(province =>
province.name.toLowerCase() === name.toLowerCase()
)
}
// Function untuk clear cache (jika diperlukan)
function clearCache() {
provincesCache.value = []
isInitialized.value = false
error.value = null
}
// Function untuk refresh data
function refreshProvinces() {
return fetchProvinces(true)
}
// Auto fetch saat composable pertama kali digunakan
if (!isInitialized.value && !isLoading.value) {
fetchProvinces()
}
return {
// Data
provinces: readonly(provincesCache),
provinceOptions,
// State
isLoading: readonly(isLoading),
isInitialized: readonly(isInitialized),
error: readonly(error),
// Methods
fetchProvinces,
refreshProvinces,
getProvinceByCode,
getProvinceByName,
clearCache,
}
}
// Export untuk direct access ke cached data (jika diperlukan)
export const useProvincesCache = () => ({
provinces: readonly(provincesCache),
isLoading: readonly(isLoading),
isInitialized: readonly(isInitialized),
})
+181
View File
@@ -0,0 +1,181 @@
import { ref, computed, watch } from 'vue'
import { refDebounced } from '@vueuse/core'
import type { Regency } from '~/models/regency'
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
import { toTitleCase } from '~/lib/utils'
import * as regencyService from '~/services/regency.service'
// Global cache untuk regencies berdasarkan province code
const regenciesCache = ref<Map<string, Regency[]>>(new Map())
const loadingStates = ref<Map<string, boolean>>(new Map())
const errorStates = ref<Map<string, string | null>>(new Map())
export function useRegencies(provinceCode: Ref<string | undefined> | string | undefined) {
// Convert provinceCode ke ref jika bukan ref
const provinceCodeRef =
typeof provinceCode === 'string' || provinceCode === undefined ? ref(provinceCode) : provinceCode
// Computed untuk mendapatkan regencies berdasarkan province code
const regencies = computed(() => {
const code = provinceCodeRef.value
if (!code) return []
return regenciesCache.value.get(code) || []
})
// Computed untuk loading state
const isLoading = computed(() => {
const code = provinceCodeRef.value
if (!code) return false
return loadingStates.value.get(code) || false
})
// Computed untuk error state
const error = computed(() => {
const code = provinceCodeRef.value
if (!code) return null
return errorStates.value.get(code) || null
})
// Computed untuk format SelectItem
const regencyOptions = computed<SelectItem[]>(() => {
return regencies.value.map((regency) => ({
label: toTitleCase(regency.name),
value: regency.code,
searchValue: `${regency.code} ${regency.name}`.trim(),
}))
})
// Function untuk fetch regencies berdasarkan province code
async function fetchRegencies(provinceCodeParam?: string, forceRefresh = false, isUserAction = false) {
const code = provinceCodeParam || provinceCodeRef.value
if (!code) return
// Jika user action atau force refresh, selalu fetch
// Jika bukan user action dan sudah ada cache, skip
if (!isUserAction && !forceRefresh && regenciesCache.value.has(code)) {
return
}
// Jika sedang loading, skip untuk mencegah duplicate calls
if (loadingStates.value.get(code)) {
return
}
// Tambahan: Cek apakah ada pending request untuk code yang sama
const pendingKey = `pending_${code}`
if (loadingStates.value.get(pendingKey)) {
return
}
loadingStates.value.set(pendingKey, true)
loadingStates.value.set(code, true)
errorStates.value.set(code, null)
try {
const response = await regencyService.getList({
'page-size': '50',
sort: 'name:asc',
province_code: code,
})
if (response.success) {
const regenciesData = response.body.data || []
regenciesCache.value.set(code, regenciesData)
} else {
errorStates.value.set(code, 'Gagal memuat data kabupaten/kota')
console.error('Failed to fetch regencies:', response)
}
} catch (err) {
errorStates.value.set(code, 'Terjadi kesalahan saat memuat data kabupaten/kota')
console.error('Error fetching regencies:', err)
} finally {
loadingStates.value.set(code, false)
loadingStates.value.delete(pendingKey)
}
}
// Function untuk mencari regency berdasarkan code
function getRegencyByCode(code: string): Regency | undefined {
const provinceCode = provinceCodeRef.value
if (!provinceCode) return undefined
const regenciesForProvince = regenciesCache.value.get(provinceCode) || []
return regenciesForProvince.find((regency) => regency.code === code)
}
// Function untuk mencari regency berdasarkan name
function getRegencyByName(name: string): Regency | undefined {
const provinceCode = provinceCodeRef.value
if (!provinceCode) return undefined
const regenciesForProvince = regenciesCache.value.get(provinceCode) || []
return regenciesForProvince.find((regency) => regency.name.toLowerCase() === name.toLowerCase())
}
// Function untuk clear cache province tertentu
function clearCache(provinceCodeParam?: string) {
const code = provinceCodeParam || provinceCodeRef.value
if (code) {
regenciesCache.value.delete(code)
loadingStates.value.delete(code)
errorStates.value.delete(code)
}
}
// Function untuk clear semua cache
function clearAllCache() {
regenciesCache.value.clear()
loadingStates.value.clear()
errorStates.value.clear()
}
// Function untuk refresh data
function refreshRegencies(provinceCodeParam?: string) {
const code = provinceCodeParam || provinceCodeRef.value
if (code) {
return fetchRegencies(code, true)
}
}
// Debounced province code untuk mencegah multiple calls
const debouncedProvinceCode = refDebounced(provinceCodeRef, 100)
// Watch perubahan province code untuk auto fetch
watch(
debouncedProvinceCode,
(newCode, oldCode) => {
if (newCode && newCode !== oldCode) {
// Jika ada oldCode berarti user action (ganti pilihan)
const isUserAction = !!oldCode
fetchRegencies(newCode, false, isUserAction)
}
},
{ immediate: true },
)
return {
// Data
regencies: readonly(regencies),
regencyOptions,
// State
isLoading: readonly(isLoading),
error: readonly(error),
// Methods
fetchRegencies,
refreshRegencies,
getRegencyByCode,
getRegencyByName,
clearCache,
clearAllCache,
}
}
// Export untuk direct access ke cached data (jika diperlukan)
export const useRegenciesCache = () => ({
regenciesCache: readonly(regenciesCache),
loadingStates: readonly(loadingStates),
errorStates: readonly(errorStates),
})
+181
View File
@@ -0,0 +1,181 @@
import { ref, computed, watch } from 'vue'
import { refDebounced } from '@vueuse/core'
import type { Village } from '~/models/village'
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
import { toTitleCase } from '~/lib/utils'
import * as villageService from '~/services/village.service'
// Global cache untuk villages berdasarkan district code
const villagesCache = ref<Map<string, Village[]>>(new Map())
const loadingStates = ref<Map<string, boolean>>(new Map())
const errorStates = ref<Map<string, string | null>>(new Map())
export function useVillages(districtCode: Ref<string | undefined> | string | undefined) {
// Convert districtCode ke ref jika bukan ref
const districtCodeRef =
typeof districtCode === 'string' || districtCode === undefined ? ref(districtCode) : districtCode
// Computed untuk mendapatkan villages berdasarkan district code
const villages = computed(() => {
const code = districtCodeRef.value
if (!code) return []
return villagesCache.value.get(code) || []
})
// Computed untuk loading state
const isLoading = computed(() => {
const code = districtCodeRef.value
if (!code) return false
return loadingStates.value.get(code) || false
})
// Computed untuk error state
const error = computed(() => {
const code = districtCodeRef.value
if (!code) return null
return errorStates.value.get(code) || null
})
// Computed untuk format SelectItem
const villageOptions = computed<SelectItem[]>(() => {
return villages.value.map((village) => ({
label: toTitleCase(village.name),
value: village.code,
searchValue: `${village.code} ${village.name}`.trim(),
}))
})
// Function untuk fetch villages berdasarkan district code
async function fetchVillages(districtCodeParam?: string, forceRefresh = false, isUserAction = false) {
const code = districtCodeParam || districtCodeRef.value
if (!code) return
// Jika user action atau force refresh, selalu fetch
// Jika bukan user action dan sudah ada cache, skip
if (!isUserAction && !forceRefresh && villagesCache.value.has(code)) {
return
}
// Jika sedang loading, skip untuk mencegah duplicate calls
if (loadingStates.value.get(code)) {
return
}
// Tambahan: Cek apakah ada pending request untuk code yang sama
const pendingKey = `pending_${code}`
if (loadingStates.value.get(pendingKey)) {
return
}
loadingStates.value.set(pendingKey, true)
loadingStates.value.set(code, true)
errorStates.value.set(code, null)
try {
const response = await villageService.getList({
'page-size': '50',
sort: 'name:asc',
district_code: code,
})
if (response.success) {
const villagesData = response.body.data || []
villagesCache.value.set(code, villagesData)
} else {
errorStates.value.set(code, 'Gagal memuat data kelurahan')
console.error('Failed to fetch villages:', response)
}
} catch (err) {
errorStates.value.set(code, 'Terjadi kesalahan saat memuat data kelurahan')
console.error('Error fetching villages:', err)
} finally {
loadingStates.value.set(code, false)
loadingStates.value.delete(pendingKey)
}
}
// Function untuk mencari village berdasarkan code
function getVillageByCode(code: string): Village | undefined {
const districtCode = districtCodeRef.value
if (!districtCode) return undefined
const villagesForDistrict = villagesCache.value.get(districtCode) || []
return villagesForDistrict.find((village) => village.code === code)
}
// Function untuk mencari village berdasarkan name
function getVillageByName(name: string): Village | undefined {
const districtCode = districtCodeRef.value
if (!districtCode) return undefined
const villagesForDistrict = villagesCache.value.get(districtCode) || []
return villagesForDistrict.find((village) => village.name.toLowerCase() === name.toLowerCase())
}
// Function untuk clear cache district tertentu
function clearCache(districtCodeParam?: string) {
const code = districtCodeParam || districtCodeRef.value
if (code) {
villagesCache.value.delete(code)
loadingStates.value.delete(code)
errorStates.value.delete(code)
}
}
// Function untuk clear semua cache
function clearAllCache() {
villagesCache.value.clear()
loadingStates.value.clear()
errorStates.value.clear()
}
// Function untuk refresh data
function refreshVillages(districtCodeParam?: string) {
const code = districtCodeParam || districtCodeRef.value
if (code) {
return fetchVillages(code, true)
}
}
// Debounced district code untuk mencegah multiple calls
const debouncedDistrictCode = refDebounced(districtCodeRef, 100)
// Watch perubahan district code untuk auto fetch
watch(
debouncedDistrictCode,
(newCode, oldCode) => {
if (newCode && newCode !== oldCode) {
// Jika ada oldCode berarti user action (ganti pilihan)
const isUserAction = !!oldCode
fetchVillages(newCode, false, isUserAction)
}
},
{ immediate: true },
)
return {
// Data
villages: readonly(villages),
villageOptions,
// State
isLoading: readonly(isLoading),
error: readonly(error),
// Methods
fetchVillages,
refreshVillages,
getVillageByCode,
getVillageByName,
clearCache,
clearAllCache,
}
}
// Export untuk direct access ke cached data (jika diperlukan)
export const useVillagesCache = () => ({
villagesCache: readonly(villagesCache),
loadingStates: readonly(loadingStates),
errorStates: readonly(errorStates),
})
+9
View File
@@ -27,6 +27,15 @@ export function mapToComboboxOptList(items: Record<string, string>): SelectOptio
return result
}
/**
* Mengkonversi string menjadi title case (huruf pertama setiap kata kapital)
* @param str - String yang akan dikonversi
* @returns String dalam format title case
*/
export function toTitleCase(str: string): string {
return str.toLowerCase().replace(/\b\w/g, (char) => char.toUpperCase())
}
/**
* Menghitung umur berdasarkan tanggal lahir
* @param birthDate - Tanggal lahir dalam format Date atau string
+16
View File
@@ -0,0 +1,16 @@
import { type Base, genBase } from './_base'
export interface District extends Base {
regency_code: string
code: string
name: string
}
export function genDistrict(): District {
return {
...genBase(),
regency_code: '',
name: '',
code: '',
}
}
+4 -4
View File
@@ -45,7 +45,7 @@ export function genPatient(props: genPatientProps): PatientEntity {
const personContacts: PersonContact[] = []
// jika alamat ktp sama dengan domisili saat ini
if (cardAddress.isSameAddress === '1') {
if (cardAddress.isSameAddress) {
addresses.push({ ...genBase(), person_id: 0, locationType: '', ...residentAddress })
}
@@ -97,7 +97,7 @@ export function genPatient(props: genPatientProps): PatientEntity {
person: {
id: 0,
name: patient.fullName,
alias: patient.alias,
// alias: patient.alias,
birthDate: patient.birthDate,
birthRegency_code: patient.birthPlace,
gender_code: patient.gender,
@@ -111,7 +111,7 @@ export function genPatient(props: genPatientProps): PatientEntity {
ethnic_code: patient.ethnicity,
language_code: patient.language,
communicationIssueStatus: patient.communicationBarrier,
disability: patient.disability,
disability: patient.disabilityType || '',
nationality: patient.nationality,
// residentIdentityFileUrl: patient.residentIdentityFileUrl,
// passportFileUrl: patient.passportFileUrl,
@@ -126,7 +126,7 @@ export function genPatient(props: genPatientProps): PatientEntity {
personRelatives: familiesContact,
registeredAt: new Date(),
status_code: 'active',
newBornStatus: false,
newBornStatus: patient.isNewBorn,
person_id: 0,
id: 0,
number: '0x000000000000000000000000000000',
+2 -2
View File
@@ -1,10 +1,10 @@
import { type Base, genBase } from "./_base"
import { type Base, genBase } from './_base'
export interface Person extends Base {
// todo: awaiting approve from stake holder: buat field sapaan
// todo: adjust field ketika person Balita
name: string
alias?: string
// alias?: string
frontTitle?: string
endTitle?: string
birthDate?: Date | string
+16
View File
@@ -0,0 +1,16 @@
import { type Base, genBase } from './_base'
export interface Village extends Base {
district_code: string
code: string
name: string
}
export function genVillage(): Village {
return {
...genBase(),
district_code: '',
code: '',
name: '',
}
}
+36 -6
View File
@@ -6,6 +6,12 @@ const CommunicationBarrierSchema = z
})
.transform((val) => val === 'YA')
const IsNewBornSchema = z
.enum(['YA', 'TIDAK'], {
required_error: 'Mohon lengkapi status pasien',
})
.transform((val) => val === 'YA')
const PatientSchema = z
.object({
// Data Diri Pasien
@@ -19,9 +25,9 @@ const PatientSchema = z
// familyCardFile: z.instanceof(File, { message: 'File KK harus dipilih' }),
// Informasi Dasar
alias: z.string({
required_error: 'Mohon pilih sapaan',
}),
// alias: z.string({
// required_error: 'Mohon pilih sapaan',
// }),
fullName: z.string({
required_error: 'Mohon lengkapi Nama',
}),
@@ -71,6 +77,7 @@ const PatientSchema = z
nationality: z.string({
required_error: 'Pilih Kebangsaan',
}),
isNewBorn: IsNewBornSchema,
language: z.string({
required_error: 'Mohon pilih Preferensi Bahasa',
}),
@@ -99,14 +106,37 @@ const PatientSchema = z
note: z.string().optional(),
drivingLicenseNumber: z.string().optional(),
})
.refine((data) => (data.disability === 'TIDAK' ? !data.disabilityType : true), {
message: "DisabilityType hanya boleh diisi jika disability = 'YA'",
.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) => (data.disability === 'YA' ? !!data.disabilityType?.trim() : true), {
.refine((data) => {
// Jika disability = 'YA', maka disabilityType wajib diisi
if (data.disability === 'YA') {
return !!data.disabilityType?.trim()
}
return true
}, {
message: 'Mohon pilih Jenis Disabilitas',
path: ['disabilityType'],
})
.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,
}
})
type PatientFormData = z.infer<typeof PatientSchema>
+17 -12
View File
@@ -7,19 +7,24 @@ const OptionalAddressSchema = PersonAddressSchema.partial()
// Schema untuk alamat required ketika isSameAddress = '0'
const RequiredAddressSchema = PersonAddressSchema
const PersonAddressRelativeSchema = z.discriminatedUnion('isSameAddress', [
z
.object({
isSameAddress: z.literal('1').default('1'),
})
.merge(OptionalAddressSchema),
const PersonAddressRelativeSchema = z
.discriminatedUnion('isSameAddress', [
z
.object({
isSameAddress: z.literal('1').default('1'),
})
.merge(OptionalAddressSchema),
z
.object({
isSameAddress: z.literal('0'),
})
.merge(RequiredAddressSchema),
])
z
.object({
isSameAddress: z.literal('0'),
})
.merge(RequiredAddressSchema),
])
.transform((data) => ({
...data,
isSameAddress: data.isSameAddress === '1',
}))
type PersonAddressRelativeFormData = z.infer<typeof PersonAddressRelativeSchema>
+25
View File
@@ -0,0 +1,25 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/district'
const name = 'district'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id, name)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}
+25
View File
@@ -0,0 +1,25 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/province'
const name = 'province'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id, name)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}
+25
View File
@@ -0,0 +1,25 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/regency'
const name = 'regency'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id, name)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}
+25
View File
@@ -0,0 +1,25 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/village'
const name = 'village'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id, name)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}