import { emptyInventoryCounts, inventoryKey, inventoryOperation, inventoryColor, } from "../../../shared/assets/inventory"; import type { VehicleListItem } from "../types"; import type { RegionalInventoryStats } from "../types"; import type { VehicleModalCategory, VehicleModalSelection } from "../model"; export type InventoryGrouping = "region" | "type"; export function groupInventory( rows: RegionalInventoryStats[], grouping: InventoryGrouping, ascending = false, ) { const groups = new Map(); for (const row of rows) { const key = row[grouping] || "未归属"; if (!groups.has(key)) groups.set(key, []); groups.get(key)!.push(row); } return [...groups] .map(([name, items]) => { const cities = new Map(); for (const row of items) { // A city may be missing or share a name with a city in another region. const key = JSON.stringify([row.region, row.city]); if (!cities.has(key)) cities.set(key, []); cities.get(key)!.push(row); } return { name, total: items.reduce((sum, row) => sum + row.quantity, 0), cities: [...cities] .map(([key, records]) => ({ key, name: records[0].city || "未归属城市", region: records[0].region || "未归属", total: records.reduce((sum, row) => sum + row.quantity, 0), rows: [...records].sort( (a, b) => (ascending ? 1 : -1) * (a.quantity - b.quantity) || a.model.localeCompare(b.model, "zh-CN"), ), })) .sort( (a, b) => b.total - a.total || a.name.localeCompare(b.name, "zh-CN"), ), }; }) .sort((a, b) => b.total - a.total || a.name.localeCompare(b.name, "zh-CN")); } export function inventorySelection( row: RegionalInventoryStats, category: VehicleModalCategory = "Inventory", ): VehicleModalSelection { return { batch: "All", source: "asset", category, brand: row.brand, model: row.model, location: row.city, type: row.type, }; } export const INVENTORY_TYPES = [ "4.5T普货", "4.5T冷链", "18T", "49T", "其他", ] as const; export function inventoryColumn(type: string) { return INVENTORY_TYPES.includes(type as (typeof INVENTORY_TYPES)[number]) ? type : "其他"; } export function inventoryTotals(rows: RegionalInventoryStats[]) { const result = emptyInventoryCounts(); for (const row of rows) { for (const key of [ "quantity", "available", "awaiting", "abnormal", "pending", ] as const) result[key] += row[key] || 0; result.unclassified += row.unclassified ?? Math.max(0, row.quantity - (row.available || 0) - (row.awaiting || 0)); for (const [type, count] of Object.entries(row.abnormalTypes || {})) result.abnormalTypes[type] = (result.abnormalTypes[type] || 0) + count; } return result; } /** Display labels never replace the original model used for exact drill-down. */ export function inventoryModel(model: string, brand: string) { const parentheses = [...model.matchAll(/[((]([^))]+)[))]/g)].map( (match) => match[1], ); const colorOnly = /^(?:红|白|蓝|绿|灰|黑|黄|银|蓝白绿)(?:色|车)?$/; const operator = parentheses .filter((value) => !colorOnly.test(value)) .map((value) => value.replace(/[--](?:红|白|蓝白绿)(?:色|车)?$/, "")) .join(" · "); let name = model.replace(/[((][^))]*[))]/g, "").trim(); for (const label of [ brand, "现代", "帕力安牌", "宇通", "飞驰", "苏龙", "跃进", "楚风", ]) { if (label && name.startsWith(label)) name = name.slice(label.length).replace(/^-/, ""); if (label && name.endsWith(label)) name = name.slice(0, -label.length); } return { name: name || model, operator }; } export interface InventoryScope { rows: RegionalInventoryStats[]; label: string; initialCategory?: "Abnormal" | "Pending"; } export function scopedInventorySelection( scope: InventoryScope, extra: Partial = {}, ): VehicleModalSelection { return { batch: "All", model: "All", location: "All", source: "asset", category: "Inventory", inventoryKeys: scope.rows.map(inventoryKey), scopeLabel: scope.label, ...extra, }; } export function matchesInventorySelection( vehicle: VehicleListItem, selection: VehicleModalSelection, ) { return ( (!selection.inventoryKeys || selection.inventoryKeys.includes(vehicle.inventoryKey || "")) && (!selection.inventoryOperation || inventoryOperation(vehicle.operationStatus) === selection.inventoryOperation) && (!selection.inventoryColor || inventoryColor(vehicle.color) === inventoryColor(selection.inventoryColor)) && (!selection.abnormalType || (vehicle.abnormalMove?.type || "未提供") === selection.abnormalType) ); }