Commit Graph

27 Commits

Author SHA1 Message Date
kkfluous
91202bdf71 fix asset module database migration
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
2026-06-17 11:53:39 +08:00
kkfluous
200172f0af feat(scheduling): role-based access + align list count with qualifiedCount
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- 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>
2026-04-17 15:42:21 +08:00
kkfluous
a954fb90f6 refactor(scheduling): 考核剩余 → 年度考核剩余
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 11:04:08 +08:00
kkfluous
ba1e0e9f16 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>
2026-04-17 09:25:59 +08:00
kkfluous
1b2ad68743 fix(scheduling): enforce one active intervention per current vehicle
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>
2026-04-17 09:19:43 +08:00
kkfluous
210db7f8ff feat(scheduling): rename 通知→干预, allow drill-in on intervened items
- 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>
2026-04-17 09:14:53 +08:00
kkfluous
1d9f4cb43d feat(scheduling): history view, execute/cancel lifecycle, CSV export, 7d trend
- 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>
2026-04-16 23:47:31 +08:00
kkfluous
3ef0d4edfa 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>
2026-04-16 23:43:21 +08:00
kkfluous
31716c6547 refactor(scheduling): shared types, structured reason, cross-region candidates
- 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>
2026-04-16 23:36:38 +08:00
kkfluous
aa9a29fed8 fix(scheduling): use each candidate's own daysLeft for prediction
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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>
2026-04-16 22:58:47 +08:00
kkfluous
7aa0d961ce feat(scheduling): show all candidates instead of top 5, update section title
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-16 22:44:51 +08:00
kkfluous
d0984a430b refactor(scheduling): improve reason text, fix classification, polish detail view
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Classification: qualified requires actual completionRate >= 100% (not just predicted)
- Reason text: structured two-column layout (客户日均 | 考核周期剩余)
- Conclusion line in red bold (预估无法达标,需替换 / 已达标,建议换上未达标车辆)
- Remove verbose subtitle from candidate section
- Remove redundant middle line (预估考核期里程 vs 考核里程)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:32:50 +08:00
kkfluous
0a7a9a096d refactor(scheduling): condense reason text to data-only one-liner
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-16 22:11:37 +08:00
kkfluous
694e9a207a feat(scheduling): enable department/personal permission filtering
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- 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>
2026-04-16 22:04:52 +08:00
kkfluous
0785c78382 fix(scheduling): only show candidates that can actually qualify after swap
replace_qualified (换下):
- Exclude already-qualified inventory (totalMileage >= yearTarget, gap=0)
- Only keep candidates where canQualifyAfterSwap=true
- Skip suggestions with no qualifiable candidates (e.g., too few days left)
- Reason text now shows customer's remaining capacity: "日均 318km × 53天 ≈ 1.7万km"

Before: showed 粤AGP9738 (缺口 0, already at target) — pointless
After: shows 粤AGQ5808 (缺口 1.7万, 换后 3.0万, 可达标) — meaningful

All replace_qualified candidates now guaranteed canQualifyAfterSwap=true.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:41:08 +08:00
kkfluous
afec75a1cc fix(scheduling): rescue candidates should be close-to-qualifying, not zero-mileage
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>
2026-04-16 21:36:53 +08:00
kkfluous
1d1f8901aa fix(scheduling): exclude near-qualified vehicles from rescue candidates
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>
2026-04-16 21:34:05 +08:00
kkfluous
9398688829 feat(scheduling): add department/manager filters, refine color palette
- 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>
2026-04-16 21:28:04 +08:00
kkfluous
48fa3bc73f refactor(scheduling): rewrite terminology to match core business logic
Core story: 里程高的车换下来,里程少的车换上去。

- Summary cards: "里程高·需换下" / "里程低·需换走" / "替换建议"
- List tags: "换下" (amber) / "换走" (blue) with matching color bars
- Detail modal title: "里程高·换下此车" / "里程低·换走此车"
- Candidate section: explains WHY these vehicles are recommended
  - 换下: "以下车辆里程缺口大,换到该高里程客户处可加速达标"
  - 换走: "以下车辆里程已充足,可调给当前客户,将此车换走给高里程客户冲刺"
- Reason text: states current situation + clear action recommendation
  with specific numbers (已跑, 缺口, 日均, 完成率)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:23:35 +08:00
kkfluous
bcbeb64e28 fix(scheduling): use current year mileage for consistent data display
- 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>
2026-04-16 21:08:29 +08:00
kkfluous
ec3b079311 fix(scheduling): only suggest replacements for rented/operated vehicles
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>
2026-04-16 20:52:16 +08:00
kkfluous
033af15814 fix(scheduling): include soft-deleted trucks and infer type from target name
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>
2026-04-16 20:47:00 +08:00
kkfluous
253cc2f2c0 fix(scheduling): fix vehicle type classification and algorithm candidate matching
- 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>
2026-04-16 20:31:44 +08:00
kkfluous
4169e04a9c feat(scheduling): add suggestions route with data aggregation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 20:23:56 +08:00
kkfluous
86d5bc8738 feat(scheduling): add notify route and wire up scheduling router
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 20:23:29 +08:00
kkfluous
460c9906e1 feat(scheduling): add algorithm pure functions and export mapRegion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 20:21:50 +08:00
kkfluous
ebe46c6f73 feat(scheduling): add backend type definitions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 20:20:18 +08:00