From aa9a29fed84a6ccfadb84b21f7b440d250a271c4 Mon Sep 17 00:00:00 2001 From: kkfluous Date: Thu, 16 Apr 2026 22:58:47 +0800 Subject: [PATCH] fix(scheduling): use each candidate's own daysLeft for prediction Different assessment targets have different end dates. Previously all candidates used the current vehicle's daysLeft, causing wrong predictions. Now each inventory vehicle computes its own daysLeft from its assessment target's current_year_assessment_end_date. predictedAfterSwap uses the candidate's own daysLeft instead of the current vehicle's. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/modules/scheduling/SuggestionDetail.tsx | 2 +- src/modules/scheduling/types.ts | 1 + src/server/routes/scheduling/algorithm.ts | 16 +++++++++------- src/server/routes/scheduling/suggestions.ts | 9 +++++++++ src/server/routes/scheduling/types.ts | 2 ++ 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/modules/scheduling/SuggestionDetail.tsx b/src/modules/scheduling/SuggestionDetail.tsx index 19b17e3..96f6ff3 100644 --- a/src/modules/scheduling/SuggestionDetail.tsx +++ b/src/modules/scheduling/SuggestionDetail.tsx @@ -229,7 +229,7 @@ export default function SuggestionDetail({ suggestion: s, onClose, onNotifySucce {c.region} {c.vehicleType} {c.targetName || '库存'} - 剩余{v.daysLeft}天 + 剩余{c.daysLeft}天 {c.canQualifyAfterSwap ? ( diff --git a/src/modules/scheduling/types.ts b/src/modules/scheduling/types.ts index a90bbbd..d7a0c18 100644 --- a/src/modules/scheduling/types.ts +++ b/src/modules/scheduling/types.ts @@ -25,6 +25,7 @@ export interface CandidateVehicle { totalMileage: number; completionRate: number; yearTarget: number | null; + daysLeft: number; region: string; province: string; mileageGap: number; diff --git a/src/server/routes/scheduling/algorithm.ts b/src/server/routes/scheduling/algorithm.ts index 4caa611..5dea032 100644 --- a/src/server/routes/scheduling/algorithm.ts +++ b/src/server/routes/scheduling/algorithm.ts @@ -90,13 +90,10 @@ export function generateSuggestions( // Among those, prefer the one with the smallest gap (easiest to finish). // Exclude already-qualified (>= 100%) — no value in swapping those. for (const vehicle of hopeless) { - const customerCanAdd = vehicle.customerAvgDaily * vehicle.daysLeft; - const candidates: CandidateVehicle[] = inventoryVehicles .filter((inv) => { if (!isTypeCompatible(vehicle.vehicleType, inv.vehicleType)) return false; if (inv.region !== vehicle.region) return false; - // Exclude already fully qualified const effectiveTarget = inv.yearTarget ?? vehicle.yearTarget; if (effectiveTarget > 0 && inv.totalMileage >= effectiveTarget) return false; return true; @@ -104,7 +101,9 @@ export function generateSuggestions( .map((inv) => { const effectiveTarget = inv.yearTarget ?? vehicle.yearTarget; const mileageGap = Math.max(0, effectiveTarget - inv.totalMileage); - const predictedAfterSwap = inv.totalMileage + customerCanAdd; + // Use candidate's own daysLeft for prediction + const candidateCanAdd = vehicle.customerAvgDaily * inv.daysLeft; + const predictedAfterSwap = inv.totalMileage + candidateCanAdd; const canQualifyAfterSwap = predictedAfterSwap >= effectiveTarget; return { plateNumber: inv.plateNumber, @@ -114,6 +113,7 @@ export function generateSuggestions( totalMileage: inv.totalMileage, completionRate: inv.completionRate, yearTarget: inv.yearTarget ?? vehicle.yearTarget, + daysLeft: inv.daysLeft, region: inv.region, province: inv.province, mileageGap, @@ -133,7 +133,7 @@ export function generateSuggestions( const gap = Math.max(0, vehicle.yearTarget - vehicle.currentYearMileage); const dailyReq = vehicle.daysLeft > 0 ? Math.round(gap / vehicle.daysLeft) : 0; - const predictedTotal = Math.round(vehicle.currentYearMileage + customerCanAdd); + const predictedTotal = Math.round(vehicle.currentYearMileage + vehicle.customerAvgDaily * vehicle.daysLeft); const reason = `客户日均 ${Math.round(vehicle.customerAvgDaily)} km | 考核周期剩余 ${vehicle.daysLeft} 天 · 日均需 ${fmtKmSimple(dailyReq)} km\n!!预估无法达标,需替换`; suggestions.push({ @@ -165,8 +165,9 @@ export function generateSuggestions( .map((inv) => { const effectiveTarget = inv.yearTarget ?? vehicle.yearTarget; const mileageGap = Math.max(0, effectiveTarget - inv.totalMileage); - const predictedAfterSwap = - inv.totalMileage + vehicle.customerAvgDaily * vehicle.daysLeft; + // Use candidate's own daysLeft for prediction + const candidateCanAdd = vehicle.customerAvgDaily * inv.daysLeft; + const predictedAfterSwap = inv.totalMileage + candidateCanAdd; const canQualifyAfterSwap = predictedAfterSwap >= effectiveTarget; return { plateNumber: inv.plateNumber, @@ -176,6 +177,7 @@ export function generateSuggestions( totalMileage: inv.totalMileage, completionRate: inv.completionRate, yearTarget: inv.yearTarget ?? vehicle.yearTarget, + daysLeft: inv.daysLeft, region: inv.region, province: inv.province, mileageGap, diff --git a/src/server/routes/scheduling/suggestions.ts b/src/server/routes/scheduling/suggestions.ts index d0b3924..5c9f892 100644 --- a/src/server/routes/scheduling/suggestions.ts +++ b/src/server/routes/scheduling/suggestions.ts @@ -250,12 +250,21 @@ app.get('/', async (c) => { // Cross-reference with assessment data const assessment = assessmentByPlate.get(plate); + // Compute this vehicle's own daysLeft from its assessment end date + let invDaysLeft = 0; + if (assessment?.current_year_assessment_end_date) { + const endDate = new Date(assessment.current_year_assessment_end_date); + invDaysLeft = Math.max(1, Math.ceil((endDate.getTime() - now.getTime()) / 86400000)); + } else { + invDaysLeft = Math.max(1, Math.ceil((yearEnd.getTime() - now.getTime()) / 86400000)); + } inventoryVehicles.push({ plateNumber: plate, vehicleType, region, province, totalMileage: assessment ? Number(assessment.vehicle_total_mileage) || 0 : 0, + daysLeft: invDaysLeft, targetId: assessment ? (assessment.target_id as number) : null, targetName: assessment ? (targetMap.get(assessment.target_id)?.targetName ?? null) : null, yearTarget: assessment ? Number(assessment.current_year_mileage_task) || null : null, diff --git a/src/server/routes/scheduling/types.ts b/src/server/routes/scheduling/types.ts index 5cf7b43..65a998b 100644 --- a/src/server/routes/scheduling/types.ts +++ b/src/server/routes/scheduling/types.ts @@ -25,6 +25,7 @@ export interface CandidateVehicle { totalMileage: number; completionRate: number; yearTarget: number | null; + daysLeft: number; region: string; province: string; mileageGap: number; @@ -97,6 +98,7 @@ export interface InventoryVehicle { region: string; province: string; totalMileage: number; + daysLeft: number; targetId: number | null; targetName: string | null; yearTarget: number | null;