diff --git a/data/page_status.json b/data/page_status.json
index f1c3021..dc37fa7 100644
--- a/data/page_status.json
+++ b/data/page_status.json
@@ -102,7 +102,7 @@
{
"route": "/admin/pages",
"name": "頁面管理",
- "status": "dev",
+ "status": "released",
"drawer_id": "dev-tools",
"order": 1
},
diff --git a/docs/migration/full-modernization-architecture-blueprint/quality_gate_report.json b/docs/migration/full-modernization-architecture-blueprint/quality_gate_report.json
index 52fd667..c072630 100644
--- a/docs/migration/full-modernization-architecture-blueprint/quality_gate_report.json
+++ b/docs/migration/full-modernization-architecture-blueprint/quality_gate_report.json
@@ -2,8 +2,7 @@
"mode": "block",
"errors": [],
"warnings": [
- "/excel-query uses shell tokens without fallback ['--portal-brand-end', '--portal-brand-start', '--portal-shadow-panel'] in frontend/src/excel-query/style.css with approved exception",
- "/query-tool uses shell tokens without fallback ['--portal-brand-end', '--portal-brand-start', '--portal-shadow-panel'] in frontend/src/query-tool/style.css with approved exception"
+ "/excel-query uses shell tokens without fallback ['--portal-shadow-panel'] in frontend/src/excel-query/style.css with approved exception"
],
"info": [],
"passed": true
diff --git a/docs/reject_history_performance.md b/docs/reject_history_performance.md
new file mode 100644
index 0000000..97d4820
--- /dev/null
+++ b/docs/reject_history_performance.md
@@ -0,0 +1,44 @@
+# Reject 歷史績效表設計說明
+
+## 目標
+使用 `DW_MES_LOTREJECTHISTORY` 為主,輔以其他維度表,建立可直接用於報表的 `reject` 歷史績效表(按日彙總),解決原始資料直接查詢時的績效與一致性問題。
+
+## 使用資料表
+- `DWH.DW_MES_LOTREJECTHISTORY`: 不良/報廢事實表(主來源)
+- `DWH.DW_MES_CONTAINER`: 補齊 `PJ_TYPE`、`PRODUCTLINENAME`、`MFGORDERNAME`
+- `DWH.DW_MES_SPEC_WORKCENTER_V`: 對應 `WORKCENTER_GROUP` 與排序欄位
+
+## 資料評估重點(2026-02-13,近 30 天樣本)
+- `DW_MES_LOTREJECTHISTORY` 共 `230,074` 筆;`HISTORYMAINLINEID` 僅 `75,683` 個。
+- `HISTORYMAINLINEID` 多筆情況明顯(`30,784` 個主事件,平均每主事件 `6.02` 筆),代表同主事件會拆成多個 `LOSSREASONNAME`。
+- 若直接加總 `MOVEINQTY`,分母會被重複計算。近 30 天樣本中:
+ - `NAIVE_MOVEIN = 44,836,693,831`
+ - `DEDUP_MOVEIN = 35,658,750,247`
+ - 膨脹比 `1.2574`(約高估 25.74%)
+- 指標定義依業務規則分開處理:
+ - `REJECT_TOTAL_QTY = REJECTQTY + STANDBYQTY + QTYTOPROCESS + INPROCESSQTY + PROCESSEDQTY`(扣帳報廢)
+ - `DEFECT_QTY = DEFECTQTY`(不扣帳報廢)
+- `DW_MES_SPEC_WORKCENTER_V` 若直接以 `WORK_CENTER` join 會放大筆數;需先彙整為唯一 `WORK_CENTER -> GROUP/SEQUENCE` 對照表再 join。
+
+## 績效表欄位與計算邏輯
+- 粒度:`日 + 工站群組 + 工站 + 站點規格 + 設備 + 產品維度 + 不良原因`
+- 核心指標:
+ - `REJECT_EVENT_ROWS`: 原始 reject 紀錄筆數
+ - `AFFECTED_LOT_COUNT`: 受影響 lot 數(distinct `CONTAINERID`)
+ - `MOVEIN_QTY`: 以 `HISTORYMAINLINEID` 去重後的投入量
+ - `REJECT_QTY`: 原始 `REJECTQTY` 加總(五欄之一)
+ - `REJECT_TOTAL_QTY`: 五個 reject 相關欄位加總(扣帳報廢)
+ - `DEFECT_QTY`: `DEFECTQTY` 加總(不扣帳報廢)
+ - `REJECT_RATE_PCT = REJECT_TOTAL_QTY / MOVEIN_QTY * 100`
+ - `DEFECT_RATE_PCT = DEFECT_QTY / MOVEIN_QTY * 100`
+ - `REJECT_SHARE_PCT = REJECT_TOTAL_QTY / (REJECT_TOTAL_QTY + DEFECT_QTY) * 100`
+
+## 交付檔案
+- 建表 + 刷新 SQL:`docs/reject_history_performance.sql`
+- 可被應用層直接載入的查詢 SQL:`src/mes_dashboard/sql/reject_history/performance_daily.sql`
+
+## 建議排程
+- 每日跑前一日增量:
+ - `:start_date = TRUNC(SYSDATE - 1)`
+ - `:end_date = TRUNC(SYSDATE - 1)`
+- 每月第一天補跑前 31 天,避免補數漏失。
diff --git a/docs/reject_history_performance.sql b/docs/reject_history_performance.sql
new file mode 100644
index 0000000..220b2bd
--- /dev/null
+++ b/docs/reject_history_performance.sql
@@ -0,0 +1,242 @@
+/*
+Reject 歷史績效表建置腳本
+目的:
+ - 以 DW_MES_LOTREJECTHISTORY 為主,建立可直接報表化的日彙總績效表
+ - 補齊產品/工站維度,並區分扣帳報廢與不扣帳報廢
+*/
+
+/* ============================================================
+ 1) 建表 (執行一次)
+ ============================================================ */
+
+CREATE TABLE DWH.DW_PJ_REJECT_HISTORY_PERF_D (
+ TXN_DAY DATE NOT NULL,
+ TXN_MONTH VARCHAR2(7) NOT NULL,
+ WORKCENTER_GROUP VARCHAR2(40) NOT NULL,
+ WORKCENTERSEQUENCE_GROUP NUMBER(10) NOT NULL,
+ WORKCENTERNAME VARCHAR2(40) NOT NULL,
+ SPECNAME VARCHAR2(40) NOT NULL,
+ EQUIPMENTNAME VARCHAR2(255) NOT NULL,
+ PRIMARY_EQUIPMENTNAME VARCHAR2(40) NOT NULL,
+ PRODUCTLINENAME VARCHAR2(40) NOT NULL,
+ PJ_TYPE VARCHAR2(40) NOT NULL,
+ LOSSREASONNAME VARCHAR2(40) NOT NULL,
+ REJECTCATEGORYNAME VARCHAR2(40) NOT NULL,
+ REJECT_EVENT_ROWS NUMBER NOT NULL,
+ AFFECTED_LOT_COUNT NUMBER NOT NULL,
+ AFFECTED_WORKORDER_COUNT NUMBER NOT NULL,
+ MOVEIN_QTY NUMBER NOT NULL,
+ REJECT_QTY NUMBER NOT NULL,
+ REJECT_TOTAL_QTY NUMBER NOT NULL,
+ DEFECT_QTY NUMBER NOT NULL,
+ STANDBY_QTY NUMBER NOT NULL,
+ QTYTOPROCESS_QTY NUMBER NOT NULL,
+ INPROCESS_QTY NUMBER NOT NULL,
+ PROCESSED_QTY NUMBER NOT NULL,
+ REJECT_RATE_PCT NUMBER(18, 4) NOT NULL,
+ DEFECT_RATE_PCT NUMBER(18, 4) NOT NULL,
+ REJECT_SHARE_PCT NUMBER(18, 4) NOT NULL,
+ LAST_REFRESH_TS DATE NOT NULL
+);
+
+CREATE INDEX DWH.IDX_RJH_PERF_D_01 ON DWH.DW_PJ_REJECT_HISTORY_PERF_D (TXN_DAY);
+CREATE INDEX DWH.IDX_RJH_PERF_D_02 ON DWH.DW_PJ_REJECT_HISTORY_PERF_D (WORKCENTER_GROUP, TXN_DAY);
+CREATE INDEX DWH.IDX_RJH_PERF_D_03 ON DWH.DW_PJ_REJECT_HISTORY_PERF_D (PRIMARY_EQUIPMENTNAME, TXN_DAY);
+CREATE INDEX DWH.IDX_RJH_PERF_D_04 ON DWH.DW_PJ_REJECT_HISTORY_PERF_D (LOSSREASONNAME, TXN_DAY);
+
+
+/* ============================================================
+ 2) 區間刷新 (可每日排程)
+ 綁定參數:
+ :start_date (YYYY-MM-DD)
+ :end_date (YYYY-MM-DD)
+ ============================================================ */
+
+DELETE FROM DWH.DW_PJ_REJECT_HISTORY_PERF_D
+WHERE TXN_DAY >= TO_DATE(:start_date, 'YYYY-MM-DD')
+ AND TXN_DAY < TO_DATE(:end_date, 'YYYY-MM-DD') + 1;
+
+INSERT /*+ APPEND */ INTO DWH.DW_PJ_REJECT_HISTORY_PERF_D (
+ TXN_DAY,
+ TXN_MONTH,
+ WORKCENTER_GROUP,
+ WORKCENTERSEQUENCE_GROUP,
+ WORKCENTERNAME,
+ SPECNAME,
+ EQUIPMENTNAME,
+ PRIMARY_EQUIPMENTNAME,
+ PRODUCTLINENAME,
+ PJ_TYPE,
+ LOSSREASONNAME,
+ REJECTCATEGORYNAME,
+ REJECT_EVENT_ROWS,
+ AFFECTED_LOT_COUNT,
+ AFFECTED_WORKORDER_COUNT,
+ MOVEIN_QTY,
+ REJECT_QTY,
+ REJECT_TOTAL_QTY,
+ DEFECT_QTY,
+ STANDBY_QTY,
+ QTYTOPROCESS_QTY,
+ INPROCESS_QTY,
+ PROCESSED_QTY,
+ REJECT_RATE_PCT,
+ DEFECT_RATE_PCT,
+ REJECT_SHARE_PCT,
+ LAST_REFRESH_TS
+)
+WITH workcenter_map AS (
+ SELECT
+ WORK_CENTER,
+ MIN(WORK_CENTER_GROUP) KEEP (
+ DENSE_RANK FIRST ORDER BY WORKCENTERSEQUENCE_GROUP
+ ) AS WORKCENTER_GROUP,
+ MIN(WORKCENTERSEQUENCE_GROUP) AS WORKCENTERSEQUENCE_GROUP
+ FROM DWH.DW_MES_SPEC_WORKCENTER_V
+ WHERE WORK_CENTER IS NOT NULL
+ GROUP BY WORK_CENTER
+),
+reject_raw AS (
+ SELECT
+ TRUNC(r.TXNDATE) AS TXN_DAY,
+ TO_CHAR(TRUNC(r.TXNDATE), 'YYYY-MM') AS TXN_MONTH,
+ r.CONTAINERID,
+ NVL(TRIM(r.PJ_WORKORDER), TRIM(c.MFGORDERNAME)) AS PJ_WORKORDER,
+ NVL(TRIM(c.PJ_TYPE), '(NA)') AS PJ_TYPE,
+ NVL(TRIM(c.PRODUCTLINENAME), '(NA)') AS PRODUCTLINENAME,
+ NVL(TRIM(r.WORKCENTERNAME), '(NA)') AS WORKCENTERNAME,
+ NVL(TRIM(wm.WORKCENTER_GROUP), NVL(TRIM(r.WORKCENTERNAME), '(NA)')) AS WORKCENTER_GROUP,
+ NVL(wm.WORKCENTERSEQUENCE_GROUP, 999) AS WORKCENTERSEQUENCE_GROUP,
+ NVL(TRIM(r.SPECNAME), '(NA)') AS SPECNAME,
+ NVL(TRIM(r.EQUIPMENTNAME), '(NA)') AS EQUIPMENTNAME,
+ NVL(
+ TRIM(REGEXP_SUBSTR(r.EQUIPMENTNAME, '[^,]+', 1, 1)),
+ NVL(TRIM(r.EQUIPMENTNAME), '(NA)')
+ ) AS PRIMARY_EQUIPMENTNAME,
+ NVL(TRIM(r.LOSSREASONNAME), '(未填寫)') AS LOSSREASONNAME,
+ NVL(TRIM(r.REJECTCATEGORYNAME), '(未填寫)') AS REJECTCATEGORYNAME,
+ NVL(r.MOVEINQTY, 0) AS MOVEINQTY,
+ NVL(r.REJECTQTY, 0) AS REJECT_QTY,
+ NVL(r.STANDBYQTY, 0) AS STANDBY_QTY,
+ NVL(r.QTYTOPROCESS, 0) AS QTYTOPROCESS_QTY,
+ NVL(r.INPROCESSQTY, 0) AS INPROCESS_QTY,
+ NVL(r.PROCESSEDQTY, 0) AS PROCESSED_QTY,
+ NVL(r.REJECTQTY, 0)
+ + NVL(r.STANDBYQTY, 0)
+ + NVL(r.QTYTOPROCESS, 0)
+ + NVL(r.INPROCESSQTY, 0)
+ + NVL(r.PROCESSEDQTY, 0) AS REJECT_TOTAL_QTY,
+ NVL(r.DEFECTQTY, 0) AS DEFECT_QTY,
+ ROW_NUMBER() OVER (
+ PARTITION BY NVL(
+ TRIM(r.HISTORYMAINLINEID),
+ TRIM(r.CONTAINERID) || ':' || TO_CHAR(r.TXNDATE, 'YYYYMMDDHH24MISS') || ':' || NVL(TRIM(r.SPECID), '-')
+ )
+ ORDER BY NVL(TRIM(r.LOSSREASONNAME), ' ')
+ ) AS EVENT_RN
+ FROM DWH.DW_MES_LOTREJECTHISTORY r
+ LEFT JOIN DWH.DW_MES_CONTAINER c
+ ON c.CONTAINERID = r.CONTAINERID
+ LEFT JOIN workcenter_map wm
+ ON wm.WORK_CENTER = r.WORKCENTERNAME
+ WHERE r.TXNDATE >= TO_DATE(:start_date, 'YYYY-MM-DD')
+ AND r.TXNDATE < TO_DATE(:end_date, 'YYYY-MM-DD') + 1
+),
+daily_agg AS (
+ SELECT
+ TXN_DAY,
+ TXN_MONTH,
+ WORKCENTER_GROUP,
+ WORKCENTERSEQUENCE_GROUP,
+ WORKCENTERNAME,
+ SPECNAME,
+ EQUIPMENTNAME,
+ PRIMARY_EQUIPMENTNAME,
+ PRODUCTLINENAME,
+ PJ_TYPE,
+ LOSSREASONNAME,
+ REJECTCATEGORYNAME,
+ COUNT(*) AS REJECT_EVENT_ROWS,
+ COUNT(DISTINCT CONTAINERID) AS AFFECTED_LOT_COUNT,
+ COUNT(DISTINCT PJ_WORKORDER) AS AFFECTED_WORKORDER_COUNT,
+ SUM(CASE WHEN EVENT_RN = 1 THEN MOVEINQTY ELSE 0 END) AS MOVEIN_QTY,
+ SUM(REJECT_QTY) AS REJECT_QTY,
+ SUM(REJECT_TOTAL_QTY) AS REJECT_TOTAL_QTY,
+ SUM(DEFECT_QTY) AS DEFECT_QTY,
+ SUM(STANDBY_QTY) AS STANDBY_QTY,
+ SUM(QTYTOPROCESS_QTY) AS QTYTOPROCESS_QTY,
+ SUM(INPROCESS_QTY) AS INPROCESS_QTY,
+ SUM(PROCESSED_QTY) AS PROCESSED_QTY
+ FROM reject_raw
+ GROUP BY
+ TXN_DAY,
+ TXN_MONTH,
+ WORKCENTER_GROUP,
+ WORKCENTERSEQUENCE_GROUP,
+ WORKCENTERNAME,
+ SPECNAME,
+ EQUIPMENTNAME,
+ PRIMARY_EQUIPMENTNAME,
+ PRODUCTLINENAME,
+ PJ_TYPE,
+ LOSSREASONNAME,
+ REJECTCATEGORYNAME
+)
+SELECT
+ TXN_DAY,
+ TXN_MONTH,
+ WORKCENTER_GROUP,
+ WORKCENTERSEQUENCE_GROUP,
+ WORKCENTERNAME,
+ SPECNAME,
+ EQUIPMENTNAME,
+ PRIMARY_EQUIPMENTNAME,
+ PRODUCTLINENAME,
+ PJ_TYPE,
+ LOSSREASONNAME,
+ REJECTCATEGORYNAME,
+ REJECT_EVENT_ROWS,
+ AFFECTED_LOT_COUNT,
+ AFFECTED_WORKORDER_COUNT,
+ MOVEIN_QTY,
+ REJECT_QTY,
+ REJECT_TOTAL_QTY,
+ DEFECT_QTY,
+ STANDBY_QTY,
+ QTYTOPROCESS_QTY,
+ INPROCESS_QTY,
+ PROCESSED_QTY,
+ CASE
+ WHEN MOVEIN_QTY = 0 THEN 0
+ ELSE ROUND(REJECT_TOTAL_QTY * 100 / MOVEIN_QTY, 4)
+ END AS REJECT_RATE_PCT,
+ CASE
+ WHEN MOVEIN_QTY = 0 THEN 0
+ ELSE ROUND(DEFECT_QTY * 100 / MOVEIN_QTY, 4)
+ END AS DEFECT_RATE_PCT,
+ CASE
+ WHEN (REJECT_TOTAL_QTY + DEFECT_QTY) = 0 THEN 0
+ ELSE ROUND(REJECT_TOTAL_QTY * 100 / (REJECT_TOTAL_QTY + DEFECT_QTY), 4)
+ END AS REJECT_SHARE_PCT,
+ SYSDATE AS LAST_REFRESH_TS
+FROM daily_agg;
+
+COMMIT;
+
+
+/* ============================================================
+ 3) 快速驗證查詢
+ ============================================================ */
+
+SELECT
+ TXN_DAY,
+ WORKCENTER_GROUP,
+ SUM(MOVEIN_QTY) AS MOVEIN_QTY,
+ SUM(REJECT_QTY) AS REJECT_QTY,
+ SUM(REJECT_TOTAL_QTY) AS REJECT_TOTAL_QTY,
+ SUM(DEFECT_QTY) AS DEFECT_QTY,
+ ROUND(SUM(REJECT_TOTAL_QTY) * 100 / NULLIF(SUM(MOVEIN_QTY), 0), 4) AS REJECT_RATE_PCT
+FROM DWH.DW_PJ_REJECT_HISTORY_PERF_D
+WHERE TXN_DAY BETWEEN TO_DATE(:start_date, 'YYYY-MM-DD') AND TO_DATE(:end_date, 'YYYY-MM-DD')
+GROUP BY TXN_DAY, WORKCENTER_GROUP
+ORDER BY TXN_DAY DESC, WORKCENTER_GROUP;
diff --git a/frontend/src/portal-shell/nativeModuleRegistry.js b/frontend/src/portal-shell/nativeModuleRegistry.js
index 3126929..5a16462 100644
--- a/frontend/src/portal-shell/nativeModuleRegistry.js
+++ b/frontend/src/portal-shell/nativeModuleRegistry.js
@@ -56,12 +56,16 @@ const NATIVE_MODULE_LOADERS = Object.freeze({
),
'/query-tool': createNativeLoader(
() => import('../query-tool/App.vue'),
- [() => import('../resource-shared/styles.css'), () => import('../query-tool/style.css')],
+ [() => import('../resource-shared/styles.css')],
),
'/tmtt-defect': createNativeLoader(
() => import('../tmtt-defect/App.vue'),
[() => import('../tmtt-defect/style.css')],
),
+ '/tables': createNativeLoader(
+ () => import('../tables/App.vue'),
+ [() => import('../tables/style.css')],
+ ),
});
export function getNativeModuleLoader(route) {
diff --git a/frontend/src/query-tool/App.vue b/frontend/src/query-tool/App.vue
index 8784ba3..d07b238 100644
--- a/frontend/src/query-tool/App.vue
+++ b/frontend/src/query-tool/App.vue
@@ -1,342 +1,394 @@
-
-