refactor(scheduling): improve reason text, fix classification, polish detail view
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Classification: qualified requires actual completionRate >= 100% (not just predicted) - Reason text: structured two-column layout (客户日均 | 考核周期剩余) - Conclusion line in red bold (预估无法达标,需替换 / 已达标,建议换上未达标车辆) - Remove verbose subtitle from candidate section - Remove redundant middle line (预估考核期里程 vs 考核里程) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -83,26 +83,45 @@ export default function SuggestionDetail({ suggestion: s, onClose, onNotifySucce
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reason */}
|
||||
<div className="px-4 py-2 text-[11px] text-slate-500 leading-relaxed border-b border-slate-100 bg-amber-50/50">
|
||||
<span className="text-amber-700 font-bold">建议:</span>
|
||||
<span className="text-slate-600">{s.reason}</span>
|
||||
{/* Reason — structured lines */}
|
||||
<div className="px-4 py-2.5 border-b border-slate-100 bg-slate-50/60 space-y-1">
|
||||
{s.reason.split('\n').map((line, i) => {
|
||||
const isConclusion = line.startsWith('!!');
|
||||
const text = isConclusion ? line.slice(2) : line;
|
||||
if (isConclusion) {
|
||||
return (
|
||||
<div key={i} className="mt-1.5 pt-1.5 border-t border-slate-200">
|
||||
<span className="text-xs font-bold text-rose-600">{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Split by | for two-column layout
|
||||
if (text.includes('|')) {
|
||||
const parts = text.split('|').map(p => p.trim());
|
||||
return (
|
||||
<div key={i} className="flex items-center justify-between text-[11px] py-0.5">
|
||||
<span className="text-slate-600">{parts[0]}</span>
|
||||
<span className="text-slate-600">{parts[1]}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-1.5 text-[11px] py-0.5">
|
||||
<span className="w-1 h-1 rounded-full bg-slate-300 flex-shrink-0" />
|
||||
<span className="text-slate-600">{text}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Candidates */}
|
||||
<div className="px-4 py-3">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center justify-between mb-2.5">
|
||||
<span className="text-xs font-bold text-slate-700">
|
||||
{isRescue ? '从库存调入替换' : '换上以下里程少的车'}
|
||||
{isRescue ? '建议替换车辆' : '建议替换车辆'}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400">{s.candidates.length} 辆可选</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-400 mb-2.5">
|
||||
{isRescue
|
||||
? '以下车辆快达标,换到当前客户处利用剩余天数即可冲线'
|
||||
: '以下车辆里程缺口大,换到该高里程客户处可加速达标'
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{s.candidates.map(c => {
|
||||
|
||||
@@ -25,11 +25,15 @@ export function isTypeCompatible(sourceType: string, candidateType: string): boo
|
||||
|
||||
export function classifyVehicle(
|
||||
currentYearIsQualified: boolean,
|
||||
predictedYearEnd: number,
|
||||
currentYearMileage: number,
|
||||
yearTarget: number,
|
||||
predictedYearEnd: number,
|
||||
): VehicleClassification {
|
||||
if (currentYearIsQualified || predictedYearEnd / yearTarget >= 1.2) return 'qualified';
|
||||
if (predictedYearEnd / yearTarget < 0.6) return 'hopeless';
|
||||
// qualified: current year mileage already >= target (actually done, not just predicted)
|
||||
const actualRate = yearTarget > 0 ? currentYearMileage / yearTarget : 0;
|
||||
if (currentYearIsQualified || actualRate >= 1.0) return 'qualified';
|
||||
// hopeless: even with remaining days, predicted < 60% of target
|
||||
if (yearTarget > 0 && predictedYearEnd / yearTarget < 0.6) return 'hopeless';
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
@@ -127,9 +131,10 @@ export function generateSuggestions(
|
||||
})
|
||||
.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 = `客户日均 ${Math.round(vehicle.customerAvgDaily)} km · 完成率 ${yearRate}% · 缺口 ${fmtKmSimple(gap)} km · 剩余 ${vehicle.daysLeft} 天(约 ${fmtKmSimple(Math.round(customerCanAdd))} km)`;
|
||||
const dailyReq = vehicle.daysLeft > 0 ? Math.round(gap / vehicle.daysLeft) : 0;
|
||||
const predictedTotal = Math.round(vehicle.currentYearMileage + customerCanAdd);
|
||||
const reason = `客户日均 ${Math.round(vehicle.customerAvgDaily)} km | 考核周期剩余 ${vehicle.daysLeft} 天 · 日均需 ${fmtKmSimple(dailyReq)} km\n!!预估无法达标,需替换`;
|
||||
|
||||
suggestions.push({
|
||||
id: `hopeless-${vehicle.plateNumber}`,
|
||||
@@ -194,7 +199,7 @@ export function generateSuggestions(
|
||||
|
||||
const yearRate = vehicle.yearTarget > 0 ? Math.round((vehicle.currentYearMileage / vehicle.yearTarget) * 100) : 0;
|
||||
const canAddKm = vehicle.customerAvgDaily * vehicle.daysLeft;
|
||||
const reason = `客户日均 ${Math.round(vehicle.customerAvgDaily)} km · 完成率 ${yearRate}% · 剩余 ${vehicle.daysLeft} 天(约 ${fmtKmSimple(Math.round(canAddKm))} km)`;
|
||||
const reason = `客户日均 ${Math.round(vehicle.customerAvgDaily)} km\n已完成考核(完成率 ${yearRate}%)\n考核周期剩余 ${vehicle.daysLeft} 天,可为新车贡献约 ${fmtKmSimple(Math.round(canAddKm))} km\n!!已达标,建议换上未达标车辆`;
|
||||
|
||||
suggestions.push({
|
||||
id: `qualified-${vehicle.plateNumber}`,
|
||||
|
||||
@@ -212,7 +212,7 @@ app.get('/', async (c) => {
|
||||
const predictedYearEnd = currentYearMileage + customerAvgDaily * daysLeft;
|
||||
|
||||
const currentYearIsQualified = row.current_year_is_qualified === 1;
|
||||
const classification = classifyVehicle(currentYearIsQualified, predictedYearEnd, yearTarget);
|
||||
const classification = classifyVehicle(currentYearIsQualified, currentYearMileage, yearTarget, predictedYearEnd);
|
||||
|
||||
enrichedVehicles.push({
|
||||
plateNumber: plate,
|
||||
|
||||
Reference in New Issue
Block a user