|
|
|
|
@@ -1,7 +1,7 @@
|
|
|
|
|
import { Hono } from 'hono';
|
|
|
|
|
import pool from '../../db.js';
|
|
|
|
|
import mileagePool from '../../mileage-db.js';
|
|
|
|
|
import { getCache, queryDateMileage } from './cache.js';
|
|
|
|
|
import { getCache } from './cache.js';
|
|
|
|
|
import { fetchVehicleInfoByPlates } from './vehicle-info.js';
|
|
|
|
|
import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js';
|
|
|
|
|
|
|
|
|
|
@@ -183,18 +183,28 @@ app.get('/', async (c) => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cutoffMileageMapByDate = new Map<string, Map<string, number>>();
|
|
|
|
|
for (const date of Array.from(new Set(endedPeriodDates))) {
|
|
|
|
|
const vehicles = await queryDateMileage(date);
|
|
|
|
|
const map = new Map<string, number>();
|
|
|
|
|
for (const vehicle of vehicles) {
|
|
|
|
|
if (vehicle.totalKm != null) map.set(vehicle.plate, vehicle.totalKm);
|
|
|
|
|
const postPeriodDailyMap = new Map<string, { date: string; km: number }[]>();
|
|
|
|
|
const allTargetPlates = Array.from(new Set(targetVehicleRows.map(row => row.plate_number)));
|
|
|
|
|
const minEndedDate = endedPeriodDates.sort()[0];
|
|
|
|
|
if (minEndedDate && allTargetPlates.length > 0) {
|
|
|
|
|
const [postPeriodDailyRows] = await mileagePool.execute(
|
|
|
|
|
`SELECT plate,
|
|
|
|
|
DATE_FORMAT(stat_date, '%Y-%m-%d') as stat_date,
|
|
|
|
|
MAX(GREATEST(daily_km, 0)) as daily_km
|
|
|
|
|
FROM v_vehicle_daily_stats
|
|
|
|
|
WHERE stat_date > ? AND plate IN (${allTargetPlates.map(() => '?').join(',')})
|
|
|
|
|
GROUP BY plate, stat_date`,
|
|
|
|
|
[minEndedDate, ...allTargetPlates]
|
|
|
|
|
) as [{ plate: string; stat_date: string; daily_km: string | number | null }[], unknown];
|
|
|
|
|
|
|
|
|
|
for (const row of postPeriodDailyRows) {
|
|
|
|
|
const list = postPeriodDailyMap.get(row.plate) || [];
|
|
|
|
|
list.push({ date: row.stat_date, km: Number(row.daily_km) || 0 });
|
|
|
|
|
postPeriodDailyMap.set(row.plate, list);
|
|
|
|
|
}
|
|
|
|
|
cutoffMileageMapByDate.set(date, map);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const yearlyMetricMap = new Map<string, {
|
|
|
|
|
actualMileage: number;
|
|
|
|
|
completed: number;
|
|
|
|
|
remaining: number;
|
|
|
|
|
completionRate: number;
|
|
|
|
|
@@ -204,7 +214,6 @@ app.get('/', async (c) => {
|
|
|
|
|
}>();
|
|
|
|
|
const yearlyMetricDraftMap = new Map<string, {
|
|
|
|
|
target: number;
|
|
|
|
|
actualMileage: number;
|
|
|
|
|
completed: number;
|
|
|
|
|
remaining: number;
|
|
|
|
|
vehicleCount: number;
|
|
|
|
|
@@ -216,17 +225,19 @@ app.get('/', async (c) => {
|
|
|
|
|
const target = targetRuleMap.get(row.target_id);
|
|
|
|
|
const annualMileage = Number(target?.annual_mileage_per_vehicle) || 0;
|
|
|
|
|
const maxYear = Math.min(Number(target?.assessment_years) || 0, Number(row.current_year_number) || 0);
|
|
|
|
|
const postDailyRows = postPeriodDailyMap.get(row.plate_number) || [];
|
|
|
|
|
|
|
|
|
|
for (let year = 1; year <= maxYear; year++) {
|
|
|
|
|
const key = `${row.target_id}-${year}`;
|
|
|
|
|
const goal = annualMileage * year;
|
|
|
|
|
const endDate = addYearsMinusOneDay(row.assessment_start_date, year);
|
|
|
|
|
const cutoffMap = endDate < todayStr ? cutoffMileageMapByDate.get(endDate) : undefined;
|
|
|
|
|
const mileageAtCutoff = Math.max(0, cutoffMap?.get(row.plate_number) ?? (Number(row.current_mileage) || 0));
|
|
|
|
|
const postPeriodMileage = endDate < todayStr
|
|
|
|
|
? postDailyRows.reduce((sum, item) => item.date > endDate ? sum + item.km : sum, 0)
|
|
|
|
|
: 0;
|
|
|
|
|
const mileageAtCutoff = Math.max(0, (Number(row.current_mileage) || 0) - postPeriodMileage);
|
|
|
|
|
const completed = Math.min(mileageAtCutoff, goal);
|
|
|
|
|
const draft = yearlyMetricDraftMap.get(key) || {
|
|
|
|
|
target: 0,
|
|
|
|
|
actualMileage: 0,
|
|
|
|
|
completed: 0,
|
|
|
|
|
remaining: 0,
|
|
|
|
|
vehicleCount: 0,
|
|
|
|
|
@@ -235,7 +246,6 @@ app.get('/', async (c) => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
draft.target += goal;
|
|
|
|
|
draft.actualMileage += mileageAtCutoff;
|
|
|
|
|
draft.completed += completed;
|
|
|
|
|
draft.remaining += Math.max(goal - mileageAtCutoff, 0);
|
|
|
|
|
draft.vehicleCount += 1;
|
|
|
|
|
@@ -247,7 +257,6 @@ app.get('/', async (c) => {
|
|
|
|
|
|
|
|
|
|
for (const [key, draft] of yearlyMetricDraftMap) {
|
|
|
|
|
yearlyMetricMap.set(key, {
|
|
|
|
|
actualMileage: round2(draft.actualMileage),
|
|
|
|
|
completed: round2(draft.completed),
|
|
|
|
|
remaining: round2(draft.remaining),
|
|
|
|
|
completionRate: round2(draft.target > 0 ? (draft.completed / draft.target) * 100 : 0),
|
|
|
|
|
@@ -286,7 +295,6 @@ app.get('/', async (c) => {
|
|
|
|
|
label: `第${yearNumber}年`,
|
|
|
|
|
vehicleCount,
|
|
|
|
|
target: Number(row.target_mileage) || 0,
|
|
|
|
|
actualMileage: cutoffMetrics?.actualMileage ?? (Number(row.completed_mileage) || 0),
|
|
|
|
|
completed: cutoffMetrics?.completed ?? (Number(row.completed_mileage) || 0),
|
|
|
|
|
remaining: cutoffMetrics?.remaining ?? remainingMileage,
|
|
|
|
|
completionRate: cutoffMetrics?.completionRate ?? ((Number(row.completion_rate) || 0) * 100),
|
|
|
|
|
|