refaktor címjegyzék

This commit is contained in:
Roo
2026-07-02 11:52:22 +00:00
parent 7654913d21
commit 07b59032ce
192 changed files with 2936 additions and 1376 deletions

View File

@@ -324,49 +324,21 @@
-->
<div v-if="activeTab === 'location'" class="max-w-3xl">
<form @submit.prevent="saveForm">
<!-- Smart Address Form (autocomplete zip/city + structured fields) -->
<SharedAddressForm v-model="form.address" />
<!-- Additional location fields -->
<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>
<h2 class="text-lg font-semibold text-white mb-4">További helyszín adatok</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="md:col-span-2">
<label class="block text-sm font-medium text-slate-300 mb-1">Teljes cím</label>
<label class="block text-sm font-medium text-slate-300 mb-1">Teljes cím szövege</label>
<input
v-model="form.address"
v-model="form.address.full_address_text"
placeholder="Teljes cím szövege"
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-sm font-medium text-slate-300 mb-1">Város</label>
<input
v-model="form.city"
placeholder="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>
<label class="block text-sm font-medium text-slate-300 mb-1">Irányítószám</label>
<input
v-model="form.address_zip"
placeholder="1011"
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-sm font-medium text-slate-300 mb-1">Utca</label>
<input
v-model="form.address_street_name"
placeholder="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>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Házszám</label>
<input
v-model="form.address_house_number"
placeholder="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>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-slate-300 mb-1">Plus Code</label>
<input
@@ -670,6 +642,21 @@ const form = reactive({
} as Record<string, string[]>,
opening_hours: {} as Record<string, { open: string; close: string } | null>,
is_always_open: false,
// Nested address object matching AddressIn schema (used by SharedAddressForm)
address: {
zip: '',
city: '',
street_name: '',
street_type: '',
house_number: '',
stairwell: '',
floor: '',
door: '',
parcel_id: '',
full_address_text: '',
latitude: null as number | null,
longitude: null as number | null,
},
})
// ── Computed ───────────────────────────────────────────
@@ -690,12 +677,14 @@ const hasChanges = computed(() => {
// Scalar field comparisons
if (form.name !== (p.name || '')) return true
if (form.address !== (p.address || '')) return true
if (form.city !== (p.city || '')) return true
if (form.address_zip !== (p.address_zip || '')) return true
if (form.address_street_name !== (p.address_street_name || '')) return true
if (form.address_street_type !== (p.address_street_type || '')) return true
if (form.address_house_number !== (p.address_house_number || '')) return true
if (form.plus_code !== (p.plus_code || '')) return true
// Nested address comparison (via SharedAddressForm)
if (form.address.zip !== (p.address_zip || '')) return true
if (form.address.city !== (p.city || '')) return true
if (form.address.street_name !== (p.address_street_name || '')) return true
if (form.address.street_type !== (p.address_street_type || '')) return true
if (form.address.house_number !== (p.address_house_number || '')) return true
if (form.contact_phone !== (p.contact_phone || '')) return true
if (form.contact_email !== (p.contact_email || '')) return true
if (form.website !== (p.website || '')) return true
@@ -777,11 +766,20 @@ function mapProviderToForm(p: ProviderDetail) {
form.name = p.name || ''
form.address = p.address || ''
form.city = p.city || ''
form.address_zip = p.address_zip || ''
form.address_street_name = p.address_street_name || ''
form.address_street_type = p.address_street_type || ''
form.address_house_number = p.address_house_number || ''
form.plus_code = p.plus_code || ''
// Populate nested address object from address_detail (unified AddressOut)
const addr = p.address_detail || {}
form.address_zip = addr.zip || ''
form.address_street_name = addr.street_name || ''
form.address_street_type = addr.street_type || ''
form.address_house_number = addr.house_number || ''
form.address.zip = addr.zip || ''
form.address.city = addr.city || p.city || ''
form.address.street_name = addr.street_name || ''
form.address.street_type = addr.street_type || ''
form.address.house_number = addr.house_number || ''
form.address.full_address_text = addr.full_address_text || p.address || ''
form.contact_phone = p.contact_phone || ''
form.contact_email = p.contact_email || ''
form.website = p.website || ''
@@ -1050,7 +1048,7 @@ async function saveForm() {
// CRITICAL BUGFIX (2026-07-01): address is NOT NULL in the DB schema,
// so we must never send null for it. Fall back to name if empty.
const safeAddress = form.address?.trim() || form.name
const safeAddress = form.address?.full_address_text?.trim() || form.name
const payload: Record<string, any> = {
name: form.name,
@@ -1059,17 +1057,27 @@ async function saveForm() {
contact_phone: form.contact_phone || null,
contact_email: form.contact_email || null,
website: form.website || null,
city: form.city || null,
address_zip: form.address_zip || null,
address_street_name: form.address_street_name || null,
address_street_type: form.address_street_type || null,
address_house_number: form.address_house_number || null,
plus_code: form.plus_code || null,
category_ids: form.category_ids,
supported_vehicle_classes: form.supported_vehicle_classes,
specializations: form.specializations,
opening_hours: form.opening_hours,
is_always_open: form.is_always_open,
// Send nested address object matching AddressIn schema
address_detail: {
zip: form.address.zip || null,
city: form.address.city || null,
street_name: form.address.street_name || null,
street_type: form.address.street_type || null,
house_number: form.address.house_number || null,
stairwell: form.address.stairwell || null,
floor: form.address.floor || null,
door: form.address.door || null,
parcel_id: form.address.parcel_id || null,
full_address_text: safeAddress,
latitude: form.address.latitude,
longitude: form.address.longitude,
},
}
try {

View File

@@ -280,8 +280,17 @@
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">Moderációs műveletek</h2>
<!-- Pending Actions -->
<div v-if="provider.status === 'pending'" class="space-y-3">
<!-- Pending / Research In Progress Actions -->
<div v-if="provider.status === 'pending' || provider.status === 'research_in_progress'" class="space-y-3">
<NuxtLink
:to="`/providers/${provider.id}/edit`"
class="w-full px-4 py-3 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-sm font-medium transition flex items-center justify-center gap-2"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Szerkesztés
</NuxtLink>
<button
@click="approveProvider"
:disabled="actionLoading"
@@ -506,6 +515,62 @@
</div>
</div>
<!-- Tab: Crawler / Nyers Adatok -->
<div v-if="activeTab === 'crawler'" class="max-w-4xl">
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">Crawler / Nyers Adatok</h2>
<p class="text-sm text-slate-400 mb-6">A robot által begyűjtött nyers adatok és metaadatok.</p>
<!-- Badges Row -->
<div class="flex flex-wrap gap-3 mb-6">
<!-- Source Badge -->
<div class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-slate-700 rounded-lg text-xs font-medium">
<span class="text-slate-400">Forrás:</span>
<code class="text-cyan-300 font-mono">{{ provider.source || '—' }}</code>
</div>
<!-- Trust Score Badge -->
<div
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium"
:class="trustScoreBadgeClass"
>
<span class="text-slate-400">Trust Score:</span>
<span class="font-semibold">{{ provider.trust_score ?? '—' }}</span>
</div>
</div>
<!-- Rejection Reason Alert -->
<div
v-if="provider.rejection_reason"
class="flex items-start gap-3 p-4 mb-6 bg-rose-500/10 border border-rose-500/30 rounded-lg"
>
<svg class="w-5 h-5 text-rose-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<div>
<p class="text-sm font-medium text-rose-300">Elutasítás indoklása</p>
<p class="text-sm text-rose-200/70 mt-0.5">{{ provider.rejection_reason }}</p>
</div>
</div>
<!-- Raw Data Viewer -->
<div class="mb-6">
<h3 class="text-sm font-medium text-slate-300 mb-3">Nyers adatok (raw_data)</h3>
<div class="bg-slate-900/50 rounded-lg border border-slate-700 p-4 overflow-x-auto">
<RawDataViewer :data="provider.raw_data" />
</div>
</div>
<!-- Audit Trail -->
<div v-if="provider.audit_trail && Object.keys(provider.audit_trail).length > 0">
<h3 class="text-sm font-medium text-slate-300 mb-3">Audit Trail</h3>
<div class="bg-slate-900/50 rounded-lg border border-slate-700 p-4 overflow-x-auto">
<RawDataViewer :data="provider.audit_trail" />
</div>
</div>
</div>
</div>
<!-- Tab: History -->
<div v-if="activeTab === 'history'">
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
@@ -687,7 +752,7 @@ const showFlagModal = ref(false)
const flagReason = ref('')
// Tabs
const activeTab = ref<'data' | 'status' | 'validations' | 'history'>('data')
const activeTab = ref<'data' | 'status' | 'validations' | 'history' | 'crawler'>('data')
// Validations
const validations = ref<any[]>([])
@@ -710,6 +775,7 @@ const tabs = [
{ key: 'data', label: 'Adatok', icon: 'svg' },
{ key: 'status', label: 'Státusz', icon: 'svg' },
{ key: 'validations', label: 'Validációk', icon: 'svg' },
{ key: 'crawler', label: 'Crawler / Nyers Adatok', icon: 'svg' },
{ key: 'history', label: 'Előzmények', icon: 'svg' },
]
@@ -718,6 +784,7 @@ const statusBannerClass = computed(() => {
switch (provider.value?.status) {
case 'approved': return 'bg-emerald-500/10 border-emerald-500/30'
case 'pending': return 'bg-amber-500/10 border-amber-500/30'
case 'research_in_progress': return 'bg-blue-500/10 border-blue-500/30'
default: return 'bg-rose-500/10 border-rose-500/30'
}
})
@@ -726,6 +793,7 @@ const statusIconClass = computed(() => {
switch (provider.value?.status) {
case 'approved': return 'bg-emerald-500/20 text-emerald-400'
case 'pending': return 'bg-amber-500/20 text-amber-400'
case 'research_in_progress': return 'bg-blue-500/20 text-blue-400'
default: return 'bg-rose-500/20 text-rose-400'
}
})
@@ -734,6 +802,7 @@ const statusTextClass = computed(() => {
switch (provider.value?.status) {
case 'approved': return 'text-emerald-400'
case 'pending': return 'text-amber-400'
case 'research_in_progress': return 'text-blue-400'
default: return 'text-rose-400'
}
})
@@ -742,6 +811,7 @@ const statusSubtextClass = computed(() => {
switch (provider.value?.status) {
case 'approved': return 'text-emerald-400/60'
case 'pending': return 'text-amber-400/60'
case 'research_in_progress': return 'text-blue-400/60'
default: return 'text-rose-400/60'
}
})
@@ -768,6 +838,18 @@ const hasSpecializations = computed(() => {
(Array.isArray(spec.propulsion) && spec.propulsion.length > 0)
})
/**
* Trust score badge color based on value.
* Green if > 80, yellow if > 50, red if <= 50.
*/
const trustScoreBadgeClass = computed(() => {
const score = provider.value?.trust_score
if (score === null || score === undefined) return 'bg-slate-700 text-slate-400'
if (score > 80) return 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
if (score > 50) return 'bg-amber-500/20 text-amber-400 border border-amber-500/30'
return 'bg-rose-500/20 text-rose-400 border border-rose-500/30'
})
// ── DAYS constant ──────────────────────────────────────
const DAYS = [
{ key: 'monday', label: 'Hétfő' },
@@ -797,6 +879,7 @@ function statusLabel(status: string): string {
switch (status) {
case 'approved': return 'Jóváhagyva'
case 'pending': return 'Függőben'
case 'research_in_progress': return 'Kutatás folyamatban'
case 'rejected': return 'Elutasítva'
default: return 'Ismeretlen'
}
@@ -806,6 +889,7 @@ function statusDescription(status: string): string {
switch (status) {
case 'approved': return 'A szolgáltató aktív és látható a felhasználók számára.'
case 'pending': return 'A szolgáltató még nem lett jóváhagyva, moderálásra vár.'
case 'research_in_progress': return 'A szolgáltató adatai még feldolgozás alatt állnak, automatikus kutatás folyik.'
case 'rejected': return 'A szolgáltató elutasításra került, nem látható a felhasználók számára.'
default: return ''
}