feat: add state, data loading, and helpers for 3 new modules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-03-27 18:18:20 +08:00
parent 01a64431dc
commit b4fdbd14a7

View File

@@ -10,10 +10,13 @@ import {
ChevronRight,
Info,
Loader2,
Search,
Filter,
ArrowRightLeft,
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import type { SummaryData, TypeSummary, VehicleListItem } from './types';
import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail } from './api';
import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats } from './types';
import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats } from './api';
import type { WeeklyDetailItem } from './api';
export default function App() {
@@ -24,7 +27,12 @@ export default function App() {
batch: string;
model: string;
location: string;
category?: 'Inventory' | 'Pending' | 'Delivered' | 'Returned' | 'Replaced';
category?: 'Inventory' | 'Pending' | 'Delivered' | 'Returned' | 'Replaced' | 'Operating';
vehicleType?: string;
manager?: string;
customer?: string;
isColdChain?: boolean;
isTrailer?: boolean;
} | null>(null);
// Data state
@@ -37,16 +45,43 @@ export default function App() {
const [lastUpdate, setLastUpdate] = useState<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 [regionFilters, setRegionFilters] = useState({ region: '', city: '', customer: '' });
const [isRegionFilterOpen, setIsRegionFilterOpen] = useState(false);
// Customer section state
const [expandedCustomers, setExpandedCustomers] = useState<Set<string>>(new Set());
const [customerFilters, setCustomerFilters] = useState({ customer: '', brand: '', department: '', manager: '', region: '' });
const [isCustomerFilterOpen, setIsCustomerFilterOpen] = useState(false);
const loadData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const [s, byType] = await Promise.all([
const [s, byType, dept, region, cust] = await Promise.all([
fetchSummary(),
fetchByType(),
fetchDeptStats(),
fetchRegionStats(),
fetchCustomerStats(),
]);
setSummary(s);
setProcessedData(byType);
setDeptData(dept);
setRegionData(region);
setCustomerData(cust);
setLastUpdate(new Date().toLocaleString('zh-CN'));
} catch (e) {
setError(e instanceof Error ? e.message : '数据加载失败');
@@ -85,10 +120,16 @@ export default function App() {
// Normal vehicle list
setModalWeeklyDetail([]);
const params: Record<string, string> = {};
if (showPlateNumbers.vehicleType) params.vehicleType = showPlateNumbers.vehicleType;
if (showPlateNumbers.batch !== 'All') params.batch = showPlateNumbers.batch;
if (showPlateNumbers.model !== 'All') params.model = showPlateNumbers.model;
if (showPlateNumbers.location !== 'All') params.location = showPlateNumbers.location;
if (cat === 'Inventory') params.status = 'Inventory';
if (cat === 'Operating') params.category = 'Operating';
if (showPlateNumbers.manager) params.manager = showPlateNumbers.manager;
if (showPlateNumbers.customer) params.customer = showPlateNumbers.customer;
if (showPlateNumbers.isColdChain !== undefined) params.isColdChain = String(showPlateNumbers.isColdChain);
if (showPlateNumbers.isTrailer !== undefined) params.isTrailer = String(showPlateNumbers.isTrailer);
fetchVehicleList(params)
.then(setModalVehicles)
.catch(() => setModalVehicles([]))
@@ -119,7 +160,57 @@ export default function App() {
setExpandedModels(newSet);
};
const toggleDept = (dept: string) => {
const newSet = new Set(expandedDepts);
if (newSet.has(dept)) newSet.delete(dept);
else newSet.add(dept);
setExpandedDepts(newSet);
};
const toggleManagerDetails = (manager: string) => {
const newSet = new Set(expandedManagerDetails);
if (newSet.has(manager)) newSet.delete(manager);
else newSet.add(manager);
setExpandedManagerDetails(newSet);
};
const toggleRegion = (region: string) => {
const newSet = new Set(expandedRegions);
if (newSet.has(region)) newSet.delete(region);
else newSet.add(region);
setExpandedRegions(newSet);
};
const toggleCustomer = (customer: string) => {
const newSet = new Set(expandedCustomers);
if (newSet.has(customer)) newSet.delete(customer);
else newSet.add(customer);
setExpandedCustomers(newSet);
};
// Derived data for dept section
const allManagersList = deptData.flatMap((d) => d.managers.map((m) => m.manager)).filter((v, i, a) => a.indexOf(v) === i).sort();
const managerStats = deptData
.flatMap((d) => d.managers)
.filter((m) => selectedManager === 'All' || m.manager === selectedManager)
.sort((a, b) => b.total - a.total);
// Derived data for customer section
const filteredCustomerStats = customerData.filter((s) => {
const mc = !customerFilters.customer || s.customer.toLowerCase().includes(customerFilters.customer.toLowerCase());
const mb = !customerFilters.brand || s.brand === customerFilters.brand;
const md = !customerFilters.department || s.department === customerFilters.department;
const mm = !customerFilters.manager || s.manager.toLowerCase().includes(customerFilters.manager.toLowerCase());
const mr = !customerFilters.region || s.region === customerFilters.region;
return mc && mb && md && mm && mr;
});
const uniqueBrands = Array.from(new Set(customerData.map((s) => s.brand).filter(Boolean)));
const uniqueDepts = Array.from(new Set(customerData.map((s) => s.department).filter(Boolean)));
const uniqueRegions = Array.from(new Set(customerData.map((s) => s.region)));
const uniqueCities = Array.from(new Set(customerData.map((s) => s.city).filter(Boolean)));
// Derived data for region section
const filteredRegionData = regionData.filter((r) => !regionFilters.region || r.region === regionFilters.region);
if (loading && !summary) {
return (
@@ -346,17 +437,61 @@ export default function App() {
<tr className="bg-yellow-50/50 text-xs font-bold border-b border-gray-200">
<td className="p-3 border-r border-gray-100 text-gray-700"></td>
<td className="p-3 border-r border-gray-100"></td>
<td className="p-3 text-center border-r border-gray-100 text-gray-800">{processedData.reduce((s, t) => s + t.totalAssets, 0)}</td>
<td className="p-3 text-center border-r border-gray-100 text-blue-700">{processedData.reduce((s, t) => s + t.totalInventory, 0)}</td>
<td className="p-3 text-center border-r border-gray-100">
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All' })} className="text-gray-800 hover:text-blue-600 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.totalAssets, 0)}
</button>
</td>
<td className="p-3 text-center border-r border-gray-100">
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Inventory' })} className="text-blue-700 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.totalInventory, 0)}
</button>
</td>
{['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => {
const val = processedData.reduce((s, t) => s + (t.inventoryRegions?.[reg] || 0), 0);
return <td key={reg} className="p-3 text-center border-r border-gray-100 text-blue-700">{val || ''}</td>;
return (
<td key={reg} className="p-3 text-center border-r border-gray-100">
{val > 0 ? (
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: reg, category: 'Inventory' })} className="text-blue-700 hover:underline font-bold">
{val}
</button>
) : ''}
</td>
);
})}
<td className="p-3 text-center border-r border-gray-100 text-gray-700">{processedData.reduce((s, t) => s + t.pending, 0) || ''}</td>
<td className="p-3 text-center border-r border-gray-100 text-green-700 bg-green-50/10">{processedData.reduce((s, t) => s + t.totalOperating, 0)}</td>
<td className="p-3 text-center border-r border-gray-100 text-blue-700 bg-blue-50/5">{processedData.reduce((s, t) => s + t.weeklyDelivered, 0) || ''}</td>
<td className="p-3 text-center border-r border-gray-100 text-orange-700 bg-orange-50/5">{processedData.reduce((s, t) => s + t.weeklyReturned, 0) || ''}</td>
<td className="p-3 text-center text-purple-700 bg-purple-50/5">{processedData.reduce((s, t) => s + t.weeklyReplaced, 0) || ''}</td>
<td className="p-3 text-center border-r border-gray-100">
{processedData.reduce((s, t) => s + t.pending, 0) > 0 ? (
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Pending' })} className="text-gray-700 hover:text-blue-600 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.pending, 0)}
</button>
) : ''}
</td>
<td className="p-3 text-center border-r border-gray-100 bg-green-50/10">
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Operating' })} className="text-green-700 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.totalOperating, 0)}
</button>
</td>
<td className="p-3 text-center border-r border-gray-100 bg-blue-50/5">
{processedData.reduce((s, t) => s + t.weeklyDelivered, 0) > 0 ? (
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Delivered' })} className="text-blue-700 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.weeklyDelivered, 0)}
</button>
) : ''}
</td>
<td className="p-3 text-center border-r border-gray-100 bg-orange-50/5">
{processedData.reduce((s, t) => s + t.weeklyReturned, 0) > 0 ? (
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Returned' })} className="text-orange-700 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.weeklyReturned, 0)}
</button>
) : ''}
</td>
<td className="p-3 text-center bg-purple-50/5">
{processedData.reduce((s, t) => s + t.weeklyReplaced, 0) > 0 ? (
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Replaced' })} className="text-purple-700 hover:underline font-bold">
{processedData.reduce((s, t) => s + t.weeklyReplaced, 0)}
</button>
) : ''}
</td>
</tr>
</thead>
<tbody className="text-xs">
@@ -389,18 +524,58 @@ export default function App() {
</div>
</td>
<td className={`p-3 border-r border-gray-100 text-[11px] ${theme === 'vibrant' ? 'text-white/70' : 'text-gray-400'} italic`}></td>
<td className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white' : 'text-gray-700'}`}>{typeGroup.totalAssets}</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white' : 'text-blue-600'}`}>{typeGroup.totalInventory}</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white' : ''}`}>
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white' : 'text-gray-700 hover:text-blue-600'}`}>
{typeGroup.totalAssets}
</button>
</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white' : ''}`}>
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Inventory', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white' : 'text-blue-600'}`}>
{typeGroup.totalInventory}
</button>
</td>
{['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => (
<td key={reg} className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-blue-600'}`}>
{(typeGroup.inventoryRegions?.[reg] || 0) > 0 ? typeGroup.inventoryRegions[reg] : ''}
<td key={reg} className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white/80' : ''}`}>
{(typeGroup.inventoryRegions?.[reg] || 0) > 0 ? (
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: reg, category: 'Inventory', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-blue-600'}`}>
{typeGroup.inventoryRegions[reg]}
</button>
) : ''}
</td>
))}
<td className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white' : 'text-gray-600'}`}>{typeGroup.pending || ''}</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold bg-green-50/10 ${theme === 'vibrant' ? 'text-white' : 'text-green-600'}`}>{typeGroup.totalOperating}</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold bg-blue-50/5 ${theme === 'vibrant' ? 'text-white/80' : 'text-blue-600'}`}>{typeGroup.weeklyDelivered || ''}</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold bg-orange-50/5 ${theme === 'vibrant' ? 'text-white/80' : 'text-orange-600'}`}>{typeGroup.weeklyReturned || ''}</td>
<td className={`p-3 text-center font-bold bg-purple-50/5 ${theme === 'vibrant' ? 'text-white/80' : 'text-purple-600'}`}>{typeGroup.weeklyReplaced || ''}</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white' : ''}`}>
{typeGroup.pending > 0 ? (
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Pending', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white' : 'text-gray-600 hover:text-blue-600'}`}>
{typeGroup.pending}
</button>
) : ''}
</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold bg-green-50/10 ${theme === 'vibrant' ? 'text-white' : ''}`}>
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Operating', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white' : 'text-green-600'}`}>
{typeGroup.totalOperating}
</button>
</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold bg-blue-50/5 ${theme === 'vibrant' ? 'text-white/80' : ''}`}>
{typeGroup.weeklyDelivered > 0 ? (
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Delivered', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-blue-600'}`}>
{typeGroup.weeklyDelivered}
</button>
) : ''}
</td>
<td className={`p-3 text-center border-r border-gray-100 font-bold bg-orange-50/5 ${theme === 'vibrant' ? 'text-white/80' : ''}`}>
{typeGroup.weeklyReturned > 0 ? (
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Returned', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-orange-600'}`}>
{typeGroup.weeklyReturned}
</button>
) : ''}
</td>
<td className={`p-3 text-center font-bold bg-purple-50/5 ${theme === 'vibrant' ? 'text-white/80' : ''}`}>
{typeGroup.weeklyReplaced > 0 ? (
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Replaced', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-purple-600'}`}>
{typeGroup.weeklyReplaced}
</button>
) : ''}
</td>
</>
)}
</tr>
@@ -470,8 +645,11 @@ export default function App() {
''
)}
</td>
<td className="p-3 text-center border-r border-gray-100 text-green-600 font-bold bg-green-50/10">
{model.operating}
<td className="p-3 text-center border-r border-gray-100 font-bold bg-green-50/10">
<button
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Operating' }); }}
className="text-green-600 hover:underline font-bold"
>{model.operating}</button>
</td>
<td className="p-3 text-center border-r border-gray-100 text-blue-600 bg-blue-50/5">
{model.weeklyDelivered > 0 ? (
@@ -694,14 +872,16 @@ export default function App() {
<div className="p-4 border-b border-gray-100 flex justify-between items-center bg-blue-600 text-white">
<div>
<h3 className="font-bold text-sm">
{showPlateNumbers.batch === 'All' ? '全量' : showPlateNumbers.batch} -
{showPlateNumbers.vehicleType || (showPlateNumbers.model !== 'All' ? showPlateNumbers.model : '全量')} -
</h3>
<p className="text-[10px] opacity-80">
{showPlateNumbers.model === 'All' ? '全量型号' : showPlateNumbers.model} |{' '}
{showPlateNumbers.vehicleType ? (showPlateNumbers.model === 'All' ? '全量型号' : showPlateNumbers.model) : (showPlateNumbers.batch === 'All' ? '' : showPlateNumbers.batch)} |{' '}
{!showPlateNumbers.category
? '全部车辆'
: showPlateNumbers.category === 'Inventory'
? (showPlateNumbers.location === 'All' ? '库存' : `${showPlateNumbers.location}库存`)
: showPlateNumbers.category === 'Operating'
? '在运营'
: showPlateNumbers.category === 'Pending'
? '待交车'
: showPlateNumbers.category === 'Delivered'