1 line
30 KiB
JSON
1 line
30 KiB
JSON
{"file":"index-Ci9UUd4C.js","mappings":"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","names":["$t","_ssrRenderAttr","_ssrInterpolate","_ssrRenderList","_push","_parent","_createTextVNode","_toDisplayString","_ssrRenderClass"],"sources":["../../../../pages/persons/index.vue"],"sourcesContent":["<template>\n <div>\n <!-- Page Header -->\n <div class=\"mb-8\">\n <h1 class=\"text-2xl font-bold text-white\">{{ $t('persons.title') }}</h1>\n <p class=\"text-slate-400 mt-1\">{{ $t('persons.subtitle') }}</p>\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('persons.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 flex-wrap\">\n <!-- Ghost Filter -->\n <select\n v-model=\"ghostFilter\"\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('persons.all_ghost_statuses') }}</option>\n <option value=\"no\">{{ $t('persons.ghost_no') }}</option>\n <option value=\"yes\">{{ $t('persons.ghost_yes') }}</option>\n </select>\n <!-- Active Filter -->\n <select\n v-model=\"activeFilter\"\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('persons.all_active_statuses') }}</option>\n <option value=\"active\">{{ $t('persons.active_yes') }}</option>\n <option value=\"inactive\">{{ $t('persons.active_no') }}</option>\n </select>\n <!-- Has User Filter -->\n <select\n v-model=\"hasUserFilter\"\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('persons.all_user_statuses') }}</option>\n <option value=\"yes\">{{ $t('persons.has_user_yes') }}</option>\n <option value=\"no\">{{ $t('persons.has_user_no') }}</option>\n </select>\n <!-- Merged Filter -->\n <select\n v-model=\"mergedFilter\"\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('persons.all_merged_statuses') }}</option>\n <option value=\"no\">{{ $t('persons.merged_no') }}</option>\n <option value=\"yes\">{{ $t('persons.merged_yes') }}</option>\n </select>\n </div>\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('persons.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=\"fetchPersons\" class=\"px-4 py-2 text-sm bg-indigo-600/20 text-indigo-300 hover:bg-indigo-600/30 rounded-lg transition\">\n {{ $t('persons.retry') }}\n </button>\n </div>\n\n <!-- Content -->\n <template v-else>\n <!-- Stats Cards Row -->\n <div class=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8\">\n <!-- Total Persons -->\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=\"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('persons.total_persons') }}</p>\n <p class=\"text-2xl font-bold text-white\">{{ totalCount }}</p>\n </div>\n </div>\n </div>\n <!-- Active Persons -->\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('persons.active_persons') }}</p>\n <p class=\"text-2xl font-bold text-emerald-400\">{{ activeCount }}</p>\n </div>\n </div>\n </div>\n <!-- Ghost Persons -->\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=\"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\" />\n </svg>\n </div>\n <div>\n <p class=\"text-xs text-slate-400 uppercase tracking-wider\">{{ $t('persons.ghost_persons') }}</p>\n <p class=\"text-2xl font-bold text-amber-400\">{{ ghostCount }}</p>\n </div>\n </div>\n </div>\n <!-- Merged Persons -->\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-red-500/10 text-red-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=\"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\" />\n </svg>\n </div>\n <div>\n <p class=\"text-xs text-slate-400 uppercase tracking-wider\">{{ $t('persons.merged_persons') }}</p>\n <p class=\"text-2xl font-bold text-red-400\">{{ mergedCount }}</p>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Filtered results indicator -->\n <div v-if=\"isFilterActive\" class=\"mb-4 text-xs text-slate-500\">\n {{ $t('persons.filtered_results') }}: <span class=\"text-slate-300 font-semibold\">{{ persons.length }}</span> / {{ totalCount }} db\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('persons.name') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('persons.phone') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('persons.birth_date') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('persons.status') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('persons.users_count') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('persons.active_user') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('persons.address') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('persons.actions') }}</th>\n </tr>\n </thead>\n <tbody>\n <tr\n v-for=\"person in persons\"\n :key=\"person.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\">{{ person.id }}</td>\n <!-- Name -->\n <td class=\"py-3.5 px-4\">\n <NuxtLink\n :to=\"'/persons/' + person.id\"\n class=\"text-sm font-medium text-white hover:underline cursor-pointer hover:text-indigo-400 transition-colors\"\n >\n {{ person.last_name }} {{ person.first_name }}\n </NuxtLink>\n </td>\n <!-- Phone -->\n <td class=\"py-3.5 px-4 text-slate-300\">{{ person.phone || '—' }}</td>\n <!-- Birth Date -->\n <td class=\"py-3.5 px-4 text-slate-300 text-xs\">{{ formatDate(person.birth_date) }}</td>\n <!-- Status -->\n <td class=\"py-3.5 px-4\">\n <div class=\"flex flex-wrap gap-1\">\n <span\n class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium\"\n :class=\"person.is_active ? 'bg-emerald-500/10 text-emerald-400' : 'bg-amber-500/10 text-amber-400'\"\n >\n <span\n class=\"w-1.5 h-1.5 rounded-full mr-1.5\"\n :class=\"person.is_active ? 'bg-emerald-400' : 'bg-amber-400'\"\n ></span>\n {{ person.is_active ? $t('persons.active') : $t('persons.inactive') }}\n </span>\n <span\n v-if=\"person.is_ghost\"\n class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-500/20 text-purple-300\"\n >\n {{ $t('persons.ghost') }}\n </span>\n <span\n v-if=\"person.merged_into_id\"\n class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-500/20 text-red-300\"\n >\n {{ $t('persons.merged') }}\n </span>\n </div>\n </td>\n <!-- Users Count -->\n <td class=\"py-3.5 px-4\">\n <span class=\"text-slate-300\">{{ person.users_count }}</span>\n </td>\n <!-- Active User -->\n <td class=\"py-3.5 px-4\">\n <div v-if=\"person.active_user\">\n <p class=\"text-slate-300 text-xs\">{{ person.active_user.email }}</p>\n <span\n class=\"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium\"\n :class=\"roleBadgeClass(person.active_user.role)\"\n >\n {{ person.active_user.role }}\n </span>\n </div>\n <span v-else class=\"text-slate-500\">—</span>\n </td>\n <!-- Address -->\n <td class=\"py-3.5 px-4\">\n <span v-if=\"person.address\" class=\"text-slate-300 text-xs\">{{ person.address.full_address_text || (person.address.address_city + ', ' + person.address.address_street_name + ' ' + (person.address.address_house_number || '')) }}</span>\n <span v-else class=\"text-slate-500\">—</span>\n </td>\n <!-- Actions -->\n <td class=\"py-3.5 px-4\">\n <div class=\"flex items-center gap-2\">\n <NuxtLink\n :to=\"'/persons/' + person.id\"\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 >\n {{ $t('persons.view') }}\n </NuxtLink>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Empty State -->\n <div\n v-if=\"persons.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=\"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 <p class=\"text-slate-400 text-sm\">{{ $t('persons.no_results') }}</p>\n </div>\n </div>\n\n <!-- Pagination -->\n <div v-if=\"totalCount > limit\" class=\"flex items-center justify-between mt-6\">\n <p class=\"text-xs text-slate-500\">\n {{ $t('persons.showing') }} {{ skip + 1 }}–{{ Math.min(skip + limit, totalCount) }} / {{ totalCount }}\n </p>\n <div class=\"flex items-center gap-2\">\n <button\n @click=\"prevPage\"\n :disabled=\"skip === 0\"\n class=\"px-3 py-1.5 text-xs font-medium rounded-lg transition disabled:opacity-30 disabled:cursor-not-allowed\"\n :class=\"skip > 0 ? 'bg-slate-700 text-slate-300 hover:bg-slate-600' : 'bg-slate-800 text-slate-600'\"\n >\n {{ $t('persons.prev') }}\n </button>\n <button\n @click=\"nextPage\"\n :disabled=\"skip + limit >= totalCount\"\n class=\"px-3 py-1.5 text-xs font-medium rounded-lg transition disabled:opacity-30 disabled:cursor-not-allowed\"\n :class=\"skip + limit < totalCount ? 'bg-slate-700 text-slate-300 hover:bg-slate-600' : 'bg-slate-800 text-slate-600'\"\n >\n {{ $t('persons.next') }}\n </button>\n </div>\n </div>\n </template>\n\n <!-- Notification -->\n <div\n v-if=\"notification\"\n class=\"fixed bottom-6 right-6 px-5 py-3 rounded-xl shadow-xl text-sm font-medium z-50\"\n :class=\"notification.type === 'success'\n ? 'bg-emerald-600 text-white'\n : 'bg-red-600 text-white'\"\n >\n {{ notification.message }}\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onUnmounted, watch } from 'vue'\nimport { useRouter } from 'vue-router'\nimport { useAuthStore } from '~/stores/auth'\n\nconst router = useRouter()\nconst auth = useAuthStore()\n\ndefinePageMeta({\n middleware: 'auth',\n})\n\n// ── RBAC Permission Check ──────────────────────────────────────────\n\nconst canViewPersons = computed(() => {\n if (auth.isAdmin) return true\n return !!auth.user?.system_capabilities?.['persons:view']\n})\n\nconst canEditPersons = computed(() => {\n if (auth.isAdmin) return true\n return !!auth.user?.system_capabilities?.['persons:edit']\n})\n\nconst canMergePersons = computed(() => {\n if (auth.isAdmin) return true\n return !!auth.user?.system_capabilities?.['persons:merge']\n})\n\n// Redirect if no view permission\nif (!canViewPersons.value && !auth.isAdmin) {\n router.push('/')\n}\n\n// ── Types ──────────────────────────────────────────────────────────\n\ninterface PersonListItemActiveUser {\n id: number\n email: string | null\n role: string | null\n}\n\ninterface PersonAddress {\n address_zip: string | null\n address_city: string | null\n address_street_name: string | null\n address_street_type: string | null\n address_house_number: string | null\n address_stairwell: string | null\n address_floor: string | null\n address_door: string | null\n address_hrsz: string | null\n full_address_text: string | null\n latitude: number | null\n longitude: number | null\n}\n\ninterface PersonListItem {\n id: number\n last_name: string\n first_name: string\n phone: string | null\n birth_date: string | null\n is_ghost: boolean\n is_active: boolean\n merged_into_id: number | null\n users_count: number\n active_user: PersonListItemActiveUser | null\n address: PersonAddress | null\n}\n\ninterface PersonListResponse {\n total: number\n skip: number\n limit: number\n items: PersonListItem[]\n}\n\n// ── State ──────────────────────────────────────────────────────────\n\nconst loading = ref(true)\nconst error = ref<string | null>(null)\nconst persons = ref<PersonListItem[]>([])\nconst totalCount = ref(0)\nconst skip = ref(0)\nconst limit = ref(50)\n\nconst searchQuery = ref('')\nconst ghostFilter = ref('all')\nconst activeFilter = ref('all')\nconst hasUserFilter = ref('all')\nconst mergedFilter = ref('all')\n\n// Notification\nconst notification = ref<{ type: 'success' | 'error'; message: string } | null>(null)\n\n// ── Computed Stats ─────────────────────────────────────────────────\n\nconst activeCount = computed(() => persons.value.filter(p => p.is_active).length)\nconst ghostCount = computed(() => persons.value.filter(p => p.is_ghost).length)\nconst mergedCount = computed(() => persons.value.filter(p => p.merged_into_id).length)\n\n// ── Debounced Search Timer ──────────────────────────────────────────\n\nlet searchDebounceTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction debouncedFetchPersons() {\n if (searchDebounceTimer) {\n clearTimeout(searchDebounceTimer)\n }\n searchDebounceTimer = setTimeout(() => {\n skip.value = 0\n fetchPersons()\n }, 400)\n}\n\n// ── Watchers ────────────────────────────────────────────────────────\n\nwatch(searchQuery, () => {\n debouncedFetchPersons()\n})\n\nwatch([ghostFilter, activeFilter, hasUserFilter, mergedFilter], () => {\n skip.value = 0\n fetchPersons()\n})\n\n// ── Computed ───────────────────────────────────────────────────────\n\nconst isFilterActive = computed(() => {\n return searchQuery.value !== ''\n || ghostFilter.value !== 'all'\n || activeFilter.value !== 'all'\n || hasUserFilter.value !== 'all'\n || mergedFilter.value !== 'all'\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 fetchPersons() {\n loading.value = true\n error.value = null\n try {\n const params: Record<string, any> = { skip: skip.value, limit: limit.value }\n\n if (searchQuery.value) {\n params.search = searchQuery.value\n }\n if (ghostFilter.value !== 'all') {\n params.is_ghost = ghostFilter.value === 'yes'\n }\n if (activeFilter.value !== 'all') {\n params.is_active = activeFilter.value === 'active'\n }\n if (hasUserFilter.value !== 'all') {\n params.has_user = hasUserFilter.value === 'yes'\n }\n if (mergedFilter.value !== 'all') {\n params.is_merged = mergedFilter.value === 'yes'\n }\n\n const response = await $fetch<PersonListResponse>('/api/v1/admin/persons', {\n params,\n headers: getAuthHeaders(),\n })\n persons.value = response.items\n totalCount.value = response.total\n } catch (err: any) {\n console.error('[Persons] Failed to fetch persons:', err)\n error.value = err?.data?.detail || err?.message || 'Failed to load persons'\n } finally {\n loading.value = false\n }\n}\n\n// ── Pagination ─────────────────────────────────────────────────────\n\nfunction nextPage() {\n if (skip.value + limit.value < totalCount.value) {\n skip.value += limit.value\n fetchPersons()\n }\n}\n\nfunction prevPage() {\n if (skip.value > 0) {\n skip.value = Math.max(0, skip.value - limit.value)\n fetchPersons()\n }\n}\n\n// ── Helpers ────────────────────────────────────────────────────────\n\nfunction roleBadgeClass(role: string | null): string {\n const r = (role || '').toLowerCase()\n if (r === 'superadmin') return 'bg-purple-500/20 text-purple-300'\n if (r === 'admin') return 'bg-indigo-500/20 text-indigo-300'\n if (r === 'staff') return 'bg-cyan-500/20 text-cyan-300'\n return 'bg-slate-500/20 text-slate-300'\n}\n\nfunction formatDate(dateStr: string | null): string {\n if (!dateStr) return '—'\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 notification.value = { type, message }\n setTimeout(() => {\n notification.value = null\n }, 4000)\n}\n\n// ── Lifecycle ──────────────────────────────────────────────────────\n\nonMounted(() => {\n fetchPersons()\n})\n</script>\n"],"version":3} |