'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 { api } from '@/lib/api'; import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence'; import { SERVER_DATA_CACHE_MS } from '@/lib/server-data'; import { loadV22RequirementsPage, type V22RequirementsQuery } from '@/lib/v22-api'; import { useAuthStore } from './useAuthStore'; import { useMemberStore } from './useMemberStore'; interface RequirementState { requirements: Requirement[]; sourceTargets: SourceTarget[]; types: DictItem[]; platforms: DictItem[]; loading: boolean; loaded: boolean; fetchRequirements: (options?: { force?: boolean; productId?: string; projectId?: string; versionId?: string }) => Promise; createRequirement: (data: Omit) => void; updateRequirement: (id: string, data: Partial) => 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; type RequirementGovernanceKind = 'requirement_type' | 'requirement_platform' | 'requirement_source'; interface GovernanceDictionaryRow { id: string; name: string; group?: string | null; createdAt?: string; } // --- Store --- export const useRequirementStore = create((set, get) => { return { requirements: MOCK_REQUIREMENTS, sourceTargets: PRESET_SOURCE_TARGETS, types: PRESET_TYPES, platforms: PRESET_PLATFORMS, loading: false, loaded: false, fetchRequirements: async (options) => { const hasRelationScope = Boolean(options?.productId); if (!hasRelationScope && !options?.force && get().loaded && Date.now() - lastRequirementsFetchAt < SERVER_DATA_CACHE_MS) return; const [governed, scopedRequirements] = await Promise.all([ loadRequirementGovernanceDictionaries().catch(() => null), hasRelationScope ? loadScopedRequirementsFromRelation(options).catch(() => null) : Promise.resolve(null), ]); if (!hasRelationScope && !options?.force && get().loaded && Date.now() - lastRequirementsFetchAt < SERVER_DATA_CACHE_MS) return; lastRequirementsFetchAt = Date.now(); set({ requirements: scopedRequirements ? mergeRequirementsForScope(get().requirements, options, scopedRequirements) : get().requirements, sourceTargets: governed?.sourceTargets ?? PRESET_SOURCE_TARGETS, types: governed?.types ?? PRESET_TYPES, platforms: governed?.platforms ?? PRESET_PLATFORMS, loaded: true, }); }, createRequirement: (data) => { const { requirements } = 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 () => { 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, }); }, expected: updated, getCurrent: () => get().requirements, rollback: () => set({ requirements, loaded: true }), }); }, updateRequirement: (id, data) => { const { requirements } = 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 () => { if (!productId) throw new Error('missing requirement productId'); const domainPatch = { ...data }; const status = domainPatch.status; delete domainPatch.status; delete domainPatch.productId; delete domainPatch.addedToVersionBy; if (hasDomainPatch(domainPatch)) { await updateRequirementByProductId(productId, id, domainPatch); } if (status) { await updateRequirementStatusByProductId(productId, id, status); } }, expected: updated, getCurrent: () => get().requirements, rollback: () => set({ requirements, loaded: true }), }); }, deleteRequirement: (id, scopedProductId) => { const { requirements } = 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 () => { if (!productId) throw new Error('missing requirement productId'); await deleteRequirementByProductId(productId, id); }, expected: updated, getCurrent: () => get().requirements, rollback: () => set({ requirements, loaded: true }), }); }, // --- SourceTarget dict --- addSourceTarget: (name, sourceType) => { const item: SourceTarget = { id: `st-${Date.now()}`, name, sourceType, createdAt: new Date().toISOString().slice(0, 10) }; const { sourceTargets } = get(); const updated = [...sourceTargets, item]; set({ sourceTargets: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { const created = await createGovernanceDictionary('requirement_source', name, sourceType); set({ sourceTargets: get().sourceTargets.map((target) => ( target.id === item.id ? governanceRowToSourceTarget(created, sourceType) : target )), loaded: true, }); }, expected: updated, getCurrent: () => get().sourceTargets, rollback: () => set({ sourceTargets, loaded: true }), }); }, updateSourceTarget: (id, name) => { const { sourceTargets } = get(); const current = sourceTargets.find((target) => target.id === id); const updated = sourceTargets.map((s) => (s.id === id ? { ...s, name } : s)); set({ sourceTargets: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { await updateGovernanceDictionary('requirement_source', id, name, current?.sourceType ?? 'customer'); }, expected: updated, getCurrent: () => get().sourceTargets, rollback: () => set({ sourceTargets, loaded: true }), }); }, deleteSourceTarget: (id) => { const { sourceTargets } = get(); const updated = sourceTargets.filter((s) => s.id !== id); set({ sourceTargets: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { await deleteGovernanceDictionary('requirement_source', id); }, expected: updated, getCurrent: () => get().sourceTargets, rollback: () => set({ sourceTargets, loaded: true }), }); }, // --- Type dict --- addType: (name) => { const item: DictItem = { id: `type-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) }; const { types } = get(); const updated = [...types, item]; set({ types: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { const created = await createGovernanceDictionary('requirement_type', name); set({ types: get().types.map((type) => (type.id === item.id ? governanceRowToDictItem(created) : type)), loaded: true, }); }, expected: updated, getCurrent: () => get().types, rollback: () => set({ types, loaded: true }), }); }, updateType: (id, name) => { const { types } = get(); const updated = types.map((t) => (t.id === id ? { ...t, name } : t)); set({ types: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { await updateGovernanceDictionary('requirement_type', id, name); }, expected: updated, getCurrent: () => get().types, rollback: () => set({ types, loaded: true }), }); }, deleteType: (id) => { const { types } = get(); const updated = types.filter((t) => t.id !== id); set({ types: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { await deleteGovernanceDictionary('requirement_type', id); }, expected: updated, getCurrent: () => get().types, rollback: () => set({ types, loaded: true }), }); }, // --- Platform dict --- addPlatform: (name) => { const item: DictItem = { id: `platform-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) }; const { platforms } = get(); const updated = [...platforms, item]; set({ platforms: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { const created = await createGovernanceDictionary('requirement_platform', name); set({ platforms: get().platforms.map((platform) => (platform.id === item.id ? governanceRowToDictItem(created) : platform)), loaded: true, }); }, expected: updated, getCurrent: () => get().platforms, rollback: () => set({ platforms, loaded: true }), }); }, updatePlatform: (id, name) => { const { platforms } = get(); const updated = platforms.map((p) => (p.id === id ? { ...p, name } : p)); set({ platforms: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { await updateGovernanceDictionary('requirement_platform', id, name); }, expected: updated, getCurrent: () => get().platforms, rollback: () => set({ platforms, loaded: true }), }); }, deletePlatform: (id) => { const { platforms } = get(); const updated = platforms.filter((p) => p.id !== id); set({ platforms: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { await deleteGovernanceDictionary('requirement_platform', id); }, expected: updated, getCurrent: () => get().platforms, rollback: () => set({ platforms, loaded: true }), }); }, }; }); function hasDomainPatch(data: Partial) { return Object.values(data).some((value) => value !== undefined); } async function loadScopedRequirementsFromRelation( options?: { productId?: string; projectId?: string; versionId?: string }, ): Promise { if (!options?.productId) return []; const items: Requirement[] = []; let cursor: string | undefined; do { const query: V22RequirementsQuery = { productId: options.productId, projectId: options.projectId, versionId: options.versionId, sort: 'created_at_desc', limit: 500, cursor, }; const page = await loadV22RequirementsPage(query); items.push(...page.items); cursor = page.nextCursor; } while (cursor); return items; } function mergeRequirementsForScope( current: Requirement[], scope: { productId?: string; projectId?: string; versionId?: string } | undefined, scopedRequirements: Requirement[], ) { if (!scope?.productId) return current; return [ ...current.filter((requirement) => !isRequirementInScope(requirement, scope)), ...scopedRequirements, ]; } function isRequirementInScope( requirement: Requirement, scope: { productId?: string; projectId?: string; versionId?: string }, ) { if (scope.versionId) return requirement.versionId === scope.versionId; if (scope.projectId) return requirement.productId === scope.productId && requirement.projectId === scope.projectId; return requirement.productId === scope.productId; } async function loadRequirementGovernanceDictionaries() { const [types, platforms, sourceTargets] = await Promise.all([ listGovernanceDictionary('requirement_type'), listGovernanceDictionary('requirement_platform'), listGovernanceDictionary('requirement_source'), ]); return { types: types.length > 0 ? types.map(governanceRowToDictItem) : PRESET_TYPES, platforms: platforms.length > 0 ? platforms.map(governanceRowToDictItem) : PRESET_PLATFORMS, sourceTargets: sourceTargets.length > 0 ? sourceTargets.map((row) => governanceRowToSourceTarget(row)) : PRESET_SOURCE_TARGETS, }; } function listGovernanceDictionary(kind: RequirementGovernanceKind) { return api.get(`/governance/dictionaries?kind=${kind}`); } function createGovernanceDictionary(kind: RequirementGovernanceKind, name: string, group?: string) { const { actorId, permissions } = getGovernanceActorContext(); return api.post('/governance/dictionaries', { actorId, permissions, kind, name, group }); } function updateGovernanceDictionary(kind: RequirementGovernanceKind, id: string, name: string, group?: string) { const { actorId, permissions } = getGovernanceActorContext(); return api.patch(`/governance/dictionaries/${kind}/${id}`, { actorId, permissions, kind, name, group }); } function deleteGovernanceDictionary(kind: RequirementGovernanceKind, id: string) { const { actorId, permissions } = getGovernanceActorContext(); return api.deleteWithBody(`/governance/dictionaries/${kind}/${id}`, { actorId, permissions }); } function getGovernanceActorContext() { const user = useAuthStore.getState().user; const role = useMemberStore.getState().roles.find((item) => item.id === user?.roleId); return { actorId: user?.id ?? '', permissions: role?.permissions ?? [] }; } function governanceRowToDictItem(row: GovernanceDictionaryRow): DictItem { return { id: row.id, name: row.name, createdAt: normalizeGovernanceCreatedAt(row.createdAt), }; } function governanceRowToSourceTarget(row: GovernanceDictionaryRow, fallbackSourceType?: SourceType): SourceTarget { return { id: row.id, name: row.name, sourceType: normalizeGovernanceSourceType(row.group, fallbackSourceType), createdAt: normalizeGovernanceCreatedAt(row.createdAt), }; } function normalizeGovernanceCreatedAt(value?: string) { return value ? value.slice(0, 10) : new Date().toISOString().slice(0, 10); } function normalizeGovernanceSourceType(value?: string | null, fallback: SourceType = 'customer'): SourceType { return ['customer', 'internal', 'operation', 'aftersale', 'market', 'competitor', 'management'].includes(value ?? '') ? value as SourceType : fallback; }