Files
ftb-project-management/apps/web/stores/useBugStore.ts
2026-07-03 19:36:18 +08:00

141 lines
5.2 KiB
TypeScript

'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 { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence';
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
import {
makeBugCreatedActivity,
makeBugStatusActivity,
makeBugTransferredActivity,
} from '@/lib/work-activity-factory';
import { useWorkActivityStore } from './useWorkActivityStore';
let lastBugsFetchAt = 0;
async function loadStored(): Promise<Bug[] | null> {
try {
return await loadServerData<Bug[]>('bugs');
} catch {}
return null;
}
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 }) => Promise<void>;
createBug: (data: Omit<Bug, 'id' | 'bugNo' | 'createdAt' | 'updatedAt' | 'status' | 'logs'>, operator: string) => Bug;
updateBug: (id: string, data: Partial<Bug>) => 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<BugState>((set, get) => ({
bugs: [],
loaded: false,
fetchBugs: async (options) => {
if (!options?.force && get().loaded && Date.now() - lastBugsFetchAt < SERVER_DATA_CACHE_MS) return;
const cached = await loadStored();
if (!options?.force && get().loaded && Date.now() - lastBugsFetchAt < SERVER_DATA_CACHE_MS) return;
lastBugsFetchAt = Date.now();
set({ bugs: 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: () => saveServerData('bugs', updated),
expected: updated,
getCurrent: () => get().bugs,
rollback: () => set({ bugs: list, loaded: true }),
});
useWorkActivityStore.getState().addActivity(makeBugCreatedActivity(bug, operator));
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: () => saveServerData('bugs', updated),
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: () => saveServerData('bugs', updated),
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 };
get().updateBug(id, result.patch);
const activity = makeBugStatusActivity(bug, bug.status, to, operator);
if (activity) useWorkActivityStore.getState().addActivity(activity);
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);
get().updateBug(id, { assigneeId: newAssigneeId, logs: [...(bug.logs || []), log] });
useWorkActivityStore.getState().addActivity(makeBugTransferredActivity(bug, operator, newAssigneeId));
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);
},
}));