Feat: Fixing equipment settings form

This commit is contained in:
2024-06-05 10:13:41 -04:00
parent 6ec3eacb8d
commit c13b3740f2
10 changed files with 545 additions and 693 deletions

View File

@@ -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<State>)
case receive(TaskResult<EquipmentMeasurement.AirHandler?>)
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<State, Action> {
BindingReducer()
Reduce<State, Action> { 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<Action> {
.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<AirHandlerMeasurementForm>
public init(
store: StoreOf<AirHandlerMeasurementForm>
) {
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<Double?>,
fractionLength: Int = 2
) -> TextField<Text> {
.init(title, value: value, fractionLength: fractionLength, prompt: Text(title))
}
}
#Preview {
AirHandlerMeasurementFormView(
store: Store(initialState: AirHandlerMeasurementForm.State()) {
AirHandlerMeasurementForm()
}
)
}

View File

@@ -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
@@ -148,15 +151,15 @@ 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()
}
)
}
}

View File

@@ -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<State>)
case destination(PresentationAction<Destination.Action>)
case view(View)
@CasePathable
public enum View {
case infoButtonTapped(InfoView)
case nextButtonTapped
case resetButtonTapped
case submitField
}
}
public var body: some Reducer<State, Action> {
BindingReducer()
Reduce<State, Action> { 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<EquipmentSettingsForm>
public init(store: StoreOf<EquipmentSettingsForm>) {
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<Double>
// ) -> some View {
// TextField(title, value: value, format: .percent, prompt: Text(title))
// }
private func textField(
_ title: String,
value: Binding<Double>,
fractionLength: Int = 2
) -> some View {
TextField(title, value: value, fractionLength: fractionLength, prompt: Text(title))
}
private func textField(
_ title: String,
value: Binding<Double?>,
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
}
}

View File

@@ -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<State>)
case destination(PresentationAction<Destination.Action>)
case view(View)
@CasePathable
public enum View {
case infoButtonTapped(InfoView)
case nextButtonTapped
case resetButtonTapped
case submitField
}
}
public var body: some Reducer<State, Action> {
BindingReducer()
Reduce<State, Action> { 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<EstimateSettingsForm>
public init(store: StoreOf<EstimateSettingsForm>) {
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<Double>
) -> 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
}
}

View File

@@ -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
@@ -12,5 +12,32 @@ public enum CoolingCapacity: Double, Equatable, CaseIterable {
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"
}
}
}

View File

@@ -1,5 +1,6 @@
import Foundation
#warning("Make values non-optional")
public enum EquipmentMeasurement: Equatable {
case airHandler(AirHandler)

View File

@@ -0,0 +1,19 @@
import SharedModels
import SwiftUI
public struct EquipmentTypePicker: View {
@Binding var selection: EquipmentType
public init(selection: Binding<EquipmentType>) {
self._selection = selection
}
public var body: some View {
Picker("Equipment Type", selection: $selection) {
ForEach(EquipmentType.allCases) {
Text($0.description)
.tag($0)
}
}
}
}

View File

@@ -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"))))
// }
//}

View File

@@ -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 {

View File

@@ -30,4 +30,32 @@ extension TextField where Label == Text {
)
}
public init(
_ titleKey: LocalizedStringKey,
value: Binding<Double>,
fractionLength: Int,
prompt: Text? = nil
) {
self.init(
titleKey,
value: value,
format: .number.precision(.fractionLength(fractionLength)),
prompt: prompt
)
}
public init<S: StringProtocol>(
_ titleKey: S,
value: Binding<Double>,
fractionLength: Int,
prompt: Text? = nil
) {
self.init(
titleKey,
value: value,
format: .number.precision(.fractionLength(fractionLength)),
prompt: prompt
)
}
}