Files
service-finder/frontend_admin/.nuxt/dist/server/_nuxt/index-o-V5Lz0_.js.map.json

1 line
34 KiB
JSON

{"file":"index-o-V5Lz0_.js","mappings":"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","names":["$t","_ssrInterpolate","_ssrRenderAttr","_ssrRenderList","_ssrRenderClass","_ssrIncludeBooleanAttr"],"sources":["../../../../pages/garages/index.vue"],"sourcesContent":["<template>\n <div>\n <!-- Page Header -->\n <div class=\"mb-8\">\n <h1 class=\"text-2xl font-bold text-white\">{{ $t('garages.title') }}</h1>\n <p class=\"text-slate-400 mt-1\">{{ $t('garages.subtitle') }}</p>\n </div>\n\n <!-- Loading State -->\n <div v-if=\"loading\" class=\"flex items-center justify-center py-20\">\n <div class=\"flex flex-col items-center gap-3\">\n <svg class=\"w-8 h-8 text-indigo-400 animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n <p class=\"text-slate-400 text-sm\">{{ $t('garages.loading') }}</p>\n </div>\n </div>\n\n <!-- Error State -->\n <div v-else-if=\"error\" class=\"flex flex-col items-center justify-center py-20 text-center\">\n <svg class=\"w-12 h-12 text-red-500 mb-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n <p class=\"text-red-400 text-sm mb-2\">{{ error }}</p>\n <button @click=\"fetchGarages\" class=\"px-4 py-2 text-sm bg-indigo-600/20 text-indigo-300 hover:bg-indigo-600/30 rounded-lg transition\">\n {{ $t('garages.retry') }}\n </button>\n </div>\n\n <!-- Content -->\n <template v-else>\n <!-- Stats Cards -->\n <div class=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8\">\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-4\">\n <div class=\"flex items-center gap-3\">\n <div class=\"p-2 rounded-lg bg-indigo-500/10 text-indigo-400\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n </div>\n <div>\n <p class=\"text-xs text-slate-400 uppercase tracking-wider\">{{ $t('garages.total_garages') }}</p>\n <p class=\"text-2xl font-bold text-white\">{{ garages.length }}</p>\n </div>\n </div>\n </div>\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-4\">\n <div class=\"flex items-center gap-3\">\n <div class=\"p-2 rounded-lg bg-emerald-500/10 text-emerald-400\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n </div>\n <div>\n <p class=\"text-xs text-slate-400 uppercase tracking-wider\">{{ $t('garages.active_garages') }}</p>\n <p class=\"text-2xl font-bold text-emerald-400\">{{ activeCount }}</p>\n </div>\n </div>\n </div>\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-4\">\n <div class=\"flex items-center gap-3\">\n <div class=\"p-2 rounded-lg bg-amber-500/10 text-amber-400\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n </div>\n <div>\n <p class=\"text-xs text-slate-400 uppercase tracking-wider\">{{ $t('garages.premium_garages') }}</p>\n <p class=\"text-2xl font-bold text-amber-400\">{{ premiumCount }}</p>\n </div>\n </div>\n </div>\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-4\">\n <div class=\"flex items-center gap-3\">\n <div class=\"p-2 rounded-lg bg-purple-500/10 text-purple-400\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n </div>\n <div>\n <p class=\"text-xs text-slate-400 uppercase tracking-wider\">{{ $t('garages.enterprise_garages') }}</p>\n <p class=\"text-2xl font-bold text-purple-400\">{{ enterpriseCount }}</p>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Search & Filter Bar -->\n <div class=\"flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6\">\n <div class=\"relative flex-1 max-w-md\">\n <svg class=\"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\" />\n </svg>\n <input\n v-model=\"searchQuery\"\n type=\"text\"\n :placeholder=\"$t('garages.search_placeholder')\"\n class=\"w-full pl-10 pr-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 transition\"\n />\n </div>\n <div class=\"flex items-center gap-2\">\n <select\n v-model=\"filterTier\"\n class=\"px-3 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition\"\n >\n <option value=\"all\">{{ $t('garages.all_tiers') }}</option>\n <option value=\"free\">Free</option>\n <option value=\"premium\">Premium</option>\n <option value=\"enterprise\">Enterprise</option>\n </select>\n </div>\n </div>\n\n <!-- Data Table -->\n <div class=\"bg-slate-800/50 border border-slate-700 rounded-xl overflow-hidden\">\n <div class=\"overflow-x-auto\">\n <table class=\"w-full text-sm\">\n <thead>\n <tr class=\"border-b border-slate-700 bg-slate-800/80\">\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">ID</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('garages.company_name') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('garages.status') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('garages.current_package') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('garages.expiration_date') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('garages.actions') }}</th>\n </tr>\n </thead>\n <tbody>\n <tr\n v-for=\"garage in filteredGarages\"\n :key=\"garage.id\"\n class=\"border-b border-slate-700/50 hover:bg-slate-700/30 transition\"\n >\n <!-- ID -->\n <td class=\"py-3.5 px-4 text-slate-400 font-mono text-xs\">{{ garage.id }}</td>\n\n <!-- Company Name (display_name priority, fallback to name) -->\n <td class=\"py-3.5 px-4\">\n <div class=\"flex items-center gap-3\">\n <div\n class=\"w-8 h-8 rounded-lg flex items-center justify-center text-sm font-bold text-white\"\n :class=\"getInitialsColor(garageDisplayName(garage))\"\n >\n {{ getInitials(garageDisplayName(garage)) }}\n </div>\n <div>\n <p class=\"text-sm font-medium text-white\">{{ garageDisplayName(garage) }}</p>\n <p class=\"text-xs text-slate-500\">{{ garage.city || '—' }} · {{ garage.member_count }} {{ $t('garages.members') }}</p>\n </div>\n </div>\n </td>\n\n <!-- Status -->\n <td class=\"py-3.5 px-4\">\n <span\n class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium\"\n :class=\"statusBadgeClass(garage.status)\"\n >\n <span\n class=\"w-1.5 h-1.5 rounded-full mr-1.5\"\n :class=\"statusDotClass(garage.status)\"\n ></span>\n {{ statusLabel(garage.status) }}\n </span>\n </td>\n\n <!-- Current Package -->\n <td class=\"py-3.5 px-4\">\n <span\n class=\"px-2.5 py-0.5 rounded-full text-xs font-medium\"\n :class=\"tierBadgeClass(garage.subscription_tier_name)\"\n >\n {{ garage.subscription_tier_name }}\n </span>\n </td>\n\n <!-- Expiration Date -->\n <td class=\"py-3.5 px-4\">\n <span v-if=\"garage.subscription_expires_at\" class=\"text-slate-300 text-xs\">\n {{ formatDate(garage.subscription_expires_at) }}\n </span>\n <span v-else class=\"text-slate-500 text-xs italic\">{{ $t('garages.indefinite') }}</span>\n </td>\n\n <!-- Actions -->\n <td class=\"py-3.5 px-4\">\n <div class=\"flex items-center gap-2\">\n <button\n @click=\"openDetails(garage)\"\n class=\"px-3 py-1.5 text-xs font-medium bg-slate-600/20 text-slate-300 hover:bg-slate-600/30 rounded-lg transition\"\n :title=\"$t('garages.view_details')\"\n >\n {{ $t('garages.view_details') }}\n </button>\n <button\n @click=\"openSubscriptionModal(garage)\"\n class=\"px-3 py-1.5 text-xs font-medium bg-indigo-600/20 text-indigo-300 hover:bg-indigo-600/30 rounded-lg transition\"\n >\n {{ $t('garages.manage_subscription') }}\n </button>\n <button\n @click=\"toggleStatus(garage)\"\n class=\"px-3 py-1.5 text-xs font-medium rounded-lg transition\"\n :class=\"garage.is_active\n ? 'bg-red-500/10 text-red-300 hover:bg-red-500/20'\n : 'bg-emerald-500/10 text-emerald-300 hover:bg-emerald-500/20'\"\n >\n {{ garage.is_active ? $t('garages.deactivate') : $t('garages.activate') }}\n </button>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Empty State -->\n <div\n v-if=\"filteredGarages.length === 0 && !loading\"\n class=\"flex flex-col items-center justify-center py-16 text-center\"\n >\n <svg class=\"w-12 h-12 text-slate-600 mb-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" 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\" />\n </svg>\n <p class=\"text-slate-400 text-sm\">{{ $t('garages.no_results') }}</p>\n </div>\n </div>\n </template>\n\n <!-- ================================================================ -->\n <!-- Subscription Management Modal -->\n <!-- ================================================================ -->\n <div\n v-if=\"showSubscriptionModal\"\n class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm\"\n @click.self=\"closeSubscriptionModal\"\n >\n <div class=\"bg-slate-800 border border-slate-700 rounded-2xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden\">\n <!-- Modal Header -->\n <div class=\"px-6 py-4 border-b border-slate-700 flex items-center justify-between\">\n <div>\n <h3 class=\"text-lg font-semibold text-white\">{{ $t('garages.manage_subscription') }}</h3>\n <p class=\"text-sm text-slate-400 mt-0.5\">{{ selectedGarage ? garageDisplayName(selectedGarage) : '' }}</p>\n </div>\n <button @click=\"closeSubscriptionModal\" class=\"p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-700 transition\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n <!-- Modal Body -->\n <div class=\"px-6 py-5 space-y-5\">\n <!-- Current Subscription Info -->\n <div class=\"bg-slate-900/50 rounded-xl p-4 border border-slate-700/50\">\n <p class=\"text-xs text-slate-500 uppercase tracking-wider mb-1\">{{ $t('garages.current_package') }}</p>\n <p class=\"text-sm font-medium text-white\">\n {{ selectedGarage?.subscription_tier_name || $t('garages.free_fallback') }}\n </p>\n <p v-if=\"selectedGarage?.subscription_expires_at\" class=\"text-xs text-slate-400 mt-1\">\n {{ $t('garages.expires') }}: {{ formatDate(selectedGarage.subscription_expires_at) }}\n </p>\n </div>\n\n <!-- Tier Selection -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-2\">{{ $t('garages.select_package') }}</label>\n <select\n v-model=\"selectedTierId\"\n 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\"\n >\n <option value=\"\" disabled>{{ $t('garages.select_package_placeholder') }}</option>\n <option\n v-for=\"tier in availableTiers\"\n :key=\"tier.id\"\n :value=\"tier.id\"\n >\n {{ tier.name }} (Level {{ tier.tier_level }})\n </option>\n </select>\n </div>\n\n <!-- Custom Expiration Date -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-2\">\n {{ $t('garages.custom_expiration') }}\n <span class=\"text-xs text-slate-500 ml-1\">({{ $t('garages.optional') }})</span>\n </label>\n <input\n v-model=\"customExpiresAt\"\n type=\"datetime-local\"\n 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\"\n />\n <p class=\"text-xs text-slate-500 mt-1.5\">{{ $t('garages.custom_expiration_hint') }}</p>\n </div>\n\n <!-- Save Error -->\n <div\n v-if=\"saveError\"\n class=\"bg-red-500/10 border border-red-500/20 rounded-lg px-4 py-3\"\n >\n <p class=\"text-sm text-red-400\">{{ saveError }}</p>\n </div>\n </div>\n\n <!-- Modal Footer -->\n <div class=\"px-6 py-4 border-t border-slate-700 flex items-center justify-end gap-3\">\n <button\n @click=\"closeSubscriptionModal\"\n class=\"px-4 py-2 text-sm font-medium text-slate-300 hover:text-white transition\"\n >\n {{ $t('garages.cancel') }}\n </button>\n <button\n @click=\"saveSubscription\"\n :disabled=\"saving || !selectedTierId\"\n class=\"px-5 py-2 text-sm font-medium rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed\"\n :class=\"saving\n ? 'bg-indigo-600/50 text-indigo-200 cursor-wait'\n : 'bg-indigo-600 text-white hover:bg-indigo-500'\"\n >\n <span v-if=\"saving\" class=\"flex items-center gap-2\">\n <svg class=\"w-4 h-4 animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n {{ $t('garages.saving') }}\n </span>\n <span v-else>{{ $t('garages.save') }}</span>\n </button>\n </div>\n </div>\n </div>\n\n <!-- Save Notification -->\n <div\n v-if=\"saveNotification\"\n class=\"fixed bottom-6 right-6 px-5 py-3 rounded-xl shadow-xl text-sm font-medium z-50\"\n :class=\"saveNotification.type === 'success'\n ? 'bg-emerald-600 text-white'\n : 'bg-red-600 text-white'\"\n >\n {{ saveNotification.message }}\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onUnmounted } from 'vue'\n\ndefinePageMeta({\n middleware: 'auth',\n})\n\n// ── Types ──────────────────────────────────────────────────────────\n\ninterface SubscriptionInfo {\n id: number\n org_id: number\n tier_id: number\n tier_name: string\n valid_from: string | null\n valid_until: string | null\n is_active: boolean\n extra_allowances: Record<string, unknown>\n}\n\ninterface Garage {\n id: number\n name: string\n full_name: string\n display_name: string | null\n email: string | null\n status: string\n is_active: boolean\n is_verified: boolean\n is_deleted: boolean\n org_type: string\n city: string | null\n region: string | null\n segment: string | null\n member_count: number\n created_at: string | null\n subscription: SubscriptionInfo | null\n subscription_tier_name: string\n subscription_expires_at: string | null\n}\n\ninterface Tier {\n id: number\n name: string\n tier_level: number\n rules: Record<string, unknown>\n}\n\n// ── State ──────────────────────────────────────────────────────────\n\nconst loading = ref(true)\nconst error = ref<string | null>(null)\nconst garages = ref<Garage[]>([])\nconst availableTiers = ref<Tier[]>([])\nconst searchQuery = ref('')\nconst filterTier = ref('all')\n\n// Modal state\nconst showSubscriptionModal = ref(false)\nconst selectedGarage = ref<Garage | null>(null)\nconst selectedTierId = ref<number | ''>('')\nconst customExpiresAt = ref<string>('')\nconst saving = ref(false)\nconst saveError = ref<string | null>(null)\n\n// Notification\nconst saveNotification = ref<{ type: 'success' | 'error'; message: string } | null>(null)\n\n// ── Computed ───────────────────────────────────────────────────────\n\nconst activeCount = computed(() => garages.value.filter(g => g.is_active).length)\nconst premiumCount = computed(() => {\n return garages.value.filter(g => {\n const name = g.subscription_tier_name.toLowerCase()\n return name.includes('premium') || name.includes('pro')\n }).length\n})\nconst enterpriseCount = computed(() => {\n return garages.value.filter(g => {\n const name = g.subscription_tier_name.toLowerCase()\n return name.includes('enterprise') || name.includes('vip') || name.includes('corp')\n }).length\n})\n\nconst filteredGarages = computed(() => {\n return garages.value.filter(g => {\n // Search filter — search across display_name, name, full_name, and city\n if (searchQuery.value) {\n const q = searchQuery.value.toLowerCase()\n const displayName = garageDisplayName(g).toLowerCase()\n const matchesSearch =\n displayName.includes(q) ||\n g.full_name.toLowerCase().includes(q) ||\n g.name.toLowerCase().includes(q) ||\n (g.city && g.city.toLowerCase().includes(q))\n if (!matchesSearch) return false\n }\n // Tier filter\n if (filterTier.value !== 'all') {\n const name = g.subscription_tier_name.toLowerCase()\n if (filterTier.value === 'free' && !name.includes('free') && !name.includes('fallback')) return false\n if (filterTier.value === 'premium' && !name.includes('premium') && !name.includes('pro')) return false\n if (filterTier.value === 'enterprise' && !name.includes('enterprise') && !name.includes('vip') && !name.includes('corp')) return false\n }\n return true\n })\n})\n\n// ── Auth Helper ────────────────────────────────────────────────────\n\nfunction getAuthHeaders() {\n const tokenCookie = useCookie('access_token')\n const token = tokenCookie.value\n return token ? { Authorization: `Bearer ${token}` } : {}\n}\n\n// ── Data Fetching ──────────────────────────────────────────────────\n\nasync function fetchGarages() {\n loading.value = true\n error.value = null\n try {\n const response = await $fetch<{ total: number; skip: number; limit: number; garages: Garage[] }>('/api/v1/admin/organizations', {\n params: { limit: 200 },\n headers: getAuthHeaders(),\n })\n garages.value = response.garages\n } catch (err: any) {\n console.error('[Garages] Failed to fetch garages:', err)\n error.value = err?.data?.detail || err?.message || 'Failed to load garages'\n } finally {\n loading.value = false\n }\n}\n\nasync function fetchTiers() {\n try {\n const response = await $fetch<{ total: number; tiers: Tier[] }>('/api/v1/admin/packages', {\n params: { include_hidden: true, limit: 100 },\n headers: getAuthHeaders(),\n })\n availableTiers.value = response.tiers\n } catch (err: any) {\n console.error('[Garages] Failed to fetch tiers:', err)\n }\n}\n\n// ── Subscription Modal ─────────────────────────────────────────────\n\nfunction openSubscriptionModal(garage: Garage) {\n selectedGarage.value = garage\n selectedTierId.value = garage.subscription?.tier_id || ''\n customExpiresAt.value = ''\n saveError.value = null\n showSubscriptionModal.value = true\n}\n\nfunction closeSubscriptionModal() {\n showSubscriptionModal.value = false\n selectedGarage.value = null\n selectedTierId.value = ''\n customExpiresAt.value = ''\n saveError.value = null\n}\n\nasync function saveSubscription() {\n if (!selectedGarage.value || !selectedTierId.value) return\n\n saving.value = true\n saveError.value = null\n\n try {\n const payload: Record<string, unknown> = {\n tier_id: selectedTierId.value,\n }\n\n // If custom expiration date is set, include it\n if (customExpiresAt.value) {\n payload.expires_at = new Date(customExpiresAt.value).toISOString()\n }\n\n await $fetch(`/api/v1/admin/organizations/${selectedGarage.value.id}/subscription`, {\n method: 'PATCH',\n headers: {\n ...getAuthHeaders(),\n 'Content-Type': 'application/json',\n },\n body: payload,\n })\n\n showNotification('success', `\"${garageDisplayName(selectedGarage.value)}\" ${useNuxtApp().$i18n.t('garages.subscription_updated')}`)\n closeSubscriptionModal()\n\n // Refresh the list\n await fetchGarages()\n } catch (err: any) {\n console.error('[Garages] Failed to save subscription:', err)\n saveError.value = err?.data?.detail || err?.message || 'Failed to save subscription'\n } finally {\n saving.value = false\n }\n}\n\n// ── Status Toggle ──────────────────────────────────────────────────\n\nasync function toggleStatus(garage: Garage) {\n // This would call a PATCH endpoint to toggle is_active\n // For now, we just show a notification that this is not yet implemented\n showNotification('error', `Status toggle for \"${garageDisplayName(garage)}\" not yet implemented via API`)\n}\n\n// ── Helpers ────────────────────────────────────────────────────────\n\n/**\n * Returns the display name for a garage, prioritizing display_name over name.\n * This ensures clean, human-readable names are shown in the UI.\n */\nfunction garageDisplayName(garage: Garage): string {\n return garage.display_name || garage.name || garage.full_name\n}\n\nfunction openDetails(garage: Garage) {\n navigateTo(`/garages/${garage.id}`)\n}\n\nfunction getInitials(name: string): string {\n return name\n .split(' ')\n .map(w => w.charAt(0))\n .join('')\n .toUpperCase()\n .slice(0, 2)\n}\n\nfunction getInitialsColor(name: string): string {\n const colors = [\n 'bg-amber-600', 'bg-purple-600', 'bg-slate-600',\n 'bg-indigo-600', 'bg-emerald-600', 'bg-rose-600',\n 'bg-cyan-600', 'bg-orange-600', 'bg-teal-600',\n ]\n let hash = 0\n for (let i = 0; i < name.length; i++) {\n hash = name.charCodeAt(i) + ((hash << 5) - hash)\n }\n return colors[Math.abs(hash) % colors.length]\n}\n\nfunction statusBadgeClass(status: string): string {\n if (status === 'active') return 'bg-emerald-500/10 text-emerald-400'\n if (status === 'suspended') return 'bg-red-500/10 text-red-400'\n if (status === 'pending_verification') return 'bg-amber-500/10 text-amber-400'\n return 'bg-slate-500/10 text-slate-400'\n}\n\nfunction statusDotClass(status: string): string {\n if (status === 'active') return 'bg-emerald-400'\n if (status === 'suspended') return 'bg-red-400'\n if (status === 'pending_verification') return 'bg-amber-400'\n return 'bg-slate-400'\n}\n\nfunction statusLabel(status: string): string {\n if (status === 'active') return 'Aktív'\n if (status === 'suspended') return 'Felfüggesztve'\n if (status === 'pending_verification') return 'Függőben'\n return 'Inaktív'\n}\n\nfunction tierBadgeClass(tierName: string): string {\n const name = tierName.toLowerCase()\n if (name.includes('enterprise') || name.includes('vip') || name.includes('corp')) return 'bg-purple-500/20 text-purple-300'\n if (name.includes('premium') || name.includes('pro')) return 'bg-amber-500/20 text-amber-300'\n return 'bg-slate-500/20 text-slate-300'\n}\n\nfunction formatDate(dateStr: string): string {\n try {\n const d = new Date(dateStr)\n return d.toLocaleDateString('hu-HU', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n })\n } catch {\n return dateStr\n }\n}\n\nfunction showNotification(type: 'success' | 'error', message: string) {\n saveNotification.value = { type, message }\n setTimeout(() => {\n saveNotification.value = null\n }, 4000)\n}\n\n// ── Lifecycle ──────────────────────────────────────────────────────\n\nonMounted(() => {\n fetchGarages()\n fetchTiers()\n})\n</script>\n"],"version":3}