Files
ftb-project-management/apps/web/stores/useWorkActivityStore.ts
2026-07-08 15:41:39 +08:00

119 lines
4.0 KiB
TypeScript

'use client';
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 {
createWorkActivityDomain,
deleteWorkActivityDomain,
listWorkActivitiesDomain,
} from '@/lib/domain-api';
import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence';
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
interface WorkActivityState {
activities: WorkActivity[];
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,
options: { mergeRemote?: boolean } = {},
) {
try {
if (options.mergeRemote === false) {
await saveServerData('work-activities', items);
return;
}
const remote = await loadServerData<WorkActivity[]>('work-activities', { force: true });
const merged = mergeWorkActivities(Array.isArray(remote) ? remote : [], items);
setActivities?.(merged);
await saveServerData('work-activities', merged);
} catch {
saveServerData('work-activities', items).catch(() => {});
}
}
async function loadStored(): Promise<WorkActivity[] | null> {
try {
return await loadServerData<WorkActivity[]>('work-activities');
} catch {}
return null;
}
function createActivityId(): string {
return `act-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
}
export const useWorkActivityStore = create<WorkActivityState>((set, get) => ({
activities: [],
loaded: false,
fetchActivities: async (options) => {
if (!options?.force && get().loaded && Date.now() - lastActivitiesFetchAt < SERVER_DATA_CACHE_MS) return;
const cached = await listWorkActivitiesDomain().catch(loadStored);
if (!options?.force && get().loaded && Date.now() - lastActivitiesFetchAt < SERVER_DATA_CACHE_MS) return;
lastActivitiesFetchAt = Date.now();
set({ activities: cached ?? [], loaded: true });
},
addActivity: (data) => {
const previous = get().activities;
const now = new Date();
const item: WorkActivity = {
...data,
id: createActivityId(),
date: data.date ?? formatLocalDate(now),
occurredAt: data.occurredAt ?? now.toISOString(),
};
const updated = [...previous, item];
set({ activities: updated, loaded: true });
scheduleSaveWithOptimisticRollback({
save: async () => {
try {
const created = await createWorkActivityDomain(item);
set({
activities: get().activities.map((activity) => (activity.id === item.id ? { ...item, ...created } : activity)),
loaded: true,
});
} catch {
await saveStored(updated, (activities) => set({ activities, loaded: true }));
}
},
expected: updated,
getCurrent: () => get().activities,
rollback: () => set({ activities: previous, loaded: true }),
});
return item;
},
addProgressNote: (data) => {
return get().addActivity(makeProgressNoteActivity(data));
},
deleteActivity: (id) => {
const previous = get().activities;
const updated = previous.filter((activity) => activity.id !== id);
set({ activities: updated, loaded: true });
scheduleSaveWithOptimisticRollback({
save: async () => {
try {
await deleteWorkActivityDomain(id);
} catch {
await saveStored(updated, (activities) => set({ activities, loaded: true }), { mergeRemote: false });
}
},
expected: updated,
getCurrent: () => get().activities,
rollback: () => set({ activities: previous, loaded: true }),
});
},
}));