Files
ln-bi/src/modules/assets/library/inventory-view.ts
T

165 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
);
}