'use client'; import { create } from 'zustand'; import type { DevTask, DevTaskStatus } from '@/lib/dev-task'; import { canTransition, generateTaskNo, isLegacyTask } from '@/lib/dev-task'; import { findTasksToAutoStart, deriveActualStartAt } from '@/lib/dev-task-transitions'; const STORAGE_KEY = 'ftb_dev_tasks_v2'; const LEGACY_KEY = 'ftb_dev_tasks_v1'; function saveLocal(items: DevTask[]) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); } catch {} } function loadLocal(): DevTask[] | null { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw); if (Array.isArray(parsed) && parsed.some(isLegacyTask)) { localStorage.removeItem(STORAGE_KEY); return null; } return parsed; } if (localStorage.getItem(LEGACY_KEY)) { localStorage.removeItem(LEGACY_KEY); } } catch {} return null; } interface DevTaskState { tasks: DevTask[]; fetchTasks: () => void; 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: [], fetchTasks: () => { const cached = loadLocal(); const list = cached ?? []; const now = new Date(); const autoStarts = findTasksToAutoStart(list, now); if (autoStarts.length > 0) { const map = new Map(autoStarts.map((a) => [a.taskId, a.actualStartAt])); const updated = list.map((t) => map.has(t.id) ? { ...t, status: 'in_progress' as DevTaskStatus, actualStartAt: map.get(t.id)!, updatedAt: now.toISOString() } : t, ); set({ tasks: updated }); saveLocal(updated); } else { set({ tasks: list }); } }, createTask: (data) => { const list = get().tasks; const now = new Date().toISOString(); const task: DevTask = { ...data, id: `task-${Date.now()}`, taskNo: generateTaskNo(list), isBlocked: false, createdAt: now, updatedAt: now, }; const updated = [...list, task]; set({ tasks: updated }); saveLocal(updated); return task; }, updateTask: (id, data) => { const updated = get().tasks.map((t) => t.id === id ? { ...t, ...data, updatedAt: new Date().toISOString() } : t, ); set({ tasks: updated }); saveLocal(updated); }, deleteTask: (id) => { const updated = get().tasks.filter((t) => t.id !== id); set({ tasks: updated }); saveLocal(updated); }, changeStatus: (id, to, opts) => { const task = get().tasks.find((t) => t.id === id); if (!task) return { ok: false, message: '任务不存在' }; if (!canTransition(task.status, to)) { return { ok: false, message: `不允许从「${task.status}」流转到「${to}」` }; } const now = new Date(); const patch: Partial = { status: to }; if (to === 'in_progress' && !task.actualStartAt) { patch.actualStartAt = deriveActualStartAt(task, 'manual', now); if (opts?.delayReason) patch.delayReason = opts.delayReason; } if (to === 'submitted') { patch.actualEndAt = now.toISOString(); } get().updateTask(id, patch); return { ok: true }; }, setBlocked: (id, blocked, reason, blockedById) => { get().updateTask(id, { isBlocked: blocked, blockReason: blocked ? reason : undefined, blockedById: blocked ? blockedById : undefined, }); }, 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); }, }));