feat(scheduling): rename 通知→干预, allow drill-in on intervened items

- Globally rename user-facing 通知 → 干预 (list badge, detail button, batch
  modal, CSV header, server response messages, db table comment)
- 已干预 row in detail is now clickable — opens SwapPreview which shows
  a read-only summary plus a 取消干预 action (PATCH notify /:id with
  status=cancelled). Sending is blocked while already intervened.
- Selected suggestion now follows the latest data snapshot so status
  changes from within the detail flow propagate immediately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-17 09:14:53 +08:00
parent 1d9f4cb43d
commit 210db7f8ff
8 changed files with 69 additions and 32 deletions

View File

@@ -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 (
<div className="fixed inset-0 z-[80] bg-[#F0F4F8] flex flex-col">
{/* Header */}
@@ -115,16 +135,32 @@ export default function SwapPreview({ suggestion: s, candidate: c, onClose, onSu
{/* Bottom */}
<div className="px-5 pb-6 pt-2 flex-shrink-0 bg-[#F0F4F8]">
<div className="max-w-sm mx-auto">
<button
onClick={handleSend}
disabled={sending || sent}
className={`w-full flex items-center justify-center gap-2 py-3.5 rounded-xl text-sm font-bold transition-all cursor-pointer ${
sent ? 'bg-emerald-100 text-emerald-600' : 'bg-slate-800 hover:bg-slate-900 text-white active:scale-[0.98] shadow-lg'
}`}
>
{sent ? <><CheckCircle size={16} /> </> : <><Send size={16} /> </>}
</button>
<div className="max-w-sm mx-auto space-y-2">
{alreadyIntervened && (
<div className="rounded-xl bg-emerald-50 border border-emerald-200 px-3 py-2 text-[11px] text-emerald-700 flex items-center gap-2">
<CheckCircle size={13} />
<span>{isExecuted ? '执行干预' : '登记干预'}</span>
</div>
)}
{alreadyIntervened ? (
<button
onClick={handleCancel}
disabled={cancelling}
className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl text-sm font-bold bg-white text-rose-600 border border-rose-200 hover:bg-rose-50 active:scale-[0.98] transition-all cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<Ban size={16} /> {cancelling ? '取消中...' : '取消干预'}
</button>
) : (
<button
onClick={handleSend}
disabled={sending || sent}
className={`w-full flex items-center justify-center gap-2 py-3.5 rounded-xl text-sm font-bold transition-all cursor-pointer ${
sent ? 'bg-emerald-100 text-emerald-600' : 'bg-slate-800 hover:bg-slate-900 text-white active:scale-[0.98] shadow-lg'
}`}
>
{sent ? <><CheckCircle size={16} /> </> : <><Send size={16} /> </>}
</button>
)}
</div>
</div>
</div>