import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Activity, RotateCcw, Search, ChevronDown } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { fetchSuggestions, sendNotifyBatch } from './api'; import type { SchedulingResponse, SchedulingSuggestion } from './types'; import SuggestionList from './SuggestionList'; import SuggestionDetail from './SuggestionDetail'; import NotificationHistory from './NotificationHistory'; import { exportSuggestionsCsv } from './csv-export'; import RotatingFooterHint from '../../components/RotatingFooterHint'; import { PageFrame } from '../../components/ui/surface'; import ActiveFilterTags from './scheduling-module/ActiveFilterTags'; import BatchActionBar from './scheduling-module/BatchActionBar'; import BatchConfirmModal from './scheduling-module/BatchConfirmModal'; import ListHeader from './scheduling-module/ListHeader'; import ListLoadingSkeleton from './scheduling-module/ListLoadingSkeleton'; import SchedulingSkeleton from './scheduling-module/SchedulingSkeleton'; import SummaryCards from './scheduling-module/SummaryCards'; import { EMPTY_FILTERS, buildBatchItems, buildFilterOptions, countActiveFilters, filterSuggestions, hasResettableFilters, type AdvancedFilters, type TypeFilter, } from './scheduling-module/model'; function FilterSelect({ label, options, value, onChange, placeholder }: { label: string; options: string[]; value: string; onChange: (v: string) => void; placeholder: string; }) { const [open, setOpen] = useState(false); const [search, setSearch] = useState(''); const ref = useRef(null); const filtered = options.filter(o => o.toLowerCase().includes(search.toLowerCase())); useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); return (
{open && (
{options.length > 5 && (
setSearch(e.target.value)} placeholder="搜索..." autoFocus className="w-full pl-7 pr-2 py-1.5 text-xs bg-slate-50 rounded border-none outline-none" />
)}
{filtered.map(opt => ( ))}
)}
); } export default function SchedulingModule() { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [selectedTargetId, setSelectedTargetId] = useState(undefined); const [selectedSuggestion, setSelectedSuggestion] = useState(null); const [typeFilter, setTypeFilter] = useState('all'); const [showFilter, setShowFilter] = useState(false); const [filters, setFilters] = useState(EMPTY_FILTERS); const [tempFilters, setTempFilters] = useState(EMPTY_FILTERS); const [selectMode, setSelectMode] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); const [showBatchConfirm, setShowBatchConfirm] = useState(false); const [batchInFlight, setBatchInFlight] = useState(false); const [batchResultMsg, setBatchResultMsg] = useState(null); const [showHistory, setShowHistory] = useState(false); const [historyRecentOnly, setHistoryRecentOnly] = useState(false); const loadData = useCallback(async () => { setLoading(true); try { setData(await fetchSuggestions(selectedTargetId)); } finally { setLoading(false); } }, [selectedTargetId]); useEffect(() => { loadData(); }, [loadData]); const handleNotifySuccess = useCallback(() => { loadData(); }, [loadData]); // Keep selectedSuggestion synced with latest data so candidate notification // status changes (登记 / 取消干预) propagate into the open detail modal. useEffect(() => { if (!selectedSuggestion || !data) return; const fresh = data.suggestions.find(s => s.id === selectedSuggestion.id); if (!fresh) setSelectedSuggestion(null); else if (fresh !== selectedSuggestion) setSelectedSuggestion(fresh); }, [data, selectedSuggestion]); const toggleSelect = useCallback((id: string) => { setSelectedIds(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }, []); const exitSelectMode = useCallback(() => { setSelectMode(false); setSelectedIds(new Set()); setShowBatchConfirm(false); }, []); const batchItems = useMemo(() => { return buildBatchItems(data, selectedIds); }, [data, selectedIds]); const handleBatchSubmit = useCallback(async () => { if (batchItems.length === 0) return; setBatchInFlight(true); try { const resp = await sendNotifyBatch({ items: batchItems.map(i => ({ suggestionId: i.suggestion.id, currentPlate: i.suggestion.currentVehicle.plateNumber, candidatePlate: i.candidate.plateNumber, })), }); setBatchResultMsg(resp.message); await loadData(); exitSelectMode(); } catch (e) { console.error('batch notify failed:', e); setBatchResultMsg('批量干预失败,请重试'); } finally { setBatchInFlight(false); } }, [batchItems, loadData, exitSelectMode]); const filterOptions = useMemo(() => { return buildFilterOptions(data); }, [data]); const filteredSuggestions = useMemo(() => { return filterSuggestions(data, typeFilter, filters); }, [data, typeFilter, filters]); const summary = data?.summary; const activeFilterCount = countActiveFilters(filters); // Initial load — full page skeleton if (loading && !data) return ; return ( 刷新建议 )} > {/* ===== Summary Cards ===== */} { setShowHistory(true); setHistoryRecentOnly(true); }} /> {/* ===== List Card ===== */}
{/* Header */} 0} onRefresh={loadData} onExport={() => exportSuggestionsCsv(filteredSuggestions)} onShowHistory={() => { setShowHistory(true); setHistoryRecentOnly(false); }} onToggleSelectMode={() => { if (selectMode) exitSelectMode(); else { setSelectMode(true); setSelectedSuggestion(null); } }} onToggleFilter={() => { setShowFilter(!showFilter); setTempFilters(filters); }} onSelectAll={() => { setSelectedTargetId(undefined); setTypeFilter('all'); }} onSelectTarget={targetId => { setSelectedTargetId(targetId); setTypeFilter('all'); }} /> {/* Filter Panel */} {showFilter && (
高级筛选 {hasResettableFilters(tempFilters) && ( )}
setTempFilters(prev => ({ ...prev, plateSearch: e.target.value }))} placeholder="搜索车牌号..." className="w-full pl-8 pr-3 py-2 bg-white rounded-lg text-xs border border-slate-200 outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all" />
setTempFilters(prev => ({ ...prev, region: v }))} placeholder="全部区域" /> setTempFilters(prev => ({ ...prev, vehicleType: v }))} placeholder="全部类型" />
setTempFilters(prev => ({ ...prev, department: v }))} placeholder="全部部门" /> setTempFilters(prev => ({ ...prev, manager: v }))} placeholder="全部负责人" />
setTempFilters(prev => ({ ...prev, customer: v }))} placeholder="全部客户" />
)}
{/* Active filter tags */} {activeFilterCount > 0 && !showFilter && ( setFilters(EMPTY_FILTERS)} /> )} {(activeFilterCount > 0 || typeFilter !== 'all') && (
共 {filteredSuggestions.length} 条结果
)} {loading ? ( /* List skeleton while refreshing */ ) : ( )}
{selectedSuggestion && ( setSelectedSuggestion(null)} onNotifySuccess={handleNotifySuccess} /> )} {showHistory && ( setShowHistory(false)} onChange={loadData} recentOnly={historyRecentOnly} suggestions={data?.suggestions} /> )} {/* Batch action bar */} {selectMode && ( setShowBatchConfirm(true)} /> )} {/* Batch confirmation modal */} {showBatchConfirm && ( setShowBatchConfirm(false)} onSubmit={handleBatchSubmit} /> )}
); }