Replace front-end mock data with live API backed by:
- tab_energy_hydrogen_bill (66.5K rows) joined with
tab_hydrogen_site (internal stations) and tab_outside_hydrogen_site
(external stations, joined via inner_site_id)
- tab_energy_electricity_bill (4.4K rows, all 龙王路充电站)
New server routes (src/server/routes/energy/):
- GET /api/energy/hydrogen/overview → KPI + Top5 站点 + 区域占比
- GET /api/energy/hydrogen/daily?range=&customer= → 日级 + 站点级下钻
- GET /api/energy/electric/overview → KPI + 本月柱图 (fallback to last
available month if current month has no data)
- GET /api/energy/electric/monthly?customer= → 6 个月分组日级表
Business rules encoded server-side:
- 客户类型: customer_id IS NULL = 羚牛承担, NOT NULL = 外部
- 时区: DATETIME 列字面值是 UTC,分组前 +8h 转成 CST
- 数据清理: hydrogen_time >= 2024-01-01 (排除 1900 年脏数据)
- 站点名 fallback: short_name → name → fixed_station_name → station_name → '未知站点'
- 区域归一化: SUBSTRING_INDEX(city, '-', -1) 取最后一段,去掉 '省'/'市'
让 '四川省-成都市' 和 '成都市' 合并为 '成都'
Component changes:
- All 4 components (HydrogenOverview, HydrogenDaily, ElectricOverview,
ElectricDaily) now use useEffect + fetch with loading/error states
- HydrogenDaily filtering moved to server (range + customer params)
→ drops client-side TODAY constant + isInPick switch
- ElectricOverview chart title is dynamic: shows 'YYYY-MM 每日充电'
when fallback kicks in (current month has no data)
- mock.ts deleted
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Gate 智能调度 module on BI-SCHEDULE-OPT role (or full-access roles)
via shared canAccessScheduling helper, replacing hardcoded userId allowlist
- Thread roles[] through JWT payload → middleware → frontend nav
- Add router guard that 403s non-authorized users on /api/scheduling/*
- Emit replace_qualified suggestion for every qualified vehicle so list
count matches the 已完成考核目标 card; recalc qualifiedCount /
hopelessCount post-permission-filter for card↔list consistency
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
- Disable BYPASS_AUTH (was true, now false) — backend enforces JWT auth
- Scheduling suggestions filtered by department/manager permissions:
- full: see all suggestions
- department: see only own department's vehicles
- personal: see only own managed vehicles
- Candidate vehicles (inventory) remain fully visible to all
- Summary recalculated after permission filtering
- Consistent with mileage module permission model
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>
For rescue_hopeless (换走) scenario, filter out inventory candidates
where totalMileage/yearTarget >= 80%. These are already near target
and swapping them in adds no value.
Instead, prioritize candidates with biggest mileage gaps — they benefit
most from accumulating any mileage, even at a low-mileage customer.
Before: showed 粤AGR6869 (93% done, 缺口 1990) as "可达标" — pointless
After: shows 浙FF58720 (0% done, 缺口 60000) — genuinely needs mileage
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>
- Add currentYearMileage to SchedulingVehicleInfo (backend + frontend)
- Compute completionRate as currentYearMileage/yearTarget (year-based)
instead of using overall completion_rate from DB
- Display "本年已跑" instead of "累计" in detail modal
- Fix reason text to show year completion rate
Before: 累计 4.6万, 考核 3.0万, 完成率 12.1% (mismatched periods)
After: 本年已跑 8.3万, 考核 3.0万, 完成率 275% (consistent year-based)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Filter enriched vehicles to only include rent_status = '租赁' or '自营'.
Inventory candidates already filtered by truck_rent_status = 0 (在库).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
411 of 451 assessment vehicles had is_deleted=1 in tab_truck, causing type
classification to fall back to "其他" and miss all inventory matches. Fix:
- Remove is_deleted=0 filter from truck type query (assessment vehicles need type info regardless)
- Add inferTypeFromTargetName() fallback deriving type from target name when truck record is missing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- classifyVehicleType now parses dic_type.dic_name (e.g. "4.5吨冷链车") instead of raw model code
- Remove overly strict completionRate >= 0.8 filter for hopeless candidates
- Use vehicle's yearTarget as fallback when inventory has no assessment target
- Filter out suggestions with no candidates (not actionable)
- estimatedGain counts rescue_hopeless suggestions as potential gains
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>