@@ -0,0 +1,676 @@
import { Hono } from 'hono' ;
import type {
Vehicle ,
SummaryData ,
TypeSummary ,
ModelSummary ,
BatchSummary ,
BatchGroup ,
InventoryTypeSummary ,
} from '../../types.js' ;
import { filterByPermission , maskCustomerNames , maskCustomerName } from '../../auth/permissions.js' ;
import type { AuthUser } from '../../auth/types.js' ;
import type { Context } from 'hono' ;
import {
INVENTORY_REGIONS ,
SUMMARY_TYPE_FILTERS ,
VEHICLE_TYPE_FILTERS ,
classifyVehicleType ,
compareModelNames ,
filterByLocation ,
mapInventoryRegion ,
mapMacroRegion ,
resolveCity ,
} from './model.js' ;
import {
compareDepartmentNames ,
countByType ,
getMileageStats ,
getStats ,
listDateRange ,
normalizeDateRange ,
} from './utils.js' ;
import { vehicleRepository , type FlowDetailRow , type WeeklyDetailType } from './repository.js' ;
export { mapRegion } from './model.js' ;
const app = new Hono ( ) ;
async function getVehiclesForUser ( c : Context ) : Promise < Vehicle [ ] > {
const all = await vehicleRepository . getVehicles ( ) ;
const user = ( ( c as any ) . get ? . ( 'user' ) || ( c as any ) . var ? . user ) as AuthUser | undefined ;
let list = user ? filterByPermission ( all , user ) : all ;
list = applySubjectFilter ( c , list ) ;
return maskCustomerNames ( list ) ;
}
// 归属公司筛选(所属公司 = vehicle_info.registered_ownership, 即 Vehicle.subjectOrg)
function getSubjectParam ( c : Context ) : string | null {
const raw = ( c . req . query ( 'subject' ) || '' ) . trim ( ) ;
return raw ? raw : null ;
}
function applySubjectFilter ( c : Context , vehicles : Vehicle [ ] ) : Vehicle [ ] {
const subject = getSubjectParam ( c ) ;
if ( ! subject ) return vehicles ;
return vehicles . filter ( ( v ) = > ( v . subjectOrg || '' ) === subject ) ;
}
// GET /api/vehicles/summary
app . get ( '/summary' , async ( c ) = > {
const [ vehicles , weeklyIds ] = await Promise . all ( [ getVehiclesForUser ( c ) , vehicleRepository . getWeeklyTruckIds ( ) ] ) ;
const vehicleIds = new Set ( vehicles . map ( v = > String ( v . id ) ) ) ;
const summary : SummaryData = {
totalAssets : vehicles.length ,
operating : {
total : vehicles.filter ( ( v ) = > v . status === 'Operating' && ( v . operationStatus === '1' || v . operationStatus === '2' ) ) . length ,
self : vehicles.filter ( ( v ) = > v . status === 'Operating' && v . operationStatus === '2' ) . length ,
leased : vehicles.filter ( ( v ) = > v . status === 'Operating' && v . operationStatus === '1' ) . length ,
public : vehicles . filter ( ( v ) = > v . status === 'Operating' && v . ownership === 'Public' ) . length ,
hanging : 0 ,
} ,
inventory : {
total : vehicles.filter ( ( v ) = > v . status === 'Inventory' ) . length ,
inStock : vehicles.filter ( ( v ) = > v . status === 'Inventory' ) . length ,
abnormal : vehicles.filter ( ( v ) = > v . status === 'Abnormal' ) . length ,
} ,
pendingDelivery : vehicles.filter ( ( v ) = > v . status === 'Pending' ) . length ,
weeklyNew : 0 ,
weeklyRemoved : 0 ,
weeklyDelivered : [ . . . weeklyIds . delivered ] . filter ( id = > vehicleIds . has ( id ) ) . length ,
weeklyReturned : [ . . . weeklyIds . returned ] . filter ( id = > vehicleIds . has ( id ) ) . length ,
weeklyReplaced : [ . . . weeklyIds . replaced ] . filter ( id = > vehicleIds . has ( id ) ) . length ,
} ;
return c . json ( summary ) ;
} ) ;
// GET /api/vehicles/by-type
app . get ( '/by-type' , async ( c ) = > {
const [ vehicles , weeklyIds ] = await Promise . all ( [ getVehiclesForUser ( c ) , vehicleRepository . getWeeklyTruckIds ( ) ] ) ;
const result : TypeSummary [ ] = SUMMARY_TYPE_FILTERS . map ( ( t ) = > {
const typeVehicles = vehicles . filter ( t . filter ) ;
const models = Array . from ( new Set ( typeVehicles . map ( ( v ) = > v . model ) ) ) ;
const modelSummaries : ModelSummary [ ] = models . map ( ( model ) = > {
const modelVehicles = typeVehicles . filter ( ( v ) = > v . model === model ) ;
const batches = Array . from ( new Set ( modelVehicles . map ( ( v ) = > v . contractNo || '未知' ) ) ) . filter ( Boolean ) ;
return {
model ,
. . . getStats ( modelVehicles , weeklyIds ) ,
batches : batches.map ( ( batch ) = > ( {
batch ,
. . . getStats ( modelVehicles . filter ( ( v ) = > ( v . contractNo || '未知' ) === batch ) , weeklyIds ) ,
} ) ) ,
} ;
} ) ;
const typeStats = getStats ( typeVehicles , weeklyIds ) ;
return {
type : t . name ,
totalAssets : typeVehicles.length ,
totalInventory : typeStats.inventory ,
totalAbnormal : typeStats.abnormal ,
totalOperating : typeStats.operating ,
inventoryRegions : typeStats.inventoryRegions ,
pending : typeStats.pending ,
weeklyDelivered : typeStats.weeklyDelivered ,
weeklyReturned : typeStats.weeklyReturned ,
weeklyReplaced : typeStats.weeklyReplaced ,
models : modelSummaries.sort ( ( a , b ) = > compareModelNames ( a . model , b . model ) ) ,
} ;
} ) ;
return c . json ( result ) ;
} ) ;
// GET /api/vehicles/by-batch
app . get ( '/by-batch' , async ( c ) = > {
const vehicles = await getVehiclesForUser ( c ) ;
const batches = Array . from ( new Set ( vehicles . map ( ( v ) = > v . contractNo || '未知' ) ) )
. filter ( Boolean )
. sort ( )
. reverse ( ) ;
const result : BatchGroup [ ] = batches . map ( ( batch ) = > {
const batchVehicles = vehicles . filter ( ( v ) = > ( v . contractNo || '未知' ) === batch ) ;
const models = Array . from ( new Set ( batchVehicles . map ( ( v ) = > v . model ) ) ) ;
return {
batch ,
. . . getStats ( batchVehicles ) ,
models : models.map ( ( model ) = > {
const modelVehicles = batchVehicles . filter ( ( v ) = > v . model === model ) ;
return {
model ,
type : modelVehicles [ 0 ] ? . type || '' ,
. . . getStats ( modelVehicles ) ,
} ;
} ) ,
} ;
} ) ;
return c . json ( result ) ;
} ) ;
// GET /api/vehicles/inventory-analysis — 库存分析,不设数据权限,对所有人开放
app . get ( '/inventory-analysis' , async ( c ) = > {
const vehicles = applySubjectFilter ( c , await vehicleRepository . getVehicles ( ) ) ;
const result : InventoryTypeSummary [ ] = SUMMARY_TYPE_FILTERS . map ( ( t ) = > {
const typeVehicles = vehicles . filter ( t . filter ) ;
const models = Array . from ( new Set ( typeVehicles . map ( ( v ) = > v . model ) ) ) ;
const modelData = models . map ( ( model ) = > {
const modelVehicles = typeVehicles . filter ( ( v ) = > v . model === model ) ;
const inventoryVehicles = modelVehicles . filter ( ( v ) = > v . status === 'Inventory' ) ;
return {
model ,
totalAssets : modelVehicles.length ,
totalInventory : inventoryVehicles.length ,
regions : INVENTORY_REGIONS.reduce (
( acc , reg ) = > {
acc [ reg ] = inventoryVehicles . filter ( ( v ) = > mapInventoryRegion ( v . location ) === reg ) . length ;
return acc ;
} ,
{ } as Record < string , number > ,
) ,
} ;
} ) ;
const typeInventory = typeVehicles . filter ( ( v ) = > v . status === 'Inventory' ) ;
return {
type : t . name ,
totalAssets : typeVehicles.length ,
totalInventory : typeInventory.length ,
models : modelData ,
regionSubtotals : INVENTORY_REGIONS.reduce (
( acc , reg ) = > {
acc [ reg ] = typeInventory . filter ( ( v ) = > mapInventoryRegion ( v . location ) === reg ) . length ;
return acc ;
} ,
{ } as Record < string , number > ,
) ,
} ;
} ) ;
return c . json ( result ) ;
} ) ;
// GET /api/vehicles/dept-stats — department & manager breakdown with mileage/attendance
app . get ( '/dept-stats' , async ( c ) = > {
const vehicles = await getVehiclesForUser ( c ) ;
const withManager = vehicles . filter ( ( v ) = > v . status === 'Operating' ) ;
// Query realtime day_mileage from tab_truck_remote_sync_realtime_info
const realtimeRows = await vehicleRepository . getRealtimeMileageRows ( ) ;
const todayMileageMap = new Map < string , number > ( ) ;
for ( const row of realtimeRows as any [ ] ) {
const plate = ( row . plate_number || '' ) . trim ( ) ;
if ( plate ) todayMileageMap . set ( plate , Number ( row . day_mileage ) || 0 ) ;
}
// 不在部门列表展示的用户(非业务员或管理账号)
const EXCLUDED_MANAGERS = new Set ( [ '超级用户' , '刘思宇' , '潘舒' , '黄卓华' , '许铮杰' ] ) ;
const deptMap = new Map < string , Map < string , Vehicle [ ] > > ( ) ;
for ( const v of withManager ) {
const isPublicServiceVehicle = v . model === '公务车/挂靠车' ;
const dept = isPublicServiceVehicle ? '公务车' : ( v . departmentName || '未分配部门' ) ;
const mgr = v . customerManager || '未分配' ;
if ( EXCLUDED_MANAGERS . has ( mgr ) ) continue ;
if ( ! deptMap . has ( dept ) ) deptMap . set ( dept , new Map ( ) ) ;
const mgrMap = deptMap . get ( dept ) ! ;
if ( ! mgrMap . has ( mgr ) ) mgrMap . set ( mgr , [ ] ) ;
mgrMap . get ( mgr ) ! . push ( v ) ;
}
const result = Array . from ( deptMap . entries ( ) ) . map ( ( [ department , mgrMap ] ) = > {
const allDeptVehicles = Array . from ( mgrMap . values ( ) ) . flat ( ) ;
const deptMileage = getMileageStats ( allDeptVehicles , todayMileageMap ) ;
const managers = Array . from ( mgrMap . entries ( ) )
. map ( ( [ manager , mvs ] ) = > ( { manager , department , . . . countByType ( mvs ) } ) )
. sort ( ( a , b ) = > b . total - a . total ) ;
return {
department ,
totalAssets : allDeptVehicles.length ,
operatingCount : allDeptVehicles.filter ( ( v ) = > ( todayMileageMap . get ( v . plateNumber ) || 0 ) > 0 ) . length ,
idleCount : allDeptVehicles.filter ( ( v ) = > ( todayMileageMap . get ( v . plateNumber ) || 0 ) === 0 ) . length ,
attendanceRate : deptMileage.attendanceRate ,
avgMileage : deptMileage.avgMileage ,
managers ,
} ;
} ) . sort ( ( a , b ) = > compareDepartmentNames ( a . department , b . department ) ) ;
return c . json ( result ) ;
} ) ;
// GET /api/vehicles/region-stats — macro-region with city drill-down
app . get ( '/region-stats' , async ( c ) = > {
const vehicles = await getVehiclesForUser ( c ) ;
const { customer , city : filterCity , region : filterRegion } = c . req . query ( ) ;
let operating = vehicles . filter ( ( v ) = > v . status === 'Operating' || v . status === 'Pending' ) ;
if ( customer ) operating = operating . filter ( ( v ) = > v . customerName === customer ) ;
if ( filterCity ) operating = operating . filter ( ( v ) = > resolveCity ( v . city , v . province ) === filterCity ) ;
if ( filterRegion ) operating = operating . filter ( ( v ) = > mapMacroRegion ( v . province , v . city ) === filterRegion ) ;
const regionCityMap = new Map < string , Map < string , Vehicle [ ] > > ( ) ;
for ( const v of operating ) {
const region = mapMacroRegion ( v . province , v . city ) ;
const city = resolveCity ( v . city , v . province ) ;
if ( ! regionCityMap . has ( region ) ) regionCityMap . set ( region , new Map ( ) ) ;
const cityMap = regionCityMap . get ( region ) ! ;
if ( ! cityMap . has ( city ) ) cityMap . set ( city , [ ] ) ;
cityMap . get ( city ) ! . push ( v ) ;
}
const getTypeBreakdown = ( vList : Vehicle [ ] ) = > {
const KNOWN = [ '4.5T' , '18T' , '49T' ] as const ;
const make = ( label : string , tv : Vehicle [ ] ) = > ( {
type : label ,
total : tv.length ,
operating : tv.filter ( ( v ) = > v . status === 'Operating' ) . length ,
inventory : tv.filter ( ( v ) = > v . status === 'Inventory' ) . length ,
abnormal : tv.filter ( ( v ) = > v . status === 'Abnormal' ) . length ,
pending : tv.filter ( ( v ) = > v . status === 'Pending' ) . length ,
customers : Array.from ( new Set ( tv . map ( ( v ) = > v . customerName ) . filter ( Boolean ) ) ) as string [ ] ,
} ) ;
const known = KNOWN . map ( ( type ) = > make ( type , vList . filter ( ( v ) = > v . type === type ) ) ) ;
const other = vList . filter ( ( v ) = > ! KNOWN . includes ( v . type as typeof KNOWN [ number ] ) ) ;
if ( other . length > 0 ) known . push ( make ( '其他' , other ) ) ;
return known . filter ( ( t ) = > t . total > 0 ) ;
} ;
const regionOrder = [ '华东' , '华南' , '华北' , '华中' , '西南' , '西北' , '其他' ] ;
const result = regionOrder
. filter ( ( r ) = > regionCityMap . has ( r ) )
. map ( ( region ) = > {
const cityMap = regionCityMap . get ( region ) ! ;
const allVehicles = Array . from ( cityMap . values ( ) ) . flat ( ) ;
const customers = Array . from ( new Set ( allVehicles . map ( ( v ) = > v . customerName ) . filter ( Boolean ) ) ) as string [ ] ;
const allCities = Array . from ( cityMap . entries ( ) )
. map ( ( [ city , cv ] ) = > ( {
city ,
totalAssets : cv.length ,
operatingCount : cv.filter ( ( v ) = > v . status === 'Operating' ) . length ,
pendingCount : cv.filter ( ( v ) = > v . status === 'Pending' ) . length ,
customers : Array.from ( new Set ( cv . map ( ( v ) = > v . customerName ) . filter ( Boolean ) ) ) as string [ ] ,
typeBreakdown : getTypeBreakdown ( cv ) ,
} ) )
. sort ( ( a , b ) = > b . totalAssets - a . totalAssets ) ;
// Top 8 cities + merge rest into "其他"
const topCities = allCities . slice ( 0 , 8 ) ;
const restCities = allCities . slice ( 8 ) ;
if ( restCities . length > 0 ) {
const restVehicles = restCities . flatMap ( ( c ) = > {
const key = c . city ;
return cityMap . get ( key ) || [ ] ;
} ) ;
topCities . push ( {
city : '其他' ,
totalAssets : restCities.reduce ( ( s , c ) = > s + c . totalAssets , 0 ) ,
operatingCount : restCities.reduce ( ( s , c ) = > s + c . operatingCount , 0 ) ,
pendingCount : restCities.reduce ( ( s , c ) = > s + ( c . pendingCount || 0 ) , 0 ) ,
customers : Array.from ( new Set ( restVehicles . map ( ( v ) = > v . customerName ) . filter ( Boolean ) ) ) as string [ ] ,
typeBreakdown : getTypeBreakdown ( restVehicles ) ,
} ) ;
}
const cities = topCities ;
return {
region ,
totalAssets : allVehicles.length ,
operatingCount : allVehicles.filter ( ( v ) = > v . status === 'Operating' ) . length ,
pendingCount : allVehicles.filter ( ( v ) = > v . status === 'Pending' ) . length ,
customers ,
typeBreakdown : getTypeBreakdown ( allVehicles ) ,
cities ,
} ;
} ) ;
return c . json ( result ) ;
} ) ;
// GET /api/vehicles/customer-stats — per-customer breakdown for operating vehicles
app . get ( '/customer-stats' , async ( c ) = > {
const vehicles = await getVehiclesForUser ( c ) ;
const operating = vehicles . filter ( ( v ) = > v . status === 'Operating' ) ;
const custMap = new Map < string , Vehicle [ ] > ( ) ;
for ( const v of operating ) {
const cust = v . customerName || '未分配客户' ;
if ( ! custMap . has ( cust ) ) custMap . set ( cust , [ ] ) ;
custMap . get ( cust ) ! . push ( v ) ;
}
const result = Array . from ( custMap . entries ( ) )
. map ( ( [ customer , cvs ] ) = > {
const first = cvs [ 0 ] ;
return {
customer ,
manager : first.customerManager || '' ,
brand : first.brandLabel || '' ,
department : first.departmentName || '' ,
region : mapMacroRegion ( first . province , first . city ) ,
city : first.city || '' ,
. . . countByType ( cvs ) ,
} ;
} )
. sort ( ( a , b ) = > b . total - a . total ) ;
return c . json ( result ) ;
} ) ;
// GET /api/vehicles/list — flat list with optional filters
app . get ( '/list' , async ( c ) = > {
const vehicles = await getVehiclesForUser ( c ) ;
const { batch , model , location , status , category , vehicleType , manager , customer , isColdChain , isTrailer , department , attendance } = c . req . query ( ) ;
let filtered = vehicles ;
// attendance filter: active = day_mileage > 0, idle = day_mileage = 0 (only for Operating vehicles)
if ( attendance === 'active' || attendance === 'idle' ) {
const realtimeRows = await vehicleRepository . getAttendanceMileageRows ( ) ;
const todayMap = new Map < string , number > ( ) ;
for ( const row of realtimeRows as any [ ] ) todayMap . set ( ( row . plate_number || '' ) . trim ( ) , Number ( row . day_mileage ) || 0 ) ;
filtered = filtered . filter ( ( v ) = > v . status === 'Operating' ) ;
if ( attendance === 'active' ) {
filtered = filtered . filter ( ( v ) = > ( todayMap . get ( v . plateNumber ) || 0 ) > 0 ) ;
} else {
filtered = filtered . filter ( ( v ) = > ( todayMap . get ( v . plateNumber ) || 0 ) === 0 ) ;
}
}
if ( vehicleType ) {
if ( VEHICLE_TYPE_FILTERS [ vehicleType ] ) {
filtered = filtered . filter ( VEHICLE_TYPE_FILTERS [ vehicleType ] ) ;
} else if ( vehicleType === '4.5T' ) {
filtered = filtered . filter ( ( v ) = > v . type === '4.5T' ) ;
} else {
filtered = filtered . filter ( ( v ) = > v . type === vehicleType ) ;
}
}
if ( batch && batch !== 'All' ) {
filtered = filtered . filter ( ( v ) = > ( v . contractNo || '未知' ) === batch ) ;
}
if ( model && model !== 'All' ) {
filtered = filtered . filter ( ( v ) = > v . model === model ) ;
}
if ( location && location !== 'All' ) {
filtered = filterByLocation ( filtered , location , c . req . query ( 'source' ) ) ;
}
if ( status && status !== 'All' ) {
filtered = filtered . filter ( ( v ) = > v . status === status ) ;
}
if ( category ) {
if ( category === 'Inventory' ) {
filtered = filtered . filter ( ( v ) = > v . status === 'Inventory' ) ;
} else if ( category === 'Abnormal' ) {
filtered = filtered . filter ( ( v ) = > v . status === 'Abnormal' ) ;
} else if ( category === 'Operating' ) {
filtered = filtered . filter ( ( v ) = > v . status === 'Operating' ) ;
} else if ( category === 'Pending' ) {
filtered = filtered . filter ( ( v ) = > v . status === 'Pending' ) ;
}
}
if ( manager ) {
filtered = filtered . filter ( ( v ) = > manager === '未分配' ? ! v.customerManager : v.customerManager === manager ) ;
}
if ( customer ) {
filtered = filtered . filter ( ( v ) = > customer === '未分配客户' ? ! v.customerName : v.customerName === customer ) ;
}
if ( department ) {
filtered = filtered . filter ( ( v ) = > {
if ( department === '公务车' ) return v . model === '公务车/挂靠车' ;
if ( department === '未分配部门' ) return v . model !== '公务车/挂靠车' && ! v . departmentName ;
return v . departmentName === department ;
} ) ;
}
if ( isColdChain !== undefined ) {
const wantCold = isColdChain === 'true' ;
filtered = filtered . filter ( ( v ) = > wantCold ? v . model . includes ( '冷链' ) : ! v . model . includes ( '冷链' ) ) ;
}
if ( isTrailer !== undefined ) {
const wantTrailer = isTrailer === 'true' ;
filtered = filtered . filter ( ( v ) = > wantTrailer ? ( v . type === '挂车' || v . model . includes ( '挂车' ) ) : ! ( v . type === '挂车' || v . model . includes ( '挂车' ) ) ) ;
}
return c . json (
filtered . map ( ( v ) = > ( {
id : v.id ,
plateNumber : v.plateNumber ,
vin : v.vin ,
type : v . type ,
model : v.model ,
location : v.location ,
province : v.province ,
city : v.city ,
status : v.status ,
ownership : v.ownership ,
rentCompany : v.rentCompany ,
contractNo : v.contractNo ,
customerName : v.customerName ,
subjectOrg : v.subjectOrg ,
departmentName : v.departmentName ,
customerManager : v.customerManager ,
brandLabel : v.brandLabel ,
orgName : v.orgName ,
} ) ) ,
) ;
} ) ;
// GET /api/vehicles/inventory-stats — 库存统计,不设数据权限,对所有人开放
app . get ( '/inventory-stats' , async ( c ) = > {
const vehicles = applySubjectFilter ( c , await vehicleRepository . getVehicles ( ) ) ;
const inventory = vehicles . filter ( ( v ) = > v . status === 'Inventory' ) ;
const TYPE_NAME_MAP : Record < string , string > = {
t4_5 : '4.5T普货' ,
t4_5c : '4.5T冷链' ,
t18 : '18T' ,
t49 : '49T' ,
trailer : '挂车' ,
other : '其他' ,
} ;
const groups = new Map < string , number > ( ) ;
for ( const v of inventory ) {
const typeCategory = classifyVehicleType ( v ) ;
const typeName = TYPE_NAME_MAP [ typeCategory ] ;
const region = mapMacroRegion ( v . province , v . city ) ;
const city = resolveCity ( v . city , v . province ) ;
const brand = v . brandLabel || '未知' ;
const model = v . model ;
const key = ` ${ region } | ${ city } | ${ brand } | ${ typeName } | ${ model } ` ;
groups . set ( key , ( groups . get ( key ) || 0 ) + 1 ) ;
}
const result = Array . from ( groups . entries ( ) )
. map ( ( [ key , quantity ] ) = > {
const [ region , city , brand , type , model ] = key . split ( '|' ) ;
return { region , city , brand , type , model , batch : model , quantity } ;
} )
. sort ( ( a , b ) = > b . quantity - a . quantity ) ;
return c . json ( result ) ;
} ) ;
// GET /api/vehicles/weekly-detail?type=delivered|returned|replaced|pending
// Optional filters: model, batch, location, source — 按缓存车辆集合的 truck_id 交集过滤
app . get ( '/weekly-detail' , async ( c ) = > {
const type = c . req . query ( 'type' ) ;
const { model , batch , location } = c . req . query ( ) ;
const source = c . req . query ( 'source' ) ;
const validTypes : WeeklyDetailType [ ] = [ 'delivered' , 'returned' , 'replaced' , 'pending' , 'new' ] ;
if ( ! validTypes . includes ( type as WeeklyDetailType ) ) {
return c . json ( [ ] ) ;
}
let result = await vehicleRepository . getWeeklyDetailRows ( type as WeeklyDetailType ) ;
// 按型号/批次/区域过滤:借助缓存车辆集,取 truck_id 交集
const hasModelFilter = model && model !== 'All' ;
const hasBatchFilter = batch && batch !== 'All' ;
const hasLocationFilter = location && location !== 'All' ;
if ( hasModelFilter || hasBatchFilter || hasLocationFilter ) {
const vehicles = await getVehiclesForUser ( c ) ;
let pool2 = vehicles ;
if ( hasModelFilter ) pool2 = pool2 . filter ( ( v ) = > v . model === model ) ;
if ( hasBatchFilter ) pool2 = pool2 . filter ( ( v ) = > ( v . contractNo || '未知' ) === batch ) ;
if ( hasLocationFilter ) pool2 = filterByLocation ( pool2 , location , source ) ;
const truckSet = new Set ( pool2 . map ( ( v ) = > String ( v . id ) ) ) ;
result = result . filter ( ( r : any ) = > truckSet . has ( String ( r . truck_id ) ) ) ;
}
const masked = result . map ( r = > ( { . . . r , customer_name : maskCustomerName ( r . customer_name ) } ) ) ;
return c . json ( masked ) ;
} ) ;
// GET /api/vehicles/flow-stats?start=YYYY-MM-DD&end=YYYY-MM-DD
// 资产流转日报:按运维签章时间统计业务车次;交车包含关联替换单的记录,替换作为其中的子项,同车多笔业务分别累计。
app . get ( '/flow-stats' , async ( c ) = > {
const { start , end } = normalizeDateRange ( c . req . query ( 'start' ) , c . req . query ( 'end' ) ) ;
const allowedVehicles = await getVehiclesForUser ( c ) ;
const allowedTruckIds = new Set ( allowedVehicles . map ( ( v ) = > String ( v . id ) ) ) ;
const rows = await vehicleRepository . getFlowDetailRows ( start , end ) ;
const details = ( rows as FlowDetailRow [ ] )
. filter ( ( row ) = > allowedTruckIds . has ( String ( row . truck_id ) ) )
. map ( ( row ) = > ( {
id : row.id ,
type : row . type ,
typeLabel : row.type_label ,
date : row.stat_date ,
truckId : row.truck_id ,
plateNumber : row.plate_number ,
eventTime : row.event_time ,
submitTime : row.submit_time ,
department : row.department || '' ,
manager : row.manager || '' ,
customerName : maskCustomerName ( row . customer_name ) ,
} ) ) ;
const dailyMap = new Map < string , { date : string ; delivered : number ; returned : number ; replaced : number ; total : number } > ( ) ;
for ( const date of listDateRange ( start , end ) ) dailyMap . set ( date , { date , delivered : 0 , returned : 0 , replaced : 0 , total : 0 } ) ;
for ( const item of details ) {
const stat = dailyMap . get ( item . date ) ;
if ( ! stat ) continue ;
stat [ item . type ] += 1 ;
if ( item . type === 'replaced' ) stat . delivered += 1 ;
stat . total += 1 ;
}
const daily = Array . from ( dailyMap . values ( ) ) ;
const totals = daily . reduce (
( acc , item ) = > ( {
delivered : acc.delivered + item . delivered ,
returned : acc.returned + item . returned ,
replaced : acc.replaced + item . replaced ,
total : acc.total + item . total ,
} ) ,
{ delivered : 0 , returned : 0 , replaced : 0 , total : 0 } ,
) ;
return c . json ( { start , end , daily , totals , details } ) ;
} ) ;
// GET /api/vehicles/subjects — 归属公司列表(含台数预览),用于顶部筛选下拉
app . get ( '/subjects' , async ( c ) = > {
const all = await vehicleRepository . getVehicles ( ) ;
const user = ( ( c as any ) . get ? . ( 'user' ) || ( c as any ) . var ? . user ) as AuthUser | undefined ;
const visible = user ? filterByPermission ( all , user ) : all ;
const map = new Map < string , { total : number ; inventory : number ; operating : number } > ( ) ;
for ( const v of visible ) {
const name = ( v . subjectOrg || '' ) . trim ( ) ;
if ( ! name ) continue ;
if ( ! map . has ( name ) ) map . set ( name , { total : 0 , inventory : 0 , operating : 0 } ) ;
const s = map . get ( name ) ! ;
s . total += 1 ;
if ( v . status === 'Inventory' ) s . inventory += 1 ;
if ( v . status === 'Operating' ) s . operating += 1 ;
}
const result = Array . from ( map . entries ( ) )
. map ( ( [ name , stats ] ) = > ( { name , . . . stats } ) )
. sort ( ( a , b ) = > b . total - a . total ) ;
return c . json ( result ) ;
} ) ;
// GET /api/vehicles/refresh — force cache refresh
app . get ( '/refresh' , async ( c ) = > {
vehicleRepository . invalidateRefreshCaches ( ) ;
const vehicles = await getVehiclesForUser ( c ) ;
return c . json ( { message : 'Cache refreshed' , count : vehicles.length } ) ;
} ) ;
// GET /api/vehicles/debug — debug weekly date range and raw counts
app . get ( '/debug' , async ( c ) = > {
const { dateRange , deliveredAll , deliveredRecent , latestTake , returnedRecent , latestReturn } = await vehicleRepository . getWeeklyDebugData ( ) ;
return c . json ( {
weekRange : dateRange ,
delivered : { total : deliveredAll [ 0 ] ? . cnt , thisWeek : deliveredRecent [ 0 ] ? . cnt , latestDate : latestTake [ 0 ] ? . latest } ,
returned : { thisWeek : returnedRecent [ 0 ] ? . cnt , latestDate : latestReturn [ 0 ] ? . latest } ,
} ) ;
} ) ;
// GET /api/vehicles/region-chart — aggregated chart data with top N + "其他"
app . get ( '/region-chart' , async ( c ) = > {
const vehicles = await getVehiclesForUser ( c ) ;
const operating = vehicles . filter ( ( v ) = > v . status === 'Operating' ) ;
const groupBy = c . req . query ( 'groupBy' ) || 'region' ; // 'region' | 'province'
const source = c . req . query ( 'source' ) || 'realtime' ; // 'realtime' | 'vehicle'
const top = Number ( c . req . query ( 'top' ) ) || 8 ;
let counts : Map < string , number > ;
if ( groupBy === 'province' ) {
counts = new Map < string , number > ( ) ;
if ( source === 'vehicle' ) {
// Use vehicle table's own province field
for ( const v of operating ) {
const prov = ( v . province || '' ) . replace ( /省|市$/ , '' ) . trim ( ) || '未知' ;
counts . set ( prov , ( counts . get ( prov ) || 0 ) + 1 ) ;
}
} else {
// Use realtime table province
const rows = await vehicleRepository . getRealtimeProvinceRows ( ) ;
const plateProvince = new Map < string , string > ( ) ;
for ( const row of rows as any [ ] ) {
const plate = ( row . plate_number || '' ) . trim ( ) ;
const prov = ( row . province || '' ) . replace ( /省|市$/ , '' ) . trim ( ) ;
if ( plate && prov ) plateProvince . set ( plate , prov ) ;
}
for ( const v of operating ) {
const prov = plateProvince . get ( v . plateNumber ) || '未知' ;
counts . set ( prov , ( counts . get ( prov ) || 0 ) + 1 ) ;
}
}
} else {
counts = new Map < string , number > ( ) ;
for ( const v of operating ) {
const key = mapMacroRegion ( v . province , v . city ) ;
counts . set ( key , ( counts . get ( key ) || 0 ) + 1 ) ;
}
}
// 分离"其他"和"未知",对剩余排序取 Top N,其余全部合入"其他"
const otherCount = ( counts . get ( '其他' ) || 0 ) + ( counts . get ( '未知' ) || 0 ) ;
counts . delete ( '其他' ) ;
counts . delete ( '未知' ) ;
const sorted = Array . from ( counts . entries ( ) )
. map ( ( [ name , value ] ) = > ( { name , value } ) )
. sort ( ( a , b ) = > b . value - a . value ) ;
const result = sorted . slice ( 0 , top ) ;
const restTotal = sorted . slice ( top ) . reduce ( ( s , item ) = > s + item . value , 0 ) + otherCount ;
if ( restTotal > 0 ) result . push ( { name : '其他' , value : restTotal } ) ;
return c . json ( result ) ;
} ) ;
export default app ;