From fb42791f8d91bc5328ea4ff9d1a6b6b6e4e35a08 Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:18:45 +0200 Subject: [PATCH] The card beside it had boxes, so this one gets boxes Charger information was one long list of everything the service knows; the readings card next to it had been splitting its fields into a box per group all along. Same treatment here: Device, Status, Network and On the account, plus the service's own fields and the per-charger views, each in its own sunken section under a heading. Both clients, since the web and the phone draw the same card. Co-Authored-By: Claude Opus 5 --- Phone App/assets/i18n/da.json | 6 + Phone App/assets/i18n/en.json | 6 + Phone App/assets/i18n/pl.json | 6 + Phone App/lib/screens/charging_screen.dart | 231 +++++++++++---------- Web App/web/src/i18n/da.json | 6 + Web App/web/src/i18n/en.json | 6 + Web App/web/src/i18n/pl.json | 6 + Web App/web/src/views/Charging.vue | 133 +++++++----- 8 files changed, 234 insertions(+), 166 deletions(-) diff --git a/Phone App/assets/i18n/da.json b/Phone App/assets/i18n/da.json index 03c7b24..fe67459 100644 --- a/Phone App/assets/i18n/da.json +++ b/Phone App/assets/i18n/da.json @@ -310,6 +310,12 @@ "relatedBy": "Kan nås via", "timeZone": "Tidszone", "linked": "Tilknyttet", + "groups": { + "device": "Enhed", + "status": "Status", + "network": "Netværk", + "account": "På kontoen" + }, "views": { "station": "Stationspost", "totals": "Ladetotaler", diff --git a/Phone App/assets/i18n/en.json b/Phone App/assets/i18n/en.json index f451188..ae86e47 100644 --- a/Phone App/assets/i18n/en.json +++ b/Phone App/assets/i18n/en.json @@ -310,6 +310,12 @@ "relatedBy": "Reachable by", "timeZone": "Time zone", "linked": "Linked", + "groups": { + "device": "Device", + "status": "Status", + "network": "Network", + "account": "On the account" + }, "views": { "station": "Station record", "totals": "Charging totals", diff --git a/Phone App/assets/i18n/pl.json b/Phone App/assets/i18n/pl.json index 87641e7..325c994 100644 --- a/Phone App/assets/i18n/pl.json +++ b/Phone App/assets/i18n/pl.json @@ -312,6 +312,12 @@ "relatedBy": "Dostępna przez", "timeZone": "Strefa czasowa", "linked": "Powiązano", + "groups": { + "device": "Urządzenie", + "status": "Status", + "network": "Sieć", + "account": "Na koncie" + }, "views": { "station": "Rekord stacji", "totals": "Sumy ładowania", diff --git a/Phone App/lib/screens/charging_screen.dart b/Phone App/lib/screens/charging_screen.dart index d475b2f..716f78f 100644 --- a/Phone App/lib/screens/charging_screen.dart +++ b/Phone App/lib/screens/charging_screen.dart @@ -2342,83 +2342,74 @@ class _HomeTabState extends State<_HomeTab> { children: [ if (charger != null) ...[ const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: DriverVault.isDark(context) ? DriverVault.darkSunken : DriverVault.ink50, - borderRadius: BorderRadius.circular(DriverVault.radiusControl), + Row(children: [ + // The product shot the app shows for this model, when the account + // sent one. Decorative: the name beside it says everything the + // picture does. + if ((_liveFor(charger)?.imageUrl ?? "").isNotEmpty) ...[ + Image.network(_liveFor(charger)!.imageUrl, + width: 28, + height: 28, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const SizedBox.shrink()), + const SizedBox(width: 8), + ], + Expanded( + child: Text(charger.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - // The product shot the app shows for this model, when the - // account sent one. Decorative: the name beside it says - // everything the picture does. - if ((_liveFor(charger)?.imageUrl ?? "").isNotEmpty) ...[ - Image.network(_liveFor(charger)!.imageUrl, - width: 28, - height: 28, - fit: BoxFit.contain, - errorBuilder: (_, __, ___) => const SizedBox.shrink()), - const SizedBox(width: 8), - ], - Expanded( - child: Text(charger.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), - ), - // Reachability, said either way. A charger the service says - // nothing about stays silent: unknown is not offline. - if (_liveFor(charger)?.online == true) ...[ - _StatusBadge(label: t("charging.info.online"), ok: true), - const SizedBox(width: 6), - ] else if (_liveFor(charger)?.online == false) ...[ - _StatusBadge(label: t("charging.info.offline"), ok: false), - const SizedBox(width: 6), - ], - if (charger.provider.isNotEmpty) - _NeutralBadge(label: _providerLabel(charger.provider)), - ]), - const SizedBox(height: 8), - _PairList(rows: _infoRows(charger), breakLong: true), - // The rest of what the service knows, in the service's own - // words. It appears only when there is something in it, so a - // charger the cloud says nothing more about stays quiet. - if (_attrRows(charger).isNotEmpty) ...[ - const SizedBox(height: 10), - Text( - t("charging.info.rawTitle"), - style: DriverVault.mono(context, size: 10, weight: FontWeight.w500, color: muted) - .copyWith(letterSpacing: 1.4), - ), - const SizedBox(height: 4), + // Reachability, said either way. A charger the service says nothing + // about stays silent: unknown is not offline. + if (_liveFor(charger)?.online == true) ...[ + _StatusBadge(label: t("charging.info.online"), ok: true), + const SizedBox(width: 6), + ] else if (_liveFor(charger)?.online == false) ...[ + _StatusBadge(label: t("charging.info.offline"), ok: false), + const SizedBox(width: 6), + ], + if (charger.provider.isNotEmpty) + _NeutralBadge(label: _providerLabel(charger.provider)), + ]), + const SizedBox(height: 8), + // A box per group, the way the readings card draws its own: one list + // of everything the service knows is harder to find a field in than + // several short ones under headings. + for (final (heading, rows) in _infoGroups(charger)) ...[ + _ReadingSection(heading: heading, child: _PairList(rows: rows, breakLong: true)), + const SizedBox(height: 8), + ], + // The rest of what the service knows, in the service's own words. It + // appears only when there is something in it, so a charger the cloud + // says nothing more about stays quiet. + if (_attrRows(charger).isNotEmpty) ...[ + _ReadingSection( + heading: t("charging.info.rawTitle"), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ _PairList(rows: _attrRows(charger), breakLong: true), - const SizedBox(height: 4), + const SizedBox(height: 6), Text(t("charging.info.rawHint"), style: TextStyle(fontSize: 11, color: muted)), ], - // The views that answer per charger rather than per account. - // Each says what it knows, or why it could not be read — an - // account that is not the charger's owner cannot read the - // cards, which is a fact about the account rather than a - // failure. - for (final view in _detailViews(charger)) ...[ - const SizedBox(height: 10), - Text( - t("charging.info.views.${view.id}"), - style: DriverVault.mono(context, size: 10, weight: FontWeight.w500, color: muted) - .copyWith(letterSpacing: 1.4), - ), - const SizedBox(height: 4), - if (view.error.isNotEmpty) - Text(view.error, style: TextStyle(fontSize: 11, color: muted)) - else - _PairList(rows: view.rows, breakLong: true), - ], - ], + ), ), - ), + const SizedBox(height: 8), + ], + // The views that answer per charger rather than per account. Each says + // what it knows, or why it could not be read — an account that is not + // the charger's owner cannot read the cards, which is a fact about the + // account rather than a failure. + for (final view in _detailViews(charger)) ...[ + _ReadingSection( + heading: t("charging.info.views.${view.id}"), + child: view.error.isNotEmpty + ? Text(view.error, style: TextStyle(fontSize: 11, color: muted)) + : _PairList(rows: view.rows, breakLong: true), + ), + const SizedBox(height: 8), + ], ], if (_homeChargers.isEmpty) Padding( @@ -2437,49 +2428,65 @@ class _HomeTabState extends State<_HomeTab> { ); } - /// Everything the card can say about one charger. Every row is drawn every - /// time, a field nothing supplied included: which fields a charger has an - /// answer for is itself worth seeing, and a row that comes and goes with the - /// data makes two chargers impossible to read against each other. Nothing to - /// say is said with a dash. - List<(String, String)> _infoRows(HomeCharger c) { + /// Everything the card can say about one charger, in groups: what the box is, + /// what it is doing, how it is connected, and how it sits on the account. Each + /// group gets a box of its own, the way the readings card draws its groups. + /// Every row is drawn every time, a field nothing supplied included: which + /// fields a charger has an answer for is itself worth seeing, and a row that + /// comes and goes with the data makes two chargers impossible to read against + /// each other. Nothing to say is said with a dash. + List<(String, List<(String, String)>)> _infoGroups(HomeCharger c) { final live = _liveFor(c); - final pairs = <(String, String)>[ - ("vendor", c.vendor), - ("model", c.model), - ("firmware", live?.firmware ?? ""), - ("serial", c.serial), - ("site", c.siteName.isNotEmpty ? c.siteName : (live?.siteName ?? "")), - ("siteId", live?.siteId ?? ""), - ("sources", _sourcesLabel(live?.sources ?? const [])), - ("power", c.powerKw > 0 ? "${c.powerKw} kW" : ""), - ("connector", c.connector), - ("state", _stateLabel(live?.status ?? "")), - // Relayed as the service words it — the unit is upstream's, so putting one - // on it here would be inventing it. - ("chargePower", live?.power ?? ""), - ("ocpp", live?.ocppLabel ?? ""), - // The box on the wall, as opposed to the charging: the networks it is on, - // where it thinks it is, and when the account first saw it. - ("wifiName", live?.wifiName ?? ""), - ("wifiMac", live?.wifiMac ?? ""), - ("signal", live?.wifiRssi == null ? "" : "${live!.wifiRssi} dBm"), - ("bleMac", live?.bleMac ?? ""), - ("relatedBy", (live?.relatedBy ?? const []).join(" · ")), - ("timeZone", live?.timeZone ?? ""), - ( - "linked", - live?.linkedAt == null - ? "" - : formatDateTime( - DateTime.fromMillisecondsSinceEpoch((live!.linkedAt! * 1000).round())) - ), - ("providerId", c.providerChargerId), - ("added", c.created.isEmpty ? "" : formatDateTime(DateTime.tryParse(c.created)?.toLocal())), + final groups = <(String, List<(String, String)>)>[ + ("device", [ + ("vendor", c.vendor), + ("model", c.model), + ("firmware", live?.firmware ?? ""), + ("serial", c.serial), + ("power", c.powerKw > 0 ? "${c.powerKw} kW" : ""), + ("connector", c.connector), + ]), + ("status", [ + ("state", _stateLabel(live?.status ?? "")), + // Relayed as the service words it — the unit is upstream's, so putting + // one on it here would be inventing it. + ("chargePower", live?.power ?? ""), + ("ocpp", live?.ocppLabel ?? ""), + ]), + // The box on the wall, as opposed to the charging: the networks it is on. + ("network", [ + ("wifiName", live?.wifiName ?? ""), + ("wifiMac", live?.wifiMac ?? ""), + ("signal", live?.wifiRssi == null ? "" : "${live!.wifiRssi} dBm"), + ("bleMac", live?.bleMac ?? ""), + ("relatedBy", (live?.relatedBy ?? const []).join(" · ")), + ]), + // Where it thinks it is, and how the account came to know it. + ("account", [ + ("site", c.siteName.isNotEmpty ? c.siteName : (live?.siteName ?? "")), + ("siteId", live?.siteId ?? ""), + ("sources", _sourcesLabel(live?.sources ?? const [])), + ("timeZone", live?.timeZone ?? ""), + ( + "linked", + live?.linkedAt == null + ? "" + : formatDateTime( + DateTime.fromMillisecondsSinceEpoch((live!.linkedAt! * 1000).round())) + ), + ("providerId", c.providerChargerId), + ("added", c.created.isEmpty ? "" : formatDateTime(DateTime.tryParse(c.created)?.toLocal())), + ]), ]; return [ - for (final (key, value) in pairs) - (t("charging.info.$key"), value.isEmpty ? "—" : value), + for (final (id, pairs) in groups) + ( + t("charging.info.groups.$id"), + [ + for (final (key, value) in pairs) + (t("charging.info.$key"), value.isEmpty ? "—" : value), + ], + ), ]; } diff --git a/Web App/web/src/i18n/da.json b/Web App/web/src/i18n/da.json index c6e77d4..5212c43 100644 --- a/Web App/web/src/i18n/da.json +++ b/Web App/web/src/i18n/da.json @@ -270,6 +270,12 @@ "relatedBy": "Kan nås via", "timeZone": "Tidszone", "linked": "Tilknyttet", + "groups": { + "device": "Enhed", + "status": "Status", + "network": "Netværk", + "account": "På kontoen" + }, "views": { "station": "Stationspost", "totals": "Ladetotaler", diff --git a/Web App/web/src/i18n/en.json b/Web App/web/src/i18n/en.json index 93d6fea..740e7b9 100644 --- a/Web App/web/src/i18n/en.json +++ b/Web App/web/src/i18n/en.json @@ -256,6 +256,12 @@ "relatedBy": "Reachable by", "timeZone": "Time zone", "linked": "Linked", + "groups": { + "device": "Device", + "status": "Status", + "network": "Network", + "account": "On the account" + }, "views": { "station": "Station record", "totals": "Charging totals", diff --git a/Web App/web/src/i18n/pl.json b/Web App/web/src/i18n/pl.json index af70ada..1042f27 100644 --- a/Web App/web/src/i18n/pl.json +++ b/Web App/web/src/i18n/pl.json @@ -272,6 +272,12 @@ "relatedBy": "Dostępna przez", "timeZone": "Strefa czasowa", "linked": "Powiązano", + "groups": { + "device": "Urządzenie", + "status": "Status", + "network": "Sieć", + "account": "Na koncie" + }, "views": { "station": "Rekord stacji", "totals": "Sumy ładowania", diff --git a/Web App/web/src/views/Charging.vue b/Web App/web/src/views/Charging.vue index 05346c6..6efbe14 100644 --- a/Web App/web/src/views/Charging.vue +++ b/Web App/web/src/views/Charging.vue @@ -868,44 +868,59 @@ function homeChargerSubtitle(c) { return [c.serial, c.model, c.siteName].filter(Boolean).join(" · "); } -// Everything the card can say about one charger, as label/value rows. Every row -// is drawn every time, a field nothing supplied included: which fields a charger -// has an answer for is itself worth seeing, and a row that comes and goes with -// the data makes two chargers side by side impossible to read against each -// other. Nothing to say is said with a dash. -function chargerInfoRows(c) { +// Everything the card can say about one charger, in groups: what the box is, +// what it is doing, how it is connected, and how it sits on the account. Four +// short lists under headings, one box each the way the readings card draws its +// own, beat one long list nobody can find a field in. Every row is drawn every +// time, a field nothing supplied included: which fields a charger has an answer +// for is itself worth seeing, and a row that comes and goes with the data makes +// two chargers side by side impossible to read against each other. Nothing to +// say is said with a dash. +function chargerInfoGroups(c) { const live = liveFor(c) || {}; - const rows = [ - ["vendor", c.vendor], - ["model", c.model], - ["firmware", live.firmware], - ["serial", c.serial], - ["site", c.siteName || live.siteName], - ["siteId", live.siteId], - ["sources", sourcesLabel(live.sources)], - ["power", c.powerKw ? `${c.powerKw} kW` : ""], - ["connector", c.connector], - ["state", chargerStateLabel(live.status)], - // Relayed as the service words it — the unit is upstream's, so putting one - // on it here would be inventing it. - ["chargePower", live.power], - ["ocpp", ocppStatusLabel(live)], - // The box on the wall, as opposed to the charging: the networks it is on, - // where it thinks it is, and when the account first saw it. - ["wifiName", live.wifiName], - ["wifiMac", live.wifiMac], - ["signal", isSet(live.wifiRssi) ? `${live.wifiRssi} dBm` : ""], - ["bleMac", live.bleMac], - ["relatedBy", (live.relatedBy || []).join(" · ")], - ["timeZone", live.timeZone], - ["linked", live.linkedAt ? formatDateTime(new Date(live.linkedAt * 1000)) : ""], - ["providerId", c.providerChargerId], - ["added", c.created ? formatDateTime(c.created) : ""], + const groups = [ + ["device", [ + ["vendor", c.vendor], + ["model", c.model], + ["firmware", live.firmware], + ["serial", c.serial], + ["power", c.powerKw ? `${c.powerKw} kW` : ""], + ["connector", c.connector], + ]], + ["status", [ + ["state", chargerStateLabel(live.status)], + // Relayed as the service words it — the unit is upstream's, so putting one + // on it here would be inventing it. + ["chargePower", live.power], + ["ocpp", ocppStatusLabel(live)], + ]], + // The box on the wall, as opposed to the charging: the networks it is on. + ["network", [ + ["wifiName", live.wifiName], + ["wifiMac", live.wifiMac], + ["signal", isSet(live.wifiRssi) ? `${live.wifiRssi} dBm` : ""], + ["bleMac", live.bleMac], + ["relatedBy", (live.relatedBy || []).join(" · ")], + ]], + // Where it thinks it is, and how the account came to know it. + ["account", [ + ["site", c.siteName || live.siteName], + ["siteId", live.siteId], + ["sources", sourcesLabel(live.sources)], + ["timeZone", live.timeZone], + ["linked", live.linkedAt ? formatDateTime(new Date(live.linkedAt * 1000)) : ""], + ["providerId", c.providerChargerId], + ["added", c.created ? formatDateTime(c.created) : ""], + ]], ]; - return rows.map(([key, value]) => ({ - key, - label: t(`charging.info.${key}`), - value: value === "" || value == null ? "—" : value, + return groups.map(([id, rows]) => ({ + id, + label: t(`charging.info.groups.${id}`), + rows: rows.map(([key, value]) => ({ + key, + label: t(`charging.info.${key}`), + value: value === "" || value == null ? "—" : value, + })), })); } @@ -1865,8 +1880,11 @@ onMounted(async () => {
- -
+ +
- +

{{ t("charging.info.rawHint") }}

+ - +