Files
service-finder/frontend_admin/.nuxt/dist/server/_nuxt/index-Ci9UUd4C.js.map.json
2026-06-29 14:11:15 +00:00

1 line
30 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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}