frontend admin refakctorálás
This commit is contained in:
285
frontend_admin/composables/useCategories.js
Normal file
285
frontend_admin/composables/useCategories.js
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user