'use client'; import { create } from 'zustand'; import type { Bug, BugStatus, BugLog } from '@/lib/bug'; import { generateBugNo } from '@/lib/bug'; import { applyBugTransition } from '@/lib/bug-workflow'; import { createBugByVersionId, deleteBugByVersionId, listBugsByVersionId, transferBugByVersionId, updateBugByVersionId, updateBugStatusByVersionId, } from '@/lib/domain-api'; import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence'; import { SERVER_DATA_CACHE_MS } from '@/lib/server-data'; import type { WorkActivity } from '@/lib/work-activity'; import { makeBugStatusActivity, makeBugTransferredActivity, } from '@/lib/work-activity-factory'; import { useWorkActivityStore } from './useWorkActivityStore'; let lastBugsFetchAt = 0; function makeLog(action: BugLog['action'], operator: string, from?: string, to?: string, remark?: string): BugLog { return { id: `log-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, action, fromValue: from, toValue: to, operator, remark, createdAt: new Date().toISOString() }; } interface BugState { bugs: Bug[]; loaded: boolean; fetchBugs: (options?: { force?: boolean; versionId?: string }) => Promise; createBug: (data: Omit, operator: string) => Bug; updateBug: (id: string, data: Partial) => void; deleteBug: (id: string) => void; changeStatus: (id: string, to: BugStatus, operator: string, extra?: { resolution?: string }) => { ok: boolean; message?: string }; transferBug: (id: string, newAssigneeId: string, operator: string, remark?: string) => { ok: boolean; message?: string }; getByTestCase: (caseId: string) => Bug[]; getByVersion: (versionId: string) => Bug[]; getByAssignee: (assigneeId: string) => Bug[]; } export const useBugStore = create((set, get) => ({ bugs: [], loaded: false, fetchBugs: async (options) => { if (!options?.versionId && !options?.force && get().loaded && Date.now() - lastBugsFetchAt < SERVER_DATA_CACHE_MS) return; const cached = options?.versionId ? await listBugsByVersionId(options.versionId).catch(() => []) : get().bugs; if (!options?.versionId && !options?.force && get().loaded && Date.now() - lastBugsFetchAt < SERVER_DATA_CACHE_MS) return; lastBugsFetchAt = Date.now(); set({ bugs: options?.versionId ? mergeBugsForVersion(get().bugs, options.versionId, cached ?? []) : cached ?? [], loaded: true, }); }, createBug: (data, operator) => { const list = get().bugs; const now = new Date().toISOString(); const log = makeLog('create', operator); const bug: Bug = { ...data, id: `bug-${Date.now()}`, bugNo: generateBugNo(list), status: 'open', logs: [log], createdAt: now, updatedAt: now, }; const updated = [...list, bug]; set({ bugs: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { const result = await createBugByVersionId(bug.versionId, bug); set({ bugs: get().bugs.map((item) => (item.id === bug.id ? { ...bug, ...result.item, logs: bug.logs } : item)), loaded: true, }); appendDomainActivities(result.activities); }, expected: updated, getCurrent: () => get().bugs, rollback: () => set({ bugs: list, loaded: true }), }); return bug; }, updateBug: (id, data) => { const previous = get().bugs; const updated = previous.map((b) => b.id === id ? { ...b, ...data, updatedAt: new Date().toISOString() } : b, ); set({ bugs: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { const versionId = previous.find((bug) => bug.id === id)?.versionId; if (!versionId) throw new Error('missing versionId'); const result = await updateBugByVersionId(versionId, id, data); appendDomainActivities(result.activities); }, expected: updated, getCurrent: () => get().bugs, rollback: () => set({ bugs: previous, loaded: true }), }); }, deleteBug: (id) => { const previous = get().bugs; const updated = previous.filter((b) => b.id !== id); set({ bugs: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { const versionId = previous.find((bug) => bug.id === id)?.versionId; if (!versionId) throw new Error('missing versionId'); await deleteBugByVersionId(versionId, id); }, expected: updated, getCurrent: () => get().bugs, rollback: () => set({ bugs: previous, loaded: true }), }); }, changeStatus: (id, to, operator, extra) => { const bug = get().bugs.find((b) => b.id === id); if (!bug) return { ok: false, message: 'Bug不存在' }; const result = applyBugTransition(bug, to, operator, { now: new Date(), resolution: extra?.resolution, }); if (!result.ok || !result.patch) return { ok: false, message: result.message }; const previous = get().bugs; const updated = previous.map((item) => item.id === id ? { ...item, ...result.patch, updatedAt: new Date().toISOString() } : item, ); set({ bugs: updated, loaded: true }); const activity = makeBugStatusActivity(bug, bug.status, to, operator); scheduleSaveWithOptimisticRollback({ save: async () => { const result = await updateBugStatusByVersionId(bug.versionId, id, to, { operator, resolution: extra?.resolution, }); appendDomainActivities(result.activities); if (result.activities.length === 0 && activity) useWorkActivityStore.getState().addActivity(activity); }, expected: updated, getCurrent: () => get().bugs, rollback: () => set({ bugs: previous, loaded: true }), }); return { ok: true }; }, transferBug: (id, newAssigneeId, operator, remark) => { const bug = get().bugs.find((b) => b.id === id); if (!bug) return { ok: false, message: 'Bug不存在' }; if (bug.assigneeId === newAssigneeId) return { ok: false, message: '已是当前负责人' }; const log = makeLog('transfer', operator, bug.assigneeId, newAssigneeId, remark); const activity = makeBugTransferredActivity(bug, operator, newAssigneeId); const previous = get().bugs; const updated = previous.map((item) => item.id === id ? { ...item, assigneeId: newAssigneeId, logs: [...(item.logs || []), log], updatedAt: new Date().toISOString() } : item, ); set({ bugs: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { const result = await transferBugByVersionId(bug.versionId, id, newAssigneeId, operator); appendDomainActivities(result.activities); if (result.activities.length === 0) useWorkActivityStore.getState().addActivity(activity); }, expected: updated, getCurrent: () => get().bugs, rollback: () => set({ bugs: previous, loaded: true }), }); return { ok: true }; }, getByTestCase: (caseId) => { return get().bugs.filter((b) => b.testCaseId === caseId); }, getByVersion: (versionId) => { return get().bugs.filter((b) => b.versionId === versionId); }, getByAssignee: (assigneeId) => { return get().bugs.filter((b) => b.assigneeId === assigneeId); }, })); function mergeBugsForVersion(current: Bug[], versionId: string, scopedBugs: Bug[]) { return [ ...current.filter((bug) => bug.versionId !== versionId), ...scopedBugs, ]; } function appendDomainActivities(activities: WorkActivity[]) { if (activities.length === 0) return; useWorkActivityStore.setState((state) => ({ activities: [...state.activities, ...activities], loaded: true, })); }