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
172 lines
5.1 KiB
Vue
172 lines
5.1 KiB
Vue
<script setup lang="ts">
|
|
import { SelectRoot } from 'radix-vue'
|
|
import { watch } from 'vue'
|
|
import SelectContent from '~/components/pub/ui/select/SelectContent.vue'
|
|
import SelectGroup from '~/components/pub/ui/select/SelectGroup.vue'
|
|
import SelectItem from '~/components/pub/ui/select/SelectItem.vue'
|
|
import SelectLabel from '~/components/pub/ui/select/SelectLabel.vue'
|
|
import SelectSeparator from '~/components/pub/ui/select/SelectSeparator.vue'
|
|
import SelectTrigger from '~/components/pub/ui/select/SelectTrigger.vue'
|
|
import SelectValue from '~/components/pub/ui/select/SelectValue.vue'
|
|
import { cn } from '~/lib/utils'
|
|
|
|
export interface SelectItem {
|
|
value: string
|
|
label: string
|
|
code?: string
|
|
priority?: number // Priority untuk sorting: negatif = bawah, positif = atas, 0/undefined = normal sorting
|
|
}
|
|
|
|
const props = defineProps<{
|
|
modelValue?: string
|
|
items: SelectItem[]
|
|
placeholder?: string
|
|
label?: string
|
|
separator?: boolean
|
|
class?: string
|
|
isSelectedFirst?: boolean
|
|
preserveOrder?: boolean
|
|
isDisabled?: boolean
|
|
autoWidth?: boolean
|
|
autoFill?: boolean
|
|
// otherPlacement sudah tidak digunakan, diganti dengan priority system di Item interface
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: string]
|
|
}>()
|
|
|
|
const processedItems = computed(() => {
|
|
const itemsWithSelection = props.items.map((item) => ({
|
|
...item,
|
|
isSelected: item.value === props.modelValue,
|
|
}))
|
|
|
|
// Jika preserveOrder true, kembalikan urutan array asli
|
|
if (props.preserveOrder) {
|
|
return itemsWithSelection
|
|
}
|
|
|
|
// Sorting dengan priority system
|
|
return itemsWithSelection.sort((a, b) => {
|
|
const aPriority = a.priority ?? 0
|
|
const bPriority = b.priority ?? 0
|
|
|
|
// Jika ada priority, sort berdasarkan priority (descending: tinggi ke rendah)
|
|
if (aPriority !== bPriority) {
|
|
return bPriority - aPriority
|
|
}
|
|
|
|
// Jika priority sama (termasuk 0/undefined), lakukan sorting normal
|
|
if (props.isSelectedFirst) {
|
|
if (a.isSelected && !b.isSelected) return -1
|
|
if (!a.isSelected && b.isSelected) return 1
|
|
}
|
|
|
|
return a.label.localeCompare(b.label)
|
|
})
|
|
})
|
|
|
|
// Computed property untuk menghitung width optimal berdasarkan konten terpanjang
|
|
const optimalWidth = computed(() => {
|
|
// Jika autoWidth false atau undefined, gunakan full width
|
|
if (!props.autoWidth) return '100%'
|
|
|
|
if (!props.items.length) return 'auto'
|
|
|
|
// Mencari label terpanjang
|
|
const longestLabel = props.items.reduce((longest, item) => {
|
|
const itemText = item.code ? `${item.label} ${item.code}` : item.label
|
|
return itemText.length > longest.length ? itemText : longest
|
|
}, '')
|
|
|
|
// Menghitung width berdasarkan panjang karakter (estimasi)
|
|
// Setiap karakter ~0.6em, ditambah padding dan space untuk icon
|
|
const estimatedWidth = Math.max(longestLabel.length * 0.6 + 3, 8) // minimum 8em
|
|
|
|
return `${Math.min(estimatedWidth, 25)}em` // maksimum 25em
|
|
})
|
|
|
|
function onValueChange(value: string) {
|
|
emit('update:modelValue', value)
|
|
}
|
|
|
|
// Auto fill logic - automatically select first item if autoFill is enabled
|
|
watch(
|
|
() => props.items,
|
|
(newItems) => {
|
|
if (props.autoFill && newItems.length > 0 && !props.modelValue) {
|
|
// Auto select first item only if no value is currently selected
|
|
const firstItem = newItems[0]
|
|
if (firstItem?.value) {
|
|
emit('update:modelValue', firstItem.value)
|
|
}
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<SelectRoot
|
|
:model-value="modelValue"
|
|
:disabled="isDisabled"
|
|
@update:model-value="onValueChange"
|
|
>
|
|
<SelectTrigger
|
|
:class="
|
|
cn(
|
|
'rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white',
|
|
{
|
|
'cursor-not-allowed bg-gray-100 opacity-50': isDisabled,
|
|
'bg-white text-black dark:bg-gray-800 dark:text-white': !isDisabled,
|
|
'w-full': !autoWidth,
|
|
},
|
|
props.class,
|
|
)
|
|
"
|
|
:style="autoWidth ? { width: optimalWidth, minWidth: '8em' } : {}"
|
|
icon-name="i-radix-icons-chevron-down"
|
|
icon-class="text-gray-500 dark:text-gray-300"
|
|
>
|
|
<SelectValue
|
|
:placeholder="placeholder || 'Pilih item'"
|
|
:class="
|
|
cn('text-sm', {
|
|
'text-gray-400': !props.modelValue,
|
|
'text-black dark:text-white': props.modelValue,
|
|
'text-gray-500': isDisabled,
|
|
})
|
|
"
|
|
/>
|
|
</SelectTrigger>
|
|
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
<SelectLabel v-if="label">
|
|
{{ label }}
|
|
</SelectLabel>
|
|
|
|
<SelectItem
|
|
v-for="item in processedItems"
|
|
:key="item.value"
|
|
:value="item.value"
|
|
class="cursor-pointer hover:bg-primary hover:text-white focus:bg-primary focus:text-white"
|
|
>
|
|
<div class="flex w-full items-center justify-between">
|
|
<span>{{ item.label }}</span>
|
|
<span
|
|
v-if="item.code"
|
|
class="ml-2 text-xs text-muted-foreground"
|
|
>
|
|
{{ item.code }}
|
|
</span>
|
|
</div>
|
|
</SelectItem>
|
|
|
|
<SelectSeparator v-if="separator" />
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</SelectRoot>
|
|
</template>
|