Files
mykopkb-fe/src/modules/profile/pages/ProfileTab.vue
T

822 lines
29 KiB
Vue

<script lang="ts" setup>
import { computed, onMounted, reactive, ref, watch } from 'vue'
import * as select from '@zag-js/select'
import Swal from 'sweetalert2'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Box } from '@/components/ui/box'
import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { Lucide } from '@/components/ui/lucide'
import {
SelectRoot,
SelectControl,
SelectTrigger,
SelectValueText,
SelectContent,
SelectItemGroup,
SelectItemGroupLabel,
SelectItem,
SelectItemText,
} from '@/components/ui/select'
import { getApiErrorMessage, getApiValidationErrors } from '@/core/utils/getApiErrorMessage'
import {
createAddress,
deleteAddress,
listAddresses,
updateAddress,
} from '@/modules/profile/services/address.service'
import { updateProfile } from '@/modules/profile/services/profile.service'
import type { Address, AddressPayload } from '@/modules/profile/types/address.types'
import { useAuthStore } from '@/stores/auth'
defineProps<{
embedded?: boolean
}>()
const authStore = useAuthStore()
const saving = ref(false)
const form = reactive({
name: '',
ic_number: '',
position: '',
phone_number: '',
birth_date: '',
birth_place: '',
})
type ProfileFieldKey = 'gender' | 'marriage_status' | 'birth_date' | 'birth_place'
const PROFILE_FIELD_KEYS: ProfileFieldKey[] = [
'gender',
'marriage_status',
'birth_date',
'birth_place',
]
const profileErrors = reactive<Partial<Record<ProfileFieldKey, string>>>({})
const addresses = ref<Address[]>([])
const loadingAddresses = ref(false)
const savingAddress = ref(false)
const deletingAddressId = ref<string | null>(null)
const editingAddressId = ref<string | null>(null)
type AddressFieldKey =
| 'address_type'
| 'address_line_1'
| 'address_line_2'
| 'city'
| 'state'
| 'postcode'
| 'country'
const ADDRESS_FIELD_KEYS: AddressFieldKey[] = [
'address_type',
'address_line_1',
'address_line_2',
'city',
'state',
'postcode',
'country',
]
const addressErrors = reactive<Partial<Record<AddressFieldKey, string>>>({})
type SelectOption = { label: string; value: string }
const GENDER_OPTIONS: SelectOption[] = [
{ label: 'Lelaki', value: 'Lelaki' },
{ label: 'Perempuan', value: 'Perempuan' },
]
const MARRIAGE_STATUS_OPTIONS: SelectOption[] = [
{ label: 'Belum Berkahwin', value: 'Belum Berkahwin' },
{ label: 'Berkahwin', value: 'Berkahwin' },
{ label: 'Bercerai', value: 'Bercerai' },
{ label: 'Balu', value: 'Balu' },
{ label: 'Duda', value: 'Duda' },
]
const ADDRESS_TYPE_OPTIONS: SelectOption[] = [
{ label: 'Rumah', value: 'home' },
{ label: 'Pejabat', value: 'office' },
{ label: 'Bil', value: 'billing' },
]
const MALAYSIA_STATE_OPTIONS: SelectOption[] = [
{ label: 'Johor', value: 'Johor' },
{ label: 'Kedah', value: 'Kedah' },
{ label: 'Kelantan', value: 'Kelantan' },
{ label: 'Melaka', value: 'Melaka' },
{ label: 'Negeri Sembilan', value: 'Negeri Sembilan' },
{ label: 'Pahang', value: 'Pahang' },
{ label: 'Perak', value: 'Perak' },
{ label: 'Perlis', value: 'Perlis' },
{ label: 'Pulau Pinang', value: 'Pulau Pinang' },
{ label: 'Sabah', value: 'Sabah' },
{ label: 'Sarawak', value: 'Sarawak' },
{ label: 'Selangor', value: 'Selangor' },
{ label: 'Terengganu', value: 'Terengganu' },
{ label: 'Wilayah Persekutuan Kuala Lumpur', value: 'Wilayah Persekutuan Kuala Lumpur' },
{ label: 'Wilayah Persekutuan Labuan', value: 'Wilayah Persekutuan Labuan' },
{ label: 'Wilayah Persekutuan Putrajaya', value: 'Wilayah Persekutuan Putrajaya' },
]
function createSelectCollection(options: SelectOption[]) {
return select.collection({
items: options,
itemToValue: (item) => item.label,
})
}
function labelToApiValue(options: SelectOption[], label: string | undefined): string | null {
if (!label) return null
return options.find((option) => option.label === label)?.value ?? null
}
function apiValueToLabel(options: SelectOption[], value: string | null | undefined): string[] {
if (!value) return []
const option = options.find((item) => item.value === value)
return option ? [option.label] : [value]
}
const genderCollection = createSelectCollection(GENDER_OPTIONS)
const marriageStatusCollection = createSelectCollection(MARRIAGE_STATUS_OPTIONS)
const addressTypeCollection = createSelectCollection(ADDRESS_TYPE_OPTIONS)
const stateCollection = createSelectCollection(MALAYSIA_STATE_OPTIONS)
const genderValue = ref<string[]>([])
const marriageStatusValue = ref<string[]>([])
const genderInitial = ref<string[]>([])
const marriageStatusInitial = ref<string[]>([])
const profileSelectKey = ref(0)
const addressTypeValue = ref<string[]>([])
const stateValue = ref<string[]>([])
const addressTypeInitial = ref<string[]>([])
const stateInitial = ref<string[]>([])
function clearProfileFieldError(field: ProfileFieldKey) {
delete profileErrors[field]
}
function clearProfileErrors() {
for (const field of PROFILE_FIELD_KEYS) {
delete profileErrors[field]
}
}
function setProfileErrorsFromApi(error: unknown): boolean {
const apiErrors = getApiValidationErrors(error)
if (!apiErrors) return false
for (const [field, messages] of Object.entries(apiErrors)) {
if (PROFILE_FIELD_KEYS.includes(field as ProfileFieldKey) && messages[0]) {
profileErrors[field as ProfileFieldKey] = messages[0]
}
}
return Object.keys(profileErrors).length > 0
}
function setGenderValue(details: { value: string[] }) {
genderValue.value = details.value
clearProfileFieldError('gender')
}
function setMarriageStatusValue(details: { value: string[] }) {
marriageStatusValue.value = details.value
clearProfileFieldError('marriage_status')
}
function clearAddressFieldError(field: AddressFieldKey) {
delete addressErrors[field]
}
function clearAddressErrors() {
for (const field of ADDRESS_FIELD_KEYS) {
delete addressErrors[field]
}
}
function setAddressErrorsFromApi(error: unknown): boolean {
const apiErrors = getApiValidationErrors(error)
if (!apiErrors) return false
for (const [field, messages] of Object.entries(apiErrors)) {
if (ADDRESS_FIELD_KEYS.includes(field as AddressFieldKey) && messages[0]) {
addressErrors[field as AddressFieldKey] = messages[0]
}
}
return Object.keys(addressErrors).length > 0
}
function validateAddressForm(): boolean {
clearAddressErrors()
let valid = true
if (
!addressTypeValue.value[0] ||
!labelToApiValue(ADDRESS_TYPE_OPTIONS, addressTypeValue.value[0])
) {
addressErrors.address_type = 'Jenis alamat diperlukan.'
valid = false
}
if (!addressForm.address_line_1.trim()) {
addressErrors.address_line_1 = 'Alamat 1 diperlukan.'
valid = false
}
if (!addressForm.city.trim()) {
addressErrors.city = 'Bandar diperlukan.'
valid = false
}
if (!labelToApiValue(MALAYSIA_STATE_OPTIONS, stateValue.value[0]) && !addressForm.state.trim()) {
addressErrors.state = 'Negeri diperlukan.'
valid = false
}
if (!addressForm.postcode.trim()) {
addressErrors.postcode = 'Poskod diperlukan.'
valid = false
}
if (!addressForm.country.trim()) {
addressErrors.country = 'Negara diperlukan.'
valid = false
}
return valid
}
function setAddressTypeValue(details: { value: string[] }) {
addressTypeValue.value = details.value
clearAddressFieldError('address_type')
addressForm.address_type =
labelToApiValue(ADDRESS_TYPE_OPTIONS, details.value[0]) ?? ''
}
function setStateValue(details: { value: string[] }) {
stateValue.value = details.value
addressForm.state = details.value[0] ?? ''
clearAddressFieldError('state')
}
function emptyAddressForm(): AddressPayload {
return {
address_type: '',
address_line_1: '',
address_line_2: '',
city: '',
state: '',
postcode: '',
country: 'Malaysia',
}
}
const addressForm = reactive(emptyAddressForm())
const addressTypeLabel = computed(() =>
Object.fromEntries(ADDRESS_TYPE_OPTIONS.map((option) => [option.value, option.label])),
)
const isEditingAddress = computed(() => editingAddressId.value !== null)
const displayValue = (value: string | number | null | undefined) => {
if (value === null || value === undefined || value === '') return '-'
return String(value).trim() || '-'
}
function formatDate(value: string | null | undefined): string {
if (!value) return '-'
return value.slice(0, 10)
}
function calculateAge(birthDate: string | null | undefined): number | null {
if (!birthDate) return null
const birth = new Date(birthDate.slice(0, 10))
if (Number.isNaN(birth.getTime())) return null
const today = new Date()
let age = today.getFullYear() - birth.getFullYear()
const monthDiff = today.getMonth() - birth.getMonth()
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--
}
return age >= 0 ? age : null
}
const ageLabel = computed(() => {
const age = calculateAge(form.birth_date || authStore.userBirthDate)
if (age === null) return '-'
return `${age} tahun`
})
function toDateInputValue(value: string | null | undefined): string {
if (!value) return ''
return value.slice(0, 10)
}
function syncProfileSelectValues() {
genderValue.value = apiValueToLabel(GENDER_OPTIONS, authStore.user?.gender)
genderInitial.value = [...genderValue.value]
marriageStatusValue.value = apiValueToLabel(MARRIAGE_STATUS_OPTIONS, authStore.user?.marriage_status)
marriageStatusInitial.value = [...marriageStatusValue.value]
profileSelectKey.value++
}
function syncFormFromUser() {
const user = authStore.user
if (!user) return
form.name = user.name ?? ''
form.ic_number = user.ic_number ?? ''
form.position = user.position ?? ''
form.phone_number = user.phone_number ?? ''
form.birth_date = toDateInputValue(user.birth_date)
form.birth_place = user.birth_place ?? ''
syncProfileSelectValues()
}
async function onSaveProfile() {
saving.value = true
clearProfileErrors()
const gender = labelToApiValue(GENDER_OPTIONS, genderValue.value[0])
const marriageStatus = labelToApiValue(MARRIAGE_STATUS_OPTIONS, marriageStatusValue.value[0])
try {
const res = await updateProfile({
name: form.name.trim(),
ic_number: form.ic_number.trim(),
position: form.position.trim(),
phone_number: form.phone_number.trim(),
gender: gender ?? undefined,
marriage_status: marriageStatus ?? undefined,
birth_date: form.birth_date || undefined,
birth_place: form.birth_place.trim() || undefined,
})
if (!res.success) {
throw new Error(res.message ?? 'Gagal mengemas kini profil.')
}
authStore.setUserProfile(res.data)
syncFormFromUser()
await Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: 'Profil berjaya dikemas kini.',
showConfirmButton: false,
timer: 3000,
})
} catch (error) {
if (!setProfileErrorsFromApi(error)) {
await Swal.fire({
icon: 'error',
title: 'Ralat',
text: getApiErrorMessage(error, 'Gagal mengemas kini profil.'),
})
}
} finally {
saving.value = false
}
}
function syncAddressSelectValues() {
addressTypeValue.value = apiValueToLabel(ADDRESS_TYPE_OPTIONS, addressForm.address_type)
addressTypeInitial.value = [...addressTypeValue.value]
stateValue.value = apiValueToLabel(MALAYSIA_STATE_OPTIONS, addressForm.state)
stateInitial.value = [...stateValue.value]
}
function resetAddressForm() {
Object.assign(addressForm, emptyAddressForm())
editingAddressId.value = null
clearAddressErrors()
syncAddressSelectValues()
}
function formatAddressLine(address: Address) {
return [address.address_line_1, address.address_line_2, address.postcode, address.city, address.state, address.country]
.filter((part) => part?.trim())
.join(', ')
}
async function fetchAddresses() {
loadingAddresses.value = true
try {
const res = await listAddresses()
addresses.value = res.data
} catch (error) {
await Swal.fire({
icon: 'error',
title: 'Ralat',
text: getApiErrorMessage(error, 'Gagal memuatkan alamat.'),
})
} finally {
loadingAddresses.value = false
}
}
function startEditAddress(address: Address) {
clearAddressErrors()
editingAddressId.value = address.id
addressForm.address_type = address.address_type
addressForm.address_line_1 = address.address_line_1
addressForm.address_line_2 = address.address_line_2 ?? ''
addressForm.city = address.city
addressForm.state = address.state
addressForm.postcode = address.postcode
addressForm.country = address.country
syncAddressSelectValues()
}
async function onSaveAddress() {
if (!validateAddressForm()) {
return
}
savingAddress.value = true
const wasEditing = isEditingAddress.value
const payload: AddressPayload = {
address_type: labelToApiValue(ADDRESS_TYPE_OPTIONS, addressTypeValue.value[0]) ?? addressForm.address_type,
address_line_1: addressForm.address_line_1.trim(),
address_line_2: addressForm.address_line_2?.trim() || undefined,
city: addressForm.city.trim(),
state: labelToApiValue(MALAYSIA_STATE_OPTIONS, stateValue.value[0]) ?? addressForm.state.trim(),
postcode: addressForm.postcode.trim(),
country: addressForm.country.trim(),
}
try {
const res = wasEditing
? await updateAddress(editingAddressId.value!, payload)
: await createAddress(payload)
if (!res.success) {
throw new Error(res.message ?? 'Gagal menyimpan alamat.')
}
await fetchAddresses()
resetAddressForm()
await Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: (wasEditing ? 'Alamat berjaya dikemas kini.' : 'Alamat berjaya ditambah.'),
showConfirmButton: false,
timer: 3000,
})
} catch (error) {
if (!setAddressErrorsFromApi(error)) {
await Swal.fire({
icon: 'error',
title: 'Ralat',
text: getApiErrorMessage(error, 'Gagal menyimpan alamat.'),
})
}
} finally {
savingAddress.value = false
}
}
async function onDeleteAddress(address: Address) {
const result = await Swal.fire({
icon: 'warning',
title: 'Padam alamat?',
text: 'Tindakan ini tidak boleh dibatalkan.',
showCancelButton: true,
confirmButtonText: 'Padam',
cancelButtonText: 'Batal',
})
if (!result.isConfirmed) return
deletingAddressId.value = address.id
try {
const res = await deleteAddress(address.id)
if (!res.success) {
throw new Error(res.message ?? 'Gagal memadam alamat.')
}
if (editingAddressId.value === address.id) {
resetAddressForm()
}
await fetchAddresses()
await Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: 'Alamat berjaya dipadam.',
showConfirmButton: false,
timer: 3000,
})
} catch (error) {
await Swal.fire({
icon: 'error',
title: 'Ralat',
text: getApiErrorMessage(error, 'Gagal memadam alamat.'),
})
} finally {
deletingAddressId.value = null
}
}
watch(() => authStore.user, syncFormFromUser, { immediate: true })
onMounted(async () => {
syncFormFromUser()
syncAddressSelectValues()
await fetchAddresses()
})
</script>
<template>
<div :class="embedded ? 'space-y-8' : 'mt-5 space-y-8'">
<Box raised="single" class="p-6">
<form class="space-y-6" @submit.prevent="onSaveProfile">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 class="text-lg font-semibold text-slate-900">Maklumat Peribadi</h3>
<p class="mt-1 text-sm text-slate-500">
Kemas kini maklumat peribadi anda.
</p>
</div>
<Button type="submit" variant="primary" :disabled="saving">
{{ saving ? 'Menyimpan...' : 'Simpan' }}
</Button>
</div>
<FieldGroup>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<Field>
<FieldLabel for="profile-name">Nama</FieldLabel>
<Input id="profile-name" v-model="form.name" type="text" placeholder="Nama penuh" required />
</Field>
<Field>
<FieldLabel for="profile-email">E-mel</FieldLabel>
<Input id="profile-email" :model-value="authStore.user?.email ?? ''" type="email" disabled />
</Field>
<Field>
<FieldLabel for="profile-ic">No. Kad Pengenalan</FieldLabel>
<Input id="profile-ic" v-model="form.ic_number" type="text" placeholder="No. kad pengenalan" />
</Field>
<Field>
<FieldLabel for="profile-phone">No. Telefon</FieldLabel>
<Input id="profile-phone" v-model="form.phone_number" type="tel" pattern="[0-9]*"
placeholder="0123456789" />
</Field>
<Field class="md:col-span-2">
<FieldLabel for="profile-position">Jawatan</FieldLabel>
<Input id="profile-position" v-model="form.position" type="text" placeholder="Jawatan" />
</Field>
<Field>
<FieldLabel>Jantina</FieldLabel>
<SelectRoot :key="`profile-gender-${profileSelectKey}`" class="w-full" :collection="genderCollection"
:default-value="genderInitial" :disabled="saving" @value-change="setGenderValue">
<SelectControl>
<SelectTrigger :aria-invalid="!!profileErrors.gender">
<SelectValueText placeholder="Pilih jantina" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel>Jantina</SelectItemGroupLabel>
<SelectItem v-for="item in genderCollection.items" :key="item.label" :item="item">
<SelectItemText>{{ item.label }}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
<FieldError v-if="profileErrors.gender">{{ profileErrors.gender }}</FieldError>
</Field>
<Field>
<FieldLabel for="profile-age">Umur</FieldLabel>
<Input id="profile-age" :model-value="ageLabel" type="text" disabled />
</Field>
<Field>
<FieldLabel>Status Perkahwinan</FieldLabel>
<SelectRoot :key="`profile-marriage-${profileSelectKey}`" class="w-full"
:collection="marriageStatusCollection" :default-value="marriageStatusInitial" :disabled="saving"
@value-change="setMarriageStatusValue">
<SelectControl>
<SelectTrigger :aria-invalid="!!profileErrors.marriage_status">
<SelectValueText placeholder="Pilih status perkahwinan" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel>Status Perkahwinan</SelectItemGroupLabel>
<SelectItem v-for="item in marriageStatusCollection.items" :key="item.label" :item="item">
<SelectItemText>{{ item.label }}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
<FieldError v-if="profileErrors.marriage_status">{{ profileErrors.marriage_status }}</FieldError>
</Field>
<Field>
<FieldLabel for="profile-member-number">Nombor Anggota</FieldLabel>
<Input id="profile-member-number" :model-value="displayValue(authStore.userMemberNumber)" type="text"
disabled />
</Field>
<Field>
<FieldLabel for="profile-member-type">Jenis Anggota</FieldLabel>
<Input id="profile-member-type" :model-value="displayValue(authStore.userMemberType)" type="text"
disabled />
</Field>
<Field>
<FieldLabel for="profile-join-date">Tarikh Sertai</FieldLabel>
<Input id="profile-join-date" :model-value="formatDate(authStore.userJoinDate)" type="text" disabled />
</Field>
<Field>
<FieldLabel for="profile-birth-date">Tarikh Lahir</FieldLabel>
<Input id="profile-birth-date" v-model="form.birth_date" type="date" :disabled="saving"
:aria-invalid="!!profileErrors.birth_date" @input="clearProfileFieldError('birth_date')" />
<FieldError v-if="profileErrors.birth_date">{{ profileErrors.birth_date }}</FieldError>
</Field>
<Field class="md:col-span-2">
<FieldLabel for="profile-birth-place">Tempat Lahir</FieldLabel>
<Input id="profile-birth-place" v-model="form.birth_place" type="text" placeholder="Tempat lahir"
:disabled="saving" :aria-invalid="!!profileErrors.birth_place"
@input="clearProfileFieldError('birth_place')" />
<FieldError v-if="profileErrors.birth_place">{{ profileErrors.birth_place }}</FieldError>
</Field>
</div>
</FieldGroup>
</form>
</Box>
<Box raised="single" class="p-6">
<div class="space-y-6">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 class="text-lg font-semibold text-slate-900">Alamat</h3>
<p class="mt-1 text-sm text-slate-500">
Urus alamat rumah, pejabat, atau bil anda.
</p>
</div>
</div>
<div v-if="loadingAddresses" class="text-sm text-slate-500">
Memuatkan alamat...
</div>
<div v-else-if="addresses.length" class="space-y-3">
<div v-for="address in addresses" :key="address.id"
class="flex flex-col gap-4 rounded-lg border border-foreground/10 p-4 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<Badge look="outline">
{{ addressTypeLabel[address.address_type] ?? address.address_type }}
</Badge>
</div>
<p class="mt-2 text-sm text-slate-700">
{{ formatAddressLine(address) }}
</p>
</div>
<div class="flex shrink-0 gap-2">
<Button type="button" variant="ghost" class="border border-foreground/15 shadow-none"
:disabled="deletingAddressId === address.id" @click="startEditAddress(address)">
<Lucide class="mr-2 size-4" icon="Pencil" />
Kemaskini
</Button>
<Button type="button" variant="ghost" class="border border-foreground/15 shadow-none text-danger"
:disabled="deletingAddressId === address.id" @click="onDeleteAddress(address)">
<Lucide class="mr-2 size-4" :icon="deletingAddressId === address.id ? 'LoaderCircle' : 'Trash'"
:class="{ 'animate-spin': deletingAddressId === address.id }" />
Padam
</Button>
</div>
</div>
</div>
<div v-else class="rounded-lg border border-dashed border-foreground/15 p-6 text-center text-sm text-slate-500">
Tiada alamat direkodkan.
</div>
<form class="space-y-6 border-t border-foreground/10 pt-6" @submit.prevent="onSaveAddress">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h4 class="text-base font-semibold text-slate-900">
{{ isEditingAddress ? 'Kemaskini Alamat' : 'Tambah Alamat' }}
</h4>
<p class="mt-1 text-sm text-slate-500">
{{ isEditingAddress ?
'Kemas kini maklumat alamat yang dipilih.' : 'Tambah alamat baharu ke profil anda.' }}
</p>
</div>
<div class="flex gap-2">
<Button v-if="isEditingAddress" type="button" variant="ghost"
class="border border-foreground/15 shadow-none" :disabled="savingAddress" @click="resetAddressForm">
Batal
</Button>
<Button type="submit" variant="primary" :disabled="savingAddress">
{{ savingAddress ? 'Menyimpan...' : isEditingAddress ? 'Kemaskini' : 'Tambah' }}
</Button>
</div>
</div>
<!-- Address Form -->
<FieldGroup>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<Field>
<FieldLabel>Jenis Alamat</FieldLabel>
<SelectRoot :key="`address-type-${editingAddressId ?? 'new'}`" class="w-full"
:collection="addressTypeCollection" :default-value="addressTypeInitial" :disabled="savingAddress"
@value-change="setAddressTypeValue">
<SelectControl>
<SelectTrigger :aria-invalid="!!addressErrors.address_type">
<SelectValueText placeholder="Pilih jenis alamat" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel>Jenis Alamat</SelectItemGroupLabel>
<SelectItem v-for="item in addressTypeCollection.items" :key="item.label" :item="item">
<SelectItemText>{{ item.label }}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
<FieldError v-if="addressErrors.address_type">{{ addressErrors.address_type }}</FieldError>
</Field>
<Field>
<FieldLabel for="address-country">Negara</FieldLabel>
<Input id="address-country" v-model="addressForm.country" type="text" placeholder="Negara"
:aria-invalid="!!addressErrors.country" @input="clearAddressFieldError('country')" />
<FieldError v-if="addressErrors.country">{{ addressErrors.country }}</FieldError>
</Field>
<Field class="md:col-span-2">
<FieldLabel for="address-line-1">Alamat 1</FieldLabel>
<Input id="address-line-1" v-model="addressForm.address_line_1" type="text"
placeholder="Alamat baris pertama" :aria-invalid="!!addressErrors.address_line_1"
@input="clearAddressFieldError('address_line_1')" />
<FieldError v-if="addressErrors.address_line_1">{{ addressErrors.address_line_1 }}</FieldError>
</Field>
<Field class="md:col-span-2">
<FieldLabel for="address-line-2">Alamat 2</FieldLabel>
<Input id="address-line-2" v-model="addressForm.address_line_2" type="text"
placeholder="Alamat baris kedua (pilihan)" :aria-invalid="!!addressErrors.address_line_2"
@input="clearAddressFieldError('address_line_2')" />
<FieldError v-if="addressErrors.address_line_2">{{ addressErrors.address_line_2 }}</FieldError>
</Field>
<Field>
<FieldLabel for="address-city">Bandar</FieldLabel>
<Input id="address-city" v-model="addressForm.city" type="text" placeholder="Bandar"
:aria-invalid="!!addressErrors.city" @input="clearAddressFieldError('city')" />
<FieldError v-if="addressErrors.city">{{ addressErrors.city }}</FieldError>
</Field>
<Field>
<FieldLabel>Negeri</FieldLabel>
<SelectRoot :key="`address-state-${editingAddressId ?? 'new'}`" class="w-full"
:collection="stateCollection" :default-value="stateInitial" :disabled="savingAddress"
@value-change="setStateValue">
<SelectControl>
<SelectTrigger :aria-invalid="!!addressErrors.state">
<SelectValueText placeholder="Pilih negeri" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel>Negeri</SelectItemGroupLabel>
<SelectItem v-for="item in stateCollection.items" :key="item.label" :item="item">
<SelectItemText>{{ item.label }}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
<FieldError v-if="addressErrors.state">{{ addressErrors.state }}</FieldError>
</Field>
<Field>
<FieldLabel for="address-postcode">Poskod</FieldLabel>
<Input id="address-postcode" v-model="addressForm.postcode" type="text" placeholder="Poskod"
:aria-invalid="!!addressErrors.postcode" @input="clearAddressFieldError('postcode')" />
<FieldError v-if="addressErrors.postcode">{{ addressErrors.postcode }}</FieldError>
</Field>
</div>
</FieldGroup>
</form>
</div>
</Box>
</div>
</template>