# Three Operations Modules Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add department, region, and customer operations statistics sections to the dashboard, ported from lnoneos prototype with real MySQL data. **Architecture:** All 3 new API endpoints aggregate from the existing `getVehicles()` cache (no new DB queries). A new macro-region mapping function converts province/city to 华东/华南/etc. The frontend adds 3 new collapsible sections below the existing asset summary table, each with desktop table + mobile card views. The existing vehicle list modal is extended with new filter params (manager, customer, isColdChain, isTrailer). **Tech Stack:** Hono (backend), React + Tailwind CSS + Motion (frontend), TypeScript throughout. **Reference:** lnoneos prototype at `/Users/kkfluous/Projects/ai-coding/lnoneos/src/App.tsx` --- ### Task 1: Backend — Macro-region mapping + vehicle type classification helpers **Files:** - Modify: `src/server/routes/vehicles.ts` (add functions after line ~111) - [ ] **Step 1: Add macro-region mapping function** Add after `mapInventoryRegion` (line 111) in `src/server/routes/vehicles.ts`: ```typescript // Macro-region mapping: province/city -> 华东/华南/华北/华中/西南/西北/其他 function mapMacroRegion(province: string | null, city: string | null): string { const prov = (province || '').trim(); const c = (city || '').trim(); const loc = prov + c; // 华东: 上海/江苏/浙江/安徽/福建/江西/山东 if (/上海|江苏|浙江|安徽|福建|江西|山东|南京|杭州|合肥|济南|青岛|苏州|宁波|厦门|嘉兴|无锡/.test(loc)) return '华东'; // 华南: 广东/广西/海南 if (/广东|广西|海南|广州|深圳|佛山|东莞|珠海|惠州|中山|南宁/.test(loc)) return '华南'; // 华北: 北京/天津/河北/山西/内蒙古 if (/北京|天津|河北|山西|内蒙古|石家庄|太原|呼和浩特/.test(loc)) return '华北'; // 华中: 河南/湖北/湖南 if (/河南|湖北|湖南|郑州|武汉|长沙/.test(loc)) return '华中'; // 西南: 重庆/四川/贵州/云南/西藏 if (/重庆|四川|贵州|云南|西藏|成都|昆明|贵阳/.test(loc)) return '西南'; // 西北: 陕西/甘肃/青海/宁夏/新疆 if (/陕西|甘肃|青海|宁夏|新疆|西安|兰州|乌鲁木齐/.test(loc)) return '西北'; return '其他'; } // Vehicle type classification for per-type counts type VehicleTypeCounts = { t4_5: number; t4_5c: number; t18: number; t49: number; trailer: number; other: number; total: number }; function classifyVehicleType(v: Vehicle): keyof Omit { if (v.type === '4.5T' && !v.model.includes('冷链')) return 't4_5'; if (v.type === '4.5T' && v.model.includes('冷链')) return 't4_5c'; if (v.type === '18T') return 't18'; if (v.type === '49T') return 't49'; if (v.type === '挂车' || v.model.includes('挂车')) return 'trailer'; return 'other'; } function countByType(vehicles: Vehicle[]): VehicleTypeCounts { const counts: VehicleTypeCounts = { t4_5: 0, t4_5c: 0, t18: 0, t49: 0, trailer: 0, other: 0, total: 0 }; for (const v of vehicles) { counts[classifyVehicleType(v)]++; counts.total++; } return counts; } ``` - [ ] **Step 2: Verify TypeScript compiles** Run: `npx tsc --noEmit` Expected: no errors - [ ] **Step 3: Commit** ```bash git add src/server/routes/vehicles.ts git commit -m "feat: add macro-region mapping and vehicle type classification helpers" ``` --- ### Task 2: Backend — Three new API endpoints **Files:** - Modify: `src/server/routes/vehicles.ts` (add endpoints before the `/list` endpoint) - [ ] **Step 1: Add `/dept-stats` endpoint** Add before the `VEHICLE_TYPE_FILTERS` const (which is before `/list`) in `src/server/routes/vehicles.ts`: ```typescript // GET /api/vehicles/dept-stats app.get('/dept-stats', async (c) => { const vehicles = await getVehicles(); // Only count operating vehicles for department stats (those with a customerManager) const withManager = vehicles.filter((v) => v.customerManager); const deptMap = new Map>(); for (const v of withManager) { const dept = v.departmentName || '未分配'; const mgr = v.customerManager!; if (!deptMap.has(dept)) deptMap.set(dept, new Map()); const mgrMap = deptMap.get(dept)!; if (!mgrMap.has(mgr)) mgrMap.set(mgr, []); mgrMap.get(mgr)!.push(v); } const result = Array.from(deptMap.entries()).map(([department, mgrMap]) => { const allDeptVehicles = Array.from(mgrMap.values()).flat(); const managers = Array.from(mgrMap.entries()) .map(([manager, mvs]) => ({ manager, department, ...countByType(mvs), })) .sort((a, b) => b.total - a.total); return { department, totalAssets: allDeptVehicles.length, operatingCount: allDeptVehicles.filter((v) => v.status === 'Operating').length, idleCount: allDeptVehicles.filter((v) => v.status !== 'Operating').length, managers, }; }).sort((a, b) => b.totalAssets - a.totalAssets); return c.json(result); }); ``` - [ ] **Step 2: Add `/region-stats` endpoint** ```typescript // GET /api/vehicles/region-stats app.get('/region-stats', async (c) => { const vehicles = await getVehicles(); const operating = vehicles.filter((v) => v.status === 'Operating'); const regionMap = new Map(); for (const v of operating) { const region = mapMacroRegion(v.province, v.city); if (!regionMap.has(region)) regionMap.set(region, []); regionMap.get(region)!.push(v); } const regionOrder = ['华东', '华南', '华北', '华中', '西南', '西北', '其他']; const result = regionOrder .filter((r) => regionMap.has(r)) .map((region) => { const rv = regionMap.get(region)!; const customers = Array.from(new Set(rv.map((v) => v.customerName).filter(Boolean))) as string[]; const typeBreakdown = ['4.5T', '18T', '49T'].map((type) => { const typeVehicles = rv.filter((v) => v.type === type); return { type, total: typeVehicles.length, operating: typeVehicles.filter((v) => v.status === 'Operating').length, inventory: typeVehicles.filter((v) => v.status === 'Inventory').length, customers: Array.from(new Set(typeVehicles.map((v) => v.customerName).filter(Boolean))) as string[], }; }).filter((t) => t.total > 0); return { region, totalAssets: rv.length, operatingCount: rv.filter((v) => v.status === 'Operating').length, inventoryCount: rv.filter((v) => v.status === 'Inventory').length, customers, typeBreakdown, }; }); return c.json(result); }); ``` - [ ] **Step 3: Add `/customer-stats` endpoint** ```typescript // GET /api/vehicles/customer-stats app.get('/customer-stats', async (c) => { const vehicles = await getVehicles(); const operating = vehicles.filter((v) => v.status === 'Operating' && v.customerName); const custMap = new Map(); for (const v of operating) { const cust = v.customerName!; if (!custMap.has(cust)) custMap.set(cust, []); custMap.get(cust)!.push(v); } const result = Array.from(custMap.entries()) .map(([customer, cvs]) => { const first = cvs[0]; return { customer, manager: first.customerManager || '', brand: first.brandLabel || '', department: first.departmentName || '', region: mapMacroRegion(first.province, first.city), city: first.city || '', ...countByType(cvs), }; }) .sort((a, b) => b.total - a.total); return c.json(result); }); ``` - [ ] **Step 4: Verify TypeScript compiles** Run: `npx tsc --noEmit` Expected: no errors - [ ] **Step 5: Commit** ```bash git add src/server/routes/vehicles.ts git commit -m "feat: add dept-stats, region-stats, customer-stats API endpoints" ``` --- ### Task 3: Backend — Extend `/list` with new filter params **Files:** - Modify: `src/server/routes/vehicles.ts` (the `/list` endpoint) - [ ] **Step 1: Add manager, customer, isColdChain, isTrailer filters** In the `/list` endpoint, after the existing `category` filter block, add: ```typescript const { batch, model, location, status, category, vehicleType, manager, customer, isColdChain, isTrailer } = c.req.query(); ``` (Replace the existing destructure line.) Then after the `if (category)` block, add: ```typescript if (manager) { filtered = filtered.filter((v) => v.customerManager === manager); } if (customer) { filtered = filtered.filter((v) => v.customerName === customer); } if (isColdChain !== undefined) { const wantCold = isColdChain === 'true'; filtered = filtered.filter((v) => wantCold ? v.model.includes('冷链') : !v.model.includes('冷链')); } if (isTrailer !== undefined) { const wantTrailer = isTrailer === 'true'; filtered = filtered.filter((v) => wantTrailer ? (v.type === '挂车' || v.model.includes('挂车')) : !(v.type === '挂车' || v.model.includes('挂车'))); } ``` - [ ] **Step 2: Verify TypeScript compiles and build passes** Run: `npx tsc --noEmit && npx vite build` Expected: no errors, successful build - [ ] **Step 3: Commit** ```bash git add src/server/routes/vehicles.ts git commit -m "feat: extend /list endpoint with manager, customer, coldchain, trailer filters" ``` --- ### Task 4: Frontend — Types and API client **Files:** - Modify: `src/types.ts` - Modify: `src/api.ts` - [ ] **Step 1: Add new interfaces to `src/types.ts`** Append at end of file: ```typescript export interface ManagerStats { manager: string; department: string; t4_5: number; t4_5c: number; t18: number; t49: number; trailer: number; other: number; total: number; } export interface DeptGroup { department: string; totalAssets: number; operatingCount: number; idleCount: number; managers: ManagerStats[]; } export interface RegionGroup { region: string; totalAssets: number; operatingCount: number; inventoryCount: number; customers: string[]; typeBreakdown: { type: string; total: number; operating: number; inventory: number; customers: string[] }[]; } export interface CustomerStats { customer: string; manager: string; brand: string; department: string; region: string; city: string; t4_5: number; t4_5c: number; t18: number; t49: number; trailer: number; other: number; total: number; } ``` - [ ] **Step 2: Add API functions to `src/api.ts`** Add imports at top: ```typescript import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats, } from './types'; ``` Add after `fetchVehicleList`: ```typescript export async function fetchDeptStats(): Promise { return fetchJson(`${BASE}/dept-stats`); } export async function fetchRegionStats(): Promise { return fetchJson(`${BASE}/region-stats`); } export async function fetchCustomerStats(): Promise { return fetchJson(`${BASE}/customer-stats`); } ``` Also update `fetchVehicleList` params type to include new filters: ```typescript export async function fetchVehicleList(params: { batch?: string; model?: string; location?: string; status?: string; category?: string; vehicleType?: string; manager?: string; customer?: string; isColdChain?: string; isTrailer?: string; }): Promise { const query = new URLSearchParams(); if (params.batch) query.set('batch', params.batch); if (params.model) query.set('model', params.model); if (params.location) query.set('location', params.location); if (params.status) query.set('status', params.status); if (params.category) query.set('category', params.category); if (params.vehicleType) query.set('vehicleType', params.vehicleType); if (params.manager) query.set('manager', params.manager); if (params.customer) query.set('customer', params.customer); if (params.isColdChain) query.set('isColdChain', params.isColdChain); if (params.isTrailer) query.set('isTrailer', params.isTrailer); return fetchJson(`${BASE}/list?${query.toString()}`); } ``` - [ ] **Step 3: Verify TypeScript compiles** Run: `npx tsc --noEmit` Expected: no errors - [ ] **Step 4: Commit** ```bash git add src/types.ts src/api.ts git commit -m "feat: add frontend types and API client for dept/region/customer stats" ``` --- ### Task 5: Frontend — Extend App.tsx state, data loading, imports, and showPlateNumbers **Files:** - Modify: `src/App.tsx` - [ ] **Step 1: Update imports** Replace the existing import lines at top of `src/App.tsx`: ```typescript import { Truck, Warehouse, Activity, PlusCircle, MinusCircle, History, ChevronDown, ChevronRight, Info, Loader2, Search, Filter, ArrowRightLeft, } from 'lucide-react'; ``` Update type imports: ```typescript import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats } from './types'; import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats } from './api'; ``` - [ ] **Step 2: Add new state variables** After the existing state declarations (after `const [modalLoading, setModalLoading] = useState(false);`), add: ```typescript // Dept/Region/Customer data const [deptData, setDeptData] = useState([]); const [regionData, setRegionData] = useState([]); const [customerData, setCustomerData] = useState([]); // Dept section state const [deptViewMode, setDeptViewMode] = useState<'department' | 'manager'>('department'); const [expandedDepts, setExpandedDepts] = useState>(new Set()); const [expandedManagerDetails, setExpandedManagerDetails] = useState>(new Set()); const [selectedManager, setSelectedManager] = useState('All'); // Region section state const [expandedRegions, setExpandedRegions] = useState>(new Set()); const [regionFilters, setRegionFilters] = useState({ region: '', city: '', customer: '' }); const [isRegionFilterOpen, setIsRegionFilterOpen] = useState(false); // Customer section state const [expandedCustomers, setExpandedCustomers] = useState>(new Set()); const [customerFilters, setCustomerFilters] = useState({ customer: '', brand: '', department: '', manager: '', region: '' }); const [isCustomerFilterOpen, setIsCustomerFilterOpen] = useState(false); ``` - [ ] **Step 3: Update loadData to fetch all 3 new endpoints** Update the `loadData` callback: ```typescript const loadData = useCallback(async () => { try { setLoading(true); setError(null); 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 : '数据加载失败'); } finally { setLoading(false); } }, []); ``` - [ ] **Step 4: Extend showPlateNumbers type** Update the showPlateNumbers state type: ```typescript const [showPlateNumbers, setShowPlateNumbers] = useState<{ batch: string; model: string; location: string; category?: 'Inventory' | 'Pending' | 'Delivered' | 'Returned' | 'Replaced' | 'Operating'; vehicleType?: string; manager?: string; customer?: string; isColdChain?: boolean; isTrailer?: boolean; } | null>(null); ``` - [ ] **Step 5: Update modal loading to pass new filter params** In the `useEffect` for modal loading, update the params block (the "Normal vehicle list" section): ```typescript // Normal vehicle list setModalWeeklyDetail([]); const params: Record = {}; if (showPlateNumbers.vehicleType) params.vehicleType = showPlateNumbers.vehicleType; if (showPlateNumbers.batch !== 'All') params.batch = showPlateNumbers.batch; if (showPlateNumbers.model !== 'All') params.model = showPlateNumbers.model; if (showPlateNumbers.location !== 'All') params.location = showPlateNumbers.location; if (cat === 'Inventory') params.status = 'Inventory'; if (cat === 'Operating') params.category = 'Operating'; if (showPlateNumbers.manager) params.manager = showPlateNumbers.manager; if (showPlateNumbers.customer) params.customer = showPlateNumbers.customer; if (showPlateNumbers.isColdChain !== undefined) params.isColdChain = String(showPlateNumbers.isColdChain); if (showPlateNumbers.isTrailer !== undefined) params.isTrailer = String(showPlateNumbers.isTrailer); ``` - [ ] **Step 6: Add toggle helpers and derived data** After the existing `toggleModel` function, add: ```typescript 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); ``` - [ ] **Step 7: Verify TypeScript compiles** Run: `npx tsc --noEmit` Expected: no errors - [ ] **Step 8: Commit** ```bash git add src/App.tsx git commit -m "feat: add state, data loading, and helpers for 3 new modules" ``` --- ### Task 6: Frontend — Department Operations UI **Files:** - Modify: `src/App.tsx` (add section after the asset summary table's closing ``, before the Plate Number Modal) - [ ] **Step 1: Add the department operations section** Insert the department operations section JSX. Reference: lnoneos lines 1362-1880. This section goes right after the closing `` of the asset summary table (`bg-white rounded-sm border...`) and before `{/* Plate Number Modal */}`. The section includes: 1. Header with title "部门运营统计" 2. Dark summary bar (总资产/运营中/闲置中 — skip 平均出勤) 3. Toggle buttons (按部门 / 按业务员) + manager filter dropdown 4. Desktop table view (`hidden lg:block`) - Department mode: department rows expandable to show manager cards with 6 vehicle type cells - Manager mode: flat manager rows expandable to show 6 vehicle type cells 5. Mobile card view (`lg:hidden`) Port the JSX from lnoneos lines 1362-1880, replacing: - `MOCK_DEPT_STATS` → `deptData` - `DEPT_TOTALS.total` → `deptData.reduce((s, d) => s + d.totalAssets, 0)` - `allManagersList` / `managerStats` / `deptViewMode` / `expandedDepts` / `expandedManagerDetails` / `selectedManager` → already defined in Task 5 - `setShowPlateNumbers` calls: keep the same structure but remove `source` field (not needed in ln-bi) - Remove `ArrowRightLeft` icon usage in toggle buttons — replace with simple text button - All `rounded-2xl` → `rounded-sm` to match ln-bi style - All `shadow-sm` stay as is The `setShowPlateNumbers` calls from lnoneos use `manager`, `type`, `isColdChain`, `isTrailer` fields which we added to the state type in Task 5. - [ ] **Step 2: Verify TypeScript compiles and build passes** Run: `npx tsc --noEmit && npx vite build` Expected: no errors, successful build - [ ] **Step 3: Commit** ```bash git add src/App.tsx git commit -m "feat: add department operations statistics section" ``` --- ### Task 7: Frontend — Region Operations UI **Files:** - Modify: `src/App.tsx` (add section after department section, before Plate Number Modal) - [ ] **Step 1: Add the region operations section** Reference: lnoneos lines 1882-2174. Insert after the department section. The section includes: 1. Slate-themed header with "区域运营统计" + filter button 2. Filter popover (客户搜索 / 区域下拉 / 城市下拉) 3. Desktop table: expandable region rows → vehicle type sub-rows 4. Mobile cards: expandable region cards with type breakdown Port the JSX from lnoneos, replacing: - `MOCK_CUSTOMER_STATS` region-based filtering → use `filteredRegionData` (from Task 5) - Region stats aggregation in lnoneos used mock data with `Math.floor(totalAssets * 0.8)` for operating — use real `r.operatingCount` and `r.inventoryCount` - Type breakdown: use `r.typeBreakdown` array from API - `uniqueRegions` / `uniqueCities` → already defined in Task 5 - `setShowPlateNumbers` calls: use `vehicleType` field for type filtering instead of lnoneos's `type` field - `rounded-2xl` → `rounded-sm` - Filter popover for cities: derive from `regionData` (all unique cities from customers) Note: The region filter's city dropdown needs city data. Add to Task 5's derived data if not already there. The regionData from API contains customer names but not cities. For the city filter, we can derive from customerData filtered by region. - [ ] **Step 2: Verify TypeScript compiles and build passes** Run: `npx tsc --noEmit && npx vite build` Expected: no errors, successful build - [ ] **Step 3: Commit** ```bash git add src/App.tsx git commit -m "feat: add region operations statistics section" ``` --- ### Task 8: Frontend — Customer Operations UI **Files:** - Modify: `src/App.tsx` (add section after region section, before Plate Number Modal) - [ ] **Step 1: Add the customer operations section** Reference: lnoneos lines 2176-2496. Insert after the region section. The section includes: 1. Emerald-themed header with "客户运营统计" + filter button 2. Filter popover (客户名搜索 / 业务员搜索 / 品牌下拉 / 部门下拉 / 区域下拉) 3. Desktop table: customer rows with 6 vehicle type columns + total, expandable detail cards 4. Mobile cards: customer cards with expandable vehicle type grid Port the JSX from lnoneos, replacing: - `MOCK_CUSTOMER_STATS` → `filteredCustomerStats` (from Task 5) - `DEPT_TOTALS.total` → `customerData.reduce((s, c) => s + c.total, 0)` for asset ratio - `setShowPlateNumbers` calls: use `vehicleType` + `customer` fields - `uniqueBrands` / `uniqueDepts` / `uniqueRegions` → already defined in Task 5 - `rounded-2xl` → `rounded-sm` - [ ] **Step 2: Verify TypeScript compiles and build passes** Run: `npx tsc --noEmit && npx vite build` Expected: no errors, successful build - [ ] **Step 3: Commit** ```bash git add src/App.tsx git commit -m "feat: add customer operations statistics section" ``` --- ### Task 9: Final verification and build **Files:** All modified files - [ ] **Step 1: Full TypeScript check** Run: `npx tsc --noEmit` Expected: no errors - [ ] **Step 2: Production build** Run: `npx vite build` Expected: successful build with no warnings - [ ] **Step 3: Verify all sections render** Run: `npm run dev` and check: - Department section loads with real data - Region section loads with real data - Customer section loads with real data - Filter popovers work - Expand/collapse works - Click-through to plate number modal works - [ ] **Step 4: Final commit if any fixes needed** ```bash git add -A git commit -m "fix: address any issues from final review" ```