This commit is contained in:
@@ -65,6 +65,21 @@ interface MileageRow {
|
||||
source: string;
|
||||
}
|
||||
|
||||
interface DailyMileageRow {
|
||||
plate: string;
|
||||
vin: string | null;
|
||||
date: string;
|
||||
daily_km: string | number | null;
|
||||
source: string | null;
|
||||
}
|
||||
|
||||
export interface RangeMileageResult {
|
||||
vehicles: CachedVehicle[];
|
||||
dailyTotals: { date: string; totalKm: number }[];
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
interface TargetRow {
|
||||
id: number;
|
||||
target_name: string;
|
||||
@@ -160,31 +175,32 @@ function mergeVehicles(
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(mileageMap.values()).map(m => {
|
||||
const info = infoMap.get(m.plate);
|
||||
const dailyKm = Number(m.daily_km) || 0;
|
||||
const source = m.source || 'NONE';
|
||||
const gpsTotal = m.total_km !== null ? Number(m.total_km) : null;
|
||||
const latestPgTotal = latestPgTotalMap.get(m.plate);
|
||||
const bizTotal = bizTotalMap.get(m.plate);
|
||||
return Array.from(infoMap.values()).map(info => {
|
||||
const m = mileageMap.get(info.plate);
|
||||
const plate = info.plate;
|
||||
const dailyKm = Number(m?.daily_km) || 0;
|
||||
const source = m?.source || 'NONE';
|
||||
const gpsTotal = m?.total_km != null ? Number(m.total_km) : null;
|
||||
const latestPgTotal = latestPgTotalMap.get(plate);
|
||||
const bizTotal = bizTotalMap.get(plate);
|
||||
return {
|
||||
plate: m.plate,
|
||||
vin: m.vin,
|
||||
plate,
|
||||
vin: m?.vin || info.vin || '',
|
||||
dailyKm,
|
||||
totalKm: gpsTotal !== null ? gpsTotal : (latestPgTotal ?? bizTotal ?? null),
|
||||
source,
|
||||
isOnline: source !== 'NONE' && dailyKm > 0,
|
||||
isDataSynced: source !== 'NONE',
|
||||
customer: info?.customer || null,
|
||||
department: info?.department || null,
|
||||
manager: info?.manager || null,
|
||||
managerId: info?.manager_id || null,
|
||||
rentStatus: info?.rent_status || null,
|
||||
entity: info?.entity || null,
|
||||
project: info?.project || null,
|
||||
region: regionMap[m.plate] || null,
|
||||
targetNames: targetNamesByPlate.get(m.plate) || [],
|
||||
yesterdayKm: yesterdayMap.get(m.plate) || 0,
|
||||
customer: info.customer || null,
|
||||
department: info.department || null,
|
||||
manager: info.manager || null,
|
||||
managerId: info.manager_id || null,
|
||||
rentStatus: info.rent_status || null,
|
||||
entity: info.entity || null,
|
||||
project: info.project || null,
|
||||
region: regionMap[plate] || null,
|
||||
targetNames: targetNamesByPlate.get(plate) || [],
|
||||
yesterdayKm: yesterdayMap.get(plate) || 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -281,6 +297,110 @@ export async function queryDateMileage(dateStr: string): Promise<CachedVehicle[]
|
||||
);
|
||||
}
|
||||
|
||||
function datesBetween(start: string, end: string): string[] {
|
||||
const result: string[] = [];
|
||||
const [sy, sm, sd] = start.split('-').map(Number);
|
||||
const [ey, em, ed] = end.split('-').map(Number);
|
||||
const cursor = new Date(sy, sm - 1, sd);
|
||||
const last = new Date(ey, em - 1, ed);
|
||||
cursor.setHours(0, 0, 0, 0);
|
||||
last.setHours(0, 0, 0, 0);
|
||||
while (cursor <= last) {
|
||||
result.push(`${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, '0')}-${String(cursor.getDate()).padStart(2, '0')}`);
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function queryRangeMileage(startDate: string, endDate: string): Promise<RangeMileageResult> {
|
||||
const days = datesBetween(startDate, endDate);
|
||||
const [dailyRows, yesterdayRows, infoMap, targetRows, bizTotalMap, latestPgTotalMap] = await Promise.all([
|
||||
mileagePool.execute(
|
||||
`SELECT plate,
|
||||
DATE_FORMAT(stat_date, '%Y-%m-%d') AS date,
|
||||
vin,
|
||||
daily_km,
|
||||
source
|
||||
FROM v_vehicle_daily_stats
|
||||
WHERE stat_date >= ? AND stat_date <= ?
|
||||
ORDER BY stat_date, plate`,
|
||||
[startDate, endDate]
|
||||
).then(([r]) => r as DailyMileageRow[]),
|
||||
mileagePool.execute(
|
||||
'SELECT plate, daily_km FROM v_vehicle_daily_stats WHERE stat_date = DATE_SUB(?, INTERVAL 1 DAY)',
|
||||
[startDate]
|
||||
).then(([r]) => r as { plate: string; daily_km: string }[]),
|
||||
fetchVehicleInfoMap(),
|
||||
fetchTargetRows(),
|
||||
fetchBizTotalMileageMap(),
|
||||
fetchLatestPgTotalMileageMap(endDate),
|
||||
]);
|
||||
|
||||
const perVehicleDaily = new Map<string, Record<string, number>>();
|
||||
const perVehicleSum = new Map<string, { plate: string; vin: string; daily_km: string; total_km: null; source: string }>();
|
||||
const dailyTotals = new Map<string, number>();
|
||||
const bestDailyRows = new Map<string, DailyMileageRow>();
|
||||
|
||||
for (const day of days) dailyTotals.set(day, 0);
|
||||
|
||||
for (const row of dailyRows) {
|
||||
const key = `${row.plate}\u0000${row.date}`;
|
||||
const km = Math.max(0, Number(row.daily_km) || 0);
|
||||
const existing = bestDailyRows.get(key);
|
||||
if (!existing || km > Math.max(0, Number(existing.daily_km) || 0)) {
|
||||
bestDailyRows.set(key, row);
|
||||
}
|
||||
}
|
||||
|
||||
for (const row of bestDailyRows.values()) {
|
||||
const km = Math.max(0, Number(row.daily_km) || 0);
|
||||
const date = row.date;
|
||||
const plate = row.plate;
|
||||
dailyTotals.set(date, (dailyTotals.get(date) || 0) + km);
|
||||
|
||||
const daily = perVehicleDaily.get(plate) || {};
|
||||
daily[date] = km;
|
||||
perVehicleDaily.set(plate, daily);
|
||||
|
||||
const existing = perVehicleSum.get(plate);
|
||||
perVehicleSum.set(plate, {
|
||||
plate,
|
||||
vin: existing?.vin || row.vin || '',
|
||||
daily_km: String((Number(existing?.daily_km) || 0) + km),
|
||||
total_km: null,
|
||||
source: existing?.source !== 'NONE' && existing?.source ? existing.source : (row.source || 'NONE'),
|
||||
});
|
||||
}
|
||||
|
||||
const yesterdayMap = new Map<string, number>();
|
||||
for (const r of yesterdayRows) {
|
||||
const km = Number(r.daily_km) || 0;
|
||||
const existing = yesterdayMap.get(r.plate) || 0;
|
||||
if (km > existing) yesterdayMap.set(r.plate, km);
|
||||
}
|
||||
|
||||
const vehicles = mergeVehicles(
|
||||
Array.from(perVehicleSum.values()),
|
||||
infoMap,
|
||||
yesterdayMap,
|
||||
bizTotalMap,
|
||||
latestPgTotalMap,
|
||||
buildPlateTargetNamesMap(targetRows),
|
||||
).map(vehicle => {
|
||||
const dailyMileage = perVehicleDaily.get(vehicle.plate) || {};
|
||||
const completedDailyMileage: Record<string, number> = {};
|
||||
for (const day of days) completedDailyMileage[day] = dailyMileage[day] || 0;
|
||||
return { ...vehicle, dailyMileage: completedDailyMileage };
|
||||
});
|
||||
|
||||
return {
|
||||
vehicles,
|
||||
dailyTotals: days.map(date => ({ date, totalKm: dailyTotals.get(date) || 0 })),
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDateFilters(vehicles: CachedVehicle[]): MonitoringFilters {
|
||||
return buildFilters(vehicles, monitoringCache?.filters.targetNames || []);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Hono } from 'hono';
|
||||
import { getCache, queryDateMileage, buildDateFilters } from './cache.js';
|
||||
import { getCache, queryDateMileage, queryRangeMileage, buildDateFilters } from './cache.js';
|
||||
import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js';
|
||||
import type { AuthUser } from '../../auth/types.js';
|
||||
import type { CachedVehicle, MonitoringFilters, MonitoringResponse } from './types.js';
|
||||
@@ -64,12 +64,40 @@ function parseTargetNames(reqUrl: string): string[] {
|
||||
return Array.from(new Set(names));
|
||||
}
|
||||
|
||||
function parseYmd(value: string): Date | null {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
||||
if (!match) return null;
|
||||
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return Number.isFinite(date.getTime()) ? date : null;
|
||||
}
|
||||
|
||||
function fmtYmd(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function normalizeRange(startQuery: string, endQuery: string): { start: string; end: string } | null {
|
||||
if (!startQuery && !endQuery) return null;
|
||||
const start = parseYmd(startQuery || endQuery);
|
||||
const end = parseYmd(endQuery || startQuery);
|
||||
if (!start || !end) return null;
|
||||
const a = start <= end ? start : end;
|
||||
let b = start <= end ? end : start;
|
||||
const span = Math.round((b.getTime() - a.getTime()) / 86400000) + 1;
|
||||
if (span > 366) {
|
||||
b = new Date(a);
|
||||
b.setDate(a.getDate() + 365);
|
||||
}
|
||||
return { start: fmtYmd(a), end: fmtYmd(b) };
|
||||
}
|
||||
|
||||
app.get('/', async (c) => {
|
||||
const sortBy = c.req.query('sortBy') || 'today';
|
||||
const sortOrder = c.req.query('sortOrder') || 'desc';
|
||||
const limit = Number(c.req.query('limit')) || 50;
|
||||
const page = Number(c.req.query('page')) || 1;
|
||||
const date = c.req.query('date') || '';
|
||||
const range = normalizeRange(c.req.query('startDate') || '', c.req.query('endDate') || '');
|
||||
|
||||
const filterParams = {
|
||||
search: c.req.query('search') || '',
|
||||
@@ -88,8 +116,21 @@ app.get('/', async (c) => {
|
||||
|
||||
let allVehicles: CachedVehicle[];
|
||||
let filters: MonitoringFilters;
|
||||
let rangeDailyTotals: { date: string; totalKm: number }[] | undefined;
|
||||
let dateRange: { start: string; end: string } | undefined;
|
||||
|
||||
if (date) {
|
||||
if (range) {
|
||||
try {
|
||||
const result = await queryRangeMileage(range.start, range.end);
|
||||
allVehicles = result.vehicles;
|
||||
rangeDailyTotals = result.dailyTotals;
|
||||
dateRange = { start: result.start, end: result.end };
|
||||
filters = buildDateFilters(allVehicles);
|
||||
} catch (e: unknown) {
|
||||
console.error('monitoring range query error:', e);
|
||||
return c.json(EMPTY_RESPONSE, 500);
|
||||
}
|
||||
} else if (date) {
|
||||
try {
|
||||
allVehicles = await queryDateMileage(date);
|
||||
filters = buildDateFilters(allVehicles);
|
||||
@@ -118,6 +159,12 @@ app.get('/', async (c) => {
|
||||
}
|
||||
|
||||
const filtered = applyFilters(allVehicles, filterParams);
|
||||
if (rangeDailyTotals && filtered.length !== allVehicles.length) {
|
||||
rangeDailyTotals = rangeDailyTotals.map(item => ({
|
||||
...item,
|
||||
totalKm: filtered.reduce((sum, vehicle) => sum + (vehicle.dailyMileage?.[item.date] || 0), 0),
|
||||
}));
|
||||
}
|
||||
|
||||
const stats = {
|
||||
totalToday: filtered.reduce((sum, v) => sum + v.dailyKm, 0),
|
||||
@@ -140,10 +187,12 @@ app.get('/', async (c) => {
|
||||
vehicles: maskCustomerNames(paged),
|
||||
stats,
|
||||
filters,
|
||||
rangeDailyTotals,
|
||||
dateRange,
|
||||
total,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
updatedAt: date || getCache()?.updatedAt || new Date().toISOString(),
|
||||
updatedAt: dateRange?.end || date || getCache()?.updatedAt || new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface CachedVehicle {
|
||||
plate: string;
|
||||
vin: string;
|
||||
dailyKm: number;
|
||||
dailyMileage?: Record<string, number>;
|
||||
totalKm: number | null;
|
||||
source: string;
|
||||
isOnline: boolean;
|
||||
@@ -60,6 +61,8 @@ export interface MonitoringResponse {
|
||||
vehicles: CachedVehicle[];
|
||||
stats: MonitoringStats;
|
||||
filters: MonitoringFilters;
|
||||
rangeDailyTotals?: { date: string; totalKm: number }[];
|
||||
dateRange?: { start: string; end: string };
|
||||
total: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
@@ -69,6 +72,7 @@ export interface MonitoringResponse {
|
||||
/** 车辆关联信息(从 lingniu_prod 查出的原始行) */
|
||||
export interface VehicleInfoRow {
|
||||
plate: string;
|
||||
vin: string | null;
|
||||
customer: string | null;
|
||||
department: string | null;
|
||||
manager: string | null;
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { VehicleInfoRow } from './types.js';
|
||||
/** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目) */
|
||||
export const VEHICLE_INFO_SQL = `SELECT
|
||||
vi.plate_number AS plate,
|
||||
vi.vin AS vin,
|
||||
COALESCE(c.customer_name, vor.customer_name, ci.customer_name) AS customer,
|
||||
COALESCE(c.business_department_name, vor.business_dept) AS department,
|
||||
COALESCE(c.business_manager_name, vor.business_manager) AS manager,
|
||||
|
||||
@@ -468,6 +468,59 @@ function getStats(list: Vehicle[], weeklyIds?: WeeklyTruckIds) {
|
||||
const WEEK_START_SQL = `DATE_SUB(CURDATE(), INTERVAL (WEEKDAY(CURDATE()) + 2) % 7 DAY)`;
|
||||
const WEEK_END_SQL = `DATE_ADD(${WEEK_START_SQL}, INTERVAL 7 DAY)`;
|
||||
|
||||
type FlowType = 'delivered' | 'returned' | 'replaced';
|
||||
|
||||
interface FlowDetailRow {
|
||||
id: string;
|
||||
type: FlowType;
|
||||
type_label: string;
|
||||
stat_date: string;
|
||||
truck_id: string;
|
||||
plate_number: string;
|
||||
event_time: string | null;
|
||||
submit_time: string | null;
|
||||
department: string | null;
|
||||
manager: string | null;
|
||||
customer_name: string | null;
|
||||
}
|
||||
|
||||
function formatDateOnly(value: Date): string {
|
||||
const y = value.getFullYear();
|
||||
const m = String(value.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(value.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
function isDateParam(value: string | undefined | null): value is string {
|
||||
return Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value));
|
||||
}
|
||||
|
||||
function addDateDays(date: string, days: number): string {
|
||||
const d = new Date(`${date}T00:00:00`);
|
||||
d.setDate(d.getDate() + days);
|
||||
return formatDateOnly(d);
|
||||
}
|
||||
|
||||
function listDateRange(start: string, end: string): string[] {
|
||||
const dates: string[] = [];
|
||||
let cursor = start;
|
||||
while (cursor <= end && dates.length <= 370) {
|
||||
dates.push(cursor);
|
||||
cursor = addDateDays(cursor, 1);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
function normalizeDateRange(startRaw: string | undefined, endRaw: string | undefined): { start: string; end: string } {
|
||||
const today = formatDateOnly(new Date());
|
||||
const defaultStart = addDateDays(today, -29);
|
||||
let start = isDateParam(startRaw) ? startRaw : defaultStart;
|
||||
let end = isDateParam(endRaw) ? endRaw : today;
|
||||
if (start > end) [start, end] = [end, start];
|
||||
if (listDateRange(start, end).length > 370) start = addDateDays(end, -369);
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
interface WeeklyStats {
|
||||
pendingDelivery: number;
|
||||
weeklyNew: number;
|
||||
@@ -1122,6 +1175,141 @@ app.get('/weekly-detail', async (c) => {
|
||||
return c.json(masked);
|
||||
});
|
||||
|
||||
// GET /api/vehicles/flow-stats?start=YYYY-MM-DD&end=YYYY-MM-DD
|
||||
// 资产流转日报:按提交时间(create_time)统计交车、还车、替换车,并返回可点击明细。
|
||||
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 sql = `
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
CONCAT('delivered-', dv.id) AS id,
|
||||
'delivered' AS type,
|
||||
'交车' AS type_label,
|
||||
DATE_FORMAT(dv.create_time, '%Y-%m-%d') AS stat_date,
|
||||
CAST(dv.vehicle_id AS CHAR) AS truck_id,
|
||||
dv.plate_number,
|
||||
DATE_FORMAT(dv.delivery_time, '%Y-%m-%d %H:%i:%s') AS event_time,
|
||||
DATE_FORMAT(dv.create_time, '%Y-%m-%d %H:%i:%s') AS submit_time,
|
||||
c.business_department_name AS department,
|
||||
c.business_manager_name AS manager,
|
||||
COALESCE(dts.customer_name, c.customer_name) AS customer_name
|
||||
FROM delivery_vehicle dv
|
||||
LEFT JOIN delivery_task_subject dts
|
||||
ON dts.id = dv.delivery_task_subject_id
|
||||
AND dts.del_flag = '0'
|
||||
LEFT JOIN vehicle_lease_contract_info c
|
||||
ON c.order_id = dv.contract_id
|
||||
AND c.del_flag = '0'
|
||||
WHERE dv.del_flag = '0'
|
||||
AND dv.vehicle_id IS NOT NULL
|
||||
AND dv.create_time IS NOT NULL
|
||||
AND dv.delivery_status IN (2,3,5)
|
||||
AND dv.create_time >= ?
|
||||
AND dv.create_time < DATE_ADD(?, INTERVAL 1 DAY)
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
CONCAT('returned-', r.id) AS id,
|
||||
'returned' AS type,
|
||||
'还车' AS type_label,
|
||||
DATE_FORMAT(r.create_time, '%Y-%m-%d') AS stat_date,
|
||||
CAST(r.vehicle_id AS CHAR) AS truck_id,
|
||||
r.plate_number,
|
||||
DATE_FORMAT(r.arrival_time, '%Y-%m-%d %H:%i:%s') AS event_time,
|
||||
DATE_FORMAT(r.create_time, '%Y-%m-%d %H:%i:%s') AS submit_time,
|
||||
c.business_department_name AS department,
|
||||
c.business_manager_name AS manager,
|
||||
COALESCE(dts.customer_name, c.customer_name) AS customer_name
|
||||
FROM return_vehicle_task r
|
||||
LEFT JOIN delivery_task_subject dts
|
||||
ON dts.id = r.delivery_task_subject_id
|
||||
AND dts.del_flag = '0'
|
||||
LEFT JOIN vehicle_lease_contract_info c
|
||||
ON c.order_id = r.contract_id
|
||||
AND c.del_flag = '0'
|
||||
WHERE r.del_flag = '0'
|
||||
AND r.vehicle_id IS NOT NULL
|
||||
AND r.create_time IS NOT NULL
|
||||
AND r.status IN (2,3,5)
|
||||
AND r.create_time >= ?
|
||||
AND r.create_time < DATE_ADD(?, INTERVAL 1 DAY)
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
CONCAT('replaced-', vr.id) AS id,
|
||||
'replaced' AS type,
|
||||
'替换' AS type_label,
|
||||
DATE_FORMAT(vr.create_time, '%Y-%m-%d') AS stat_date,
|
||||
CAST(vr.new_vehicle_id AS CHAR) AS truck_id,
|
||||
vr.new_vehicle_plate AS plate_number,
|
||||
DATE_FORMAT(vr.replace_time, '%Y-%m-%d %H:%i:%s') AS event_time,
|
||||
DATE_FORMAT(vr.create_time, '%Y-%m-%d %H:%i:%s') AS submit_time,
|
||||
c.business_department_name AS department,
|
||||
c.business_manager_name AS manager,
|
||||
COALESCE(dts.customer_name, c.customer_name) AS customer_name
|
||||
FROM vehicle_replacement vr
|
||||
LEFT JOIN delivery_task_subject dts
|
||||
ON dts.id = vr.delivery_task_subject_id
|
||||
AND dts.del_flag = '0'
|
||||
LEFT JOIN vehicle_lease_contract_info c
|
||||
ON c.id = vr.contract_id
|
||||
AND c.del_flag = '0'
|
||||
WHERE vr.del_flag = '0'
|
||||
AND vr.new_vehicle_id IS NOT NULL
|
||||
AND vr.create_time IS NOT NULL
|
||||
AND vr.status = 20
|
||||
AND vr.create_time >= ?
|
||||
AND vr.create_time < DATE_ADD(?, INTERVAL 1 DAY)
|
||||
) flow
|
||||
ORDER BY flow.submit_time DESC
|
||||
`;
|
||||
|
||||
const [rows] = await pool.query<any[]>(sql, [start, end, start, end, 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;
|
||||
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 getVehicles();
|
||||
|
||||
Reference in New Issue
Block a user