diff --git a/src/modules/scheduling/NotificationHistory.tsx b/src/modules/scheduling/NotificationHistory.tsx new file mode 100644 index 0000000..69d8b2d --- /dev/null +++ b/src/modules/scheduling/NotificationHistory.tsx @@ -0,0 +1,272 @@ +import { useCallback, useEffect, useState } from 'react'; +import { X, RotateCcw, Clock, CheckCircle2, XCircle, Send, Loader2 } from 'lucide-react'; +import { motion, AnimatePresence } from 'motion/react'; +import { fetchNotifications, updateNotification } from './api'; +import type { NotificationRecord, NotificationStatus } from './types'; +import Blur from '../../components/Blur'; + +interface Props { + onClose: () => void; + onChange?: () => void; +} + +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 }: Props) { + const [records, setRecords] = useState([]); + const [loading, setLoading] = useState(false); + const [tab, setTab] = useState('all'); + const [mutatingId, setMutatingId] = useState(null); + const [executeTarget, setExecuteTarget] = useState(null); + const [afterMileageInput, setAfterMileageInput] = useState(''); + const [notesInput, setNotesInput] = useState(''); + + 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 ? ( +
+ 加载中 +
+ ) : records.length === 0 ? ( +
+ +

暂无记录

+
+ ) : ( +
+ {records.map(rec => { + const badge = statusBadge(rec.status); + const busy = mutatingId === rec.id; + return ( +
+
+
+ {rec.currentPlate} + + {rec.candidatePlate} +
+ + {badge.icon} {badge.text} + +
+
+ {rec.operatorName && 操作人 {rec.operatorName}} + {fmtDateTime(rec.createdAt)} + {rec.status === 'executed' && rec.executedAt && ( + 执行 {fmtDateTime(rec.executedAt)} + )} +
+ {rec.notes && ( +
{rec.notes}
+ )} + {rec.status === 'sent' && ( +
+ + +
+ )} +
+ ); + })} +
+ )} +
+
+ + {/* 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" + /> +
+
+ +