feat(scheduling): add 近期已干预 summary card (last 7 days)

Restore 替换建议 card and add a new emerald 近期已干预 card. Clicking
opens the history modal pre-filtered to the last 7 days (excluding
cancelled) via a toggle chip users can switch off.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-17 09:25:59 +08:00
parent 1b2ad68743
commit ba1e0e9f16
6 changed files with 73 additions and 11 deletions

View File

@@ -252,6 +252,7 @@ export function generateSuggestions(
hopelessCount: hopeless.length,
suggestionCount: filteredSuggestions.length,
estimatedGain,
recentInterventionCount: 0,
};
return { suggestions: filteredSuggestions, summary };

View File

@@ -34,6 +34,18 @@ function rowToRecord(row: any): NotificationRecord {
};
}
/**
* Count non-cancelled interventions created within the last 7 days.
*/
export async function fetchRecentInterventionCount(): Promise<number> {
const [rows] = (await pool.execute(
`SELECT COUNT(*) AS cnt FROM tab_scheduling_notifications
WHERE status != 'cancelled'
AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)`,
)) as [any[], unknown];
return rows.length > 0 ? Number(rows[0].cnt) || 0 : 0;
}
/**
* Fetch notification status map for the currently-visible (suggestion, candidate) pairs.
* Key: `${suggestionId}::${candidatePlate}` → latest non-cancelled notification.

View File

@@ -5,7 +5,7 @@ import { fetchVehicleInfoMap } from '../mileage/vehicle-info.js';
import { mapRegion } from '../vehicles.js';
import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js';
import { classifyVehicle, generateSuggestions } from './algorithm.js';
import { fetchActiveNotificationMap } from './notify.js';
import { fetchActiveNotificationMap, fetchRecentInterventionCount } from './notify.js';
import type { EnrichedVehicle, InventoryVehicle, SchedulingResponse, SchedulingSummary } from './types.js';
import type { AuthUser } from '../../auth/types.js';
@@ -342,6 +342,7 @@ app.get('/', async (c) => {
// Recalculate summary based on permission-filtered results
const filteredQualified = masked.filter((s: any) => s.type === 'replace_qualified').length;
const filteredHopeless = masked.filter((s: any) => s.type === 'rescue_hopeless').length;
const recentInterventionCount = await fetchRecentInterventionCount();
const filteredSummary: SchedulingSummary = {
qualifiedCount: summary.qualifiedCount,
hopelessCount: summary.hopelessCount,
@@ -349,6 +350,7 @@ app.get('/', async (c) => {
estimatedGain: masked.filter((s: any) =>
s.candidates?.some((c: any) => c.canQualifyAfterSwap)
).length,
recentInterventionCount,
};
const response: SchedulingResponse = {
@@ -362,7 +364,7 @@ app.get('/', async (c) => {
console.error('scheduling suggestions error:', e);
return c.json(
{
summary: { qualifiedCount: 0, hopelessCount: 0, suggestionCount: 0, estimatedGain: 0 },
summary: { qualifiedCount: 0, hopelessCount: 0, suggestionCount: 0, estimatedGain: 0, recentInterventionCount: 0 },
suggestions: [],
targets: [],
} satisfies SchedulingResponse,