admin_szolgáltatók_

This commit is contained in:
Roo
2026-07-01 02:27:38 +00:00
parent 189cbfd7ca
commit 6e627d0ebe
491 changed files with 20965 additions and 9703 deletions

View File

@@ -0,0 +1,145 @@
<template>
<div class="relative" ref="dropdownRef">
<!-- Trigger Button -->
<button
class="flex items-center gap-2 px-3 py-1.5 text-xs font-medium rounded-lg transition bg-slate-700/50 hover:bg-slate-700 text-slate-300 hover:text-white border border-slate-600/50"
@click="dropdownOpen = !dropdownOpen"
>
<span class="text-base leading-none">{{ flagFor(currentLocale) }}</span>
<span class="hidden sm:inline">{{ labelFor(currentLocale) }}</span>
<svg
class="w-3.5 h-3.5 text-slate-400 transition-transform duration-200"
:class="{ 'rotate-180': dropdownOpen }"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<!-- Dropdown Menu -->
<div
v-if="dropdownOpen"
class="absolute right-0 mt-2 w-44 bg-slate-800 border border-slate-700 rounded-xl shadow-xl py-1 z-50"
>
<button
v-for="loc in availableLocales"
:key="loc.code"
class="flex items-center gap-3 w-full px-4 py-2.5 text-sm transition"
:class="
loc.code === currentLocale
? 'bg-indigo-600/20 text-indigo-300'
: 'text-slate-300 hover:bg-slate-700 hover:text-white'
"
@click="selectLocale(loc.code)"
>
<span class="text-lg leading-none">{{ flagFor(loc.code) }}</span>
<span>{{ loc.name }}</span>
<span v-if="loc.code === currentLocale" class="ml-auto text-indigo-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</span>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { locale, locales } = useI18n()
const dropdownOpen = ref(false)
const dropdownRef = ref<HTMLElement | null>(null)
// ── Locale helpers ──────────────────────────────────────────────
interface LocaleEntry {
code: string
name: string
[key: string]: unknown
}
const availableLocales = computed(() => locales.value as LocaleEntry[])
const currentLocale = computed(() => locale.value)
/** Map of language codes to flag emojis */
const flagMap: Record<string, string> = {
hu: '🇭🇺',
en: '🇬🇧',
}
/** Map of language codes to display labels (short) */
const labelMap: Record<string, string> = {
hu: 'Magyar',
en: 'English',
}
function flagFor(code: string): string {
return flagMap[code] || '🌐'
}
function labelFor(code: string): string {
return labelMap[code] || code.toUpperCase()
}
// ── Locale switching ────────────────────────────────────────────
function getAuthHeaders() {
const tokenCookie = useCookie('access_token')
const token = tokenCookie.value
return token ? { Authorization: `Bearer ${token}` } : {}
}
async function selectLocale(code: string) {
if (code === currentLocale.value) {
dropdownOpen.value = false
return
}
// 1. Update vue-i18n locale immediately
locale.value = code
// 2. Persist to localStorage
try {
localStorage.setItem('preferred_language', code)
} catch {
// localStorage may be unavailable
}
// 3. Persist to backend
try {
await $fetch('/api/v1/auth/me/language', {
method: 'PATCH',
headers: {
...getAuthHeaders(),
'Content-Type': 'application/json',
},
body: { preferred_language: code },
})
} catch (err) {
console.error('[LanguageSwitcher] Failed to persist language preference:', err)
}
dropdownOpen.value = false
}
// ── Close on outside click ──────────────────────────────────────
function handleClickOutside(e: MouseEvent) {
if (dropdownRef.value && !dropdownRef.value.contains(e.target as Node)) {
dropdownOpen.value = false
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>

View File

@@ -0,0 +1,67 @@
<template>
<div>
<div class="flex items-center gap-2 py-1">
<button
@click="$emit('toggle-expand', node.id)"
class="text-slate-500 hover:text-slate-300 transition w-4 h-4 flex items-center justify-center"
>
<svg
class="w-3 h-3 transition-transform"
:class="expandedIds.has(node.id) ? 'rotate-90' : ''"
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
<label class="flex items-center gap-2 cursor-pointer flex-1">
<input
type="checkbox"
:checked="selectedIds.includes(node.id)"
@change="$emit('toggle', node.id)"
class="w-4 h-4 rounded border-slate-500 bg-slate-700 text-indigo-500 focus:ring-indigo-500/50"
/>
<span class="text-sm text-slate-300">{{ node.name_hu || node.name_en || node.key }}</span>
<span class="text-xs text-slate-500">(Szint {{ node.level }})</span>
</label>
</div>
<div v-if="expandedIds.has(node.id) && node.children?.length" class="ml-6 border-l border-slate-700 pl-3">
<div v-for="child in node.children" :key="child.id">
<TreeNode
:node="child"
:selected-ids="selectedIds"
:expanded-ids="expandedIds"
@toggle="(id: number) => $emit('toggle', id)"
@toggle-expand="(id: number) => $emit('toggle-expand', id)"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
/**
* Recursive TreeNode component for the category tree.
* Renders a checkbox tree with expand/collapse for nested categories.
*/
interface CategoryTreeNode {
id: number
key: string
name_hu?: string | null
name_en?: string | null
level: number
path?: string | null
is_official: boolean
children: CategoryTreeNode[]
}
defineProps<{
node: CategoryTreeNode
selectedIds: number[]
expandedIds: Set<number>
}>()
defineEmits<{
toggle: [id: number]
'toggle-expand': [id: number]
}>()
</script>