diff --git a/src/modules/scheduling/NotificationHistory.tsx b/src/modules/scheduling/NotificationHistory.tsx index 69d8b2d..e1bd56a 100644 --- a/src/modules/scheduling/NotificationHistory.tsx +++ b/src/modules/scheduling/NotificationHistory.tsx @@ -81,7 +81,7 @@ export default function NotificationHistory({ onClose, onChange }: Props) { }; const handleCancel = async (rec: NotificationRecord) => { - if (!confirm(`确定取消 ${rec.currentPlate} → ${rec.candidatePlate} 的替换通知?`)) return; + if (!confirm(`确定取消 ${rec.currentPlate} → ${rec.candidatePlate} 的干预?`)) return; setMutatingId(rec.id); try { await updateNotification(rec.id, { status: 'cancelled' }); diff --git a/src/modules/scheduling/SchedulingModule.tsx b/src/modules/scheduling/SchedulingModule.tsx index edc19df..7ee6b87 100644 --- a/src/modules/scheduling/SchedulingModule.tsx +++ b/src/modules/scheduling/SchedulingModule.tsx @@ -177,11 +177,13 @@ export default function SchedulingModule() { useEffect(() => { loadData(); }, [loadData]); const handleNotifySuccess = useCallback(() => { loadData(); }, [loadData]); - // Close detail modal if selected suggestion is filtered out or no longer exists + // Keep selectedSuggestion synced with latest data so candidate notification + // status changes (登记 / 取消干预) propagate into the open detail modal. useEffect(() => { if (!selectedSuggestion || !data) return; - const stillExists = data.suggestions.some(s => s.id === selectedSuggestion.id); - if (!stillExists) setSelectedSuggestion(null); + const fresh = data.suggestions.find(s => s.id === selectedSuggestion.id); + if (!fresh) setSelectedSuggestion(null); + else if (fresh !== selectedSuggestion) setSelectedSuggestion(fresh); }, [data, selectedSuggestion]); const toggleSelect = useCallback((id: string) => { @@ -227,7 +229,7 @@ export default function SchedulingModule() { exitSelectMode(); } catch (e) { console.error('batch notify failed:', e); - setBatchResultMsg('批量通知失败,请重试'); + setBatchResultMsg('批量干预失败,请重试'); } finally { setBatchInFlight(false); } @@ -534,7 +536,7 @@ export default function SchedulingModule() { disabled={selectedIds.size === 0} className="flex items-center gap-1.5 text-xs font-bold bg-blue-600 hover:bg-blue-500 disabled:bg-slate-700 disabled:text-slate-400 text-white px-3 py-1.5 rounded-lg cursor-pointer disabled:cursor-not-allowed transition-colors" > - 批量通知 + 批量干预 @@ -551,7 +553,7 @@ export default function SchedulingModule() { className="bg-white rounded-t-2xl sm:rounded-2xl shadow-2xl w-full sm:max-w-md overflow-hidden flex flex-col max-h-[80vh] sm:mx-4" >
- 确认批量通知 + 确认批量干预

- 将发送 {batchItems.length} 条替换通知,已排除无可用候选车的建议。 + 将登记 {batchItems.length} 条干预,已排除无可用候选车的建议。

{batchItems.map(({ suggestion, candidate }) => ( @@ -597,7 +599,7 @@ export default function SchedulingModule() { disabled={batchInFlight || batchItems.length === 0} className="flex-1 py-2 text-xs font-bold text-white bg-blue-600 hover:bg-blue-500 rounded-lg cursor-pointer disabled:opacity-50 transition-colors" > - {batchInFlight ? '发送中...' : `确认发送 ${batchItems.length} 条`} + {batchInFlight ? '登记中...' : `确认登记 ${batchItems.length} 条`}
diff --git a/src/modules/scheduling/SuggestionDetail.tsx b/src/modules/scheduling/SuggestionDetail.tsx index 71e922f..2d23d57 100644 --- a/src/modules/scheduling/SuggestionDetail.tsx +++ b/src/modules/scheduling/SuggestionDetail.tsx @@ -113,14 +113,13 @@ export default function SuggestionDetail({ suggestion: s, onClose, onNotifySucce
diff --git a/src/modules/scheduling/SuggestionList.tsx b/src/modules/scheduling/SuggestionList.tsx index 9fa62cd..9d467a0 100644 --- a/src/modules/scheduling/SuggestionList.tsx +++ b/src/modules/scheduling/SuggestionList.tsx @@ -133,7 +133,7 @@ export default function SuggestionList({ suggestions, onSelect, selectMode = fal {v.region} {notified && ( - 已通知 + 已干预 )} diff --git a/src/modules/scheduling/SwapPreview.tsx b/src/modules/scheduling/SwapPreview.tsx index 28874ea..470d4cb 100644 --- a/src/modules/scheduling/SwapPreview.tsx +++ b/src/modules/scheduling/SwapPreview.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { ArrowDownUp, CheckCircle, Send, X } from 'lucide-react'; -import { sendNotify } from './api'; +import { ArrowDownUp, CheckCircle, Send, X, Ban } from 'lucide-react'; +import { sendNotify, updateNotification } from './api'; import type { SchedulingSuggestion, CandidateVehicle } from './types'; import Blur from '../../components/Blur'; @@ -23,10 +23,15 @@ function fmtRate(rate: number): string { export default function SwapPreview({ suggestion: s, candidate: c, onClose, onSuccess }: Props) { const [sending, setSending] = useState(false); const [sent, setSent] = useState(false); + const [cancelling, setCancelling] = useState(false); const v = s.currentVehicle; + const alreadyIntervened = + !sent && (c.notificationStatus === 'sent' || c.notificationStatus === 'executed'); + const isExecuted = c.notificationStatus === 'executed'; + const handleSend = async () => { - if (sending || sent) return; + if (sending || sent || alreadyIntervened) return; setSending(true); try { const result = await sendNotify({ suggestionId: s.id, currentPlate: v.plateNumber, candidatePlate: c.plateNumber }); @@ -34,6 +39,21 @@ export default function SwapPreview({ suggestion: s, candidate: c, onClose, onSu } catch { alert('网络错误'); } finally { setSending(false); } }; + const handleCancel = async () => { + if (!c.notificationId || cancelling) return; + if (isExecuted) { + if (!confirm('此干预已标记为执行。确定要取消吗?')) return; + } else { + if (!confirm(`确定取消 ${v.plateNumber} → ${c.plateNumber} 的干预?`)) return; + } + setCancelling(true); + try { + const result = await updateNotification(c.notificationId, { status: 'cancelled' }); + if (result.success) { onSuccess(); onClose(); } + else { alert(result.message || '取消失败'); } + } catch { alert('网络错误'); } finally { setCancelling(false); } + }; + return (
{/* Header */} @@ -115,16 +135,32 @@ export default function SwapPreview({ suggestion: s, candidate: c, onClose, onSu {/* Bottom */}
-
- +
+ {alreadyIntervened && ( +
+ + 此车已{isExecuted ? '执行干预' : '登记干预'},如需重新干预请先取消。 +
+ )} + {alreadyIntervened ? ( + + ) : ( + + )}
diff --git a/src/modules/scheduling/csv-export.ts b/src/modules/scheduling/csv-export.ts index 223d3c4..d6454e0 100644 --- a/src/modules/scheduling/csv-export.ts +++ b/src/modules/scheduling/csv-export.ts @@ -41,7 +41,7 @@ const HEADERS = [ '候选替换后预估(km)', '候选可达标', '候选区域', - '通知状态', + '干预状态', ] as const; export function buildSuggestionsCsv(suggestions: SchedulingSuggestion[]): string { diff --git a/src/server/routes/scheduling/db-schema.ts b/src/server/routes/scheduling/db-schema.ts index e92c19f..0e046e8 100644 --- a/src/server/routes/scheduling/db-schema.ts +++ b/src/server/routes/scheduling/db-schema.ts @@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS tab_scheduling_notifications ( INDEX idx_candidate_plate (candidate_plate), INDEX idx_status (status), INDEX idx_created_at (created_at) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能调度通知/执行记录' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能调度干预/执行记录' `; export async function ensureSchedulingTables(): Promise { diff --git a/src/server/routes/scheduling/notify.ts b/src/server/routes/scheduling/notify.ts index 6b74641..50e6e62 100644 --- a/src/server/routes/scheduling/notify.ts +++ b/src/server/routes/scheduling/notify.ts @@ -119,12 +119,12 @@ app.post('/', async (c) => { return c.json({ success: true, - message: `替换通知已发送:${currentPlate} → ${candidatePlate}`, + message: `干预已登记:${currentPlate} → ${candidatePlate}`, record: result, }); } catch (e: unknown) { console.error('scheduling notify error:', e); - return c.json({ success: false, message: '发送通知失败' }, 500); + return c.json({ success: false, message: '登记干预失败' }, 500); } }); @@ -166,12 +166,12 @@ app.post('/batch', async (c) => { return c.json({ success: true, - message: `批量通知:成功 ${result.success},跳过 ${result.skipped},失败 ${result.failed}`, + message: `批量干预:成功 ${result.success},跳过 ${result.skipped},失败 ${result.failed}`, result, }); } catch (e: unknown) { console.error('scheduling batch notify error:', e); - return c.json({ success: false, message: '批量通知失败' }, 500); + return c.json({ success: false, message: '批量干预失败' }, 500); } });