Feat: Working on pressure estimations feature

This commit is contained in:
2024-06-04 13:06:49 -04:00
parent 04c899da8e
commit 076488beb4
19 changed files with 1206 additions and 128 deletions

View File

@@ -0,0 +1,242 @@
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

@@ -0,0 +1,236 @@
import ComposableArchitecture
import SharedModels
import Styleguide
import SwiftUI
@Reducer
public struct EstimateSettingsForm {
@ObservableState
public struct State: Equatable {
public var budgets: BudgetedPercentEnvelope
public let equipmentType: EquipmentType
public var fanType: FanType
public var focusedField: Field? = nil
public var updatedAirflow: Double?
public init(
equipmentType: EquipmentType,
fanType: FanType = .constantSpeed,
updatedAirflow: Double? = nil
) {
self.budgets = .init(equipmentType: equipmentType, fanType: fanType)
self.equipmentType = equipmentType
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 view(View)
@CasePathable
public enum View {
case infoButtonTapped(InfoButton)
case resetButtonTapped
case submitField
@CasePathable
public enum InfoButton {
case budgets
case updatedAirflow
}
}
}
public var body: some Reducer<State, Action> {
BindingReducer()
Reduce<State, Action> { state, action in
switch action {
case .binding(\.fanType):
state.budgets = .init(
equipmentType: state.equipmentType,
fanType: state.fanType
)
return .none
case .binding:
return .none
case let .view(action):
switch action {
case .infoButtonTapped:
#warning("Fix me.")
return .none
case .resetButtonTapped:
state.budgets = .init(equipmentType: state.equipmentType, fanType: state.fanType)
state.updatedAirflow = nil
return .none
case .submitField:
state.focusedField = state.focusedField?.next
return .none
}
}
}
}
}
@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.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 {
} header: {
HStack {
Text("Updated Airflow")
Spacer()
InfoButton { send(.infoButtonTapped(.updatedAirflow)) }
}
} footer: {
HStack {
ResetButton { send(.resetButtonTapped) }
Spacer()
Button("Next") {
#warning("Fix me.")
}
}
.padding(.top)
}
}
.labelsHidden()
.bind($focusedField, to: $store.focusedField)
.navigationTitle("Estimate Settings")
}
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
}
}
}
#Preview {
NavigationStack {
EstimateSettingsFormView(
store: Store(
initialState: EstimateSettingsForm.State(equipmentType: .furnaceAndCoil)
) {
EstimateSettingsForm()
}
)
#if os(macOS)
.frame(width: 400, height: 600)
.padding()
#endif
}
}

View File

@@ -0,0 +1,4 @@
import ComposableArchitecture
import SharedModels
import SwiftUI
import TCAExtras