feat(hold-overview): add Hold Lot Overview page with TreeMap, Matrix, and cascade filtering

Provide managers with a dedicated page to analyze hold lots across all stations.
Extends existing service functions (get_hold_detail_summary, get_hold_detail_lots,
get_wip_matrix) with optional parameters for backward compatibility, adds one new
function (get_hold_overview_treemap), and registers the page in the portal navigation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
egg
2026-02-10 13:02:24 +08:00
parent af59031f95
commit 8225863a85
31 changed files with 3414 additions and 44 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-10

View File

@@ -0,0 +1,127 @@
## Context
MES Dashboard 已有 WIP Overview全局 RUN/QUEUE/HOLD 概況)和 Hold Detail單一 Hold Reason 明細)。主管需要一個專用頁面,聚焦在 Hold Lot 的全局分析。
現有架構Vue 3 SFC + Flask + OracleDWH.DW_MES_LOT_V使用 Redis cache + snapshot indexes 加速查詢。經審計wip_service.py 中已有大量可複用的函數和 cache 基礎設施:
**可直接呼叫**:
- `get_wip_matrix(status='HOLD', hold_type=...)` — Matrix 查詢,零改動
- `_select_with_snapshot_indexes(status='HOLD', hold_type=...)` — 已有 `wip_status['HOLD']``hold_type['quality'|'non-quality']` snapshot indexes
- `_get_wip_dataframe()` — L1 process cache (30s) → L2 Redis cache`AGEBYDAYS` 欄位已在 View 中預先計算
**可擴充(向後相容)**:
- `get_hold_detail_summary(reason)` — 已有 totalLots/totalQty/avgAge/maxAge/workcenterCountreason 改 optional 即可
- `get_hold_detail_lots(reason, ...)` — 已有完整分頁邏輯 + workcenter/package/age_range 過濾reason 改 optional + 加 hold_type 即可
**前端可直接 import**:
- `hold-detail/SummaryCards.vue` — props `{ totalLots, totalQty, avgAge, maxAge, workcenterCount }` 完全相容
- `wip-shared/Pagination.vue``useAutoRefresh``core/api.js``wip-shared/constants.js`
## Goals / Non-Goals
**Goals:**
- 提供主管一覽各站 Hold Lot 情況的獨立頁面
- TreeMap 視覺化讓嚴重程度一目了然(面積=QTY顏色=滯留天數)
- Matrix + TreeMap + Table 三層 cascade 篩選,互動流暢
- 預設品質異常 Hold可切換
- 最大化複用現有 service 函數、cache 基礎設施和前端元件
**Non-Goals:**
- 不取代或修改現有 Hold Detail 頁面(擴充 service 函數需向後相容)
- 不新增資料庫 view 或 table — 完全複用 DWH.DW_MES_LOT_V
- 不新增 SQL 模板 — 複用現有 summary.sql / matrix.sql / detail.sql + QueryBuilder WHERE clause
- 不實作 autocomplete 搜尋(篩選僅 Hold Type + Reason dropdown
- 不實作 Lot 點擊展開的 detail panel明細表為純展示
## Decisions
### D1: 擴充現有 service 函數,非新建
**決定**: 擴充 `get_hold_detail_summary()``get_hold_detail_lots()` 的參數簽名,而非建立新的 `get_hold_overview_summary()` / `get_hold_overview_lots()`。唯一全新的函數是 `get_hold_overview_treemap()`WC × Reason 聚合邏輯不存在於現有函數)。
**擴充方式**:
- `get_hold_detail_summary(reason)``get_hold_detail_summary(reason=None, hold_type=None)`
- `reason=None` 時聚合所有 HOLD lots
- `hold_type='quality'|'non-quality'` 進一步過濾
- 原有 Hold Detail 呼叫 `get_hold_detail_summary(reason='xxx')` 行為不變
- `get_hold_detail_lots(reason, ...)``get_hold_detail_lots(reason=None, hold_type=None, treemap_reason=None, ...)`
- `reason=None` 時返回所有 HOLD lots
- `treemap_reason` 支援 TreeMap 點擊篩選
- 現有 Hold Detail 呼叫簽名不受影響
**理由**: 這兩個函數的核心邏輯cache path + Oracle fallback + 分頁 + 過濾)完全相同,差異僅在 reason 是否為必填。新建函數會複製 80%+ 相同邏輯,增加維護負擔。
**替代方案**: 新建獨立函數 — 但會造成大量重複的 cache 查詢路徑和 Oracle fallback 邏輯。
### D2: Matrix API 直接呼叫 get_wip_matrix僅擴充 reason 參數
**決定**: Hold Overview 的 Matrix API 直接呼叫現有 `get_wip_matrix(status='HOLD', hold_type=...)` 函數。唯一需要擴充的是新增 optional `reason` 參數,支援 Filter Bar 的 Reason 篩選。
**理由**: `get_wip_matrix` 已支援 `status``hold_type` 參數,能完全滿足 Hold Overview Matrix 的需求。擴充 reason 參數的改動量極小,且 reason=None 時行為與現有完全一致。
**替代方案**: 複製 matrix 邏輯到新函數 — 違反 DRY且 matrix 排序/分頁邏輯會分散維護。
### D3: TreeMap 聚合是唯一需要全新建立的 service 函數
**決定**: 新增 `get_hold_overview_treemap()` 函數,後端返回已聚合的 `{ workcenter, reason, qty, lots, avgAge }` 陣列。此函數使用與其他函數相同的 `_select_with_snapshot_indexes()` + Oracle fallback 模式。
**理由**: 現有函數都沒有「按 (WORKCENTER_GROUP, HOLDREASONNAME) 二維聚合」的邏輯。這是 TreeMap 視覺化特有的需求值得獨立建立。Hold Lot 可能數千筆,前端 groupBy 聚合會造成不必要的資料傳輸和 CPU 開銷。
**替代方案**: 前端從 lots API 取全部資料後自行聚合 — 資料量大時效能差,且需一次載入所有 lot。
### D4: TreeMap 顏色映射使用固定 4 級色階
**決定**: 平均滯留天數映射到 4 個顏色等級:
- `< 1 天` → 綠色 (#22c55e)
- `1-3 天` → 黃色 (#eab308)
- `3-7 天` → 橙色 (#f97316)
- `> 7 天` → 紅色 (#ef4444)
ECharts TreeMap 使用 `visualMap` 組件實現連續色階。
**理由**: 與 Hold Detail 的 Age Distribution 分段一致0-1, 1-3, 3-7, 7+),主管認知模型統一。
### D5: Filter cascade 為前端狀態管理,不影響 Summary 和 Matrix 的 API 呼叫
**決定**:
- Filter BarHold Type / Reason變更 → 呼叫全部 4 支 API
- Matrix 點擊 → 前端設定 `matrixFilter`,僅呼叫 treemap + lots API
- TreeMap 點擊 → 前端設定 `treemapFilter`,僅呼叫 lots API
**理由**: Summary 和 Matrix 反映全局數據,不應被 Matrix/TreeMap 的 drilldown 操作影響。這與 WIP Overview 的 StatusCards 不影響 Summary 的模式一致。
### D6: 路由結構與 Blueprint 獨立
**決定**: 新建 `hold_overview_routes.py` 作為獨立 Blueprint`hold_overview_bp`),路由前綴 `/api/hold-overview/`。頁面路由 `GET /hold-overview` 由此 Blueprint 提供。
**理由**: 與 `hold_routes.py`Hold Detail`wip_routes.py`WIP Overview平行職責分離。
### D7: 前端元件複用策略 — import > 擴充 > 新建
**決定**:
| 元件 | 策略 | 理由 |
|------|------|------|
| `hold-detail/SummaryCards.vue` | **直接 import** | props 形狀 `{ totalLots, totalQty, avgAge, maxAge, workcenterCount }` 完全相容 |
| `wip-shared/Pagination.vue` | **直接 import** | 已由 hold-detail/LotTable 使用,通用元件 |
| `wip-overview/MatrixTable.vue` | **參考新建 `HoldMatrix.vue`** | 需要 cell click + column click + active highlight — 原有只有 row drilldown改動幅度大不適合直接修改原件 |
| `hold-detail/LotTable.vue` | **參考新建 `LotTable.vue`** | 需加 Hold Reason 欄位 + 移除 Spec 欄位 — hold-detail 不需要 reason 欄(已在 URL 參數中),直接修改會破壞現有頁面 |
| `HoldTreeMap.vue` | **全新** | 無現有 TreeMap 元件 |
| `FilterBar.vue` | **全新** | Hold Type radio + Reason dropdown 是此頁獨有的 UI |
| `FilterIndicator.vue` | **全新** | cascade filter 指示器是此頁獨有的 UI |
**理由**: 直接修改跨頁面共用的元件有破壞現有頁面的風險。props 完全相容的元件直接 import需要結構性改動的元件則基於現有程式碼新建保留一致的 coding pattern 但避免耦合。
### D8: ECharts TreeMap 模組 tree-shaking
**決定**: 前端使用 `import { TreemapChart } from 'echarts/charts'` 按需導入,搭配現有 `vendor-echarts` chunk。
**理由**: 現有 ECharts vendor chunk 已包含 BarChart、LineChart 等。TreemapChart 加入後仍在同一 chunk不增加額外 HTTP request。
## Risks / Trade-offs
- **[向後相容]** 擴充 `get_hold_detail_summary()``get_hold_detail_lots()` 簽名時,必須確保 reason 參數的預設行為不變 → 使用 `reason=None` 預設值,現有呼叫端傳入 reason 的行為完全不變;需補充單元測試覆蓋原有 Hold Detail 的呼叫路徑
- **[TreeMap 資料量]** 如果 Hold Reason 種類很多(>20TreeMap 小區塊會難以辨識 → 可考慮只顯示 Top N reason其餘歸為「其他」
- **[Matrix 與 TreeMap 同時篩選]** 使用者可能忘記已有 matrix 篩選,誤以為 TreeMap 是全局 → 需要明確的 active filter 指示器和一鍵清除功能
- **[ECharts TreeMap 效能]** 大量區塊時 TreeMap 渲染可能卡頓 → ECharts TreeMap 有內建 leafDepth 限制,測試時注意超過 200 個葉節點的情境
- **[Cache 一致性]** Hold Overview 與 WIP Overview 共用同一份 cacheauto-refresh 週期相同10 分鐘),不需調整 cache 策略

View File

@@ -0,0 +1,36 @@
## Why
主管需要一個獨立頁面,專注於線上 Hold Lot 的全局觀。目前 WIP Overview 的 Pareto 圖混合在所有 WIP 資料中,而 Hold Detail 只能看單一 Hold Reason 的明細。缺少一個可以「一覽各站 Hold Lot 情況」的專用分析頁面,讓主管能快速掌握哪些站別、哪些原因造成最多 Hold以及滯留嚴重程度。
## What Changes
- 新增 `/hold-overview` 頁面Vue 3 SFC + ECharts TreeMap獨立於現有 WIP Overview 和 Hold Detail
- 新增 Flask Blueprint 與 4 支 API endpointssummary / matrix / treemap / lots
- 頁面預設只顯示品質異常 Hold可切換至非品質異常或全部
- 提供 Workcenter x Package Matrix如 WIP Overview數字可點擊篩選下方所有資料
- 提供 TreeMap 視覺化WC → Reason 層級,面積=QTY顏色=平均滯留天數)
- 提供 paginated Hold Lot 明細表
- 篩選 cascade 機制Filter Bar → 全部重載Matrix 點擊 → TreeMap + TableTreeMap 點擊 → Table
- 新增 Vite multi-entry 設定
## Capabilities
### New Capabilities
- `hold-overview-page`: Hold Lot Overview 頁面的完整功能規格包含篩選器、Summary Cards、Matrix、TreeMap、明細表及 filter cascade 互動邏輯
- `hold-overview-api`: Hold Overview 後端 API 端點summary / matrix / treemap / lots從 DWH.DW_MES_LOT_V 查詢 Hold Lot 資料
### Modified Capabilities
- `vue-vite-page-architecture`: 新增 `hold-overview` 作為 Vite multi-entry HTML entry point
## Impact
- **Backend擴充現有**: 擴充 `wip_service.py``get_hold_detail_summary()``get_hold_detail_lots()` — 將 `reason` 改為 optional 並新增 `hold_type` 參數,向後相容;擴充 `get_wip_matrix()` 新增 optional `reason` 參數
- **Backend唯一新增函數**: `get_hold_overview_treemap()` — WC × Reason 聚合 + avgAge 計算
- **Backend新增路由**: `src/mes_dashboard/routes/hold_overview_routes.py`Flask Blueprint4 支 API
- **Frontend直接複用**: `hold-detail/SummaryCards.vue``wip-shared/Pagination.vue``useAutoRefresh``core/api.js``wip-shared/constants.js`
- **Frontend基於現有擴充**: 基於 `MatrixTable.vue``HoldMatrix.vue`(加 cell/column click基於 `hold-detail/LotTable.vue``LotTable.vue`(加 Hold Reason 欄位)
- **Frontend全新元件**: `HoldTreeMap.vue``FilterBar.vue``FilterIndicator.vue`
- **Vite Config**: `vite.config.js` 新增 `hold-overview` entry
- **Dependencies**: `echarts/charts``TreemapChart`(進入現有 `vendor-echarts` chunk
- **Cache**: 完全複用現有 Redis cache + snapshot indexes已有 `wip_status['HOLD']``hold_type['quality'|'non-quality']` 索引),零改動
- **SQL**: 不需新增 SQL 模板 — 複用現有 summary.sql / matrix.sql / detail.sql + QueryBuilder WHERE clause

View File

@@ -0,0 +1,137 @@
## ADDED Requirements
### Requirement: Hold Overview API SHALL provide summary statistics
The API SHALL return aggregated summary KPIs for hold lots.
#### Scenario: Summary endpoint delegates to extended get_hold_detail_summary
- **WHEN** `GET /api/hold-overview/summary` is called with `hold_type=quality`
- **THEN** the route SHALL delegate to the extended `get_hold_detail_summary(reason=None, hold_type='quality')`
- **THEN** the response SHALL return `{ success: true, data: { totalLots, totalQty, workcenterCount, avgAge, maxAge, dataUpdateDate } }`
- **THEN** only lots with EQUIPMENTCOUNT=0 AND CURRENTHOLDCOUNT>0 SHALL be included
- **THEN** hold_type classification SHALL use the same NON_QUALITY_HOLD_REASONS set as existing hold endpoints
#### Scenario: Summary with reason filter
- **WHEN** `GET /api/hold-overview/summary?hold_type=quality&reason=品質確認` is called
- **THEN** the response SHALL only include lots where HOLDREASONNAME equals the specified reason
#### Scenario: Summary hold_type=all
- **WHEN** `GET /api/hold-overview/summary?hold_type=all` is called
- **THEN** the response SHALL include both quality and non-quality hold lots
#### Scenario: Summary error
- **WHEN** the database query fails
- **THEN** the response SHALL return `{ success: false, error: '查詢失敗' }` with HTTP 500
### Requirement: Hold Overview API SHALL provide workcenter x package matrix
The API SHALL return a cross-tabulation of workcenters and packages for hold lots.
#### Scenario: Matrix endpoint
- **WHEN** `GET /api/hold-overview/matrix` is called with `hold_type=quality`
- **THEN** the response SHALL return the same matrix structure as `/api/wip/overview/matrix`: `{ workcenters, packages, matrix, workcenter_totals, package_totals, grand_total }`
- **THEN** workcenters SHALL be sorted by WORKCENTERSEQUENCE_GROUP
- **THEN** packages SHALL be sorted by total QTY descending
- **THEN** only HOLD status lots matching the hold_type SHALL be included
#### Scenario: Matrix delegates to existing get_wip_matrix
- **WHEN** the matrix endpoint is called
- **THEN** it SHALL delegate to `get_wip_matrix(status='HOLD', hold_type=...)` from wip_service.py
#### Scenario: Matrix with reason filter
- **WHEN** `GET /api/hold-overview/matrix?hold_type=quality&reason=品質確認` is called
- **THEN** the matrix SHALL only include lots where HOLDREASONNAME equals the specified reason
### Requirement: Hold Overview API SHALL provide TreeMap aggregation data
The API SHALL return aggregated data suitable for TreeMap visualization.
#### Scenario: TreeMap endpoint uses new get_hold_overview_treemap function
- **WHEN** `GET /api/hold-overview/treemap` is called with `hold_type=quality`
- **THEN** the route SHALL delegate to `get_hold_overview_treemap()` (the only new service function)
- **THEN** the response SHALL return `{ success: true, data: { items: [...] } }`
- **THEN** each item SHALL contain `{ workcenter, reason, lots, qty, avgAge }`
- **THEN** items SHALL be grouped by (WORKCENTER_GROUP, HOLDREASONNAME)
- **THEN** avgAge SHALL be calculated using the pre-computed AGEBYDAYS column from DW_MES_LOT_V
#### Scenario: TreeMap with matrix filter
- **WHEN** `GET /api/hold-overview/treemap?hold_type=quality&workcenter=WC-MOLD&package=PKG-A` is called
- **THEN** the response SHALL only include lots matching the workcenter AND package filters
#### Scenario: TreeMap with reason filter
- **WHEN** `GET /api/hold-overview/treemap?hold_type=quality&reason=品質確認` is called
- **THEN** the response SHALL only include lots where HOLDREASONNAME equals the specified reason
#### Scenario: TreeMap empty result
- **WHEN** no hold lots match the filters
- **THEN** the response SHALL return `{ success: true, data: { items: [] } }`
### Requirement: Hold Overview API SHALL provide paginated lot details
The API SHALL return a paginated list of hold lot details.
#### Scenario: Lots endpoint delegates to extended get_hold_detail_lots
- **WHEN** `GET /api/hold-overview/lots?hold_type=quality&page=1&per_page=50` is called
- **THEN** the route SHALL delegate to the extended `get_hold_detail_lots(reason=None, hold_type='quality', ...)`
- **THEN** the response SHALL return `{ success: true, data: { lots: [...], pagination: { page, perPage, total, totalPages } } }`
- **THEN** each lot SHALL contain: lotId, workorder, qty, package, workcenter, holdReason, age, holdBy, dept, holdComment
- **THEN** lots SHALL be sorted by age descending (longest hold first)
#### Scenario: Lots with matrix filter
- **WHEN** `GET /api/hold-overview/lots?hold_type=quality&workcenter=WC-MOLD&package=PKG-A` is called
- **THEN** only lots matching the workcenter AND package filters SHALL be returned
#### Scenario: Lots with treemap filter
- **WHEN** `GET /api/hold-overview/lots?hold_type=quality&workcenter=WC-MOLD&treemap_reason=品質確認` is called
- **THEN** only lots matching the workcenter AND treemap_reason SHALL be returned
#### Scenario: Lots with all filters combined
- **WHEN** `GET /api/hold-overview/lots?hold_type=quality&reason=品質確認&workcenter=WC-MOLD&package=PKG-A&treemap_reason=品質確認` is called
- **THEN** all filters SHALL be applied as AND conditions
#### Scenario: Lots pagination bounds
- **WHEN** `page` is less than 1
- **THEN** page SHALL be treated as 1
- **WHEN** `per_page` exceeds 200
- **THEN** per_page SHALL be capped at 200
#### Scenario: Lots error
- **WHEN** the database query fails
- **THEN** the response SHALL return `{ success: false, error: '查詢失敗' }` with HTTP 500
### Requirement: Hold Overview API SHALL apply rate limiting
The API SHALL apply rate limiting to expensive endpoints.
#### Scenario: Rate limit on lots endpoint
- **WHEN** the lots endpoint receives excessive requests
- **THEN** rate limiting SHALL be applied using `configured_rate_limit` with a default of 90 requests per 60 seconds
#### Scenario: Rate limit on matrix endpoint
- **WHEN** the matrix endpoint receives excessive requests
- **THEN** rate limiting SHALL be applied using `configured_rate_limit` with a default of 120 requests per 60 seconds
### Requirement: Hold Overview page route SHALL serve static Vite HTML
The Flask route SHALL serve the pre-built Vite HTML file.
#### Scenario: Page route
- **WHEN** user navigates to `/hold-overview`
- **THEN** Flask SHALL serve the pre-built HTML file from `static/dist/hold-overview.html` via `send_from_directory`
- **THEN** the HTML SHALL NOT pass through Jinja2 template rendering
#### Scenario: Fallback HTML
- **WHEN** the pre-built HTML file does not exist
- **THEN** Flask SHALL return a minimal HTML page with the correct script tag and module import
### Requirement: Extended service functions SHALL maintain backward compatibility
The extended `get_hold_detail_summary()`, `get_hold_detail_lots()`, and `get_wip_matrix()` SHALL not break existing callers.
#### Scenario: Hold Detail summary backward compatibility
- **WHEN** existing Hold Detail code calls `get_hold_detail_summary(reason='xxx')`
- **THEN** the result SHALL be identical to the pre-extension behavior
- **THEN** the new `hold_type` parameter SHALL default to None (no additional filtering)
#### Scenario: Hold Detail lots backward compatibility
- **WHEN** existing Hold Detail code calls `get_hold_detail_lots(reason='xxx', workcenter=..., package=..., age_range=...)`
- **THEN** the result SHALL be identical to the pre-extension behavior
- **THEN** the new `hold_type` and `treemap_reason` parameters SHALL default to None
#### Scenario: WIP Overview matrix backward compatibility
- **WHEN** existing WIP Overview code calls `get_wip_matrix(status='HOLD', hold_type='quality')`
- **THEN** the result SHALL be identical to the pre-extension behavior
- **THEN** the new `reason` parameter SHALL default to None (no HOLDREASONNAME filtering)

View File

@@ -0,0 +1,196 @@
## ADDED Requirements
### Requirement: Hold Overview page SHALL display a filter bar with Hold Type and Reason
The page SHALL provide a filter bar for selecting hold type and hold reason.
#### Scenario: Hold Type radio default
- **WHEN** the page loads
- **THEN** the Hold Type filter SHALL default to "品質異常"
- **THEN** three radio options SHALL display: 品質異常, 非品質異常, 全部
#### Scenario: Hold Type change reloads all data
- **WHEN** user changes the Hold Type selection
- **THEN** all four API calls (summary, matrix, treemap, lots) SHALL reload with the new hold_type parameter
- **THEN** any active matrix and treemap filters SHALL be cleared
#### Scenario: Reason dropdown populated from current data
- **WHEN** summary data is loaded
- **THEN** the Reason dropdown SHALL display "全部" plus all distinct hold reasons from the treemap data
- **THEN** selecting a specific reason SHALL reload all four API calls filtered by that reason
- **THEN** any active matrix and treemap filters SHALL be cleared
### Requirement: Hold Overview page SHALL display summary KPI cards
The page SHALL show summary statistics for all hold lots matching the current filter.
#### Scenario: Summary cards rendering
- **WHEN** summary data is loaded from `GET /api/hold-overview/summary`
- **THEN** five cards SHALL display: Hold Lots, Hold QTY, 站別數, 平均滯留天數, 最大滯留天數
- **THEN** lot and QTY values SHALL use zh-TW number formatting
- **THEN** age values SHALL display with "天" suffix and one decimal place
#### Scenario: Summary reflects filter bar only
- **WHEN** user clicks a matrix cell or treemap block
- **THEN** summary cards SHALL NOT change (they only respond to filter bar changes)
### Requirement: Hold Overview page SHALL display a Workcenter x Package matrix
The page SHALL display a cross-tabulation matrix of workcenters vs packages for hold lots.
#### Scenario: Matrix table rendering
- **WHEN** matrix data is loaded from `GET /api/hold-overview/matrix`
- **THEN** the table SHALL display workcenters as rows and packages as columns
- **THEN** cell values SHALL show QTY with zh-TW number formatting
- **THEN** the first column (Workcenter) SHALL be sticky on horizontal scroll
- **THEN** a Total row and Total column SHALL be displayed
- **THEN** cells with zero value SHALL display "-"
#### Scenario: Matrix cell click filters TreeMap and lot table
- **WHEN** user clicks a QTY cell in the matrix (intersection of workcenter and package)
- **THEN** `matrixFilter` SHALL be set to `{ workcenter, package }`
- **THEN** TreeMap SHALL reload showing only data for that workcenter + package combination
- **THEN** lot table SHALL reload filtered by that workcenter + package
- **THEN** the clicked cell SHALL show an active highlight
#### Scenario: Matrix workcenter row click
- **WHEN** user clicks a workcenter name or its Total cell
- **THEN** `matrixFilter` SHALL be set to `{ workcenter }` (all packages)
- **THEN** TreeMap and lot table SHALL reload filtered by that workcenter
#### Scenario: Matrix package column click
- **WHEN** user clicks a package column header or its Total cell
- **THEN** `matrixFilter` SHALL be set to `{ package }` (all workcenters)
- **THEN** TreeMap and lot table SHALL reload filtered by that package
#### Scenario: Matrix click toggle
- **WHEN** user clicks the same cell/row/column that is already active
- **THEN** `matrixFilter` SHALL be cleared
- **THEN** TreeMap and lot table SHALL reload without matrix filter
#### Scenario: Matrix reflects filter bar only
- **WHEN** user clicks a treemap block
- **THEN** matrix SHALL NOT change (it only responds to filter bar changes)
### Requirement: Hold Overview page SHALL display active filter indicators
The page SHALL show a clear indicator of active cascade filters.
#### Scenario: Matrix filter indicator
- **WHEN** a matrix filter is active
- **THEN** a filter indicator SHALL display between the matrix and TreeMap sections
- **THEN** the indicator SHALL show the active workcenter and/or package name
- **THEN** a clear button (✕) SHALL remove the matrix filter
#### Scenario: TreeMap filter indicator
- **WHEN** a treemap filter is active
- **THEN** a filter indicator SHALL display between the TreeMap and lot table sections
- **THEN** the indicator SHALL show the active workcenter and reason name
- **THEN** a clear button (✕) SHALL remove the treemap filter
#### Scenario: Clear all filters
- **WHEN** user clicks a "清除所有篩選" button
- **THEN** both matrixFilter and treemapFilter SHALL be cleared
- **THEN** TreeMap and lot table SHALL reload without cascade filters
### Requirement: Hold Overview page SHALL display a TreeMap visualization
The page SHALL display a TreeMap chart showing hold lot distribution by workcenter and reason.
#### Scenario: TreeMap rendering
- **WHEN** treemap data is loaded from `GET /api/hold-overview/treemap`
- **THEN** the TreeMap SHALL display with two levels: Workcenter (parent) → Hold Reason (child)
- **THEN** block area SHALL represent QTY
- **THEN** block color SHALL represent average age at current station using a 4-level color scale
- **THEN** the color scale legend SHALL display: 綠(<1天), (1-3天), (3-7天), (>7天)
#### Scenario: TreeMap tooltip
- **WHEN** user hovers over a TreeMap block
- **THEN** a tooltip SHALL display: Workcenter, Reason, Lots count, QTY, and average age
#### Scenario: TreeMap narrows on matrix filter (Option A)
- **WHEN** a matrix filter is active (e.g., workcenter=WC-MOLD, package=PKG-A)
- **THEN** the TreeMap SHALL only show data matching the matrix filter
- **THEN** the TreeMap API SHALL be called with workcenter and/or package parameters
#### Scenario: TreeMap click filters lot table
- **WHEN** user clicks a leaf block in the TreeMap (a specific reason within a workcenter)
- **THEN** `treemapFilter` SHALL be set to `{ workcenter, reason }`
- **THEN** lot table SHALL reload filtered by that workcenter + reason
- **THEN** the clicked block SHALL show a visual highlight (border or opacity change)
#### Scenario: TreeMap click toggle
- **WHEN** user clicks the same block that is already active
- **THEN** `treemapFilter` SHALL be cleared
- **THEN** lot table SHALL reload without treemap filter
#### Scenario: Empty TreeMap
- **WHEN** treemap data returns zero items
- **THEN** the TreeMap area SHALL display "目前無 Hold 資料"
### Requirement: Hold Overview page SHALL display paginated lot details
The page SHALL display a detailed lot table with server-side pagination.
#### Scenario: Lot table rendering
- **WHEN** lot data is loaded from `GET /api/hold-overview/lots`
- **THEN** a table SHALL display with columns: LOTID, WORKORDER, QTY, Package, Workcenter, Hold Reason, Age, Hold By, Dept, Hold Comment
- **THEN** age values SHALL display with "天" suffix
#### Scenario: Lot table responds to all cascade filters
- **WHEN** matrixFilter is `{ workcenter: WC-A, package: PKG-1 }` and treemapFilter is `{ reason: 品質確認 }`
- **THEN** lots API SHALL be called with `workcenter=WC-A&package=PKG-1&treemap_reason=品質確認`
- **THEN** only lots matching all active filters SHALL be displayed
#### Scenario: Pagination
- **WHEN** total lots exceeds per_page (50)
- **THEN** Prev/Next buttons and page info SHALL display
- **THEN** page info SHALL show "顯示 {start} - {end} / {total}"
#### Scenario: Filter changes reset pagination
- **WHEN** any filter changes (filter bar, matrix click, or treemap click)
- **THEN** pagination SHALL reset to page 1
#### Scenario: Empty lot result
- **WHEN** a query returns zero lots
- **THEN** the lot table SHALL display a "No data" placeholder
### Requirement: Hold Overview page SHALL auto-refresh and handle request cancellation
The page SHALL automatically refresh data and prevent stale request pile-up.
#### Scenario: Auto-refresh interval
- **WHEN** the page is loaded
- **THEN** data SHALL auto-refresh every 10 minutes using `useAutoRefresh` composable
- **THEN** auto-refresh SHALL be skipped when the tab is hidden
#### Scenario: Visibility change refresh
- **WHEN** the tab becomes visible after being hidden
- **THEN** data SHALL refresh immediately
#### Scenario: Request cancellation
- **WHEN** a new data load is triggered while a previous request is in-flight
- **THEN** the previous request SHALL be cancelled via AbortController
- **THEN** the cancelled request SHALL NOT update the UI
#### Scenario: Manual refresh
- **WHEN** user clicks the "重新整理" button
- **THEN** all data SHALL reload and the auto-refresh timer SHALL reset
- **THEN** all cascade filters (matrixFilter, treemapFilter) SHALL be preserved during refresh
### Requirement: Hold Overview page SHALL handle loading and error states
The page SHALL display appropriate feedback during API calls and on errors.
#### Scenario: Initial loading overlay
- **WHEN** the page first loads
- **THEN** a full-page loading overlay SHALL display until all data is loaded
#### Scenario: API error handling
- **WHEN** an API call fails
- **THEN** an error banner SHALL display with the error message
- **THEN** the page SHALL NOT crash or become unresponsive
#### Scenario: Refresh indicator
- **WHEN** data is being refreshed (not initial load)
- **THEN** a spinning refresh indicator SHALL display in the header
- **THEN** a success checkmark SHALL flash briefly on completion
### Requirement: Hold Overview page SHALL have back navigation
The page SHALL provide navigation back to WIP Overview.
#### Scenario: Back button
- **WHEN** user clicks the "← WIP Overview" button in the header
- **THEN** the page SHALL navigate to `/wip-overview`

View File

@@ -0,0 +1,40 @@
## MODIFIED Requirements
### Requirement: Vite config SHALL support Vue SFC and HTML entry points
The Vite build configuration SHALL support Vue Single File Components alongside existing vanilla JS entries.
#### Scenario: Vue plugin coexistence
- **WHEN** `vite build` is executed
- **THEN** Vue SFC (`.vue` files) SHALL be compiled by `@vitejs/plugin-vue`
- **THEN** existing vanilla JS entry points SHALL continue to build without modification
#### Scenario: HTML entry point
- **WHEN** a page uses an HTML file as its Vite entry point
- **THEN** Vite SHALL process the HTML and its referenced JS/CSS into `static/dist/`
- **THEN** the output SHALL include `<page-name>.html`, `<page-name>.js`, and `<page-name>.css`
#### Scenario: Chunk splitting
- **WHEN** Vite builds the project
- **THEN** Vue runtime SHALL be split into a `vendor-vue` chunk
- **THEN** ECharts modules (including TreemapChart) SHALL be split into the existing `vendor-echarts` chunk
- **THEN** chunk splitting SHALL NOT affect existing page bundles
#### Scenario: Migrated page entry replacement
- **WHEN** a vanilla JS page is migrated to Vue 3
- **THEN** its Vite entry SHALL change from JS file to HTML file (e.g., `src/wip-overview/main.js``src/wip-overview/index.html`)
- **THEN** the original JS entry SHALL be replaced, not kept alongside
#### Scenario: Hold Overview entry point
- **WHEN** the hold-overview page is added
- **THEN** `vite.config.js` input SHALL include `'hold-overview': resolve(__dirname, 'src/hold-overview/index.html')`
- **THEN** the build SHALL produce `hold-overview.html`, `hold-overview.js`, and `hold-overview.css` in `static/dist/`
#### Scenario: Shared CSS import across migrated pages
- **WHEN** multiple migrated pages import a shared CSS module (e.g., `wip-shared/styles.css`)
- **THEN** Vite SHALL bundle the shared CSS into each page's output CSS
- **THEN** shared CSS SHALL NOT create a separate shared chunk that requires additional HTTP requests
#### Scenario: Shared composable import across module boundaries
- **WHEN** a migrated page imports a composable from another shared module (e.g., `hold-overview` imports `useAutoRefresh` from `wip-shared/`)
- **THEN** the composable SHALL be bundled into the importing page's JS output
- **THEN** cross-module imports SHALL NOT create unexpected shared chunks

View File

@@ -0,0 +1,72 @@
## 1. Backend — 擴充現有 Service 函數
- [x] 1.1 擴充 `get_hold_detail_summary()` 簽名:`reason` 改為 `Optional[str] = None`,新增 `hold_type: Optional[str] = None` 參數reason=None 時聚合所有 HOLD lotshold_type 過濾品質/非品質cache path 和 Oracle fallback 都需支援;確保現有 Hold Detail 呼叫 `get_hold_detail_summary(reason='xxx')` 行為不變
- [x] 1.2 擴充 `get_hold_detail_lots()` 簽名:`reason` 改為 `Optional[str] = None`,新增 `hold_type: Optional[str] = None``treemap_reason: Optional[str] = None` 參數reason=None 時返回所有 HOLD lotstreemap_reason 作為額外 HOLDREASONNAME 過濾TreeMap 點擊篩選用);增加 holdReason 欄位到 lot 回傳資料中;確保現有 Hold Detail 呼叫不受影響
- [x] 1.3 擴充 `get_wip_matrix()` 簽名:新增 `reason: Optional[str] = None` 參數,過濾 HOLDREASONNAMEcache path 用 DataFrame filterOracle fallback 用 QueryBuilderreason=None 時行為不變,確保 WIP Overview 呼叫不受影響
- [x] 1.4 新增 `get_hold_overview_treemap()` 函數(唯一全新函數):使用 `_select_with_snapshot_indexes(status='HOLD', hold_type=...)` 取得 HOLD DataFrame按 (WORKCENTER_GROUP, HOLDREASONNAME) groupBy 聚合,回傳 `[{ workcenter, reason, lots, qty, avgAge }]`;接受 `hold_type`, `reason`, `workcenter`, `package` 參數;含 Oracle fallback
## 2. Backend — 路由
- [x] 2.1 建立 `src/mes_dashboard/routes/hold_overview_routes.py`Flask Blueprint `hold_overview_bp`;頁面路由 `GET /hold-overview``send_from_directory` 提供 static Vite HTML含 fallback HTML
- [x] 2.2 實作 `GET /api/hold-overview/summary`:解析 `hold_type`(預設 `quality`)和 `reason` query params委派給擴充後的 `get_hold_detail_summary(reason=reason, hold_type=hold_type)`
- [x] 2.3 實作 `GET /api/hold-overview/matrix`:委派給現有 `get_wip_matrix(status='HOLD', hold_type=..., reason=...)`;套用 rate limiting (120 req/60s)
- [x] 2.4 實作 `GET /api/hold-overview/treemap`:解析 `hold_type`, `reason`, `workcenter`, `package` params委派給 `get_hold_overview_treemap()`
- [x] 2.5 實作 `GET /api/hold-overview/lots`:解析所有 filter params + 分頁,委派給擴充後的 `get_hold_detail_lots(reason=reason, hold_type=hold_type, treemap_reason=treemap_reason, ...)`;套用 rate limiting (90 req/60s)per_page 上限 200
- [x] 2.6 在 Flask app factory`routes/__init__.py`)中註冊 `hold_overview_bp`
## 3. Backend — 向後相容驗證
- [x] 3.1 驗證 Hold Detail 頁面現有 3 支 APIsummary/distribution/lots在擴充後行為不變`get_hold_detail_summary(reason='xxx')``get_hold_detail_lots(reason='xxx', ...)` 結果與擴充前一致
- [x] 3.2 驗證 WIP Overview 的 `get_wip_matrix()` 呼叫在新增 reason 參數後行為不變reason=None 預設值)
## 4. Frontend — 腳手架
- [x] 4.1 建立 `frontend/src/hold-overview/` 目錄結構:`index.html`, `main.js`, `App.vue`, `style.css`, `components/`
- [x] 4.2 在 `vite.config.js` 的 input 加入 `'hold-overview': resolve(__dirname, 'src/hold-overview/index.html')`
- [x] 4.3 建立 `index.html`Vue 3 mount point`main.js``createApp(App).mount('#app')`import `style.css``wip-shared/styles.css`
## 5. Frontend — FilterBar全新
- [x] 5.1 建立 `components/FilterBar.vue`Hold Type radio group品質異常 default, 非品質異常, 全部)+ Reason dropdown全部 + dynamic reasonsemit `change` 事件帶 `{ holdType, reason }`
## 6. Frontend — SummaryCards直接 import
- [x] 6.1 在 App.vue 中直接 `import SummaryCards from '../hold-detail/components/SummaryCards.vue'`props 形狀 `{ totalLots, totalQty, avgAge, maxAge, workcenterCount }` 完全相容,無需新建元件
## 7. Frontend — HoldMatrix基於 MatrixTable 新建)
- [x] 7.1 建立 `components/HoldMatrix.vue`,以 `wip-overview/MatrixTable.vue` 為基礎:保留 matrix 渲染邏輯sticky 首欄、Total row/column、"-" 零值、zh-TW 格式化)
- [x] 7.2 擴充互動cell click → emit `{ workcenter, package }`、workcenter name/row total click → emit `{ workcenter }`、package header/column total click → emit `{ package }`active cell/row/column highlighttoggle logic再次點擊同一項 = 清除)
## 8. Frontend — HoldTreeMap全新
- [x] 8.1 建立 `components/HoldTreeMap.vue`ECharts TreeMap`import { TreemapChart } from 'echarts/charts'`兩層結構WC parent → Reason child面積=QTY`visualMap` 色階 for avgAge<1天, 黃1-3天, 橙3-7天, >7天
- [x] 8.2 實作 tooltipworkcenter, reason, lots, qty, avgAge和 click handler → emit `{ workcenter, reason }`toggle logic"目前無 Hold 資料" empty state
- [x] 8.3 實作 `autoresize` 和 responsive height
## 9. Frontend — LotTable基於 hold-detail/LotTable 新建)
- [x] 9.1 建立 `components/LotTable.vue`,以 `hold-detail/LotTable.vue` 為基礎:保留分頁邏輯(已 import `wip-shared/Pagination.vue`、loading/error/empty 狀態、filter indicator替換欄位移除 Spec新增 Hold Reason 欄位holdReason
## 10. Frontend — FilterIndicator全新
- [x] 10.1 建立 `components/FilterIndicator.vue`:顯示 active matrixFilter 和/或 treemapFilter 標籤,含 ✕ 清除按鈕;任一 cascade filter 啟用時顯示「清除所有篩選」按鈕
## 11. Frontend — App.vue 整合
- [x] 11.1 串接 App.vueimport 所有元件SummaryCards 從 hold-detail import、其餘從 local components設定 reactive state for `filterBar`, `matrixFilter`, `treemapFilter`, `page`
- [x] 11.2 實作資料載入:`loadAllData()` 平行呼叫 4 支 API`loadTreemapAndLots()` for matrix filter 變更;`loadLots()` for treemap filter 變更;使用 `useAutoRefresh` composable`wip-shared/composables/useAutoRefresh.js` import
- [x] 11.3 實作 filter cascadefilter bar 變更 → 清除 matrixFilter + treemapFilter → `loadAllData()`matrix click → set matrixFilter, 清除 treemapFilter → `loadTreemapAndLots()`treemap click → set treemapFilter → `loadLots()`
- [x] 11.4 實作 loading statesinitialLoading overlay、refreshing indicator、refresh success/error、error handling、手動重新整理按鈕、AbortController request cancellation
- [x] 11.5 從 treemap 資料的 distinct reasons 填充 Reason dropdown
## 12. Frontend — 樣式
- [x] 12.1 建立 `style.css`,沿用 `wip-overview/style.css``hold-detail/style.css` 的 pattern包含 header、summary cards、matrix table、treemap section、lot table、filter indicator、filter bar、loading overlay、error banner 樣式
## 13. Build & 驗證
- [x] 13.1 執行 `npm --prefix frontend run build`,確認 `static/dist/` 生成 `hold-overview.html`, `hold-overview.js`, `hold-overview.css`
- [x] 13.2 驗證 Flask serve `/hold-overview` 正常4 支 API endpoint 回應正確
- [x] 13.3 端對端測試filter bar toggle → matrix click → treemap click → lot table cascade驗證每層正確回應
- [x] 13.4 回歸測試:確認 Hold Detail 頁面(`/hold-detail?reason=xxx`)功能正常不受影響;確認 WIP Overview Matrix 功能正常不受影響

View File

@@ -0,0 +1,141 @@
## Purpose
Define stable requirements for hold-overview-api.
## Requirements
### Requirement: Hold Overview API SHALL provide summary statistics
The API SHALL return aggregated summary KPIs for hold lots.
#### Scenario: Summary endpoint delegates to extended get_hold_detail_summary
- **WHEN** `GET /api/hold-overview/summary` is called with `hold_type=quality`
- **THEN** the route SHALL delegate to the extended `get_hold_detail_summary(reason=None, hold_type='quality')`
- **THEN** the response SHALL return `{ success: true, data: { totalLots, totalQty, workcenterCount, avgAge, maxAge, dataUpdateDate } }`
- **THEN** only lots with EQUIPMENTCOUNT=0 AND CURRENTHOLDCOUNT>0 SHALL be included
- **THEN** hold_type classification SHALL use the same NON_QUALITY_HOLD_REASONS set as existing hold endpoints
#### Scenario: Summary with reason filter
- **WHEN** `GET /api/hold-overview/summary?hold_type=quality&reason=品質確認` is called
- **THEN** the response SHALL only include lots where HOLDREASONNAME equals the specified reason
#### Scenario: Summary hold_type=all
- **WHEN** `GET /api/hold-overview/summary?hold_type=all` is called
- **THEN** the response SHALL include both quality and non-quality hold lots
#### Scenario: Summary error
- **WHEN** the database query fails
- **THEN** the response SHALL return `{ success: false, error: '查詢失敗' }` with HTTP 500
### Requirement: Hold Overview API SHALL provide workcenter x package matrix
The API SHALL return a cross-tabulation of workcenters and packages for hold lots.
#### Scenario: Matrix endpoint
- **WHEN** `GET /api/hold-overview/matrix` is called with `hold_type=quality`
- **THEN** the response SHALL return the same matrix structure as `/api/wip/overview/matrix`: `{ workcenters, packages, matrix, workcenter_totals, package_totals, grand_total }`
- **THEN** workcenters SHALL be sorted by WORKCENTERSEQUENCE_GROUP
- **THEN** packages SHALL be sorted by total QTY descending
- **THEN** only HOLD status lots matching the hold_type SHALL be included
#### Scenario: Matrix delegates to existing get_wip_matrix
- **WHEN** the matrix endpoint is called
- **THEN** it SHALL delegate to `get_wip_matrix(status='HOLD', hold_type=...)` from wip_service.py
#### Scenario: Matrix with reason filter
- **WHEN** `GET /api/hold-overview/matrix?hold_type=quality&reason=品質確認` is called
- **THEN** the matrix SHALL only include lots where HOLDREASONNAME equals the specified reason
### Requirement: Hold Overview API SHALL provide TreeMap aggregation data
The API SHALL return aggregated data suitable for TreeMap visualization.
#### Scenario: TreeMap endpoint uses new get_hold_overview_treemap function
- **WHEN** `GET /api/hold-overview/treemap` is called with `hold_type=quality`
- **THEN** the route SHALL delegate to `get_hold_overview_treemap()` (the only new service function)
- **THEN** the response SHALL return `{ success: true, data: { items: [...] } }`
- **THEN** each item SHALL contain `{ workcenter, reason, lots, qty, avgAge }`
- **THEN** items SHALL be grouped by (WORKCENTER_GROUP, HOLDREASONNAME)
- **THEN** avgAge SHALL be calculated using the pre-computed AGEBYDAYS column from DW_MES_LOT_V
#### Scenario: TreeMap with matrix filter
- **WHEN** `GET /api/hold-overview/treemap?hold_type=quality&workcenter=WC-MOLD&package=PKG-A` is called
- **THEN** the response SHALL only include lots matching the workcenter AND package filters
#### Scenario: TreeMap with reason filter
- **WHEN** `GET /api/hold-overview/treemap?hold_type=quality&reason=品質確認` is called
- **THEN** the response SHALL only include lots where HOLDREASONNAME equals the specified reason
#### Scenario: TreeMap empty result
- **WHEN** no hold lots match the filters
- **THEN** the response SHALL return `{ success: true, data: { items: [] } }`
### Requirement: Hold Overview API SHALL provide paginated lot details
The API SHALL return a paginated list of hold lot details.
#### Scenario: Lots endpoint delegates to extended get_hold_detail_lots
- **WHEN** `GET /api/hold-overview/lots?hold_type=quality&page=1&per_page=50` is called
- **THEN** the route SHALL delegate to the extended `get_hold_detail_lots(reason=None, hold_type='quality', ...)`
- **THEN** the response SHALL return `{ success: true, data: { lots: [...], pagination: { page, perPage, total, totalPages } } }`
- **THEN** each lot SHALL contain: lotId, workorder, qty, package, workcenter, holdReason, age, holdBy, dept, holdComment
- **THEN** lots SHALL be sorted by age descending (longest hold first)
#### Scenario: Lots with matrix filter
- **WHEN** `GET /api/hold-overview/lots?hold_type=quality&workcenter=WC-MOLD&package=PKG-A` is called
- **THEN** only lots matching the workcenter AND package filters SHALL be returned
#### Scenario: Lots with treemap filter
- **WHEN** `GET /api/hold-overview/lots?hold_type=quality&workcenter=WC-MOLD&treemap_reason=品質確認` is called
- **THEN** only lots matching the workcenter AND treemap_reason SHALL be returned
#### Scenario: Lots with all filters combined
- **WHEN** `GET /api/hold-overview/lots?hold_type=quality&reason=品質確認&workcenter=WC-MOLD&package=PKG-A&treemap_reason=品質確認` is called
- **THEN** all filters SHALL be applied as AND conditions
#### Scenario: Lots pagination bounds
- **WHEN** `page` is less than 1
- **THEN** page SHALL be treated as 1
- **WHEN** `per_page` exceeds 200
- **THEN** per_page SHALL be capped at 200
#### Scenario: Lots error
- **WHEN** the database query fails
- **THEN** the response SHALL return `{ success: false, error: '查詢失敗' }` with HTTP 500
### Requirement: Hold Overview API SHALL apply rate limiting
The API SHALL apply rate limiting to expensive endpoints.
#### Scenario: Rate limit on lots endpoint
- **WHEN** the lots endpoint receives excessive requests
- **THEN** rate limiting SHALL be applied using `configured_rate_limit` with a default of 90 requests per 60 seconds
#### Scenario: Rate limit on matrix endpoint
- **WHEN** the matrix endpoint receives excessive requests
- **THEN** rate limiting SHALL be applied using `configured_rate_limit` with a default of 120 requests per 60 seconds
### Requirement: Hold Overview page route SHALL serve static Vite HTML
The Flask route SHALL serve the pre-built Vite HTML file.
#### Scenario: Page route
- **WHEN** user navigates to `/hold-overview`
- **THEN** Flask SHALL serve the pre-built HTML file from `static/dist/hold-overview.html` via `send_from_directory`
- **THEN** the HTML SHALL NOT pass through Jinja2 template rendering
#### Scenario: Fallback HTML
- **WHEN** the pre-built HTML file does not exist
- **THEN** Flask SHALL return a minimal HTML page with the correct script tag and module import
### Requirement: Extended service functions SHALL maintain backward compatibility
The extended `get_hold_detail_summary()`, `get_hold_detail_lots()`, and `get_wip_matrix()` SHALL not break existing callers.
#### Scenario: Hold Detail summary backward compatibility
- **WHEN** existing Hold Detail code calls `get_hold_detail_summary(reason='xxx')`
- **THEN** the result SHALL be identical to the pre-extension behavior
- **THEN** the new `hold_type` parameter SHALL default to None (no additional filtering)
#### Scenario: Hold Detail lots backward compatibility
- **WHEN** existing Hold Detail code calls `get_hold_detail_lots(reason='xxx', workcenter=..., package=..., age_range=...)`
- **THEN** the result SHALL be identical to the pre-extension behavior
- **THEN** the new `hold_type` and `treemap_reason` parameters SHALL default to None
#### Scenario: WIP Overview matrix backward compatibility
- **WHEN** existing WIP Overview code calls `get_wip_matrix(status='HOLD', hold_type='quality')`
- **THEN** the result SHALL be identical to the pre-extension behavior
- **THEN** the new `reason` parameter SHALL default to None (no HOLDREASONNAME filtering)

View File

@@ -0,0 +1,200 @@
## Purpose
Define stable requirements for hold-overview-page.
## Requirements
### Requirement: Hold Overview page SHALL display a filter bar with Hold Type and Reason
The page SHALL provide a filter bar for selecting hold type and hold reason.
#### Scenario: Hold Type radio default
- **WHEN** the page loads
- **THEN** the Hold Type filter SHALL default to "品質異常"
- **THEN** three radio options SHALL display: 品質異常, 非品質異常, 全部
#### Scenario: Hold Type change reloads all data
- **WHEN** user changes the Hold Type selection
- **THEN** all four API calls (summary, matrix, treemap, lots) SHALL reload with the new hold_type parameter
- **THEN** any active matrix and treemap filters SHALL be cleared
#### Scenario: Reason dropdown populated from current data
- **WHEN** summary data is loaded
- **THEN** the Reason dropdown SHALL display "全部" plus all distinct hold reasons from the treemap data
- **THEN** selecting a specific reason SHALL reload all four API calls filtered by that reason
- **THEN** any active matrix and treemap filters SHALL be cleared
### Requirement: Hold Overview page SHALL display summary KPI cards
The page SHALL show summary statistics for all hold lots matching the current filter.
#### Scenario: Summary cards rendering
- **WHEN** summary data is loaded from `GET /api/hold-overview/summary`
- **THEN** five cards SHALL display: Hold Lots, Hold QTY, 站別數, 平均滯留天數, 最大滯留天數
- **THEN** lot and QTY values SHALL use zh-TW number formatting
- **THEN** age values SHALL display with "天" suffix and one decimal place
#### Scenario: Summary reflects filter bar only
- **WHEN** user clicks a matrix cell or treemap block
- **THEN** summary cards SHALL NOT change (they only respond to filter bar changes)
### Requirement: Hold Overview page SHALL display a Workcenter x Package matrix
The page SHALL display a cross-tabulation matrix of workcenters vs packages for hold lots.
#### Scenario: Matrix table rendering
- **WHEN** matrix data is loaded from `GET /api/hold-overview/matrix`
- **THEN** the table SHALL display workcenters as rows and packages as columns
- **THEN** cell values SHALL show QTY with zh-TW number formatting
- **THEN** the first column (Workcenter) SHALL be sticky on horizontal scroll
- **THEN** a Total row and Total column SHALL be displayed
- **THEN** cells with zero value SHALL display "-"
#### Scenario: Matrix cell click filters TreeMap and lot table
- **WHEN** user clicks a QTY cell in the matrix (intersection of workcenter and package)
- **THEN** `matrixFilter` SHALL be set to `{ workcenter, package }`
- **THEN** TreeMap SHALL reload showing only data for that workcenter + package combination
- **THEN** lot table SHALL reload filtered by that workcenter + package
- **THEN** the clicked cell SHALL show an active highlight
#### Scenario: Matrix workcenter row click
- **WHEN** user clicks a workcenter name or its Total cell
- **THEN** `matrixFilter` SHALL be set to `{ workcenter }` (all packages)
- **THEN** TreeMap and lot table SHALL reload filtered by that workcenter
#### Scenario: Matrix package column click
- **WHEN** user clicks a package column header or its Total cell
- **THEN** `matrixFilter` SHALL be set to `{ package }` (all workcenters)
- **THEN** TreeMap and lot table SHALL reload filtered by that package
#### Scenario: Matrix click toggle
- **WHEN** user clicks the same cell/row/column that is already active
- **THEN** `matrixFilter` SHALL be cleared
- **THEN** TreeMap and lot table SHALL reload without matrix filter
#### Scenario: Matrix reflects filter bar only
- **WHEN** user clicks a treemap block
- **THEN** matrix SHALL NOT change (it only responds to filter bar changes)
### Requirement: Hold Overview page SHALL display active filter indicators
The page SHALL show a clear indicator of active cascade filters.
#### Scenario: Matrix filter indicator
- **WHEN** a matrix filter is active
- **THEN** a filter indicator SHALL display between the matrix and TreeMap sections
- **THEN** the indicator SHALL show the active workcenter and/or package name
- **THEN** a clear button (✕) SHALL remove the matrix filter
#### Scenario: TreeMap filter indicator
- **WHEN** a treemap filter is active
- **THEN** a filter indicator SHALL display between the TreeMap and lot table sections
- **THEN** the indicator SHALL show the active workcenter and reason name
- **THEN** a clear button (✕) SHALL remove the treemap filter
#### Scenario: Clear all filters
- **WHEN** user clicks a "清除所有篩選" button
- **THEN** both matrixFilter and treemapFilter SHALL be cleared
- **THEN** TreeMap and lot table SHALL reload without cascade filters
### Requirement: Hold Overview page SHALL display a TreeMap visualization
The page SHALL display a TreeMap chart showing hold lot distribution by workcenter and reason.
#### Scenario: TreeMap rendering
- **WHEN** treemap data is loaded from `GET /api/hold-overview/treemap`
- **THEN** the TreeMap SHALL display with two levels: Workcenter (parent) → Hold Reason (child)
- **THEN** block area SHALL represent QTY
- **THEN** block color SHALL represent average age at current station using a 4-level color scale
- **THEN** the color scale legend SHALL display: 綠(<1天), (1-3天), (3-7天), (>7天)
#### Scenario: TreeMap tooltip
- **WHEN** user hovers over a TreeMap block
- **THEN** a tooltip SHALL display: Workcenter, Reason, Lots count, QTY, and average age
#### Scenario: TreeMap narrows on matrix filter (Option A)
- **WHEN** a matrix filter is active (e.g., workcenter=WC-MOLD, package=PKG-A)
- **THEN** the TreeMap SHALL only show data matching the matrix filter
- **THEN** the TreeMap API SHALL be called with workcenter and/or package parameters
#### Scenario: TreeMap click filters lot table
- **WHEN** user clicks a leaf block in the TreeMap (a specific reason within a workcenter)
- **THEN** `treemapFilter` SHALL be set to `{ workcenter, reason }`
- **THEN** lot table SHALL reload filtered by that workcenter + reason
- **THEN** the clicked block SHALL show a visual highlight (border or opacity change)
#### Scenario: TreeMap click toggle
- **WHEN** user clicks the same block that is already active
- **THEN** `treemapFilter` SHALL be cleared
- **THEN** lot table SHALL reload without treemap filter
#### Scenario: Empty TreeMap
- **WHEN** treemap data returns zero items
- **THEN** the TreeMap area SHALL display "目前無 Hold 資料"
### Requirement: Hold Overview page SHALL display paginated lot details
The page SHALL display a detailed lot table with server-side pagination.
#### Scenario: Lot table rendering
- **WHEN** lot data is loaded from `GET /api/hold-overview/lots`
- **THEN** a table SHALL display with columns: LOTID, WORKORDER, QTY, Package, Workcenter, Hold Reason, Age, Hold By, Dept, Hold Comment
- **THEN** age values SHALL display with "天" suffix
#### Scenario: Lot table responds to all cascade filters
- **WHEN** matrixFilter is `{ workcenter: WC-A, package: PKG-1 }` and treemapFilter is `{ reason: 品質確認 }`
- **THEN** lots API SHALL be called with `workcenter=WC-A&package=PKG-1&treemap_reason=品質確認`
- **THEN** only lots matching all active filters SHALL be displayed
#### Scenario: Pagination
- **WHEN** total lots exceeds per_page (50)
- **THEN** Prev/Next buttons and page info SHALL display
- **THEN** page info SHALL show "顯示 {start} - {end} / {total}"
#### Scenario: Filter changes reset pagination
- **WHEN** any filter changes (filter bar, matrix click, or treemap click)
- **THEN** pagination SHALL reset to page 1
#### Scenario: Empty lot result
- **WHEN** a query returns zero lots
- **THEN** the lot table SHALL display a "No data" placeholder
### Requirement: Hold Overview page SHALL auto-refresh and handle request cancellation
The page SHALL automatically refresh data and prevent stale request pile-up.
#### Scenario: Auto-refresh interval
- **WHEN** the page is loaded
- **THEN** data SHALL auto-refresh every 10 minutes using `useAutoRefresh` composable
- **THEN** auto-refresh SHALL be skipped when the tab is hidden
#### Scenario: Visibility change refresh
- **WHEN** the tab becomes visible after being hidden
- **THEN** data SHALL refresh immediately
#### Scenario: Request cancellation
- **WHEN** a new data load is triggered while a previous request is in-flight
- **THEN** the previous request SHALL be cancelled via AbortController
- **THEN** the cancelled request SHALL NOT update the UI
#### Scenario: Manual refresh
- **WHEN** user clicks the "重新整理" button
- **THEN** all data SHALL reload and the auto-refresh timer SHALL reset
- **THEN** all cascade filters (matrixFilter, treemapFilter) SHALL be preserved during refresh
### Requirement: Hold Overview page SHALL handle loading and error states
The page SHALL display appropriate feedback during API calls and on errors.
#### Scenario: Initial loading overlay
- **WHEN** the page first loads
- **THEN** a full-page loading overlay SHALL display until all data is loaded
#### Scenario: API error handling
- **WHEN** an API call fails
- **THEN** an error banner SHALL display with the error message
- **THEN** the page SHALL NOT crash or become unresponsive
#### Scenario: Refresh indicator
- **WHEN** data is being refreshed (not initial load)
- **THEN** a spinning refresh indicator SHALL display in the header
- **THEN** a success checkmark SHALL flash briefly on completion
### Requirement: Hold Overview page SHALL have back navigation
The page SHALL provide navigation back to WIP Overview.
#### Scenario: Back button
- **WHEN** user clicks the "← WIP Overview" button in the header
- **THEN** the page SHALL navigate to `/wip-overview`

View File

@@ -33,7 +33,7 @@ The Vite build configuration SHALL support Vue Single File Components alongside
#### Scenario: Chunk splitting
- **WHEN** Vite builds the project
- **THEN** Vue runtime SHALL be split into a `vendor-vue` chunk
- **THEN** ECharts modules SHALL be split into the existing `vendor-echarts` chunk
- **THEN** ECharts modules (including TreemapChart) SHALL be split into the existing `vendor-echarts` chunk
- **THEN** chunk splitting SHALL NOT affect existing page bundles
#### Scenario: Migrated page entry replacement
@@ -41,13 +41,18 @@ The Vite build configuration SHALL support Vue Single File Components alongside
- **THEN** its Vite entry SHALL change from JS file to HTML file (e.g., `src/wip-overview/main.js``src/wip-overview/index.html`)
- **THEN** the original JS entry SHALL be replaced, not kept alongside
#### Scenario: Hold Overview entry point
- **WHEN** the hold-overview page is added
- **THEN** `vite.config.js` input SHALL include `'hold-overview': resolve(__dirname, 'src/hold-overview/index.html')`
- **THEN** the build SHALL produce `hold-overview.html`, `hold-overview.js`, and `hold-overview.css` in `static/dist/`
#### Scenario: Shared CSS import across migrated pages
- **WHEN** multiple migrated pages import a shared CSS module (e.g., `wip-shared/styles.css`)
- **THEN** Vite SHALL bundle the shared CSS into each page's output CSS
- **THEN** shared CSS SHALL NOT create a separate shared chunk that requires additional HTTP requests
#### Scenario: Shared composable import across module boundaries
- **WHEN** a migrated page imports a composable from another shared module (e.g., `resource-status` imports `useAutoRefresh` from `wip-shared/`)
- **WHEN** a migrated page imports a composable from another shared module (e.g., `hold-overview` imports `useAutoRefresh` from `wip-shared/`)
- **THEN** the composable SHALL be bundled into the importing page's JS output
- **THEN** cross-module imports SHALL NOT create unexpected shared chunks