feat(scheduling): history view, execute/cancel lifecycle, CSV export, 7d trend

- Add 调度记录 modal: lists notifications by status, supports 标记已执行 (with
  after-mileage + notes) and 取消 for open records
- Add CSV export of filtered suggestions (UTF-8 BOM for Excel); top candidate
  per row picked by same-region > can-qualify preference
- Compute customer 7-day average alongside 30-day baseline in a single query;
  show trend indicator (up/down/flat) next to 客户日均 in list and detail card

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-16 23:47:31 +08:00
parent 3ef0d4edfa
commit 1d9f4cb43d
9 changed files with 459 additions and 18 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useMemo } from 'react';
import {
X, MapPin, AlertTriangle, CheckCircle, ArrowDown, ArrowUp, ArrowRight, ArrowUpDown,
TrendingUp, TrendingDown, Minus,
} from 'lucide-react';
import { motion } from 'motion/react';
import type { SchedulingSuggestion, CandidateVehicle } from './types';
@@ -175,7 +176,28 @@ export default function SuggestionDetail({ suggestion: s, onClose, onNotifySucce
{v.manager && <span><b className="text-slate-700">{v.manager}</b></span>}
{(v.department || v.manager) && <span className="text-slate-200">|</span>}
<span> <b className="text-slate-700"><Blur>{v.customer || '-'}</Blur></b></span>
<span> <b className="text-slate-700">{Math.round(v.customerAvgDaily)}</b> km</span>
<span className="flex items-center gap-1">
30 <b className="text-slate-700">{Math.round(v.customerAvgDaily)}</b> km
{v.customerAvgDaily > 0 && v.customerAvgDaily7d > 0 && (() => {
const diff = (v.customerAvgDaily7d - v.customerAvgDaily) / v.customerAvgDaily;
const pct = Math.round(diff * 100);
if (diff >= 0.1) return (
<span className="text-emerald-600 flex items-center gap-0.5" title={`7日均 ${Math.round(v.customerAvgDaily7d)} km`}>
<TrendingUp size={10} /> 7 +{pct}%
</span>
);
if (diff <= -0.1) return (
<span className="text-rose-500 flex items-center gap-0.5" title={`7日均 ${Math.round(v.customerAvgDaily7d)} km`}>
<TrendingDown size={10} /> 7 {pct}%
</span>
);
return (
<span className="text-slate-400 flex items-center gap-0.5" title={`7日均 ${Math.round(v.customerAvgDaily7d)} km`}>
<Minus size={10} /> 7
</span>
);
})()}
</span>
</div>
{/* Metrics */}
<div className="px-3 pb-2">