diff --git a/src/App.tsx b/src/App.tsx index 996ef92..996cf6c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,10 +10,13 @@ import { ChevronRight, Info, Loader2, + Search, + Filter, + ArrowRightLeft, } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; -import type { SummaryData, TypeSummary, VehicleListItem } from './types'; -import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail } from './api'; +import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats } from './types'; +import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats } from './api'; import type { WeeklyDetailItem } from './api'; export default function App() { @@ -24,7 +27,12 @@ export default function App() { batch: string; model: string; location: string; - category?: 'Inventory' | 'Pending' | 'Delivered' | 'Returned' | 'Replaced'; + category?: 'Inventory' | 'Pending' | 'Delivered' | 'Returned' | 'Replaced' | 'Operating'; + vehicleType?: string; + manager?: string; + customer?: string; + isColdChain?: boolean; + isTrailer?: boolean; } | null>(null); // Data state @@ -37,16 +45,43 @@ export default function App() { const [lastUpdate, setLastUpdate] = useState(''); const [modalLoading, setModalLoading] = useState(false); + // Dept/Region/Customer data + const [deptData, setDeptData] = useState([]); + const [regionData, setRegionData] = useState([]); + const [customerData, setCustomerData] = useState([]); + + // Dept section state + const [deptViewMode, setDeptViewMode] = useState<'department' | 'manager'>('department'); + const [expandedDepts, setExpandedDepts] = useState>(new Set()); + const [expandedManagerDetails, setExpandedManagerDetails] = useState>(new Set()); + const [selectedManager, setSelectedManager] = useState('All'); + + // Region section state + const [expandedRegions, setExpandedRegions] = useState>(new Set()); + const [regionFilters, setRegionFilters] = useState({ region: '', city: '', customer: '' }); + const [isRegionFilterOpen, setIsRegionFilterOpen] = useState(false); + + // Customer section state + const [expandedCustomers, setExpandedCustomers] = useState>(new Set()); + const [customerFilters, setCustomerFilters] = useState({ customer: '', brand: '', department: '', manager: '', region: '' }); + const [isCustomerFilterOpen, setIsCustomerFilterOpen] = useState(false); + const loadData = useCallback(async () => { try { setLoading(true); setError(null); - const [s, byType] = await Promise.all([ + const [s, byType, dept, region, cust] = await Promise.all([ fetchSummary(), fetchByType(), + fetchDeptStats(), + fetchRegionStats(), + fetchCustomerStats(), ]); setSummary(s); setProcessedData(byType); + setDeptData(dept); + setRegionData(region); + setCustomerData(cust); setLastUpdate(new Date().toLocaleString('zh-CN')); } catch (e) { setError(e instanceof Error ? e.message : '数据加载失败'); @@ -85,10 +120,16 @@ export default function App() { // Normal vehicle list setModalWeeklyDetail([]); const params: Record = {}; + if (showPlateNumbers.vehicleType) params.vehicleType = showPlateNumbers.vehicleType; if (showPlateNumbers.batch !== 'All') params.batch = showPlateNumbers.batch; if (showPlateNumbers.model !== 'All') params.model = showPlateNumbers.model; if (showPlateNumbers.location !== 'All') params.location = showPlateNumbers.location; if (cat === 'Inventory') params.status = 'Inventory'; + if (cat === 'Operating') params.category = 'Operating'; + if (showPlateNumbers.manager) params.manager = showPlateNumbers.manager; + if (showPlateNumbers.customer) params.customer = showPlateNumbers.customer; + if (showPlateNumbers.isColdChain !== undefined) params.isColdChain = String(showPlateNumbers.isColdChain); + if (showPlateNumbers.isTrailer !== undefined) params.isTrailer = String(showPlateNumbers.isTrailer); fetchVehicleList(params) .then(setModalVehicles) .catch(() => setModalVehicles([])) @@ -119,7 +160,57 @@ export default function App() { setExpandedModels(newSet); }; + const toggleDept = (dept: string) => { + const newSet = new Set(expandedDepts); + if (newSet.has(dept)) newSet.delete(dept); + else newSet.add(dept); + setExpandedDepts(newSet); + }; + const toggleManagerDetails = (manager: string) => { + const newSet = new Set(expandedManagerDetails); + if (newSet.has(manager)) newSet.delete(manager); + else newSet.add(manager); + setExpandedManagerDetails(newSet); + }; + + const toggleRegion = (region: string) => { + const newSet = new Set(expandedRegions); + if (newSet.has(region)) newSet.delete(region); + else newSet.add(region); + setExpandedRegions(newSet); + }; + + const toggleCustomer = (customer: string) => { + const newSet = new Set(expandedCustomers); + if (newSet.has(customer)) newSet.delete(customer); + else newSet.add(customer); + setExpandedCustomers(newSet); + }; + + // Derived data for dept section + const allManagersList = deptData.flatMap((d) => d.managers.map((m) => m.manager)).filter((v, i, a) => a.indexOf(v) === i).sort(); + const managerStats = deptData + .flatMap((d) => d.managers) + .filter((m) => selectedManager === 'All' || m.manager === selectedManager) + .sort((a, b) => b.total - a.total); + + // Derived data for customer section + const filteredCustomerStats = customerData.filter((s) => { + const mc = !customerFilters.customer || s.customer.toLowerCase().includes(customerFilters.customer.toLowerCase()); + const mb = !customerFilters.brand || s.brand === customerFilters.brand; + const md = !customerFilters.department || s.department === customerFilters.department; + const mm = !customerFilters.manager || s.manager.toLowerCase().includes(customerFilters.manager.toLowerCase()); + const mr = !customerFilters.region || s.region === customerFilters.region; + return mc && mb && md && mm && mr; + }); + const uniqueBrands = Array.from(new Set(customerData.map((s) => s.brand).filter(Boolean))); + const uniqueDepts = Array.from(new Set(customerData.map((s) => s.department).filter(Boolean))); + const uniqueRegions = Array.from(new Set(customerData.map((s) => s.region))); + const uniqueCities = Array.from(new Set(customerData.map((s) => s.city).filter(Boolean))); + + // Derived data for region section + const filteredRegionData = regionData.filter((r) => !regionFilters.region || r.region === regionFilters.region); if (loading && !summary) { return ( @@ -346,17 +437,61 @@ export default function App() { 总计 - {processedData.reduce((s, t) => s + t.totalAssets, 0)} - {processedData.reduce((s, t) => s + t.totalInventory, 0)} + + + + + + {['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => { const val = processedData.reduce((s, t) => s + (t.inventoryRegions?.[reg] || 0), 0); - return {val || ''}; + return ( + + {val > 0 ? ( + + ) : ''} + + ); })} - {processedData.reduce((s, t) => s + t.pending, 0) || ''} - {processedData.reduce((s, t) => s + t.totalOperating, 0)} - {processedData.reduce((s, t) => s + t.weeklyDelivered, 0) || ''} - {processedData.reduce((s, t) => s + t.weeklyReturned, 0) || ''} - {processedData.reduce((s, t) => s + t.weeklyReplaced, 0) || ''} + + {processedData.reduce((s, t) => s + t.pending, 0) > 0 ? ( + + ) : ''} + + + + + + {processedData.reduce((s, t) => s + t.weeklyDelivered, 0) > 0 ? ( + + ) : ''} + + + {processedData.reduce((s, t) => s + t.weeklyReturned, 0) > 0 ? ( + + ) : ''} + + + {processedData.reduce((s, t) => s + t.weeklyReplaced, 0) > 0 ? ( + + ) : ''} + @@ -389,18 +524,58 @@ export default function App() { 小计 - {typeGroup.totalAssets} - {typeGroup.totalInventory} + + + + + + {['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => ( - - {(typeGroup.inventoryRegions?.[reg] || 0) > 0 ? typeGroup.inventoryRegions[reg] : ''} + + {(typeGroup.inventoryRegions?.[reg] || 0) > 0 ? ( + + ) : ''} ))} - {typeGroup.pending || ''} - {typeGroup.totalOperating} - {typeGroup.weeklyDelivered || ''} - {typeGroup.weeklyReturned || ''} - {typeGroup.weeklyReplaced || ''} + + {typeGroup.pending > 0 ? ( + + ) : ''} + + + + + + {typeGroup.weeklyDelivered > 0 ? ( + + ) : ''} + + + {typeGroup.weeklyReturned > 0 ? ( + + ) : ''} + + + {typeGroup.weeklyReplaced > 0 ? ( + + ) : ''} + )} @@ -470,8 +645,11 @@ export default function App() { '' )} - - {model.operating} + + {model.weeklyDelivered > 0 ? ( @@ -694,21 +872,23 @@ export default function App() {

- {showPlateNumbers.batch === 'All' ? '全量' : showPlateNumbers.batch} - 车牌明细 + {showPlateNumbers.vehicleType || (showPlateNumbers.model !== 'All' ? showPlateNumbers.model : '全量')} - 车牌明细

- {showPlateNumbers.model === 'All' ? '全量型号' : showPlateNumbers.model} |{' '} + {showPlateNumbers.vehicleType ? (showPlateNumbers.model === 'All' ? '全量型号' : showPlateNumbers.model) : (showPlateNumbers.batch === 'All' ? '' : showPlateNumbers.batch)} |{' '} {!showPlateNumbers.category ? '全部车辆' : showPlateNumbers.category === 'Inventory' ? (showPlateNumbers.location === 'All' ? '库存' : `${showPlateNumbers.location}库存`) - : showPlateNumbers.category === 'Pending' - ? '待交车' - : showPlateNumbers.category === 'Delivered' - ? '本周已交车' - : showPlateNumbers.category === 'Returned' - ? '已还车' - : '已替换'} + : showPlateNumbers.category === 'Operating' + ? '在运营' + : showPlateNumbers.category === 'Pending' + ? '待交车' + : showPlateNumbers.category === 'Delivered' + ? '本周已交车' + : showPlateNumbers.category === 'Returned' + ? '已还车' + : '已替换'}