feat(scheduling): persist notifications, batch notify flow, dedup protection

- Add tab_scheduling_notifications table with bootstrap via ensureSchedulingTables()
- Notify endpoint rewritten: dedup by (suggestion_id, candidate_plate), history list, PATCH /:id for execute/cancel lifecycle
- Batch notify endpoint returns success/skipped/failed counts
- Suggestions response now carries notificationId + notificationStatus per candidate (joined from active-notification map)
- UI: select mode with checkboxes, floating action bar, confirmation modal listing each swap; already-notified items are dimmed and skipped
- Detail view badges show sent/executed state, preventing duplicate notify

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-16 23:43:21 +08:00
parent 31716c6547
commit 3ef0d4edfa
12 changed files with 614 additions and 33 deletions

View File

@@ -5,6 +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 type { EnrichedVehicle, InventoryVehicle, SchedulingResponse, SchedulingSummary } from './types.js';
import type { AuthUser } from '../../auth/types.js';
@@ -275,6 +276,19 @@ app.get('/', async (c) => {
// ---- Run algorithm ----
const { suggestions, summary } = generateSuggestions(enrichedVehicles, inventoryVehicles);
// ---- Attach notification status to candidates ----
const notificationMap = await fetchActiveNotificationMap();
for (const s of suggestions) {
for (const c of s.candidates) {
const key = `${s.id}::${c.plateNumber}`;
const notif = notificationMap.get(key);
if (notif) {
c.notificationId = notif.id;
c.notificationStatus = notif.status;
}
}
}
// ---- Permission filtering & customer name masking ----
const user = (c as any).get('user') as AuthUser | undefined;