Files
ln-bi/src/App.tsx
kkfluous e910deac51
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat: 按城市改为按省份,数据从realtime表province获取,展示前5
- 后端region-chart支持groupBy=province,从realtime表读取省份
- 区域柱状图和客户饼图"按城市"改为"按省份"
- 省份展示前5,其余合入"其他"
- 前端state类型从'city'改为'province'

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 09:26:54 +08:00

2876 lines
192 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, useMemo, useRef } 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';
// --- SearchSelect Component ---
function SearchSelect({ value, onChange, options, placeholder, className }: {
value: string;
onChange: (v: string) => void;
options: string[];
placeholder: string;
className?: string;
}) {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
const filtered = useMemo(() => {
if (!query) return options;
const q = query.toLowerCase();
return options.filter((o) => o.toLowerCase().includes(q));
}, [options, query]);
const displayValue = value || '';
return (
<div ref={ref} className="relative">
<div
className={`flex items-center bg-white border border-gray-200 rounded-lg shadow-sm cursor-pointer transition-all focus-within:ring-2 focus-within:ring-blue-500/20 focus-within:border-blue-500 ${className || 'text-xs py-1.5 px-2'}`}
onClick={() => setOpen(!open)}
>
<input
type="text"
className="flex-1 outline-none bg-transparent min-w-0 text-inherit"
placeholder={displayValue || placeholder}
value={open ? query : displayValue}
onChange={(e) => { setQuery(e.target.value); if (!open) setOpen(true); }}
onFocus={() => { setOpen(true); setQuery(''); }}
/>
<ChevronDown size={14} className={`text-gray-400 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`} />
</div>
{open && (
<div className="absolute z-50 left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-xl max-h-48 overflow-auto">
<div
className="px-2 py-1.5 text-xs text-gray-400 cursor-pointer hover:bg-gray-50"
onClick={() => { onChange(''); setQuery(''); setOpen(false); }}
>
{placeholder}
</div>
{filtered.map((o) => (
<div
key={o}
className={`px-2 py-1.5 text-xs cursor-pointer hover:bg-blue-50 transition-colors ${o === value ? 'bg-blue-50 text-blue-600 font-bold' : 'text-gray-700'}`}
onClick={() => { onChange(o); setQuery(''); setOpen(false); }}
>
{o}
</div>
))}
{filtered.length === 0 && (
<div className="px-2 py-3 text-xs text-gray-400 text-center"></div>
)}
</div>
)}
</div>
);
}
// --- 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 [tabReady, setTabReady] = useState(true);
const prevTabRef = useRef(activeTab);
useEffect(() => {
if (prevTabRef.current !== activeTab) {
setTabReady(false);
prevTabRef.current = activeTab;
const id = requestAnimationFrame(() => { setTabReady(true); });
return () => cancelAnimationFrame(id);
}
}, [activeTab]);
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;
department?: string;
attendance?: 'active' | 'idle';
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);
const [draftRegionFilters, setDraftRegionFilters] = useState({ region: '', city: '', customer: '' });
// 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);
const [draftCustomerFilters, setDraftCustomerFilters] = useState({ customer: '', brand: '', department: '', manager: '', region: '' });
// 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: '', type: '', model: '' });
const [isInventoryFilterOpen, setIsInventoryFilterOpen] = useState(false);
const [draftInventoryFilters, setDraftInventoryFilters] = useState({ region: '', city: '', brand: '', type: '', model: '' });
// Chart view states
const [customerChartView, setCustomerChartView] = useState<'region' | 'province'>('region');
const [regionChartView, setRegionChartView] = useState<'region' | 'province'>('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]);
// Re-fetch region data when filters change
useEffect(() => {
const hasFilter = regionFilters.customer || regionFilters.city || regionFilters.region;
if (hasFilter) {
fetchRegionStats({ customer: regionFilters.customer || undefined, city: regionFilters.city || undefined, region: regionFilters.region || undefined })
.then(setRegionData).catch(() => {});
} else {
// No filters: use data from the main loadData cycle
fetchRegionStats().then(setRegionData).catch(() => {});
}
}, [regionFilters]);
// Fetch region chart data when view changes
useEffect(() => {
fetchRegionChart(regionChartView, regionChartView === 'province' ? 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.category = 'Inventory';
if (cat === 'Operating') params.category = 'Operating';
if (showPlateNumbers.manager) params.manager = showPlateNumbers.manager;
if (showPlateNumbers.customer) params.customer = showPlateNumbers.customer;
if (showPlateNumbers.department) params.department = showPlateNumbers.department;
if (showPlateNumbers.attendance) params.attendance = showPlateNumbers.attendance;
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 = useMemo(() => 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 mt = !inventoryFilters.type || s.type === inventoryFilters.type;
const mm = !inventoryFilters.model || s.model === inventoryFilters.model;
return mr && mc && mb && mt && mm;
}), [inventoryData, inventoryFilters]);
const uniqueInventoryBrands = useMemo(() => Array.from(new Set(inventoryData.map((s) => s.brand).filter(Boolean))), [inventoryData]);
const uniqueInventoryRegions = useMemo(() => Array.from(new Set(inventoryData.map((s) => s.region))), [inventoryData]);
const uniqueInventoryCities = useMemo(() => Array.from(new Set(inventoryData.map((s) => s.city).filter(Boolean))), [inventoryData]);
const INVENTORY_TYPE_ORDER_LIST = ['4.5T普货', '4.5T冷链', '18T', '49T', '挂车', '其他'];
const uniqueInventoryTypes = useMemo(() => {
const types = Array.from(new Set(inventoryData.map((s) => s.type).filter(Boolean)));
return types.sort((a, b) => INVENTORY_TYPE_ORDER_LIST.indexOf(a) - INVENTORY_TYPE_ORDER_LIST.indexOf(b));
}, [inventoryData]);
const uniqueInventoryModelsForType = useMemo(() => {
const typeFilter = isInventoryFilterOpen ? draftInventoryFilters.type : inventoryFilters.type;
const source = typeFilter ? inventoryData.filter((s) => s.type === typeFilter) : inventoryData;
return Array.from(new Set(source.map((s) => s.model).filter(Boolean)));
}, [inventoryData, inventoryFilters.type, draftInventoryFilters.type, isInventoryFilterOpen]);
const inventoryByRegion = useMemo(() => {
const result: Record<string, Record<string, RegionalInventoryStats[]>> = {};
for (const s of filteredInventoryStats) {
if (!result[s.region]) result[s.region] = {};
if (!result[s.region][s.city]) result[s.region][s.city] = [];
result[s.region][s.city].push(s);
}
return result;
}, [filteredInventoryStats]);
const inventoryByModel = useMemo(() => {
const INVENTORY_TYPE_ORDER = ['4.5T普货', '4.5T冷链', '18T', '49T', '挂车', '其他'];
const raw: Record<string, Record<string, RegionalInventoryStats[]>> = {};
for (const s of filteredInventoryStats) {
if (!raw[s.type]) raw[s.type] = {};
if (!raw[s.type][s.model]) raw[s.type][s.model] = [];
raw[s.type][s.model].push(s);
}
const result: Record<string, Record<string, RegionalInventoryStats[]>> = {};
for (const t of INVENTORY_TYPE_ORDER) { if (raw[t]) result[t] = raw[t]; }
for (const t of Object.keys(raw)) { if (!result[t]) result[t] = raw[t]; }
return result;
}, [filteredInventoryStats]);
// Derived data for dept section
const allManagersList = useMemo(() => deptData.flatMap((d) => d.managers.map((m) => m.manager)).filter((v, i, a) => a.indexOf(v) === i).sort(), [deptData]);
const managersGroupedByDept = useMemo(() => deptData.map((d) => ({ department: d.department, managers: d.managers.map((m) => m.manager) })), [deptData]);
const managerStats = useMemo(() => deptData
.flatMap((d) => d.managers)
.filter((m) => selectedManager === 'All' || m.manager === selectedManager)
.sort((a, b) => b.total - a.total), [deptData, selectedManager]);
// Derived data for customer section
const filteredCustomerStats = useMemo(() => customerData.filter((s) => {
const mc = !customerFilters.customer || s.customer === customerFilters.customer;
const mb = !customerFilters.brand || s.brand === customerFilters.brand;
const md = !customerFilters.department || s.department === customerFilters.department;
const mm = !customerFilters.manager || s.manager === customerFilters.manager;
const mr = !customerFilters.region || s.region === customerFilters.region;
return mc && mb && md && mm && mr;
}), [customerData, customerFilters]);
const uniqueBrands = useMemo(() => Array.from(new Set(customerData.map((s) => s.brand).filter(Boolean))), [customerData]);
const uniqueDepts = useMemo(() => {
const numMap: Record<string, number> = { '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 };
const getOrder = (name: string) => { const m = name.match(/[一二三四五六七八九十]/); return m ? (numMap[m[0]] || 99) : 99; };
return Array.from(new Set(customerData.map((s) => s.department).filter(Boolean))).sort((a, b) => getOrder(a) - getOrder(b));
}, [customerData]);
const uniqueRegions = useMemo(() => Array.from(new Set(customerData.map((s) => s.region))), [customerData]);
const uniqueCities = useMemo(() => Array.from(new Set(customerData.map((s) => s.city).filter(Boolean))), [customerData]);
const uniqueCustomerNames = useMemo(() => Array.from(new Set(customerData.map((s) => s.customer).filter(Boolean))), [customerData]);
const uniqueCustomerManagers = useMemo(() => Array.from(new Set(customerData.map((s) => s.manager).filter(Boolean))), [customerData]);
const customerManagersGroupedByDept = useMemo(() => {
const numMap: Record<string, number> = { '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 };
const getDeptOrder = (name: string) => {
if (name === '公务车') return 100;
const m = name.match(/[一二三四五六七八九十]/);
return m ? (numMap[m[0]] || 99) : 99;
};
// Use deptData order as primary source (already sorted), fallback to customerData
const deptMap = new Map<string, Set<string>>();
// First add from deptData to get correct dept order and all managers
for (const d of deptData) {
if (!deptMap.has(d.department)) deptMap.set(d.department, new Set());
for (const m of d.managers) deptMap.get(d.department)!.add(m.manager);
}
// Then add any extra from customerData
for (const s of customerData) {
if (!s.manager || !s.department) continue;
if (!deptMap.has(s.department)) deptMap.set(s.department, new Set());
deptMap.get(s.department)!.add(s.manager);
}
return Array.from(deptMap.entries())
.sort((a, b) => getDeptOrder(a[0]) - getDeptOrder(b[0]))
.map(([dept, mgrs]) => ({ department: dept, managers: Array.from(mgrs) }));
}, [customerData, deptData]);
const uniqueInventoryModels = useMemo(() => Array.from(new Set(inventoryData.map((s) => s.model).filter(Boolean))), [inventoryData]);
const uniqueModalPlates = useMemo(() => Array.from(new Set(modalVehicles.map(v => v.plateNumber || v.vin).filter(Boolean))), [modalVehicles]);
const uniqueModalModels = useMemo(() => Array.from(new Set(modalVehicles.map(v => v.model).filter(Boolean))), [modalVehicles]);
const uniqueModalBrands = useMemo(() => Array.from(new Set(modalVehicles.map(v => v.brandLabel).filter((x): x is string => Boolean(x)))), [modalVehicles]);
const uniqueModalLocations = useMemo(() => Array.from(new Set(modalVehicles.map(v => v.location).filter(Boolean))), [modalVehicles]);
// Derived data for region section
// regionData is already filtered by backend based on regionFilters
// Filtered modal vehicles based on modal filters
const filteredModalVehicles = useMemo(() => modalVehicles.filter((v) => {
const mp = !modalFilters.plateNumber || (v.plateNumber || v.vin) === modalFilters.plateNumber;
const mm = !modalFilters.model || v.model === modalFilters.model;
const mb = !modalFilters.brand || v.brandLabel === modalFilters.brand;
const ml = !modalFilters.location || v.location === modalFilters.location;
return mp && mm && mb && ml;
}), [modalVehicles, modalFilters]);
const filteredModalWeeklyDetail = useMemo(() => modalWeeklyDetail.filter((v) => {
const mp = !modalFilters.plateNumber || v.plate_number === modalFilters.plateNumber;
return mp;
}), [modalWeeklyDetail, modalFilters.plateNumber]);
const [customerProvinceData, setCustomerProvinceData] = useState<{ name: string; value: number }[]>([]);
useEffect(() => {
if (customerChartView === 'province') {
fetchRegionChart('province', 5).then(setCustomerProvinceData).catch(() => setCustomerProvinceData([]));
}
}, [customerChartView]);
const customerPieData = useMemo(() => {
if (customerChartView === 'region') {
const map: Record<string, number> = {};
customerData.forEach(item => { map[item.region] = (map[item.region] || 0) + item.total; });
return Object.entries(map).map(([name, value]) => ({ name, value })).sort((a, b) => b.value - a.value);
} else {
return customerProvinceData;
}
}, [customerData, customerChartView, customerProvinceData]);
const watermarkText = useMemo(() => `羚牛氢能-${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, '-')}`, [lastUpdate]);
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!;
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="hidden sm:block 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>
{/* Main Content Area */}
<div className="flex flex-col gap-6">
{!tabReady && (
<div className="flex items-center justify-center py-20">
<Loader2 className="animate-spin text-blue-500" size={28} />
</div>
)}
{tabReady && activeTab === 'overview' && (
<>
{/* Header Summary - Ultra Compact */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-2 mb-2">
{/* Total Assets */}
<div className="bg-white p-2 rounded-sm border border-gray-100 shadow-sm flex items-center gap-2 cursor-pointer hover:bg-gray-50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', source: 'asset', title: '资产概览' })}>
<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 cursor-pointer hover:bg-blue-50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Operating', source: 'asset', title: '正在运营' })}>
<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 cursor-pointer hover:bg-gray-50 transition-colors"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Inventory', source: 'asset', title: '库存总数' })}>
<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', source: 'asset', title: '待交车' })}>
<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', source: 'asset', title: '本周交车' })}>
<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', source: 'asset', title: '本周还车' })}>
<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', source: 'asset', title: '本周替换' })}>
<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>
{/* Asset Summary Table */}
<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"></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>
</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 colSpan={14} className={`p-3 font-bold ${theme === 'vibrant' ? 'text-white' : 'text-blue-700'}`}>
<div className="flex items-center justify-between">
<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>
<div className={`flex gap-6 text-[11px] font-normal mr-4 ${theme === 'vibrant' ? 'text-white/80' : 'text-gray-500'}`}>
<span> <span className={theme === 'vibrant' ? 'font-bold text-white' : 'font-bold text-gray-700'}>{typeGroup.totalAssets}</span></span>
<span> <span className={theme === 'vibrant' ? 'font-bold text-white' : 'font-bold text-blue-600'}>{typeGroup.totalInventory}</span></span>
<span> <span className={theme === 'vibrant' ? 'font-bold text-white' : 'font-bold text-green-600'}>{typeGroup.totalOperating}</span></span>
</div>
</div>
</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 justify-between gap-2">
<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={expandedModels.has(model.model) ? 'font-bold text-blue-700' : ''}>{model.model}</span>
</div>
<div className="flex gap-3 text-[9px] font-normal text-gray-400">
<span> <span className="font-bold text-gray-600">{model.total}</span></span>
<span> <span className="font-bold text-blue-500">{model.inventory}</span></span>
<span> <span className="font-bold text-green-500">{model.operating}</span></span>
</div>
</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', source: 'asset', title: model.model });
}}
className="text-blue-500 hover:underline font-medium"
>
{model.total}
</button>
</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', category: 'Inventory', source: 'asset', title: `${model.model} - 库存` });
}}
className="text-blue-500 hover:underline font-medium"
>
{model.inventory}
</button>
</td>
{['嘉兴', '广东', '北京', '新疆', '其他'].map(reg => (
<td key={reg} className="p-3 text-center border-r border-gray-100">
{model.inventoryRegions[reg] > 0 ? (
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: model.model, location: reg, category: 'Inventory', source: 'asset', title: `${model.model} - ${reg} 库存` });
}}
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', source: 'asset', title: `${model.model} - 待交车` });
}}
className="text-blue-500 hover:underline font-medium"
>
{model.pending}
</button>
) : model.pending}
</td>
<td className="p-3 text-center border-r border-gray-100 text-green-600 font-bold bg-green-50/10">
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Operating', source: 'asset', title: `${model.model} - 在运营` });
}}
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', source: 'asset', title: `${model.model} - 本周交车` });
}}
className="text-blue-500 hover:underline font-medium"
>
{model.weeklyDelivered}
</button>
) : model.weeklyDelivered}
</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', source: 'asset', title: `${model.model} - 本周还车` });
}}
className="text-blue-500 hover:underline font-medium"
>
{model.weeklyReturned}
</button>
) : model.weeklyReturned}
</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', source: 'asset', title: `${model.model} - 本周替换` });
}}
className="text-blue-500 hover:underline font-medium"
>
{model.weeklyReplaced}
</button>
) : model.weeklyReplaced}
</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 cursor-pointer active:bg-blue-100" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', source: 'asset', title: model.model }); }}> {model.total}</span>
<span className="text-[10px] bg-orange-50 text-orange-600 px-1.5 py-0.5 rounded font-bold cursor-pointer active:bg-orange-100" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Inventory', source: 'asset', title: `${model.model} - 库存` }); }}> {model.inventory}</span>
<span className="text-[10px] bg-green-50 text-green-600 px-1.5 py-0.5 rounded font-bold cursor-pointer active:bg-green-100" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Operating', source: 'asset', title: `${model.model} - 在运营` }); }}> {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 ? (
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: model.model, location: reg, category: 'Inventory', 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 min-h-[420px]">
<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={() => { if (!isInventoryFilterOpen) setDraftInventoryFilters({...inventoryFilters}); 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.type || 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.type || inventoryFilters.model) && (
<span className="w-2 h-2 bg-white rounded-full animate-pulse"></span>
)}
</button>
<AnimatePresence>
{isInventoryFilterOpen && (
<>
<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="fixed inset-x-4 top-20 max-h-[80vh] overflow-auto sm:inset-auto sm:top-20 sm:right-4 sm: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>
<button onClick={() => setDraftInventoryFilters({ region: '', city: '', brand: '', type: '', model: '' })} className="text-[10px] text-blue-500 hover:underline"></button>
</div>
<div className="space-y-3 text-left">
<div>
<label className="text-[10px] text-slate-400 block mb-1"></label>
<select value={draftInventoryFilters.region} onChange={(e) => setDraftInventoryFilters({...draftInventoryFilters, 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 cursor-pointer">
<option value=""></option>
{uniqueInventoryRegions.map(r => <option key={r} value={r}>{r}</option>)}
</select>
</div>
<div>
<label className="text-[10px] text-slate-400 block mb-1"></label>
<select value={draftInventoryFilters.city} onChange={(e) => setDraftInventoryFilters({...draftInventoryFilters, 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 cursor-pointer">
<option value=""></option>
{uniqueInventoryCities.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div>
<label className="text-[10px] text-slate-400 block mb-1"></label>
<select value={draftInventoryFilters.brand} onChange={(e) => setDraftInventoryFilters({...draftInventoryFilters, 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 cursor-pointer">
<option value=""></option>
{uniqueInventoryBrands.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</div>
<div>
<label className="text-[10px] text-slate-400 block mb-1"></label>
<select value={draftInventoryFilters.type} onChange={(e) => setDraftInventoryFilters({...draftInventoryFilters, type: e.target.value, model: ''})} 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 cursor-pointer">
<option value=""></option>
{uniqueInventoryTypes.map(t => <option key={t} value={t}>{t}</option>)}
</select>
</div>
<div>
<label className="text-[10px] text-slate-400 block mb-1"></label>
<select value={draftInventoryFilters.model} onChange={(e) => setDraftInventoryFilters({...draftInventoryFilters, model: 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 cursor-pointer">
<option value=""></option>
{uniqueInventoryModelsForType.map(m => <option key={m} value={m}>{m}</option>)}
</select>
</div>
</div>
<button onClick={() => { setInventoryFilters({...draftInventoryFilters}); setIsInventoryFilterOpen(false); }} className="w-full mt-4 py-2 bg-blue-600 text-white rounded-lg text-xs font-bold hover:bg-blue-700 transition-colors"></button>
</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.type || inventoryFilters.model) && (
<div className="px-4 py-2 border-b border-gray-100 flex flex-wrap gap-2 items-center">
{inventoryFilters.region && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-[10px] flex items-center gap-1">
: {inventoryFilters.region}
<button onClick={() => setInventoryFilters({...inventoryFilters, region: ''})} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{inventoryFilters.city && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-[10px] flex items-center gap-1">
: {inventoryFilters.city}
<button onClick={() => setInventoryFilters({...inventoryFilters, city: ''})} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{inventoryFilters.brand && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-[10px] flex items-center gap-1">
: {inventoryFilters.brand}
<button onClick={() => setInventoryFilters({...inventoryFilters, brand: ''})} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{inventoryFilters.type && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-[10px] flex items-center gap-1">
: {inventoryFilters.type}
<button onClick={() => setInventoryFilters({...inventoryFilters, type: '', model: ''})} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{inventoryFilters.model && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-[10px] flex items-center gap-1">
: {inventoryFilters.model}
<button onClick={() => setInventoryFilters({...inventoryFilters, model: ''})} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
<button onClick={() => setInventoryFilters({ region: '', city: '', brand: '', type: '', model: '' })} className="text-[11px] text-red-500 font-bold ml-auto hover:text-red-600"></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>
</>
)}
{tabReady && activeTab === 'department' && (
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6">
<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 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', source: 'department', title: '部门运营统计' })}>
<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 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', attendance: 'active', source: 'department', title: '部门运营统计 - 出勤车辆' })}>
<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 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', attendance: 'idle', source: 'department', title: '部门运营统计 - 闲置车辆' })}>
<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 pointer-events-none" size={14} />
<select
value={selectedManager}
onChange={(e) => setSelectedManager(e.target.value)}
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 appearance-none cursor-pointer font-bold text-gray-700"
>
<option value="All"></option>
{managersGroupedByDept.filter(g => g.department !== '公务车').map(g => (
<optgroup key={g.department} label={g.department}>
{g.managers.map(m => <option key={m} value={m}>{m}</option>)}
</optgroup>
))}
</select>
<ChevronDown className="absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none" size={14} />
</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-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">
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, source: 'department', title: `部门运营统计 - ${dept.department}` });
}}
className="text-gray-800 hover:underline font-black"
>
{dept.totalAssets}
</button>
</td>
<td className="p-2 border-r border-gray-100 text-center font-black text-green-500 text-sm">
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, attendance: 'active', source: 'department', title: `部门运营统计 - ${dept.department} - 出勤车辆` }); }} className="text-green-500 hover:underline font-black">{dept.operatingCount}</button>
</td>
<td className="p-2 border-r border-gray-100 text-center font-black text-gray-400 text-sm">
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, attendance: 'idle', source: 'department', title: `部门运营统计 - ${dept.department} - 闲置车辆` }); }} className="text-gray-400 hover:underline font-black">{dept.idleCount}</button>
</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={6} 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', title: `部门运营统计 - ${m.manager} - 正在运营` });
}}
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', title: `部门运营统计 - ${m.manager} - 4.5T` })}
>
<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', title: `部门运营统计 - ${m.manager} - 4.5T冷链` })}
>
<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', title: `部门运营统计 - ${m.manager} - 18T` })}
>
<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', title: `部门运营统计 - ${m.manager} - 49T` })}
>
<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', title: `部门运营统计 - ${m.manager} - 挂车` })}
>
<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', title: `部门运营统计 - ${m.manager} - 其他` })}
>
<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', title: `部门运营统计 - ${m.manager} - 正在运营` });
}}
>
{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', title: `部门运营统计 - ${m.manager} - 正在运营` });
}}
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', title: `部门运营统计 - ${m.manager} - 4.5T` })}>
<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', title: `部门运营统计 - ${m.manager} - 4.5T冷链` })}>
<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', title: `部门运营统计 - ${m.manager} - 18T` })}>
<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', title: `部门运营统计 - ${m.manager} - 49T` })}>
<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', title: `部门运营统计 - ${m.manager} - 挂车` })}>
<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', title: `部门运营统计 - ${m.manager} - 其他` })}>
<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-3 gap-2">
<div className="text-center cursor-pointer hover:bg-gray-50 rounded p-1"
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, source: 'department', title: `部门运营统计 - ${dept.department}` }); }}>
<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 cursor-pointer hover:bg-green-50 rounded p-1"
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, attendance: 'active', source: 'department', title: `部门运营统计 - ${dept.department} - 出勤车辆` }); }}>
<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 cursor-pointer hover:bg-gray-100 rounded p-1"
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, attendance: 'idle', source: 'department', title: `部门运营统计 - ${dept.department} - 闲置车辆` }); }}>
<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', title: `部门运营统计 - ${m.manager} - 正在运营` });
}}
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', title: `部门运营统计 - ${m.manager} - 4.5T` })}
>
<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', title: `部门运营统计 - ${m.manager} - 4.5T冷链` })}
>
<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', title: `部门运营统计 - ${m.manager} - 18T` })}
>
<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', title: `部门运营统计 - ${m.manager} - 49T` })}
>
<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', title: `部门运营统计 - ${m.manager} - 挂车` })}
>
<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', title: `部门运营统计 - ${m.manager} - 其他` })}
>
<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', title: `部门运营统计 - ${m.manager} - 正在运营` });
}}
>
: {m.total}
</div>
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department', title: `部门运营统计 - ${m.manager} - 正在运营` });
}}
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', title: `部门运营统计 - ${m.manager} - 4.5T` })}
>
<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', title: `部门运营统计 - ${m.manager} - 4.5T冷链` })}
>
<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', title: `部门运营统计 - ${m.manager} - 18T` })}
>
<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', title: `部门运营统计 - ${m.manager} - 49T` })}
>
<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', title: `部门运营统计 - ${m.manager} - 挂车` })}
>
<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', title: `部门运营统计 - ${m.manager} - 其他` })}
>
<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>
)}
{tabReady && 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('province')}
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${regionChartView === 'province' ? 'bg-white text-blue-700 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
></button>
</div>
</div>
<div className="h-64 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 === 'province' ? 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 min-h-[420px]">
<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={() => { if (!isRegionFilterOpen) setDraftRegionFilters({...regionFilters}); 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" />
<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="fixed inset-x-4 top-20 max-h-[80vh] overflow-auto sm:inset-auto sm:top-20 sm:right-4 sm: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={() => setDraftRegionFilters({ 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>
<SearchSelect value={draftRegionFilters.customer} onChange={(v) => setDraftRegionFilters(prev => ({ ...prev, customer: v }))} options={uniqueCustomerNames} placeholder="所有客户" className="text-xs py-2 px-2" />
</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>
<select className="w-full bg-white 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 cursor-pointer shadow-sm" value={draftRegionFilters.region} onChange={(e) => setDraftRegionFilters(prev => ({ ...prev, region: e.target.value }))}>
<option value=""></option>
{uniqueRegions.map(r => <option key={r} value={r}>{r}</option>)}
</select>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider"></label>
<select className="w-full bg-white 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 cursor-pointer shadow-sm" value={draftRegionFilters.city} onChange={(e) => setDraftRegionFilters(prev => ({ ...prev, city: e.target.value }))}>
<option value=""></option>
{uniqueCities.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
</div>
</div>
<button onClick={() => { setRegionFilters({...draftRegionFilters}); setIsRegionFilterOpen(false); }} className="w-full mt-4 py-2 bg-slate-800 text-white rounded-lg text-xs font-bold hover:bg-slate-900 transition-colors"></button>
</motion.div>
</>
)}
</AnimatePresence>
</div>
</div>
{Object.values(regionFilters).some(v => v !== '') && (
<div className="px-3 py-2 border-b border-gray-100 flex flex-wrap gap-2 items-center">
{regionFilters.customer && (
<span className="px-2 py-0.5 bg-slate-100 text-slate-700 rounded text-[10px] flex items-center gap-1">
: {regionFilters.customer}
<button onClick={() => setRegionFilters(prev => ({...prev, customer: ''}))} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{regionFilters.region && (
<span className="px-2 py-0.5 bg-slate-100 text-slate-700 rounded text-[10px] flex items-center gap-1">
: {regionFilters.region}
<button onClick={() => setRegionFilters(prev => ({...prev, region: ''}))} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{regionFilters.city && (
<span className="px-2 py-0.5 bg-slate-100 text-slate-700 rounded text-[10px] flex items-center gap-1">
: {regionFilters.city}
<button onClick={() => setRegionFilters(prev => ({...prev, city: ''}))} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
<button onClick={() => setRegionFilters({ region: '', city: '', customer: '' })} className="text-[11px] text-red-500 font-bold ml-auto hover:text-red-600"></button>
</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.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, source: 'region', title: `区域运营统计 - ${r.region}` }); }}>{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', source: 'region', title: `区域运营统计 - ${r.region} - 正在运营` }); }}>{r.operatingCount}</td>
<td className="p-2 text-center text-orange-600 font-bold cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Pending', source: 'region', title: `区域运营统计 - ${r.region} - 待交车` }); }}>{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, source: 'region', title: `区域运营统计 - ${city.city}` }); }}>{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', source: 'region', title: `区域运营统计 - ${city.city} - 正在运营` }); }}>{city.operatingCount}</td>
<td className="p-2 text-center text-orange-600 cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, category: 'Pending', source: 'region', title: `区域运营统计 - ${city.city} - 待交车` }); }}>{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, source: 'region', title: `区域运营统计 - ${city.city} - ${tb.type}` })}>{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', source: 'region', title: `区域运营统计 - ${city.city} - ${tb.type} - 正在运营` })}>{tb.operating}</td>
<td className="p-2 text-center text-orange-500 cursor-pointer hover:underline" onClick={() => { setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, vehicleType: tb.type, category: 'Inventory', source: 'region', title: `区域运营统计 - ${city.city} - ${tb.type} - 库存` }); }}>{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.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: 'region', title: `区域运营统计 - ${r.region} - 正在运营` })}
>
<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: 'region', title: `区域运营统计 - ${r.region} - 待交车` })}
>
<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: 'region', title: `区域运营统计 - ${r.region} - ${tb.type} - 正在运营` })}
>
:{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: 'Inventory', source: 'region', title: `区域运营统计 - ${r.region} - ${tb.type} - 库存` })}
>
:{tb.inventory}
</span>
</div>
</div>
))}
</div>
</>
)}
</div>
);
})}
</div>
</div>
</section>
</div>
)}
{tabReady && 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('province')}
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${customerChartView === 'province' ? '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'];
const pieData = customerPieData;
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 min-h-[420px]">
<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={() => { if (!isCustomerFilterOpen) setDraftCustomerFilters({...customerFilters}); 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 */}
<div className="fixed inset-0 z-40" />
{/* 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="fixed inset-x-4 top-20 max-h-[80vh] overflow-auto sm:inset-auto sm:top-20 sm:right-4 sm: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>
<div className="flex items-center gap-2">
<button onClick={() => setDraftCustomerFilters({ customer: '', brand: '', department: '', manager: '', region: '' })} className="text-[10px] text-emerald-600 hover:text-emerald-700 font-medium"></button>
</div>
</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>
<SearchSelect value={draftCustomerFilters.customer} onChange={(v) => setDraftCustomerFilters(prev => ({ ...prev, customer: v }))} options={uniqueCustomerNames} placeholder="所有客户" className="text-xs py-2 px-2" />
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider"></label>
<select className="w-full bg-white 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 cursor-pointer shadow-sm" value={draftCustomerFilters.manager} onChange={(e) => setDraftCustomerFilters(prev => ({ ...prev, manager: e.target.value }))}>
<option value=""></option>
{customerManagersGroupedByDept
.filter(g => g.department !== '公务车')
.filter(g => !draftCustomerFilters.department || g.department === draftCustomerFilters.department)
.map(g => (
<optgroup key={g.department} label={g.department}>
{g.managers.map(m => <option key={m} value={m}>{m}</option>)}
</optgroup>
))}
</select>
</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>
<select className="w-full bg-white 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 cursor-pointer shadow-sm" value={draftCustomerFilters.brand} onChange={(e) => setDraftCustomerFilters(prev => ({ ...prev, brand: e.target.value }))}>
<option value=""></option>
{uniqueBrands.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider"></label>
<select className="w-full bg-white 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 cursor-pointer shadow-sm" value={draftCustomerFilters.department} onChange={(e) => setDraftCustomerFilters(prev => ({ ...prev, department: e.target.value, manager: '' }))}>
<option value=""></option>
{uniqueDepts.map(d => <option key={d} value={d}>{d}</option>)}
</select>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider"></label>
<select className="w-full bg-white 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 cursor-pointer shadow-sm" value={draftCustomerFilters.region} onChange={(e) => setDraftCustomerFilters(prev => ({ ...prev, region: e.target.value }))}>
<option value=""></option>
{uniqueRegions.map(r => <option key={r} value={r}>{r}</option>)}
</select>
</div>
</div>
</div>
<button onClick={() => { setCustomerFilters({...draftCustomerFilters}); setIsCustomerFilterOpen(false); }} className="w-full mt-4 py-2 bg-emerald-600 text-white rounded-lg text-xs font-bold hover:bg-emerald-700 transition-colors"></button>
</motion.div>
</>
)}
</AnimatePresence>
</div>
</div>
{Object.values(customerFilters).some(v => v !== '') && (
<div className="px-3 py-2 border-b border-gray-100 flex flex-wrap gap-2 items-center">
{customerFilters.customer && (
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded text-[10px] flex items-center gap-1">
: {customerFilters.customer}
<button onClick={() => setCustomerFilters(prev => ({...prev, customer: ''}))} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{customerFilters.manager && (
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded text-[10px] flex items-center gap-1">
: {customerFilters.manager}
<button onClick={() => setCustomerFilters(prev => ({...prev, manager: ''}))} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{customerFilters.brand && (
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded text-[10px] flex items-center gap-1">
: {customerFilters.brand}
<button onClick={() => setCustomerFilters(prev => ({...prev, brand: ''}))} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{customerFilters.department && (
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded text-[10px] flex items-center gap-1">
: {customerFilters.department}
<button onClick={() => setCustomerFilters(prev => ({...prev, department: ''}))} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{customerFilters.region && (
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded text-[10px] flex items-center gap-1">
: {customerFilters.region}
<button onClick={() => setCustomerFilters(prev => ({...prev, region: ''}))} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
<button onClick={() => setCustomerFilters({ customer: '', brand: '', department: '', manager: '', region: '' })} className="text-[11px] text-red-500 font-bold ml-auto hover:text-red-600"></button>
</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', isColdChain: false, source: 'customer', title: `客户运营统计 - ${cust.customer} - 4.5T` }); }}>{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', isColdChain: true, source: 'customer', title: `客户运营统计 - ${cust.customer} - 4.5T冷链` }); }}>{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', title: `客户运营统计 - ${cust.customer} - 18T` }); }}>{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', title: `客户运营统计 - ${cust.customer} - 49T` }); }}>{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', title: `客户运营统计 - ${cust.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', title: `客户运营统计 - ${cust.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', title: `客户运营统计 - ${cust.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 cursor-pointer hover:bg-emerald-200 transition-colors"
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, source: 'customer', title: `客户运营统计 - ${cust.customer}` }); }}
>
: {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', isColdChain: false, source: 'customer', title: `客户运营统计 - ${cust.customer} - 4.5T` })}
>
<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', isColdChain: true, source: 'customer', title: `客户运营统计 - ${cust.customer} - 4.5T冷链` })}
>
<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', title: `客户运营统计 - ${cust.customer} - 18T` })}
>
<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', title: `客户运营统计 - ${cust.customer} - 49T` })}
>
<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', title: `客户运营统计 - ${cust.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', title: `客户运营统计 - ${cust.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="w-40 sm:w-64" onClick={(e) => e.stopPropagation()}>
<SearchSelect value={modalFilters.plateNumber} onChange={(v) => setModalFilters({...modalFilters, plateNumber: v})} options={uniqueModalPlates} placeholder="快速搜索车牌..." className="text-[11px] py-1 px-2" />
</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>
<SearchSelect value={modalFilters.plateNumber} onChange={(v) => setModalFilters({...modalFilters, plateNumber: v})} options={uniqueModalPlates} placeholder="全部车牌" className="text-[11px] py-1.5 px-2" />
</div>
<div className="space-y-1">
<label className="block text-[10px] text-gray-500 font-medium"></label>
<select value={modalFilters.model} onChange={(e) => setModalFilters({...modalFilters, model: e.target.value})} className="w-full text-[11px] px-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 cursor-pointer">
<option value=""></option>
{uniqueModalModels.map(m => <option key={m} value={m}>{m}</option>)}
</select>
</div>
<div className="space-y-1">
<label className="block text-[10px] text-gray-500 font-medium"></label>
<select value={modalFilters.brand} onChange={(e) => setModalFilters({...modalFilters, brand: e.target.value})} className="w-full text-[11px] px-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 cursor-pointer">
<option value=""></option>
{uniqueModalBrands.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</div>
<div className="space-y-1">
<label className="block text-[10px] text-gray-500 font-medium"></label>
<select value={modalFilters.location} onChange={(e) => setModalFilters({...modalFilters, location: e.target.value})} className="w-full text-[11px] px-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 cursor-pointer">
<option value=""></option>
{uniqueModalLocations.map(l => <option key={l} value={l}>{l}</option>)}
</select>
</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 sticky top-0 z-20 shadow-sm">
<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 w-full">
<table className="min-w-full w-max text-left border-collapse">
<thead className="sticky top-0 z-20 shadow-sm">
<tr className="bg-slate-700 text-white text-[10px] uppercase tracking-wider whitespace-nowrap">
{showPlateNumbers.source === 'customer' ? (
<>
<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' && showPlateNumbers.category !== 'Inventory' && (
<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 whitespace-nowrap ${idx % 2 === 0 ? 'bg-white' : 'bg-gray-50/30'}`}>
{showPlateNumbers.source === 'customer' ? (
<>
<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]">{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 === '其他' ? '对接中' : 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]">{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' && showPlateNumbers.category !== 'Inventory' && (
<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 === '其他' ? '对接中' : v.location}</td>
</>
)}
</tr>
))}
{filteredModalVehicles.length === 0 && (
<tr>
<td colSpan={showPlateNumbers.source === 'customer' ? 13 : ((showPlateNumbers.source === 'asset' || showPlateNumbers.category === 'Inventory') ? 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>
);
}