feat: Begins flagged measurement list view.

This commit is contained in:
2024-06-06 22:23:45 -04:00
parent d253a470ca
commit 425b1d0c8f
12 changed files with 622 additions and 34 deletions

View File

@@ -80,66 +80,66 @@ extension EstimatedPressureDependency {
}
public func estimatedPressure(
for equipmentMeasurement: EquipmentMeasurement,
at upgradedAirflow: Double
equipmentMeasurement: EquipmentMeasurement,
airflow updatedAirflow: Double
) async throws -> EquipmentMeasurement {
switch equipmentMeasurement {
case let .airHandler(airHandler):
guard let airflow = airHandler.airflow else {
guard let existingAirflow = airHandler.airflow else {
throw InvalidAirflow()
}
return try await .airHandler(
.init(
airflow: upgradedAirflow,
airflow: updatedAirflow,
returnPlenumPressure: self.estimatedPressure(
existingPressure: airHandler.$returnPlenumPressure,
existingAirflow: airflow,
targetAirflow: upgradedAirflow
existingAirflow: existingAirflow,
targetAirflow: updatedAirflow
),
postFilterPressure: self.estimatedPressure(
existingPressure: airHandler.$postFilterPressure,
existingAirflow: airflow,
targetAirflow: upgradedAirflow
existingAirflow: existingAirflow,
targetAirflow: updatedAirflow
),
postCoilPressure: self.estimatedPressure(
existingPressure: airHandler.$postCoilPressure,
existingAirflow: airflow,
targetAirflow: upgradedAirflow
existingAirflow: existingAirflow,
targetAirflow: updatedAirflow
),
supplyPlenumPressure: self.estimatedPressure(
existingPressure: airHandler.$supplyPlenumPressure,
existingAirflow: airflow,
targetAirflow: upgradedAirflow
existingAirflow: existingAirflow,
targetAirflow: updatedAirflow
)
)
)
case let .furnaceAndCoil(furnaceAndCoil):
guard let airflow = furnaceAndCoil.airflow else {
guard let existingAirflow = furnaceAndCoil.airflow else {
throw InvalidAirflow()
}
return try await .furnaceAndCoil(
.init(
airflow: upgradedAirflow,
airflow: updatedAirflow,
returnPlenumPressure: self.estimatedPressure(
existingPressure: furnaceAndCoil.$returnPlenumPressure,
existingAirflow: airflow,
targetAirflow: upgradedAirflow
existingAirflow: existingAirflow,
targetAirflow: updatedAirflow
),
postFilterPressure: self.estimatedPressure(
existingPressure: furnaceAndCoil.$postFilterPressure,
existingAirflow: airflow,
targetAirflow: upgradedAirflow
existingAirflow: existingAirflow,
targetAirflow: updatedAirflow
),
preCoilPressure: self.estimatedPressure(
existingPressure: furnaceAndCoil.$preCoilPressure,
existingAirflow: airflow,
targetAirflow: upgradedAirflow
existingAirflow: existingAirflow,
targetAirflow: updatedAirflow
),
supplyPlenumPressure: self.estimatedPressure(
existingPressure: furnaceAndCoil.$supplyPlenumPressure,
existingAirflow: airflow,
targetAirflow: upgradedAirflow
existingAirflow: existingAirflow,
targetAirflow: updatedAirflow
)
)
)
@@ -148,13 +148,13 @@ extension EstimatedPressureDependency {
}
public func estimatedPressure(
for equipmentMeasurement: EquipmentMeasurement,
at upgradedAirflow: Double,
with filterPressureDrop: Positive<Double>
equipmentMeasurement: EquipmentMeasurement,
airflow updatedAirflow: Double,
filterPressureDrop: Positive<Double>
) async throws -> EquipmentMeasurement {
let estimate = try await estimatedPressure(
for: equipmentMeasurement,
at: upgradedAirflow
equipmentMeasurement: equipmentMeasurement,
airflow: updatedAirflow
)
switch estimate {
@@ -167,8 +167,24 @@ extension EstimatedPressureDependency {
furnaceAndCoil.postFilterPressure = furnaceAndCoil.returnPlenumPressure + filterPressureDrop.positiveValue
return .furnaceAndCoil(furnaceAndCoil)
}
}
public func estimatedPressure(
equipmentMeasurement: EquipmentMeasurement,
airflow updatedAirflow: Double,
filterPressureDrop: Positive<Double>?
) async throws -> EquipmentMeasurement {
guard let filterPressureDrop else {
return try await estimatedPressure(
equipmentMeasurement: equipmentMeasurement,
airflow: updatedAirflow
)
}
return try await estimatedPressure(
equipmentMeasurement: equipmentMeasurement,
airflow: updatedAirflow,
filterPressureDrop: filterPressureDrop
)
}
}

View File

@@ -3,6 +3,7 @@ import SharedModels
import Styleguide
import SwiftUI
@Reducer
public struct EquipmentMeasurementForm {

View File

@@ -3,6 +3,7 @@ import SharedModels
import Styleguide
import SwiftUI
@Reducer
public struct EquipmentSettingsForm {

View File

@@ -0,0 +1,126 @@
import ComposableArchitecture
import SharedModels
import Styleguide
import SwiftUI
@Reducer
public struct EstimationForm {
public init() { }
@ObservableState
public struct State: Equatable {
public var cfmPerTon: Int
public var coolingCapacity: CoolingCapacity
public var filterPressureDrop: Double?
public var name: String
public init(
cfmPerTon: Int = 350,
coolingCapacity: CoolingCapacity = .default,
filterPressureDrop: Double? = nil,
name: String = ""
) {
self.cfmPerTon = cfmPerTon
self.filterPressureDrop = filterPressureDrop
self.coolingCapacity = coolingCapacity
self.name = name
}
public var airflow: Double {
Double(cfmPerTon) * coolingCapacity.rawValue
}
public var isValid: Bool { !name.isEmpty }
}
public enum Action: BindableAction {
case binding(BindingAction<State>)
}
public var body: some Reducer<State, Action> {
BindingReducer()
Reduce<State, Action> { state, action in
switch action {
case .binding:
return .none
}
}
}
}
public struct EstimationFormView: View {
@Bindable public var store: StoreOf<EstimationForm>
public var body: some View {
Form {
Section("Estimation Name") {
HStack {
TextLabel("Name")
.padding(.trailing, 40)
TextField(
"Name",
text: $store.name,
prompt: Text("Required")
)
}
}
Section("Airflow") {
Grid(alignment: .leading, horizontalSpacing: 40) {
GridRow {
HStack {
TextLabel("Capacity")
Spacer()
CoolingCapacityPicker(
selection: $store.coolingCapacity
)
}
.gridCellColumns(2)
}
GridRow {
HStack {
TextLabel("CFM / Ton")
Spacer()
TextField(
"CFM / Ton",
value: $store.cfmPerTon,
format: .number,
prompt: Text("CFM")
)
.frame(width: 100)
.numberPad()
}
.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()
}
}
}
.labelsHidden()
.textLabelStyle(.boldSecondary)
.textFieldStyle(.roundedBorder)
}
}
#Preview {
EstimationFormView(
store: Store(initialState: EstimationForm.State()) {
EstimationForm()
}
)
}

View File

@@ -0,0 +1,307 @@
import ComposableArchitecture
import DependenciesAdditions
import EstimatedPressureDependency
import SharedModels
import Styleguide
import SwiftUI
import TCAExtras
@Reducer
public struct FlaggedMeasurementsList {
@Reducer(state: .equatable)
public enum Destination {
case estimationForm(EstimationForm)
}
@ObservableState
public struct State: Equatable {
@Presents public var destination: Destination.State?
@Shared var sharedSettings: SharedSettings
public var estimatedMeasurements: IdentifiedArrayOf<FlaggedMeasurementContainer>
init(
destination: Destination.State? = nil,
sharedSettings: Shared<SharedSettings>,
estimatedMeasurements: IdentifiedArrayOf<FlaggedMeasurementContainer> = []
) {
self.destination = destination
self._sharedSettings = sharedSettings
self.estimatedMeasurements = estimatedMeasurements
}
public struct FlaggedMeasurementContainer: Equatable, Identifiable {
public let id: UUID
public var flaggedMeasurement: FlaggedEquipmentMeasurement
public var name: String
public init(
id: UUID,
name: String,
flaggedMeasurement: FlaggedEquipmentMeasurement
) {
self.id = id
self.name = name
self.flaggedMeasurement = flaggedMeasurement
}
}
}
public enum Action: ViewAction, ReceiveAction {
case destination(PresentationAction<Destination.Action>)
case receive(TaskResult<ReceiveAction>)
case view(View)
@CasePathable
public enum ReceiveAction {
case existingFlaggedMeasurement(FlaggedEquipmentMeasurement)
case estimatedFlaggedMeasurement(name: String, measurement: FlaggedEquipmentMeasurement)
}
@CasePathable
public enum View {
case addButtonTapped
case destination(DestinationAction)
case editButtonTapped(id: State.FlaggedMeasurementContainer.ID)
case onAppear
@CasePathable
public enum DestinationAction {
case cancelButtonTapped
case doneButtonTapped
}
}
}
@Dependency(\.estimatedPressuresClient) var estimatedPressuresClient
@Dependency(\.logger["\(Self.self)"]) var logger
@Dependency(\.uuid) var uuid
public var body: some Reducer<State, Action> {
ReceiveReducer { state, action in
switch action {
case let .existingFlaggedMeasurement(measurement):
state.sharedSettings.flaggedEquipmentMeasurement = measurement
return .none
case let .estimatedFlaggedMeasurement(name: name, measurement: measurement):
state.estimatedMeasurements.append(
.init(
id: uuid(),
name: name,
flaggedMeasurement: measurement
)
)
return .none
}
}
.onFailure(.log(logger: logger))
Reduce<State, Action> { state, action in
switch action {
case .destination:
return .none
case .receive:
return .none
case let .view(action):
switch action {
case .addButtonTapped:
state.destination = .estimationForm(.init(
coolingCapacity: state.sharedSettings.coolingCapacity
))
return .none
case let .destination(action):
switch action {
case .cancelButtonTapped:
state.destination = nil
return .none
case .doneButtonTapped:
guard case let .estimationForm(form) = state.destination else {
return .fail(
"""
Received estimation form done button tapped action, but form
was not presented.
This is considered an application logic error.
""",
logger: logger
)
}
state.destination = nil
return handleEstimationForm(form: form, state: state)
}
case .editButtonTapped(id: _):
return .none
case .onAppear:
guard let equipmentMeasurement = state.sharedSettings.equipmentMeasurement,
let budgets = state.sharedSettings.budgets
else {
return .none
}
state.sharedSettings.flaggedEquipmentMeasurement = .init(
budgets: budgets,
measurement: equipmentMeasurement,
ratedPressures: state.sharedSettings.ratedStaticPressures,
tons: state.sharedSettings.coolingCapacity
)
return .none
}
}
}
.ifLet(\.$destination, action: \.destination)
}
private func handleEstimationForm(form: EstimationForm.State, state: State) -> Effect<Action> {
guard let equipmentMeasurement = state.sharedSettings.equipmentMeasurement,
let budgets = state.sharedSettings.budgets
else {
return .fail(
"""
Received estimation form done button tapped action, original
equipment measurement or budgets are not set on the shared state.
This is considered an application logic error.
""",
logger: logger
)
}
return .receive(action: \.receive) { [ratedStaticPressures = state.sharedSettings.ratedStaticPressures] in
let filterPressureDrop = form.filterPressureDrop != nil
? Positive(wrappedValue: form.filterPressureDrop!)
: nil
let measurement = try await estimatedPressuresClient.estimatedPressure(
equipmentMeasurement: equipmentMeasurement,
airflow: form.airflow,
filterPressureDrop: filterPressureDrop
)
let flaggedMeasurement = FlaggedEquipmentMeasurement(
budgets: budgets,
measurement: measurement,
ratedPressures: ratedStaticPressures,
tons: form.coolingCapacity
)
return .estimatedFlaggedMeasurement(name: form.name, measurement: flaggedMeasurement)
}
}
}
@ViewAction(for: FlaggedMeasurementsList.self)
public struct FlaggedMeasurementListView: View {
@Bindable public var store: StoreOf<FlaggedMeasurementsList>
public init(store: StoreOf<FlaggedMeasurementsList>) {
self.store = store
}
public var body: some View {
List {
if let existingMeasurement = store.sharedSettings.flaggedEquipmentMeasurement {
Section {
FlaggedEquipmentMeasurementView(existingMeasurement)
} header: {
HStack {
Text("Existing Measurements")
Spacer()
// Button("Edit") { }
}
}
}
ForEach(store.estimatedMeasurements) { measurement in
Section {
FlaggedEquipmentMeasurementView(measurement.flaggedMeasurement)
} header: {
HStack {
Text(measurement.name)
Spacer()
Button("Edit") { send(.editButtonTapped(id: measurement.id)) }
}
}
}
}
.toolbar {
Button { send(.addButtonTapped) } label: {
Label("Add", systemImage: "plus")
}
.sheet(
item: $store.scope(
state: \.destination?.estimationForm,
action: \.destination.estimationForm
)
) { store in
NavigationStack {
EstimationFormView(store: store)
.navigationTitle("Estimation")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button{ send(.destination(.cancelButtonTapped)) } label: {
Text("Cancel")
.foregroundStyle(Color.red)
}
}
ToolbarItem(placement: .confirmationAction) {
DoneButton { send(.destination(.doneButtonTapped)) }
.disabled(!store.isValid)
}
}
}
}
}
.onAppear { send(.onAppear) }
}
}
#if DEBUG
private let sharedSettings = SharedSettings(
budgets: .init(equipmentType: .airHandler, fanType: .constantSpeed),
equipmentMeasurement: .mock(type: .airHandler),
flaggedEquipmentMeasurement: nil
)
private let flaggedMeasurements = IdentifiedArrayOf<FlaggedMeasurementsList.State.FlaggedMeasurementContainer>(
uniqueElements: [
.init(
id: UUID(0),
name: "Existing",
flaggedMeasurement: .init(
budgets: sharedSettings.budgets!,
measurement: sharedSettings.equipmentMeasurement!,
ratedPressures: sharedSettings.ratedStaticPressures,
tons: sharedSettings.coolingCapacity
)
),
]
)
#Preview {
NavigationStack {
FlaggedMeasurementListView(
store: Store(
initialState: FlaggedMeasurementsList.State(
sharedSettings: Shared(sharedSettings)
)
) {
FlaggedMeasurementsList()
}
)
}
}
#endif

View File

@@ -0,0 +1,83 @@
import ComposableArchitecture
import SharedModels
import Styleguide
import SwiftUI
import TCAExtras
@Reducer
public struct PressureEstimationsFeature {
public init() { }
@Reducer(state: .equatable)
public enum Destination {
case equipmentMeasurements(EquipmentMeasurementForm)
}
@ObservableState
public struct State: Equatable {
@Presents public var destination: Destination.State?
public var equipmentSettings: EquipmentSettingsForm.State
public var equipmentMeasurements: EquipmentMeasurementForm.State?
public init(
destination: Destination.State? = nil,
equipmentSettings: EquipmentSettingsForm.State = .init(),
equipmentMeasurements: EquipmentMeasurementForm.State? = nil
) {
self.destination = destination
self.equipmentSettings = equipmentSettings
self.equipmentMeasurements = equipmentMeasurements
}
}
public enum Action: ViewAction {
case destination(PresentationAction<Destination.Action>)
case equipmentSettings(EquipmentSettingsForm.Action)
case view(View)
@CasePathable
public enum View {
case nextButtonTapped
}
}
public var body: some Reducer<State, Action> {
Scope(state: \.equipmentSettings, action: \.equipmentSettings) {
EquipmentSettingsForm()
}
Reduce<State, Action> { state, action in
switch action {
case .destination:
return .none
case .equipmentSettings:
return .none
case let .view(action):
switch action {
case .nextButtonTapped:
return .none
}
}
}
.ifLet(\.$destination, action: \.destination)
}
}
@ViewAction(for: PressureEstimationsFeature.self)
public struct PressureEstimationsView: View {
@Bindable public var store: StoreOf<PressureEstimationsFeature>
public init(store: StoreOf<PressureEstimationsFeature>) {
self.store = store
}
public var body: some View {
EquipmentSettingsFormView(
store: store.scope(state: \.equipmentSettings, action: \.equipmentSettings)
)
}
}

View File

@@ -0,0 +1,37 @@
import ComposableArchitecture
import SharedModels
struct SharedSettings: Equatable {
var budgets: BudgetedPercentEnvelope?
var coolingCapacity: CoolingCapacity
var equipmentMeasurement: EquipmentMeasurement?
var fanType: FanType
var flaggedEquipmentMeasurement: FlaggedEquipmentMeasurement?
var heatingCapacity: Double?
var ratedStaticPressures: RatedStaticPressures
init(
budgets: BudgetedPercentEnvelope? = nil,
coolingCapacity: CoolingCapacity = .default,
equipmentMeasurement: EquipmentMeasurement? = nil,
fanType: FanType = .constantSpeed,
flaggedEquipmentMeasurement: FlaggedEquipmentMeasurement? = nil,
heatingCapacity: Double? = nil,
ratedStaticPressures: RatedStaticPressures = .init()
) {
self.budgets = budgets
self.coolingCapacity = coolingCapacity
self.equipmentMeasurement = equipmentMeasurement
self.fanType = fanType
self.flaggedEquipmentMeasurement = flaggedEquipmentMeasurement
self.heatingCapacity = heatingCapacity
self.ratedStaticPressures = ratedStaticPressures
}
}
extension PersistenceReaderKey where Self == InMemoryKey<SharedSettings> {
static var sharedSettings: Self {
.inMemory("sharedSettings")
}
}

View File

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

View File

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

View File

@@ -172,7 +172,9 @@ extension Flagged {
.foregroundStyle(flagColor)
}
public var messageView: some View { flaggedMessageView(flagged: self) }
public var messageView: some View {
flaggedMessageView(flagged: self)
}
}
@ViewBuilder