'use client'; import { create } from 'zustand'; import type { VersionPlan } from '@/lib/version-plan'; import { mergeVersionPlanPatch } from '@/lib/version-plan'; import { completeVersionPlanByVersionId, createVersionPlanByVersionId, deleteVersionPlanByVersionId, listVersionPlansByVersionId, updateVersionPlanByVersionId, } from '@/lib/domain-api'; import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence'; import { SERVER_DATA_CACHE_MS, SERVER_DATA_SAVE_ERROR_EVENT } from '@/lib/server-data'; import { getPlanCompletionState } from '@/lib/version-plan-workflow'; import type { PlanResultPayload } from '@/lib/version-plan-workflow'; import { makeVersionPlanCompletedActivity, makeVersionPlanRequirementProgressActivity, makeVersionPlanResearchDirectionProgressActivity, makeVersionPlanStartedActivity, } from '@/lib/work-activity-factory'; import type { WorkActivityDraft } from '@/lib/work-activity'; import type { WorkActivity } from '@/lib/work-activity'; import { useWorkActivityStore } from './useWorkActivityStore'; const MOCK_PLANS: VersionPlan[] = []; let lastPlansFetchAt = 0; interface VersionPlanState { plans: VersionPlan[]; loaded: boolean; fetchPlans: (options?: { force?: boolean; versionId?: string }) => Promise; createPlan: (data: Omit) => void; updatePlan: (id: string, data: Partial) => void; completePlan: (id: string, result: PlanResultPayload) => { ok: boolean; message?: string }; deletePlan: (id: string) => void; } export const useVersionPlanStore = create((set, get) => ({ plans: MOCK_PLANS, loaded: false, fetchPlans: async (options) => { if (!options?.versionId && !options?.force && get().loaded && Date.now() - lastPlansFetchAt < SERVER_DATA_CACHE_MS) return; const cached = options?.versionId ? await listVersionPlansByVersionId(options.versionId).catch(() => []) : get().plans; if (!options?.versionId && !options?.force && get().loaded && Date.now() - lastPlansFetchAt < SERVER_DATA_CACHE_MS) return; lastPlansFetchAt = Date.now(); set({ plans: options?.versionId ? mergeVersionPlansForVersion(get().plans, options.versionId, cached ?? MOCK_PLANS) : cached ?? MOCK_PLANS, loaded: true, }); }, createPlan: (data) => { const previous = get().plans; const now = new Date().toISOString(); const autoStarted = shouldAutoStartCreatedPlan(data, now); const plan: VersionPlan = { ...data, status: autoStarted ? 'in_progress' : data.status, actualStartAt: autoStarted ? data.actualStartAt ?? now : data.actualStartAt, id: `plan-${Date.now()}`, createdAt: now.slice(0, 10), }; const plans = [...previous, plan]; set({ plans, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { try { const result = await createVersionPlanByVersionId(plan.versionId, plan); set({ plans: get().plans.map((item) => ( item.id === plan.id ? mergeCreatedVersionPlan(plan, item, result.item) : item )), loaded: true, }); appendDomainActivities(result.activities); } catch (error) { notifyVersionPlanSaveFailure(error); throw error; } }, expected: plans, getCurrent: () => get().plans, rollback: () => set({ plans: previous, loaded: true }), }); }, updatePlan: (id, data) => { const previous = get().plans; const now = new Date().toISOString(); const activities: WorkActivityDraft[] = []; let apiPatch: Partial | null = null; const plans = previous.map((p) => { if (p.id !== id) return p; const patch = { ...mergeVersionPlanPatch(p, data) }; if (patch.status === 'in_progress' && !p.actualStartAt) { (patch as any).actualStartAt = now; } if (patch.status === 'completed' && !p.completedAt) { (patch as any).completedAt = now; } const next = { ...p, ...patch }; if (p.status !== 'in_progress' && next.status === 'in_progress') { activities.push(makeVersionPlanStartedActivity(next, next.owner)); } if (p.status !== 'completed' && next.status === 'completed') { activities.push(makeVersionPlanCompletedActivity(next, next.owner)); } const previousLogIds = new Set((p.logs ?? []).map((log) => log.id)); for (const log of next.logs ?? []) { if (previousLogIds.has(log.id)) continue; const requirementActivity = makeVersionPlanRequirementProgressActivity(next, log); if (requirementActivity) activities.push(requirementActivity); const directionActivity = makeVersionPlanResearchDirectionProgressActivity(next, log); if (directionActivity) activities.push(directionActivity); } apiPatch = patch; return next; }); set({ plans, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { try { const versionId = previous.find((plan) => plan.id === id)?.versionId; if (!versionId) throw new Error('missing versionId'); const result = await updateVersionPlanByVersionId(versionId, id, apiPatch ?? data); appendDomainActivities(result.activities); if (result.activities.length === 0) { activities.forEach((activity) => useWorkActivityStore.getState().addActivity(activity)); } } catch (error) { notifyVersionPlanSaveFailure(error); throw error; } }, expected: plans, getCurrent: () => get().plans, rollback: () => set({ plans: previous, loaded: true }), }); }, completePlan: (id, result) => { let response: { ok: boolean; message?: string } = { ok: false, message: '计划不存在' }; const previous = get().plans; const plans = previous.map((p) => { if (p.id !== id) return p; const next = { ...p, ...result, status: 'completed' as const, completedAt: new Date().toISOString() }; const state = getPlanCompletionState(next); if (!state.canComplete) { response = { ok: false, message: state.missingReasons.join('、') || '计划未满足完成条件' }; return p; } response = { ok: true }; useWorkActivityStore.getState().addActivity(makeVersionPlanCompletedActivity(next, next.owner)); return next; }); set({ plans, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { try { const versionId = previous.find((plan) => plan.id === id)?.versionId; if (!versionId) throw new Error('missing versionId'); const apiResult = await completeVersionPlanByVersionId(versionId, id, result); appendDomainActivities(apiResult.activities); } catch (error) { notifyVersionPlanSaveFailure(error); throw error; } }, expected: plans, getCurrent: () => get().plans, rollback: () => set({ plans: previous, loaded: true }), }); return response; }, deletePlan: (id) => { const previous = get().plans; const plans = previous.filter((p) => p.id !== id); set({ plans, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { try { const versionId = previous.find((plan) => plan.id === id)?.versionId; if (!versionId) throw new Error('missing versionId'); await deleteVersionPlanByVersionId(versionId, id); } catch (error) { notifyVersionPlanSaveFailure(error); throw error; } }, expected: plans, getCurrent: () => get().plans, rollback: () => set({ plans: previous, loaded: true }), }); }, })); function appendDomainActivities(activities: WorkActivity[]) { if (activities.length === 0) return; useWorkActivityStore.setState((state) => ({ activities: [...state.activities, ...activities], loaded: true, })); } function mergeVersionPlansForVersion(current: VersionPlan[], versionId: string, scopedPlans: VersionPlan[]) { return [ ...current.filter((plan) => plan.versionId !== versionId), ...scopedPlans, ]; } function shouldAutoStartCreatedPlan(data: Omit, nowIso: string): boolean { if (data.status !== 'pending' || !data.startTime || data.actualStartAt) return false; return new Date(data.startTime).getTime() <= new Date(nowIso).getTime(); } function mergeCreatedVersionPlan(initial: VersionPlan, current: VersionPlan, persisted: VersionPlan): VersionPlan { const localChanged = current.status !== initial.status || current.actualStartAt !== initial.actualStartAt || current.completedAt !== initial.completedAt; return { ...current, ...persisted, status: localChanged ? current.status : persisted.status, actualStartAt: current.actualStartAt ?? persisted.actualStartAt, completedAt: current.completedAt ?? persisted.completedAt, tasks: current.tasks ?? initial.tasks ?? persisted.tasks, }; } function notifyVersionPlanSaveFailure(error: unknown) { if (typeof window === 'undefined') return; window.dispatchEvent( new CustomEvent(SERVER_DATA_SAVE_ERROR_EVENT, { detail: { key: 'version-plans', message: error instanceof Error && error.message ? error.message : '版本计划保存失败,请检查 API 服务', }, }), ); }