Files
swift-estimated-pressures-core/Sources/PressureEstimationsFeature/FlaggedMeasurementsList.swift

349 lines
10 KiB
Swift

import ComposableArchitecture
import DependenciesAdditions
import EstimatedPressureDependency
import FlaggedViews
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: SharedPressureEstimationSettings
public var estimatedMeasurements: IdentifiedArrayOf<FlaggedMeasurementContainer>
init(
destination: Destination.State? = nil,
sharedSettings: Shared<SharedPressureEstimationSettings>,
estimatedMeasurements: IdentifiedArrayOf<FlaggedMeasurementContainer> = []
) {
self.destination = destination
self._sharedSettings = sharedSettings
self.estimatedMeasurements = estimatedMeasurements
}
var ignoreIfZeroFields: [EquipmentMeasurement.FlaggedMeasurement.FieldKey] {
guard let measurement = sharedSettings.equipmentMeasurement else { return [] }
guard measurement.equipmentType == .airHandler else { return [] }
return [.coilDrop, .filterDrop]
}
#warning("Move to shared settings.")
public struct FlaggedMeasurementContainer: Equatable, Identifiable {
public let id: UUID
public var flaggedMeasurement: EquipmentMeasurement.FlaggedMeasurement
public var name: String
public init(
id: UUID,
name: String,
flaggedMeasurement: EquipmentMeasurement.FlaggedMeasurement
) {
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(EquipmentMeasurement.FlaggedMeasurement)
case estimatedFlaggedMeasurement(name: String, measurement: EquipmentMeasurement.FlaggedMeasurement)
}
@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.equipmentMetadata.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 else {
return .none
}
if state.sharedSettings.budgets == nil {
state.sharedSettings.budgets = .init(
equipmentType: state.sharedSettings.equipmentMeasurement!.equipmentType,
fanType: state.sharedSettings.equipmentMetadata.fanType
)
}
state.sharedSettings.flaggedEquipmentMeasurement = .init(
budgets: state.sharedSettings.budgets!,
measurement: equipmentMeasurement,
ratedPressures: state.sharedSettings.equipmentMetadata.ratedStaticPressures,
tons: state.sharedSettings.equipmentMetadata.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) { [sharedSettings = state.sharedSettings] in
var filterPressureDrop: Positive<Double>? = nil
if sharedSettings.flaggedEquipmentMeasurement?.filterPressureDrop.wrappedValue != 0 {
filterPressureDrop = form.filterPressureDrop != nil
? Positive(wrappedValue: form.filterPressureDrop!)
: nil
}
let measurement = try await estimatedPressuresClient.estimatedPressure(
equipmentMeasurement: equipmentMeasurement,
airflow: form.airflow,
filterPressureDrop: filterPressureDrop
)
let flaggedMeasurement = EquipmentMeasurement.FlaggedMeasurement(
budgets: budgets,
measurement: measurement,
ratedPressures: sharedSettings.ratedStaticPressures,
tons: form.coolingCapacity
)
return .estimatedFlaggedMeasurement(name: form.name, measurement: flaggedMeasurement)
}
}
}
@ViewAction(for: FlaggedMeasurementsList.self)
public struct FlaggedMeasurementListView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@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,
ignoreIfZero: store.ignoreIfZeroFields
)
} header: {
HStack {
Text("Existing Measurements")
}
}
}
ForEach(store.estimatedMeasurements) { measurement in
Section {
FlaggedEquipmentMeasurementView(
measurement.flaggedMeasurement,
ignoreIfZero: store.ignoreIfZeroFields
)
} 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) }
.flaggedMessageViewStyle(
.automatic(horizontalSizeClass: horizontalSizeClass)
)
.flaggedStatusLabelStyle(.textLabel)
.flaggedMessageLabelStyle(.font(.caption))
}
}
#if DEBUG
private let sharedPressureEstimationSettings = SharedPressureEstimationSettings(
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: sharedPressureEstimationSettings.budgets!,
measurement: sharedPressureEstimationSettings.equipmentMeasurement!,
ratedPressures: sharedPressureEstimationSettings.equipmentMetadata.ratedStaticPressures,
tons: sharedPressureEstimationSettings.equipmentMetadata.coolingCapacity
)
),
]
)
#Preview {
NavigationStack {
FlaggedMeasurementListView(
store: Store(
initialState: FlaggedMeasurementsList.State(
sharedSettings: Shared(sharedPressureEstimationSettings)
)
) {
FlaggedMeasurementsList()
}
)
}
}
#Preview("Landscape", traits: .landscapeLeft) {
NavigationStack {
FlaggedMeasurementListView(
store: Store(
initialState: FlaggedMeasurementsList.State(
sharedSettings: Shared(sharedPressureEstimationSettings)
)
) {
FlaggedMeasurementsList()
}
)
}
}
#endif