feat: add material trace page for bidirectional LOT/material query
Implement full-stack material trace feature enabling forward (LOT/工單 → 原物料) and reverse (原物料 → LOT) queries with wildcard support, safeguards (memory guard, IN-clause batching, Oracle slow-query channel), CSV export, and portal-shell integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-03
|
||||
@@ -0,0 +1,78 @@
|
||||
## Context
|
||||
|
||||
工程師需要查詢 LOT/工單對應的原物料消耗記錄,以及反向從原物料批號追溯使用該批料的所有 LOT。目前原物料資訊只能在 Query Tool 的 LotDetail "原物料" tab 逐筆查看(透過 `/api/trace/events?domains=["materials"]`),不支援批量輸入或反向查詢。
|
||||
|
||||
資料來源 `DWH.DW_MES_LOTMATERIALSHISTORY` 有 1800 萬筆記錄,已建立四個索引:
|
||||
- IDX1: `CONTAINERID`(正向 LOT 查詢)
|
||||
- IDX2: `PJ_WORKORDER`(正向工單查詢)
|
||||
- IDX3: `MATERIALPARTNAME`(料號,本次不使用)
|
||||
- IDX4: `MATERIALLOTNAME`(反向原物料批號查詢)
|
||||
|
||||
站群組(WORKCENTER_GROUP)對應由 `filter_cache.get_workcenter_mapping()` 提供,從 `DW_MES_SPEC_WORKCENTER_V` 載入,每小時刷新。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 提供獨立頁面,支援正向(LOT ID / 工單 → 原物料)和反向(原物料批號 → LOT)雙向查詢
|
||||
- 正向查詢支援 LOT ID 和工單兩種輸入模式切換
|
||||
- 支援多筆輸入(換行/逗號分隔)
|
||||
- 結果含站群組篩選、分頁、CSV 匯出
|
||||
- 使用既有 Oracle 索引,查詢效率可控
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不支援 MATERIALPARTNAME(料號)反向查詢(資料量風險過高,同一料號可能數萬筆)
|
||||
- 不需日期範圍篩選(以 LOT/工單/原物料批號為查詢條件即可)
|
||||
- 不做 Redis 快取或 BatchQueryEngine 分片(查詢範圍由輸入筆數控制,非時間範圍)
|
||||
- 不做 BOM 對照或原物料品質統計
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: 使用 `read_sql_df`(pooled connection)而非 `read_sql_df_slow`
|
||||
|
||||
**決定**: 查詢使用 pooled connection(`read_sql_df`),不走 slow query path。
|
||||
|
||||
**理由**: 此查詢依賴索引命中,預期回應時間 < 5s。不像 reject-history 的全表掃描需要 dedicated connection。正向查詢最多幾千筆結果,反向查詢設結果上限 10,000 筆。
|
||||
|
||||
**替代方案**: 使用 `read_sql_df_slow`。
|
||||
**為何不採用**: 佔用 slow query semaphore 會排擠需要長時間執行的查詢(reject-history、resource-history)。
|
||||
|
||||
### D2: 正向查詢先解析 LOT ID → CONTAINERID
|
||||
|
||||
**決定**: LOT ID 輸入模式需要先將 CONTAINERNAME 轉換為 CONTAINERID(16-char hex),因為 `LOTMATERIALSHISTORY` 的索引是 CONTAINERID。使用 `DW_MES_CONTAINER` 做 batch lookup。工單模式直接查 `PJ_WORKORDER` 索引,不需轉換。
|
||||
|
||||
**理由**: 使用者輸入的是可讀的 LOT 名稱(如 GA25060001-A01),但資料表索引是 CONTAINERID。直接 JOIN 會讓 optimizer 可能選擇低效計畫。先 batch resolve 再用 IN clause 更可預測。
|
||||
|
||||
**替代方案**: SQL 內直接 JOIN CONTAINER 表。
|
||||
**為何不採用**: 對於多筆 LOT 輸入,兩步驟(resolve + query)的執行計畫更穩定,且 resolve 結果可重用於顯示。
|
||||
|
||||
### D3: 站群組篩選在後端 enrichment 而非 SQL WHERE
|
||||
|
||||
**決定**: SQL 查詢不加 WORKCENTERNAME 過濾。查詢結果回來後,後端用 `get_workcenter_mapping()` 對每列添加 `WORKCENTER_GROUP` 欄位,前端可做篩選。若使用者選了站群組篩選,後端先 resolve 站群組 → WORKCENTERNAME 清單,再在 SQL 加 `AND WORKCENTERNAME IN (...)` 過濾。
|
||||
|
||||
**理由**: 如果不篩選,使用者能看到所有站點的資料(含站群組欄位)。如果篩選了,SQL 層就縮減結果集,減少傳輸和分頁壓力。
|
||||
|
||||
### D4: 反向查詢結果數上限 10,000 筆
|
||||
|
||||
**決定**: 反向查詢(原物料批號 → LOT)加入 `FETCH FIRST 10001 ROWS ONLY` 上限。若回傳超過 10,000 筆,前端顯示警告「結果超過上限,請縮小查詢範圍」。
|
||||
|
||||
**理由**: 一批常用原物料可能被上千個 LOT 使用。無上限的反向查詢可能回傳數萬筆,壓垮前端和 Oracle 連線。10,000 筆足以覆蓋絕大多數場景。
|
||||
|
||||
### D5: 前端頁面結構沿用 Vite multi-page 模式
|
||||
|
||||
**決定**: 新增 `frontend/material-trace.html` + `frontend/src/material-trace/App.vue` 作為獨立 Vite entry point。沿用 reject-history 的單檔 App.vue + 子元件模式。
|
||||
|
||||
**理由**: 專案的所有查詢頁面(reject-history、hold-history、resource-history)都是獨立 Vite entry。統一架構。
|
||||
|
||||
### D6: 輸入筆數上限
|
||||
|
||||
**決定**: 正向查詢(LOT ID / 工單)輸入上限 200 筆,反向查詢(原物料批號)輸入上限 50 筆。
|
||||
|
||||
**理由**: 正向查詢每筆 LOT 平均產生 10-50 筆原物料記錄,200 筆 LOT 最多 10,000 筆結果。反向查詢每批原物料可能對應 100-1000 個 LOT,50 批已有碰上 10,000 筆上限的風險。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[低] CONTAINERID resolve 多一次 round-trip** — LOT ID 模式需先查 `DW_MES_CONTAINER` 轉換。→ Container 表有 CONTAINERNAME 索引,batch IN query 很快(< 1s)。
|
||||
- **[低] 站群組 mapping 可能未涵蓋所有 WORKCENTERNAME** — `DW_MES_SPEC_WORKCENTER_V` 可能缺少新站點。→ 未映射的站點在結果中站群組欄位顯示空值,不影響查詢結果。
|
||||
- **[中] 反向查詢結果截斷** — 10,000 筆上限可能截斷大量使用的原物料批號結果。→ 前端明確顯示截斷警告,引導使用者縮小範圍。
|
||||
@@ -0,0 +1,35 @@
|
||||
## Why
|
||||
|
||||
生產追溯過程中,工程師需要查詢「某個 LOT/工單在哪個站群組用了什麼原物料」以及「某批原物料被哪些 LOT 使用」。目前原物料消耗資訊散落在 Query Tool 的 LotDetail "原物料" tab 中,只能逐筆 LOT 查看,無法批量查詢或反向追溯。缺少專屬頁面讓原物料異常時的影響範圍評估非常耗時。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增「原物料追溯查詢」獨立頁面,提供雙向查詢能力:
|
||||
- **正向查詢**:輸入 LOT ID 或工單號碼(多筆),查詢對應的原物料消耗記錄,可依站群組篩選
|
||||
- **反向查詢**:輸入原物料批號 MATERIALLOTNAME(多筆),查詢該批原物料被哪些 LOT 使用
|
||||
- 結果表格含分頁、站群組篩選、CSV 匯出
|
||||
- 後端新增 `/api/material-trace/query` 和 `/api/material-trace/export` API 端點
|
||||
- 查詢資料來源:`DWH.DW_MES_LOTMATERIALSHISTORY`(1800 萬筆),利用既有索引(CONTAINERID, PJ_WORKORDER, MATERIALLOTNAME)
|
||||
- 站群組對應透過 `filter_cache.get_workcenter_mapping()` 解析(與設備歷史績效共用同一份 mapping)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `material-trace-page`: 原物料追溯查詢頁面 — 前端 UI、查詢模式切換、結果表格、分頁、CSV 匯出
|
||||
- `material-trace-api`: 原物料追溯 API — 正向/反向查詢端點、輸入驗證、結果分頁、匯出端點、rate limiting
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
(無既有 spec 需修改)
|
||||
|
||||
## Impact
|
||||
|
||||
- **新增後端服務** — `src/mes_dashboard/services/material_trace_service.py`:正向/反向查詢邏輯、站群組 enrichment
|
||||
- **新增後端路由** — `src/mes_dashboard/routes/material_trace_routes.py`:API 端點註冊
|
||||
- **新增 SQL** — `src/mes_dashboard/sql/material_trace/`:3 個查詢檔(forward_by_lot、forward_by_workorder、reverse_by_material_lot)
|
||||
- **新增前端頁面** — `frontend/src/material-trace/`:App.vue + 子元件(FilterPanel、ResultTable)
|
||||
- **新增前端入口** — `frontend/material-trace.html` + Vite entry
|
||||
- **共用依賴** — `filter_cache.get_workcenter_mapping()` 提供站群組對應、`parseMultiLineInput()` 處理多筆輸入
|
||||
- **資料庫** — 查詢 `DWH.DW_MES_LOTMATERIALSHISTORY`,使用既有索引,無 schema 變更
|
||||
- **Sidebar** — 需在導覽列新增頁面入口
|
||||
@@ -0,0 +1,120 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Material Trace API SHALL provide forward query endpoint
|
||||
The API SHALL accept LOT IDs or work order numbers and return corresponding material consumption records from `DW_MES_LOTMATERIALSHISTORY`.
|
||||
|
||||
#### Scenario: Forward query by LOT ID
|
||||
- **WHEN** `POST /api/material-trace/query` is called with `mode: "lot"` and `values: ["GA25060001-A01", "GA25060502"]`
|
||||
- **THEN** the API SHALL resolve LOT names to CONTAINERIDs via `DW_MES_CONTAINER`
|
||||
- **THEN** the API SHALL return material consumption records matching those CONTAINERIDs
|
||||
- **THEN** each record SHALL include CONTAINERID, CONTAINERNAME, PJ_WORKORDER, WORKCENTERNAME, WORKCENTER_GROUP, MATERIALPARTNAME, MATERIALLOTNAME, VENDORLOTNUMBER, QTYREQUIRED, QTYCONSUMED, EQUIPMENTNAME, TXNDATE, PRIMARY_CATEGORY, SECONDARY_CATEGORY
|
||||
|
||||
#### Scenario: Forward query by work order
|
||||
- **WHEN** `POST /api/material-trace/query` is called with `mode: "workorder"` and `values: ["WO-2025-001", "WO-2025-002"]`
|
||||
- **THEN** the API SHALL query `DW_MES_LOTMATERIALSHISTORY` using `PJ_WORKORDER` index directly
|
||||
- **THEN** the response format SHALL be identical to LOT ID mode
|
||||
|
||||
#### Scenario: Forward query with workcenter group filter
|
||||
- **WHEN** `POST /api/material-trace/query` includes `workcenter_groups: ["焊接_DB"]`
|
||||
- **THEN** the API SHALL resolve group names to WORKCENTERNAME list via `filter_cache.get_workcenter_mapping()`
|
||||
- **THEN** the SQL query SHALL include `AND WORKCENTERNAME IN (...)` filter
|
||||
- **THEN** results SHALL only contain records from workcenters belonging to the selected groups
|
||||
|
||||
#### Scenario: Forward query input limit
|
||||
- **WHEN** `POST /api/material-trace/query` with `mode: "lot"` or `mode: "workorder"` contains more than 200 values
|
||||
- **THEN** the API SHALL return HTTP 400 with error message indicating the 200-value limit
|
||||
|
||||
### Requirement: Material Trace API SHALL provide reverse query endpoint
|
||||
The API SHALL accept material lot names and return LOTs that consumed those materials.
|
||||
|
||||
#### Scenario: Reverse query by material lot name
|
||||
- **WHEN** `POST /api/material-trace/query` is called with `mode: "material_lot"` and `values: ["WIRE-LOT-20250101-A"]`
|
||||
- **THEN** the API SHALL query `DW_MES_LOTMATERIALSHISTORY` using `MATERIALLOTNAME` index
|
||||
- **THEN** each record SHALL include the same fields as forward query results
|
||||
|
||||
#### Scenario: Reverse query with workcenter group filter
|
||||
- **WHEN** reverse query includes `workcenter_groups` parameter
|
||||
- **THEN** the same workcenter group filtering logic as forward query SHALL apply
|
||||
|
||||
#### Scenario: Reverse query input limit
|
||||
- **WHEN** `POST /api/material-trace/query` with `mode: "material_lot"` contains more than 50 values
|
||||
- **THEN** the API SHALL return HTTP 400 with error message indicating the 50-value limit
|
||||
|
||||
#### Scenario: Reverse query result limit
|
||||
- **WHEN** reverse query results exceed 10,000 rows
|
||||
- **THEN** the API SHALL return exactly 10,000 rows
|
||||
- **THEN** the response `meta` SHALL include `truncated: true` and `max_rows: 10000`
|
||||
|
||||
### Requirement: Material Trace API SHALL validate query parameters
|
||||
The API SHALL validate input parameters before executing database queries.
|
||||
|
||||
#### Scenario: Missing required fields
|
||||
- **WHEN** `POST /api/material-trace/query` is called without `mode` or `values`
|
||||
- **THEN** the API SHALL return HTTP 400 with descriptive validation error
|
||||
|
||||
#### Scenario: Invalid mode
|
||||
- **WHEN** `mode` is not one of `lot`, `workorder`, `material_lot`
|
||||
- **THEN** the API SHALL return HTTP 400
|
||||
|
||||
#### Scenario: Empty values
|
||||
- **WHEN** `values` is an empty array or all values are blank after trimming
|
||||
- **THEN** the API SHALL return HTTP 400 with error message "請輸入至少一筆查詢條件"
|
||||
|
||||
#### Scenario: Unresolvable LOT IDs
|
||||
- **WHEN** some LOT names cannot be resolved to CONTAINERIDs
|
||||
- **THEN** the API SHALL proceed with the resolved subset
|
||||
- **THEN** the response `meta` SHALL include `unresolved` array listing unresolvable LOT names
|
||||
|
||||
### Requirement: Material Trace API SHALL support paginated results
|
||||
The API SHALL support server-side pagination for query results.
|
||||
|
||||
#### Scenario: Pagination parameters
|
||||
- **WHEN** `POST /api/material-trace/query` includes `page` and `per_page`
|
||||
- **THEN** results SHALL be paginated accordingly
|
||||
- **THEN** response SHALL include `pagination: { page, per_page, total, total_pages }`
|
||||
|
||||
#### Scenario: Default pagination
|
||||
- **WHEN** `page` or `per_page` is not provided
|
||||
- **THEN** `page` SHALL default to 1
|
||||
- **THEN** `per_page` SHALL default to 50
|
||||
|
||||
#### Scenario: Per-page cap
|
||||
- **WHEN** `per_page` exceeds 200
|
||||
- **THEN** `per_page` SHALL be capped at 200
|
||||
|
||||
### Requirement: Material Trace API SHALL provide CSV export endpoint
|
||||
The API SHALL provide CSV export using the same query parameters as the query endpoint.
|
||||
|
||||
#### Scenario: Export request
|
||||
- **WHEN** `POST /api/material-trace/export` is called with the same parameters as query
|
||||
- **THEN** the response SHALL be a CSV file with UTF-8 BOM encoding
|
||||
- **THEN** CSV headers SHALL be in Chinese
|
||||
- **THEN** all matching records SHALL be included (no pagination, subject to result limits)
|
||||
|
||||
#### Scenario: Export result limit
|
||||
- **WHEN** export results exceed 50,000 rows
|
||||
- **THEN** the export SHALL be truncated at 50,000 rows
|
||||
- **THEN** a warning header SHALL indicate truncation
|
||||
|
||||
### Requirement: Material Trace API SHALL enrich results with workcenter group
|
||||
The API SHALL add WORKCENTER_GROUP to each result row based on `filter_cache.get_workcenter_mapping()`.
|
||||
|
||||
#### Scenario: Workcenter group enrichment
|
||||
- **WHEN** query results are returned
|
||||
- **THEN** each row SHALL include a `WORKCENTER_GROUP` field
|
||||
- **THEN** the value SHALL be resolved from `filter_cache.get_workcenter_mapping()` using the row's `WORKCENTERNAME`
|
||||
|
||||
#### Scenario: Unknown workcenter
|
||||
- **WHEN** a row's WORKCENTERNAME has no mapping in the workcenter cache
|
||||
- **THEN** `WORKCENTER_GROUP` SHALL be empty string
|
||||
|
||||
### Requirement: Material Trace API SHALL apply rate limiting
|
||||
The API SHALL rate-limit query and export endpoints to protect Oracle resources.
|
||||
|
||||
#### Scenario: Query rate limit
|
||||
- **WHEN** `/api/material-trace/query` receives excessive requests
|
||||
- **THEN** requests beyond 30 per 60 seconds SHALL be rejected with HTTP 429
|
||||
|
||||
#### Scenario: Export rate limit
|
||||
- **WHEN** `/api/material-trace/export` receives excessive requests
|
||||
- **THEN** requests beyond 10 per 60 seconds SHALL be rejected with HTTP 429
|
||||
@@ -0,0 +1,120 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Material Trace page SHALL provide bidirectional query mode switching
|
||||
The page SHALL provide two query directions with explicit tab switching.
|
||||
|
||||
#### Scenario: Forward query mode (default)
|
||||
- **WHEN** the page loads
|
||||
- **THEN** "正向查詢:LOT/工單 → 原物料" tab SHALL be active by default
|
||||
- **THEN** the input area SHALL show input type selector (LOT ID / 工單) and a multi-line text input
|
||||
|
||||
#### Scenario: Reverse query mode
|
||||
- **WHEN** user clicks "反向查詢:原物料 → LOT" tab
|
||||
- **THEN** the input area SHALL switch to material lot name multi-line input
|
||||
- **THEN** query results and pagination SHALL be cleared
|
||||
|
||||
#### Scenario: Forward input type switching
|
||||
- **WHEN** forward mode is active
|
||||
- **THEN** user SHALL be able to switch between "LOT ID" and "工單" input types
|
||||
- **THEN** switching input type SHALL clear the input field and results
|
||||
|
||||
### Requirement: Material Trace page SHALL accept multi-line input
|
||||
The page SHALL accept multiple values separated by newlines or commas.
|
||||
|
||||
#### Scenario: Multi-line input parsing
|
||||
- **WHEN** user enters values separated by newlines, commas, or mixed delimiters
|
||||
- **THEN** the system SHALL parse and deduplicate values using the same logic as `parseMultiLineInput()`
|
||||
|
||||
#### Scenario: Input count display
|
||||
- **WHEN** user enters values
|
||||
- **THEN** the input area SHALL display the parsed count (e.g., "已輸入 5 筆")
|
||||
|
||||
#### Scenario: Forward input limit feedback
|
||||
- **WHEN** user enters more than 200 values in forward mode
|
||||
- **THEN** the page SHALL display an error message "正向查詢上限 200 筆"
|
||||
- **THEN** the query SHALL NOT be sent
|
||||
|
||||
#### Scenario: Reverse input limit feedback
|
||||
- **WHEN** user enters more than 50 values in reverse mode
|
||||
- **THEN** the page SHALL display an error message "反向查詢上限 50 筆"
|
||||
- **THEN** the query SHALL NOT be sent
|
||||
|
||||
### Requirement: Material Trace page SHALL provide workcenter group filter
|
||||
The page SHALL allow filtering results by workcenter group.
|
||||
|
||||
#### Scenario: Workcenter group options
|
||||
- **WHEN** the page loads
|
||||
- **THEN** workcenter group filter SHALL be populated from `filter_cache.get_workcenter_groups()`
|
||||
- **THEN** the filter SHALL support multi-select
|
||||
- **THEN** default SHALL be "全部站點" (no filter)
|
||||
|
||||
#### Scenario: Filter applied to query
|
||||
- **WHEN** user selects workcenter groups and clicks "查詢"
|
||||
- **THEN** the selected groups SHALL be sent as `workcenter_groups` parameter to the API
|
||||
- **THEN** results SHALL only contain records from workcenters in the selected groups
|
||||
|
||||
### Requirement: Material Trace page SHALL display query results in a paginated table
|
||||
The page SHALL display results in a sortable, paginated detail table.
|
||||
|
||||
#### Scenario: Result table columns
|
||||
- **WHEN** query results are loaded
|
||||
- **THEN** the table SHALL display: LOT ID (CONTAINERNAME), 工單 (PJ_WORKORDER), 站群組 (WORKCENTER_GROUP), 站點 (WORKCENTERNAME), 料號 (MATERIALPARTNAME), 物料批號 (MATERIALLOTNAME), 供應商批號 (VENDORLOTNUMBER), 應領量 (QTYREQUIRED), 實際消耗 (QTYCONSUMED), 機台 (EQUIPMENTNAME), 交易日期 (TXNDATE), 主分類 (PRIMARY_CATEGORY), 副分類 (SECONDARY_CATEGORY)
|
||||
|
||||
#### Scenario: Pagination controls
|
||||
- **WHEN** results exceed per-page size
|
||||
- **THEN** pagination controls SHALL display "上一頁" / "下一頁" buttons and page info in Chinese
|
||||
- **THEN** default per-page size SHALL be 50
|
||||
|
||||
#### Scenario: Empty results
|
||||
- **WHEN** query returns no matching records
|
||||
- **THEN** the table area SHALL display "查無資料" message
|
||||
|
||||
#### Scenario: Unresolved LOT IDs warning
|
||||
- **WHEN** the API response contains `meta.unresolved` array
|
||||
- **THEN** a warning banner SHALL display listing the unresolvable LOT names
|
||||
|
||||
#### Scenario: Result truncation warning
|
||||
- **WHEN** the API response contains `meta.truncated: true`
|
||||
- **THEN** an amber warning banner SHALL display "查詢結果超過 10,000 筆上限,請縮小查詢範圍"
|
||||
|
||||
### Requirement: Material Trace page SHALL support CSV export
|
||||
The page SHALL allow exporting current query results to CSV.
|
||||
|
||||
#### Scenario: Export button
|
||||
- **WHEN** query results are loaded
|
||||
- **THEN** an "匯出 CSV" button SHALL be visible
|
||||
- **WHEN** user clicks "匯出 CSV"
|
||||
- **THEN** the export request SHALL use the same query parameters as the current query
|
||||
|
||||
#### Scenario: Export disabled without results
|
||||
- **WHEN** no query has been executed or results are empty
|
||||
- **THEN** the "匯出 CSV" button SHALL be disabled
|
||||
|
||||
### Requirement: Material Trace page SHALL provide loading and error states
|
||||
The page SHALL provide clear feedback during loading and error conditions.
|
||||
|
||||
#### Scenario: Loading state
|
||||
- **WHEN** a query is in progress
|
||||
- **THEN** a loading indicator SHALL be visible
|
||||
- **THEN** the query button SHALL be disabled
|
||||
|
||||
#### Scenario: API error
|
||||
- **WHEN** the API returns an error
|
||||
- **THEN** a red error banner SHALL display the error message
|
||||
|
||||
#### Scenario: Error cleared on new query
|
||||
- **WHEN** user initiates a new query
|
||||
- **THEN** previous error and warning banners SHALL be cleared
|
||||
|
||||
### Requirement: Material Trace page SHALL use Chinese labels
|
||||
The page SHALL display all UI text in Traditional Chinese consistent with the rest of the application.
|
||||
|
||||
#### Scenario: Page title
|
||||
- **WHEN** the page is rendered
|
||||
- **THEN** the page title SHALL be "原物料追溯查詢"
|
||||
|
||||
#### Scenario: Button labels
|
||||
- **WHEN** the page is rendered
|
||||
- **THEN** the query button SHALL display "查詢"
|
||||
- **THEN** the export button SHALL display "匯出 CSV"
|
||||
- **THEN** the clear button SHALL display "清除"
|
||||
@@ -0,0 +1,63 @@
|
||||
## 1. SQL 查詢檔
|
||||
|
||||
- [x] 1.1 建立 `src/mes_dashboard/sql/material_trace/` 目錄
|
||||
- [x] 1.2 新增 `forward_by_lot.sql`:以 CONTAINERID IN (:ids) 查詢 `DW_MES_LOTMATERIALSHISTORY`,LEFT JOIN `DW_MES_CONTAINER` 取 CONTAINERNAME,含可選 WORKCENTERNAME IN 篩選
|
||||
- [x] 1.3 新增 `forward_by_workorder.sql`:以 PJ_WORKORDER IN (:ids) 查詢,結構與 forward_by_lot 相同
|
||||
- [x] 1.4 新增 `reverse_by_material_lot.sql`:以 MATERIALLOTNAME IN (:ids) 查詢,LEFT JOIN `DW_MES_CONTAINER` 取 CONTAINERNAME,含 FETCH FIRST 10001 ROWS ONLY 上限,含可選 WORKCENTERNAME IN 篩選
|
||||
- [x] 1.5 新增 `resolve_container_ids.sql`:批次將 CONTAINERNAME 轉換為 CONTAINERID
|
||||
|
||||
## 2. 後端 Service
|
||||
|
||||
- [x] 2.1 新增 `src/mes_dashboard/services/material_trace_service.py`,包含 `forward_query(mode, values, workcenter_groups, page, per_page)` 函式
|
||||
- [x] 2.2 在 `forward_query` 中實作 LOT ID 模式:呼叫 `resolve_container_ids.sql` 將 CONTAINERNAME 批次轉換為 CONTAINERID,記錄未解析的名稱到 `meta.unresolved`
|
||||
- [x] 2.3 在 `forward_query` 中實作工單模式:直接以 PJ_WORKORDER 查詢
|
||||
- [x] 2.4 實作 `reverse_query(values, workcenter_groups, page, per_page)` 函式,以 MATERIALLOTNAME 查詢,檢查結果是否超過 10,000 筆並設定 `meta.truncated`
|
||||
- [x] 2.5 實作共用 `_enrich_workcenter_group(df)` 函式:使用 `filter_cache.get_workcenter_mapping()` 對 DataFrame 添加 WORKCENTER_GROUP 欄位
|
||||
- [x] 2.6 實作共用 `_apply_workcenter_group_filter(workcenter_groups)` 函式:透過 `filter_cache.get_workcenter_mapping()` 將站群組名稱解析為 WORKCENTERNAME 清單,供 SQL WHERE 使用
|
||||
- [x] 2.7 實作 `export_csv(mode, values, workcenter_groups)` 函式,結果上限 50,000 筆,回傳 UTF-8 BOM CSV
|
||||
|
||||
## 3. 後端 Route
|
||||
|
||||
- [x] 3.1 新增 `src/mes_dashboard/routes/material_trace_routes.py`,建立 `material_trace_bp` Blueprint,prefix `/api/material-trace`
|
||||
- [x] 3.2 實作 `POST /query` 端點:驗證 mode(lot/workorder/material_lot)、values 非空、筆數上限(正向 200 / 反向 50);根據 mode 呼叫 `forward_query` 或 `reverse_query`;回傳分頁結果
|
||||
- [x] 3.3 實作 `POST /export` 端點:與 query 相同參數驗證,呼叫 `export_csv`,回傳 CSV response
|
||||
- [x] 3.4 實作 `GET /filter-options` 端點:回傳 `filter_cache.get_workcenter_groups()` 供前端站群組下拉選單使用
|
||||
- [x] 3.5 加入 rate limiting:query 30/60s,export 10/60s
|
||||
- [x] 3.6 在 `routes/__init__.py` 註冊 `material_trace_bp`
|
||||
|
||||
## 4. 前端頁面基礎
|
||||
|
||||
- [x] 4.1 新增 `frontend/material-trace.html` Vite entry point
|
||||
- [x] 4.2 新增 `frontend/src/material-trace/main.js` 初始化 Vue app
|
||||
- [x] 4.3 新增 `frontend/src/material-trace/App.vue` 主元件:包含 queryMode(forward/reverse)、forwardInputType(lot/workorder)、inputText、workcenterGroups、results、pagination、loading、error 等 reactive state
|
||||
- [x] 4.4 新增 `frontend/src/material-trace/style.css`,沿用 reject-history 的表格/banner 樣式基礎
|
||||
- [x] 4.5 在 `vite.config.js` 加入 `material-trace` entry
|
||||
- [x] 4.6 在 Flask 後端 `templates/` 新增頁面路由(或 Jinja template),確認頁面可存取
|
||||
|
||||
## 5. 前端元件
|
||||
|
||||
- [x] 5.1 實作查詢模式切換 tab(正向查詢 / 反向查詢),切換時清空輸入和結果
|
||||
- [x] 5.2 實作正向模式的輸入類型選擇(LOT ID / 工單),切換時清空輸入
|
||||
- [x] 5.3 實作多筆輸入 textarea,使用 `parseMultiLineInput()` 解析,顯示已輸入筆數
|
||||
- [x] 5.4 實作前端輸入筆數驗證(正向 200 筆 / 反向 50 筆),超過時顯示 error banner 並阻止查詢
|
||||
- [x] 5.5 實作站群組多選篩選下拉(options 從 `/api/material-trace/filter-options` 載入)
|
||||
- [x] 5.6 實作 `executePrimaryQuery()` 函式:呼叫 `/api/material-trace/query` API,處理結果、分頁、error、unresolved、truncated 警告
|
||||
- [x] 5.7 實作結果表格,含 13 個欄位(CONTAINERNAME、PJ_WORKORDER、WORKCENTER_GROUP、WORKCENTERNAME、MATERIALPARTNAME、MATERIALLOTNAME、VENDORLOTNUMBER、QTYREQUIRED、QTYCONSUMED、EQUIPMENTNAME、TXNDATE、PRIMARY_CATEGORY、SECONDARY_CATEGORY)
|
||||
- [x] 5.8 實作分頁控制(上一頁/下一頁/頁碼顯示),server-side 分頁
|
||||
- [x] 5.9 實作匯出 CSV 按鈕,呼叫 `/api/material-trace/export`,無結果時 disabled
|
||||
- [x] 5.10 實作 loading overlay、error banner、warning banner(unresolved LOT / 結果截斷)
|
||||
|
||||
## 6. 導覽整合
|
||||
|
||||
- [x] 6.1 在 sidebar/drawer 導覽列新增「原物料追溯查詢」頁面入口
|
||||
|
||||
## 7. 測試
|
||||
|
||||
- [x] 7.1 新增 `tests/test_material_trace_service.py`:測試正向 LOT 模式查詢(mock Oracle 回傳),驗證 CONTAINERID resolve + 結果 enrichment
|
||||
- [x] 7.2 測試正向工單模式查詢,驗證 PJ_WORKORDER 直接查詢
|
||||
- [x] 7.3 測試反向查詢,驗證結果上限 10,000 筆截斷邏輯
|
||||
- [x] 7.4 測試站群組篩選:mock `get_workcenter_mapping()` 回傳 mapping,驗證 WORKCENTERNAME IN 過濾
|
||||
- [x] 7.5 測試未解析 LOT ID 的 `meta.unresolved` 回傳
|
||||
- [x] 7.6 新增 `tests/test_material_trace_routes.py`:測試輸入驗證(mode 無效、values 空、超過筆數上限)回傳 HTTP 400
|
||||
- [x] 7.7 測試 query 端點回傳正確分頁結構
|
||||
- [x] 7.8 測試 export 端點回傳 CSV content-type 和 UTF-8 BOM
|
||||
120
openspec/specs/material-trace-api/spec.md
Normal file
120
openspec/specs/material-trace-api/spec.md
Normal file
@@ -0,0 +1,120 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Material Trace API SHALL provide forward query endpoint
|
||||
The API SHALL accept LOT IDs or work order numbers and return corresponding material consumption records from `DW_MES_LOTMATERIALSHISTORY`.
|
||||
|
||||
#### Scenario: Forward query by LOT ID
|
||||
- **WHEN** `POST /api/material-trace/query` is called with `mode: "lot"` and `values: ["GA25060001-A01", "GA25060502"]`
|
||||
- **THEN** the API SHALL resolve LOT names to CONTAINERIDs via `DW_MES_CONTAINER`
|
||||
- **THEN** the API SHALL return material consumption records matching those CONTAINERIDs
|
||||
- **THEN** each record SHALL include CONTAINERID, CONTAINERNAME, PJ_WORKORDER, WORKCENTERNAME, WORKCENTER_GROUP, MATERIALPARTNAME, MATERIALLOTNAME, VENDORLOTNUMBER, QTYREQUIRED, QTYCONSUMED, EQUIPMENTNAME, TXNDATE, PRIMARY_CATEGORY, SECONDARY_CATEGORY
|
||||
|
||||
#### Scenario: Forward query by work order
|
||||
- **WHEN** `POST /api/material-trace/query` is called with `mode: "workorder"` and `values: ["WO-2025-001", "WO-2025-002"]`
|
||||
- **THEN** the API SHALL query `DW_MES_LOTMATERIALSHISTORY` using `PJ_WORKORDER` index directly
|
||||
- **THEN** the response format SHALL be identical to LOT ID mode
|
||||
|
||||
#### Scenario: Forward query with workcenter group filter
|
||||
- **WHEN** `POST /api/material-trace/query` includes `workcenter_groups: ["焊接_DB"]`
|
||||
- **THEN** the API SHALL resolve group names to WORKCENTERNAME list via `filter_cache.get_workcenter_mapping()`
|
||||
- **THEN** the SQL query SHALL include `AND WORKCENTERNAME IN (...)` filter
|
||||
- **THEN** results SHALL only contain records from workcenters belonging to the selected groups
|
||||
|
||||
#### Scenario: Forward query input limit
|
||||
- **WHEN** `POST /api/material-trace/query` with `mode: "lot"` or `mode: "workorder"` contains more than 200 values
|
||||
- **THEN** the API SHALL return HTTP 400 with error message indicating the 200-value limit
|
||||
|
||||
### Requirement: Material Trace API SHALL provide reverse query endpoint
|
||||
The API SHALL accept material lot names and return LOTs that consumed those materials.
|
||||
|
||||
#### Scenario: Reverse query by material lot name
|
||||
- **WHEN** `POST /api/material-trace/query` is called with `mode: "material_lot"` and `values: ["WIRE-LOT-20250101-A"]`
|
||||
- **THEN** the API SHALL query `DW_MES_LOTMATERIALSHISTORY` using `MATERIALLOTNAME` index
|
||||
- **THEN** each record SHALL include the same fields as forward query results
|
||||
|
||||
#### Scenario: Reverse query with workcenter group filter
|
||||
- **WHEN** reverse query includes `workcenter_groups` parameter
|
||||
- **THEN** the same workcenter group filtering logic as forward query SHALL apply
|
||||
|
||||
#### Scenario: Reverse query input limit
|
||||
- **WHEN** `POST /api/material-trace/query` with `mode: "material_lot"` contains more than 50 values
|
||||
- **THEN** the API SHALL return HTTP 400 with error message indicating the 50-value limit
|
||||
|
||||
#### Scenario: Reverse query result limit
|
||||
- **WHEN** reverse query results exceed 10,000 rows
|
||||
- **THEN** the API SHALL return exactly 10,000 rows
|
||||
- **THEN** the response `meta` SHALL include `truncated: true` and `max_rows: 10000`
|
||||
|
||||
### Requirement: Material Trace API SHALL validate query parameters
|
||||
The API SHALL validate input parameters before executing database queries.
|
||||
|
||||
#### Scenario: Missing required fields
|
||||
- **WHEN** `POST /api/material-trace/query` is called without `mode` or `values`
|
||||
- **THEN** the API SHALL return HTTP 400 with descriptive validation error
|
||||
|
||||
#### Scenario: Invalid mode
|
||||
- **WHEN** `mode` is not one of `lot`, `workorder`, `material_lot`
|
||||
- **THEN** the API SHALL return HTTP 400
|
||||
|
||||
#### Scenario: Empty values
|
||||
- **WHEN** `values` is an empty array or all values are blank after trimming
|
||||
- **THEN** the API SHALL return HTTP 400 with error message "請輸入至少一筆查詢條件"
|
||||
|
||||
#### Scenario: Unresolvable LOT IDs
|
||||
- **WHEN** some LOT names cannot be resolved to CONTAINERIDs
|
||||
- **THEN** the API SHALL proceed with the resolved subset
|
||||
- **THEN** the response `meta` SHALL include `unresolved` array listing unresolvable LOT names
|
||||
|
||||
### Requirement: Material Trace API SHALL support paginated results
|
||||
The API SHALL support server-side pagination for query results.
|
||||
|
||||
#### Scenario: Pagination parameters
|
||||
- **WHEN** `POST /api/material-trace/query` includes `page` and `per_page`
|
||||
- **THEN** results SHALL be paginated accordingly
|
||||
- **THEN** response SHALL include `pagination: { page, per_page, total, total_pages }`
|
||||
|
||||
#### Scenario: Default pagination
|
||||
- **WHEN** `page` or `per_page` is not provided
|
||||
- **THEN** `page` SHALL default to 1
|
||||
- **THEN** `per_page` SHALL default to 50
|
||||
|
||||
#### Scenario: Per-page cap
|
||||
- **WHEN** `per_page` exceeds 200
|
||||
- **THEN** `per_page` SHALL be capped at 200
|
||||
|
||||
### Requirement: Material Trace API SHALL provide CSV export endpoint
|
||||
The API SHALL provide CSV export using the same query parameters as the query endpoint.
|
||||
|
||||
#### Scenario: Export request
|
||||
- **WHEN** `POST /api/material-trace/export` is called with the same parameters as query
|
||||
- **THEN** the response SHALL be a CSV file with UTF-8 BOM encoding
|
||||
- **THEN** CSV headers SHALL be in Chinese
|
||||
- **THEN** all matching records SHALL be included (no pagination, subject to result limits)
|
||||
|
||||
#### Scenario: Export result limit
|
||||
- **WHEN** export results exceed 50,000 rows
|
||||
- **THEN** the export SHALL be truncated at 50,000 rows
|
||||
- **THEN** a warning header SHALL indicate truncation
|
||||
|
||||
### Requirement: Material Trace API SHALL enrich results with workcenter group
|
||||
The API SHALL add WORKCENTER_GROUP to each result row based on `filter_cache.get_workcenter_mapping()`.
|
||||
|
||||
#### Scenario: Workcenter group enrichment
|
||||
- **WHEN** query results are returned
|
||||
- **THEN** each row SHALL include a `WORKCENTER_GROUP` field
|
||||
- **THEN** the value SHALL be resolved from `filter_cache.get_workcenter_mapping()` using the row's `WORKCENTERNAME`
|
||||
|
||||
#### Scenario: Unknown workcenter
|
||||
- **WHEN** a row's WORKCENTERNAME has no mapping in the workcenter cache
|
||||
- **THEN** `WORKCENTER_GROUP` SHALL be empty string
|
||||
|
||||
### Requirement: Material Trace API SHALL apply rate limiting
|
||||
The API SHALL rate-limit query and export endpoints to protect Oracle resources.
|
||||
|
||||
#### Scenario: Query rate limit
|
||||
- **WHEN** `/api/material-trace/query` receives excessive requests
|
||||
- **THEN** requests beyond 30 per 60 seconds SHALL be rejected with HTTP 429
|
||||
|
||||
#### Scenario: Export rate limit
|
||||
- **WHEN** `/api/material-trace/export` receives excessive requests
|
||||
- **THEN** requests beyond 10 per 60 seconds SHALL be rejected with HTTP 429
|
||||
120
openspec/specs/material-trace-page/spec.md
Normal file
120
openspec/specs/material-trace-page/spec.md
Normal file
@@ -0,0 +1,120 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Material Trace page SHALL provide bidirectional query mode switching
|
||||
The page SHALL provide two query directions with explicit tab switching.
|
||||
|
||||
#### Scenario: Forward query mode (default)
|
||||
- **WHEN** the page loads
|
||||
- **THEN** "正向查詢:LOT/工單 → 原物料" tab SHALL be active by default
|
||||
- **THEN** the input area SHALL show input type selector (LOT ID / 工單) and a multi-line text input
|
||||
|
||||
#### Scenario: Reverse query mode
|
||||
- **WHEN** user clicks "反向查詢:原物料 → LOT" tab
|
||||
- **THEN** the input area SHALL switch to material lot name multi-line input
|
||||
- **THEN** query results and pagination SHALL be cleared
|
||||
|
||||
#### Scenario: Forward input type switching
|
||||
- **WHEN** forward mode is active
|
||||
- **THEN** user SHALL be able to switch between "LOT ID" and "工單" input types
|
||||
- **THEN** switching input type SHALL clear the input field and results
|
||||
|
||||
### Requirement: Material Trace page SHALL accept multi-line input
|
||||
The page SHALL accept multiple values separated by newlines or commas.
|
||||
|
||||
#### Scenario: Multi-line input parsing
|
||||
- **WHEN** user enters values separated by newlines, commas, or mixed delimiters
|
||||
- **THEN** the system SHALL parse and deduplicate values using the same logic as `parseMultiLineInput()`
|
||||
|
||||
#### Scenario: Input count display
|
||||
- **WHEN** user enters values
|
||||
- **THEN** the input area SHALL display the parsed count (e.g., "已輸入 5 筆")
|
||||
|
||||
#### Scenario: Forward input limit feedback
|
||||
- **WHEN** user enters more than 200 values in forward mode
|
||||
- **THEN** the page SHALL display an error message "正向查詢上限 200 筆"
|
||||
- **THEN** the query SHALL NOT be sent
|
||||
|
||||
#### Scenario: Reverse input limit feedback
|
||||
- **WHEN** user enters more than 50 values in reverse mode
|
||||
- **THEN** the page SHALL display an error message "反向查詢上限 50 筆"
|
||||
- **THEN** the query SHALL NOT be sent
|
||||
|
||||
### Requirement: Material Trace page SHALL provide workcenter group filter
|
||||
The page SHALL allow filtering results by workcenter group.
|
||||
|
||||
#### Scenario: Workcenter group options
|
||||
- **WHEN** the page loads
|
||||
- **THEN** workcenter group filter SHALL be populated from `filter_cache.get_workcenter_groups()`
|
||||
- **THEN** the filter SHALL support multi-select
|
||||
- **THEN** default SHALL be "全部站點" (no filter)
|
||||
|
||||
#### Scenario: Filter applied to query
|
||||
- **WHEN** user selects workcenter groups and clicks "查詢"
|
||||
- **THEN** the selected groups SHALL be sent as `workcenter_groups` parameter to the API
|
||||
- **THEN** results SHALL only contain records from workcenters in the selected groups
|
||||
|
||||
### Requirement: Material Trace page SHALL display query results in a paginated table
|
||||
The page SHALL display results in a sortable, paginated detail table.
|
||||
|
||||
#### Scenario: Result table columns
|
||||
- **WHEN** query results are loaded
|
||||
- **THEN** the table SHALL display: LOT ID (CONTAINERNAME), 工單 (PJ_WORKORDER), 站群組 (WORKCENTER_GROUP), 站點 (WORKCENTERNAME), 料號 (MATERIALPARTNAME), 物料批號 (MATERIALLOTNAME), 供應商批號 (VENDORLOTNUMBER), 應領量 (QTYREQUIRED), 實際消耗 (QTYCONSUMED), 機台 (EQUIPMENTNAME), 交易日期 (TXNDATE), 主分類 (PRIMARY_CATEGORY), 副分類 (SECONDARY_CATEGORY)
|
||||
|
||||
#### Scenario: Pagination controls
|
||||
- **WHEN** results exceed per-page size
|
||||
- **THEN** pagination controls SHALL display "上一頁" / "下一頁" buttons and page info in Chinese
|
||||
- **THEN** default per-page size SHALL be 50
|
||||
|
||||
#### Scenario: Empty results
|
||||
- **WHEN** query returns no matching records
|
||||
- **THEN** the table area SHALL display "查無資料" message
|
||||
|
||||
#### Scenario: Unresolved LOT IDs warning
|
||||
- **WHEN** the API response contains `meta.unresolved` array
|
||||
- **THEN** a warning banner SHALL display listing the unresolvable LOT names
|
||||
|
||||
#### Scenario: Result truncation warning
|
||||
- **WHEN** the API response contains `meta.truncated: true`
|
||||
- **THEN** an amber warning banner SHALL display "查詢結果超過 10,000 筆上限,請縮小查詢範圍"
|
||||
|
||||
### Requirement: Material Trace page SHALL support CSV export
|
||||
The page SHALL allow exporting current query results to CSV.
|
||||
|
||||
#### Scenario: Export button
|
||||
- **WHEN** query results are loaded
|
||||
- **THEN** an "匯出 CSV" button SHALL be visible
|
||||
- **WHEN** user clicks "匯出 CSV"
|
||||
- **THEN** the export request SHALL use the same query parameters as the current query
|
||||
|
||||
#### Scenario: Export disabled without results
|
||||
- **WHEN** no query has been executed or results are empty
|
||||
- **THEN** the "匯出 CSV" button SHALL be disabled
|
||||
|
||||
### Requirement: Material Trace page SHALL provide loading and error states
|
||||
The page SHALL provide clear feedback during loading and error conditions.
|
||||
|
||||
#### Scenario: Loading state
|
||||
- **WHEN** a query is in progress
|
||||
- **THEN** a loading indicator SHALL be visible
|
||||
- **THEN** the query button SHALL be disabled
|
||||
|
||||
#### Scenario: API error
|
||||
- **WHEN** the API returns an error
|
||||
- **THEN** a red error banner SHALL display the error message
|
||||
|
||||
#### Scenario: Error cleared on new query
|
||||
- **WHEN** user initiates a new query
|
||||
- **THEN** previous error and warning banners SHALL be cleared
|
||||
|
||||
### Requirement: Material Trace page SHALL use Chinese labels
|
||||
The page SHALL display all UI text in Traditional Chinese consistent with the rest of the application.
|
||||
|
||||
#### Scenario: Page title
|
||||
- **WHEN** the page is rendered
|
||||
- **THEN** the page title SHALL be "原物料追溯查詢"
|
||||
|
||||
#### Scenario: Button labels
|
||||
- **WHEN** the page is rendered
|
||||
- **THEN** the query button SHALL display "查詢"
|
||||
- **THEN** the export button SHALL display "匯出 CSV"
|
||||
- **THEN** the clear button SHALL display "清除"
|
||||
Reference in New Issue
Block a user