refactor(workbench): simplify appointment tasks
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user