From 9d1e8c4d30bfdcb8a84c616164b6d712eb05b19c Mon Sep 17 00:00:00 2001 From: kkfluous Date: Fri, 17 Apr 2026 09:30:07 +0800 Subject: [PATCH] feat(scheduling): enrich history records with customer/dept/manager + drill-in to swap plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each row in 调度记录 now shows 业务部门(简)/业务负责人/客户 beneath the plate line, and is clickable to open the reusable SwapPreview showing the full replacement plan (current mileage, 考核目标, 替换后预测). Drill-in is only enabled when the suggestion is still in the active scheduling view; the user can still 取消干预 from the preview. Co-Authored-By: Claude Opus 4.7 --- .../scheduling/NotificationHistory.tsx | 65 ++++++++++++++++--- src/modules/scheduling/SchedulingModule.tsx | 1 + 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/modules/scheduling/NotificationHistory.tsx b/src/modules/scheduling/NotificationHistory.tsx index 34c8e6f..3d8fd5c 100644 --- a/src/modules/scheduling/NotificationHistory.tsx +++ b/src/modules/scheduling/NotificationHistory.tsx @@ -1,19 +1,26 @@ -import { useCallback, useEffect, useState } from 'react'; -import { X, RotateCcw, Clock, CheckCircle2, XCircle, Send, Loader2 } from 'lucide-react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { X, RotateCcw, Clock, CheckCircle2, XCircle, Send, Loader2, ChevronRight } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { fetchNotifications, updateNotification } from './api'; -import type { NotificationRecord, NotificationStatus } from './types'; +import type { NotificationRecord, NotificationStatus, SchedulingSuggestion, CandidateVehicle } from './types'; import Blur from '../../components/Blur'; +import SwapPreview from './SwapPreview'; interface Props { onClose: () => void; onChange?: () => void; /** When true, pre-filter to the last 7 days (excluding cancelled). */ recentOnly?: boolean; + /** Current suggestions used to enrich records with customer/dept/manager and enable drill-down. */ + suggestions?: SchedulingSuggestion[]; } const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; +function shortDept(dept: string | null | undefined): string { + return (dept || '').replace('业务', ''); +} + type StatusTab = 'all' | NotificationStatus; const STATUS_TABS: { key: StatusTab; label: string }[] = [ @@ -40,7 +47,7 @@ function fmtDateTime(iso: string): string { return `${y}-${m}-${day} ${hh}:${mm}`; } -export default function NotificationHistory({ onClose, onChange, recentOnly = false }: Props) { +export default function NotificationHistory({ onClose, onChange, recentOnly = false, suggestions }: Props) { const [records, setRecords] = useState([]); const [loading, setLoading] = useState(false); const [tab, setTab] = useState('all'); @@ -49,6 +56,13 @@ export default function NotificationHistory({ onClose, onChange, recentOnly = fa const [executeTarget, setExecuteTarget] = useState(null); const [afterMileageInput, setAfterMileageInput] = useState(''); const [notesInput, setNotesInput] = useState(''); + const [drillTarget, setDrillTarget] = useState<{ suggestion: SchedulingSuggestion; candidate: CandidateVehicle } | null>(null); + + const suggestionById = useMemo(() => { + const map = new Map(); + for (const s of suggestions ?? []) map.set(s.id, s); + return map; + }, [suggestions]); const visibleRecords = recent7d ? records.filter(r => { @@ -170,18 +184,41 @@ export default function NotificationHistory({ onClose, onChange, recentOnly = fa {visibleRecords.map(rec => { const badge = statusBadge(rec.status); const busy = mutatingId === rec.id; + const suggestion = suggestionById.get(rec.suggestionId); + const candidate = suggestion?.candidates.find(c => c.plateNumber === rec.candidatePlate) ?? null; + const canDrill = !!suggestion && !!candidate; + const v = suggestion?.currentVehicle; + + const handleRowClick = () => { + if (canDrill && suggestion && candidate) setDrillTarget({ suggestion, candidate }); + }; + return ( -
+
{rec.currentPlate} {rec.candidatePlate}
- - {badge.icon} {badge.text} - +
+ + {badge.icon} {badge.text} + + {canDrill && } +
+ {v && ( +
+ {v.department && {shortDept(v.department)}} + {v.manager && {v.manager}} + {v.customer || '-'} +
+ )}
{rec.operatorName && 操作人 {rec.operatorName}} {fmtDateTime(rec.createdAt)} @@ -193,7 +230,7 @@ export default function NotificationHistory({ onClose, onChange, recentOnly = fa
{rec.notes}
)} {rec.status === 'sent' && ( -
+
e.stopPropagation()}>
)} + + {/* Drill-down: replacement plan */} + {drillTarget && ( + setDrillTarget(null)} + onSuccess={() => { load(); onChange?.(); }} + /> + )}
); } diff --git a/src/modules/scheduling/SchedulingModule.tsx b/src/modules/scheduling/SchedulingModule.tsx index bc5de46..85e8528 100644 --- a/src/modules/scheduling/SchedulingModule.tsx +++ b/src/modules/scheduling/SchedulingModule.tsx @@ -532,6 +532,7 @@ export default function SchedulingModule() { onClose={() => setShowHistory(false)} onChange={loadData} recentOnly={historyRecentOnly} + suggestions={data?.suggestions} /> )}