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:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-10
|
||||
@@ -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 的 hold(RELEASETXNDATE 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 看全文。
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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` — 呼叫 service,rate 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` — 呼叫 service,rate 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
|
||||
Reference in New Issue
Block a user