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:
beabigegg
2026-02-02 13:40:21 +08:00
parent 2a1cda30bd
commit 88d30065cd
10 changed files with 602 additions and 71 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-02

View File

@@ -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 張卡片在不同螢幕寬度的顯示效果

View File

@@ -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` - 回傳資料結構調整

View 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、NSTSHALL 顯示台數與佔比。
#### 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、NSTSHALL 顯示小時數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 顯示查詢期間內不重複的機台數量

View File

@@ -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 表中有資料的不重複機台數
- 兩者數量可能有差異(例如新設備或閒置設備),屬預期行為

View 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、NSTSHALL 顯示台數與佔比。
#### 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、NSTSHALL 顯示小時數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 顯示查詢期間內不重複的機台數量

View File

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

View File

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

View File

@@ -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();
}

View File

@@ -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')}`;