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, 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 }[] = [ { key: 'all', label: '全部' }, { key: 'sent', label: '待执行' }, { key: 'executed', label: '已执行' }, { key: 'cancelled', label: '已取消' }, ]; function statusBadge(status: NotificationStatus) { if (status === 'sent') return { text: '待执行', icon: , cls: 'text-amber-700 bg-amber-50' }; if (status === 'executed') return { text: '已执行', icon: , cls: 'text-emerald-700 bg-emerald-50' }; return { text: '已取消', icon: , cls: 'text-slate-500 bg-slate-100' }; } function fmtDateTime(iso: string): string { if (!iso) return ''; const d = new Date(iso); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); return `${y}-${m}-${day} ${hh}:${mm}`; } export default function NotificationHistory({ onClose, onChange, recentOnly = false, suggestions }: Props) { const [records, setRecords] = useState([]); const [loading, setLoading] = useState(false); const [tab, setTab] = useState('all'); const [recent7d, setRecent7d] = useState(recentOnly); const [mutatingId, setMutatingId] = useState(null); 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 => { const t = Date.parse(r.createdAt); return Number.isFinite(t) && Date.now() - t <= SEVEN_DAYS_MS; }) : records; const load = useCallback(async () => { setLoading(true); try { const resp = await fetchNotifications(tab === 'all' ? undefined : tab); setRecords(resp.records); } finally { setLoading(false); } }, [tab]); useEffect(() => { load(); }, [load]); const handleExecuteClick = (rec: NotificationRecord) => { setExecuteTarget(rec); setAfterMileageInput(''); setNotesInput(''); }; const handleExecuteConfirm = async () => { if (!executeTarget) return; setMutatingId(executeTarget.id); try { const body: { status: NotificationStatus; notes?: string; afterMileage?: number } = { status: 'executed' }; if (notesInput.trim()) body.notes = notesInput.trim(); const parsed = Number(afterMileageInput); if (Number.isFinite(parsed) && parsed > 0) body.afterMileage = parsed; await updateNotification(executeTarget.id, body); setExecuteTarget(null); await load(); onChange?.(); } finally { setMutatingId(null); } }; const handleCancel = async (rec: NotificationRecord) => { if (!confirm(`确定取消 ${rec.currentPlate} → ${rec.candidatePlate} 的干预?`)) return; setMutatingId(rec.id); try { await updateNotification(rec.id, { status: 'cancelled' }); await load(); onChange?.(); } finally { setMutatingId(null); } }; return (
e.stopPropagation()} className="bg-white rounded-t-2xl sm:rounded-2xl shadow-2xl w-full sm:max-w-xl overflow-hidden flex flex-col max-h-[85vh] sm:mx-4" > {/* Header */}
调度记录
{/* Status tabs */}
{STATUS_TABS.map(t => ( ))}
{/* Body */}
{loading && records.length === 0 ? (
加载中
) : visibleRecords.length === 0 ? (

{recent7d ? '最近 7 天暂无干预记录' : '暂无记录'}

) : (
{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} {canDrill && }
{v && (
{v.department && {shortDept(v.department)}} {v.manager && {v.manager}} {v.customer || '-'}
)}
{rec.operatorName && 操作人 {rec.operatorName}} {fmtDateTime(rec.createdAt)} {rec.status === 'executed' && rec.executedAt && ( 执行 {fmtDateTime(rec.executedAt)} )}
{rec.notes && (
{rec.notes}
)} {rec.status === 'sent' && (
e.stopPropagation()}>
)}
); })}
)}
{/* Execute confirmation modal */} {executeTarget && (
mutatingId === null && setExecuteTarget(null)} > e.stopPropagation()} className="bg-white rounded-t-2xl sm:rounded-2xl shadow-2xl w-full sm:max-w-sm overflow-hidden flex flex-col sm:mx-4" >
确认已执行
{executeTarget.currentPlate} {executeTarget.candidatePlate}
setAfterMileageInput(e.target.value)} placeholder="例如 45230" className="w-full px-3 py-2 text-xs bg-slate-50 rounded-lg border border-slate-200 outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-400 transition-all" />