import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { article, category } from '../api'; import { catchBackendError, resolveBackendError } from '../utils/backendError'; import { logout } from './authSlice'; import { loadCurrentCompany } from './companySlice'; export const ROOT_CATEGORY_NAME = 'Home'; export function normalizeCategoryNode(item) { return { ...item, id: item.id, parentId: item.parentId ?? 0, name: item.name?.trim() || null, }; } /** Backend: { tree: { tree: [...], articles: [] } } */ export function parseCategoryTreeResponse(data, companyRootCategoryId = null) { const payload = data?.tree ?? data; const rawNodes = payload?.tree ?? payload?.categories ?? data?.categories ?? []; const list = Array.isArray(rawNodes) ? rawNodes : Object.values(rawNodes); const categories = Object.fromEntries( list.map((item) => [String(item.id), normalizeCategoryNode(item)]), ); const rootCategoryId = data?.rootCategoryId ?? companyRootCategoryId ?? null; const rootArticles = Array.isArray(payload?.articles) ? payload.articles : []; return { rootCategoryId, categories, rootArticles }; } export function categoryDisplayName(name, categoryId, rootCategoryId) { if (rootCategoryId != null && String(categoryId) === String(rootCategoryId)) { return ROOT_CATEGORY_NAME; } return name?.trim() || ''; } export function normalizeCategoryMap(data) { return parseCategoryTreeResponse(data).categories; } export function getCategoryChildren(categories, parentId) { if (!categories) return []; const parentKey = String(parentId); return Object.values(categories) .filter((item) => String(item.parentId ?? 0) === parentKey) .sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0) || a.id - b.id); } export const loadCategoryTree = createAsyncThunk( 'category/loadTree', async (companyId, { rejectWithValue, getState }) => { try { const data = await category.getTree({ companyId }); const fallbackRootId = getState().company.current?.rootCategoryId ?? null; const { rootCategoryId, categories, rootArticles } = parseCategoryTreeResponse( data, fallbackRootId, ); return { companyId, rootCategoryId, categories, rootArticles, }; } catch (err) { return catchBackendError(rejectWithValue)(err); } }, ); export const loadCategoryArticles = createAsyncThunk( 'category/loadArticles', async ({ categoryId, offset = 0, maxCount = 200 } = {}, { rejectWithValue }) => { try { const data = await article.byCategory({ categoryId, offset, maxCount }); return { categoryId, recordset: data.recordset ?? [], length: data.length ?? data.recordset?.length ?? 0, }; } catch (err) { return catchBackendError(rejectWithValue)(err); } }, ); const initialState = { companyId: null, categories: null, rootCategoryId: null, rootArticles: [], currentCategoryId: null, articles: [], articlesTotal: 0, treeStatus: 'idle', articlesStatus: 'idle', error: null, }; const categorySlice = createSlice({ name: 'category', initialState, reducers: { navigateToCategory(state, action) { state.currentCategoryId = action.payload; state.articles = []; state.articlesTotal = 0; state.articlesStatus = 'idle'; state.error = null; }, navigateBack(state) { const current = state.categories?.[String(state.currentCategoryId)]; const parentId = current?.parentId; if (parentId == null || parentId === 0) return; if (String(parentId) === String(state.rootCategoryId) || state.categories?.[String(parentId)]) { state.currentCategoryId = parentId; state.error = null; if (String(parentId) === String(state.rootCategoryId)) { state.articles = state.rootArticles; state.articlesTotal = state.rootArticles.length; state.articlesStatus = 'succeeded'; } else { state.articles = []; state.articlesTotal = 0; state.articlesStatus = 'idle'; } } }, resetCategoryState() { return initialState; }, }, extraReducers: (builder) => { builder .addCase(loadCategoryTree.pending, (state) => { state.treeStatus = 'loading'; state.error = null; }) .addCase(loadCategoryTree.fulfilled, (state, action) => { state.treeStatus = 'succeeded'; state.companyId = action.payload.companyId; state.categories = action.payload.categories; state.rootCategoryId = action.payload.rootCategoryId; state.currentCategoryId = action.payload.rootCategoryId; state.rootArticles = action.payload.rootArticles ?? []; state.articles = state.rootArticles; state.articlesTotal = state.rootArticles.length; state.articlesStatus = 'succeeded'; }) .addCase(loadCategoryTree.rejected, (state, action) => { state.treeStatus = 'failed'; state.error = resolveBackendError(action); }) .addCase(loadCategoryArticles.pending, (state) => { state.articlesStatus = 'loading'; state.error = null; }) .addCase(loadCategoryArticles.fulfilled, (state, action) => { state.articlesStatus = 'succeeded'; state.articles = action.payload.recordset; state.articlesTotal = action.payload.length; }) .addCase(loadCategoryArticles.rejected, (state, action) => { state.articlesStatus = 'failed'; state.articles = []; state.articlesTotal = 0; state.error = resolveBackendError(action); }) .addCase(loadCurrentCompany.fulfilled, (state, action) => { const nextCompanyId = action.payload?.id ?? null; if (nextCompanyId != null && nextCompanyId !== state.companyId) { return { ...initialState, companyId: nextCompanyId, }; } }) .addCase(logout.fulfilled, () => initialState); }, }); export const { navigateToCategory, navigateBack, resetCategoryState, } = categorySlice.actions; export const selectCategoryMap = (state) => state.category.categories; export const selectCurrentCategoryId = (state) => state.category.currentCategoryId; export const selectCategoryRootId = (state) => state.category.rootCategoryId; export const selectCategoryTreeStatus = (state) => state.category.treeStatus; export const selectCategoryArticles = (state) => state.category.articles; export const selectCategoryArticlesStatus = (state) => state.category.articlesStatus; export const selectCategoryError = (state) => state.category.error; export const selectCurrentCategory = (state) => { const id = state.category.currentCategoryId; if (id == null) return null; return state.category.categories?.[String(id)] ?? null; }; export const selectChildCategories = (state) => getCategoryChildren(state.category.categories, state.category.currentCategoryId); export const selectCanNavigateBack = (state) => { const { currentCategoryId, rootCategoryId, categories } = state.category; if (currentCategoryId == null || rootCategoryId == null) return false; if (String(currentCategoryId) === String(rootCategoryId)) return false; const parentId = categories?.[String(currentCategoryId)]?.parentId; return parentId != null && parentId !== 0; }; export default categorySlice.reducer;