From c13b3740f218dce76a4325ed718b6380182c4340 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Wed, 5 Jun 2024 10:13:41 -0400 Subject: [PATCH] Feat: Fixing equipment settings form --- .../AirHandlerMeasurementForm.swift | 242 ----------- .../EquipmentMeasurementForm.swift | 96 +++-- .../EquipmentSettingsForm.swift | 405 ++++++++++++++++++ .../EstimateSettingsForm.swift | 303 ------------- Sources/SharedModels/CoolingCapacity.swift | 31 +- .../SharedModels/EquipmentMeasurement.swift | 1 + Sources/Styleguide/EquipmentTypePicker.swift | 19 + Sources/Styleguide/FlaggedView.swift | 106 ----- Sources/Styleguide/InfoView.swift | 7 +- Sources/Styleguide/TextField+Precision.swift | 28 ++ 10 files changed, 545 insertions(+), 693 deletions(-) delete mode 100644 Sources/PressureEstimationsFeature/AirHandlerMeasurementForm.swift create mode 100644 Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift delete mode 100644 Sources/PressureEstimationsFeature/EstimateSettingsForm.swift create mode 100644 Sources/Styleguide/EquipmentTypePicker.swift diff --git a/Sources/PressureEstimationsFeature/AirHandlerMeasurementForm.swift b/Sources/PressureEstimationsFeature/AirHandlerMeasurementForm.swift deleted file mode 100644 index fc2a38a..0000000 --- a/Sources/PressureEstimationsFeature/AirHandlerMeasurementForm.swift +++ /dev/null @@ -1,242 +0,0 @@ -import ComposableArchitecture -import DependenciesAdditions -import EstimatedPressureDependency -import SharedModels -import Styleguide -import SwiftUI -import TCAExtras - -@Reducer -public struct AirHandlerMeasurementForm { - - @ObservableState - public struct State: Equatable { - public var calculatedMeasurement: EquipmentMeasurement.AirHandler? - public var focusedField: Field? - public var measurement: EquipmentMeasurement.AirHandler - public var updatedAirflow: Double? - public var fanType: FanType - public var ratedStaticPressures = RatedStaticPressures() - - public init( - calculatedMeasurement: EquipmentMeasurement.AirHandler? = nil, - focusedField: Field? = nil, - measurement: EquipmentMeasurement.AirHandler = .init(), - updatedAirflow: Double? = nil, - fanType: FanType = .constantSpeed - ) { - self.calculatedMeasurement = calculatedMeasurement - self.focusedField = focusedField - self.measurement = measurement - self.updatedAirflow = updatedAirflow - self.fanType = fanType - } - - public var isValid: Bool { - return measurement.returnPlenumPressure != nil - && measurement.postFilterPressure != nil - && measurement.postCoilPressure != nil - && measurement.supplyPlenumPressure != nil - && measurement.airflow != nil - && updatedAirflow != nil - } - - public enum Field: String, Equatable, CaseIterable, FocusableField { - case returnPlenumPressure - case postFilterPressure - case postCoilPressure - case supplyPlenumPressure - case airflow - case updatedAirflow - } - } - - public enum Action: BindableAction, ViewAction { - case binding(BindingAction) - case receive(TaskResult) - case view(View) - - @CasePathable - public enum View { - case infoButtonTapped - case resetButtonTapped - case submitField - } - } - - @Dependency(\.estimatedPressuresClient) var estimatedPressuresClient - @Dependency(\.logger["\(Self.self)"]) var logger - - public var body: some Reducer { - BindingReducer() - Reduce { state, action in - switch action { - case .binding: - return .none - - case let .receive(.success(calculatedMeasurement)): - state.calculatedMeasurement = calculatedMeasurement - return .none - - case .receive: - return .none - - case let .view(action): - switch action { - - case .infoButtonTapped: - #warning("Fix me.") - return .none - - case .resetButtonTapped: - state.measurement = .init() - state.updatedAirflow = nil - return .none - - case .submitField: - state.focusedField = state.focusedField?.next - guard state.isValid else { return .none } - return calculateEstimates(state: state) - - } - } - } - .onFailure(case: \.receive, .log(logger: logger)) - } - - private func calculateEstimates(state: State) -> Effect { - .receive(action: \.receive) { - let result = try await estimatedPressuresClient.estimatedPressure( - for: .airHandler(state.measurement), - at: state.updatedAirflow ?? 0 - ) - - guard case let .airHandler(airHandler) = result else { - return .none - } - return airHandler - } - } -} - -@ViewAction(for: AirHandlerMeasurementForm.self) -public struct AirHandlerMeasurementFormView: View { - @FocusState private var focusedField: AirHandlerMeasurementForm.State.Field? - @Bindable public var store: StoreOf - - public init( - store: StoreOf - ) { - self.store = store - self.focusedField = store.focusedField - } - - - public var body: some View { - Form { - Section { - textField( - "Return Plenum Pressure", - value: $store.measurement.returnPlenumPressure - ) - .focused($focusedField, equals: .returnPlenumPressure) - .onSubmit { send(.submitField) } - .decimalPad() - - textField( - "Post-Filter Pressure", - value: $store.measurement.postFilterPressure - ) - .focused($focusedField, equals: .postFilterPressure) - .onSubmit { send(.submitField) } - .decimalPad() - - textField( - "Post-Coil Pressure", - value: $store.measurement.postCoilPressure - ) - .focused($focusedField, equals: .postCoilPressure) - .onSubmit { send(.submitField) } - .decimalPad() - - textField( - "Supply Plenum Pressure", - value: $store.measurement.supplyPlenumPressure - ) - .focused($focusedField, equals: .supplyPlenumPressure) - .onSubmit { send(.submitField) } - .decimalPad() - - textField( - "Airflow", - value: $store.measurement.airflow, - fractionLength: 0 - ) - .focused($focusedField, equals: .airflow) - .onSubmit { send(.submitField) } - .numberPad() - - } header: { - HStack { - Text("System Measurements") - Spacer() - InfoButton { - send(.infoButtonTapped) - } - .font(.title3) - .labelStyle(.iconOnly) - .buttonStyle(.plain) - .foregroundStyle(Color.accentColor) - } - } - - Section { - FanTypePicker(selection: $store.fanType) - .pickerStyle(.segmented) - - textField( - "Updated Airflow", - value: $store.updatedAirflow, - fractionLength: 0 - ) - .focused($focusedField, equals: .updatedAirflow) - .onSubmit { send(.submitField) } - .numberPad() - - } header: { - Text("Estimate Settings") - } footer: { - HStack { - Spacer() - ResetButton { send(.resetButtonTapped) } - Spacer() - } - .padding(.top) - } - - if let calculatedMeasurement = store.calculatedMeasurement { - Section { - Text("Display calculated measurement here.") - } - } - - } - .bind($focusedField, to: $store.focusedField) - } - - private func textField( - _ title: LocalizedStringKey, - value: Binding, - fractionLength: Int = 2 - ) -> TextField { - .init(title, value: value, fractionLength: fractionLength, prompt: Text(title)) - } -} - -#Preview { - AirHandlerMeasurementFormView( - store: Store(initialState: AirHandlerMeasurementForm.State()) { - AirHandlerMeasurementForm() - } - ) -} diff --git a/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift b/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift index cd1d9ed..4de6185 100644 --- a/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift +++ b/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift @@ -16,35 +16,38 @@ public struct EquipmentMeasurementForm { @ObservableState public struct State: Equatable { @Presents public var destination: Destination.State? + public var allowEquipmentTypeSelection: Bool public var equipmentType: EquipmentType public var focusedField: Field? - public var fields: FormFields + public var measurements: Measurements public init( + allowEquipmentTypeSelection: Bool = true, destination: Destination.State? = nil, equipmentType: EquipmentType = .airHandler, focusedField: Field? = nil, - fields: FormFields = .init() + measurements: Measurements = .init() ) { + self.allowEquipmentTypeSelection = allowEquipmentTypeSelection self.destination = destination self.equipmentType = equipmentType self.focusedField = focusedField - self.fields = fields + self.measurements = measurements } public var equipmentMeasurement: EquipmentMeasurement { - fields.equipmentMeasurement(type: equipmentType) + measurements.equipmentMeasurement(type: equipmentType) } public var isValid: Bool { - fields.airflow != nil - && fields.returnPlenumPressure != nil - && fields.postFilterPressure != nil - && fields.coilPressure != nil - && fields.supplyPlenumPressure != nil + measurements.airflow != nil + && measurements.returnPlenumPressure != nil + && measurements.postFilterPressure != nil + && measurements.coilPressure != nil + && measurements.supplyPlenumPressure != nil } - public struct FormFields: Equatable { + public struct Measurements: Equatable { public var airflow: Double? public var returnPlenumPressure: Double? public var postFilterPressure: Double? @@ -112,7 +115,7 @@ public struct EquipmentMeasurementForm { } } - public enum Field: Hashable, CaseIterable, FocusableField, Identifiable { + public enum Field: CaseIterable, FocusableField, Hashable, Identifiable { case returnPlenumPressure case postFilterPressure case coilPressure @@ -147,16 +150,16 @@ public struct EquipmentMeasurementForm { case .destination: return .none - case let .view(action): switch action { case .infoButtonTapped: + state.destination = .infoView(.init()) return .none case .resetButtonTapped: - state.fields = .init() + state.measurements = .init() return .none case .submitField: @@ -220,21 +223,21 @@ public struct EquipmentMeasurementFormView: View { public var body: some View { Form { - Section { - - } header: { - Text("Equipment Type") - } footer: { - Picker("Equipment Type", selection: $store.equipmentType) { - ForEach(EquipmentType.allCases) { - Text($0.description) - .tag($0) + if store.allowEquipmentTypeSelection { + Section { + } header: { + Text("Equipment Type") + } footer: { + Picker("Equipment Type", selection: $store.equipmentType) { + ForEach(EquipmentType.allCases) { + Text($0.description) + .tag($0) + } } + .pickerStyle(.segmented) + .labelsHidden() } - .pickerStyle(.segmented) - .labelsHidden() } - Section { Grid(alignment: .leading, horizontalSpacing: 40) { ForEach(store.pressureFields) { field in @@ -268,8 +271,16 @@ public struct EquipmentMeasurementFormView: View { } } } + .navigationTitle("Existing Measurements") .textLabelStyle(.boldSecondary) .textFieldStyle(.roundedBorder) + .sheet( + item: $store.scope(state: \.destination?.infoView, action: \.destination.infoView) + ){ store in + NavigationStack { + InfoView(store: store) + } + } } private func textField( @@ -280,15 +291,15 @@ public struct EquipmentMeasurementFormView: View { switch field { case .returnPlenumPressure: - value = $store.fields.returnPlenumPressure + value = $store.measurements.returnPlenumPressure case .postFilterPressure: - value = $store.fields.postFilterPressure + value = $store.measurements.postFilterPressure case .coilPressure: - value = $store.fields.coilPressure + value = $store.measurements.coilPressure case .supplyPlenumPressure: - value = $store.fields.supplyPlenumPressure + value = $store.measurements.supplyPlenumPressure case .airflow: - value = $store.fields.airflow + value = $store.measurements.airflow fractionLength = 0 } @@ -325,13 +336,26 @@ public struct EquipmentMeasurementFormView: View { .decimalPad() } } - +} + +fileprivate extension InfoViewFeature.State { + + init() { + self.init( + title: "Existing Measurements", + body: """ + Record the current static pressure and airflow measurements of the existing system. + """ + ) + } } #Preview { - EquipmentMeasurementFormView( - store: Store(initialState: EquipmentMeasurementForm.State()) { - EquipmentMeasurementForm() - } - ) + NavigationStack { + EquipmentMeasurementFormView( + store: Store(initialState: EquipmentMeasurementForm.State()) { + EquipmentMeasurementForm() + } + ) + } } diff --git a/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift b/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift new file mode 100644 index 0000000..3999ad6 --- /dev/null +++ b/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift @@ -0,0 +1,405 @@ +import ComposableArchitecture +import SharedModels +import Styleguide +import SwiftUI + +@Reducer +public struct EquipmentSettingsForm { + + @CasePathable + public enum InfoView { + case capacities + case ratedStaticPressures + } + + @Reducer(state: .equatable) + public enum Destination { + case infoView(InfoViewFeature) + } + + @ObservableState + public struct State: Equatable { + + @Presents public var destination: Destination.State? + public var coolingCapacity: CoolingCapacity + public var equipmentType: EquipmentType + public var fanType: FanType + public var focusedField: Field? = nil + public var heatingCapacity: Double? + public var ratedStaticPressures: RatedStaticPressures + + public init( + coolingCapacity: CoolingCapacity = .default, + destination: Destination.State? = nil, + equipmentType: EquipmentType = .airHandler, + fanType: FanType = .constantSpeed, + heatingCapacity: Double? = nil, + ratedStaticPressures: RatedStaticPressures = .init() + ) { + self.coolingCapacity = coolingCapacity + self.destination = destination + self.equipmentType = equipmentType + self.fanType = fanType + self.heatingCapacity = heatingCapacity + self.ratedStaticPressures = ratedStaticPressures + } + + public var isValid: Bool { + guard equipmentType == .furnaceAndCoil else { return true } + return heatingCapacity != nil + } + + // Note: These need to be in display order. + public enum Field: Hashable, CaseIterable, FocusableField { + case heatingCapacity + case minimumStaticPressure + case maximumStaticPressure + case ratedStaticPressure + } + } + + public enum Action: BindableAction, ViewAction { + case binding(BindingAction) + case destination(PresentationAction) + case view(View) + + @CasePathable + public enum View { + case infoButtonTapped(InfoView) + case nextButtonTapped + case resetButtonTapped + case submitField + } + } + + public var body: some Reducer { + BindingReducer() + Reduce { state, action in + switch action { + + case .binding: + return .none + + case .destination(.dismiss): + state.destination = nil + return .none + + case .destination: + return .none + + case let .view(action): + switch action { + + case let .infoButtonTapped(infoView): + state.destination = .infoView(.init(view: infoView)) + return .none + + case .nextButtonTapped: + #warning("Fix me.") + return .none + + case .resetButtonTapped: + state.heatingCapacity = nil + return .none + + case .submitField: + state.focusedField = state.focusedField?.next + return .none + + } + } + } + .ifLet(\.$destination, action: \.destination) + } +} + +@ViewAction(for: EquipmentSettingsForm.self) +public struct EquipmentSettingsFormView: View { + + @FocusState private var focusedField: EquipmentSettingsForm.State.Field? + @Bindable public var store: StoreOf + + public init(store: StoreOf) { + self.store = store + self.focusedField = store.focusedField + } + + public var body: some View { + Form { + Section { + EmptyView() + } header: { + Text("Equipment Type") + } footer: { + EquipmentTypePicker(selection: $store.equipmentType) + .pickerStyle(.segmented) + } + Section { + EmptyView() + } header: { + Text("Fan Type") + #if os(macOS) + .font(.title2) + #endif + } footer: { + FanTypePicker(selection: $store.fanType) + .pickerStyle(.segmented) + } + + Section { + Grid(alignment: .leading, horizontalSpacing: 40) { + GridRow { + TextLabel("Cooling") + Picker("Cooling Capcity", selection: $store.coolingCapacity) { + ForEach(CoolingCapacity.allCases) { + Text($0.description) + .tag($0) + } + } + } + if store.equipmentType == .furnaceAndCoil { + GridRow { + TextLabel("Heating") + textField( + "Heating Capacity", + value: $store.heatingCapacity, + fractionLength: 0 + ) + .focused($focusedField, equals: .heatingCapacity) + .numberPad() + } + } + } + } header: { + header("Capacities", infoView: .capacities) + } + + Section { + Grid(alignment: .leading, horizontalSpacing: 40) { + GridRow { + TextLabel("Minimum") + textField( + "Minimum Pressure", + value: $store.ratedStaticPressures.minimum, + fractionLength: 2 + ) + .focused($focusedField, equals: .minimumStaticPressure) + .decimalPad() + } + GridRow { + TextLabel("Maximum") + textField( + "Maximum Pressure", + value: $store.ratedStaticPressures.maximum, + fractionLength: 2 + ) + .focused($focusedField, equals: .maximumStaticPressure) + .decimalPad() + } + GridRow { + TextLabel("Rated") + textField( + "Rated Pressure", + value: $store.ratedStaticPressures.rated, + fractionLength: 2 + ) + .focused($focusedField, equals: .ratedStaticPressure) + .decimalPad() + } + } + } header: { + header("Rated Static Pressure", infoView: .ratedStaticPressures) + } + +// Section { +// Grid(alignment: .leading, horizontalSpacing: 40) { +// if store.equipmentMeasurement.equipmentType == .furnaceAndCoil { +// GridRow { +// TextLabel("Coil") +// percentField("Coil Budget", value: $store.budgets.coilBudget.fraction) +// .focused($focusedField, equals: .coilBudget) +// .onSubmit { send(.submitField) } +// .numberPad() +// } +// } +// +// GridRow { +// TextLabel("Filter") +// percentField("Filter Budget", value: $store.budgets.filterBudget.fraction) +// .focused($focusedField, equals: .filterBudget) +// .onSubmit { send(.submitField) } +// .numberPad() +// } +// +// GridRow { +// TextLabel("Return") +// percentField("Return Plenum Budget", value: $store.budgets.returnPlenumBudget.fraction) +// .focused($focusedField, equals: .returnPlenumBudget) +// .onSubmit { send(.submitField) } +// .numberPad() +// } +// +// GridRow { +// TextLabel("Supply") +// percentField("Supply Plenum Budget", value: $store.budgets.supplyPlenumBudget.fraction) +// .focused($focusedField, equals: .supplyPlenumBudget) +// .onSubmit { send(.submitField) } +// .numberPad() +// } +// } +// +// } header: { +// VStack { +// header("Budgets", infoView: .budgets) +// #if os(macOS) +// .font(.title2) +// .padding(.top, 20) +// #endif +// FlaggedView( +// flagged: Flagged(wrappedValue: store.budgets.total.rawValue, .percent()) +// ) +// .flaggedViewStyle(BudgetFlagViewStyle()) +// } +// } + +// Section { +// TextField( +// "Airflow", +// value: $store.updatedAirflow, +// fractionLength: 0 +// ) +// } header: { +// header("Updated Airflow", infoView: .updatedAirflow) +// } footer: { +// HStack { +// ResetButton { send(.resetButtonTapped) } +// Spacer() +// NextButton { send(.nextButtonTapped) } +// .disabled(!store.isValid) +// } +// .padding(.top) +// } + } + .labelsHidden() + .bind($focusedField, to: $store.focusedField) + .navigationTitle("Equipment Data") + .textLabelStyle(.boldSecondary) + .textFieldStyle(.roundedBorder) + .sheet( + item: $store.scope( + state: \.destination?.infoView, + action: \.destination.infoView + ) + ) { store in + NavigationStack { + InfoView(store: store) + } + } + } + + private func header( + _ title: String, + infoView: EquipmentSettingsForm.InfoView + ) -> some View { + HStack { + Text(title) + Spacer() + InfoButton { send(.infoButtonTapped(infoView)) } + } + } + +// private func percentField( +// _ title: LocalizedStringKey, +// value: Binding +// ) -> some View { +// TextField(title, value: value, format: .percent, prompt: Text(title)) +// } + + private func textField( + _ title: String, + value: Binding, + fractionLength: Int = 2 + ) -> some View { + TextField(title, value: value, fractionLength: fractionLength, prompt: Text(title)) + } + + private func textField( + _ title: String, + value: Binding, + fractionLength: Int = 2 + ) -> some View { + TextField(title, value: value, fractionLength: fractionLength, prompt: Text(title)) + } +} + +fileprivate struct BudgetFlagViewStyle: FlaggedViewStyle { + + func makeBody(configuration: Configuration) -> some View { + HStack { + configuration.flagged.flagImage + Spacer() + configuration.flagged.messageView + } + } +} + +fileprivate extension InfoViewFeature.State { + init(view: EquipmentSettingsForm.InfoView) { + switch view { + case .capacities: + self.init( + title: "Capacities", + body: """ + Record the cooling and heating capacities of the system. + """ + ) +// case .budgets: +// self.init( +// title: "Budgets", +// body: """ +// Budgeted percentages for static pressure estimations, these generally are set to +// reasonable defaults, however you can change them if desired. +// +// Note: These must total up to 100%. +// """ +// ) + case .ratedStaticPressures: + self.init( + title: "Rated Static", + body: """ + Record the rated static pressures of the system. + + These are generally found on the nameplate of the equipment or in the installation + manual. + + The defaults are generally acceptable for most unitary equipment. + """ + ) + +// case .updatedAirflow: +// self.init( +// title: "Updated Airflow", +// body: """ +// This is used to generated estimated static pressures at the updated airflow +// compared to the existing airflow of the system. +// """ +// ) + } + } +} + +#Preview { + NavigationStack { + EquipmentSettingsFormView( + store: Store( + initialState: EquipmentSettingsForm.State() + ) { + EquipmentSettingsForm() + } + ) + #if os(macOS) + .frame(width: 400, height: 600) + .padding() + #endif + } +} diff --git a/Sources/PressureEstimationsFeature/EstimateSettingsForm.swift b/Sources/PressureEstimationsFeature/EstimateSettingsForm.swift deleted file mode 100644 index 5e70283..0000000 --- a/Sources/PressureEstimationsFeature/EstimateSettingsForm.swift +++ /dev/null @@ -1,303 +0,0 @@ -import ComposableArchitecture -import SharedModels -import Styleguide -import SwiftUI - -@Reducer -public struct EstimateSettingsForm { - - @CasePathable - public enum InfoView { - case budgets - case updatedAirflow - } - - @Reducer(state: .equatable) - public enum Destination { - case infoView(InfoViewFeature) - } - - @ObservableState - public struct State: Equatable { - - @Presents public var destination: Destination.State? - public var budgets: BudgetedPercentEnvelope - public let equipmentMeasurement: EquipmentMeasurement - public var fanType: FanType - public var focusedField: Field? = nil - public var updatedAirflow: Double? - - public init( - destination: Destination.State? = nil, - equipmentMeasurement: EquipmentMeasurement, - fanType: FanType = .constantSpeed, - updatedAirflow: Double? = nil - ) { - self.destination = destination - self.equipmentMeasurement = equipmentMeasurement - self.budgets = .init( - equipmentType: equipmentMeasurement.equipmentType, - fanType: fanType - ) - self.fanType = fanType - self.updatedAirflow = updatedAirflow - } - - public var isValid: Bool { - updatedAirflow != nil - && Int(budgets.total.rawValue) == 100 - } - - public enum Field: Hashable, CaseIterable, FocusableField { - case coilBudget - case filterBudget - case returnPlenumBudget - case supplyPlenumBudget - case updatedAirflow - } - } - - public enum Action: BindableAction, ViewAction { - case binding(BindingAction) - case destination(PresentationAction) - case view(View) - - @CasePathable - public enum View { - case infoButtonTapped(InfoView) - case nextButtonTapped - case resetButtonTapped - case submitField - } - } - - public var body: some Reducer { - BindingReducer() - Reduce { state, action in - switch action { - - case .binding(\.fanType): - state.budgets = .init( - equipmentType: state.equipmentMeasurement.equipmentType, - fanType: state.fanType - ) - return .none - - case .binding: - return .none - - case .destination(.dismiss): - state.destination = nil - return .none - - case .destination: - return .none - - case let .view(action): - switch action { - - case let .infoButtonTapped(infoView): - state.destination = .infoView(.init(view: infoView)) - return .none - - case .nextButtonTapped: - #warning("Fix me.") - return .none - - case .resetButtonTapped: - state.budgets = .init( - equipmentType: state.equipmentMeasurement.equipmentType, - fanType: state.fanType - ) - state.updatedAirflow = nil - return .none - - case .submitField: - state.focusedField = state.focusedField?.next - return .none - - } - } - } - .ifLet(\.$destination, action: \.destination) - } -} - -@ViewAction(for: EstimateSettingsForm.self) -public struct EstimateSettingsFormView: View { - - @FocusState private var focusedField: EstimateSettingsForm.State.Field? - @Bindable public var store: StoreOf - - public init(store: StoreOf) { - self.store = store - self.focusedField = store.focusedField - } - - public var body: some View { - Form { - Section { - EmptyView() - } header: { - TextLabel("Fan Type") - #if os(macOS) - .font(.title2) - #endif - } footer: { - FanTypePicker(selection: $store.fanType) - .pickerStyle(.segmented) - } - - Section { - Grid(alignment: .leading, horizontalSpacing: 40) { - if store.equipmentMeasurement.equipmentType == .furnaceAndCoil { - GridRow { - TextLabel("Coil") - percentField("Coil Budget", value: $store.budgets.coilBudget.fraction) - .focused($focusedField, equals: .coilBudget) - .onSubmit { send(.submitField) } - .numberPad() - } - } - - GridRow { - TextLabel("Filter") - percentField("Filter Budget", value: $store.budgets.filterBudget.fraction) - .focused($focusedField, equals: .filterBudget) - .onSubmit { send(.submitField) } - .numberPad() - } - - GridRow { - TextLabel("Return") - percentField("Return Plenum Budget", value: $store.budgets.returnPlenumBudget.fraction) - .focused($focusedField, equals: .returnPlenumBudget) - .onSubmit { send(.submitField) } - .numberPad() - } - - GridRow { - TextLabel("Supply") - percentField("Supply Plenum Budget", value: $store.budgets.supplyPlenumBudget.fraction) - .focused($focusedField, equals: .supplyPlenumBudget) - .onSubmit { send(.submitField) } - .numberPad() - } - } - .textLabelStyle(.boldSecondary) - .textFieldStyle(.roundedBorder) - - } header: { - VStack { - HStack { - Text("Budgets") - Spacer() - InfoButton { send(.infoButtonTapped(.budgets)) } - } - #if os(macOS) - .font(.title2) - .padding(.top, 20) - #endif - FlaggedView( - flagged: Flagged(wrappedValue: store.budgets.total.rawValue, .percent()) - ) - .flaggedViewStyle(BudgetFlagViewStyle()) - } - } - - Section { - TextField( - "Airflow", - value: $store.updatedAirflow, - fractionLength: 0 - ) - } header: { - HStack { - Text("Updated Airflow") - Spacer() - InfoButton { send(.infoButtonTapped(.updatedAirflow)) } - } - } footer: { - HStack { - ResetButton { send(.resetButtonTapped) } - Spacer() - NextButton { send(.nextButtonTapped) } - .disabled(!store.isValid) - } - .padding(.top) - } - } - .labelsHidden() - .bind($focusedField, to: $store.focusedField) - .navigationTitle("Estimate Settings") - .sheet( - item: $store.scope( - state: \.destination?.infoView, - action: \.destination.infoView - ) - ) { store in - NavigationStack { - InfoView(store: store) - } - } - } - - private func percentField( - _ title: LocalizedStringKey, - value: Binding - ) -> some View { - TextField(title, value: value, format: .percent, prompt: Text(title)) - } -} - -fileprivate struct BudgetFlagViewStyle: FlaggedViewStyle { - - func makeBody(configuration: Configuration) -> some View { - HStack { - configuration.flagged.flagImage - Spacer() - configuration.flagged.messageView - } - } -} - -fileprivate extension InfoViewFeature.State { - init(view: EstimateSettingsForm.InfoView) { - switch view { - case .budgets: - self.init( - title: "Budgets", - body: """ - Budgeted percentages for static pressure estimations, these generally are set to - reasonable defaults, however you can change them if desired. - - Note: These must total up to 100%. - """ - ) - case .updatedAirflow: - self.init( - title: "Updated Airflow", - body: """ - This is used to generated estimated static pressures at the updated airflow - compared to the existing airflow of the system. - """ - ) - } - } -} - -#Preview { - NavigationStack { - EstimateSettingsFormView( - store: Store( - initialState: EstimateSettingsForm.State(equipmentMeasurement: .furnaceAndCoil(.init())) - ) { - EstimateSettingsForm() - } - ) - #if os(macOS) - .frame(width: 400, height: 600) - .padding() - #endif - } -} diff --git a/Sources/SharedModels/CoolingCapacity.swift b/Sources/SharedModels/CoolingCapacity.swift index 0f33764..6339dfb 100644 --- a/Sources/SharedModels/CoolingCapacity.swift +++ b/Sources/SharedModels/CoolingCapacity.swift @@ -1,6 +1,6 @@ import Foundation -public enum CoolingCapacity: Double, Equatable, CaseIterable { +public enum CoolingCapacity: Double, Hashable, CaseIterable, Identifiable, CustomStringConvertible { case half = 0.5 case threeQuarter = 0.75 case one = 1 @@ -11,6 +11,33 @@ public enum CoolingCapacity: Double, Equatable, CaseIterable { case threeAndAHalf = 3.5 case four = 4 case five = 5 - + + public var id: Self { self } + public static var `default`: Self { .three } + + public var description: String { + switch self { + case .half: + return "1/2 Ton" + case .threeQuarter: + return "3/4 Ton" + case .one: + return "1 Ton" + case .oneAndAHalf: + return "1.5 Tons" + case .two: + return "2 Tons" + case .twoAndAHalf: + return "2.5 Tons" + case .three: + return "3 Tons" + case .threeAndAHalf: + return "3.5 Tons" + case .four: + return "4 Tons" + case .five: + return "5 Tons" + } + } } diff --git a/Sources/SharedModels/EquipmentMeasurement.swift b/Sources/SharedModels/EquipmentMeasurement.swift index 381e78c..5f6d42d 100644 --- a/Sources/SharedModels/EquipmentMeasurement.swift +++ b/Sources/SharedModels/EquipmentMeasurement.swift @@ -1,5 +1,6 @@ import Foundation +#warning("Make values non-optional") public enum EquipmentMeasurement: Equatable { case airHandler(AirHandler) diff --git a/Sources/Styleguide/EquipmentTypePicker.swift b/Sources/Styleguide/EquipmentTypePicker.swift new file mode 100644 index 0000000..4d7c059 --- /dev/null +++ b/Sources/Styleguide/EquipmentTypePicker.swift @@ -0,0 +1,19 @@ +import SharedModels +import SwiftUI + +public struct EquipmentTypePicker: View { + @Binding var selection: EquipmentType + + public init(selection: Binding) { + self._selection = selection + } + + public var body: some View { + Picker("Equipment Type", selection: $selection) { + ForEach(EquipmentType.allCases) { + Text($0.description) + .tag($0) + } + } + } +} diff --git a/Sources/Styleguide/FlaggedView.swift b/Sources/Styleguide/FlaggedView.swift index 06dd7f8..02b4c7e 100644 --- a/Sources/Styleguide/FlaggedView.swift +++ b/Sources/Styleguide/FlaggedView.swift @@ -34,109 +34,3 @@ extension FlaggedView where Label == EmptyView { self.init(flagged: flagged) { EmptyView() } } } - -//public struct FlaggedView: View { -// -// public let style: Style? -// public let title: String -// public let flagged: Flagged -// public let fractionLength: Int -// -// public init( -// style: Style? = nil, -// title: String, -// flagged: Flagged, -// fractionLength: Int = 3 -// ) { -// self.style = style -// self.title = title -// self.flagged = flagged -// self.fractionLength = fractionLength -// } -// -// public init( -// _ title: String, -// flagged: Flagged, -// style: Style? = nil, -// fractionLength: Int = 3 -// ) { -// self.style = style -// self.title = title -// self.flagged = flagged -// self.fractionLength = fractionLength -// } -// -// @ViewBuilder -// private var messageView: some View { -// if let message = flagged.message { -// HStack { -// Text(flagged.projectedValue.key.title) -// .bold() -// .foregroundStyle(flagged.projectedValue.key.flagColor) -// -// Text(message) -// .foregroundStyle(Color.secondary) -// } -// .font(.caption) -// } -// } -// -// public var body: some View { -// switch style { -// case .none: -// VStack(alignment: .leading, spacing: 8) { -// HStack { -// Text(title) -// .foregroundStyle(Color.secondary) -// Spacer() -// Text( -// flagged.wrappedValue, -// format: .number.precision(.fractionLength(fractionLength)) -// ) -// Image(systemName: "flag.fill") -// .foregroundStyle(flagged.projectedValue.key.flagColor) -// } -// messageView -// } -// case .some(.gridRow): -// GridRow { -// VStack(alignment: .leading, spacing: 8) { -// Text(title) -// .foregroundStyle(Color.secondary) -// messageView -// } -// -// Spacer() -// -// Text(flagged.wrappedValue, format: .number.precision(.fractionLength(fractionLength))) -// .gridCellAnchor(.trailing) -// -// Image(systemName: "flag.fill") -// .foregroundStyle(flagged.flagColor) -// .gridCellAnchor(.trailing) -// } -// -// } -// } -// -// public enum Style { -// case gridRow -// } -// -// public static func gridRow(_ title: String, flagged: Flagged) -> Self { -// .init(style: .gridRow, title: title, flagged: flagged) -// } -//} - - -//#Preview { -// List { -// Grid(horizontalSpacing: 20) { -// FlaggedView(style: .gridRow, title: "Test", flagged: .error(message: "Error message")) -// FlaggedView(style: .gridRow, title: "Test", flagged: .init(wrappedValue: 0.5, .result(.good("Good message")))) -// } -// -// FlaggedView(style: nil, title: "Test", flagged: .error(message: "Error message")) -// FlaggedView(style: nil, title: "Test", flagged: .init(wrappedValue: 0.5, .result(.good("Good message")))) -// } -//} diff --git a/Sources/Styleguide/InfoView.swift b/Sources/Styleguide/InfoView.swift index 87f797b..05bd5a5 100644 --- a/Sources/Styleguide/InfoView.swift +++ b/Sources/Styleguide/InfoView.swift @@ -44,13 +44,12 @@ public struct InfoView: View { } public var body: some View { - VStack(spacing: 40) { - Text(store.bodyText) - .font(.callout) - .padding() + VStack { + TextLabel(store.bodyText) Spacer() } .navigationTitle(store.titleText) + .textLabelStyle(.secondary) .padding(.horizontal) .navigationBarBackButtonHidden() .toolbar { diff --git a/Sources/Styleguide/TextField+Precision.swift b/Sources/Styleguide/TextField+Precision.swift index 2fecd5d..ad0c20e 100644 --- a/Sources/Styleguide/TextField+Precision.swift +++ b/Sources/Styleguide/TextField+Precision.swift @@ -30,4 +30,32 @@ extension TextField where Label == Text { ) } + public init( + _ titleKey: LocalizedStringKey, + value: Binding, + fractionLength: Int, + prompt: Text? = nil + ) { + self.init( + titleKey, + value: value, + format: .number.precision(.fractionLength(fractionLength)), + prompt: prompt + ) + } + + public init( + _ titleKey: S, + value: Binding, + fractionLength: Int, + prompt: Text? = nil + ) { + self.init( + titleKey, + value: value, + format: .number.precision(.fractionLength(fractionLength)), + prompt: prompt + ) + } + }