import { CalendarClock, ChevronDown, ChevronRight, ClipboardList, History, RefreshCw, X, XCircle } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { MAP_TOOL_PANEL_SURFACE_CLASS_NAME, showMapNotice } from "@/features/map/core"; import { isGeneratedScheduledConditionSessionId } from "../data/scheduled-conditions"; import type { ScheduledConditionItem, WorkbenchRuntimeDataSource, WorkbenchWorkOrderDataSource } from "../types"; import { createConditionConversationPrompt } from "../utils/scheduled-condition-prompts"; import type { OperationalTaskDecision } from "../api/operational-task-client"; import type { OperationalTaskDataSnapshot } from "../api/operational-task-client"; import { cn } from "@/shared/ui/cn"; import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger } from "@/shared/ui/dropdown-menu"; import { StatusIcon as SemanticStatusIcon } from "@/shared/ui/status"; import { ConditionDetailPanel, StatusPill } from "./scheduled-condition-detail-panel"; import { WORKBENCH_TASK_CATEGORIES, getWorkbenchTaskCategory, groupConditionsByTaskCategory, type WorkbenchTaskCategory } from "./task-category"; import { CONDITION_FILTER_ALL, createStatusFilterOptions, createTaskFilterOptions, formatTime, getStatusActivity, getStatusIcon, getStatusTone, groupScheduledConditions, sortConditionsNewestFirst, type ConditionStatusFilterOption, type ConditionStatusFilterValue, type ConditionTaskFilterOption, type ConditionTaskFilterValue } from "./scheduled-condition-feed-utils"; const CONDITION_CONTENT_SURFACE_CLASS_NAME = "surface-reading rounded-xl border border-slate-200/70"; const CONDITION_CONTROL_SURFACE_CLASS_NAME = "surface-control rounded-xl border border-slate-200/70"; const MINIMUM_REFRESH_FEEDBACK_MS = 3_000; export type ScheduledConditionFeedPresentation = | "desktop-dock" | "desktop-floating" | "mobile-sheet"; type ScheduledConditionFeedProps = { presentation?: ScheduledConditionFeedPresentation; conditions?: ScheduledConditionItem[]; expanded?: boolean; focusRequest?: ScheduledConditionFeedFocusRequest | null; loading?: boolean; backendLoading?: boolean; backendError?: string | null; dataSource?: WorkbenchRuntimeDataSource; workOrderDataSource?: WorkbenchWorkOrderDataSource; visible?: boolean; selectedConditionId?: string | null; onExpandedChange?: (expanded: boolean) => void; onFocusRequestHandled?: (requestId: number) => void; onSelectedConditionChange?: (conditionId: string | null) => void; onExpandAgent: () => void; onClose?: () => void; onLoadHistorySession: (sessionId: string) => void | Promise; onSubmitPrompt: (prompt: string) => void | Promise; onOpenTaskCollaboration?: (taskId: string) => Promise; onTaskDecision?: (taskId: string, actionId: string, expectedVersion: number, decision: OperationalTaskDecision, comment?: string) => Promise; onTaskExecute?: (taskId: string, actionId: string, expectedVersion: number) => Promise; onTaskDraftUpdate?: (taskId: string, actionId: string, expectedVersion: number, payload: Record) => Promise; onTaskConfirmDispatch?: (taskId: string, actionId: string, expectedVersion: number) => Promise; onTaskAccessDecision?: (taskId: string, accessRequestId: string, expectedVersion: number, decision: "approved" | "rejected", scopes: string[]) => Promise; onTaskReconcile?: (taskId: string, actionId: string, expectedVersion: number) => Promise; onLoadTaskEvidenceSnapshot?: (taskId: string, evidenceId: string) => Promise; onTaskRefresh?: () => void; onRefresh?: () => void | Promise; }; type ScheduledConditionFeedFocusRequest = { conditionId: string; requestId: number; }; export function ScheduledConditionFeed({ presentation = "desktop-dock", conditions = [], expanded: controlledExpanded, focusRequest, loading = false, backendLoading = false, backendError = null, dataSource = "mock", workOrderDataSource = "mock", visible = true, selectedConditionId: controlledSelectedConditionId, onExpandedChange, onFocusRequestHandled, onSelectedConditionChange, onExpandAgent, onClose, onLoadHistorySession, onSubmitPrompt, onOpenTaskCollaboration, onTaskDecision, onTaskExecute, onTaskDraftUpdate, onTaskConfirmDispatch, onTaskAccessDecision, onTaskReconcile, onLoadTaskEvidenceSnapshot, onTaskRefresh, onRefresh }: ScheduledConditionFeedProps) { const [uncontrolledExpanded, setUncontrolledExpanded] = useState(false); const [uncontrolledSelectedConditionId, setUncontrolledSelectedConditionId] = useState< string | null >(null); const [taskFilter, setTaskFilter] = useState(CONDITION_FILTER_ALL); const [statusFilter, setStatusFilter] = useState(CONDITION_FILTER_ALL); const [taskCategory, setTaskCategory] = useState("scheduled"); const [refreshing, setRefreshing] = useState(false); const refreshingRef = useRef(false); const mobileSheet = presentation === "mobile-sheet"; const desktopDock = presentation === "desktop-dock"; const compactPresentation = mobileSheet || desktopDock; const desktopFloating = presentation === "desktop-floating"; const expanded = controlledExpanded ?? uncontrolledExpanded; const selectedConditionId = controlledSelectedConditionId ?? uncontrolledSelectedConditionId; const conditionsByCategory = useMemo( () => groupConditionsByTaskCategory(conditions), [conditions] ); const categoryConditions = conditionsByCategory[taskCategory]; const activeLoading = taskCategory === "scheduled" ? loading : backendLoading; const { recentConditions: allRecentConditions, futureWorkOrders: allFutureWorkOrders } = useMemo( () => groupScheduledConditions(categoryConditions), [categoryConditions] ); const taskFilterOptions = useMemo( () => createTaskFilterOptions(categoryConditions), [categoryConditions] ); const taskFilteredConditions = useMemo( () => categoryConditions.filter( (condition) => taskFilter === CONDITION_FILTER_ALL || condition.title === taskFilter ), [categoryConditions, taskFilter] ); const statusFilterOptions = useMemo( () => createStatusFilterOptions(taskFilteredConditions), [taskFilteredConditions] ); const taskFilteredConditionCount = taskFilteredConditions.length; const filteredConditions = useMemo( () => taskFilteredConditions.filter( (condition) => statusFilter === CONDITION_FILTER_ALL || condition.status === statusFilter ), [statusFilter, taskFilteredConditions] ); const { recentConditions, futureWorkOrders } = useMemo( () => groupScheduledConditions(filteredConditions), [filteredConditions] ); const timelineConditions = useMemo( () => 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; const filteredConditionCount = recentConditions.length + futureWorkOrders.length; const headerSummary = loading && backendLoading ? "正在加载工况任务" : `定时 ${conditionsByCategory.scheduled.length} · 调查 ${conditionsByCategory.investigation.length} · 预约 ${conditionsByCategory.appointment.length}`; const selectedCondition = timelineConditions.find((condition) => condition.id === selectedConditionId) ?? timelineConditions[0] ?? null; const focusConditionId = focusRequest?.conditionId; const focusRequestId = focusRequest?.requestId; const updateExpanded = useCallback( (nextExpanded: boolean) => { if (controlledExpanded === undefined) { setUncontrolledExpanded(nextExpanded); } onExpandedChange?.(nextExpanded); }, [controlledExpanded, onExpandedChange] ); const updateSelectedConditionId = useCallback( (nextConditionId: string | null) => { if (controlledSelectedConditionId === undefined) { setUncontrolledSelectedConditionId(nextConditionId); } onSelectedConditionChange?.(nextConditionId); }, [controlledSelectedConditionId, onSelectedConditionChange] ); useEffect(() => { if (taskFilter === CONDITION_FILTER_ALL) { return; } if (!taskFilterOptions.some((option) => option.value === taskFilter)) { setTaskFilter(CONDITION_FILTER_ALL); } }, [taskFilter, taskFilterOptions]); useEffect(() => { if (!expanded) { return; } const nextSelectedConditionId = selectedCondition?.id ?? null; if (selectedConditionId !== nextSelectedConditionId) { updateSelectedConditionId(nextSelectedConditionId); } }, [expanded, selectedCondition?.id, selectedConditionId, updateSelectedConditionId]); useEffect(() => { if (!focusConditionId || focusRequestId === undefined) { return; } const targetCondition = conditions.find( (condition) => condition.id === focusConditionId ); if (!targetCondition) { return; } const targetCategory = getWorkbenchTaskCategory(targetCondition); if (!targetCategory) return; setTaskCategory(targetCategory); setTaskFilter(CONDITION_FILTER_ALL); setStatusFilter(CONDITION_FILTER_ALL); updateSelectedConditionId(targetCondition.id); updateExpanded(true); onFocusRequestHandled?.(focusRequestId); }, [ conditions, focusConditionId, focusRequestId, onFocusRequestHandled, updateExpanded, updateSelectedConditionId ]); function handleToggleExpanded() { updateExpanded(!expanded); updateSelectedConditionId( selectedConditionId ?? timelineConditions[0]?.id ?? null ); } function handleTaskCategoryChange(nextCategory: WorkbenchTaskCategory) { setTaskCategory(nextCategory); setTaskFilter(CONDITION_FILTER_ALL); setStatusFilter(CONDITION_FILTER_ALL); updateSelectedConditionId( sortConditionsNewestFirst(conditionsByCategory[nextCategory])[0]?.id ?? null ); } async function handleRefresh() { if (!onRefresh || refreshingRef.current) return; refreshingRef.current = true; setRefreshing(true); const startedAt = Date.now(); let refreshError: unknown = null; try { await onRefresh(); } catch (error) { refreshError = error; } finally { const remainingFeedbackMs = MINIMUM_REFRESH_FEEDBACK_MS - (Date.now() - startedAt); if (remainingFeedbackMs > 0) { await new Promise((resolve) => window.setTimeout(resolve, remainingFeedbackMs)); } refreshingRef.current = false; setRefreshing(false); } if (refreshError) { showMapNotice({ id: "scheduled-condition-refresh", tone: "error", title: "工况任务刷新失败", message: refreshError instanceof Error ? refreshError.message : "暂时无法重新获取工况任务。" }); } } function handleSelectTimelineCondition(conditionId: string) { updateSelectedConditionId(conditionId); if (!expanded) { updateExpanded(true); } } function handleContinueConversation(condition: ScheduledConditionItem) { if (condition.kind === "work_order") { return; } if (condition.operationalTask) { onExpandAgent(); void Promise.resolve(onOpenTaskCollaboration?.(condition.operationalTask.task_id)) .catch((error) => showMapNotice({ id: "operational-task-collaboration", tone: "error", title: "Agent 协作打开失败", message: error instanceof Error ? error.message : "无法打开这条任务的协作问答。" })); return; } onExpandAgent(); if (!condition.sessionId) { showMapNotice({ id: "scheduled-condition-missing-session", tone: "error", title: "无法打开历史会话", message: "这条工况没有可加载的会话记录。" }); return; } if (isGeneratedScheduledConditionSessionId(condition.sessionId)) { void Promise.resolve(onSubmitPrompt(createConditionConversationPrompt( condition, condition.dataSource ?? dataSource ))) .then(() => { showMapNotice({ id: "scheduled-condition-generated-session", tone: "success", title: "已发起工况对话", message: "已将当前工况上下文发送到 Agent 面板。" }); }) .catch((submitError) => { showMapNotice({ id: "scheduled-condition-generated-session", tone: "error", title: "工况对话发起失败", message: submitError instanceof Error ? submitError.message : "无法将当前工况发送到 Agent。" }); }); return; } void Promise.resolve(onLoadHistorySession(condition.sessionId)); } return (

工况任务

{headerSummary}

{onRefresh ? ( ) : null} {mobileSheet && onClose ? ( ) : null}
{expanded ? ( ) : null}
{activeLoading && expanded ? ( ) : expanded && selectedCondition ? ( handleContinueConversation(selectedCondition)} onTaskDecision={onTaskDecision} onTaskExecute={onTaskExecute} onTaskDraftUpdate={onTaskDraftUpdate} onTaskConfirmDispatch={onTaskConfirmDispatch} onTaskAccessDecision={onTaskAccessDecision} onTaskReconcile={onTaskReconcile} onLoadTaskEvidenceSnapshot={onLoadTaskEvidenceSnapshot} onTaskRefresh={onTaskRefresh} /> ) : null}
item.id === taskCategory)?.label ?? "后端任务"}暂不可用` : undefined } emptyDescription={ backendError && taskCategory !== "scheduled" ? backendError : undefined } recentConditions={recentConditions} futureWorkOrders={futureWorkOrders} timelineConditions={timelineConditions} taskFilter={taskFilter} statusFilter={statusFilter} taskFilterOptions={taskFilterOptions} statusFilterOptions={statusFilterOptions} selectedConditionId={expanded ? (selectedCondition?.id ?? null) : null} totalConditionCount={totalConditionCount} taskFilteredConditionCount={taskFilteredConditionCount} filteredConditionCount={filteredConditionCount} onTaskFilterChange={setTaskFilter} onStatusFilterChange={setStatusFilter} onResetFilters={() => { setTaskFilter(CONDITION_FILTER_ALL); setStatusFilter(CONDITION_FILTER_ALL); }} onSelectCondition={handleSelectTimelineCondition} />
); } function TaskCategorySwitch({ activeCategory, counts, onChange }: { activeCategory: WorkbenchTaskCategory; counts: Record; onChange: (category: WorkbenchTaskCategory) => void; }) { return ( ); } type ConditionTimelinePanelProps = { expanded: boolean; presentation: ScheduledConditionFeedPresentation; taskCategory: WorkbenchTaskCategory; loading: boolean; recentConditions: ScheduledConditionItem[]; futureWorkOrders: ScheduledConditionItem[]; timelineConditions: ScheduledConditionItem[]; taskFilter: ConditionTaskFilterValue; statusFilter: ConditionStatusFilterValue; taskFilterOptions: ConditionTaskFilterOption[]; statusFilterOptions: ConditionStatusFilterOption[]; selectedConditionId: string | null; totalConditionCount: number; taskFilteredConditionCount: number; filteredConditionCount: number; emptyTitle?: string; emptyDescription?: string; onTaskFilterChange: (value: ConditionTaskFilterValue) => void; onStatusFilterChange: (value: ConditionStatusFilterValue) => void; onResetFilters: () => void; onSelectCondition: (conditionId: string) => void; }; function ConditionTimelinePanel({ expanded, presentation, taskCategory, loading, recentConditions, futureWorkOrders, timelineConditions, taskFilter, statusFilter, taskFilterOptions, statusFilterOptions, selectedConditionId, totalConditionCount, taskFilteredConditionCount, filteredConditionCount, emptyTitle, emptyDescription, onTaskFilterChange, onStatusFilterChange, onResetFilters, onSelectCondition }: ConditionTimelinePanelProps) { const filterActive = taskFilter !== CONDITION_FILTER_ALL || statusFilter !== CONDITION_FILTER_ALL; const mainConditions = taskCategory === "appointment" ? timelineConditions : recentConditions; return (
{expanded && !loading ? ( ) : null} {taskCategory !== "appointment" && (expanded || presentation === "mobile-sheet") && !loading && futureWorkOrders.length > 0 ? (
) : null}
{loading ? ( ) : mainConditions.length > 0 ? ( ) : ( )}
); } type ConditionFilterBarProps = { taskFilter: ConditionTaskFilterValue; statusFilter: ConditionStatusFilterValue; taskFilterOptions: ConditionTaskFilterOption[]; statusFilterOptions: ConditionStatusFilterOption[]; filteredConditionCount: number; totalConditionCount: number; taskFilteredConditionCount: number; onTaskFilterChange: (value: ConditionTaskFilterValue) => void; onStatusFilterChange: (value: ConditionStatusFilterValue) => void; onResetFilters: () => void; }; function ConditionFilterBar({ taskFilter, statusFilter, taskFilterOptions, statusFilterOptions, filteredConditionCount, totalConditionCount, taskFilteredConditionCount, onTaskFilterChange, onStatusFilterChange, onResetFilters }: ConditionFilterBarProps) { const filterActive = taskFilter !== CONDITION_FILTER_ALL || statusFilter !== CONDITION_FILTER_ALL; return (
筛选 {filteredConditionCount}/{totalConditionCount}
onStatusFilterChange(value as ConditionStatusFilterValue)} options={[ { value: CONDITION_FILTER_ALL, label: "全部状态", count: taskFilteredConditionCount }, ...statusFilterOptions ]} />
{filterActive ? ( ) : null}
); } type ConditionFilterDropdownProps = { label: string; value: TValue; options: { value: TValue; label: string; count: number; }[]; onValueChange: (value: TValue) => void; }; function ConditionFilterDropdown({ label, value, options, onValueChange }: ConditionFilterDropdownProps) { const selectedOption = options.find((option) => option.value === value) ?? options[0]; return (
{label} onValueChange(nextValue as TValue)} > {options.map((option) => ( {option.label} {option.count} ))}
); } function ConditionEmptyState({ title, description }: { title: string; description: string }) { return (

{title}

{description}

); } function ConditionTimelineSkeleton({ rows }: { rows: number }) { return (
{Array.from({ length: rows }).map((_, index) => (
{index < rows - 1 ? (
))}
); } function ConditionDetailSkeleton() { return (
{Array.from({ length: 5 }).map((_, index) => (
))}
); } function DetailScroll({ children }: { children: React.ReactNode }) { return (
{children}
); } type ConditionGroupHeaderProps = { icon: typeof History; title: string; count: number; compact?: boolean; }; function ConditionGroupHeader({ icon: Icon, title, count, compact = false }: ConditionGroupHeaderProps) { return (
{count}
); } type ConditionRowsProps = { conditions: ScheduledConditionItem[]; selectedConditionId: string | null; onSelectCondition: (conditionId: string) => void; }; function ConditionRows({ conditions, selectedConditionId, onSelectCondition }: ConditionRowsProps) { return (
{conditions.map((condition, index) => ( onSelectCondition(condition.id)} /> ))}
); } type ConditionTimelineRowProps = { condition: ScheduledConditionItem; selected: boolean; isLast: boolean; onSelect: () => void; }; function ConditionTimelineRow({ condition, selected, isLast, onSelect }: ConditionTimelineRowProps) { const Icon = getStatusIcon(condition.status); const tone = getStatusTone(condition.status); const activity = getStatusActivity(condition.status); return ( ); }