專案

一般

配置概況

討論 #1486

是由 陳國瑋 於 27 天 前更新

## 目的 

 定義 Backend Admin Dashboard「即時關鍵指標」區塊的顯示內容、資料來源、計算邊界、更新方式與異常狀態。V1 僅呈現既有資料,不新增預測、告警、派工或任何作業邏輯。 

 ## Mock 現況(全部為展示假資料) 

 目前工作版有六張 KPI card: 

 1. Charge Point 連線。 
 2. Connector 可服務。 
 3. 充電進行中。 
 4. 等待供電。 
 5. 今日累積電量。 
 6. 上月電費。 

 上述數值仍為展示假資料;六張 KPI 的名稱、使用者語意與主要計算方式已由 KPI-DEC-001~006 確認,下一步只需確認 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。 

 ## KPI-DEC-003:第二張 KPI 顯示「Connector 可服務」 

 **狀態:已確認(2026-09-04,Ken)** 

 ### 這張卡要讓使用者知道什麼 

 這張卡回答:「本案場已啟用設備底下的 Connector,有多少目前沒有已知故障、不可用或連線問題?」它衡量的是設備能否持續提供既有或後續充電服務,不是「現在是否空閒」。 

 因此: 

 - `AVAILABLE` 是空閒、可立即開始充電;但「可服務」的範圍更廣。 
 - 正在充電、準備中、排隊、暫停輸出、結束中或已預約,仍屬已知的正常服務生命週期,不因忙碌就判成不可服務。 
 - 整張卡導向既有 `/connector/list`;Dashboard 只呈現與導覽,不執行 Remote Start/Stop、Reset、狀態修正或其他設備操作。 

 ### 分母與分子 

 **分母 `totalConnectorCount`** 

 - 指定 `buildingId` 下,所屬 Charge Point 為 `enabled = true` 的所有 Connector。 
 - 目前 Connector entity 沒有獨立 enabled 欄位,因此 V1 不虛構 Connector enabled 條件。 
 - 掛在 disabled Charge Point 下的 Connector 不進入分子或分母。 

 **分子 `serviceableCount` 必須同時符合兩項條件** 

 1. 所屬 Charge Point 的 persisted `ocppConnectionStatus = ONLINE`。 
 2. Connector persisted status 屬於以下 allowlist: 
    - `AVAILABLE` 
    - `PREPARING` 
    - `ENQUEUED` 
    - `CHARGING` 
    - `SUSPENDED_EV` 
    - `SUSPENDED_EVSE` 
    - `FINISHING` 
    - `RESERVED` 

 **下列情況不算可服務** 

 - Connector status 為 `FAULTED` 或 `UNAVAILABLE`。 
 - Connector status 為 null、未知 code 或無法解析。 
 - 所屬 Charge Point 的 OCPP connection 為 `OFFLINE` 或 `UNKNOWN`;即使 Connector 留有舊的 CHARGING 狀態,也不能讓它看起來仍是可正常通訊的設備。 

 `nonServiceableCount = totalConnectorCount - serviceableCount`。`faultedCount` 是不可服務項目的資訊子集合,可能與 parent CP 離線同時成立,因此 UI 使用「其中 N 個故障」,不把各原因數量再次相加當成總數。 

 ### Backend 要開發的內容 

 - Overview API 依已授權 `buildingId` 聚合並回傳 `serviceableCount`、`totalConnectorCount`、`nonServiceableCount`、`faultedCount`、`dataStatus` 與 `updatedAt`。 
 - 聚合必須 join 所屬 Charge Point,套用 building、CP enabled、CP OCPP connection 與 Connector status allowlist;不得使用目前沒有 building filter 的全 Branch `queryStatisticsOfConnector()` 結果。 
 - 必須符合 `serviceableCount + nonServiceableCount = totalConnectorCount`;未知/null status 安全歸入不可服務,不得 fallback 成 AVAILABLE。 
 - `faultedCount` 直接來自既有 persisted Connector status,不依 error text、lastStatusChange 或 Dashboard 自訂 timeout 推測。 
 - REST overview 提供初始完整數字。Connector status 或 parent CP connection transition commit 後,案場級 Dashboard WebSocket 以 `CONNECTOR_OVERVIEW_UPDATED` replacement 更新整組 Connector summary。 
 - REST 與 WebSocket 必須共用同一 aggregation/presentation rule;FE 不自行掃描 preview cards 重算。 
 - 不新增 Connector health scheduler、timeout、狀態修正或任何 charging/OCPP mutation。 

 ### Frontend 要開發的內容 

 - 主數字顯示 `{serviceableCount} / {totalConnectorCount} 個`。 
 - 說明列顯示「{nonServiceableCount} 個目前不可服務」;`faultedCount > 0` 時補充「其中 {faultedCount} 個故障」。 
 - 文字必須明確說明「可服務不等於立即空閒」;不得把 serviceableCount 改標成 AVAILABLE 數量。 
 - 卡片使用 Backend 回傳的聚合值,不從畫面上有限筆 preview、Connector Drawer 或其他 KPI 相減重算。 
 - 整張卡使用固定 route `/connector/list`,支援滑鼠、鍵盤及可見 focus;Backend payload 不回傳任意 URL。 
 - 套用同 building、較新 sequence 的 `CONNECTOR_OVERVIEW_UPDATED` replacement;sequence gap/重連時重新取得 REST snapshot。 

 ### Mock 顯示例子 

 工作版假資料為 12 個 Connector:11 個符合可服務條件、1 個 `FAULTED`。 

 - 主數字:`11 / 12 個` 
 - 說明:`1 個目前不可服務・其中 1 個故障` 

 這只是用來驗證版面與文案,不代表 A17 即時營運結果,也不作為正式驗收固定數字。 

 ### Acceptance Criteria 

 - [ ] 分母只包含指定 building 下 enabled CP 的 Connector;disabled CP 的 Connector 完全排除。 
 - [ ] 分子同時要求 parent CP ONLINE 與 Connector status 位於固定 serviceable allowlist。 
 - [ ] FAULTED、UNAVAILABLE、未知 status,以及 parent CP OFFLINE/UNKNOWN 均不算可服務。 
 - [ ] serviceableCount + nonServiceableCount 等於 totalConnectorCount;faultedCount 只作「其中」說明,不被再次加總。 
 - [ ] CHARGING/PREPARING/ENQUEUED/SUSPENDED/FINISHING/RESERVED 不因不是 AVAILABLE 就被誤判為不可服務。 
 - [ ] REST snapshot 與 CONNECTOR_OVERVIEW_UPDATED 使用完全相同的 Backend 計算規則。 
 - [ ] Card 可使用滑鼠及鍵盤前往 `/connector/list`,且不執行任何設備 mutation。 
 - [ ] Dashboard 不新增 health timeout、排程或狀態修正行為。 

 ## KPI-DEC-004:第三張 KPI 顯示「充電進行中」 

 **狀態:已確認(2026-09-04,Ken;中文名稱依 Ken 意見調整)** 

 ### 正式名稱與使用者語意 

 - 使用者可見名稱採「充電進行中」,不使用語序較不自然的「進行中充電」。 
 - 這張卡回答:「系統目前仍認定有多少筆尚未結束的充電 Session?」 
 - 它計算 ACTIVE transaction,不等同於 Connector raw status 恰好為 `CHARGING`,也不保證每一筆此刻都正在輸出電力。 

 ### Active Session 計數規則 

 Backend 對指定 `buildingId` 的每個 Connector,沿用既有 Admin Connector snapshot 的 current transaction 解析方式;每個 Connector 最多計一筆: 

 1. 若 `connector.currentTransactionId` 指向一筆 `status = ACTIVE` 的 transaction,該 Connector 計一筆。 
 2. 若 current pointer 缺漏、失效或尚未同步,只有該 Connector「最新一筆 transaction」的 status 為 ACTIVE 時才計一筆。 
 3. 若最新一筆已是 COMPLETED/CANCELLED/ERROR,較舊且殘留的孤兒 ACTIVE row 不得計入。 

 補充邊界: 

 - 不要求 Connector status 必須等於 CHARGING。ACTIVE Session 在 `SUSPENDED_EV`、`SUSPENDED_EVSE`、`FINISHING`,或 parent CP 暫時 OFFLINE 時仍計入,直到 transaction 正式結束。 
 - 即使 Charge Point 後來被 disabled,已存在且尚未結束的 ACTIVE Session 仍須顯示,避免隱藏待結束/待 reconciliation 的交易。 
 - Queue waiting、off-peak waiting、Remote Start 已送出/接受但尚未建立 StartTransaction,都不算 ACTIVE Session;它們由「等待供電」或 Connector 狀態呈現。 
 - Connector raw status = CHARGING,但依上述 current transaction 規則找不到 ACTIVE transaction 時,不計入 Session 數;V1 不因此新增 mismatch 告警或狀態修正。 

 ### Backend 要開發的內容 

 - Overview API 回傳 `activeSessionCount`、`dataStatus` 與 `updatedAt`,scope 必須經授權並限制於指定 building。 
 - 聚合查詢須以每個 Connector 的 canonical current transaction 為準,避免直接 `COUNT(status = ACTIVE)` 把舊孤兒資料重複算入。 
 - 必須保證同一 Connector 最多一筆,且 `activeSessionCount` 不受 Connector preview 顯示上限影響。 
 - Transaction start/stop commit 後,以案場級 `CONNECTOR_OVERVIEW_UPDATED` replacement 更新 activeSessionCount;REST overview 與 WebSocket 共用同一規則。 
 - 卡片次要文字的 `currentPowerKw` 直接重用已確認的 Live Load current snapshot;不為此 KPI 重新加總 MeterValue。 
 - Active Session source 與 Live Load source 的 dataStatus 分開:交易計數可正常顯示時,不因即時功率暫時未知就把 Session 數清為 0。 
 - 不新增 transaction timeout、自動結束、補寫、狀態修正或其他 charging lifecycle。 

 ### Frontend 要開發的內容 

 - 標題固定顯示「充電進行中」,主數字顯示 `{activeSessionCount} Sessions`。 
 - 數字直接使用 Backend aggregate;不得以 Connector status = CHARGING 的 card 數、preview 數量或 current power 是否大於 0 重新推導。 
 - 說明列可顯示已確認 Live Load 的 `currentPowerKw`,例如「即時功率 21.84 kW」;功率未知時顯示 `—` 與對應資料狀態,不影響已知的 Session 數。 
 - `CONNECTOR_OVERVIEW_UPDATED` 更新主數字;`LIVE_LOAD_UPDATED` 更新次要功率,兩者可獨立刷新。 
 - 整張卡使用固定 route `/connector/list`,支援滑鼠、鍵盤及可見 focus;Backend payload 不回傳任意 URL。 

 ### Mock 顯示例子 

 工作版使用 3 筆假 ACTIVE Session: 

 - 標題:`充電進行中` 
 - 主數字:`3 Sessions` 
 - 說明:`即時功率 21.84 kW` 

 數字僅供版面 review,不代表 A17 即時營運結果。 

 ### Acceptance Criteria 

 - [ ] 使用者可見名稱必須是「充電進行中」。 
 - [ ] 每個 Connector 依 canonical current transaction 最多計一筆 ACTIVE Session。 
 - [ ] 最新 transaction 已結束時,不得因較舊孤兒 ACTIVE row 而增加計數。 
 - [ ] ACTIVE Session 在 suspended、finishing、CP offline 或 CP disabled 情境仍保留,直到 transaction 正式結束。 
 - [ ] Queue/off-peak waiting/尚未建立 StartTransaction 的 remote operation 不計入。 
 - [ ] Session 主數字不依 Connector CHARGING card 數或 currentPowerKw 推導。 
 - [ ] REST 與 CONNECTOR_OVERVIEW_UPDATED 使用相同 aggregate;Live Load 功率獨立由 LIVE_LOAD_UPDATED 更新。 
 - [ ] Card 可使用滑鼠及鍵盤前往 `/connector/list`,且不執行 Stop 或其他 mutation。 
 - [ ] Dashboard 不新增 transaction timeout、自動結束或資料修正行為。 

 ## KPI-DEC-005:第四張 KPI 顯示「等待供電」 

 **狀態:已確認(2026-09-04,Ken)** 

 ### 使用者語意 

 - 這張卡回答:「目前有多少個 Connector 已有供電需求,且既有 Queue 明確判定為可供電?」 
 - 主數字只表示等待供電的 Connector 數,不表示 Queue row 數,也不包含未插槍或已阻擋的 Connector。 

 ### 狀態來源與計數規則 

 - Backend 直接使用既有 `e_connector_priority.charge_status`;不由 Connector runtime、等待秒數、可用 slot 或排程時間重新推導 Queue 狀態。 
 - `waitingForPowerCount = COUNT(DISTINCT connector_id)`,條件為 Queue `charge_status = ELIGIBLE`,且 Connector/Charge Point 屬於目前授權且選定的 building。 
 - 同一 Connector 即使同時有 MANUAL/OFF_PEAK 等多筆 Queue row,主數字最多計 1 個。 
 - `NOT_PLUGGED` 表示目前沒有插槍需求,不計入主數字;另以 `notPluggedCount` 作為次要資訊。 
 - 為使「另有 N 個未插槍」與主數字互斥,同一 Connector 只要已有任一 `ELIGIBLE` row,就只歸入 waitingForPowerCount;notPluggedCount 只計「有 NOT_PLUGGED、且沒有 ELIGIBLE」的 distinct Connector。 
 - `BLOCKED` 不計入主數字,仍由已確認的「需留意事項」Queue BLOCKED category 呈現。 
 - `CHARGING`、`PAUSED`、`FINISHED`、null/unknown 均不計入等待供電。 

 ### Backend 要開發的內容 

 - Overview API 回傳 `waitingForPowerCount`、`notPluggedCount`、`dataStatus` 與 `updatedAt`;scope 必須限制於指定 building。 
 - REST overview 與 WebSocket 必須共用同一個 distinct aggregation,避免前端從 Connector preview 或 Queue rows 自行加總。 
 - Queue 狀態完成資料庫 commit 且使 aggregate 改變後,以案場級 `CONNECTOR_OVERVIEW_UPDATED` replacement 更新 waitingForPowerCount 與 notPluggedCount。 
 - 本 KPI 不新增 Queue timeout、SLA、scheduler、狀態轉換或自動修正行為。 

 ### Frontend 要開發的內容 

 - Card 標題固定為「等待供電」,主數字顯示 `waitingForPowerCount`,單位顯示「個」。 
 - `notPluggedCount > 0` 時,次要文字顯示「另有 N 個未插槍(不計入)」;不得把它加回主數字。 
 - FE 直接顯示 Backend aggregate,不從 bounded Connector preview、等待時間或其他畫面狀態重算。 
 - 整張卡固定導向既有 `/connector/list`,支援滑鼠與鍵盤;只做導覽,不執行 Queue 或充電操作。 

 ### Mock 顯示例子 

 - 主數字:`2 個` 
 - 次要文字:`另有 1 個未插槍(不計入)` 

 以上均為假資料,只用於確認 UI 語意與版面。 

 ### Acceptance Criteria 

 - [ ] waitingForPowerCount 只計 Queue 狀態為 ELIGIBLE 的 distinct Connector。 
 - [ ] 同一 Connector 多筆 ELIGIBLE row 只計 1 個;同時有 ELIGIBLE 與 NOT_PLUGGED 時只歸入主數字。 
 - [ ] NOT_PLUGGED 不計入主數字,並以獨立 notPluggedCount 顯示;BLOCKED 只由「需留意事項」呈現。 
 - [ ] CHARGING/PAUSED/FINISHED/null/unknown 不計入等待供電。 
 - [ ] REST 與 CONNECTOR_OVERVIEW_UPDATED 使用完全相同的 Backend aggregation;FE 不自行重算。 
 - [ ] Card 可使用滑鼠及鍵盤前往 `/connector/list`,且不執行任何設備或 Queue mutation。 
 - [ ] Dashboard 不新增 Queue timeout、SLA、scheduler 或狀態轉換。 

 ## KPI-DEC-006:第五張 KPI 改為「今日已完成充電量」 

 **狀態:已確認(2026-09-04,Ken)** 

 ### 正式名稱與使用者語意 

 - 使用者可見名稱由「今日累積電量」改為「今日已完成充電量」。 
 - 這張卡回答:「依本案場日期歸屬規則,今天目前已有多少最終確認的充電量?」 
 - 名稱中的「已完成」明確表示不包含 ACTIVE transaction 的暫估電量;它不是逐時電表報表,也不是「今天實際流過的全部電量」。 

 ### 唯一資料來源與計算規則 

 - 不建立第二套今日電量公式;卡片直接重用 #1461「30 日充電用量」中日期等於 `rangeEndDate` 的 today daily bucket。 
 - 主數字取 `todayBucket.validEnergyKWh`:只加總 `status = COMPLETED` 且 `energy_consumed` 非 null、非負的交易,Wh 轉為 kWh。 
 - 次要數字取 `todayBucket.sessionCount`:只計 `status = COMPLETED` 且 `energy_consumed > 0` 的交易。 
 - 日期依 building timezone 與 transaction `start_timestamp` 歸屬;跨日交易整筆歸入開始日,不拆分 MeterValue。 
 - ACTIVE、CANCELLED、ERROR 不納入;COMPLETED 但 energy_consumed 為 null 或負值時,依 #1461 排除並標示 PARTIAL。 
 - 因此,今天開始但仍在充電的 Session 不會出現在本卡;完成並取得最終 energy_consumed 後,才會在下一次成功 REST refresh 納入。 

 ### Backend 要開發的內容 

 - 沿用 #1461 USG-DEC-001~009 的 Usage read model、today bucket、building timezone、eligibility、日期歸屬與資料狀態。 
 - 不新增 Dashboard 專用 active-energy query、MeterValue 差值估算、跨日拆分或第二組聚合欄位。 
 - Overview 必須讓 FE 可依 `rangeEndDate` 唯一取得對應 daily bucket;同一 response 內不得出現兩個互相矛盾的今日電量值。 
 - 資料以 REST 首載、每 5 分鐘 refetch 及頁面手動刷新取得;V1 不新增 Usage/Energy WebSocket event。 
 - 本卡只讀取資料,不修改 Transaction、MeterValue、Billing 或充電狀態。 

 ### Frontend 要開發的內容 

 - 主數字顯示 today bucket 的 validEnergyKWh,單位固定為 kWh;次要文字顯示「已完成 N 次充電」。 
 - 移除「較昨日同時段 ±N%」;V1 不查詢、不推算昨日同時段比較。 
 - Card 固定導向同頁 `#usage` 的「30 日充電用量」區塊,支援滑鼠與鍵盤。 
 - 0/null/PARTIAL/ERROR/refresh error with cache/STALE 完全沿用 #1461:成功但今日無有效交易顯示 0 kWh/0 次;未知不得顯示成 0;快取資料不得假裝最新。 
 - Tooltip/輔助說明需表達「依交易開始日歸屬,僅包含已完成交易」,避免使用者誤認為即時 active 能量。 

 ### Mock 顯示例子 

 - 標題:`今日已完成充電量` 
 - 主數字:`84.6 kWh` 
 - 次要文字:`已完成 6 次充電` 

 以上均為假資料,只用於確認 UI 語意與版面,不代表 A17 即時結果。 

 ### Acceptance Criteria 

 - [ ] Card 名稱固定為「今日已完成充電量」,不再顯示「今日累積電量」。 
 - [ ] 主數字與 Session 數分別等於 #1461 today daily bucket 的 validEnergyKWh 與 sessionCount;FE 不自行重算。 
 - [ ] 日期使用 Backend rangeEndDate/building timezone,並依 transaction startTimestamp 歸屬;跨日不拆分。 
 - [ ] ACTIVE transaction 與 MeterValue 暫估值不納入;完成後才於成功 REST refresh 反映。 
 - [ ] 0、null、PARTIAL、ERROR、cached refresh error 與 STALE 的顯示符合 #1461,未知不得冒充 0。 
 - [ ] Card 可使用滑鼠及鍵盤前往同頁 `#usage`;不顯示昨日同時段比較,也不執行任何 mutation。 
 - [ ] V1 不新增 Usage/Energy WebSocket event 或第二套今日電量聚合。 

 ## KPI-DEC-007:六張 KPI 共用資料狀態 

 **狀態:已確認(2026-09-04,Ken)** 

 ### 核心原則 

 - 六張 Card 的標題與位置固定保留;Loading、無資料或錯誤時不得讓 Card 消失,避免版面跳動。 
 - `0` 是 Backend 已確認的有效數值;`null`/缺值是未知。Frontend 不得把 null、欄位缺漏或 request 尚未完成轉成 0。 
 - 資料狀態不能只靠顏色表示,必須有可見文字,並可由 assistive technology 讀取。 
 - 某個 source 失敗時,只影響依賴該 source 的 Card;其他 source 正常的 KPI 繼續顯示。共用同一 source 的多張 Card 可一起受影響,例如 Connector overview 會同時影響 Connector 可服務、充電進行中與等待供電。 

 ### 狀態與 UI 對照 

 1. **首次 Loading** 
    - 保留 Card 標題。 
    - 數字與次要文字位置顯示 skeleton。 
    - 不先顯示 0、`—` 或上一個 building 的數字。 
 2. **COMPLETE** 
    - 顯示 Backend 回傳值;有效值為 0 時明確顯示 0。 
    - KPI Card 不另外縮成 Empty State;「成功但沒有符合資料」依各 KPI 規則顯示 0 或既有業務文案。 
 3. **PARTIAL** 
    - Backend 若仍回傳可用的已知值,保留該值並顯示「部分資料缺漏・數字可能不完整」。 
    - Backend 若判定沒有可安全顯示的值而回 null,主數字顯示 `—`,並顯示「部分資料無法取得」。 
    - FE 不自行加總可見資料來製造 partial value。 
 4. **Initial ERROR** 
    - 沒有同 building 的成功快取時顯示 `—` 與「暫時無法取得」。 
    - 不 fallback 到 fixture,也不影響其他正常 source 的 Card。 
 5. **Refresh Error with Cache** 
    - 已有同 building 的成功資料時保留舊值,顯示「更新失敗・顯示 {lastSuccessfulAt} 資料」。 
    - 切換 building 後不得沿用前一案場快取。 
 6. **STALE** 
    - 有上次成功值時保留數字,明確顯示「資料已過期・最後更新 {lastSuccessfulAt/updatedAt}」。 
    - 不使用正常/成功文案或綠色狀態;沒有可用舊值時顯示 `—`。 

 ### Backend 要開發的內容 

 - 每個 KPI 所依賴的 source/section 必須提供 `dataStatus`(COMPLETE/PARTIAL/ERROR)、`freshness`、`updatedAt`,需要時提供 `lastSuccessfulAt`;數值欄位允許為 null。 
 - 子查詢失敗不得 catch 後回 0;Overview 仍回傳其他成功 sections,整體標示 PARTIAL 或可診斷的 source status。 
 - PARTIAL 是否有可安全顯示的 value 由 Backend 判定並明確回傳;FE 不從 preview、其他 Card 或殘缺 rows 重算。 
 - REST snapshot 與 WebSocket replacement 必須使用相同的 value/null/status 語意。WebSocket 中斷本身不清空最後值;freshness 仍依 #1424 Backend policy 判定。 
 - 本決策只定義 read model 與顯示狀態,不觸發資料修正、設備控制、帳務重算或 Queue 操作。 

 ### Frontend 要開發的內容 

 - 依上述狀態呈現 skeleton、值、`—` 與明確文字;不得以顏色作為唯一狀態訊號。 
 - Query cache key 必須包含 buildingId;refresh error 只可保留同一 building 的上次成功 snapshot。 
 - 新 snapshot/replacement 成功後移除 Error/Stale 提示並套用最新值;資料來源間彼此隔離。 
 - Mock 提供 `?kpi=loading|partial|error|refresh-error|stale|zero`,以「今日已完成充電量」作為共用狀態示例;正式版不依 query parameter 切換狀態。 

 ### Acceptance Criteria 

 - [ ] 首次 Loading 保留六張標題並顯示 skeleton,任何 Card 都不先顯示 0 或前一案場資料。 
 - [ ] COMPLETE 的有效 0 顯示為 0;null、欄位缺漏與 request 未完成不得顯示為 0。 
 - [ ] PARTIAL 有值時保留值並標示可能不完整;PARTIAL 無值時顯示 `—`,FE 不自行建立部分合計。 
 - [ ] Initial ERROR 顯示 `—`/暫時無法取得;Refresh Error 有同 building 快取時保留舊值並顯示 lastSuccessfulAt。 
 - [ ] STALE 保留可用舊值但明確標示資料已過期,不沿用正常/綠色狀態;無舊值時顯示 `—`。 
 - [ ] 一個 source 失敗只影響依賴它的 Card,其他正常 KPI 不消失;切換 building 不沿用舊案場快取。 
 - [ ] REST 與 WebSocket 對 value、null、dataStatus 與 freshness 的語意一致。 
 - [ ] 狀態均有可見文字及可讀取的 status/aria label,不只使用顏色;Production error 不 fallback 到 mock fixture。 
 - [ ] 本決策不新增任何設備、交易、Queue 或帳務 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)。 
 - [x] Connector 可服務要求 parent CP ONLINE 且 status 位於固定 allowlist;FAULTED、UNAVAILABLE、unknown 或 parent CP 非 ONLINE 均排除,分母只含 enabled CP 的 Connector(KPI-DEC-003)。 
 - [x] 「充電進行中」依每個 Connector 的 canonical ACTIVE transaction 計算;不以 raw CHARGING 或功率推導,且保留 offline/disabled CP 下尚未結束的 Session(KPI-DEC-004)。 
 - [x] 「等待供電」只計 Queue `ELIGIBLE` 的 distinct Connector;NOT_PLUGGED 另列且不計入,BLOCKED 留在需留意事項,其餘狀態排除(KPI-DEC-005)。 
 - [x] 第五張改為「今日已完成充電量」,直接重用 #1461 today daily bucket;只含依開始日歸屬的有效 COMPLETED 交易,不估算 ACTIVE(KPI-DEC-006)。 
 - [x] 六張 KPI 的資料分工已確認:連線/Connector/Session/Queue 採 REST bootstrap + 對應 WebSocket replacement;今日已完成充電量與上月電費採 REST。 
 - [x] KPI 共用 Loading、有效 0、PARTIAL、Initial Error、Refresh Error with Cache 與 STALE 顯示規則(KPI-DEC-007)。 
 - [ ] KPI responsive layout 規則。 
 - [ ] KPI keyboard/accessibility 共用 Empty/Partial/Error、responsive、accessibility 與 deep link 共用規則。 規則。 

 ## 需求管理狀態 

 - Parent:#1422 
 - Related:#1424(REST/WebSocket)、#1460(結算流程)、#1461(30 日充電用量)、#1463(需留意事項) 
 - Requirement status:討論中;KPI-DEC-001~007 已確認六張 status:討論中;KPI-DEC-001~006 六張 KPI 與共用資料狀態,目前只剩 responsive layout,以及 keyboard/accessibility/deep 已全部確認,目前只剩 KPI 共用 Empty/Partial/Error、responsive、accessibility 與 deep link 共用規則待確認 規則待確認 
 - Final specification:所有 Dashboard 區塊確認後整併至 #1422 
 - Implementation tickets:需求鎖定後才建立 FE/BE/QA child issues 

返回