loads info bar above list
This commit is contained in:
@@ -72,6 +72,19 @@
|
||||
"tab.batteries" = "Batteries";
|
||||
"tab.chargers" = "Chargers";
|
||||
|
||||
"loads.overview.header.title" = "Load Overview";
|
||||
"loads.overview.metric.count" = "Loads";
|
||||
"loads.overview.metric.current" = "Total Current";
|
||||
"loads.overview.metric.power" = "Total Power";
|
||||
"loads.overview.status.missing_details.title" = "Missing load details";
|
||||
"loads.overview.status.missing_details.message" = "Enter cable length and wire size for %d %@ to see accurate recommendations.";
|
||||
"loads.overview.status.missing_details.singular" = "load";
|
||||
"loads.overview.status.missing_details.plural" = "loads";
|
||||
"loads.overview.status.missing_details.banner" = "Finish configuring your loads";
|
||||
"loads.metric.fuse" = "Fuse";
|
||||
"loads.metric.cable" = "Cable";
|
||||
"loads.metric.length" = "Length";
|
||||
|
||||
"battery.bank.header.title" = "Battery Bank";
|
||||
"battery.bank.metric.count" = "Batteries";
|
||||
"battery.bank.metric.capacity" = "Capacity";
|
||||
|
||||
@@ -183,9 +183,6 @@ struct BatteriesView: View {
|
||||
Text(bankTitle)
|
||||
.font(.headline.weight(.semibold))
|
||||
Spacer()
|
||||
Text(system.name)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
ViewThatFits(in: .horizontal) {
|
||||
|
||||
@@ -22,6 +22,8 @@ struct LoadsView: View {
|
||||
@State private var showingSystemBOM = false
|
||||
@State private var selectedComponentTab: ComponentTab = .components
|
||||
@State private var batteryDraft: BatteryConfiguration?
|
||||
@State private var activeStatus: LoadStatus?
|
||||
@State private var editMode: EditMode = .inactive
|
||||
|
||||
let system: ElectricalSystem
|
||||
private let presentSystemEditorOnAppear: Bool
|
||||
@@ -89,6 +91,7 @@ struct LoadsView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.environment(\.editMode, $editMode)
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -118,6 +121,14 @@ struct LoadsView: View {
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack {
|
||||
if selectedComponentTab == .components {
|
||||
Button(action: {
|
||||
showingComponentLibrary = true
|
||||
}) {
|
||||
Image(systemName: "books.vertical")
|
||||
}
|
||||
.accessibilityIdentifier("component-library-button")
|
||||
}
|
||||
if !savedLoads.isEmpty && selectedComponentTab == .components {
|
||||
Button(action: {
|
||||
showingSystemBOM = true
|
||||
@@ -132,8 +143,14 @@ struct LoadsView: View {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
.disabled(selectedComponentTab == .chargers)
|
||||
if selectedComponentTab == .components || selectedComponentTab == .batteries {
|
||||
if selectedComponentTab == .components {
|
||||
EditButton()
|
||||
.environment(\.editMode, $editMode)
|
||||
.disabled(savedLoads.isEmpty)
|
||||
} else if selectedComponentTab == .batteries {
|
||||
EditButton()
|
||||
.environment(\.editMode, $editMode)
|
||||
.disabled(savedBatteries.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,6 +204,23 @@ struct LoadsView: View {
|
||||
)
|
||||
)
|
||||
}
|
||||
.alert(item: $activeStatus) { status in
|
||||
let detail = detailInfo(for: status)
|
||||
return Alert(
|
||||
title: Text(detail.title),
|
||||
message: Text(detail.message),
|
||||
dismissButton: .default(
|
||||
Text(
|
||||
NSLocalizedString(
|
||||
"battery.bank.status.dismiss",
|
||||
bundle: .main,
|
||||
value: "Got it",
|
||||
comment: "Dismiss button title for load status alert"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
.onAppear {
|
||||
if presentSystemEditorOnAppear && !hasPresentedSystemEditorOnAppear {
|
||||
hasPresentedSystemEditorOnAppear = true
|
||||
@@ -202,47 +236,87 @@ struct LoadsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedComponentTab) { newValue in
|
||||
if newValue == .chargers {
|
||||
editMode = .inactive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var librarySection: some View {
|
||||
private var summarySection: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Component Library")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
Text("Browse electrical components from VoltPlan")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text(loadsSummaryTitle)
|
||||
.font(.headline.weight(.semibold))
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
showingComponentLibrary = true
|
||||
}) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Browse")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.caption)
|
||||
|
||||
ViewThatFits(in: .horizontal) {
|
||||
HStack(spacing: 16) {
|
||||
summaryMetric(
|
||||
icon: "square.stack.3d.up",
|
||||
label: loadsCountLabel,
|
||||
value: "\(savedLoads.count)",
|
||||
tint: .blue
|
||||
)
|
||||
summaryMetric(
|
||||
icon: "bolt.fill",
|
||||
label: loadsCurrentLabel,
|
||||
value: formattedCurrent(totalCurrent),
|
||||
tint: .orange
|
||||
)
|
||||
summaryMetric(
|
||||
icon: "gauge.medium",
|
||||
label: loadsPowerLabel,
|
||||
value: formattedPower(totalPower),
|
||||
tint: .green
|
||||
)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
summaryMetric(
|
||||
icon: "square.stack.3d.up",
|
||||
label: loadsCountLabel,
|
||||
value: "\(savedLoads.count)",
|
||||
tint: .blue
|
||||
)
|
||||
summaryMetric(
|
||||
icon: "bolt.fill",
|
||||
label: loadsCurrentLabel,
|
||||
value: formattedCurrent(totalCurrent),
|
||||
tint: .orange
|
||||
)
|
||||
summaryMetric(
|
||||
icon: "gauge.medium",
|
||||
label: loadsPowerLabel,
|
||||
value: formattedPower(totalPower),
|
||||
tint: .green
|
||||
)
|
||||
}
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if let status = loadStatus {
|
||||
Button {
|
||||
activeStatus = status
|
||||
} label: {
|
||||
statusBanner(for: status)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.padding(.vertical, 10)
|
||||
.background(Color(.systemGroupedBackground))
|
||||
|
||||
|
||||
Divider()
|
||||
.background(Color(.separator))
|
||||
}
|
||||
}
|
||||
|
||||
private var componentsTab: some View {
|
||||
VStack(spacing: 0) {
|
||||
librarySection
|
||||
summarySection
|
||||
|
||||
List {
|
||||
ForEach(savedLoads) { load in
|
||||
@@ -304,17 +378,17 @@ struct LoadsView: View {
|
||||
ViewThatFits(in: .horizontal) {
|
||||
HStack(spacing: 12) {
|
||||
metricBadge(
|
||||
label: "Fuse",
|
||||
label: fuseMetricLabel,
|
||||
value: "\(recommendedFuse(for: load)) A",
|
||||
tint: .pink
|
||||
)
|
||||
metricBadge(
|
||||
label: "Cable",
|
||||
label: cableMetricLabel,
|
||||
value: wireGaugeString(for: load),
|
||||
tint: .teal
|
||||
)
|
||||
metricBadge(
|
||||
label: "Length",
|
||||
label: lengthMetricLabel,
|
||||
value: lengthString(for: load),
|
||||
tint: .orange
|
||||
)
|
||||
@@ -323,17 +397,17 @@ struct LoadsView: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
metricBadge(
|
||||
label: "Fuse",
|
||||
label: fuseMetricLabel,
|
||||
value: "\(recommendedFuse(for: load)) A",
|
||||
tint: .pink
|
||||
)
|
||||
metricBadge(
|
||||
label: "Cable",
|
||||
label: cableMetricLabel,
|
||||
value: wireGaugeString(for: load),
|
||||
tint: .teal
|
||||
)
|
||||
metricBadge(
|
||||
label: "Length",
|
||||
label: lengthMetricLabel,
|
||||
value: lengthString(for: load),
|
||||
tint: .orange
|
||||
)
|
||||
@@ -381,6 +455,219 @@ struct LoadsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var fuseMetricLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.metric.fuse",
|
||||
bundle: .main,
|
||||
value: "Fuse",
|
||||
comment: "Label for fuse metric in load detail row"
|
||||
)
|
||||
}
|
||||
|
||||
private var cableMetricLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.metric.cable",
|
||||
bundle: .main,
|
||||
value: "Cable",
|
||||
comment: "Label for cable metric in load detail row"
|
||||
)
|
||||
}
|
||||
|
||||
private var lengthMetricLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.metric.length",
|
||||
bundle: .main,
|
||||
value: "Length",
|
||||
comment: "Label for cable length metric in load detail row"
|
||||
)
|
||||
}
|
||||
|
||||
private var loadsSummaryTitle: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.header.title",
|
||||
bundle: .main,
|
||||
value: "Load Overview",
|
||||
comment: "Title for the loads overview summary section"
|
||||
)
|
||||
}
|
||||
|
||||
private var loadsCountLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.metric.count",
|
||||
bundle: .main,
|
||||
value: "Loads",
|
||||
comment: "Label for number of loads metric"
|
||||
)
|
||||
}
|
||||
|
||||
private var loadsCurrentLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.metric.current",
|
||||
bundle: .main,
|
||||
value: "Total Current",
|
||||
comment: "Label for total load current metric"
|
||||
)
|
||||
}
|
||||
|
||||
private var loadsPowerLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.metric.power",
|
||||
bundle: .main,
|
||||
value: "Total Power",
|
||||
comment: "Label for total load power metric"
|
||||
)
|
||||
}
|
||||
|
||||
private var totalCurrent: Double {
|
||||
savedLoads.reduce(0) { result, load in
|
||||
result + max(load.current, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private var totalPower: Double {
|
||||
savedLoads.reduce(0) { result, load in
|
||||
result + max(load.power, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private var loadStatus: LoadStatus? {
|
||||
guard !savedLoads.isEmpty else { return nil }
|
||||
let incompleteLoads = savedLoads.filter { load in
|
||||
load.length <= 0 || load.crossSection <= 0
|
||||
}
|
||||
if !incompleteLoads.isEmpty {
|
||||
return .missingDetails(count: incompleteLoads.count)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func summaryMetric(icon: String, label: String, value: String, tint: Color) -> some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(tint)
|
||||
Text(value)
|
||||
.font(.body.weight(.semibold))
|
||||
}
|
||||
Text(label.uppercased())
|
||||
.font(.caption2)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private func statusBanner(for status: LoadStatus) -> some View {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: status.symbol)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(status.tint)
|
||||
Text(status.bannerText)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(status.tint)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 10)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(status.tint.opacity(0.12))
|
||||
)
|
||||
}
|
||||
|
||||
private func formattedCurrent(_ value: Double) -> String {
|
||||
let numberString = LoadsView.numberFormatter.string(from: NSNumber(value: value)) ?? String(format: "%.1f", value)
|
||||
return "\(numberString) A"
|
||||
}
|
||||
|
||||
private func formattedPower(_ value: Double) -> String {
|
||||
let numberString = LoadsView.numberFormatter.string(from: NSNumber(value: value)) ?? String(format: "%.1f", value)
|
||||
return "\(numberString) W"
|
||||
}
|
||||
|
||||
private func detailInfo(for status: LoadStatus) -> LoadStatusDetail {
|
||||
switch status {
|
||||
case .missingDetails(let count):
|
||||
let title = NSLocalizedString(
|
||||
"loads.overview.status.missing_details.title",
|
||||
bundle: .main,
|
||||
value: "Missing load details",
|
||||
comment: "Alert title when loads are missing required details"
|
||||
)
|
||||
let format = NSLocalizedString(
|
||||
"loads.overview.status.missing_details.message",
|
||||
bundle: .main,
|
||||
value: "Enter cable length and wire size for %d %@ to see accurate recommendations.",
|
||||
comment: "Alert message when loads are missing required details"
|
||||
)
|
||||
let loadWord = count == 1
|
||||
? NSLocalizedString(
|
||||
"loads.overview.status.missing_details.singular",
|
||||
bundle: .main,
|
||||
value: "load",
|
||||
comment: "Singular noun for load"
|
||||
)
|
||||
: NSLocalizedString(
|
||||
"loads.overview.status.missing_details.plural",
|
||||
bundle: .main,
|
||||
value: "loads",
|
||||
comment: "Plural noun for loads"
|
||||
)
|
||||
let message = String(format: format, count, loadWord)
|
||||
return LoadStatusDetail(title: title, message: message)
|
||||
}
|
||||
}
|
||||
|
||||
private static let numberFormatter: NumberFormatter = {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = .current
|
||||
formatter.minimumFractionDigits = 0
|
||||
formatter.maximumFractionDigits = 1
|
||||
return formatter
|
||||
}()
|
||||
|
||||
private enum LoadStatus: Identifiable {
|
||||
case missingDetails(count: Int)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .missingDetails(let count):
|
||||
return "missing-details-\(count)"
|
||||
}
|
||||
}
|
||||
|
||||
var symbol: String {
|
||||
switch self {
|
||||
case .missingDetails:
|
||||
return "exclamationmark.triangle.fill"
|
||||
}
|
||||
}
|
||||
|
||||
var tint: Color {
|
||||
switch self {
|
||||
case .missingDetails:
|
||||
return .orange
|
||||
}
|
||||
}
|
||||
|
||||
var bannerText: String {
|
||||
switch self {
|
||||
case .missingDetails:
|
||||
return NSLocalizedString(
|
||||
"loads.overview.status.missing_details.banner",
|
||||
bundle: .main,
|
||||
value: "Finish configuring your loads",
|
||||
comment: "Short banner text describing loads that need additional details"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct LoadStatusDetail {
|
||||
let title: String
|
||||
let message: String
|
||||
}
|
||||
|
||||
private func metricBadge(label: String, value: String, tint: Color) -> some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(label.uppercased())
|
||||
|
||||
@@ -140,6 +140,19 @@
|
||||
"tab.batteries" = "Batterien";
|
||||
"tab.chargers" = "Ladegeräte";
|
||||
|
||||
"loads.overview.header.title" = "Verbraucherübersicht";
|
||||
"loads.overview.metric.count" = "Verbraucher";
|
||||
"loads.overview.metric.current" = "Strom";
|
||||
"loads.overview.metric.power" = "Leistung";
|
||||
"loads.overview.status.missing_details.title" = "Fehlende Verbraucherdetails";
|
||||
"loads.overview.status.missing_details.message" = "Gib Kabellänge und Leitungsquerschnitt für %d %@ ein, um genaue Empfehlungen zu erhalten.";
|
||||
"loads.overview.status.missing_details.singular" = "Verbraucher";
|
||||
"loads.overview.status.missing_details.plural" = "Verbraucher";
|
||||
"loads.overview.status.missing_details.banner" = "Konfiguration deiner Verbraucher abschließen";
|
||||
"loads.metric.fuse" = "Sicherung";
|
||||
"loads.metric.cable" = "Querschnitt";
|
||||
"loads.metric.length" = "Länge";
|
||||
|
||||
"battery.bank.header.title" = "Batteriebank";
|
||||
"battery.bank.metric.count" = "Batterien";
|
||||
"battery.bank.metric.capacity" = "Kapazität";
|
||||
|
||||
@@ -139,6 +139,19 @@
|
||||
"tab.batteries" = "Baterías";
|
||||
"tab.chargers" = "Cargadores";
|
||||
|
||||
"loads.overview.header.title" = "Resumen de cargas";
|
||||
"loads.overview.metric.count" = "Cargas";
|
||||
"loads.overview.metric.current" = "Corriente total";
|
||||
"loads.overview.metric.power" = "Potencia total";
|
||||
"loads.overview.status.missing_details.title" = "Faltan detalles de la carga";
|
||||
"loads.overview.status.missing_details.message" = "Introduce la longitud del cable y el calibre del conductor para %d %@ para obtener recomendaciones precisas.";
|
||||
"loads.overview.status.missing_details.singular" = "carga";
|
||||
"loads.overview.status.missing_details.plural" = "cargas";
|
||||
"loads.overview.status.missing_details.banner" = "Completa la configuración de tus cargas";
|
||||
"loads.metric.fuse" = "Fusible";
|
||||
"loads.metric.cable" = "Cable";
|
||||
"loads.metric.length" = "Longitud";
|
||||
|
||||
"battery.bank.header.title" = "Banco de baterías";
|
||||
"battery.bank.metric.count" = "Baterías";
|
||||
"battery.bank.metric.capacity" = "Capacidad";
|
||||
|
||||
@@ -139,6 +139,19 @@
|
||||
"tab.batteries" = "Batteries";
|
||||
"tab.chargers" = "Chargeurs";
|
||||
|
||||
"loads.overview.header.title" = "Aperçu des charges";
|
||||
"loads.overview.metric.count" = "Charges";
|
||||
"loads.overview.metric.current" = "Courant total";
|
||||
"loads.overview.metric.power" = "Puissance totale";
|
||||
"loads.overview.status.missing_details.title" = "Détails de charge manquants";
|
||||
"loads.overview.status.missing_details.message" = "Saisissez la longueur de câble et la section du conducteur pour %d %@ afin d'obtenir des recommandations précises.";
|
||||
"loads.overview.status.missing_details.singular" = "charge";
|
||||
"loads.overview.status.missing_details.plural" = "charges";
|
||||
"loads.overview.status.missing_details.banner" = "Terminez la configuration de vos charges";
|
||||
"loads.metric.fuse" = "Fusible";
|
||||
"loads.metric.cable" = "Câble";
|
||||
"loads.metric.length" = "Longueur";
|
||||
|
||||
"battery.bank.header.title" = "Banque de batteries";
|
||||
"battery.bank.metric.count" = "Batteries";
|
||||
"battery.bank.metric.capacity" = "Capacité";
|
||||
|
||||
@@ -139,6 +139,19 @@
|
||||
"tab.batteries" = "Batterijen";
|
||||
"tab.chargers" = "Laders";
|
||||
|
||||
"loads.overview.header.title" = "Lastenoverzicht";
|
||||
"loads.overview.metric.count" = "Lasten";
|
||||
"loads.overview.metric.current" = "Totale stroom";
|
||||
"loads.overview.metric.power" = "Totaal vermogen";
|
||||
"loads.overview.status.missing_details.title" = "Ontbrekende lastdetails";
|
||||
"loads.overview.status.missing_details.message" = "Voer kabellengte en kabeldoorsnede in voor %d %@ om nauwkeurige aanbevelingen te krijgen.";
|
||||
"loads.overview.status.missing_details.singular" = "last";
|
||||
"loads.overview.status.missing_details.plural" = "lasten";
|
||||
"loads.overview.status.missing_details.banner" = "Rond de configuratie van je lasten af";
|
||||
"loads.metric.fuse" = "Zekering";
|
||||
"loads.metric.cable" = "Kabel";
|
||||
"loads.metric.length" = "Lengte";
|
||||
|
||||
"battery.bank.header.title" = "Accubank";
|
||||
"battery.bank.metric.count" = "Batterijen";
|
||||
"battery.bank.metric.capacity" = "Capaciteit";
|
||||
|
||||
Reference in New Issue
Block a user