diff --git a/src/server/routes/mileage/targets.ts b/src/server/routes/mileage/targets.ts new file mode 100644 index 0000000..293a50f --- /dev/null +++ b/src/server/routes/mileage/targets.ts @@ -0,0 +1,180 @@ +import { Hono } from 'hono'; +import pool from '../../db.js'; +import mileagePool from '../../mileage-db.js'; +import { getCache } from './cache.js'; +import { fetchVehicleInfoByPlates } from './vehicle-info.js'; + +const app = new Hono(); + +app.get('/', async (c) => { + try { + const [targets] = await pool.execute( + 'SELECT * FROM tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id' + ) as [any[], unknown]; + + const [vehicleStats] = await pool.execute(` + SELECT + target_id, COUNT(*) as total, + SUM(today_mileage) as today_total, + SUM(current_mileage) as cumulative_total, + AVG(current_year_completion_rate) as avg_completion, + SUM(CASE WHEN is_qualified = 1 THEN 1 ELSE 0 END) as qualified_count, + SUM(CASE WHEN current_year_is_qualified = 1 THEN 1 ELSE 0 END) as year_qualified_count, + SUM(CASE WHEN current_year_completion_rate >= 0.5 THEN 1 ELSE 0 END) as half_qualified_count, + SUM(current_year_mileage_task) as current_year_target, + SUM(current_year_mileage) as current_year_completed, + MAX(current_year_assessment_end_date) as year_end_date + FROM tab_mileage_assessment_vehicle WHERE is_deleted = 0 + GROUP BY target_id + `) as [any[], unknown]; + + const statsMap = new Map(); + for (const s of vehicleStats) statsMap.set(s.target_id, s); + + const [periodRows] = await pool.execute(` + SELECT target_id, + DATE_FORMAT(assessment_start_date, '%Y-%m-%d') as start_date, + DATE_FORMAT(assessment_end_date, '%Y-%m-%d') as end_date, + COUNT(*) as cnt + FROM tab_mileage_assessment_vehicle WHERE is_deleted = 0 + GROUP BY target_id, assessment_start_date, assessment_end_date + ORDER BY target_id, assessment_start_date + `) as [any[], unknown]; + + const periodsMap = new Map(); + 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(); + if (cache) { + for (const v of cache.vehicles) { + cacheVehicleMap.set(v.plate, Math.max(0, v.dailyKm || 0)); + } + } + + const [targetVehicleRows] = await pool.execute( + 'SELECT target_id, plate_number FROM tab_mileage_assessment_vehicle WHERE is_deleted = 0' + ) as [{ target_id: number; plate_number: string }[], unknown]; + + const targetIdPlatesMap = new Map(); + 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 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 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: (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, + }; + }); + + 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 pool.execute( + `SELECT plate_number, today_mileage, vehicle_total_mileage, + completion_rate, is_qualified, current_year_is_qualified, + daily_required_mileage + FROM tab_mileage_assessment_vehicle + WHERE target_id = ? AND is_deleted = 0 + ORDER BY today_mileage DESC`, + [targetId] + ) as [any[], unknown]; + + const plates: string[] = rows.map((r: any) => r.plate_number); + const infoMap = await fetchVehicleInfoByPlates(plates); + + const dateMileageMap = new Map(); + if (date && plates.length > 0) { + const [mileageRows] = await mileagePool.execute( + `SELECT plate, daily_km, total_km, source FROM v_vehicle_daily_stats + WHERE stat_date = ? AND plate IN (${plates.map(() => '?').join(',')})`, + [date, ...plates] + ) as [any[], unknown]; + for (const m of mileageRows) { + const existing = dateMileageMap.get(m.plate); + const dailyKm = Number(m.daily_km) || 0; + if (!existing || dailyKm > existing.dailyKm) { + const source = m.source || 'NONE'; + dateMileageMap.set(m.plate, { + dailyKm, + totalKm: m.total_km !== null ? Number(m.total_km) : null, + isOnline: source !== 'NONE' && 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, + }; + }); + + return c.json(result); + } catch (e: unknown) { + console.error('target vehicles error:', e); + return c.json([], 500); + } +}); + +export default app;