frontend admin refakctorálás

This commit is contained in:
Roo
2026-07-08 08:03:57 +00:00
parent 07b59032ce
commit 2e0abc62a7
455 changed files with 14428 additions and 21651 deletions

View File

@@ -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.
}
/**

View File

@@ -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>

View File

@@ -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>