Files
service-finder/frontend_admin/.nuxt/dist/server/_nuxt/edit-ZNmjSMhu.js.map.json
2026-07-02 11:52:22 +00:00

1 line
78 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":"edit-ZNmjSMhu.js","mappings":"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","names":["_ssrRenderClass","_ssrIncludeBooleanAttr","_ssrInterpolate","$emit","_ssrRenderAttrs","_mergeProps","_ssrRenderAttr","DAYS","$route","_ssrRenderList","_ssrRenderVNode","_createVNode","_resolveDynamicComponent","_ssrLooseContain"],"sources":["../../../../components/TreeNode.vue","../../../../components/SmartLocationInput.vue","../../../../components/SharedAddressForm.vue","../../../../pages/providers/[id]/edit.vue"],"sourcesContent":["<template>\n <div>\n <div class=\"flex items-center gap-2 py-1\">\n <button\n @click=\"$emit('toggle-expand', node.id)\"\n class=\"text-slate-500 hover:text-slate-300 transition w-4 h-4 flex items-center justify-center\"\n >\n <svg\n class=\"w-3 h-3 transition-transform\"\n :class=\"expandedIds.has(node.id) ? 'rotate-90' : ''\"\n fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n <label class=\"flex items-center gap-2 cursor-pointer flex-1\">\n <input\n type=\"checkbox\"\n :checked=\"selectedIds.includes(node.id)\"\n @change=\"$emit('toggle', node.id)\"\n class=\"w-4 h-4 rounded border-slate-500 bg-slate-700 text-indigo-500 focus:ring-indigo-500/50\"\n />\n <span class=\"text-sm text-slate-300\">{{ node.name_hu || node.name_en || node.key }}</span>\n <span class=\"text-xs text-slate-500\">(Szint {{ node.level }})</span>\n </label>\n </div>\n <div v-if=\"expandedIds.has(node.id) && node.children?.length\" class=\"ml-6 border-l border-slate-700 pl-3\">\n <div v-for=\"child in node.children\" :key=\"child.id\">\n <TreeNode\n :node=\"child\"\n :selected-ids=\"selectedIds\"\n :expanded-ids=\"expandedIds\"\n @toggle=\"(id: number) => $emit('toggle', id)\"\n @toggle-expand=\"(id: number) => $emit('toggle-expand', id)\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\n/**\n * Recursive TreeNode component for the category tree.\n * Renders a checkbox tree with expand/collapse for nested categories.\n */\ninterface CategoryTreeNode {\n id: number\n key: string\n name_hu?: string | null\n name_en?: string | null\n level: number\n path?: string | null\n is_official: boolean\n children: CategoryTreeNode[]\n}\n\ndefineProps<{\n node: CategoryTreeNode\n selectedIds: number[]\n expandedIds: Set<number>\n}>()\n\ndefineEmits<{\n toggle: [id: number]\n 'toggle-expand': [id: number]\n}>()\n</script>\n","<template>\n <div class=\"relative\">\n <label v-if=\"label\" class=\"block text-sm font-medium text-slate-300 mb-1\">{{ label }}</label>\n <div class=\"relative\">\n <input\n ref=\"inputRef\"\n :value=\"displayText\"\n @input=\"onInput\"\n @focus=\"onFocus\"\n @blur=\"onBlur\"\n @keydown.down.prevent=\"highlightNext\"\n @keydown.up.prevent=\"highlightPrev\"\n @keydown.enter.prevent=\"selectHighlighted\"\n @keydown.escape=\"closeDropdown\"\n :placeholder=\"placeholder\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n <!-- Loading spinner -->\n <svg\n v-if=\"searching\"\n class=\"absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 animate-spin text-slate-400\"\n fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\" />\n </svg>\n <!-- Clear button -->\n <button\n v-if=\"selectedItem && !searching\"\n @click=\"clearSelection\"\n class=\"absolute right-3 top-1/2 -translate-y-1/2 text-slate-500 hover:text-white transition\"\n type=\"button\"\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=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n <!-- Dropdown -->\n <div\n v-if=\"showDropdown && filteredItems.length > 0\"\n class=\"absolute z-50 mt-1 w-full bg-slate-800 border border-slate-600 rounded-lg shadow-xl max-h-60 overflow-y-auto\"\n >\n <button\n v-for=\"(item, index) in filteredItems\"\n :key=\"item.id\"\n @mousedown.prevent=\"selectItem(item)\"\n class=\"w-full text-left px-3 py-2 text-sm transition flex items-center gap-2\"\n :class=\"index === highlightedIndex\n ? 'bg-indigo-500/20 text-white'\n : 'text-slate-300 hover:bg-slate-700'\"\n >\n <span class=\"font-mono text-indigo-400\">{{ item.zip_code }}</span>\n <span>{{ item.city }}</span>\n <span class=\"text-xs text-slate-500 ml-auto\">{{ item.country_code }}</span>\n </button>\n </div>\n\n <!-- No results -->\n <div\n v-if=\"showDropdown && query.length >= minChars && filteredItems.length === 0 && !searching\"\n class=\"absolute z-50 mt-1 w-full bg-slate-800 border border-slate-600 rounded-lg shadow-xl p-3 text-sm text-slate-400\"\n >\n Nincs találat\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\n/**\n * SmartLocationInput.vue\n * ======================\n * \n * Autocomplete/combobox component for selecting a city/zip code pair.\n * Calls the backend GET /api/v1/locations/autocomplete endpoint.\n * \n * Usage:\n * <SmartLocationInput v-model=\"selected\" />\n * \n * The v-model emits the selected GeoPostalCode object:\n * { id: number, zip_code: string, city: string, country_code: string }\n * \n * Props:\n * modelValue: The currently selected location (null or the object)\n * label: Optional label text above the input\n * placeholder: Placeholder text\n * minChars: Minimum characters before triggering search (default: 2)\n * countryCode: Filter by country code (default: 'HU')\n * debounceMs: Debounce delay in ms (default: 300)\n */\n\nimport { ref, computed, watch, nextTick } from 'vue'\n\ninterface LocationItem {\n id: number\n zip_code: string\n city: string\n country_code: string\n}\n\nconst props = withDefaults(defineProps<{\n modelValue?: LocationItem | null\n label?: string\n placeholder?: string\n minChars?: number\n countryCode?: string\n debounceMs?: number\n}>(), {\n modelValue: null,\n label: '',\n placeholder: 'Kezdj el gépelni (irányítószám vagy város)...',\n minChars: 2,\n countryCode: 'HU',\n debounceMs: 300,\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: LocationItem | null]\n 'select': [value: LocationItem]\n}>()\n\n// ── State ──────────────────────────────────────────────\nconst inputRef = ref<HTMLInputElement | null>(null)\nconst query = ref('')\nconst selectedItem = ref<LocationItem | null>(props.modelValue)\nconst items = ref<LocationItem[]>([])\nconst searching = ref(false)\nconst showDropdown = ref(false)\nconst highlightedIndex = ref(0)\nlet debounceTimer: ReturnType<typeof setTimeout> | null = null\n\n// ── Computed ───────────────────────────────────────────\n\n/**\n * The display text shown in the input.\n * If a location is selected, shows \"zip_code city\".\n * Otherwise shows the raw query text.\n */\nconst displayText = computed(() => {\n if (selectedItem.value) {\n return `${selectedItem.value.zip_code} ${selectedItem.value.city}`\n }\n return query.value\n})\n\n/**\n * Filtered items based on the current query.\n * The backend already filters, but we keep client-side filtering\n * for instant feedback when the user types more after results load.\n */\nconst filteredItems = computed(() => {\n if (!query.value || query.value.length < props.minChars) return []\n const q = query.value.toLowerCase()\n return items.value.filter(\n (item) =>\n item.zip_code.toLowerCase().includes(q) ||\n item.city.toLowerCase().includes(q)\n )\n})\n\n// ── Watch modelValue from parent ───────────────────────\nwatch(\n () => props.modelValue,\n (val) => {\n selectedItem.value = val\n }\n)\n\n// ── Methods ────────────────────────────────────────────\n\n/**\n * Handle input events from the text field.\n * Starts debounced search when the user types.\n */\nfunction onInput(event: Event) {\n const target = event.target as HTMLInputElement\n query.value = target.value\n\n // If the user is typing, clear the selected item\n if (selectedItem.value) {\n selectedItem.value = null\n emit('update:modelValue', null)\n }\n\n // Debounced search\n if (debounceTimer) clearTimeout(debounceTimer)\n if (query.value.length >= props.minChars) {\n debounceTimer = setTimeout(() => {\n searchLocations(query.value)\n }, props.debounceMs)\n } else {\n items.value = []\n showDropdown.value = false\n }\n}\n\n/**\n * Call the backend autocomplete API.\n */\nasync function searchLocations(searchQuery: string) {\n if (!searchQuery || searchQuery.length < props.minChars) return\n searching.value = true\n try {\n const res = await $fetch('/api/v1/locations/autocomplete', {\n params: {\n q: searchQuery,\n country_code: props.countryCode,\n limit: 15,\n },\n headers: getHeaders(),\n })\n items.value = (res as LocationItem[]) || []\n highlightedIndex.value = 0\n showDropdown.value = items.value.length > 0\n } catch (err: any) {\n console.error('Location autocomplete error:', err)\n items.value = []\n } finally {\n searching.value = false\n }\n}\n\n/**\n * Get auth headers for API calls.\n */\nfunction getHeaders(): Record<string, string> {\n const token = useCookie('access_token').value\n const headers: Record<string, string> = { 'Content-Type': 'application/json' }\n if (token) headers['Authorization'] = `Bearer ${token}`\n return headers\n}\n\n/**\n * Select an item from the dropdown.\n */\nfunction selectItem(item: LocationItem) {\n selectedItem.value = item\n query.value = ''\n showDropdown.value = false\n emit('update:modelValue', item)\n emit('select', item)\n}\n\n/**\n * Clear the current selection.\n */\nfunction clearSelection() {\n selectedItem.value = null\n query.value = ''\n items.value = []\n showDropdown.value = false\n emit('update:modelValue', null)\n nextTick(() => {\n inputRef.value?.focus()\n })\n}\n\n/**\n * Close the dropdown.\n */\nfunction closeDropdown() {\n showDropdown.value = false\n}\n\n/**\n * Handle focus - show dropdown if there are results.\n */\nfunction onFocus() {\n if (filteredItems.value.length > 0) {\n showDropdown.value = true\n }\n}\n\n/**\n * Handle blur - close dropdown with a delay to allow click.\n */\nfunction onBlur() {\n // Delay to allow mousedown on dropdown items\n setTimeout(() => {\n showDropdown.value = false\n }, 200)\n}\n\n/**\n * Highlight the next item in the dropdown.\n */\nfunction highlightNext() {\n if (filteredItems.value.length > 0) {\n highlightedIndex.value = (highlightedIndex.value + 1) % filteredItems.value.length\n }\n}\n\n/**\n * Highlight the previous item in the dropdown.\n */\nfunction highlightPrev() {\n if (filteredItems.value.length > 0) {\n highlightedIndex.value = (highlightedIndex.value - 1 + filteredItems.value.length) % filteredItems.value.length\n }\n}\n\n/**\n * Select the currently highlighted item.\n */\nfunction selectHighlighted() {\n if (filteredItems.value.length > 0 && highlightedIndex.value >= 0) {\n selectItem(filteredItems.value[highlightedIndex.value])\n }\n}\n</script>\n\n<style scoped>\n.overflow-y-auto::-webkit-scrollbar {\n width: 6px;\n}\n.overflow-y-auto::-webkit-scrollbar-track {\n background: transparent;\n}\n.overflow-y-auto::-webkit-scrollbar-thumb {\n background-color: rgba(100, 116, 139, 0.5);\n border-radius: 3px;\n}\n</style>\n","<template>\n <div>\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-6 mb-6\">\n <h2 class=\"text-lg font-semibold text-white mb-4\">Cím adatok</h2>\n\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <!-- Smart Location Input (zip + city autocomplete) -->\n <div class=\"md:col-span-2\">\n <SmartLocationInput\n :model-value=\"selectedLocation\"\n @update:model-value=\"onLocationSelected\"\n label=\"Irányítószám / Város\"\n placeholder=\"Kezdj el gépelni (pl. 1011 vagy Budapest)...\"\n />\n </div>\n\n <!-- Street name -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Utca neve</label>\n <input\n :value=\"modelValue.street_name\"\n @input=\"updateField('street_name', ($event.target as HTMLInputElement).value)\"\n placeholder=\"pl. Egressy\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n\n <!-- Street type -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Közterület jellege</label>\n <select\n :value=\"modelValue.street_type\"\n @change=\"updateField('street_type', ($event.target as HTMLSelectElement).value)\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n >\n <option value=\"\">Válassz...</option>\n <option value=\"utca\">utca</option>\n <option value=\"út\">út</option>\n <option value=\"tér\">tér</option>\n <option value=\"körút\">körút</option>\n <option value=\"sugárút\">sugárút</option>\n <option value=\"köz\">köz</option>\n <option value=\"sétány\">sétány</option>\n <option value=\"lakópark\">lakópark</option>\n <option value=\"liget\">liget</option>\n <option value=\"erdő\">erdő</option>\n <option value=\"dűlő\">dűlő</option>\n <option value=\"major\">major</option>\n <option value=\"puszta\">puszta</option>\n <option value=\"sziget\">sziget</option>\n <option value=\"telep\">telep</option>\n </select>\n </div>\n\n <!-- House number -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Házszám</label>\n <input\n :value=\"modelValue.house_number\"\n @input=\"updateField('house_number', ($event.target as HTMLInputElement).value)\"\n placeholder=\"pl. 4/b\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n\n <!-- Stairwell -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Lépcsőház</label>\n <input\n :value=\"modelValue.stairwell\"\n @input=\"updateField('stairwell', ($event.target as HTMLInputElement).value)\"\n placeholder=\"pl. 1\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n\n <!-- Floor -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Emelet</label>\n <input\n :value=\"modelValue.floor\"\n @input=\"updateField('floor', ($event.target as HTMLInputElement).value)\"\n placeholder=\"pl. 3\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n\n <!-- Door -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Ajtó</label>\n <input\n :value=\"modelValue.door\"\n @input=\"updateField('door', ($event.target as HTMLInputElement).value)\"\n placeholder=\"pl. A\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n\n <!-- Parcel ID (hrsz) -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Helyrajzi szám (hrsz)</label>\n <input\n :value=\"modelValue.parcel_id\"\n @input=\"updateField('parcel_id', ($event.target as HTMLInputElement).value)\"\n placeholder=\"pl. 12345/6\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\n/**\n * SharedAddressForm.vue\n * ======================\n * \n * Reusable address form component that uses SmartLocationInput for\n * zip/city autocomplete and provides all other address fields.\n * \n * Usage:\n * <SharedAddressForm v-model=\"form.address\" />\n * \n * The v-model expects an object matching the AddressIn schema:\n * {\n * zip?: string\n * city?: string\n * street_name?: string\n * street_type?: string\n * house_number?: string\n * stairwell?: string\n * floor?: string\n * door?: string\n * parcel_id?: string\n * full_address_text?: string\n * latitude?: number\n * longitude?: number\n * }\n */\n\nimport { ref, computed } from 'vue'\n\ninterface AddressForm {\n zip?: string | null\n city?: string | null\n street_name?: string | null\n street_type?: string | null\n house_number?: string | null\n stairwell?: string | null\n floor?: string | null\n door?: string | null\n parcel_id?: string | null\n full_address_text?: string | null\n latitude?: number | null\n longitude?: number | null\n}\n\ninterface LocationItem {\n id: number\n zip_code: string\n city: string\n country_code: string\n}\n\nconst props = withDefaults(defineProps<{\n modelValue?: AddressForm\n}>(), {\n modelValue: () => ({\n zip: null,\n city: null,\n street_name: null,\n street_type: null,\n house_number: null,\n stairwell: null,\n floor: null,\n door: null,\n parcel_id: null,\n full_address_text: null,\n latitude: null,\n longitude: null,\n }),\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: AddressForm]\n}>()\n\n/**\n * Computed property for the SmartLocationInput selected value.\n * Converts the address zip/city to a LocationItem if both are present.\n */\nconst selectedLocation = computed<LocationItem | null>(() => {\n if (props.modelValue?.zip && props.modelValue?.city) {\n return {\n id: 0, // We don't have the DB id here, but SmartLocationInput handles this\n zip_code: props.modelValue.zip,\n city: props.modelValue.city,\n country_code: 'HU',\n }\n }\n return null\n})\n\n/**\n * Handle location selection from SmartLocationInput.\n * Updates the zip and city fields in the address model.\n */\nfunction onLocationSelected(location: LocationItem | null) {\n const updated = { ...props.modelValue }\n if (location) {\n updated.zip = location.zip_code\n updated.city = location.city\n } else {\n updated.zip = null\n updated.city = null\n }\n emit('update:modelValue', updated)\n}\n\n/**\n * Update a single field in the address model.\n */\nfunction updateField(field: keyof AddressForm, value: string) {\n const updated = { ...props.modelValue }\n ;(updated as any)[field] = value || null\n emit('update:modelValue', updated)\n}\n</script>\n","<template>\n <div>\n <!-- Back Button & Header -->\n <div class=\"mb-8\">\n <button\n @click=\"goBack\"\n class=\"inline-flex items-center gap-1.5 text-sm text-slate-400 hover:text-white transition mb-4\"\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=\"M15 19l-7-7 7-7\" />\n </svg>\n Vissza a szolgáltatóhoz\n </button>\n <h1 class=\"text-2xl font-bold text-white\">Szolgáltató szerkesztése</h1>\n <p class=\"text-slate-400 mt-1\">Szolgáltató #{{ $route.params.id }}</p>\n </div>\n\n <!-- Loading State -->\n <div v-if=\"loading\" class=\"flex items-center justify-center py-20\">\n <div class=\"text-slate-400 flex items-center gap-3\">\n <svg class=\"w-5 h-5 animate-spin\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\" />\n </svg>\n <span>Szolgáltató betöltése...</span>\n </div>\n </div>\n\n <!-- Error State -->\n <div v-else-if=\"error\" class=\"bg-rose-500/10 border border-rose-500/30 rounded-xl p-6 mb-8\">\n <p class=\"text-rose-400\">Hiba történt a szolgáltató betöltése közben.</p>\n <button @click=\"fetchProvider\" class=\"mt-3 text-sm text-rose-300 hover:text-rose-200 underline\">Újrapróbálkozás</button>\n </div>\n\n <!-- Not Found -->\n <div v-else-if=\"!provider\" class=\"bg-slate-800 rounded-xl border border-slate-700 p-6\">\n <p class=\"text-slate-400 text-center py-12\">A szolgáltató nem található.</p>\n </div>\n\n <template v-else>\n <!-- Tab Navigation -->\n <div class=\"border-b border-slate-700 mb-6\">\n <nav class=\"flex gap-6 -mb-px\">\n <button\n v-for=\"tab in tabs\"\n :key=\"tab.key\"\n @click=\"activeTab = tab.key\"\n class=\"pb-3 text-sm font-medium border-b-2 transition flex items-center gap-2\"\n :class=\"activeTab === tab.key ? 'text-indigo-400 border-indigo-400' : 'text-slate-400 border-transparent hover:text-slate-300'\"\n >\n <component :is=\"tab.icon\" class=\"w-4 h-4\" />\n {{ tab.label }}\n </button>\n </nav>\n </div>\n\n <!-- ═══════════════════════════════════════════════════\n TAB 1: Alapadatok & Kapcsolat (Basic Info & Contact)\n ═══════════════════════════════════════════════════ -->\n <div v-if=\"activeTab === 'basic'\" class=\"max-w-3xl\">\n <form @submit.prevent=\"saveForm\">\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-6 mb-6\">\n <h2 class=\"text-lg font-semibold text-white mb-4\">Alapadatok</h2>\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <!-- Name -->\n <div class=\"md:col-span-2\">\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Név *</label>\n <input\n v-model=\"form.name\"\n required\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n <!-- Category -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Kategória</label>\n <input\n v-model=\"form.category\"\n placeholder=\"pl. Autószerviz, Gumiszerviz\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n <!-- Source (read-only) -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Forrás</label>\n <div class=\"w-full px-3 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-slate-400 text-sm\">\n {{ provider.source }}\n </div>\n </div>\n </div>\n </div>\n\n <!-- Contact Information -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-6 mb-6\">\n <h2 class=\"text-lg font-semibold text-white mb-4\">Elérhetőség</h2>\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Telefon</label>\n <input\n v-model=\"form.contact_phone\"\n placeholder=\"+36 30 123 4567\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Email</label>\n <input\n v-model=\"form.contact_email\"\n type=\"email\"\n placeholder=\"info@example.com\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n <div class=\"md:col-span-2\">\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Weboldal</label>\n <input\n v-model=\"form.website\"\n placeholder=\"https://...\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n </div>\n </div>\n </form>\n </div>\n\n <!-- ═══════════════════════════════════════════════════\n TAB 2: Szolgáltatások & Járművek (The 3D Matrix)\n ═══════════════════════════════════════════════════ -->\n <div v-if=\"activeTab === 'matrix'\" class=\"max-w-4xl\">\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-6 mb-6\">\n <h2 class=\"text-lg font-semibold text-white mb-2\">3D Képesség Mátrix</h2>\n <p class=\"text-sm text-slate-400 mb-6\">\n Állítsd be, hogy a szolgáltató milyen járműosztályokhoz, szolgáltatási kategóriákhoz\n és specializációkhoz tartozik.\n </p>\n\n <!-- ── Dimension A: Vehicle Classes ── -->\n <div class=\"mb-8\">\n <h3 class=\"text-md font-medium text-white mb-3 flex items-center gap-2\">\n <span class=\"w-6 h-6 rounded-full bg-indigo-500/20 text-indigo-400 flex items-center justify-center text-xs font-bold\">A</span>\n Járműosztályok (Vehicle Classes)\n </h3>\n <p class=\"text-xs text-slate-500 mb-3\">Mely járműtípusokat szervizeli a szolgáltató?</p>\n <div class=\"grid grid-cols-2 md:grid-cols-4 gap-3\">\n <label\n v-for=\"vc in VEHICLE_CLASSES\"\n :key=\"vc.key\"\n class=\"flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition\"\n :class=\"form.supported_vehicle_classes.includes(vc.key)\n ? 'bg-indigo-500/10 border-indigo-500/40 text-indigo-300'\n : 'bg-slate-700/50 border-slate-600 hover:border-slate-500 text-slate-300'\"\n >\n <input\n type=\"checkbox\"\n :value=\"vc.key\"\n :checked=\"form.supported_vehicle_classes.includes(vc.key)\"\n @change=\"toggleVehicleClass(vc.key)\"\n class=\"w-4 h-4 rounded border-slate-500 bg-slate-700 text-indigo-500 focus:ring-indigo-500/50\"\n />\n <span class=\"text-sm\">{{ vc.label }}</span>\n </label>\n </div>\n </div>\n\n <!-- ── Dimension B: Service Categories (Expertise Tags) ── -->\n <div class=\"mb-8\">\n <h3 class=\"text-md font-medium text-white mb-3 flex items-center gap-2\">\n <span class=\"w-6 h-6 rounded-full bg-emerald-500/20 text-emerald-400 flex items-center justify-center text-xs font-bold\">B</span>\n Szolgáltatási Kategóriák (Expertise Tags)\n </h3>\n <p class=\"text-xs text-slate-500 mb-3\">Válaszd ki a szolgáltatóhoz tartozó kategóriákat a hierarchikus fából.</p>\n\n <!-- Loading tree -->\n <div v-if=\"treeLoading\" class=\"flex items-center gap-2 text-sm text-slate-400 py-4\">\n <svg class=\"w-4 h-4 animate-spin\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\" />\n </svg>\n <span>Kategóriák betöltése...</span>\n </div>\n\n <!-- Tree -->\n <div v-else class=\"space-y-1 max-h-96 overflow-y-auto pr-2\">\n <div v-for=\"node in categoryTree\" :key=\"node.id\" class=\"ml-0\">\n <!-- Level 0 node (root) -->\n <div class=\"flex items-center gap-2 py-1\">\n <button\n @click=\"toggleTreeNode(node.id)\"\n class=\"text-slate-500 hover:text-slate-300 transition w-4 h-4 flex items-center justify-center\"\n >\n <svg\n class=\"w-3 h-3 transition-transform\"\n :class=\"expandedNodes.has(node.id) ? 'rotate-90' : ''\"\n fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n <label class=\"flex items-center gap-2 cursor-pointer flex-1\">\n <input\n type=\"checkbox\"\n :checked=\"isCategorySelected(node.id)\"\n @change=\"toggleCategory(node.id)\"\n class=\"w-4 h-4 rounded border-slate-500 bg-slate-700 text-indigo-500 focus:ring-indigo-500/50\"\n />\n <span class=\"text-sm font-medium text-white\">{{ node.name_hu || node.name_en || node.key }}</span>\n <span class=\"text-xs text-slate-500\">(Szint {{ node.level }})</span>\n </label>\n </div>\n <!-- Children (recursive) -->\n <div v-if=\"expandedNodes.has(node.id) && node.children?.length\" class=\"ml-6 border-l border-slate-700 pl-3\">\n <div v-for=\"child in node.children\" :key=\"child.id\">\n <TreeNode\n :node=\"child\"\n :selected-ids=\"form.category_ids\"\n :expanded-ids=\"expandedNodes\"\n @toggle=\"toggleCategory\"\n @toggle-expand=\"toggleTreeNode\"\n />\n </div>\n </div>\n </div>\n </div>\n\n <!-- Selected categories summary -->\n <div v-if=\"form.category_ids.length > 0\" class=\"mt-4 flex flex-wrap gap-2\">\n <span\n v-for=\"id in form.category_ids\"\n :key=\"id\"\n class=\"inline-flex items-center gap-1 px-2 py-1 bg-indigo-500/10 text-indigo-300 rounded-full text-xs\"\n >\n {{ getCategoryName(id) }}\n <button @click=\"removeCategory(id)\" class=\"hover:text-white transition\">\n <svg class=\"w-3 h-3\" 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 </span>\n </div>\n </div>\n\n <!-- ── Dimension C: Specializations ── -->\n <div class=\"mb-4\">\n <h3 class=\"text-md font-medium text-white mb-3 flex items-center gap-2\">\n <span class=\"w-6 h-6 rounded-full bg-amber-500/20 text-amber-400 flex items-center justify-center text-xs font-bold\">C</span>\n Specializációk\n </h3>\n <p class=\"text-xs text-slate-500 mb-3\">Adj meg speciális márkákat, hajtáslánc típusokat vagy egyéb címkéket.</p>\n\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n <!-- Brands -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Márkák (Brands)</label>\n <div class=\"flex flex-wrap gap-2 mb-2\">\n <span\n v-for=\"(brand, idx) in form.specializations.brands\"\n :key=\"idx\"\n class=\"inline-flex items-center gap-1 px-2 py-1 bg-amber-500/10 text-amber-300 rounded-full text-xs\"\n >\n {{ brand }}\n <button @click=\"removeBrand(idx)\" class=\"hover:text-white transition\">\n <svg class=\"w-3 h-3\" 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 </span>\n </div>\n <div class=\"flex gap-2\">\n <input\n v-model=\"brandInput\"\n @keydown.enter.prevent=\"addBrand\"\n placeholder=\"pl. Volvo, Scania\"\n class=\"flex-1 px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n <button\n @click=\"addBrand\"\n :disabled=\"!brandInput.trim()\"\n class=\"px-3 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:bg-indigo-600/50 text-white rounded-lg text-sm transition\"\n >\n Hozzáad\n </button>\n </div>\n </div>\n\n <!-- Propulsion -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Hajtáslánc (Propulsion)</label>\n <div class=\"flex flex-wrap gap-2 mb-2\">\n <span\n v-for=\"(prop, idx) in form.specializations.propulsion\"\n :key=\"idx\"\n class=\"inline-flex items-center gap-1 px-2 py-1 bg-emerald-500/10 text-emerald-300 rounded-full text-xs\"\n >\n {{ prop }}\n <button @click=\"removePropulsion(idx)\" class=\"hover:text-white transition\">\n <svg class=\"w-3 h-3\" 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 </span>\n </div>\n <div class=\"flex gap-2\">\n <input\n v-model=\"propulsionInput\"\n @keydown.enter.prevent=\"addPropulsion\"\n placeholder=\"pl. EV, Hybrid, Diesel\"\n class=\"flex-1 px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n <button\n @click=\"addPropulsion\"\n :disabled=\"!propulsionInput.trim()\"\n class=\"px-3 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:bg-indigo-600/50 text-white rounded-lg text-sm transition\"\n >\n Hozzáad\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- ═══════════════════════════════════════════════════\n TAB 3: Helyszín & Lokáció (Location)\n ═══════════════════════════════════════════════════ -->\n <div v-if=\"activeTab === 'location'\" class=\"max-w-3xl\">\n <form @submit.prevent=\"saveForm\">\n <!-- Smart Address Form (autocomplete zip/city + structured fields) -->\n <SharedAddressForm v-model=\"form.address\" />\n\n <!-- Additional location fields -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-6 mb-6\">\n <h2 class=\"text-lg font-semibold text-white mb-4\">További helyszín adatok</h2>\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div class=\"md:col-span-2\">\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Teljes cím szövege</label>\n <input\n v-model=\"form.address.full_address_text\"\n placeholder=\"Teljes cím szövege\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n <div class=\"md:col-span-2\">\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">Plus Code</label>\n <input\n v-model=\"form.plus_code\"\n placeholder=\"8FVC9X8V+XV\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm\"\n />\n </div>\n </div>\n </div>\n </form>\n </div>\n\n <!-- ═══════════════════════════════════════════════════\n TAB 4: Nyitvatartás (Opening Hours)\n ═══════════════════════════════════════════════════ -->\n <div v-if=\"activeTab === 'hours'\" class=\"max-w-3xl\">\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-6 mb-6\">\n <h2 class=\"text-lg font-semibold text-white mb-2\">Nyitvatartás</h2>\n <p class=\"text-sm text-slate-400 mb-6\">\n Állítsd be a szolgáltató nyitvatartási idejét napokra bontva.\n </p>\n\n <!-- Always-open toggle -->\n <div class=\"mb-6 p-4 rounded-lg border bg-indigo-500/5 border-indigo-500/20\">\n <label class=\"flex items-center gap-3 cursor-pointer\">\n <input\n type=\"checkbox\"\n v-model=\"form.is_always_open\"\n @change=\"onAlwaysOpenChange\"\n class=\"w-5 h-5 rounded border-slate-500 bg-slate-700 text-indigo-500 focus:ring-indigo-500/50\"\n />\n <div>\n <span class=\"text-sm font-medium text-white\">Folyamatos (0-24) nyitvatartás</span>\n <p class=\"text-xs text-slate-400 mt-0.5\">A szolgáltató a hét minden napján 0-24 óráig nyitva van</p>\n </div>\n </label>\n </div>\n\n <!-- Quick action buttons (disabled when always-open is active) -->\n <div class=\"flex flex-wrap gap-2 mb-6\" :class=\"{ 'opacity-50 pointer-events-none': form.is_always_open }\">\n <button\n @click=\"setAllDaysOpen('00:00', '23:59')\"\n class=\"px-3 py-1.5 text-xs font-medium rounded-lg border border-emerald-600/30 text-emerald-400 hover:bg-emerald-600/10 transition\"\n >\n 24/7 Folyamatos nyitvatartás\n </button>\n <button\n @click=\"copyWeekdays\"\n class=\"px-3 py-1.5 text-xs font-medium rounded-lg border border-indigo-600/30 text-indigo-400 hover:bg-indigo-600/10\"\n >\n Hétköznapok másolása (Hétfő → K-P)\n </button>\n <button\n @click=\"clearAllHours\"\n class=\"px-3 py-1.5 text-xs font-medium rounded-lg border border-rose-600/30 text-rose-400 hover:bg-rose-600/10\"\n >\n Összes törlése\n </button>\n </div>\n\n <!-- 7-day grid (disabled when always-open is active) -->\n <div class=\"space-y-3\" :class=\"{ 'opacity-50 pointer-events-none': form.is_always_open }\">\n <div\n v-for=\"day in DAYS\"\n :key=\"day.key\"\n class=\"flex items-center gap-4 p-4 rounded-lg border\"\n :class=\"form.opening_hours[day.key]\n ? 'bg-indigo-500/5 border-indigo-500/20'\n : 'bg-slate-700/30 border-slate-600'\"\n >\n <!-- Day name + checkbox -->\n <label class=\"flex items-center gap-2 w-36 flex-shrink-0 cursor-pointer\">\n <input\n type=\"checkbox\"\n :checked=\"!!form.opening_hours[day.key]\"\n @change=\"toggleDay(day.key)\"\n :disabled=\"form.is_always_open\"\n class=\"w-4 h-4 rounded border-slate-500 bg-slate-700 text-indigo-500 focus:ring-indigo-500/50\"\n />\n <span class=\"text-sm font-medium text-white\">{{ day.label }}</span>\n </label>\n\n <!-- Time inputs (visible only when checked) -->\n <template v-if=\"form.opening_hours[day.key]\">\n <div class=\"flex items-center gap-2\">\n <input\n type=\"time\"\n :value=\"form.opening_hours[day.key]?.open || '08:00'\"\n @input=\"updateDayTime(day.key, 'open', ($event.target as HTMLInputElement).value)\"\n :disabled=\"form.is_always_open\"\n class=\"w-28 px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 disabled:opacity-50\"\n />\n <span class=\"text-slate-500 text-sm\"></span>\n <input\n type=\"time\"\n :value=\"form.opening_hours[day.key]?.close || '17:00'\"\n @input=\"updateDayTime(day.key, 'close', ($event.target as HTMLInputElement).value)\"\n :disabled=\"form.is_always_open\"\n class=\"w-28 px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 disabled:opacity-50\"\n />\n </div>\n </template>\n\n <!-- \"Zárva\" label when unchecked -->\n <span v-else class=\"text-sm text-slate-500 italic\">Zárva</span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- ═══════════════════════════════════════════════════\n SAVE BUTTON (fixed bottom bar)\n ═══════════════════════════════════════════════════ -->\n <div class=\"sticky bottom-0 bg-slate-900/90 backdrop-blur-sm border-t border-slate-700 -mx-6 px-6 py-4 mt-8\">\n <div class=\"flex items-center justify-between max-w-4xl\">\n <div class=\"text-sm text-slate-400\">\n <span v-if=\"hasChanges\" class=\"text-amber-400\">● Módosítások el nem mentve</span>\n <span v-else class=\"text-slate-500\">Nincs változás</span>\n </div>\n <div class=\"flex gap-3\">\n <button\n @click=\"goBack\"\n class=\"px-4 py-2 text-sm text-slate-400 hover:text-white transition\"\n >\n Mégse\n </button>\n <button\n @click=\"saveForm\"\n :disabled=\"saveLoading || !hasChanges\"\n class=\"px-6 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:bg-indigo-600/50 text-white rounded-lg text-sm font-medium transition flex items-center gap-2\"\n >\n <svg v-if=\"saveLoading\" class=\"w-4 h-4 animate-spin\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\" />\n </svg>\n <svg v-else 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=\"M5 13l4 4L19 7\" />\n </svg>\n Mentés\n </button>\n </div>\n </div>\n </div>\n </template>\n\n <!-- Toast Notification -->\n <div\n v-if=\"toast\"\n class=\"fixed top-4 right-4 z-50 px-4 py-3 rounded-lg shadow-xl text-sm font-medium transition-all duration-300\"\n :class=\"toast.type === 'success' ? 'bg-emerald-600 text-white' : 'bg-rose-600 text-white'\"\n >\n {{ toast.message }}\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\n/**\n * 🏛️ Service Provider Dedicated Edit Page\n * =========================================\n * \n * Architectural Context (P0 - Card #384):\n * ----------------------------------------\n * This page replaces the missing modal-based edit system with a full-page\n * 3-tab editor. It integrates with the existing PATCH /admin/providers/{id}\n * endpoint which supports:\n * - Basic fields: name, category, contact info\n * - 3D Matrix: supported_vehicle_classes, category_ids (expertise tags),\n * specializations (brands + propulsion)\n * - Location: address, city, zip, street, house_number, plus_code\n * \n * Data Flow:\n * ----------\n * 1. On mount: fetch provider detail from GET /admin/providers/{id}\n * 2. On mount: fetch category tree from GET /providers/categories/tree\n * 3. User edits fields across 3 tabs\n * 4. On save: PATCH /admin/providers/{id} with full payload\n * 5. On success: navigate back to /providers/{id}\n * \n * Vehicle Classes (Dimension A):\n * ------------------------------\n * The 8 system vehicle classes matching the backend enum:\n * car, motorcycle, truck, hgv, light_commercial,\n * personal, boat, other\n */\n\nimport { ref, computed, onMounted, reactive } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\n\n// ── Types ──────────────────────────────────────────────\ninterface CategoryTreeNode {\n id: number\n key: string\n name_hu?: string | null\n name_en?: string | null\n level: number\n path?: string | null\n is_official: boolean\n children: CategoryTreeNode[]\n}\n\ninterface ProviderDetail {\n id: number\n name: string\n address: string\n city?: string | null\n address_zip?: string | null\n address_street_name?: string | null\n address_street_type?: string | null\n address_house_number?: string | null\n plus_code?: string | null\n contact_phone?: string | null\n contact_email?: string | null\n website?: string | null\n category?: string | null\n status: string\n source: string\n validation_score: number\n evidence_image_path?: string | null\n added_by_user_id?: number | null\n category_ids?: number[] | null\n supported_vehicle_classes?: string[] | null\n specializations?: Record<string, any> | null\n opening_hours?: Record<string, any> | null\n created_at?: string | null\n}\n\n// ── Constants ──────────────────────────────────────────\n/**\n * The 8 system vehicle classes.\n * These match the backend VehicleClass enum values.\n */\nconst VEHICLE_CLASSES = [\n { key: 'car', label: 'Személyautó' },\n { key: 'motorcycle', label: 'Motorkerékpár' },\n { key: 'truck', label: 'Teherautó' },\n { key: 'hgv', label: 'Nehézgépjármű (HGV)' },\n { key: 'light_commercial', label: 'Kishaszonjármű' },\n { key: 'personal', label: 'Személyes' },\n { key: 'boat', label: 'Csónak/Vízi' },\n { key: 'other', label: 'Egyéb' },\n]\n\n// ── Route ──────────────────────────────────────────────\nconst route = useRoute()\nconst router = useRouter()\nconst providerId = computed(() => Number(route.params.id))\n\n// ── State ──────────────────────────────────────────────\nconst loading = ref(true)\nconst error = ref(false)\nconst provider = ref<ProviderDetail | null>(null)\nconst saveLoading = ref(false)\nconst toast = ref<{ message: string; type: 'success' | 'error' } | null>(null)\n\n// Tab state\nconst activeTab = ref<'basic' | 'matrix' | 'location' | 'hours'>('basic')\nconst tabs = [\n { key: 'basic', label: 'Alapadatok & Kapcsolat', icon: 'svg' },\n { key: 'matrix', label: 'Szolgáltatások & Járművek', icon: 'svg' },\n { key: 'location', label: 'Helyszín & Lokáció', icon: 'svg' },\n { key: 'hours', label: 'Nyitvatartás', icon: 'svg' },\n]\n\n// Category tree state\nconst categoryTree = ref<CategoryTreeNode[]>([])\nconst treeLoading = ref(false)\nconst expandedNodes = ref<Set<number>>(new Set())\n\n// Tag inputs\nconst brandInput = ref('')\nconst propulsionInput = ref('')\n\n/**\n * The reactive form model.\n * Initialized with defaults, then populated from provider data on load.\n * \n * Mapping from backend ProviderUpdateInput schema:\n * - name, city, address_zip, address_street_name, address_street_type,\n * address_house_number, plus_code, contact_phone, contact_email,\n * website, category, category_ids, supported_vehicle_classes, specializations\n */\nconst form = reactive({\n name: '',\n address: '',\n city: '',\n address_zip: '',\n address_street_name: '',\n address_street_type: '',\n address_house_number: '',\n plus_code: '',\n contact_phone: '',\n contact_email: '',\n website: '',\n category: '',\n category_ids: [] as number[],\n supported_vehicle_classes: [] as string[],\n specializations: {\n brands: [] as string[],\n propulsion: [] as string[],\n } as Record<string, string[]>,\n opening_hours: {} as Record<string, { open: string; close: string } | null>,\n is_always_open: false,\n // Nested address object matching AddressIn schema (used by SharedAddressForm)\n address: {\n zip: '',\n city: '',\n street_name: '',\n street_type: '',\n house_number: '',\n stairwell: '',\n floor: '',\n door: '',\n parcel_id: '',\n full_address_text: '',\n latitude: null as number | null,\n longitude: null as number | null,\n },\n})\n\n// ── Computed ───────────────────────────────────────────\n\n/**\n * Deep comparison between original provider data and current form state.\n * Used to enable/disable the save button and show dirty state indicator.\n *\n * CRITICAL BUGFIX (2026-07-01):\n * - Normalize specializations before comparison to avoid {} vs {brands:[],propulsion:[]} mismatch\n * - Use JSON.stringify with sorted arrays for reliable array comparison\n * - All scalar fields compared with || '' fallback for null-safety\n */\nconst hasChanges = computed(() => {\n if (!provider.value) return false\n const p = provider.value\n\n // Scalar field comparisons\n if (form.name !== (p.name || '')) return true\n if (form.address !== (p.address || '')) return true\n if (form.plus_code !== (p.plus_code || '')) return true\n\n // Nested address comparison (via SharedAddressForm)\n if (form.address.zip !== (p.address_zip || '')) return true\n if (form.address.city !== (p.city || '')) return true\n if (form.address.street_name !== (p.address_street_name || '')) return true\n if (form.address.street_type !== (p.address_street_type || '')) return true\n if (form.address.house_number !== (p.address_house_number || '')) return true\n if (form.contact_phone !== (p.contact_phone || '')) return true\n if (form.contact_email !== (p.contact_email || '')) return true\n if (form.website !== (p.website || '')) return true\n if (form.category !== (p.category || '')) return true\n\n // Array comparisons with deep equality (JSON.stringify + sort for order-independence)\n const sortedFormCategories = [...form.category_ids].sort()\n const sortedOrigCategories = [...(p.category_ids || [])].sort()\n if (JSON.stringify(sortedFormCategories) !== JSON.stringify(sortedOrigCategories)) return true\n\n const sortedFormClasses = [...form.supported_vehicle_classes].sort()\n const sortedOrigClasses = [...(p.supported_vehicle_classes || [])].sort()\n if (JSON.stringify(sortedFormClasses) !== JSON.stringify(sortedOrigClasses)) return true\n\n // Specializations comparison: normalize both sides to the same shape\n // to avoid false-positive when API returns {} vs form default {brands:[],propulsion:[]}\n const normalizeSpec = (spec: any) => ({\n brands: Array.isArray(spec?.brands) ? [...spec.brands].sort() : [],\n propulsion: Array.isArray(spec?.propulsion) ? [...spec.propulsion].sort() : [],\n })\n if (JSON.stringify(normalizeSpec(form.specializations)) !== JSON.stringify(normalizeSpec(p.specializations))) return true\n\n // Opening hours comparison: normalize both sides to the same shape\n // Supports both flat format {monday: {...}} and nested {always_open: true, days: {monday: {...}}}\n const DAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']\n const normalizeHours = (hours: any): Record<string, any> => {\n const result: Record<string, any> = {}\n // Detect nested format with always_open metadata\n const daysSource = hours?.days || hours\n for (const day of DAYS) {\n const slot = daysSource?.[day]\n if (slot && typeof slot === 'object' && slot.open && slot.close) {\n result[day] = { open: slot.open, close: slot.close }\n } else {\n result[day] = null\n }\n }\n return result\n }\n if (JSON.stringify(normalizeHours(form.opening_hours)) !== JSON.stringify(normalizeHours(p.opening_hours))) return true\n\n // is_always_open comparison\n if (form.is_always_open !== (p.opening_hours?.always_open === true)) return true\n\n return false\n})\n\n// ── Auth helpers ───────────────────────────────────────\nfunction getHeaders(): Record<string, string> {\n const token = useCookie('access_token').value\n const headers: Record<string, string> = { 'Content-Type': 'application/json' }\n if (token) headers['Authorization'] = `Bearer ${token}`\n return headers\n}\n\n// ── API: Fetch provider ────────────────────────────────\nasync function fetchProvider() {\n loading.value = true\n error.value = false\n try {\n const res = await $fetch(`/api/v1/admin/providers/${providerId.value}`, {\n headers: getHeaders(),\n })\n provider.value = res as ProviderDetail\n mapProviderToForm(res as ProviderDetail)\n } catch (err: any) {\n console.error('Failed to fetch provider:', err)\n error.value = true\n } finally {\n loading.value = false\n }\n}\n\n/**\n * Maps the provider detail response to the reactive form.\n * Handles null/undefined values and ensures arrays/dicts have defaults.\n */\nfunction mapProviderToForm(p: ProviderDetail) {\n form.name = p.name || ''\n form.address = p.address || ''\n form.city = p.city || ''\n form.address_zip = p.address_zip || ''\n form.address_street_name = p.address_street_name || ''\n form.address_street_type = p.address_street_type || ''\n form.address_house_number = p.address_house_number || ''\n form.plus_code = p.plus_code || ''\n\n // Populate nested address object from flat provider fields\n form.address.zip = p.address_zip || ''\n form.address.city = p.city || ''\n form.address.street_name = p.address_street_name || ''\n form.address.street_type = p.address_street_type || ''\n form.address.house_number = p.address_house_number || ''\n form.address.full_address_text = p.address || ''\n form.contact_phone = p.contact_phone || ''\n form.contact_email = p.contact_email || ''\n form.website = p.website || ''\n form.category = p.category || ''\n form.category_ids = p.category_ids || []\n form.supported_vehicle_classes = p.supported_vehicle_classes || []\n\n // Parse specializations - handle both object and string formats\n const spec = p.specializations || {}\n form.specializations = {\n brands: Array.isArray(spec.brands) ? spec.brands : [],\n propulsion: Array.isArray(spec.propulsion) ? spec.propulsion : [],\n }\n\n // Parse opening_hours - support both flat format {monday: {...}, ...}\n // and the new nested format {always_open: true, days: {monday: {...}, ...}}\n const rawHours = p.opening_hours || {}\n const DAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']\n\n // Detect if the backend stored the nested format with always_open metadata\n const isAlwaysOpen = rawHours.always_open === true\n const daysSource = rawHours.days || rawHours\n\n form.is_always_open = isAlwaysOpen\n\n const hours: Record<string, { open: string; close: string } | null> = {}\n for (const day of DAYS) {\n const slot = daysSource[day]\n if (slot && typeof slot === 'object' && slot.open && slot.close) {\n hours[day] = { open: slot.open, close: slot.close }\n } else {\n hours[day] = null\n }\n }\n form.opening_hours = hours\n}\n\n// ── API: Fetch category tree ───────────────────────────\nasync function fetchCategoryTree() {\n treeLoading.value = true\n try {\n const res = await $fetch('/api/v1/providers/categories/tree', {\n headers: getHeaders(),\n })\n categoryTree.value = (res as CategoryTreeNode[]) || []\n } catch (err: any) {\n console.error('Failed to fetch category tree:', err)\n } finally {\n treeLoading.value = false\n }\n}\n\n// ── Category tree helpers ──────────────────────────────\n\n/**\n * Recursively finds a category node by ID in the tree.\n */\nfunction findCategoryById(id: number, nodes: CategoryTreeNode[]): CategoryTreeNode | null {\n for (const node of nodes) {\n if (node.id === id) return node\n if (node.children?.length) {\n const found = findCategoryById(id, node.children)\n if (found) return found\n }\n }\n return null\n}\n\n/**\n * Gets the display name for a category ID (for the selected chips).\n */\nfunction getCategoryName(id: number): string {\n const node = findCategoryById(id, categoryTree.value)\n if (!node) return `#${id}`\n return node.name_hu || node.name_en || node.key\n}\n\n// ── Dimension A: Vehicle Class toggles ─────────────────\n\n/**\n * Toggles a vehicle class in the form.supported_vehicle_classes array.\n * If already selected, removes it; otherwise adds it.\n */\nfunction toggleVehicleClass(key: string) {\n const idx = form.supported_vehicle_classes.indexOf(key)\n if (idx >= 0) {\n form.supported_vehicle_classes.splice(idx, 1)\n } else {\n form.supported_vehicle_classes.push(key)\n }\n}\n\n// ── Dimension B: Category toggles ──────────────────────\n\n/**\n * Toggles a category ID in the form.category_ids array.\n * If already selected, removes it; otherwise adds it.\n */\nfunction toggleCategory(id: number) {\n const idx = form.category_ids.indexOf(id)\n if (idx >= 0) {\n form.category_ids.splice(idx, 1)\n } else {\n form.category_ids.push(id)\n }\n}\n\n/**\n * Toggles the expanded/collapsed state of a tree node.\n */\nfunction toggleTreeNode(id: number) {\n const newSet = new Set(expandedNodes.value)\n if (newSet.has(id)) {\n newSet.delete(id)\n } else {\n newSet.add(id)\n }\n expandedNodes.value = newSet\n}\n\n/**\n * Checks if a category ID is currently selected.\n */\nfunction isCategorySelected(id: number): boolean {\n return form.category_ids.includes(id)\n}\n\n/**\n * Removes a category ID from the selection.\n */\nfunction removeCategory(id: number) {\n const idx = form.category_ids.indexOf(id)\n if (idx >= 0) {\n form.category_ids.splice(idx, 1)\n }\n}\n\n// ── Dimension C: Specialization tag management ─────────\n\n/**\n * Adds the current brandInput value to the brands array.\n */\nfunction addBrand() {\n const val = brandInput.value.trim()\n if (val && !form.specializations.brands.includes(val)) {\n form.specializations.brands.push(val)\n }\n brandInput.value = ''\n}\n\n/**\n * Removes a brand at the given index.\n */\nfunction removeBrand(idx: number) {\n form.specializations.brands.splice(idx, 1)\n}\n\n/**\n * Adds the current propulsionInput value to the propulsion array.\n */\nfunction addPropulsion() {\n const val = propulsionInput.value.trim()\n if (val && !form.specializations.propulsion.includes(val)) {\n form.specializations.propulsion.push(val)\n }\n propulsionInput.value = ''\n}\n\n/**\n * Removes a propulsion entry at the given index.\n */\nfunction removePropulsion(idx: number) {\n form.specializations.propulsion.splice(idx, 1)\n}\n\n// ── DAYS constant ──────────────────────────────────────\nconst DAYS = [\n { key: 'monday', label: 'Hétfő' },\n { key: 'tuesday', label: 'Kedd' },\n { key: 'wednesday', label: 'Szerda' },\n { key: 'thursday', label: 'Csütörtök' },\n { key: 'friday', label: 'Péntek' },\n { key: 'saturday', label: 'Szombat' },\n { key: 'sunday', label: 'Vasárnap' },\n]\n\n// ── Opening Hours helpers ──────────────────────────────\n\n/**\n * Called when the \"Folyamatos (0-24) nyitvatartás\" checkbox is toggled.\n * When checked: sets all 7 days to 00:00-23:59.\n * When unchecked: clears all hours so the user can set custom times.\n */\nfunction onAlwaysOpenChange() {\n if (form.is_always_open) {\n setAllDaysOpen('00:00', '23:59')\n } else {\n clearAllHours()\n }\n}\n\n/**\n * Toggles a day on/off in the opening_hours form.\n * When enabling, defaults to 08:00-17:00.\n */\nfunction toggleDay(dayKey: string) {\n if (form.opening_hours[dayKey]) {\n form.opening_hours[dayKey] = null\n } else {\n form.opening_hours[dayKey] = { open: '08:00', close: '17:00' }\n }\n}\n\n/**\n * Updates the open or close time for a given day.\n */\nfunction updateDayTime(dayKey: string, field: 'open' | 'close', value: string) {\n const slot = form.opening_hours[dayKey]\n if (slot) {\n slot[field] = value\n }\n}\n\n/**\n * Sets all 7 days to the same open/close times (e.g. 00:00-23:59 for 24/7).\n */\nfunction setAllDaysOpen(open: string, close: string) {\n for (const day of DAYS) {\n form.opening_hours[day.key] = { open, close }\n }\n}\n\n/**\n * Copies Monday's opening hours to Tuesday through Friday.\n * If Monday is closed, does nothing.\n */\nfunction copyWeekdays() {\n const monday = form.opening_hours['monday']\n if (!monday) return\n const weekdayKeys = ['tuesday', 'wednesday', 'thursday', 'friday']\n for (const key of weekdayKeys) {\n form.opening_hours[key] = { open: monday.open, close: monday.close }\n }\n}\n\n/**\n * Clears all opening hours (sets all days to null/closed).\n */\nfunction clearAllHours() {\n for (const day of DAYS) {\n form.opening_hours[day.key] = null\n }\n}\n\n// ── Save ───────────────────────────────────────────────\n\n/**\n * Saves the form data via PATCH /admin/providers/{id}.\n * Builds the payload matching the ProviderUpdateInput schema.\n * On success: navigates back to the provider detail page.\n * On error: shows error toast.\n */\nasync function saveForm() {\n if (saveLoading.value) return\n saveLoading.value = true\n\n // CRITICAL BUGFIX (2026-07-01): address is NOT NULL in the DB schema,\n // so we must never send null for it. Fall back to name if empty.\n const safeAddress = form.address?.full_address_text?.trim() || form.name\n\n const payload: Record<string, any> = {\n name: form.name,\n address: safeAddress,\n category: form.category || null,\n contact_phone: form.contact_phone || null,\n contact_email: form.contact_email || null,\n website: form.website || null,\n plus_code: form.plus_code || null,\n category_ids: form.category_ids,\n supported_vehicle_classes: form.supported_vehicle_classes,\n specializations: form.specializations,\n opening_hours: form.opening_hours,\n is_always_open: form.is_always_open,\n // Send nested address object matching AddressIn schema\n address_detail: {\n zip: form.address.zip || null,\n city: form.address.city || null,\n street_name: form.address.street_name || null,\n street_type: form.address.street_type || null,\n house_number: form.address.house_number || null,\n stairwell: form.address.stairwell || null,\n floor: form.address.floor || null,\n door: form.address.door || null,\n parcel_id: form.address.parcel_id || null,\n full_address_text: safeAddress,\n latitude: form.address.latitude,\n longitude: form.address.longitude,\n },\n }\n\n try {\n await $fetch(`/api/v1/admin/providers/${providerId.value}`, {\n method: 'PATCH',\n headers: getHeaders(),\n body: payload,\n })\n showToast('success', 'Szolgáltató sikeresen frissítve!')\n setTimeout(() => {\n router.push(`/providers/${providerId.value}`)\n }, 1200)\n } catch (err: any) {\n console.error('Failed to save provider:', err)\n const msg = err?.data?.detail || err?.message || 'Ismeretlen hiba történt mentés közben.'\n showToast('error', `Hiba: ${msg}`)\n } finally {\n saveLoading.value = false\n }\n}\n\n// ── Navigation ─────────────────────────────────────────\n\n/**\n * Navigates back to the provider detail page.\n */\nfunction goBack() {\n router.push(`/providers/${providerId.value}`)\n}\n\n// ── Toast ──────────────────────────────────────────────\n\n/**\n * Shows a temporary toast notification that auto-hides after 3 seconds.\n */\nfunction showToast(type: 'success' | 'error', message: string) {\n toast.value = { type, message }\n setTimeout(() => {\n toast.value = null\n }, 3000)\n}\n\n// ── Lifecycle ──────────────────────────────────────────\n\nonMounted(async () => {\n await fetchProvider()\n await fetchCategoryTree()\n})\n</script>\n\n<style scoped>\n/* Custom scrollbar for the category tree */\n.overflow-y-auto::-webkit-scrollbar {\n width: 6px;\n}\n.overflow-y-auto::-webkit-scrollbar-track {\n background: transparent;\n}\n.overflow-y-auto::-webkit-scrollbar-thumb {\n background-color: rgba(100, 116, 139, 0.5);\n border-radius: 3px;\n}\n.overflow-y-auto::-webkit-scrollbar-thumb:hover {\n background-color: rgba(100, 116, 139, 0.7);\n}\n</style>"],"version":3}