diff --git a/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/.openspec.yaml b/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/.openspec.yaml new file mode 100644 index 0000000..8b00a11 --- /dev/null +++ b/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-02 diff --git a/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/design.md b/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/design.md new file mode 100644 index 0000000..a6a315d --- /dev/null +++ b/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/design.md @@ -0,0 +1,78 @@ +## Context + +目前設備即時機況與設備歷史績效兩個頁面的 KPI 卡片設計不一致: + +**設備即時機況** (`resource_status.html`): +- 顯示 6 張卡片:總設備數、PRD、SBY、UDT/SDT(合併)、EGT、其他/未排程 +- 格式:台數 + 佔比% +- 資料來源:`/api/resource/status/summary` → 從 Redis 快取取得即時狀態 + +**設備歷史績效** (`resource_history.html`): +- 顯示 7 張卡片:OU%、Availability%、PRD、UDT、SDT、EGT、機台數 +- 格式:HR 數字(無佔比) +- 資料來源:`/api/resource/history/dashboard` → 從 Oracle DWH 查詢歷史資料 + +## Goals / Non-Goals + +**Goals:** +- 統一兩個頁面的卡片數量、排序、標籤 +- 即時機況新增 OU%、Availability%、NST 卡片,拆分 UDT/SDT +- 歷史績效新增 SBY、NST 卡片,所有狀態卡片新增佔比顯示 +- 統一佔比計算公式(分母包含 NST) + +**Non-Goals:** +- 不修改卡片的視覺樣式(顏色、大小、間距) +- 不修改資料表結構或快取機制 +- 不修改篩選器邏輯 + +## Decisions + +### 1. 卡片統一結構 + +**決定**:兩頁面統一為 9 張卡片,順序固定 + +| 順序 | 主標籤 | 副標籤 | 即時機況 | 歷史績效 | +|------|--------|--------|----------|----------| +| 1 | OU% | 稼動率 | 百分比 | 百分比 | +| 2 | Availability% | 可用率 | 百分比 | 百分比 | +| 3 | PRD | 生產 | 台數+佔比% | HR+佔比% | +| 4 | SBY | 待機 | 台數+佔比% | HR+佔比% | +| 5 | UDT | 非計畫停機 | 台數+佔比% | HR+佔比% | +| 6 | SDT | 計畫停機 | 台數+佔比% | HR+佔比% | +| 7 | EGT | 工程 | 台數+佔比% | HR+佔比% | +| 8 | NST | 未排程 | 台數+佔比% | HR+佔比% | +| 9 | 機台數 | 設備總數 | 總設備數 | 不重複機台 | + +**理由**:統一排序便於使用者快速比對兩頁面數據 + +### 2. 佔比計算公式 + +**決定**:佔比% = 該狀態 / (PRD + SBY + UDT + SDT + EGT + NST) × 100 + +**理由**:包含 NST 可呈現完整時間分布,與 Availability% 分母一致 + +### 3. 即時機況 OU% / Availability% 計算 + +**決定**: +- OU% = PRD台數 / (PRD + SBY + UDT + SDT + EGT) 台數 × 100 +- Availability% = (PRD + SBY + EGT) 台數 / 總設備數 × 100 + +**替代方案考量**: +- 使用即時狀態的累計時數計算 → 放棄,因即時快取無歷史時數資料 +- 使用機台數比例計算 → 採用,符合即時監控需求 + +### 4. 前端實作方式 + +**決定**:直接修改現有 HTML 模板的卡片區塊,不抽取為共用組件 + +**理由**: +- 兩頁面的資料格式不同(台數 vs HR),共用組件反增複雜度 +- 變更範圍有限,直接修改更直觀 + +## Risks / Trade-offs + +**[即時機況 OU% 定義與歷史績效不同]** → 在卡片副標籤或 tooltip 說明計算方式差異 + +**[NST 狀態在即時快取可能無資料]** → 後端需處理無 NST 狀態的情況,預設為 0 + +**[卡片數量增加影響響應式佈局]** → 需測試 9 張卡片在不同螢幕寬度的顯示效果 diff --git a/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/proposal.md b/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/proposal.md new file mode 100644 index 0000000..76300ac --- /dev/null +++ b/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/proposal.md @@ -0,0 +1,32 @@ +## Why + +設備即時機況與設備歷史績效兩個頁面的 KPI 卡片設計不一致,造成使用者在切換頁面時需要重新理解卡片含義。統一卡片設計可提升使用體驗並確保數據呈現的一致性。 + +## What Changes + +- **統一卡片數量與排序**:兩個頁面統一為 9 張卡片,順序相同 +- **統一卡片標籤**:主標籤(OU%、Availability%、PRD、SBY、UDT、SDT、EGT、NST、機台數)與副標籤(稼動率、可用率、生產、待機、非計畫停機、計畫停機、工程、未排程、設備總數) +- **即時機況新增指標**:新增 OU%、Availability%、NST 卡片;將 UDT/SDT 合併卡片拆分為獨立卡片 +- **歷史績效新增指標**:新增 SBY、NST 卡片;所有狀態卡片新增佔比顯示 +- **統一佔比計算**:佔比% = 該狀態 / (PRD + SBY + UDT + SDT + EGT + NST) × 100 + +## Capabilities + +### New Capabilities + +- `equipment-status-cards`: 統一的設備狀態 KPI 卡片組件規格,定義卡片結構、排序、標籤、計算公式 + +### Modified Capabilities + +(無既有規格需修改) + +## Impact + +- **前端模板**: + - `src/mes_dashboard/templates/resource_status.html` - 即時機況頁面卡片區塊 + - `src/mes_dashboard/templates/resource_history.html` - 歷史績效頁面卡片區塊 +- **後端服務**: + - `src/mes_dashboard/services/resource_service.py` - 即時機況 API 需新增 OU%、Availability% 計算 + - `src/mes_dashboard/services/resource_history_service.py` - 歷史績效 API 需新增 SBY、NST 欄位 +- **API 端點**: + - `/api/resource/status/summary` - 回傳資料結構調整 diff --git a/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/specs/equipment-status-cards/spec.md b/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/specs/equipment-status-cards/spec.md new file mode 100644 index 0000000..a8a4573 --- /dev/null +++ b/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/specs/equipment-status-cards/spec.md @@ -0,0 +1,109 @@ +## ADDED Requirements + +### Requirement: Unified Card Structure +兩個頁面(設備即時機況、設備歷史績效)的 KPI 卡片 SHALL 包含相同的 9 張卡片,順序固定如下: +1. OU%(稼動率) +2. Availability%(可用率) +3. PRD(生產) +4. SBY(待機) +5. UDT(非計畫停機) +6. SDT(計畫停機) +7. EGT(工程) +8. NST(未排程) +9. 機台數(設備總數) + +#### Scenario: Card order verification on real-time status page +- **WHEN** 使用者開啟設備即時機況頁面 +- **THEN** 頁面 SHALL 依序顯示 9 張卡片:OU%、Availability%、PRD、SBY、UDT、SDT、EGT、NST、機台數 + +#### Scenario: Card order verification on historical performance page +- **WHEN** 使用者開啟設備歷史績效頁面 +- **THEN** 頁面 SHALL 依序顯示 9 張卡片:OU%、Availability%、PRD、SBY、UDT、SDT、EGT、NST、機台數 + +### Requirement: Unified Card Labels +每張卡片 SHALL 顯示統一的主標籤與副標籤。 + +| 主標籤 | 副標籤 | +|--------|--------| +| OU% | 稼動率 | +| Availability% | 可用率 | +| PRD | 生產 | +| SBY | 待機 | +| UDT | 非計畫停機 | +| SDT | 計畫停機 | +| EGT | 工程 | +| NST | 未排程 | +| 機台數 | 設備總數 | + +#### Scenario: Label consistency across pages +- **WHEN** 使用者查看任一頁面的 PRD 卡片 +- **THEN** 主標籤 SHALL 為「PRD」,副標籤 SHALL 為「生產」 + +### Requirement: Real-time Status Card Display Format +設備即時機況頁面的狀態卡片(PRD、SBY、UDT、SDT、EGT、NST)SHALL 顯示台數與佔比。 + +#### Scenario: Real-time PRD card display +- **WHEN** 系統有 120 台設備,其中 42 台處於 PRD 狀態 +- **THEN** PRD 卡片 SHALL 顯示「42」作為主要數值,並顯示佔比百分比 + +#### Scenario: Real-time card with zero count +- **WHEN** 某狀態無任何機台 +- **THEN** 該狀態卡片 SHALL 顯示「0」作為主要數值,佔比顯示「0.0%」 + +### Requirement: Historical Performance Card Display Format +設備歷史績效頁面的狀態卡片(PRD、SBY、UDT、SDT、EGT、NST)SHALL 顯示小時數(HR)與佔比。 + +#### Scenario: Historical PRD card display +- **WHEN** 查詢期間 PRD 總時數為 1234 小時,總時數為 2800 小時 +- **THEN** PRD 卡片 SHALL 顯示「1,234 HR」或「1.2K HR」作為主要數值,並顯示佔比百分比 + +#### Scenario: Historical card large number formatting +- **WHEN** 某狀態時數 >= 1000 小時 +- **THEN** 該狀態卡片 SHALL 以 K 為單位顯示(如 1.2K HR) + +### Requirement: Status Percentage Calculation +狀態佔比 SHALL 使用以下公式計算: +``` +佔比% = 該狀態值 / (PRD + SBY + UDT + SDT + EGT + NST) × 100 +``` +分母包含所有 6 種狀態的總和。 + +#### Scenario: Percentage calculation with all statuses +- **WHEN** PRD=100, SBY=50, UDT=20, SDT=10, EGT=15, NST=5(總計 200) +- **THEN** PRD 佔比 SHALL 為 50.0%,SBY 佔比 SHALL 為 25.0% + +#### Scenario: Percentage when total is zero +- **WHEN** 所有狀態值皆為 0 +- **THEN** 所有狀態佔比 SHALL 顯示「--」或「0.0%」 + +### Requirement: Real-time OU Percentage Calculation +設備即時機況頁面的 OU% SHALL 使用以下公式計算: +``` +OU% = PRD台數 / (PRD + SBY + UDT + SDT + EGT) 台數 × 100 +``` +分母不包含 NST。 + +#### Scenario: Real-time OU calculation +- **WHEN** PRD=42, SBY=30, UDT=10, SDT=5, EGT=8, NST=25 台 +- **THEN** OU% SHALL 為 42/(42+30+10+5+8)×100 = 44.2% + +### Requirement: Real-time Availability Percentage Calculation +設備即時機況頁面的 Availability% SHALL 使用以下公式計算: +``` +Availability% = (PRD + SBY + EGT) 台數 / 總設備數 × 100 +``` + +#### Scenario: Real-time Availability calculation +- **WHEN** PRD=42, SBY=30, EGT=8,總設備數=120 台 +- **THEN** Availability% SHALL 為 (42+30+8)/120×100 = 66.7% + +### Requirement: Machine Count Card +機台數卡片 SHALL 顯示設備總數。 + +#### Scenario: Real-time machine count +- **WHEN** 使用者查看設備即時機況頁面 +- **THEN** 機台數卡片 SHALL 顯示符合篩選條件的總設備數 + +#### Scenario: Historical machine count +- **WHEN** 使用者查看設備歷史績效頁面 +- **THEN** 機台數卡片 SHALL 顯示查詢期間內不重複的機台數量 diff --git a/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/tasks.md b/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/tasks.md new file mode 100644 index 0000000..ea41fc2 --- /dev/null +++ b/openspec/changes/archive/2026-02-02-unify-equipment-status-cards/tasks.md @@ -0,0 +1,40 @@ +## 1. 後端 API 調整 + +- [x] 1.1 修改 `resource_service.py` 的 `get_resource_status_summary()` 函數,新增 OU%、Availability% 計算邏輯 +- [x] 1.2 修改 `resource_service.py` 回傳資料結構,將 UDT/SDT 分開統計,新增 NST 統計 +- [x] 1.3 修改 `resource_history_service.py` 的 `_build_kpi_from_df()` 函數,新增 SBY、NST 欄位 +- [x] 1.4 修改 `resource_history_service.py` 新增各狀態佔比計算邏輯 + +## 2. 設備即時機況前端 + +- [x] 2.1 修改 `resource_status.html` 卡片 HTML 結構,調整為 9 張卡片 +- [x] 2.2 新增 OU%、Availability% 卡片 +- [x] 2.3 將 UDT/SDT 合併卡片拆分為兩張獨立卡片 +- [x] 2.4 新增 NST 卡片 +- [x] 2.5 調整機台數卡片位置至最後 +- [x] 2.6 更新 `loadSummary()` JavaScript 函數,綁定新的資料欄位 +- [x] 2.7 統一所有卡片的主標籤與副標籤文字 + +## 3. 設備歷史績效前端 + +- [x] 3.1 修改 `resource_history.html` 卡片 HTML 結構,調整為 9 張卡片 +- [x] 3.2 新增 SBY、NST 卡片 +- [x] 3.3 為所有狀態卡片新增佔比顯示區域 +- [x] 3.4 調整機台數卡片位置至最後 +- [x] 3.5 更新 `updateKpiCards()` JavaScript 函數,綁定新的資料欄位與佔比 +- [x] 3.6 統一所有卡片的主標籤與副標籤文字 + +## 4. 測試與驗證 + +- [x] 4.1 驗證設備即時機況頁面 9 張卡片顯示正確 +- [x] 4.2 驗證設備歷史績效頁面 9 張卡片顯示正確 +- [x] 4.3 驗證兩頁面卡片排序一致 +- [x] 4.4 驗證佔比計算公式正確(分母含 NST) +- [x] 4.5 驗證 OU%、Availability% 計算正確 +- [x] 4.6 測試無資料或零值情況的顯示 + +## 備註 + +- 即時機況的「機台數」= resource_cache 中的總設備數 +- 歷史績效的「機台數」= 查詢時間範圍內在 SHIFT 表中有資料的不重複機台數 +- 兩者數量可能有差異(例如新設備或閒置設備),屬預期行為 diff --git a/openspec/specs/equipment-status-cards/spec.md b/openspec/specs/equipment-status-cards/spec.md new file mode 100644 index 0000000..a8a4573 --- /dev/null +++ b/openspec/specs/equipment-status-cards/spec.md @@ -0,0 +1,109 @@ +## ADDED Requirements + +### Requirement: Unified Card Structure +兩個頁面(設備即時機況、設備歷史績效)的 KPI 卡片 SHALL 包含相同的 9 張卡片,順序固定如下: +1. OU%(稼動率) +2. Availability%(可用率) +3. PRD(生產) +4. SBY(待機) +5. UDT(非計畫停機) +6. SDT(計畫停機) +7. EGT(工程) +8. NST(未排程) +9. 機台數(設備總數) + +#### Scenario: Card order verification on real-time status page +- **WHEN** 使用者開啟設備即時機況頁面 +- **THEN** 頁面 SHALL 依序顯示 9 張卡片:OU%、Availability%、PRD、SBY、UDT、SDT、EGT、NST、機台數 + +#### Scenario: Card order verification on historical performance page +- **WHEN** 使用者開啟設備歷史績效頁面 +- **THEN** 頁面 SHALL 依序顯示 9 張卡片:OU%、Availability%、PRD、SBY、UDT、SDT、EGT、NST、機台數 + +### Requirement: Unified Card Labels +每張卡片 SHALL 顯示統一的主標籤與副標籤。 + +| 主標籤 | 副標籤 | +|--------|--------| +| OU% | 稼動率 | +| Availability% | 可用率 | +| PRD | 生產 | +| SBY | 待機 | +| UDT | 非計畫停機 | +| SDT | 計畫停機 | +| EGT | 工程 | +| NST | 未排程 | +| 機台數 | 設備總數 | + +#### Scenario: Label consistency across pages +- **WHEN** 使用者查看任一頁面的 PRD 卡片 +- **THEN** 主標籤 SHALL 為「PRD」,副標籤 SHALL 為「生產」 + +### Requirement: Real-time Status Card Display Format +設備即時機況頁面的狀態卡片(PRD、SBY、UDT、SDT、EGT、NST)SHALL 顯示台數與佔比。 + +#### Scenario: Real-time PRD card display +- **WHEN** 系統有 120 台設備,其中 42 台處於 PRD 狀態 +- **THEN** PRD 卡片 SHALL 顯示「42」作為主要數值,並顯示佔比百分比 + +#### Scenario: Real-time card with zero count +- **WHEN** 某狀態無任何機台 +- **THEN** 該狀態卡片 SHALL 顯示「0」作為主要數值,佔比顯示「0.0%」 + +### Requirement: Historical Performance Card Display Format +設備歷史績效頁面的狀態卡片(PRD、SBY、UDT、SDT、EGT、NST)SHALL 顯示小時數(HR)與佔比。 + +#### Scenario: Historical PRD card display +- **WHEN** 查詢期間 PRD 總時數為 1234 小時,總時數為 2800 小時 +- **THEN** PRD 卡片 SHALL 顯示「1,234 HR」或「1.2K HR」作為主要數值,並顯示佔比百分比 + +#### Scenario: Historical card large number formatting +- **WHEN** 某狀態時數 >= 1000 小時 +- **THEN** 該狀態卡片 SHALL 以 K 為單位顯示(如 1.2K HR) + +### Requirement: Status Percentage Calculation +狀態佔比 SHALL 使用以下公式計算: +``` +佔比% = 該狀態值 / (PRD + SBY + UDT + SDT + EGT + NST) × 100 +``` +分母包含所有 6 種狀態的總和。 + +#### Scenario: Percentage calculation with all statuses +- **WHEN** PRD=100, SBY=50, UDT=20, SDT=10, EGT=15, NST=5(總計 200) +- **THEN** PRD 佔比 SHALL 為 50.0%,SBY 佔比 SHALL 為 25.0% + +#### Scenario: Percentage when total is zero +- **WHEN** 所有狀態值皆為 0 +- **THEN** 所有狀態佔比 SHALL 顯示「--」或「0.0%」 + +### Requirement: Real-time OU Percentage Calculation +設備即時機況頁面的 OU% SHALL 使用以下公式計算: +``` +OU% = PRD台數 / (PRD + SBY + UDT + SDT + EGT) 台數 × 100 +``` +分母不包含 NST。 + +#### Scenario: Real-time OU calculation +- **WHEN** PRD=42, SBY=30, UDT=10, SDT=5, EGT=8, NST=25 台 +- **THEN** OU% SHALL 為 42/(42+30+10+5+8)×100 = 44.2% + +### Requirement: Real-time Availability Percentage Calculation +設備即時機況頁面的 Availability% SHALL 使用以下公式計算: +``` +Availability% = (PRD + SBY + EGT) 台數 / 總設備數 × 100 +``` + +#### Scenario: Real-time Availability calculation +- **WHEN** PRD=42, SBY=30, EGT=8,總設備數=120 台 +- **THEN** Availability% SHALL 為 (42+30+8)/120×100 = 66.7% + +### Requirement: Machine Count Card +機台數卡片 SHALL 顯示設備總數。 + +#### Scenario: Real-time machine count +- **WHEN** 使用者查看設備即時機況頁面 +- **THEN** 機台數卡片 SHALL 顯示符合篩選條件的總設備數 + +#### Scenario: Historical machine count +- **WHEN** 使用者查看設備歷史績效頁面 +- **THEN** 機台數卡片 SHALL 顯示查詢期間內不重複的機台數量 diff --git a/src/mes_dashboard/services/resource_history_service.py b/src/mes_dashboard/services/resource_history_service.py index 3148cf6..0b345a7 100644 --- a/src/mes_dashboard/services/resource_history_service.py +++ b/src/mes_dashboard/services/resource_history_service.py @@ -675,6 +675,11 @@ def _calc_availability_pct(prd: float, sby: float, udt: float, sdt: float, egt: return round(numerator / denominator * 100, 1) if denominator > 0 else 0 +def _calc_status_pct(value: float, total: float) -> float: + """Calculate status percentage = value / total * 100.""" + return round(value / total * 100, 1) if total > 0 else 0 + + def _build_kpi_from_df(df: pd.DataFrame) -> Dict[str, Any]: """Build KPI dict from query result DataFrame.""" if df is None or len(df) == 0: @@ -682,11 +687,17 @@ def _build_kpi_from_df(df: pd.DataFrame) -> Dict[str, Any]: 'ou_pct': 0, 'availability_pct': 0, 'prd_hours': 0, + 'prd_pct': 0, 'sby_hours': 0, + 'sby_pct': 0, 'udt_hours': 0, + 'udt_pct': 0, 'sdt_hours': 0, + 'sdt_pct': 0, 'egt_hours': 0, + 'egt_pct': 0, 'nst_hours': 0, + 'nst_pct': 0, 'machine_count': 0 } @@ -699,15 +710,24 @@ def _build_kpi_from_df(df: pd.DataFrame) -> Dict[str, Any]: nst = _safe_float(row['NST_HOURS']) machine_count = int(_safe_float(row['MACHINE_COUNT'])) + # Total hours for percentage calculation (includes NST) + total_hours = prd + sby + udt + sdt + egt + nst + return { 'ou_pct': _calc_ou_pct(prd, sby, udt, sdt, egt), 'availability_pct': _calc_availability_pct(prd, sby, udt, sdt, egt, nst), 'prd_hours': round(prd, 1), + 'prd_pct': _calc_status_pct(prd, total_hours), 'sby_hours': round(sby, 1), + 'sby_pct': _calc_status_pct(sby, total_hours), 'udt_hours': round(udt, 1), + 'udt_pct': _calc_status_pct(udt, total_hours), 'sdt_hours': round(sdt, 1), + 'sdt_pct': _calc_status_pct(sdt, total_hours), 'egt_hours': round(egt, 1), + 'egt_pct': _calc_status_pct(egt, total_hours), 'nst_hours': round(nst, 1), + 'nst_pct': _calc_status_pct(nst, total_hours), 'machine_count': machine_count } diff --git a/src/mes_dashboard/services/resource_service.py b/src/mes_dashboard/services/resource_service.py index 46fa595..5b876af 100644 --- a/src/mes_dashboard/services/resource_service.py +++ b/src/mes_dashboard/services/resource_service.py @@ -543,7 +543,7 @@ def get_resource_status_summary( is_monitor: Filter by PJ_ISMONITOR flag Returns: - Dict with summary statistics. + Dict with summary statistics including OU%, Availability%, and per-status counts. """ # Get merged data with filters (except status_categories) data = get_merged_resource_status( @@ -557,17 +557,29 @@ def get_resource_status_summary( return { 'total_count': 0, 'by_status_category': {}, + 'by_status': {}, 'by_workcenter_group': {}, 'with_active_job': 0, 'with_wip': 0, + 'ou_pct': 0, + 'availability_pct': 0, } - # Count by status category + # Count by status category (for backward compatibility) by_status_category = {} for record in data: cat = record.get('STATUS_CATEGORY') or 'UNKNOWN' by_status_category[cat] = by_status_category.get(cat, 0) + 1 + # Count by individual E10 status (PRD, SBY, UDT, SDT, EGT, NST) + by_status = {'PRD': 0, 'SBY': 0, 'UDT': 0, 'SDT': 0, 'EGT': 0, 'NST': 0, 'OTHER': 0} + for record in data: + status = record.get('EQUIPMENTASSETSSTATUS') or 'UNKNOWN' + if status in by_status: + by_status[status] += 1 + else: + by_status['OTHER'] += 1 + # Count by workcenter group by_workcenter_group = {} for record in data: @@ -580,12 +592,30 @@ def get_resource_status_summary( # Count with WIP with_wip = sum(1 for r in data if (r.get('LOT_COUNT') or 0) > 0) + # Calculate OU% = PRD / (PRD + SBY + UDT + SDT + EGT) * 100 + prd = by_status['PRD'] + sby = by_status['SBY'] + udt = by_status['UDT'] + sdt = by_status['SDT'] + egt = by_status['EGT'] + nst = by_status['NST'] + + ou_denominator = prd + sby + udt + sdt + egt + ou_pct = round(prd / ou_denominator * 100, 1) if ou_denominator > 0 else 0 + + # Calculate Availability% = (PRD + SBY + EGT) / total * 100 + total_count = len(data) + availability_pct = round((prd + sby + egt) / total_count * 100, 1) if total_count > 0 else 0 + return { - 'total_count': len(data), + 'total_count': total_count, 'by_status_category': by_status_category, + 'by_status': by_status, 'by_workcenter_group': by_workcenter_group, 'with_active_job': with_active_job, 'with_wip': with_wip, + 'ou_pct': ou_pct, + 'availability_pct': availability_pct, } diff --git a/src/mes_dashboard/templates/resource_history.html b/src/mes_dashboard/templates/resource_history.html index ddbdcdd..6682a39 100644 --- a/src/mes_dashboard/templates/resource_history.html +++ b/src/mes_dashboard/templates/resource_history.html @@ -290,41 +290,63 @@ /* KPI Cards */ .kpi-row { display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 14px; + grid-template-columns: repeat(9, 1fr); + gap: 12px; margin-bottom: 16px; } + @media (max-width: 1400px) { + .kpi-row { + grid-template-columns: repeat(5, 1fr); + } + } + + @media (max-width: 900px) { + .kpi-row { + grid-template-columns: repeat(3, 1fr); + } + } + .kpi-card { background: var(--card-bg); border-radius: 10px; - padding: 18px; + padding: 14px 10px; text-align: center; border: 1px solid var(--border); box-shadow: var(--shadow); } .kpi-label { - font-size: 13px; + font-size: 12px; color: var(--muted); - margin-bottom: 6px; + margin-bottom: 4px; } .kpi-value { - font-size: 28px; + font-size: 24px; font-weight: bold; } .kpi-value.green { color: var(--success); } .kpi-value.blue { color: var(--primary); } + .kpi-value.prd { color: #3B82F6; } + .kpi-value.cyan { color: #06b6d4; } .kpi-value.red { color: var(--danger); } .kpi-value.yellow { color: var(--warning); } + .kpi-value.purple { color: #8b5cf6; } + .kpi-value.slate { color: #94a3b8; } .kpi-value.gray { color: var(--neutral); } .kpi-sub { + font-size: 10px; + color: var(--muted); + margin-top: 2px; + } + + .kpi-pct { font-size: 11px; color: var(--muted); - margin-top: 4px; + margin-top: 2px; } /* Charts Row */ @@ -614,29 +636,39 @@
可用率
-
PRD 時數
-
--
-
生產時間
+
PRD
+
--
+
生產
-
UDT 時數
+
SBY
+
--
+
待機
+
+
+
UDT
--
-
非計畫停機
+
非計畫停機
-
SDT 時數
+
SDT
--
-
計畫停機
+
計畫停機
-
EGT 時數
-
--
-
工程時間
+
EGT
+
--
+
工程
+
+
+
NST
+
--
+
未排程
機台數
--
-
不重複機台
+
設備總數
@@ -1000,12 +1032,35 @@ // KPI Cards // ============================================================ function updateKpiCards(kpi) { + // OU% and Availability% document.getElementById('kpiOuPct').textContent = kpi.ou_pct + '%'; document.getElementById('kpiAvailabilityPct').textContent = kpi.availability_pct + '%'; + + // PRD document.getElementById('kpiPrdHours').textContent = formatHours(kpi.prd_hours); + document.getElementById('kpiPrdPct').textContent = `生產 (${kpi.prd_pct || 0}%)`; + + // SBY + document.getElementById('kpiSbyHours').textContent = formatHours(kpi.sby_hours); + document.getElementById('kpiSbyPct').textContent = `待機 (${kpi.sby_pct || 0}%)`; + + // UDT document.getElementById('kpiUdtHours').textContent = formatHours(kpi.udt_hours); + document.getElementById('kpiUdtPct').textContent = `非計畫停機 (${kpi.udt_pct || 0}%)`; + + // SDT document.getElementById('kpiSdtHours').textContent = formatHours(kpi.sdt_hours); + document.getElementById('kpiSdtPct').textContent = `計畫停機 (${kpi.sdt_pct || 0}%)`; + + // EGT document.getElementById('kpiEgtHours').textContent = formatHours(kpi.egt_hours); + document.getElementById('kpiEgtPct').textContent = `工程 (${kpi.egt_pct || 0}%)`; + + // NST + document.getElementById('kpiNstHours').textContent = formatHours(kpi.nst_hours); + document.getElementById('kpiNstPct').textContent = `未排程 (${kpi.nst_pct || 0}%)`; + + // Machine count document.getElementById('kpiMachineCount').textContent = kpi.machine_count.toLocaleString(); } diff --git a/src/mes_dashboard/templates/resource_status.html b/src/mes_dashboard/templates/resource_status.html index 58e1cb7..c135d75 100644 --- a/src/mes_dashboard/templates/resource_status.html +++ b/src/mes_dashboard/templates/resource_status.html @@ -153,15 +153,27 @@ /* Summary Cards */ .summary-grid { display: grid; - grid-template-columns: repeat(6, 1fr); - gap: 12px; + grid-template-columns: repeat(9, 1fr); + gap: 10px; margin-bottom: 16px; } + @media (max-width: 1400px) { + .summary-grid { + grid-template-columns: repeat(5, 1fr); + } + } + + @media (max-width: 900px) { + .summary-grid { + grid-template-columns: repeat(3, 1fr); + } + } + .summary-card { background: var(--card-bg); border-radius: 10px; - padding: 16px; + padding: 14px 10px; text-align: center; border: 1px solid var(--border); box-shadow: var(--shadow); @@ -178,12 +190,15 @@ height: 3px; } - .summary-card.total::before { background: var(--primary); } - .summary-card.productive::before { background: var(--success); } + .summary-card.ou::before { background: var(--primary); } + .summary-card.availability::before { background: var(--success); } + .summary-card.productive::before { background: #3B82F6; } .summary-card.standby::before { background: var(--info); } - .summary-card.down::before { background: var(--danger); } + .summary-card.udt::before { background: var(--danger); } + .summary-card.sdt::before { background: var(--warning); } .summary-card.engineering::before { background: var(--purple); } - .summary-card.other::before { background: var(--muted); } + .summary-card.nst::before { background: #94a3b8; } + .summary-card.total::before { background: var(--muted); } .summary-label { font-size: 12px; @@ -198,12 +213,15 @@ font-weight: 700; } - .summary-card.total .summary-value { color: var(--primary); } - .summary-card.productive .summary-value { color: var(--success); } + .summary-card.ou .summary-value { color: var(--primary); } + .summary-card.availability .summary-value { color: var(--success); } + .summary-card.productive .summary-value { color: #3B82F6; } .summary-card.standby .summary-value { color: var(--info); } - .summary-card.down .summary-value { color: var(--danger); } + .summary-card.udt .summary-value { color: var(--danger); } + .summary-card.sdt .summary-value { color: var(--warning); } .summary-card.engineering .summary-value { color: var(--purple); } - .summary-card.other .summary-value { color: var(--muted); } + .summary-card.nst .summary-value { color: #94a3b8; } + .summary-card.total .summary-value { color: var(--muted); } .summary-pct { font-size: 12px; @@ -591,34 +609,50 @@
-
-
總設備數
-
--
+
+
OU%
+
--
+
稼動率
+
+
+
Availability%
+
--
+
可用率
-
生產中 (PRD)
-
--
-
--
+
PRD
+
--
+
生產
-
待機 (SBY)
-
--
-
--
+
SBY
+
--
+
待機
-
-
停機 (UDT/SDT)
-
--
-
--
+
+
UDT
+
--
+
非計畫停機
+
+
+
SDT
+
--
+
計畫停機
-
工程 (EGT)
-
--
-
--
+
EGT
+
--
+
工程
-
-
其他/未排程
-
--
-
--
+
+
NST
+
--
+
未排程
+
+
+
機台數
+
--
+
設備總數
@@ -717,25 +751,44 @@ if (result.success) { const d = result.data; const total = d.total_count || 0; - const cats = d.by_status_category || {}; + const status = d.by_status || {}; - const productive = cats.PRODUCTIVE || 0; - const standby = cats.STANDBY || 0; - const down = (cats.DOWN || 0); - const eng = cats.ENGINEERING || 0; - const other = (cats.NOT_SCHEDULED || 0) + (cats.INACTIVE || 0) + (cats.OTHER || 0) + (cats.UNKNOWN || 0); + // Get individual status counts + const prd = status.PRD || 0; + const sby = status.SBY || 0; + const udt = status.UDT || 0; + const sdt = status.SDT || 0; + const egt = status.EGT || 0; + const nst = status.NST || 0; + // Calculate percentage denominator (includes NST) + const totalStatus = prd + sby + udt + sdt + egt + nst; + + // Update OU% and Availability% + document.getElementById('ouPct').textContent = d.ou_pct ? `${d.ou_pct}%` : '--'; + document.getElementById('availabilityPct').textContent = d.availability_pct ? `${d.availability_pct}%` : '--'; + + // Update status cards with count and percentage + document.getElementById('prdCount').textContent = prd; + document.getElementById('prdPct').textContent = totalStatus ? `生產 (${((prd/totalStatus)*100).toFixed(1)}%)` : '生產'; + + document.getElementById('sbyCount').textContent = sby; + document.getElementById('sbyPct').textContent = totalStatus ? `待機 (${((sby/totalStatus)*100).toFixed(1)}%)` : '待機'; + + document.getElementById('udtCount').textContent = udt; + document.getElementById('udtPct').textContent = totalStatus ? `非計畫停機 (${((udt/totalStatus)*100).toFixed(1)}%)` : '非計畫停機'; + + document.getElementById('sdtCount').textContent = sdt; + document.getElementById('sdtPct').textContent = totalStatus ? `計畫停機 (${((sdt/totalStatus)*100).toFixed(1)}%)` : '計畫停機'; + + document.getElementById('egtCount').textContent = egt; + document.getElementById('egtPct').textContent = totalStatus ? `工程 (${((egt/totalStatus)*100).toFixed(1)}%)` : '工程'; + + document.getElementById('nstCount').textContent = nst; + document.getElementById('nstPct').textContent = totalStatus ? `未排程 (${((nst/totalStatus)*100).toFixed(1)}%)` : '未排程'; + + // Update total count document.getElementById('totalCount').textContent = total; - document.getElementById('productiveCount').textContent = productive; - document.getElementById('productivePct').textContent = total ? `${((productive/total)*100).toFixed(1)}%` : '--'; - document.getElementById('standbyCount').textContent = standby; - document.getElementById('standbyPct').textContent = total ? `${((standby/total)*100).toFixed(1)}%` : '--'; - document.getElementById('downCount').textContent = down; - document.getElementById('downPct').textContent = total ? `${((down/total)*100).toFixed(1)}%` : '--'; - document.getElementById('engCount').textContent = eng; - document.getElementById('engPct').textContent = total ? `${((eng/total)*100).toFixed(1)}%` : '--'; - document.getElementById('otherCount').textContent = other; - document.getElementById('otherPct').textContent = total ? `${((other/total)*100).toFixed(1)}%` : '--'; } } catch (e) { console.error('載入摘要失敗:', e); @@ -1020,12 +1073,14 @@ const dot = document.getElementById('cacheDot'); const status = document.getElementById('cacheStatus'); + const resCache = data.resource_cache || {}; const eqCache = data.equipment_status_cache || {}; - if (eqCache.enabled && eqCache.loaded) { + // 使用 resource_cache 的數量(過濾後的設備數) + if (resCache.enabled && resCache.loaded) { dot.className = 'cache-dot'; - status.textContent = `快取正常 (${eqCache.count} 筆)`; - } else if (eqCache.enabled) { + status.textContent = `快取正常 (${resCache.count} 筆)`; + } else if (resCache.enabled) { dot.className = 'cache-dot loading'; status.textContent = '快取載入中...'; } else { @@ -1033,6 +1088,7 @@ status.textContent = '快取未啟用'; } + // 使用 equipment_status_cache 的更新時間(即時狀態更新時間) if (eqCache.updated_at) { document.getElementById('lastUpdate').textContent = `更新: ${new Date(eqCache.updated_at).toLocaleString('zh-TW')}`;