diff --git a/src/modules/mileage/StatisticsView.tsx b/src/modules/mileage/StatisticsView.tsx index 1036b6e..c82fa0f 100644 --- a/src/modules/mileage/StatisticsView.tsx +++ b/src/modules/mileage/StatisticsView.tsx @@ -381,12 +381,16 @@ export default function StatisticsView() {

{fmtKm(assessment.target)} km

-

{assessment.label}已完成

-

{fmtKm(assessment.completed)} km

+

{assessment.label}累计里程

+

{fmtKm(assessment.actualMileage)} km

数据截至 {assessment.daysLeft === 0 ? fmtDateLabel(assessment.endDate) : currentDateLabel}

+
+

{assessment.label}计入完成

+

{fmtKm(assessment.completed)} km

+

{assessment.label}完成率

{fmtPercent(assessment.completionRate)}

diff --git a/src/modules/mileage/types.ts b/src/modules/mileage/types.ts index e7371ca..fb8819b 100644 --- a/src/modules/mileage/types.ts +++ b/src/modules/mileage/types.ts @@ -83,6 +83,7 @@ export interface TargetYearlyAssessment { label: string; vehicleCount: number; target: number; + actualMileage: number; completed: number; remaining: number; completionRate: number; diff --git a/src/server/routes/mileage/targets.ts b/src/server/routes/mileage/targets.ts index edcc864..93dc128 100644 --- a/src/server/routes/mileage/targets.ts +++ b/src/server/routes/mileage/targets.ts @@ -1,7 +1,7 @@ import { Hono } from 'hono'; import pool from '../../db.js'; import mileagePool from '../../mileage-db.js'; -import { getCache } from './cache.js'; +import { getCache, queryDateMileage } from './cache.js'; import { fetchVehicleInfoByPlates } from './vehicle-info.js'; import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js'; @@ -183,28 +183,18 @@ app.get('/', async (c) => { } } - const postPeriodDailyMap = new Map(); - 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); + const cutoffMileageMapByDate = new Map>(); + for (const date of Array.from(new Set(endedPeriodDates))) { + const vehicles = await queryDateMileage(date); + const map = new Map(); + for (const vehicle of vehicles) { + if (vehicle.totalKm != null) map.set(vehicle.plate, vehicle.totalKm); } + cutoffMileageMapByDate.set(date, map); } const yearlyMetricMap = new Map { }>(); const yearlyMetricDraftMap = new Map { 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 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 cutoffMap = endDate < todayStr ? cutoffMileageMapByDate.get(endDate) : undefined; + const mileageAtCutoff = Math.max(0, cutoffMap?.get(row.plate_number) ?? (Number(row.current_mileage) || 0)); const completed = Math.min(mileageAtCutoff, goal); const draft = yearlyMetricDraftMap.get(key) || { target: 0, + actualMileage: 0, completed: 0, remaining: 0, vehicleCount: 0, @@ -246,6 +235,7 @@ app.get('/', async (c) => { }; draft.target += goal; + draft.actualMileage += mileageAtCutoff; draft.completed += completed; draft.remaining += Math.max(goal - mileageAtCutoff, 0); draft.vehicleCount += 1; @@ -257,6 +247,7 @@ 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), @@ -295,6 +286,7 @@ 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),