200 lines
6.8 KiB
TypeScript
200 lines
6.8 KiB
TypeScript
import type {
|
|
MileageSourceCategory,
|
|
MileageSourceGroup,
|
|
MileageSourceProtocol,
|
|
MonitoringData,
|
|
TargetSummary,
|
|
TargetVehicle,
|
|
TrendPoint,
|
|
} from './types';
|
|
import { fetchJson } from '../../auth/api-client';
|
|
import {
|
|
buildDailyReport,
|
|
type DailyReportData,
|
|
} from './daily-report';
|
|
|
|
export type {
|
|
DailyReportData,
|
|
DailyReportModel,
|
|
DailyReportVehicle,
|
|
} from './daily-report';
|
|
|
|
const BASE = '/api/mileage';
|
|
|
|
export async function fetchMonitoring(params?: {
|
|
sortBy?: 'today' | 'total' | 'statisticTime';
|
|
sortOrder?: 'asc' | 'desc';
|
|
limit?: number;
|
|
page?: number;
|
|
search?: string;
|
|
dept?: string;
|
|
customer?: string;
|
|
project?: string;
|
|
entity?: string;
|
|
rentStatus?: string;
|
|
platePrefix?: string;
|
|
targetName?: string;
|
|
targetNames?: string[];
|
|
region?: string;
|
|
plate?: string;
|
|
mileageMin?: string;
|
|
mileageMax?: string;
|
|
date?: string;
|
|
startDate?: string;
|
|
endDate?: string;
|
|
brands?: string[];
|
|
sourcePriority?: MileageSourceGroup[];
|
|
force?: boolean;
|
|
}): Promise<MonitoringData> {
|
|
const query = new URLSearchParams();
|
|
if (params?.sortBy) query.set('sortBy', params.sortBy);
|
|
if (params?.sortOrder) query.set('sortOrder', params.sortOrder);
|
|
if (params?.limit) query.set('limit', String(params.limit));
|
|
if (params?.page) query.set('page', String(params.page));
|
|
if (params?.search) query.set('search', params.search);
|
|
if (params?.dept) query.set('dept', params.dept);
|
|
if (params?.customer) query.set('customer', params.customer);
|
|
if (params?.project) query.set('project', params.project);
|
|
if (params?.entity) query.set('entity', params.entity);
|
|
if (params?.rentStatus) query.set('rentStatus', params.rentStatus);
|
|
if (params?.platePrefix) query.set('platePrefix', params.platePrefix);
|
|
if (params?.targetName) query.set('targetName', params.targetName);
|
|
if (params?.targetNames) {
|
|
params.targetNames.forEach(name => {
|
|
if (name) query.append('targetName', name);
|
|
});
|
|
}
|
|
if (params?.region) query.set('region', params.region);
|
|
if (params?.brands) {
|
|
params.brands.forEach(brand => {
|
|
if (brand) query.append('brand', brand);
|
|
});
|
|
}
|
|
if (params?.plate) query.set('plate', params.plate);
|
|
if (params?.mileageMin) query.set('mileageMin', params.mileageMin);
|
|
if (params?.mileageMax) query.set('mileageMax', params.mileageMax);
|
|
if (params?.date) query.set('date', params.date);
|
|
if (params?.startDate) query.set('startDate', params.startDate);
|
|
if (params?.endDate) query.set('endDate', params.endDate);
|
|
if (params?.sourcePriority?.length) query.set('sourcePriority', params.sourcePriority.join(','));
|
|
if (params?.force) query.set('force', '1');
|
|
const qs = query.toString();
|
|
return fetchJson<MonitoringData>(`${BASE}/monitoring${qs ? `?${qs}` : ''}`);
|
|
}
|
|
|
|
export async function fetchTargets(): Promise<TargetSummary[]> {
|
|
return fetchJson<TargetSummary[]>(`${BASE}/targets`);
|
|
}
|
|
|
|
export async function fetchTargetVehicles(targetId: number, date?: string): Promise<TargetVehicle[]> {
|
|
const params = new URLSearchParams();
|
|
if (date) params.set('date', date);
|
|
const qs = params.toString();
|
|
return fetchJson<TargetVehicle[]>(`${BASE}/target/${targetId}/vehicles${qs ? `?${qs}` : ''}`);
|
|
}
|
|
|
|
export async function fetchTrend(targetId?: number, days = 7): Promise<TrendPoint[]> {
|
|
const params = new URLSearchParams();
|
|
if (targetId) params.set('targetId', String(targetId));
|
|
params.set('days', String(days));
|
|
return fetchJson<TrendPoint[]>(`${BASE}/trend?${params.toString()}`);
|
|
}
|
|
|
|
export interface VehicleRecentDay {
|
|
date: string;
|
|
dailyKm: number;
|
|
isDataSynced: boolean;
|
|
sourceProtocol: MileageSourceProtocol | null;
|
|
sourceCategory: Exclude<MileageSourceCategory, 'MIXED'> | null;
|
|
}
|
|
|
|
export interface VehicleRecentResponse {
|
|
plate: string;
|
|
start?: string;
|
|
end?: string;
|
|
days: VehicleRecentDay[];
|
|
sourcePriority?: MileageSourceGroup[];
|
|
}
|
|
|
|
export async function fetchVehicleRecent(
|
|
plate: string,
|
|
range: {
|
|
days?: number;
|
|
start?: string;
|
|
end?: string;
|
|
sourcePriority?: MileageSourceGroup[];
|
|
} = { days: 15 },
|
|
): Promise<VehicleRecentResponse> {
|
|
const params = new URLSearchParams();
|
|
if (range.start) params.set('start', range.start);
|
|
if (range.end) params.set('end', range.end);
|
|
if (range.days != null) params.set('days', String(range.days));
|
|
if (range.sourcePriority?.length) params.set('sourcePriority', range.sourcePriority.join(','));
|
|
return fetchJson<VehicleRecentResponse>(
|
|
`${BASE}/vehicle/${encodeURIComponent(plate)}/recent?${params.toString()}`
|
|
);
|
|
}
|
|
|
|
function reportDateFromUpdatedAt(updatedAt: string): string {
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(updatedAt)) return updatedAt;
|
|
const d = new Date(updatedAt);
|
|
if (!Number.isNaN(d.getTime())) return d.toISOString().slice(0, 10);
|
|
return new Date().toISOString().slice(0, 10);
|
|
}
|
|
|
|
let dailyReportRequest: Promise<DailyReportData> | null = null;
|
|
let dailyReportCache: { data: DailyReportData; expiresAt: number } | null = null;
|
|
const DAILY_REPORT_CACHE_MS = 60_000;
|
|
|
|
async function loadDailyReport(): Promise<DailyReportData> {
|
|
const [targets, monitoring] = await Promise.all([
|
|
fetchTargets(),
|
|
fetchMonitoring({ sortBy: 'today', sortOrder: 'desc', limit: 5 }),
|
|
]);
|
|
const reportDate = monitoring.dateRange?.end || reportDateFromUpdatedAt(monitoring.updatedAt);
|
|
|
|
const targetVehicles = await Promise.all(
|
|
targets.map(async target => {
|
|
const vehicles = await fetchTargetVehicles(target.id, reportDate);
|
|
return { target, vehicles };
|
|
}),
|
|
);
|
|
|
|
return buildDailyReport(targets, targetVehicles, monitoring, [], reportDate);
|
|
}
|
|
|
|
export async function fetchDailyReport(force = false): Promise<DailyReportData> {
|
|
if (!force && dailyReportCache && dailyReportCache.expiresAt > Date.now()) {
|
|
return dailyReportCache.data;
|
|
}
|
|
if (!force && dailyReportRequest) return dailyReportRequest;
|
|
const request = loadDailyReport();
|
|
dailyReportRequest = request;
|
|
try {
|
|
const data = await request;
|
|
dailyReportCache = { data, expiresAt: Date.now() + DAILY_REPORT_CACHE_MS };
|
|
return data;
|
|
} finally {
|
|
if (dailyReportRequest === request) dailyReportRequest = null;
|
|
}
|
|
}
|
|
|
|
let dailyReportTrendRequest: Promise<TrendPoint[]> | null = null;
|
|
let dailyReportTrendCache: { data: TrendPoint[]; expiresAt: number } | null = null;
|
|
|
|
export async function fetchDailyReportTrend(force = false): Promise<TrendPoint[]> {
|
|
if (!force && dailyReportTrendCache && dailyReportTrendCache.expiresAt > Date.now()) {
|
|
return dailyReportTrendCache.data;
|
|
}
|
|
if (!force && dailyReportTrendRequest) return dailyReportTrendRequest;
|
|
const request = fetchTrend(undefined, 7);
|
|
dailyReportTrendRequest = request;
|
|
try {
|
|
const data = await request;
|
|
dailyReportTrendCache = { data, expiresAt: Date.now() + DAILY_REPORT_CACHE_MS };
|
|
return data;
|
|
} finally {
|
|
if (dailyReportTrendRequest === request) dailyReportTrendRequest = null;
|
|
}
|
|
}
|