Compare commits
3 Commits
a3dfe7ab8c
...
cc778f3701
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc778f3701 | ||
|
|
74d6efe261 | ||
|
|
a124e31fab |
@@ -381,16 +381,12 @@ export default function StatisticsView() {
|
|||||||
<p className="text-[10px] font-black text-slate-700">{fmtKm(assessment.target)} km</p>
|
<p className="text-[10px] font-black text-slate-700">{fmtKm(assessment.target)} km</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}累计里程</p>
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}已完成</p>
|
||||||
<p className="text-[10px] font-black text-slate-700">{fmtKm(assessment.actualMileage)} km</p>
|
<p className="text-[10px] font-black text-emerald-600">{fmtKm(assessment.completed)} km</p>
|
||||||
<p className="text-[8px] font-bold text-slate-300">
|
<p className="text-[8px] font-bold text-slate-300">
|
||||||
数据截至 {assessment.daysLeft === 0 ? fmtDateLabel(assessment.endDate) : currentDateLabel}
|
数据截至 {assessment.daysLeft === 0 ? fmtDateLabel(assessment.endDate) : currentDateLabel}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5">
|
|
||||||
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}计入完成</p>
|
|
||||||
<p className="text-[10px] font-black text-emerald-600">{fmtKm(assessment.completed)} km</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}完成率</p>
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}完成率</p>
|
||||||
<p className="text-[10px] font-black text-blue-600">{fmtPercent(assessment.completionRate)}</p>
|
<p className="text-[10px] font-black text-blue-600">{fmtPercent(assessment.completionRate)}</p>
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ export interface TargetYearlyAssessment {
|
|||||||
label: string;
|
label: string;
|
||||||
vehicleCount: number;
|
vehicleCount: number;
|
||||||
target: number;
|
target: number;
|
||||||
actualMileage: number;
|
|
||||||
completed: number;
|
completed: number;
|
||||||
remaining: number;
|
remaining: number;
|
||||||
completionRate: number;
|
completionRate: number;
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ function mileageCell(v: MonitoringVehicle, kind: 'today' | 'total'): string | nu
|
|||||||
if (kind === 'today') {
|
if (kind === 'today') {
|
||||||
// 当日未对接但有历史累计,视作今日 0;只有完全无数据才标「未对接」
|
// 当日未对接但有历史累计,视作今日 0;只有完全无数据才标「未对接」
|
||||||
if (!v.isDataSynced && v.totalKm == null) return '未对接';
|
if (!v.isDataSynced && v.totalKm == null) return '未对接';
|
||||||
return Math.max(0, Math.round(v.dailyKm || 0));
|
return Math.max(0, v.dailyKm || 0);
|
||||||
}
|
}
|
||||||
return v.totalKm != null ? Math.round(v.totalKm) : '未对接';
|
return v.totalKm != null ? v.totalKm : '未对接';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportContext): void {
|
export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportContext): void {
|
||||||
@@ -57,6 +57,13 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
|
|||||||
|
|
||||||
ws['!freeze'] = { xSplit: 0, ySplit: 1 } as never;
|
ws['!freeze'] = { xSplit: 0, ySplit: 1 } as never;
|
||||||
|
|
||||||
|
for (let r = 1; r < data.length; r++) {
|
||||||
|
for (const c of [7, 8]) {
|
||||||
|
const ref = XLSX.utils.encode_cell({ r, c });
|
||||||
|
if (ws[ref]?.t === 'n') ws[ref].z = '0.##########';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 表头样式(在客户端 SheetJS 社区版仅基本样式生效)
|
// 表头样式(在客户端 SheetJS 社区版仅基本样式生效)
|
||||||
for (let c = 0; c < HEADERS.length; c++) {
|
for (let c = 0; c < HEADERS.length; c++) {
|
||||||
const ref = XLSX.utils.encode_cell({ r: 0, c });
|
const ref = XLSX.utils.encode_cell({ r: 0, c });
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import pool from '../../db.js';
|
import pool from '../../db.js';
|
||||||
import mileagePool from '../../mileage-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 { fetchVehicleInfoByPlates } from './vehicle-info.js';
|
||||||
import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js';
|
import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js';
|
||||||
|
|
||||||
@@ -183,22 +183,28 @@ app.get('/', async (c) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cutoffMileageMapByDate = new Map<string, Map<string, number>>();
|
const postPeriodDailyMap = new Map<string, { date: string; km: number }[]>();
|
||||||
for (const date of Array.from(new Set(endedPeriodDates))) {
|
const allTargetPlates = Array.from(new Set(targetVehicleRows.map(row => row.plate_number)));
|
||||||
const vehicles = await queryDateMileage(date);
|
const minEndedDate = endedPeriodDates.sort()[0];
|
||||||
const map = new Map<string, number>();
|
if (minEndedDate && allTargetPlates.length > 0) {
|
||||||
for (const vehicle of vehicles) {
|
const [postPeriodDailyRows] = await mileagePool.execute(
|
||||||
if (vehicle.totalKm != null) map.set(vehicle.plate, vehicle.totalKm);
|
`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 currentMileageMap = new Map<string, number>();
|
|
||||||
for (const vehicle of await queryDateMileage(todayStr)) {
|
|
||||||
if (vehicle.totalKm != null) currentMileageMap.set(vehicle.plate, vehicle.totalKm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const yearlyMetricMap = new Map<string, {
|
const yearlyMetricMap = new Map<string, {
|
||||||
actualMileage: number;
|
|
||||||
completed: number;
|
completed: number;
|
||||||
remaining: number;
|
remaining: number;
|
||||||
completionRate: number;
|
completionRate: number;
|
||||||
@@ -208,7 +214,6 @@ app.get('/', async (c) => {
|
|||||||
}>();
|
}>();
|
||||||
const yearlyMetricDraftMap = new Map<string, {
|
const yearlyMetricDraftMap = new Map<string, {
|
||||||
target: number;
|
target: number;
|
||||||
actualMileage: number;
|
|
||||||
completed: number;
|
completed: number;
|
||||||
remaining: number;
|
remaining: number;
|
||||||
vehicleCount: number;
|
vehicleCount: number;
|
||||||
@@ -220,17 +225,19 @@ app.get('/', async (c) => {
|
|||||||
const target = targetRuleMap.get(row.target_id);
|
const target = targetRuleMap.get(row.target_id);
|
||||||
const annualMileage = Number(target?.annual_mileage_per_vehicle) || 0;
|
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 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++) {
|
for (let year = 1; year <= maxYear; year++) {
|
||||||
const key = `${row.target_id}-${year}`;
|
const key = `${row.target_id}-${year}`;
|
||||||
const goal = annualMileage * year;
|
const goal = annualMileage * year;
|
||||||
const endDate = addYearsMinusOneDay(row.assessment_start_date, year);
|
const endDate = addYearsMinusOneDay(row.assessment_start_date, year);
|
||||||
const cutoffMap = endDate < todayStr ? cutoffMileageMapByDate.get(endDate) : currentMileageMap;
|
const postPeriodMileage = endDate < todayStr
|
||||||
const mileageAtCutoff = Math.max(0, cutoffMap?.get(row.plate_number) ?? (Number(row.current_mileage) || 0));
|
? 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 completed = Math.min(mileageAtCutoff, goal);
|
||||||
const draft = yearlyMetricDraftMap.get(key) || {
|
const draft = yearlyMetricDraftMap.get(key) || {
|
||||||
target: 0,
|
target: 0,
|
||||||
actualMileage: 0,
|
|
||||||
completed: 0,
|
completed: 0,
|
||||||
remaining: 0,
|
remaining: 0,
|
||||||
vehicleCount: 0,
|
vehicleCount: 0,
|
||||||
@@ -239,7 +246,6 @@ app.get('/', async (c) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
draft.target += goal;
|
draft.target += goal;
|
||||||
draft.actualMileage += mileageAtCutoff;
|
|
||||||
draft.completed += completed;
|
draft.completed += completed;
|
||||||
draft.remaining += Math.max(goal - mileageAtCutoff, 0);
|
draft.remaining += Math.max(goal - mileageAtCutoff, 0);
|
||||||
draft.vehicleCount += 1;
|
draft.vehicleCount += 1;
|
||||||
@@ -251,7 +257,6 @@ app.get('/', async (c) => {
|
|||||||
|
|
||||||
for (const [key, draft] of yearlyMetricDraftMap) {
|
for (const [key, draft] of yearlyMetricDraftMap) {
|
||||||
yearlyMetricMap.set(key, {
|
yearlyMetricMap.set(key, {
|
||||||
actualMileage: round2(draft.actualMileage),
|
|
||||||
completed: round2(draft.completed),
|
completed: round2(draft.completed),
|
||||||
remaining: round2(draft.remaining),
|
remaining: round2(draft.remaining),
|
||||||
completionRate: round2(draft.target > 0 ? (draft.completed / draft.target) * 100 : 0),
|
completionRate: round2(draft.target > 0 ? (draft.completed / draft.target) * 100 : 0),
|
||||||
@@ -290,7 +295,6 @@ app.get('/', async (c) => {
|
|||||||
label: `第${yearNumber}年`,
|
label: `第${yearNumber}年`,
|
||||||
vehicleCount,
|
vehicleCount,
|
||||||
target: Number(row.target_mileage) || 0,
|
target: Number(row.target_mileage) || 0,
|
||||||
actualMileage: cutoffMetrics?.actualMileage ?? (Number(row.completed_mileage) || 0),
|
|
||||||
completed: cutoffMetrics?.completed ?? (Number(row.completed_mileage) || 0),
|
completed: cutoffMetrics?.completed ?? (Number(row.completed_mileage) || 0),
|
||||||
remaining: cutoffMetrics?.remaining ?? remainingMileage,
|
remaining: cutoffMetrics?.remaining ?? remainingMileage,
|
||||||
completionRate: cutoffMetrics?.completionRate ?? ((Number(row.completion_rate) || 0) * 100),
|
completionRate: cutoffMetrics?.completionRate ?? ((Number(row.completion_rate) || 0) * 100),
|
||||||
|
|||||||
Reference in New Issue
Block a user