240 lines
9.6 KiB
Vue
240 lines
9.6 KiB
Vue
<template>
|
|
<div class="flex flex-col gap-5">
|
|
<div class="flex justify-between items-center">
|
|
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1>
|
|
<UButton color="info" @click="openModal()">
|
|
<UIcon name="mdi:add-bold" />
|
|
{{ t('Add Address') }}
|
|
</UButton>
|
|
</div>
|
|
|
|
<div v-if="store.loading" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
{{ t('Loading...') }}
|
|
</div>
|
|
<div v-else-if="store.error" class="text-center py-8 text-red-500">
|
|
{{ store.error }}
|
|
</div>
|
|
<div v-else-if="store.addresses.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<div v-for="addr in store.addresses" :key="addr.id"
|
|
class="border border-(--border-light) dark:border-(--border-dark) rounded-md p-4 bg-(--second-light) dark:bg-(--main-dark) flex justify-between">
|
|
<div class="flex flex-col gap-1">
|
|
<p class="font-semibold text-black dark:text-white">{{ addr.address_unparsed.recipient }}</p>
|
|
<p class="text-sm text-black dark:text-white">
|
|
{{ addr.address_unparsed.street }} {{ addr.address_unparsed.building_no
|
|
}}{{ addr.address_unparsed.apartment_no ? '/' + addr.address_unparsed.apartment_no : '' }}
|
|
</p>
|
|
<p class="text-sm text-black dark:text-white">
|
|
{{ addr.address_unparsed.postal_code }}, {{ addr.address_unparsed.city }}
|
|
</p>
|
|
</div>
|
|
<div class="flex flex-col items-end justify-between">
|
|
<UButton size="xs" color="error" variant="ghost" :title="t('Delete')"
|
|
@click="confirmDelete(addr.id)">
|
|
<UIcon name="material-symbols:delete" class="text-[18px]" />
|
|
</UButton>
|
|
<UButton size="sm" color="neutral" variant="outline" @click="openModal(addr)">
|
|
{{ t('edit') }}
|
|
<UIcon name="ic:sharp-edit" class="text-[14px]" />
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
{{ t('No addresses found') }}
|
|
</div>
|
|
|
|
<UModal v-model:open="showModal">
|
|
<template #header>
|
|
<h3 class="text-lg font-semibold text-black dark:text-white">
|
|
{{ editingId ? t('Edit Address') : t('Add Address') }}
|
|
</h3>
|
|
</template>
|
|
<template #body>
|
|
<div class="flex flex-col gap-5">
|
|
<USelectMenu v-model="selectedCountry" :items="countries" class="w-full"
|
|
@update:model-value="onCountryChange" :searchInput="false">
|
|
<template #default>
|
|
<div class="flex flex-col items-start leading-tight">
|
|
<span class="text-xs text-gray-400">{{ t('Country') }}</span>
|
|
<span v-if="selectedCountry" class="font-medium text-black dark:text-white">
|
|
{{ selectedCountry.name }}
|
|
</span>
|
|
<span v-else class="text-gray-400">{{ t('Select country') }}</span>
|
|
</div>
|
|
</template>
|
|
<template #item-leading="{ item }">
|
|
<span class="text-lg mr-1">{{ item.flag }} {{ item.name }}</span>
|
|
</template>
|
|
</USelectMenu>
|
|
|
|
<div v-if="templateLoading" class="text-center py-4 text-gray-500 dark:text-gray-400">
|
|
{{ t('Loading...') }}
|
|
</div>
|
|
<p v-else-if="!selectedCountry" class="text-center text-sm text-gray-400 dark:text-gray-500">
|
|
{{ t('Select a country to continue') }}
|
|
</p>
|
|
<UForm v-else :validate="validate" :state="formData" @submit="save" class="space-y-4">
|
|
<UFormField v-for="field in templateKeys" :key="field" :label="fieldLabel(field)" :name="field"
|
|
:required="!optionalFields.has(field)">
|
|
<UInput v-model="formData[field]" :placeholder="fieldLabel(field)" class="w-full" />
|
|
</UFormField>
|
|
|
|
<div class="flex justify-end gap-2 pt-2">
|
|
<UButton variant="outline" color="neutral" @click="showModal = false">
|
|
{{ t('Cancel') }}
|
|
</UButton>
|
|
<UButton type="submit" color="info">
|
|
{{ t('Save') }}
|
|
</UButton>
|
|
</div>
|
|
</UForm>
|
|
</div>
|
|
</template>
|
|
</UModal>
|
|
|
|
<UModal v-model:open="showDeleteConfirm">
|
|
<template #body>
|
|
<div class="flex flex-col items-center gap-3 py-2">
|
|
<UIcon name="f7:exclamationmark-triangle" class="text-[40px] text-red-600" />
|
|
<p class="font-semibold text-black dark:text-white">{{ t('Confirm Delete') }}</p>
|
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
|
{{ t('Are you sure you want to delete this address?') }}
|
|
</p>
|
|
</div>
|
|
</template>
|
|
<template #footer>
|
|
<div class="flex justify-center gap-4">
|
|
<UButton variant="outline" color="neutral" @click="showDeleteConfirm = false">
|
|
{{ t('Cancel') }}
|
|
</UButton>
|
|
<UButton variant="outline" color="error" @click="deleteAddress">
|
|
{{ t('Delete') }}
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
</UModal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { countries } from '@/router/langs'
|
|
import { useAddressStore } from '@/stores/customer/address'
|
|
import type { Country } from '@/types'
|
|
import type { Address } from '@/stores/customer/address'
|
|
|
|
const { t } = useI18n()
|
|
const store = useAddressStore()
|
|
|
|
// --- Modal state ---
|
|
const showModal = ref(false)
|
|
const editingId = ref<number | null>(null)
|
|
const selectedCountry = ref<Country | null>(null)
|
|
const templateLoading = ref(false)
|
|
const template = ref<Record<string, string>>({})
|
|
const formData = reactive<Record<string, string>>({})
|
|
|
|
const templateKeys = computed(() => Object.keys(template.value))
|
|
const optionalFields = new Set(['address_line2'])
|
|
|
|
// --- Delete state ---
|
|
const showDeleteConfirm = ref(false)
|
|
const deleteId = ref<number | null>(null)
|
|
|
|
const fieldLabels: Record<string, string> = {
|
|
recipient: 'Recipient',
|
|
street: 'Street',
|
|
thoroughfare: 'Street',
|
|
building_no: 'Building No',
|
|
building_name: 'Building Name',
|
|
house_number: 'House Number',
|
|
orientation_number: 'Orientation Number',
|
|
apartment_no: 'Apartment No',
|
|
sub_building: 'Sub Building',
|
|
postal_code: 'Zip Code',
|
|
post_town: 'City',
|
|
city: 'City',
|
|
county: 'County',
|
|
region: 'Region',
|
|
voivodeship: 'Region / Voivodeship',
|
|
address_line2: 'Address Line 2'
|
|
}
|
|
|
|
function fieldLabel(key: string) {
|
|
return t(fieldLabels[key] ?? key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()))
|
|
}
|
|
|
|
function validate() {
|
|
return templateKeys.value
|
|
.filter((key) => !optionalFields.has(key) && !formData[key]?.trim())
|
|
.map((key) => ({ name: key, message: t(`${fieldLabel(key)} is required`) }))
|
|
}
|
|
|
|
function applyTemplate(tpl: Record<string, string>, existing?: Record<string, string>) {
|
|
Object.keys(formData).forEach((k) => delete formData[k])
|
|
Object.keys(tpl).forEach((k) => {
|
|
formData[k] = existing?.[k] ?? ''
|
|
})
|
|
}
|
|
|
|
function openModal(addr?: Address) {
|
|
template.value = {}
|
|
Object.keys(formData).forEach((k) => delete formData[k])
|
|
|
|
if (addr) {
|
|
editingId.value = addr.id
|
|
selectedCountry.value = countries.find((c) => c.id === addr.country_id) ?? null
|
|
loadTemplate(addr.country_id, addr.address_unparsed)
|
|
} else {
|
|
editingId.value = null
|
|
selectedCountry.value = null
|
|
}
|
|
|
|
showModal.value = true
|
|
}
|
|
|
|
async function onCountryChange(country: Country | null) {
|
|
if (!country) {
|
|
template.value = {}
|
|
Object.keys(formData).forEach((k) => delete formData[k])
|
|
return
|
|
}
|
|
await loadTemplate(country.id)
|
|
}
|
|
|
|
async function loadTemplate(countryId: number, existing?: Record<string, string>) {
|
|
templateLoading.value = true
|
|
try {
|
|
const tpl = await store.getTemplate(countryId)
|
|
template.value = tpl
|
|
applyTemplate(tpl, existing)
|
|
} finally {
|
|
templateLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function save() {
|
|
if (!selectedCountry.value) return
|
|
if (editingId.value) {
|
|
await store.updateAddress(editingId.value, selectedCountry.value.id, { ...formData })
|
|
} else {
|
|
await store.createAddress(selectedCountry.value.id, { ...formData })
|
|
}
|
|
showModal.value = false
|
|
}
|
|
|
|
function confirmDelete(id: number) {
|
|
deleteId.value = id
|
|
showDeleteConfirm.value = true
|
|
}
|
|
|
|
async function deleteAddress() {
|
|
if (deleteId.value) await store.deleteAddress(deleteId.value)
|
|
showDeleteConfirm.value = false
|
|
deleteId.value = null
|
|
}
|
|
|
|
store.fetchAddresses()
|
|
</script>
|