'use client'; import { create } from 'zustand'; import type { TaskCategory, CategoryGroup } from '@/lib/task-category'; import { PRESET_CATEGORIES, normalizeTaskCategories, resolveAiTaskCategoryByName } from '@/lib/task-category'; import { createTaskCategoryDomain, deleteTaskCategoryDomain, listTaskCategoriesDomain, updateTaskCategoryDomain, } from '@/lib/domain-api'; import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence'; import { SERVER_DATA_CACHE_MS } from '@/lib/server-data'; let lastCategoriesFetchAt = 0; interface TaskCategoryState { categories: TaskCategory[]; loaded: boolean; fetchCategories: (options?: { force?: boolean }) => Promise; addCategory: (name: string, group: CategoryGroup, color?: string) => void; ensureCategory: (name: string, group: CategoryGroup, options?: { allowCreate?: boolean; fallbackCode?: string }) => TaskCategory; updateCategory: (id: string, data: Partial) => void; deleteCategory: (id: string) => boolean; } export const useTaskCategoryStore = create((set, get) => ({ categories: PRESET_CATEGORIES, loaded: false, fetchCategories: async (options) => { if (!options?.force && get().loaded && Date.now() - lastCategoriesFetchAt < SERVER_DATA_CACHE_MS) return; const cached = await listTaskCategoriesDomain().catch(() => null); if (!options?.force && get().loaded && Date.now() - lastCategoriesFetchAt < SERVER_DATA_CACHE_MS) return; lastCategoriesFetchAt = Date.now(); set({ categories: cached ? normalizeTaskCategories(cached) : PRESET_CATEGORIES, loaded: true }); }, addCategory: (name, group, color) => { const list = get().categories; const item: TaskCategory = { id: `cat-${Date.now()}`, code: name.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '_').replace(/^_+|_+$/g, '') || `cat_${Date.now()}`, name, group, color, sortOrder: list.length + 1, isSystem: false, }; const updated = [...list, item]; set({ categories: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { const created = await createTaskCategoryDomain(item); set({ categories: get().categories.map((category) => (category.id === item.id ? { ...item, ...created } : category)), loaded: true, }); }, expected: updated, getCurrent: () => get().categories, rollback: () => set({ categories: list, loaded: true }), }); }, ensureCategory: (name, group, options) => { const previous = get().categories; const result = resolveAiTaskCategoryByName(get().categories, name, group, options); if (result.created) { set({ categories: result.categories, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { await createTaskCategoryDomain(result.category); }, expected: result.categories, getCurrent: () => get().categories, rollback: () => set({ categories: previous, loaded: true }), }); } return result.category; }, updateCategory: (id, data) => { const previous = get().categories; const updated = previous.map((c) => (c.id === id ? { ...c, ...data } : c)); set({ categories: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { await updateTaskCategoryDomain(id, data); }, expected: updated, getCurrent: () => get().categories, rollback: () => set({ categories: previous, loaded: true }), }); }, deleteCategory: (id) => { const target = get().categories.find((c) => c.id === id); if (!target || target.isSystem) return false; const previous = get().categories; const updated = previous.filter((c) => c.id !== id); set({ categories: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { await deleteTaskCategoryDomain(id); }, expected: updated, getCurrent: () => get().categories, rollback: () => set({ categories: previous, loaded: true }), }); return true; }, }));