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