Feature #1365
是由 陳國瑋 於 27 天 前更新
## 目的 背景
調整 LINE LIFF 充電樁狀態頁面的 Branch Admin UI 後續會串接 Connector Status UI mapping,讓 websocket。這張票定義 FE 能依 收到 websocket / initial API 回傳的 `connectorData.status` 正確呈現狀態、CTA、Label、離峰充電 switch 與排隊行為。 connector status 後的顯示與 CTA 規則,避免未知或未處理狀態被誤判成可開始充電。
此需求目標是 `react/web` 的 LIFF 充電頁,不是 目前程式碼調查結果:
- Branch Admin UI。
## 適用畫面
現況尚未透過 websocket 取得 connector status,connector detail 目前主要走 `GET /api/connector/status/{connectorId}` polling。
- Public charging page:`/charger/{connectorId}` `ems_branch` 的 websocket endpoint 目前是 OCPP charge point 使用的 `/ws/ocpp/*`,不是 browser admin UI 使用的 connector status websocket。
- Private charging page:`/charger/{connectorId}/private` browser 使用者端既有 connector update websocket 由 `ems_hq` 發送,payload DTO 為 `ConnectorUpdateMessage`,狀態位於 `connectorData.status`。
- `ems_branch` 發 MQ connector event 時,status 來源是 `ConnectorStatusMapper.mapToDisplayStatus(connector)`。
- 另開 API ticket 要求 websocket response 在 `connectorData` 增加 `statusLabel`,用來表示 status 的中文顯示文字。
## 資料來源 WebSocket response 欄位需求
FE 應使用以下資料來源合併顯示: 預期 websocket message 外層格式:
- Initial API:`GET /api/connector/info/{buildingId}/{connectorId}` ```json
- WebSocket connector update {
"eventType": "UPDATED",
"buildingId": "building-id",
"connectorId": "connector-id",
"timestamp": "2026-07-06T18:30:00",
"connectorData": {
"id": "connector-id",
"buildingId": "building-id",
"areaType": "public",
"status": "CHARGING",
"statusLabel": "充電中",
"connectorType": "J1772",
"image": null,
"startTime": "2026-07-06T18:00:00",
"totalEnergy": 12.34,
"energyUsage": 3.21,
"remainTime": 45,
"currentVoltage": 220.0,
"currentCurrent": 16.0,
"currentPower": 3.52,
"costPredict": 25.0,
"currentTariff": 7.5,
"enableOffPeak": true,
"nextOffPeakTimeStart": "2026-07-06T22:00:00",
"nextOffPeakTimeEnd": "2026-07-07T06:00:00"
}
- `connectorData.status`:UI 邏輯判斷來源 }
```
`statusLabel` 說明:
- `connectorData.statusLabel`:中文顯示文字來源 欄位位置:`connectorData.statusLabel`
- `connectorData.enableOffPeak`:離峰充電 switch 狀態 型別:`string | null`
- `connectorData.nextOffPeakTimeStart` 目的:後端提供 status 對應的中文顯示文字,讓不同 client 對中文狀態命名一致。
- `connectorData.nextOffPeakTimeEnd` FE fallback:若 `statusLabel` 為空或尚未由 API 提供,FE 仍必須依本票 UI mapping table 的 label 顯示,不可 fallback 成 `AVAILABLE`。
`statusLabel` 只作為顯示文字,不應作為 ## 後端可能回傳的 status
後端 enum 來源:`java/ems_branch/src/main/java/com/sylksoft/ems/branch/model/enums/ConnectorStatus.java`
注意:實際 event payload 使用 enum 的 `code`,所以 suspended 狀態會是 `SUSPENDEDEVSE` / `SUSPENDEDEV`,不是 enum name 的 `SUSPENDED_EVSE` / `SUSPENDED_EV`。
## FE 邏輯判斷來源。 正規化規則
1. 對 raw status 做 `trim()` 與 `toUpperCase()`。
2. Alias mapping:
- `SUSPENDED_EVSE` 必須視為 `SUSPENDEDEVSE`
- `SUSPENDED_EV` 必須視為 `SUSPENDEDEV`
3. `null`、空字串、未列入表格的新 status,不可以 fallback 成 `AVAILABLE`。
4. Unknown status 應顯示 `statusLabel || '未知狀態'`,不顯示開始/停止 CTA,並保留 raw status 供 debug。
5. 正常狀態的畫面文案優先使用 `connectorData.statusLabel`;若沒有此欄位或值為空,才使用 UI mapping table 的中文 label fallback。
Alias 原因:後端真實 payload 目前來自 `ConnectorStatus.code`,但 OpenAPI/TypeScript schema 或 enum name 可能出現 `SUSPENDED_EVSE` / `SUSPENDED_EV`。FE 同時支援兩種寫法,可以避免序列化或 schema 差異造成 UI fallback 錯誤。
## Status Mapping UI mapping table
| Raw status | statusLabel Accepted alias | Fallback UI Tone label | 行為 Expected `statusLabel` | 語意 | Badge class | 色碼參考 | Indicator | Animation | Primary CTA | CTA enabled | 備註 |
|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|---|---|
| AVAILABLE `AVAILABLE` | - | 待命 | green 待命 | 可開始充電;Private 可切換離峰充電 可開始使用 | `bg-blue-100 text-blue-700 border-blue-200` | bg `#DBEAFE`, text `#1D4ED8`, border `#BFDBFE` | `bg-blue-500` (`#3B82F6`) | none | 開始充電 | yes | 只有這個 status 可以顯示開始充電。 |
| ENQUEUED `ENQUEUED` | - | 排隊中 | blue 排隊中 | Private 顯示離峰排隊狀態與取消排隊 已進入排隊/輪充等待 |
`bg-cyan-100 text-cyan-700 border-cyan-200` 或 semantic `bg-info/10 text-info border-info/20` | PREPARING cyan indicator `#06B6D4` | 準備中 `bg-cyan-500` (`#06B6D4`) | orange none | 可停止充電;不可切換離峰充電 取消排隊 / read-only | conditional | 若 branch admin 有取消排隊 API/權限,顯示取消排隊;否則僅 read-only,不可顯示開始充電。 |
| CHARGING `PREPARING` | 充電中 - | green 準備中 | 可停止充電;顯示充電資訊 準備中 |
插槍/交易準備中 | SUSPENDEDEVSE `bg-orange-100 text-orange-700 border-orange-200` | 充電站暫停 bg `#FFEDD5`, text `#C2410C`, border `#FED7AA` | yellow `bg-orange-500` (`#F97316`) | 可停止充電;不可開始充電;不可切換離峰充電 pulse | 停止充電 | yes | 充電流程已啟動,不可顯示開始充電。 |
| SUSPENDEDEV `CHARGING` | 車輛暫停 - | yellow 充電中 | 可停止充電;不可開始充電;不可切換離峰充電 充電中 |
充電進行中 | FINISHING `bg-emerald-100 text-emerald-700 border-emerald-200` | 結束中 bg `#D1FAE5`, text `#047857`, border `#A7F3D0` | yellow `bg-emerald-500` (`#10B981`) | read-only;不可再次充電 pulse | 停止充電 | yes | 顯示即時充電資訊。 |
| RESERVED `SUSPENDEDEVSE` | 已預約 `SUSPENDED_EVSE` | blue/neutral 充電站暫停 | read-only;不可開始充電 充電站暫停 |
充電站端暫停供電 | UNAVAILABLE `bg-orange-100 text-orange-700 border-orange-200` | 不可用 bg `#FFEDD5`, text `#C2410C`, border `#FED7AA` | gray `bg-orange-500` (`#F97316`) | read-only pulse | 停止充電 | yes | 仍屬於既有交易/插槍流程,不可顯示開始充電;用 warning,不用 fault red。 |
| FAULTED `SUSPENDEDEV` | 故障 `SUSPENDED_EV` | red 車輛暫停 | read-only 車輛暫停 |
車輛端暫停取電 | unknown/null `bg-orange-100 text-orange-700 border-orange-200` | 未知狀態 bg `#FFEDD5`, text `#C2410C`, border `#FED7AA` | gray `bg-orange-500` (`#F97316`) | read-only;不可 fallback 成 AVAILABLE pulse |
## UI Color Tone
請 follow 目前 `react/web` 既有色碼基準:
停止充電 | Tone yes | 建議色碼 仍屬於既有交易/插槍流程,不可顯示開始充電;用 warning,不用 fault red。 |
|---|---|
| green `FINISHING` | `#00c300` - |
結束中 | red 結束中 | `#ef4444` 交易正在結束 |
`bg-yellow-100 text-yellow-700 border-yellow-200` | orange bg `#FEF9C3`, text `#A16207`, border `#FEF08A` | `#f59e0b` `bg-yellow-500` (`#EAB308`) |
none | yellow 不顯示開始;可 disabled 顯示「結束中」 | `#facc15` no | OCPP `FINISHING` 不是完成可用;真正可再開始是 `AVAILABLE`。 |
| blue `RESERVED` | `#3b82f6` - |
已預約 | gray 已預約 | `#9ca3af` 已被預約/佔用 |
## CTA 行為
Public page:
`bg-gray-100 text-gray-700 border-gray-200` | status bg `#F3F4F6`, text `#374151`, border `#E5E7EB` | CTA `bg-gray-400` (`#9CA3AF`) |
|---|---|
none | AVAILABLE 不顯示開始;可 disabled 顯示「已預約」 | 顯示開始充電 no | 中性佔用狀態,不是故障,也不是可操作狀態。 |
| PREPARING / CHARGING / SUSPENDEDEVSE / SUSPENDEDEV `UNAVAILABLE` | 顯示停止充電 - |
不可用 | FINISHING / RESERVED / UNAVAILABLE / FAULTED / unknown/null 不可用 | 不顯示可操作 CTA,只顯示狀態 站點不可用/停用 |
Private page:
`bg-gray-100 text-gray-700 border-gray-200` | status bg `#F3F4F6`, text `#374151`, border `#E5E7EB` | CTA `bg-gray-400` (`#9CA3AF`) |
|---|---|
none | AVAILABLE + enableOffPeak=false 不顯示開始;可 disabled 顯示「不可用」 | 顯示立即充電 no | 不可誤判為待命。 |
| AVAILABLE + enableOffPeak=true `FAULTED` | 立即充電 disabled,因為已啟用離峰充電 - |
故障 | ENQUEUED 故障 | 顯示取消排隊 故障 |
`bg-red-100 text-red-700 border-red-200` | PREPARING / CHARGING / SUSPENDEDEVSE / SUSPENDEDEV bg `#FEE2E2`, text `#B91C1C`, border `#FECACA` | 顯示停止充電 `bg-red-500` (`#EF4444`) | pulse | 不顯示開始;可 disabled 顯示「故障」 | no | 若 payload 有 error/vendor code,應顯示或保留於 detail/debug。 |
| FINISHING unknown / RESERVED empty / UNAVAILABLE / FAULTED / unknown/null null | 不顯示可操作 CTA,只顯示狀態 - | 未知狀態 | null 或後端原樣提供 | 未支援狀態 | `bg-gray-100 text-gray-700 border-gray-200` | bg `#F3F4F6`, text `#374151`, border `#E5E7EB` | `bg-gray-400` (`#9CA3AF`) | none | 不顯示開始/停止 | no | 必須保留 raw status;不可 fallback 成 `AVAILABLE`。 |
## 離峰充電行為 Acceptance Criteria
Private page 才顯示離峰充電 switch。
- 只有 `AVAILABLE` 狀態可以切換離峰充電 switch。 Branch Admin connector status UI 有一份集中式 status mapping,websocket 與現有 HTTP polling status 顯示共用同一套規則。
- `enableOffPeak=true` 時,不可立即充電。 FE 讀取 websocket payload 的 `connectorData.status` 作為狀態邏輯判斷依據。
- `ENQUEUED` 時顯示離峰排隊狀態。 FE 顯示中文狀態時優先使用 `connectorData.statusLabel`;若該欄位不存在、為空或為 null,才使用本票 mapping table 的 fallback label。
- `ENQUEUED` 時顯示 `nextOffPeakTimeStart` / `nextOffPeakTimeEnd` 作為離峰時段。 FE 正確處理表格內所有 raw status:`AVAILABLE`、`PREPARING`、`ENQUEUED`、`CHARGING`、`SUSPENDEDEVSE`、`SUSPENDEDEV`、`FINISHING`、`RESERVED`、`UNAVAILABLE`、`FAULTED`。
- `ENQUEUED` 時提供取消排隊 CTA。 FE 同時接受 alias:`SUSPENDED_EVSE`、`SUSPENDED_EV`。
- 取消排隊成功後,離峰充電模式應同步呈現為 OFF。 `AVAILABLE` 是唯一可顯示「開始充電」CTA 的狀態。
- `enableOffPeak` 為 null 或 undefined 時,不可沿用舊 switch 狀態造成 stale UI;應以 disabled/read-only 狀態處理。
## 實作要求
`PREPARING`、`CHARGING`、`SUSPENDEDEVSE`、`SUSPENDEDEV` 可以顯示「停止充電」CTA。
- 建立集中式 status metadata / mapping,避免 Public / Private page 各自寫分散判斷。 `ENQUEUED` 只有在 branch admin 已有取消排隊 API/權限時才顯示「取消排隊」;否則 read-only。
- `status` 是 UI 邏輯判斷來源。 `FINISHING` 顯示為「結束中」,不可顯示開始/重新開始 CTA。
- `statusLabel` 是中文顯示來源。 `RESERVED`、`UNAVAILABLE`、`FAULTED`、unknown/null/empty 都不可顯示開始 CTA。
- unknown/null unknown/null/empty 不可 fallback 成 `AVAILABLE`,並應保留 raw status 必須顯示 `未知狀態`,且不可被視為 `AVAILABLE`。 方便 debug。
- 移除目前 `FINISHING` 可重新開始充電的 UI 行為。
- 補齊 `SUSPENDEDEVSE`、`SUSPENDEDEV`、`RESERVED` 請補上每個 status 的 UI 狀態。
- websocket live data 與 initial API data 合併後,畫面應即時更新。 mock fixture、unit test 或 story/manual QA case;若目前專案沒有對應測試框架,至少要有可手動驗證的 fixture/demo 狀態清單。
## 驗收條件 Code references
- LIFF Public / Private Connector Status page 都能正確顯示所有 status。 `java/ems_branch/src/main/java/com/sylksoft/ems/branch/model/enums/ConnectorStatus.java`
- unknown/null status 不會顯示為待命,也不會出現開始充電 CTA。 `java/ems_branch/src/main/java/com/sylksoft/ems/branch/util/ConnectorStatusMapper.java`
- `FINISHING` 不會顯示開始充電或再次充電 CTA。 `java/ems_branch/src/main/java/com/sylksoft/ems/branch/mq/publisher/ConnectorEventPublisher.java`
- Private page 只有 `AVAILABLE` 可切換離峰充電。 `java/ems_hq/src/main/java/com/sylksoft/ems/hq/websocket/dto/ConnectorUpdateMessage.java`
- `enableOffPeak=true` 時,立即充電不可操作。 `java/ems_hq/src/main/java/com/sylksoft/ems/hq/mq/model/ConnectorEventData.java`
- `ENQUEUED` 顯示排隊中、離峰時段與取消排隊。 `react/branch/app/(dashboard)/connector/components/connector-status.tsx`
- websocket 收到 status update 後,status label、CTA、離峰 switch、排隊資訊會同步更新。 `react/branch/app/(dashboard)/connector/hooks/connectorHooks.ts`
返回