feat(scheduling): enrich history records with customer/dept/manager + drill-in to swap plan
Each row in 调度记录 now shows 业务部门(简)/业务负责人/客户 beneath the plate line, and is clickable to open the reusable SwapPreview showing the full replacement plan (current mileage, 考核目标, 替换后预测). Drill-in is only enabled when the suggestion is still in the active scheduling view; the user can still 取消干预 from the preview. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,26 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { X, RotateCcw, Clock, CheckCircle2, XCircle, Send, Loader2 } from 'lucide-react';
|
||||
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 } from './types';
|
||||
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 }[] = [
|
||||
@@ -40,7 +47,7 @@ function fmtDateTime(iso: string): string {
|
||||
return `${y}-${m}-${day} ${hh}:${mm}`;
|
||||
}
|
||||
|
||||
export default function NotificationHistory({ onClose, onChange, recentOnly = false }: Props) {
|
||||
export default function NotificationHistory({ onClose, onChange, recentOnly = false, suggestions }: Props) {
|
||||
const [records, setRecords] = useState<NotificationRecord[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tab, setTab] = useState<StatusTab>('all');
|
||||
@@ -49,6 +56,13 @@ export default function NotificationHistory({ onClose, onChange, recentOnly = fa
|
||||
const [executeTarget, setExecuteTarget] = useState<NotificationRecord | null>(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<string, SchedulingSuggestion>();
|
||||
for (const s of suggestions ?? []) map.set(s.id, s);
|
||||
return map;
|
||||
}, [suggestions]);
|
||||
|
||||
const visibleRecords = recent7d
|
||||
? records.filter(r => {
|
||||
@@ -170,18 +184,41 @@ export default function NotificationHistory({ onClose, onChange, recentOnly = fa
|
||||
{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 (
|
||||
<div key={rec.id} className="px-4 py-3">
|
||||
<div
|
||||
key={rec.id}
|
||||
onClick={handleRowClick}
|
||||
className={`px-4 py-3 transition-colors ${canDrill ? 'cursor-pointer hover:bg-slate-50/60 active:bg-slate-100' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 mb-1">
|
||||
<div className="flex items-center gap-1.5 text-xs min-w-0">
|
||||
<span className="font-mono font-bold text-slate-900"><Blur>{rec.currentPlate}</Blur></span>
|
||||
<span className="text-slate-400">→</span>
|
||||
<span className="font-mono font-bold text-blue-700"><Blur>{rec.candidatePlate}</Blur></span>
|
||||
</div>
|
||||
<span className={`text-[9px] font-bold px-1.5 py-0.5 rounded flex items-center gap-0.5 flex-shrink-0 ${badge.cls}`}>
|
||||
{badge.icon} {badge.text}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
<span className={`text-[9px] font-bold px-1.5 py-0.5 rounded flex items-center gap-0.5 ${badge.cls}`}>
|
||||
{badge.icon} {badge.text}
|
||||
</span>
|
||||
{canDrill && <ChevronRight size={12} className="text-slate-300" />}
|
||||
</div>
|
||||
</div>
|
||||
{v && (
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-slate-500 mb-0.5 truncate">
|
||||
{v.department && <span className="font-medium">{shortDept(v.department)}</span>}
|
||||
{v.manager && <span>{v.manager}</span>}
|
||||
<span className="text-slate-400 truncate"><Blur>{v.customer || '-'}</Blur></span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3 text-[10px] text-slate-400">
|
||||
{rec.operatorName && <span>操作人 {rec.operatorName}</span>}
|
||||
<span>{fmtDateTime(rec.createdAt)}</span>
|
||||
@@ -193,7 +230,7 @@ export default function NotificationHistory({ onClose, onChange, recentOnly = fa
|
||||
<div className="mt-1 text-[10px] text-slate-500 bg-slate-50 rounded px-2 py-1">{rec.notes}</div>
|
||||
)}
|
||||
{rec.status === 'sent' && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<div className="mt-2 flex items-center gap-2" onClick={e => e.stopPropagation()}>
|
||||
<button
|
||||
onClick={() => handleExecuteClick(rec)}
|
||||
disabled={busy}
|
||||
@@ -290,6 +327,16 @@ export default function NotificationHistory({ onClose, onChange, recentOnly = fa
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Drill-down: replacement plan */}
|
||||
{drillTarget && (
|
||||
<SwapPreview
|
||||
suggestion={drillTarget.suggestion}
|
||||
candidate={drillTarget.candidate}
|
||||
onClose={() => setDrillTarget(null)}
|
||||
onSuccess={() => { load(); onChange?.(); }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user