fix(scheduling): rescue candidates should be close-to-qualifying, not zero-mileage
For rescue_hopeless (换走) scenario, completely rethought candidate logic: Before: showed biggest-gap candidates (0 mileage) → pointless, customer can't drive them to target After: prioritize candidates where customer's remaining driving can push them over the target line (canQualifyAfterSwap), sorted by smallest gap first Example: customer drives 178km/day × 57 days = ~1万km remaining. - 粤AGR6869 (缺口 1990km) → 换后 3.8万, 可达标 ✅ (shown first) - 浙FF58720 (缺口 6万km) → 换后 1万, 远不达标 (no longer shown first) Also updated reason text to explain the math: "该客户剩余57天还能跑约1万km,足以帮缺口小的车冲线" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -121,7 +121,7 @@ export default function SuggestionDetail({ suggestion: s, onClose, onNotifySucce
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-400 mb-2.5">
|
||||
{isRescue
|
||||
? '以下车辆里程已充足,可调给当前客户,将此车换走给高里程客户冲刺'
|
||||
? '以下车辆快达标,换到当前客户处利用剩余天数即可冲线'
|
||||
: '以下车辆里程缺口大,换到该高里程客户处可加速达标'
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -76,26 +76,31 @@ export function generateSuggestions(
|
||||
const suggestions: SchedulingSuggestion[] = [];
|
||||
|
||||
// --- rescue_hopeless (high priority) ---
|
||||
// For this scenario: take the hopeless car away to a high-mileage customer,
|
||||
// and give the low-mileage customer a replacement from inventory.
|
||||
// Exclude near-qualified candidates (completionRate >= 80%) — no point swapping
|
||||
// in a car that's basically already at target.
|
||||
// Instead, pick cars with BIG gaps: they benefit from any mileage, even at a low customer.
|
||||
// Take the hopeless car away → give to high-mileage customer to sprint.
|
||||
// Replace with an inventory car that is CLOSE to qualifying — the low-mileage
|
||||
// customer's remaining driving days can push it over the finish line.
|
||||
//
|
||||
// Key insight: pick candidates where
|
||||
// candidate.totalMileage + customer.avgDaily × daysLeft >= yearTarget
|
||||
// i.e., the customer's daily driving is enough to finish the candidate's target.
|
||||
// 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 near-qualified: yearTarget known and already >= 80% done
|
||||
// Exclude already fully qualified
|
||||
const effectiveTarget = inv.yearTarget ?? vehicle.yearTarget;
|
||||
if (effectiveTarget > 0 && inv.totalMileage / effectiveTarget >= 0.8) return false;
|
||||
if (effectiveTarget > 0 && inv.totalMileage >= effectiveTarget) return false;
|
||||
return true;
|
||||
})
|
||||
.map((inv) => {
|
||||
const effectiveTarget = inv.yearTarget ?? vehicle.yearTarget;
|
||||
const mileageGap = Math.max(0, effectiveTarget - inv.totalMileage);
|
||||
const predictedAfterSwap =
|
||||
inv.totalMileage + vehicle.customerAvgDaily * vehicle.daysLeft;
|
||||
const predictedAfterSwap = inv.totalMileage + customerCanAdd;
|
||||
const canQualifyAfterSwap = predictedAfterSwap >= effectiveTarget;
|
||||
return {
|
||||
plateNumber: inv.plateNumber,
|
||||
@@ -113,15 +118,20 @@ export function generateSuggestions(
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Prefer biggest gap first — these benefit most from any mileage
|
||||
return b.mileageGap - a.mileageGap;
|
||||
// 1. Prefer "can qualify after swap" first
|
||||
if (a.canQualifyAfterSwap !== b.canQualifyAfterSwap)
|
||||
return a.canQualifyAfterSwap ? -1 : 1;
|
||||
// 2. Among qualifiable: smallest gap first (easiest to finish)
|
||||
// Among non-qualifiable: smallest gap first (closest to target)
|
||||
return a.mileageGap - b.mileageGap;
|
||||
})
|
||||
.slice(0, 5);
|
||||
|
||||
const yearRate = vehicle.yearTarget > 0 ? Math.round((vehicle.currentYearMileage / vehicle.yearTarget) * 100) : 0;
|
||||
const gap = Math.max(0, vehicle.yearTarget - vehicle.currentYearMileage);
|
||||
const reason = `该车在客户「${vehicle.customer}」处日均仅 ${Math.round(vehicle.customerAvgDaily)} km,本年完成率 ${yearRate}%,还差 ${fmtKmSimple(gap)} km 达标,按当前速度年底无法完成。`
|
||||
+ `\n建议:将此车调配给高里程客户冲刺达标,同时从库存调一辆已达标的车给当前客户。`;
|
||||
const canAddKm = Math.round(customerCanAdd);
|
||||
const reason = `该车在客户「${vehicle.customer}」处日均仅 ${Math.round(vehicle.customerAvgDaily)} km,完成率 ${yearRate}%,还差 ${fmtKmSimple(gap)} km,年底无法达标。`
|
||||
+ `\n建议:将此车换走给高里程客户冲刺,换上一辆快达标的车——该客户剩余 ${vehicle.daysLeft} 天还能跑约 ${fmtKmSimple(canAddKm)} km,足以帮缺口小的车冲线。`;
|
||||
|
||||
suggestions.push({
|
||||
id: `hopeless-${vehicle.plateNumber}`,
|
||||
|
||||
Reference in New Issue
Block a user