307 lines
12 KiB
TypeScript
307 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { create } from 'zustand';
|
|
import type { Requirement, RequirementStatus, Effort, DictItem, SourceType, SourceTarget } from '@/lib/requirement';
|
|
import type { Priority } from '@/lib/derive';
|
|
import {
|
|
createRequirementByProductId,
|
|
deleteRequirementByProductId,
|
|
updateRequirementByProductId,
|
|
updateRequirementStatusByProductId,
|
|
} from '@/lib/domain-api';
|
|
import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence';
|
|
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
|
|
|
interface RequirementState {
|
|
requirements: Requirement[];
|
|
sourceTargets: SourceTarget[];
|
|
types: DictItem[];
|
|
platforms: DictItem[];
|
|
loading: boolean;
|
|
loaded: boolean;
|
|
fetchRequirements: (options?: { force?: boolean }) => Promise<void>;
|
|
createRequirement: (data: Omit<Requirement, 'id' | 'code' | 'createdAt'>) => void;
|
|
updateRequirement: (id: string, data: Partial<Requirement>) => void;
|
|
deleteRequirement: (id: string, productId?: string) => void;
|
|
addSourceTarget: (name: string, sourceType: SourceType) => void;
|
|
updateSourceTarget: (id: string, name: string) => void;
|
|
deleteSourceTarget: (id: string) => void;
|
|
addType: (name: string) => void;
|
|
updateType: (id: string, name: string) => void;
|
|
deleteType: (id: string) => void;
|
|
addPlatform: (name: string) => void;
|
|
updatePlatform: (id: string, name: string) => void;
|
|
deletePlatform: (id: string) => void;
|
|
}
|
|
|
|
// --- Preset dictionaries ---
|
|
|
|
const PRESET_SOURCE_TARGETS: SourceTarget[] = [];
|
|
|
|
const PRESET_TYPES: DictItem[] = [
|
|
{ id: 'type-1', name: '新功能', createdAt: '2024-01-01' },
|
|
{ id: 'type-2', name: 'BUG修复', createdAt: '2024-01-01' },
|
|
{ id: 'type-3', name: '数据需求/埋点需求', createdAt: '2024-01-01' },
|
|
{ id: 'type-4', name: '优化需求', createdAt: '2024-01-01' },
|
|
{ id: 'type-5', name: '模块重构', createdAt: '2024-01-01' },
|
|
{ id: 'type-6', name: '缺陷修复', createdAt: '2024-01-01' },
|
|
{ id: 'type-7', name: '功能优化', createdAt: '2024-01-01' },
|
|
{ id: 'type-8', name: '插单/紧急需求', createdAt: '2024-01-01' },
|
|
];
|
|
|
|
const PRESET_PLATFORMS: DictItem[] = [
|
|
{ id: 'platform-1', name: 'Web', createdAt: '2024-01-01' },
|
|
{ id: 'platform-2', name: 'App(iOS)', createdAt: '2024-01-01' },
|
|
{ id: 'platform-3', name: 'App(Android)', createdAt: '2024-01-01' },
|
|
{ id: 'platform-4', name: '小程序', createdAt: '2024-01-01' },
|
|
{ id: 'platform-5', name: 'H5', createdAt: '2024-01-01' },
|
|
];
|
|
|
|
// --- Mock requirements ---
|
|
|
|
const MOCK_REQUIREMENTS: Requirement[] = [];
|
|
let lastRequirementsFetchAt = 0;
|
|
|
|
async function loadStored(): Promise<{ requirements: Requirement[]; sourceTargets: SourceTarget[]; types: DictItem[]; platforms: DictItem[] } | null> {
|
|
try {
|
|
return await loadServerData<{ requirements: Requirement[]; sourceTargets: SourceTarget[]; types: DictItem[]; platforms: DictItem[] }>('requirements');
|
|
} catch {}
|
|
return null;
|
|
}
|
|
|
|
// --- Store ---
|
|
|
|
export const useRequirementStore = create<RequirementState>((set, get) => {
|
|
return {
|
|
requirements: MOCK_REQUIREMENTS,
|
|
sourceTargets: PRESET_SOURCE_TARGETS,
|
|
types: PRESET_TYPES,
|
|
platforms: PRESET_PLATFORMS,
|
|
loading: false,
|
|
loaded: false,
|
|
|
|
fetchRequirements: async (options) => {
|
|
if (!options?.force && get().loaded && Date.now() - lastRequirementsFetchAt < SERVER_DATA_CACHE_MS) return;
|
|
const cached = await loadStored();
|
|
if (!options?.force && get().loaded && Date.now() - lastRequirementsFetchAt < SERVER_DATA_CACHE_MS) return;
|
|
lastRequirementsFetchAt = Date.now();
|
|
if (cached) {
|
|
set({ requirements: cached.requirements, sourceTargets: cached.sourceTargets, types: cached.types, platforms: cached.platforms, loaded: true });
|
|
} else {
|
|
set({ loaded: true });
|
|
}
|
|
},
|
|
|
|
createRequirement: (data) => {
|
|
const { requirements, sourceTargets, types, platforms } = get();
|
|
const code = `REQ-${String(requirements.length + 1).padStart(3, '0')}`;
|
|
const newReq: Requirement = {
|
|
...data,
|
|
status: data.status ?? 'pending_review',
|
|
id: `req-${Date.now()}`,
|
|
code,
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
const updated = [...requirements, newReq];
|
|
set({ requirements: updated, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: async () => {
|
|
try {
|
|
const created = await createRequirementByProductId(newReq.productId, newReq);
|
|
set({
|
|
requirements: get().requirements.map((item) => (
|
|
item.id === newReq.id ? { ...created, creator: newReq.creator || created.creator } : item
|
|
)),
|
|
loaded: true,
|
|
});
|
|
} catch {
|
|
await saveRequirementsFallback({ requirements: updated, sourceTargets, types, platforms });
|
|
}
|
|
},
|
|
expected: updated,
|
|
getCurrent: () => get().requirements,
|
|
rollback: () => set({ requirements, loaded: true }),
|
|
});
|
|
},
|
|
|
|
updateRequirement: (id, data) => {
|
|
const { requirements, sourceTargets, types, platforms } = get();
|
|
const current = requirements.find((r) => r.id === id);
|
|
const productId = data.productId ?? current?.productId;
|
|
const updated = requirements.map((r) => (r.id === id ? { ...r, ...data } : r));
|
|
set({ requirements: updated, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: async () => {
|
|
try {
|
|
if (!productId) throw new Error('missing requirement productId');
|
|
const domainPatch = { ...data };
|
|
const status = domainPatch.status;
|
|
delete domainPatch.status;
|
|
delete domainPatch.productId;
|
|
delete domainPatch.productOwner;
|
|
delete domainPatch.addedToVersionBy;
|
|
if (hasDomainPatch(domainPatch)) {
|
|
await updateRequirementByProductId(productId, id, domainPatch);
|
|
}
|
|
if (status) {
|
|
await updateRequirementStatusByProductId(productId, id, status);
|
|
}
|
|
} catch {
|
|
await saveRequirementsFallback({ requirements: updated, sourceTargets, types, platforms });
|
|
}
|
|
},
|
|
expected: updated,
|
|
getCurrent: () => get().requirements,
|
|
rollback: () => set({ requirements, loaded: true }),
|
|
});
|
|
},
|
|
|
|
deleteRequirement: (id, scopedProductId) => {
|
|
const { requirements, sourceTargets, types, platforms } = get();
|
|
const productId = scopedProductId ?? requirements.find((r) => r.id === id)?.productId;
|
|
const updated = requirements.filter((r) => r.id !== id);
|
|
set({ requirements: updated, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: async () => {
|
|
try {
|
|
if (!productId) throw new Error('missing requirement productId');
|
|
await deleteRequirementByProductId(productId, id);
|
|
} catch {
|
|
await saveRequirementsFallback({ requirements: updated, sourceTargets, types, platforms });
|
|
}
|
|
},
|
|
expected: updated,
|
|
getCurrent: () => get().requirements,
|
|
rollback: () => set({ requirements, loaded: true }),
|
|
});
|
|
},
|
|
|
|
// --- SourceTarget dict ---
|
|
addSourceTarget: (name, sourceType) => {
|
|
const { requirements, sourceTargets, types, platforms } = get();
|
|
const item: SourceTarget = { id: `st-${Date.now()}`, name, sourceType, createdAt: new Date().toISOString().slice(0, 10) };
|
|
const updated = [...sourceTargets, item];
|
|
set({ sourceTargets: updated, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: () => saveServerData('requirements', { requirements, sourceTargets: updated, types, platforms }),
|
|
expected: updated,
|
|
getCurrent: () => get().sourceTargets,
|
|
rollback: () => set({ sourceTargets, loaded: true }),
|
|
});
|
|
},
|
|
|
|
updateSourceTarget: (id, name) => {
|
|
const { requirements, sourceTargets, types, platforms } = get();
|
|
const updated = sourceTargets.map((s) => (s.id === id ? { ...s, name } : s));
|
|
set({ sourceTargets: updated, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: () => saveServerData('requirements', { requirements, sourceTargets: updated, types, platforms }),
|
|
expected: updated,
|
|
getCurrent: () => get().sourceTargets,
|
|
rollback: () => set({ sourceTargets, loaded: true }),
|
|
});
|
|
},
|
|
|
|
deleteSourceTarget: (id) => {
|
|
const { requirements, sourceTargets, types, platforms } = get();
|
|
const updated = sourceTargets.filter((s) => s.id !== id);
|
|
set({ sourceTargets: updated, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: () => saveServerData('requirements', { requirements, sourceTargets: updated, types, platforms }),
|
|
expected: updated,
|
|
getCurrent: () => get().sourceTargets,
|
|
rollback: () => set({ sourceTargets, loaded: true }),
|
|
});
|
|
},
|
|
|
|
// --- Type dict ---
|
|
addType: (name) => {
|
|
const { requirements, sourceTargets, types, platforms } = get();
|
|
const item: DictItem = { id: `type-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) };
|
|
const updated = [...types, item];
|
|
set({ types: updated, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: () => saveServerData('requirements', { requirements, sourceTargets, types: updated, platforms }),
|
|
expected: updated,
|
|
getCurrent: () => get().types,
|
|
rollback: () => set({ types, loaded: true }),
|
|
});
|
|
},
|
|
|
|
updateType: (id, name) => {
|
|
const { requirements, sourceTargets, types, platforms } = get();
|
|
const updated = types.map((t) => (t.id === id ? { ...t, name } : t));
|
|
set({ types: updated, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: () => saveServerData('requirements', { requirements, sourceTargets, types: updated, platforms }),
|
|
expected: updated,
|
|
getCurrent: () => get().types,
|
|
rollback: () => set({ types, loaded: true }),
|
|
});
|
|
},
|
|
|
|
deleteType: (id) => {
|
|
const { requirements, sourceTargets, types, platforms } = get();
|
|
const updated = types.filter((t) => t.id !== id);
|
|
set({ types: updated, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: () => saveServerData('requirements', { requirements, sourceTargets, types: updated, platforms }),
|
|
expected: updated,
|
|
getCurrent: () => get().types,
|
|
rollback: () => set({ types, loaded: true }),
|
|
});
|
|
},
|
|
|
|
// --- Platform dict ---
|
|
addPlatform: (name) => {
|
|
const { requirements, sourceTargets, types, platforms } = get();
|
|
const item: DictItem = { id: `platform-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) };
|
|
const updated = [...platforms, item];
|
|
set({ platforms: updated, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: () => saveServerData('requirements', { requirements, sourceTargets, types, platforms: updated }),
|
|
expected: updated,
|
|
getCurrent: () => get().platforms,
|
|
rollback: () => set({ platforms, loaded: true }),
|
|
});
|
|
},
|
|
|
|
updatePlatform: (id, name) => {
|
|
const { requirements, sourceTargets, types, platforms } = get();
|
|
const updated = platforms.map((p) => (p.id === id ? { ...p, name } : p));
|
|
set({ platforms: updated, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: () => saveServerData('requirements', { requirements, sourceTargets, types, platforms: updated }),
|
|
expected: updated,
|
|
getCurrent: () => get().platforms,
|
|
rollback: () => set({ platforms, loaded: true }),
|
|
});
|
|
},
|
|
|
|
deletePlatform: (id) => {
|
|
const { requirements, sourceTargets, types, platforms } = get();
|
|
const updated = platforms.filter((p) => p.id !== id);
|
|
set({ platforms: updated, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: () => saveServerData('requirements', { requirements, sourceTargets, types, platforms: updated }),
|
|
expected: updated,
|
|
getCurrent: () => get().platforms,
|
|
rollback: () => set({ platforms, loaded: true }),
|
|
});
|
|
},
|
|
};
|
|
});
|
|
|
|
async function saveRequirementsFallback(data: {
|
|
requirements: Requirement[];
|
|
sourceTargets: SourceTarget[];
|
|
types: DictItem[];
|
|
platforms: DictItem[];
|
|
}) {
|
|
await saveServerData('requirements', data);
|
|
}
|
|
|
|
function hasDomainPatch(data: Partial<Requirement>) {
|
|
return Object.values(data).some((value) => value !== undefined);
|
|
}
|