Files
service-finder/frontend_admin/pages/providers/[id]/index.vue
2026-07-02 11:52:22 +00:00

1244 lines
54 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<!-- Back Button & Header -->
<div class="mb-8">
<button
@click="goBack"
class="inline-flex items-center gap-1.5 text-sm text-slate-400 hover:text-white transition mb-4"
>
<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="M15 19l-7-7 7-7" />
</svg>
Vissza a szolgáltatókhoz
</button>
<h1 class="text-2xl font-bold text-white">Szolgáltató részletek</h1>
<p class="text-slate-400 mt-1">Szolgáltató #{{ $route.params.id }}</p>
</div>
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center py-20">
<div class="text-slate-400 flex items-center gap-3">
<svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>Szolgáltató betöltése...</span>
</div>
</div>
<!-- Error State -->
<div v-else-if="error" class="bg-rose-500/10 border border-rose-500/30 rounded-xl p-6 mb-8">
<p class="text-rose-400">Hiba történt a szolgáltató betöltése közben.</p>
<button @click="fetchProvider" class="mt-3 text-sm text-rose-300 hover:text-rose-200 underline">Újrapróbálkozás</button>
</div>
<!-- Not Found -->
<div v-else-if="!provider" class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<p class="text-slate-400 text-center py-12">A szolgáltató nem található.</p>
</div>
<template v-else>
<!-- Status Banner -->
<div
class="rounded-xl border p-4 mb-6 flex items-center gap-3"
:class="statusBannerClass"
>
<span class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center" :class="statusIconClass">
<svg v-if="provider.status === 'approved'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<svg v-else-if="provider.status === 'pending'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<svg v-else class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</span>
<div>
<p class="text-sm font-medium" :class="statusTextClass">{{ statusLabel(provider.status) }}</p>
<p class="text-xs mt-0.5" :class="statusSubtextClass">{{ statusDescription(provider.status) }}</p>
</div>
</div>
<!-- Tab Navigation -->
<div class="border-b border-slate-700 mb-6">
<nav class="flex gap-6 -mb-px">
<button
v-for="tab in tabs"
:key="tab.key"
@click="activeTab = tab.key"
class="pb-3 text-sm font-medium border-b-2 transition flex items-center gap-2"
:class="activeTab === tab.key ? 'text-indigo-400 border-indigo-400' : 'text-slate-400 border-transparent hover:text-slate-300'"
>
<component :is="tab.icon" class="w-4 h-4" />
{{ tab.label }}
</button>
</nav>
</div>
<!-- Tab: Data -->
<div v-if="activeTab === 'data'" class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Left Column: Basic Info -->
<div class="lg:col-span-2 space-y-6">
<!-- Basic Information -->
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">Alapadatok</h2>
<dl class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Név</dt>
<dd class="text-sm text-white mt-1 font-medium">{{ provider.name }}</dd>
</div>
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Kategória</dt>
<dd class="text-sm text-white mt-1">{{ provider.category || '—' }}</dd>
</div>
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Forrás</dt>
<dd class="mt-1">
<code class="text-sm font-mono text-amber-300 bg-amber-500/10 px-2 py-0.5 rounded">{{ provider.source }}</code>
</dd>
</div>
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Validációs pontszám</dt>
<dd class="mt-1">
<div class="flex items-center gap-2">
<div class="w-24 h-2 bg-slate-700 rounded-full overflow-hidden">
<div
class="h-full rounded-full transition-all"
:class="provider.validation_score >= 50 ? 'bg-emerald-500' : provider.validation_score >= 20 ? 'bg-amber-500' : 'bg-slate-500'"
:style="{ width: Math.min(provider.validation_score, 100) + '%' }"
/>
</div>
<span class="text-sm text-white font-medium">{{ provider.validation_score }}</span>
</div>
</dd>
</div>
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Létrehozva</dt>
<dd class="text-sm text-white mt-1">{{ formatDate(provider.created_at) }}</dd>
</div>
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Beküldő user ID</dt>
<dd class="text-sm text-white mt-1">{{ provider.added_by_user_id || '—' }}</dd>
</div>
</dl>
</div>
<!-- Address Information -->
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">Cím adatok</h2>
<dl class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="md:col-span-2">
<dt class="text-xs text-slate-500 uppercase tracking-wider">Teljes cím</dt>
<dd class="text-sm text-white mt-1">{{ provider.address || '—' }}</dd>
</div>
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Város</dt>
<dd class="text-sm text-white mt-1">{{ provider.city || '—' }}</dd>
</div>
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Irányítószám</dt>
<dd class="text-sm text-white mt-1">{{ provider.address_zip || '—' }}</dd>
</div>
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Utca</dt>
<dd class="text-sm text-white mt-1">{{ provider.address_street_name || '—' }}</dd>
</div>
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Házszám</dt>
<dd class="text-sm text-white mt-1">{{ provider.address_house_number || '—' }}</dd>
</div>
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Plus Code</dt>
<dd class="text-sm text-white mt-1">{{ provider.plus_code || '—' }}</dd>
</div>
</dl>
</div>
<!-- Contact Information -->
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">Elérhetőség</h2>
<dl class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Telefon</dt>
<dd class="text-sm text-white mt-1">{{ provider.contact_phone || '—' }}</dd>
</div>
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Email</dt>
<dd class="text-sm text-white mt-1">{{ provider.contact_email || '—' }}</dd>
</div>
<div>
<dt class="text-xs text-slate-500 uppercase tracking-wider">Weboldal</dt>
<dd class="text-sm text-white mt-1">
<a v-if="provider.website" :href="provider.website" target="_blank" class="text-indigo-400 hover:text-indigo-300 underline">
{{ provider.website }}
</a>
<span v-else></span>
</dd>
</div>
</dl>
</div>
<!-- Services (Categories) -->
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">Szolgáltatások</h2>
<div v-if="provider.category_ids && provider.category_ids.length > 0" class="flex flex-wrap gap-2">
<span
v-for="catId in provider.category_ids"
:key="catId"
class="inline-flex items-center gap-1 px-3 py-1.5 bg-indigo-500/10 text-indigo-300 rounded-full text-xs font-medium"
>
#{{ catId }}
</span>
</div>
<p v-else class="text-sm text-slate-500 italic">Nincsenek kategóriák beállítva</p>
</div>
<!-- Vehicle Classes -->
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">Járműosztályok</h2>
<div v-if="provider.supported_vehicle_classes && provider.supported_vehicle_classes.length > 0" class="flex flex-wrap gap-2">
<span
v-for="vc in provider.supported_vehicle_classes"
:key="vc"
class="inline-flex items-center gap-1 px-3 py-1.5 bg-emerald-500/10 text-emerald-300 rounded-full text-xs font-medium"
>
{{ vehicleClassLabel(vc) }}
</span>
</div>
<p v-else class="text-sm text-slate-500 italic">Nincsenek járműosztályok beállítva</p>
</div>
<!-- Specializations -->
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">Specializációk</h2>
<div v-if="hasSpecializations" class="space-y-3">
<!-- Brands -->
<div v-if="provider.specializations?.brands?.length">
<h3 class="text-xs text-slate-500 uppercase tracking-wider mb-2">Márkák</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="brand in provider.specializations.brands"
:key="brand"
class="inline-flex items-center gap-1 px-3 py-1.5 bg-amber-500/10 text-amber-300 rounded-full text-xs font-medium"
>
{{ brand }}
</span>
</div>
</div>
<!-- Propulsion -->
<div v-if="provider.specializations?.propulsion?.length">
<h3 class="text-xs text-slate-500 uppercase tracking-wider mb-2">Hajtáslánc</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="prop in provider.specializations.propulsion"
:key="prop"
class="inline-flex items-center gap-1 px-3 py-1.5 bg-cyan-500/10 text-cyan-300 rounded-full text-xs font-medium"
>
{{ prop }}
</span>
</div>
</div>
</div>
<p v-else class="text-sm text-slate-500 italic">Nincsenek specializációk beállítva</p>
</div>
<!-- Opening Hours -->
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">Nyitvatartás</h2>
<div v-if="hasOpeningHours" class="overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-700">
<th class="text-left text-xs text-slate-500 uppercase tracking-wider pb-2 pr-4">Nap</th>
<th class="text-left text-xs text-slate-500 uppercase tracking-wider pb-2">Nyitva</th>
</tr>
</thead>
<tbody>
<tr v-for="day in DAYS" :key="day.key" class="border-b border-slate-700/50 last:border-0">
<td class="py-2 pr-4 text-white font-medium">{{ day.label }}</td>
<td class="py-2">
<template v-if="getDayHours(day.key)">
<span class="text-white">{{ getDayHours(day.key)?.open }}</span>
<span class="text-slate-500 mx-1"></span>
<span class="text-white">{{ getDayHours(day.key)?.close }}</span>
</template>
<span v-else class="text-slate-500 italic">Zárva</span>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else>
<p class="text-sm text-slate-500 italic">Nincs beállítva nyitvatartás</p>
</div>
</div>
</div>
<!-- Right Column: Actions -->
<div class="space-y-4">
<!-- Moderation Actions -->
<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 / 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"
class="w-full px-4 py-3 bg-emerald-600 hover:bg-emerald-500 disabled:bg-emerald-600/50 text-white rounded-lg text-sm font-medium transition flex items-center justify-center gap-2"
>
<svg v-if="actionLoading" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<svg v-else 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="M5 13l4 4L19 7" />
</svg>
Jóváhagyás
</button>
<button
@click="openRejectModal"
:disabled="actionLoading"
class="w-full px-4 py-3 bg-rose-600/20 hover:bg-rose-600/40 disabled:opacity-50 text-rose-400 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="M6 18L18 6M6 6l12 12" />
</svg>
Elutasítás
</button>
</div>
<!-- Approved Actions -->
<div v-else-if="provider.status === 'approved'" 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="openFlagModal"
:disabled="actionLoading"
class="w-full px-4 py-3 bg-purple-600/20 hover:bg-purple-600/40 disabled:opacity-50 text-purple-400 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="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
</svg>
Megjelölés (Flag)
</button>
<button
@click="deleteProvider"
:disabled="actionLoading"
class="w-full px-4 py-3 bg-rose-600/20 hover:bg-rose-600/40 disabled:opacity-50 text-rose-400 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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Törlés (Soft Delete)
</button>
</div>
<!-- Rejected / Flagged Actions -->
<div v-else class="space-y-3">
<p class="text-sm text-slate-400 text-center py-2">
A szolgáltató {{ provider.status === 'rejected' ? 'elutasításra' : 'megjelölésre' }} került.
</p>
<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
v-if="provider.status === 'rejected'"
@click="restoreProvider"
:disabled="actionLoading"
class="w-full px-4 py-3 bg-emerald-600 hover:bg-emerald-500 disabled:bg-emerald-600/50 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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Visszaállítás
</button>
<button
@click="deleteProvider"
:disabled="actionLoading"
class="w-full px-4 py-3 bg-rose-600/20 hover:bg-rose-600/40 disabled:opacity-50 text-rose-400 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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Törlés
</button>
</div>
</div>
<!-- Evidence Image -->
<div v-if="provider.evidence_image_path" class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">Bizonyíték kép</h2>
<img
:src="provider.evidence_image_path"
alt="Evidence"
class="w-full rounded-lg border border-slate-600"
/>
</div>
</div>
</div>
<!-- Tab: Status -->
<div v-if="activeTab === 'status'" class="max-w-2xl">
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">Státusz kezelés</h2>
<p class="text-sm text-slate-400 mb-6">Módosítsd a szolgáltató státuszát és validációs pontszámát.</p>
<form @submit.prevent="saveStatusEdit">
<div class="space-y-4 mb-6">
<!-- Status Select -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Státusz</label>
<select
v-model="statusForm.status"
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="pending">Függőben</option>
<option value="approved">Jóváhagyott</option>
<option value="rejected">Elutasított</option>
</select>
</div>
<!-- Validation Score -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">
Validációs pontszám (0-100)
</label>
<div class="flex items-center gap-3">
<input
v-model.number="statusForm.validation_score"
type="range"
min="0"
max="100"
class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-indigo-500"
/>
<span class="text-sm text-white font-medium w-8 text-right">{{ statusForm.validation_score }}</span>
</div>
</div>
<!-- Reason -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Indoklás (opcionális)</label>
<textarea
v-model="statusForm.reason"
rows="3"
placeholder="Add meg a státuszváltoztatás okát..."
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 resize-none"
></textarea>
</div>
</div>
<div class="flex justify-end gap-3">
<button type="button" @click="resetStatusForm" class="px-4 py-2 text-sm text-slate-400 hover:text-white transition">Mégsem</button>
<button type="submit" :disabled="statusLoading" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:bg-indigo-600/50 text-white rounded-lg text-sm font-medium transition flex items-center gap-2">
<svg v-if="statusLoading" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Státusz mentése
</button>
</div>
</form>
</div>
</div>
<!-- Tab: Validations -->
<div v-if="activeTab === 'validations'">
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">Gamification Validációk</h2>
<p class="text-sm text-slate-400 mb-6">A szolgáltatóhoz tartozó gamification validációs bejegyzések listája.</p>
<!-- Loading -->
<div v-if="validationsLoading" class="flex items-center justify-center py-8">
<svg class="w-5 h-5 animate-spin text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<!-- Empty -->
<div v-else-if="validations.length === 0" class="text-center py-8">
<p class="text-slate-500">Nincsenek validációs bejegyzések ehhez a szolgáltatóhoz.</p>
</div>
<!-- Validation List -->
<div v-else class="space-y-3">
<div
v-for="v in validations"
:key="v.id"
class="bg-slate-700/50 rounded-lg border border-slate-600 p-4"
>
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span
class="text-xs font-medium px-2 py-0.5 rounded-full"
:class="validationTypeClass(v.validation_type)"
>
{{ validationTypeLabel(v.validation_type) }}
</span>
<span class="text-xs text-slate-500">súly: {{ v.weight }}</span>
</div>
<p class="text-sm text-slate-300">
<span class="text-slate-400">Szavazó:</span>
{{ v.voter_name || `User #${v.voter_user_id}` }}
</p>
<p v-if="v.validation_metadata?.reason" class="text-xs text-slate-500 mt-1">
Indoklás: {{ v.validation_metadata.reason }}
</p>
</div>
<div class="text-xs text-slate-500 whitespace-nowrap">
{{ formatDate(v.created_at) }}
</div>
</div>
</div>
</div>
</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">
<h2 class="text-lg font-semibold text-white mb-4">Állapotváltoztatások előzményei</h2>
<p class="text-sm text-slate-400 mb-6">Idővonal: ki, mikor és mit változtatott a szolgáltatóval kapcsolatban.</p>
<!-- Loading -->
<div v-if="historyLoading" class="flex items-center justify-center py-8">
<svg class="w-5 h-5 animate-spin text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<!-- Empty -->
<div v-else-if="history.length === 0" class="text-center py-8">
<p class="text-slate-500">Nincsenek előzmények ehhez a szolgáltatóhoz.</p>
</div>
<!-- Timeline -->
<div v-else class="relative">
<!-- Timeline line -->
<div class="absolute left-4 top-0 bottom-0 w-0.5 bg-slate-700"></div>
<div v-for="(entry, index) in history" :key="entry.id" class="relative pl-10 pb-6 last:pb-0">
<!-- Timeline dot -->
<div
class="absolute left-2.5 w-3 h-3 rounded-full border-2 mt-1.5"
:class="historyDotClass(entry.action)"
></div>
<div class="bg-slate-700/50 rounded-lg border border-slate-600 p-4">
<div class="flex items-start justify-between gap-4 mb-2">
<div>
<span
class="text-xs font-medium px-2 py-0.5 rounded-full"
:class="historyActionClass(entry.action)"
>
{{ historyActionLabel(entry.action) }}
</span>
<span class="text-xs text-slate-500 ml-2">
User #{{ entry.user_id || '?' }}
</span>
</div>
<div class="text-xs text-slate-500 whitespace-nowrap">
{{ formatDate(entry.timestamp) }}
</div>
</div>
<!-- Status change details -->
<div v-if="entry.new_data" class="text-xs text-slate-400 space-y-1 mt-2">
<div v-if="entry.new_data.old_status || entry.new_data.new_status" class="flex items-center gap-2">
<span class="text-slate-500">Státusz:</span>
<span class="text-rose-400">{{ entry.new_data.old_status || '?' }}</span>
<svg class="w-3 h-3 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<span class="text-emerald-400">{{ entry.new_data.new_status || '?' }}</span>
</div>
<div v-if="entry.new_data.reason && entry.new_data.reason !== 'Admin edit'">
<span class="text-slate-500">Indoklás:</span> {{ entry.new_data.reason }}
</div>
<!-- Field changes from PATCH update -->
<div v-if="entry.new_data.changes" class="mt-2">
<details class="text-xs">
<summary class="text-slate-500 cursor-pointer hover:text-slate-400">Részletes változások</summary>
<div class="mt-2 space-y-1">
<div v-for="(value, key) in entry.new_data.changes" :key="key" class="flex gap-2">
<span class="text-slate-500 w-28 flex-shrink-0">{{ key }}:</span>
<span class="text-slate-300 truncate">{{ value ?? '—' }}</span>
</div>
</div>
</details>
</div>
</div>
<!-- Old data diff -->
<div v-if="entry.old_data" class="mt-2">
<details class="text-xs">
<summary class="text-slate-500 cursor-pointer hover:text-slate-400">Régi adatok</summary>
<div class="mt-2 space-y-1">
<div v-for="(value, key) in entry.old_data" :key="key" class="flex gap-2">
<span class="text-slate-500 w-28 flex-shrink-0">{{ key }}:</span>
<span class="text-slate-300 truncate">{{ value ?? '—' }}</span>
</div>
</div>
</details>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- Reject Modal -->
<div v-if="showRejectModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showRejectModal = false">
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 w-full max-w-lg mx-4 shadow-2xl">
<h2 class="text-lg font-semibold text-white mb-2">Szolgáltató elutasítása</h2>
<p class="text-sm text-slate-400 mb-6">
Biztosan elutasítod a(z) <strong class="text-white">{{ provider?.name }}</strong> szolgáltatót?
</p>
<form @submit.prevent="rejectProvider">
<div class="mb-4">
<label class="block text-sm font-medium text-slate-300 mb-1">Indoklás (opcionális)</label>
<textarea
v-model="rejectReason"
rows="3"
placeholder="Add meg az elutasítás okát..."
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-rose-500/50 focus:border-rose-500 text-sm resize-none"
></textarea>
</div>
<div class="flex justify-end gap-3">
<button type="button" @click="showRejectModal = false" class="px-4 py-2 text-sm text-slate-400 hover:text-white transition">Mégse</button>
<button type="submit" :disabled="actionLoading" class="px-4 py-2 bg-rose-600 hover:bg-rose-500 disabled:bg-rose-600/50 text-white rounded-lg text-sm font-medium transition flex items-center gap-2">
<svg v-if="actionLoading" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Elutasítás
</button>
</div>
</form>
</div>
</div>
<!-- Flag Modal -->
<div v-if="showFlagModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showFlagModal = false">
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 w-full max-w-lg mx-4 shadow-2xl">
<h2 class="text-lg font-semibold text-white mb-2">Szolgáltató megjelölése</h2>
<p class="text-sm text-slate-400 mb-6">
Biztosan megjelölöd a(z) <strong class="text-white">{{ provider?.name }}</strong> szolgáltatót?
</p>
<form @submit.prevent="flagProvider">
<div class="mb-4">
<label class="block text-sm font-medium text-slate-300 mb-1">Indoklás (opcionális)</label>
<textarea
v-model="flagReason"
rows="3"
placeholder="Add meg a megjelölés okát..."
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-purple-500/50 focus:border-purple-500 text-sm resize-none"
></textarea>
</div>
<div class="flex justify-end gap-3">
<button type="button" @click="showFlagModal = false" class="px-4 py-2 text-sm text-slate-400 hover:text-white transition">Mégse</button>
<button type="submit" :disabled="actionLoading" class="px-4 py-2 bg-purple-600 hover:bg-purple-500 disabled:bg-purple-600/50 text-white rounded-lg text-sm font-medium transition flex items-center gap-2">
<svg v-if="actionLoading" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Megjelölés
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// ── State ──────────────────────────────────────────────
const loading = ref(true)
const error = ref(false)
const provider = ref<any>(null)
const actionLoading = ref(false)
// Toast
const toast = ref<{ message: string; type: 'success' | 'error' } | null>(null)
// Reject modal
const showRejectModal = ref(false)
const rejectReason = ref('')
// Flag modal
const showFlagModal = ref(false)
const flagReason = ref('')
// Tabs
const activeTab = ref<'data' | 'status' | 'validations' | 'history' | 'crawler'>('data')
// Validations
const validations = ref<any[]>([])
const validationsLoading = ref(false)
// History
const history = ref<any[]>([])
const historyLoading = ref(false)
// Status form
const statusForm = ref({
status: 'pending',
validation_score: 0,
reason: '',
})
const statusLoading = ref(false)
// ── Tab definitions ────────────────────────────────────
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' },
]
// ── Computed ───────────────────────────────────────────
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'
}
})
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'
}
})
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'
}
})
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'
}
})
/**
* Returns true if the provider has at least one day with opening hours set.
*/
const hasOpeningHours = computed(() => {
const hours = provider.value?.opening_hours
if (!hours || typeof hours !== 'object') return false
return DAYS.some(day => {
const slot = hours[day.key]
return slot && typeof slot === 'object' && slot.open && slot.close
})
})
/**
* Returns true if the provider has any specializations (brands or propulsion).
*/
const hasSpecializations = computed(() => {
const spec = provider.value?.specializations
if (!spec || typeof spec !== 'object') return false
return (Array.isArray(spec.brands) && spec.brands.length > 0) ||
(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ő' },
{ key: 'tuesday', label: 'Kedd' },
{ key: 'wednesday', label: 'Szerda' },
{ key: 'thursday', label: 'Csütörtök' },
{ key: 'friday', label: 'Péntek' },
{ key: 'saturday', label: 'Szombat' },
{ key: 'sunday', label: 'Vasárnap' },
]
/**
* Returns the opening hours slot for a given day key, or null if closed.
*/
function getDayHours(dayKey: string): { open: string; close: string } | null {
const hours = provider.value?.opening_hours
if (!hours || typeof hours !== 'object') return null
const slot = hours[dayKey]
if (slot && typeof slot === 'object' && slot.open && slot.close) {
return { open: slot.open, close: slot.close }
}
return null
}
// ── Helper functions ───────────────────────────────────
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'
}
}
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 ''
}
}
/**
* Maps vehicle class keys to Hungarian labels.
*/
function vehicleClassLabel(key: string): string {
const labels: Record<string, string> = {
car: 'Személyautó',
motorcycle: 'Motorkerékpár',
truck: 'Teherautó',
hgv: 'Nehézgépjármű (HGV)',
light_commercial: 'Kishaszonjármű',
personal: 'Személyes',
boat: 'Csónak/Vízi',
other: 'Egyéb',
}
return labels[key] || key
}
function validationTypeLabel(type: string): string {
const labels: Record<string, string> = {
phone_verified: 'Telefon igazolt',
email_verified: 'Email igazolt',
address_verified: 'Cím igazolt',
website_verified: 'Weboldal igazolt',
manual_review: 'Kézi ellenőrzés',
community_vouch: 'Közösségi ajánlás',
business_hours: 'Nyitvatartás ellenőrzött',
photo_evidence: 'Fénykép bizonyíték',
}
return labels[type] || type
}
function validationTypeClass(type: string): string {
const classes: Record<string, string> = {
phone_verified: 'bg-emerald-500/20 text-emerald-400',
email_verified: 'bg-blue-500/20 text-blue-400',
address_verified: 'bg-amber-500/20 text-amber-400',
website_verified: 'bg-purple-500/20 text-purple-400',
manual_review: 'bg-indigo-500/20 text-indigo-400',
community_vouch: 'bg-pink-500/20 text-pink-400',
business_hours: 'bg-cyan-500/20 text-cyan-400',
photo_evidence: 'bg-orange-500/20 text-orange-400',
}
return classes[type] || 'bg-slate-500/20 text-slate-400'
}
function historyActionLabel(action: string): string {
const labels: Record<string, string> = {
approve: 'Jóváhagyás',
reject: 'Elutasítás',
restore: 'Visszaállítás',
flag: 'Megjelölés',
update: 'Módosítás',
delete: 'Törlés',
create: 'Létrehozás',
}
return labels[action] || action
}
function historyActionClass(action: string): string {
const classes: Record<string, string> = {
approve: 'bg-emerald-500/20 text-emerald-400',
reject: 'bg-rose-500/20 text-rose-400',
restore: 'bg-blue-500/20 text-blue-400',
flag: 'bg-purple-500/20 text-purple-400',
update: 'bg-amber-500/20 text-amber-400',
delete: 'bg-red-500/20 text-red-400',
create: 'bg-green-500/20 text-green-400',
}
return classes[action] || 'bg-slate-500/20 text-slate-400'
}
function historyDotClass(action: string): string {
const classes: Record<string, string> = {
approve: 'border-emerald-500 bg-emerald-500/30',
reject: 'border-rose-500 bg-rose-500/30',
restore: 'border-blue-500 bg-blue-500/30',
flag: 'border-purple-500 bg-purple-500/30',
update: 'border-amber-500 bg-amber-500/30',
delete: 'border-red-500 bg-red-500/30',
create: 'border-green-500 bg-green-500/30',
}
return classes[action] || 'border-slate-500 bg-slate-500/30'
}
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return '—'
try {
const d = new Date(dateStr)
return d.toLocaleDateString('hu-HU', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit',
})
} catch {
return dateStr
}
}
// ── Auth helpers ───────────────────────────────────────
function getHeaders(): Record<string, string> {
const token = useCookie('access_token').value
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (token) headers['Authorization'] = `Bearer ${token}`
return headers
}
// ── API calls ──────────────────────────────────────────
async function fetchProvider() {
loading.value = true
error.value = false
try {
const res = await $fetch(`/api/v1/admin/providers/${route.params.id}`, {
headers: getHeaders(),
})
provider.value = res as any
// Initialize status form from provider data
statusForm.value.status = (res as any).status || 'pending'
statusForm.value.validation_score = (res as any).validation_score || 0
} catch (err: any) {
console.error('Failed to fetch provider:', err)
error.value = true
} finally {
loading.value = false
}
}
async function fetchValidations() {
validationsLoading.value = true
try {
const res = await $fetch(`/api/v1/admin/providers/${route.params.id}/validations`, {
headers: getHeaders(),
})
validations.value = (res as any[]) || []
} catch (err: any) {
console.error('Failed to fetch validations:', err)
validations.value = []
} finally {
validationsLoading.value = false
}
}
async function fetchHistory() {
historyLoading.value = true
try {
const res = await $fetch(`/api/v1/admin/providers/${route.params.id}/history`, {
headers: getHeaders(),
})
history.value = (res as any[]) || []
} catch (err: any) {
console.error('Failed to fetch history:', err)
history.value = []
} finally {
historyLoading.value = false
}
}
// ── Moderation actions ─────────────────────────────────
async function approveProvider() {
actionLoading.value = true
try {
await $fetch(`/api/v1/admin/providers/${route.params.id}/approve`, {
method: 'POST',
headers: getHeaders(),
})
showToast('Szolgáltató jóváhagyva!', 'success')
await fetchProvider()
} catch (err: any) {
showToast(err?.data?.detail || 'Hiba a jóváhagyás során', 'error')
} finally {
actionLoading.value = false
}
}
async function rejectProvider() {
actionLoading.value = true
try {
await $fetch(`/api/v1/admin/providers/${route.params.id}/reject`, {
method: 'POST',
headers: getHeaders(),
body: { reason: rejectReason.value || undefined },
})
showRejectModal.value = false
rejectReason.value = ''
showToast('Szolgáltató elutasítva!', 'success')
await fetchProvider()
} catch (err: any) {
showToast(err?.data?.detail || 'Hiba az elutasítás során', 'error')
} finally {
actionLoading.value = false
}
}
async function restoreProvider() {
actionLoading.value = true
try {
await $fetch(`/api/v1/admin/providers/${route.params.id}/restore`, {
method: 'POST',
headers: getHeaders(),
body: {},
})
showToast('Szolgáltató visszaállítva!', 'success')
await fetchProvider()
} catch (err: any) {
showToast(err?.data?.detail || 'Hiba a visszaállítás során', 'error')
} finally {
actionLoading.value = false
}
}
async function flagProvider() {
actionLoading.value = true
try {
await $fetch(`/api/v1/admin/providers/${route.params.id}/flag`, {
method: 'POST',
headers: getHeaders(),
body: { reason: flagReason.value || undefined },
})
showFlagModal.value = false
flagReason.value = ''
showToast('Szolgáltató megjelölve!', 'success')
await fetchProvider()
} catch (err: any) {
showToast(err?.data?.detail || 'Hiba a megjelölés során', 'error')
} finally {
actionLoading.value = false
}
}
async function deleteProvider() {
if (!confirm('Biztosan törlöd ezt a szolgáltatót? Ez a művelet nem visszavonható.')) return
actionLoading.value = true
try {
await $fetch(`/api/v1/admin/providers/${route.params.id}`, {
method: 'DELETE',
headers: getHeaders(),
})
showToast('Szolgáltató törölve!', 'success')
router.push('/providers')
} catch (err: any) {
showToast(err?.data?.detail || 'Hiba a törlés során', 'error')
} finally {
actionLoading.value = false
}
}
// ── Status Edit (PATCH) ────────────────────────────────
async function saveStatusEdit() {
statusLoading.value = true
try {
const body: Record<string, any> = {
status: statusForm.value.status,
validation_score: statusForm.value.validation_score,
}
if (statusForm.value.reason) {
body.reason = statusForm.value.reason
}
await $fetch(`/api/v1/admin/providers/${route.params.id}`, {
method: 'PATCH',
headers: getHeaders(),
body,
})
showToast('Státusz frissítve!', 'success')
await fetchProvider()
} catch (err: any) {
showToast(err?.data?.detail || 'Hiba a státusz mentése során', 'error')
} finally {
statusLoading.value = false
}
}
// ── UI helpers ─────────────────────────────────────────
function openRejectModal() {
rejectReason.value = ''
showRejectModal.value = true
}
function openFlagModal() {
flagReason.value = ''
showFlagModal.value = true
}
function goBack() {
router.push('/providers')
}
function showToast(message: string, type: 'success' | 'error') {
toast.value = { message, type }
setTimeout(() => { toast.value = null }, 4000)
}
function resetStatusForm() {
if (provider.value) {
statusForm.value.status = provider.value.status || 'pending'
statusForm.value.validation_score = provider.value.validation_score || 0
}
statusForm.value.reason = ''
}
// ── Tab change watcher ─────────────────────────────────
function onTabChange(tab: string) {
if (tab === 'validations' && validations.value.length === 0) {
fetchValidations()
}
if (tab === 'history' && history.value.length === 0) {
fetchHistory()
}
}
watch(activeTab, onTabChange)
// ── Lifecycle ──────────────────────────────────────────
onMounted(() => {
fetchProvider()
})
</script>
<style scoped>
/* Custom range slider styling */
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #6366f1;
cursor: pointer;
border: 2px solid #1e293b;
}
input[type='range']::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: #6366f1;
cursor: pointer;
border: 2px solid #1e293b;
}
/* Timeline animation */
.timeline-enter-active {
transition: all 0.3s ease-out;
}
.timeline-enter-from {
opacity: 0;
transform: translateX(-10px);
}
</style>