This commit is contained in:
@@ -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