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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user