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

1 line
57 KiB
JSON

{"file":"index-BPETUF7a.js","mappings":"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","names":["$t","_ssrRenderList","_ssrInterpolate","_ssrRenderAttr","_ssrIncludeBooleanAttr","_ssrLooseContain","_ssrLooseEqual","_unref","_ssrRenderClass"],"sources":["../../../../composables/useFormatter.ts","../../../../pages/packages/index.vue"],"sourcesContent":["import { useRegionStore } from '~/stores/region'\n\n/**\n * 🌍 useFormatter Composable\n *\n * Provides locale-aware formatting utilities for numbers, currencies, dates,\n * and percentages. Uses the active region configuration from the RegionStore\n * to determine the appropriate locale, currency, and timezone.\n *\n * Falls back to 'en-EU' locale and 'EUR' currency if no region is configured.\n */\nexport function useFormatter() {\n const regionStore = useRegionStore()\n\n /**\n * Format a number according to the active locale.\n * Example: 1234567.89 → \"1,234,567.89\" (en) or \"1 234 567,89\" (hu)\n */\n function formatNumber(value: number, options?: Intl.NumberFormatOptions): string {\n const locale = regionStore.activeLocale || 'en-EU'\n try {\n return new Intl.NumberFormat(locale, options).format(value)\n } catch {\n return new Intl.NumberFormat('en-EU', options).format(value)\n }\n }\n\n /**\n * Format a currency value according to the active region's currency and locale.\n * Example: 1234.50 → \"€1,234.50\" (EUR/en) or \"3 456 Ft\" (HUF/hu)\n */\n function formatCurrency(\n value: number,\n currency?: string,\n options?: Intl.NumberFormatOptions,\n ): string {\n const locale = regionStore.activeLocale || 'en-EU'\n const curr = currency || regionStore.activeCurrency || 'EUR'\n try {\n return new Intl.NumberFormat(locale, {\n style: 'currency',\n currency: curr,\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n ...options,\n }).format(value)\n } catch {\n return `${value.toFixed(2)} ${curr}`\n }\n }\n\n /**\n * Format a date according to the active locale and timezone.\n * Example: 2026-06-24 → \"24/06/2026\" (en-GB) or \"2026. 06. 24.\" (hu-HU)\n */\n function formatDate(\n date: Date | string | number,\n options?: Intl.DateTimeFormatOptions,\n ): string {\n const locale = regionStore.activeLocale || 'en-EU'\n const timezone = regionStore.activeTimezone || 'Europe/Brussels'\n const d = typeof date === 'string' || typeof date === 'number' ? new Date(date) : date\n\n try {\n return new Intl.DateTimeFormat(locale, {\n timezone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n ...options,\n }).format(d)\n } catch {\n return d.toLocaleDateString('en-GB')\n }\n }\n\n /**\n * Format a datetime with time according to the active locale and timezone.\n * Example: \"2026-06-24T14:30:00Z\" → \"24/06/2026, 16:30\" (hu-HU/Budapest)\n */\n function formatDateTime(\n date: Date | string | number,\n options?: Intl.DateTimeFormatOptions,\n ): string {\n const locale = regionStore.activeLocale || 'en-EU'\n const timezone = regionStore.activeTimezone || 'Europe/Brussels'\n const d = typeof date === 'string' || typeof date === 'number' ? new Date(date) : date\n\n try {\n return new Intl.DateTimeFormat(locale, {\n timezone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n ...options,\n }).format(d)\n } catch {\n return d.toLocaleString('en-GB')\n }\n }\n\n /**\n * Format a percentage value.\n * Example: 27 → \"27%\" or 27.5 → \"27,5%\" (hu)\n */\n function formatPercent(\n value: number,\n options?: Intl.NumberFormatOptions,\n ): string {\n const locale = regionStore.activeLocale || 'en-EU'\n try {\n return new Intl.NumberFormat(locale, {\n style: 'percent',\n minimumFractionDigits: 0,\n maximumFractionDigits: 1,\n ...options,\n }).format(value / 100)\n } catch {\n return `${value}%`\n }\n }\n\n /**\n * Calculate price including VAT for the active region.\n * VAT rate comes from the active region's default_vat_rate.\n */\n function calculatePriceWithVat(\n netPrice: number,\n vatRate?: number,\n ): { net: number; vat: number; gross: number; vatRate: number } {\n const rate = vatRate ?? regionStore.activeVatRate ?? 0\n const vat = netPrice * (rate / 100)\n const gross = netPrice + vat\n return { net: netPrice, vat, gross, vatRate: rate }\n }\n\n /**\n * Format a price range with VAT breakdown.\n * Example: (100, 27) → \"Net: €100.00 | VAT: €27.00 (27%) | Gross: €127.00\"\n */\n function formatPriceWithVat(\n netPrice: number,\n vatRate?: number,\n currency?: string,\n ): string {\n const { net, vat, gross, vatRate: rate } = calculatePriceWithVat(netPrice, vatRate)\n const curr = currency || regionStore.activeCurrency || 'EUR'\n return `Net: ${formatCurrency(net, curr)} | VAT: ${formatCurrency(vat, curr)} (${rate}%) | Gross: ${formatCurrency(gross, curr)}`\n }\n\n return {\n formatNumber,\n formatCurrency,\n formatDate,\n formatDateTime,\n formatPercent,\n calculatePriceWithVat,\n formatPriceWithVat,\n }\n}\n","<template>\n <div>\n <!-- Page Header -->\n <div class=\"mb-8 flex items-center justify-between\">\n <div>\n <h1 class=\"text-2xl font-bold text-white\">{{ $t('packages.title') }}</h1>\n <p class=\"text-slate-400 mt-1\">{{ $t('packages.subtitle') }}</p>\n </div>\n <button\n @click=\"openCreateModal\"\n class=\"px-5 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white font-medium rounded-lg transition text-sm flex items-center gap-2\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\" />\n </svg>\n {{ $t('packages.create_new') }}\n </button>\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=\"animate-spin h-8 w-8 text-indigo-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path>\n </svg>\n <span class=\"text-slate-400 text-sm\">{{ $t('packages.loading') }}</span>\n </div>\n </div>\n\n <!-- Error State -->\n <div v-else-if=\"error\" class=\"bg-red-900/30 border border-red-700 rounded-xl p-6 text-center\">\n <p class=\"text-red-300 mb-3\">{{ error }}</p>\n <button\n @click=\"fetchPackages\"\n class=\"px-4 py-2 bg-red-700 hover:bg-red-600 text-white rounded-lg transition text-sm\"\n >\n {{ $t('packages.retry') }}\n </button>\n </div>\n\n <!-- Empty State -->\n <div v-else-if=\"packages.length === 0\" class=\"bg-slate-800/50 border border-slate-700 rounded-xl p-12 text-center\">\n <svg class=\"w-16 h-16 mx-auto 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=\"M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\" />\n </svg>\n <h3 class=\"text-lg font-medium text-slate-300 mb-2\">{{ $t('packages.no_packages') }}</h3>\n <p class=\"text-slate-500 mb-6\">{{ $t('packages.no_packages_desc') }}</p>\n <button\n @click=\"openCreateModal\"\n class=\"px-5 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white font-medium rounded-lg transition text-sm\"\n >\n {{ $t('packages.create_first') }}\n </button>\n </div>\n\n <!-- Package Tier Cards Grid -->\n <div v-else class=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 mb-8\">\n <div\n v-for=\"pkg in packages\"\n :key=\"pkg.id\"\n class=\"bg-slate-800 rounded-xl border border-slate-700 p-6 flex flex-col transition-all hover:border-slate-600\"\n >\n <!-- Card Header -->\n <div class=\"flex items-center justify-between mb-4\">\n <div class=\"flex items-center gap-3\">\n <div\n class=\"p-2 rounded-lg\"\n :class=\"getTierColorClass(pkg.tier_level)\"\n >\n <span class=\"w-5 h-5 text-lg\" v-html=\"getTierIcon(pkg.tier_level)\" />\n </div>\n <div>\n <h2 class=\"text-lg font-semibold text-white\">{{ getDisplayName(pkg) }}</h2>\n <span class=\"text-xs text-slate-500 font-mono\">{{ pkg.name }}</span>\n </div>\n </div>\n <div class=\"flex items-center gap-1.5 flex-wrap\">\n <span\n v-if=\"pkg.tier_level >= 1\"\n class=\"px-2 py-0.5 text-xs font-semibold rounded-full bg-indigo-500/20 text-indigo-300\"\n >\n {{ $t('packages.popular') }}\n </span>\n <span\n v-if=\"pkg.is_default_fallback\"\n class=\"px-2 py-0.5 text-xs font-semibold rounded-full bg-amber-500/20 text-amber-300\"\n title=\"Default fallback package for expired subscriptions\"\n >\n 🔒 {{ $t('packages.fallback_badge') }}\n </span>\n <span\n v-if=\"pkg.trial_days_on_signup > 0\"\n class=\"px-2 py-0.5 text-xs font-semibold rounded-full bg-emerald-500/20 text-emerald-300\"\n :title=\"`${pkg.trial_days_on_signup} days trial on signup`\"\n >\n 🎯 {{ $t('packages.trial_badge') }}: {{ pkg.trial_days_on_signup }}d\n </span>\n </div>\n </div>\n\n <!-- Price -->\n <div class=\"mb-4\">\n <span class=\"text-3xl font-bold text-white\">{{ getFormattedPrice(pkg) }}</span>\n <span class=\"text-sm text-slate-400 ml-1\">/ {{ $t('packages.month') }}</span>\n </div>\n\n <!-- Description / Subtitle -->\n <p class=\"text-sm text-slate-400 mb-4 flex-1\">{{ getDescription(pkg) }}</p>\n\n <!-- Allowances -->\n <div class=\"border-t border-slate-700 pt-4 mb-4\">\n <h3 class=\"text-xs font-semibold uppercase tracking-wider text-slate-500 mb-3\">\n {{ $t('packages.allowances') }}\n </h3>\n <div class=\"space-y-2\">\n <div class=\"flex items-center justify-between text-sm\">\n <span class=\"text-slate-400\">{{ $t('packages.max_vehicles') }}</span>\n <span class=\"text-slate-200 font-medium\">{{ getAllowance(pkg, 'max_vehicles') }}</span>\n </div>\n <div class=\"flex items-center justify-between text-sm\">\n <span class=\"text-slate-400\">{{ $t('packages.max_garages') }}</span>\n <span class=\"text-slate-200 font-medium\">{{ getAllowance(pkg, 'max_garages') }}</span>\n </div>\n <div class=\"flex items-center justify-between text-sm\">\n <span class=\"text-slate-400\">{{ $t('packages.monthly_credits') }}</span>\n <span class=\"text-slate-200 font-medium\">{{ getAllowance(pkg, 'monthly_free_credits') }}</span>\n </div>\n </div>\n </div>\n\n <!-- Tier Level Badge -->\n <div class=\"mb-4\">\n <span class=\"text-xs text-slate-500\">{{ $t('packages.tier_level') }}: </span>\n <span class=\"text-xs font-mono text-slate-400\">{{ pkg.tier_level }}</span>\n </div>\n\n <!-- Actions -->\n <div class=\"flex gap-2\">\n <button\n @click=\"openEditModal(pkg)\"\n class=\"flex-1 px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-medium rounded-lg transition text-sm\"\n >\n {{ $t('packages.edit') }}\n </button>\n <button\n @click=\"confirmDelete(pkg)\"\n class=\"px-3 py-2.5 bg-red-700 hover:bg-red-600 text-red-200 rounded-lg transition\"\n :title=\"$t('packages.delete')\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <!-- ── Create / Edit Modal (Tabbed Layout) ──────────────────────── -->\n <div\n v-if=\"showModal\"\n class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/60\"\n @click.self=\"showModal = false\"\n >\n <div class=\"bg-slate-800 border border-slate-700 rounded-xl w-full max-w-3xl mx-4 shadow-2xl max-h-[90vh] flex flex-col\">\n <!-- Modal Header -->\n <div class=\"flex items-center justify-between px-6 py-4 border-b border-slate-700\">\n <h3 class=\"text-lg font-semibold text-white\">\n {{ isCreating ? $t('packages.create_title') : ($t('packages.edit_title') + ': ' + getDisplayName(editingPackage!)) }}\n </h3>\n <button\n @click=\"showModal = false\"\n class=\"p-1 rounded-lg text-slate-400 hover:text-white hover:bg-slate-700 transition\"\n >\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 <!-- Tab Navigation -->\n <div class=\"flex border-b border-slate-700 px-6\">\n <button\n v-for=\"tab in tabs\"\n :key=\"tab.key\"\n @click=\"activeTab = tab.key\"\n class=\"px-4 py-3 text-sm font-medium border-b-2 transition\"\n :class=\"activeTab === tab.key\n ? 'border-indigo-500 text-indigo-300'\n : 'border-transparent text-slate-400 hover:text-slate-200 hover:border-slate-500'\"\n >\n {{ tab.label }}\n </button>\n </div>\n\n <!-- Tab Content (scrollable) -->\n <div class=\"flex-1 overflow-y-auto px-6 py-5\">\n <!-- ═══ TAB 1: Basic Info ═══ -->\n <div v-if=\"activeTab === 'basic'\" class=\"space-y-4\">\n <!-- Name -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">{{ $t('packages.form_name') }}</label>\n <input\n v-model=\"form.name\"\n type=\"text\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n :placeholder=\"$t('packages.form_name_placeholder')\"\n />\n </div>\n\n <!-- Display Name -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">{{ $t('packages.form_display_name') }}</label>\n <input\n v-model=\"form.display_name\"\n type=\"text\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n :placeholder=\"$t('packages.form_display_name_placeholder')\"\n />\n </div>\n\n <!-- Type & Tier Level -->\n <div class=\"grid grid-cols-2 gap-4\">\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">{{ $t('packages.form_type') }}</label>\n <select\n v-model=\"form.type\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n >\n <option value=\"private\">{{ $t('packages.type_private') }}</option>\n <option value=\"corporate\">{{ $t('packages.type_corporate') }}</option>\n </select>\n </div>\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">{{ $t('packages.form_tier_level') }}</label>\n <input\n v-model.number=\"form.tier_level\"\n type=\"number\"\n min=\"0\"\n max=\"10\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n />\n <p class=\"text-xs text-slate-500 mt-1\">{{ $t('packages.form_tier_level_hint') }}</p>\n </div>\n </div>\n\n <!-- Subtitle & Badge -->\n <div class=\"grid grid-cols-2 gap-4\">\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">{{ $t('packages.form_subtitle') }}</label>\n <input\n v-model=\"form.subtitle\"\n type=\"text\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n :placeholder=\"$t('packages.form_subtitle_placeholder')\"\n />\n </div>\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">{{ $t('packages.form_badge') }}</label>\n <input\n v-model=\"form.badge\"\n type=\"text\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n :placeholder=\"$t('packages.form_badge_placeholder')\"\n />\n </div>\n </div>\n\n <!-- Is Custom -->\n <div class=\"flex items-center gap-3\">\n <input\n v-model=\"form.is_custom\"\n type=\"checkbox\"\n id=\"is_custom\"\n class=\"rounded bg-slate-700 border-slate-600 text-indigo-600 focus:ring-indigo-500\"\n />\n <label for=\"is_custom\" class=\"text-sm text-slate-300\">{{ $t('packages.form_is_custom') }}</label>\n </div>\n\n <!-- Singleton Rules -->\n <div class=\"border-t border-slate-700 pt-4\">\n <h4 class=\"text-sm font-semibold text-slate-300 mb-3\">{{ $t('packages.singleton_rules') }}</h4>\n <div class=\"space-y-4\">\n <div class=\"flex items-center gap-3\">\n <input\n v-model=\"form.is_default_fallback\"\n type=\"checkbox\"\n id=\"is_default_fallback\"\n class=\"rounded bg-slate-700 border-slate-600 text-amber-500 focus:ring-amber-500\"\n />\n <label for=\"is_default_fallback\" class=\"text-sm text-slate-300\">\n {{ $t('packages.form_is_default_fallback') }}\n </label>\n </div>\n <div>\n <label class=\"block text-sm text-slate-300 mb-1\">{{ $t('packages.form_trial_days') }}</label>\n <input\n v-model.number=\"form.trial_days_on_signup\"\n type=\"number\"\n min=\"0\"\n max=\"365\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-emerald-500 focus:border-transparent\"\n />\n <p class=\"text-xs text-slate-500 mt-1\">{{ $t('packages.form_trial_days_hint') }}</p>\n </div>\n </div>\n </div>\n </div>\n\n <!-- ═══ TAB 2: Pricing (Per-Region + Smart Calculator) ═══ -->\n <div v-if=\"activeTab === 'pricing'\" class=\"space-y-6\">\n <!-- Per-Region Pricing Grid -->\n <div>\n <h4 class=\"text-sm font-semibold text-slate-300 mb-3\">{{ $t('packages.per_region_pricing') }}</h4>\n <div class=\"overflow-x-auto\">\n <table class=\"w-full text-sm\">\n <thead>\n <tr class=\"border-b border-slate-700\">\n <th class=\"text-left py-2 px-3 text-slate-400 font-medium\">Region</th>\n <th class=\"text-left py-2 px-3 text-slate-400 font-medium\">{{ $t('packages.form_monthly_price') }}</th>\n <th class=\"text-left py-2 px-3 text-slate-400 font-medium\">{{ $t('packages.form_yearly_price') }}</th>\n <th class=\"text-left py-2 px-3 text-slate-400 font-medium\">{{ $t('packages.form_currency') }}</th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"region in regionStore.regions\" :key=\"region.country_code\" class=\"border-b border-slate-700/50\">\n <td class=\"py-2 px-3 text-slate-200 font-medium\">\n <span class=\"inline-flex items-center gap-1.5\">\n <span>{{ region.country_code }}</span>\n <span class=\"text-xs text-slate-500\">({{ region.currency }}, {{ region.default_vat_rate }}% VAT)</span>\n </span>\n </td>\n <td class=\"py-2 px-3\">\n <input\n :value=\"getZonePrice(region.country_code, 'monthly')\"\n @input=\"setZonePrice(region.country_code, 'monthly', ($event.target as HTMLInputElement).value)\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n class=\"w-full px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n />\n </td>\n <td class=\"py-2 px-3\">\n <input\n :value=\"getZonePrice(region.country_code, 'yearly')\"\n @input=\"setZonePrice(region.country_code, 'yearly', ($event.target as HTMLInputElement).value)\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n class=\"w-full px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n />\n </td>\n <td class=\"py-2 px-3\">\n <input\n :value=\"getZoneCurrency(region.country_code)\"\n @input=\"setZoneCurrency(region.country_code, ($event.target as HTMLInputElement).value)\"\n type=\"text\"\n maxlength=\"3\"\n class=\"w-full px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent uppercase\"\n />\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n\n <!-- Smart Pricing Calculator -->\n <div class=\"border-t border-slate-700 pt-5\">\n <h4 class=\"text-sm font-semibold text-slate-300 mb-3\">🧮 {{ $t('packages.smart_calculator') }}</h4>\n <div class=\"bg-slate-700/30 rounded-xl p-4 space-y-4\">\n <!-- Calculator Inputs -->\n <div class=\"grid grid-cols-3 gap-4\">\n <div>\n <label class=\"block text-xs text-slate-400 mb-1\">{{ $t('packages.calc_net_price') }}</label>\n <input\n v-model.number=\"calcNetPrice\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-emerald-500 focus:border-transparent\"\n placeholder=\"0.00\"\n />\n </div>\n <div>\n <label class=\"block text-xs text-slate-400 mb-1\">{{ $t('packages.calc_vat_rate') }}</label>\n <select\n v-model=\"calcVatRate\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-emerald-500 focus:border-transparent\"\n >\n <option v-for=\"region in regionStore.regions\" :key=\"region.country_code\" :value=\"region.default_vat_rate\">\n {{ region.country_code }} — {{ region.default_vat_rate }}%\n </option>\n </select>\n </div>\n <div>\n <label class=\"block text-xs text-slate-400 mb-1\">{{ $t('packages.calc_currency') }}</label>\n <select\n v-model=\"calcCurrency\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-emerald-500 focus:border-transparent\"\n >\n <option v-for=\"region in regionStore.regions\" :key=\"region.country_code\" :value=\"region.currency\">\n {{ region.currency }} ({{ region.country_code }})\n </option>\n </select>\n </div>\n </div>\n\n <!-- Calculator Results -->\n <div class=\"grid grid-cols-3 gap-4 pt-2\">\n <div class=\"bg-slate-800 rounded-lg p-3 text-center\">\n <p class=\"text-xs text-slate-400 mb-1\">{{ $t('packages.calc_net') }}</p>\n <p class=\"text-lg font-bold text-white\">{{ formatCurrency(calcNetPrice, calcCurrency) }}</p>\n </div>\n <div class=\"bg-slate-800 rounded-lg p-3 text-center\">\n <p class=\"text-xs text-slate-400 mb-1\">{{ $t('packages.calc_vat') }} ({{ calcVatRate }}%)</p>\n <p class=\"text-lg font-bold text-amber-400\">{{ formatCurrency(calcNetPrice * (calcVatRate / 100), calcCurrency) }}</p>\n </div>\n <div class=\"bg-slate-800 rounded-lg p-3 text-center ring-2 ring-emerald-500/50\">\n <p class=\"text-xs text-slate-400 mb-1\">{{ $t('packages.calc_gross') }}</p>\n <p class=\"text-lg font-bold text-emerald-400\">{{ formatCurrency(calcNetPrice * (1 + calcVatRate / 100), calcCurrency) }}</p>\n </div>\n </div>\n\n <!-- Quick Apply Button -->\n <div class=\"flex justify-end pt-1\">\n <button\n @click=\"applyCalculatorToActiveRegion\"\n class=\"px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white text-sm font-medium rounded-lg transition flex items-center gap-2\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 10V3L4 14h7v7l9-11h-7z\" />\n </svg>\n {{ $t('packages.calc_apply') }}\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <!-- ═══ TAB 3: Features ═══ -->\n <div v-if=\"activeTab === 'features'\" class=\"space-y-5\">\n <!-- Allowances -->\n <div>\n <h4 class=\"text-sm font-semibold text-slate-300 mb-3\">{{ $t('packages.allowances') }}</h4>\n <div class=\"grid grid-cols-3 gap-4\">\n <div>\n <label class=\"block text-xs text-slate-400 mb-1\">{{ $t('packages.max_vehicles') }}</label>\n <input\n v-model.number=\"form.max_vehicles\"\n type=\"number\"\n min=\"0\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n />\n </div>\n <div>\n <label class=\"block text-xs text-slate-400 mb-1\">{{ $t('packages.max_garages') }}</label>\n <input\n v-model.number=\"form.max_garages\"\n type=\"number\"\n min=\"0\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n />\n </div>\n <div>\n <label class=\"block text-xs text-slate-400 mb-1\">{{ $t('packages.monthly_credits') }}</label>\n <input\n v-model.number=\"form.monthly_free_credits\"\n type=\"number\"\n min=\"0\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n />\n </div>\n </div>\n </div>\n\n <!-- Feature Capabilities (JSON editor) -->\n <div class=\"border-t border-slate-700 pt-4\">\n <h4 class=\"text-sm font-semibold text-slate-300 mb-3\">{{ $t('packages.feature_capabilities') }}</h4>\n <textarea\n v-model=\"form.feature_capabilities_json\"\n rows=\"8\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm font-mono focus:ring-2 focus:ring-indigo-500 focus:border-transparent\"\n placeholder='{\\n \"ai_analysis\": true,\\n \"priority_support\": false,\\n \"api_access\": true\\n}'\n ></textarea>\n <p class=\"text-xs text-slate-500 mt-1\">{{ $t('packages.feature_capabilities_hint') }}</p>\n <p v-if=\"featureCapabilitiesError\" class=\"text-xs text-red-400 mt-1\">{{ featureCapabilitiesError }}</p>\n </div>\n </div>\n </div>\n\n <!-- Modal Footer -->\n <div class=\"flex items-center justify-between px-6 py-4 border-t border-slate-700\">\n <div class=\"text-xs text-slate-500\">\n {{ activeTabLabel }}\n </div>\n <div class=\"flex items-center gap-3\">\n <button\n @click=\"showModal = false\"\n class=\"px-4 py-2 text-sm text-slate-400 hover:text-white transition\"\n >\n {{ $t('packages.cancel') }}\n </button>\n <button\n @click=\"savePackage\"\n :disabled=\"saving\"\n class=\"px-6 py-2 bg-emerald-600 hover:bg-emerald-500 disabled:bg-slate-600 disabled:cursor-not-allowed text-white font-medium rounded-lg transition text-sm flex items-center gap-2\"\n >\n <svg v-if=\"saving\" class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path>\n </svg>\n {{ isCreating ? $t('packages.create') : $t('packages.save') }}\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Delete Confirmation Modal -->\n <div\n v-if=\"showDeleteConfirm && deletingPackage\"\n class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/60\"\n @click.self=\"showDeleteConfirm = false\"\n >\n <div class=\"bg-slate-800 border border-slate-700 rounded-xl w-full max-w-md mx-4 p-6 shadow-2xl\">\n <h3 class=\"text-lg font-semibold text-white mb-2\">{{ $t('packages.delete_title') }}</h3>\n <p class=\"text-slate-400 text-sm mb-6\">\n {{ $t('packages.delete_confirm', { name: getDisplayName(deletingPackage) }) }}\n </p>\n <div class=\"flex items-center justify-end gap-3\">\n <button\n @click=\"showDeleteConfirm = false\"\n class=\"px-4 py-2 text-sm text-slate-400 hover:text-white transition\"\n >\n {{ $t('packages.cancel') }}\n </button>\n <button\n @click=\"deletePackage\"\n :disabled=\"saving\"\n class=\"px-6 py-2 bg-red-600 hover:bg-red-500 disabled:bg-slate-600 disabled:cursor-not-allowed text-white font-medium rounded-lg transition text-sm\"\n >\n {{ $t('packages.delete_btn') }}\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, reactive, computed, onMounted } from 'vue'\nimport { useRegionStore } from '~/stores/region'\nimport { useFormatter } from '~/composables/useFormatter'\n\ndefinePageMeta({\n middleware: 'auth',\n})\n\n// ── Region Store & Formatter ──────────────────────────────────────\nconst regionStore = useRegionStore()\nconst { formatCurrency } = useFormatter()\n\n// ── Types ──────────────────────────────────────────────────────────\n\ninterface SubscriptionTier {\n id: number\n name: string\n rules: {\n type: string\n display_name?: string | null\n pricing?: {\n monthly_price: number\n yearly_price: number\n currency: string\n credit_price?: number | null\n } | null\n pricing_zones?: Record<string, {\n monthly_price: number\n yearly_price: number\n currency: string\n credit_price?: number | null\n }> | null\n allowances?: {\n max_vehicles: number\n max_garages: number\n monthly_free_credits: number\n } | null\n marketing?: {\n subtitle?: string | null\n badge?: string | null\n highlight_color?: string | null\n } | null\n lifecycle?: {\n is_public?: boolean\n available_until?: string | null\n } | null\n }\n is_custom: boolean\n tier_level: number\n feature_capabilities: Record<string, any>\n is_default_fallback: boolean\n trial_days_on_signup: number\n}\n\ninterface PackageForm {\n name: string\n display_name: string\n type: string\n tier_level: number\n monthly_price: number\n yearly_price: number\n currency: string\n max_vehicles: number\n max_garages: number\n monthly_free_credits: number\n subtitle: string\n badge: string\n is_custom: boolean\n is_default_fallback: boolean\n trial_days_on_signup: number\n feature_capabilities_json: string\n pricing_zones: Record<string, { monthly_price: number; yearly_price: number; currency: string }>\n}\n\n// ── Tab Configuration ──────────────────────────────────────────────\n\nconst tabs = [\n { key: 'basic', label: '📋 Basic Info' },\n { key: 'pricing', label: '💰 Pricing' },\n { key: 'features', label: '⚡ Features' },\n]\n\nconst activeTab = ref('basic')\n\nconst activeTabLabel = computed(() => {\n const tab = tabs.find(t => t.key === activeTab.value)\n return tab ? tab.label : ''\n})\n\n// ── State ──────────────────────────────────────────────────────────\n\nconst packages = ref<SubscriptionTier[]>([])\nconst loading = ref(true)\nconst error = ref<string | null>(null)\nconst showModal = ref(false)\nconst showDeleteConfirm = ref(false)\nconst editingPackage = ref<SubscriptionTier | null>(null)\nconst deletingPackage = ref<SubscriptionTier | null>(null)\nconst isCreating = ref(false)\nconst saving = ref(false)\nconst saveNotification = ref<{ type: 'success' | 'error'; message: string } | null>(null)\nconst featureCapabilitiesError = ref<string | null>(null)\n\n// ── Smart Pricing Calculator State ─────────────────────────────────\n\nconst calcNetPrice = ref(0)\nconst calcVatRate = ref(27)\nconst calcCurrency = ref('EUR')\n\n// ── Default Form ───────────────────────────────────────────────────\n\nconst defaultForm: PackageForm = {\n name: '',\n display_name: '',\n type: 'private',\n tier_level: 0,\n monthly_price: 0,\n yearly_price: 0,\n currency: 'EUR',\n max_vehicles: 1,\n max_garages: 1,\n monthly_free_credits: 0,\n subtitle: '',\n badge: '',\n is_custom: false,\n is_default_fallback: false,\n trial_days_on_signup: 0,\n feature_capabilities_json: '{\\n \"ai_analysis\": false,\\n \"priority_support\": false,\\n \"api_access\": false\\n}',\n pricing_zones: {}\n}\n\nconst form = reactive<PackageForm>({ ...defaultForm })\n\n// ── Pricing Zone Helpers ───────────────────────────────────────────\n\nfunction getZonePrice(countryCode: string, period: 'monthly' | 'yearly'): number {\n const zone = form.pricing_zones[countryCode]\n if (!zone) return 0\n return period === 'monthly' ? zone.monthly_price : zone.yearly_price\n}\n\nfunction setZonePrice(countryCode: string, period: 'monthly' | 'yearly', value: string) {\n const num = parseFloat(value) || 0\n if (!form.pricing_zones[countryCode]) {\n form.pricing_zones[countryCode] = { monthly_price: 0, yearly_price: 0, currency: 'EUR' }\n }\n if (period === 'monthly') {\n form.pricing_zones[countryCode].monthly_price = num\n } else {\n form.pricing_zones[countryCode].yearly_price = num\n }\n}\n\nfunction getZoneCurrency(countryCode: string): string {\n const zone = form.pricing_zones[countryCode]\n if (!zone) return 'EUR'\n return zone.currency\n}\n\nfunction setZoneCurrency(countryCode: string, value: string) {\n if (!form.pricing_zones[countryCode]) {\n form.pricing_zones[countryCode] = { monthly_price: 0, yearly_price: 0, currency: 'EUR' }\n }\n form.pricing_zones[countryCode].currency = value.toUpperCase()\n}\n\n/**\n * Apply the Smart Pricing Calculator result to the currently selected region's monthly price.\n */\nfunction applyCalculatorToActiveRegion() {\n const targetRegion = regionStore.regions.find(r => r.default_vat_rate === calcVatRate)\n if (!targetRegion) {\n showNotification('error', 'No region found with the selected VAT rate')\n return\n }\n const code = targetRegion.country_code\n if (!form.pricing_zones[code]) {\n form.pricing_zones[code] = { monthly_price: 0, yearly_price: 0, currency: targetRegion.currency }\n }\n form.pricing_zones[code].monthly_price = calcNetPrice\n form.pricing_zones[code].currency = calcCurrency\n showNotification('success', `Applied net price ${formatCurrency(calcNetPrice, calcCurrency)} to ${code} region`)\n}\n\n// ── API Helpers ────────────────────────────────────────────────────\n\nfunction getAuthHeaders() {\n const tokenCookie = useCookie('access_token')\n const token = tokenCookie.value\n return token ? { Authorization: `Bearer ${token}` } : {}\n}\n\nasync function fetchPackages() {\n loading.value = true\n error.value = null\n try {\n const res = await $fetch<{ total: number; tiers: SubscriptionTier[] }>('/api/v1/admin/packages', {\n headers: getAuthHeaders(),\n })\n packages.value = res.tiers || []\n } catch (err: any) {\n error.value = err?.data?.detail || err?.message || 'Failed to load packages'\n console.error('[Packages] Fetch error:', err)\n } finally {\n loading.value = false\n }\n}\n\n// ── Display Helpers ────────────────────────────────────────────────\n\nfunction getDisplayName(pkg: SubscriptionTier): string {\n return pkg.rules?.display_name || pkg.name\n}\n\nfunction getDescription(pkg: SubscriptionTier): string {\n return pkg.rules?.marketing?.subtitle || pkg.rules?.marketing?.badge || ''\n}\n\nfunction getFormattedPrice(pkg: SubscriptionTier): string {\n const zones = pkg.rules?.pricing_zones\n const pricing = pkg.rules?.pricing\n if (zones && zones['DEFAULT']) {\n const p = zones['DEFAULT']\n const currency = p.currency === 'HUF' ? 'Ft' : '€'\n return `${p.monthly_price} ${currency}`\n }\n if (pricing) {\n const currency = pricing.currency === 'HUF' ? 'Ft' : '€'\n return `${pricing.monthly_price} ${currency}`\n }\n return '—'\n}\n\nfunction getAllowance(pkg: SubscriptionTier, key: string): number | string {\n const allowances = pkg.rules?.allowances\n if (!allowances) return '—'\n return (allowances as any)[key] ?? '—'\n}\n\nfunction getTierColorClass(level: number): string {\n if (level >= 2) return 'bg-purple-500/10 text-purple-400'\n if (level >= 1) return 'bg-amber-500/10 text-amber-400'\n return 'bg-slate-500/10 text-slate-400'\n}\n\nfunction getTierIcon(level: number): string {\n if (level >= 2) return '🏢'\n if (level >= 1) return '⭐'\n return '🔓'\n}\n\n// ── Modal Actions ──────────────────────────────────────────────────\n\nfunction openCreateModal() {\n isCreating.value = true\n editingPackage.value = null\n activeTab.value = 'basic'\n Object.assign(form, defaultForm)\n showModal.value = true\n}\n\nfunction openEditModal(pkg: SubscriptionTier) {\n isCreating.value = false\n editingPackage.value = pkg\n activeTab.value = 'basic'\n\n const rules = pkg.rules || {} as any\n const pricing = rules.pricing_zones?.DEFAULT || rules.pricing || {}\n const allowances = rules.allowances || {}\n\n form.name = pkg.name\n form.display_name = rules.display_name || ''\n form.type = rules.type || 'private'\n form.tier_level = pkg.tier_level\n form.monthly_price = pricing.monthly_price || 0\n form.yearly_price = pricing.yearly_price || 0\n form.currency = pricing.currency || 'EUR'\n form.max_vehicles = allowances.max_vehicles || 1\n form.max_garages = allowances.max_garages || 1\n form.monthly_free_credits = allowances.monthly_free_credits || 0\n form.subtitle = rules.marketing?.subtitle || ''\n form.badge = rules.marketing?.badge || ''\n form.is_custom = pkg.is_custom\n form.is_default_fallback = pkg.is_default_fallback\n form.trial_days_on_signup = pkg.trial_days_on_signup\n\n // Load pricing zones\n form.pricing_zones = {}\n if (rules.pricing_zones) {\n for (const [code, zone] of Object.entries(rules.pricing_zones)) {\n form.pricing_zones[code] = {\n monthly_price: zone.monthly_price || 0,\n yearly_price: zone.yearly_price || 0,\n currency: zone.currency || 'EUR',\n }\n }\n }\n\n // Load feature capabilities as JSON string\n form.feature_capabilities_json = JSON.stringify(pkg.feature_capabilities || {}, null, 2)\n\n showModal.value = true\n}\n\nfunction confirmDelete(pkg: SubscriptionTier) {\n deletingPackage.value = pkg\n showDeleteConfirm.value = true\n}\n\n// ── Save / Create ──────────────────────────────────────────────────\n\nasync function savePackage() {\n if (!form.name.trim()) {\n showNotification('error', 'Package name is required')\n return\n }\n\n // Validate feature_capabilities JSON\n let featureCapabilities: Record<string, any> = {}\n try {\n featureCapabilities = JSON.parse(form.feature_capabilities_json)\n featureCapabilitiesError.value = null\n } catch {\n featureCapabilitiesError.value = 'Invalid JSON in feature capabilities'\n activeTab.value = 'features'\n return\n }\n\n saving.value = true\n try {\n // Build pricing_zones from form, ensuring DEFAULT is always present\n const pricingZones: Record<string, any> = { ...form.pricing_zones }\n if (!pricingZones['DEFAULT']) {\n pricingZones['DEFAULT'] = {\n monthly_price: form.monthly_price,\n yearly_price: form.yearly_price,\n currency: form.currency.toUpperCase(),\n }\n }\n\n const rulesPayload: any = {\n type: form.type,\n display_name: form.display_name || null,\n pricing_zones: pricingZones,\n allowances: {\n max_vehicles: form.max_vehicles,\n max_garages: form.max_garages,\n monthly_free_credits: form.monthly_free_credits,\n },\n marketing: {\n subtitle: form.subtitle || null,\n badge: form.badge || null,\n },\n lifecycle: {\n is_public: true,\n },\n }\n\n if (isCreating.value) {\n // POST - Create new package\n await $fetch('/api/v1/admin/packages', {\n method: 'POST',\n headers: {\n ...getAuthHeaders(),\n 'Content-Type': 'application/json',\n },\n body: {\n name: form.name.trim().toLowerCase(),\n rules: rulesPayload,\n is_custom: form.is_custom,\n tier_level: form.tier_level,\n feature_capabilities: featureCapabilities,\n is_default_fallback: form.is_default_fallback,\n trial_days_on_signup: form.trial_days_on_signup,\n },\n })\n showNotification('success', `Package \"${form.name}\" created successfully`)\n } else if (editingPackage.value) {\n // PATCH - Update existing package\n await $fetch(`/api/v1/admin/packages/${editingPackage.value.id}`, {\n method: 'PATCH',\n headers: {\n ...getAuthHeaders(),\n 'Content-Type': 'application/json',\n },\n body: {\n name: form.name.trim().toLowerCase(),\n rules: rulesPayload,\n is_custom: form.is_custom,\n tier_level: form.tier_level,\n feature_capabilities: featureCapabilities,\n is_default_fallback: form.is_default_fallback,\n trial_days_on_signup: form.trial_days_on_signup,\n },\n })\n showNotification('success', `Package \"${form.name}\" updated successfully`)\n }\n\n showModal.value = false\n await fetchPackages()\n } catch (err: any) {\n const detail = err?.data?.detail || err?.message || 'Operation failed'\n showNotification('error', detail)\n console.error('[Packages] Save error:', err)\n } finally {\n saving.value = false\n }\n}\n\nasync function deletePackage() {\n if (!deletingPackage.value) return\n\n saving.value = true\n try {\n await $fetch(`/api/v1/admin/packages/${deletingPackage.value.id}`, {\n method: 'DELETE',\n headers: getAuthHeaders(),\n })\n showNotification('success', `Package \"${getDisplayName(deletingPackage.value)}\" deactivated`)\n showDeleteConfirm.value = false\n deletingPackage.value = null\n await fetchPackages()\n } catch (err: any) {\n const detail = err?.data?.detail || err?.message || 'Delete failed'\n showNotification('error', detail)\n console.error('[Packages] Delete error:', err)\n } finally {\n saving.value = false\n }\n}\n\n// ── Notification ───────────────────────────────────────────────────\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 fetchPackages()\n regionStore.fetchRegions()\n})\n</script>"],"version":3}