refactor(workbench): simplify appointment tasks
This commit is contained in:
@@ -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(<WorkOrderSubtasksPanel condition={workOrder} />);
|
||||
const timeLabel = screen.getByText("待安排", { exact: true });
|
||||
expect(timeLabel).toHaveClass("tabular-nums");
|
||||
expect(timeLabel).not.toHaveClass("font-mono");
|
||||
render(<WorkOrderActivityPanel condition={workOrder} />);
|
||||
expect(screen.getByText("工单创建")).toBeVisible();
|
||||
expect(screen.getByText("回填处理结果")).toBeVisible();
|
||||
expect(screen.queryByText("子任务")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows only persisted evidence for a failed backend condition", () => {
|
||||
|
||||
@@ -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 (
|
||||
<StatusBadge
|
||||
tone={
|
||||
isAgentEvent ? "neutral" : isWorkOrder
|
||||
? getWorkOrderStageTone(currentStage?.status)
|
||||
: getStatusTone(condition.status)
|
||||
isAgentEvent ? "neutral" : getStatusTone(condition.status)
|
||||
}
|
||||
activity={
|
||||
isAgentEvent ? "static" : isWorkOrder
|
||||
? getWorkOrderStageActivity(currentStage?.status)
|
||||
: getStatusActivity(condition.status)
|
||||
isAgentEvent ? "static" : getStatusActivity(condition.status)
|
||||
}
|
||||
className={className}
|
||||
>
|
||||
{isAgentEvent ? "事件调查" : isWorkOrder ? currentStage?.title ?? "工单" : statusLabels[condition.status]}
|
||||
{isAgentEvent ? "事件调查" : statusLabels[condition.status]}
|
||||
</StatusBadge>
|
||||
);
|
||||
}
|
||||
@@ -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 ? <WorkOrderSubtasksPanel condition={externalWorkOrder} /> : null}
|
||||
{externalWorkOrder ? <WorkOrderActivityPanel condition={externalWorkOrder} /> : null}
|
||||
{condition.operationalTask.task_type === "external_work_order"
|
||||
&& !condition.operationalTask.work_order_snapshot?.subtasks ? (
|
||||
&& !condition.operationalTask.work_order_snapshot ? (
|
||||
<DetailSectionCard>
|
||||
<SectionTitle icon={ListChecks} title="预约过程" />
|
||||
<p className="mt-2 text-xs leading-5 text-slate-500">后端尚未提供工单过程,不根据总状态推测阶段。</p>
|
||||
<p className="mt-2 text-xs leading-5 text-slate-500">后端尚未提供工单快照,不根据任务状态推测现场进展。</p>
|
||||
</DetailSectionCard>
|
||||
) : null}
|
||||
<AgentExecutionPlanPanel
|
||||
@@ -456,9 +446,6 @@ function WorkOrderDetailPanel({
|
||||
condition: ScheduledWorkOrderItem;
|
||||
dataSource: WorkbenchWorkOrderDataSource;
|
||||
}) {
|
||||
const currentSubtask = getWorkOrderCurrentSubtask(condition);
|
||||
const currentStage = getWorkOrderCurrentStage(condition);
|
||||
|
||||
return (
|
||||
<DetailScroll>
|
||||
<DetailShell>
|
||||
@@ -471,17 +458,15 @@ function WorkOrderDetailPanel({
|
||||
<SourceBoundaryNotice
|
||||
title={dataSource === "backend" ? "后端工单记录" : "演示工单"}
|
||||
description={dataSource === "backend"
|
||||
? "工单的子任务、阶段、责任人和复令要求均来自后端持久化记录。"
|
||||
? "工单状态、负责人、回执要求与活动记录均来自后端持久化记录。"
|
||||
: "这条工单来自前端演示数据,不代表真实派发、执行或复令状态。"}
|
||||
/>
|
||||
<WorkOrderSubtasksPanel condition={condition} />
|
||||
<WorkOrderActivityPanel condition={condition} />
|
||||
<DecisionActionStrip
|
||||
icon={CalendarClock}
|
||||
title={currentSubtask && currentStage
|
||||
? `${currentSubtask.title} · ${currentStage.title}`
|
||||
: "工单流转"}
|
||||
value={condition.recommendation ?? "按子任务及其阶段跟踪现场处置闭环。"}
|
||||
actionLabel={currentStage?.id === "reply" ? "待复令" : "跟踪工单"}
|
||||
title="工单跟踪"
|
||||
value={condition.recommendation ?? "按工单活动记录跟踪现场处置。"}
|
||||
actionLabel={condition.status === "awaiting_reply" ? "待复令" : "跟踪工单"}
|
||||
actionDisabled
|
||||
actionTitle="工单流程由调度和现场班组流转"
|
||||
/>
|
||||
@@ -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 (
|
||||
<DetailSectionCard>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<SectionTitle icon={ListChecks} title="子任务" />
|
||||
<StatusBadge tone={completedCount === condition.subtasks.length ? "success" : "info"}>
|
||||
{completedCount}/{condition.subtasks.length} 完成
|
||||
</StatusBadge>
|
||||
<SectionTitle icon={ClipboardList} title="预约工单" />
|
||||
<StatusBadge tone={getStatusTone(condition.status)}>{statusLabels[condition.status]}</StatusBadge>
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-2 gap-1.5 sm:grid-cols-4">
|
||||
<ReportMetaTile label="来源" value={condition.source} />
|
||||
<ReportMetaTile label="派发人" value={condition.dispatcher} />
|
||||
<ReportMetaTile label="负责人" value={condition.assignee ?? "待分配"} />
|
||||
<ReportMetaTile label="优先级" value={condition.priority === "urgent" ? "紧急" : "普通"} />
|
||||
<ReportMetaTile label="预计耗时" value={formatDuration(condition.durationMinutes, condition.status)} />
|
||||
</div>
|
||||
<div className="mt-2 grid gap-2">
|
||||
{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 (
|
||||
<Disclosure
|
||||
key={subtask.id}
|
||||
className="surface-control rounded-xl border border-slate-200/70 [&>summary]:px-3 [&>summary]:py-2.5"
|
||||
initiallyOpen={active}
|
||||
label={(
|
||||
<span className="flex min-w-0 items-center justify-between gap-2">
|
||||
<span className="min-w-0 truncate">{index + 1}. {subtask.title}</span>
|
||||
<StatusBadge
|
||||
tone={getWorkOrderSubtaskTone(subtask.status)}
|
||||
activity={subtask.status === "running" ? "live" : "static"}
|
||||
>
|
||||
{getWorkOrderSubtaskStatusLabel(subtask.status)}
|
||||
</StatusBadge>
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
<div className="border-t border-slate-200/70 px-3 py-3">
|
||||
<p className="text-xs leading-5 text-slate-700">{subtask.summary}</p>
|
||||
<div className="mt-2 grid gap-1 text-xs leading-5 text-slate-600 sm:grid-cols-2">
|
||||
<span><b className="text-slate-400">位置:</b>{subtask.location}</span>
|
||||
<span><b className="text-slate-400">班组:</b>{subtask.assignee}</span>
|
||||
<span><b className="text-slate-400">窗口:</b><span className="font-mono">{formatTimeRange(subtask.scheduledAt, subtask.durationMinutes)}</span></span>
|
||||
<span><b className="text-slate-400">复令:</b>完成后 {subtask.replyWindowMinutes} 分钟内</span>
|
||||
</div>
|
||||
<ol className="mt-3 grid gap-2">
|
||||
{steps.map((step, stepIndex) => (
|
||||
<TaskExecutionStepRow
|
||||
key={step.id}
|
||||
step={step}
|
||||
isLast={stepIndex === steps.length - 1}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
{subtask.replyRequirements.length > 0 ? (
|
||||
<ul className="mt-3 grid gap-1.5 border-t border-slate-200/70 pt-3">
|
||||
{subtask.replyRequirements.map((item) => (
|
||||
<li key={item} className="flex gap-2 text-xs leading-5 text-slate-700">
|
||||
<CheckCircle2 className="mt-0.5 shrink-0 text-blue-600" size={14} />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
</Disclosure>
|
||||
);
|
||||
})}
|
||||
<ReportMetaTile label="预约时间" value={formatTime(condition.scheduledAt)} />
|
||||
</div>
|
||||
{condition.location ? <p className="mt-3 text-xs text-slate-600">位置:{condition.location}</p> : null}
|
||||
{condition.feedbackRequirements.length ? (
|
||||
<section className="mt-3 border-t border-slate-200/70 pt-3">
|
||||
<h4 className="text-xs font-semibold text-slate-700">回执要求</h4>
|
||||
<ul className="mt-2 grid gap-1.5">
|
||||
{condition.feedbackRequirements.map((item, index) => <li key={`${index}-${item}`} className="flex gap-2 text-xs leading-5 text-slate-700"><CheckCircle2 className="mt-0.5 shrink-0 text-blue-600" size={14} />{item}</li>)}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
<section className="mt-3 border-t border-slate-200/70 pt-3">
|
||||
<h4 className="text-xs font-semibold text-slate-700">活动记录</h4>
|
||||
{condition.activity.length ? (
|
||||
<ol className="mt-2 border-l border-blue-200 pl-4">
|
||||
{[...condition.activity].sort((a, b) => b.sequence - a.sequence).map((entry) => (
|
||||
<li key={entry.sequence} className="relative pb-4 last:pb-0 before:absolute before:-left-[21px] before:top-1 before:h-2.5 before:w-2.5 before:rounded-full before:bg-blue-500">
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-1"><span className="text-xs font-semibold text-slate-800">{entry.title}</span><time className="text-[10px] text-slate-500">{formatTime(entry.occurredAt)}</time></div>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-600">{entry.detail}</p>
|
||||
<p className="mt-1 text-[10px] text-slate-400">{entry.actor}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : <p className="mt-2 text-xs text-slate-500">暂无活动记录;不根据工单状态推测处置过程。</p>}
|
||||
</section>
|
||||
</DetailSectionCard>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<DetailSectionCard>
|
||||
|
||||
@@ -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: []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<ScheduledConditionItemStatus, typeof CheckCircle2> = {
|
||||
pending: Clock3
|
||||
};
|
||||
|
||||
const workOrderStageTones = {
|
||||
done: "success",
|
||||
current: "info",
|
||||
pending: "neutral",
|
||||
blocked: "danger",
|
||||
skipped: "neutral"
|
||||
} satisfies Record<ScheduledWorkOrderStage["status"], StatusTone>;
|
||||
|
||||
const workOrderStageActivities = {
|
||||
done: "static",
|
||||
current: "loading",
|
||||
pending: "static",
|
||||
blocked: "static",
|
||||
skipped: "static"
|
||||
} satisfies Record<ScheduledWorkOrderStage["status"], StatusActivity>;
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
<ConditionTimelinePanel
|
||||
expanded={expanded}
|
||||
presentation={presentation}
|
||||
loading={activeLoading}
|
||||
emptyTitle={
|
||||
backendError && taskCategory !== "scheduled"
|
||||
? `${WORKBENCH_TASK_CATEGORIES.find((item) => 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 (
|
||||
<section
|
||||
@@ -718,7 +721,10 @@ function ConditionTimelinePanel({
|
||||
onResetFilters={onResetFilters}
|
||||
/>
|
||||
) : null}
|
||||
{(expanded || presentation === "mobile-sheet") && !loading && futureWorkOrders.length > 0 ? (
|
||||
{taskCategory !== "appointment"
|
||||
&& (expanded || presentation === "mobile-sheet")
|
||||
&& !loading
|
||||
&& futureWorkOrders.length > 0 ? (
|
||||
<section className="mb-2">
|
||||
<ConditionGroupHeader
|
||||
icon={ClipboardList}
|
||||
@@ -736,16 +742,16 @@ function ConditionTimelinePanel({
|
||||
</section>
|
||||
) : null}
|
||||
<ConditionGroupHeader
|
||||
icon={History}
|
||||
icon={taskCategory === "appointment" ? ClipboardList : History}
|
||||
title="最近工况"
|
||||
count={loading ? 0 : recentConditions.length}
|
||||
count={loading ? 0 : mainConditions.length}
|
||||
/>
|
||||
<div className="scheduled-feed-scroll -mr-2 min-h-0 flex-1 overflow-y-auto pb-4 pl-0.5 pr-2 pt-0.5">
|
||||
{loading ? (
|
||||
<ConditionTimelineSkeleton rows={expanded ? 7 : 6} />
|
||||
) : recentConditions.length > 0 ? (
|
||||
) : mainConditions.length > 0 ? (
|
||||
<ConditionRows
|
||||
conditions={recentConditions}
|
||||
conditions={mainConditions}
|
||||
selectedConditionId={selectedConditionId}
|
||||
onSelectCondition={onSelectCondition}
|
||||
/>
|
||||
@@ -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 (
|
||||
<button
|
||||
|
||||
@@ -16,7 +16,7 @@ export const WORKBENCH_TASK_CATEGORIES = [
|
||||
{
|
||||
id: "appointment",
|
||||
label: "预约任务",
|
||||
sourceLabel: "后端 / 演示"
|
||||
sourceLabel: "后端"
|
||||
}
|
||||
] as const satisfies readonly {
|
||||
id: WorkbenchTaskCategory;
|
||||
|
||||
Reference in New Issue
Block a user