feat: 統一設備即時機況與歷史績效頁面的 KPI 卡片
- 統一 9 張卡片順序: OU%, Availability%, PRD, SBY, UDT, SDT, EGT, NST, 機台數 - 即時機況顯示機台數與佔比%, 歷史績效顯示 HR 與佔比% - 佔比計算分母包含 NST (PRD+SBY+UDT+SDT+EGT+NST) - 新增 by_status 字典提供各 E10 狀態的獨立計數 - 統一快取狀態顯示使用過濾後的設備數 (resource_cache) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-02
|
||||
@@ -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 張卡片在不同螢幕寬度的顯示效果
|
||||
@@ -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` - 回傳資料結構調整
|
||||
@@ -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 顯示查詢期間內不重複的機台數量
|
||||
@@ -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 表中有資料的不重複機台數
|
||||
- 兩者數量可能有差異(例如新設備或閒置設備),屬預期行為
|
||||
109
openspec/specs/equipment-status-cards/spec.md
Normal file
109
openspec/specs/equipment-status-cards/spec.md
Normal file
@@ -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 顯示查詢期間內不重複的機台數量
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
<div class="kpi-sub">可用率</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">PRD 時數</div>
|
||||
<div class="kpi-value" style="color: #3B82F6;" id="kpiPrdHours">--</div>
|
||||
<div class="kpi-sub">生產時間</div>
|
||||
<div class="kpi-label">PRD</div>
|
||||
<div class="kpi-value prd" id="kpiPrdHours">--</div>
|
||||
<div class="kpi-pct" id="kpiPrdPct">生產</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">UDT 時數</div>
|
||||
<div class="kpi-label">SBY</div>
|
||||
<div class="kpi-value cyan" id="kpiSbyHours">--</div>
|
||||
<div class="kpi-pct" id="kpiSbyPct">待機</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">UDT</div>
|
||||
<div class="kpi-value red" id="kpiUdtHours">--</div>
|
||||
<div class="kpi-sub">非計畫停機</div>
|
||||
<div class="kpi-pct" id="kpiUdtPct">非計畫停機</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">SDT 時數</div>
|
||||
<div class="kpi-label">SDT</div>
|
||||
<div class="kpi-value yellow" id="kpiSdtHours">--</div>
|
||||
<div class="kpi-sub">計畫停機</div>
|
||||
<div class="kpi-pct" id="kpiSdtPct">計畫停機</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">EGT 時數</div>
|
||||
<div class="kpi-value" style="color: #8b5cf6;" id="kpiEgtHours">--</div>
|
||||
<div class="kpi-sub">工程時間</div>
|
||||
<div class="kpi-label">EGT</div>
|
||||
<div class="kpi-value purple" id="kpiEgtHours">--</div>
|
||||
<div class="kpi-pct" id="kpiEgtPct">工程</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">NST</div>
|
||||
<div class="kpi-value slate" id="kpiNstHours">--</div>
|
||||
<div class="kpi-pct" id="kpiNstPct">未排程</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">機台數</div>
|
||||
<div class="kpi-value gray" id="kpiMachineCount">--</div>
|
||||
<div class="kpi-sub">不重複機台</div>
|
||||
<div class="kpi-sub">設備總數</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card total">
|
||||
<div class="summary-label">總設備數</div>
|
||||
<div class="summary-value" id="totalCount">--</div>
|
||||
<div class="summary-card ou">
|
||||
<div class="summary-label">OU%</div>
|
||||
<div class="summary-value" id="ouPct">--</div>
|
||||
<div class="summary-pct">稼動率</div>
|
||||
</div>
|
||||
<div class="summary-card availability">
|
||||
<div class="summary-label">Availability%</div>
|
||||
<div class="summary-value" id="availabilityPct">--</div>
|
||||
<div class="summary-pct">可用率</div>
|
||||
</div>
|
||||
<div class="summary-card productive">
|
||||
<div class="summary-label">生產中 (PRD)</div>
|
||||
<div class="summary-value" id="productiveCount">--</div>
|
||||
<div class="summary-pct" id="productivePct">--</div>
|
||||
<div class="summary-label">PRD</div>
|
||||
<div class="summary-value" id="prdCount">--</div>
|
||||
<div class="summary-pct" id="prdPct">生產</div>
|
||||
</div>
|
||||
<div class="summary-card standby">
|
||||
<div class="summary-label">待機 (SBY)</div>
|
||||
<div class="summary-value" id="standbyCount">--</div>
|
||||
<div class="summary-pct" id="standbyPct">--</div>
|
||||
<div class="summary-label">SBY</div>
|
||||
<div class="summary-value" id="sbyCount">--</div>
|
||||
<div class="summary-pct" id="sbyPct">待機</div>
|
||||
</div>
|
||||
<div class="summary-card down">
|
||||
<div class="summary-label">停機 (UDT/SDT)</div>
|
||||
<div class="summary-value" id="downCount">--</div>
|
||||
<div class="summary-pct" id="downPct">--</div>
|
||||
<div class="summary-card udt">
|
||||
<div class="summary-label">UDT</div>
|
||||
<div class="summary-value" id="udtCount">--</div>
|
||||
<div class="summary-pct" id="udtPct">非計畫停機</div>
|
||||
</div>
|
||||
<div class="summary-card sdt">
|
||||
<div class="summary-label">SDT</div>
|
||||
<div class="summary-value" id="sdtCount">--</div>
|
||||
<div class="summary-pct" id="sdtPct">計畫停機</div>
|
||||
</div>
|
||||
<div class="summary-card engineering">
|
||||
<div class="summary-label">工程 (EGT)</div>
|
||||
<div class="summary-value" id="engCount">--</div>
|
||||
<div class="summary-pct" id="engPct">--</div>
|
||||
<div class="summary-label">EGT</div>
|
||||
<div class="summary-value" id="egtCount">--</div>
|
||||
<div class="summary-pct" id="egtPct">工程</div>
|
||||
</div>
|
||||
<div class="summary-card other">
|
||||
<div class="summary-label">其他/未排程</div>
|
||||
<div class="summary-value" id="otherCount">--</div>
|
||||
<div class="summary-pct" id="otherPct">--</div>
|
||||
<div class="summary-card nst">
|
||||
<div class="summary-label">NST</div>
|
||||
<div class="summary-value" id="nstCount">--</div>
|
||||
<div class="summary-pct" id="nstPct">未排程</div>
|
||||
</div>
|
||||
<div class="summary-card total">
|
||||
<div class="summary-label">機台數</div>
|
||||
<div class="summary-value" id="totalCount">--</div>
|
||||
<div class="summary-pct">設備總數</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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')}`;
|
||||
|
||||
Reference in New Issue
Block a user