admin felület bekötése. pontosítások2
This commit is contained in:
@@ -84,7 +84,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-2xl font-bold text-white">{{ garageDisplayName(garage) }}</h1>
|
||||
<h1 class="text-2xl font-bold text-white">{{ cleanGarageName }}</h1>
|
||||
<!-- P0: Status badge prominently displayed next to Garage Name -->
|
||||
<span
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium"
|
||||
@@ -96,7 +96,6 @@
|
||||
</div>
|
||||
<p class="text-sm text-slate-400 mt-0.5">
|
||||
{{ garage.full_name }}
|
||||
<span class="ml-2 text-slate-500">· #{{ garage.id }}</span>
|
||||
<span v-if="garage.tax_number" class="ml-2 text-slate-500">· {{ $t('garages.details.tax_number') }}: {{ garage.tax_number }}</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -192,46 +191,15 @@
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider mb-3">{{ $t('garages.details.notification_address') }}</p>
|
||||
<p class="text-sm text-white">{{ formattedNotificationAddress }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-slate-700 flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-amber-500/10 text-amber-400">
|
||||
<svg 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="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-white">{{ $t('garages.details.subscription_info') }}</h3>
|
||||
</div>
|
||||
<div class="px-6 py-5">
|
||||
<div v-if="garage.subscription" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider">{{ $t('garages.details.tier_name') }}</p>
|
||||
<span class="inline-flex mt-1 px-2.5 py-0.5 rounded-full text-xs font-medium" :class="tierBadgeClass(garage.subscription.tier_name)">
|
||||
{{ garage.subscription.tier_name }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider">{{ $t('garages.details.tier_level') }}</p>
|
||||
<p class="text-sm text-white mt-1">{{ $t('garages.details.level_n', { n: garage.subscription.tier_level }) }}</p>
|
||||
</div>
|
||||
<!-- System IDs Footer -->
|
||||
<div class="mt-6 pt-4 border-t border-slate-700/50">
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider mb-3">Rendszer Azonosítók</p>
|
||||
<div class="flex gap-6 text-xs font-mono text-slate-500">
|
||||
<span>Garázs ID: <span class="text-slate-400">{{ garage.id }}</span></span>
|
||||
<span>User ID: <span class="text-slate-400">{{ garage.owner_user_id ?? 'N/A' }}</span></span>
|
||||
<span>Person ID: <span class="text-slate-400">{{ garage.owner_person_id ?? 'N/A' }}</span></span>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider">{{ $t('garages.details.expires_at') }}</p>
|
||||
<p v-if="garage.subscription.expires_at" class="text-sm text-white mt-1">{{ formatDate(garage.subscription.expires_at) }}</p>
|
||||
<p v-else class="text-sm text-slate-400 mt-1 italic">{{ $t('garages.indefinite') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider">{{ $t('garages.details.asset_limit') }}</p>
|
||||
<p class="text-sm text-white mt-1">{{ garage.subscription.asset_limit }} {{ $t('garages.details.vehicles') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-4">
|
||||
<p class="text-sm text-slate-400">{{ $t('garages.details.no_subscription') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -338,6 +306,10 @@
|
||||
<div>
|
||||
<p class="text-sm font-medium text-white">{{ memberFullName(member) }}</p>
|
||||
<p class="text-xs text-slate-500">ID: {{ member.user_id || member.id }}</p>
|
||||
<div class="flex gap-3 mt-0.5 text-xs text-slate-500 font-mono">
|
||||
<span>User_id: <span class="text-slate-400">{{ member.user_id || 'N/A' }}</span></span>
|
||||
<span>Person_id: <span class="text-slate-400">{{ member.person?.id || 'N/A' }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -492,14 +464,477 @@
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Analytics Tab -->
|
||||
<!-- Subscription Tab - Package Management -->
|
||||
<!-- ============================================================ -->
|
||||
<div v-if="activeTab === 'subscription'" class="space-y-6">
|
||||
<!-- Current Package Card - Densified with Utilization Stats -->
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-slate-700 flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-amber-500/10 text-amber-400">
|
||||
<svg 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="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-white">{{ $t('garages.details.subscription_info') }}</h3>
|
||||
</div>
|
||||
<div class="px-6 py-5">
|
||||
<div v-if="garage.subscription" class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<!-- Tier Name -->
|
||||
<div class="bg-slate-900/30 rounded-lg p-3">
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider mb-1">{{ $t('garages.details.tier_name') }}</p>
|
||||
<span class="inline-flex px-2.5 py-0.5 rounded-full text-xs font-medium" :class="tierBadgeClass(garage.subscription.tier_name)">
|
||||
{{ garage.subscription.tier_name }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Tier Level -->
|
||||
<div class="bg-slate-900/30 rounded-lg p-3">
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider mb-1">{{ $t('garages.details.tier_level') }}</p>
|
||||
<p class="text-sm font-semibold text-white">{{ $t('garages.details.level_n', { n: garage.subscription.tier_level }) }}</p>
|
||||
</div>
|
||||
<!-- Expiration -->
|
||||
<div class="bg-slate-900/30 rounded-lg p-3">
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider mb-1">{{ $t('garages.details.expires_at') }}</p>
|
||||
<p v-if="garage.subscription.expires_at" class="text-sm font-semibold text-white">{{ formatDate(garage.subscription.expires_at) }}</p>
|
||||
<p v-else class="text-sm text-slate-400 italic">{{ $t('garages.indefinite') }}</p>
|
||||
</div>
|
||||
<!-- Asset Limit -->
|
||||
<div class="bg-slate-900/30 rounded-lg p-3">
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider mb-1">{{ $t('garages.details.asset_limit') }}</p>
|
||||
<p class="text-sm font-semibold text-white">{{ garage.subscription.asset_limit }} {{ $t('garages.details.vehicles') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Utilization Stats Row -->
|
||||
<div v-if="garage.subscription" class="mt-4 grid grid-cols-2 gap-4">
|
||||
<!-- Vehicles Utilization -->
|
||||
<div class="bg-slate-900/30 rounded-lg p-3">
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider mb-1">{{ $t('garages.details.vehicles_used') }}</p>
|
||||
<p class="text-sm font-semibold text-white">
|
||||
{{ fleetVehicles.length }} / {{ garage.subscription?.asset_limit || '∞' }}
|
||||
</p>
|
||||
<div class="mt-2 w-full h-1.5 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-500"
|
||||
:class="vehicleQuotaPercent >= 90 ? 'bg-red-500' : vehicleQuotaPercent >= 70 ? 'bg-amber-500' : 'bg-blue-500'"
|
||||
:style="{ width: Math.min(100, vehicleQuotaPercent) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Branches Utilization -->
|
||||
<div class="bg-slate-900/30 rounded-lg p-3">
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider mb-1">{{ $t('garages.details.branches_used') }}</p>
|
||||
<p class="text-sm font-semibold text-white">
|
||||
{{ garage.branches?.length || 0 }} / {{ garage.subscription?.branch_limit || '∞' }}
|
||||
</p>
|
||||
<div class="mt-2 w-full h-1.5 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-500"
|
||||
:class="branchQuotaPercent >= 90 ? 'bg-red-500' : branchQuotaPercent >= 70 ? 'bg-amber-500' : 'bg-emerald-500'"
|
||||
:style="{ width: Math.min(100, branchQuotaPercent) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Employees Count (P0 ADD-ON: Now with user_limit bar) -->
|
||||
<div class="bg-slate-900/30 rounded-lg p-3">
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider mb-1">{{ $t('garages.details.employees_used') }}</p>
|
||||
<p class="text-sm font-semibold text-white">
|
||||
{{ garage.member_count || 0 }} / {{ garage.subscription?.user_limit || '∞' }}
|
||||
</p>
|
||||
<div v-if="garage.subscription?.user_limit" class="mt-2 w-full h-1.5 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-500"
|
||||
:class="employeeQuotaPercent >= 90 ? 'bg-red-500' : employeeQuotaPercent >= 70 ? 'bg-amber-500' : 'bg-purple-500'"
|
||||
:style="{ width: Math.min(100, employeeQuotaPercent) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status Badge -->
|
||||
<div class="bg-slate-900/30 rounded-lg p-3 flex flex-col justify-center">
|
||||
<p class="text-xs text-slate-500 uppercase tracking-wider mb-1">{{ $t('garages.details.status') }}</p>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium self-start"
|
||||
:class="statusBadgeClass(garage.status)"
|
||||
>
|
||||
<span class="w-1.5 h-1.5 rounded-full" :class="statusDotClass(garage.status)"></span>
|
||||
{{ statusLabel(garage.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-4">
|
||||
<p class="text-sm text-slate-400">{{ $t('garages.details.no_subscription') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Package Section -->
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-slate-700 flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-indigo-500/10 text-indigo-400">
|
||||
<svg 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="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>
|
||||
<h3 class="text-base font-semibold text-white">{{ $t('garages.change_package') }}</h3>
|
||||
</div>
|
||||
<div class="px-6 py-5 space-y-5">
|
||||
<!-- Tier Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-2">{{ $t('garages.select_package') }}</label>
|
||||
<select
|
||||
v-model="selectedTierId"
|
||||
class="w-full px-3 py-2.5 bg-slate-900 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition"
|
||||
>
|
||||
<option value="" disabled>{{ $t('garages.select_package_placeholder') }}</option>
|
||||
<option
|
||||
v-for="tier in availableTiers"
|
||||
:key="tier.id"
|
||||
:value="tier.id"
|
||||
>
|
||||
{{ tier.name }} (Level {{ tier.tier_level }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Custom Expiration Date -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-2">
|
||||
{{ $t('garages.custom_expiration') }}
|
||||
<span class="text-xs text-slate-500 ml-1">({{ $t('garages.optional') }})</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="customExpiresAt"
|
||||
type="datetime-local"
|
||||
class="w-full px-3 py-2.5 bg-slate-900 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition"
|
||||
/>
|
||||
<p class="text-xs text-slate-500 mt-1.5">{{ $t('garages.custom_expiration_hint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- ── P0 ADD-ON UPGRADE: Extra Allowances (Add-ons) ── -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-3">
|
||||
{{ $t('garages.addons_title') || 'Kiegészítők (Add-ons)' }}
|
||||
<span class="text-xs text-slate-500 ml-1">({{ $t('garages.optional') }})</span>
|
||||
</label>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<!-- Extra Vehicles -->
|
||||
<div class="bg-slate-900/30 border border-slate-700/50 rounded-lg p-3">
|
||||
<label class="block text-xs text-slate-400 mb-1.5">{{ $t('garages.addon_vehicles') || 'Extra Jármű' }}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="extraVehicles = Math.max(0, extraVehicles - 1)"
|
||||
class="w-8 h-8 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center transition text-sm font-bold"
|
||||
>−</button>
|
||||
<input
|
||||
v-model.number="extraVehicles"
|
||||
type="number"
|
||||
min="0"
|
||||
max="999"
|
||||
class="w-full px-2 py-1.5 bg-slate-900 border border-slate-700 rounded-lg text-sm text-white text-center focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
/>
|
||||
<button
|
||||
@click="extraVehicles = Math.min(999, extraVehicles + 1)"
|
||||
class="w-8 h-8 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center transition text-sm font-bold"
|
||||
>+</button>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mt-1">+db {{ $t('garages.details.vehicles') || 'jármű' }}</p>
|
||||
</div>
|
||||
<!-- Extra Branches -->
|
||||
<div class="bg-slate-900/30 border border-slate-700/50 rounded-lg p-3">
|
||||
<label class="block text-xs text-slate-400 mb-1.5">{{ $t('garages.addon_branches') || 'Extra Telephely' }}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="extraBranches = Math.max(0, extraBranches - 1)"
|
||||
class="w-8 h-8 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center transition text-sm font-bold"
|
||||
>−</button>
|
||||
<input
|
||||
v-model.number="extraBranches"
|
||||
type="number"
|
||||
min="0"
|
||||
max="999"
|
||||
class="w-full px-2 py-1.5 bg-slate-900 border border-slate-700 rounded-lg text-sm text-white text-center focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
/>
|
||||
<button
|
||||
@click="extraBranches = Math.min(999, extraBranches + 1)"
|
||||
class="w-8 h-8 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center transition text-sm font-bold"
|
||||
>+</button>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mt-1">+db {{ $t('garages.details.branches') || 'telephely' }}</p>
|
||||
</div>
|
||||
<!-- Extra Users -->
|
||||
<div class="bg-slate-900/30 border border-slate-700/50 rounded-lg p-3">
|
||||
<label class="block text-xs text-slate-400 mb-1.5">{{ $t('garages.addon_users') || 'Extra Dolgozó' }}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="extraUsers = Math.max(0, extraUsers - 1)"
|
||||
class="w-8 h-8 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center transition text-sm font-bold"
|
||||
>−</button>
|
||||
<input
|
||||
v-model.number="extraUsers"
|
||||
type="number"
|
||||
min="0"
|
||||
max="999"
|
||||
class="w-full px-2 py-1.5 bg-slate-900 border border-slate-700 rounded-lg text-sm text-white text-center focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
/>
|
||||
<button
|
||||
@click="extraUsers = Math.min(999, extraUsers + 1)"
|
||||
class="w-8 h-8 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center transition text-sm font-bold"
|
||||
>+</button>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mt-1">+db {{ $t('garages.details.employees') || 'dolgozó' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Error -->
|
||||
<div
|
||||
v-if="subscriptionSaveError"
|
||||
class="bg-red-500/10 border border-red-500/20 rounded-lg px-4 py-3"
|
||||
>
|
||||
<p class="text-sm text-red-400">{{ subscriptionSaveError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="flex items-center justify-end gap-3 pt-2">
|
||||
<button
|
||||
@click="updateSubscription"
|
||||
:disabled="subscriptionSaving || !selectedTierId"
|
||||
class="px-5 py-2 text-sm font-medium rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:class="subscriptionSaving
|
||||
? 'bg-indigo-600/50 text-indigo-200 cursor-wait'
|
||||
: 'bg-indigo-600 text-white hover:bg-indigo-500'"
|
||||
>
|
||||
<span v-if="subscriptionSaving" class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
{{ $t('garages.saving') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('garages.save') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subscription History Section -->
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-slate-700 flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-cyan-500/10 text-cyan-400">
|
||||
<svg 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>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-white">{{ $t('garages.details.subscription_history') }}</h3>
|
||||
</div>
|
||||
<div class="px-6 py-5">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-700">
|
||||
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.details.history_date') }}</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.details.history_package') }}</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.details.history_action') }}</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.details.history_status') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Empty State -->
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-12 text-center">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-10 h-10 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p class="text-sm text-slate-400">{{ $t('garages.details.no_subscription_history') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Analytics Tab - Full Dashboard with Gamification -->
|
||||
<!-- ============================================================ -->
|
||||
<div v-if="activeTab === 'analytics'" class="space-y-6">
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl p-12 text-center">
|
||||
<svg class="w-12 h-12 text-slate-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
<p class="text-sm text-slate-400">{{ $t('garages.details.coming_soon') }}</p>
|
||||
|
||||
<!-- SECTION 1: Financial & Revenue (Top Row - 3 Cards) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Card 1: LTV - Élettartam Érték -->
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl p-5">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="p-2 rounded-lg bg-emerald-500/10 text-emerald-400">
|
||||
<svg 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="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.analytics.ltv') }}</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-white">150 000 Ft</p>
|
||||
<p class="text-xs text-emerald-400 mt-1 flex items-center gap-1">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
||||
</svg>
|
||||
+12% ezen a hónapon
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: MRR - Havi Bevétel -->
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl p-5">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="p-2 rounded-lg bg-blue-500/10 text-blue-400">
|
||||
<svg 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 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.analytics.mrr') }}</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-white">15 000 Ft</p>
|
||||
<p class="text-xs text-slate-400 mt-1">{{ $t('garages.analytics.monthly_recurring') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Kintlévőség -->
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl p-5">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="p-2 rounded-lg bg-amber-500/10 text-amber-400">
|
||||
<svg 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="M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2zM10 8.5a.5.5 0 11-1 0 .5.5 0 011 0zm5 5a.5.5 0 11-1 0 .5.5 0 011 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.analytics.outstanding') }}</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-white">0 Ft</p>
|
||||
<p class="text-xs text-emerald-400 mt-1 flex items-center gap-1">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{{ $t('garages.analytics.all_settled') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 2: Gamification & Rewards (Second Row - 3 Cards) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Card 1: Szerzett Kreditek -->
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl p-5">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="p-2 rounded-lg bg-yellow-500/10 text-yellow-400">
|
||||
<svg 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="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.analytics.earned_credits') }}</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-yellow-300">450 XP</p>
|
||||
<p class="text-xs text-slate-400 mt-1">{{ $t('garages.analytics.contribution_xp') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Meghívott Cégek -->
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl p-5">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="p-2 rounded-lg bg-purple-500/10 text-purple-400">
|
||||
<svg 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="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.analytics.invited_companies') }}</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-white">12</p>
|
||||
<p class="text-xs text-slate-400 mt-1">{{ $t('garages.analytics.companies_joined') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Validált Garázsok -->
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl p-5">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="p-2 rounded-lg bg-cyan-500/10 text-cyan-400">
|
||||
<svg 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="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.analytics.validated_garages') }}</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-white">8</p>
|
||||
<p class="text-xs text-slate-400 mt-1">{{ $t('garages.analytics.garages_confirmed') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 3 & 4: Bottom Row - Utilization + Marketplace -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- SECTION 3: Utilization Bars (Bottom Left) -->
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl p-5">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 rounded-lg bg-indigo-500/10 text-indigo-400">
|
||||
<svg 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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-sm font-semibold text-white">{{ $t('garages.analytics.utilization') }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Vehicle Quota Bar -->
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<span class="text-xs text-slate-400">{{ $t('garages.analytics.vehicle_quota') }}</span>
|
||||
<span class="text-xs text-slate-500">{{ fleetVehicles.length }} / {{ vehicleQuotaLimit }}</span>
|
||||
</div>
|
||||
<div class="w-full h-2 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-500"
|
||||
:class="vehicleQuotaPercent >= 90 ? 'bg-red-500' : vehicleQuotaPercent >= 70 ? 'bg-amber-500' : 'bg-blue-500'"
|
||||
:style="{ width: vehicleQuotaPercent + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mt-1">{{ vehicleQuotaPercent }}% {{ $t('garages.analytics.utilized') }}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SECTION 4: Marketplace & Engagement (Bottom Right) -->
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl p-5">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 rounded-lg bg-pink-500/10 text-pink-400">
|
||||
<svg 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="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-sm font-semibold text-white">{{ $t('garages.analytics.marketplace_engagement') }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Average Rating -->
|
||||
<div class="flex items-center justify-between py-2 border-b border-slate-700/50">
|
||||
<span class="text-xs text-slate-400">{{ $t('garages.analytics.avg_rating') }}</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="flex items-center">
|
||||
<svg v-for="i in 5" :key="i" class="w-4 h-4" :class="i <= 4 ? 'text-yellow-400' : 'text-slate-600'" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-white">4.8</span>
|
||||
<span class="text-xs text-slate-500">/ 5.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Review Count -->
|
||||
<div class="flex items-center justify-between py-2 border-b border-slate-700/50">
|
||||
<span class="text-xs text-slate-400">{{ $t('garages.analytics.reviews') }}</span>
|
||||
<span class="text-sm text-white font-medium">124 {{ $t('garages.analytics.reviews_count') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Last Login -->
|
||||
<div class="flex items-center justify-between py-2 border-b border-slate-700/50">
|
||||
<span class="text-xs text-slate-400">{{ $t('garages.analytics.last_activity') }}</span>
|
||||
<span class="text-sm text-white font-medium">{{ $t('garages.analytics.last_login_mock') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 30-Day Services -->
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<span class="text-xs text-slate-400">{{ $t('garages.analytics.services_30d') }}</span>
|
||||
<span class="text-sm text-white font-medium">42</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1019,11 +1454,23 @@ const removing = ref(false)
|
||||
// Notification
|
||||
const notification = ref({ show: false, type: 'success' as 'success' | 'error', message: '' })
|
||||
|
||||
// Subscription tab state
|
||||
const availableTiers = ref<any[]>([])
|
||||
const selectedTierId = ref<number | ''>('')
|
||||
const customExpiresAt = ref('')
|
||||
const subscriptionSaving = ref(false)
|
||||
const subscriptionSaveError = ref<string | null>(null)
|
||||
// ── P0 ADD-ON UPGRADE: Extra allowances (add-ons) ──
|
||||
const extraVehicles = ref(0)
|
||||
const extraBranches = ref(0)
|
||||
const extraUsers = ref(0)
|
||||
|
||||
// Tab definitions
|
||||
const tabs = [
|
||||
{ key: 'general', label: 'garages.details.generalTab' },
|
||||
{ key: 'employees', label: 'garages.details.employeesTab' },
|
||||
{ key: 'fleet', label: 'garages.details.fleetTab' },
|
||||
{ key: 'subscription', label: 'garages.details.subscriptionTab' },
|
||||
{ key: 'analytics', label: 'garages.details.analyticsTab' },
|
||||
]
|
||||
|
||||
@@ -1042,6 +1489,31 @@ function hasOrgPermission(perm: string): boolean {
|
||||
}
|
||||
|
||||
// Computed
|
||||
const vehicleQuotaLimit = computed(() => {
|
||||
if (!garage.value?.subscription?.asset_limit) return 5
|
||||
return garage.value.subscription.asset_limit
|
||||
})
|
||||
|
||||
const vehicleQuotaPercent = computed(() => {
|
||||
const limit = vehicleQuotaLimit.value
|
||||
if (limit <= 0) return 0
|
||||
return Math.min(100, Math.round((fleetVehicles.value.length / limit) * 100))
|
||||
})
|
||||
|
||||
const branchQuotaPercent = computed(() => {
|
||||
const branches = garage.value?.branches?.length || 0
|
||||
const limit = garage.value?.subscription?.branch_limit
|
||||
if (!limit || limit <= 0) return 0
|
||||
return Math.min(100, Math.round((branches / limit) * 100))
|
||||
})
|
||||
|
||||
const employeeQuotaPercent = computed(() => {
|
||||
const count = garage.value?.member_count || 0
|
||||
const limit = garage.value?.subscription?.user_limit
|
||||
if (!limit || limit <= 0) return 0
|
||||
return Math.min(100, Math.round((count / limit) * 100))
|
||||
})
|
||||
|
||||
const missingFields = computed(() => {
|
||||
if (!garage.value) return []
|
||||
const missing: { key: string; label: string }[] = []
|
||||
@@ -1345,15 +1817,91 @@ function showNotification(type: 'success' | 'error', message: string) {
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
// ── Subscription Tab Functions ──────────────────────────────────────
|
||||
|
||||
async function fetchTiers() {
|
||||
try {
|
||||
const tokenCookie = useCookie('access_token')
|
||||
const token = (tokenCookie.value || auth.token || '').trim()
|
||||
const res = await fetch('/api/v1/admin/packages?include_hidden=true&limit=100', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
if (!res.ok) throw new Error(`Failed to fetch tiers: ${res.status}`)
|
||||
const data = await res.json()
|
||||
availableTiers.value = data.tiers || []
|
||||
} catch (e: any) {
|
||||
console.error('[GarageDetails] Failed to fetch tiers:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSubscription() {
|
||||
if (!selectedTierId.value) return
|
||||
|
||||
subscriptionSaving.value = true
|
||||
subscriptionSaveError.value = null
|
||||
|
||||
try {
|
||||
const tokenCookie = useCookie('access_token')
|
||||
const token = (tokenCookie.value || auth.token || '').trim()
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
tier_id: selectedTierId.value,
|
||||
}
|
||||
|
||||
if (customExpiresAt.value) {
|
||||
payload.expires_at = new Date(customExpiresAt.value).toISOString()
|
||||
}
|
||||
|
||||
// P0 ADD-ON UPGRADE: Include extra allowances in the payload
|
||||
if (extraVehicles.value > 0) payload.extra_vehicles = extraVehicles.value
|
||||
if (extraBranches.value > 0) payload.extra_branches = extraBranches.value
|
||||
if (extraUsers.value > 0) payload.extra_users = extraUsers.value
|
||||
|
||||
const res = await fetch(`/api/v1/admin/organizations/${orgId}/subscription`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}))
|
||||
throw new Error(errData.detail || `Update subscription failed: ${res.status}`)
|
||||
}
|
||||
|
||||
showNotification('success', 'Subscription updated successfully')
|
||||
selectedTierId.value = ''
|
||||
customExpiresAt.value = ''
|
||||
await fetchDetails()
|
||||
} catch (e: any) {
|
||||
subscriptionSaveError.value = e.message || 'Failed to update subscription'
|
||||
showNotification('error', e.message || 'Failed to update subscription')
|
||||
} finally {
|
||||
subscriptionSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
function goBack() {
|
||||
router.push('/garages')
|
||||
}
|
||||
|
||||
// Helpers
|
||||
const cleanGarageName = computed(() => {
|
||||
if (!garage.value) return ''
|
||||
const raw = garage.value.display_name || garage.value.name || garage.value.full_name || ''
|
||||
// Strip trailing (#\d+) or -\s*#\d+ patterns
|
||||
return raw.replace(/[\s-]*#\d+\s*$/g, '').trim() || 'Garage #' + garage.value.id
|
||||
})
|
||||
|
||||
function garageDisplayName() {
|
||||
if (!garage.value) return ''
|
||||
return garage.value.name || garage.value.email || garage.value.id
|
||||
return garage.value.name || garage.value.email || garage.value.full_name || 'Garage #' + garage.value.id
|
||||
}
|
||||
|
||||
function getInitials(name: string): string {
|
||||
@@ -1555,5 +2103,9 @@ watch(activeTab, (newTab) => {
|
||||
if (newTab === 'fleet' && fleetVehicles.value.length === 0) {
|
||||
fetchFleetVehicles()
|
||||
}
|
||||
// Fetch available tiers when the Subscription tab becomes active
|
||||
if (newTab === 'subscription' && availableTiers.value.length === 0) {
|
||||
fetchTiers()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
>
|
||||
<option value="all">{{ $t('garages.all_tiers') }}</option>
|
||||
<option
|
||||
v-for="tier in uniqueTiers"
|
||||
v-for="tier in availableTierNames"
|
||||
:key="tier"
|
||||
:value="tier"
|
||||
>
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<!-- Content -->
|
||||
<template v-else>
|
||||
<!-- P0: Dynamic Stats Cards - based on real org_type data, not fake subscription tiers -->
|
||||
<!-- P0 HOTFIX: Global Stats Cards - always show unfiltered global counts -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -78,7 +78,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wider">{{ $t('garages.total_garages') }}</p>
|
||||
<p class="text-2xl font-bold text-white">{{ garages.length }}</p>
|
||||
<p class="text-2xl font-bold text-white">{{ globalTotalCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wider">{{ $t('garages.private_garages') }}</p>
|
||||
<p class="text-2xl font-bold text-emerald-400">{{ individualCount }}</p>
|
||||
<p class="text-2xl font-bold text-emerald-400">{{ globalIndividualCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,12 +104,17 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wider">{{ $t('garages.corporate_garages') }}</p>
|
||||
<p class="text-2xl font-bold text-amber-400">{{ corporateCount }}</p>
|
||||
<p class="text-2xl font-bold text-amber-400">{{ globalCorporateCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- P0 HOTFIX: Filtered results indicator -->
|
||||
<div v-if="isFilterActive" class="mb-4 text-xs text-slate-500">
|
||||
Szűrt találat: <span class="text-slate-300 font-semibold">{{ garages.length }}</span> db
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div class="bg-slate-800/50 border border-slate-700 rounded-xl overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
@@ -143,7 +148,12 @@
|
||||
{{ getInitials(garageDisplayName(garage)) }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-white">{{ garageDisplayName(garage) }}</p>
|
||||
<NuxtLink
|
||||
:to="'/garages/' + garage.id"
|
||||
class="text-sm font-medium text-white hover:underline cursor-pointer hover:text-indigo-400 transition-colors"
|
||||
>
|
||||
{{ garageDisplayName(garage) }}
|
||||
</NuxtLink>
|
||||
<p class="text-xs text-slate-500">{{ garage.city || '—' }} · {{ garage.member_count }} {{ $t('garages.members') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -402,6 +412,10 @@ const searchQuery = ref('')
|
||||
const filterType = ref('all')
|
||||
const selectedPackage = ref('all')
|
||||
|
||||
// P0 HOTFIX: Accumulating tier names — never shrinks, only grows.
|
||||
// This prevents the dropdown from losing options when filtering by tier.
|
||||
const availableTierNames = ref<string[]>([])
|
||||
|
||||
// Modal state
|
||||
const showSubscriptionModal = ref(false)
|
||||
const selectedGarage = ref<Garage | null>(null)
|
||||
@@ -450,20 +464,30 @@ watch(selectedPackage, () => {
|
||||
|
||||
// ── Computed ───────────────────────────────────────────────────────
|
||||
|
||||
// P0: Dynamic stats based on real org_type from the database
|
||||
const individualCount = computed(() => garages.value.filter(g => g.org_type === 'individual').length)
|
||||
const corporateCount = computed(() => garages.value.filter(g => g.org_type !== 'individual').length)
|
||||
// P0 HOTFIX: Global stats stored from initial unfiltered load
|
||||
// These NEVER change when filters are applied, so the stat cards
|
||||
// always reflect the true database totals.
|
||||
const globalTotalCount = ref(0)
|
||||
const globalIndividualCount = ref(0)
|
||||
const globalCorporateCount = ref(0)
|
||||
|
||||
// P0: Extract unique subscription tier names for the filter dropdown
|
||||
const uniqueTiers = computed(() => {
|
||||
const tiers = new Set<string>()
|
||||
for (const g of garages.value) {
|
||||
// P0 HOTFIX: Detect if any filter is active
|
||||
const isFilterActive = computed(() => {
|
||||
return searchQuery.value !== '' || filterType.value !== 'all' || selectedPackage.value !== 'all'
|
||||
})
|
||||
|
||||
// P0 HOTFIX: Accumulating tier names — never shrinks, only grows.
|
||||
// Watcher on garages adds newly discovered tier names to availableTierNames.
|
||||
// This prevents the dropdown from losing options when filtering by tier.
|
||||
watch(garages, (newGarages) => {
|
||||
const currentSet = new Set(availableTierNames.value)
|
||||
for (const g of newGarages) {
|
||||
if (g.subscription_tier_name) {
|
||||
tiers.add(g.subscription_tier_name)
|
||||
currentSet.add(g.subscription_tier_name)
|
||||
}
|
||||
}
|
||||
return Array.from(tiers).sort()
|
||||
})
|
||||
availableTierNames.value = Array.from(currentSet).sort()
|
||||
}, { immediate: true, deep: true })
|
||||
|
||||
// P0: Server-side filtered — the API handles all filtering.
|
||||
// This computed is now a simple pass-through; the real filtering
|
||||
@@ -507,6 +531,16 @@ async function fetchGarages() {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
garages.value = response.garages
|
||||
|
||||
// P0 HOTFIX: Store global counts from unfiltered API response.
|
||||
// When no filters are active, the API returns the full dataset,
|
||||
// so we capture the global totals here. When filters ARE active,
|
||||
// we keep the previously captured global counts unchanged.
|
||||
if (!isFilterActive.value) {
|
||||
globalTotalCount.value = response.total
|
||||
globalIndividualCount.value = garages.value.filter(g => g.org_type === 'individual').length
|
||||
globalCorporateCount.value = garages.value.filter(g => g.org_type !== 'individual').length
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[Garages] Failed to fetch garages:', err)
|
||||
error.value = err?.data?.detail || err?.message || 'Failed to load garages'
|
||||
|
||||
Reference in New Issue
Block a user