diff --git a/src/App.tsx b/src/App.tsx index 037dde9..7452bbb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,8 +15,8 @@ import { ArrowRightLeft, } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; -import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats } from './types'; -import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats } from './api'; +import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats, RegionalInventoryStats } from './types'; +import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats, fetchInventoryStats } from './api'; import type { WeeklyDetailItem } from './api'; export default function App() { @@ -35,6 +35,7 @@ export default function App() { isTrailer?: boolean; type?: string; source?: string; + title?: string; } | null>(null); // Data state @@ -68,22 +69,43 @@ export default function App() { const [customerFilters, setCustomerFilters] = useState({ customer: '', brand: '', department: '', manager: '', region: '' }); const [isCustomerFilterOpen, setIsCustomerFilterOpen] = useState(false); + // Inventory statistics section state + const [inventoryData, setInventoryData] = useState([]); + const [inventoryTab, setInventoryTab] = useState<'region' | 'model'>('region'); + const [expandedInventoryRegions, setExpandedInventoryRegions] = useState>(new Set()); + const [expandedInventoryTypes, setExpandedInventoryTypes] = useState>(new Set(['4.5T普货'])); + const [inventoryFilters, setInventoryFilters] = useState({ region: '', city: '', brand: '', batch: '', model: '' }); + const [isInventoryFilterOpen, setIsInventoryFilterOpen] = useState(false); + + // Modal filter state + const [modalFilters, setModalFilters] = useState({ plateNumber: '', model: '', brand: '', location: '' }); + const [isModalFilterExpanded, setIsModalFilterExpanded] = useState(false); + + // Reset modal filters when modal opens + useEffect(() => { + if (showPlateNumbers) { + setModalFilters({ plateNumber: '', model: '', brand: '', location: '' }); + } + }, [showPlateNumbers]); + const loadData = useCallback(async () => { try { setLoading(true); setError(null); - const [s, byType, dept, region, cust] = await Promise.all([ + const [s, byType, dept, region, cust, inv] = await Promise.all([ fetchSummary(), fetchByType(), fetchDeptStats(), fetchRegionStats(), fetchCustomerStats(), + fetchInventoryStats(), ]); setSummary(s); setProcessedData(byType); setDeptData(dept); setRegionData(region); setCustomerData(cust); + setInventoryData(inv); setLastUpdate(new Date().toLocaleString('zh-CN')); } catch (e) { setError(e instanceof Error ? e.message : '数据加载失败'); @@ -212,6 +234,49 @@ export default function App() { setExpandedCustomers(newSet); }; + const toggleInventoryRegion = (region: string) => { + const newSet = new Set(expandedInventoryRegions); + if (newSet.has(region)) newSet.delete(region); + else newSet.add(region); + setExpandedInventoryRegions(newSet); + }; + + const toggleInventoryType = (type: string) => { + const newSet = new Set(expandedInventoryTypes); + if (newSet.has(type)) newSet.delete(type); + else newSet.add(type); + setExpandedInventoryTypes(newSet); + }; + + // Derived data for inventory section + const filteredInventoryStats = inventoryData.filter((s) => { + const mr = !inventoryFilters.region || s.region === inventoryFilters.region; + const mc = !inventoryFilters.city || s.city === inventoryFilters.city; + const mb = !inventoryFilters.brand || s.brand === inventoryFilters.brand; + const mbt = !inventoryFilters.batch || s.batch === inventoryFilters.batch; + const mm = !inventoryFilters.model || s.model.toLowerCase().includes(inventoryFilters.model.toLowerCase()); + return mr && mc && mb && mbt && mm; + }); + + const uniqueInventoryBrands = Array.from(new Set(inventoryData.map((s) => s.brand).filter(Boolean))); + const uniqueInventoryRegions = Array.from(new Set(inventoryData.map((s) => s.region))); + const uniqueInventoryCities = Array.from(new Set(inventoryData.map((s) => s.city).filter(Boolean))); + const uniqueInventoryBatches = Array.from(new Set(inventoryData.map((s) => s.batch).filter(Boolean))); + + const inventoryByRegion: Record> = {}; + for (const s of filteredInventoryStats) { + if (!inventoryByRegion[s.region]) inventoryByRegion[s.region] = {}; + if (!inventoryByRegion[s.region][s.city]) inventoryByRegion[s.region][s.city] = []; + inventoryByRegion[s.region][s.city].push(s); + } + + const inventoryByModel: Record> = {}; + for (const s of filteredInventoryStats) { + if (!inventoryByModel[s.type]) inventoryByModel[s.type] = {}; + if (!inventoryByModel[s.type][s.model]) inventoryByModel[s.type][s.model] = []; + inventoryByModel[s.type][s.model].push(s); + } + // 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 @@ -236,6 +301,20 @@ export default function App() { // Derived data for region section const filteredRegionData = regionData.filter((r) => !regionFilters.region || r.region === regionFilters.region); + // Filtered modal vehicles based on modal filters + const filteredModalVehicles = modalVehicles.filter((v) => { + const mp = !modalFilters.plateNumber || v.plateNumber.toLowerCase().includes(modalFilters.plateNumber.toLowerCase()); + const mm = !modalFilters.model || v.model.toLowerCase().includes(modalFilters.model.toLowerCase()); + const mb = !modalFilters.brand || (v.brandLabel || '').toLowerCase().includes(modalFilters.brand.toLowerCase()); + const ml = !modalFilters.location || v.location.toLowerCase().includes(modalFilters.location.toLowerCase()); + return mp && mm && mb && ml; + }); + + const filteredModalWeeklyDetail = modalWeeklyDetail.filter((v) => { + const mp = !modalFilters.plateNumber || v.plate_number.toLowerCase().includes(modalFilters.plateNumber.toLowerCase()); + return mp; + }); + if (loading && !summary) { return (
@@ -859,6 +938,415 @@ export default function App() {
+ {/* Inventory Statistics */} +
+
+
+
+
+

库存统计

+ 实时更新库存数据 +
+
+ +
+
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Inventory', source: 'asset', title: '库存总数' })}> + 总库存数据 + + {filteredInventoryStats.reduce((acc, s) => acc + s.quantity, 0)} + + +
+ +
+ + + + {isInventoryFilterOpen && ( + <> +
setIsInventoryFilterOpen(false)} /> + +
+

库存统计 - 数据筛选

+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + setInventoryFilters({...inventoryFilters, model: e.target.value})} + className="w-full text-xs bg-white border border-slate-200 rounded-lg pl-7 pr-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm" + /> +
+
+
+
+ + )} + +
+
+
+ +
+
+ + +
+
+ + {/* Active Filters Bar */} + {(inventoryFilters.region || inventoryFilters.city || inventoryFilters.brand || inventoryFilters.batch || inventoryFilters.model) && ( +
+ 当前筛选: + {inventoryFilters.region && ( + + 区域: {inventoryFilters.region} + + + )} + {inventoryFilters.city && ( + + 城市: {inventoryFilters.city} + + + )} + {inventoryFilters.brand && ( + + 品牌: {inventoryFilters.brand} + + + )} + {inventoryFilters.batch && ( + + 批次: {inventoryFilters.batch} + + + )} + {inventoryFilters.model && ( + + 车型: {inventoryFilters.model} + + + )} + +
+ )} + + {/* Desktop View Table */} +
+ + + + + + + + + + + {inventoryTab === 'region' ? ( + Object.entries(inventoryByRegion).map(([region, cities]) => ( + + toggleInventoryRegion(region)} + > + + + + {expandedInventoryRegions.has(region) && Object.entries(cities).map(([city, stats]) => ( + + {stats.map((stat, idx) => ( + + + + + + + ))} + + ))} + + + )) + ) : ( + Object.entries(inventoryByModel).map(([type, models]) => ( + + toggleInventoryType(type)} + > + + + + {expandedInventoryTypes.has(type) && Object.entries(models).map(([model, stats]) => ( + + {stats.map((stat, idx) => ( + + + + + + + ))} + + ))} + + + )) + )} + +
{inventoryTab === 'region' ? '区域 / 城市' : '车型分类 / 型号'}{inventoryTab === 'region' ? '品牌' : '品牌'}{inventoryTab === 'region' ? '车型' : '所在区域/城市'}库存数量
+
+ {expandedInventoryRegions.has(region) ? : } + {region} + { + e.stopPropagation(); + setShowPlateNumbers({ batch: 'All', model: 'All', location: region, category: 'Inventory', source: 'asset', title: `库存统计 - ${region}` }); + }}> + (共 {Object.values(cities).flat().reduce((acc, s) => acc + s.quantity, 0)} 台) + +
+
+ {idx === 0 ? {city} : ''} + {stat.brand}{stat.model} + +
+
+ {expandedInventoryTypes.has(type) ? : } + {type} + { + e.stopPropagation(); + setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', type: type, category: 'Inventory', source: 'asset', title: `库存统计 - ${type}` }); + }}> + (共 {Object.values(models).flat().reduce((acc, s) => acc + s.quantity, 0)} 台) + +
+
+ {idx === 0 ? {model} : ''} + {stat.brand}{stat.region} / {stat.city} + +
+
+ + {/* Mobile View */} +
+ {inventoryTab === 'region' ? ( + Object.entries(inventoryByRegion).map(([region, cities]) => ( +
+
toggleInventoryRegion(region)} + > +
+ {expandedInventoryRegions.has(region) ? : } + {region} +
+ + {Object.values(cities).flat().reduce((acc, s) => acc + s.quantity, 0)} 台 + +
+ + {expandedInventoryRegions.has(region) && ( +
+ {Object.entries(cities).map(([city, stats]) => ( +
+
{city}
+
+ {stats.map((stat, idx) => ( +
+
+ {stat.brand} + {stat.model} +
+ +
+ ))} +
+
+ ))} +
+ )} +
+ )) + ) : ( + Object.entries(inventoryByModel).map(([type, models]) => ( +
+
toggleInventoryType(type)} + > +
+ {expandedInventoryTypes.has(type) ? : } + {type} +
+ + {Object.values(models).flat().reduce((acc, s) => acc + s.quantity, 0)} 台 + +
+ + {expandedInventoryTypes.has(type) && ( +
+ {Object.entries(models).map(([model, stats]) => ( +
+
{model}
+
+ {stats.map((stat, idx) => ( +
+
+ {stat.brand} + {stat.region} / {stat.city} +
+ +
+ ))} +
+
+ ))} +
+ )} +
+ )) + )} +
+
{/* Department Operations Statistics */}
@@ -2011,9 +2499,11 @@ export default function App() {

- {showPlateNumbers.manager ? `${showPlateNumbers.manager} 的${showPlateNumbers.type || ''}车辆` : - showPlateNumbers.customer ? `${showPlateNumbers.customer} 的${showPlateNumbers.type || ''}车辆` : - showPlateNumbers.batch === 'All' ? '全量批次' : `${showPlateNumbers.batch} 批次`} - 运营明细 + {showPlateNumbers.title || ( + (showPlateNumbers.manager ? `${showPlateNumbers.manager} 的${showPlateNumbers.type || ''}车辆` : + showPlateNumbers.customer ? `${showPlateNumbers.customer} 的${showPlateNumbers.type || ''}车辆` : + showPlateNumbers.batch === 'All' ? '全量批次' : `${showPlateNumbers.batch} 批次`) + ' - 运营明细' + )}

{showPlateNumbers.model === 'All' ? '全量型号' : showPlateNumbers.model} | @@ -2030,6 +2520,114 @@ export default function App() {

+ {/* Modal Filters */} +
+
setIsModalFilterExpanded(!isModalFilterExpanded)} + > +
+ + 筛选条件 +
+
+ {/* Quick Search always visible when collapsed */} + {!isModalFilterExpanded && ( +
e.stopPropagation()}> + + setModalFilters({...modalFilters, plateNumber: e.target.value})} + placeholder="快速搜索车牌..." + className="w-full text-[11px] pl-7 pr-2 py-1 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm" + /> +
+ )} + + + +
+
+ + + {isModalFilterExpanded && ( + +
+
+ +
+ + setModalFilters({...modalFilters, plateNumber: e.target.value})} + placeholder="搜索车牌..." + className="w-full text-[11px] pl-7 pr-2 py-1.5 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm" + /> +
+
+
+ +
+ + setModalFilters({...modalFilters, model: e.target.value})} + placeholder="搜索车型..." + className="w-full text-[11px] pl-7 pr-2 py-1.5 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm" + /> +
+
+
+ +
+ + setModalFilters({...modalFilters, brand: e.target.value})} + placeholder="搜索品牌..." + className="w-full text-[11px] pl-7 pr-2 py-1.5 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm" + /> +
+
+
+ +
+ + setModalFilters({...modalFilters, location: e.target.value})} + placeholder="搜索所在地..." + className="w-full text-[11px] pl-7 pr-2 py-1.5 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm" + /> +
+
+
+
+ +
+
+ )} +
+
+
{modalLoading ? (
@@ -2046,7 +2644,7 @@ export default function App() { - {modalWeeklyDetail.map((v, i) => ( + {filteredModalWeeklyDetail.map((v, i) => ( {v.plate_number} {v.customer_name || '—'} @@ -2092,7 +2690,7 @@ export default function App() { - {modalVehicles.map((v, idx) => ( + {filteredModalVehicles.map((v, idx) => ( {showPlateNumbers.source === 'customer' ? ( <> @@ -2131,7 +2729,7 @@ export default function App() { )} ))} - {modalVehicles.length === 0 && ( + {filteredModalVehicles.length === 0 && ( 暂无符合条件的车辆数据 @@ -2146,7 +2744,7 @@ export default function App() {
- 共计 {modalWeeklyDetail.length > 0 ? modalWeeklyDetail.length : modalVehicles.length} 台车辆 + 共计 {filteredModalWeeklyDetail.length > 0 ? filteredModalWeeklyDetail.length : filteredModalVehicles.length} 台车辆