306 lines
7.7 KiB
Swift
306 lines
7.7 KiB
Swift
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()
|
|
Button("Next") {
|
|
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
|
|
}
|
|
}
|