From e21d736b3ed3af54aca3f605f74ff5c17d20632c Mon Sep 17 00:00:00 2001 From: beabigegg Date: Wed, 28 Jan 2026 15:07:24 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=96=B0=E5=A2=9E=E6=9C=AA=E8=BF=BD?= =?UTF-8?q?=E8=B9=A4=E7=9A=84=E5=B0=88=E6=A1=88=E6=AA=94=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenSpec Changes: - archive/2026-01-27-wip-status-filter: WIP 狀態篩選功能規格 - db-connection-stability: 資料庫連線穩定性規格(進行中) Documents: - DW_PJ_LOT_V_POWERBI_SQL.txt: Power BI SQL 查詢參考 Frontend Design: - WIP_main.pen: WIP 主頁設計稿 - Hold_detail.pen: Hold 明細頁設計稿 Co-Authored-By: Claude Opus 4.5 --- docs/DW_PJ_LOT_V_POWERBI_SQL.txt | 60 + frontend_design/Hold_detail.pen | 2182 +++++++++++++++++ frontend_design/WIP_main.pen | 614 +++++ .../2026-01-27-wip-status-filter/design.md | 348 +++ .../2026-01-27-wip-status-filter/proposal.md | 86 + .../2026-01-27-wip-status-filter/tasks.md | 99 + .../changes/db-connection-stability/design.md | 293 +++ .../db-connection-stability/proposal.md | 56 + .../changes/db-connection-stability/tasks.md | 76 + 9 files changed, 3814 insertions(+) create mode 100644 docs/DW_PJ_LOT_V_POWERBI_SQL.txt create mode 100644 frontend_design/Hold_detail.pen create mode 100644 frontend_design/WIP_main.pen create mode 100644 openspec/changes/archive/2026-01-27-wip-status-filter/design.md create mode 100644 openspec/changes/archive/2026-01-27-wip-status-filter/proposal.md create mode 100644 openspec/changes/archive/2026-01-27-wip-status-filter/tasks.md create mode 100644 openspec/changes/db-connection-stability/design.md create mode 100644 openspec/changes/db-connection-stability/proposal.md create mode 100644 openspec/changes/db-connection-stability/tasks.md diff --git a/docs/DW_PJ_LOT_V_POWERBI_SQL.txt b/docs/DW_PJ_LOT_V_POWERBI_SQL.txt new file mode 100644 index 0000000..fc8d9cb --- /dev/null +++ b/docs/DW_PJ_LOT_V_POWERBI_SQL.txt @@ -0,0 +1,60 @@ +SELECT L.LOTID AS ""Run Card Lot ID"", + L.Workorder AS ""Work Order ID"", + L.Qty AS ""Lot Qty(pcs)"", + L.Qty2 AS ""Lot Qty(Wafer pcs)"", + L.Status AS ""Run Card Status"", + L.HOLDREASONNAME AS ""Hold Reason"", + L.CurrentHoldCount AS ""Hold Count"", + L.Owner AS ""Work Order Owner"", + L.StartDate AS ""Run Card Start Date"", + L.UTS, + L.Product AS ""Product P/N"", + L.Productlinename AS ""Package"", + L.Package_LEF as ""Package(LF)"", + L.PJ_FUNCTION AS ""Product Function"", + L.Pj_Type AS ""Product Type"", + L.BOP, + L.FirstName AS ""Wafer Lot ID"", + L.WAFERNAME AS ""Wafer P/N"", + L.WaferLot ""Wafer Lot ID(Prefix)"", + L.SpecName AS ""Spec"", + L.SPECSEQUENCE AS ""Spec Sequence"", + L.SPECSEQUENCE || '_' || L.SpecName AS ""Spec(Order)"", + L.Workcentername AS ""Work Center"", + L.WorkCenterSequence AS ""Work Center Sequence"", + L.WorkCenter_Group AS ""Work Center(Group)"", + L.WorkCenter_Short AS ""Work Center(Short)"", + L.WorkCenterSequence_Group AS ""Work Center Sequence(Group)"", + L.WorkCenterSequence_Group || '_' || L.WorkCenter_Group AS ""Work Center Group(Order)"", + L.AgeByDays AS ""Age By Days"", + L.Equipments AS ""Equipment ID"", + L.EquipmentCount AS ""Equipment Count"", + L.Workflowname AS ""Work Flow Name"", + L.Datecode AS ""Product Date Code"", + L.LEADFRAMENAME AS ""LF Material Part"", + L.LEADFRAMEOPTION AS ""LF Option ID"", + L.COMNAME AS ""Compound Material Part"", + L.LOCATIONNAME AS ""Run Card Location"", + L.Eventname AS ""NCR ID"", + L.Occurrencedate AS ""NCR-issued Time"", + L.ReleaseTime AS ""Release Time"", + L.ReleaseEmp AS ""Release Employee"", + L.ReleaseReason AS ""Release Comment"", + L.COMMENT_HOLD AS ""Hold Comment"", + L.CONTAINERCOMMENTS AS ""Comment"", + L.COMMENT_DATE AS ""Run Card Comment"", + L.COMMENT_EMP AS ""Run Card Comment Employee"", + L.COMMENT_FUTURE AS ""Future Hold Comment"", + L.HOLDEMP AS ""Hold Employee"", + L.DEPTNAME AS ""Hold Employee Dept"", + L.PJ_PRODUCEREGION AS ""Produce Region"", + L.Prioritycodename AS ""Work Order Priority"", + L.TMTT_R AS ""TMTT Remaining"", + L.wafer_factor AS ""Die Consumption Qty"", + Case When (L.EquipmentCount>0) Then 'RUN' + When (L.CurrentHoldCount>0) Then 'HOLD' + ELSE 'QUENE' End AS ""WIP Status"", + Case When (L.EquipmentCount>0) Then 1 + When (L.CurrentHoldCount>0) Then 3 + ELSE 2 End AS ""WIP Status Sequence"", + sys_date AS ""Data Update Date"" \ No newline at end of file diff --git a/frontend_design/Hold_detail.pen b/frontend_design/Hold_detail.pen new file mode 100644 index 0000000..541f2ff --- /dev/null +++ b/frontend_design/Hold_detail.pen @@ -0,0 +1,2182 @@ +{ + "version": "2.6", + "children": [ + { + "type": "frame", + "id": "bi8Au", + "x": 0, + "y": 0, + "name": "Frame", + "clip": true, + "width": 800, + "height": 600, + "fill": "#FFFFFF", + "layout": "none" + }, + { + "type": "frame", + "id": "7V3YX", + "x": 0, + "y": 0, + "name": "Hold Detail Page", + "width": 1400, + "fill": "#F5F7FA", + "layout": "vertical", + "gap": 16, + "padding": 20, + "children": [ + { + "type": "frame", + "id": "I5lpc", + "name": "header", + "width": "fill_container", + "fill": { + "type": "gradient", + "gradientType": "linear", + "enabled": true, + "rotation": 135, + "size": { + "height": 1 + }, + "colors": [ + { + "color": "#667eea", + "position": 0 + }, + { + "color": "#764ba2", + "position": 1 + } + ] + }, + "cornerRadius": 10, + "padding": [ + 18, + 22 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "EdWXi", + "name": "headerLeft", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "MVVGK", + "name": "backBtn", + "width": 36, + "height": 36, + "fill": "rgba(255,255,255,0.2)", + "cornerRadius": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "ZJEO4", + "name": "backIcon", + "width": 20, + "height": 20, + "iconFontName": "arrow-left", + "iconFontFamily": "lucide", + "fill": "#FFFFFF" + } + ] + }, + { + "type": "frame", + "id": "8YBhs", + "name": "titleGroup", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "W3jyy", + "name": "pageTitle", + "fill": "#FFFFFF", + "content": "Hold Detail: 缺陷", + "fontFamily": "Inter", + "fontSize": 22, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "V3p7U", + "name": "holdBadge", + "fill": "#FEE2E2", + "cornerRadius": 4, + "gap": 6, + "padding": [ + 4, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "6Mvg0", + "name": "badgeText", + "fill": "#991B1B", + "content": "品質異常 Hold", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "JpXu7", + "name": "headerRight", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Bg1BI", + "name": "lastUpdate", + "fill": "rgba(255,255,255,0.8)", + "content": "Last Update: 2026-01-28 10:30:00", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dH5jc", + "name": "summaryRow", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "Jcuku", + "name": "card1", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "gap": 8, + "padding": 20, + "children": [ + { + "type": "text", + "id": "NO3qg", + "name": "card1Label", + "fill": "#666666", + "content": "Total Lots", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "S6x9b", + "name": "card1Value", + "fill": "#222222", + "content": "127", + "fontFamily": "Inter", + "fontSize": 32, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "k5Fqe", + "name": "card2", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "gap": 8, + "padding": 20, + "children": [ + { + "type": "text", + "id": "OzCf3", + "name": "card2Label", + "fill": "#666666", + "content": "Total QTY (pcs)", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wrtwd", + "name": "card2Value", + "fill": "#222222", + "content": "458,920", + "fontFamily": "Inter", + "fontSize": 32, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "tGRHt", + "name": "card3", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "gap": 8, + "padding": 20, + "children": [ + { + "type": "text", + "id": "WbSE9", + "name": "card3Label", + "fill": "#666666", + "content": "平均當站滯留", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wUiN4", + "name": "card3Value", + "fill": "#F59E0B", + "content": "3.2 天", + "fontFamily": "Inter", + "fontSize": 32, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "BlhLH", + "name": "card4", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "gap": 8, + "padding": 20, + "children": [ + { + "type": "text", + "id": "CLGML", + "name": "card4Label", + "fill": "#666666", + "content": "最久當站滯留", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aGrnM", + "name": "card4Value", + "fill": "#EF4444", + "content": "15 天", + "fontFamily": "Inter", + "fontSize": 32, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "7uKhR", + "name": "card5", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "gap": 8, + "padding": 20, + "children": [ + { + "type": "text", + "id": "P1gHF", + "name": "card5Label", + "fill": "#666666", + "content": "影響站群", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EoKkT", + "name": "card5Value", + "fill": "#222222", + "content": "8", + "fontFamily": "Inter", + "fontSize": 32, + "fontWeight": "700" + } + ] + } + ] + }, + { + "type": "frame", + "id": "qYy5R", + "name": "contentGrid", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "n6o9k", + "name": "leftCard", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "TQjuU", + "name": "leftHeader", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#E2E6EF" + }, + "padding": [ + 16, + 20 + ], + "children": [ + { + "type": "text", + "id": "9ZCN9", + "name": "leftTitle", + "fill": "#222222", + "content": "依站群分佈 (By Workcenter) - 點擊可篩選", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "sRUro", + "name": "clickHint1", + "fill": "#667eea", + "cornerRadius": 4, + "padding": [ + 4, + 8 + ], + "children": [ + { + "type": "text", + "id": "Er9HP", + "name": "clickText1", + "fill": "#FFFFFF", + "content": "可點擊篩選", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "6dcCv", + "name": "leftBody", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "KJxxR", + "name": "tableHeader", + "width": "fill_container", + "fill": "#F9FAFB", + "gap": 16, + "padding": [ + 12, + 20 + ], + "children": [ + { + "type": "text", + "id": "pl1PL", + "name": "th1", + "fill": "#666666", + "content": "Workcenter", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "0Nler", + "name": "th2", + "fill": "#666666", + "content": "Lots", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "v8SQy", + "name": "th3", + "fill": "#666666", + "content": "QTY", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "hgtG9", + "name": "th4", + "fill": "#666666", + "content": "佔比", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "FeNSs", + "name": "row1", + "width": "fill_container", + "fill": "#EEF2FF", + "stroke": { + "align": "inside", + "thickness": 2, + "fill": "#667eea" + }, + "gap": 16, + "padding": [ + 12, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pzbjE", + "name": "r1c1", + "fill": "#222222", + "content": "DIE_BOND", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ufZSd", + "name": "r1c2", + "fill": "#222222", + "content": "45", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "sNXbx", + "name": "r1c3", + "fill": "#222222", + "content": "156,230", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "R0Zvo", + "name": "r1c4", + "fill": "#667eea", + "content": "34.1%", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "QcUcl", + "name": "row2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#F0F0F0" + }, + "gap": 16, + "padding": [ + 12, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pnDQb", + "name": "r2c1", + "fill": "#222222", + "content": "WIRE_BOND", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "8BYJg", + "name": "r2c2", + "fill": "#222222", + "content": "38", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "0ItAD", + "name": "r2c3", + "fill": "#222222", + "content": "128,450", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EjrRN", + "name": "r2c4", + "fill": "#667eea", + "content": "28.0%", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "0E6IL", + "name": "row3", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#F0F0F0" + }, + "gap": 16, + "padding": [ + 12, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BuPpm", + "name": "r3c1", + "fill": "#222222", + "content": "MOLDING", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EePKK", + "name": "r3c2", + "fill": "#222222", + "content": "28", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "2F2i9", + "name": "r3c3", + "fill": "#222222", + "content": "98,120", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mBXbQ", + "name": "r3c4", + "fill": "#667eea", + "content": "21.4%", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "XoxQ0", + "name": "row4", + "width": "fill_container", + "gap": 16, + "padding": [ + 12, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jiGXt", + "name": "r4c1", + "fill": "#888888", + "content": "Others (5)", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RJDFw", + "name": "r4c2", + "fill": "#888888", + "content": "16", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TO1my", + "name": "r4c3", + "fill": "#888888", + "content": "76,120", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vTEIy", + "name": "r4c4", + "fill": "#888888", + "content": "16.5%", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "jJRVr", + "name": "rightCard", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "D431a", + "name": "rightHeader", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#E2E6EF" + }, + "padding": [ + 16, + 20 + ], + "children": [ + { + "type": "text", + "id": "wbKWf", + "name": "rightTitle", + "fill": "#222222", + "content": "依 Package 分佈 - 點擊可篩選", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "Pq3Qm", + "name": "clickHint2", + "fill": "#667eea", + "cornerRadius": 4, + "padding": [ + 4, + 8 + ], + "children": [ + { + "type": "text", + "id": "vMiTb", + "name": "clickText2", + "fill": "#FFFFFF", + "content": "可點擊篩選", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "crSW4", + "name": "rightBody", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "CAnxl", + "name": "pTableHeader", + "width": "fill_container", + "fill": "#F9FAFB", + "gap": 16, + "padding": [ + 12, + 20 + ], + "children": [ + { + "type": "text", + "id": "hbJ0a", + "name": "pth1", + "fill": "#666666", + "content": "Package", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "AjQJS", + "name": "pth2", + "fill": "#666666", + "content": "Lots", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "JYohj", + "name": "pth3", + "fill": "#666666", + "content": "QTY", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "MV6jF", + "name": "pth4", + "fill": "#666666", + "content": "佔比", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "j9Ycq", + "name": "prow1", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#F0F0F0" + }, + "gap": 16, + "padding": [ + 12, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "5PO2Y", + "name": "pr1c1", + "fill": "#222222", + "content": "QFN", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "1CgoT", + "name": "pr1c2", + "fill": "#222222", + "content": "52", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uQB2r", + "name": "pr1c3", + "fill": "#222222", + "content": "189,450", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wY306", + "name": "pr1c4", + "fill": "#667eea", + "content": "41.3%", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "qI6Ae", + "name": "prow2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#F0F0F0" + }, + "gap": 16, + "padding": [ + 12, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "E02Zh", + "name": "pr2c1", + "fill": "#222222", + "content": "DFN", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hFdSa", + "name": "pr2c2", + "fill": "#222222", + "content": "35", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ZegQX", + "name": "pr2c3", + "fill": "#222222", + "content": "145,230", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pQQf8", + "name": "pr2c4", + "fill": "#667eea", + "content": "31.6%", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "3zbTw", + "name": "prow3", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#F0F0F0" + }, + "gap": 16, + "padding": [ + 12, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "eago9", + "name": "pr3c1", + "fill": "#222222", + "content": "SOT", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "h4G0R", + "name": "pr3c2", + "fill": "#222222", + "content": "22", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mwqGr", + "name": "pr3c3", + "fill": "#222222", + "content": "78,120", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "3sOgp", + "name": "pr3c4", + "fill": "#667eea", + "content": "17.0%", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "lDXi1", + "name": "prow4", + "width": "fill_container", + "gap": 16, + "padding": [ + 12, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "m8vMX", + "name": "pr4c1", + "fill": "#888888", + "content": "Others (4)", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HNQIl", + "name": "pr4c2", + "fill": "#888888", + "content": "18", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uLIWM", + "name": "pr4c3", + "fill": "#888888", + "content": "46,120", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "R4W2c", + "name": "pr4c4", + "fill": "#888888", + "content": "10.1%", + "textAlign": "right", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "IRtLq", + "name": "ageSection", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "QczQu", + "name": "ageHeader", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#E2E6EF" + }, + "padding": [ + 16, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rjWxw", + "name": "ageTitle", + "fill": "#222222", + "content": "當站滯留天數分佈 (Age at Current Station)", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "text", + "id": "o8zso", + "name": "ageNote", + "fill": "#888888", + "content": "依 MOVEIN 時間計算 | 點擊可篩選", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "1TyeC", + "name": "ageBody", + "width": "fill_container", + "gap": 16, + "padding": 20, + "justifyContent": "space_between", + "children": [ + { + "type": "frame", + "id": "llWVM", + "name": "bucket1", + "width": "fill_container", + "fill": "#F0FDF4", + "cornerRadius": 8, + "stroke": { + "align": "inside", + "thickness": 2, + "fill": "#22C55E" + }, + "layout": "vertical", + "gap": 8, + "padding": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "NQoCS", + "name": "b1Label", + "fill": "#166534", + "content": "0-1 天", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "text", + "id": "ovS4b", + "name": "b1Value", + "fill": "#166534", + "content": "42 lots", + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "700" + }, + { + "type": "text", + "id": "DWt6p", + "name": "b1Pct", + "fill": "#22C55E", + "content": "33.1%", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vgoxY", + "name": "b1Qty", + "fill": "#166534", + "content": "152,300", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ndAGZ", + "name": "bucket2", + "width": "fill_container", + "fill": "#FFFBEB", + "cornerRadius": 8, + "stroke": { + "align": "inside", + "thickness": 2, + "fill": "#F59E0B" + }, + "layout": "vertical", + "gap": 8, + "padding": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "icdfd", + "name": "b2Label", + "fill": "#92400E", + "content": "1-3 天", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Rvns8", + "name": "b2Value", + "fill": "#92400E", + "content": "38 lots", + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "700" + }, + { + "type": "text", + "id": "TuQKb", + "name": "b2Pct", + "fill": "#F59E0B", + "content": "29.9%", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kEr4r", + "name": "b2Qty", + "fill": "#92400E", + "content": "138,450", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "gy2x0", + "name": "bucket3", + "width": "fill_container", + "fill": "#FFF7ED", + "cornerRadius": 8, + "stroke": { + "align": "inside", + "thickness": 2, + "fill": "#F97316" + }, + "layout": "vertical", + "gap": 8, + "padding": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "B98JF", + "name": "b3Label", + "fill": "#9A3412", + "content": "3-7 天", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "text", + "id": "TdRbd", + "name": "b3Value", + "fill": "#9A3412", + "content": "28 lots", + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "700" + }, + { + "type": "text", + "id": "KF1LK", + "name": "b3Pct", + "fill": "#F97316", + "content": "22.0%", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "IcmHX", + "name": "b3Qty", + "fill": "#9A3412", + "content": "98,120", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "iDYN9", + "name": "bucket4", + "width": "fill_container", + "fill": "#FEF2F2", + "cornerRadius": 8, + "stroke": { + "align": "inside", + "thickness": 2, + "fill": "#EF4444" + }, + "layout": "vertical", + "gap": 8, + "padding": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Peb5J", + "name": "b4Label", + "fill": "#991B1B", + "content": "7+ 天", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "text", + "id": "ydAd9", + "name": "b4Value", + "fill": "#991B1B", + "content": "19 lots", + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "700" + }, + { + "type": "text", + "id": "DVUNT", + "name": "b4Pct", + "fill": "#EF4444", + "content": "15.0%", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "r1aMo", + "name": "b4Qty", + "fill": "#991B1B", + "content": "70,050", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "EkLV0", + "name": "lotSection", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "6Tg6Z", + "name": "lotHeader", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#E2E6EF" + }, + "padding": [ + 16, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "hlYwY", + "name": "lotTitleGroup", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UvnOY", + "name": "lotTitle", + "fill": "#222222", + "content": "Lot Details", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "54DK5", + "name": "lotCount", + "fill": "#667eea", + "cornerRadius": 12, + "padding": [ + 4, + 10 + ], + "children": [ + { + "type": "text", + "id": "65lwE", + "name": "lotCountText", + "fill": "#FFFFFF", + "content": "127 lots", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "OCvgO", + "name": "filterIndicator", + "fill": "#F0FDF4", + "cornerRadius": 12, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#22C55E" + }, + "padding": [ + 4, + 10 + ], + "children": [ + { + "type": "text", + "id": "dYBGb", + "name": "filterText", + "fill": "#166534", + "content": "篩選: DIE_BOND", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "hVBnP", + "name": "clearBtn", + "fill": "#FEF2F2", + "cornerRadius": 12, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#EF4444" + }, + "padding": [ + 4, + 10 + ], + "children": [ + { + "type": "text", + "id": "Upvp6", + "name": "clearText", + "fill": "#991B1B", + "content": "✕ 清除篩選", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "text", + "id": "UAIbQ", + "name": "lotInfo", + "fill": "#888888", + "content": "依滯留天數排序 (最久優先)", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "k2PhE", + "name": "lotBody", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "FYGFO", + "name": "lotTableHeader", + "width": "fill_container", + "fill": "#F9FAFB", + "gap": 8, + "padding": [ + 12, + 20 + ], + "children": [ + { + "type": "text", + "id": "LLmJp", + "name": "lth1", + "fill": "#666666", + "content": "LOTID", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "at5RZ", + "name": "lth2", + "fill": "#666666", + "content": "WORKORDER", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "ujPfX", + "name": "lth3", + "fill": "#666666", + "content": "QTY", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "UoBHt", + "name": "lth4", + "fill": "#666666", + "content": "Package", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "zV5Ma", + "name": "lth5", + "fill": "#666666", + "content": "Workcenter", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "ArmE7", + "name": "lth6", + "fill": "#666666", + "content": "Spec", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "jgkw4", + "name": "lth7", + "fill": "#666666", + "content": "Age", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "IZD0x", + "name": "lth8", + "fill": "#666666", + "content": "Hold By", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "wk2g6", + "name": "lth9", + "fill": "#666666", + "content": "Dept", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "U7daK", + "name": "lotRow1", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#F0F0F0" + }, + "gap": 8, + "padding": [ + 12, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "t3ceS", + "name": "lr1c1", + "fill": "#222222", + "content": "LOT2401150001", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LV0bf", + "name": "lr1c2", + "fill": "#222222", + "content": "WO20240115001", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "KPyX9", + "name": "lr1c3", + "fill": "#222222", + "content": "5,200", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "4IAJD", + "name": "lr1c4", + "fill": "#222222", + "content": "QFN", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ejmJF", + "name": "lr1c5", + "fill": "#222222", + "content": "DIE_BOND", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "YpYn3", + "name": "lr1c6", + "fill": "#222222", + "content": "Die Attach", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "0MDI8", + "name": "lr1c7", + "fill": "#FEE2E2", + "cornerRadius": 4, + "padding": [ + 2, + 8 + ], + "children": [ + { + "type": "text", + "id": "wGDd5", + "name": "lr1c7t", + "fill": "#991B1B", + "content": "15d", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "bddVn", + "name": "lr1c8", + "fill": "#222222", + "content": "王小明", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DrnAd", + "name": "lr1c9", + "fill": "#222222", + "content": "QC", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "XKBSi", + "name": "lotRow2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#F0F0F0" + }, + "gap": 8, + "padding": [ + 12, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lHSqf", + "name": "lr2c1", + "fill": "#222222", + "content": "LOT2401180023", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xXL5X", + "name": "lr2c2", + "fill": "#222222", + "content": "WO20240118005", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "PCg5f", + "name": "lr2c3", + "fill": "#222222", + "content": "3,800", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UsM4m", + "name": "lr2c4", + "fill": "#222222", + "content": "DFN", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "soi3b", + "name": "lr2c5", + "fill": "#222222", + "content": "WIRE_BOND", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rHrFq", + "name": "lr2c6", + "fill": "#222222", + "content": "Wire Bond", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "kKLNP", + "name": "lr2c7", + "fill": "#FFF7ED", + "cornerRadius": 4, + "padding": [ + 2, + 8 + ], + "children": [ + { + "type": "text", + "id": "jVduw", + "name": "lr2c7t", + "fill": "#9A3412", + "content": "8d", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "Nsja6", + "name": "lr2c8", + "fill": "#222222", + "content": "李大華", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "0VS56", + "name": "lr2c9", + "fill": "#222222", + "content": "PE", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "mUQgn", + "name": "lotRow3", + "width": "fill_container", + "gap": 8, + "padding": [ + 12, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yxhdB", + "name": "lr3c1", + "fill": "#222222", + "content": "LOT2401200045", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zB9xG", + "name": "lr3c2", + "fill": "#222222", + "content": "WO20240120008", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "0GVPK", + "name": "lr3c3", + "fill": "#222222", + "content": "4,500", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "y5oaF", + "name": "lr3c4", + "fill": "#222222", + "content": "SOT", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lkcvd", + "name": "lr3c5", + "fill": "#222222", + "content": "MOLDING", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RqRPi", + "name": "lr3c6", + "fill": "#222222", + "content": "Molding", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "kMWnK", + "name": "lr3c7", + "fill": "#FFFBEB", + "cornerRadius": 4, + "padding": [ + 2, + 8 + ], + "children": [ + { + "type": "text", + "id": "S80Dg", + "name": "lr3c7t", + "fill": "#92400E", + "content": "2d", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "vC6Ph", + "name": "lr3c8", + "fill": "#222222", + "content": "張三豐", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xKBf2", + "name": "lr3c9", + "fill": "#222222", + "content": "QC", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "fgdNX", + "name": "pagination", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "#E2E6EF" + }, + "padding": [ + 12, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UkFtQ", + "name": "pageInfo", + "fill": "#888888", + "content": "Showing 1-50 of 127 lots", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "b2LAR", + "name": "pageButtons", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "QERaB", + "name": "prevBtn", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "padding": [ + 8, + 16 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Msjmx", + "name": "prevText", + "fill": "#666666", + "content": "← Previous", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "PFfh6", + "name": "pageNum", + "fill": "#667eea", + "cornerRadius": 6, + "padding": [ + 8, + 16 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "glqu5", + "name": "pageNumText", + "fill": "#FFFFFF", + "content": "1", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "aWWrR", + "name": "nextBtn", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "padding": [ + 8, + 16 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "4pKqZ", + "name": "nextText", + "fill": "#666666", + "content": "Next →", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend_design/WIP_main.pen b/frontend_design/WIP_main.pen new file mode 100644 index 0000000..d00b336 --- /dev/null +++ b/frontend_design/WIP_main.pen @@ -0,0 +1,614 @@ +{ + "version": "2.6", + "children": [ + { + "type": "frame", + "id": "GIoPU", + "x": 950, + "y": 0, + "name": "WIP Overview - Integrated", + "width": 1200, + "fill": "#F5F7FA", + "layout": "vertical", + "gap": 16, + "padding": 20, + "children": [ + { + "type": "frame", + "id": "N2qxA", + "name": "header", + "width": "fill_container", + "height": 64, + "fill": { + "type": "gradient", + "gradientType": "linear", + "enabled": true, + "rotation": 135, + "size": { + "height": 1 + }, + "colors": [ + { + "color": "#667eea", + "position": 0 + }, + { + "color": "#764ba2", + "position": 1 + } + ] + }, + "cornerRadius": 10, + "padding": [ + 0, + 22 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "7h8YC", + "name": "headerTitle", + "fill": "#FFFFFF", + "content": "WIP Overview Dashboard", + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "JyskI", + "name": "headerRight", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "8rtgc", + "name": "lastUpdate", + "fill": "rgba(255,255,255,0.8)", + "content": "Last Update: 2026-01-27 14:30", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ZH0PW", + "name": "refreshBtn", + "fill": "rgba(255,255,255,0.2)", + "cornerRadius": 8, + "padding": [ + 9, + 20 + ], + "children": [ + { + "type": "text", + "id": "wlrMh", + "name": "refreshText", + "fill": "#FFFFFF", + "content": "重新整理", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "aYXjP", + "name": "Summary Section", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "pFof4", + "name": "kpiRow", + "width": "fill_container", + "gap": 14, + "children": [ + { + "type": "frame", + "id": "0kWPh", + "name": "kpi1", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "gap": 6, + "padding": [ + 16, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "DTtUq", + "name": "kpi1Label", + "fill": "#666666", + "content": "Total Lots", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vdmq8", + "name": "kpi1Value", + "fill": "#667eea", + "content": "1,234", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "wEupl", + "name": "kpi2", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "gap": 6, + "padding": [ + 16, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "59OHd", + "name": "kpi2Label", + "fill": "#666666", + "content": "Total QTY", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "YkPVl", + "name": "kpi2Value", + "fill": "#667eea", + "content": "56,789", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": "700" + } + ] + } + ] + }, + { + "type": "frame", + "id": "g65nT", + "name": "wipStatusRow", + "width": "fill_container", + "gap": 14, + "children": [ + { + "type": "frame", + "id": "sbKdU", + "name": "runCard", + "width": "fill_container", + "fill": "#F0FDF4", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 2, + "fill": "#22C55E" + }, + "layout": "vertical", + "gap": 8, + "padding": [ + 16, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "EQzBo", + "name": "runLeft", + "width": "fill_container", + "gap": 10, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 5, + "id": "m7Prk", + "name": "runDot", + "fill": "#22C55E", + "width": 10, + "height": 10 + }, + { + "type": "text", + "id": "1DMEu", + "name": "runLabel", + "fill": "#166534", + "content": "RUN", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "ZVtRH", + "name": "runRight", + "width": "fill_container", + "gap": 24, + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "OLwma", + "name": "runLots", + "fill": "#0D0D0D", + "content": "500 lots", + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "700" + }, + { + "type": "text", + "id": "OI5f5", + "name": "runQty", + "fill": "#166534", + "content": "30,000 pcs", + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "700" + } + ] + } + ] + }, + { + "type": "frame", + "id": "uibRH", + "name": "queueCard", + "width": "fill_container", + "fill": "#FFFBEB", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 2, + "fill": "#F59E0B" + }, + "layout": "vertical", + "gap": 8, + "padding": [ + 16, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "xeGDP", + "name": "queueLeft", + "width": "fill_container", + "gap": 10, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 5, + "id": "KuAgl", + "name": "queueDot", + "fill": "#F59E0B", + "width": 10, + "height": 10 + }, + { + "type": "text", + "id": "TsD9B", + "name": "queueLabel", + "fill": "#92400E", + "content": "QUEUE", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "41Db3", + "name": "queueRight", + "width": "fill_container", + "gap": 24, + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "dtaqd", + "name": "queueLots", + "fill": "#0D0D0D", + "content": "634 lots", + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "700" + }, + { + "type": "text", + "id": "BVusD", + "name": "queueQty", + "fill": "#92400E", + "content": "21,789 pcs", + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "700" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Y5gLu", + "name": "holdCard", + "width": "fill_container", + "fill": "#FEF2F2", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 2, + "fill": "#EF4444" + }, + "layout": "vertical", + "gap": 8, + "padding": [ + 16, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "juHZC", + "name": "holdLeft", + "width": "fill_container", + "gap": 10, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 5, + "id": "FW9Vv", + "name": "holdDot", + "fill": "#EF4444", + "width": 10, + "height": 10 + }, + { + "type": "text", + "id": "gEojA", + "name": "holdLabel", + "fill": "#991B1B", + "content": "HOLD", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "3imiS", + "name": "holdRight", + "width": "fill_container", + "gap": 24, + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "AlTi3", + "name": "holdLots", + "fill": "#0D0D0D", + "content": "100 lots", + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "700" + }, + { + "type": "text", + "id": "oKc0i", + "name": "holdQty", + "fill": "#991B1B", + "content": "5,000 pcs", + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "700" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "uRXyA", + "name": "Content Grid", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "7HMip", + "name": "matrixCard", + "width": "fill_container", + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "pxsYm", + "name": "matrixHeader", + "width": "fill_container", + "fill": "#FAFBFC", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#E2E6EF" + }, + "padding": [ + 14, + 20 + ], + "children": [ + { + "type": "text", + "id": "JhSDl", + "name": "matrixTitle", + "fill": "#222222", + "content": "Workcenter x Package Matrix (QTY)", + "fontFamily": "Inter", + "fontSize": 15, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "4hQZP", + "name": "matrixBody", + "width": "fill_container", + "height": 200, + "layout": "vertical", + "padding": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lH6Yr", + "name": "matrixPlaceholder", + "fill": "#999999", + "content": "[ Matrix Table ]", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "FOIFS", + "name": "holdSummaryCard", + "width": 320, + "fill": "#FFFFFF", + "cornerRadius": 10, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#E2E6EF" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "uikVi", + "name": "holdSummaryHeader", + "width": "fill_container", + "fill": "#FAFBFC", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#E2E6EF" + }, + "padding": [ + 14, + 20 + ], + "children": [ + { + "type": "text", + "id": "VBWBv", + "name": "holdSummaryTitle", + "fill": "#222222", + "content": "Hold Summary", + "fontFamily": "Inter", + "fontSize": 15, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "cFEPm", + "name": "holdSummaryBody", + "width": "fill_container", + "height": 200, + "layout": "vertical", + "padding": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "s7sa1", + "name": "holdSummaryPlaceholder", + "fill": "#999999", + "content": "[ Hold Table ]", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/openspec/changes/archive/2026-01-27-wip-status-filter/design.md b/openspec/changes/archive/2026-01-27-wip-status-filter/design.md new file mode 100644 index 0000000..b7c60da --- /dev/null +++ b/openspec/changes/archive/2026-01-27-wip-status-filter/design.md @@ -0,0 +1,348 @@ +# WIP Status Filter - 技術設計 + +## 架構概覽 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ wip_overview.html (前端) │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ RUN │ │ QUEUE │ │ HOLD │ ← 可點擊卡片 │ +│ └────┬────┘ └────┬────┘ └────┬────┘ │ +│ │ │ │ │ +│ └───────────┼───────────┘ │ +│ ▼ │ +│ activeStatusFilter (state) │ +│ │ │ +│ ▼ │ +│ GET /api/wip/overview/matrix?status={RUN|QUEUE|HOLD} │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ wip_routes.py │ +│ api_overview_matrix() ← 新增 status 參數解析 │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ wip_service.py │ +│ get_wip_matrix(status=...) ← 新增 status 篩選邏輯 │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 後端變更 + +### 1. wip_service.py - get_wip_matrix() + +**新增參數**:`status: Optional[str] = None` + +**SQL 條件**: +```python +# 在 _build_base_conditions 之後加入 +if status: + status_upper = status.upper() + if status_upper == 'RUN': + conditions.append("EQUIPMENTCOUNT > 0") + elif status_upper == 'HOLD': + conditions.append("EQUIPMENTCOUNT = 0 AND CURRENTHOLDCOUNT > 0") + elif status_upper == 'QUEUE': + conditions.append("EQUIPMENTCOUNT = 0 AND CURRENTHOLDCOUNT = 0") +``` + +**函數簽名變更**: +```python +def get_wip_matrix( + include_dummy: bool = False, + workorder: Optional[str] = None, + lotid: Optional[str] = None, + status: Optional[str] = None # 新增 +) -> Optional[Dict[str, Any]]: +``` + +### 2. wip_routes.py - api_overview_matrix() + +**新增參數解析**: +```python +status = request.args.get('status', '').strip().upper() or None + +# 驗證 status 值 +if status and status not in ('RUN', 'QUEUE', 'HOLD'): + return jsonify({ + 'success': False, + 'error': 'Invalid status. Use RUN, QUEUE, or HOLD' + }), 400 + +result = get_wip_matrix( + include_dummy=include_dummy, + workorder=workorder, + lotid=lotid, + status=status # 新增 +) +``` + +--- + +## 前端變更 + +### 1. 新增狀態變數 + +```javascript +// 在 state 物件旁邊新增 +let activeStatusFilter = null; // null | 'run' | 'queue' | 'hold' +``` + +### 2. 新增 CSS 樣式 + +```css +/* 可點擊的卡片 */ +.wip-status-card { + cursor: pointer; + transition: all 0.2s ease; +} + +.wip-status-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); +} + +/* 選中狀態 */ +.wip-status-card.active { + border-width: 3px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); +} + +.wip-status-card.run.active { + box-shadow: 0 4px 20px rgba(34, 197, 94, 0.4); +} + +.wip-status-card.queue.active { + box-shadow: 0 4px 20px rgba(245, 158, 11, 0.4); +} + +.wip-status-card.hold.active { + box-shadow: 0 4px 20px rgba(239, 68, 68, 0.4); +} + +/* 篩選提示標籤 */ +.filter-badge { + display: none; + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + margin-left: 8px; + background: rgba(0, 0, 0, 0.1); +} + +.wip-status-card.active .filter-badge { + display: inline-block; +} +``` + +### 3. HTML 變更 + +為卡片添加 `onclick` 和篩選標籤: + +```html +
+
+ RUN + FILTERED +
+
+ - + - +
+
+``` + +### 4. JavaScript 邏輯 + +```javascript +function toggleStatusFilter(status) { + if (activeStatusFilter === status) { + // 解除篩選 + activeStatusFilter = null; + } else { + // 套用新篩選 + activeStatusFilter = status; + } + + // 更新卡片樣式 + updateCardStyles(); + + // 重新載入 Matrix(帶篩選參數) + loadMatrix(); +} + +function updateCardStyles() { + document.querySelectorAll('.wip-status-card').forEach(card => { + card.classList.remove('active'); + }); + + if (activeStatusFilter) { + const activeCard = document.querySelector(`.wip-status-card.${activeStatusFilter}`); + if (activeCard) { + activeCard.classList.add('active'); + } + } +} + +function loadMatrix() { + const params = new URLSearchParams(); + + // 現有篩選條件 + const workorder = document.getElementById('workorder').value.trim(); + const lotid = document.getElementById('lotid').value.trim(); + if (workorder) params.append('workorder', workorder); + if (lotid) params.append('lotid', lotid); + + // 狀態篩選 + if (activeStatusFilter) { + params.append('status', activeStatusFilter.toUpperCase()); + } + + const url = '/api/wip/overview/matrix' + (params.toString() ? '?' + params : ''); + + // 顯示載入狀態 + document.getElementById('matrixContainer').innerHTML = + '
Loading...
'; + + fetch(url) + .then(response => response.json()) + .then(result => { + if (result.success) { + renderMatrix(result.data); + } else { + document.getElementById('matrixContainer').innerHTML = + '
Error loading data
'; + } + }) + .catch(error => { + console.error('Matrix load error:', error); + document.getElementById('matrixContainer').innerHTML = + '
Error loading data
'; + }); +} +``` + +### 5. 整合 loadAllData() + +修改 `loadAllData()` 確保 Matrix 載入時考慮 `activeStatusFilter`: + +```javascript +async function loadAllData(showLoading = false) { + // ... 現有邏輯 ... + + // Matrix 改為獨立載入(支援狀態篩選) + loadMatrix(); + + // Summary 和 Hold 維持不變 + // ... +} +``` + +### 6. Auto-refresh 整合 + +確保自動刷新時保留篩選狀態: + +```javascript +function startAutoRefresh() { + refreshInterval = setInterval(() => { + loadAllData(false); // loadMatrix() 內部會讀取 activeStatusFilter + }, 600000); +} +``` + +--- + +## Matrix 表格標題顯示篩選狀態 + +當有篩選時,更新 Matrix 標題: + +```javascript +function updateMatrixTitle() { + const titleEl = document.querySelector('.card-title'); + const baseTitle = 'Workcenter x Package Matrix (QTY)'; + + if (activeStatusFilter) { + const statusLabel = activeStatusFilter.toUpperCase(); + titleEl.textContent = `${baseTitle} - ${statusLabel} Only`; + } else { + titleEl.textContent = baseTitle; + } +} +``` + +--- + +## 資料流程 + +### 篩選開啟流程 + +``` +1. 使用者點擊 RUN 卡片 + │ +2. toggleStatusFilter('run') + │ +3. activeStatusFilter = 'run' + │ +4. updateCardStyles() → RUN 卡片加上 .active + │ +5. loadMatrix() → GET /api/wip/overview/matrix?status=RUN + │ +6. 後端過濾 EQUIPMENTCOUNT > 0 的資料 + │ +7. 前端 renderMatrix() 顯示篩選結果 +``` + +### 篩選關閉流程 + +``` +1. 使用者再次點擊 RUN 卡片 + │ +2. toggleStatusFilter('run') + │ +3. activeStatusFilter = null(因為已是 'run') + │ +4. updateCardStyles() → 移除所有 .active + │ +5. loadMatrix() → GET /api/wip/overview/matrix(無 status 參數) + │ +6. 後端返回全部資料 + │ +7. 前端 renderMatrix() 顯示完整 Matrix +``` + +--- + +## 邊界情況 + +| 情況 | 處理方式 | +|------|----------| +| 篩選結果為空 | 顯示 "No data available" | +| 與 workorder/lotid 組合篩選 | 條件疊加(AND) | +| 無效的 status 參數 | API 返回 400 錯誤 | +| 自動刷新時 | 保留 activeStatusFilter 狀態 | +| 手動刷新時 | 保留 activeStatusFilter 狀態 | + +--- + +## 測試要點 + +1. **基本功能** + - 點擊 RUN → Matrix 只顯示 RUN 狀態數量 + - 點擊 QUEUE → Matrix 只顯示 QUEUE 狀態數量 + - 點擊 HOLD → Matrix 只顯示 HOLD 狀態數量 + - 再次點擊 → 恢復全部 + +2. **視覺回饋** + - 選中卡片有明顯樣式變化 + - 載入時顯示 Loading 提示 + +3. **組合篩選** + - 同時輸入 workorder + 點選 RUN → 兩個條件都生效 + +4. **自動刷新** + - 篩選狀態在刷新後保持 diff --git a/openspec/changes/archive/2026-01-27-wip-status-filter/proposal.md b/openspec/changes/archive/2026-01-27-wip-status-filter/proposal.md new file mode 100644 index 0000000..21b8263 --- /dev/null +++ b/openspec/changes/archive/2026-01-27-wip-status-filter/proposal.md @@ -0,0 +1,86 @@ +# WIP Status Filter + +## 問題描述 + +WIP 即時概況頁面目前顯示三個狀態卡片(RUN / QUEUE / HOLD)和 Workcenter × Package Matrix 表格,但兩者之間沒有互動關係。 + +使用者想快速查看「目前有多少 WIP 正在 RUN」或「HOLD 的量分布在哪些工站」時,需要另外到 Detail 頁面篩選,操作不便。 + +## 目標 + +在 WIP Overview 頁面新增卡片篩選功能: +- 點選 RUN / QUEUE / HOLD 卡片時,Matrix 表格只顯示該狀態的數量 +- 再點一次同一卡片則解除篩選(toggle 行為) +- 提供清晰的視覺回饋,讓使用者知道目前的篩選狀態 + +## 目標用戶 + +- 生產主管:快速了解各狀態的分布 +- 值班人員:追蹤 HOLD 量的工站分布 + +## 預期行為 + +1. **初始狀態**:Matrix 顯示全部數量(現行行為) +2. **點選卡片**: + - 卡片顯示「選中」樣式(例如外框加粗、背景變深) + - Matrix 重新載入,只顯示該狀態的數量 + - 其他兩張卡片恢復「未選中」樣式 +3. **再次點選同一卡片**: + - 卡片恢復「未選中」樣式 + - Matrix 恢復顯示全部數量 +4. **篩選期間**: + - Summary 區域的 Total Lots / Total QTY 不變(保持全局統計) + - Hold Summary 區塊不受影響 + +## 範圍界定 + +### 包含 +- 前端:卡片點擊事件、選中樣式、Matrix 重載邏輯 +- 後端:`/api/wip/overview/matrix` 新增 `status` 參數(可選,值為 RUN/QUEUE/HOLD) +- 後端:`get_wip_matrix()` 函數新增 `status` 篩選條件 + +### 不包含 +- Summary 卡片的數值變動(維持全局統計) +- Hold Summary 區塊的篩選 +- URL 參數同步(不影響分享連結) +- 鍵盤快捷鍵 + +## 技術考量 + +### WIP 狀態判斷邏輯(與 IT 定義一致) + +```sql +CASE + WHEN EQUIPMENTCOUNT > 0 THEN 'RUN' + WHEN CURRENTHOLDCOUNT > 0 THEN 'HOLD' + ELSE 'QUEUE' +END AS WIP_STATUS +``` + +### API 變更 + +``` +GET /api/wip/overview/matrix?status=RUN +GET /api/wip/overview/matrix?status=QUEUE +GET /api/wip/overview/matrix?status=HOLD +GET /api/wip/overview/matrix (不帶參數 = 全部) +``` + +### 前端狀態 + +```javascript +let activeStatusFilter = null; // null | 'run' | 'queue' | 'hold' +``` + +## 風險評估 + +| 風險 | 影響 | 緩解措施 | +|------|------|----------| +| API 查詢變慢 | 低 | 狀態條件可利用現有索引 | +| 使用者混淆篩選狀態 | 中 | 明確的視覺回饋 + 載入提示 | + +## 成功標準 + +- 點擊卡片後 Matrix 正確顯示篩選結果 +- 視覺回饋清晰,使用者能直觀理解當前狀態 +- 篩選操作響應時間 < 1 秒 diff --git a/openspec/changes/archive/2026-01-27-wip-status-filter/tasks.md b/openspec/changes/archive/2026-01-27-wip-status-filter/tasks.md new file mode 100644 index 0000000..6717f85 --- /dev/null +++ b/openspec/changes/archive/2026-01-27-wip-status-filter/tasks.md @@ -0,0 +1,99 @@ +## Tasks + +### Phase 1: 後端 API 支援 + +- [x] **修改 wip_service.py - get_wip_matrix()** + - 新增 `status` 參數(Optional[str]) + - 加入 WIP 狀態篩選條件 + - RUN: `EQUIPMENTCOUNT > 0` + - HOLD: `EQUIPMENTCOUNT = 0 AND CURRENTHOLDCOUNT > 0` + - QUEUE: `EQUIPMENTCOUNT = 0 AND CURRENTHOLDCOUNT = 0` + +- [x] **修改 wip_routes.py - api_overview_matrix()** + - 解析 `status` query parameter + - 驗證 status 值(RUN/QUEUE/HOLD 或空) + - 傳遞給 get_wip_matrix() + +### Phase 2: 前端互動 + +- [x] **新增 CSS 樣式** + - `.wip-status-card` 加入 cursor: pointer 和 hover 效果 + - `.wip-status-card.active` 選中狀態樣式 + - 各狀態的選中陰影顏色 + +- [x] **修改 HTML 結構** + - 三張卡片加入 `onclick="toggleStatusFilter('xxx')"` + +- [x] **實作 JavaScript 邏輯** + - 新增 `activeStatusFilter` 狀態變數 + - 實作 `toggleStatusFilter(status)` 函數 + - 實作 `updateCardStyles()` 函數 + - 實作 `loadMatrixOnly()` 獨立載入 Matrix + - 修改 `fetchMatrix()` 支援 status 參數 + +### Phase 3: 整合與優化 + +- [x] **整合 loadAllData()** + - fetchMatrix() 內部已讀取 activeStatusFilter + - Summary 和 Hold Summary 不受影響 + +- [x] **Matrix 標題顯示篩選狀態** + - 實作 `updateMatrixTitle()` 函數 + - 有篩選時顯示 "- RUN Only" 等後綴 + +### Phase 4: WIP Detail 頁面整合 + +- [x] **增強視覺效果(wip_overview.html)** + - 更強的 active 狀態(scale 1.03, border 4px) + - 更深的背景色(#DCFCE7, #FEF3C7, #FEE2E2) + - 非選中卡片變暗(opacity: 0.5) + +- [x] **套用至 WIP Detail 頁面** + - 新增可點擊的 CSS 樣式到 summary cards + - 移除 Status 下拉選單(原本是 ACTIVE/HOLD) + - Summary cards 加入 onclick handlers + - 新增 `activeStatusFilter` 狀態變數 + - 實作 `toggleStatusFilter()`, `updateCardStyles()`, `updateTableTitle()` + - 修改 `fetchDetail()` 支援 status 參數 + - 新增 `loadTableOnly()` 獨立載入函數(避免 isLoading 阻擋切換) + +- [x] **後端 API 更新** + - 修改 `get_wip_detail()` 支援 RUN/QUEUE/HOLD 篩選 + - 修改 `api_detail()` 驗證 status 參數 + +### 驗證測試 + +- [x] **WIP Overview 功能測試** + - 點擊 RUN → Matrix 只顯示 RUN 數量 + - 點擊 QUEUE → Matrix 只顯示 QUEUE 數量 + - 點擊 HOLD → Matrix 只顯示 HOLD 數量 + - 再次點擊 → 恢復全部 + +- [x] **WIP Detail 功能測試** + - 點擊 RUN → Table 只顯示 RUN lots + - 點擊 QUEUE → Table 只顯示 QUEUE lots + - 點擊 HOLD → Table 只顯示 HOLD lots + - 再次點擊 → 恢復全部 + +- [x] **視覺測試** + - 選中卡片樣式正確(兩頁面) + - 非選中卡片變暗 + - 載入時有提示 + +- [x] **組合篩選測試** + - workorder + status 同時篩選 + - lotid + status 同時篩選 + - package + status 同時篩選(Detail) + +### Bug 修復 + +- [x] **WIP Detail 無法直接切換篩選狀態** + - 問題:`loadAllData()` 有 `isLoading` 保護,載入中點擊其他卡片會被忽略 + - 解法:新增 `loadTableOnly()` 獨立載入函數,與 WIP Overview 的 `loadMatrixOnly()` 行為一致 + +- [x] **快速切換篩選導致連線堆積 Timeout** + - 問題:連續快速點擊不同狀態卡片,多個請求同時發出,耗盡連線池 + - 解法:使用 `AbortController` 取消前一個進行中的請求 + - WIP Overview: `matrixAbortController` + `loadMatrixOnly()` 取消邏輯 + - WIP Detail: `tableAbortController` + `loadTableOnly()` 取消邏輯 + - `fetchWithTimeout()` 新增 `externalSignal` 參數支援外部取消 diff --git a/openspec/changes/db-connection-stability/design.md b/openspec/changes/db-connection-stability/design.md new file mode 100644 index 0000000..9aed434 --- /dev/null +++ b/openspec/changes/db-connection-stability/design.md @@ -0,0 +1,293 @@ +# Technical Design + +## Decision: 連線池策略 + +**選擇**: SQLAlchemy QueuePool(而非 oracledb SessionPool) + +**原因**: +- 現有程式碼已使用 SQLAlchemy +- QueuePool 提供內建的連線健康檢查(pool_pre_ping) +- 更容易與現有 `read_sql_df()` 整合 + +**設定**: +```python +create_engine( + CONNECTION_STRING, + pool_size=5, # 基本連線數 + max_overflow=10, # 額外連線數(尖峰時) + pool_timeout=30, # 等待連線的超時 + pool_recycle=1800, # 30分鐘回收連線 + pool_pre_ping=True, # 使用前檢查連線健康 +) +``` + +--- + +## Decision: Logging 策略 + +**選擇**: 使用 Python logging 模組 + 結構化格式 + +**Logger 配置**: +```python +import logging + +logger = logging.getLogger('mes_dashboard.database') +logger.setLevel(logging.INFO) + +# Format: [時間] [層級] [模組] 訊息 +formatter = logging.Formatter( + '%(asctime)s [%(levelname)s] %(name)s: %(message)s' +) +``` + +**記錄內容**: +- 連線成功/失敗(含 ORA 錯誤碼) +- 查詢時間(標記 >1s 為慢查詢) +- 重試次數 + +--- + +## Decision: 密碼 URL 編碼 + +**選擇**: 使用 `urllib.parse.quote_plus()` 編碼密碼 + +**實作**: +```python +from urllib.parse import quote_plus + +CONNECTION_STRING = ( + f"oracle+oracledb://{DB_USER}:{quote_plus(DB_PASSWORD)}" + f"@{DB_HOST}:{DB_PORT}/?service_name={DB_SERVICE}" +) +``` + +--- + +## Implementation Approach + +### 1. config/database.py 修改 + +```python +from urllib.parse import quote_plus + +# 安全的連線字串(密碼已編碼) +CONNECTION_STRING = ( + f"oracle+oracledb://{DB_USER}:{quote_plus(DB_PASSWORD)}" + f"@{DB_HOST}:{DB_PORT}/?service_name={DB_SERVICE}" +) +``` + +### 2. core/database.py 修改 + +```python +import logging +import time +from functools import wraps + +logger = logging.getLogger('mes_dashboard.database') + +def get_engine(): + global _ENGINE + if _ENGINE is None: + _ENGINE = create_engine( + CONNECTION_STRING, + pool_size=5, + max_overflow=10, + pool_timeout=30, + pool_recycle=1800, + pool_pre_ping=True, + connect_args={ + "tcp_connect_timeout": 15, + "retry_count": 2, + "retry_delay": 1, + } + ) + logger.info("Database engine created with QueuePool") + return _ENGINE + + +def read_sql_df(sql: str, params=None) -> pd.DataFrame: + """Execute SQL with timing and error logging.""" + start_time = time.time() + engine = get_engine() + + try: + with engine.connect() as conn: + df = pd.read_sql(text(sql), conn, params=params) + df.columns = [str(c).upper() for c in df.columns] + + elapsed = time.time() - start_time + if elapsed > 1.0: + logger.warning(f"Slow query ({elapsed:.2f}s): {sql[:100]}...") + else: + logger.debug(f"Query completed in {elapsed:.3f}s") + + return df + + except Exception as exc: + elapsed = time.time() - start_time + # 擷取 ORA 錯誤碼 + ora_code = _extract_ora_code(exc) + logger.error( + f"Query failed after {elapsed:.2f}s - " + f"ORA-{ora_code}: {exc}" + ) + raise + + +def _extract_ora_code(exc: Exception) -> str: + """從例外中擷取 ORA 錯誤碼.""" + import re + match = re.search(r'ORA-(\d+)', str(exc)) + return match.group(1) if match else 'UNKNOWN' +``` + +--- + +## Data Flow + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 改進後的連線流程 │ +├──────────────────────────────────────────────────────────────┤ +│ │ +│ API Request │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ read_sql_df() │ │ +│ │ - 記錄開始時間 │ │ +│ │ - 從 QueuePool 取得連線(或等待/新建) │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ pool_pre_ping (健康檢查) │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 執行查詢 │ │ +│ │ - 成功: 記錄查詢時間,標記慢查詢 │ │ +│ │ - 失敗: 記錄 ORA 錯誤碼,考慮重試 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ 連線歸還 Pool(不關閉) │ +│ ▼ │ +│ API Response │ +│ │ +└──────────────────────────────────────────────────────────────┘ +``` + +--- + +## Testing Strategy + +### 1. 單元測試 +- Mock engine 測試 logging 輸出 +- 測試 ORA 錯誤碼擷取 + +### 2. 整合測試 +- 驗證連線池行為(pool_size, max_overflow) +- 模擬連線失敗,驗證重試邏輯 + +### 3. 監控驗證 +- 確認 error.log 記錄 DB 錯誤 +- 確認慢查詢被標記 + +--- + +## 額外考量 + +### 連線池大小與 Gunicorn 配置協調 + +``` +Gunicorn: 2 workers × 4 threads = 8 併發 +Pool: pool_size=5 + max_overflow=10 = 最多 15 連線/worker + +實際最大連線數: 2 workers × 15 = 30 連線 +``` + +**注意**: 確認 Oracle 端 `SESSIONS` 參數足夠(通常 >100 沒問題) + +### 應用啟動時驗證連線 + +```python +def init_db(app): + app.teardown_appcontext(close_db) + + # 啟動時驗證連線 + try: + with get_engine().connect() as conn: + conn.execute(text("SELECT 1 FROM DUAL")) + logger.info("Database connection verified on startup") + except Exception as e: + logger.error(f"Database connection failed on startup: {e}") + # 不中斷啟動,讓應用可以在 DB 恢復後自動運作 +``` + +### Gunicorn preload_app 考量 + +```python +# gunicorn.conf.py +preload_app = False # 建議保持 False,讓每個 worker 建立自己的 pool +``` + +若啟用 `preload_app = True`,需在 `post_fork` hook 重新初始化 engine。 + +### 連線池監控(可觀測性) + +```python +from sqlalchemy import event + +@event.listens_for(engine, "checkout") +def log_checkout(dbapi_conn, connection_record, connection_proxy): + logger.debug("Connection checked out from pool") + +@event.listens_for(engine, "checkin") +def log_checkin(dbapi_conn, connection_record): + logger.debug("Connection returned to pool") +``` + +### Circuit Breaker 模式(Phase 3 可選) + +```python +class CircuitBreaker: + def __init__(self, failure_threshold=5, reset_timeout=60): + self.failures = 0 + self.last_failure = None + self.threshold = failure_threshold + self.reset_timeout = reset_timeout + + def is_open(self): + if self.failures >= self.threshold: + if datetime.now() - self.last_failure < timedelta(seconds=self.reset_timeout): + return True # 熔斷中,直接拒絕 + self.failures = 0 # 重置嘗試 + return False + + def record_failure(self): + self.failures += 1 + self.last_failure = datetime.now() + + def record_success(self): + self.failures = 0 +``` + +--- + +## 風險矩陣 + +| 項目 | 風險 | 優先級 | +|------|------|--------| +| 密碼 URL 編碼 | 低 | 高 | +| print → logging | 低 | 高 | +| 查詢時間統計 | 低 | 高 | +| NullPool → QueuePool | 中 | 高 | +| 啟動時驗證連線 | 低 | 中 | +| Pool 監控事件 | 低 | 低 | +| Circuit Breaker | 中 | 低 | + +--- + +## 實作順序建議 + +1. **Phase 1**(logging + URL 編碼)→ 先上線觀察 +2. **Phase 2**(連線池化 + 啟動驗證)→ 根據 Phase 1 的 log 調整參數 +3. **Phase 3**(Circuit Breaker + 監控)→ 視需求加入 diff --git a/openspec/changes/db-connection-stability/proposal.md b/openspec/changes/db-connection-stability/proposal.md new file mode 100644 index 0000000..618334d --- /dev/null +++ b/openspec/changes/db-connection-stability/proposal.md @@ -0,0 +1,56 @@ +# DB Connection Stability 提案 + +## 問題描述 + +目前資料庫連線架構存在以下問題,導致「看起來像連線不穩」的現象: + +### 1. 無連線池化(NullPool) +- 每次查詢都建立新連線 +- 高併發時造成連線風暴 +- 容易碰到 Oracle listener/SESSION 限制 + +### 2. 併發放大效應 +- Gunicorn 2 workers × 4 threads +- 前端同頁多 API 並行請求 + 定時刷新 +- 短時間大量新連線 + +### 3. 超時設定問題 +- Gunicorn worker timeout 60s +- 前端 fetch 超時 60s +- 查詢稍慢就會被中止,呈現 500/請求中斷 + +### 4. 密碼 URL 編碼問題 +- CONNECTION_STRING 未對密碼做 URL encode +- 若密碼含 `@:/?#` 等特殊字元會被誤解析 + +### 5. 錯誤紀錄不足 +- 連線失敗用 `print()` 而非 logging +- 錯誤不一定進入 error.log + +## 解決方案 + +### Phase 1: 結構化錯誤紀錄(低風險) +- 將所有 `print()` 改為 `logging` +- 記錄 ORA error code +- 加入查詢時間統計 + +### Phase 2: 連線池化(中風險) +- 改用 SQLAlchemy QueuePool +- 設定合理的 pool_size 和 max_overflow +- 加入 pool_pre_ping 健康檢查 + +### Phase 3: 重試策略優化(低風險) +- 增加 query-level retry +- 針對特定 ORA 錯誤碼進行重試 + +## 影響範圍 + +- `src/mes_dashboard/config/database.py` - 密碼 URL 編碼 +- `src/mes_dashboard/core/database.py` - 連線池、logging、重試 +- 所有使用 `read_sql_df()` 的 service + +## 風險評估 + +- Phase 1: 低風險,僅增加 logging +- Phase 2: 中風險,需測試連線池行為 +- Phase 3: 低風險,僅增加重試邏輯 diff --git a/openspec/changes/db-connection-stability/tasks.md b/openspec/changes/db-connection-stability/tasks.md new file mode 100644 index 0000000..fe8dbf7 --- /dev/null +++ b/openspec/changes/db-connection-stability/tasks.md @@ -0,0 +1,76 @@ +## Tasks + +### Phase 1: 結構化錯誤紀錄 + +- [x] **修改 config/database.py - 密碼 URL 編碼** + - 使用 `urllib.parse.quote_plus()` 編碼密碼 + - 防止特殊字元造成連線字串解析錯誤 + +- [x] **修改 core/database.py - 加入 logging** + - 將所有 `print()` 改為 `logging.logger` + - 設定 logger: `mes_dashboard.database` + - 記錄連線成功/失敗事件 + +- [x] **加入查詢時間統計** + - 在 `read_sql_df()` 加入計時 + - 標記 >1s 查詢為 WARNING(慢查詢) + - 正常查詢記錄為 DEBUG + +- [x] **加入 ORA 錯誤碼擷取** + - 從例外訊息擷取 ORA-XXXXX + - 錯誤訊息包含錯誤碼便於追蹤 + +- [x] **配置 logging handler** + - 在 app.py 加入 `_configure_logging()` 函數 + - 設定 `mes_dashboard` logger 輸出至 stderr + - Gunicorn `--capture-output` 會將 stderr 導向 error.log + +### Phase 2: 連線池化 + +- [x] **將 NullPool 改為 QueuePool** + - pool_size=5(基本連線數) + - max_overflow=10(尖峰額外連線) + - pool_timeout=30(等待連線超時) + - pool_recycle=1800(30 分鐘回收) + +- [x] **加入 pool_pre_ping** + - 使用連線前先做健康檢查 + - 避免使用已斷線的連線 + +- [x] **更新 Keep-Alive 機制** + - 改為定期 ping(每 5 分鐘)保持連線活躍 + - 防止防火牆/NAT 斷開閒置連線 + +### Phase 2.5: 啟動驗證與監控 + +- [x] **加入連線池監控事件** + - SQLAlchemy event: checkout / checkin / invalidate / connect + - 記錄 pool 使用狀況至 DEBUG log + +- [ ] **應用啟動時驗證連線** + - 在 init_db() 加入 SELECT 1 FROM DUAL 測試 + - 失敗時記錄錯誤但不中斷啟動 + +### Phase 3: 重試策略與熔斷(可選) + +- [ ] **加入 query-level retry decorator** + - 針對可重試的 ORA 錯誤碼(如 ORA-03113, ORA-03114) + - 最多重試 2 次,間隔 1 秒 + +- [ ] **Circuit Breaker 模式** + - 連續失敗 5 次後熔斷 60 秒 + - 熔斷期間直接返回錯誤,不嘗試連線 + +### 測試驗證 + +- [ ] **驗證 logging 輸出** + - 確認錯誤進入 error.log + - 確認慢查詢被記錄 + +- [ ] **驗證連線池行為** + - 併發請求時連線數符合預期 + - 閒置連線正確回收 + +- [ ] **壓力測試** + - 模擬高併發請求 + - 確認無連線洩漏