Previously the toggle hid cancelled records, so users who clicked a
record timestamped within 7 days but later cancelled would see nothing.
Now 近7天 filters purely by createdAt; combine with status tabs to
narrow further.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Each row in 调度记录 now shows 业务部门(简)/业务负责人/客户 beneath
the plate line, and is clickable to open the reusable SwapPreview
showing the full replacement plan (current mileage, 考核目标, 替换后预测).
Drill-in is only enabled when the suggestion is still in the active
scheduling view; the user can still 取消干预 from the preview.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Business rule: a running vehicle can hold AT MOST ONE active (sent|executed)
intervention. Switching to a different candidate requires cancelling the
prior one first.
- Server: insertNotification dedup key changes from (suggestion_id,
candidate_plate) to just suggestion_id; 409 response includes the blocking
candidate plate
- Detail modal: shows a banner naming the locked candidate; non-active
candidates render a disabled "该车已有其他干预,请先解除" hint instead
of the action button
- Batch: pickBestCandidate returns null for any suggestion already holding
an active intervention — the whole suggestion is excluded
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Globally rename user-facing 通知 → 干预 (list badge, detail button, batch
modal, CSV header, server response messages, db table comment)
- 已干预 row in detail is now clickable — opens SwapPreview which shows
a read-only summary plus a 取消干预 action (PATCH notify /:id with
status=cancelled). Sending is blocked while already intervened.
- Selected suggestion now follows the latest data snapshot so status
changes from within the detail flow propagate immediately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add 调度记录 modal: lists notifications by status, supports 标记已执行 (with
after-mileage + notes) and 取消 for open records
- Add CSV export of filtered suggestions (UTF-8 BOM for Excel); top candidate
per row picked by same-region > can-qualify preference
- Compute customer 7-day average alongside 30-day baseline in a single query;
show trend indicator (up/down/flat) next to 客户日均 in list and detail card
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
- Extract shared types to src/shared/scheduling/types.ts (client/server both re-export)
- Convert SchedulingSuggestion.reason from string to structured { lines, conclusion }
- Remove hard region filter; algorithm keeps cross-region candidates with isSameRegion flag
- SuggestionDetail renders same-region vs cross-region sections with a divider
- Close detail modal when selected suggestion no longer exists in data
- Unify estimatedGain definition (strict canQualifyAfterSwap) between algorithm and API layers
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Different assessment targets have different end dates. Previously all
candidates used the current vehicle's daysLeft, causing wrong predictions.
Now each inventory vehicle computes its own daysLeft from its assessment
target's current_year_assessment_end_date. predictedAfterSwap uses the
candidate's own daysLeft instead of the current vehicle's.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove type badge, reason section — too verbose
- Two clean white cards connected by arrow (swap diagram)
- Result section: predicted mileage, target, conclusion badge
- Tighter spacing, no redundant labels
- Professional tone, no childish wording
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
- Full-page skeleton on initial load: card placeholders + list row placeholders
- List skeleton on refresh: 6 rows with pulse animation
- Skeleton blocks match actual layout (color bar, plate, badges, info line)
- Uses Tailwind animate-pulse for smooth loading effect
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Customer name in list items: truncate with max-w-[40%]
- Daily km and completion rate: flex-shrink-0 to stay on same line
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add department and manager fields to backend types and suggestions API
- Add department/manager to advanced filter panel
- Refine card colors: orange (换下) / blue (换走) / dark slate (全部)
- Selected card uses solid bg color, inactive uses gradient
- Batch pills use dark slate, confirm button uses dark slate
- Background changed to #F0F4F8 for subtle cool tone
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>