import type { FlowStatsResponse, FlowType, WeeklyDetailItem } from './api'; import type { CustomerStats, DeptGroup, RegionalInventoryStats, VehicleListItem, } from './types'; const INVENTORY_TYPE_ORDER = ['4.5T普货', '4.5T冷链', '18T', '49T', '挂车', '其他']; const CHINESE_NUMBER_ORDER: Record = { 一: 1, 二: 2, 三: 3, 四: 4, 五: 5, 六: 6, 七: 7, 八: 8, 九: 9, 十: 10, }; export interface DateRange { start: string; end: string; } export interface InventoryFilters { region: string; city: string; brand: string; type: string; model: string; } export interface CustomerFilters { customer: string[]; brand: string; department: string; manager: string; region: string; } export interface ModalVehicleFilters { plateNumber: string; model: string; brand: string; location: string; } export type VehicleModalCategory = | 'Inventory' | 'Pending' | 'Delivered' | 'Returned' | 'Replaced' | 'Operating'; export interface VehicleModalSelection { batch: string; model: string; location: string; category?: VehicleModalCategory; vehicleType?: string; manager?: string; customer?: string; department?: string; attendance?: 'active' | 'idle'; isColdChain?: boolean; isTrailer?: boolean; type?: string; source?: string; title?: string; } export interface VehicleListRequestParams { batch?: string; model?: string; location?: string; category?: 'Inventory' | 'Operating' | 'Pending'; vehicleType?: string; manager?: string; customer?: string; isColdChain?: string; isTrailer?: string; department?: string; attendance?: string; subject?: string | null; source?: string; } export type VehicleModalRequest = | { kind: 'weekly'; type: FlowType; filters: { model: string; batch: string; location: string; source?: string }; } | { kind: 'vehicles'; params: VehicleListRequestParams }; export interface FlowSelection { date: string; type: FlowType; } export function formatLocalDateTime(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } export function formatLocalDate(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function addDays(date: Date, days: number): Date { const next = new Date(date); next.setDate(next.getDate() + days); return next; } // 资产流转周报按周六至周五统计;周末查看时仍停留在刚结束的周五。 export function getWeeklyFlowRange(referenceDate = new Date()): DateRange { const day = referenceDate.getDay(); const end = day === 6 ? addDays(referenceDate, -1) : day === 0 ? addDays(referenceDate, -2) : addDays(referenceDate, 5 - day); return { start: formatLocalDate(addDays(end, -6)), end: formatLocalDate(end), }; } const WEEKLY_FLOW_TYPE_BY_CATEGORY: Partial> = { Delivered: 'delivered', Returned: 'returned', Replaced: 'replaced', }; // Pending 不是周流转事件,必须继续走车辆列表接口以保留型号、批次和区域筛选。 export function buildVehicleModalRequest( selection: VehicleModalSelection, subject: string | null, ): VehicleModalRequest { const weeklyType = selection.category ? WEEKLY_FLOW_TYPE_BY_CATEGORY[selection.category] : undefined; if (weeklyType) { return { kind: 'weekly', type: weeklyType, filters: { model: selection.model, batch: selection.batch, location: selection.location, source: selection.source, }, }; } const params: VehicleListRequestParams = {}; if (selection.vehicleType) params.vehicleType = selection.vehicleType; if (selection.batch !== 'All') params.batch = selection.batch; if (selection.model !== 'All') params.model = selection.model; if (selection.location !== 'All') params.location = selection.location; if (selection.source) params.source = selection.source; if (selection.category === 'Inventory') params.category = 'Inventory'; if (selection.category === 'Operating') params.category = 'Operating'; if (selection.category === 'Pending') params.category = 'Pending'; if (selection.manager) params.manager = selection.manager; if (selection.customer) params.customer = selection.customer; if (selection.department) params.department = selection.department; if (selection.attendance) params.attendance = selection.attendance; if (!selection.type) { if (selection.isColdChain !== undefined) params.isColdChain = String(selection.isColdChain); if (selection.isTrailer !== undefined) params.isTrailer = String(selection.isTrailer); } // 页面车型分组与列表接口的 vehicleType 取值并不完全一致,这里集中保留既有映射。 if (selection.type === '4.5T') { if (selection.isColdChain === true) params.vehicleType = '4.5T冷链'; if (selection.isColdChain === false) params.vehicleType = '4.5T普货'; } else if ( selection.type === '4.5T普货' || selection.type === '4.5T冷链' || selection.type === '18T' || selection.type === '49T' || selection.type === '挂车' || selection.type === '其他' ) { params.vehicleType = selection.type; } else if (selection.type === '其他车型') { if (selection.isTrailer === true) params.isTrailer = 'true'; if (selection.isTrailer === false) params.vehicleType = '其他'; } return { kind: 'vehicles', params: { ...params, subject } }; } function uniqueNonEmpty(values: Array): string[] { return Array.from(new Set(values.filter((value): value is string => Boolean(value)))); } function getDepartmentOrder(name: string): number { const match = name.match(/[一二三四五六七八九十]/); return match ? (CHINESE_NUMBER_ORDER[match[0]] || 99) : 99; } export function filterInventoryStats( inventory: RegionalInventoryStats[], filters: InventoryFilters, ): RegionalInventoryStats[] { return inventory.filter((item) => ( (!filters.region || item.region === filters.region) && (!filters.city || item.city === filters.city) && (!filters.brand || item.brand === filters.brand) && (!filters.type || item.type === filters.type) && (!filters.model || item.model === filters.model) )); } export function groupInventoryByRegion( inventory: RegionalInventoryStats[], ): Record> { const result: Record> = {}; for (const item of inventory) { if (!result[item.region]) result[item.region] = {}; if (!result[item.region][item.city]) result[item.region][item.city] = []; result[item.region][item.city].push(item); } return result; } export function groupInventoryByModel( inventory: RegionalInventoryStats[], ): Record> { const raw: Record> = {}; for (const item of inventory) { if (!raw[item.type]) raw[item.type] = {}; if (!raw[item.type][item.model]) raw[item.type][item.model] = []; raw[item.type][item.model].push(item); } // 已知车型按报表约定排序,新增的未知车型保留接口返回时的首次出现顺序。 const result: Record> = {}; for (const type of INVENTORY_TYPE_ORDER) { if (raw[type]) result[type] = raw[type]; } for (const type of Object.keys(raw)) { if (!result[type]) result[type] = raw[type]; } return result; } export function deriveInventoryView( inventory: RegionalInventoryStats[], filters: InventoryFilters, modelTypeFilter: string, ) { const filtered = filterInventoryStats(inventory, filters); const modelSource = modelTypeFilter ? inventory.filter((item) => item.type === modelTypeFilter) : inventory; const types = uniqueNonEmpty(inventory.map((item) => item.type)); return { filtered, brands: uniqueNonEmpty(inventory.map((item) => item.brand)), regions: Array.from(new Set(inventory.map((item) => item.region))), cities: uniqueNonEmpty(inventory.map((item) => item.city)), types: types.sort( (left, right) => INVENTORY_TYPE_ORDER.indexOf(left) - INVENTORY_TYPE_ORDER.indexOf(right), ), modelsForType: uniqueNonEmpty(modelSource.map((item) => item.model)), byRegion: groupInventoryByRegion(filtered), byModel: groupInventoryByModel(filtered), }; } export function deriveDepartmentView(departments: DeptGroup[], selectedManager: string) { const managers = departments .flatMap((department) => department.managers.map((manager) => manager.manager)) .filter((value, index, all) => all.indexOf(value) === index) .sort(); const groupedManagers = departments.map((department) => ({ department: department.department, managers: department.managers.map((manager) => manager.manager), })); const managerStats = departments .flatMap((department) => department.managers) .filter((manager) => selectedManager === 'All' || manager.manager === selectedManager) .sort((left, right) => right.total - left.total); return { managers, groupedManagers, managerStats }; } export function filterCustomerStats( customers: CustomerStats[], filters: CustomerFilters, ): CustomerStats[] { return customers.filter((customer) => ( (filters.customer.length === 0 || filters.customer.includes(customer.customer)) && (!filters.brand || customer.brand === filters.brand) && (!filters.department || customer.department === filters.department) && (!filters.manager || customer.manager === filters.manager) && (!filters.region || customer.region === filters.region) )); } function groupCustomerManagers( customers: CustomerStats[], departments: DeptGroup[], ): Array<{ department: string; managers: string[] }> { const departmentManagers = new Map>(); for (const department of departments) { if (!departmentManagers.has(department.department)) { departmentManagers.set(department.department, new Set()); } for (const manager of department.managers) { departmentManagers.get(department.department)!.add(manager.manager); } } for (const customer of customers) { if (!customer.manager || !customer.department) continue; if (!departmentManagers.has(customer.department)) { departmentManagers.set(customer.department, new Set()); } departmentManagers.get(customer.department)!.add(customer.manager); } return Array.from(departmentManagers.entries()) .sort((left, right) => { const leftOrder = left[0] === '公务车' ? 100 : getDepartmentOrder(left[0]); const rightOrder = right[0] === '公务车' ? 100 : getDepartmentOrder(right[0]); return leftOrder - rightOrder; }) .map(([department, managers]) => ({ department, managers: Array.from(managers) })); } export function deriveCustomerView( customers: CustomerStats[], departments: DeptGroup[], filters: CustomerFilters, ) { return { filtered: filterCustomerStats(customers, filters), brands: uniqueNonEmpty(customers.map((customer) => customer.brand)), departments: uniqueNonEmpty(customers.map((customer) => customer.department)) .sort((left, right) => getDepartmentOrder(left) - getDepartmentOrder(right)), regions: Array.from(new Set(customers.map((customer) => customer.region))), cities: uniqueNonEmpty(customers.map((customer) => customer.city)), customerNames: uniqueNonEmpty(customers.map((customer) => customer.customer)), managersByDepartment: groupCustomerManagers(customers, departments), }; } export function deriveModalVehicleView( vehicles: VehicleListItem[], weeklyDetails: WeeklyDetailItem[], filters: ModalVehicleFilters, ) { return { plates: uniqueNonEmpty(vehicles.map((vehicle) => vehicle.plateNumber || vehicle.vin)), models: uniqueNonEmpty(vehicles.map((vehicle) => vehicle.model)), brands: uniqueNonEmpty(vehicles.map((vehicle) => vehicle.brandLabel)), locations: uniqueNonEmpty(vehicles.map((vehicle) => vehicle.location)), filteredVehicles: vehicles.filter((vehicle) => ( (!filters.plateNumber || (vehicle.plateNumber || vehicle.vin) === filters.plateNumber) && (!filters.model || vehicle.model === filters.model) && (!filters.brand || vehicle.brandLabel === filters.brand) && (!filters.location || vehicle.location === filters.location) )), filteredWeeklyDetails: weeklyDetails.filter((detail) => ( !filters.plateNumber || detail.plate_number === filters.plateNumber )), }; } export function selectFlowDetails( flowStats: FlowStatsResponse | null, selection: FlowSelection | null, ) { if (!flowStats || !selection) return []; return flowStats.details.filter((detail) => ( detail.date === selection.date && detail.type === selection.type )); } export function buildCustomerPieData( customers: CustomerStats[], view: 'region' | 'province', provinceData: Array<{ name: string; value: number }>, ): Array<{ name: string; value: number }> { if (view === 'province') return provinceData; const totals: Record = {}; for (const customer of customers) { totals[customer.region] = (totals[customer.region] || 0) + customer.total; } return Object.entries(totals) .map(([name, value]) => ({ name, value })) .sort((left, right) => right.value - left.value); }