260 lines
9.8 KiB
Vue
260 lines
9.8 KiB
Vue
<template>
|
|
<div>
|
|
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 mb-6">
|
|
<h2 class="text-lg font-semibold text-white mb-4">Cím adatok</h2>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<!-- Smart Location Input (zip + city autocomplete) -->
|
|
<div class="md:col-span-2">
|
|
<SmartLocationInput
|
|
:model-value="selectedLocation"
|
|
@update:model-value="onLocationSelected"
|
|
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 -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Utca neve</label>
|
|
<input
|
|
:value="modelValue.street_name"
|
|
@input="updateField('street_name', ($event.target as HTMLInputElement).value)"
|
|
placeholder="pl. Egressy"
|
|
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>
|
|
|
|
<!-- Street type -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Közterület jellege</label>
|
|
<select
|
|
:value="modelValue.street_type"
|
|
@change="updateField('street_type', ($event.target as HTMLSelectElement).value)"
|
|
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm"
|
|
>
|
|
<option value="">Válassz...</option>
|
|
<option value="utca">utca</option>
|
|
<option value="út">út</option>
|
|
<option value="tér">tér</option>
|
|
<option value="körút">körút</option>
|
|
<option value="sugárút">sugárút</option>
|
|
<option value="köz">köz</option>
|
|
<option value="sétány">sétány</option>
|
|
<option value="lakópark">lakópark</option>
|
|
<option value="liget">liget</option>
|
|
<option value="erdő">erdő</option>
|
|
<option value="dűlő">dűlő</option>
|
|
<option value="major">major</option>
|
|
<option value="puszta">puszta</option>
|
|
<option value="sziget">sziget</option>
|
|
<option value="telep">telep</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- House number -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Házszám</label>
|
|
<input
|
|
:value="modelValue.house_number"
|
|
@input="updateField('house_number', ($event.target as HTMLInputElement).value)"
|
|
placeholder="pl. 4/b"
|
|
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>
|
|
|
|
<!-- Stairwell -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Lépcsőház</label>
|
|
<input
|
|
:value="modelValue.stairwell"
|
|
@input="updateField('stairwell', ($event.target as HTMLInputElement).value)"
|
|
placeholder="pl. 1"
|
|
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>
|
|
|
|
<!-- Floor -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Emelet</label>
|
|
<input
|
|
:value="modelValue.floor"
|
|
@input="updateField('floor', ($event.target as HTMLInputElement).value)"
|
|
placeholder="pl. 3"
|
|
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>
|
|
|
|
<!-- Door -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Ajtó</label>
|
|
<input
|
|
:value="modelValue.door"
|
|
@input="updateField('door', ($event.target as HTMLInputElement).value)"
|
|
placeholder="pl. A"
|
|
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>
|
|
|
|
<!-- Parcel ID (hrsz) -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Helyrajzi szám (hrsz)</label>
|
|
<input
|
|
:value="modelValue.parcel_id"
|
|
@input="updateField('parcel_id', ($event.target as HTMLInputElement).value)"
|
|
placeholder="pl. 12345/6"
|
|
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>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
/**
|
|
* SharedAddressForm.vue
|
|
* ======================
|
|
*
|
|
* Reusable address form component that uses SmartLocationInput for
|
|
* zip/city autocomplete and provides all other address fields.
|
|
*
|
|
* Usage:
|
|
* <SharedAddressForm v-model="form.address" />
|
|
*
|
|
* The v-model expects an object matching the AddressIn schema:
|
|
* {
|
|
* zip?: string
|
|
* city?: string
|
|
* street_name?: string
|
|
* street_type?: string
|
|
* house_number?: string
|
|
* stairwell?: string
|
|
* floor?: string
|
|
* door?: string
|
|
* parcel_id?: string
|
|
* full_address_text?: string
|
|
* latitude?: number
|
|
* longitude?: number
|
|
* }
|
|
*/
|
|
|
|
import { ref, computed } from 'vue'
|
|
|
|
interface AddressForm {
|
|
zip?: string | null
|
|
city?: string | null
|
|
street_name?: string | null
|
|
street_type?: string | null
|
|
house_number?: string | null
|
|
stairwell?: string | null
|
|
floor?: string | null
|
|
door?: string | null
|
|
parcel_id?: string | null
|
|
full_address_text?: string | null
|
|
latitude?: number | null
|
|
longitude?: number | null
|
|
}
|
|
|
|
interface LocationItem {
|
|
id: number
|
|
zip_code: string
|
|
city: string
|
|
country_code: string
|
|
}
|
|
|
|
const props = withDefaults(defineProps<{
|
|
modelValue?: AddressForm
|
|
}>(), {
|
|
modelValue: () => ({
|
|
zip: null,
|
|
city: null,
|
|
street_name: null,
|
|
street_type: null,
|
|
house_number: null,
|
|
stairwell: null,
|
|
floor: null,
|
|
door: null,
|
|
parcel_id: null,
|
|
full_address_text: null,
|
|
latitude: null,
|
|
longitude: null,
|
|
}),
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: AddressForm]
|
|
}>()
|
|
|
|
/**
|
|
* Computed property for the SmartLocationInput selected value.
|
|
* Converts the address zip/city to a LocationItem if both are present.
|
|
*/
|
|
const selectedLocation = computed<LocationItem | null>(() => {
|
|
if (props.modelValue?.zip && props.modelValue?.city) {
|
|
return {
|
|
id: 0, // We don't have the DB id here, but SmartLocationInput handles this
|
|
zip_code: props.modelValue.zip,
|
|
city: props.modelValue.city,
|
|
country_code: 'HU',
|
|
}
|
|
}
|
|
return 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) {
|
|
if (location) {
|
|
const updated = { ...props.modelValue }
|
|
updated.zip = location.zip_code
|
|
updated.city = location.city
|
|
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.
|
|
}
|
|
|
|
/**
|
|
* Update a single field in the address model.
|
|
*/
|
|
function updateField(field: keyof AddressForm, value: string) {
|
|
const updated = { ...props.modelValue }
|
|
;(updated as any)[field] = value || null
|
|
emit('update:modelValue', updated)
|
|
}
|
|
</script>
|