1 line
78 KiB
JSON
1 line
78 KiB
JSON
{"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} |