2112 lines
110 KiB
Vue
2112 lines
110 KiB
Vue
<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">​</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">​</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">​</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>
|