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

@@ -1,5 +1,13 @@
import { fetchJson } from '../../auth/api-client';
import type { SchedulingResponse } from './types';
import type {
SchedulingResponse,
NotifyRequest,
NotifyBatchRequest,
NotifyBatchResult,
NotificationRecord,
NotificationStatus,
UpdateNotificationRequest,
} from './types';
const BASE = '/api/scheduling';
@@ -10,14 +18,44 @@ export async function fetchSuggestions(targetId?: number): Promise<SchedulingRes
return fetchJson<SchedulingResponse>(`${BASE}/suggestions${qs ? `?${qs}` : ''}`);
}
export async function sendNotify(body: {
suggestionId: string;
currentPlate: string;
candidatePlate: string;
}): Promise<{ success: boolean; message: string }> {
return fetchJson<{ success: boolean; message: string }>(`${BASE}/notify`, {
export async function sendNotify(
body: NotifyRequest,
): Promise<{ success: boolean; message: string; record?: NotificationRecord }> {
return fetchJson(`${BASE}/notify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}
export async function sendNotifyBatch(
body: NotifyBatchRequest,
): Promise<{ success: boolean; message: string; result: NotifyBatchResult }> {
return fetchJson(`${BASE}/notify/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}
export async function fetchNotifications(
status?: NotificationStatus,
limit?: number,
): Promise<{ records: NotificationRecord[] }> {
const params = new URLSearchParams();
if (status) params.set('status', status);
if (limit) params.set('limit', String(limit));
const qs = params.toString();
return fetchJson(`${BASE}/notify${qs ? `?${qs}` : ''}`);
}
export async function updateNotification(
id: number,
body: UpdateNotificationRequest,
): Promise<{ success: boolean; record?: NotificationRecord; message?: string }> {
return fetchJson(`${BASE}/notify/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}