255 lines
13 KiB
TypeScript
255 lines
13 KiB
TypeScript
import type { Vehicle, VehicleRow } from '../../types.js';
|
|
import type { VehicleTypeCategory } from './types.js';
|
|
|
|
export const REGIONS = ['嘉兴', '广东', '北京', '新疆', '其他'] as const;
|
|
export const INVENTORY_REGIONS = ['江浙沪', '广东', '新疆', '其它'] as const;
|
|
|
|
export function mapRegion(province: string | null, city: string | null): string {
|
|
if (!province && !city) return '其他';
|
|
const loc = (city || province || '').trim();
|
|
if (loc.includes('嘉兴') || loc.includes('浙江') || loc.includes('上海') || loc.includes('江苏')) return '嘉兴';
|
|
if (loc.includes('广东') || loc.includes('广州') || loc.includes('深圳') || loc.includes('佛山') || loc.includes('东莞')) return '广东';
|
|
if (loc.includes('北京')) return '北京';
|
|
if (loc.includes('新疆') || loc.includes('乌鲁木齐')) return '新疆';
|
|
|
|
const prov = (province || '').trim();
|
|
if (prov.includes('浙江') || prov.includes('上海') || prov.includes('江苏')) return '嘉兴';
|
|
if (prov.includes('广东')) return '广东';
|
|
if (prov.includes('北京')) return '北京';
|
|
if (prov.includes('新疆')) return '新疆';
|
|
return '其他';
|
|
}
|
|
|
|
export function mapInventoryRegion(region: string): string {
|
|
if (region === '嘉兴') return '江浙沪';
|
|
if (region === '广东') return '广东';
|
|
if (region === '新疆') return '新疆';
|
|
return '其它';
|
|
}
|
|
|
|
export function mapMacroRegion(province: string | null, city: string | null): string {
|
|
const prov = (province || '').trim();
|
|
const cityName = (city || '').trim();
|
|
const location = prov + cityName;
|
|
if (/上海|江苏|浙江|安徽|福建|江西|山东|南京|杭州|合肥|济南|青岛|苏州|宁波|厦门|嘉兴|无锡/.test(location)) return '华东';
|
|
if (/广东|广西|海南|广州|深圳|佛山|东莞|珠海|惠州|中山|南宁/.test(location)) return '华南';
|
|
if (/北京|天津|河北|山西|内蒙古|石家庄|太原|呼和浩特/.test(location)) return '华北';
|
|
if (/河南|湖北|湖南|郑州|武汉|长沙/.test(location)) return '华中';
|
|
if (/重庆|四川|贵州|云南|西藏|成都|昆明|贵阳/.test(location)) return '西南';
|
|
if (/陕西|甘肃|青海|宁夏|新疆|西安|兰州|乌鲁木齐/.test(location)) return '西北';
|
|
return '其他';
|
|
}
|
|
|
|
export function classifyVehicleType(vehicle: Vehicle): VehicleTypeCategory {
|
|
if (vehicle.type === '4.5T' && !vehicle.model.includes('冷链')) return 't4_5';
|
|
if (vehicle.type === '4.5T' && vehicle.model.includes('冷链')) return 't4_5c';
|
|
if (vehicle.type === '18T') return 't18';
|
|
if (vehicle.type === '49T') return 't49';
|
|
if (vehicle.type === '挂车' || vehicle.model.includes('挂车')) return 'trailer';
|
|
return 'other';
|
|
}
|
|
|
|
// vehicle_status has priority because pending (4) and abnormal (14) override operation_status.
|
|
// operation_status: 1=租赁, 2=自营, 3=可运营, 4=待运营, 5=退出运营.
|
|
export function mapStatus(operationStatus: string | null, vehicleStatus: string | null): Vehicle['status'] {
|
|
const operation = (operationStatus || '').trim();
|
|
const vehicle = (vehicleStatus || '').trim();
|
|
if (vehicle === '4') return 'Pending';
|
|
if (vehicle === '14') return 'Abnormal';
|
|
if (operation === '1' || operation === '2') return 'Operating';
|
|
if (operation === '3' || operation === '4') return 'Inventory';
|
|
if (operation === '租赁' || operation === '自营') return 'Operating';
|
|
if (operation === '可运营' || operation === '待运营' || operation === '在库') return 'Inventory';
|
|
if (operation === '异动') return 'Abnormal';
|
|
return 'Inventory';
|
|
}
|
|
|
|
export function mapOwnership(rentStatusLabel: string | null): string {
|
|
if (!rentStatusLabel) return 'Unknown';
|
|
const status = rentStatusLabel.trim();
|
|
if (status === '0') return 'Self';
|
|
if (status === '1') return 'Hanging';
|
|
if (status === '2') return 'Leased';
|
|
if (status === '3') return 'Self';
|
|
if (status === '自有') return 'Self';
|
|
if (status === '外租') return 'Leased';
|
|
if (status === '自营') return 'Self';
|
|
if (status === '租赁') return 'Leased';
|
|
if (status === '挂靠') return 'Hanging';
|
|
return 'Unknown';
|
|
}
|
|
|
|
export function resolveCity(city: string | null, province: string | null): string {
|
|
const cityName = (city || '').replace(/[\[\]]/g, '').trim();
|
|
if (cityName) return cityName;
|
|
const provinceName = (province || '').replace(/[\[\]]/g, '').trim();
|
|
return provinceName || '其他';
|
|
}
|
|
|
|
// vehicle_type is the new-system category code: 1=4.5T, 3=牵引车头, 5/6=挂车.
|
|
export function deriveType(modelLabel: string | null, vehicleTypeCode: string | null): string {
|
|
const label = (modelLabel || '').trim();
|
|
const code = (vehicleTypeCode || '').trim();
|
|
if (label.includes('半挂车') || code === '5' || code === '6') return '挂车';
|
|
if (label.includes('4.5吨')) return '4.5T';
|
|
if (label.includes('18吨')) return '18T';
|
|
if (label.includes('49吨')) return '49T';
|
|
if (label.includes('35吨')) return '35T';
|
|
if (code === '1') return '4.5T';
|
|
if (code === '3') return '49T';
|
|
if (label.includes('叉车')) return '叉车';
|
|
return '其他车型';
|
|
}
|
|
|
|
export function normalizeModelLabel(modelLabel: string | null): string | null {
|
|
const label = (modelLabel || '').trim();
|
|
if (label === '帕力安牌4.5吨冷链车') return '4.5吨冷链车';
|
|
if (label === '帕力安牌18吨双飞翼货车') return '18吨双飞翼货车';
|
|
if (label === '海格牌18吨双飞翼货车') return '18吨双飞翼货车';
|
|
return label || null;
|
|
}
|
|
|
|
// Key format: brand-model-color[+rentCompany when ownership is 外租].
|
|
// Multiple source tags intentionally share aliases and order values to merge dirty or equivalent data.
|
|
const MODEL_ALIAS_MAP: Record<string, { alias: string; order: number }> = {
|
|
// 4.5T 普货
|
|
'现代-4.5吨货车-白色广州开发区交投氢能运营管理有限公司': { alias: '现代4.5T普货(交投)', order: 101 },
|
|
'现代-4.5吨货车-白恒运': { alias: '现代4.5T普货(恒运)', order: 102 },
|
|
'现代-4.5吨货车-白色恒运': { alias: '现代4.5T普货(恒运)', order: 102 },
|
|
'现代-4.5吨货车-白色': { alias: '现代4.5T普货', order: 101 },
|
|
'现代-4.5吨货车-白': { alias: '现代4.5T普货', order: 101 },
|
|
// 4.5T 冷链
|
|
'帕力安牌-4.5吨冷链车-白色广州开发区交投氢能运营管理有限公司': { alias: '现代4.5T冷链(交投)', order: 201 },
|
|
'帕力安牌-4.5吨冷链车-白色': { alias: '现代4.5T冷链(羚牛)', order: 202 },
|
|
'跃进-4.5吨冷链车-白/绿/灰': { alias: '跃进4.5T冷链', order: 203 },
|
|
// 18T
|
|
'飞驰-18吨厢式货车-红': { alias: '飞驰18T(红车)', order: 301 },
|
|
'飞驰-18吨厢式货车-白/绿': { alias: '飞驰18T(白车)', order: 302 },
|
|
'楚风-18吨厢式货车-白': { alias: '楚风18T厢货', order: 303 },
|
|
'苏龙-18吨双飞翼货车-白': { alias: '苏龙18T飞翼', order: 304 },
|
|
'苏龙-18吨双飞翼货车-白色': { alias: '苏龙18T飞翼', order: 304 },
|
|
'苏龙-18吨双飞翼货车-白安吉天地物流科技有限公司': { alias: '苏龙18T飞翼(安吉)', order: 305 },
|
|
'帕力安牌-18吨双飞翼货车-白': { alias: '现代18T双飞翼(羚牛)', order: 306 },
|
|
'帕力安牌-18吨双飞翼货车-白/绿': { alias: '现代18T双飞翼(羚牛)', order: 306 },
|
|
// 49T
|
|
'宇通-49吨牵引车头-白': { alias: '49T宇通', order: 401 },
|
|
'飞驰-49吨牵引车头-白/蓝/绿': { alias: '49T飞驰', order: 402 },
|
|
'飞驰-49吨牵引车头-白/蓝/绿嘉兴氢能产业发展股份有限公司': { alias: '49T飞驰(嘉氢)', order: 403 },
|
|
'飞驰-49吨牵引车头-红': { alias: '49T飞驰(红)', order: 402 },
|
|
'飞驰-49吨牵引车头-红嘉兴氢能产业发展股份有限公司': { alias: '49T飞驰(嘉氢)', order: 403 },
|
|
'飞驰-49吨牵引车头-红浙江氢能产业发展有限公司': { alias: '49T飞驰(浙氢-红)', order: 404 },
|
|
'飞驰-49吨牵引车头-白/蓝/绿浙江氢能产业发展有限公司': { alias: '49T飞驰(浙氢-蓝白绿)', order: 405 },
|
|
'楚风-49吨牵引车头-蓝/黑': { alias: '49T楚风', order: 406 },
|
|
'楚风-49吨牵引车头-蓝/黑海珀特科技(北京)有限公司': { alias: '49T楚风(海珀特)', order: 406 },
|
|
// 其他
|
|
'红岩-35吨牵引车头-红色': { alias: '35T油车', order: 501 },
|
|
'其他-氢能叉车-蓝白绿': { alias: '氢能叉车', order: 502 },
|
|
'通华-重型集装箱半挂车-红色浙江锦昌仓储有限公司': { alias: '挂车', order: 503 },
|
|
'通华-重型集装箱半挂车-红色嘉兴市鼎义物流有限公司': { alias: '挂车', order: 503 },
|
|
'通华-重型集装箱半挂车-红色': { alias: '挂车', order: 503 },
|
|
'大通-重型集装箱半挂车-红色': { alias: '挂车', order: 503 },
|
|
'明威-重型集装箱半挂车-红色': { alias: '挂车', order: 503 },
|
|
'明威-重型集装箱半挂车-红': { alias: '挂车', order: 503 },
|
|
'万风-重型平板半挂车-红': { alias: '挂车', order: 503 },
|
|
'舒捷-SJ型蓄电池观光车-蓝白': { alias: '观光车', order: 504 },
|
|
'东风-挂靠油车-白色': { alias: '公务车/挂靠车', order: 505 },
|
|
'东风-挂靠油车-白': { alias: '公务车/挂靠车', order: 505 },
|
|
'腾势-公务用车/小客车-黑': { alias: '公务车/挂靠车', order: 505 },
|
|
'腾势-公务用车/小客车-白': { alias: '公务车/挂靠车', order: 505 },
|
|
'其他-公务用车/小客车-蓝色': { alias: '公务车/挂靠车', order: 505 },
|
|
'其他-公务用车/小客车-灰': { alias: '公务车/挂靠车', order: 505 },
|
|
'远程牌-公务用车/小客车-白': { alias: '公务车/挂靠车', order: 505 },
|
|
'大通-公务用车/小客车-灰': { alias: '公务车/挂靠车', order: 505 },
|
|
};
|
|
|
|
export function deriveModelTag(
|
|
brandLabel: string | null,
|
|
modelLabel: string | null,
|
|
color: string | null,
|
|
ownershipLabel: string | null,
|
|
rentCompany: string | null,
|
|
): string {
|
|
const brand = (brandLabel || '').trim();
|
|
const model = (normalizeModelLabel(modelLabel) || '').trim();
|
|
const vehicleColor = (color || '').trim();
|
|
if (model === '公务用车/小客车') return '公务车/挂靠车';
|
|
const company = ownershipLabel?.trim() === '外租' ? (rentCompany || '').trim() : '';
|
|
|
|
if (!brand && !model) return '未知车型';
|
|
const tag = `${brand}-${model}-${vehicleColor}${company}`;
|
|
return MODEL_ALIAS_MAP[tag]?.alias || tag;
|
|
}
|
|
|
|
export function getModelOrder(model: string): number {
|
|
for (const entry of Object.values(MODEL_ALIAS_MAP)) {
|
|
if (entry.alias === model) return entry.order;
|
|
}
|
|
return 999;
|
|
}
|
|
|
|
export function compareModelNames(left: string, right: string): number {
|
|
return getModelOrder(left) - getModelOrder(right);
|
|
}
|
|
|
|
export function transformRow(row: VehicleRow): Vehicle {
|
|
const region = mapRegion(row.省, row.市);
|
|
return {
|
|
id: row.id,
|
|
plateNumber: row.车牌号 || '',
|
|
vin: row.vin || '',
|
|
type: deriveType(row.车辆型号Label, row.车辆类型参数),
|
|
model: deriveModelTag(row.车辆品牌Label, row.车辆型号Label, row.车辆颜色, row.车辆归属状态Label, row.租赁公司),
|
|
color: row.车辆颜色 || '',
|
|
location: region,
|
|
region,
|
|
province: row.省,
|
|
city: row.市,
|
|
status: mapStatus(row.车辆租赁状态Label, row.车辆租赁状态),
|
|
operationStatus: row.车辆租赁状态Label,
|
|
ownership: mapOwnership(row.车辆归属状态Label),
|
|
rentCompany: row.租赁公司 || '',
|
|
contractNo: row.合同编码,
|
|
customerName: row.客户名称,
|
|
orgName: row.合同归属公司,
|
|
departmentName: row.合同归属部门,
|
|
subjectOrg: row.主体,
|
|
projectName: row.项目名称,
|
|
customerManager: row.客户经理,
|
|
managerId: row.经理ID || null,
|
|
brandLabel: row.车辆品牌Label,
|
|
};
|
|
}
|
|
|
|
export const SUMMARY_TYPE_FILTERS: ReadonlyArray<{ name: string; filter: (vehicle: Vehicle) => boolean }> = [
|
|
{ name: '4.5T普货', filter: (vehicle) => vehicle.type === '4.5T' && !vehicle.model.includes('冷链') },
|
|
{ name: '4.5T冷链', filter: (vehicle) => vehicle.type === '4.5T' && vehicle.model.includes('冷链') },
|
|
{ name: '18T', filter: (vehicle) => vehicle.type === '18T' },
|
|
{ name: '49T', filter: (vehicle) => vehicle.type === '49T' },
|
|
{ name: '其他', filter: (vehicle) => !['4.5T', '18T', '49T'].includes(vehicle.type) },
|
|
];
|
|
|
|
// List filtering keeps trailers separate, while summary grouping intentionally includes them in “其他”.
|
|
export const VEHICLE_TYPE_FILTERS: Record<string, (vehicle: Vehicle) => boolean> = {
|
|
'4.5T普货': (vehicle) => vehicle.type === '4.5T' && !vehicle.model.includes('冷链'),
|
|
'4.5T冷链': (vehicle) => vehicle.type === '4.5T' && vehicle.model.includes('冷链'),
|
|
'18T': (vehicle) => vehicle.type === '18T',
|
|
'49T': (vehicle) => vehicle.type === '49T',
|
|
'挂车': (vehicle) => vehicle.type === '挂车' || vehicle.model.includes('挂车'),
|
|
'其他': (vehicle) => classifyVehicleType(vehicle) === 'other',
|
|
};
|
|
|
|
export function filterByLocation(vehicles: Vehicle[], location: string, source?: string): Vehicle[] {
|
|
const macroRegions = ['华东', '华南', '华北', '华中', '西南', '西北'];
|
|
const isMacro = macroRegions.includes(location) || (location === '其他' && source !== 'asset');
|
|
if (isMacro) {
|
|
return vehicles.filter((vehicle) => mapMacroRegion(vehicle.province, vehicle.city) === location);
|
|
}
|
|
const inventoryRegionMap: Record<string, string> = { '江浙沪': '嘉兴', '其它': '其他' };
|
|
const mappedLocation = inventoryRegionMap[location] || location;
|
|
return vehicles.filter(
|
|
(vehicle) => vehicle.location === mappedLocation
|
|
|| vehicle.city === location
|
|
|| resolveCity(vehicle.city, vehicle.province) === location,
|
|
);
|
|
}
|