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 @@