feat(workspace): 增加工作活动日报引擎
This commit is contained in:
@@ -4,6 +4,12 @@ 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 {
|
||||
makeBugCreatedActivity,
|
||||
makeBugStatusActivity,
|
||||
makeBugTransferredActivity,
|
||||
} from '@/lib/work-activity-factory';
|
||||
import { useWorkActivityStore } from './useWorkActivityStore';
|
||||
|
||||
function saveStored(items: Bug[]) {
|
||||
saveServerData('bugs', items).catch(() => {});
|
||||
@@ -57,6 +63,7 @@ export const useBugStore = create<BugState>((set, get) => ({
|
||||
const updated = [...list, bug];
|
||||
set({ bugs: updated });
|
||||
saveStored(updated);
|
||||
useWorkActivityStore.getState().addActivity(makeBugCreatedActivity(bug, operator));
|
||||
return bug;
|
||||
},
|
||||
|
||||
@@ -83,6 +90,8 @@ export const useBugStore = create<BugState>((set, get) => ({
|
||||
});
|
||||
if (!result.ok || !result.patch) return { ok: false, message: result.message };
|
||||
get().updateBug(id, result.patch);
|
||||
const activity = makeBugStatusActivity(bug, bug.status, to, operator);
|
||||
if (activity) useWorkActivityStore.getState().addActivity(activity);
|
||||
return { ok: true };
|
||||
},
|
||||
|
||||
@@ -92,6 +101,7 @@ export const useBugStore = create<BugState>((set, get) => ({
|
||||
if (bug.assigneeId === newAssigneeId) return { ok: false, message: '已是当前负责人' };
|
||||
const log = makeLog('transfer', operator, bug.assigneeId, newAssigneeId, remark);
|
||||
get().updateBug(id, { assigneeId: newAssigneeId, logs: [...(bug.logs || []), log] });
|
||||
useWorkActivityStore.getState().addActivity(makeBugTransferredActivity(bug, operator, newAssigneeId));
|
||||
return { ok: true };
|
||||
},
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ 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 {
|
||||
makeDevTaskBlockedActivity,
|
||||
makeDevTaskCreatedActivity,
|
||||
makeDevTaskStatusActivity,
|
||||
makeDevTaskUnblockedActivity,
|
||||
} from '@/lib/work-activity-factory';
|
||||
import { useWorkActivityStore } from './useWorkActivityStore';
|
||||
|
||||
function saveStored(items: DevTask[]) {
|
||||
saveServerData('dev-tasks', items).catch(() => {});
|
||||
@@ -60,6 +67,7 @@ export const useDevTaskStore = create<DevTaskState>((set, get) => ({
|
||||
const updated = [...list, task];
|
||||
set({ tasks: updated });
|
||||
saveStored(updated);
|
||||
useWorkActivityStore.getState().addActivity(makeDevTaskCreatedActivity(task, task.createdBy || task.assigneeId));
|
||||
return task;
|
||||
},
|
||||
|
||||
@@ -86,15 +94,23 @@ export const useDevTaskStore = create<DevTaskState>((set, get) => ({
|
||||
});
|
||||
if (!result.ok || !result.patch) return { ok: false, message: result.message };
|
||||
get().updateTask(id, result.patch);
|
||||
const activity = makeDevTaskStatusActivity(task, task.status, to, task.assigneeId);
|
||||
if (activity) useWorkActivityStore.getState().addActivity(activity);
|
||||
return { ok: true };
|
||||
},
|
||||
|
||||
setBlocked: (id, blocked, reason, blockedById) => {
|
||||
const task = get().tasks.find((t) => t.id === id);
|
||||
if (!task) return;
|
||||
get().updateTask(id, {
|
||||
isBlocked: blocked,
|
||||
blockReason: blocked ? reason : undefined,
|
||||
blockedById: blocked ? blockedById : undefined,
|
||||
});
|
||||
const activity = blocked
|
||||
? makeDevTaskBlockedActivity(task, task.assigneeId, reason, blockedById)
|
||||
: makeDevTaskUnblockedActivity(task, task.assigneeId);
|
||||
useWorkActivityStore.getState().addActivity(activity);
|
||||
},
|
||||
|
||||
getByRequirement: (requirementId) => {
|
||||
|
||||
@@ -5,6 +5,11 @@ 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 {
|
||||
makeTestCaseCreatedActivity,
|
||||
makeTestCaseStatusActivity,
|
||||
} from '@/lib/work-activity-factory';
|
||||
import { useWorkActivityStore } from './useWorkActivityStore';
|
||||
|
||||
function saveStored(items: TestCase[]) {
|
||||
saveServerData('test-cases', items).catch(() => {});
|
||||
@@ -56,6 +61,7 @@ export const useTestCaseStore = create<TestCaseState>((set, get) => ({
|
||||
const updated = [...list, tc];
|
||||
set({ testCases: updated });
|
||||
saveStored(updated);
|
||||
useWorkActivityStore.getState().addActivity(makeTestCaseCreatedActivity(tc, tc.createdBy));
|
||||
return tc;
|
||||
},
|
||||
|
||||
@@ -78,6 +84,9 @@ export const useTestCaseStore = create<TestCaseState>((set, get) => ({
|
||||
}
|
||||
set({ testCases: list });
|
||||
saveStored(list);
|
||||
created.forEach((tc) => {
|
||||
useWorkActivityStore.getState().addActivity(makeTestCaseCreatedActivity(tc, tc.createdBy));
|
||||
});
|
||||
return created;
|
||||
},
|
||||
|
||||
@@ -105,6 +114,9 @@ export const useTestCaseStore = create<TestCaseState>((set, get) => ({
|
||||
});
|
||||
if (!result.ok || !result.patch) return { ok: false, message: result.message };
|
||||
get().updateTestCase(id, result.patch);
|
||||
const actorId = tc.assigneeId || tc.executedBy || tc.createdBy;
|
||||
const activity = makeTestCaseStatusActivity(tc, tc.status, to, actorId);
|
||||
if (activity) useWorkActivityStore.getState().addActivity(activity);
|
||||
return { ok: true };
|
||||
},
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@ import type { VersionPlan, PlanType } from '@/lib/version-plan';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
import { getPlanCompletionState } from '@/lib/version-plan-workflow';
|
||||
import type { PlanResultPayload } from '@/lib/version-plan-workflow';
|
||||
import {
|
||||
makeVersionPlanCompletedActivity,
|
||||
makeVersionPlanCreatedActivity,
|
||||
makeVersionPlanStartedActivity,
|
||||
} from '@/lib/work-activity-factory';
|
||||
import { useWorkActivityStore } from './useWorkActivityStore';
|
||||
|
||||
const MOCK_PLANS: VersionPlan[] = [];
|
||||
|
||||
@@ -40,10 +46,12 @@ export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
|
||||
const plans = [...get().plans, plan];
|
||||
set({ plans });
|
||||
saveStored(plans);
|
||||
useWorkActivityStore.getState().addActivity(makeVersionPlanCreatedActivity(plan, plan.addedBy || plan.owner));
|
||||
},
|
||||
|
||||
updatePlan: (id, data) => {
|
||||
const now = new Date().toISOString();
|
||||
const activities: ReturnType<typeof makeVersionPlanStartedActivity>[] = [];
|
||||
const plans = get().plans.map((p) => {
|
||||
if (p.id !== id) return p;
|
||||
const patch = { ...data };
|
||||
@@ -53,10 +61,18 @@ export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
|
||||
if (patch.status === 'completed' && !p.completedAt) {
|
||||
(patch as any).completedAt = now;
|
||||
}
|
||||
return { ...p, ...patch };
|
||||
const next = { ...p, ...patch };
|
||||
if (p.status !== 'in_progress' && next.status === 'in_progress') {
|
||||
activities.push(makeVersionPlanStartedActivity(next, next.owner));
|
||||
}
|
||||
if (p.status !== 'completed' && next.status === 'completed') {
|
||||
activities.push(makeVersionPlanCompletedActivity(next, next.owner));
|
||||
}
|
||||
return next;
|
||||
});
|
||||
set({ plans });
|
||||
saveStored(plans);
|
||||
activities.forEach((activity) => useWorkActivityStore.getState().addActivity(activity));
|
||||
},
|
||||
|
||||
completePlan: (id, result) => {
|
||||
@@ -70,6 +86,7 @@ export const useVersionPlanStore = create<VersionPlanState>((set, get) => ({
|
||||
return p;
|
||||
}
|
||||
response = { ok: true };
|
||||
useWorkActivityStore.getState().addActivity(makeVersionPlanCompletedActivity(next, next.owner));
|
||||
return next;
|
||||
});
|
||||
set({ plans });
|
||||
|
||||
108
apps/web/stores/useWorkActivityStore.ts
Normal file
108
apps/web/stores/useWorkActivityStore.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import { formatLocalDate } from '@/lib/format';
|
||||
import { mergeWorkActivities, type WorkActivity, type WorkActivityDraft, type WorkActivitySourceType } from '@/lib/work-activity';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
|
||||
interface ProgressNoteInput {
|
||||
actorId: string;
|
||||
sourceType: WorkActivitySourceType;
|
||||
sourceId: string;
|
||||
title: string;
|
||||
note: string;
|
||||
blocker?: string;
|
||||
helperId?: string;
|
||||
delayRisk?: string;
|
||||
}
|
||||
|
||||
interface WorkActivityState {
|
||||
activities: WorkActivity[];
|
||||
fetchActivities: () => Promise<void>;
|
||||
addActivity: (data: WorkActivityDraft) => WorkActivity;
|
||||
addProgressNote: (data: ProgressNoteInput) => WorkActivity;
|
||||
deleteActivity: (id: string) => void;
|
||||
}
|
||||
|
||||
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');
|
||||
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: [],
|
||||
|
||||
fetchActivities: async () => {
|
||||
const cached = await loadStored();
|
||||
if (cached) set({ activities: cached });
|
||||
},
|
||||
|
||||
addActivity: (data) => {
|
||||
const now = new Date();
|
||||
const item: WorkActivity = {
|
||||
...data,
|
||||
id: createActivityId(),
|
||||
date: data.date ?? formatLocalDate(now),
|
||||
occurredAt: data.occurredAt ?? now.toISOString(),
|
||||
};
|
||||
const updated = [...get().activities, item];
|
||||
set({ activities: updated });
|
||||
saveStored(updated, (activities) => set({ activities }));
|
||||
return item;
|
||||
},
|
||||
|
||||
addProgressNote: (data) => {
|
||||
const details = [
|
||||
data.note.trim(),
|
||||
data.blocker?.trim() ? `阻塞:${data.blocker.trim()}` : '',
|
||||
data.helperId?.trim() ? `需协助:${data.helperId.trim()}` : '',
|
||||
data.delayRisk?.trim() ? `延期风险:${data.delayRisk.trim()}` : '',
|
||||
].filter(Boolean);
|
||||
|
||||
return get().addActivity({
|
||||
actorId: data.actorId,
|
||||
sourceType: data.sourceType,
|
||||
sourceId: data.sourceId,
|
||||
action: 'progress_note_added',
|
||||
category: data.blocker?.trim() || data.delayRisk?.trim() ? 'risk' : 'note',
|
||||
title: data.title,
|
||||
summary: `补充进展:${details.join(';')}`,
|
||||
metadata: {
|
||||
note: data.note.trim(),
|
||||
blocker: data.blocker?.trim() || undefined,
|
||||
helperId: data.helperId?.trim() || undefined,
|
||||
delayRisk: data.delayRisk?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
deleteActivity: (id) => {
|
||||
const updated = get().activities.filter((activity) => activity.id !== id);
|
||||
set({ activities: updated });
|
||||
saveStored(updated, (activities) => set({ activities }), { mergeRemote: false });
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user