feat(hold-history): add Hold 歷史績效 Dashboard with trend, pareto, duration, and detail views

New independent report page based on DWH.DW_MES_HOLDRELEASEHISTORY providing
historical hold/release performance analysis. Includes daily trend with Redis
caching, reason Pareto with click-to-filter, duration distribution with
click-to-filter, multi-select record type filter (new/on_hold/released),
workcenter-group mapping via memory cache, and server-side paginated detail
table. All 32 backend tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
egg
2026-02-10 18:03:08 +08:00
parent 8225863a85
commit 9a4e08810b
39 changed files with 4566 additions and 208 deletions

View File

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

View File

@@ -0,0 +1,94 @@
## Context
Hold Overview (DW_MES_LOT_V, Redis cache) 提供即時快照Hold Detail 深入單一 Reason。但主管缺乏歷史視角——趨勢、時長分析、部門績效都只能透過 BI 工具 (PJMES043) 手動操作。
本設計在既有 Dashboard 架構上新增一個歷史績效頁面,直接查詢 `DWH.DW_MES_HOLDRELEASEHISTORY` 表 (~310K rows),搭配 Redis 快取加速近期資料。
## Goals / Non-Goals
**Goals:**
- 提供自由日期區間的 Hold 歷史每日趨勢圖On Hold/新增/解除/Future Hold
- 提供 Reason Pareto、Duration 分布、負責人統計(部門+個人)分析
- 提供 paginated Hold/Release 明細表
- Reason Pareto 點擊可 cascade filter 負責人統計與明細表
- 近二月資料使用 Redis 快取12hr TTL前端切換 Hold Type 免 re-call
**Non-Goals:**
- 不替代既有 Hold Overview / Hold Detail 的即時功能
- 不引入即時 WebSocket 推送
- 不做跨頁面的 drill-through本頁面自成體系
- 不修改 HOLDRELEASEHISTORY 表結構或新增 index
## Decisions
### 1. 資料來源:直接查詢 HOLDRELEASEHISTORY vs. 預建聚合表
**選擇**: 直接查詢 + Redis 快取聚合結果
**理由**: 310K 行規模適中calendar-spine cross-join 月級查詢在秒級內完成。預建聚合表增加 ETL 複雜度且歷史數據變動低頻12hr 快取已足夠。
**替代方案**: 在 DWH 建立物化視圖 → 拒絕,因需 DBA 協調且 Dashboard 應盡量自包含。
### 2. 快取策略:近二月 Redis vs. 全量快取 vs. 無快取
**選擇**: 近二月(當月 + 前一月Redis 快取12hr TTL
**理由**: 多數使用者查看近期資料。近二月快取命中率高,超過二月的查詢較少且可接受直接 Oracle 查詢的延遲。全量快取浪費記憶體且過期管理複雜。
**Redis key**: `hold_history:daily:{YYYY-MM}`
**結構**: 一份快取包含 quality / non_quality / all 三種 hold_type 的每日聚合,前端切換免 API re-call。
**跨月查詢**: 後端從多個月快取中切出需要的日期範圍後合併回傳。
### 3. trend API 回傳三種 hold_type vs. 按需查詢
**選擇**: trend API 一次回傳三種 hold_type 的每日資料
**理由**: 趨勢是最常操作的圖表,切換 hold_type 應即時響應。三種 hold_type 資料已在同一份 Redis 快取中,回傳全部不增加 I/O但大幅改善 UX。其餘 4 支 API (pareto/duration/department/list) 按 hold_type 過濾,因為它們的 payload 可能很大。
### 4. SQL 集中管理sql/hold_history/ 目錄
**選擇**: SQL 檔案放在 `src/mes_dashboard/sql/hold_history/` 目錄
**理由**: 遵循既有 `sql/query_tool/``sql/dashboard/``sql/resource/``sql/wip/` 的集中管理模式。SQL 與 Python 分離便於 review 和維護。
**檔案規劃**:
- `trend.sql` — calendar-spine cross-join 每日聚合(翻譯自 hold_history.md
- `reason_pareto.sql` — GROUP BY HOLDREASONNAME
- `duration.sql` — 已 release 的 hold 時長分布
- `department.sql` — GROUP BY HOLDEMPDEPTNAME / HOLDEMP
- `list.sql` — paginated 明細查詢
### 5. 商業邏輯07:30 班別邊界
**選擇**: 忠實保留 hold_history.md 的班別邊界邏輯
**理由**: 這是工廠既有的日報定義07:30 後的交易歸入隔天。偏離此定義會導致 Dashboard 數字與既有 BI 報表不一致。
**實作**: 在 SQL 層處理 (`CASE WHEN TO_CHAR(HOLDTXNDATE,'HH24MI') >= '0730' THEN TRUNC(HOLDTXNDATE) + 1 ELSE TRUNC(HOLDTXNDATE) END`)。
### 6. 前端架構
**選擇**: 獨立 Vue 3 SFC 頁面,複用 wip-shared composables
**元件規劃**:
- `App.vue` — 頁面主容器、狀態管理、API 呼叫
- `FilterBar.vue` — DatePicker + Hold Type radio
- `SummaryCards.vue` — 6 張 KPI 卡片
- `DailyTrend.vue` — ECharts 折線+柱狀混合圖
- `ReasonPareto.vue` — ECharts Pareto 圖(可點擊)
- `DurationChart.vue` — ECharts 橫向柱狀圖
- `DepartmentTable.vue` — 可展開的部門/個人統計表
- `DetailTable.vue` — paginated 明細表
### 7. Duration 分布的計算範圍
**選擇**: 僅計算已 Release 的 holdRELEASETXNDATE IS NOT NULL
**理由**: 仍在 hold 中的無法確定最終時長,納入會扭曲分布。明細表中仍顯示未 release 的 hold以 SYSDATE 計算到目前時長)。
## Risks / Trade-offs
- **[HOLDTXNDATE 無 index]** → HOLDRELEASEHISTORY 僅有 HISTORYMAINLINEID 和 CONTAINERID 的 index。日期範圍查詢走 full table scan (~310K rows)。緩解12hr Redis 快取 + 月級查詢粒度限制。若未來資料量成長,可考慮請 DBA 加 HOLDTXNDATE index。
- **[Calendar-spine cross-join 效能]** → 月曆骨幹 × 全表 cross join 是最重的查詢。緩解Redis 快取近二月,超過二月直接查詢但接受較長 loading。
- **[Redis 快取一致性]** → 12hr TTL 意味資料最多延遲 12 小時。緩解:歷史資料本身就是 T-1 更新12hr 延遲對管理決策無影響。
- **[明細表回傳 HOLDCOMMENTS/RELEASECOMMENTS]** → 文字欄位可能很長。緩解:前端 truncate 顯示hover 看全文。

View File

@@ -0,0 +1,32 @@
## Why
Hold Overview 和 Hold Detail 都是基於 DW_MES_LOT_V 的即時快照,只能回答「現在線上有什麼 Hold」。主管需要追蹤歷史趨勢來回答「Hold 狀況是在改善還是惡化?哪些原因最耗時?哪個部門處理最慢?」。目前這些分析只能透過 BI 工具 (PJMES043) 手動查詢,無法即時在 Dashboard 上呈現。
## What Changes
- 新增 `/hold-history` 頁面,提供 Hold/Release 歷史績效 Dashboard
- 新增 5 支 API endpoints (`/api/hold-history/trend`, `reason-pareto`, `duration`, `department`, `list`)
- 新增 `hold_history_service.py` 服務層,查詢 `DWH.DW_MES_HOLDRELEASEHISTORY`
- 新增 SQL 檔案集中管理在 `src/mes_dashboard/sql/hold_history/` 目錄
- trend API 採用 Redis 快取策略近二月聚合資料12hr TTL
- 翻譯 `docs/hold_history.md` 中的 calendar-spine cross-join 商業邏輯為參數化 SQL
- 新增 Vite entry point `src/hold-history/index.html`
- 新增頁面註冊至 `data/page_status.json`
## Capabilities
### New Capabilities
- `hold-history-page`: Hold 歷史績效 Dashboard 前端頁面包含篩選器、Summary KPIs、Daily Trend 圖、Reason Pareto、Duration 分布、負責人統計、明細表,及 Reason Pareto 的 cascade filter 機制
- `hold-history-api`: Hold 歷史績效 API 後端,包含 5 支 endpoints、Oracle 查詢(含 calendar-spine 商業邏輯、Redis 快取策略、SQL 集中管理
### Modified Capabilities
- `vue-vite-page-architecture`: 新增 Hold History entry point 至 Vite 配置
## Impact
- **後端**: 新增 Flask Blueprint `hold_history_routes.py`、服務層 `hold_history_service.py`、SQL 檔案 `sql/hold_history/`
- **前端**: 新增 `frontend/src/hold-history/` 頁面目錄,使用 ECharts (BarChart, LineChart) 及 wip-shared composables
- **資料庫**: 直接查詢 `DWH.DW_MES_HOLDRELEASEHISTORY`~310K rows無 schema 變更
- **Redis**: 新增 `hold_history:daily:{YYYY-MM}` 快取 key
- **配置**: `vite.config.js` 新增 entry、`page_status.json` 新增頁面註冊
- **既有功能**: 無影響,完全獨立的新頁面和新 API

View File

@@ -0,0 +1,172 @@
## ADDED Requirements
### Requirement: Hold History API SHALL provide daily trend data with Redis caching
The API SHALL return daily aggregated hold/release metrics for the selected date range.
#### Scenario: Trend endpoint returns all three hold types
- **WHEN** `GET /api/hold-history/trend?start_date=2025-01-01&end_date=2025-01-31` is called
- **THEN** the response SHALL return `{ success: true, data: { days: [...] } }`
- **THEN** each day item SHALL contain `{ date, quality: { holdQty, newHoldQty, releaseQty, futureHoldQty }, non_quality: { ... }, all: { ... } }`
- **THEN** all three hold_type variants SHALL be included in a single response
#### Scenario: Trend uses shift boundary at 07:30
- **WHEN** daily aggregation is calculated
- **THEN** transactions with time >= 07:30 SHALL be attributed to the next calendar day
- **THEN** transactions with time < 07:30 SHALL be attributed to the current calendar day
#### Scenario: Trend deduplicates same-day multiple holds
- **WHEN** a lot is held multiple times on the same day
- **THEN** only one hold event SHALL be counted for that day (using ROW_NUMBER per CONTAINERID per day)
#### Scenario: Trend deduplicates future holds
- **WHEN** the same lot has multiple future holds for the same reason
- **THEN** only the first occurrence SHALL be counted (using ROW_NUMBER per CONTAINERID per HOLDREASONID)
#### Scenario: Trend hold type classification
- **WHEN** trend data is aggregated by hold type
- **THEN** quality classification SHALL use the same NON_QUALITY_HOLD_REASONS set as existing hold endpoints
- **THEN** holds with HOLDREASONNAME NOT in NON_QUALITY_HOLD_REASONS SHALL be classified as quality
- **THEN** the "all" variant SHALL include both quality and non-quality holds
#### Scenario: Trend Redis cache for recent two months
- **WHEN** the requested date range falls within the current month or previous month
- **THEN** the service SHALL check Redis for cached data at key `hold_history:daily:{YYYY-MM}`
- **THEN** if cache exists, data SHALL be returned from Redis
- **THEN** if cache is missing, data SHALL be queried from Oracle and stored in Redis with 12-hour TTL
#### Scenario: Trend direct Oracle query for older data
- **WHEN** the requested date range includes months older than the previous month
- **THEN** the service SHALL query Oracle directly without caching
#### Scenario: Trend cross-month query assembly
- **WHEN** the requested date range spans multiple months (e.g., 2025-01-15 to 2025-02-15)
- **THEN** the service SHALL fetch each month's data independently (from cache or Oracle)
- **THEN** the service SHALL trim the combined result to the exact requested date range
- **THEN** the response SHALL contain only days within start_date and end_date inclusive
#### Scenario: Trend error
- **WHEN** the database query fails
- **THEN** the response SHALL return `{ success: false, error: '查詢失敗' }` with HTTP 500
### Requirement: Hold History API SHALL provide reason Pareto data
The API SHALL return hold reason distribution for Pareto analysis.
#### Scenario: Reason Pareto endpoint
- **WHEN** `GET /api/hold-history/reason-pareto?start_date=2025-01-01&end_date=2025-01-31&hold_type=quality` is called
- **THEN** the response SHALL return `{ success: true, data: { items: [...] } }`
- **THEN** each item SHALL contain `{ reason, count, qty, pct, cumPct }`
- **THEN** items SHALL be sorted by count descending
- **THEN** pct SHALL be percentage of total hold events
- **THEN** cumPct SHALL be running cumulative percentage
#### Scenario: Reason Pareto uses shift boundary
- **WHEN** hold events are counted for Pareto
- **THEN** the 07:30 shift boundary rule SHALL be applied to HOLDTXNDATE
#### Scenario: Reason Pareto hold type filter
- **WHEN** hold_type is "quality"
- **THEN** only quality hold reasons SHALL be included
- **WHEN** hold_type is "non-quality"
- **THEN** only non-quality hold reasons SHALL be included
- **WHEN** hold_type is "all"
- **THEN** all hold reasons SHALL be included
### Requirement: Hold History API SHALL provide hold duration distribution
The API SHALL return hold duration distribution buckets.
#### Scenario: Duration endpoint
- **WHEN** `GET /api/hold-history/duration?start_date=2025-01-01&end_date=2025-01-31&hold_type=quality` is called
- **THEN** the response SHALL return `{ success: true, data: { items: [...] } }`
- **THEN** items SHALL contain 4 buckets: `{ range: "<4h", count, pct }`, `{ range: "4-24h", count, pct }`, `{ range: "1-3d", count, pct }`, `{ range: ">3d", count, pct }`
#### Scenario: Duration only includes released holds
- **WHEN** duration is calculated
- **THEN** only hold records with RELEASETXNDATE IS NOT NULL SHALL be included
- **THEN** duration SHALL be calculated as RELEASETXNDATE - HOLDTXNDATE
#### Scenario: Duration date range filter
- **WHEN** start_date and end_date are provided
- **THEN** only holds with HOLDTXNDATE within the date range (applying 07:30 shift boundary) SHALL be included
### Requirement: Hold History API SHALL provide department statistics
The API SHALL return hold/release statistics aggregated by department with optional person detail.
#### Scenario: Department endpoint
- **WHEN** `GET /api/hold-history/department?start_date=2025-01-01&end_date=2025-01-31&hold_type=quality` is called
- **THEN** the response SHALL return `{ success: true, data: { items: [...] } }`
- **THEN** each item SHALL contain `{ dept, holdCount, releaseCount, avgHoldHours, persons: [{ name, holdCount, releaseCount, avgHoldHours }] }`
- **THEN** items SHALL be sorted by holdCount descending
#### Scenario: Department with reason filter
- **WHEN** `GET /api/hold-history/department?start_date=2025-01-01&end_date=2025-01-31&hold_type=quality&reason=品質確認` is called
- **THEN** only hold records matching the specified reason SHALL be included in department and person statistics
#### Scenario: Department hold count vs release count
- **WHEN** department statistics are calculated
- **THEN** holdCount SHALL count records where HOLDEMPDEPTNAME equals the department AND HOLDTXNDATE is within the date range
- **THEN** releaseCount SHALL count records where RELEASEEMPDEPTNAME equals the department AND RELEASETXNDATE is within the date range
- **THEN** avgHoldHours SHALL be the average of (RELEASETXNDATE - HOLDTXNDATE) in hours for released holds initiated by that department
### Requirement: Hold History API SHALL provide paginated detail list
The API SHALL return a paginated list of individual hold/release records.
#### Scenario: List endpoint
- **WHEN** `GET /api/hold-history/list?start_date=2025-01-01&end_date=2025-01-31&hold_type=quality&page=1&per_page=50` is called
- **THEN** the response SHALL return `{ success: true, data: { items: [...], pagination: { page, perPage, total, totalPages } } }`
- **THEN** each item SHALL contain: lotId, workorder, workcenter, holdReason, holdDate, holdEmp, holdComment, releaseDate, releaseEmp, releaseComment, holdHours, ncr
- **THEN** items SHALL be sorted by HOLDTXNDATE descending
#### Scenario: List with reason filter
- **WHEN** `GET /api/hold-history/list?start_date=2025-01-01&end_date=2025-01-31&hold_type=quality&reason=品質確認` is called
- **THEN** only records matching the specified HOLDREASONNAME SHALL be returned
#### Scenario: List unreleased hold records
- **WHEN** a hold record has RELEASETXNDATE IS NULL
- **THEN** releaseDate SHALL be null
- **THEN** holdHours SHALL be calculated as (SYSDATE - HOLDTXNDATE) * 24
#### Scenario: List 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: List date range uses shift boundary
- **WHEN** records are filtered by date range
- **THEN** the 07:30 shift boundary rule SHALL be applied to HOLDTXNDATE
### Requirement: Hold History API SHALL use centralized SQL files
The API SHALL load SQL queries from files in the `src/mes_dashboard/sql/hold_history/` directory.
#### Scenario: SQL file organization
- **WHEN** the hold history service executes a query
- **THEN** the SQL SHALL be loaded from `sql/hold_history/<query_name>.sql`
- **THEN** the following SQL files SHALL exist: `trend.sql`, `reason_pareto.sql`, `duration.sql`, `department.sql`, `list.sql`
#### Scenario: SQL parameterization
- **WHEN** SQL queries are executed
- **THEN** all user-provided parameters (dates, hold_type, reason) SHALL be passed as bind parameters
- **THEN** no string interpolation SHALL be used for user input
### Requirement: Hold History API SHALL apply rate limiting
The API SHALL apply rate limiting to expensive endpoints.
#### Scenario: Rate limit on list endpoint
- **WHEN** the list 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 trend endpoint
- **WHEN** the trend endpoint receives excessive requests
- **THEN** rate limiting SHALL be applied using `configured_rate_limit` with a default of 60 requests per 60 seconds
### Requirement: Hold History 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-history`
- **THEN** Flask SHALL serve the pre-built HTML file from `static/dist/hold-history.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

View File

@@ -0,0 +1,172 @@
## ADDED Requirements
### Requirement: Hold History page SHALL display a filter bar with date range and hold type
The page SHALL provide a filter bar for selecting date range and hold type classification.
#### Scenario: Default date range
- **WHEN** the page loads
- **THEN** the date range SHALL default to the first and last day of the current month
#### Scenario: Hold Type radio default
- **WHEN** the page loads
- **THEN** the Hold Type filter SHALL default to "品質異常"
- **THEN** three radio options SHALL display: 品質異常, 非品質異常, 全部
#### Scenario: Filter bar change reloads all data
- **WHEN** user changes the date range or Hold Type selection
- **THEN** all API calls (trend, reason-pareto, duration, department, list) SHALL reload with the new parameters
- **THEN** any active Reason Pareto filter SHALL be cleared
- **THEN** pagination SHALL reset to page 1
### Requirement: Hold History page SHALL display summary KPI cards
The page SHALL show 6 summary KPI cards derived from the trend data for the selected period.
#### Scenario: Summary cards rendering
- **WHEN** trend data is loaded
- **THEN** six cards SHALL display: Release 數量, New Hold 數量, Future Hold 數量, 淨變動, 期末 On Hold, 平均 Hold 時長
- **THEN** Release SHALL be displayed as a positive indicator (green)
- **THEN** New Hold and Future Hold SHALL be displayed as negative indicators (red/orange)
- **THEN** 淨變動 SHALL equal Release - New Hold - Future Hold
- **THEN** 期末 On Hold SHALL be the HOLDQTY of the last day in the selected range
- **THEN** number values SHALL use zh-TW number formatting
#### Scenario: Summary reflects filter bar only
- **WHEN** user clicks a Reason Pareto block
- **THEN** summary cards SHALL NOT change (they only respond to filter bar changes)
### Requirement: Hold History page SHALL display a Daily Trend chart
The page SHALL display a mixed line+bar chart showing daily hold stock and flow.
#### Scenario: Daily Trend chart rendering
- **WHEN** trend data is loaded
- **THEN** an ECharts mixed chart SHALL display with dual Y-axes
- **THEN** the left Y-axis SHALL show flow quantities (Release, New Hold, Future Hold)
- **THEN** the right Y-axis SHALL show HOLDQTY stock level
- **THEN** the X-axis SHALL show dates within the selected range
#### Scenario: Bar direction encoding
- **WHEN** daily trend bars are rendered
- **THEN** Release bars SHALL extend upward (positive direction, green color)
- **THEN** New Hold bars SHALL extend downward (negative direction, red color)
- **THEN** Future Hold bars SHALL extend downward (negative direction, orange color, stacked with New Hold)
- **THEN** HOLDQTY SHALL display as a line on the right Y-axis
#### Scenario: Hold Type switching without re-call
- **WHEN** user changes the Hold Type radio on the filter bar
- **THEN** if the date range has not changed, the trend chart SHALL update from locally cached data
- **THEN** no additional API call SHALL be made for the trend endpoint
#### Scenario: Daily Trend reflects filter bar only
- **WHEN** user clicks a Reason Pareto block
- **THEN** the Daily Trend chart SHALL NOT change (it only responds to filter bar changes)
### Requirement: Hold History page SHALL display a Reason Pareto chart
The page SHALL display a Pareto chart showing hold reason distribution.
#### Scenario: Reason Pareto rendering
- **WHEN** reason-pareto data is loaded
- **THEN** a Pareto chart SHALL display with bars (count per reason) and a cumulative percentage line
- **THEN** reasons SHALL be sorted by count descending
- **THEN** the cumulative line SHALL reach 100% at the rightmost bar
#### Scenario: Reason Pareto click filters downstream
- **WHEN** user clicks a reason bar in the Pareto chart
- **THEN** `reasonFilter` SHALL be set to the clicked reason name
- **THEN** Department table SHALL reload filtered by that reason
- **THEN** Detail table SHALL reload filtered by that reason
- **THEN** the clicked bar SHALL show a visual highlight
#### Scenario: Reason Pareto click toggle
- **WHEN** user clicks the same reason bar that is already active
- **THEN** `reasonFilter` SHALL be cleared
- **THEN** Department table and Detail table SHALL reload without reason filter
#### Scenario: Reason Pareto reflects filter bar only
- **WHEN** user clicks a reason bar
- **THEN** Summary KPIs, Daily Trend, and Duration chart SHALL NOT change
### Requirement: Hold History page SHALL display Hold Duration distribution
The page SHALL display a horizontal bar chart showing hold duration distribution.
#### Scenario: Duration chart rendering
- **WHEN** duration data is loaded
- **THEN** a horizontal bar chart SHALL display with 4 buckets: <4h, 4-24h, 1-3天, >3天
- **THEN** each bar SHALL show count and percentage
- **THEN** only released holds (RELEASETXNDATE IS NOT NULL) SHALL be included
#### Scenario: Duration reflects filter bar only
- **WHEN** user clicks a Reason Pareto block
- **THEN** the Duration chart SHALL NOT change (it only responds to filter bar changes)
### Requirement: Hold History page SHALL display Department statistics with expandable rows
The page SHALL display a table showing hold/release statistics per department, expandable to show individual persons.
#### Scenario: Department table rendering
- **WHEN** department data is loaded
- **THEN** a table SHALL display with columns: 部門, Hold 次數, Release 次數, 平均 Hold 時長(hr)
- **THEN** departments SHALL be sorted by Hold 次數 descending
- **THEN** each department row SHALL have an expand toggle
#### Scenario: Department row expansion
- **WHEN** user clicks the expand toggle on a department row
- **THEN** individual person rows SHALL display below the department row
- **THEN** person rows SHALL show: 人員名稱, Hold 次數, Release 次數, 平均 Hold 時長(hr)
#### Scenario: Department table responds to reason filter
- **WHEN** a Reason Pareto filter is active
- **THEN** department data SHALL reload filtered by the selected reason
- **THEN** only holds matching the reason SHALL be included in statistics
### Requirement: Hold History page SHALL display paginated Hold/Release detail list
The page SHALL display a detailed list of individual hold/release records with server-side pagination.
#### Scenario: Detail table columns
- **WHEN** detail data is loaded
- **THEN** a table SHALL display with columns: Lot ID, WorkOrder, 站別, Hold Reason, Hold 時間, Hold 人員, Hold Comment, Release 時間, Release 人員, Release Comment, 時長(hr), NCR
#### Scenario: Unreleased hold display
- **WHEN** a hold record has RELEASETXNDATE IS NULL
- **THEN** the Release 時間 column SHALL display "仍在 Hold"
- **THEN** the 時長 column SHALL display the duration from HOLDTXNDATE to current time
#### Scenario: Detail table pagination
- **WHEN** total records exceed per_page (50)
- **THEN** Prev/Next buttons and page info SHALL display
- **THEN** page info SHALL show "顯示 {start} - {end} / {total}"
#### Scenario: Detail table responds to reason filter
- **WHEN** a Reason Pareto filter is active
- **THEN** detail data SHALL reload filtered by the selected reason
- **THEN** pagination SHALL reset to page 1
#### Scenario: Filter changes reset pagination
- **WHEN** any filter changes (filter bar or Reason Pareto click)
- **THEN** pagination SHALL reset to page 1
### Requirement: Hold History page SHALL display active filter indicator
The page SHALL show a clear indicator when a Reason Pareto filter is active.
#### Scenario: Reason filter indicator
- **WHEN** a reason filter is active
- **THEN** a filter indicator SHALL display above the Department table section
- **THEN** the indicator SHALL show the active reason name
- **THEN** a clear button (✕) SHALL remove the reason filter
### Requirement: Hold History 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
### Requirement: Hold History page SHALL have navigation links
The page SHALL provide navigation to related pages.
#### Scenario: Back to Hold Overview
- **WHEN** user clicks the "← Hold Overview" button in the header
- **THEN** the page SHALL navigate to `/hold-overview`

View File

@@ -0,0 +1,45 @@
## 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, BarChart, LineChart) 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: Hold History entry point
- **WHEN** the hold-history page is added
- **THEN** `vite.config.js` input SHALL include `'hold-history': resolve(__dirname, 'src/hold-history/index.html')`
- **THEN** the build SHALL produce `hold-history.html`, `hold-history.js`, and `hold-history.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-history` 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,66 @@
## 1. SQL 檔案建立
- [x] 1.1 建立 `src/mes_dashboard/sql/hold_history/` 目錄
- [x] 1.2 建立 `trend.sql` — calendar-spine cross-join 每日聚合查詢(翻譯 hold_history.md 邏輯,含 07:30 班別邊界、同日去重、Future Hold 去重、品質分類)
- [x] 1.3 建立 `reason_pareto.sql` — GROUP BY HOLDREASONNAME含 count/qty/pct/cumPct 計算
- [x] 1.4 建立 `duration.sql` — 已 release hold 的時長分布4 bucket: <4h, 4-24h, 1-3d, >3d
- [x] 1.5 建立 `department.sql` — GROUP BY HOLDEMPDEPTNAME / HOLDEMP含 hold/release 計數及平均時長
- [x] 1.6 建立 `list.sql` — paginated 明細查詢(含 HOLDCOMMENTS/RELEASECOMMENTS未 release 用 SYSDATE 計算時長)
## 2. 後端服務層
- [x] 2.1 建立 `src/mes_dashboard/services/hold_history_service.py`,實作 SQL 載入輔助函式(從 sql/hold_history/ 讀取 .sql 檔案)
- [x] 2.2 實作 `get_hold_history_trend(start_date, end_date)` — 執行 trend.sql回傳三種 hold_type 的每日聚合資料
- [x] 2.3 實作 trend Redis 快取邏輯 — 近二月快取key: `hold_history:daily:{YYYY-MM}`TTL 12hr跨月查詢拼接超過二月直接 Oracle
- [x] 2.4 實作 `get_hold_history_reason_pareto(start_date, end_date, hold_type)` — 執行 reason_pareto.sql
- [x] 2.5 實作 `get_hold_history_duration(start_date, end_date, hold_type)` — 執行 duration.sql
- [x] 2.6 實作 `get_hold_history_department(start_date, end_date, hold_type, reason=None)` — 執行 department.sql回傳部門層級含 persons 陣列
- [x] 2.7 實作 `get_hold_history_list(start_date, end_date, hold_type, reason=None, page=1, per_page=50)` — 執行 list.sql回傳 paginated 結果
## 3. 後端路由層
- [x] 3.1 建立 `src/mes_dashboard/routes/hold_history_routes.py` Flask Blueprint
- [x] 3.2 實作 `GET /hold-history` 頁面路由 — send_from_directory / fallback HTML
- [x] 3.3 實作 `GET /api/hold-history/trend` — 呼叫 servicerate limit 60/60s
- [x] 3.4 實作 `GET /api/hold-history/reason-pareto` — 呼叫 service
- [x] 3.5 實作 `GET /api/hold-history/duration` — 呼叫 service
- [x] 3.6 實作 `GET /api/hold-history/department` — 呼叫 service含 optional reason 參數
- [x] 3.7 實作 `GET /api/hold-history/list` — 呼叫 servicerate limit 90/60s含 optional reason 參數
- [x] 3.8 在 `routes/__init__.py` 註冊 hold_history_bp Blueprint
## 4. 頁面註冊與 Vite 配置
- [x] 4.1 在 `data/page_status.json` 新增 `/hold-history` 頁面status: dev, drawer: reports
- [x] 4.2 在 `frontend/vite.config.js` 新增 `'hold-history': resolve(__dirname, 'src/hold-history/index.html')` entry point
## 5. 前端頁面骨架
- [x] 5.1 建立 `frontend/src/hold-history/` 目錄結構index.html, main.js, App.vue, style.css
- [x] 5.2 實作 `App.vue` — 頁面主容器、狀態管理filterBar, reasonFilter, pagination、API 呼叫流程、cascade filter 邏輯
- [x] 5.3 實作 `FilterBar.vue` — DatePicker預設當月+ Hold Type radio品質異常/非品質異常/全部)
## 6. 前端元件 — KPI 與趨勢圖
- [x] 6.1 實作 `SummaryCards.vue` — 6 張 KPI 卡片Release, New Hold, Future Hold, 淨變動, 期末 On Hold, 平均時長Release 綠色正向、New/Future 紅/橙負向
- [x] 6.2 實作 `DailyTrend.vue` — ECharts 折線+柱狀混合圖,左 Y 軸增減量Release↑綠, New↓紅, Future↓橙 stacked右 Y 軸 On Hold 折線
## 7. 前端元件 — 分析圖表
- [x] 7.1 實作 `ReasonPareto.vue` — ECharts Pareto 圖(柱狀 count + 累積%折線),可點擊觸發 reasonFilter toggle
- [x] 7.2 實作 `DurationChart.vue` — ECharts 橫向柱狀圖(<4h, 4-24h, 1-3天, >3天顯示 count 和百分比
## 8. 前端元件 — 表格
- [x] 8.1 實作 `FilterIndicator.vue` — 顯示 active reason filter 及清除按鈕
- [x] 8.2 實作 `DepartmentTable.vue` — 部門統計表,可展開看個人層級,受 reasonFilter 篩選
- [x] 8.3 實作 `DetailTable.vue` — paginated 明細表12 欄位),未 release 顯示 "仍在 Hold",受 reasonFilter 篩選
## 9. 後端測試
- [x] 9.1 建立 `tests/test_hold_history_routes.py` — 測試頁面路由(含 admin session、5 支 API endpoint 參數傳遞、rate limiting、error handling
- [x] 9.2 建立 `tests/test_hold_history_service.py` — 測試 trend 快取邏輯cache hit/miss/cross-month、各 service function 的 Oracle 查詢與回傳格式、hold_type 分類、shift boundary 邏輯
## 10. 整合驗證
- [x] 10.1 執行既有測試確認無回歸test_hold_overview_routes, test_wip_service, test_page_registry
- [x] 10.2 驗證 vite build 成功產出 hold-history.html/js/css 且不影響既有 entry points