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,10 +1,12 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { Filter, RotateCcw, X, Search, ChevronDown, CheckSquare, Send } from 'lucide-react';
import { Filter, RotateCcw, X, Search, ChevronDown, CheckSquare, Send, Clock, Download } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { fetchSuggestions, sendNotifyBatch } from './api';
import type { SchedulingResponse, SchedulingSuggestion, CandidateVehicle } from './types';
import SuggestionList from './SuggestionList';
import SuggestionDetail from './SuggestionDetail';
import NotificationHistory from './NotificationHistory';
import { exportSuggestionsCsv } from './csv-export';
import Blur from '../../components/Blur';
type TypeFilter = 'all' | 'qualified' | 'hopeless';
@@ -165,6 +167,7 @@ export default function SchedulingModule() {
const [showBatchConfirm, setShowBatchConfirm] = useState(false);
const [batchInFlight, setBatchInFlight] = useState(false);
const [batchResultMsg, setBatchResultMsg] = useState<string | null>(null);
const [showHistory, setShowHistory] = useState(false);
const loadData = useCallback(async () => {
setLoading(true);
@@ -346,6 +349,21 @@ export default function SchedulingModule() {
className="p-1.5 text-slate-400 hover:text-slate-600 transition-colors rounded-lg hover:bg-slate-50 cursor-pointer">
<RotateCcw size={15} className={loading ? 'animate-spin' : ''} />
</button>
<button
onClick={() => exportSuggestionsCsv(filteredSuggestions)}
disabled={filteredSuggestions.length === 0}
className="p-1.5 text-slate-400 hover:text-slate-600 transition-colors rounded-lg hover:bg-slate-50 cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
title="导出 CSV"
>
<Download size={15} />
</button>
<button
onClick={() => setShowHistory(true)}
className="p-1.5 text-slate-400 hover:text-slate-600 transition-colors rounded-lg hover:bg-slate-50 cursor-pointer"
title="调度记录"
>
<Clock size={15} />
</button>
<button
onClick={() => {
if (selectMode) exitSelectMode();
@@ -486,6 +504,10 @@ export default function SchedulingModule() {
<SuggestionDetail suggestion={selectedSuggestion} onClose={() => setSelectedSuggestion(null)} onNotifySuccess={handleNotifySuccess} />
)}
{showHistory && (
<NotificationHistory onClose={() => setShowHistory(false)} onChange={loadData} />
)}
{/* Batch action bar */}
<AnimatePresence>
{selectMode && (