perf(web): 优化大数据量页面切换与聚合性能
This commit is contained in:
@@ -3,7 +3,7 @@ import { create } from 'zustand';
|
||||
import type { Bug, BugStatus, BugLog } from '@/lib/bug';
|
||||
import { generateBugNo } from '@/lib/bug';
|
||||
import { applyBugTransition } from '@/lib/bug-workflow';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
import {
|
||||
makeBugCreatedActivity,
|
||||
makeBugStatusActivity,
|
||||
@@ -15,6 +15,8 @@ function saveStored(items: Bug[]) {
|
||||
saveServerData('bugs', items).catch(() => {});
|
||||
}
|
||||
|
||||
let lastBugsFetchAt = 0;
|
||||
|
||||
async function loadStored(): Promise<Bug[] | null> {
|
||||
try {
|
||||
return await loadServerData<Bug[]>('bugs');
|
||||
@@ -28,7 +30,8 @@ function makeLog(action: BugLog['action'], operator: string, from?: string, to?:
|
||||
|
||||
interface BugState {
|
||||
bugs: Bug[];
|
||||
fetchBugs: () => Promise<void>;
|
||||
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;
|
||||
@@ -41,10 +44,14 @@ interface BugState {
|
||||
|
||||
export const useBugStore = create<BugState>((set, get) => ({
|
||||
bugs: [],
|
||||
loaded: false,
|
||||
|
||||
fetchBugs: async () => {
|
||||
fetchBugs: async (options) => {
|
||||
if (!options?.force && get().loaded && Date.now() - lastBugsFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const cached = await loadStored();
|
||||
if (cached) set({ bugs: cached });
|
||||
if (!options?.force && get().loaded && Date.now() - lastBugsFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastBugsFetchAt = Date.now();
|
||||
set({ bugs: cached ?? [], loaded: true });
|
||||
},
|
||||
|
||||
createBug: (data, operator) => {
|
||||
@@ -61,7 +68,7 @@ export const useBugStore = create<BugState>((set, get) => ({
|
||||
updatedAt: now,
|
||||
};
|
||||
const updated = [...list, bug];
|
||||
set({ bugs: updated });
|
||||
set({ bugs: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
useWorkActivityStore.getState().addActivity(makeBugCreatedActivity(bug, operator));
|
||||
return bug;
|
||||
@@ -71,13 +78,13 @@ export const useBugStore = create<BugState>((set, get) => ({
|
||||
const updated = get().bugs.map((b) =>
|
||||
b.id === id ? { ...b, ...data, updatedAt: new Date().toISOString() } : b,
|
||||
);
|
||||
set({ bugs: updated });
|
||||
set({ bugs: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
deleteBug: (id) => {
|
||||
const updated = get().bugs.filter((b) => b.id !== id);
|
||||
set({ bugs: updated });
|
||||
set({ bugs: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { DevTask, DevTaskStatus } from '@/lib/dev-task';
|
||||
import { generateTaskNo, isLegacyTask } from '@/lib/dev-task';
|
||||
import { applyDevTaskTransition, normalizeDevTaskOnCreate } from '@/lib/dev-task-workflow';
|
||||
import { createEntityId, dedupeEntityIds } from '@/lib/entity-id';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
import {
|
||||
makeDevTaskBlockedActivity,
|
||||
makeDevTaskCreatedActivity,
|
||||
@@ -17,6 +17,8 @@ function saveStored(items: DevTask[]) {
|
||||
saveServerData('dev-tasks', items).catch(() => {});
|
||||
}
|
||||
|
||||
let lastTasksFetchAt = 0;
|
||||
|
||||
async function loadStored(): Promise<DevTask[] | null> {
|
||||
try {
|
||||
const parsed = await loadServerData<DevTask[]>('dev-tasks');
|
||||
@@ -34,7 +36,8 @@ async function loadStored(): Promise<DevTask[] | null> {
|
||||
|
||||
interface DevTaskState {
|
||||
tasks: DevTask[];
|
||||
fetchTasks: () => Promise<void>;
|
||||
loaded: boolean;
|
||||
fetchTasks: (options?: { force?: boolean }) => Promise<void>;
|
||||
createTask: (data: Omit<DevTask, 'id' | 'taskNo' | 'createdAt' | 'updatedAt' | 'isBlocked'>) => DevTask;
|
||||
updateTask: (id: string, data: Partial<DevTask>) => void;
|
||||
deleteTask: (id: string) => void;
|
||||
@@ -47,10 +50,14 @@ interface DevTaskState {
|
||||
|
||||
export const useDevTaskStore = create<DevTaskState>((set, get) => ({
|
||||
tasks: [],
|
||||
loaded: false,
|
||||
|
||||
fetchTasks: async () => {
|
||||
fetchTasks: async (options) => {
|
||||
if (!options?.force && get().loaded && Date.now() - lastTasksFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const cached = await loadStored();
|
||||
set({ tasks: cached ?? [] });
|
||||
if (!options?.force && get().loaded && Date.now() - lastTasksFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastTasksFetchAt = Date.now();
|
||||
set({ tasks: cached ?? [], loaded: true });
|
||||
},
|
||||
|
||||
createTask: (data) => {
|
||||
@@ -65,7 +72,7 @@ export const useDevTaskStore = create<DevTaskState>((set, get) => ({
|
||||
updatedAt: now,
|
||||
} as DevTask);
|
||||
const updated = [...list, task];
|
||||
set({ tasks: updated });
|
||||
set({ tasks: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
useWorkActivityStore.getState().addActivity(makeDevTaskCreatedActivity(task, task.createdBy || task.assigneeId));
|
||||
return task;
|
||||
@@ -75,13 +82,13 @@ export const useDevTaskStore = create<DevTaskState>((set, get) => ({
|
||||
const updated = get().tasks.map((t) =>
|
||||
t.id === id ? { ...t, ...data, aiDraft: false, updatedAt: new Date().toISOString() } : t,
|
||||
);
|
||||
set({ tasks: updated });
|
||||
set({ tasks: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
deleteTask: (id) => {
|
||||
const updated = get().tasks.filter((t) => t.id !== id);
|
||||
set({ tasks: updated });
|
||||
set({ tasks: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '@/lib/member-system';
|
||||
import { DEFAULT_ROLE_PERMISSIONS, DEFAULT_ROLE_PRESETS } from '@/lib/permissions';
|
||||
import { mergePresetRolePermissions } from '@/lib/role-permission-migration';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
|
||||
const PRESET_DEPARTMENTS: Department[] = [
|
||||
{ id: 'dept-1', name: '产品部', order: 1, createdAt: '2024-01-01' },
|
||||
@@ -28,6 +28,7 @@ const PRESET_ROLES: RoleItem[] = DEFAULT_ROLE_PRESETS;
|
||||
const MOCK_MEMBERS: Member[] = [
|
||||
SYSTEM_ADMIN_MEMBER,
|
||||
];
|
||||
let lastMembersFetchAt = 0;
|
||||
|
||||
interface MemberState {
|
||||
departments: Department[];
|
||||
@@ -35,7 +36,8 @@ interface MemberState {
|
||||
roles: RoleItem[];
|
||||
passwordRule: PasswordRule;
|
||||
loading: boolean;
|
||||
fetchMembers: () => void;
|
||||
loaded: boolean;
|
||||
fetchMembers: (options?: { force?: boolean }) => Promise<void>;
|
||||
updatePasswordRule: (rule: PasswordRule) => void;
|
||||
createDepartment: (data: Omit<Department, 'id' | 'createdAt'>) => void;
|
||||
updateDepartment: (id: string, data: Partial<Department>) => void;
|
||||
@@ -65,9 +67,13 @@ export const useMemberStore = create<MemberState>((set, get) => ({
|
||||
roles: PRESET_ROLES,
|
||||
passwordRule: DEFAULT_PASSWORD_RULE,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
|
||||
fetchMembers: async () => {
|
||||
fetchMembers: async (options) => {
|
||||
if (!options?.force && get().loaded && Date.now() - lastMembersFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const cached = await loadStored();
|
||||
if (!options?.force && get().loaded && Date.now() - lastMembersFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastMembersFetchAt = Date.now();
|
||||
if (cached) {
|
||||
// 迁移:旧 RoleItem 没 permissions 字段,补默认值
|
||||
const migratedRoles = cached.roles.map((r) => {
|
||||
@@ -94,8 +100,10 @@ export const useMemberStore = create<MemberState>((set, get) => ({
|
||||
roles,
|
||||
passwordRule: cached.passwordRule || DEFAULT_PASSWORD_RULE,
|
||||
};
|
||||
set(next);
|
||||
set({ ...next, loaded: true });
|
||||
if (ensured.changed || usernameEnsured.changed || rolesChanged) saveStored(next);
|
||||
} else {
|
||||
set({ loaded: true });
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { create } from 'zustand';
|
||||
import type { OvertimeRecord } from '@/lib/overtime';
|
||||
import type { DictItem } from '@/lib/requirement';
|
||||
import { calcDuration } from '@/lib/overtime';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
|
||||
const PRESET_REASONS: DictItem[] = [
|
||||
{ id: 'reason-1', name: '需求变更', createdAt: '2024-01-01' },
|
||||
@@ -20,12 +20,14 @@ const PRESET_REASONS: DictItem[] = [
|
||||
];
|
||||
|
||||
const MOCK_RECORDS: OvertimeRecord[] = [];
|
||||
let lastOvertimeFetchAt = 0;
|
||||
|
||||
interface OvertimeState {
|
||||
records: OvertimeRecord[];
|
||||
reasons: DictItem[];
|
||||
loading: boolean;
|
||||
fetchRecords: () => Promise<void>;
|
||||
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;
|
||||
@@ -49,10 +51,14 @@ export const useOvertimeStore = create<OvertimeState>((set, get) => ({
|
||||
records: MOCK_RECORDS,
|
||||
reasons: PRESET_REASONS,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
|
||||
fetchRecords: async () => {
|
||||
fetchRecords: async (options) => {
|
||||
if (!options?.force && get().loaded && Date.now() - lastOvertimeFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const cached = await loadStored();
|
||||
if (cached) set({ records: cached.records, reasons: cached.reasons });
|
||||
if (!options?.force && get().loaded && Date.now() - lastOvertimeFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastOvertimeFetchAt = Date.now();
|
||||
set({ records: cached?.records ?? MOCK_RECORDS, reasons: cached?.reasons ?? PRESET_REASONS, loaded: true });
|
||||
},
|
||||
|
||||
createRecord: (data) => {
|
||||
@@ -64,7 +70,7 @@ export const useOvertimeStore = create<OvertimeState>((set, get) => ({
|
||||
createdAt: new Date().toISOString().slice(0, 10),
|
||||
};
|
||||
const records = [...get().records, record];
|
||||
set({ records });
|
||||
set({ records, loaded: true });
|
||||
saveStored({ records, reasons: get().reasons });
|
||||
},
|
||||
|
||||
@@ -77,32 +83,32 @@ export const useOvertimeStore = create<OvertimeState>((set, get) => ({
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
set({ records });
|
||||
set({ records, loaded: true });
|
||||
saveStored({ records, reasons: get().reasons });
|
||||
},
|
||||
|
||||
deleteRecord: (id) => {
|
||||
const records = get().records.filter((r) => r.id !== id);
|
||||
set({ records });
|
||||
set({ records, loaded: true });
|
||||
saveStored({ records, reasons: get().reasons });
|
||||
},
|
||||
|
||||
addReason: (name) => {
|
||||
const item: DictItem = { id: `reason-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const reasons = [...get().reasons, item];
|
||||
set({ reasons });
|
||||
set({ reasons, loaded: true });
|
||||
saveStored({ records: get().records, reasons });
|
||||
},
|
||||
|
||||
updateReason: (id, name) => {
|
||||
const reasons = get().reasons.map((r) => r.id === id ? { ...r, name } : r);
|
||||
set({ reasons });
|
||||
set({ reasons, loaded: true });
|
||||
saveStored({ records: get().records, reasons });
|
||||
},
|
||||
|
||||
deleteReason: (id) => {
|
||||
const reasons = get().reasons.filter((r) => r.id !== id);
|
||||
set({ reasons });
|
||||
set({ reasons, loaded: true });
|
||||
saveStored({ records: get().records, reasons });
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -4,7 +4,7 @@ import { create } from 'zustand';
|
||||
import { Product } from '@ftb/shared';
|
||||
import { api } from '@/lib/api';
|
||||
import { shouldPersistRemoteOverview } from '@/lib/product-overview-persistence';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
import type { Stage, Role } from '@/lib/stage';
|
||||
import type { Priority, VersionLinks } from '@/lib/derive';
|
||||
|
||||
@@ -35,6 +35,7 @@ interface VersionItem {
|
||||
}
|
||||
|
||||
const MOCK_OVERVIEW: ProductOverview[] = [];
|
||||
let lastOverviewFetchAt = 0;
|
||||
|
||||
interface ProductWithCount {
|
||||
id: string;
|
||||
@@ -55,6 +56,7 @@ interface ProductState {
|
||||
overview: ProductOverview[];
|
||||
currentProduct: (Product & { requirements?: any[]; versions?: any[] }) | null;
|
||||
loading: boolean;
|
||||
overviewLoaded: boolean;
|
||||
error: string | null;
|
||||
fetchProducts: () => Promise<void>;
|
||||
fetchOverview: () => Promise<void>;
|
||||
@@ -77,6 +79,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
overview: [],
|
||||
currentProduct: null,
|
||||
loading: false,
|
||||
overviewLoaded: false,
|
||||
error: null,
|
||||
|
||||
fetchProducts: async () => {
|
||||
@@ -90,22 +93,24 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
},
|
||||
|
||||
fetchOverview: async () => {
|
||||
if (get().overview.length > 0) return;
|
||||
if (get().overviewLoaded && Date.now() - lastOverviewFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const cached = await loadStoredOverview();
|
||||
if (get().overviewLoaded && Date.now() - lastOverviewFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastOverviewFetchAt = Date.now();
|
||||
if (cached) {
|
||||
// 有本地缓存,直接用,不再调远端覆盖(mock 模式核心数据在本地)
|
||||
set({ overview: cached, loading: false });
|
||||
set({ overview: cached, loading: false, overviewLoaded: true });
|
||||
return;
|
||||
}
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const overview = await api.get<ProductOverview[]>('/products/overview');
|
||||
set({ overview, loading: false });
|
||||
set({ overview, loading: false, overviewLoaded: true });
|
||||
if (shouldPersistRemoteOverview(overview)) {
|
||||
saveStoredOverview(overview);
|
||||
}
|
||||
} catch {
|
||||
set({ overview: MOCK_OVERVIEW, error: null, loading: false });
|
||||
set({ overview: MOCK_OVERVIEW, error: null, loading: false, overviewLoaded: true });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -137,7 +142,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
_count: { requirements: 0, projects: 0, versions: 0 },
|
||||
};
|
||||
const updated = [newProduct, ...get().overview];
|
||||
set({ overview: updated });
|
||||
set({ overview: updated, overviewLoaded: true });
|
||||
saveStoredOverview(updated);
|
||||
// 异步尝试同步远端(失败也无所谓,本地已更新)
|
||||
try { await api.post('/products', data); } catch {}
|
||||
@@ -146,14 +151,12 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
updateProduct: async (id, data) => {
|
||||
try {
|
||||
await api.patch(`/products/${id}`, data);
|
||||
await get().fetchOverview();
|
||||
} catch {
|
||||
const updated = get().overview.map((p) =>
|
||||
p.id === id ? { ...p, ...data } : p,
|
||||
);
|
||||
set({ overview: updated });
|
||||
saveStoredOverview(updated);
|
||||
}
|
||||
} catch {}
|
||||
const updated = get().overview.map((p) =>
|
||||
p.id === id ? { ...p, ...data } : p,
|
||||
);
|
||||
set({ overview: updated, overviewLoaded: true });
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
deleteProduct: async (id) => {
|
||||
@@ -164,6 +167,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
set({
|
||||
products: get().products.filter((p) => p.id !== id),
|
||||
overview: updated,
|
||||
overviewLoaded: true,
|
||||
});
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
@@ -171,7 +175,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
reorderProducts: (ids) => {
|
||||
const map = new Map(get().overview.map((p) => [p.id, p]));
|
||||
const reordered = ids.map((id) => map.get(id)).filter(Boolean) as ProductOverview[];
|
||||
set({ overview: reordered });
|
||||
set({ overview: reordered, overviewLoaded: true });
|
||||
saveStoredOverview(reordered);
|
||||
},
|
||||
|
||||
@@ -194,7 +198,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
const updated = get().overview
|
||||
.filter((p) => p.id !== sourceId)
|
||||
.map((p) => (p.id === targetId ? migratedTarget : p));
|
||||
set({ overview: updated });
|
||||
set({ overview: updated, overviewLoaded: true });
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
@@ -213,7 +217,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
_count: { ...p._count, projects: (p._count?.projects ?? 0) + 1 } as any,
|
||||
};
|
||||
});
|
||||
set({ overview: updated });
|
||||
set({ overview: updated, overviewLoaded: true });
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
@@ -250,7 +254,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
versions: nextVersions,
|
||||
};
|
||||
});
|
||||
set({ overview: updated });
|
||||
set({ overview: updated, overviewLoaded: true });
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
@@ -271,7 +275,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
_count: { ...p._count, versions: (p._count?.versions ?? 0) + 1 } as any,
|
||||
};
|
||||
});
|
||||
set({ overview: updated });
|
||||
set({ overview: updated, overviewLoaded: true });
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
@@ -285,7 +289,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
),
|
||||
};
|
||||
});
|
||||
set({ overview: updated });
|
||||
set({ overview: updated, overviewLoaded: true });
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
@@ -298,7 +302,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
_count: { ...p._count, versions: Math.max(0, (p._count?.versions ?? 0) - 1) } as any,
|
||||
};
|
||||
});
|
||||
set({ overview: updated });
|
||||
set({ overview: updated, overviewLoaded: true });
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
|
||||
@@ -311,7 +315,7 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
||||
_count: { ...p._count, projects: Math.max(0, (p._count?.projects ?? 0) - 1) } as any,
|
||||
};
|
||||
});
|
||||
set({ overview: updated });
|
||||
set({ overview: updated, overviewLoaded: true });
|
||||
saveStoredOverview(updated);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Requirement, RequirementStatus, Effort, DictItem, SourceType, SourceTarget } from '@/lib/requirement';
|
||||
import type { Priority } from '@/lib/derive';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
|
||||
interface RequirementState {
|
||||
requirements: Requirement[];
|
||||
@@ -11,7 +11,8 @@ interface RequirementState {
|
||||
types: DictItem[];
|
||||
platforms: DictItem[];
|
||||
loading: boolean;
|
||||
fetchRequirements: () => Promise<void>;
|
||||
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) => void;
|
||||
@@ -52,6 +53,7 @@ const PRESET_PLATFORMS: DictItem[] = [
|
||||
// --- Mock requirements ---
|
||||
|
||||
const MOCK_REQUIREMENTS: Requirement[] = [];
|
||||
let lastRequirementsFetchAt = 0;
|
||||
|
||||
// --- Server persistence helpers ---
|
||||
|
||||
@@ -75,11 +77,17 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
types: PRESET_TYPES,
|
||||
platforms: PRESET_PLATFORMS,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
|
||||
fetchRequirements: async () => {
|
||||
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 });
|
||||
set({ requirements: cached.requirements, sourceTargets: cached.sourceTargets, types: cached.types, platforms: cached.platforms, loaded: true });
|
||||
} else {
|
||||
set({ loaded: true });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -94,21 +102,21 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const updated = [...requirements, newReq];
|
||||
set({ requirements: updated });
|
||||
set({ requirements: updated, loaded: true });
|
||||
saveStored({ requirements: updated, sourceTargets, types, platforms });
|
||||
},
|
||||
|
||||
updateRequirement: (id, data) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = requirements.map((r) => (r.id === id ? { ...r, ...data } : r));
|
||||
set({ requirements: updated });
|
||||
set({ requirements: updated, loaded: true });
|
||||
saveStored({ requirements: updated, sourceTargets, types, platforms });
|
||||
},
|
||||
|
||||
deleteRequirement: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = requirements.filter((r) => r.id !== id);
|
||||
set({ requirements: updated });
|
||||
set({ requirements: updated, loaded: true });
|
||||
saveStored({ requirements: updated, sourceTargets, types, platforms });
|
||||
},
|
||||
|
||||
@@ -117,21 +125,21 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
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 });
|
||||
set({ sourceTargets: updated, loaded: true });
|
||||
saveStored({ requirements, sourceTargets: updated, types, platforms });
|
||||
},
|
||||
|
||||
updateSourceTarget: (id, name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = sourceTargets.map((s) => (s.id === id ? { ...s, name } : s));
|
||||
set({ sourceTargets: updated });
|
||||
set({ sourceTargets: updated, loaded: true });
|
||||
saveStored({ requirements, sourceTargets: updated, types, platforms });
|
||||
},
|
||||
|
||||
deleteSourceTarget: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = sourceTargets.filter((s) => s.id !== id);
|
||||
set({ sourceTargets: updated });
|
||||
set({ sourceTargets: updated, loaded: true });
|
||||
saveStored({ requirements, sourceTargets: updated, types, platforms });
|
||||
},
|
||||
|
||||
@@ -140,21 +148,21 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
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 });
|
||||
set({ types: updated, loaded: true });
|
||||
saveStored({ requirements, sourceTargets, types: updated, platforms });
|
||||
},
|
||||
|
||||
updateType: (id, name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = types.map((t) => (t.id === id ? { ...t, name } : t));
|
||||
set({ types: updated });
|
||||
set({ types: updated, loaded: true });
|
||||
saveStored({ requirements, sourceTargets, types: updated, platforms });
|
||||
},
|
||||
|
||||
deleteType: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = types.filter((t) => t.id !== id);
|
||||
set({ types: updated });
|
||||
set({ types: updated, loaded: true });
|
||||
saveStored({ requirements, sourceTargets, types: updated, platforms });
|
||||
},
|
||||
|
||||
@@ -163,21 +171,21 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
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 });
|
||||
set({ platforms: updated, loaded: true });
|
||||
saveStored({ requirements, sourceTargets, types, platforms: updated });
|
||||
},
|
||||
|
||||
updatePlatform: (id, name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = platforms.map((p) => (p.id === id ? { ...p, name } : p));
|
||||
set({ platforms: updated });
|
||||
set({ platforms: updated, loaded: true });
|
||||
saveStored({ requirements, sourceTargets, types, platforms: updated });
|
||||
},
|
||||
|
||||
deletePlatform: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const updated = platforms.filter((p) => p.id !== id);
|
||||
set({ platforms: updated });
|
||||
set({ platforms: updated, loaded: true });
|
||||
saveStored({ requirements, sourceTargets, types, platforms: updated });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
import { create } from 'zustand';
|
||||
import type { TaskCategory, CategoryGroup } from '@/lib/task-category';
|
||||
import { PRESET_CATEGORIES, normalizeTaskCategories, resolveAiTaskCategoryByName } from '@/lib/task-category';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
|
||||
function saveStored(items: TaskCategory[]) {
|
||||
saveServerData('task-categories', items).catch(() => {});
|
||||
}
|
||||
|
||||
let lastCategoriesFetchAt = 0;
|
||||
|
||||
async function loadStored(): Promise<TaskCategory[] | null> {
|
||||
try {
|
||||
return await loadServerData<TaskCategory[]>('task-categories');
|
||||
@@ -17,7 +19,8 @@ async function loadStored(): Promise<TaskCategory[] | null> {
|
||||
|
||||
interface TaskCategoryState {
|
||||
categories: TaskCategory[];
|
||||
fetchCategories: () => Promise<void>;
|
||||
loaded: boolean;
|
||||
fetchCategories: (options?: { force?: boolean }) => Promise<void>;
|
||||
addCategory: (name: string, group: CategoryGroup, color?: string) => void;
|
||||
ensureCategory: (name: string, group: CategoryGroup, options?: { allowCreate?: boolean; fallbackCode?: string }) => TaskCategory;
|
||||
updateCategory: (id: string, data: Partial<TaskCategory>) => void;
|
||||
@@ -26,10 +29,14 @@ interface TaskCategoryState {
|
||||
|
||||
export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
|
||||
categories: PRESET_CATEGORIES,
|
||||
loaded: false,
|
||||
|
||||
fetchCategories: async () => {
|
||||
fetchCategories: async (options) => {
|
||||
if (!options?.force && get().loaded && Date.now() - lastCategoriesFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const cached = await loadStored();
|
||||
if (cached) set({ categories: normalizeTaskCategories(cached) });
|
||||
if (!options?.force && get().loaded && Date.now() - lastCategoriesFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastCategoriesFetchAt = Date.now();
|
||||
set({ categories: cached ? normalizeTaskCategories(cached) : PRESET_CATEGORIES, loaded: true });
|
||||
},
|
||||
|
||||
addCategory: (name, group, color) => {
|
||||
@@ -44,14 +51,14 @@ export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
|
||||
isSystem: false,
|
||||
};
|
||||
const updated = [...list, item];
|
||||
set({ categories: updated });
|
||||
set({ categories: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
ensureCategory: (name, group, options) => {
|
||||
const result = resolveAiTaskCategoryByName(get().categories, name, group, options);
|
||||
if (result.created) {
|
||||
set({ categories: result.categories });
|
||||
set({ categories: result.categories, loaded: true });
|
||||
saveStored(result.categories);
|
||||
}
|
||||
return result.category;
|
||||
@@ -59,7 +66,7 @@ export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
|
||||
|
||||
updateCategory: (id, data) => {
|
||||
const updated = get().categories.map((c) => (c.id === id ? { ...c, ...data } : c));
|
||||
set({ categories: updated });
|
||||
set({ categories: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
@@ -67,7 +74,7 @@ export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
|
||||
const target = get().categories.find((c) => c.id === id);
|
||||
if (!target || target.isSystem) return false;
|
||||
const updated = get().categories.filter((c) => c.id !== id);
|
||||
set({ categories: updated });
|
||||
set({ categories: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import type { TaskWorklog } from '@/lib/task-worklog';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
|
||||
function saveStored(items: TaskWorklog[]) {
|
||||
saveServerData('task-worklogs', items).catch(() => {});
|
||||
}
|
||||
|
||||
let lastWorklogsFetchAt = 0;
|
||||
|
||||
async function loadStored(): Promise<TaskWorklog[] | null> {
|
||||
try {
|
||||
return await loadServerData<TaskWorklog[]>('task-worklogs');
|
||||
@@ -16,7 +18,8 @@ async function loadStored(): Promise<TaskWorklog[] | null> {
|
||||
|
||||
interface TaskWorklogState {
|
||||
worklogs: TaskWorklog[];
|
||||
fetchWorklogs: () => Promise<void>;
|
||||
loaded: boolean;
|
||||
fetchWorklogs: (options?: { force?: boolean }) => Promise<void>;
|
||||
addWorklog: (data: Omit<TaskWorklog, 'id' | 'createdAt'>) => void;
|
||||
deleteWorklog: (id: string) => void;
|
||||
getActualHours: (taskId: string) => number;
|
||||
@@ -24,10 +27,14 @@ interface TaskWorklogState {
|
||||
|
||||
export const useTaskWorklogStore = create<TaskWorklogState>((set, get) => ({
|
||||
worklogs: [],
|
||||
loaded: false,
|
||||
|
||||
fetchWorklogs: async () => {
|
||||
fetchWorklogs: async (options) => {
|
||||
if (!options?.force && get().loaded && Date.now() - lastWorklogsFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const cached = await loadStored();
|
||||
if (cached) set({ worklogs: cached });
|
||||
if (!options?.force && get().loaded && Date.now() - lastWorklogsFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastWorklogsFetchAt = Date.now();
|
||||
set({ worklogs: cached ?? [], loaded: true });
|
||||
},
|
||||
|
||||
addWorklog: (data) => {
|
||||
@@ -37,13 +44,13 @@ export const useTaskWorklogStore = create<TaskWorklogState>((set, get) => ({
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const updated = [...get().worklogs, item];
|
||||
set({ worklogs: updated });
|
||||
set({ worklogs: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
deleteWorklog: (id) => {
|
||||
const updated = get().worklogs.filter((w) => w.id !== id);
|
||||
set({ worklogs: updated });
|
||||
set({ worklogs: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { CreateTestCaseInput, TestCase, TestCaseStatus } from '@/lib/test-c
|
||||
import { generateCaseNo, normalizeTestCases } from '@/lib/test-case';
|
||||
import { applyTestCaseTransition, normalizeTestCaseOnCreate } from '@/lib/test-case-workflow';
|
||||
import { createEntityId, dedupeEntityIds } from '@/lib/entity-id';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
import {
|
||||
makeTestCaseCreatedActivity,
|
||||
makeTestCaseStatusActivity,
|
||||
@@ -15,6 +15,8 @@ function saveStored(items: TestCase[]) {
|
||||
saveServerData('test-cases', items).catch(() => {});
|
||||
}
|
||||
|
||||
let lastTestCasesFetchAt = 0;
|
||||
|
||||
async function loadStored(): Promise<TestCase[] | null> {
|
||||
try {
|
||||
return await loadServerData<TestCase[]>('test-cases');
|
||||
@@ -24,7 +26,8 @@ async function loadStored(): Promise<TestCase[] | null> {
|
||||
|
||||
interface TestCaseState {
|
||||
testCases: TestCase[];
|
||||
fetchTestCases: () => Promise<void>;
|
||||
loaded: boolean;
|
||||
fetchTestCases: (options?: { force?: boolean }) => Promise<void>;
|
||||
createTestCase: (data: CreateTestCaseInput) => TestCase;
|
||||
createTestCases: (items: CreateTestCaseInput[]) => TestCase[];
|
||||
updateTestCase: (id: string, data: Partial<TestCase>) => void;
|
||||
@@ -37,13 +40,19 @@ interface TestCaseState {
|
||||
|
||||
export const useTestCaseStore = create<TestCaseState>((set, get) => ({
|
||||
testCases: [],
|
||||
loaded: false,
|
||||
|
||||
fetchTestCases: async () => {
|
||||
fetchTestCases: async (options) => {
|
||||
if (!options?.force && get().loaded && Date.now() - lastTestCasesFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const cached = await loadStored();
|
||||
if (!options?.force && get().loaded && Date.now() - lastTestCasesFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastTestCasesFetchAt = Date.now();
|
||||
if (cached) {
|
||||
const result = dedupeEntityIds(normalizeTestCases(cached), 'tc');
|
||||
if (result.changed) saveStored(result.items);
|
||||
set({ testCases: result.items });
|
||||
set({ testCases: result.items, loaded: true });
|
||||
} else {
|
||||
set({ loaded: true });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -59,7 +68,7 @@ export const useTestCaseStore = create<TestCaseState>((set, get) => ({
|
||||
updatedAt: now,
|
||||
} as TestCase);
|
||||
const updated = [...list, tc];
|
||||
set({ testCases: updated });
|
||||
set({ testCases: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
useWorkActivityStore.getState().addActivity(makeTestCaseCreatedActivity(tc, tc.createdBy));
|
||||
return tc;
|
||||
@@ -82,7 +91,7 @@ export const useTestCaseStore = create<TestCaseState>((set, get) => ({
|
||||
created.push(tc);
|
||||
list = [...list, tc];
|
||||
}
|
||||
set({ testCases: list });
|
||||
set({ testCases: list, loaded: true });
|
||||
saveStored(list);
|
||||
created.forEach((tc) => {
|
||||
useWorkActivityStore.getState().addActivity(makeTestCaseCreatedActivity(tc, tc.createdBy));
|
||||
@@ -94,13 +103,13 @@ export const useTestCaseStore = create<TestCaseState>((set, get) => ({
|
||||
const updated = get().testCases.map((c) =>
|
||||
c.id === id ? { ...c, ...data, aiDraft: false, updatedAt: new Date().toISOString() } : c,
|
||||
);
|
||||
set({ testCases: updated });
|
||||
set({ testCases: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
deleteTestCase: (id) => {
|
||||
const updated = get().testCases.filter((c) => c.id !== id);
|
||||
set({ testCases: updated });
|
||||
set({ testCases: updated, loaded: true });
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import type { VersionPlan, PlanType } from '@/lib/version-plan';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
import { getPlanCompletionState } from '@/lib/version-plan-workflow';
|
||||
import type { PlanResultPayload } from '@/lib/version-plan-workflow';
|
||||
import {
|
||||
@@ -15,6 +15,7 @@ import type { WorkActivityDraft } from '@/lib/work-activity';
|
||||
import { useWorkActivityStore } from './useWorkActivityStore';
|
||||
|
||||
const MOCK_PLANS: VersionPlan[] = [];
|
||||
let lastPlansFetchAt = 0;
|
||||
|
||||
function saveStored(plans: VersionPlan[]) {
|
||||
saveServerData('version-plans', plans).catch(() => {});
|
||||
@@ -29,7 +30,8 @@ async function loadStored(): Promise<VersionPlan[] | null> {
|
||||
|
||||
interface VersionPlanState {
|
||||
plans: VersionPlan[];
|
||||
fetchPlans: () => Promise<void>;
|
||||
loaded: boolean;
|
||||
fetchPlans: (options?: { force?: boolean }) => Promise<void>;
|
||||
createPlan: (data: Omit<VersionPlan, 'id' | 'createdAt'>) => void;
|
||||
updatePlan: (id: string, data: Partial<VersionPlan>) => void;
|
||||
completePlan: (id: string, result: PlanResultPayload) => { ok: boolean; message?: string };
|
||||
@@ -38,16 +40,20 @@ interface VersionPlanState {
|
||||
|
||||
export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
|
||||
plans: MOCK_PLANS,
|
||||
loaded: false,
|
||||
|
||||
fetchPlans: async () => {
|
||||
fetchPlans: async (options) => {
|
||||
if (!options?.force && get().loaded && Date.now() - lastPlansFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const cached = await loadStored();
|
||||
if (cached) set({ plans: cached });
|
||||
if (!options?.force && get().loaded && Date.now() - lastPlansFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastPlansFetchAt = Date.now();
|
||||
set({ plans: cached ?? MOCK_PLANS, loaded: true });
|
||||
},
|
||||
|
||||
createPlan: (data) => {
|
||||
const plan: VersionPlan = { ...data, id: `plan-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const plans = [...get().plans, plan];
|
||||
set({ plans });
|
||||
set({ plans, loaded: true });
|
||||
saveStored(plans);
|
||||
useWorkActivityStore.getState().addActivity(makeVersionPlanCreatedActivity(plan, plan.addedBy || plan.owner));
|
||||
},
|
||||
@@ -81,7 +87,7 @@ export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
|
||||
}
|
||||
return next;
|
||||
});
|
||||
set({ plans });
|
||||
set({ plans, loaded: true });
|
||||
saveStored(plans);
|
||||
activities.forEach((activity) => useWorkActivityStore.getState().addActivity(activity));
|
||||
},
|
||||
@@ -100,14 +106,14 @@ export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
|
||||
useWorkActivityStore.getState().addActivity(makeVersionPlanCompletedActivity(next, next.owner));
|
||||
return next;
|
||||
});
|
||||
set({ plans });
|
||||
set({ plans, loaded: true });
|
||||
saveStored(plans);
|
||||
return response;
|
||||
},
|
||||
|
||||
deletePlan: (id) => {
|
||||
const plans = get().plans.filter((p) => p.id !== id);
|
||||
set({ plans });
|
||||
set({ plans, loaded: true });
|
||||
saveStored(plans);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -3,16 +3,19 @@ import { create } from 'zustand';
|
||||
import { formatLocalDate } from '@/lib/format';
|
||||
import { mergeWorkActivities, type WorkActivity, type WorkActivityDraft } from '@/lib/work-activity';
|
||||
import { makeProgressNoteActivity, type ProgressNoteActivityInput } from '@/lib/work-activity-factory';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
|
||||
interface WorkActivityState {
|
||||
activities: WorkActivity[];
|
||||
fetchActivities: () => Promise<void>;
|
||||
loaded: boolean;
|
||||
fetchActivities: (options?: { force?: boolean }) => Promise<void>;
|
||||
addActivity: (data: WorkActivityDraft) => WorkActivity;
|
||||
addProgressNote: (data: ProgressNoteActivityInput) => WorkActivity;
|
||||
deleteActivity: (id: string) => void;
|
||||
}
|
||||
|
||||
let lastActivitiesFetchAt = 0;
|
||||
|
||||
async function saveStored(
|
||||
items: WorkActivity[],
|
||||
setActivities?: (items: WorkActivity[]) => void,
|
||||
@@ -23,7 +26,7 @@ async function saveStored(
|
||||
await saveServerData('work-activities', items);
|
||||
return;
|
||||
}
|
||||
const remote = await loadServerData<WorkActivity[]>('work-activities');
|
||||
const remote = await loadServerData<WorkActivity[]>('work-activities', { force: true });
|
||||
const merged = mergeWorkActivities(Array.isArray(remote) ? remote : [], items);
|
||||
setActivities?.(merged);
|
||||
await saveServerData('work-activities', merged);
|
||||
@@ -45,10 +48,14 @@ function createActivityId(): string {
|
||||
|
||||
export const useWorkActivityStore = create<WorkActivityState>((set, get) => ({
|
||||
activities: [],
|
||||
loaded: false,
|
||||
|
||||
fetchActivities: async () => {
|
||||
fetchActivities: async (options) => {
|
||||
if (!options?.force && get().loaded && Date.now() - lastActivitiesFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const cached = await loadStored();
|
||||
if (cached) set({ activities: cached });
|
||||
if (!options?.force && get().loaded && Date.now() - lastActivitiesFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastActivitiesFetchAt = Date.now();
|
||||
set({ activities: cached ?? [], loaded: true });
|
||||
},
|
||||
|
||||
addActivity: (data) => {
|
||||
@@ -60,8 +67,8 @@ export const useWorkActivityStore = create<WorkActivityState>((set, get) => ({
|
||||
occurredAt: data.occurredAt ?? now.toISOString(),
|
||||
};
|
||||
const updated = [...get().activities, item];
|
||||
set({ activities: updated });
|
||||
saveStored(updated, (activities) => set({ activities }));
|
||||
set({ activities: updated, loaded: true });
|
||||
saveStored(updated, (activities) => set({ activities, loaded: true }));
|
||||
return item;
|
||||
},
|
||||
|
||||
@@ -71,7 +78,7 @@ export const useWorkActivityStore = create<WorkActivityState>((set, get) => ({
|
||||
|
||||
deleteActivity: (id) => {
|
||||
const updated = get().activities.filter((activity) => activity.id !== id);
|
||||
set({ activities: updated });
|
||||
saveStored(updated, (activities) => set({ activities }), { mergeRemote: false });
|
||||
set({ activities: updated, loaded: true });
|
||||
saveStored(updated, (activities) => set({ activities, loaded: true }), { mergeRemote: false });
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
import {
|
||||
mergeDailySnapshotCacheForSave,
|
||||
mergeInsightCacheForSave,
|
||||
@@ -42,6 +42,7 @@ async function loadInsights(): Promise<XiaobaoRiskInsightCacheItem[] | null> {
|
||||
|
||||
let snapshotSaveQueue: Promise<void> = Promise.resolve();
|
||||
let insightSaveQueue: Promise<void> = Promise.resolve();
|
||||
let lastRiskDataFetchAt = 0;
|
||||
|
||||
export const useXiaobaoRiskStore = create<XiaobaoRiskState>((set, get) => ({
|
||||
snapshots: [],
|
||||
@@ -52,8 +53,11 @@ export const useXiaobaoRiskStore = create<XiaobaoRiskState>((set, get) => ({
|
||||
error: undefined,
|
||||
|
||||
fetchRiskData: async () => {
|
||||
if (get().riskDataLoaded && Date.now() - lastRiskDataFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
set({ riskDataLoaded: false });
|
||||
const [snapshots, insights] = await Promise.all([loadSnapshots(), loadInsights()]);
|
||||
if (get().riskDataLoaded && Date.now() - lastRiskDataFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastRiskDataFetchAt = Date.now();
|
||||
set({
|
||||
...(snapshots ? { snapshots } : {}),
|
||||
...(insights ? { insights } : {}),
|
||||
@@ -66,7 +70,7 @@ export const useXiaobaoRiskStore = create<XiaobaoRiskState>((set, get) => ({
|
||||
const optimistic = upsertDailySnapshot(get().snapshots, item);
|
||||
set({ snapshots: optimistic, error: undefined });
|
||||
const task = snapshotSaveQueue.then(async () => {
|
||||
const remote = await loadServerData<XiaobaoRiskSnapshot[]>('xiaobao-risk-snapshots');
|
||||
const remote = await loadServerData<XiaobaoRiskSnapshot[]>('xiaobao-risk-snapshots', { force: true });
|
||||
const snapshots = mergeDailySnapshotCacheForSave(get().snapshots, Array.isArray(remote) ? remote : [], item);
|
||||
set({ snapshots, error: undefined });
|
||||
await saveServerData('xiaobao-risk-snapshots', snapshots);
|
||||
@@ -84,7 +88,7 @@ export const useXiaobaoRiskStore = create<XiaobaoRiskState>((set, get) => ({
|
||||
const optimistic = upsertInsight(get().insights, item);
|
||||
set({ insights: optimistic, error: undefined });
|
||||
const task = insightSaveQueue.then(async () => {
|
||||
const remote = await loadServerData<XiaobaoRiskInsightCacheItem[]>('xiaobao-risk-insights');
|
||||
const remote = await loadServerData<XiaobaoRiskInsightCacheItem[]>('xiaobao-risk-insights', { force: true });
|
||||
const insights = mergeInsightCacheForSave(get().insights, Array.isArray(remote) ? remote : [], item);
|
||||
set({ insights, error: undefined });
|
||||
await saveServerData('xiaobao-risk-insights', insights);
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
markXiaobaoWarningRead,
|
||||
type XiaobaoWarningReadState,
|
||||
} from '@/lib/xiaobao-warning-view';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
|
||||
interface XiaobaoWarningReadStoreState {
|
||||
readStates: XiaobaoWarningReadState[];
|
||||
@@ -17,6 +17,7 @@ interface XiaobaoWarningReadStoreState {
|
||||
}
|
||||
|
||||
let readStateSaveQueue: Promise<void> = Promise.resolve();
|
||||
let lastReadStatesFetchAt = 0;
|
||||
|
||||
async function loadReadStates(): Promise<XiaobaoWarningReadState[] | null> {
|
||||
try {
|
||||
@@ -63,7 +64,10 @@ export const useXiaobaoWarningReadStore = create<XiaobaoWarningReadStoreState>((
|
||||
error: undefined,
|
||||
|
||||
fetchReadStates: async () => {
|
||||
if (get().readStateLoaded && Date.now() - lastReadStatesFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const rows = await loadReadStates();
|
||||
if (get().readStateLoaded && Date.now() - lastReadStatesFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastReadStatesFetchAt = Date.now();
|
||||
set({
|
||||
readStates: rows ? mergeReadStates(rows, get().readStates) : get().readStates,
|
||||
readStateLoaded: rows !== null,
|
||||
@@ -77,7 +81,7 @@ export const useXiaobaoWarningReadStore = create<XiaobaoWarningReadStoreState>((
|
||||
set({ readStates: optimistic, error: undefined });
|
||||
|
||||
const task = readStateSaveQueue.then(async () => {
|
||||
const remote = await loadServerData<XiaobaoWarningReadState[]>('xiaobao-warning-views');
|
||||
const remote = await loadServerData<XiaobaoWarningReadState[]>('xiaobao-warning-views', { force: true });
|
||||
const merged = mergeReadStates(normalizeReadStates(remote), get().readStates);
|
||||
set({ readStates: merged, readStateLoaded: true, error: undefined });
|
||||
await saveServerData('xiaobao-warning-views', merged);
|
||||
|
||||
Reference in New Issue
Block a user