feat(assets): migrate asset views and reconcile inventory drilldowns
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user