From ddf953b64110978fba6261972bc60109c09e1b5b Mon Sep 17 00:00:00 2001 From: Huarch Date: Thu, 24 Sep 2026 12:01:11 +0800 Subject: [PATCH] refactor(workbench): simplify appointment tasks --- .../workbench/api/operational-task-client.ts | 37 +- .../api/workbench-runtime-client.test.ts | 58 +-- .../workbench/api/workbench-runtime-client.ts | 90 +---- .../scheduled-condition-detail-panel.test.tsx | 38 +- .../scheduled-condition-detail-panel.tsx | 193 +++------- .../scheduled-condition-feed-utils.test.ts | 124 +++---- .../scheduled-condition-feed-utils.ts | 66 +--- .../components/scheduled-condition-feed.tsx | 80 ++-- .../workbench/components/task-category.ts | 2 +- .../data/scheduled-conditions.test.ts | 39 +- .../workbench/data/scheduled-conditions.ts | 350 +----------------- .../hooks/use-operational-tasks.test.ts | 44 ++- .../workbench/hooks/use-operational-tasks.ts | 40 +- .../hooks/use-workbench-runtime-data.test.ts | 35 +- .../hooks/use-workbench-runtime-data.ts | 6 +- .../operational-timeline-model.test.ts | 30 +- .../operational-timeline-model.ts | 65 +--- .../peripheral-sample/client.test.ts | 7 +- .../peripheral-sample/generated/schema.d.ts | 93 ++--- .../managed-test-resources.tsx | 210 ++--------- .../work-order-agent-trigger.test.ts | 2 + src/features/workbench/types.ts | 34 +- tests/browser/mobile-workbench-sheet.e2e.ts | 9 +- .../browser/operational-task-approval.e2e.ts | 25 +- tests/browser/peripheral-sample-panel.e2e.ts | 40 +- .../scheduled-condition-acrylic.e2e.ts | 19 +- .../support/mock-hierarchical-work-order.ts | 38 +- .../browser/workbench-runtime-backend.e2e.ts | 53 +-- 28 files changed, 443 insertions(+), 1384 deletions(-) diff --git a/src/features/workbench/api/operational-task-client.ts b/src/features/workbench/api/operational-task-client.ts index e3f18f6..5235cda 100644 --- a/src/features/workbench/api/operational-task-client.ts +++ b/src/features/workbench/api/operational-task-client.ts @@ -156,29 +156,21 @@ const taskPermissionsSchema = z.object({ can_decide_commands: z.boolean(), can_execute_commands: z.boolean() }).strict(); -const externalWorkOrderStageSchema = z.object({ - id: z.string().min(1), +const externalWorkOrderActivitySchema = z.object({ sequence: z.number().int().positive(), + occurred_at: workbenchDateTimeSchema, + actor: z.string().min(1), + kind: z.enum(["created", "updated", "feedback"]), title: z.string().min(1), - status: z.enum(["done", "current", "pending", "blocked", "skipped"]), - owner: z.string().min(1), - time_label: z.string().min(1), - description: z.string().min(1) + detail: z.string().min(1) }).strict(); -const externalWorkOrderSubtaskSchema = z.object({ - id: z.string().min(1), - title: z.string().min(1), - summary: z.string().min(1), - status: z.enum(["pending", "running", "blocked", "completed", "cancelled"]), - scheduled_at: workbenchDateTimeSchema, - location: z.string().min(1), - assignee: z.string().min(1), - duration_minutes: z.number().int().positive(), - reply_window_minutes: z.number().int().nonnegative(), - stages: z.array(externalWorkOrderStageSchema).min(1), - reply_requirements: z.array(z.string().min(1)) -}).strict(); -const externalWorkOrderSnapshotSchema = z.object({ +const externalWorkOrderSnapshotSchema = z.preprocess((value) => { + if (value && typeof value === "object" && !Array.isArray(value)) { + const { subtasks: _legacySubtasks, ...flat } = value as Record; + return flat; + } + return value; +}, z.object({ work_order_id: z.string().uuid(), revision: z.number().int().positive(), title: z.string().min(1), @@ -190,10 +182,11 @@ const externalWorkOrderSnapshotSchema = z.object({ work_order_type: z.enum(["general", "water_quality_complaint", "burst_report"]), location: z.string().min(1).nullable(), scenario_code: z.string().min(1).nullable(), - subtasks: z.array(externalWorkOrderSubtaskSchema).min(1).nullable(), + feedback_requirements: z.array(z.string().min(1)).default([]), + activity: z.array(externalWorkOrderActivitySchema).default([]), origin: z.literal("synthetic"), updated_at: workbenchDateTimeSchema -}).strict(); +}).strict()); const operationalTaskSchema = taskSummarySchema.extend({ latest_plan: planSnapshotSchema.nullable(), latest_report: reportRevisionSchema.nullable(), diff --git a/src/features/workbench/api/workbench-runtime-client.test.ts b/src/features/workbench/api/workbench-runtime-client.test.ts index eb3b777..b2516b1 100644 --- a/src/features/workbench/api/workbench-runtime-client.test.ts +++ b/src/features/workbench/api/workbench-runtime-client.test.ts @@ -260,47 +260,11 @@ describe("workbench runtime API client", () => { source: "生产工单系统", dispatcher: "调度中心", priority: "urgent", - subtasks: [{ - id: "wo-1-subtask-1", - title: "阀门开度复核", - summary: "现场复核阀门开度。", - status: "completed", - scheduled_at: "2026-09-17T03:00:00Z", - location: "北区阀门 V-1", - assignee: "北区班组", - duration_minutes: 45, - reply_window_minutes: 20, - stages: [ - { - id: "dispatch", - sequence: 1, - title: "派发", - status: "done", - owner: "调度中心", - time_label: "10:50", - description: "已派发。" - }, - { - id: "execution", - sequence: 2, - title: "执行中", - status: "done", - owner: "北区班组", - time_label: "11:00—11:45", - description: "现场执行中。" - }, - { - id: "reply", - sequence: 3, - title: "待复令", - status: "done", - owner: "北区班组", - time_label: "11:45—12:05", - description: "等待复令。" - } - ], - reply_requirements: ["回传阀门开度"] - }] + location: "北区阀门 V-1", + assignee: "北区班组", + feedback_requirements: ["回传阀门开度"], + activity: [{ sequence: 1, occurred_at: "2026-09-17T02:30:00Z", actor: "调度中心", + kind: "created", title: "工单创建", detail: "等待现场复核。" }] }; const snapshot = parseWorkbenchRuntimeSnapshot({ ...runtimeSnapshot, @@ -315,14 +279,10 @@ describe("workbench runtime API client", () => { code: "WO-20260917-001", status: "completed", source: "生产工单系统", - subtasks: expect.arrayContaining([ - expect.objectContaining({ - id: "wo-1-subtask-1", - stages: expect.arrayContaining([ - expect.objectContaining({ id: "execution", sequence: 2, timeLabel: "11:00—11:45" }) - ]) - }) - ]) + location: "北区阀门 V-1", + assignee: "北区班组", + feedbackRequirements: ["回传阀门开度"], + activity: [expect.objectContaining({ title: "工单创建" })] })]); }); diff --git a/src/features/workbench/api/workbench-runtime-client.ts b/src/features/workbench/api/workbench-runtime-client.ts index 067bc62..2b50d5a 100644 --- a/src/features/workbench/api/workbench-runtime-client.ts +++ b/src/features/workbench/api/workbench-runtime-client.ts @@ -87,50 +87,13 @@ const runtimeLatestAnalysisSchema = z.object({ map_payload: z.unknown().nullable() }); -const runtimeWorkOrderStageSchema = z.object({ - id: z.string().trim().min(1), +const runtimeWorkOrderActivitySchema = z.object({ sequence: z.number().int().positive(), + occurred_at: z.string().datetime({ offset: true }), + actor: z.string().trim().min(1), + kind: z.enum(["created", "updated", "feedback"]), title: z.string().trim().min(1), - status: z.enum(["done", "current", "pending", "blocked", "skipped"]), - owner: z.string().trim().min(1), - time_label: z.string().trim().min(1), - description: z.string().trim().min(1) -}); - -const runtimeWorkOrderSubtaskSchema = z.object({ - id: z.string().trim().min(1), - title: z.string().trim().min(1), - summary: z.string().trim().min(1), - status: z.enum(["pending", "running", "blocked", "completed", "cancelled"]), - scheduled_at: z.string().datetime({ offset: true }), - location: z.string().trim().min(1), - assignee: z.string().trim().min(1), - duration_minutes: z.number().int().positive(), - reply_window_minutes: z.number().int().positive(), - stages: z.array(runtimeWorkOrderStageSchema).min(1), - reply_requirements: z.array(z.string().trim().min(1)) -}).superRefine((subtask, context) => { - if (subtask.stages.some((stage, index) => stage.sequence !== index + 1)) { - context.addIssue({ - code: z.ZodIssueCode.custom, - message: "子任务阶段序号必须从 1 连续递增", - path: ["stages"] - }); - } - if (new Set(subtask.stages.map((stage) => stage.id)).size !== subtask.stages.length) { - context.addIssue({ - code: z.ZodIssueCode.custom, - message: "子任务阶段编号不能重复", - path: ["stages"] - }); - } - if (subtask.stages.filter((stage) => stage.status === "current").length > 1) { - context.addIssue({ - code: z.ZodIssueCode.custom, - message: "单个子任务最多只能有一个当前阶段", - path: ["stages"] - }); - } + detail: z.string().trim().min(1) }); const runtimeWorkOrderSchema = z.object({ @@ -147,16 +110,11 @@ const runtimeWorkOrderSchema = z.object({ duration_minutes: z.number().int().positive(), source: z.string().trim().min(1), dispatcher: z.string().trim().min(1), + location: z.string().trim().min(1).nullable().default(null), + assignee: z.string().trim().min(1).nullable().default(null), priority: z.enum(["normal", "urgent"]), - subtasks: z.array(runtimeWorkOrderSubtaskSchema).min(1) -}).superRefine((workOrder, context) => { - if (new Set(workOrder.subtasks.map((subtask) => subtask.id)).size !== workOrder.subtasks.length) { - context.addIssue({ - code: z.ZodIssueCode.custom, - message: "工单子任务编号不能重复", - path: ["subtasks"] - }); - } + feedback_requirements: z.array(z.string().trim().min(1)).default([]), + activity: z.array(runtimeWorkOrderActivitySchema).default([]) }); const runtimeSnapshotSchema = z.object({ @@ -278,26 +236,16 @@ export function parseWorkbenchRuntimeSnapshot(payload: unknown): WorkbenchRuntim dispatcher: workOrder.dispatcher, priority: workOrder.priority, dataSource: "backend", - subtasks: workOrder.subtasks.map((subtask) => ({ - id: subtask.id, - title: subtask.title, - summary: subtask.summary, - status: subtask.status, - scheduledAt: subtask.scheduled_at, - location: subtask.location, - assignee: subtask.assignee, - durationMinutes: subtask.duration_minutes, - replyWindowMinutes: subtask.reply_window_minutes, - stages: subtask.stages.map((stage) => ({ - id: stage.id, - sequence: stage.sequence, - title: stage.title, - status: stage.status, - owner: stage.owner, - timeLabel: stage.time_label, - description: stage.description - })), - replyRequirements: subtask.reply_requirements + location: workOrder.location ?? undefined, + assignee: workOrder.assignee ?? undefined, + feedbackRequirements: workOrder.feedback_requirements, + activity: workOrder.activity.map((entry) => ({ + sequence: entry.sequence, + occurredAt: entry.occurred_at, + actor: entry.actor, + kind: entry.kind, + title: entry.title, + detail: entry.detail })) })) }; diff --git a/src/features/workbench/components/scheduled-condition-detail-panel.test.tsx b/src/features/workbench/components/scheduled-condition-detail-panel.test.tsx index fe3c188..acb4348 100644 --- a/src/features/workbench/components/scheduled-condition-detail-panel.test.tsx +++ b/src/features/workbench/components/scheduled-condition-detail-panel.test.tsx @@ -1,7 +1,7 @@ import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { ScheduledConditionRecord, ScheduledWorkOrderItem } from "../types"; -import { ConditionDetailPanel, WorkOrderSubtasksPanel } from "./scheduled-condition-detail-panel"; +import { ConditionDetailPanel, WorkOrderActivityPanel } from "./scheduled-condition-detail-panel"; import { createVisibleTickerCards } from "./agent-task-ticker-utils"; import { createConditionConversationPrompt } from "../utils/scheduled-condition-prompts"; @@ -34,7 +34,7 @@ it("does not infer water-quality progress or conversation steps from elapsed tim describe("scheduled condition backend boundaries", () => { afterEach(cleanup); - it("keeps Chinese work-order time labels in the application font", () => { + it("shows a flat work order with its recorded activity", () => { const workOrder: ScheduledWorkOrderItem = { id: "demo-work-order", kind: "work_order", @@ -49,33 +49,17 @@ describe("scheduled condition backend boundaries", () => { dispatcher: "调度岗", priority: "normal", dataSource: "mock", - subtasks: [{ - id: "field-service", - title: "现场维修", - summary: "按预约窗口处理", - status: "pending", - scheduledAt: "2026-09-23T10:00:00Z", - location: "测试点", - assignee: "维修班组", - durationMinutes: 60, - replyWindowMinutes: 30, - stages: [{ - id: "dispatch", - sequence: 1, - title: "派发", - status: "pending", - owner: "调度岗", - timeLabel: "待安排", - description: "确认预约窗口。" - }], - replyRequirements: ["回填处理结果"] - }] + location: "测试点", + assignee: "维修班组", + feedbackRequirements: ["回填处理结果"], + activity: [{ sequence: 1, occurredAt: "2026-09-23T09:00:00Z", actor: "调度岗", + kind: "created", title: "工单创建", detail: "确认预约窗口。" }] }; - render(); - const timeLabel = screen.getByText("待安排", { exact: true }); - expect(timeLabel).toHaveClass("tabular-nums"); - expect(timeLabel).not.toHaveClass("font-mono"); + render(); + expect(screen.getByText("工单创建")).toBeVisible(); + expect(screen.getByText("回填处理结果")).toBeVisible(); + expect(screen.queryByText("子任务")).not.toBeInTheDocument(); }); it("shows only persisted evidence for a failed backend condition", () => { diff --git a/src/features/workbench/components/scheduled-condition-detail-panel.tsx b/src/features/workbench/components/scheduled-condition-detail-panel.tsx index e4d5025..95ff96d 100644 --- a/src/features/workbench/components/scheduled-condition-detail-panel.tsx +++ b/src/features/workbench/components/scheduled-condition-detail-panel.tsx @@ -54,10 +54,6 @@ import { getStatusActivity, getStatusTone, getRiskLabel, - getWorkOrderCurrentSubtask, - getWorkOrderCurrentStage, - getWorkOrderStageActivity, - getWorkOrderStageTone, statusLabels } from "./scheduled-condition-feed-utils"; import { getConditionReportRiskLevel } from "./condition-report-risk"; @@ -125,25 +121,19 @@ export function StatusPill({ condition: ScheduledConditionItem; className?: string; }) { - const isWorkOrder = condition.kind === "work_order"; const isAgentEvent = condition.kind === "condition" && condition.taskId === "water-quality-complaint"; - const currentStage = isWorkOrder ? getWorkOrderCurrentStage(condition) : null; return ( - {isAgentEvent ? "事件调查" : isWorkOrder ? currentStage?.title ?? "工单" : statusLabels[condition.status]} + {isAgentEvent ? "事件调查" : statusLabels[condition.status]} ); } @@ -161,7 +151,7 @@ export function ConditionDetailPanel({ }: ConditionDetailPanelProps) { const detailKey = condition.kind === "work_order" - ? `${condition.id}-${getWorkOrderCurrentSubtask(condition)?.id ?? "none"}-${getWorkOrderCurrentStage(condition)?.id ?? condition.status}` + ? `${condition.id}-${condition.updatedAt}-${condition.status}` : condition.id; const content = condition.kind === "work_order" ? ( @@ -226,7 +216,7 @@ function ConditionRecordDetailPanel({ const header = getConditionDetailHeader(condition); const riskLevel = getConditionReportRiskLevel(condition.status, condition.riskLevel); const bodyKey = `${condition.id}-${condition.status}-${condition.riskLevel}`; - const externalWorkOrder = condition.operationalTask?.work_order_snapshot?.subtasks + const externalWorkOrder = condition.operationalTask?.work_order_snapshot ? toScheduledWorkOrderSnapshot(condition.operationalTask.work_order_snapshot) : null; @@ -262,12 +252,12 @@ function ConditionRecordDetailPanel({ > {condition.operationalTask ? ( <> - {externalWorkOrder ? : null} + {externalWorkOrder ? : null} {condition.operationalTask.task_type === "external_work_order" - && !condition.operationalTask.work_order_snapshot?.subtasks ? ( + && !condition.operationalTask.work_order_snapshot ? ( -

后端尚未提供工单过程,不根据总状态推测阶段。

+

后端尚未提供工单快照,不根据任务状态推测现场进展。

) : null} @@ -471,17 +458,15 @@ function WorkOrderDetailPanel({ - + @@ -1286,93 +1271,42 @@ function TaskExecutionStepRow({ ); } -export function WorkOrderSubtasksPanel({ condition }: { condition: ScheduledWorkOrderItem }) { - const currentSubtask = getWorkOrderCurrentSubtask(condition); - const completedCount = condition.subtasks.filter((subtask) => - subtask.status === "completed" || subtask.status === "cancelled" - ).length; - +export function WorkOrderActivityPanel({ condition }: { condition: ScheduledWorkOrderItem }) { return (
- - - {completedCount}/{condition.subtasks.length} 完成 - + + {statusLabels[condition.status]}
- + - -
-
- {condition.subtasks.map((subtask, index) => { - const active = subtask.id === currentSubtask?.id; - const steps: ExecutionStep[] = subtask.stages.map((stage) => { - const awaitingReply = stage.id === "reply" && stage.status === "current"; - return { - id: `${subtask.id}-${stage.id}`, - title: stage.title, - status: awaitingReply ? "confirmation" : stage.status, - statusLabel: awaitingReply ? "待复令" : undefined, - description: stage.description, - meta: [ - { label: "责任人", value: stage.owner }, - { label: "时间", value: stage.timeLabel } - ] - }; - }); - - return ( - - {index + 1}. {subtask.title} - - {getWorkOrderSubtaskStatusLabel(subtask.status)} - - - )} - > -
-

{subtask.summary}

-
- 位置:{subtask.location} - 班组:{subtask.assignee} - 窗口:{formatTimeRange(subtask.scheduledAt, subtask.durationMinutes)} - 复令:完成后 {subtask.replyWindowMinutes} 分钟内 -
-
    - {steps.map((step, stepIndex) => ( - - ))} -
- {subtask.replyRequirements.length > 0 ? ( -
    - {subtask.replyRequirements.map((item) => ( -
  • - - {item} -
  • - ))} -
- ) : null} -
-
- ); - })} +
+ {condition.location ?

位置:{condition.location}

: null} + {condition.feedbackRequirements.length ? ( +
+

回执要求

+
    + {condition.feedbackRequirements.map((item, index) =>
  • {item}
  • )} +
+
+ ) : null} +
+

活动记录

+ {condition.activity.length ? ( +
    + {[...condition.activity].sort((a, b) => b.sequence - a.sequence).map((entry) => ( +
  1. +
    {entry.title}
    +

    {entry.detail}

    +

    {entry.actor}

    +
  2. + ))} +
+ ) :

暂无活动记录;不根据工单状态推测处置过程。

} +
); } @@ -1396,53 +1330,20 @@ function toScheduledWorkOrderSnapshot(snapshot: ExternalWorkOrderSnapshot): Sche : "pending", riskLevel: snapshot.priority === "urgent" || snapshot.priority === "high" ? "attention" : "normal", updatedAt: Date.parse(snapshot.updated_at), - durationMinutes: snapshot.subtasks?.reduce((total, item) => total + item.duration_minutes, 0), detail: snapshot.summary, source: "外围工单系统", dispatcher: "外围系统同步", priority: snapshot.priority === "urgent" || snapshot.priority === "high" ? "urgent" : "normal", dataSource: "backend", - subtasks: (snapshot.subtasks ?? []).map((subtask) => ({ - id: subtask.id, - title: subtask.title, - summary: subtask.summary, - status: subtask.status, - scheduledAt: subtask.scheduled_at, - location: subtask.location, - assignee: subtask.assignee, - durationMinutes: subtask.duration_minutes, - replyWindowMinutes: subtask.reply_window_minutes, - stages: subtask.stages.map((stage) => ({ - id: stage.id, - sequence: stage.sequence, - title: stage.title, - status: stage.status, - owner: stage.owner, - timeLabel: stage.time_label, - description: stage.description - })), - replyRequirements: subtask.reply_requirements - })) + location: snapshot.location ?? undefined, + assignee: snapshot.assignee ?? undefined, + feedbackRequirements: snapshot.feedback_requirements, + activity: snapshot.activity.map((entry) => ({ sequence: entry.sequence, + occurredAt: entry.occurred_at, actor: entry.actor, kind: entry.kind, + title: entry.title, detail: entry.detail })) }; } -function getWorkOrderSubtaskTone(status: ScheduledWorkOrderItem["subtasks"][number]["status"]): StatusTone { - if (status === "completed") return "success"; - if (status === "running") return "info"; - if (status === "blocked") return "danger"; - return "neutral"; -} - -function getWorkOrderSubtaskStatusLabel(status: ScheduledWorkOrderItem["subtasks"][number]["status"]) { - return { - pending: "待处理", - running: "处理中", - blocked: "受阻", - completed: "已完成", - cancelled: "已取消" - }[status]; -} - function RunningEvidencePreview({ items }: { items: string[] }) { return ( diff --git a/src/features/workbench/components/scheduled-condition-feed-utils.test.ts b/src/features/workbench/components/scheduled-condition-feed-utils.test.ts index 727b269..8fcd92f 100644 --- a/src/features/workbench/components/scheduled-condition-feed-utils.test.ts +++ b/src/features/workbench/components/scheduled-condition-feed-utils.test.ts @@ -1,12 +1,11 @@ import { describe, expect, it } from "vitest"; import { + groupScheduledConditions, getStatusActivity, getStatusTone, - getWorkOrderStageActivity, - getWorkOrderStageTone, - getWorkOrderCurrentSubtask + sortConditionsNewestFirst } from "./scheduled-condition-feed-utils"; -import type { ScheduledWorkOrderItem } from "../types"; +import type { ScheduledConditionItem, ScheduledWorkOrderItem } from "../types"; describe("scheduled condition status presentation", () => { it("maps lifecycle states to the shared semantic language", () => { @@ -29,74 +28,73 @@ describe("scheduled condition status presentation", () => { expect(getStatusActivity("awaiting_reply")).toBe("static"); }); - it("maps work-order stage states to the shared semantic language", () => { - expect({ - done: getWorkOrderStageTone("done"), - current: getWorkOrderStageTone("current"), - pending: getWorkOrderStageTone("pending"), - blocked: getWorkOrderStageTone("blocked"), - skipped: getWorkOrderStageTone("skipped") - }).toEqual({ - done: "success", - current: "info", - pending: "neutral", - blocked: "danger", - skipped: "neutral" - }); - expect(getWorkOrderStageActivity("current")).toBe("loading"); - }); + it("sorts mixed task representations by their full scheduled timestamp", () => { + const conditions = [ + createCondition("older-condition", "condition", "2026-09-17T10:00:00+08:00"), + createCondition("newer-work-order", "work_order", "2026-09-18T09:00:00+08:00"), + createCondition("newest-condition", "condition", "2026-09-18T11:00:00+08:00") + ]; - it("prefers an executing subtask over a pending subtask at its dispatch stage", () => { - const workOrder = { - id: "work-order-1", - kind: "work_order", - code: "WO-1", - scheduledAt: "2026-09-17T10:00:00+08:00", - title: "多子任务工单", - summary: "测试当前子任务选择。", - status: "running", - riskLevel: "normal", - updatedAt: 1, - durationMinutes: 30, - source: "测试", - dispatcher: "调度中心", - priority: "normal", - subtasks: [ - createSubtask("pending-subtask", "pending", "dispatch"), - createSubtask("running-subtask", "running", "execution") - ] - } satisfies ScheduledWorkOrderItem; - - expect(getWorkOrderCurrentSubtask(workOrder)?.id).toBe("running-subtask"); + expect(sortConditionsNewestFirst(conditions).map((condition) => condition.id)).toEqual([ + "newest-condition", + "newer-work-order", + "older-condition" + ]); + const grouped = groupScheduledConditions(conditions); + expect(grouped.recentConditions.map((condition) => condition.id)).toEqual([ + "newest-condition", + "older-condition" + ]); + expect(grouped.futureWorkOrders.map((condition) => condition.id)).toEqual([ + "newer-work-order" + ]); }); }); -function createSubtask( +function createCondition( id: string, - status: "pending" | "running", - currentStage: "dispatch" | "execution" -) { + kind: ScheduledConditionItem["kind"], + scheduledAt: string +): ScheduledConditionItem { + if (kind === "work_order") { + return { + ...createWorkOrder(id), + scheduledAt + }; + } + return { id, + kind: "condition", + taskId: "scada-diagnosis", + sessionId: `session-${id}`, title: id, summary: id, - status, + status: "completed", + riskLevel: "normal", + scheduledAt, + updatedAt: Date.parse(scheduledAt) + }; +} + +function createWorkOrder(id: string): ScheduledWorkOrderItem { + return { + id, + kind: "work_order", + code: id, scheduledAt: "2026-09-17T10:00:00+08:00", - location: "演示点位", - assignee: "演示班组", - durationMinutes: 15, - replyWindowMinutes: 10, - stages: [ - { - id: currentStage, - sequence: 1, - title: currentStage, - status: "current" as const, - owner: "演示班组", - timeLabel: "10:00", - description: "当前阶段" - } - ], - replyRequirements: [] + title: id, + summary: id, + status: "pending", + riskLevel: "normal", + updatedAt: 1, + durationMinutes: 30, + source: "测试", + dispatcher: "调度中心", + priority: "normal", + location: "测试点", + assignee: "测试班组", + feedbackRequirements: [], + activity: [] }; } diff --git a/src/features/workbench/components/scheduled-condition-feed-utils.ts b/src/features/workbench/components/scheduled-condition-feed-utils.ts index 57eec3a..13b8857 100644 --- a/src/features/workbench/components/scheduled-condition-feed-utils.ts +++ b/src/features/workbench/components/scheduled-condition-feed-utils.ts @@ -8,9 +8,7 @@ import { import type { StatusActivity, StatusTone } from "@/shared/ui/status"; import type { ScheduledConditionItem, - ScheduledConditionItemStatus, - ScheduledWorkOrderItem, - ScheduledWorkOrderStage + ScheduledConditionItemStatus } from "../types"; export const CONDITION_FILTER_ALL = "__all__"; @@ -55,22 +53,6 @@ const statusIcons: Record = { pending: Clock3 }; -const workOrderStageTones = { - done: "success", - current: "info", - pending: "neutral", - blocked: "danger", - skipped: "neutral" -} satisfies Record; - -const workOrderStageActivities = { - done: "static", - current: "loading", - pending: "static", - blocked: "static", - skipped: "static" -} satisfies Record; - const conditionStatusOrder: ScheduledConditionItemStatus[] = [ "running", "awaiting_reply", @@ -96,14 +78,14 @@ export type ConditionStatusFilterOption = { count: number; }; -export function groupScheduledConditions(conditions: ScheduledConditionItem[]) { - const recentConditions = conditions - .filter((condition) => condition.kind === "condition") - .sort((a, b) => getConditionTime(b) - getConditionTime(a)); +export function sortConditionsNewestFirst(conditions: ScheduledConditionItem[]) { + return [...conditions].sort((a, b) => getConditionTime(b) - getConditionTime(a)); +} - const futureWorkOrders = conditions - .filter((condition) => condition.kind === "work_order") - .sort((a, b) => getConditionTime(a) - getConditionTime(b)); +export function groupScheduledConditions(conditions: ScheduledConditionItem[]) { + const newestFirst = sortConditionsNewestFirst(conditions); + const recentConditions = newestFirst.filter((condition) => condition.kind === "condition"); + const futureWorkOrders = newestFirst.filter((condition) => condition.kind === "work_order"); return { recentConditions, futureWorkOrders }; } @@ -178,38 +160,6 @@ export function getRiskLabel(riskLevel: ScheduledConditionItem["riskLevel"]) { return "正常"; } -export function getWorkOrderCurrentSubtask(condition: ScheduledWorkOrderItem) { - return ( - condition.subtasks.find((subtask) => subtask.status === "running") ?? - condition.subtasks.find((subtask) => subtask.status === "blocked") ?? - condition.subtasks.find((subtask) => - subtask.stages.some((stage) => stage.status === "current") - ) ?? - condition.subtasks.find((subtask) => subtask.status === "pending") ?? - condition.subtasks.at(-1) ?? - null - ); -} - -export function getWorkOrderCurrentStage(condition: ScheduledWorkOrderItem) { - const subtask = getWorkOrderCurrentSubtask(condition); - return ( - subtask?.stages.find((stage) => stage.status === "current") ?? - subtask?.stages.find((stage) => stage.status === "blocked") ?? - subtask?.stages.find((stage) => stage.status === "pending") ?? - subtask?.stages.at(-1) ?? - null - ); -} - -export function getWorkOrderStageTone(status?: ScheduledWorkOrderStage["status"]) { - return status ? workOrderStageTones[status] : "neutral"; -} - -export function getWorkOrderStageActivity(status?: ScheduledWorkOrderStage["status"]) { - return status ? workOrderStageActivities[status] : "static"; -} - export function getStatusIcon(status: ScheduledConditionItemStatus) { return statusIcons[status]; } diff --git a/src/features/workbench/components/scheduled-condition-feed.tsx b/src/features/workbench/components/scheduled-condition-feed.tsx index 2c91002..bca1ee8 100644 --- a/src/features/workbench/components/scheduled-condition-feed.tsx +++ b/src/features/workbench/components/scheduled-condition-feed.tsx @@ -46,14 +46,11 @@ import { createStatusFilterOptions, createTaskFilterOptions, formatTime, - getConditionTime, getStatusActivity, getStatusIcon, getStatusTone, - getWorkOrderCurrentStage, - getWorkOrderStageActivity, - getWorkOrderStageTone, groupScheduledConditions, + sortConditionsNewestFirst, type ConditionStatusFilterOption, type ConditionStatusFilterValue, type ConditionTaskFilterOption, @@ -171,10 +168,6 @@ export function ScheduledConditionFeed({ () => groupScheduledConditions(categoryConditions), [categoryConditions] ); - const allTimelineConditions = useMemo( - () => [...allRecentConditions, ...allFutureWorkOrders], - [allFutureWorkOrders, allRecentConditions] - ); const taskFilterOptions = useMemo( () => createTaskFilterOptions(categoryConditions), [categoryConditions] @@ -203,8 +196,10 @@ export function ScheduledConditionFeed({ [filteredConditions] ); const timelineConditions = useMemo( - () => [...recentConditions, ...futureWorkOrders], - [futureWorkOrders, recentConditions] + () => taskCategory === "appointment" + ? sortConditionsNewestFirst([...recentConditions, ...futureWorkOrders]) + : [...recentConditions, ...futureWorkOrders], + [futureWorkOrders, recentConditions, taskCategory] ); const filterActive = taskFilter !== CONDITION_FILTER_ALL || statusFilter !== CONDITION_FILTER_ALL; const totalConditionCount = allRecentConditions.length + allFutureWorkOrders.length; @@ -214,8 +209,7 @@ export function ScheduledConditionFeed({ : `定时 ${conditionsByCategory.scheduled.length} · 调查 ${conditionsByCategory.investigation.length} · 预约 ${conditionsByCategory.appointment.length}`; const selectedCondition = timelineConditions.find((condition) => condition.id === selectedConditionId) ?? - recentConditions[0] ?? - futureWorkOrders[0] ?? + timelineConditions[0] ?? null; const focusConditionId = focusRequest?.conditionId; const focusRequestId = focusRequest?.requestId; @@ -294,7 +288,7 @@ export function ScheduledConditionFeed({ function handleToggleExpanded() { updateExpanded(!expanded); updateSelectedConditionId( - selectedConditionId ?? recentConditions[0]?.id ?? futureWorkOrders[0]?.id ?? null + selectedConditionId ?? timelineConditions[0]?.id ?? null ); } @@ -303,7 +297,9 @@ export function ScheduledConditionFeed({ if (nextCategory !== "scheduled") setScheduleOpen(false); setTaskFilter(CONDITION_FILTER_ALL); setStatusFilter(CONDITION_FILTER_ALL); - updateSelectedConditionId(conditionsByCategory[nextCategory][0]?.id ?? null); + updateSelectedConditionId( + sortConditionsNewestFirst(conditionsByCategory[nextCategory])[0]?.id ?? null + ); } function handleToggleSchedule() { @@ -562,19 +558,21 @@ export function ScheduledConditionFeed({ item.id === taskCategory)?.label ?? "后端任务"}暂不可用` - : undefined - } - emptyDescription={ - backendError && taskCategory !== "scheduled" - ? backendError - : undefined - } + taskCategory={taskCategory} + loading={activeLoading} + emptyTitle={ + backendError && taskCategory !== "scheduled" + ? `${WORKBENCH_TASK_CATEGORIES.find((item) => item.id === taskCategory)?.label ?? "后端任务"}暂不可用` + : undefined + } + emptyDescription={ + backendError && taskCategory !== "scheduled" + ? backendError + : undefined + } recentConditions={recentConditions} futureWorkOrders={futureWorkOrders} + timelineConditions={timelineConditions} taskFilter={taskFilter} statusFilter={statusFilter} taskFilterOptions={taskFilterOptions} @@ -648,9 +646,11 @@ function formatLocalDateTimeInput(value: Date) { type ConditionTimelinePanelProps = { expanded: boolean; presentation: ScheduledConditionFeedPresentation; + taskCategory: WorkbenchTaskCategory; loading: boolean; recentConditions: ScheduledConditionItem[]; futureWorkOrders: ScheduledConditionItem[]; + timelineConditions: ScheduledConditionItem[]; taskFilter: ConditionTaskFilterValue; statusFilter: ConditionStatusFilterValue; taskFilterOptions: ConditionTaskFilterOption[]; @@ -670,9 +670,11 @@ type ConditionTimelinePanelProps = { function ConditionTimelinePanel({ expanded, presentation, + taskCategory, loading, recentConditions, futureWorkOrders, + timelineConditions, taskFilter, statusFilter, taskFilterOptions, @@ -689,6 +691,7 @@ function ConditionTimelinePanel({ onSelectCondition }: ConditionTimelinePanelProps) { const filterActive = taskFilter !== CONDITION_FILTER_ALL || statusFilter !== CONDITION_FILTER_ALL; + const mainConditions = taskCategory === "appointment" ? timelineConditions : recentConditions; return (
) : null} - {(expanded || presentation === "mobile-sheet") && !loading && futureWorkOrders.length > 0 ? ( + {taskCategory !== "appointment" + && (expanded || presentation === "mobile-sheet") + && !loading + && futureWorkOrders.length > 0 ? (
) : null}
{loading ? ( - ) : recentConditions.length > 0 ? ( + ) : mainConditions.length > 0 ? ( @@ -1078,18 +1084,8 @@ function ConditionTimelineRow({ onSelect }: ConditionTimelineRowProps) { const Icon = getStatusIcon(condition.status); - const isWorkOrder = condition.kind === "work_order"; - const workOrderStage = isWorkOrder ? getWorkOrderCurrentStage(condition)?.status : undefined; - const tone = isWorkOrder - ? condition.status === "running" - ? getWorkOrderStageTone(workOrderStage) - : getStatusTone(condition.status) - : getStatusTone(condition.status); - const activity = isWorkOrder - ? condition.status === "running" - ? getWorkOrderStageActivity(workOrderStage) - : getStatusActivity(condition.status) - : getStatusActivity(condition.status); + const tone = getStatusTone(condition.status); + const activity = getStatusActivity(condition.status); return (
{editing ?

工单类型:{workOrderTypeLabel(editing.work_order_type)},创建后不可修改。

: null} - +