Undo the full-tree rollback in 6ea1b3d and restore the pre-rollback v1.2.0 code. Preserve the new asset flow rules, abnormal inventory separation, range drilldowns and date picker; adapt the regression test to the restored routes layout.
224 lines
9.2 KiB
TypeScript
224 lines
9.2 KiB
TypeScript
import {
|
|
loadAssessmentFirstYearStats,
|
|
loadAssessmentPeriodStats,
|
|
loadAssessmentTargetRows,
|
|
loadAssessmentVehicleStats,
|
|
loadAssessmentYearlyPeriodStats,
|
|
loadAssessmentYearlyStats,
|
|
loadTargetVehicleRows,
|
|
loadYearlyAssessments,
|
|
} from './repository.js';
|
|
import { Hono } from 'hono';
|
|
import pool from '../../db/mysql.js';
|
|
import { getCache } from './cache.js';
|
|
import { fetchOneOsDailyMileage } from './oneos-api.js';
|
|
import { fetchVehicleInfoByPlates } from './vehicle-info.js';
|
|
import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js';
|
|
|
|
const app = new Hono();
|
|
|
|
app.get('/', async (c) => {
|
|
try {
|
|
const targets = await loadAssessmentTargetRows(pool);
|
|
|
|
const vehicleStats = await loadAssessmentVehicleStats(pool);
|
|
|
|
const statsMap = new Map<number, any>();
|
|
for (const s of vehicleStats) statsMap.set(s.target_id, s);
|
|
|
|
const firstYearRows = await loadAssessmentFirstYearStats(pool);
|
|
|
|
const firstYearMap = new Map<number, any>();
|
|
for (const s of firstYearRows) firstYearMap.set(s.target_id, s);
|
|
|
|
const yearlyRows = await loadAssessmentYearlyStats(pool);
|
|
|
|
const yearlyMap = new Map<number, any[]>();
|
|
for (const row of yearlyRows) {
|
|
const list = yearlyMap.get(row.target_id) || [];
|
|
list.push(row);
|
|
yearlyMap.set(row.target_id, list);
|
|
}
|
|
|
|
const yearlyPeriodRows = await loadAssessmentYearlyPeriodStats(pool);
|
|
|
|
const yearlyPeriodsMap = new Map<string, string[]>();
|
|
for (const row of yearlyPeriodRows) {
|
|
const key = `${row.target_id}-${row.year_number}`;
|
|
const list = yearlyPeriodsMap.get(key) || [];
|
|
list.push(`${row.start_date} ~ ${row.end_date} (${row.cnt}台)`);
|
|
yearlyPeriodsMap.set(key, list);
|
|
}
|
|
|
|
const periodRows = await loadAssessmentPeriodStats(pool);
|
|
|
|
const periodsMap = new Map<number, string[]>();
|
|
for (const p of periodRows) {
|
|
const list = periodsMap.get(p.target_id) || [];
|
|
list.push(`${p.start_date} ~ ${p.end_date} (${p.cnt}台)`);
|
|
periodsMap.set(p.target_id, list);
|
|
}
|
|
|
|
const cache = getCache();
|
|
const cacheVehicleMap = new Map<string, number>();
|
|
if (cache) {
|
|
for (const v of cache.vehicles) {
|
|
cacheVehicleMap.set(v.plate, Math.max(0, v.dailyKm || 0));
|
|
}
|
|
}
|
|
|
|
const targetVehicleRows = await loadTargetVehicleRows(pool);
|
|
|
|
const targetIdPlatesMap = new Map<number, string[]>();
|
|
for (const r of targetVehicleRows) {
|
|
const list = targetIdPlatesMap.get(r.target_id) || [];
|
|
list.push(r.plate_number);
|
|
targetIdPlatesMap.set(r.target_id, list);
|
|
}
|
|
|
|
const now = new Date();
|
|
const result = targets.map((t: any) => {
|
|
const s = statsMap.get(t.id) || {};
|
|
const fy = firstYearMap.get(t.id) || {};
|
|
const currentYearTarget = Number(s.current_year_target) || 0;
|
|
const currentYearCompleted = Number(s.current_year_completed) || 0;
|
|
const remaining = Math.max(0, currentYearTarget - currentYearCompleted);
|
|
const yearEnd = s.year_end_date ? new Date(s.year_end_date) : now;
|
|
const daysLeft = Math.max(1, Math.ceil((yearEnd.getTime() - now.getTime()) / 86400000));
|
|
const dailyTarget = remaining / daysLeft;
|
|
const firstYearEnd = fy.first_year_end_date ? new Date(fy.first_year_end_date) : now;
|
|
const firstYearDaysLeft = Math.max(0, Math.ceil((firstYearEnd.getTime() - now.getTime()) / 86400000));
|
|
const firstYearRemaining = Number(fy.first_year_remaining) || 0;
|
|
const firstYearVehicleCount = Number(fy.first_year_total) || 0;
|
|
const firstYearQualifiedCount = Number(fy.first_year_qualified_count) || 0;
|
|
const yearlyAssessments = (yearlyMap.get(t.id) || []).map((row: any) => {
|
|
const vehicleCount = Number(row.vehicle_count) || 0;
|
|
const qualifiedCount = Number(row.qualified_count) || 0;
|
|
const remainingMileage = Number(row.remaining_mileage) || 0;
|
|
const endDate = row.end_date ? new Date(row.end_date) : now;
|
|
const assessmentDaysLeft = Math.max(0, Math.ceil((endDate.getTime() - now.getTime()) / 86400000));
|
|
const yearNumber = Number(row.year_number) || 0;
|
|
|
|
return {
|
|
yearNumber,
|
|
label: `第${yearNumber}年`,
|
|
vehicleCount,
|
|
target: Number(row.target_mileage) || 0,
|
|
completed: Number(row.completed_mileage) || 0,
|
|
remaining: remainingMileage,
|
|
completionRate: (Number(row.completion_rate) || 0) * 100,
|
|
qualifiedCount,
|
|
qualifiedRate: vehicleCount > 0 ? (qualifiedCount / vehicleCount) * 100 : 0,
|
|
halfQualifiedCount: Number(row.half_qualified_count) || 0,
|
|
daysLeft: assessmentDaysLeft,
|
|
dailyTarget: assessmentDaysLeft > 0 ? Math.round((remainingMileage / assessmentDaysLeft) * 10) / 10 : 0,
|
|
startDate: row.start_date || null,
|
|
endDate: row.end_date || null,
|
|
periods: yearlyPeriodsMap.get(`${row.target_id}-${row.year_number}`) || [],
|
|
};
|
|
});
|
|
|
|
const periods = periodsMap.get(t.id) || [];
|
|
if (periods.length === 0) {
|
|
const startDate = t.default_start_date ? new Date(t.default_start_date).toISOString().split('T')[0] : '';
|
|
const endDate = t.default_end_date ? new Date(t.default_end_date).toISOString().split('T')[0] : '';
|
|
if (startDate || endDate) periods.push(`${startDate} ~ ${endDate}`);
|
|
}
|
|
|
|
return {
|
|
id: t.id,
|
|
targetName: t.target_name,
|
|
vehicleCount: Number(s.total) || t.vehicle_count,
|
|
totalMileagePerVehicle: Number(t.total_mileage_per_vehicle),
|
|
annualMileagePerVehicle: Number(t.annual_mileage_per_vehicle),
|
|
assessmentYears: t.assessment_years,
|
|
periods,
|
|
todayTotal: Math.round((targetIdPlatesMap.get(t.id) || []).reduce((sum, plate) => sum + (cacheVehicleMap.get(plate) || 0), 0)),
|
|
cumulativeTotal: Number(s.cumulative_total) || 0,
|
|
avgCompletion: (Number(s.avg_completion) || 0) * 100,
|
|
qualifiedCount: Number(s.qualified_count) || 0,
|
|
yearQualifiedCount: Number(s.year_qualified_count) || 0,
|
|
halfQualifiedCount: Number(s.half_qualified_count) || 0,
|
|
currentYearTarget,
|
|
currentYearCompleted,
|
|
remaining,
|
|
daysLeft,
|
|
dailyTarget: Math.round(dailyTarget * 10) / 10,
|
|
firstYearVehicleCount,
|
|
firstYearTarget: Number(fy.first_year_target) || 0,
|
|
firstYearCompleted: Number(fy.first_year_completed) || 0,
|
|
firstYearRemaining,
|
|
firstYearCompletionRate: (Number(fy.first_year_completion_rate) || 0) * 100,
|
|
firstYearQualifiedCount,
|
|
firstYearQualifiedRate: firstYearVehicleCount > 0 ? (firstYearQualifiedCount / firstYearVehicleCount) * 100 : 0,
|
|
firstYearHalfQualifiedCount: Number(fy.first_year_half_qualified_count) || 0,
|
|
firstYearDaysLeft,
|
|
firstYearDailyTarget: firstYearDaysLeft > 0 ? Math.round((firstYearRemaining / firstYearDaysLeft) * 10) / 10 : 0,
|
|
firstYearStartDate: fy.first_year_start_date || null,
|
|
firstYearEndDate: fy.first_year_end_date || null,
|
|
yearlyAssessments,
|
|
};
|
|
});
|
|
|
|
return c.json(result);
|
|
} catch (e: unknown) {
|
|
console.error('targets error:', e);
|
|
return c.json([], 500);
|
|
}
|
|
});
|
|
|
|
app.get('/:id/vehicles', async (c) => {
|
|
const targetId = c.req.param('id');
|
|
const date = c.req.query('date') || '';
|
|
|
|
try {
|
|
const rows = await loadYearlyAssessments(pool, { targetId });
|
|
|
|
const plates: string[] = rows.map((r: any) => r.plate_number);
|
|
const infoMap = await fetchVehicleInfoByPlates(plates);
|
|
|
|
const dateMileageMap = new Map<string, { dailyKm: number; totalKm: number | null; isOnline: boolean }>();
|
|
if (date && plates.length > 0) {
|
|
const mileageRows = await fetchOneOsDailyMileage(date, plates);
|
|
for (const m of mileageRows) {
|
|
const existing = dateMileageMap.get(m.plateNumber);
|
|
const dailyKm = m.status === 'NORMAL' ? (Number(m.dailyMileageKm) || 0) : 0;
|
|
if (!existing || dailyKm > existing.dailyKm) {
|
|
dateMileageMap.set(m.plateNumber, {
|
|
dailyKm,
|
|
totalKm: m.totalMileageKm,
|
|
isOnline: m.status === 'NORMAL' && dailyKm > 0,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = rows.map((r: any) => {
|
|
const info = infoMap.get(r.plate_number);
|
|
const dateMileage = date ? dateMileageMap.get(r.plate_number) : null;
|
|
return {
|
|
plateNumber: r.plate_number,
|
|
todayMileage: dateMileage ? dateMileage.dailyKm : (Number(r.today_mileage) || 0),
|
|
totalMileage: dateMileage?.totalKm ?? (Number(r.vehicle_total_mileage) || 0),
|
|
completionRate: Number(r.completion_rate) || 0,
|
|
isQualified: r.is_qualified === 1,
|
|
currentYearIsQualified: r.current_year_is_qualified === 1,
|
|
dailyRequiredMileage: Number(r.daily_required_mileage) || 0,
|
|
rentStatus: info?.rent_status || null,
|
|
department: info?.department || null,
|
|
customer: info?.customer || null,
|
|
isOnline: dateMileage ? dateMileage.isOnline : true,
|
|
};
|
|
});
|
|
|
|
const user = (c as any).get('user') as import('../../auth/types.js').AuthUser | undefined;
|
|
const filtered = user ? filterByPermission(result, user) : result;
|
|
return c.json(maskCustomerNames(filtered));
|
|
} catch (e: unknown) {
|
|
console.error('target vehicles error:', e);
|
|
return c.json([], 500);
|
|
}
|
|
});
|
|
|
|
export default app;
|