frontend admin refakctorálás
This commit is contained in:
@@ -12,6 +12,30 @@
|
||||
label="Irányítószám / Város"
|
||||
placeholder="Kezdj el gépelni (pl. 1011 vagy Budapest)..."
|
||||
/>
|
||||
<!-- P0 BUGFIX: Fallback plain text inputs for unknown zip/city
|
||||
When the autocomplete returns no results, the user can still
|
||||
type any zip/city combination manually. These fields are always
|
||||
visible and editable, ensuring data is never lost. -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-400 mb-1">Irányítószám (kézi)</label>
|
||||
<input
|
||||
:value="modelValue.zip"
|
||||
@input="updateField('zip', ($event.target as HTMLInputElement).value)"
|
||||
placeholder="pl. 1151"
|
||||
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-400 mb-1">Város (kézi)</label>
|
||||
<input
|
||||
:value="modelValue.city"
|
||||
@input="updateField('city', ($event.target as HTMLInputElement).value)"
|
||||
placeholder="pl. Budapest"
|
||||
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Street name -->
|
||||
@@ -205,17 +229,23 @@ const selectedLocation = computed<LocationItem | null>(() => {
|
||||
/**
|
||||
* Handle location selection from SmartLocationInput.
|
||||
* Updates the zip and city fields in the address model.
|
||||
*
|
||||
* P0 BUGFIX: When the autocomplete emits null (user types something
|
||||
* that doesn't match any known location), we NO LONGER clear zip/city.
|
||||
* Instead, we preserve the existing values. The user can manually edit
|
||||
* zip/city using the fallback plain text inputs below the autocomplete.
|
||||
* This ensures that typing "1151 Budapest" (which may not be in the
|
||||
* autocomplete DB) doesn't silently erase the data.
|
||||
*/
|
||||
function onLocationSelected(location: LocationItem | null) {
|
||||
const updated = { ...props.modelValue }
|
||||
if (location) {
|
||||
const updated = { ...props.modelValue }
|
||||
updated.zip = location.zip_code
|
||||
updated.city = location.city
|
||||
} else {
|
||||
updated.zip = null
|
||||
updated.city = null
|
||||
emit('update:modelValue', updated)
|
||||
}
|
||||
emit('update:modelValue', updated)
|
||||
// P0 BUGFIX: When location is null, do NOT clear zip/city.
|
||||
// The fallback text inputs allow manual entry of unknown locations.
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
@change="$emit('toggle', node.id)"
|
||||
class="w-4 h-4 rounded border-slate-500 bg-slate-700 text-indigo-500 focus:ring-indigo-500/50"
|
||||
/>
|
||||
<span class="text-sm text-slate-300">{{ node.name_hu || node.name_en || node.key }}</span>
|
||||
<span class="text-sm text-slate-300">{{ resolveNodeLabel(node) }}</span>
|
||||
<span class="text-xs text-slate-500">(Szint {{ node.level }})</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -42,19 +42,27 @@
|
||||
/**
|
||||
* Recursive TreeNode component for the category tree.
|
||||
* Renders a checkbox tree with expand/collapse for nested categories.
|
||||
*
|
||||
* i18n Resolution (Phase 4):
|
||||
* Uses name_i18n JSONB dictionary with fallback chain:
|
||||
* 1. Active language in name_i18n dict
|
||||
* 2. Hungarian (hu) in name_i18n dict
|
||||
* 3. Deprecated name_hu field
|
||||
* 4. Fallback to node.key
|
||||
*/
|
||||
interface CategoryTreeNode {
|
||||
id: number
|
||||
key: string
|
||||
name_hu?: string | null
|
||||
name_en?: string | null
|
||||
name_i18n?: Record<string, string> | null
|
||||
level: number
|
||||
path?: string | null
|
||||
is_official: boolean
|
||||
children: CategoryTreeNode[]
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
node: CategoryTreeNode
|
||||
selectedIds: number[]
|
||||
expandedIds: Set<number>
|
||||
@@ -64,4 +72,42 @@ defineEmits<{
|
||||
toggle: [id: number]
|
||||
'toggle-expand': [id: number]
|
||||
}>()
|
||||
|
||||
// ── i18n locale (dynamic) ─────────────────────────────
|
||||
let currentLocale = 'hu'
|
||||
try {
|
||||
const i18n = useI18n()
|
||||
currentLocale = i18n.locale?.value || i18n.locale || 'hu'
|
||||
} catch {
|
||||
// useI18n() may throw outside of setup context; fallback to 'hu'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a tree node's display label using i18n JSONB dictionary.
|
||||
*
|
||||
* Fallback chain:
|
||||
* 1. Active language in name_i18n dict
|
||||
* 2. Hungarian (hu) in name_i18n dict
|
||||
* 3. Deprecated name_hu field
|
||||
* 4. Fallback to node.key
|
||||
*/
|
||||
function resolveNodeLabel(node: CategoryTreeNode): string {
|
||||
// 1. Try active language in i18n dict
|
||||
if (node.name_i18n?.[currentLocale]) {
|
||||
return node.name_i18n[currentLocale]
|
||||
}
|
||||
|
||||
// 2. Fallback to Hungarian in i18n dict
|
||||
if (node.name_i18n?.hu) {
|
||||
return node.name_i18n.hu
|
||||
}
|
||||
|
||||
// 3. Fallback to deprecated name_hu (safe transition)
|
||||
if (node.name_hu) {
|
||||
return node.name_hu
|
||||
}
|
||||
|
||||
// 4. Fallback to key
|
||||
return node.key
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -157,39 +157,46 @@ function formatDate(dateStr: string | null | undefined): string {
|
||||
}
|
||||
}
|
||||
|
||||
const formattedAddress = computed(() => {
|
||||
if (!props.garage) return ''
|
||||
const parts = [
|
||||
props.garage.address_zip,
|
||||
props.garage.address_city,
|
||||
props.garage.address_street_name,
|
||||
props.garage.address_street_type,
|
||||
props.garage.address_house_number,
|
||||
/**
|
||||
* Format an address object (address_detail) into a display string.
|
||||
* Falls back to flat fields (address_zip, address_city, etc.) if address_detail is not available.
|
||||
*/
|
||||
function formatAddress(addr: Record<string, any> | null | undefined, flatPrefix: string = 'address'): string {
|
||||
if (!props.garage) return '-'
|
||||
|
||||
// Try address_detail first
|
||||
if (addr && typeof addr === 'object') {
|
||||
const parts = [
|
||||
addr.zip,
|
||||
addr.city,
|
||||
addr.street_name,
|
||||
addr.street_type,
|
||||
addr.house_number,
|
||||
].filter(Boolean)
|
||||
if (parts.length > 0) return parts.join(' ')
|
||||
if (addr.full_address_text) return addr.full_address_text
|
||||
}
|
||||
|
||||
// Fallback to flat fields
|
||||
const flatParts = [
|
||||
props.garage[`${flatPrefix}_zip`],
|
||||
props.garage[`${flatPrefix}_city`],
|
||||
props.garage[`${flatPrefix}_street_name`],
|
||||
props.garage[`${flatPrefix}_street_type`],
|
||||
props.garage[`${flatPrefix}_house_number`],
|
||||
].filter(Boolean)
|
||||
return parts.join(' ') || '-'
|
||||
return flatParts.join(' ') || '-'
|
||||
}
|
||||
|
||||
const formattedAddress = computed(() => {
|
||||
return formatAddress(props.garage?.address_detail, 'address')
|
||||
})
|
||||
|
||||
const formattedBillingAddress = computed(() => {
|
||||
if (!props.garage) return ''
|
||||
const parts = [
|
||||
props.garage.billing_zip,
|
||||
props.garage.billing_city,
|
||||
props.garage.billing_street_name,
|
||||
props.garage.billing_street_type,
|
||||
props.garage.billing_house_number,
|
||||
].filter(Boolean)
|
||||
return parts.join(' ') || ''
|
||||
return formatAddress(props.garage?.billing_address_detail, 'billing')
|
||||
})
|
||||
|
||||
const formattedNotificationAddress = computed(() => {
|
||||
if (!props.garage) return ''
|
||||
const parts = [
|
||||
props.garage.notification_zip,
|
||||
props.garage.notification_city,
|
||||
props.garage.notification_street_name,
|
||||
props.garage.notification_street_type,
|
||||
props.garage.notification_house_number,
|
||||
].filter(Boolean)
|
||||
return parts.join(' ') || ''
|
||||
return formatAddress(props.garage?.notification_address_detail, 'notification')
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user