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 => ( setTab(t.key)} className={`text-[11px] px-3 py-1 rounded-full font-medium cursor-pointer transition-colors ${ tab === t.key ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200' }`} > {t.label} ))} setRecent7d(v => !v)} className={`text-[11px] px-3 py-1 rounded-full font-medium cursor-pointer transition-colors ${ recent7d ? 'bg-emerald-600 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200' }`} title="仅看最近 7 天" > 近 7 天 {/* 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()}> handleExecuteClick(rec)} disabled={busy} className="text-[10px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 px-2.5 py-1 rounded cursor-pointer transition-colors flex items-center gap-1" > 标记已执行 handleCancel(rec)} disabled={busy} className="text-[10px] font-medium text-slate-500 hover:text-rose-600 disabled:opacity-50 px-2 py-1 rounded cursor-pointer transition-colors" > 取消 )} ); })} )} {/* 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" > 确认已执行 mutatingId === null && setExecuteTarget(null)} disabled={mutatingId !== null} className="text-emerald-100 hover:text-white p-1 cursor-pointer disabled:opacity-50" > {executeTarget.currentPlate} → {executeTarget.candidatePlate} 执行后里程 (km, 可选) 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" /> 备注 (可选) setNotesInput(e.target.value)} rows={2} placeholder="例如:司机已到位,交接完成" 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 resize-none" /> setExecuteTarget(null)} disabled={mutatingId !== null} className="flex-1 py-2 text-xs font-bold text-slate-500 bg-slate-50 hover:bg-slate-100 rounded-lg cursor-pointer disabled:opacity-50 transition-colors" > 取消 {mutatingId !== null ? '保存中...' : '确认'} )} {/* Drill-down: replacement plan */} {drillTarget && ( setDrillTarget(null)} onSuccess={() => { load(); onChange?.(); }} /> )} ); }
{recent7d ? '最近 7 天暂无干预记录' : '暂无记录'}