Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
- 移除已迁移业务 AppData 运行时 fallback,改走领域 API 和关系表快读 - 补齐需求产品负责人、版本计划任务 JSON 和成员 username 回填迁移 - 统一治理字典入口,并补充 AI provider、数据源契约和领域服务测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
176 lines
6.1 KiB
TypeScript
176 lines
6.1 KiB
TypeScript
'use client';
|
|
import { create } from 'zustand';
|
|
import type { OvertimeRecord } from '@/lib/overtime';
|
|
import type { DictItem } from '@/lib/requirement';
|
|
import { calcDuration } from '@/lib/overtime';
|
|
import {
|
|
createOvertimeRecordDomain,
|
|
deleteOvertimeRecordDomain,
|
|
listOvertimeRecordsDomain,
|
|
updateOvertimeRecordDomain,
|
|
} from '@/lib/domain-api';
|
|
import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence';
|
|
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
|
|
|
const PRESET_REASONS: DictItem[] = [
|
|
{ id: 'reason-1', name: '需求变更', createdAt: '2024-01-01' },
|
|
{ id: 'reason-2', name: '需求新增', createdAt: '2024-01-01' },
|
|
{ id: 'reason-3', name: '版本冲刺', createdAt: '2024-01-01' },
|
|
{ id: 'reason-4', name: 'Bug修复', createdAt: '2024-01-01' },
|
|
{ id: 'reason-5', name: '线上问题修复', createdAt: '2024-01-01' },
|
|
{ id: 'reason-6', name: '测试问题整改', createdAt: '2024-01-01' },
|
|
{ id: 'reason-7', name: '技术难点攻关', createdAt: '2024-01-01' },
|
|
{ id: 'reason-8', name: '联调问题', createdAt: '2024-01-01' },
|
|
{ id: 'reason-9', name: '上游交付延误', createdAt: '2024-01-01' },
|
|
{ id: 'reason-10', name: '工作量超负荷', createdAt: '2024-01-01' },
|
|
{ id: 'reason-11', name: '返工修改', createdAt: '2024-01-01' },
|
|
];
|
|
|
|
const MOCK_RECORDS: OvertimeRecord[] = [];
|
|
let lastOvertimeFetchAt = 0;
|
|
|
|
interface OvertimeState {
|
|
records: OvertimeRecord[];
|
|
reasons: DictItem[];
|
|
loading: boolean;
|
|
loaded: boolean;
|
|
fetchRecords: (options?: { force?: boolean }) => Promise<void>;
|
|
createRecord: (data: Omit<OvertimeRecord, 'id' | 'duration' | 'createdAt'>) => void;
|
|
updateRecord: (id: string, data: Partial<OvertimeRecord>) => void;
|
|
deleteRecord: (id: string) => void;
|
|
addReason: (name: string) => void;
|
|
updateReason: (id: string, name: string) => void;
|
|
deleteReason: (id: string) => void;
|
|
}
|
|
|
|
async function loadStoredReasons(): Promise<DictItem[] | null> {
|
|
try {
|
|
const stored = await loadServerData<{ records?: OvertimeRecord[]; reasons?: DictItem[] }>('overtime');
|
|
return Array.isArray(stored?.reasons) ? stored.reasons : null;
|
|
} catch {}
|
|
return null;
|
|
}
|
|
|
|
export const useOvertimeStore = create<OvertimeState>((set, get) => ({
|
|
records: MOCK_RECORDS,
|
|
reasons: PRESET_REASONS,
|
|
loading: false,
|
|
loaded: false,
|
|
|
|
fetchRecords: async (options) => {
|
|
if (!options?.force && get().loaded && Date.now() - lastOvertimeFetchAt < SERVER_DATA_CACHE_MS) return;
|
|
const [storedReasons, domainRecords] = await Promise.all([
|
|
loadStoredReasons(),
|
|
listOvertimeRecordsDomain().catch(() => null),
|
|
]);
|
|
if (!options?.force && get().loaded && Date.now() - lastOvertimeFetchAt < SERVER_DATA_CACHE_MS) return;
|
|
lastOvertimeFetchAt = Date.now();
|
|
set({
|
|
records: domainRecords ?? MOCK_RECORDS,
|
|
reasons: storedReasons ?? PRESET_REASONS,
|
|
loaded: true,
|
|
});
|
|
},
|
|
|
|
createRecord: (data) => {
|
|
const previous = get().records;
|
|
const duration = calcDuration(data.startTime, data.endTime);
|
|
const record: OvertimeRecord = {
|
|
...data,
|
|
id: `ot-${Date.now()}`,
|
|
duration,
|
|
createdAt: new Date().toISOString().slice(0, 10),
|
|
};
|
|
const records = [...previous, record];
|
|
set({ records, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: async () => {
|
|
const created = await createOvertimeRecordDomain(record);
|
|
set({
|
|
records: get().records.map((item) => (item.id === record.id ? { ...record, ...created } : item)),
|
|
loaded: true,
|
|
});
|
|
},
|
|
expected: records,
|
|
getCurrent: () => get().records,
|
|
rollback: () => set({ records: previous, loaded: true }),
|
|
});
|
|
},
|
|
|
|
updateRecord: (id, data) => {
|
|
const previous = get().records;
|
|
const records = previous.map((r) => {
|
|
if (r.id !== id) return r;
|
|
const updated = { ...r, ...data };
|
|
if (data.startTime || data.endTime) {
|
|
updated.duration = calcDuration(updated.startTime, updated.endTime);
|
|
}
|
|
return updated;
|
|
});
|
|
set({ records, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: async () => {
|
|
await updateOvertimeRecordDomain(id, records.find((record) => record.id === id) ?? {});
|
|
},
|
|
expected: records,
|
|
getCurrent: () => get().records,
|
|
rollback: () => set({ records: previous, loaded: true }),
|
|
});
|
|
},
|
|
|
|
deleteRecord: (id) => {
|
|
const previous = get().records;
|
|
const records = previous.filter((r) => r.id !== id);
|
|
set({ records, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: async () => {
|
|
await deleteOvertimeRecordDomain(id);
|
|
},
|
|
expected: records,
|
|
getCurrent: () => get().records,
|
|
rollback: () => set({ records: previous, loaded: true }),
|
|
});
|
|
},
|
|
|
|
addReason: (name) => {
|
|
const previous = get().reasons;
|
|
const item: DictItem = { id: `reason-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) };
|
|
const reasons = [...previous, item];
|
|
set({ reasons, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: () => saveOvertimeReasons(reasons),
|
|
expected: reasons,
|
|
getCurrent: () => get().reasons,
|
|
rollback: () => set({ reasons: previous, loaded: true }),
|
|
});
|
|
},
|
|
|
|
updateReason: (id, name) => {
|
|
const previous = get().reasons;
|
|
const reasons = previous.map((r) => r.id === id ? { ...r, name } : r);
|
|
set({ reasons, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: () => saveOvertimeReasons(reasons),
|
|
expected: reasons,
|
|
getCurrent: () => get().reasons,
|
|
rollback: () => set({ reasons: previous, loaded: true }),
|
|
});
|
|
},
|
|
|
|
deleteReason: (id) => {
|
|
const previous = get().reasons;
|
|
const reasons = previous.filter((r) => r.id !== id);
|
|
set({ reasons, loaded: true });
|
|
scheduleSaveWithOptimisticRollback({
|
|
save: () => saveOvertimeReasons(reasons),
|
|
expected: reasons,
|
|
getCurrent: () => get().reasons,
|
|
rollback: () => set({ reasons: previous, loaded: true }),
|
|
});
|
|
},
|
|
}));
|
|
|
|
async function saveOvertimeReasons(reasons: DictItem[]) {
|
|
await saveServerData('overtime', { records: [], reasons });
|
|
}
|