frontend admin refakctorálás

This commit is contained in:
Roo
2026-07-08 08:03:57 +00:00
parent 07b59032ce
commit 2e0abc62a7
455 changed files with 14428 additions and 21651 deletions

View File

@@ -0,0 +1,285 @@
/**
* 🌍 Global Category Store (Composable)
* =======================================
*
* P0 ARCHITECTURAL REFACTOR (2026-07-07):
* This composable fetches GET /api/v1/categories once and stores them
* in a reactive Map (id -> { name, name_i18n, parent_id }). It provides:
*
* - categoryMap: Map<number, { name: string, name_i18n: object|null, parent_id: number|null }>
* for fast O(1) lookups by ID
* - categoryTree: ref<CategoryTreeNode[]> — the hierarchical tree for UI
* - getCategoryName(id): returns the display name for a category ID
* (resolves from name_i18n using the active locale)
* - getCategoryParentId(id): returns the parent_id for hierarchy automation
* - isLoading: ref<boolean> — loading state
* - error: ref<string | null> — error state
*
* i18n Resolution (Phase 4):
* The backend now returns name_i18n as a JSONB dictionary, e.g.
* { "hu": "Bontó", "en": "Dismantler" }. The getCategoryName() helper
* resolves the active locale via @nuxtjs/i18n (useI18n()) and falls back:
* 1. Active language in name_i18n dict
* 2. Hungarian (hu) in name_i18n dict
* 3. Deprecated name_hu field
* 4. Fallback to #id
*
* Usage:
* const { categoryMap, getCategoryName, isLoading } = useCategories()
* const name = getCategoryName(755) // → "Üzemanyag" (if locale=hu)
*
* Singleton Pattern:
* The composable uses a module-level cache (let cache) so that
* categories are fetched ONLY ONCE across all components.
*/
// ── Module-level cache (singleton) ──────────────────────
let cache = null
/**
* Build a flat Map<id, { name, name_i18n, parent_id }> from the tree.
* Also returns the tree itself.
*/
function flattenTree(nodes, parentId = null) {
const map = new Map()
for (const node of nodes) {
map.set(node.id, {
name: node.name_hu || node.name_en || node.key,
name_i18n: node.name_i18n || null,
parent_id: parentId,
level: node.level,
key: node.key,
})
if (node.children && node.children.length > 0) {
const childMap = flattenTree(node.children, node.id)
for (const [k, v] of childMap) {
map.set(k, v)
}
}
}
return map
}
/**
* Build a hierarchical tree from the flat category list.
* The API returns categories as a flat array with parent_id references.
*/
function buildTree(categories) {
const map = new Map()
const roots = []
// First pass: create nodes
for (const cat of categories) {
map.set(cat.id, {
id: cat.id,
key: cat.key,
name_hu: cat.name_hu || null,
name_en: cat.name_en || null,
name_i18n: cat.name_i18n || null,
level: cat.level || 0,
path: cat.path || null,
is_official: cat.is_official || false,
parent_id: cat.parent_id || null,
children: [],
})
}
// Second pass: link children to parents
for (const cat of categories) {
const node = map.get(cat.id)
if (cat.parent_id && map.has(cat.parent_id)) {
map.get(cat.parent_id).children.push(node)
} else {
roots.push(node)
}
}
return roots
}
/**
* Resolve a category display name 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 #id
*/
function resolveCategoryName(entry, locale, id) {
if (!entry) return `#${id}`
// 1. Try active language in i18n dict
if (entry.name_i18n && entry.name_i18n[locale]) {
return entry.name_i18n[locale]
}
// 2. Fallback to Hungarian in i18n dict
if (entry.name_i18n && entry.name_i18n['hu']) {
return entry.name_i18n['hu']
}
// 3. Fallback to deprecated name_hu (safe transition)
if (entry.name_hu) {
return entry.name_hu
}
// 4. Fallback to ID
return `#${id}`
}
/**
* useCategories — Global Category Store Composable
*
* Fetches categories from the API once and caches them.
* Returns reactive references to the flat map, tree, and helpers.
*/
export function useCategories() {
const { $fetch } = useNuxtApp() || {}
const fetchFn = $fetch || globalThis.$fetch
// ── Reactive State ────────────────────────────────────
const categoryMap = ref(new Map())
const categoryTree = ref([])
const isLoading = ref(false)
const error = ref(null)
const isLoaded = ref(false)
// ── 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'
}
// ── Fetch Categories ──────────────────────────────────
async function fetchCategories() {
// Return cached data if already loaded
if (cache) {
categoryMap.value = cache.map
categoryTree.value = cache.tree
isLoaded.value = true
return
}
isLoading.value = true
error.value = null
try {
// Try the flat list endpoint first
let data = null
try {
data = await fetchFn('/api/v1/categories', {
headers: getAuthHeaders(),
})
} catch (_) {
// Fallback: try the tree endpoint
data = await fetchFn('/api/v1/providers/categories/tree', {
headers: getAuthHeaders(),
})
}
if (!data) {
throw new Error('No data returned from categories API')
}
// Determine if data is flat list or tree
if (Array.isArray(data)) {
if (data.length > 0 && data[0].children !== undefined) {
// It's a tree — flatten it
categoryTree.value = data
categoryMap.value = flattenTree(data)
} else if (data.length > 0 && data[0].parent_id !== undefined) {
// It's a flat list — build tree and map
categoryMap.value = new Map(
data.map(cat => [
cat.id,
{
name: cat.name_hu || cat.name_en || cat.key,
name_i18n: cat.name_i18n || null,
parent_id: cat.parent_id || null,
level: cat.level || 0,
key: cat.key,
},
])
)
categoryTree.value = buildTree(data)
} else {
// Unknown format — treat as tree anyway
categoryTree.value = data
categoryMap.value = flattenTree(data)
}
} else {
throw new Error('Unexpected categories response format')
}
// Cache the result
cache = {
map: categoryMap.value,
tree: categoryTree.value,
}
isLoaded.value = true
} catch (err) {
console.error('[useCategories] Failed to fetch categories:', err)
error.value = err?.message || 'Failed to load categories'
} finally {
isLoading.value = false
}
}
// ── Helper: Get auth headers ──────────────────────────
function getAuthHeaders() {
try {
const token = useCookie('access_token').value
const headers = { 'Content-Type': 'application/json' }
if (token) headers['Authorization'] = `Bearer ${token}`
return headers
} catch {
return { 'Content-Type': 'application/json' }
}
}
// ── Helper: Get category name by ID (i18n-aware) ──────
function getCategoryName(id) {
if (!id && id !== 0) return '—'
const entry = categoryMap.value.get(Number(id))
return resolveCategoryName(entry, currentLocale, id)
}
// ── Helper: Get category parent_id by ID ──────────────
function getCategoryParentId(id) {
if (!id && id !== 0) return null
const entry = categoryMap.value.get(Number(id))
return entry ? entry.parent_id : null
}
// ── Helper: Get full category info by ID ──────────────
function getCategory(id) {
if (!id && id !== 0) return null
return categoryMap.value.get(Number(id)) || null
}
// ── Auto-fetch on first use ───────────────────────────
if (!isLoaded.value && !cache) {
fetchCategories()
}
return {
// State
categoryMap,
categoryTree,
isLoading,
error,
isLoaded,
// Helpers
getCategoryName,
getCategoryParentId,
getCategory,
// Actions
fetchCategories,
}
}