diff --git a/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift b/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift index 8208d64..55e075c 100644 --- a/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift +++ b/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift @@ -304,9 +304,14 @@ public struct EquipmentMeasurementFormView: View { } Section { - Grid(alignment: .leading, horizontalSpacing: 60) { + Grid(alignment: .leading, horizontalSpacing: 80) { gridRow(for: .airflow) } + } header: { + HStack { + Spacer() + Text(store.sharedSettings.equipmentMetadata.coolingCapacity.description) + } } footer: { HStack { Spacer() diff --git a/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift b/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift index b943e03..002bd2b 100644 --- a/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift +++ b/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift @@ -355,45 +355,6 @@ fileprivate enum RatingsField: String, Hashable, CaseIterable, Identifiable { var prompt: String { "\(label) Pressure" } } -//fileprivate extension Store where State == EquipmentSettingsForm.State { -// -// -// func label(for ratingsField: RatingsField) -> String { -// self.label(for: ratingsField.field) -// } -// -// func prompt(for ratingsField: RatingsField) -> String { -// self.prompt(for: ratingsField.field) -// } -// -// func label(for field: EquipmentSettingsForm.State.Field) -> String { -// switch field { -// case .heatingCapacity: -// return "Heating" -// case .minimumStaticPressure: -// return "Minimum" -// case .maximumStaticPressure: -// return "Maximum" -// case .ratedStaticPressure: -// return "Rated" -// case .manufacturersIncludedFilterPressureDrop: -// return "Filter Drop" -// } -// } -// -// -// func prompt(for field: EquipmentSettingsForm.State.Field) -> String { -// switch field { -// case .heatingCapacity: -// return "Heating Capacity" -// case .minimumStaticPressure, .maximumStaticPressure, .ratedStaticPressure: -// return "\(label(for: field)) Pressure" -// case .manufacturersIncludedFilterPressureDrop: -// return label(for: field) -// } -// } -//} - #Preview { NavigationStack { EquipmentSettingsFormView( diff --git a/Sources/PressureEstimationsFeature/EstimationsForm.swift b/Sources/PressureEstimationsFeature/EstimationsForm.swift index bc555c1..840a4de 100644 --- a/Sources/PressureEstimationsFeature/EstimationsForm.swift +++ b/Sources/PressureEstimationsFeature/EstimationsForm.swift @@ -3,35 +3,82 @@ import SharedModels import Styleguide import SwiftUI -#warning("Use shared settings, don't display filter pressure drop if current flagged measurement pressure drop is not set.") @Reducer public struct EstimationForm { public init() { } @ObservableState public struct State: Equatable, Sendable { - public var cfmPerTon: Int + @SharedReader public var existingMeasurement: EquipmentMeasurement? + + public let id: SharedPressureEstimationState.FlaggedEstimationContainer.ID? + public var airflowSelection: AirflowSelection + public var cfmTextField: Int? public var coolingCapacity: EquipmentMetadata.CoolingCapacity public var filterPressureDrop: Double? public var name: String public init( - cfmPerTon: Int = 350, + id: SharedPressureEstimationState.FlaggedEstimationContainer.ID? = nil, + existingMeasurement: SharedReader, + airflowSelection: AirflowSelection = .cfmPerTon, + cfmTextField: Int? = nil, coolingCapacity: EquipmentMetadata.CoolingCapacity = .default, filterPressureDrop: Double? = nil, name: String = "" ) { - self.cfmPerTon = cfmPerTon + self.id = id + self._existingMeasurement = existingMeasurement + self.airflowSelection = airflowSelection + self.cfmTextField = cfmTextField self.filterPressureDrop = filterPressureDrop self.coolingCapacity = coolingCapacity self.name = name } public var airflow: Double { - Double(cfmPerTon) * coolingCapacity.rawValue + let cfmTextField = Double(self.cfmTextField ?? 0) + switch airflowSelection { + case .cfmPerTon: + return Double(cfmTextField) * coolingCapacity.rawValue + case .cfm: + return Double(cfmTextField) + } } - public var isValid: Bool { !name.isEmpty } + public var isValid: Bool { + !name.isEmpty + && cfmTextField != nil + } + + // Note: Keep in display order of the picker. + public enum AirflowSelection: Hashable, CaseIterable, Identifiable, CustomStringConvertible { + case cfmPerTon + case cfm + + init( + _ container: SharedPressureEstimationState.FlaggedEstimationContainer.EstimationState.CFMContainer + ) { + switch container { + case .cfm: + self = .cfm + case .cfmPerTon: + self = .cfmPerTon + } + } + + public var id: Self { self } + + public var description: String { + switch self { + case .cfm: + return "CFM" + case .cfmPerTon: + return "CFM / Ton" + } + } + } + } public enum Action: BindableAction { @@ -68,47 +115,61 @@ public struct EstimationFormView: View { } } Section("Airflow") { - Grid(alignment: .leading, horizontalSpacing: 40) { - GridRow { - HStack { - TextLabel("Capacity") - Spacer() - CoolingCapacityPicker( - selection: $store.coolingCapacity - ) + VStack { + CaseIterablePicker( + "Airflow Type", + selection: $store.airflowSelection + ) + .pickerStyle(.segmented) + + Grid(alignment: .leading, horizontalSpacing: 40) { + if store.airflowSelection == .cfmPerTon { + GridRow { + HStack { + TextLabel("Capacity") + Spacer() + CoolingCapacityPicker( + selection: $store.coolingCapacity + ) + } + .gridCellColumns(2) + } } - .gridCellColumns(2) - } - GridRow { - HStack { - TextLabel("CFM / Ton") - Spacer() - TextField( - "CFM / Ton", - value: $store.cfmPerTon, - format: .number, - prompt: Text("CFM") - ) - .frame(width: 100) - .numberPad() + GridRow { + HStack { + TextLabel(store.airflowSelection.description) + Spacer() + TextField( + "CFM / Ton", + value: $store.cfmTextField, + format: .number, + prompt: Text("CFM") + ) + .frame(width: 100) + .numberPad() + } + .gridCellColumns(2) } - .gridCellColumns(2) } } } - Section("Filter Pressure Drop") { - HStack { - TextLabel("Pressure Drop") - Spacer() - TextField( - "Filter Drop", - value: $store.filterPressureDrop, - fractionLength: 2, - prompt: Text("Optional") - ) - .frame(width: 100) - .decimalPad() + if let existingsMeasurement = store.existingMeasurement, + existingsMeasurement.hasFilterDrop + { + Section("Filter Pressure Drop") { + HStack { + TextLabel("Pressure Drop") + Spacer() + TextField( + "Filter Drop", + value: $store.filterPressureDrop, + fractionLength: 2, + prompt: Text("Optional") + ) + .frame(width: 100) + .decimalPad() + } } } } @@ -118,9 +179,27 @@ public struct EstimationFormView: View { } } +fileprivate extension EquipmentMeasurement { + + var hasFilterDrop: Bool { + switch self { + case let .airHandler(airHandler): + return airHandler.postFilterPressure > 0 + case let .furnaceAndCoil(furnace): + return furnace.postFilterPressure > 0 + } + } +} + #Preview { EstimationFormView( - store: Store(initialState: EstimationForm.State()) { + store: Store( + initialState: EstimationForm.State( + existingMeasurement: SharedReader( + Shared(EquipmentMeasurement.mock(type: .airHandler)) + ) + ) + ) { EstimationForm() } ) diff --git a/Sources/PressureEstimationsFeature/FlaggedMeasurementsList.swift b/Sources/PressureEstimationsFeature/FlaggedMeasurementsList.swift index 31fd867..b154679 100644 --- a/Sources/PressureEstimationsFeature/FlaggedMeasurementsList.swift +++ b/Sources/PressureEstimationsFeature/FlaggedMeasurementsList.swift @@ -57,8 +57,9 @@ public struct FlaggedMeasurementsList: Sendable { @CasePathable public enum View { case addButtonTapped + case deleteEstimationButtonTapped(id: SharedPressureEstimationState.FlaggedEstimationContainer.ID) case destination(DestinationAction) - case editButtonTapped(id: SharedPressureEstimationState.FlaggedMeasurementContainer.ID) + case editButtonTapped(id: SharedPressureEstimationState.FlaggedEstimationContainer.ID) case onAppear @CasePathable @@ -107,10 +108,15 @@ public struct FlaggedMeasurementsList: Sendable { case .addButtonTapped: state.destination = .estimationForm(.init( + existingMeasurement: state.$sharedSettings.equipmentMeasurement, coolingCapacity: state.sharedSettings.equipmentMetadata.coolingCapacity )) return .none - + + case let .deleteEstimationButtonTapped(id: id): + state.sharedSettings.flaggedEstimations.remove(id: id) + return .none + case let .destination(action): switch action { case .cancelButtonTapped: @@ -237,7 +243,16 @@ public struct FlaggedMeasurementListView: View { HStack { Text(measurement.name) Spacer() - Button("Edit") { send(.editButtonTapped(id: measurement.id)) } + Menu { + EditButton { send(.editButtonTapped(id: measurement.id)) } + DeleteButton { send(.deleteEstimationButtonTapped(id: measurement.id)) } + } label: { + Label("Actions", systemImage: "list.dash") + } primaryAction: { + send(.editButtonTapped(id: measurement.id)) + } + .labelStyle(.iconOnly) + .menuStyle(ButtonMenuStyle()) } } } @@ -282,7 +297,7 @@ public struct FlaggedMeasurementListView: View { #if DEBUG private let budgets = BudgetedPercentEnvelope(equipmentType: .airHandler, fanType: .constantSpeed) -private let flaggedMeasurements = IdentifiedArrayOf( +private let flaggedMeasurements = IdentifiedArrayOf( uniqueElements: [ .init( id: UUID(0), diff --git a/Sources/PressureEstimationsFeature/SharedPressureEstimationState.swift b/Sources/PressureEstimationsFeature/SharedPressureEstimationState.swift index 731c6e9..103f06a 100644 --- a/Sources/PressureEstimationsFeature/SharedPressureEstimationState.swift +++ b/Sources/PressureEstimationsFeature/SharedPressureEstimationState.swift @@ -9,7 +9,7 @@ public struct SharedPressureEstimationState: Equatable, Sendable { public var equipmentMeasurement: EquipmentMeasurement? public var equipmentMetadata: EquipmentMetadata public var flaggedEquipmentMeasurement: EquipmentMeasurement.FlaggedMeasurement? - public var flaggedEstimations: IdentifiedArrayOf + public var flaggedEstimations: IdentifiedArrayOf public var heatingCapacity: Double? public var manufacturersIncludedFilterPressureDrop: Double? @@ -18,7 +18,7 @@ public struct SharedPressureEstimationState: Equatable, Sendable { equipmentMeasurement: EquipmentMeasurement? = nil, equipmentMetadata: EquipmentMetadata = .init(), flaggedEquipmentMeasurement: EquipmentMeasurement.FlaggedMeasurement? = nil, - flaggedEstimations: IdentifiedArrayOf = [], + flaggedEstimations: IdentifiedArrayOf = [], heatingCapacity: Double? = nil, manufacturersIncludedFilterPressureDrop: Double? = nil ) { @@ -36,7 +36,8 @@ public struct SharedPressureEstimationState: Equatable, Sendable { set { equipmentMetadata[keyPath: keyPath] = newValue } } - public struct FlaggedMeasurementContainer: Equatable, Identifiable, Sendable { + #warning("Needs to hold onto estimation state, so it can be editable") + public struct FlaggedEstimationContainer: Equatable, Identifiable, Sendable { public let id: UUID public var flaggedMeasurement: EquipmentMeasurement.FlaggedMeasurement public var name: String @@ -50,6 +51,44 @@ public struct SharedPressureEstimationState: Equatable, Sendable { self.name = name self.flaggedMeasurement = flaggedMeasurement } + + public struct EstimationState: Equatable, Sendable { + public var cfm: CFMContainer + public var filterPressureDrop: Double? + public var name: String? + + public init( + cfm: CFMContainer, + filterPressureDrop: Double? = nil, + name: String? = nil + ) { + self.cfm = cfm + self.filterPressureDrop = filterPressureDrop + self.name = name + } + + var displayName: String { + guard let name else { + return "@\(Int(cfm.airflow)) CFM" + } + return name + } + + public enum CFMContainer: Equatable, Sendable { + case cfm(Int) + case cfmPerTon(Int, EquipmentMetadata.CoolingCapacity) + + var airflow: Double { + switch self { + case let .cfm(cfm): + return Double(cfm) + case let .cfmPerTon(cfmPerTon, capacity): + return Double(cfmPerTon) * capacity.rawValue + } + } + } + } + } } diff --git a/Sources/Styleguide/Buttons.swift b/Sources/Styleguide/Buttons.swift index a5f269f..efa58b0 100644 --- a/Sources/Styleguide/Buttons.swift +++ b/Sources/Styleguide/Buttons.swift @@ -1,5 +1,19 @@ import SwiftUI +public struct DeleteButton: View { + let action: () -> Void + + public init(action: @escaping () -> Void) { + self.action = action + } + + public var body: some View { + Button(role: .destructive, action: action) { + Label("Delete", systemImage: "trash") + } + } +} + public struct DoneButton: View { let action: () -> Void @@ -15,6 +29,24 @@ public struct DoneButton: View { } } +public struct EditButton: View { + + @Environment(\.editButtonStyle) private var style + + let action: () -> Void + + public init(action: @escaping () -> Void) { + self.action = action + } + + public var body: some View { + Button(action: action) { + Label("Edit", systemImage: "square.and.pencil") + } + .buttonStyle(style) + } +} + public struct InfoButton: View { @Environment(\.infoButtonStyle) private var infoButtonStyle diff --git a/Sources/Styleguide/Styles/ButtonStyles.swift b/Sources/Styleguide/Styles/ButtonStyles.swift index 6c8c4cd..bc18cb8 100644 --- a/Sources/Styleguide/Styles/ButtonStyles.swift +++ b/Sources/Styleguide/Styles/ButtonStyles.swift @@ -1,5 +1,10 @@ import SwiftUI +#warning("Remove button types and just make styles stand-alone") + +/// A name space for edit button styles. +public enum EditButtonType { } + /// A name space for info button styles. public enum InfoButtonType { } @@ -119,6 +124,10 @@ private struct ResetButtonStyleKey: EnvironmentKey { extension EnvironmentValues { // @Entry var infoButtonStyle: AnyButtonStyle = AnyButtonStyle.default + @Entry var editButtonStyle = MainActor.assumeIsolated { + AnyPrimitiveButtonStyle(DefaultInfoButtonStyle(labelStyle: .automatic)) + } + var infoButtonStyle: AnyPrimitiveButtonStyle { get { self[InfoButtonStyleKey.self] } set { self[InfoButtonStyleKey.self] = newValue } @@ -137,6 +146,16 @@ extension EnvironmentValues { extension View { + /// Sets the button style for the ``EditButton`` type. + public func editButtonStyle(_ style: AnyPrimitiveButtonStyle) -> some View { + environment(\.editButtonStyle, style) + } + + /// Sets the button style for the ``EditButton`` type. + public func editButtonStyle(_ style: S) -> some View { + editButtonStyle(AnyPrimitiveButtonStyle(style)) + } + /// Sets the button style for the ``InfoButton`` type. public func infoButtonStyle(_ style: AnyPrimitiveButtonStyle) -> some View { environment(\.infoButtonStyle, style)