討論 #1486
是由 陳國瑋 於 27 天 前更新
## 目的
定義 Backend Admin Dashboard「即時關鍵指標」區塊的顯示內容、資料來源、計算邊界、更新方式與異常狀態。V1 僅呈現既有資料,不新增預測、告警、派工或任何作業邏輯。
## Mock 現況(全部為展示假資料)
目前工作版有六張 KPI card:
1. Charge Point 連線。 在線。
2. Connector 可服務。
3. 正在充電。
4. 等待供電。
5. 今日累積電量。
6. 上月電費。
上述數值仍為展示假資料;「上月電費」與「Charge Point 連線」已分別由 KPI-DEC-001/002 上述數值仍為展示假資料;「上月電費」的金額語意已由 KPI-DEC-001 確認,其餘 KPI 的名稱與公式仍須逐項確認。
## 已知約束
- Dashboard 必須適用所有案場;A17 只作第一個資料與效能驗證基準。
- CP connection、Connector runtime、Queue state 必須分層,不能互相覆寫。
- null 代表未知,不得以 0 冒充。
- 案場日期與「今日」以 building timezone 計算。
- V1 read-only,不調整 OCPP、Queue、rotation、結算或排程行為。
- V1 已明確移除「本月預估電費」及 forecast;第六張改用 KPI-DEC-001 定義的「上月電費」。
- KPI 的即時更新應沿用 #1424 已確認的 REST bootstrap/WebSocket replacement 架構,不為每張卡建立獨立連線。
## KPI-DEC-001:第六張 KPI 顯示「上月電費」
**狀態:已確認(2026-09-04,Ken)**
### 這張卡要讓使用者知道什麼
- 顯示指定案場「上一個完整日曆月」的門牌帳單應收總額。
- 「上月」以該案場的 building timezone 判定;例如案場當地時間已進入 9 月,目標期間就是 8 月。
- 金額代表所有門牌月結算資料 `e_bill_settlement.cost_predict` 的合計,不是已收款金額,也不是 Connector 請款金額。
- `paid`、`pending` 等 bill status 不改變當月應收金額,因此均納入合計。
- 只有該月每一筆門牌帳單的 `calculation_status` 都是 `COMPLETE` 時才顯示金額。
- 只要仍有 `PENDING_DATA` 或 `ERROR`,金額顯示 `—`,並清楚顯示完成筆數,例如「8 月結算未完成・10 / 11 筆」。
- 不顯示已完成資料的部分合計,也不自動退回更早月份,避免讓不完整或過期金額看起來像完整結果。
- 整張卡可導向 Dashboard 同頁 `#billing` 結算流程區塊,方便查看是哪個步驟尚未完成;卡片本身不執行任何帳務操作。
### 為何選 `e_bill_settlement.cost_predict`
目前程式碼與 schema 有三種不同金額:
1. `e_bill_settlement.cost_predict`:每門牌月結算應收金額,包含用量費與 private/public basic charge。
2. `e_connector_settlement.fee`:每個 Connector/idTag 的服務費,不等同住戶門牌帳單總額。
3. `InvoiceReportDataDto.totalFee`:Connector settlement fee 的請款報表合計,也不是門牌帳單總額。
「上月電費」採第 1 種語意。此 KPI 不把 `bill_status` 當成收款報表,也不混用第 2、3 種金額。
### Backend 要開發的內容
- 以 Overview API 的 `generatedAt` 轉換為指定 building timezone,求得上一個完整日曆月的 `periodYear`/`periodMonth`。
- 僅查詢指定 `buildingId` 與該期間的 `e_bill_settlement`,不可跨案場彙總。
- 回傳 `periodYear`、`periodMonth`、`amount`、`currencyCode`、`completedBillCount`、`totalBillCount`、`dataStatus` 與 `updatedAt`。V1 的 `currencyCode` 為 `TWD`,但 FE 不自行寫死幣別格式。
- 至少有一筆帳單且全部 `calculation_status = COMPLETE` 時,`dataStatus = COMPLETE`,`amount` 為所有 `cost_predict` 合計。
- 任一筆為 `PENDING_DATA` 時回 `dataStatus = PARTIAL`、`amount = null`;任一筆為 `ERROR` 時同樣不得回傳部分金額,並透過 source status 保留錯誤語意。
- 不依 `bill_status` 過濾 paid/pending,不改用 `e_connector_settlement.fee` 或 Invoice totalFee。
- 不回退查詢更早月份,不觸發重算、覆核、付款、Invoice 更新或其他 mutation。
- 本 KPI 隨 Dashboard REST snapshot/既有 refresh 更新;沿用 #1460 的 V1 邊界,不增加帳務 WebSocket。
> 完全沒有上月帳單資料時的共用 Empty 文案,將與其餘 KPI 的 Empty/Partial/Error 規格一起確認;在確認前不得把 0 筆解讀為 0 元。
### Frontend 要開發的內容
- 標題固定為「上月電費」,月份與金額只格式化 Backend 回傳值,不以瀏覽器時區自行推算。
- `COMPLETE` 時依 `currencyCode` 顯示完整金額;真正合計為 0 才能顯示 0。
- `PARTIAL`/計算錯誤時顯示 `—` 與「{月份} 月結算未完成・{completedBillCount} / {totalBillCount} 筆」,不可顯示部分合計。
- 卡片使用可鍵盤操作的同頁連結導向 `#billing`,具可見 focus 樣式及清楚的 accessible name。
- 不新增 forecast 說明、收款比例、帳務按鈕、Drawer 或 KPI 專用明細 API。
### A17 驗收基準(2026-09-04 唯讀查詢)
- 前一日曆月為 2026/08,`e_bill_settlement` 共 11 筆:10 筆 COMPLETE、1 筆 ERROR。
- 因資料未全部完成,卡片必須顯示 `—` 與「8 月結算未完成・10 / 11 筆」。
- 不得顯示 COMPLETE 部分合計 8,983.75 元,也不得把含 ERROR 資料的 9,533.63 元顯示成完整結果。
- 待 11 筆全部完成後,才依當時正式資料重新加總並顯示金額;驗收不把 mock 金額寫死為固定值。
### Acceptance Criteria
- [ ] 上月由 building timezone 與 generatedAt 決定,跨年時可正確得到前一年 12 月。
- [ ] 金額只使用指定 building/period 的 `e_bill_settlement.cost_predict`,paid/pending 均納入。
- [ ] 全部帳單計算完成前 `amount = null`,FE 顯示 `—`、期間與完成筆數,不顯示部分合計。
- [ ] 不因本月資料存在而改顯示本月,也不因上月未完成而退回更早月份。
- [ ] Card 可使用滑鼠及鍵盤跳到同頁結算流程,且不送出帳務 mutation。
- [ ] A17 2026/08 的 10 / 11 未完成情境符合上述顯示。
## KPI-DEC-002:第一張 KPI 顯示「Charge Point 連線」
**狀態:已確認(2026-09-04,Ken)**
### 這張卡要讓使用者知道什麼
這張卡只回答:「本案場目前應該連線的 Charge Point 中,有幾台已與 Branch 建立有效的 OCPP 連線?」它不回答設備是否可充電,也不把 runtime `FAULTED`/`CHARGING` 等狀態混入連線數。
- 使用者可見名稱由「Charge Point 在線」改為「Charge Point 連線」,避免把「有連線」誤解成「設備運作完全正常」。
- 主數字格式為 `{onlineCount} / {enabledChargePointCount} 台啟用`。
- 分母 `enabledChargePointCount`:指定 `buildingId` 下 `enabled = true` 的 Charge Point 數量。
- 分子 `onlineCount`:上述已啟用設備中,persisted `ocppConnectionStatus = ONLINE` 的數量。
- `OFFLINE` 與 `UNKNOWN` 不算在線,分別回傳並顯示,例如「1 台離線・1 台尚未確認」。
- `enabled = false` 的設備不放入分子或分母,避免計畫停用/除役設備拉低連線比例;若 `disabledCount > 0`,補充顯示「另有 N 台停用」。
- 整張卡導向既有 `/cp/list`;卡片本身只提供導覽,不執行啟用、停用、Reset 或其他設備操作。
### 為何不由 Dashboard 再看 Heartbeat 時間
現有 Backend 已將連線存活與設備運作狀態拆成兩個欄位:
- `ocppConnectionStatus`:`ONLINE`/`OFFLINE`/`UNKNOWN`,由 WebSocket 建連/斷線、BootNotification、Heartbeat 與既有 offline watchdog 維護。
- Charge Point runtime `status`:由 StatusNotification 等既有訊號維護,可為 AVAILABLE、CHARGING、FAULTED 等。
現有 repository/test 也明確要求在線統計讀取 persisted `ocppConnectionStatus`,不得再建立另一個 Heartbeat 時窗。因此 Dashboard 只統計已存在的 authoritative connection state;不比較 `lastHeartbeat`、不設定第二套 cutoff,也不修改 watchdog 行為。
### Backend 要開發的內容
- Overview API 依已授權的 `buildingId` 聚合並回傳:`onlineCount`、`offlineCount`、`unknownCount`、`enabledChargePointCount`、`disabledCount`、`dataStatus`、`updatedAt`。
- 必須符合 `onlineCount + offlineCount + unknownCount = enabledChargePointCount`;unknown enum/null 一律歸入 UNKNOWN,不可當成 ONLINE。
- 聚合查詢必須同時套用 building 與 enabled 條件;不能沿用目前未分 building 的全 Branch count 當成 Dashboard 數字。
- `disabledCount` 只供說明,不加入連線比例,也不因此建立「離線」需留意項目。
- REST overview 提供初始完整數字。既有 `ocppConnectionStatus` 發生 ONLINE/OFFLINE/UNKNOWN transition 且 transaction commit 後,案場級 Dashboard WebSocket 以 `NETWORK_HEALTH_UPDATED` replacement 更新整組計數。
- 每次 Heartbeat 若沒有造成 connection class 改變,不推送事件;不為每台 CP 建立獨立 Dashboard socket。
- Backend 不因 Dashboard 查詢或推送而改寫 connection/runtime status。
### Frontend 要開發的內容
- 顯示 Backend 回傳的分子、分母與 OFFLINE/UNKNOWN 數量,不從 CP list 或 Connector 卡片自行重算。
- `disabledCount = 0` 時可省略停用文案;大於 0 時顯示「另有 N 台停用」。
- 有 OFFLINE/UNKNOWN 時用文字清楚說明,不只靠顏色;runtime 故障另由 Connector/需留意區塊呈現。
- REST bootstrap 後,只接受同一 `buildingId` 且 sequence 較新的 `NETWORK_HEALTH_UPDATED` replacement。重連或 sequence gap 時重新取得 authoritative snapshot。
- 整張卡使用可鍵盤操作的固定 route link `/cp/list`,具可見 focus 狀態;Backend payload 不回傳任意 URL。
### 顯示例子
案場有 6 台 Charge Point,其中 5 台啟用:4 台 ONLINE、1 台 OFFLINE,另有 1 台停用。
- 主數字:`4 / 5 台啟用`
- 說明:`1 台離線・0 台尚未確認・另有 1 台停用`
停用設備不會被顯示成離線,也不會讓主數字變成 `4 / 6`。
### Acceptance Criteria
- [ ] 分子、分母只涵蓋指定 building;分母只計 enabled = true。
- [ ] ONLINE/OFFLINE/UNKNOWN 三者總和等於 enabledChargePointCount。
- [ ] Disabled CP 不計入分母、不算 OFFLINE;存在時可另外顯示 disabledCount。
- [ ] Dashboard 不依 lastHeartbeat 自行重判 ONLINE/OFFLINE,也不修改既有 watchdog cutoff。
- [ ] OCPP connection 與 runtime status 分開;ONLINE + FAULTED 是合法且可被正確呈現的組合。
- [ ] 初始 REST 與 NETWORK_HEALTH_UPDATED WebSocket replacement 使用相同聚合規則。
- [ ] Heartbeat 未造成狀態 transition 時不逐筆推送 Dashboard event。
- [ ] Card 可使用滑鼠及鍵盤前往 `/cp/list`,且不執行任何設備 mutation。
## 待確認事項
- [x] 第六張移除「本月預估電費」,改為「上月電費」;採前一完整日曆月的門牌帳單應收總額,且全部計算完成才顯示金額(KPI-DEC-001)。
- [x] [ ] Charge Point 連線使用指定 building 的 enabled CP 為分母、persisted OCPP ONLINE 為分子;OFFLINE/UNKNOWN/disabled 分開,採 REST bootstrap + NETWORK_HEALTH_UPDATED replacement(KPI-DEC-002)。 在線的 connection/freshness 定義。
- [ ] Connector 可服務的 status allowlist 與 unknown 處理。
- [ ] 正在充電與等待供電的精確來源/distinct 規則。
- [ ] 今日累積電量的日期、有效資料與 Partial 規則。
- [ ] 其餘即時 KPI 的 WebSocket replacement/REST refresh 分工。
- [ ] KPI 共用 Empty/Partial/Error、responsive、accessibility 與 deep link 規則。
## 需求管理狀態
- Parent:#1422
- Related:#1424(REST/WebSocket)、#1460(結算流程)、#1461(30 日充電用量)、#1463(需留意事項)
- Requirement status:討論中;KPI-DEC-001「上月電費」及 KPI-DEC-002「Charge Point 連線」已確認,其餘四張 status:討論中;KPI-DEC-001「上月電費」的期間、金額來源、完整性與 deep link 已確認,其餘五張 KPI 與共用狀態待逐項確認 待逐項確認
- Final specification:所有 Dashboard 區塊確認後整併至 #1422
- Implementation tickets:需求鎖定後才建立 FE/BE/QA child issues
返回