165 lines
5.0 KiB
TypeScript
165 lines
5.0 KiB
TypeScript
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<string, RegionalInventoryStats[]>();
|
||
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<string, RegionalInventoryStats[]>();
|
||
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> = {},
|
||
): 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)
|
||
);
|
||
}
|