From 939868882987ef0e23519352e63e9fb37f0d7d4f Mon Sep 17 00:00:00 2001 From: kkfluous Date: Thu, 16 Apr 2026 21:28:04 +0800 Subject: [PATCH] feat(scheduling): add department/manager filters, refine color palette MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add department and manager fields to backend types and suggestions API - Add department/manager to advanced filter panel - Refine card colors: orange (换下) / blue (换走) / dark slate (全部) - Selected card uses solid bg color, inactive uses gradient - Batch pills use dark slate, confirm button uses dark slate - Background changed to #F0F4F8 for subtle cool tone Co-Authored-By: Claude Opus 4.6 (1M context) --- src/modules/scheduling/SchedulingModule.tsx | 316 ++++++-------------- src/modules/scheduling/types.ts | 2 + src/server/routes/scheduling/algorithm.ts | 2 + src/server/routes/scheduling/suggestions.ts | 2 + src/server/routes/scheduling/types.ts | 4 + 5 files changed, 109 insertions(+), 217 deletions(-) diff --git a/src/modules/scheduling/SchedulingModule.tsx b/src/modules/scheduling/SchedulingModule.tsx index 55cc995..89c81e9 100644 --- a/src/modules/scheduling/SchedulingModule.tsx +++ b/src/modules/scheduling/SchedulingModule.tsx @@ -13,9 +13,11 @@ interface AdvancedFilters { region: string; vehicleType: string; customer: string; + department: string; + manager: string; } -const EMPTY_FILTERS: AdvancedFilters = { plateSearch: '', region: '', vehicleType: '', customer: '' }; +const EMPTY_FILTERS: AdvancedFilters = { plateSearch: '', region: '', vehicleType: '', customer: '', department: '', manager: '' }; function shortTargetName(name: string): string { const match = name.match(/(\d+)[辆台](.+)/); @@ -32,24 +34,16 @@ function hasActiveFilters(f: AdvancedFilters): boolean { return f.plateSearch !== '' || f.region !== '' || f.vehicleType !== '' || f.customer !== ''; } -/** Dropdown select with search */ function FilterSelect({ label, options, value, onChange, placeholder }: { - label: string; - options: string[]; - value: string; - onChange: (v: string) => void; - placeholder: string; + 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); - }; + 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); }, []); @@ -65,37 +59,22 @@ function FilterSelect({ label, options, value, onChange, placeholder }: { {open && ( -
+
{options.length > 5 && (
- setSearch(e.target.value)} - placeholder="搜索..." - className="w-full pl-7 pr-2 py-1.5 text-xs bg-slate-50 rounded border-none outline-none" - autoFocus - /> + 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 => ( - + ))}
@@ -116,169 +95,153 @@ export default function SchedulingModule() { const loadData = useCallback(async () => { setLoading(true); - try { - const result = await fetchSuggestions(selectedTargetId); - setData(result); - } finally { - setLoading(false); - } + try { setData(await fetchSuggestions(selectedTargetId)); } finally { setLoading(false); } }, [selectedTargetId]); useEffect(() => { loadData(); }, [loadData]); const handleNotifySuccess = useCallback(() => { loadData(); }, [loadData]); - // Compute filter options from data const filterOptions = useMemo(() => { - if (!data) return { regions: [], vehicleTypes: [], customers: [] }; - const regions = new Set(); - const vehicleTypes = new Set(); - const customers = new Set(); + if (!data) return { regions: [], vehicleTypes: [], customers: [], departments: [], managers: [] }; + const r = new Set(), t = new Set(), c = new Set(), d = new Set(), m = new Set(); for (const s of data.suggestions) { const v = s.currentVehicle; - if (v.region) regions.add(v.region); - if (v.vehicleType) vehicleTypes.add(v.vehicleType); - if (v.customer) customers.add(v.customer); + if (v.region) r.add(v.region); + if (v.vehicleType) t.add(v.vehicleType); + if (v.customer) c.add(v.customer); + if (v.department) d.add(v.department); + if (v.manager) m.add(v.manager); } - return { - regions: Array.from(regions).sort(), - vehicleTypes: Array.from(vehicleTypes).sort(), - customers: Array.from(customers).sort(), - }; + return { regions: [...r].sort(), vehicleTypes: [...t].sort(), customers: [...c].sort(), departments: [...d].sort(), managers: [...m].sort() }; }, [data]); const filteredSuggestions = useMemo(() => { if (!data) return []; let list = data.suggestions; - - // Type filter from cards if (typeFilter === 'qualified') list = list.filter(s => s.type === 'replace_qualified'); if (typeFilter === 'hopeless') list = list.filter(s => s.type === 'rescue_hopeless'); - - // Advanced filters - if (filters.plateSearch) { - const q = filters.plateSearch.toLowerCase(); - list = list.filter(s => s.currentVehicle.plateNumber.toLowerCase().includes(q)); - } + if (filters.plateSearch) { const q = filters.plateSearch.toLowerCase(); list = list.filter(s => s.currentVehicle.plateNumber.toLowerCase().includes(q)); } if (filters.region) list = list.filter(s => s.currentVehicle.region === filters.region); if (filters.vehicleType) list = list.filter(s => s.currentVehicle.vehicleType === filters.vehicleType); if (filters.customer) list = list.filter(s => s.currentVehicle.customer === filters.customer); - + if (filters.department) list = list.filter(s => s.currentVehicle.department === filters.department); + if (filters.manager) list = list.filter(s => s.currentVehicle.manager === filters.manager); return list; }, [data, typeFilter, filters]); const summary = data?.summary; - const activeFilterCount = [filters.plateSearch, filters.region, filters.vehicleType, filters.customer].filter(Boolean).length; + const activeFilterCount = [filters.plateSearch, filters.region, filters.vehicleType, filters.customer, filters.department, filters.manager].filter(Boolean).length; return ( -
+
- {/* ========== Top: Summary Cards ========== */} -
+ {/* ===== Summary Cards ===== */} +
+ {/* 里程高·换下 — warm orange */} + {/* 里程低·换走 — cool blue */} + {/* 替换建议 — neutral dark */}
- {/* ========== Bottom: List Card ========== */} -
+ {/* ===== List Card ===== */} +
{/* Header */}

智能调度干预清单

-
- {/* Batch selector pills */}
{data?.targets.map(t => ( -
- {/* Advanced Filter Panel */} + {/* Filter Panel */} {showFilter && ( - -
+ +
高级筛选 {hasActiveFilters(tempFilters) && ( - + )}
- - {/* Plate search */}
- 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, 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" />
- - {/* Selects in 2-column grid */}
- setTempFilters(prev => ({ ...prev, region: v }))} - placeholder="全部区域" - /> - setTempFilters(prev => ({ ...prev, vehicleType: v }))} - placeholder="全部类型" - /> + setTempFilters(prev => ({ ...prev, region: v }))} placeholder="全部区域" /> + setTempFilters(prev => ({ ...prev, vehicleType: v }))} placeholder="全部类型" />
- - setTempFilters(prev => ({ ...prev, customer: v }))} - placeholder="全部客户" - /> - - {/* Apply / Cancel */} +
+ setTempFilters(prev => ({ ...prev, department: v }))} placeholder="全部部门" /> + setTempFilters(prev => ({ ...prev, manager: v }))} placeholder="全部负责人" /> +
+ setTempFilters(prev => ({ ...prev, customer: v }))} placeholder="全部客户" />
- - + +
@@ -374,66 +291,31 @@ export default function SchedulingModule() { {activeFilterCount > 0 && !showFilter && (
筛选: - {filters.plateSearch && ( - - 车牌 "{filters.plateSearch}" - setFilters(prev => ({ ...prev, plateSearch: '' }))} /> - - )} - {filters.region && ( - - {filters.region} - setFilters(prev => ({ ...prev, region: '' }))} /> - - )} - {filters.vehicleType && ( - - {filters.vehicleType} - setFilters(prev => ({ ...prev, vehicleType: '' }))} /> - - )} - {filters.customer && ( - - {filters.customer} - setFilters(prev => ({ ...prev, customer: '' }))} /> - - )} - + {filters.plateSearch && 车牌 "{filters.plateSearch}" setFilters(prev => ({ ...prev, plateSearch: '' }))} />} + {filters.region && {filters.region} setFilters(prev => ({ ...prev, region: '' }))} />} + {filters.vehicleType && {filters.vehicleType} setFilters(prev => ({ ...prev, vehicleType: '' }))} />} + {filters.department && {filters.department} setFilters(prev => ({ ...prev, department: '' }))} />} + {filters.manager && {filters.manager} setFilters(prev => ({ ...prev, manager: '' }))} />} + {filters.customer && {filters.customer} setFilters(prev => ({ ...prev, customer: '' }))} />} +
)} - {/* Result count when filtered */} {(activeFilterCount > 0 || typeFilter !== 'all') && ( -
- 共 {filteredSuggestions.length} 条结果 -
+
共 {filteredSuggestions.length} 条结果
)} - {/* List body */} {loading && !data ? (
) : ( - + )}
- {/* Detail Modal */} {selectedSuggestion && ( - setSelectedSuggestion(null)} - onNotifySuccess={handleNotifySuccess} - /> + setSelectedSuggestion(null)} onNotifySuccess={handleNotifySuccess} /> )}
diff --git a/src/modules/scheduling/types.ts b/src/modules/scheduling/types.ts index f78fd0d..a90bbbd 100644 --- a/src/modules/scheduling/types.ts +++ b/src/modules/scheduling/types.ts @@ -10,6 +10,8 @@ export interface SchedulingVehicleInfo { region: string; province: string; customer: string | null; + department: string | null; + manager: string | null; customerAvgDaily: number; predictedYearEnd: number; daysLeft: number; diff --git a/src/server/routes/scheduling/algorithm.ts b/src/server/routes/scheduling/algorithm.ts index b23c460..9961556 100644 --- a/src/server/routes/scheduling/algorithm.ts +++ b/src/server/routes/scheduling/algorithm.ts @@ -54,6 +54,8 @@ export function toVehicleInfo(v: EnrichedVehicle): SchedulingVehicleInfo { region: v.region, province: v.province, customer: v.customer, + department: v.department, + manager: v.manager, customerAvgDaily: v.customerAvgDaily, predictedYearEnd: v.predictedYearEnd, daysLeft: v.daysLeft, diff --git a/src/server/routes/scheduling/suggestions.ts b/src/server/routes/scheduling/suggestions.ts index 662baa9..387daab 100644 --- a/src/server/routes/scheduling/suggestions.ts +++ b/src/server/routes/scheduling/suggestions.ts @@ -229,6 +229,8 @@ app.get('/', async (c) => { region, province, customer, + department: info?.department || null, + manager: info?.manager || null, customerAvgDaily, predictedYearEnd, daysLeft, diff --git a/src/server/routes/scheduling/types.ts b/src/server/routes/scheduling/types.ts index 73e6158..5cf7b43 100644 --- a/src/server/routes/scheduling/types.ts +++ b/src/server/routes/scheduling/types.ts @@ -10,6 +10,8 @@ export interface SchedulingVehicleInfo { region: string; province: string; customer: string | null; + department: string | null; + manager: string | null; customerAvgDaily: number; predictedYearEnd: number; daysLeft: number; @@ -81,6 +83,8 @@ export interface EnrichedVehicle { region: string; province: string; customer: string | null; + department: string | null; + manager: string | null; customerAvgDaily: number; predictedYearEnd: number; daysLeft: number;