refactor(workbench): simplify appointment tasks

This commit is contained in:
2026-09-24 12:01:11 +08:00
parent 436d5500f1
commit ddf953b641
28 changed files with 443 additions and 1384 deletions
@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";
import type { OperationalTask } from "../api/operational-task-client";
import { toOperationalTaskCondition } from "./use-operational-tasks";
import type { WorkbenchEvent } from "../api/workbench-sync-client";
import {
collectOperationalTaskEventChanges,
toOperationalTaskCondition
} from "./use-operational-tasks";
function task(taskType: OperationalTask["task_type"]): OperationalTask {
return {
@@ -69,7 +73,8 @@ function task(taskType: OperationalTask["task_type"]): OperationalTask {
work_order_type: "general",
location: "测试点",
scenario_code: null,
subtasks: null,
feedback_requirements: [],
activity: [],
origin: "synthetic",
updated_at: "2026-09-23T09:00:00Z"
}
@@ -99,3 +104,38 @@ describe("operational task condition mapper", () => {
expect(toOperationalTaskCondition(task("event_investigation")).detail).toBe("初次同步时的旧描述");
});
});
describe("operational task event invalidation", () => {
it("removes deleted tasks without requesting their missing details", () => {
const deletedTaskId = "22222222-2222-4222-8222-222222222222";
const changedTaskId = "33333333-3333-4333-8333-333333333333";
const changes = collectOperationalTaskEventChanges([
event(1, deletedTaskId, { change: "deleted" }),
event(2, deletedTaskId, { task_id: deletedTaskId }),
event(3, changedTaskId, { task_id: changedTaskId })
]);
expect([...changes.deletedTaskIds]).toEqual([deletedTaskId]);
expect([...changes.taskIds]).toEqual([changedTaskId]);
expect(changes.notificationsChanged).toBe(true);
});
});
function event(
eventSequence: number,
taskId: string,
metadata: Record<string, unknown>
): WorkbenchEvent {
return {
schema: "workbench-event@1",
event_seq: eventSequence,
event_id: `${eventSequence.toString().padStart(8, "0")}-0000-4000-8000-000000000000`,
project_id: "11111111-1111-4111-8111-111111111111",
topic: "operational.task.changed",
aggregate_type: "operational_task",
aggregate_id: taskId,
aggregate_revision: 1,
occurred_at: "2026-09-24T00:00:00Z",
metadata
};
}
@@ -15,6 +15,7 @@ import {
type OperationalTaskNotification
} from "../api/operational-task-client";
import type { ScheduledConditionRecord, ScheduledConditionRiskLevel, ScheduledConditionStatus } from "../types";
import type { WorkbenchEvent } from "../api/workbench-sync-client";
import type { WorkbenchSyncState } from "./use-workbench-sync";
@@ -30,6 +31,31 @@ export type CreateInvestigationInput = {
triggerKind?: "event" | "manual";
};
export function collectOperationalTaskEventChanges(events: WorkbenchEvent[]) {
const taskIds = new Set<string>();
const deletedTaskIds = new Set<string>();
let notificationsChanged = false;
for (const event of events) {
if (event.topic === "operational.notification.changed") {
notificationsChanged = true;
continue;
}
if (!event.topic.startsWith("operational.")) continue;
const taskId = typeof event.metadata.task_id === "string"
? event.metadata.task_id
: event.aggregate_type === "operational_task" ? event.aggregate_id : null;
if (!taskId) continue;
if (event.topic === "operational.task.changed" && event.metadata.change === "deleted") {
taskIds.delete(taskId);
deletedTaskIds.add(taskId);
notificationsChanged = true;
} else if (!deletedTaskIds.has(taskId)) {
taskIds.add(taskId);
}
}
return { taskIds, deletedTaskIds, notificationsChanged };
}
export function useOperationalTasks(
getAccessToken: AccessTokenProvider | undefined,
sync: WorkbenchSyncState
@@ -99,15 +125,11 @@ export function useOperationalTasks(
if (events.length === 0) return;
const controller = new AbortController();
const lastSequence = events.at(-1)!.event_seq;
const taskIds = new Set<string>();
let notificationsChanged = false;
for (const event of events) {
if (event.topic === "operational.notification.changed") notificationsChanged = true;
if (!event.topic.startsWith("operational.") || event.topic === "operational.notification.changed") continue;
const taskId = typeof event.metadata.task_id === "string"
? event.metadata.task_id
: event.aggregate_type === "operational_task" ? event.aggregate_id : null;
if (taskId) taskIds.add(taskId);
const { taskIds, deletedTaskIds, notificationsChanged } = collectOperationalTaskEventChanges(events);
if (deletedTaskIds.size > 0) {
tasksRef.current = tasksRef.current.filter((task) => !deletedTaskIds.has(task.task_id));
setTasks(tasksRef.current);
setNotifications((current) => current.filter((item) => !deletedTaskIds.has(item.task_id)));
}
void Promise.all([
notificationsChanged
@@ -47,27 +47,10 @@ const mockWorkOrder: ScheduledWorkOrderItem = {
source: "演示",
dispatcher: "调度中心",
priority: "normal",
subtasks: [{
id: "mock-subtask-1",
title: "现场复核",
summary: "复核设备状态并回传。",
status: "pending",
scheduledAt: "2026-09-17T11:00:00+08:00",
location: "V-1",
assignee: "现场班组",
durationMinutes: 30,
replyWindowMinutes: 15,
stages: [{
id: "dispatch",
sequence: 1,
title: "派发",
status: "current",
owner: "调度中心",
timeLabel: "10:45",
description: "派发现场复核任务。"
}],
replyRequirements: ["回传设备状态"]
}]
location: "V-1",
assignee: "现场班组",
feedbackRequirements: ["回传设备状态"],
activity: []
};
describe("workbench runtime data", () => {
@@ -109,13 +92,13 @@ describe("workbench runtime data", () => {
expect(resolved.workOrderDataSource).toBe("none");
});
it("keeps explicit demo work orders visible without treating them as backend fallback", () => {
it("does not use frontend demo work orders as a backend fallback", () => {
const demoWorkOrder = { ...mockWorkOrder, dataSource: "mock" as const };
const resolved = resolveWorkbenchRuntimeData([mockCondition, demoWorkOrder], null, {
operationalDate: SEPTEMBER_17
});
expect(resolved.scheduledConditions).toEqual([mockCondition, demoWorkOrder]);
expect(resolved.scheduledConditions).toEqual([mockCondition]);
expect(resolved.workOrderDataSource).toBe("none");
});
@@ -276,7 +259,7 @@ describe("workbench runtime data", () => {
expect(resolved.runtimeDataSource).toBe("mock");
});
it("keeps explicit demo appointments when an explicit backend day has no work orders", () => {
it("treats an empty configured backend work-order partition as authoritative", () => {
const resolved = resolveWorkbenchRuntimeData([mockCondition, mockWorkOrder], {
availability: "ready",
source: "none",
@@ -294,7 +277,7 @@ describe("workbench runtime data", () => {
workOrders: []
}, { operationalDate: SEPTEMBER_17, runtimeMode: "backend" });
expect(resolved.scheduledConditions).toEqual([mockCondition, mockWorkOrder]);
expect(resolved.scheduledConditions).toEqual([mockCondition]);
expect(resolved.runtimeDataSource).toBe("mock");
expect(resolved.workOrderDataSource).toBe("backend");
});
@@ -379,7 +362,7 @@ describe("workbench runtime data", () => {
expect(resolved.workbenchAlerts).toEqual([backendAlert]);
expect(resolved.alertDataSource).toBe("backend");
expect(resolved.scheduledConditions).toEqual([mockCondition, mockWorkOrder, backendWorkOrder]);
expect(resolved.scheduledConditions).toEqual([mockCondition, backendWorkOrder]);
expect(resolved.workOrderDataSource).toBe("backend");
});
});
@@ -80,10 +80,6 @@ export function resolveWorkbenchRuntimeData(
(condition) => condition.kind === "condition"
&& isSameLocalOperationalDate(condition.scheduledAt, operationalDate)
);
const demoWorkOrders = mockConditions.filter(
(condition) => condition.kind === "work_order"
&& isSameLocalOperationalDate(condition.scheduledAt, operationalDate)
);
const backendWorkOrders = (backendSnapshot?.workOrders ?? []).filter(
(workOrder) => isSameLocalOperationalDate(workOrder.scheduledAt, operationalDate)
);
@@ -107,7 +103,7 @@ export function resolveWorkbenchRuntimeData(
);
return {
scheduledConditions: [...fallbackConditions, ...demoWorkOrders, ...resolvedWorkOrders],
scheduledConditions: [...fallbackConditions, ...resolvedWorkOrders],
workbenchAlerts: backendAlertsConfigured
? backendSnapshot!.alerts
: createScheduledConditionAlerts(fallbackConditions),