'use client'; import { create } from 'zustand'; import type { VersionPlan, PlanType } from '@/lib/version-plan'; import { completeVersionPlanByVersionId, createVersionPlanByVersionId, deleteVersionPlanByVersionId, listVersionPlansByVersionId, updateVersionPlanByVersionId, } from '@/lib/domain-api'; import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence'; import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data'; import { getPlanCompletionState } from '@/lib/version-plan-workflow'; import type { PlanResultPayload } from '@/lib/version-plan-workflow'; import { makeVersionPlanCompletedActivity, makeVersionPlanCreatedActivity, 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; async function loadStored(): Promise { try { return await loadServerData('version-plans'); } catch {} return null; } 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?.force && get().loaded && Date.now() - lastPlansFetchAt < SERVER_DATA_CACHE_MS) return; const cached = options?.versionId ? await listVersionPlansByVersionId(options.versionId).catch(loadStored) : await loadStored(); if (!options?.force && get().loaded && Date.now() - lastPlansFetchAt < SERVER_DATA_CACHE_MS) return; lastPlansFetchAt = Date.now(); set({ plans: cached ?? MOCK_PLANS, loaded: true }); }, createPlan: (data) => { const previous = get().plans; const plan: VersionPlan = { ...data, id: `plan-${Date.now()}`, createdAt: new Date().toISOString().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 ? { ...plan, ...result.item, tasks: plan.tasks } : item )), loaded: true, }); appendDomainActivities(result.activities); } catch { await saveVersionPlansFallback(plans); useWorkActivityStore.getState().addActivity(makeVersionPlanCreatedActivity(plan, plan.addedBy || plan.owner)); } }, 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[] = []; const plans = previous.map((p) => { if (p.id !== id) return p; const patch = { ...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); } 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, data); appendDomainActivities(result.activities); if (result.activities.length === 0) { activities.forEach((activity) => useWorkActivityStore.getState().addActivity(activity)); } } catch { await saveVersionPlansFallback(plans); activities.forEach((activity) => useWorkActivityStore.getState().addActivity(activity)); } }, 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 { await saveVersionPlansFallback(plans); } }, 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 { await saveVersionPlansFallback(plans); } }, expected: plans, getCurrent: () => get().plans, rollback: () => set({ plans: previous, loaded: true }), }); }, })); async function saveVersionPlansFallback(plans: VersionPlan[]) { await saveServerData('version-plans', plans); } function appendDomainActivities(activities: WorkActivity[]) { if (activities.length === 0) return; useWorkActivityStore.setState((state) => ({ activities: [...state.activities, ...activities], loaded: true, })); }