1 line
26 KiB
JSON
1 line
26 KiB
JSON
{"file":"index-7zBAzpaD.js","mappings":"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","names":["$t","_ssrRenderClass","_ssrInterpolate","_ssrRenderList"],"sources":["../../../../pages/permissions/index.vue"],"sourcesContent":["<template>\n <div>\n <!-- Page Header -->\n <div class=\"mb-8\">\n <h1 class=\"text-2xl font-bold text-white\">{{ $t('permissions.title') }}</h1>\n <p class=\"text-slate-400 mt-1\">{{ $t('permissions.subtitle') }}</p>\n </div>\n\n <!-- Loading State -->\n <div v-if=\"loading\" class=\"flex items-center justify-center py-20\">\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\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n <span class=\"ml-3 text-slate-400\">{{ $t('permissions.loading') }}</span>\n </div>\n\n <!-- Error State -->\n <div v-else-if=\"error\" class=\"bg-red-500/10 border border-red-500/20 rounded-xl p-6 mb-8\">\n <div class=\"flex items-center gap-3\">\n <svg class=\"w-6 h-6 text-red-400 flex-shrink-0\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n <div>\n <h3 class=\"text-red-300 font-medium\">{{ $t('permissions.error_title') }}</h3>\n <p class=\"text-red-400/80 text-sm mt-1\">{{ error }}</p>\n </div>\n </div>\n <button @click=\"fetchAll\" class=\"mt-4 px-4 py-2 text-sm bg-red-600 hover:bg-red-500 text-white rounded-lg transition\">\n {{ $t('permissions.retry') }}\n </button>\n </div>\n\n <!-- Main Content -->\n <template v-else>\n <!-- Save Notification -->\n <div v-if=\"saveNotification\" class=\"mb-6 px-4 py-3 rounded-lg text-sm font-medium\"\n :class=\"saveNotification.type === 'success'\n ? 'bg-emerald-500/10 border border-emerald-500/20 text-emerald-300'\n : 'bg-red-500/10 border border-red-500/20 text-red-300'\">\n {{ saveNotification.message }}\n </div>\n\n <!-- Save Button (top) -->\n <div v-if=\"hasModifications\" class=\"mb-6 flex items-center gap-3\">\n <button @click=\"savePermissions\"\n class=\"px-6 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white font-semibold rounded-lg shadow-lg shadow-emerald-600/20 transition-all hover:scale-[1.02] active:scale-[0.98] flex items-center gap-2\">\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=\"M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4\" />\n </svg>\n {{ $t('permissions.save_changes') }}\n </button>\n <span class=\"text-sm text-slate-400\">\n {{ $t('permissions.modified_count', { count: modifiedPermissions.size }) }}\n </span>\n </div>\n\n <!-- Subscription Tier Banner -->\n <div class=\"bg-indigo-500/10 border border-indigo-500/20 rounded-xl p-4 mb-8 flex items-start gap-3\">\n <svg class=\"w-5 h-5 text-indigo-400 flex-shrink-0 mt-0.5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n <div class=\"text-sm text-indigo-300\">\n <span class=\"font-semibold\">{{ $t('permissions.current_tier') }}:</span>\n <span class=\"ml-2 px-2 py-0.5 rounded-full text-xs font-medium\"\n :class=\"tierBadgeClass\">\n {{ featureFlags.tier || 'free' }}\n </span>\n <span v-if=\"featureFlags.expires_at\" class=\"ml-3 text-indigo-400/70\">\n {{ $t('permissions.expires') }}: {{ formatDate(featureFlags.expires_at) }}\n </span>\n </div>\n </div>\n\n <!-- Permission Groups Grid -->\n <div class=\"grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8\">\n <!-- Superadmin Permissions -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-6\">\n <div class=\"flex items-center gap-3 mb-4\">\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=\"M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z\" />\n </svg>\n </div>\n <h2 class=\"text-lg font-semibold text-white\">{{ $t('permissions.superadmin') }}</h2>\n </div>\n <p class=\"text-sm text-slate-400 mb-4\">{{ $t('permissions.superadmin_desc') }}</p>\n <div class=\"space-y-3\">\n <div v-for=\"perm in superadminPerms\" :key=\"perm.key\" class=\"flex items-center justify-between py-2 border-b border-slate-700/50 last:border-0\">\n <span class=\"text-sm text-slate-300\">{{ $t(`permissions.perms.${perm.key}`) }}</span>\n <label class=\"relative inline-flex items-center cursor-pointer\">\n <input type=\"checkbox\" :checked=\"isPermEnabled('superadmin', perm.key)\" class=\"sr-only peer\"\n @change=\"handleToggle('superadmin', perm.key, $event)\" />\n <div class=\"w-9 h-5 bg-slate-600 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-purple-500/50 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-purple-600\"></div>\n </label>\n </div>\n </div>\n </div>\n\n <!-- Garage Permissions -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-6\">\n <div class=\"flex items-center gap-3 mb-4\">\n <div class=\"p-2 rounded-lg bg-cyan-500/10 text-cyan-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=\"M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 00-3.213-9.193 2.056 2.056 0 00-1.58-.86H14.25M16.5 18.75h-2.25m0-11.177v-.958c0-.568-.422-1.048-.987-1.106a48.554 48.554 0 00-10.026 0 1.106 1.106 0 00-.987 1.106v7.635m12-6.677v6.677m0 4.5v-4.5m0 0h-12\" />\n </svg>\n </div>\n <h2 class=\"text-lg font-semibold text-white\">{{ $t('permissions.garages') }}</h2>\n </div>\n <p class=\"text-sm text-slate-400 mb-4\">{{ $t('permissions.garages_desc') }}</p>\n <div class=\"space-y-3\">\n <div v-for=\"perm in garagePerms\" :key=\"perm.key\" class=\"flex items-center justify-between py-2 border-b border-slate-700/50 last:border-0\">\n <span class=\"text-sm text-slate-300\">{{ $t(`permissions.perms.${perm.key}`) }}</span>\n <label class=\"relative inline-flex items-center cursor-pointer\">\n <input type=\"checkbox\" :checked=\"isPermEnabled('garage', perm.key)\" class=\"sr-only peer\"\n @change=\"handleToggle('garage', perm.key, $event)\" />\n <div class=\"w-9 h-5 bg-slate-600 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-cyan-500/50 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyan-600\"></div>\n </label>\n </div>\n </div>\n </div>\n\n <!-- Package / Feature Flags -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-6\">\n <div class=\"flex items-center gap-3 mb-4\">\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=\"M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5m8.25 3v6.75m0 0l-3-3m3 3l3-3M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z\" />\n </svg>\n </div>\n <h2 class=\"text-lg font-semibold text-white\">{{ $t('permissions.feature_flags') }}</h2>\n </div>\n <p class=\"text-sm text-slate-400 mb-4\">{{ $t('permissions.feature_flags_desc', { tier: featureFlags.tier || 'free' }) }}</p>\n <div class=\"space-y-3\">\n <div v-for=\"[key, granted] in featureEntries\" :key=\"key\" class=\"flex items-center justify-between py-2 border-b border-slate-700/50 last:border-0\">\n <div class=\"flex flex-col\">\n <span class=\"text-sm text-slate-300\">{{ formatFeatureLabel(key) }}</span>\n <span class=\"text-xs text-slate-500\">{{ key }}</span>\n </div>\n <label class=\"relative inline-flex items-center cursor-pointer\">\n <input type=\"checkbox\" :checked=\"granted\" class=\"sr-only peer\"\n :disabled=\"true\"\n @change=\"handleToggle('feature', key, $event)\" />\n <div class=\"w-9 h-5 bg-slate-600 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-amber-500/50 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all\"\n :class=\"granted ? 'peer-checked:bg-amber-600' : ''\"></div>\n </label>\n </div>\n </div>\n </div>\n </div>\n\n <!-- RBAC Permission Matrix -->\n <div class=\"bg-slate-800/50 border border-slate-700 rounded-xl p-6 mb-8\">\n <h3 class=\"text-lg font-semibold text-white mb-4\">{{ $t('permissions.rbac_title') }}</h3>\n <p class=\"text-sm text-slate-400 mb-6\">{{ $t('permissions.rbac_desc') }}</p>\n\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-3 px-4 text-slate-400 font-medium\">{{ $t('permissions.action') }}</th>\n <th v-for=\"role in permissionMatrix\" :key=\"role.role\" class=\"text-center py-3 px-4 text-slate-400 font-medium\">\n {{ role.role }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"action in allActions\" :key=\"action\" class=\"border-b border-slate-700/50 hover:bg-slate-700/30\">\n <td class=\"py-2.5 px-4 text-slate-300\">{{ action }}</td>\n <td v-for=\"role in permissionMatrix\" :key=\"role.role\" class=\"text-center py-2.5 px-4\">\n <span v-if=\"hasPermission(role, action)\" class=\"inline-flex items-center justify-center w-6 h-6 rounded-full bg-emerald-400/10\">\n <svg class=\"w-4 h-4 text-emerald-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\" />\n </svg>\n </span>\n <span v-else class=\"inline-flex items-center justify-center w-6 h-6 rounded-full bg-slate-700\">\n <svg class=\"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=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </span>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n\n <!-- Save Button (bottom) -->\n <div v-if=\"hasModifications\" class=\"flex items-center gap-3 pb-8\">\n <button @click=\"savePermissions\"\n class=\"px-6 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white font-semibold rounded-lg shadow-lg shadow-emerald-600/20 transition-all hover:scale-[1.02] active:scale-[0.98] flex items-center gap-2\">\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=\"M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4\" />\n </svg>\n {{ $t('permissions.save_changes') }}\n </button>\n <button @click=\"discardChanges\"\n class=\"px-4 py-2.5 bg-slate-700 hover:bg-slate-600 text-slate-300 font-medium rounded-lg transition\">\n {{ $t('permissions.discard') }}\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, onMounted } from 'vue'\n\ndefinePageMeta({\n middleware: 'auth',\n})\n\n// ── State ──────────────────────────────────────────────────────────\n\nconst loading = ref(true)\nconst error = ref<string | null>(null)\n\ninterface FeatureFlagsResponse {\n tier: string\n features: Record<string, boolean>\n expires_at: string | null\n}\n\ninterface PermissionMatrixEntry {\n role: string\n actions: string[]\n}\n\ninterface PermissionMatrixResponse {\n matrix: PermissionMatrixEntry[]\n all_actions: string[]\n}\n\nconst featureFlags = ref<FeatureFlagsResponse>({\n tier: 'free',\n features: {},\n expires_at: null,\n})\n\nconst permissionMatrix = ref<PermissionMatrixEntry[]>([])\nconst allActions = ref<string[]>([])\n\n// ── Editing State ───────────────────────────────────────────────────\n\n// Track modified permissions: key = \"group:permKey\", value = boolean\nconst modifiedPermissions = ref<Map<string, boolean>>(new Map())\n\n// Track original values for discard: key = \"group:permKey\", value = boolean\nconst originalPermissions = ref<Map<string, boolean>>(new Map())\n\n// Save notification\nconst saveNotification = ref<{ type: 'success' | 'error'; message: string } | null>(null)\n\n// ── Computed ───────────────────────────────────────────────────────\n\nconst featureEntries = computed(() => {\n return Object.entries(featureFlags.value.features || {})\n})\n\nconst hasModifications = computed(() => {\n return modifiedPermissions.value.size > 0\n})\n\nconst tierBadgeClass = computed(() => {\n const tier = featureFlags.value.tier\n if (tier === 'enterprise') return 'bg-purple-500/20 text-purple-300'\n if (tier === 'premium') return 'bg-amber-500/20 text-amber-300'\n return 'bg-slate-500/20 text-slate-300'\n})\n\n// Static permission definitions for the UI cards\nconst superadminPerms = [\n { key: 'user_mgmt', label: 'User Management', enabled: true },\n { key: 'billing_admin', label: 'Billing Administration', enabled: true },\n { key: 'robot_config', label: 'Robot Configuration', enabled: true },\n { key: 'system_logs', label: 'System Logs Access', enabled: true },\n { key: 'api_keys', label: 'API Key Management', enabled: false },\n]\n\nconst garagePerms = [\n { key: 'vehicle_crud', label: 'Vehicle CRUD', enabled: true },\n { key: 'service_booking', label: 'Service Booking', enabled: true },\n { key: 'customer_view', label: 'Customer Data View', enabled: true },\n { key: 'report_export', label: 'Report Export', enabled: false },\n { key: 'bulk_import', label: 'Bulk Import', enabled: false },\n]\n\n// ── Methods ────────────────────────────────────────────────────────\n\n/**\n * Check if a permission is currently enabled (considering modifications).\n */\nfunction isPermEnabled(group: string, key: string): boolean {\n const mapKey = `${group}:${key}`\n // If modified, return the modified value\n if (modifiedPermissions.value.has(mapKey)) {\n return modifiedPermissions.value.get(mapKey)!\n }\n // Otherwise return the default\n const perms = group === 'superadmin' ? superadminPerms : garagePerms\n const perm = perms.find(p => p.key === key)\n return perm ? perm.enabled : false\n}\n\nfunction formatFeatureLabel(key: string): string {\n return key\n .replace(/^cost_category:/, '')\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (c) => c.toUpperCase())\n}\n\nfunction formatDate(dateStr: string): string {\n try {\n const d = new Date(dateStr)\n return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })\n } catch {\n return dateStr\n }\n}\n\nfunction hasPermission(role: PermissionMatrixEntry, action: string): boolean {\n return role.actions.includes(action)\n}\n\nfunction handleToggle(group: string, key: string, event: Event) {\n const target = event.target as HTMLInputElement\n const newValue = target.checked\n const mapKey = `${group}:${key}`\n\n // Store original value if not already tracked\n if (!originalPermissions.value.has(mapKey)) {\n const perms = group === 'superadmin' ? superadminPerms : garagePerms\n const perm = perms.find(p => p.key === key)\n originalPermissions.value.set(mapKey, perm ? perm.enabled : false)\n }\n\n // Update modified state\n const originalValue = originalPermissions.value.get(mapKey)\n if (newValue === originalValue) {\n // Reverted to original — remove from modified\n modifiedPermissions.value.delete(mapKey)\n } else {\n modifiedPermissions.value.set(mapKey, newValue)\n }\n\n // Force reactivity by replacing the Map\n modifiedPermissions.value = new Map(modifiedPermissions.value)\n\n console.log(`[Permissions] Toggle: group=${group}, key=${key}, newValue=${newValue}`)\n}\n\nfunction clearSaveNotification() {\n setTimeout(() => {\n saveNotification.value = null\n }, 5000)\n}\n\nasync function savePermissions() {\n saveNotification.value = null\n\n try {\n const tokenCookie = useCookie('access_token')\n const token = tokenCookie.value\n if (!token) {\n saveNotification.value = { type: 'error', message: 'Not authenticated' }\n clearSaveNotification()\n return\n }\n\n // Build payload: array of { action, granted }\n const payload: { action: string; granted: boolean }[] = []\n modifiedPermissions.value.forEach((granted, mapKey) => {\n // mapKey format: \"superadmin:user_mgmt\" or \"garage:vehicle_crud\"\n const action = mapKey.replace(/^(superadmin|garage):/, '').toUpperCase()\n payload.push({ action, granted })\n })\n\n if (payload.length === 0) {\n saveNotification.value = { type: 'error', message: 'No changes to save' }\n clearSaveNotification()\n return\n }\n\n // Send PATCH to the backend for each modified permission\n // We use the admin permissions override endpoint\n const results = await Promise.allSettled(\n payload.map(item =>\n $fetch('/api/v1/admin/permissions/override/0', {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: {\n action: item.action,\n granted: item.granted,\n },\n })\n )\n )\n\n // Check for failures\n const failures = results.filter(r => r.status === 'rejected')\n if (failures.length > 0) {\n const errors = failures.map(f => (f as PromiseRejectedResult).reason?.message || 'Unknown error')\n saveNotification.value = {\n type: 'error',\n message: `Failed to save ${failures.length} permission(s): ${errors.join('; ')}`,\n }\n clearSaveNotification()\n return\n }\n\n // Success — clear modified state\n modifiedPermissions.value = new Map()\n originalPermissions.value = new Map()\n saveNotification.value = {\n type: 'success',\n message: `Successfully saved ${payload.length} permission change(s).`,\n }\n clearSaveNotification()\n\n console.log('[Permissions] Save successful:', payload)\n } catch (err: any) {\n console.error('[Permissions] Save failed:', err)\n saveNotification.value = {\n type: 'error',\n message: err?.message || 'Failed to save permissions',\n }\n clearSaveNotification()\n }\n}\n\nfunction discardChanges() {\n modifiedPermissions.value = new Map()\n originalPermissions.value = new Map()\n saveNotification.value = null\n}\n\nasync function fetchFeatureFlags() {\n try {\n const tokenCookie = useCookie('access_token')\n const token = tokenCookie.value\n if (!token) {\n console.warn('[Permissions] No access token found for feature flags')\n return\n }\n\n const data = await $fetch<FeatureFlagsResponse>('/api/v1/subscriptions/feature-flags', {\n headers: { Authorization: `Bearer ${token}` },\n })\n console.log('[Permissions] Feature flags loaded:', data)\n featureFlags.value = data\n } catch (err: any) {\n console.error('[Permissions] Feature flags fetch failed:', err?.message || err)\n // Keep default values\n }\n}\n\nasync function fetchPermissionMatrix() {\n try {\n const tokenCookie = useCookie('access_token')\n const token = tokenCookie.value\n if (!token) {\n console.warn('[Permissions] No access token found for permission matrix')\n return\n }\n\n const data = await $fetch<PermissionMatrixResponse>('/api/v1/admin/permissions/matrix', {\n headers: { Authorization: `Bearer ${token}` },\n })\n console.log('[Permissions] Permission matrix loaded:', data)\n permissionMatrix.value = data.matrix\n allActions.value = data.all_actions\n } catch (err: any) {\n console.error('[Permissions] Permission matrix fetch failed:', err?.message || err)\n }\n}\n\nasync function fetchAll() {\n loading.value = true\n error.value = null\n try {\n await Promise.all([fetchFeatureFlags(), fetchPermissionMatrix()])\n } catch (err: any) {\n console.error('[Permissions] fetchAll failed:', err?.message || err)\n error.value = err?.message || 'Failed to load permissions data'\n } finally {\n loading.value = false\n }\n}\n\nonMounted(() => {\n fetchAll()\n})\n</script>\n"],"version":3} |