'use client'; import { create } from 'zustand'; import type { DevTask, DevTaskStatus } from '@/lib/dev-task'; import { generateTaskNo, isLegacyTask } from '@/lib/dev-task'; import { applyDevTaskTransition, normalizeDevTaskOnCreate } from '@/lib/dev-task-workflow'; import { createDevTaskByVersionId, deleteDevTaskByVersionId, listDevTasksByVersionId, setDevTaskBlockedByVersionId, updateDevTaskByVersionId, updateDevTaskStatusByVersionId, } from '@/lib/domain-api'; import { createEntityId, dedupeEntityIds } from '@/lib/entity-id'; import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence'; import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data'; import type { WorkActivity } from '@/lib/work-activity'; import { makeDevTaskBlockedActivity, makeDevTaskCreatedActivity, makeDevTaskStatusActivity, makeDevTaskUnblockedActivity, } from '@/lib/work-activity-factory'; import { useWorkActivityStore } from './useWorkActivityStore'; function saveStored(items: DevTask[]) { saveServerData('dev-tasks', items).catch(() => {}); } let lastTasksFetchAt = 0; async function loadStored(): Promise { try { const parsed = await loadServerData('dev-tasks'); if (parsed) { if (Array.isArray(parsed) && parsed.some(isLegacyTask)) { return null; } const result = dedupeEntityIds(parsed, 'task'); if (result.changed) saveStored(result.items); return result.items; } } catch {} return null; } interface DevTaskState { tasks: DevTask[]; loaded: boolean; fetchTasks: (options?: { force?: boolean; versionId?: string }) => Promise; createTask: (data: Omit) => DevTask; updateTask: (id: string, data: Partial) => void; deleteTask: (id: string) => void; changeStatus: (id: string, to: DevTaskStatus, opts?: { delayReason?: string }) => { ok: boolean; message?: string }; setBlocked: (id: string, blocked: boolean, reason?: string, blockedById?: string) => void; getByRequirement: (requirementId: string) => DevTask[]; getByVersionViaRequirements: (requirementIds: string[]) => DevTask[]; getByAssignee: (assigneeId: string) => DevTask[]; } export const useDevTaskStore = create((set, get) => ({ tasks: [], loaded: false, fetchTasks: async (options) => { if (!options?.force && get().loaded && Date.now() - lastTasksFetchAt < SERVER_DATA_CACHE_MS) return; const cached = options?.versionId ? await listDevTasksByVersionId(options.versionId).catch(loadStored) : await loadStored(); if (!options?.force && get().loaded && Date.now() - lastTasksFetchAt < SERVER_DATA_CACHE_MS) return; lastTasksFetchAt = Date.now(); set({ tasks: cached ?? [], loaded: true }); }, createTask: (data) => { const list = get().tasks; const now = new Date().toISOString(); const task: DevTask = normalizeDevTaskOnCreate({ ...data, id: createEntityId('task'), taskNo: generateTaskNo(list), isBlocked: false, createdAt: now, updatedAt: now, } as DevTask); const updated = [...list, task]; set({ tasks: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { try { if (!task.versionId) throw new Error('missing versionId'); const result = await createDevTaskByVersionId(task.versionId, task); set({ tasks: get().tasks.map((item) => (item.id === task.id ? { ...task, ...result.item } : item)), loaded: true, }); appendDomainActivities(result.activities); } catch { await saveDevTasksFallback(updated); useWorkActivityStore.getState().addActivity(makeDevTaskCreatedActivity(task, task.createdBy || task.assigneeId)); } }, expected: updated, getCurrent: () => get().tasks, rollback: () => set({ tasks: list, loaded: true }), }); return task; }, updateTask: (id, data) => { const previous = get().tasks; const updated = previous.map((t) => t.id === id ? { ...t, ...data, aiDraft: false, updatedAt: new Date().toISOString() } : t, ); set({ tasks: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { try { const versionId = previous.find((task) => task.id === id)?.versionId; if (!versionId) throw new Error('missing versionId'); const result = await updateDevTaskByVersionId(versionId, id, data); appendDomainActivities(result.activities); } catch { await saveDevTasksFallback(updated); } }, expected: updated, getCurrent: () => get().tasks, rollback: () => set({ tasks: previous, loaded: true }), }); }, deleteTask: (id) => { const previous = get().tasks; const updated = previous.filter((t) => t.id !== id); set({ tasks: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { try { const versionId = previous.find((task) => task.id === id)?.versionId; if (!versionId) throw new Error('missing versionId'); await deleteDevTaskByVersionId(versionId, id); } catch { await saveDevTasksFallback(updated); } }, expected: updated, getCurrent: () => get().tasks, rollback: () => set({ tasks: previous, loaded: true }), }); }, changeStatus: (id, to, opts) => { const task = get().tasks.find((t) => t.id === id); if (!task) return { ok: false, message: '任务不存在' }; const result = applyDevTaskTransition(task, to, { now: new Date(), delayReason: opts?.delayReason, }); if (!result.ok || !result.patch) return { ok: false, message: result.message }; const previous = get().tasks; const updated = previous.map((t) => t.id === id ? { ...t, ...result.patch, aiDraft: false, updatedAt: new Date().toISOString() } : t, ); set({ tasks: updated, loaded: true }); const activity = makeDevTaskStatusActivity(task, task.status, to, task.assigneeId); scheduleSaveWithOptimisticRollback({ save: async () => { try { if (!task.versionId) throw new Error('missing versionId'); const result = await updateDevTaskStatusByVersionId(task.versionId, id, to); appendDomainActivities(result.activities); if (result.activities.length === 0 && activity) useWorkActivityStore.getState().addActivity(activity); } catch { await saveDevTasksFallback(updated); if (activity) useWorkActivityStore.getState().addActivity(activity); } }, expected: updated, getCurrent: () => get().tasks, rollback: () => set({ tasks: previous, loaded: true }), }); return { ok: true }; }, setBlocked: (id, blocked, reason, blockedById) => { const task = get().tasks.find((t) => t.id === id); if (!task) return; const previous = get().tasks; const patch = { isBlocked: blocked, blockReason: blocked ? reason : undefined, blockedById: blocked ? blockedById : undefined, }; const updated = previous.map((item) => item.id === id ? { ...item, ...patch, aiDraft: false, updatedAt: new Date().toISOString() } : item, ); set({ tasks: updated, loaded: true }); const activity = blocked ? makeDevTaskBlockedActivity(task, task.assigneeId, reason, blockedById) : makeDevTaskUnblockedActivity(task, task.assigneeId); scheduleSaveWithOptimisticRollback({ save: async () => { try { if (!task.versionId) throw new Error('missing versionId'); const result = await setDevTaskBlockedByVersionId(task.versionId, id, blocked, reason); appendDomainActivities(result.activities); if (result.activities.length === 0) useWorkActivityStore.getState().addActivity(activity); } catch { await saveDevTasksFallback(updated); useWorkActivityStore.getState().addActivity(activity); } }, expected: updated, getCurrent: () => get().tasks, rollback: () => set({ tasks: previous, loaded: true }), }); }, getByRequirement: (requirementId) => { return get().tasks.filter((t) => t.requirementId === requirementId); }, getByVersionViaRequirements: (requirementIds) => { const idSet = new Set(requirementIds); return get().tasks.filter((t) => idSet.has(t.requirementId)); }, getByAssignee: (assigneeId) => { return get().tasks.filter((t) => t.assigneeId === assigneeId); }, })); async function saveDevTasksFallback(tasks: DevTask[]) { await saveServerData('dev-tasks', tasks); } function appendDomainActivities(activities: WorkActivity[]) { if (activities.length === 0) return; useWorkActivityStore.setState((state) => ({ activities: [...state.activities, ...activities], loaded: true, })); }