feat(patient): add edit functionality to patient form - Modify genPatientEntity to accept existing patient data for updates - Add handleActionEdit handler for edit mode - Update form to handle both create and edit modes - Rename patient ref to patientDetail for clarity refactor(patient): update marital status codes and job options mapping - Change marital status enum values to standardized codes (S, M, D, W) - Simplify job options and marital status options mapping using mapToComboboxOptList - Add error handling in patient data loading ajust styling text based on combobox wip: edit patient redirect refactor(models): update type definitions and form field handling - Add field-name prop to SelectDob component for better form handling - Update Person and Patient interfaces to use null for optional fields - Add maritalStatus_code field to Person interface - Improve type safety by replacing undefined with null for optional fields fix casting radio str to boolean and parsing date error
179 lines
5.4 KiB
Vue
179 lines
5.4 KiB
Vue
<script setup lang="ts">
|
|
import { SelectRoot } from 'radix-vue'
|
|
import { watch, computed } from 'vue'
|
|
import { useFieldError } from 'vee-validate'
|
|
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
|
|
id?: string
|
|
// otherPlacement sudah tidak digunakan, diganti dengan priority system di Item interface
|
|
}>()
|
|
|
|
// Get error state from vee-validate if id is provided
|
|
const fieldError = props.id ? useFieldError(() => props.id!) : ref(null)
|
|
const hasError = computed(() => !!fieldError.value)
|
|
|
|
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(
|
|
'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,
|
|
'w-full': !autoWidth,
|
|
'border-red-500 focus:ring-red-500': hasError,
|
|
},
|
|
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-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>
|