Files
service-finder/frontend_admin/pages/garages/[id]/index.vue

2112 lines
110 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>
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center py-20">
<div class="flex flex-col items-center gap-3">
<svg class="w-8 h-8 text-indigo-400 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>
<p class="text-slate-400 text-sm">{{ $t('garages.details.loading') }}</p>
</div>
</div>
<!-- Error State -->
<div v-else-if="error" class="flex flex-col items-center justify-center py-20 text-center">
<svg class="w-12 h-12 text-red-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
<p class="text-red-400 text-sm mb-2">{{ error }}</p>
<button @click="fetchDetails" class="px-4 py-2 text-sm bg-indigo-600/20 text-indigo-300 hover:bg-indigo-600/30 rounded-lg transition">
{{ $t('garages.details.retry') }}
</button>
</div>
<!-- Content -->
<template v-else-if="garage">
<!-- ============================================================ -->
<!-- STEP 4: Missing Data Alerter -->
<!-- ============================================================ -->
<div
v-if="missingFields.length > 0"
class="mb-6 p-4 bg-amber-500/10 border border-amber-500/30 rounded-xl"
>
<div class="flex items-start gap-3">
<div class="p-1.5 rounded-lg bg-amber-500/20 text-amber-400 flex-shrink-0">
<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 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>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="text-sm font-semibold text-amber-300">{{ $t('garages.details.missing_data_title') }}</p>
<span class="text-xs text-amber-400/70">{{ $t('garages.details.missing_data_count', { count: missingFields.length }) }}</span>
</div>
<!-- P0: Activation-aware missing data description -->
<p class="text-xs text-amber-400/80 mt-1">
{{ garage.status !== 'active' ? $t('garages.details.missing_data_activation_required') : $t('garages.details.missing_data_desc') }}
</p>
<div class="flex flex-wrap gap-2 mt-3">
<span
v-for="field in missingFields"
:key="field.key"
class="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium bg-amber-500/10 text-amber-300"
>
<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="M6 18L18 6M6 6l12 12" />
</svg>
{{ $t(field.label) }}
</span>
</div>
</div>
</div>
</div>
<!-- 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>
{{ $t('garages.details.back') }}
</button>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 rounded-xl flex items-center justify-center text-lg font-bold text-white"
:class="getInitialsColor(garageDisplayName(garage))"
>
{{ getInitials(garageDisplayName(garage)) }}
</div>
<div>
<div class="flex items-center gap-3">
<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"
:class="statusBadgeClass(garage.status)"
>
<span class="w-1.5 h-1.5 rounded-full mr-1.5" :class="statusDotClass(garage.status)"></span>
{{ statusLabel(garage.status) }}
</span>
</div>
<p class="text-sm text-slate-400 mt-0.5">
{{ garage.full_name }}
<span v-if="garage.tax_number" class="ml-2 text-slate-500">· {{ $t('garages.details.tax_number') }}: {{ garage.tax_number }}</span>
</p>
</div>
</div>
<div class="flex items-center gap-3">
<!-- STEP 6: RBAC - Edit button hidden without org:edit permission -->
<button
v-if="hasOrgPermission('org:edit')"
@click="openEditModal"
class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium bg-indigo-600/20 text-indigo-300 hover:bg-indigo-600/30 rounded-lg transition"
>
<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>
{{ $t('garages.details.edit_details') }}
</button>
</div>
</div>
</div>
<!-- Tab Navigation -->
<div class="border-b border-slate-700 mb-6">
<nav class="flex gap-6">
<button
v-for="tab in tabs"
:key="tab.key"
@click="activeTab = tab.key"
class="pb-3 text-sm font-medium transition border-b-2 -mb-px"
:class="activeTab === tab.key
? 'text-indigo-400 border-indigo-400'
: 'text-slate-400 border-transparent hover:text-slate-300'"
>
{{ $t(tab.label) }}
</button>
</nav>
</div>
<!-- General Tab Content -->
<div v-if="activeTab === 'general'" class="space-y-6">
<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="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<h3 class="text-base font-semibold text-white">{{ $t('garages.details.company_info') }}</h3>
</div>
<div class="px-6 py-5">
<div 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.company_name') }}</p>
<p class="text-sm text-white mt-1">{{ garage.full_name }}</p>
</div>
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider">{{ $t('garages.details.display_name') }}</p>
<p class="text-sm text-white mt-1">{{ garage.display_name || '—' }}</p>
</div>
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider">{{ $t('garages.details.org_type') }}</p>
<p class="text-sm text-white mt-1">{{ orgTypeLabel(garage.org_type) }}</p>
</div>
</div>
<div class="space-y-4">
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider">{{ $t('garages.details.tax_number') }}</p>
<p class="text-sm text-white mt-1">{{ garage.tax_number || '—' }}</p>
</div>
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider">{{ $t('garages.details.reg_number') }}</p>
<p class="text-sm text-white mt-1">{{ garage.reg_number || '—' }}</p>
</div>
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider">{{ $t('garages.details.created_at') }}</p>
<p class="text-sm text-white mt-1">{{ formatDate(garage.created_at) }}</p>
</div>
</div>
</div>
<div class="mt-6 pt-6 border-t border-slate-700/50">
<p class="text-xs text-slate-500 uppercase tracking-wider mb-3">{{ $t('garages.details.address') }}</p>
<p class="text-sm text-white">{{ formattedAddress }}</p>
</div>
<!-- Billing Address -->
<div v-if="formattedBillingAddress" class="mt-4 pt-4 border-t border-slate-700/50">
<p class="text-xs text-slate-500 uppercase tracking-wider mb-3">{{ $t('garages.details.billing_address') }}</p>
<p class="text-sm text-white">{{ formattedBillingAddress }}</p>
</div>
<!-- Notification Address -->
<div v-if="formattedNotificationAddress" class="mt-4 pt-4 border-t border-slate-700/50">
<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>
<!-- 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>
</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-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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h3 class="text-base font-semibold text-white">{{ $t('garages.details.contact_info') }}</h3>
</div>
<div class="px-6 py-5">
<!-- Show block if ANY contact data exists (top-level fields or primary_contact) -->
<div v-if="garage.contact_person_name || garage.contact_email || garage.contact_phone || garage.primary_contact" 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.contact_name') }}</p>
<p class="text-sm text-white mt-1 font-medium">{{ garage.contact_person_name || garage.primary_contact?.full_name || '—' }}</p>
</div>
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider">{{ $t('garages.details.contact_email') }}</p>
<p class="text-sm text-white mt-1 flex items-center gap-1.5">
<svg class="w-4 h-4 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span>{{ garage.contact_email || garage.primary_contact?.email || '—' }}</span>
</p>
</div>
</div>
<div class="space-y-4">
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider">{{ $t('garages.details.contact_phone') }}</p>
<p class="text-sm text-white mt-1 flex items-center gap-1.5">
<svg class="w-4 h-4 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<span>{{ garage.contact_phone || garage.primary_contact?.phone || '—' }}</span>
</p>
</div>
<!-- Extra metadata from primary_contact if available -->
<div v-if="garage.primary_contact?.role || garage.primary_contact?.department">
<p class="text-xs text-slate-500 uppercase tracking-wider">{{ $t('garages.details.contact_role') }}</p>
<p class="text-sm text-white mt-1">{{ garage.primary_contact.role || '—' }}<span v-if="garage.primary_contact?.department"> · {{ garage.primary_contact.department }}</span></p>
</div>
</div>
</div>
<div v-else class="text-center py-4">
<p class="text-sm text-slate-400">{{ $t('garages.details.no_contact') }}</p>
</div>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- Employees Tab (Dolgozók) -->
<!-- ============================================================ -->
<div v-if="activeTab === 'employees'" class="space-y-6">
<div class="flex items-center justify-between">
<div class="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 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
<div>
<h3 class="text-base font-semibold text-white">{{ $t('garages.employees.title') }}</h3>
<p class="text-xs text-slate-400 mt-0.5">{{ $t('garages.employees.total_count', { count: members.length }) }}</p>
</div>
</div>
<!-- STEP 8: Add Employee button with RBAC -->
<button
v-if="hasOrgPermission('org:manage-members')"
@click="openAddEmployeeModal"
class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium bg-emerald-600/20 text-emerald-300 hover:bg-emerald-600/30 rounded-lg transition"
>
<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="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>
{{ $t('garages.employees.add_employee') }}
</button>
</div>
<div class="bg-slate-800/50 border border-slate-700 rounded-xl overflow-hidden">
<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.employees.name') }}</th>
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.employees.email_phone') }}</th>
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.employees.role') }}</th>
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.employees.status') }}</th>
<th class="text-right px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.employees.actions') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="member in members" :key="member.id" class="border-b border-slate-700/50 last:border-b-0 hover:bg-slate-700/30 transition">
<td class="px-4 py-3">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg flex items-center justify-center text-xs font-bold text-white flex-shrink-0" :class="getInitialsColor(memberFullName(member))">
{{ getInitials(memberFullName(member)) }}
</div>
<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>
<td class="px-4 py-3">
<div class="space-y-0.5">
<p v-if="member.person?.email" class="text-sm text-slate-300">{{ member.person.email }}</p>
<p v-else class="text-sm text-slate-500 italic"></p>
<p v-if="member.person?.phone" class="text-xs text-slate-400">{{ member.person.phone }}</p>
</div>
</td>
<td class="px-4 py-3">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium" :class="roleBadgeClass(member.role)">
{{ roleLabel(member.role) }}
</span>
</td>
<td class="px-4 py-3">
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium" :class="memberStatusBadgeClass(member.status)">
<span class="w-1.5 h-1.5 rounded-full" :class="memberStatusDotClass(member.status)"></span>
{{ memberStatusLabel(member.status) }}
</span>
</td>
<td class="px-4 py-3 text-right">
<div class="flex items-center justify-end gap-2">
<!-- STEP 6: RBAC - Edit role button hidden without org:manage-members -->
<button
v-if="hasOrgPermission('org:manage-members')"
@click="openEditRole(member)"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-indigo-300 bg-indigo-500/10 hover:bg-indigo-500/20 rounded-lg transition"
>
<svg class="w-3.5 h-3.5" 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>
{{ $t('garages.employees.edit_role') }}
</button>
<!-- STEP 6: RBAC - Remove button hidden without org:manage-members -->
<button
v-if="hasOrgPermission('org:manage-members')"
@click="confirmRemoveMember(member)"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-red-300 bg-red-500/10 hover:bg-red-500/20 rounded-lg transition"
>
<svg class="w-3.5 h-3.5" 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('garages.employees.remove') }}
</button>
</div>
</td>
</tr>
<tr v-if="members.length === 0">
<td colspan="5" class="px-4 py-12 text-center">
<svg class="w-10 h-10 text-slate-600 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<p class="text-sm text-slate-400">{{ $t('garages.employees.no_employees') }}</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- STEP 7: Fleet Tab with data table -->
<!-- ============================================================ -->
<div v-if="activeTab === 'fleet'" class="space-y-6">
<!-- RBAC guard: hide content without fleet:view -->
<template v-if="hasOrgPermission('fleet:view')">
<div class="flex items-center justify-between">
<div class="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="M9 17a2 2 0 11-4 0 2 2 0 014 0zM19 17a2 2 0 11-4 0 2 2 0 014 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1 1H9m4-1V8a1 1 0 011-1h2.586a1 1 0 01.707.293l3.414 3.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6-1a1 1 0 001 1h1M5 17a2 2 0 104 0m-4 0a2 2 0 114 0m6 0a2 2 0 104 0m-4 0a2 2 0 114 0" />
</svg>
</div>
<div>
<h3 class="text-base font-semibold text-white">{{ $t('garages.fleet.title') }}</h3>
<p class="text-xs text-slate-400 mt-0.5">{{ $t('garages.fleet.total_vehicles', { count: fleetVehicles.length }) }}</p>
</div>
</div>
</div>
<div class="bg-slate-800/50 border border-slate-700 rounded-xl overflow-hidden">
<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.fleet.plate') }}</th>
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.fleet.brand_model') }}</th>
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.fleet.year') }}</th>
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.fleet.vin') }}</th>
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.fleet.status') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="v in fleetVehicles" :key="v.id" class="border-b border-slate-700/50 last:border-b-0 hover:bg-slate-700/30 transition">
<td class="px-4 py-3">
<span class="text-sm font-bold text-white">{{ v.license_plate || '—' }}</span>
</td>
<td class="px-4 py-3">
<span class="text-sm text-slate-300">{{ v.brand || '—' }}</span>
<span v-if="v.brand && v.model" class="text-slate-500 mx-1">·</span>
<span v-if="v.model" class="text-sm text-slate-300">{{ v.model }}</span>
</td>
<td class="px-4 py-3 text-sm text-slate-300">{{ v.year || '—' }}</td>
<td class="px-4 py-3">
<span class="text-xs font-mono text-slate-400">{{ v.vin || '—' }}</span>
</td>
<td class="px-4 py-3">
<span
class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="fleetStatusBadgeClass(v.status)"
>
<span class="w-1.5 h-1.5 rounded-full" :class="fleetStatusDotClass(v.status)"></span>
{{ fleetStatusLabel(v.status) }}
</span>
</td>
</tr>
<tr v-if="fleetVehicles.length === 0 && !fleetLoading">
<td colspan="5" class="px-4 py-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 17a2 2 0 11-4 0 2 2 0 014 0zM19 17a2 2 0 11-4 0 2 2 0 014 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1 1H9m4-1V8a1 1 0 011-1h2.586a1 1 0 01.707.293l3.414 3.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6-1a1 1 0 001 1h1M5 17a2 2 0 104 0m-4 0a2 2 0 114 0m6 0a2 2 0 104 0m-4 0a2 2 0 114 0" />
</svg>
<p class="text-sm text-slate-400">{{ $t('garages.fleet.no_vehicles') }}</p>
</td>
</tr>
<tr v-if="fleetLoading">
<td colspan="5" class="px-4 py-12 text-center">
<svg class="w-8 h-8 text-indigo-400 animate-spin mx-auto" 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>
<p class="text-sm text-slate-400 mt-2">{{ $t('garages.details.loading') }}</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<!-- RBAC: No fleet access fallback -->
<template v-else>
<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="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
<p class="text-sm text-slate-400">{{ $t('garages.fleet.no_fleet_access') }}</p>
</div>
</template>
</div>
<!-- ============================================================ -->
<!-- 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">
<!-- 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>
<!-- END: template v-else-if="garage" -->
</template>
<!-- ============================================================ -->
<!-- STEP 5: Comprehensive Edit Modal with tabs -->
<!-- ============================================================ -->
<div
v-if="showEditModal"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
>
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" @click="closeEditModal"></div>
<!-- Modal -->
<div class="relative w-full max-w-2xl bg-slate-800 border border-slate-700 rounded-2xl shadow-2xl max-h-[90vh] overflow-hidden flex flex-col">
<!-- Header -->
<div class="px-6 py-4 border-b border-slate-700 flex items-center justify-between flex-shrink-0">
<h3 class="text-lg font-semibold text-white">{{ $t('garages.details.edit_details') }}</h3>
<button @click="closeEditModal" class="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-700 transition">
<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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Tab Navigation -->
<div class="px-6 pt-4 border-b border-slate-700 flex-shrink-0">
<nav class="flex gap-4">
<button
v-for="tab in editModalTabs"
:key="tab.key"
@click="editModalTab = tab.key"
class="pb-3 text-sm font-medium transition border-b-2 -mb-px"
:class="editModalTab === tab.key
? 'text-indigo-400 border-indigo-400'
: 'text-slate-400 border-transparent hover:text-slate-300'"
>
{{ $t(tab.label) }}
</button>
</nav>
</div>
<!-- Scrollable Body -->
<div class="flex-1 overflow-y-auto p-6 space-y-6">
<!-- Alapadatok Tab -->
<div v-if="editModalTab === 'basic'" class="space-y-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.company_name') }}</label>
<input
v-model="editForm.full_name"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.display_name') }}</label>
<input
v-model="editForm.display_name"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.tax_number') }}</label>
<input
v-model="editForm.tax_number"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.reg_number') }}</label>
<input
v-model="editForm.reg_number"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.zip') }}</label>
<input
v-model="editForm.address_zip"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.city') }}</label>
<input
v-model="editForm.address_city"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="md:col-span-2">
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.street_name') }}</label>
<input
v-model="editForm.address_street_name"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.street_type') }}</label>
<input
v-model="editForm.address_street_type"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.house_number') }}</label>
<input
v-model="editForm.address_house_number"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
</div>
</div>
<!-- Kapcsolat Tab -->
<div v-if="editModalTab === 'contact'" class="space-y-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.contact_person_name') }}</label>
<input
v-model="editForm.contact_person_name"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.contact_email') }}</label>
<input
v-model="editForm.contact_email"
type="email"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.contact_phone') }}</label>
<input
v-model="editForm.contact_phone"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
</div>
<!-- Címek Tab -->
<div v-if="editModalTab === 'addresses'" class="space-y-6">
<!-- Billing Address -->
<div>
<h4 class="text-sm font-semibold text-white mb-3">{{ $t('garages.details.billing_address') }}</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.billing_zip') }}</label>
<input
v-model="editForm.billing_zip"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.billing_city') }}</label>
<input
v-model="editForm.billing_city"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
<div class="md:col-span-1">
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.billing_street_name') }}</label>
<input
v-model="editForm.billing_street_name"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.billing_street_type') }}</label>
<input
v-model="editForm.billing_street_type"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.billing_house_number') }}</label>
<input
v-model="editForm.billing_house_number"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
</div>
</div>
<!-- Notification Address -->
<div class="pt-4 border-t border-slate-700/50">
<h4 class="text-sm font-semibold text-white mb-3">{{ $t('garages.details.notification_address') }}</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.notification_zip') }}</label>
<input
v-model="editForm.notification_zip"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.notification_city') }}</label>
<input
v-model="editForm.notification_city"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
<div class="md:col-span-1">
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.notification_street_name') }}</label>
<input
v-model="editForm.notification_street_name"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.notification_street_type') }}</label>
<input
v-model="editForm.notification_street_type"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1.5">{{ $t('garages.details.notification_house_number') }}</label>
<input
v-model="editForm.notification_house_number"
type="text"
class="w-full px-3 py-2 text-sm bg-slate-700/50 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"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="px-6 py-4 border-t border-slate-700 flex items-center justify-end gap-3 flex-shrink-0">
<button
@click="closeEditModal"
class="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white bg-slate-700/50 hover:bg-slate-700 rounded-lg transition"
>
{{ $t('garages.details.cancel') }}
</button>
<button
@click="saveEdit"
:disabled="saving"
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg transition inline-flex items-center gap-2"
>
<svg v-if="saving" 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>
{{ saving ? $t('garages.details.saving') : $t('garages.details.save') }}
</button>
</div>
</div>
</div>
<!-- Add Employee Modal (Dark Theme) -->
<div v-if="showAddEmployeeModal" class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-slate-900/80 backdrop-blur-sm transition-opacity" @click="showAddEmployeeModal = false"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-slate-800 rounded-2xl px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl border border-slate-700 transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-indigo-500/20 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7.5v-1m0 0a3 3 0 00-3-3m3 3a3 3 0 013 3m0 0v1m0 0a3 3 0 01-3 3m3-3h-1m-4-3a3 3 0 01-3 3m0 0a3 3 0 01-3-3m3 3v1m0 0a3 3 0 013 3m-3-3H8m4 3h1m-4-3a3 3 0 01-3-3m3 3v-1m0 0a3 3 0 013-3m-3 3h1m4-3h-1" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<h3 class="text-lg leading-6 font-medium text-white" id="modal-title">
{{ $t('garages.employees.addTitle') }}
</h3>
<div class="mt-4 space-y-4">
<div>
<label class="block text-sm font-medium text-slate-300">{{ $t('garages.employees.email') }}</label>
<input v-model="newMemberEmail" type="email" class="mt-1 block w-full rounded-lg border-slate-600 bg-slate-700/50 text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2.5 border placeholder-slate-400" :placeholder="$t('garages.employees.emailPlaceholder')" />
</div>
<div>
<label class="block text-sm font-medium text-slate-300">{{ $t('garages.employees.role') }}</label>
<select v-model="newMemberRole" class="mt-1 block w-full rounded-lg border-slate-600 bg-slate-700/50 text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2.5 border">
<option value="member">{{ $t('garages.employees.roleMember') }}</option>
<option value="manager">{{ $t('garages.employees.roleManager') }}</option>
<option value="admin">{{ $t('garages.employees.roleAdmin') }}</option>
</select>
</div>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-2">
<button @click="addEmployee" :disabled="addingMember || !newMemberEmail" type="button" class="w-full inline-flex justify-center rounded-xl border border-transparent shadow-sm px-4 py-2.5 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:w-auto sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed transition">
<svg v-if="addingMember" class="w-4 h-4 animate-spin mr-2" 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>
{{ addingMember ? $t('garages.employees.adding') : $t('garages.employees.add') }}
</button>
<button @click="showAddEmployeeModal = false" type="button" class="mt-3 w-full inline-flex justify-center rounded-xl border border-slate-600 shadow-sm px-4 py-2.5 bg-slate-700/50 text-base font-medium text-slate-300 hover:bg-slate-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm transition">
{{ $t('garages.employees.cancel') }}
</button>
</div>
</div>
</div>
</div>
<!-- Edit Role Modal (Dark Theme) -->
<div v-if="showEditRoleModal" class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-slate-900/80 backdrop-blur-sm transition-opacity" @click="showEditRoleModal = false"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-slate-800 rounded-2xl px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl border border-slate-700 transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-yellow-500/20 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-yellow-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<h3 class="text-lg leading-6 font-medium text-white">
{{ $t('garages.employees.editRoleTitle') }}
</h3>
<p class="text-sm text-slate-400 mt-1">{{ $t('garages.employees.editRoleFor', { name: memberFullName(editingMember) }) }}</p>
<div class="mt-4">
<select v-model="editingRole" class="mt-1 block w-full rounded-lg border-slate-600 bg-slate-700/50 text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2.5 border">
<option value="member">{{ $t('garages.employees.roleMember') }}</option>
<option value="manager">{{ $t('garages.employees.roleManager') }}</option>
<option value="admin">{{ $t('garages.employees.roleAdmin') }}</option>
</select>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-2">
<button @click="saveRoleEdit" :disabled="savingRole" type="button" class="w-full inline-flex justify-center rounded-xl border border-transparent shadow-sm px-4 py-2.5 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:w-auto sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed transition">
<svg v-if="savingRole" class="w-4 h-4 animate-spin mr-2" 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>
{{ savingRole ? $t('garages.employees.saving') : $t('garages.employees.saveRole') }}
</button>
<button @click="showEditRoleModal = false" type="button" class="mt-3 w-full inline-flex justify-center rounded-xl border border-slate-600 shadow-sm px-4 py-2.5 bg-slate-700/50 text-base font-medium text-slate-300 hover:bg-slate-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm transition">
{{ $t('garages.employees.cancel') }}
</button>
</div>
</div>
</div>
</div>
<!-- Remove Confirmation Modal (Dark Theme) -->
<div v-if="showRemoveModal" class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-slate-900/80 backdrop-blur-sm transition-opacity" @click="showRemoveModal = false"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-slate-800 rounded-2xl px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl border border-slate-700 transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-500/20 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-white">
{{ $t('garages.employees.removeTitle') }}
</h3>
<div class="mt-2">
<p class="text-sm text-slate-400">
{{ $t('garages.employees.removeConfirm', { name: memberFullName(removingMember) }) }}
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-2">
<button @click="executeRemove" :disabled="removing" type="button" class="w-full inline-flex justify-center rounded-xl border border-transparent shadow-sm px-4 py-2.5 bg-red-600 text-base font-medium text-white hover:bg-red-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed transition">
<svg v-if="removing" class="w-4 h-4 animate-spin mr-2" 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>
{{ removing ? $t('garages.employees.removing') : $t('garages.employees.remove') }}
</button>
<button @click="showRemoveModal = false" type="button" class="mt-3 w-full inline-flex justify-center rounded-xl border border-slate-600 shadow-sm px-4 py-2.5 bg-slate-700/50 text-base font-medium text-slate-300 hover:bg-slate-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm transition">
{{ $t('garages.employees.cancel') }}
</button>
</div>
</div>
</div>
</div>
<!-- Notification Toast (Dark Theme) -->
<transition enter-active-class="transition ease-out duration-300" enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2" enter-to-class="translate-y-0 opacity-100 sm:translate-x-0" leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100" leave-to-class="opacity-0">
<div v-if="notification.show" class="fixed top-4 right-4 z-[100] max-w-sm w-full bg-slate-800 border border-slate-700 shadow-lg rounded-xl pointer-events-auto overflow-hidden">
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg v-if="notification.type === 'success'" class="h-6 w-6 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<svg v-if="notification.type === 'error'" class="h-6 w-6 text-red-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
</div>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-medium text-white">{{ notification.message }}</p>
</div>
<div class="ml-4 flex-shrink-0 flex">
<button @click="notification.show = false" class="bg-slate-700/50 rounded-md inline-flex text-slate-400 hover:text-slate-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span class="sr-only">Close</span>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '~/stores/auth'
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const orgId = route.params.id as string
// State
const loading = ref(true)
const error = ref<string | null>(null)
const garage = ref<any>(null)
const members = ref<any[]>([])
const activeTab = ref('general')
const fleetVehicles = ref<any[]>([])
const fleetLoading = ref(false)
// Edit Modal
const showEditModal = ref(false)
const editModalTab = ref('basic')
const saving = ref(false)
const editForm = ref({
full_name: '',
name: '',
display_name: '',
email: '',
phone: '',
tax_number: '',
reg_number: '',
org_type: '',
address_zip: '',
address_city: '',
address_street_name: '',
address_street_type: '',
address_house_number: '',
contact_person_name: '',
contact_email: '',
contact_phone: '',
billing_zip: '',
billing_city: '',
billing_street_name: '',
billing_street_type: '',
billing_house_number: '',
notification_zip: '',
notification_city: '',
notification_street_name: '',
notification_street_type: '',
notification_house_number: '',
})
// Add Employee Modal
const showAddEmployeeModal = ref(false)
const newMemberEmail = ref('')
const newMemberRole = ref('member')
const addingMember = ref(false)
// Edit Role Modal
const showEditRoleModal = ref(false)
const editingMember = ref<any>(null)
const editingRole = ref('member')
const savingRole = ref(false)
// Remove Modal
const showRemoveModal = ref(false)
const removingMember = ref<any>(null)
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' },
]
const editModalTabs = [
{ key: 'basic', label: 'garages.details.editBasicTab' },
{ key: 'contact', label: 'garages.details.editContactTab' },
{ key: 'addresses', label: 'garages.details.editAddressTab' },
]
// RBAC helper
function hasOrgPermission(perm: string): boolean {
if (auth.isAdmin) return true
if (auth.user?.system_capabilities?.[perm]) return true
if (auth.user?.org_capabilities?.[orgId]?.[perm]) return true
return false
}
// 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 }[] = []
const g = garage.value
const isIndividual = g.org_type === 'individual'
// Private garages (individual) don't need tax_number or reg_number
if (!isIndividual) {
if (!g.tax_number) missing.push({ key: 'tax_number', label: 'garages.details.missing_tax_number' })
}
// Contact email/phone - always required
if (!g.contact_email && !g.email) missing.push({ key: 'contact_email', label: 'garages.details.missing_contact_email' })
if (!g.contact_phone && !g.phone) missing.push({ key: 'contact_phone', label: 'garages.details.missing_contact_phone' })
// Billing address - always required for business, optional for individual
if (!isIndividual) {
if (!g.billing_city || !g.billing_zip) missing.push({ key: 'billing', label: 'garages.details.missing_billing' })
}
// Subscription check - "Free" tier is valid, don't flag it
if (!g.subscription || !g.subscription.tier_name) {
// Only flag if there's truly no subscription object at all
// A subscription with tier_name='Free' is valid
}
return missing
})
const formattedAddress = computed(() => {
if (!garage.value) return ''
const parts = [
garage.value.address_zip,
garage.value.address_city,
garage.value.address_street_name,
garage.value.address_street_type,
garage.value.address_house_number,
].filter(Boolean)
return parts.join(' ') || '-'
})
const formattedBillingAddress = computed(() => {
if (!garage.value) return ''
const parts = [
garage.value.billing_zip,
garage.value.billing_city,
garage.value.billing_street_name,
garage.value.billing_street_type,
garage.value.billing_house_number,
].filter(Boolean)
return parts.join(' ') || ''
})
const formattedNotificationAddress = computed(() => {
if (!garage.value) return ''
const parts = [
garage.value.notification_zip,
garage.value.notification_city,
garage.value.notification_street_name,
garage.value.notification_street_type,
garage.value.notification_house_number,
].filter(Boolean)
return parts.join(' ') || ''
})
// API calls
async function fetchDetails() {
loading.value = true
error.value = null
try {
const tokenCookie = useCookie('access_token')
const token = (tokenCookie.value || auth.token || '').trim()
const headers: Record<string, string> = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
}
const res = await fetch(`/api/v1/admin/organizations/${orgId}/details`, { headers })
if (!res.ok) throw new Error(`Failed to fetch organization: ${res.status}`)
const data = await res.json()
garage.value = data
members.value = data.members || []
} catch (e: any) {
error.value = e.message || 'Failed to load garage details'
} finally {
loading.value = false
}
}
async function fetchFleetVehicles() {
fleetLoading.value = true
try {
const tokenCookie = useCookie('access_token')
const token = (tokenCookie.value || auth.token || '').trim()
const res = await fetch(`/api/v1/admin/organizations/${orgId}/vehicles`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
})
if (!res.ok) throw new Error(`Failed to fetch fleet: ${res.status}`)
const data = await res.json()
fleetVehicles.value = data.vehicles || []
} catch (e: any) {
console.error('Fleet fetch error:', e)
fleetVehicles.value = []
} finally {
fleetLoading.value = false
}
}
// Edit Modal
function openEditModal() {
if (!garage.value) return
editForm.value = {
full_name: garage.value.full_name || '',
name: garage.value.name || '',
display_name: garage.value.display_name || '',
email: garage.value.email || '',
phone: garage.value.phone || '',
tax_number: garage.value.tax_number || '',
reg_number: garage.value.reg_number || '',
org_type: garage.value.org_type || '',
address_zip: garage.value.address_zip || '',
address_city: garage.value.address_city || '',
address_street_name: garage.value.address_street_name || '',
address_street_type: garage.value.address_street_type || '',
address_house_number: garage.value.address_house_number || '',
contact_person_name: garage.value.contact_person_name || '',
contact_email: garage.value.contact_email || '',
contact_phone: garage.value.contact_phone || '',
billing_zip: garage.value.billing_zip || '',
billing_city: garage.value.billing_city || '',
billing_street_name: garage.value.billing_street_name || '',
billing_street_type: garage.value.billing_street_type || '',
billing_house_number: garage.value.billing_house_number || '',
notification_zip: garage.value.notification_zip || '',
notification_city: garage.value.notification_city || '',
notification_street_name: garage.value.notification_street_name || '',
notification_street_type: garage.value.notification_street_type || '',
notification_house_number: garage.value.notification_house_number || '',
}
editModalTab.value = 'basic'
showEditModal.value = true
}
function closeEditModal() {
showEditModal.value = false
}
async function saveEdit() {
saving.value = true
try {
const tokenCookie = useCookie('access_token')
const token = (tokenCookie.value || auth.token || '').trim()
// Build payload: only send non-empty fields
const payload: Record<string, any> = {}
for (const [key, value] of Object.entries(editForm.value)) {
if (value !== '' && value !== null && value !== undefined) {
payload[key] = value
}
}
// For individual (private) garages, also map contact_email/phone to org-level email/phone
if (garage.value?.org_type === 'individual') {
if (payload.contact_email && !payload.email) payload.email = payload.contact_email
if (payload.contact_phone && !payload.phone) payload.phone = payload.contact_phone
}
const res = await fetch(`/api/v1/admin/organizations/${orgId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
if (!res.ok) throw new Error(`Save failed: ${res.status}`)
const result = await res.json()
// API returns { status, message, org_id, updated_fields, organization: {...} }
if (result.organization) {
// Merge the returned org fields into our local garage object
garage.value = { ...garage.value, ...result.organization }
} else {
garage.value = { ...garage.value, ...result }
}
closeEditModal()
showNotification('success', 'Garage updated successfully')
// P0 HOTFIX: Refresh all data from server after successful save
await fetchDetails()
} catch (e: any) {
showNotification('error', e.message || 'Failed to save')
} finally {
saving.value = false
}
}
// Employee actions
async function addEmployee() {
if (!newMemberEmail.value) return
addingMember.value = true
try {
const tokenCookie = useCookie('access_token')
const token = (tokenCookie.value || auth.token || '').trim()
const res = await fetch(`/api/v1/admin/organizations/${orgId}/members`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: newMemberEmail.value,
role: newMemberRole.value,
}),
})
if (!res.ok) {
const errData = await res.json().catch(() => ({}))
throw new Error(errData.detail || `Add member failed: ${res.status}`)
}
showAddEmployeeModal.value = false
newMemberEmail.value = ''
newMemberRole.value = 'member'
showNotification('success', 'Member added successfully')
await fetchDetails()
} catch (e: any) {
showNotification('error', e.message || 'Failed to add member')
} finally {
addingMember.value = false
}
}
function openEditRole(member: any) {
editingMember.value = member
editingRole.value = member.role || 'member'
showEditRoleModal.value = true
}
async function saveRoleEdit() {
if (!editingMember.value) return
savingRole.value = true
try {
const tokenCookie = useCookie('access_token')
const token = (tokenCookie.value || auth.token || '').trim()
const res = await fetch(`/api/v1/admin/organizations/${orgId}/members/${editingMember.value.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ role: editingRole.value }),
})
if (!res.ok) throw new Error(`Update role failed: ${res.status}`)
showEditRoleModal.value = false
editingMember.value = null
showNotification('success', 'Role updated successfully')
await fetchDetails()
} catch (e: any) {
showNotification('error', e.message || 'Failed to update role')
} finally {
savingRole.value = false
}
}
function openRemoveModal(member: any) {
removingMember.value = member
showRemoveModal.value = true
}
async function executeRemove() {
if (!removingMember.value) return
removing.value = true
try {
const tokenCookie = useCookie('access_token')
const token = (tokenCookie.value || auth.token || '').trim()
const res = await fetch(`/api/v1/admin/organizations/${orgId}/members/${removingMember.value.id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
},
})
if (!res.ok) throw new Error(`Remove failed: ${res.status}`)
showRemoveModal.value = false
removingMember.value = null
showNotification('success', 'Member removed successfully')
await fetchDetails()
} catch (e: any) {
showNotification('error', e.message || 'Failed to remove member')
} finally {
removing.value = false
}
}
// Notification helper
function showNotification(type: 'success' | 'error', message: string) {
notification.value = { show: true, type, message }
setTimeout(() => {
notification.value.show = false
}, 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.full_name || 'Garage #' + garage.value.id
}
function getInitials(name: string): string {
if (!name) return '?'
return name.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2)
}
function getInitialsColor(name: string): string {
const colors = [
'bg-red-500', 'bg-yellow-500', 'bg-green-500', 'bg-blue-500',
'bg-indigo-500', 'bg-purple-500', 'bg-pink-500', 'bg-teal-500',
]
let hash = 0
for (let i = 0; i < (name || '').length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash)
}
return colors[Math.abs(hash) % colors.length]
}
function statusLabel(status: string): string {
const map: Record<string, string> = {
active: 'Active',
inactive: 'Inactive',
suspended: 'Suspended',
pending: 'Pending',
}
return map[status?.toLowerCase()] || status
}
function statusBadgeClass(status: string): string {
const map: Record<string, string> = {
active: 'bg-green-100 text-green-800',
inactive: 'bg-gray-100 text-gray-800',
suspended: 'bg-red-100 text-red-800',
pending: 'bg-yellow-100 text-yellow-800',
}
return map[status?.toLowerCase()] || 'bg-gray-100 text-gray-800'
}
function statusDotClass(status: string): string {
const map: Record<string, string> = {
active: 'bg-green-400',
inactive: 'bg-gray-400',
suspended: 'bg-red-400',
pending: 'bg-yellow-400',
}
return map[status?.toLowerCase()] || 'bg-gray-400'
}
function orgTypeLabel(type: string): string {
const map: Record<string, string> = {
dealership: 'Dealership',
service: 'Service Center',
rental: 'Rental Agency',
fleet: 'Fleet Operator',
individual: 'Individual',
workshop: 'Workshop',
garage: 'Garage',
other: 'Other',
}
return map[type?.toLowerCase()] || type || '-'
}
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return '-'
try {
return new Date(dateStr).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
} catch {
return dateStr
}
}
function tierBadgeClass(tier: string | null | undefined): string {
const map: Record<string, string> = {
basic: 'bg-gray-100 text-gray-800',
premium: 'bg-yellow-100 text-yellow-800',
enterprise: 'bg-purple-100 text-purple-800',
trial: 'bg-blue-100 text-blue-800',
}
return map[tier?.toLowerCase()] || 'bg-gray-100 text-gray-800'
}
function memberFullName(member: any): string {
if (!member) return ''
// P0 HOTFIX: Try person data as additional fallback chain
// for synthetic OWNER entries that only have person nested data
if (member.display_name) return member.display_name
if (member.name) return member.name
if (member.email) return member.email
// Try person nested data
if (member.person) {
const first = member.person.first_name || ''
const last = member.person.last_name || ''
const full = `${last} ${first}`.trim()
if (full) return full
if (member.person.email) return member.person.email
}
if (member.user_id) return String(member.user_id)
return 'Unknown'
}
function roleLabel(role: string): string {
const map: Record<string, string> = {
admin: 'Admin',
manager: 'Manager',
member: 'Member',
owner: 'Owner',
}
return map[role?.toLowerCase()] || role || 'Member'
}
function roleBadgeClass(role: string): string {
const map: Record<string, string> = {
admin: 'bg-purple-100 text-purple-800',
manager: 'bg-blue-100 text-blue-800',
member: 'bg-green-100 text-green-800',
owner: 'bg-yellow-100 text-yellow-800',
}
return map[role?.toLowerCase()] || 'bg-gray-100 text-gray-800'
}
function memberStatusLabel(status: string): string {
const map: Record<string, string> = {
active: 'Active',
invited: 'Invited',
suspended: 'Suspended',
inactive: 'Inactive',
}
return map[status?.toLowerCase()] || status || 'Unknown'
}
function memberStatusBadgeClass(status: string): string {
const map: Record<string, string> = {
active: 'bg-green-100 text-green-800',
invited: 'bg-blue-100 text-blue-800',
suspended: 'bg-red-100 text-red-800',
inactive: 'bg-gray-100 text-gray-800',
}
return map[status?.toLowerCase()] || 'bg-gray-100 text-gray-800'
}
function memberStatusDotClass(status: string): string {
const map: Record<string, string> = {
active: 'bg-green-400',
invited: 'bg-blue-400',
suspended: 'bg-red-400',
inactive: 'bg-gray-400',
}
return map[status?.toLowerCase()] || 'bg-gray-400'
}
function fleetStatusLabel(status: string): string {
const map: Record<string, string> = {
active: 'Active',
inactive: 'Inactive',
sold: 'Sold',
scrapped: 'Scrapped',
pending: 'Pending',
draft: 'Draft',
}
return map[status?.toLowerCase()] || status || 'Unknown'
}
function fleetStatusBadgeClass(status: string): string {
const map: Record<string, string> = {
active: 'bg-green-500/10 text-green-400',
inactive: 'bg-gray-500/10 text-gray-400',
sold: 'bg-red-500/10 text-red-400',
scrapped: 'bg-red-500/10 text-red-400',
pending: 'bg-yellow-500/10 text-yellow-400',
draft: 'bg-gray-500/10 text-gray-400',
}
return map[status?.toLowerCase()] || 'bg-gray-500/10 text-gray-400'
}
function fleetStatusDotClass(status: string): string {
const map: Record<string, string> = {
active: 'bg-green-400',
inactive: 'bg-gray-400',
sold: 'bg-red-400',
scrapped: 'bg-red-400',
pending: 'bg-yellow-400',
draft: 'bg-gray-400',
}
return map[status?.toLowerCase()] || 'bg-gray-400'
}
// Lifecycle
onMounted(() => {
fetchDetails()
})
// Watch activeTab to fetch fleet vehicles when the Fleet tab becomes active
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>