refactor: modularize application domains
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
import type { FlowStatsResponse, FlowType, WeeklyDetailItem } from './api';
|
||||
import type {
|
||||
CustomerStats,
|
||||
DeptGroup,
|
||||
RegionalInventoryStats,
|
||||
VehicleListItem,
|
||||
} from './types';
|
||||
|
||||
const INVENTORY_TYPE_ORDER = ['4.5T普货', '4.5T冷链', '18T', '49T', '挂车', '其他'];
|
||||
const CHINESE_NUMBER_ORDER: Record<string, number> = {
|
||||
一: 1,
|
||||
二: 2,
|
||||
三: 3,
|
||||
四: 4,
|
||||
五: 5,
|
||||
六: 6,
|
||||
七: 7,
|
||||
八: 8,
|
||||
九: 9,
|
||||
十: 10,
|
||||
};
|
||||
|
||||
export interface DateRange {
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
export interface InventoryFilters {
|
||||
region: string;
|
||||
city: string;
|
||||
brand: string;
|
||||
type: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export interface CustomerFilters {
|
||||
customer: string[];
|
||||
brand: string;
|
||||
department: string;
|
||||
manager: string;
|
||||
region: string;
|
||||
}
|
||||
|
||||
export interface ModalVehicleFilters {
|
||||
plateNumber: string;
|
||||
model: string;
|
||||
brand: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
export type VehicleModalCategory =
|
||||
| 'Inventory'
|
||||
| 'Pending'
|
||||
| 'Delivered'
|
||||
| 'Returned'
|
||||
| 'Replaced'
|
||||
| 'Operating';
|
||||
|
||||
export interface VehicleModalSelection {
|
||||
batch: string;
|
||||
model: string;
|
||||
location: string;
|
||||
category?: VehicleModalCategory;
|
||||
vehicleType?: string;
|
||||
manager?: string;
|
||||
customer?: string;
|
||||
department?: string;
|
||||
attendance?: 'active' | 'idle';
|
||||
isColdChain?: boolean;
|
||||
isTrailer?: boolean;
|
||||
type?: string;
|
||||
source?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface VehicleListRequestParams {
|
||||
batch?: string;
|
||||
model?: string;
|
||||
location?: string;
|
||||
category?: 'Inventory' | 'Operating' | 'Pending';
|
||||
vehicleType?: string;
|
||||
manager?: string;
|
||||
customer?: string;
|
||||
isColdChain?: string;
|
||||
isTrailer?: string;
|
||||
department?: string;
|
||||
attendance?: string;
|
||||
subject?: string | null;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export type VehicleModalRequest =
|
||||
| {
|
||||
kind: 'weekly';
|
||||
type: FlowType;
|
||||
filters: { model: string; batch: string; location: string; source?: string };
|
||||
}
|
||||
| { kind: 'vehicles'; params: VehicleListRequestParams };
|
||||
|
||||
export interface FlowSelection {
|
||||
date: string;
|
||||
type: FlowType;
|
||||
}
|
||||
|
||||
export function formatLocalDateTime(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
export function formatLocalDate(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function addDays(date: Date, days: number): Date {
|
||||
const next = new Date(date);
|
||||
next.setDate(next.getDate() + days);
|
||||
return next;
|
||||
}
|
||||
|
||||
// 资产流转周报按周六至周五统计;周末查看时仍停留在刚结束的周五。
|
||||
export function getWeeklyFlowRange(referenceDate = new Date()): DateRange {
|
||||
const day = referenceDate.getDay();
|
||||
const end = day === 6
|
||||
? addDays(referenceDate, -1)
|
||||
: day === 0
|
||||
? addDays(referenceDate, -2)
|
||||
: addDays(referenceDate, 5 - day);
|
||||
return {
|
||||
start: formatLocalDate(addDays(end, -6)),
|
||||
end: formatLocalDate(end),
|
||||
};
|
||||
}
|
||||
|
||||
const WEEKLY_FLOW_TYPE_BY_CATEGORY: Partial<Record<VehicleModalCategory, FlowType>> = {
|
||||
Delivered: 'delivered',
|
||||
Returned: 'returned',
|
||||
Replaced: 'replaced',
|
||||
};
|
||||
|
||||
// Pending 不是周流转事件,必须继续走车辆列表接口以保留型号、批次和区域筛选。
|
||||
export function buildVehicleModalRequest(
|
||||
selection: VehicleModalSelection,
|
||||
subject: string | null,
|
||||
): VehicleModalRequest {
|
||||
const weeklyType = selection.category
|
||||
? WEEKLY_FLOW_TYPE_BY_CATEGORY[selection.category]
|
||||
: undefined;
|
||||
if (weeklyType) {
|
||||
return {
|
||||
kind: 'weekly',
|
||||
type: weeklyType,
|
||||
filters: {
|
||||
model: selection.model,
|
||||
batch: selection.batch,
|
||||
location: selection.location,
|
||||
source: selection.source,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const params: VehicleListRequestParams = {};
|
||||
if (selection.vehicleType) params.vehicleType = selection.vehicleType;
|
||||
if (selection.batch !== 'All') params.batch = selection.batch;
|
||||
if (selection.model !== 'All') params.model = selection.model;
|
||||
if (selection.location !== 'All') params.location = selection.location;
|
||||
if (selection.source) params.source = selection.source;
|
||||
if (selection.category === 'Inventory') params.category = 'Inventory';
|
||||
if (selection.category === 'Operating') params.category = 'Operating';
|
||||
if (selection.category === 'Pending') params.category = 'Pending';
|
||||
if (selection.manager) params.manager = selection.manager;
|
||||
if (selection.customer) params.customer = selection.customer;
|
||||
if (selection.department) params.department = selection.department;
|
||||
if (selection.attendance) params.attendance = selection.attendance;
|
||||
|
||||
if (!selection.type) {
|
||||
if (selection.isColdChain !== undefined) params.isColdChain = String(selection.isColdChain);
|
||||
if (selection.isTrailer !== undefined) params.isTrailer = String(selection.isTrailer);
|
||||
}
|
||||
|
||||
// 页面车型分组与列表接口的 vehicleType 取值并不完全一致,这里集中保留既有映射。
|
||||
if (selection.type === '4.5T') {
|
||||
if (selection.isColdChain === true) params.vehicleType = '4.5T冷链';
|
||||
if (selection.isColdChain === false) params.vehicleType = '4.5T普货';
|
||||
} else if (
|
||||
selection.type === '4.5T普货'
|
||||
|| selection.type === '4.5T冷链'
|
||||
|| selection.type === '18T'
|
||||
|| selection.type === '49T'
|
||||
|| selection.type === '挂车'
|
||||
|| selection.type === '其他'
|
||||
) {
|
||||
params.vehicleType = selection.type;
|
||||
} else if (selection.type === '其他车型') {
|
||||
if (selection.isTrailer === true) params.isTrailer = 'true';
|
||||
if (selection.isTrailer === false) params.vehicleType = '其他';
|
||||
}
|
||||
|
||||
return { kind: 'vehicles', params: { ...params, subject } };
|
||||
}
|
||||
|
||||
function uniqueNonEmpty(values: Array<string | null | undefined>): string[] {
|
||||
return Array.from(new Set(values.filter((value): value is string => Boolean(value))));
|
||||
}
|
||||
|
||||
function getDepartmentOrder(name: string): number {
|
||||
const match = name.match(/[一二三四五六七八九十]/);
|
||||
return match ? (CHINESE_NUMBER_ORDER[match[0]] || 99) : 99;
|
||||
}
|
||||
|
||||
export function filterInventoryStats(
|
||||
inventory: RegionalInventoryStats[],
|
||||
filters: InventoryFilters,
|
||||
): RegionalInventoryStats[] {
|
||||
return inventory.filter((item) => (
|
||||
(!filters.region || item.region === filters.region)
|
||||
&& (!filters.city || item.city === filters.city)
|
||||
&& (!filters.brand || item.brand === filters.brand)
|
||||
&& (!filters.type || item.type === filters.type)
|
||||
&& (!filters.model || item.model === filters.model)
|
||||
));
|
||||
}
|
||||
|
||||
export function groupInventoryByRegion(
|
||||
inventory: RegionalInventoryStats[],
|
||||
): Record<string, Record<string, RegionalInventoryStats[]>> {
|
||||
const result: Record<string, Record<string, RegionalInventoryStats[]>> = {};
|
||||
for (const item of inventory) {
|
||||
if (!result[item.region]) result[item.region] = {};
|
||||
if (!result[item.region][item.city]) result[item.region][item.city] = [];
|
||||
result[item.region][item.city].push(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function groupInventoryByModel(
|
||||
inventory: RegionalInventoryStats[],
|
||||
): Record<string, Record<string, RegionalInventoryStats[]>> {
|
||||
const raw: Record<string, Record<string, RegionalInventoryStats[]>> = {};
|
||||
for (const item of inventory) {
|
||||
if (!raw[item.type]) raw[item.type] = {};
|
||||
if (!raw[item.type][item.model]) raw[item.type][item.model] = [];
|
||||
raw[item.type][item.model].push(item);
|
||||
}
|
||||
|
||||
// 已知车型按报表约定排序,新增的未知车型保留接口返回时的首次出现顺序。
|
||||
const result: Record<string, Record<string, RegionalInventoryStats[]>> = {};
|
||||
for (const type of INVENTORY_TYPE_ORDER) {
|
||||
if (raw[type]) result[type] = raw[type];
|
||||
}
|
||||
for (const type of Object.keys(raw)) {
|
||||
if (!result[type]) result[type] = raw[type];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function deriveInventoryView(
|
||||
inventory: RegionalInventoryStats[],
|
||||
filters: InventoryFilters,
|
||||
modelTypeFilter: string,
|
||||
) {
|
||||
const filtered = filterInventoryStats(inventory, filters);
|
||||
const modelSource = modelTypeFilter
|
||||
? inventory.filter((item) => item.type === modelTypeFilter)
|
||||
: inventory;
|
||||
const types = uniqueNonEmpty(inventory.map((item) => item.type));
|
||||
|
||||
return {
|
||||
filtered,
|
||||
brands: uniqueNonEmpty(inventory.map((item) => item.brand)),
|
||||
regions: Array.from(new Set(inventory.map((item) => item.region))),
|
||||
cities: uniqueNonEmpty(inventory.map((item) => item.city)),
|
||||
types: types.sort(
|
||||
(left, right) => INVENTORY_TYPE_ORDER.indexOf(left) - INVENTORY_TYPE_ORDER.indexOf(right),
|
||||
),
|
||||
modelsForType: uniqueNonEmpty(modelSource.map((item) => item.model)),
|
||||
byRegion: groupInventoryByRegion(filtered),
|
||||
byModel: groupInventoryByModel(filtered),
|
||||
};
|
||||
}
|
||||
|
||||
export function deriveDepartmentView(departments: DeptGroup[], selectedManager: string) {
|
||||
const managers = departments
|
||||
.flatMap((department) => department.managers.map((manager) => manager.manager))
|
||||
.filter((value, index, all) => all.indexOf(value) === index)
|
||||
.sort();
|
||||
const groupedManagers = departments.map((department) => ({
|
||||
department: department.department,
|
||||
managers: department.managers.map((manager) => manager.manager),
|
||||
}));
|
||||
const managerStats = departments
|
||||
.flatMap((department) => department.managers)
|
||||
.filter((manager) => selectedManager === 'All' || manager.manager === selectedManager)
|
||||
.sort((left, right) => right.total - left.total);
|
||||
|
||||
return { managers, groupedManagers, managerStats };
|
||||
}
|
||||
|
||||
export function filterCustomerStats(
|
||||
customers: CustomerStats[],
|
||||
filters: CustomerFilters,
|
||||
): CustomerStats[] {
|
||||
return customers.filter((customer) => (
|
||||
(filters.customer.length === 0 || filters.customer.includes(customer.customer))
|
||||
&& (!filters.brand || customer.brand === filters.brand)
|
||||
&& (!filters.department || customer.department === filters.department)
|
||||
&& (!filters.manager || customer.manager === filters.manager)
|
||||
&& (!filters.region || customer.region === filters.region)
|
||||
));
|
||||
}
|
||||
|
||||
function groupCustomerManagers(
|
||||
customers: CustomerStats[],
|
||||
departments: DeptGroup[],
|
||||
): Array<{ department: string; managers: string[] }> {
|
||||
const departmentManagers = new Map<string, Set<string>>();
|
||||
for (const department of departments) {
|
||||
if (!departmentManagers.has(department.department)) {
|
||||
departmentManagers.set(department.department, new Set());
|
||||
}
|
||||
for (const manager of department.managers) {
|
||||
departmentManagers.get(department.department)!.add(manager.manager);
|
||||
}
|
||||
}
|
||||
for (const customer of customers) {
|
||||
if (!customer.manager || !customer.department) continue;
|
||||
if (!departmentManagers.has(customer.department)) {
|
||||
departmentManagers.set(customer.department, new Set());
|
||||
}
|
||||
departmentManagers.get(customer.department)!.add(customer.manager);
|
||||
}
|
||||
|
||||
return Array.from(departmentManagers.entries())
|
||||
.sort((left, right) => {
|
||||
const leftOrder = left[0] === '公务车' ? 100 : getDepartmentOrder(left[0]);
|
||||
const rightOrder = right[0] === '公务车' ? 100 : getDepartmentOrder(right[0]);
|
||||
return leftOrder - rightOrder;
|
||||
})
|
||||
.map(([department, managers]) => ({ department, managers: Array.from(managers) }));
|
||||
}
|
||||
|
||||
export function deriveCustomerView(
|
||||
customers: CustomerStats[],
|
||||
departments: DeptGroup[],
|
||||
filters: CustomerFilters,
|
||||
) {
|
||||
return {
|
||||
filtered: filterCustomerStats(customers, filters),
|
||||
brands: uniqueNonEmpty(customers.map((customer) => customer.brand)),
|
||||
departments: uniqueNonEmpty(customers.map((customer) => customer.department))
|
||||
.sort((left, right) => getDepartmentOrder(left) - getDepartmentOrder(right)),
|
||||
regions: Array.from(new Set(customers.map((customer) => customer.region))),
|
||||
cities: uniqueNonEmpty(customers.map((customer) => customer.city)),
|
||||
customerNames: uniqueNonEmpty(customers.map((customer) => customer.customer)),
|
||||
managersByDepartment: groupCustomerManagers(customers, departments),
|
||||
};
|
||||
}
|
||||
|
||||
export function deriveModalVehicleView(
|
||||
vehicles: VehicleListItem[],
|
||||
weeklyDetails: WeeklyDetailItem[],
|
||||
filters: ModalVehicleFilters,
|
||||
) {
|
||||
return {
|
||||
plates: uniqueNonEmpty(vehicles.map((vehicle) => vehicle.plateNumber || vehicle.vin)),
|
||||
models: uniqueNonEmpty(vehicles.map((vehicle) => vehicle.model)),
|
||||
brands: uniqueNonEmpty(vehicles.map((vehicle) => vehicle.brandLabel)),
|
||||
locations: uniqueNonEmpty(vehicles.map((vehicle) => vehicle.location)),
|
||||
filteredVehicles: vehicles.filter((vehicle) => (
|
||||
(!filters.plateNumber || (vehicle.plateNumber || vehicle.vin) === filters.plateNumber)
|
||||
&& (!filters.model || vehicle.model === filters.model)
|
||||
&& (!filters.brand || vehicle.brandLabel === filters.brand)
|
||||
&& (!filters.location || vehicle.location === filters.location)
|
||||
)),
|
||||
filteredWeeklyDetails: weeklyDetails.filter((detail) => (
|
||||
!filters.plateNumber || detail.plate_number === filters.plateNumber
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
export function selectFlowDetails(
|
||||
flowStats: FlowStatsResponse | null,
|
||||
selection: FlowSelection | null,
|
||||
) {
|
||||
if (!flowStats || !selection) return [];
|
||||
return flowStats.details.filter((detail) => (
|
||||
detail.date === selection.date && detail.type === selection.type
|
||||
));
|
||||
}
|
||||
|
||||
export function buildCustomerPieData(
|
||||
customers: CustomerStats[],
|
||||
view: 'region' | 'province',
|
||||
provinceData: Array<{ name: string; value: number }>,
|
||||
): Array<{ name: string; value: number }> {
|
||||
if (view === 'province') return provinceData;
|
||||
|
||||
const totals: Record<string, number> = {};
|
||||
for (const customer of customers) {
|
||||
totals[customer.region] = (totals[customer.region] || 0) + customer.total;
|
||||
}
|
||||
return Object.entries(totals)
|
||||
.map(([name, value]) => ({ name, value }))
|
||||
.sort((left, right) => right.value - left.value);
|
||||
}
|
||||
Reference in New Issue
Block a user