Files
ln-bi/src/App.tsx
kkfluous e38484d384
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix: 客户展开详情改为业务经理卡片,移动端资产汇总适配原型
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:38:45 +08:00

3073 lines
197 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useCallback } from 'react';
import {
Truck,
Warehouse,
Activity,
PlusCircle,
MinusCircle,
History,
ChevronDown,
ChevronRight,
Info,
Loader2,
Search,
Filter,
ArrowRightLeft,
Users,
MapPin,
Building2,
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
LabelList,
} from 'recharts';
import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats, RegionalInventoryStats } from './types';
import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats, fetchInventoryStats, fetchRegionChart } from './api';
import type { WeeklyDetailItem } from './api';
// --- Constants ---
const TABS = [
{ id: 'overview', label: '总览' },
{ id: 'department', label: '按部门' },
{ id: 'region', label: '按区域' },
{ id: 'customer', label: '按客户' },
];
export default function App() {
const [activeTab, setActiveTab] = useState<'overview' | 'department' | 'region' | 'customer'>('overview');
const [theme, setTheme] = useState<'soft' | 'minimal' | 'vibrant'>('soft');
const [expandedModels, setExpandedModels] = useState<Set<string>>(new Set());
const [expandedAssetTypes, setExpandedAssetTypes] = useState<Set<string>>(new Set());
const [showPlateNumbers, setShowPlateNumbers] = useState<{
batch: string;
model: string;
location: string;
category?: 'Inventory' | 'Pending' | 'Delivered' | 'Returned' | 'Replaced' | 'Operating';
vehicleType?: string;
manager?: string;
customer?: string;
isColdChain?: boolean;
isTrailer?: boolean;
type?: string;
source?: string;
title?: string;
} | null>(null);
// Data state
const [summary, setSummary] = useState<SummaryData | null>(null);
const [processedData, setProcessedData] = useState<TypeSummary[]>([]);
const [modalVehicles, setModalVehicles] = useState<VehicleListItem[]>([]);
const [modalWeeklyDetail, setModalWeeklyDetail] = useState<WeeklyDetailItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdate, setLastUpdate] = useState<string>('');
const [modalLoading, setModalLoading] = useState(false);
// Dept/Region/Customer data
const [deptData, setDeptData] = useState<DeptGroup[]>([]);
const [regionData, setRegionData] = useState<RegionGroup[]>([]);
const [customerData, setCustomerData] = useState<CustomerStats[]>([]);
// Dept section state
const [deptViewMode, setDeptViewMode] = useState<'department' | 'manager'>('department');
const [expandedDepts, setExpandedDepts] = useState<Set<string>>(new Set());
const [expandedManagerDetails, setExpandedManagerDetails] = useState<Set<string>>(new Set());
const [selectedManager, setSelectedManager] = useState<string>('All');
// Region section state
const [expandedRegions, setExpandedRegions] = useState<Set<string>>(new Set());
const [expandedRegionCities, setExpandedRegionCities] = useState<Set<string>>(new Set());
const [regionFilters, setRegionFilters] = useState({ region: '', city: '', customer: '' });
const [isRegionFilterOpen, setIsRegionFilterOpen] = useState(false);
// Customer section state
const [expandedCustomers, setExpandedCustomers] = useState<Set<string>>(new Set());
const [customerFilters, setCustomerFilters] = useState({ customer: '', brand: '', department: '', manager: '', region: '' });
const [isCustomerFilterOpen, setIsCustomerFilterOpen] = useState(false);
// Inventory statistics section state
const [inventoryData, setInventoryData] = useState<RegionalInventoryStats[]>([]);
const [inventoryTab, setInventoryTab] = useState<'region' | 'model'>('region');
const [expandedInventoryRegions, setExpandedInventoryRegions] = useState<Set<string>>(new Set());
const [expandedInventoryTypes, setExpandedInventoryTypes] = useState<Set<string>>(new Set(['4.5T普货']));
const [inventoryFilters, setInventoryFilters] = useState({ region: '', city: '', brand: '', batch: '', model: '' });
const [isInventoryFilterOpen, setIsInventoryFilterOpen] = useState(false);
// Chart view states
const [customerChartView, setCustomerChartView] = useState<'region' | 'city'>('region');
const [regionChartView, setRegionChartView] = useState<'region' | 'city'>('region');
const [regionChartData, setRegionChartData] = useState<{ name: string; value: number }[]>([]);
// 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, 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 : '数据加载失败');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadData();
const interval = setInterval(loadData, 60 * 1000);
return () => clearInterval(interval);
}, [loadData]);
// Fetch region chart data when view changes
useEffect(() => {
fetchRegionChart(regionChartView, regionChartView === 'city' ? 5 : 8).then(setRegionChartData).catch(() => setRegionChartData([]));
}, [regionChartView]);
// Load modal vehicles
useEffect(() => {
if (!showPlateNumbers) {
setModalVehicles([]);
setModalWeeklyDetail([]);
return;
}
setModalLoading(true);
const cat = showPlateNumbers.category;
// Weekly categories use the dedicated weekly-detail endpoint
const weeklyTypes: Record<string, string> = { Delivered: 'delivered', Returned: 'returned', Replaced: 'replaced', Pending: 'pending' };
if (cat && weeklyTypes[cat]) {
setModalVehicles([]);
fetchWeeklyDetail(weeklyTypes[cat])
.then(setModalWeeklyDetail)
.catch(() => setModalWeeklyDetail([]))
.finally(() => setModalLoading(false));
return;
}
// Normal vehicle list
setModalWeeklyDetail([]);
const params: Record<string, string> = {};
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.type) {
if (showPlateNumbers.isColdChain !== undefined) params.isColdChain = String(showPlateNumbers.isColdChain);
if (showPlateNumbers.isTrailer !== undefined) params.isTrailer = String(showPlateNumbers.isTrailer);
}
// Map type field to backend vehicleType
if (showPlateNumbers.type) {
const t = showPlateNumbers.type;
if (t === '4.5T') {
if (showPlateNumbers.isColdChain === true) params.vehicleType = '4.5T冷链';
else if (showPlateNumbers.isColdChain === false) params.vehicleType = '4.5T普货';
} else if (t === '4.5T普货' || t === '4.5T冷链' || t === '18T' || t === '49T' || t === '挂车' || t === '其他') {
params.vehicleType = t;
} else if (t === '其他车型') {
if (showPlateNumbers.isTrailer === true) params.isTrailer = 'true';
else if (showPlateNumbers.isTrailer === false) params.vehicleType = '其他';
}
}
fetchVehicleList(params)
.then(setModalVehicles)
.catch(() => setModalVehicles([]))
.finally(() => setModalLoading(false));
}, [showPlateNumbers]);
const allTypesExpanded = processedData.length > 0 && processedData.every((t) => expandedAssetTypes.has(t.type));
const toggleAllAssetTypes = () => {
if (allTypesExpanded) {
setExpandedAssetTypes(new Set());
} else {
setExpandedAssetTypes(new Set(processedData.map((t) => t.type)));
}
};
const toggleAssetType = (type: string) => {
const newSet = new Set(expandedAssetTypes);
if (newSet.has(type)) newSet.delete(type);
else newSet.add(type);
setExpandedAssetTypes(newSet);
};
const toggleModel = (model: string) => {
const newSet = new Set(expandedModels);
if (newSet.has(model)) newSet.delete(model);
else newSet.add(model);
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 toggleRegionCity = (key: string) => {
const newSet = new Set(expandedRegionCities);
if (newSet.has(key)) newSet.delete(key);
else newSet.add(key);
setExpandedRegionCities(newSet);
};
const toggleCustomer = (customer: string) => {
const newSet = new Set(expandedCustomers);
if (newSet.has(customer)) newSet.delete(customer);
else newSet.add(customer);
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<string, Record<string, RegionalInventoryStats[]>> = {};
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 INVENTORY_TYPE_ORDER = ['4.5T普货', '4.5T冷链', '18T', '49T', '挂车', '其他'];
const inventoryByModelRaw: Record<string, Record<string, RegionalInventoryStats[]>> = {};
for (const s of filteredInventoryStats) {
if (!inventoryByModelRaw[s.type]) inventoryByModelRaw[s.type] = {};
if (!inventoryByModelRaw[s.type][s.model]) inventoryByModelRaw[s.type][s.model] = [];
inventoryByModelRaw[s.type][s.model].push(s);
}
const inventoryByModel: Record<string, Record<string, RegionalInventoryStats[]>> = {};
for (const t of INVENTORY_TYPE_ORDER) {
if (inventoryByModelRaw[t]) inventoryByModel[t] = inventoryByModelRaw[t];
}
for (const t of Object.keys(inventoryByModelRaw)) {
if (!inventoryByModel[t]) inventoryByModel[t] = inventoryByModelRaw[t];
}
// 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)));
const uniqueCustomerNames = Array.from(new Set(customerData.map((s) => s.customer).filter(Boolean)));
const uniqueCustomerManagers = Array.from(new Set(customerData.map((s) => s.manager).filter(Boolean)));
const uniqueInventoryModels = Array.from(new Set(inventoryData.map((s) => s.model).filter(Boolean)));
const uniqueModalPlates = Array.from(new Set(modalVehicles.map(v => v.plateNumber || v.vin).filter(Boolean)));
const uniqueModalModels = Array.from(new Set(modalVehicles.map(v => v.model).filter(Boolean)));
const uniqueModalBrands = Array.from(new Set(modalVehicles.map(v => v.brandLabel).filter((x): x is string => Boolean(x))));
const uniqueModalLocations = Array.from(new Set(modalVehicles.map(v => v.location).filter(Boolean)));
// 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()) || v.vin.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 (
<div className="min-h-screen bg-[#F8F9FB] flex items-center justify-center">
<div className="flex flex-col items-center gap-3">
<Loader2 className="animate-spin text-blue-500" size={32} />
<span className="text-sm text-gray-500">...</span>
</div>
</div>
);
}
if (error && !summary) {
return (
<div className="min-h-screen bg-[#F8F9FB] flex items-center justify-center">
<div className="flex flex-col items-center gap-3 text-center">
<div className="text-red-500 text-lg font-bold"></div>
<div className="text-sm text-gray-500">{error}</div>
<button onClick={loadData} className="mt-2 px-4 py-2 bg-blue-500 text-white rounded text-sm hover:bg-blue-600">
</button>
</div>
</div>
);
}
const SUMMARY = summary!;
const watermarkText = `羚牛氢能-${new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')}`;
return (
<div className="min-h-screen bg-[#F8F9FB] text-gray-800 font-sans p-6 pb-20 md:pb-6 relative">
{/* Watermark */}
<div className="fixed inset-0 pointer-events-none z-[9999] overflow-hidden" style={{ opacity: 0.06 }}>
<div className="absolute inset-0" style={{
backgroundImage: `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' width='320' height='200'><text x='50%' y='50%' text-anchor='middle' dominant-baseline='middle' font-size='14' font-family='sans-serif' fill='%23000' transform='rotate(-25 160 100)'>${watermarkText}</text></svg>`)}")`,
backgroundRepeat: 'repeat',
}} />
</div>
{/* Compact Header Bar */}
<div className="sticky top-0 z-40 -mx-6 -mt-6 mb-4 bg-white/95 backdrop-blur-sm border-b border-gray-100/80">
{/* Title row */}
<div className="relative flex items-center justify-center px-4 pt-3 pb-1">
<h1 className="text-base font-semibold text-gray-800 tracking-wide"></h1>
{/* Right: status + theme */}
<div className="absolute right-4 top-1/2 -translate-y-1/2 flex items-center gap-2">
<div className="hidden sm:flex items-center gap-1 text-[10px] text-gray-400">
<span className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse inline-block" />
<span>{lastUpdate}</span>
</div>
{loading && (
<div className="flex items-center gap-1 text-[10px] text-gray-400">
<Loader2 className="animate-spin" size={10} />
</div>
)}
<div className="hidden sm:flex bg-gray-100 p-0.5 rounded-lg text-[10px]">
{(['soft','minimal','vibrant'] as const).map((t) => (
<button
key={t}
onClick={() => setTheme(t)}
className={`px-2 py-0.5 rounded-md transition-all ${theme === t ? 'bg-white text-blue-600 shadow-sm font-semibold' : 'text-gray-400 hover:text-gray-600'}`}
>
{t === 'soft' ? '柔和' : t === 'minimal' ? '简约' : '经典'}
</button>
))}
</div>
</div>
</div>
{/* Tab row */}
<div className="flex items-center justify-center gap-1 px-4 pb-0 overflow-x-auto no-scrollbar">
{TABS.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as typeof activeTab)}
className={`relative px-4 py-2 text-[13px] font-normal transition-all whitespace-nowrap ${
activeTab === tab.id
? 'text-blue-600 font-medium'
: 'text-gray-400 hover:text-gray-500'
}`}
>
{tab.label}
{activeTab === tab.id && (
<motion.div
layoutId="activeTab"
className="absolute bottom-0 left-2 right-2 h-[1.5px] bg-blue-600 rounded-full"
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
/>
)}
</button>
))}
</div>
{/* Status row */}
<div className="flex items-center justify-center gap-4 py-1.5 text-[10px] text-gray-400">
<div className="flex items-center gap-1">
<span className="w-1 h-1 rounded-full bg-blue-400 inline-block" />
: {lastUpdate}
</div>
<div className="flex items-center gap-1">
<span className="w-1 h-1 rounded-full bg-green-400 animate-pulse inline-block" />
</div>
</div>
</div>
{activeTab === 'overview' && (
<>
{/* Header Summary - Ultra Compact */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-2 mb-6">
{/* Total Assets */}
<div className="bg-white p-2 rounded-sm border border-gray-100 shadow-sm flex items-center gap-2">
<div className="w-7 h-7 rounded bg-gray-50 flex items-center justify-center text-gray-400">
<Truck size={14} />
</div>
<div>
<div className="text-[9px] text-gray-400 font-medium uppercase leading-none mb-0.5"></div>
<div className="text-base font-bold text-gray-800 leading-none">{SUMMARY.totalAssets.toLocaleString()}</div>
</div>
</div>
{/* Operating */}
<div className="bg-white p-2 rounded-sm border border-gray-100 shadow-sm flex items-center gap-2">
<div className="w-7 h-7 rounded bg-blue-50 flex items-center justify-center text-blue-500">
<Activity size={14} />
</div>
<div>
<div className="text-[9px] text-gray-400 font-medium uppercase leading-none mb-0.5"></div>
<div className="flex items-baseline gap-1">
<span className="text-base font-bold text-blue-600 leading-none">{SUMMARY.operating.total}</span>
<span className="text-[8px] text-gray-400 leading-none">{SUMMARY.operating.self} {SUMMARY.operating.leased}{SUMMARY.operating.hanging > 0 ? `${SUMMARY.operating.hanging}` : ''}</span>
</div>
</div>
</div>
{/* Inventory */}
<div className="bg-white p-2 rounded-sm border border-gray-100 shadow-sm flex items-center gap-2">
<div className="w-7 h-7 rounded bg-gray-50 flex items-center justify-center text-gray-500">
<Warehouse size={14} />
</div>
<div>
<div className="text-[9px] text-gray-400 font-medium uppercase leading-none mb-0.5"></div>
<div className="flex items-baseline gap-1">
<span className="text-base font-bold text-gray-800 leading-none">{SUMMARY.inventory.total}</span>
<span className="text-[8px] text-gray-400 leading-none">{SUMMARY.inventory.inStock} {SUMMARY.inventory.abnormal}</span>
</div>
</div>
</div>
{/* Pending */}
<div className="bg-white p-2 rounded-sm border border-gray-100 shadow-sm flex items-center gap-2 cursor-pointer hover:bg-blue-50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Pending' })}>
<div className="w-7 h-7 rounded bg-blue-50 flex items-center justify-center text-blue-500">
<PlusCircle size={14} />
</div>
<div>
<div className="text-[9px] text-blue-500 font-bold uppercase leading-none mb-0.5"></div>
<div className="text-base font-bold text-blue-600 leading-none">{SUMMARY.pendingDelivery}</div>
</div>
</div>
{/* Dynamics */}
<div className="bg-white p-2 rounded-sm border border-gray-100 shadow-sm col-span-2">
<div className="flex items-center justify-between mb-1.5">
<div className="text-[9px] text-gray-400 font-bold uppercase tracking-tight"></div>
<div className="text-[7px] text-gray-300 font-normal italic">-</div>
</div>
<div className="flex justify-between items-center gap-1">
<div className="flex-1 flex flex-col items-center cursor-pointer hover:bg-green-50 py-1 rounded transition-all group">
<span className="text-xs font-bold text-gray-800 group-hover:text-green-600">{SUMMARY.weeklyNew}</span>
<span className="text-[8px] text-green-500/80 font-bold mt-0.5"></span>
</div>
<div className="w-[1px] h-3 bg-gray-100"></div>
<div className="flex-1 flex flex-col items-center cursor-pointer hover:bg-blue-50 py-1 rounded transition-all group"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Delivered' })}>
<span className="text-xs font-bold text-gray-800 group-hover:text-blue-600">{SUMMARY.weeklyDelivered}</span>
<span className="text-[8px] text-blue-500/80 font-bold mt-0.5"></span>
</div>
<div className="w-[1px] h-3 bg-gray-100"></div>
<div className="flex-1 flex flex-col items-center cursor-pointer hover:bg-orange-50 py-1 rounded transition-all group"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Returned' })}>
<span className="text-xs font-bold text-gray-800 group-hover:text-orange-600">{SUMMARY.weeklyReturned}</span>
<span className="text-[8px] text-orange-500/80 font-bold mt-0.5"></span>
</div>
<div className="w-[1px] h-3 bg-gray-100"></div>
<div className="flex-1 flex flex-col items-center cursor-pointer hover:bg-purple-50 py-1 rounded transition-all group"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Replaced' })}>
<span className="text-xs font-bold text-gray-800 group-hover:text-purple-600">{SUMMARY.weeklyReplaced}</span>
<span className="text-[8px] text-purple-500/80 font-bold mt-0.5"></span>
</div>
</div>
</div>
</div>
</>
)}
{/* Main Content Area */}
<div className="flex flex-col gap-6">
{activeTab === 'overview' && (
<>
{/* Asset Summary Table with Dimension Switch */}
<div className="bg-white rounded-sm border border-gray-100 shadow-sm overflow-hidden mb-6">
<div className="p-4 border-b border-gray-50 bg-gray-50/50 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
<div className="flex flex-wrap items-center gap-4 sm:gap-6">
<h2 className="text-sm font-bold text-gray-700"></h2>
<div className="hidden md:flex items-center gap-1 text-[10px] text-blue-500 bg-blue-50 px-2 py-0.5 rounded">
<Info size={10} />
</div>
</div>
</div>
{/* Desktop View Table */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left border-collapse table-fixed min-w-[1200px]">
<thead>
<tr className="bg-gray-50 text-[11px] text-gray-500 uppercase tracking-wider border-b border-gray-100">
<th className="p-3 font-semibold border-r border-gray-100 w-24">
<button onClick={toggleAllAssetTypes} className="flex items-center gap-1 hover:text-blue-600 transition-colors">
{allTypesExpanded ? <MinusCircle size={12} /> : <PlusCircle size={12} />}
<span></span>
</button>
</th>
<th className="p-3 font-semibold border-r border-gray-100 w-48"></th>
<th className="p-3 font-semibold border-r border-gray-100 text-center bg-blue-50/30 w-24"></th>
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24"></th>
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24">-</th>
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24">-广</th>
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24">-</th>
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24">-</th>
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24">-</th>
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24"></th>
<th className="p-3 font-semibold border-r border-gray-100 text-center bg-green-50/30 w-24"></th>
<th className="p-3 font-semibold border-r border-gray-100 text-center bg-blue-50/20 w-24"></th>
<th className="p-3 font-semibold border-r border-gray-100 text-center bg-orange-50/20 w-24"></th>
<th className="p-3 font-semibold text-center bg-purple-50/20 w-24"></th>
</tr>
<tr className="bg-yellow-50/50 text-xs font-bold border-b border-gray-200">
<td className="p-3 border-r border-gray-100 text-gray-700"></td>
<td className="p-3 border-r border-gray-100"></td>
<td className="p-3 text-center border-r border-gray-100">
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All' })} className="text-gray-800 hover:text-blue-600 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.totalAssets, 0)}
</button>
</td>
<td className="p-3 text-center border-r border-gray-100">
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Inventory' })} className="text-blue-700 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.totalInventory, 0)}
</button>
</td>
{['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => {
const val = processedData.reduce((s, t) => s + (t.inventoryRegions?.[reg] || 0), 0);
return (
<td key={reg} className="p-3 text-center border-r border-gray-100">
{val > 0 ? (
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: reg, category: 'Inventory' })} className="text-blue-700 hover:underline font-bold">
{val}
</button>
) : ''}
</td>
);
})}
<td className="p-3 text-center border-r border-gray-100">
{processedData.reduce((s, t) => s + t.pending, 0) > 0 ? (
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Pending' })} className="text-gray-700 hover:text-blue-600 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.pending, 0)}
</button>
) : ''}
</td>
<td className="p-3 text-center border-r border-gray-100 bg-green-50/10">
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Operating' })} className="text-green-700 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.totalOperating, 0)}
</button>
</td>
<td className="p-3 text-center border-r border-gray-100 bg-blue-50/5">
{processedData.reduce((s, t) => s + t.weeklyDelivered, 0) > 0 ? (
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Delivered' })} className="text-blue-700 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.weeklyDelivered, 0)}
</button>
) : ''}
</td>
<td className="p-3 text-center border-r border-gray-100 bg-orange-50/5">
{processedData.reduce((s, t) => s + t.weeklyReturned, 0) > 0 ? (
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Returned' })} className="text-orange-700 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.weeklyReturned, 0)}
</button>
) : ''}
</td>
<td className="p-3 text-center bg-purple-50/5">
{processedData.reduce((s, t) => s + t.weeklyReplaced, 0) > 0 ? (
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Replaced' })} className="text-purple-700 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.weeklyReplaced, 0)}
</button>
) : ''}
</td>
</tr>
</thead>
<tbody className="text-xs">
{processedData.map((typeGroup) => (
<React.Fragment key={typeGroup.type}>
{/* Category Header Row */}
<tr
className={`border-b border-gray-100 cursor-pointer transition-all ${
theme === 'vibrant'
? 'bg-blue-600 text-white hover:bg-blue-700'
: theme === 'minimal'
? 'bg-white border-l-4 border-blue-500 hover:bg-gray-50'
: 'bg-blue-50/50 hover:bg-blue-50 transition-colors'
}`}
onClick={() => toggleAssetType(typeGroup.type)}
>
<>
<td className={`p-3 font-bold border-r border-gray-100 ${theme === 'vibrant' ? 'text-white' : 'text-blue-700'}`}>
<div className="flex items-center gap-2">
{expandedAssetTypes.has(typeGroup.type) ? (
<ChevronDown size={16} className={theme === 'vibrant' ? 'text-white' : 'text-blue-500'} />
) : (
<ChevronRight size={16} className={theme === 'vibrant' ? 'text-white/70' : 'text-gray-400'} />
)}
<span>{typeGroup.type}</span>
</div>
</td>
<td className={`p-3 border-r border-gray-100 text-[11px] ${theme === 'vibrant' ? 'text-white/70' : 'text-gray-400'} italic`}></td>
<td className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white' : ''}`}>
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white' : 'text-gray-700 hover:text-blue-600'}`}>
{typeGroup.totalAssets}
</button>
</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white' : ''}`}>
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Inventory', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white' : 'text-blue-600'}`}>
{typeGroup.totalInventory}
</button>
</td>
{['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => (
<td key={reg} className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white/80' : ''}`}>
{(typeGroup.inventoryRegions?.[reg] || 0) > 0 ? (
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: reg, category: 'Inventory', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-blue-600'}`}>
{typeGroup.inventoryRegions[reg]}
</button>
) : ''}
</td>
))}
<td className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white' : ''}`}>
{typeGroup.pending > 0 ? (
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Pending', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white' : 'text-gray-600 hover:text-blue-600'}`}>
{typeGroup.pending}
</button>
) : ''}
</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold bg-green-50/10 ${theme === 'vibrant' ? 'text-white' : ''}`}>
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Operating', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white' : 'text-green-600'}`}>
{typeGroup.totalOperating}
</button>
</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold bg-blue-50/5 ${theme === 'vibrant' ? 'text-white/80' : ''}`}>
{typeGroup.weeklyDelivered > 0 ? (
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Delivered', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-blue-600'}`}>
{typeGroup.weeklyDelivered}
</button>
) : ''}
</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold bg-orange-50/5 ${theme === 'vibrant' ? 'text-white/80' : ''}`}>
{typeGroup.weeklyReturned > 0 ? (
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Returned', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-orange-600'}`}>
{typeGroup.weeklyReturned}
</button>
) : ''}
</td>
<td className={`p-3 text-center font-bold bg-purple-50/5 ${theme === 'vibrant' ? 'text-white/80' : ''}`}>
{typeGroup.weeklyReplaced > 0 ? (
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Replaced', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-purple-600'}`}>
{typeGroup.weeklyReplaced}
</button>
) : ''}
</td>
</>
</tr>
<AnimatePresence>
{expandedAssetTypes.has(typeGroup.type) &&
typeGroup.models.map((model) => (
<React.Fragment key={model.model}>
<motion.tr
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className={`border-b border-gray-50 hover:bg-gray-50/50 transition-colors cursor-pointer ${expandedModels.has(model.model) ? 'bg-blue-50/10' : ''}`}
onClick={() => toggleModel(model.model)}
>
<td className="p-3 border-r border-gray-100 text-gray-300 text-center italic">{typeGroup.type}</td>
<td className="p-3 border-r border-gray-100 flex items-center gap-2">
{expandedModels.has(model.model) ? (
<ChevronDown size={14} className="text-blue-500" />
) : (
<ChevronRight size={14} className="text-gray-300" />
)}
<span className={expandedModels.has(model.model) ? 'font-bold text-blue-700' : ''}>{model.model}</span>
</td>
<td className="p-3 text-center border-r border-gray-100 font-medium">
<button
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All' }); }}
className="text-blue-500 hover:underline font-medium"
>{model.total}</button>
</td>
<td className="p-3 text-center border-r border-gray-100">
{model.inventory > 0 ? (
<button
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Inventory' }); }}
className="text-blue-500 hover:underline font-medium"
>{model.inventory}</button>
) : model.inventory}
</td>
{['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => (
<td key={reg} className="p-3 text-center border-r border-gray-100">
{(model.inventoryRegions[reg] || 0) > 0 ? (
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: model.model, location: reg, category: 'Inventory' });
}}
className="text-blue-500 hover:underline font-medium"
>
{model.inventoryRegions[reg]}
</button>
) : (
''
)}
</td>
))}
<td className="p-3 text-center border-r border-gray-100">
{model.pending > 0 ? (
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Pending' });
}}
className="text-blue-500 hover:underline font-medium"
>
{model.pending}
</button>
) : (
''
)}
</td>
<td className="p-3 text-center border-r border-gray-100 font-bold bg-green-50/10">
<button
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Operating' }); }}
className="text-green-600 hover:underline font-bold"
>{model.operating}</button>
</td>
<td className="p-3 text-center border-r border-gray-100 text-blue-600 bg-blue-50/5">
{model.weeklyDelivered > 0 ? (
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Delivered' });
}}
className="text-blue-500 hover:underline font-medium"
>
{model.weeklyDelivered}
</button>
) : (
''
)}
</td>
<td className="p-3 text-center border-r border-gray-100 text-orange-600 bg-orange-50/5">
{model.weeklyReturned > 0 ? (
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Returned' });
}}
className="text-blue-500 hover:underline font-medium"
>
{model.weeklyReturned}
</button>
) : (
''
)}
</td>
<td className="p-3 text-center text-purple-600 bg-purple-50/5 font-medium">
{model.weeklyReplaced > 0 ? (
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Replaced' });
}}
className="text-blue-500 hover:underline font-medium"
>
{model.weeklyReplaced}
</button>
) : (
''
)}
</td>
</motion.tr>
{expandedModels.has(model.model) && model.batches.map((batch) => (
<motion.tr
key={batch.batch}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="border-b border-gray-50 bg-purple-50/20"
>
<td className="p-3 border-r border-gray-100"></td>
<td className="p-3 border-r border-gray-100 pl-10 text-[10px] text-purple-600 italic truncate">{batch.batch}</td>
<td className="p-3 text-center border-r border-gray-100">
<button
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All' }); }}
className="text-purple-500 hover:underline font-medium"
>{batch.total}</button>
</td>
<td className="p-3 text-center border-r border-gray-100">
{batch.inventory > 0 ? (
<button
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All', category: 'Inventory' }); }}
className="text-purple-500 hover:underline font-medium"
>{batch.inventory}</button>
) : ''}
</td>
{['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => (
<td key={reg} className="p-3 text-center border-r border-gray-100">
{(batch.inventoryRegions[reg] || 0) > 0 ? (
<button
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: reg, category: 'Inventory' }); }}
className="text-purple-500 hover:underline font-medium"
>{batch.inventoryRegions[reg]}</button>
) : ''}
</td>
))}
<td className="p-3 text-center border-r border-gray-100">
{batch.pending > 0 ? (
<button
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All', category: 'Pending' }); }}
className="text-purple-500 hover:underline font-medium"
>{batch.pending}</button>
) : ''}
</td>
<td className="p-3 text-center border-r border-gray-100 bg-green-50/5">
<button
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All', category: 'Operating' }); }}
className="text-purple-600 hover:underline font-medium"
>{batch.operating}</button>
</td>
<td className="p-3 text-center border-r border-gray-100 bg-blue-50/5">
{batch.weeklyDelivered > 0 ? (
<button
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All', category: 'Delivered' }); }}
className="text-purple-500 hover:underline font-medium"
>{batch.weeklyDelivered}</button>
) : ''}
</td>
<td className="p-3 text-center border-r border-gray-100 bg-orange-50/5">
{batch.weeklyReturned > 0 ? (
<button
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All', category: 'Returned' }); }}
className="text-purple-500 hover:underline font-medium"
>{batch.weeklyReturned}</button>
) : ''}
</td>
<td className="p-3 text-center bg-purple-50/5">
{batch.weeklyReplaced > 0 ? (
<button
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All', category: 'Replaced' }); }}
className="text-purple-500 hover:underline font-medium"
>{batch.weeklyReplaced}</button>
) : ''}
</td>
</motion.tr>
))}
</React.Fragment>
))}
</AnimatePresence>
</React.Fragment>
))}
</tbody>
</table>
</div>
{/* Mobile View Cards for Asset Summary */}
<div className="lg:hidden p-4 space-y-4">
{processedData.map((typeGroup) => (
<div key={typeGroup.type} className="space-y-3">
<div
className={`px-3 py-2 rounded flex justify-between items-center shadow-sm cursor-pointer transition-all ${
theme === 'vibrant'
? 'bg-blue-600 text-white active:bg-blue-700'
: theme === 'minimal'
? 'bg-white border-l-4 border-blue-500 text-gray-800 active:bg-gray-50'
: 'bg-blue-50 border border-blue-100 text-blue-700 active:bg-blue-100'
}`}
onClick={() => toggleAssetType(typeGroup.type)}
>
<div className="flex items-center gap-2">
{expandedAssetTypes.has(typeGroup.type) ? (
<ChevronDown size={16} className={theme === 'vibrant' ? 'text-white' : 'text-blue-500'} />
) : (
<ChevronRight size={16} className={theme === 'vibrant' ? 'text-white/70' : 'text-blue-300'} />
)}
<span className="text-xs font-bold">{typeGroup.type}</span>
</div>
<div className={`flex gap-3 text-[9px] font-normal ${theme === 'vibrant' ? 'opacity-90' : 'text-gray-500'}`}>
<span>
<span className={theme === 'vibrant' ? 'font-bold' : 'font-bold text-gray-700'}>{typeGroup.totalAssets}</span>
</span>
<span>
{' '}
<span className={theme === 'vibrant' ? 'font-bold' : 'font-bold text-blue-600'}>{typeGroup.totalInventory}</span>
</span>
<span>
{' '}
<span className={theme === 'vibrant' ? 'font-bold' : 'font-bold text-green-600'}>{typeGroup.totalOperating}</span>
</span>
</div>
</div>
<AnimatePresence>
{expandedAssetTypes.has(typeGroup.type) && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="space-y-3 overflow-hidden"
>
{typeGroup.models.map((model) => (
<div key={model.model} className="bg-white rounded-lg border border-gray-100 shadow-sm overflow-hidden">
<div
className="p-3 flex justify-between items-center cursor-pointer active:bg-gray-50"
onClick={() => toggleModel(model.model)}
>
<div className="flex items-center gap-2">
{expandedModels.has(model.model) ? (
<ChevronDown size={14} className="text-blue-500" />
) : (
<ChevronRight size={14} className="text-gray-300" />
)}
<span className="text-xs font-bold text-gray-700">{model.model}</span>
</div>
<div className="flex gap-2">
<span className="text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-bold"> {model.total}</span>
<span className="text-[10px] bg-orange-50 text-orange-600 px-1.5 py-0.5 rounded font-bold"> {model.inventory}</span>
<span className="text-[10px] bg-green-50 text-green-600 px-1.5 py-0.5 rounded font-bold"> {model.operating}</span>
</div>
</div>
{expandedModels.has(model.model) && (
<div className="px-3 pb-3 pt-1 border-t border-gray-50 bg-gray-50/30">
<div className="grid grid-cols-2 gap-y-3 gap-x-4 mt-2">
<div className="flex justify-between items-center cursor-pointer hover:bg-gray-100 p-1 rounded transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Inventory', source: 'asset', title: `${model.model} - 库存` })}>
<span className="text-[10px] text-gray-400"></span>
<span className="text-xs font-bold text-blue-600">{model.inventory}</span>
</div>
<div className="flex justify-between items-center cursor-pointer hover:bg-gray-100 p-1 rounded transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Pending', source: 'asset', title: `${model.model} - 待交车` })}>
<span className="text-[10px] text-gray-400"></span>
<span className="text-xs font-bold text-gray-600">{model.pending}</span>
</div>
<div className="col-span-2 grid grid-cols-5 gap-1 py-2 border-y border-gray-100">
{['嘉兴', '广东', '北京', '新疆', '其他'].map(reg => (
<div key={reg} className="text-center">
<div className="text-[8px] text-gray-400 mb-0.5">{reg === '嘉兴' ? '浙' : reg === '广东' ? '粤' : reg === '北京' ? '京' : reg === '新疆' ? '新' : '其'}</div>
{(model.inventoryRegions[reg] || 0) > 0 ? (
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: model.model, location: reg, source: 'asset', title: `${model.model} - ${reg}` });
}}
className="text-[10px] font-bold text-blue-500 hover:underline"
>
{model.inventoryRegions[reg]}
</button>
) : (
<div className="text-[10px] font-bold text-gray-300">-</div>
)}
</div>
))}
</div>
<div className="col-span-2 grid grid-cols-3 gap-2 pt-1">
<div className="bg-blue-50/50 p-2 rounded flex flex-col items-center cursor-pointer hover:bg-blue-100/50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Delivered', source: 'asset', title: `${model.model} - 本周交车` })}>
<span className="text-[8px] text-gray-400 mb-1"></span>
<span className="text-xs font-bold text-blue-600">{model.weeklyDelivered}</span>
</div>
<div className="bg-orange-50/50 p-2 rounded flex flex-col items-center cursor-pointer hover:bg-orange-100/50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Returned', source: 'asset', title: `${model.model} - 本周还车` })}>
<span className="text-[8px] text-gray-400 mb-1"></span>
<span className="text-xs font-bold text-orange-600">{model.weeklyReturned}</span>
</div>
<div className="bg-purple-50/50 p-2 rounded flex flex-col items-center cursor-pointer hover:bg-purple-100/50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Replaced', source: 'asset', title: `${model.model} - 本周替换` })}>
<span className="text-[8px] text-gray-400 mb-1"></span>
<span className="text-xs font-bold text-purple-600">{model.weeklyReplaced}</span>
</div>
</div>
</div>
</div>
)}
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
))}
</div>
</div>
{/* Inventory Statistics */}
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm mb-6 overflow-hidden">
<div className="p-3 sm:p-4 border-b border-gray-50 bg-white flex items-center justify-between relative">
<div className="flex items-center gap-3">
<div className="w-1.5 h-6 bg-blue-600 rounded-full"></div>
<div className="flex flex-col">
<h2 className="text-lg font-bold text-gray-800 leading-tight"></h2>
<span className="text-[10px] text-gray-400 font-medium"></span>
</div>
</div>
<div className="flex items-center gap-6 ml-auto pr-2">
<div className="flex flex-col items-end cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Inventory', source: 'asset', title: '库存总数' })}>
<span className="text-[10px] text-gray-400 font-bold tracking-wider uppercase mb-0.5"></span>
<span className="text-2xl font-black text-gray-900 leading-none">
{filteredInventoryStats.reduce((acc, s) => acc + s.quantity, 0)}
<span className="text-xs font-bold text-gray-400 ml-1"></span>
</span>
</div>
<div className="relative">
<button
onClick={() => setIsInventoryFilterOpen(!isInventoryFilterOpen)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
isInventoryFilterOpen || (inventoryFilters.region || inventoryFilters.city || inventoryFilters.brand || inventoryFilters.batch || inventoryFilters.model)
? 'bg-blue-600 text-white shadow-md'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
<Filter size={14} />
<span></span>
{(inventoryFilters.region || inventoryFilters.city || inventoryFilters.brand || inventoryFilters.batch || inventoryFilters.model) && (
<span className="w-2 h-2 bg-white rounded-full animate-pulse"></span>
)}
</button>
<AnimatePresence>
{isInventoryFilterOpen && (
<>
<div className="fixed inset-0 z-40" onClick={() => setIsInventoryFilterOpen(false)} />
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
className="absolute right-0 top-full mt-2 w-72 bg-white rounded-xl shadow-2xl border border-slate-100 z-50 p-4"
>
<div className="flex justify-between items-center mb-4">
<h3 className="text-xs font-bold text-slate-800"> - </h3>
<div className="flex items-center gap-2">
<button
onClick={() => setInventoryFilters({ region: '', city: '', brand: '', batch: '', model: '' })}
className="text-[10px] text-blue-500 hover:underline"
>
</button>
<button onClick={() => setIsInventoryFilterOpen(false)} className="text-slate-400 hover:text-slate-600">
<PlusCircle className="rotate-45" size={16} />
</button>
</div>
</div>
<div className="space-y-3 text-left">
<div>
<label className="text-[10px] text-slate-400 block mb-1"></label>
<input
list="dl-inv-region"
type="text"
placeholder="搜索或选择..."
value={inventoryFilters.region}
onChange={(e) => setInventoryFilters({...inventoryFilters, region: e.target.value})}
className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
/>
<datalist id="dl-inv-region">
{uniqueInventoryRegions.map(r => <option key={r} value={r} />)}
</datalist>
</div>
<div>
<label className="text-[10px] text-slate-400 block mb-1"></label>
<input
list="dl-inv-city"
type="text"
placeholder="搜索或选择..."
value={inventoryFilters.city}
onChange={(e) => setInventoryFilters({...inventoryFilters, city: e.target.value})}
className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
/>
<datalist id="dl-inv-city">
{uniqueInventoryCities.map(c => <option key={c} value={c} />)}
</datalist>
</div>
<div>
<label className="text-[10px] text-slate-400 block mb-1"></label>
<input
list="dl-inv-brand"
type="text"
placeholder="搜索或选择..."
value={inventoryFilters.brand}
onChange={(e) => setInventoryFilters({...inventoryFilters, brand: e.target.value})}
className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
/>
<datalist id="dl-inv-brand">
{uniqueInventoryBrands.map(b => <option key={b} value={b} />)}
</datalist>
</div>
<div>
<label className="text-[10px] text-slate-400 block mb-1"></label>
<input
list="dl-inv-batch"
type="text"
placeholder="搜索或选择..."
value={inventoryFilters.batch}
onChange={(e) => setInventoryFilters({...inventoryFilters, batch: e.target.value})}
className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
/>
<datalist id="dl-inv-batch">
{uniqueInventoryBatches.map(b => <option key={b} value={b} />)}
</datalist>
</div>
<div>
<label className="text-[10px] text-slate-400 block mb-1"></label>
<div className="relative">
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-slate-400" />
<input
list="dl-inv-model"
type="text"
placeholder="搜索车型..."
value={inventoryFilters.model}
onChange={(e) => 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"
/>
<datalist id="dl-inv-model">
{uniqueInventoryModels.map(m => <option key={m} value={m} />)}
</datalist>
</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</div>
</div>
</div>
<div className="px-4 py-3 bg-gray-50/50 border-b border-gray-100">
<div className="flex bg-gray-200/50 p-1 rounded-lg w-fit shadow-inner">
<button
onClick={() => setInventoryTab('region')}
className={`px-6 py-1.5 rounded-md text-xs font-bold transition-all ${
inventoryTab === 'region' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'
}`}
>
</button>
<button
onClick={() => setInventoryTab('model')}
className={`px-6 py-1.5 rounded-md text-xs font-bold transition-all ${
inventoryTab === 'model' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'
}`}
>
</button>
</div>
</div>
{/* Active Filters Bar */}
{(inventoryFilters.region || inventoryFilters.city || inventoryFilters.brand || inventoryFilters.batch || inventoryFilters.model) && (
<div className="px-4 py-2 bg-white border-b border-slate-50 flex flex-wrap gap-2 items-center">
<span className="text-[10px] text-slate-400 mr-1">:</span>
{inventoryFilters.region && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-[10px] flex items-center gap-1">
: {inventoryFilters.region}
<button onClick={() => setInventoryFilters({...inventoryFilters, region: ''})} className="hover:text-blue-800">×</button>
</span>
)}
{inventoryFilters.city && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-[10px] flex items-center gap-1">
: {inventoryFilters.city}
<button onClick={() => setInventoryFilters({...inventoryFilters, city: ''})} className="hover:text-blue-800">×</button>
</span>
)}
{inventoryFilters.brand && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-[10px] flex items-center gap-1">
: {inventoryFilters.brand}
<button onClick={() => setInventoryFilters({...inventoryFilters, brand: ''})} className="hover:text-blue-800">×</button>
</span>
)}
{inventoryFilters.batch && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-[10px] flex items-center gap-1">
: {inventoryFilters.batch}
<button onClick={() => setInventoryFilters({...inventoryFilters, batch: ''})} className="hover:text-blue-800">×</button>
</span>
)}
{inventoryFilters.model && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-[10px] flex items-center gap-1">
: {inventoryFilters.model}
<button onClick={() => setInventoryFilters({...inventoryFilters, model: ''})} className="hover:text-blue-800">×</button>
</span>
)}
<button
onClick={() => setInventoryFilters({ region: '', city: '', brand: '', batch: '', model: '' })}
className="text-[10px] text-slate-400 hover:text-red-500 ml-auto"
>
</button>
</div>
)}
{/* Desktop View Table */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50 text-[11px] text-slate-500 uppercase tracking-wider border-b border-slate-100">
<th className="p-3 font-semibold w-64">{inventoryTab === 'region' ? '区域 / 城市' : '车型分类 / 型号'}</th>
<th className="p-3 font-semibold">{inventoryTab === 'region' ? '品牌' : '品牌'}</th>
<th className="p-3 font-semibold">{inventoryTab === 'region' ? '车型' : '所在区域/城市'}</th>
<th className="p-3 font-semibold text-center w-32"></th>
</tr>
</thead>
<tbody className="text-xs">
{inventoryTab === 'region' ? (
Object.entries(inventoryByRegion).map(([region, cities]) => (
<React.Fragment key={region}>
<tr
className="bg-slate-50/30 border-b border-slate-100 cursor-pointer hover:bg-slate-50 transition-colors"
onClick={() => toggleInventoryRegion(region)}
>
<td colSpan={4} className="p-3 font-bold text-slate-700">
<div className="flex items-center gap-2">
{expandedInventoryRegions.has(region) ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
<span>{region}</span>
<span className="text-[10px] font-normal text-slate-400 ml-2 cursor-pointer hover:text-blue-500 transition-colors"
onClick={(e) => {
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)} )
</span>
</div>
</td>
</tr>
<AnimatePresence>
{expandedInventoryRegions.has(region) && Object.entries(cities).map(([city, stats]) => (
<React.Fragment key={city}>
{stats.map((stat, idx) => (
<motion.tr
key={`${city}-${stat.model}-${idx}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="border-b border-slate-50 hover:bg-slate-50/20 transition-colors"
>
<td className="p-3 pl-8 text-slate-500 border-r border-slate-50">
{idx === 0 ? <span className="font-medium text-slate-600">{city}</span> : ''}
</td>
<td className="p-3 text-slate-600 border-r border-slate-50">{stat.brand}</td>
<td className="p-3 text-slate-600 border-r border-slate-50">{stat.model}</td>
<td className="p-3 text-center font-bold text-blue-600">
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: stat.model, location: stat.city, category: 'Inventory', source: 'asset', title: `库存统计 - ${stat.model} - ${stat.city}` });
}}
className="text-blue-600 hover:underline font-bold"
>
{stat.quantity}
</button>
</td>
</motion.tr>
))}
</React.Fragment>
))}
</AnimatePresence>
</React.Fragment>
))
) : (
Object.entries(inventoryByModel).map(([type, models]) => (
<React.Fragment key={type}>
<tr
className="bg-slate-50/30 border-b border-slate-100 cursor-pointer hover:bg-slate-50 transition-colors"
onClick={() => toggleInventoryType(type)}
>
<td colSpan={4} className="p-3 font-bold text-slate-700">
<div className="flex items-center gap-2">
{expandedInventoryTypes.has(type) ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
<span>{type}</span>
<span className="text-[10px] font-normal text-slate-400 ml-2 cursor-pointer hover:text-blue-500 transition-colors"
onClick={(e) => {
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)} )
</span>
</div>
</td>
</tr>
<AnimatePresence>
{expandedInventoryTypes.has(type) && Object.entries(models).map(([model, stats]) => (
<React.Fragment key={model}>
{stats.map((stat, idx) => (
<motion.tr
key={`${model}-${stat.region}-${stat.city}-${idx}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="border-b border-slate-50 hover:bg-slate-50/20 transition-colors"
>
<td className="p-3 pl-8 text-slate-500 border-r border-slate-50">
{idx === 0 ? <span className="font-medium text-slate-600">{model}</span> : ''}
</td>
<td className="p-3 text-slate-600 border-r border-slate-50">{stat.brand}</td>
<td className="p-3 text-slate-600 border-r border-slate-50">{stat.region} / {stat.city}</td>
<td className="p-3 text-center font-bold text-blue-600">
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: stat.model, location: stat.city, category: 'Inventory', source: 'asset', title: `库存统计 - ${stat.model} - ${stat.city}` });
}}
className="text-blue-600 hover:underline font-bold"
>
{stat.quantity}
</button>
</td>
</motion.tr>
))}
</React.Fragment>
))}
</AnimatePresence>
</React.Fragment>
))
)}
</tbody>
</table>
</div>
{/* Mobile View */}
<div className="lg:hidden p-3 space-y-3">
{inventoryTab === 'region' ? (
Object.entries(inventoryByRegion).map(([region, cities]) => (
<div key={region} className="border border-slate-100 rounded overflow-hidden">
<div
className="bg-slate-50 p-3 flex justify-between items-center cursor-pointer"
onClick={() => toggleInventoryRegion(region)}
>
<div className="flex items-center gap-2">
{expandedInventoryRegions.has(region) ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
<span className="text-xs font-bold text-slate-700">{region}</span>
</div>
<span className="text-[10px] font-bold text-blue-600">
{Object.values(cities).flat().reduce((acc, s) => acc + s.quantity, 0)}
</span>
</div>
{expandedInventoryRegions.has(region) && (
<div className="p-2 space-y-2 bg-white">
{Object.entries(cities).map(([city, stats]) => (
<div key={city} className="border-l-2 border-slate-200 pl-3 py-1">
<div className="text-[10px] font-bold text-slate-500 mb-2">{city}</div>
<div className="space-y-2">
{stats.map((stat, idx) => (
<div key={idx} className="flex justify-between items-center text-[11px] bg-slate-50/50 p-2 rounded">
<div className="flex flex-col">
<span className="text-slate-400 text-[9px]">{stat.brand}</span>
<span className="text-slate-700 font-medium">{stat.model}</span>
</div>
<button
onClick={() => setShowPlateNumbers({ batch: 'All', model: stat.model, location: stat.city, category: 'Inventory', source: 'asset', title: `库存统计 - ${stat.model} - ${stat.city}` })}
className="font-bold text-blue-600 hover:underline"
>
{stat.quantity}
</button>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
))
) : (
Object.entries(inventoryByModel).map(([type, models]) => (
<div key={type} className="border border-slate-100 rounded overflow-hidden">
<div
className="bg-slate-50 p-3 flex justify-between items-center cursor-pointer"
onClick={() => toggleInventoryType(type)}
>
<div className="flex items-center gap-2">
{expandedInventoryTypes.has(type) ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
<span className="text-xs font-bold text-slate-700">{type}</span>
</div>
<span className="text-[10px] font-bold text-blue-600">
{Object.values(models).flat().reduce((acc, s) => acc + s.quantity, 0)}
</span>
</div>
{expandedInventoryTypes.has(type) && (
<div className="p-2 space-y-2 bg-white">
{Object.entries(models).map(([model, stats]) => (
<div key={model} className="border-l-2 border-slate-200 pl-3 py-1">
<div className="text-[10px] font-bold text-slate-500 mb-2">{model}</div>
<div className="space-y-2">
{stats.map((stat, idx) => (
<div key={idx} className="flex justify-between items-center text-[11px] bg-slate-50/50 p-2 rounded">
<div className="flex flex-col">
<span className="text-slate-400 text-[9px]">{stat.brand}</span>
<span className="text-slate-700 font-medium">{stat.region} / {stat.city}</span>
</div>
<button
onClick={() => setShowPlateNumbers({ batch: 'All', model: stat.model, location: stat.city, category: 'Inventory', source: 'asset', title: `库存统计 - ${stat.model} - ${stat.city}` })}
className="font-bold text-blue-600 hover:underline"
>
{stat.quantity}
</button>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
))
)}
</div>
</div>
</>
)}
{activeTab === 'department' && (
/* Department Operations Statistics */
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6">
<div className="p-3 sm:p-4 border-b border-gray-50 flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-1.5 h-6 bg-blue-600 rounded-full"></div>
<div>
<h2 className="text-lg font-bold text-gray-800"></h2>
<p className="text-[10px] text-gray-400 font-medium"></p>
</div>
</div>
</div>
<div className="p-0 sm:p-2 bg-gray-50/30">
{/* Overall Total Summary (Compact) - Moved to Top */}
<div className="m-2 bg-slate-800 rounded-xl p-3 text-white shadow-lg">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="flex flex-col">
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5"></span>
<span className="text-xl font-black">{deptData.reduce((s, d) => s + d.totalAssets, 0)}</span>
</div>
<div className="flex flex-col">
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5 text-green-400"></span>
<span className="text-xl font-black text-green-400">
{deptData.reduce((acc, d) => acc + d.operatingCount, 0)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5 text-slate-400"></span>
<span className="text-xl font-black text-slate-400">
{deptData.reduce((acc, d) => acc + d.idleCount, 0)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5 text-blue-400"></span>
<span className="text-xl font-black text-blue-400">
{deptData.length > 0 ? (deptData.reduce((acc, d) => acc + d.attendanceRate, 0) / deptData.length).toFixed(1) : 0}%
</span>
</div>
</div>
</div>
{/* Controls Row: Toggles Left, Filter Right */}
<div className="px-2 mb-2 flex items-center justify-between gap-4">
<div className="flex bg-gray-200/50 p-1 rounded-lg shadow-inner">
<button
onClick={() => setDeptViewMode('department')}
className={`px-4 py-1.5 text-xs font-bold rounded-md transition-all ${deptViewMode === 'department' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
>
</button>
<button
onClick={() => setDeptViewMode('manager')}
className={`px-4 py-1.5 text-xs font-bold rounded-md transition-all ${deptViewMode === 'manager' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
>
</button>
</div>
<div className="flex-1 max-w-[240px]">
{deptViewMode === 'manager' && (
<div className="relative">
<Filter className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400" size={14} />
<input
list="dl-dept-manager"
type="text"
placeholder="搜索或选择..."
value={selectedManager === 'All' ? '' : selectedManager}
onChange={(e) => setSelectedManager(e.target.value || 'All')}
className="w-full pl-9 pr-8 py-1.5 bg-white border border-gray-200 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm font-bold text-gray-700"
/>
<datalist id="dl-dept-manager">
<option value="All"></option>
{allManagersList.map(m => (
<option key={m} value={m} />
))}
</datalist>
</div>
)}
</div>
</div>
{/* Desktop Table View */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left border-collapse min-w-[900px]">
<thead>
<tr className="bg-gray-100/50 text-[11px] text-gray-500 uppercase tracking-wider border-b border-gray-200">
<th className="p-2 font-bold border-r border-gray-100 w-48">{deptViewMode === 'department' ? '部门名称' : '业务负责人'}</th>
{deptViewMode === 'manager' && <th className="p-2 font-bold border-r border-gray-100 w-32"></th>}
<th className="p-2 font-bold border-r border-gray-100 text-center w-24">{deptViewMode === 'department' ? '出勤率' : '合计资产'}</th>
{deptViewMode === 'department' && (
<>
<th className="p-2 font-bold border-r border-gray-100 text-center w-24"></th>
<th className="p-2 font-bold border-r border-gray-100 text-center w-28"></th>
<th className="p-2 font-bold border-r border-gray-100 text-center w-24 text-green-500"></th>
<th className="p-2 font-bold border-r border-gray-100 text-center w-24 text-gray-400"></th>
</>
)}
{deptViewMode === 'manager' && (
<>
<th className="p-2 font-bold border-r border-gray-100 text-center w-20">4.5T</th>
<th className="p-2 font-bold border-r border-gray-100 text-center w-20"></th>
<th className="p-2 font-bold border-r border-gray-100 text-center w-20">18T</th>
<th className="p-2 font-bold border-r border-gray-100 text-center w-20">49T</th>
<th className="p-2 font-bold border-r border-gray-100 text-center w-20"></th>
<th className="p-2 font-bold border-r border-gray-100 text-center w-20"></th>
</>
)}
<th className="p-2 font-bold text-center w-16"></th>
</tr>
</thead>
<tbody className="text-xs">
{deptViewMode === 'department' ? (
deptData.map((dept) => {
const isExpanded = expandedDepts.has(dept.department);
return (
<React.Fragment key={dept.department}>
<tr
className={`cursor-pointer transition-all border-b border-gray-100 ${
isExpanded ? 'bg-blue-50/50' : 'hover:bg-gray-50'
}`}
onClick={() => toggleDept(dept.department)}
>
<td className="p-2 border-r border-gray-100 font-bold text-gray-800">
{dept.department}
</td>
<td className="p-2 border-r border-gray-100 text-center">
<span className="bg-blue-50 text-blue-600 text-[10px] font-bold px-2 py-0.5 rounded-full">
{dept.attendanceRate}%
</span>
</td>
<td className="p-2 border-r border-gray-100 text-center font-black text-gray-800 text-sm">
{dept.totalAssets}
</td>
<td className="p-2 border-r border-gray-100 text-center">
<div className="flex items-baseline justify-center gap-1">
<span className="font-black text-gray-800 text-sm">{dept.avgMileage}</span>
<span className="text-[9px] text-gray-400 font-bold">km</span>
</div>
</td>
<td className="p-2 border-r border-gray-100 text-center font-black text-green-500 text-sm">
{dept.operatingCount}
</td>
<td className="p-2 border-r border-gray-100 text-center font-black text-gray-400 text-sm">
{dept.idleCount}
</td>
<td className="p-2 text-center">
{isExpanded ? <ChevronDown size={16} className="text-blue-500 inline" /> : <ChevronRight size={16} className="text-gray-300 inline" />}
</td>
</tr>
{isExpanded && (
<tr className="bg-gray-50/50">
<td colSpan={7} className="p-2">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
{dept.managers.map(m => {
const isManagerExpanded = expandedManagerDetails.has(m.manager);
return (
<div key={m.manager} className="bg-white rounded-lg border border-gray-100 shadow-sm overflow-hidden">
<div
className="p-2 flex justify-between items-center cursor-pointer hover:bg-gray-50 transition-colors"
onClick={() => toggleManagerDetails(m.manager)}
>
<div className="flex items-center gap-2">
{isManagerExpanded ? <ChevronDown size={14} className="text-blue-500" /> : <ChevronRight size={14} className="text-gray-300" />}
<span className="font-bold text-gray-700 text-xs">{m.manager}</span>
</div>
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department' });
}}
className="text-[10px] font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded hover:bg-blue-100 transition-colors"
>
: {m.total}
</button>
</div>
{isManagerExpanded && (
<div className="p-2 pt-0 border-t border-gray-50 bg-gray-50/30">
<div className="grid grid-cols-3 gap-1 mt-2">
<div
className="text-center bg-white p-1 rounded cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: false, category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase">4.5T</div>
<div className="text-[10px] font-bold text-gray-600">{m.t4_5}</div>
</div>
<div
className="text-center bg-white p-1 rounded cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: true, category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase"></div>
<div className="text-[10px] font-bold text-gray-600">{m.t4_5c}</div>
</div>
<div
className="text-center bg-white p-1 rounded cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '18T', category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase">18T</div>
<div className="text-[10px] font-bold text-gray-600">{m.t18}</div>
</div>
<div
className="text-center bg-white p-1 rounded cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '49T', category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase">49T</div>
<div className="text-[10px] font-bold text-gray-600">{m.t49}</div>
</div>
<div
className="text-center bg-white p-1 rounded cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: true, category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase"></div>
<div className="text-[10px] font-bold text-gray-600">{m.trailer}</div>
</div>
<div
className="text-center bg-white p-1 rounded cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: false, category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase"></div>
<div className="text-[10px] font-bold text-gray-600">{m.other}</div>
</div>
</div>
</div>
)}
</div>
);
})}
</div>
</td>
</tr>
)}
</React.Fragment>
);
})
) : (
managerStats.map((m) => {
const isManagerExpanded = expandedManagerDetails.has(m.manager);
return (
<React.Fragment key={m.manager}>
<tr
className="border-b border-gray-100 hover:bg-gray-50 transition-colors cursor-pointer"
onClick={() => toggleManagerDetails(m.manager)}
>
<td className="p-2 border-r border-gray-100 font-bold text-gray-800 flex items-center gap-1">
{isManagerExpanded ? <ChevronDown size={12} className="text-blue-400" /> : <ChevronRight size={12} className="text-gray-300" />}
{m.manager}
</td>
<td className="p-2 border-r border-gray-100 text-gray-600">{m.department}</td>
<td
className="p-2 border-r border-gray-100 text-center font-black text-blue-600 text-sm cursor-pointer hover:bg-blue-50"
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department' });
}}
>
{m.total}
</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-400">-</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-400">-</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-400">-</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-400">-</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-400">-</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-400">-</td>
<td className="p-2 text-center">
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department' });
}}
className="text-blue-500 hover:text-blue-700 transition-colors"
>
<ArrowRightLeft size={14} className="inline" />
</button>
</td>
</tr>
{isManagerExpanded && (
<tr className="bg-gray-50/50 border-b border-gray-100">
<td colSpan={10} className="p-0">
<div className="grid grid-cols-6 text-[10px] bg-white/50">
<div className="p-2 border-r border-gray-100 flex flex-col items-center cursor-pointer hover:bg-blue-50" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: false, category: 'Operating', source: 'department' })}>
<span className="text-gray-400 uppercase mb-1">4.5T</span>
<span className="font-bold text-gray-600">{m.t4_5}</span>
</div>
<div className="p-2 border-r border-gray-100 flex flex-col items-center cursor-pointer hover:bg-blue-50" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: true, category: 'Operating', source: 'department' })}>
<span className="text-gray-400 uppercase mb-1"></span>
<span className="font-bold text-gray-600">{m.t4_5c}</span>
</div>
<div className="p-2 border-r border-gray-100 flex flex-col items-center cursor-pointer hover:bg-blue-50" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '18T', category: 'Operating', source: 'department' })}>
<span className="text-gray-400 uppercase mb-1">18T</span>
<span className="font-bold text-gray-600">{m.t18}</span>
</div>
<div className="p-2 border-r border-gray-100 flex flex-col items-center cursor-pointer hover:bg-blue-50" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '49T', category: 'Operating', source: 'department' })}>
<span className="text-gray-400 uppercase mb-1">49T</span>
<span className="font-bold text-gray-600">{m.t49}</span>
</div>
<div className="p-2 border-r border-gray-100 flex flex-col items-center cursor-pointer hover:bg-blue-50" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: true, category: 'Operating', source: 'department' })}>
<span className="text-gray-400 uppercase mb-1"></span>
<span className="font-bold text-gray-600">{m.trailer}</span>
</div>
<div className="p-2 flex flex-col items-center cursor-pointer hover:bg-blue-50" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: false, category: 'Operating', source: 'department' })}>
<span className="text-gray-400 uppercase mb-1"></span>
<span className="font-bold text-gray-600">{m.other}</span>
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
);
})
)}
</tbody>
</table>
</div>
{/* Mobile Card View */}
<div className="lg:hidden p-2 space-y-2">
{deptViewMode === 'department' ? (
deptData.map((dept) => {
const isExpanded = expandedDepts.has(dept.department);
return (
<div key={dept.department} className="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div
className="p-3 cursor-pointer"
onClick={() => toggleDept(dept.department)}
>
<div className="flex justify-between items-center mb-2">
<h3 className="text-sm font-bold text-gray-800">{dept.department}</h3>
<span className="bg-blue-50 text-blue-600 text-[9px] font-bold px-2 py-0.5 rounded-full">
: {dept.attendanceRate}%
</span>
</div>
<div className="grid grid-cols-4 gap-2">
<div className="text-center">
<div className="text-[8px] text-gray-400 uppercase font-bold mb-0.5"></div>
<div className="text-xs font-black text-gray-800">{dept.totalAssets}</div>
</div>
<div className="text-center">
<div className="text-[8px] text-gray-400 uppercase font-bold mb-0.5"></div>
<div className="text-xs font-black text-gray-800">{dept.avgMileage}</div>
</div>
<div className="text-center">
<div className="text-[8px] text-green-500 uppercase font-bold mb-0.5"></div>
<div className="text-xs font-black text-green-500">{dept.operatingCount}</div>
</div>
<div className="text-center">
<div className="text-[8px] text-gray-400 uppercase font-bold mb-0.5"></div>
<div className="text-xs font-black text-gray-400">{dept.idleCount}</div>
</div>
</div>
<div className="mt-1 flex justify-center">
{isExpanded ? <ChevronDown size={14} className="text-blue-500" /> : <ChevronRight size={14} className="text-gray-300" />}
</div>
</div>
{isExpanded && (
<div className="bg-gray-50/50 p-2 border-t border-gray-50 space-y-2">
{dept.managers.map(m => {
const isManagerExpanded = expandedManagerDetails.has(m.manager);
return (
<div key={m.manager} className="bg-white rounded border border-gray-100 shadow-sm overflow-hidden">
<div
className="p-2 flex justify-between items-center cursor-pointer"
onClick={() => toggleManagerDetails(m.manager)}
>
<div className="flex items-center gap-1">
{isManagerExpanded ? <ChevronDown size={12} className="text-blue-400" /> : <ChevronRight size={12} className="text-gray-300" />}
<span className="text-[11px] font-bold text-gray-700">{m.manager}</span>
</div>
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department' });
}}
className="text-[10px] font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded"
>
: {m.total}
</button>
</div>
{isManagerExpanded && (
<div className="p-2 border-t border-gray-50 bg-gray-50/30 grid grid-cols-3 gap-1">
<div
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: false, category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase">4.5T</div>
<div className="text-[10px] font-bold text-gray-600">{m.t4_5}</div>
</div>
<div
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: true, category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase"></div>
<div className="text-[10px] font-bold text-gray-600">{m.t4_5c}</div>
</div>
<div
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '18T', category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase">18T</div>
<div className="text-[10px] font-bold text-gray-600">{m.t18}</div>
</div>
<div
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '49T', category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase">49T</div>
<div className="text-[10px] font-bold text-gray-600">{m.t49}</div>
</div>
<div
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: true, category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase"></div>
<div className="text-[10px] font-bold text-gray-600">{m.trailer}</div>
</div>
<div
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: false, category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase"></div>
<div className="text-[10px] font-bold text-gray-600">{m.other}</div>
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
})
) : (
managerStats.map((m) => {
const isManagerExpanded = expandedManagerDetails.has(m.manager);
return (
<div key={m.manager} className="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div
className="p-2 cursor-pointer flex items-center justify-between gap-2"
onClick={() => toggleManagerDetails(m.manager)}
>
<div className="flex items-center gap-2 flex-1 min-w-0">
{isManagerExpanded ? <ChevronDown size={14} className="text-blue-500" /> : <ChevronRight size={14} className="text-gray-300" />}
<div className="flex items-center gap-3 min-w-0">
<h3 className="text-sm font-bold text-gray-800 shrink-0">{m.manager}</h3>
<span className="text-[11px] text-gray-500 shrink-0">{m.department}</span>
<div
className="text-[11px] font-bold text-blue-600 whitespace-nowrap"
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department' });
}}
>
: {m.total}
</div>
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department' });
}}
className="text-blue-500 p-1 hover:bg-blue-50 rounded transition-colors flex-shrink-0"
>
<ArrowRightLeft size={14} />
</button>
</div>
{isManagerExpanded && (
<div className="p-2 border-t border-gray-50 bg-gray-50/30 grid grid-cols-3 gap-1">
<div
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: false, category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase">4.5T</div>
<div className="text-[10px] font-bold text-gray-600">{m.t4_5}</div>
</div>
<div
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: true, category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase"></div>
<div className="text-[10px] font-bold text-gray-600">{m.t4_5c}</div>
</div>
<div
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '18T', category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase">18T</div>
<div className="text-[10px] font-bold text-gray-600">{m.t18}</div>
</div>
<div
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '49T', category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase">49T</div>
<div className="text-[10px] font-bold text-gray-600">{m.t49}</div>
</div>
<div
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: true, category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase"></div>
<div className="text-[10px] font-bold text-gray-600">{m.trailer}</div>
</div>
<div
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: false, category: 'Operating', source: 'department' })}
>
<div className="text-[8px] text-gray-400 uppercase"></div>
<div className="text-[10px] font-bold text-gray-600">{m.other}</div>
</div>
</div>
)}
</div>
);
})
)}
</div>
</div>
</section>
)}
{activeTab === 'region' && (
<div className="flex flex-col gap-6">
{/* Region Distribution Chart */}
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4 sm:p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-1.5 h-6 bg-blue-600 rounded-full"></div>
<h2 className="text-lg font-bold text-gray-800"></h2>
</div>
<div className="flex items-center bg-gray-100 rounded-lg p-0.5">
<button
onClick={() => setRegionChartView('region')}
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${regionChartView === 'region' ? 'bg-white text-blue-700 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
></button>
<button
onClick={() => setRegionChartView('city')}
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${regionChartView === 'city' ? 'bg-white text-blue-700 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
></button>
</div>
</div>
<div className="h-72 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={regionChartData} margin={{ top: 20 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f0f0f0" />
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#94a3b8', fontSize: 12 }} />
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#94a3b8', fontSize: 12 }} />
<Tooltip
contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }}
cursor={{ fill: '#f8fafc' }}
/>
<Bar dataKey="value" fill="#3b82f6" radius={[4, 4, 0, 0]} barSize={regionChartView === 'city' ? 20 : 40}>
<LabelList dataKey="value" position="top" style={{ fill: '#64748b', fontSize: 11, fontWeight: 600 }} />
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</section>
{/* Region - Vehicle - Customer Section */}
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6">
<div className="p-3 sm:p-4 border-b border-gray-50 flex items-center justify-between bg-slate-50 relative">
<div className="flex items-center gap-3">
<div className="w-1.5 h-6 bg-slate-400 rounded-full"></div>
<div>
<h2 className="text-lg font-bold text-slate-800"></h2>
<p className="text-[10px] text-slate-400 font-medium">*</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setIsRegionFilterOpen(!isRegionFilterOpen)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
isRegionFilterOpen || Object.values(regionFilters).some(v => v !== '')
? 'bg-slate-200 text-slate-900 shadow-sm'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<Filter size={14} />
<span></span>
{Object.values(regionFilters).some(v => v !== '') && (
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
)}
</button>
<AnimatePresence>
{isRegionFilterOpen && (
<>
<div className="fixed inset-0 z-40" onClick={() => setIsRegionFilterOpen(false)} />
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
className="absolute top-full right-4 mt-2 w-72 bg-white rounded-xl shadow-2xl border border-gray-100 z-50 p-4 text-gray-800"
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-bold text-gray-900"></h3>
<button
onClick={() => setRegionFilters({ region: '', city: '', customer: '' })}
className="text-[10px] text-slate-600 hover:text-slate-700 font-medium"
>
</button>
</div>
<div className="space-y-4">
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider"></label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={14} />
<input
list="dl-region-customer"
type="text"
placeholder="搜索客户名称..."
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 pl-9 pr-3 text-xs focus:ring-2 focus:ring-slate-500/20 focus:border-slate-500 outline-none transition-all"
value={regionFilters.customer}
onChange={(e) => setRegionFilters(prev => ({ ...prev, customer: e.target.value }))}
/>
<datalist id="dl-region-customer">
{uniqueCustomerNames.map(c => <option key={c} value={c} />)}
</datalist>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider"></label>
<input
list="dl-region-region"
type="text"
placeholder="搜索或选择..."
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-slate-500/20 focus:border-slate-500 outline-none transition-all"
value={regionFilters.region}
onChange={(e) => setRegionFilters(prev => ({ ...prev, region: e.target.value }))}
/>
<datalist id="dl-region-region">
{uniqueRegions.map(r => <option key={r} value={r} />)}
</datalist>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider"></label>
<input
list="dl-region-city"
type="text"
placeholder="搜索或选择..."
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-slate-500/20 focus:border-slate-500 outline-none transition-all"
value={regionFilters.city}
onChange={(e) => setRegionFilters(prev => ({ ...prev, city: e.target.value }))}
/>
<datalist id="dl-region-city">
{uniqueCities.map(c => <option key={c} value={c} />)}
</datalist>
</div>
</div>
</div>
<div className="mt-6 pt-4 border-t border-gray-50">
<button
onClick={() => setIsRegionFilterOpen(false)}
className="w-full bg-slate-800 text-white py-2 rounded-lg text-xs font-bold hover:bg-slate-900 transition-colors shadow-lg shadow-slate-900/20"
>
</button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</div>
</div>
<div className="p-2">
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left border-collapse min-w-[1000px]">
<thead>
<tr className="bg-slate-50 text-slate-500 text-[11px] uppercase tracking-wider border-b border-slate-100">
<th className="p-2 font-semibold border-r border-slate-100 w-48"> / / </th>
<th className="p-2 font-semibold border-r border-slate-100 text-center w-24"></th>
<th className="p-2 font-semibold border-r border-slate-100 text-center w-24 text-green-600"></th>
<th className="p-2 font-semibold border-r border-slate-100 text-center w-24 text-orange-600"></th>
<th className="p-2 font-semibold text-center bg-slate-100/50 w-32"></th>
</tr>
</thead>
<tbody className="text-xs">
{regionData.filter(r => !regionFilters.region || r.region === regionFilters.region).map((r) => {
const isExpanded = expandedRegions.has(r.region);
return (
<React.Fragment key={r.region}>
<tr
className={`border-b border-slate-100 cursor-pointer transition-colors ${isExpanded ? 'bg-slate-50' : 'bg-white hover:bg-slate-50/50'}`}
onClick={() => toggleRegion(r.region)}
>
<td className="p-2 font-bold text-slate-700 flex items-center gap-2">
{isExpanded ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
<Truck size={14} className="text-slate-400" />
{r.region}
</td>
<td className="p-2 text-center font-bold text-slate-600 cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Operating' }); }}>{r.totalAssets}</td>
<td className="p-2 text-center text-green-600 font-bold cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Operating' }); }}>{r.operatingCount}</td>
<td className="p-2 text-center text-orange-600 font-bold cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); if (r.pendingCount) setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Pending' }); }}>{r.pendingCount || ''}</td>
<td className="p-2 text-center text-slate-500 font-medium">{r.customers.slice(0, 2).join(', ')}</td>
</tr>
{isExpanded && r.cities.map((city) => {
const cityKey = `${r.region}-${city.city}`;
const isCityExpanded = expandedRegionCities.has(cityKey);
return (
<React.Fragment key={city.city}>
<tr
className="border-b border-gray-50 hover:bg-gray-50 cursor-pointer"
onClick={() => toggleRegionCity(cityKey)}
>
<td className="p-2 pl-8 text-slate-600 flex items-center gap-2">
{isCityExpanded ? <ChevronDown size={12} className="text-slate-400" /> : <ChevronRight size={12} className="text-slate-400" />}
<MapPin size={12} className="text-slate-300" />
<span className="font-medium">{city.city}</span>
</td>
<td className="p-2 text-center text-slate-600 font-medium cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, category: 'Operating' }); }}>{city.totalAssets}</td>
<td className="p-2 text-center text-green-600 cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, category: 'Operating' }); }}>{city.operatingCount}</td>
<td className="p-2 text-center text-orange-600 cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); if (city.pendingCount) setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, category: 'Pending' }); }}>{city.pendingCount || ''}</td>
<td className="p-2 text-center text-slate-400 text-[10px] italic">{city.customers.slice(0, 2).join(', ')}</td>
</tr>
{isCityExpanded && city.typeBreakdown.map(tb => (
<tr key={tb.type} className="border-b border-gray-50/50 bg-slate-50/30">
<td className="p-2 pl-14 text-gray-400 flex items-center gap-2">
<div className="w-1 h-1 bg-slate-300 rounded-full"></div>
{tb.type}
</td>
<td className="p-2 text-center text-gray-500 cursor-pointer hover:underline" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, vehicleType: tb.type, category: 'Operating' })}>{tb.total}</td>
<td className="p-2 text-center text-green-500 cursor-pointer hover:underline" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, vehicleType: tb.type, category: 'Operating' })}>{tb.operating}</td>
<td className="p-2 text-center text-orange-500 cursor-pointer hover:underline" onClick={() => { if (tb.inventory) setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, vehicleType: tb.type, category: 'Pending' }); }}>{tb.inventory || ''}</td>
<td className="p-2 text-center text-gray-400 text-[10px] italic">{tb.customers.slice(0, 2).join(', ')}</td>
</tr>
))}
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
{/* Mobile View (Region) */}
<div className="lg:hidden p-2 space-y-3">
{regionData.filter(r => !regionFilters.region || r.region === regionFilters.region).map((r) => {
const isExpanded = expandedRegions.has(r.region);
return (
<div key={r.region} className="bg-slate-50/50 rounded-xl border border-slate-100 overflow-hidden">
<div
className="bg-white p-3 flex justify-between items-center cursor-pointer"
onClick={() => toggleRegion(r.region)}
>
<div className="flex items-center gap-2 font-bold text-slate-700">
{isExpanded ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
<Truck size={14} className="text-slate-400" />
{r.region}
</div>
<div className="text-xs font-bold text-slate-500">: {r.totalAssets}</div>
</div>
{isExpanded && (
<>
<div className="p-2 grid grid-cols-2 gap-2 text-center border-t border-slate-100">
<div
className="bg-white p-2 rounded border border-slate-100 cursor-pointer active:bg-green-50"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Operating', source: 'asset' })}
>
<div className="text-[9px] text-gray-400 uppercase"></div>
<div className="text-xs font-bold text-green-600">{r.operatingCount}</div>
</div>
<div
className="bg-white p-2 rounded border border-slate-100 cursor-pointer active:bg-orange-50"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Pending', source: 'asset' })}
>
<div className="text-[9px] text-gray-400 uppercase"></div>
<div className="text-xs font-bold text-orange-600">{r.pendingCount || ''}</div>
</div>
</div>
<div className="px-2 pb-2 space-y-1">
{r.typeBreakdown.map(tb => (
<div key={tb.type} className="flex justify-between items-center text-[10px] bg-white/80 px-2 py-1.5 rounded border border-slate-50">
<span className="text-gray-500">{tb.type} </span>
<div className="flex gap-3">
<span
className="font-bold text-green-600 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, vehicleType: tb.type, category: 'Operating', source: 'asset' })}
>
:{tb.operating}
</span>
<span
className="font-bold text-orange-600 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, vehicleType: tb.type, category: 'Pending', source: 'asset' })}
>
:{tb.inventory || ''}
</span>
</div>
</div>
))}
</div>
</>
)}
</div>
);
})}
</div>
</div>
</section>
</div>
)}
{activeTab === 'customer' && (
<div className="flex flex-col gap-6">
{/* Customer Region Distribution Chart */}
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4 sm:p-6">
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-3">
<div className="w-1.5 h-6 bg-emerald-500 rounded-full"></div>
<h2 className="text-lg font-bold text-gray-800"></h2>
</div>
<div className="flex items-center bg-gray-100 rounded-lg p-0.5">
<button
onClick={() => setCustomerChartView('region')}
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${customerChartView === 'region' ? 'bg-white text-emerald-600 shadow-sm' : 'text-gray-400 hover:text-gray-600'}`}
></button>
<button
onClick={() => setCustomerChartView('city')}
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${customerChartView === 'city' ? 'bg-white text-emerald-600 shadow-sm' : 'text-gray-400 hover:text-gray-600'}`}
></button>
</div>
</div>
{(() => {
const PIE_COLORS = ['#6366f1','#06b6d4','#f59e0b','#f43f5e','#10b981','#a855f7','#94a3b8'];
let pieData: { name: string; value: number }[] = [];
if (customerChartView === 'region') {
const map: { [k: string]: number } = {};
customerData.forEach(item => { map[item.region] = (map[item.region] || 0) + item.total; });
pieData = Object.entries(map).map(([name, value]) => ({ name, value })).sort((a,b) => b.value - a.value);
} else {
const map: { [k: string]: number } = {};
customerData.forEach(item => { map[item.city] = (map[item.city] || 0) + item.total; });
const tot = Object.values(map).reduce((a,b) => a+b, 0);
const threshold = tot * 0.05;
let other = 0;
Object.entries(map).forEach(([name, value]) => {
if (value >= threshold) pieData.push({ name, value });
else other += value;
});
if (other > 0) pieData.push({ name: '其他', value: other });
pieData.sort((a,b) => b.value - a.value);
}
const grandTotal = pieData.reduce((s,d) => s + d.value, 0);
return (
<div className="flex flex-col sm:flex-row gap-4 sm:gap-8 items-center">
{/* Donut chart */}
<div className="relative flex-shrink-0" style={{ width: 200, height: 200 }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={pieData}
cx="50%" cy="50%"
innerRadius={68} outerRadius={90}
paddingAngle={3}
startAngle={90} endAngle={-270}
dataKey="value"
>
{pieData.map((_, i) => (
<Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} stroke="white" strokeWidth={2} />
))}
</Pie>
<Tooltip
formatter={(value) => [`${value}`, '']}
contentStyle={{ borderRadius: '10px', border: 'none', boxShadow: '0 8px 24px -4px rgba(0,0,0,0.12)', fontSize: 12 }}
/>
</PieChart>
</ResponsiveContainer>
{/* Center label */}
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
<span className="text-2xl font-bold text-gray-800">{grandTotal}</span>
<span className="text-xs text-gray-400 mt-0.5"></span>
</div>
</div>
{/* Custom legend */}
<div className="flex-1 w-full space-y-2.5">
{pieData.map((item, i) => {
const pct = grandTotal > 0 ? (item.value / grandTotal * 100) : 0;
const color = PIE_COLORS[i % PIE_COLORS.length];
return (
<div key={i} className="flex items-center gap-2.5">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ background: color }} />
<span className="text-sm text-gray-600 flex-1 min-w-0 truncate">{item.name}</span>
<div className="w-20 h-1.5 bg-gray-100 rounded-full overflow-hidden flex-shrink-0">
<div className="h-full rounded-full transition-all" style={{ width: `${pct}%`, background: color }} />
</div>
<span className="text-xs font-semibold text-gray-700 w-6 text-right flex-shrink-0">{item.value}</span>
<span className="text-xs text-gray-400 w-9 text-right flex-shrink-0">{pct.toFixed(1)}%</span>
</div>
);
})}
</div>
</div>
);
})()}
</section>
{/* Customer Operations Statistics Section */}
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6">
<div className="p-3 sm:p-4 border-b border-gray-50 flex items-center justify-between bg-emerald-800 text-white relative">
<div className="flex items-center gap-3">
<div className="w-1.5 h-6 bg-emerald-400 rounded-full"></div>
<div>
<h2 className="text-lg font-bold"></h2>
<p className="text-[10px] opacity-70 font-medium">*</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setIsCustomerFilterOpen(!isCustomerFilterOpen)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
isCustomerFilterOpen || Object.values(customerFilters).some(v => v !== '')
? 'bg-emerald-400 text-emerald-900 shadow-lg shadow-emerald-900/20'
: 'bg-emerald-700/50 text-emerald-100 hover:bg-emerald-700'
}`}
>
<Filter size={14} />
<span></span>
{Object.values(customerFilters).some(v => v !== '') && (
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
)}
</button>
<AnimatePresence>
{isCustomerFilterOpen && (
<>
{/* Backdrop for closing */}
<div
className="fixed inset-0 z-40"
onClick={() => setIsCustomerFilterOpen(false)}
/>
{/* Popover Content */}
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
className="absolute top-full right-4 mt-2 w-72 bg-white rounded-xl shadow-2xl border border-gray-100 z-50 p-4 text-gray-800"
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-bold text-gray-900"></h3>
<button
onClick={() => setCustomerFilters({ customer: '', brand: '', department: '', manager: '', region: '' })}
className="text-[10px] text-emerald-600 hover:text-emerald-700 font-medium"
>
</button>
</div>
<div className="space-y-4">
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider"></label>
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" size={12} />
<input
list="dl-cust-customer"
type="text"
placeholder="搜索客户..."
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 pl-8 pr-2 text-xs focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none transition-all"
value={customerFilters.customer}
onChange={(e) => setCustomerFilters(prev => ({ ...prev, customer: e.target.value }))}
/>
<datalist id="dl-cust-customer">
{uniqueCustomerNames.map(c => <option key={c} value={c} />)}
</datalist>
</div>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider"></label>
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" size={12} />
<input
list="dl-cust-manager"
type="text"
placeholder="搜索负责人..."
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 pl-8 pr-2 text-xs focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none transition-all"
value={customerFilters.manager}
onChange={(e) => setCustomerFilters(prev => ({ ...prev, manager: e.target.value }))}
/>
<datalist id="dl-cust-manager">
{uniqueCustomerManagers.map(m => <option key={m} value={m} />)}
</datalist>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider"></label>
<input
list="dl-cust-brand"
type="text"
placeholder="搜索或选择..."
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none transition-all"
value={customerFilters.brand}
onChange={(e) => setCustomerFilters(prev => ({ ...prev, brand: e.target.value }))}
/>
<datalist id="dl-cust-brand">
{uniqueBrands.map(b => <option key={b} value={b} />)}
</datalist>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider"></label>
<input
list="dl-cust-dept"
type="text"
placeholder="搜索或选择..."
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none transition-all"
value={customerFilters.department}
onChange={(e) => setCustomerFilters(prev => ({ ...prev, department: e.target.value }))}
/>
<datalist id="dl-cust-dept">
{uniqueDepts.map(d => <option key={d} value={d} />)}
</datalist>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider"></label>
<input
list="dl-cust-region"
type="text"
placeholder="搜索或选择..."
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none transition-all"
value={customerFilters.region}
onChange={(e) => setCustomerFilters(prev => ({ ...prev, region: e.target.value }))}
/>
<datalist id="dl-cust-region">
{uniqueRegions.map(r => <option key={r} value={r} />)}
</datalist>
</div>
</div>
</div>
<div className="mt-6 pt-4 border-t border-gray-50">
<button
onClick={() => setIsCustomerFilterOpen(false)}
className="w-full bg-emerald-600 text-white py-2 rounded-lg text-xs font-bold hover:bg-emerald-700 transition-colors shadow-lg shadow-emerald-600/20"
>
</button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</div>
</div>
<div className="p-0 sm:p-2">
{/* Desktop Table View (Customer) */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left border-collapse min-w-[1000px]">
<thead>
<tr className="bg-emerald-700 text-white text-[11px] uppercase tracking-wider border-b border-emerald-800">
<th className="p-2 font-semibold border-r border-emerald-600 w-40"></th>
<th className="p-2 font-semibold border-r border-emerald-600 w-24"></th>
<th className="p-2 font-semibold border-r border-emerald-600 w-24"></th>
<th className="p-2 font-semibold border-r border-emerald-600 text-center w-20">4.5T</th>
<th className="p-2 font-semibold border-r border-emerald-600 text-center w-20">4.5T冷链</th>
<th className="p-2 font-semibold border-r border-emerald-600 text-center w-20">18T</th>
<th className="p-2 font-semibold border-r border-emerald-600 text-center w-20">49T</th>
<th className="p-2 font-semibold border-r border-emerald-600 text-center w-20"></th>
<th className="p-2 font-semibold border-r border-emerald-600 text-center w-20"></th>
<th className="p-2 font-semibold text-center bg-emerald-900/40 w-24"></th>
</tr>
</thead>
<tbody className="text-xs">
{filteredCustomerStats.map((cust) => {
const isExpanded = expandedCustomers.has(cust.customer);
return (
<React.Fragment key={cust.customer}>
<tr
className={`cursor-pointer transition-all border-b border-gray-100 ${
isExpanded ? 'bg-emerald-50/50' : 'hover:bg-gray-50'
}`}
onClick={() => toggleCustomer(cust.customer)}
>
<td className="p-2 border-r border-gray-100 font-bold text-gray-800 flex items-center gap-2">
{isExpanded ? <ChevronDown size={14} className="text-emerald-600" /> : <ChevronRight size={14} className="text-gray-400" />}
{cust.customer}
</td>
<td className="p-2 border-r border-gray-100 text-gray-600 text-center">
<span className="bg-gray-100 px-2 py-0.5 rounded text-[10px] font-medium">{cust.region}</span>
</td>
<td className="p-2 border-r border-gray-100 text-gray-600 text-center">{cust.manager}</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-500 cursor-pointer hover:bg-emerald-50 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '4.5T', source: 'customer' }); }}>{cust.t4_5}</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-500 cursor-pointer hover:bg-emerald-50 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '4.5T', source: 'customer' }); }}>{cust.t4_5c}</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-500 cursor-pointer hover:bg-emerald-50 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '18T', source: 'customer' }); }}>{cust.t18}</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-500 cursor-pointer hover:bg-emerald-50 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '49T', source: 'customer' }); }}>{cust.t49}</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-500 cursor-pointer hover:bg-emerald-50 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '其他车型', source: 'customer' }); }}>{cust.trailer}</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-500 cursor-pointer hover:bg-emerald-50 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '其他车型', source: 'customer' }); }}>{cust.other}</td>
<td className="p-2 text-center font-bold bg-emerald-50 text-emerald-800 cursor-pointer hover:bg-emerald-100 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, source: 'customer' }); }}>{cust.total}</td>
</tr>
{isExpanded && (
<tr className="bg-gray-50/30">
<td colSpan={9} className="p-2">
<div className="grid grid-cols-4 gap-2">
<div className="bg-white p-2 rounded border border-gray-100 shadow-sm">
<div className="text-[10px] text-gray-400 uppercase mb-1"></div>
<div className="text-sm font-bold text-gray-700">{cust.customer}</div>
</div>
<div className="bg-white p-2 rounded border border-gray-100 shadow-sm">
<div className="text-[10px] text-gray-400 uppercase mb-1"></div>
<div className="text-sm font-bold text-emerald-600">
{cust.t49 > cust.t18 ? '49T 重卡' : (cust.t18 > cust.t4_5c ? '18T 货车' : '4.5T 轻卡')}
</div>
</div>
<div className="bg-white p-2 rounded border border-gray-100 shadow-sm">
<div className="text-[10px] text-gray-400 uppercase mb-1"></div>
<div className="text-sm font-bold text-gray-700">{cust.manager}</div>
</div>
<div className="bg-white p-2 rounded border border-gray-100 shadow-sm">
<div className="text-[10px] text-gray-400 uppercase mb-1"></div>
<div className="text-sm font-bold text-gray-700">
{((cust.total / deptData.reduce((s, d) => s + d.totalAssets, 0)) * 100).toFixed(1)}%
</div>
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
{/* Mobile Card View (Customer) */}
<div className="lg:hidden p-2 space-y-2">
{filteredCustomerStats.map((cust) => {
const isExpanded = expandedCustomers.has(cust.customer);
return (
<div key={cust.customer} className="bg-white rounded border border-gray-100 shadow-sm overflow-hidden">
<div
className={`p-2 flex justify-between items-center cursor-pointer transition-colors ${
isExpanded ? 'bg-emerald-50' : 'bg-gray-50'
}`}
onClick={() => toggleCustomer(cust.customer)}
>
<div className="flex items-center gap-2">
{isExpanded ? <ChevronDown size={16} className="text-emerald-600" /> : <ChevronRight size={16} className="text-gray-400" />}
<div className="flex flex-col">
<span className="font-bold text-gray-800 text-sm">{cust.customer}</span>
<span className="text-[10px] text-emerald-600 font-medium">{cust.region}</span>
</div>
</div>
<div className="text-xs font-bold text-emerald-700 bg-emerald-100 px-2 py-0.5 rounded-full">
: {cust.total}
</div>
</div>
{isExpanded && (
<div className="p-2 space-y-3 bg-white">
{/* Details Cards for Mobile */}
<div className="grid grid-cols-2 gap-2">
<div className="bg-gray-50 p-2 rounded border border-gray-100">
<div className="text-[8px] text-gray-400 uppercase mb-1"></div>
<div className="text-[10px] font-bold text-gray-700">{cust.customer}</div>
</div>
<div className="bg-gray-50 p-2 rounded border border-gray-100">
<div className="text-[8px] text-gray-400 uppercase mb-1"></div>
<div className="text-xs font-bold text-emerald-600">
{cust.t49 > cust.t18 ? '49T 重卡' : (cust.t18 > cust.t4_5c ? '18T 货车' : '4.5T 轻卡')}
</div>
</div>
<div className="bg-gray-50 p-2 rounded border border-gray-100">
<div className="text-[8px] text-gray-400 uppercase mb-1"></div>
<div className="text-xs font-bold text-gray-700">{cust.manager}</div>
</div>
<div className="bg-gray-50 p-2 rounded border border-gray-100">
<div className="text-[8px] text-gray-400 uppercase mb-1"></div>
<div className="text-xs font-bold text-gray-700">
{((cust.total / deptData.reduce((s, d) => s + d.totalAssets, 0)) * 100).toFixed(1)}%
</div>
</div>
</div>
<div className="border-t border-gray-50 pt-2">
<div className="text-[8px] text-gray-400 uppercase mb-2"></div>
<div className="grid grid-cols-3 gap-2">
<div
className="text-center bg-gray-50 p-1 rounded cursor-pointer hover:bg-emerald-50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '4.5T', source: 'customer' })}
>
<div className="text-[8px] text-gray-400 uppercase">4.5T</div>
<div className="text-[10px] font-bold text-gray-600">{cust.t4_5}</div>
</div>
<div
className="text-center bg-gray-50 p-1 rounded cursor-pointer hover:bg-emerald-50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '4.5T', source: 'customer' })}
>
<div className="text-[8px] text-gray-400 uppercase"></div>
<div className="text-[10px] font-bold text-gray-600">{cust.t4_5c}</div>
</div>
<div
className="text-center bg-gray-50 p-1 rounded cursor-pointer hover:bg-emerald-50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '18T', source: 'customer' })}
>
<div className="text-[8px] text-gray-400 uppercase">18T</div>
<div className="text-[10px] font-bold text-gray-600">{cust.t18}</div>
</div>
<div
className="text-center bg-gray-50 p-1 rounded cursor-pointer hover:bg-emerald-50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '49T', source: 'customer' })}
>
<div className="text-[8px] text-gray-400 uppercase">49T</div>
<div className="text-[10px] font-bold text-gray-600">{cust.t49}</div>
</div>
<div
className="text-center bg-gray-50 p-1 rounded cursor-pointer hover:bg-emerald-50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '其他车型', source: 'customer' })}
>
<div className="text-[8px] text-gray-400 uppercase"></div>
<div className="text-[10px] font-bold text-gray-600">{cust.trailer}</div>
</div>
<div
className="text-center bg-gray-50 p-1 rounded cursor-pointer hover:bg-emerald-50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '其他车型', source: 'customer' })}
>
<div className="text-[8px] text-gray-400 uppercase"></div>
<div className="text-[10px] font-bold text-gray-600">{cust.other}</div>
</div>
</div>
</div>
</div>
)}
</div>
);
})}
</div>
</div>
</section>
</div>
)}
{/* Vehicle Detail Modal */}
<AnimatePresence>
{showPlateNumbers && (
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<motion.div
initial={{ scale: 0.9, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
className={`bg-white rounded-xl shadow-2xl w-full max-w-[95vw] ${showPlateNumbers.source === 'customer' ? 'lg:max-w-6xl' : 'lg:max-w-4xl'} overflow-hidden flex flex-col max-h-[85vh] sm:max-h-[90vh] min-h-[40vh]`}
>
<div className="p-4 border-b border-gray-100 flex justify-between items-center bg-slate-800 text-white shrink-0">
<div>
<h3 className="font-bold text-base flex items-center gap-2">
<Truck size={18} className="text-blue-400" />
{showPlateNumbers.title || (
(showPlateNumbers.manager ? `${showPlateNumbers.manager}${showPlateNumbers.type || ''}车辆` :
showPlateNumbers.customer ? `${showPlateNumbers.customer}${showPlateNumbers.type || ''}车辆` :
showPlateNumbers.batch === 'All' ? '全量批次' : `${showPlateNumbers.batch} 批次`) + ' - 运营明细'
)}
</h3>
<p className="text-[10px] opacity-60 mt-0.5">
{showPlateNumbers.model === 'All' ? '全量型号' : showPlateNumbers.model} |
{showPlateNumbers.category === 'Pending' ? '待交车' :
showPlateNumbers.category === 'Delivered' ? '本周已交车' :
showPlateNumbers.category === 'Returned' ? '已还车' :
showPlateNumbers.category === 'Replaced' ? '已替换' :
showPlateNumbers.category === 'Inventory' ? `${showPlateNumbers.location}库存` :
showPlateNumbers.category === 'Operating' ? '正在运营' : '全部状态'}
</p>
</div>
<button onClick={() => setShowPlateNumbers(null)} className="hover:bg-white/10 p-2 rounded-full transition-colors">
<PlusCircle className="rotate-45" size={24} />
</button>
</div>
{/* Modal Filters */}
<div className="px-4 py-2 bg-slate-50 border-b border-gray-200 shrink-0">
<div
className="flex justify-between items-center cursor-pointer py-1"
onClick={() => setIsModalFilterExpanded(!isModalFilterExpanded)}
>
<div className="flex items-center gap-2 text-slate-600">
<Filter size={14} />
<span className="text-xs font-bold"></span>
</div>
<div className="flex items-center gap-3">
{/* Quick Search always visible when collapsed */}
{!isModalFilterExpanded && (
<div className="relative w-40 sm:w-64" onClick={(e) => e.stopPropagation()}>
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
value={modalFilters.plateNumber}
onChange={(e) => 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"
/>
</div>
)}
<motion.div
animate={{ rotate: isModalFilterExpanded ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronDown size={16} className="text-slate-400" />
</motion.div>
</div>
</div>
<AnimatePresence>
{isModalFilterExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden"
>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 py-3 border-t border-gray-100 mt-1">
<div className="space-y-1">
<label className="block text-[10px] text-gray-500 font-medium"></label>
<div className="relative">
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" />
<input
list="dl-modal-plate"
type="text"
value={modalFilters.plateNumber}
onChange={(e) => 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"
/>
<datalist id="dl-modal-plate">
{uniqueModalPlates.map(p => <option key={p} value={p} />)}
</datalist>
</div>
</div>
<div className="space-y-1">
<label className="block text-[10px] text-gray-500 font-medium"></label>
<div className="relative">
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" />
<input
list="dl-modal-model"
type="text"
value={modalFilters.model}
onChange={(e) => 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"
/>
<datalist id="dl-modal-model">
{uniqueModalModels.map(m => <option key={m} value={m} />)}
</datalist>
</div>
</div>
<div className="space-y-1">
<label className="block text-[10px] text-gray-500 font-medium"></label>
<div className="relative">
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" />
<input
list="dl-modal-brand"
type="text"
value={modalFilters.brand}
onChange={(e) => 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"
/>
<datalist id="dl-modal-brand">
{uniqueModalBrands.map(b => <option key={b} value={b} />)}
</datalist>
</div>
</div>
<div className="space-y-1">
<label className="block text-[10px] text-gray-500 font-medium"></label>
<div className="relative">
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" />
<input
list="dl-modal-location"
type="text"
value={modalFilters.location}
onChange={(e) => 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"
/>
<datalist id="dl-modal-location">
{uniqueModalLocations.map(l => <option key={l} value={l} />)}
</datalist>
</div>
</div>
</div>
<div className="flex justify-end pb-2">
<button
onClick={() => setModalFilters({ plateNumber: '', model: '', brand: '', location: '' })}
className="text-[10px] text-blue-500 hover:text-blue-600 font-medium"
>
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<div className="flex-1 overflow-auto p-0 sm:p-4 bg-gray-50 min-h-0 overscroll-contain">
{modalLoading ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="animate-spin text-blue-500" size={32} />
</div>
) : modalWeeklyDetail.length > 0 ? (
<div className="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden w-full">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-700 text-white text-[10px] uppercase tracking-wider">
<th className="p-2 font-semibold border-r border-slate-600 text-center"></th>
<th className="p-2 font-semibold border-r border-slate-600 text-center"></th>
<th className="p-2 font-semibold text-center"></th>
</tr>
</thead>
<tbody className="text-[11px]">
{filteredModalWeeklyDetail.map((v, i) => (
<tr key={`${v.truck_id}-${i}`} className={`border-b border-gray-100 hover:bg-blue-50/50 transition-colors ${i % 2 === 0 ? 'bg-white' : 'bg-gray-50/30'}`}>
<td className="p-2 border-r border-gray-100 font-mono font-bold text-blue-700 text-center">{v.plate_number}</td>
<td className="p-2 border-r border-gray-100 text-gray-600 text-center">{v.customer_name || '—'}</td>
<td className="p-2 text-gray-500 text-center">{v.handover_date || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden w-full">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-700 text-white text-[10px] uppercase tracking-wider">
{showPlateNumbers.source === 'customer' ? (
<>
<th className="p-2 font-semibold border-r border-slate-600 sticky left-0 bg-slate-700 z-10 w-10 text-center"></th>
<th className="p-2 font-semibold border-r border-slate-600 w-24"></th>
<th className="p-2 font-semibold border-r border-slate-600 w-24"></th>
<th className="p-2 font-semibold border-r border-slate-600 w-16"></th>
<th className="p-2 font-semibold border-r border-slate-600 w-16"></th>
<th className="p-2 font-semibold border-r border-slate-600 w-48"></th>
<th className="p-2 font-semibold border-r border-slate-600 w-48"></th>
<th className="p-2 font-semibold border-r border-slate-600 w-24"></th>
<th className="p-2 font-semibold border-r border-slate-600 w-16 text-center"></th>
<th className="p-2 font-semibold border-r border-slate-600 w-24 text-center"></th>
<th className="p-2 font-semibold border-r border-slate-600 w-24 text-center"></th>
<th className="p-2 font-semibold border-r border-slate-600 w-24"></th>
<th className="p-2 font-semibold border-r border-slate-600 w-20 text-center"></th>
<th className="p-2 font-semibold w-48"></th>
</>
) : (
<>
<th className="p-2 font-semibold border-r border-slate-600 text-center"></th>
{showPlateNumbers.source !== 'asset' && (
<th className="p-2 font-semibold border-r border-slate-600 text-center"></th>
)}
<th className="p-2 font-semibold border-r border-slate-600 text-center"></th>
<th className="p-2 font-semibold border-r border-slate-600 text-center"></th>
<th className="p-2 font-semibold text-center"></th>
</>
)}
</tr>
</thead>
<tbody className="text-[11px]">
{filteredModalVehicles.map((v, idx) => (
<tr key={v.id} className={`border-b border-gray-100 hover:bg-blue-50/50 transition-colors ${idx % 2 === 0 ? 'bg-white' : 'bg-gray-50/30'}`}>
{showPlateNumbers.source === 'customer' ? (
<>
<td className="p-2 border-r border-gray-100 text-center sticky left-0 bg-inherit z-10 font-bold text-gray-400">{'—'}</td>
<td className="p-2 border-r border-gray-100 text-gray-600">{v.departmentName || '—'}</td>
<td className="p-2 border-r border-gray-100 font-medium text-gray-700">{v.customerManager || '—'}</td>
<td className="p-2 border-r border-gray-100 text-gray-600">{v.brandLabel || '—'}</td>
<td className="p-2 border-r border-gray-100 text-gray-600">{v.type}</td>
<td className="p-2 border-r border-gray-100 text-gray-500 text-[10px] leading-tight">{v.subjectOrg || '—'}</td>
<td className="p-2 border-r border-gray-100 font-bold text-gray-800">{v.customerName || '—'}</td>
<td className={`p-2 border-r border-gray-100 font-mono font-bold ${v.plateNumber ? 'text-blue-700' : 'text-orange-500'}`}>{v.plateNumber || v.vin || '—'}</td>
<td className="p-2 border-r border-gray-100 text-center">
<span className={`px-1.5 py-0.5 rounded-full text-[9px] font-bold ${
v.status === 'Operating' ? 'bg-green-100 text-green-700' :
v.status === 'Inventory' ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700'
}`}>
{v.status === 'Operating' ? '在租' : v.status === 'Inventory' ? '库存' : '异常'}
</span>
</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-500">{'—'}</td>
<td className="p-2 border-r border-gray-100 text-center text-gray-500">{'—'}</td>
<td className="p-2 border-r border-gray-100 text-gray-600">{v.location}</td>
<td className="p-2 border-r border-gray-100 text-center font-bold text-orange-600">{'—'}</td>
<td className="p-2 text-gray-500 text-[10px] leading-tight">{v.orgName || '—'}</td>
</>
) : (
<>
<td className={`p-2 border-r border-gray-100 font-mono font-bold text-center ${v.plateNumber ? 'text-blue-700' : 'text-orange-500'}`}>{v.plateNumber || v.vin || '—'}</td>
{showPlateNumbers.source !== 'asset' && (
<td className="p-2 border-r border-gray-100 font-bold text-gray-800 text-center">{v.customerName || '—'}</td>
)}
<td className="p-2 border-r border-gray-100 text-gray-600 text-center">{v.brandLabel || '—'}</td>
<td className="p-2 border-r border-gray-100 text-gray-600 text-center">{v.type}</td>
<td className="p-2 text-gray-600 text-center">{v.location}</td>
</>
)}
</tr>
))}
{filteredModalVehicles.length === 0 && (
<tr>
<td colSpan={showPlateNumbers.source === 'customer' ? 14 : (showPlateNumbers.source === 'asset' ? 4 : 5)} className="p-8 text-center text-gray-400 italic">
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</div>
<div className="p-4 bg-white border-t border-gray-100 flex justify-between items-center shrink-0">
<div className="text-xs text-gray-500">
<span className="font-bold text-blue-600">{filteredModalWeeklyDetail.length > 0 ? filteredModalWeeklyDetail.length : filteredModalVehicles.length}</span>
</div>
<button
onClick={() => setShowPlateNumbers(null)}
className="px-6 py-2 bg-slate-800 text-white rounded-lg text-xs font-bold hover:bg-slate-700 transition-colors shadow-sm"
>
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
{/* Footer / Navigation */}
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 p-2 flex justify-around items-center md:hidden z-40">
<button
onClick={() => setActiveTab('overview')}
className={`flex flex-col items-center ${activeTab === 'overview' ? 'text-blue-600' : 'text-gray-400'}`}
>
<Truck size={20} />
<span className="text-[10px] mt-1"></span>
</button>
<button
onClick={() => setActiveTab('department')}
className={`flex flex-col items-center ${activeTab === 'department' ? 'text-blue-600' : 'text-gray-400'}`}
>
<Users size={20} />
<span className="text-[10px] mt-1"></span>
</button>
<button
onClick={() => setActiveTab('region')}
className={`flex flex-col items-center ${activeTab === 'region' ? 'text-blue-600' : 'text-gray-400'}`}
>
<MapPin size={20} />
<span className="text-[10px] mt-1"></span>
</button>
<button
onClick={() => setActiveTab('customer')}
className={`flex flex-col items-center ${activeTab === 'customer' ? 'text-blue-600' : 'text-gray-400'}`}
>
<Building2 size={20} />
<span className="text-[10px] mt-1"></span>
</button>
</div>
</div>
);
}