286 lines
8.5 KiB
JavaScript
286 lines
8.5 KiB
JavaScript
/**
|
|
* 🌍 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,
|
|
}
|
|
}
|