114 lines
3.3 KiB
Vue
114 lines
3.3 KiB
Vue
<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">{{ resolveNodeLabel(node) }}</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.
|
|
*
|
|
* i18n Resolution (Phase 4):
|
|
* Uses name_i18n JSONB dictionary with fallback chain:
|
|
* 1. Active language in name_i18n dict
|
|
* 2. Hungarian (hu) in name_i18n dict
|
|
* 3. Deprecated name_hu field
|
|
* 4. Fallback to node.key
|
|
*/
|
|
interface CategoryTreeNode {
|
|
id: number
|
|
key: string
|
|
name_hu?: string | null
|
|
name_en?: string | null
|
|
name_i18n?: Record<string, string> | null
|
|
level: number
|
|
path?: string | null
|
|
is_official: boolean
|
|
children: CategoryTreeNode[]
|
|
}
|
|
|
|
const props = defineProps<{
|
|
node: CategoryTreeNode
|
|
selectedIds: number[]
|
|
expandedIds: Set<number>
|
|
}>()
|
|
|
|
defineEmits<{
|
|
toggle: [id: number]
|
|
'toggle-expand': [id: number]
|
|
}>()
|
|
|
|
// ── i18n locale (dynamic) ─────────────────────────────
|
|
let currentLocale = 'hu'
|
|
try {
|
|
const i18n = useI18n()
|
|
currentLocale = i18n.locale?.value || i18n.locale || 'hu'
|
|
} catch {
|
|
// useI18n() may throw outside of setup context; fallback to 'hu'
|
|
}
|
|
|
|
/**
|
|
* Resolve a tree node's display label using i18n JSONB dictionary.
|
|
*
|
|
* Fallback chain:
|
|
* 1. Active language in name_i18n dict
|
|
* 2. Hungarian (hu) in name_i18n dict
|
|
* 3. Deprecated name_hu field
|
|
* 4. Fallback to node.key
|
|
*/
|
|
function resolveNodeLabel(node: CategoryTreeNode): string {
|
|
// 1. Try active language in i18n dict
|
|
if (node.name_i18n?.[currentLocale]) {
|
|
return node.name_i18n[currentLocale]
|
|
}
|
|
|
|
// 2. Fallback to Hungarian in i18n dict
|
|
if (node.name_i18n?.hu) {
|
|
return node.name_i18n.hu
|
|
}
|
|
|
|
// 3. Fallback to deprecated name_hu (safe transition)
|
|
if (node.name_hu) {
|
|
return node.name_hu
|
|
}
|
|
|
|
// 4. Fallback to key
|
|
return node.key
|
|
}
|
|
</script>
|