From 51d85cc352db69285248a0d8a1230ed3158b7766 Mon Sep 17 00:00:00 2001 From: Stefan Lange-Hegermann Date: Thu, 23 Oct 2025 15:27:22 +0200 Subject: [PATCH] chargers in overview --- Cable/Base.lproj/Localizable.strings | 4 + Cable/Loads/LoadsView.swift | 5 +- Cable/Overview/SystemOverviewView.swift | 233 +++++++++++++++++++++++- Cable/de.lproj/Localizable.strings | 4 + Cable/es.lproj/Localizable.strings | 4 + Cable/fr.lproj/Localizable.strings | 4 + Cable/nl.lproj/Localizable.strings | 4 + 7 files changed, 255 insertions(+), 3 deletions(-) diff --git a/Cable/Base.lproj/Localizable.strings b/Cable/Base.lproj/Localizable.strings index e22934e..1fd375d 100644 --- a/Cable/Base.lproj/Localizable.strings +++ b/Cable/Base.lproj/Localizable.strings @@ -108,6 +108,10 @@ "battery.onboarding.title" = "Add your first battery"; "battery.onboarding.subtitle" = "Track your bank's capacity and chemistry to keep runtime expectations in check."; "battery.bank.badge.voltage" = "Voltage"; +"overview.chargers.header.title" = "Charger Overview"; +"overview.chargers.empty.title" = "No chargers configured yet"; +"overview.chargers.empty.subtitle" = "Add shore power, DC-DC, or solar chargers to understand your charging capacity."; +"overview.chargers.empty.create" = "Add Charger"; "battery.bank.badge.capacity" = "Capacity"; "battery.bank.badge.energy" = "Energy"; "battery.bank.banner.voltage" = "Voltage mismatch detected"; diff --git a/Cable/Loads/LoadsView.swift b/Cable/Loads/LoadsView.swift index 7ff4c26..624eb6d 100644 --- a/Cable/Loads/LoadsView.swift +++ b/Cable/Loads/LoadsView.swift @@ -282,11 +282,14 @@ struct LoadsView: View { system: system, loads: savedLoads, batteries: savedBatteries, + chargers: savedChargers, onSelectLoads: { selectedComponentTab = .components }, onSelectBatteries: { selectedComponentTab = .batteries }, + onSelectChargers: { selectedComponentTab = .chargers }, onCreateLoad: { createNewLoad() }, onBrowseLibrary: { showingComponentLibrary = true }, - onCreateBattery: { startBatteryConfiguration() } + onCreateBattery: { startBatteryConfiguration() }, + onCreateCharger: { startChargerConfiguration() } ) .accessibilityIdentifier("system-overview") } diff --git a/Cable/Overview/SystemOverviewView.swift b/Cable/Overview/SystemOverviewView.swift index 83b7162..48ed1a0 100644 --- a/Cable/Overview/SystemOverviewView.swift +++ b/Cable/Overview/SystemOverviewView.swift @@ -7,11 +7,14 @@ struct SystemOverviewView: View { let system: ElectricalSystem let loads: [SavedLoad] let batteries: [SavedBattery] + let chargers: [SavedCharger] let onSelectLoads: () -> Void let onSelectBatteries: () -> Void + let onSelectChargers: () -> Void let onCreateLoad: () -> Void let onBrowseLibrary: () -> Void let onCreateBattery: () -> Void + let onCreateCharger: () -> Void var body: some View { ScrollView { @@ -19,6 +22,7 @@ struct SystemOverviewView: View { systemCard loadsCard batteriesCard + chargersCard } .padding(.horizontal, 16) .padding(.vertical, 20) @@ -70,6 +74,12 @@ struct SystemOverviewView: View { value: "\(batteries.count)", tint: .green ) + summaryMetric( + icon: "bolt.fill", + label: chargerCountLabel, + value: "\(chargers.count)", + tint: .orange + ) } VStack(alignment: .leading, spacing: 12) { @@ -85,6 +95,12 @@ struct SystemOverviewView: View { value: "\(batteries.count)", tint: .green ) + summaryMetric( + icon: "bolt.fill", + label: chargerCountLabel, + value: "\(chargers.count)", + tint: .orange + ) } } @@ -339,6 +355,94 @@ struct SystemOverviewView: View { .buttonStyle(.plain) } + private var chargersCard: some View { + Button(action: onSelectChargers) { + VStack(alignment: .leading, spacing: 16) { + Text(chargerSummaryTitle) + .font(.headline.weight(.semibold)) + + if chargers.isEmpty { + VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text(chargerEmptyTitle) + .font(.subheadline.weight(.semibold)) + Text(chargerEmptySubtitle) + .font(.footnote) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + Button(action: onCreateCharger) { + Label(chargerEmptyCreateAction, systemImage: "plus") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + } + } else { + ViewThatFits(in: .horizontal) { + HStack(spacing: 16) { + summaryMetric( + icon: "bolt.fill", + label: chargerCountLabel, + value: "\(chargers.count)", + tint: .orange + ) + summaryMetric( + icon: "powerplug", + label: chargerOutputLabel, + value: formattedChargerOutput(representativeChargerOutput), + tint: .indigo + ) + summaryMetric( + icon: "gauge.medium", + label: chargerCurrentLabel, + value: formattedCurrent(totalChargerCurrent), + tint: .blue + ) + summaryMetric( + icon: "bolt.circle", + label: chargerPowerLabel, + value: formattedPower(totalChargerPower), + tint: .pink + ) + } + + VStack(alignment: .leading, spacing: 12) { + summaryMetric( + icon: "bolt.fill", + label: chargerCountLabel, + value: "\(chargers.count)", + tint: .orange + ) + summaryMetric( + icon: "powerplug", + label: chargerOutputLabel, + value: formattedChargerOutput(representativeChargerOutput), + tint: .indigo + ) + summaryMetric( + icon: "gauge.medium", + label: chargerCurrentLabel, + value: formattedCurrent(totalChargerCurrent), + tint: .blue + ) + + } + } + } + } + .padding(.horizontal, 16) + .padding(.vertical, 18) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill(Color(.systemBackground)) + ) + } + .buttonStyle(.plain) + } + private var loadStatus: LoadConfigurationStatus? { guard !loads.isEmpty else { return nil } let incomplete = loads.filter { load in @@ -374,6 +478,25 @@ struct SystemOverviewView: View { } } + private var totalChargerCurrent: Double { + chargers.reduce(0) { result, charger in + result + max(0, charger.maxCurrentAmps) + } + } + + private var totalChargerPower: Double { + chargers.reduce(0) { result, charger in + result + max(0, charger.effectivePowerWatts) + } + } + + private var representativeChargerOutput: Double? { + let outputs = chargers.map { $0.outputVoltage }.filter { $0 > 0 } + guard !outputs.isEmpty else { return nil } + let total = outputs.reduce(0, +) + return total / Double(outputs.count) + } + private var batteryWarning: BatteryWarning? { guard batteries.count > 1 else { return nil } @@ -488,6 +611,11 @@ struct SystemOverviewView: View { return "\(numberString) W" } + private func formattedChargerOutput(_ value: Double?) -> String { + guard let value, value > 0 else { return "—" } + return formattedValue(value, unit: "V") + } + private func formattedValue(_ value: Double, unit: String) -> String { let numberString = Self.numberFormatter.string(from: NSNumber(value: value)) ?? String(format: "%.1f", value) return "\(numberString) \(unit)" @@ -659,6 +787,78 @@ struct SystemOverviewView: View { ) } + private var chargerSummaryTitle: String { + NSLocalizedString( + "overview.chargers.header.title", + bundle: .main, + value: "Charger Overview", + comment: "Title for the chargers summary section" + ) + } + + private var chargerCountLabel: String { + NSLocalizedString( + "chargers.summary.metric.count", + bundle: .main, + value: "Chargers", + comment: "Label for number of chargers metric" + ) + } + + private var chargerOutputLabel: String { + NSLocalizedString( + "chargers.summary.metric.output", + bundle: .main, + value: "Output Voltage", + comment: "Label for representative output voltage metric" + ) + } + + private var chargerCurrentLabel: String { + NSLocalizedString( + "chargers.summary.metric.current", + bundle: .main, + value: "Charge Rate", + comment: "Label for total charger current metric" + ) + } + + private var chargerPowerLabel: String { + NSLocalizedString( + "chargers.summary.metric.power", + bundle: .main, + value: "Charge Power", + comment: "Label for total charger power metric" + ) + } + + private var chargerEmptyTitle: String { + NSLocalizedString( + "overview.chargers.empty.title", + bundle: .main, + value: "No chargers configured yet", + comment: "Title shown when no chargers are configured" + ) + } + + private var chargerEmptySubtitle: String { + NSLocalizedString( + "overview.chargers.empty.subtitle", + bundle: .main, + value: "Add shore power, DC-DC, or solar chargers to understand your charging capacity.", + comment: "Subtitle shown when no chargers are configured" + ) + } + + private var chargerEmptyCreateAction: String { + NSLocalizedString( + "overview.chargers.empty.create", + bundle: .main, + value: "Add Charger", + comment: "Button title to create a charger from the overview" + ) + } + private var systemOverviewTitle: String { NSLocalizedString( "overview.system.header.title", @@ -802,15 +1002,41 @@ struct SystemOverviewView: View { ) ] + let chargers: [SavedCharger] = [ + SavedCharger( + name: "Shore Power", + inputVoltage: 230, + outputVoltage: 14.4, + maxCurrentAmps: 40, + maxPowerWatts: 580, + iconName: "powerplug", + colorName: "orange", + system: system + ), + SavedCharger( + name: "DC-DC Charger", + inputVoltage: 12.8, + outputVoltage: 14.2, + maxCurrentAmps: 30, + maxPowerWatts: 0, + iconName: "bolt.circle", + colorName: "blue", + system: system + ) + ] + SystemOverviewView( system: system, loads: loads, batteries: batteries, + chargers: chargers, onSelectLoads: {}, onSelectBatteries: {}, + onSelectChargers: {}, onCreateLoad: {}, onBrowseLibrary: {}, - onCreateBattery: {} + onCreateBattery: {}, + onCreateCharger: {} ) .padding() } @@ -827,11 +1053,14 @@ struct SystemOverviewView: View { system: system, loads: [], batteries: [], + chargers: [], onSelectLoads: {}, onSelectBatteries: {}, + onSelectChargers: {}, onCreateLoad: {}, onBrowseLibrary: {}, - onCreateBattery: {} + onCreateBattery: {}, + onCreateCharger: {} ) .padding() } diff --git a/Cable/de.lproj/Localizable.strings b/Cable/de.lproj/Localizable.strings index 219ed05..7dd45a2 100644 --- a/Cable/de.lproj/Localizable.strings +++ b/Cable/de.lproj/Localizable.strings @@ -176,6 +176,10 @@ "battery.onboarding.title" = "Füge deine erste Batterie hinzu"; "battery.onboarding.subtitle" = "Behalte Kapazität und Chemie deiner Batterien im Blick, um die Laufzeit im Griff zu behalten."; "battery.bank.badge.voltage" = "Spannung"; +"overview.chargers.header.title" = "Ladegeräte"; +"overview.chargers.empty.title" = "Noch keine Ladegeräte konfiguriert"; +"overview.chargers.empty.subtitle" = "Füge Landstrom-, DC-DC- oder Solarladegeräte hinzu, um deine Ladeleistung zu verstehen."; +"overview.chargers.empty.create" = "Ladegerät hinzufügen"; "battery.bank.badge.capacity" = "Kapazität"; "battery.bank.badge.energy" = "Energie"; "battery.bank.banner.voltage" = "Spannungsabweichung erkannt"; diff --git a/Cable/es.lproj/Localizable.strings b/Cable/es.lproj/Localizable.strings index b0cb31d..15b1c1d 100644 --- a/Cable/es.lproj/Localizable.strings +++ b/Cable/es.lproj/Localizable.strings @@ -175,6 +175,10 @@ "battery.onboarding.title" = "Añade tu primera batería"; "battery.onboarding.subtitle" = "Controla la capacidad y la química del banco para mantener tus tiempos de autonomía bajo control."; "battery.bank.badge.voltage" = "Voltaje"; +"overview.chargers.header.title" = "Resumen de cargadores"; +"overview.chargers.empty.title" = "Aún no hay cargadores configurados"; +"overview.chargers.empty.subtitle" = "Añade cargadores de toma de puerto, DC-DC o solares para conocer tu capacidad de carga."; +"overview.chargers.empty.create" = "Agregar cargador"; "battery.bank.badge.capacity" = "Capacidad"; "battery.bank.badge.energy" = "Energía"; "battery.bank.banner.voltage" = "Se detectó un desajuste de voltaje"; diff --git a/Cable/fr.lproj/Localizable.strings b/Cable/fr.lproj/Localizable.strings index 3fabd71..9a9c3a6 100644 --- a/Cable/fr.lproj/Localizable.strings +++ b/Cable/fr.lproj/Localizable.strings @@ -175,6 +175,10 @@ "battery.onboarding.title" = "Ajoutez votre première batterie"; "battery.onboarding.subtitle" = "Suivez la capacité et la chimie de votre banc pour mieux maîtriser l'autonomie."; "battery.bank.badge.voltage" = "Tension"; +"overview.chargers.header.title" = "Vue d’ensemble des chargeurs"; +"overview.chargers.empty.title" = "Aucun chargeur configuré pour l’instant"; +"overview.chargers.empty.subtitle" = "Ajoutez des chargeurs secteur, DC-DC ou solaires pour comprendre votre capacité de charge."; +"overview.chargers.empty.create" = "Ajouter un chargeur"; "battery.bank.badge.capacity" = "Capacité"; "battery.bank.badge.energy" = "Énergie"; "battery.bank.banner.voltage" = "Écart de tension détecté"; diff --git a/Cable/nl.lproj/Localizable.strings b/Cable/nl.lproj/Localizable.strings index b57168a..1b5ece9 100644 --- a/Cable/nl.lproj/Localizable.strings +++ b/Cable/nl.lproj/Localizable.strings @@ -175,6 +175,10 @@ "battery.onboarding.title" = "Voeg je eerste accu toe"; "battery.onboarding.subtitle" = "Houd capaciteit en chemie van de accubank in de gaten om de gebruiksduur te beheersen."; "battery.bank.badge.voltage" = "Spanning"; +"overview.chargers.header.title" = "Overzicht van laders"; +"overview.chargers.empty.title" = "Nog geen laders geconfigureerd"; +"overview.chargers.empty.subtitle" = "Voeg walstroom-, DC-DC- of zonneladers toe om je laadvermogen te begrijpen."; +"overview.chargers.empty.create" = "Lader toevoegen"; "battery.bank.badge.capacity" = "Capaciteit"; "battery.bank.badge.energy" = "Energie"; "battery.bank.banner.voltage" = "Spanningsafwijking gedetecteerd";