refactor(workbench): simplify appointment tasks
This commit is contained in:
@@ -37,6 +37,8 @@ const workOrder = (revision: number, overrides: Partial<PeripheralWorkOrder> = {
|
||||
updated_at: "2026-09-18T00:00:00Z",
|
||||
created_by: "user",
|
||||
updated_by: "user",
|
||||
feedback_requirements: [],
|
||||
activity: [],
|
||||
...overrides
|
||||
});
|
||||
|
||||
@@ -49,7 +51,8 @@ const createWorkOrderInput: Omit<PeripheralCreateWorkOrder, "request_id"> = {
|
||||
scheduled_at: null,
|
||||
work_order_type: "general",
|
||||
location: null,
|
||||
scenario_code: null
|
||||
scenario_code: null,
|
||||
feedback_requirements: []
|
||||
};
|
||||
|
||||
describe("PeripheralSampleClient", () => {
|
||||
@@ -182,7 +185,7 @@ describe("PeripheralSampleClient", () => {
|
||||
clear_location: false,
|
||||
clear_scenario_code: false,
|
||||
clear_scheduled_at: false,
|
||||
clear_subtasks: false
|
||||
feedback_requirements: []
|
||||
})).resolves.toEqual(order);
|
||||
await expect(client.deleteWorkOrder(order.work_order_id, 2)).resolves.toEqual(deleted);
|
||||
|
||||
|
||||
+31
-62
@@ -325,6 +325,8 @@ export interface components {
|
||||
CreateWorkOrder: {
|
||||
/** Assignee */
|
||||
assignee?: string | null;
|
||||
/** Feedback Requirements */
|
||||
feedback_requirements?: string[];
|
||||
/** Location */
|
||||
location?: string | null;
|
||||
/**
|
||||
@@ -348,8 +350,6 @@ export interface components {
|
||||
* @enum {string}
|
||||
*/
|
||||
status: "draft" | "open" | "in_progress" | "awaiting_reply" | "completed" | "cancelled";
|
||||
/** Subtasks */
|
||||
subtasks?: components["schemas"]["WorkbenchRuntimeWorkOrderSubtask"][] | null;
|
||||
/** Summary */
|
||||
summary: string;
|
||||
/** Title */
|
||||
@@ -914,17 +914,16 @@ export interface components {
|
||||
* @default false
|
||||
*/
|
||||
clear_scheduled_at: boolean;
|
||||
/**
|
||||
* Clear Subtasks
|
||||
* @default false
|
||||
*/
|
||||
clear_subtasks: boolean;
|
||||
/** Expected Revision */
|
||||
expected_revision: number;
|
||||
/** Feedback Requirements */
|
||||
feedback_requirements?: string[] | null;
|
||||
/** Location */
|
||||
location?: string | null;
|
||||
/** Priority */
|
||||
priority?: ("low" | "normal" | "high" | "urgent") | null;
|
||||
/** Progress Note */
|
||||
progress_note?: string | null;
|
||||
/**
|
||||
* Request Id
|
||||
* Format: uuid
|
||||
@@ -936,8 +935,6 @@ export interface components {
|
||||
scheduled_at?: string | null;
|
||||
/** Status */
|
||||
status?: ("draft" | "open" | "in_progress" | "awaiting_reply" | "completed" | "cancelled") | null;
|
||||
/** Subtasks */
|
||||
subtasks?: components["schemas"]["WorkbenchRuntimeWorkOrderSubtask"][] | null;
|
||||
/** Summary */
|
||||
summary?: string | null;
|
||||
/** Title */
|
||||
@@ -945,6 +942,8 @@ export interface components {
|
||||
};
|
||||
/** WorkOrder */
|
||||
WorkOrder: {
|
||||
/** Activity */
|
||||
activity?: components["schemas"]["WorkOrderActivity"][];
|
||||
/** Assignee */
|
||||
assignee: string | null;
|
||||
/**
|
||||
@@ -954,6 +953,8 @@ export interface components {
|
||||
created_at: string;
|
||||
/** Created By */
|
||||
created_by: string;
|
||||
/** Feedback Requirements */
|
||||
feedback_requirements?: string[];
|
||||
/** Location */
|
||||
location?: string | null;
|
||||
/**
|
||||
@@ -982,8 +983,6 @@ export interface components {
|
||||
* @enum {string}
|
||||
*/
|
||||
status: "draft" | "open" | "in_progress" | "awaiting_reply" | "completed" | "cancelled";
|
||||
/** Subtasks */
|
||||
subtasks?: components["schemas"]["WorkbenchRuntimeWorkOrderSubtask"][] | null;
|
||||
/** Summary */
|
||||
summary: string;
|
||||
/** Title */
|
||||
@@ -1007,6 +1006,27 @@ export interface components {
|
||||
*/
|
||||
work_order_type: "general" | "water_quality_complaint" | "burst_report";
|
||||
};
|
||||
/** WorkOrderActivity */
|
||||
WorkOrderActivity: {
|
||||
/** Actor */
|
||||
actor: string;
|
||||
/** Detail */
|
||||
detail: string;
|
||||
/**
|
||||
* Kind
|
||||
* @enum {string}
|
||||
*/
|
||||
kind: "created" | "updated" | "feedback";
|
||||
/**
|
||||
* Occurred At
|
||||
* Format: date-time
|
||||
*/
|
||||
occurred_at: string;
|
||||
/** Sequence */
|
||||
sequence: number;
|
||||
/** Title */
|
||||
title: string;
|
||||
};
|
||||
/** WorkOrderPage */
|
||||
WorkOrderPage: {
|
||||
/** Items */
|
||||
@@ -1014,57 +1034,6 @@ export interface components {
|
||||
/** Next Cursor */
|
||||
next_cursor: string | null;
|
||||
};
|
||||
/** WorkbenchRuntimeWorkOrderStage */
|
||||
WorkbenchRuntimeWorkOrderStage: {
|
||||
/** Description */
|
||||
description: string;
|
||||
/** Id */
|
||||
id: string;
|
||||
/** Owner */
|
||||
owner: string;
|
||||
/** Sequence */
|
||||
sequence: number;
|
||||
/**
|
||||
* Status
|
||||
* @enum {string}
|
||||
*/
|
||||
status: "done" | "current" | "pending" | "blocked" | "skipped";
|
||||
/** Time Label */
|
||||
time_label: string;
|
||||
/** Title */
|
||||
title: string;
|
||||
};
|
||||
/** WorkbenchRuntimeWorkOrderSubtask */
|
||||
WorkbenchRuntimeWorkOrderSubtask: {
|
||||
/** Assignee */
|
||||
assignee: string;
|
||||
/** Duration Minutes */
|
||||
duration_minutes: number;
|
||||
/** Id */
|
||||
id: string;
|
||||
/** Location */
|
||||
location: string;
|
||||
/** Reply Requirements */
|
||||
reply_requirements: string[];
|
||||
/** Reply Window Minutes */
|
||||
reply_window_minutes: number;
|
||||
/**
|
||||
* Scheduled At
|
||||
* Format: date-time
|
||||
*/
|
||||
scheduled_at: string;
|
||||
/** Stages */
|
||||
stages: components["schemas"]["WorkbenchRuntimeWorkOrderStage"][];
|
||||
/**
|
||||
* Status
|
||||
* @enum {string}
|
||||
*/
|
||||
status: "pending" | "running" | "blocked" | "completed" | "cancelled";
|
||||
/** Summary */
|
||||
summary: string;
|
||||
/** Title */
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
|
||||
@@ -12,12 +12,11 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { DateTimeField } from "@/shared/ui/date-time-field";
|
||||
import { Disclosure } from "@/shared/ui/disclosure";
|
||||
import { FieldSelect } from "@/shared/ui/field-select";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { StatusBadge, type StatusTone } from "@/shared/ui/status";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { WorkOrderSubtasksPanel } from "../components/scheduled-condition-detail-panel";
|
||||
import { WorkOrderActivityPanel } from "../components/scheduled-condition-detail-panel";
|
||||
import { WorkbenchEmptyState, WorkbenchNotice, WorkbenchSection } from "../components/workbench-panel-ui";
|
||||
import type { ScheduledWorkOrderItem } from "../types";
|
||||
import type {
|
||||
@@ -26,9 +25,6 @@ import type {
|
||||
PeripheralWorkOrder
|
||||
} from "./client";
|
||||
|
||||
type PeripheralSubtask = NonNullable<PeripheralWorkOrder["subtasks"]>[number];
|
||||
type PeripheralStage = PeripheralSubtask["stages"][number];
|
||||
|
||||
export function ManagedTestResources({ client, canWrite, capabilities, refreshGeneration, onAgentTrigger }: {
|
||||
client: PeripheralSampleClient;
|
||||
canWrite: boolean;
|
||||
@@ -72,7 +68,7 @@ export function ManagedTestResources({ client, canWrite, capabilities, refreshGe
|
||||
return (
|
||||
<WorkbenchSection
|
||||
title="工单接口测试"
|
||||
description="验证创建、编辑、过程状态、反馈回填与后端同步。"
|
||||
description="验证单层预约工单、现场反馈与后端同步。"
|
||||
icon={<ClipboardList size={15} />}
|
||||
tone="teal"
|
||||
bodyClassName="space-y-3"
|
||||
@@ -119,7 +115,8 @@ function WorkOrders({ orders, canWrite, busy, mutate, client, onAgentTrigger }:
|
||||
const [location, setLocation] = useState("");
|
||||
const [scenarioCode, setScenarioCode] = useState("");
|
||||
const [scheduledAt, setScheduledAt] = useState(() => toLocalInput(new Date(Date.now() + 60 * 60_000)));
|
||||
const [subtasks, setSubtasks] = useState<PeripheralSubtask[] | null>(null);
|
||||
const [feedbackRequirements, setFeedbackRequirements] = useState("");
|
||||
const [progressNote, setProgressNote] = useState("");
|
||||
const [eventEditorOpen, setEventEditorOpen] = useState(false);
|
||||
const [eventKind, setEventKind] = useState<"water_quality_complaint" | "burst_report">("water_quality_complaint");
|
||||
const [eventLocation, setEventLocation] = useState("");
|
||||
@@ -138,13 +135,13 @@ function WorkOrders({ orders, canWrite, busy, mutate, client, onAgentTrigger }:
|
||||
setLocation("");
|
||||
setScenarioCode("");
|
||||
setScheduledAt(toLocalInput(new Date(Date.now() + 60 * 60_000)));
|
||||
setSubtasks(null);
|
||||
setFeedbackRequirements("");
|
||||
setProgressNote("");
|
||||
};
|
||||
|
||||
const create = () => {
|
||||
clear();
|
||||
setEditorOpen(true);
|
||||
setSubtasks([createDefaultSubtask()]);
|
||||
};
|
||||
|
||||
const edit = (item: PeripheralWorkOrder) => {
|
||||
@@ -158,7 +155,8 @@ function WorkOrders({ orders, canWrite, busy, mutate, client, onAgentTrigger }:
|
||||
setLocation(item.location ?? "");
|
||||
setScenarioCode(item.scenario_code ?? "");
|
||||
setScheduledAt(item.scheduled_at ? toLocalInput(new Date(item.scheduled_at)) : "");
|
||||
setSubtasks(item.subtasks ?? null);
|
||||
setFeedbackRequirements((item.feedback_requirements ?? []).join("\n"));
|
||||
setProgressNote("");
|
||||
};
|
||||
|
||||
const triggerAgent = async (workOrder: PeripheralWorkOrder) => {
|
||||
@@ -186,7 +184,7 @@ function WorkOrders({ orders, canWrite, busy, mutate, client, onAgentTrigger }:
|
||||
work_order_type: eventKind,
|
||||
location: eventLocation.trim(),
|
||||
scenario_code: waterQuality ? "WQ-SECONDARY-SUPPLY-001" : "BURST-REPORT-001",
|
||||
subtasks: null
|
||||
feedback_requirements: []
|
||||
});
|
||||
setEventEditorOpen(false);
|
||||
setEventLocation("");
|
||||
@@ -194,15 +192,14 @@ function WorkOrders({ orders, canWrite, busy, mutate, client, onAgentTrigger }:
|
||||
await triggerAgent(workOrder);
|
||||
});
|
||||
|
||||
const processValid = Boolean(subtasks?.length && subtasks.every(isValidSubtask));
|
||||
const canSave = canWrite && !busy && title.trim() && summary.trim() && processValid;
|
||||
const canSave = canWrite && !busy && title.trim() && summary.trim();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h4 className="text-xs font-semibold text-slate-800">测试工单</h4>
|
||||
<p className="mt-0.5 text-[11px] leading-4 text-slate-500">过程字段将随工单写入,并按原结构同步到预约任务。</p>
|
||||
<p className="mt-0.5 text-[11px] leading-4 text-slate-500">每张预约工单对应一条任务;修改和现场反馈按时间记录。</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button size="sm" disabled={!canWrite || Boolean(busy)} onClick={() => setEventEditorOpen((current) => !current)}>
|
||||
@@ -256,11 +253,12 @@ function WorkOrders({ orders, canWrite, busy, mutate, client, onAgentTrigger }:
|
||||
</div>
|
||||
{editing ? <p className="mt-2 text-[11px] text-slate-500">工单类型:{workOrderTypeLabel(editing.work_order_type)},创建后不可修改。</p> : null}
|
||||
|
||||
<ProcessEditor subtasks={subtasks} disabled={!canWrite || Boolean(busy)} onChange={setSubtasks} />
|
||||
<label className="mt-3 block text-xs font-medium text-slate-600">回执要求(每行一项)<Textarea className="mt-1 min-h-20" value={feedbackRequirements} onChange={(event) => setFeedbackRequirements(event.target.value)} disabled={!canWrite || Boolean(busy)} /></label>
|
||||
{editing ? <label className="mt-3 block text-xs font-medium text-slate-600">现场反馈(可选)<Textarea className="mt-1 min-h-20" value={progressNote} onChange={(event) => setProgressNote(event.target.value)} disabled={!canWrite || Boolean(busy)} placeholder="记录实际处置、复核或回执;留空则只更新工单信息" /></label> : null}
|
||||
|
||||
<div className="mt-3 flex gap-2">
|
||||
<Button size="sm" disabled={!canSave} onClick={() => void mutate("save-order", async () => {
|
||||
const normalizedSubtasks = subtasks!.map(normalizeSubtask);
|
||||
const requirements = feedbackRequirements.split("\n").map((item) => item.trim()).filter(Boolean);
|
||||
if (editing) {
|
||||
await client.updateWorkOrder(editing.work_order_id, {
|
||||
expected_revision: editing.revision,
|
||||
@@ -276,8 +274,8 @@ function WorkOrders({ orders, canWrite, busy, mutate, client, onAgentTrigger }:
|
||||
clear_location: !location.trim(),
|
||||
scenario_code: scenarioCode.trim() || null,
|
||||
clear_scenario_code: !scenarioCode.trim(),
|
||||
subtasks: normalizedSubtasks,
|
||||
clear_subtasks: false
|
||||
feedback_requirements: requirements,
|
||||
progress_note: progressNote.trim() || null
|
||||
});
|
||||
} else {
|
||||
await client.createWorkOrder({
|
||||
@@ -290,13 +288,13 @@ function WorkOrders({ orders, canWrite, busy, mutate, client, onAgentTrigger }:
|
||||
work_order_type: "general",
|
||||
location: location.trim() || null,
|
||||
scenario_code: scenarioCode.trim() || null,
|
||||
subtasks: normalizedSubtasks
|
||||
feedback_requirements: requirements
|
||||
});
|
||||
}
|
||||
clear();
|
||||
})}>
|
||||
{busy === "save-order" ? <LoaderCircle className="animate-spin" /> : editing ? <Pencil size={13} /> : <Plus size={13} />}
|
||||
{editing ? "保存工单与过程" : "创建预约工单"}
|
||||
{editing ? "保存工单" : "创建预约工单"}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={clear} disabled={Boolean(busy)}>取消</Button>
|
||||
</div>
|
||||
@@ -330,89 +328,6 @@ function WorkOrders({ orders, canWrite, busy, mutate, client, onAgentTrigger }:
|
||||
);
|
||||
}
|
||||
|
||||
function ProcessEditor({ subtasks, disabled, onChange }: {
|
||||
subtasks: PeripheralSubtask[] | null;
|
||||
disabled: boolean;
|
||||
onChange: (subtasks: PeripheralSubtask[] | null) => void;
|
||||
}) {
|
||||
if (!subtasks) {
|
||||
return (
|
||||
<div className="mt-3 rounded-xl border border-dashed border-slate-200 p-3">
|
||||
<p className="text-xs text-slate-500">未提供过程,不根据工单总状态推测阶段。</p>
|
||||
<Button className="mt-2" size="sm" variant="outline" disabled={disabled} onClick={() => onChange([createDefaultSubtask()])}>添加过程</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const updateSubtask = (index: number, next: PeripheralSubtask) => {
|
||||
onChange(subtasks.map((item, itemIndex) => itemIndex === index ? next : item));
|
||||
};
|
||||
|
||||
return (
|
||||
<section aria-label="工单过程" className="mt-3">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<h5 className="text-xs font-semibold text-slate-700">预约过程</h5>
|
||||
<span className="text-[10px] text-slate-400">{subtasks.length} 个子任务</span>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
{subtasks.map((subtask, index) => (
|
||||
<Disclosure
|
||||
key={subtask.id}
|
||||
initiallyOpen={index === 0}
|
||||
label={`${index + 1}. ${subtask.title || "未命名子任务"}`}
|
||||
className="rounded-xl border border-slate-200 bg-slate-50 px-3"
|
||||
>
|
||||
<div className="grid gap-2 border-t border-slate-200 py-3 sm:grid-cols-2">
|
||||
<label className="block text-xs font-medium text-slate-600">子任务名称<Input className="mt-1 h-9" value={subtask.title} onChange={(event) => updateSubtask(index, { ...subtask, title: event.target.value })} disabled={disabled} /></label>
|
||||
<FieldSelect label="子任务状态" value={subtask.status} onValueChange={(value) => updateSubtask(index, { ...subtask, status: value as PeripheralSubtask["status"] })} disabled={disabled} options={SUBTASK_STATUS_OPTIONS} />
|
||||
<label className="block text-xs font-medium text-slate-600 sm:col-span-2">子任务说明<Textarea className="mt-1 min-h-14" value={subtask.summary} onChange={(event) => updateSubtask(index, { ...subtask, summary: event.target.value })} disabled={disabled} /></label>
|
||||
<label className="block text-xs font-medium text-slate-600">责任班组<Input className="mt-1 h-9" value={subtask.assignee} onChange={(event) => updateSubtask(index, { ...subtask, assignee: event.target.value })} disabled={disabled} /></label>
|
||||
<label className="block text-xs font-medium text-slate-600">处置位置<Input className="mt-1 h-9" value={subtask.location} onChange={(event) => updateSubtask(index, { ...subtask, location: event.target.value })} disabled={disabled} /></label>
|
||||
<DateTimeField label="子任务预约时间" value={toLocalInput(new Date(subtask.scheduled_at))} onChange={(value) => updateSubtask(index, { ...subtask, scheduled_at: new Date(value).toISOString() })} disabled={disabled} />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<label className="block text-xs font-medium text-slate-600">预计分钟<Input className="mt-1 h-9" inputMode="numeric" value={String(subtask.duration_minutes)} onChange={(event) => updateSubtask(index, { ...subtask, duration_minutes: Number(event.target.value.replace(/\D/g, "")) })} disabled={disabled} /></label>
|
||||
<label className="block text-xs font-medium text-slate-600">复令时限<Input className="mt-1 h-9" inputMode="numeric" value={String(subtask.reply_window_minutes)} onChange={(event) => updateSubtask(index, { ...subtask, reply_window_minutes: Number(event.target.value.replace(/\D/g, "")) })} disabled={disabled} /></label>
|
||||
</div>
|
||||
<label className="block text-xs font-medium text-slate-600 sm:col-span-2">回执要求<Textarea className="mt-1 min-h-14" value={subtask.reply_requirements.join("\n")} onChange={(event) => updateSubtask(index, { ...subtask, reply_requirements: event.target.value.split("\n") })} disabled={disabled} /></label>
|
||||
</div>
|
||||
<div className="grid gap-2 pb-3">
|
||||
{subtask.stages.map((stage, stageIndex) => (
|
||||
<StageEditor
|
||||
key={stage.id}
|
||||
stage={stage}
|
||||
disabled={disabled}
|
||||
onChange={(nextStage) => updateSubtask(index, {
|
||||
...subtask,
|
||||
stages: subtask.stages.map((item, itemIndex) => itemIndex === stageIndex ? nextStage : item)
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Disclosure>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function StageEditor({ stage, disabled, onChange }: {
|
||||
stage: PeripheralStage;
|
||||
disabled: boolean;
|
||||
onChange: (stage: PeripheralStage) => void;
|
||||
}) {
|
||||
return (
|
||||
<section aria-label={`阶段 ${stage.title}`} className="rounded-lg bg-white p-2.5 shadow-[0_1px_3px_rgba(15,23,42,0.1)]">
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<label className="block text-xs font-medium text-slate-600">阶段名称<Input className="mt-1 h-9" value={stage.title} onChange={(event) => onChange({ ...stage, title: event.target.value })} disabled={disabled} /></label>
|
||||
<FieldSelect label="阶段状态" value={stage.status} onValueChange={(value) => onChange({ ...stage, status: value as PeripheralStage["status"] })} disabled={disabled} options={STAGE_STATUS_OPTIONS} />
|
||||
<label className="block text-xs font-medium text-slate-600">责任人<Input className="mt-1 h-9" value={stage.owner} onChange={(event) => onChange({ ...stage, owner: event.target.value })} disabled={disabled} /></label>
|
||||
<label className="block text-xs font-medium text-slate-600">时间说明<Input className="mt-1 h-9" value={stage.time_label} onChange={(event) => onChange({ ...stage, time_label: event.target.value })} disabled={disabled} /></label>
|
||||
<label className="block text-xs font-medium text-slate-600 sm:col-span-2">阶段说明<Textarea aria-label="阶段说明" className="mt-1 min-h-14" value={stage.description} onChange={(event) => onChange({ ...stage, description: event.target.value })} disabled={disabled} /></label>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkOrderCard({ item, index, canWrite, busy, onEdit, onDelete }: {
|
||||
item: PeripheralWorkOrder;
|
||||
index: number;
|
||||
@@ -421,7 +336,7 @@ function WorkOrderCard({ item, index, canWrite, busy, onEdit, onDelete }: {
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const process = toScheduledWorkOrder(item);
|
||||
const workOrder = toScheduledWorkOrder(item);
|
||||
return (
|
||||
<article className="rounded-xl bg-slate-50 p-3 ring-1 ring-inset ring-slate-200">
|
||||
<div className="flex items-start gap-2">
|
||||
@@ -447,75 +362,13 @@ function WorkOrderCard({ item, index, canWrite, busy, onEdit, onDelete }: {
|
||||
<span className="font-mono">r{item.revision}</span>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
{process ? (
|
||||
<WorkOrderSubtasksPanel condition={process} />
|
||||
) : (
|
||||
<WorkbenchNotice tone="neutral" title="未提供过程">这条旧工单没有子任务与阶段数据,不根据总状态推测完成情况。</WorkbenchNotice>
|
||||
)}
|
||||
<WorkOrderActivityPanel condition={workOrder} />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function createDefaultSubtask(): PeripheralSubtask {
|
||||
const scheduledAt = new Date(Date.now() + 60 * 60_000).toISOString();
|
||||
return {
|
||||
id: `subtask-${crypto.randomUUID()}`,
|
||||
title: "现场处置",
|
||||
summary: "按预约要求完成现场处置并回填结果。",
|
||||
status: "pending",
|
||||
scheduled_at: scheduledAt,
|
||||
location: "待填写位置",
|
||||
assignee: "待分配班组",
|
||||
duration_minutes: 60,
|
||||
reply_window_minutes: 30,
|
||||
stages: [
|
||||
{ id: "dispatch", sequence: 1, title: "派发", status: "pending", owner: "调度岗", time_label: "待安排", description: "确认班组、位置和预约窗口。" },
|
||||
{ id: "execution", sequence: 2, title: "执行", status: "pending", owner: "执行班组", time_label: "待安排", description: "按工单要求处置并记录现场结果。" },
|
||||
{ id: "reply", sequence: 3, title: "复令", status: "pending", owner: "调度岗", time_label: "待安排", description: "核对回执要求并确认闭环。" }
|
||||
],
|
||||
reply_requirements: ["回填现场处理结果"]
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSubtask(subtask: PeripheralSubtask): PeripheralSubtask {
|
||||
return {
|
||||
...subtask,
|
||||
id: subtask.id.trim(),
|
||||
title: subtask.title.trim(),
|
||||
summary: subtask.summary.trim(),
|
||||
location: subtask.location.trim(),
|
||||
assignee: subtask.assignee.trim(),
|
||||
stages: subtask.stages.map((stage, index) => ({
|
||||
...stage,
|
||||
id: stage.id.trim(),
|
||||
sequence: index + 1,
|
||||
title: stage.title.trim(),
|
||||
owner: stage.owner.trim(),
|
||||
time_label: stage.time_label.trim(),
|
||||
description: stage.description.trim()
|
||||
})),
|
||||
reply_requirements: subtask.reply_requirements.map((item) => item.trim()).filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
function isValidSubtask(subtask: PeripheralSubtask) {
|
||||
return Boolean(
|
||||
subtask.id.trim()
|
||||
&& subtask.title.trim()
|
||||
&& subtask.summary.trim()
|
||||
&& subtask.location.trim()
|
||||
&& subtask.assignee.trim()
|
||||
&& subtask.duration_minutes > 0
|
||||
&& subtask.reply_window_minutes >= 0
|
||||
&& !Number.isNaN(Date.parse(subtask.scheduled_at))
|
||||
&& subtask.stages.length
|
||||
&& subtask.stages.every((stage) => stage.id.trim() && stage.title.trim() && stage.owner.trim() && stage.time_label.trim() && stage.description.trim())
|
||||
);
|
||||
}
|
||||
|
||||
function toScheduledWorkOrder(item: PeripheralWorkOrder): ScheduledWorkOrderItem | null {
|
||||
if (!item.subtasks?.length) return null;
|
||||
function toScheduledWorkOrder(item: PeripheralWorkOrder): ScheduledWorkOrderItem {
|
||||
return {
|
||||
id: item.work_order_id,
|
||||
kind: "work_order",
|
||||
@@ -526,25 +379,16 @@ function toScheduledWorkOrder(item: PeripheralWorkOrder): ScheduledWorkOrderItem
|
||||
status: item.status === "in_progress" ? "running" : item.status === "awaiting_reply" ? "awaiting_reply" : item.status === "completed" ? "completed" : item.status === "cancelled" ? "cancelled" : "pending",
|
||||
riskLevel: item.priority === "urgent" || item.priority === "high" ? "attention" : "normal",
|
||||
updatedAt: Date.parse(item.updated_at),
|
||||
durationMinutes: item.subtasks.reduce((total, subtask) => total + subtask.duration_minutes, 0),
|
||||
detail: item.summary,
|
||||
source: "外围工单测试",
|
||||
dispatcher: item.updated_by,
|
||||
priority: item.priority === "urgent" || item.priority === "high" ? "urgent" : "normal",
|
||||
dataSource: "backend",
|
||||
subtasks: item.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: item.location ?? undefined,
|
||||
assignee: item.assignee ?? undefined,
|
||||
feedbackRequirements: item.feedback_requirements ?? [],
|
||||
activity: (item.activity ?? []).map((entry) => ({ sequence: entry.sequence, occurredAt: entry.occurred_at,
|
||||
actor: entry.actor, kind: entry.kind, title: entry.title, detail: entry.detail }))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -561,8 +405,6 @@ const WORK_ORDER_STATUS_OPTIONS = [
|
||||
{ value: "cancelled", label: "已取消" }
|
||||
];
|
||||
const PRIORITY_OPTIONS = [{ value: "low", label: "低" }, { value: "normal", label: "普通" }, { value: "high", label: "高" }, { value: "urgent", label: "紧急" }];
|
||||
const SUBTASK_STATUS_OPTIONS = [{ value: "pending", label: "待处理" }, { value: "running", label: "处理中" }, { value: "blocked", label: "受阻" }, { value: "completed", label: "已完成" }, { value: "cancelled", label: "已取消" }];
|
||||
const STAGE_STATUS_OPTIONS = [{ value: "pending", label: "待开始" }, { value: "current", label: "当前阶段" }, { value: "done", label: "已完成" }, { value: "blocked", label: "受阻" }, { value: "skipped", label: "已跳过" }];
|
||||
|
||||
function toLocalInput(value: Date) {
|
||||
const offset = value.getTimezoneOffset() * 60_000;
|
||||
|
||||
@@ -24,6 +24,8 @@ const order = (overrides: Partial<PeripheralWorkOrder> = {}): PeripheralWorkOrde
|
||||
updated_at: "2026-09-22T02:00:00Z",
|
||||
created_by: "user",
|
||||
updated_by: "user",
|
||||
feedback_requirements: [],
|
||||
activity: [],
|
||||
...overrides
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user