1160 lines
50 KiB
Vue
1160 lines
50 KiB
Vue
<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 Actions -->
|
||
<div v-if="provider.status === 'pending'" class="space-y-3">
|
||
<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: 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'>('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: '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'
|
||
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'
|
||
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'
|
||
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'
|
||
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)
|
||
})
|
||
|
||
// ── 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 '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 '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> |