import { useState, useMemo } from 'react'; import { X, MapPin, AlertTriangle, CheckCircle, ArrowDown, ArrowUp, ArrowRight, ArrowUpDown, } from 'lucide-react'; import { motion } from 'motion/react'; import type { SchedulingSuggestion, CandidateVehicle } from './types'; import Blur from '../../components/Blur'; import SwapPreview from './SwapPreview'; type SortKey = 'predicted' | 'current'; type SortDir = 'asc' | 'desc'; interface Props { suggestion: SchedulingSuggestion; onClose: () => void; onNotifySuccess: () => void; } function fmtKm(value: number): string { if (value >= 10000) return (value / 10000).toFixed(1) + '万'; return Math.round(value).toLocaleString(); } function fmtRate(rate: number): string { return (rate * 100).toFixed(1) + '%'; } export default function SuggestionDetail({ suggestion: s, onClose, onNotifySuccess }: Props) { const [previewCandidate, setPreviewCandidate] = useState(null); const [sentPlates, setSentPlates] = useState>(new Set()); const [batchFilter, setBatchFilter] = useState>(new Set()); const [sortKey, setSortKey] = useState('predicted'); const [sortDir, setSortDir] = useState('desc'); const v = s.currentVehicle; const isRescue = s.type === 'rescue_hopeless'; // Batch options from candidates const batchOptions = useMemo(() => { const set = new Set(); for (const c of s.candidates) if (c.targetName) set.add(c.targetName); return [...set].sort(); }, [s.candidates]); // Filtered + sorted candidates, grouped by region const { sameRegion, crossRegion } = useMemo(() => { let list = s.candidates; if (batchFilter.size > 0) list = list.filter(c => c.targetName != null && batchFilter.has(c.targetName)); const sorted = [...list].sort((a, b) => { const va = sortKey === 'predicted' ? a.predictedAfterSwap : a.totalMileage; const vb = sortKey === 'predicted' ? b.predictedAfterSwap : b.totalMileage; return sortDir === 'desc' ? vb - va : va - vb; }); return { sameRegion: sorted.filter(c => c.isSameRegion), crossRegion: sorted.filter(c => !c.isSameRegion), }; }, [s.candidates, batchFilter, sortKey, sortDir]); const displayCount = sameRegion.length + crossRegion.length; const toggleSort = (key: SortKey) => { if (sortKey === key) { setSortDir(d => d === 'desc' ? 'asc' : 'desc'); } else { setSortKey(key); setSortDir('desc'); } }; const renderCandidate = (c: CandidateVehicle) => { const sent = sentPlates.has(c.plateNumber) || c.notificationStatus === 'sent' || c.notificationStatus === 'executed'; return (
{c.plateNumber} {c.region}{!c.isSameRegion && ' · 跨区'} {c.vehicleType} {c.targetName || '库存'} 剩余{c.daysLeft}天
{c.canQualifyAfterSwap ? ( 可达标 ) : ( 需关注 )}
当前
{fmtKm(c.totalMileage)}
替换后预计
{fmtKm(c.predictedAfterSwap)}
考核
{c.yearTarget ? fmtKm(c.yearTarget) : '-'}
); }; return (
e.stopPropagation()} className="bg-white rounded-t-2xl sm:rounded-2xl shadow-2xl w-full sm:max-w-lg overflow-hidden flex flex-col max-h-[92vh] sm:max-h-[85vh] sm:mx-4" > {/* Header — unified dark slate */}
{isRescue ? : } {isRescue ? '里程低·换走此车' : '里程高·换下此车'}
{/* Body */}
{/* Current Vehicle — same format as candidate cards */}
{/* Header — same style as candidate header */}
{v.plateNumber} {v.region} {v.vehicleType} {v.targetName} 剩余{v.daysLeft}天
= 1 ? 'text-emerald-600' : 'text-rose-500'}`}> {fmtRate(v.completionRate)}
{/* Customer + dept/manager info */}
{v.department && {v.department}} {v.manager && {v.manager}} {(v.department || v.manager) && |} 客户 {v.customer || '-'} 日均 {Math.round(v.customerAvgDaily)} km
{/* Metrics */}
当前
{fmtKm(v.currentYearMileage)}
考核期结束预估
= 1 ? 'text-emerald-600' : 'text-rose-500'}`}>{fmtKm(v.currentYearMileage + v.customerAvgDaily * v.daysLeft)}
考核
{fmtKm(v.yearTarget)}
{/* Reason — structured lines */}
{s.reason.lines.map((line, i) => (
{line.label} {line.value}
))}
{s.reason.conclusion}
{/* Candidates */}
可替换在库车辆 {displayCount}/{s.candidates.length} 辆
{/* Filter + Sort controls */}
{/* Batch multi-select pills */}
{batchOptions.map(b => { const active = batchFilter.has(b); return ( ); })}
{/* Sort buttons */}
{sameRegion.length > 0 && (
{sameRegion.map(c => renderCandidate(c))}
)} {crossRegion.length > 0 && ( <>
跨区候选 · {crossRegion.length} 辆
{crossRegion.map(c => renderCandidate(c))}
)} {displayCount === 0 && (
当前筛选条件下无可替换车辆
)}
{/* Footer */}
{/* Full-screen swap preview */} {previewCandidate && ( setPreviewCandidate(null)} onSuccess={() => { setSentPlates(prev => new Set(prev).add(previewCandidate.plateNumber)); setPreviewCandidate(null); onNotifySuccess(); }} /> )}
); }