import ComposableArchitecture import DependenciesAdditions import EstimatedPressureDependency import FlaggedViews import SharedModels import Styleguide import SwiftUI import TCAExtras @Reducer public struct FlaggedMeasurementsList: Sendable { @Reducer(state: .equatable) public enum Destination { case estimationForm(EstimationForm) } @ObservableState @dynamicMemberLookup public struct State: Equatable { @Presents public var destination: Destination.State? @Shared var sharedSettings: SharedPressureEstimationSettings public init( destination: Destination.State? = nil, sharedSettings: Shared ) { self.destination = destination self._sharedSettings = sharedSettings } var ignoreIfZeroFields: [EquipmentMeasurement.FlaggedMeasurement.FieldKey] { guard let measurement = sharedSettings.equipmentMeasurement else { return [] } guard measurement.equipmentType == .airHandler else { return [] } return [.coilDrop, .filterDrop] } public subscript(dynamicMember keyPath: WritableKeyPath) -> T { get { sharedSettings[keyPath: keyPath] } set { sharedSettings[keyPath: keyPath] = newValue } } } public enum Action: ViewAction, ReceiveAction { case destination(PresentationAction) case receive(TaskResult) case view(View) @CasePathable public enum ReceiveAction: Sendable { case existingFlaggedMeasurement(EquipmentMeasurement.FlaggedMeasurement) case estimatedFlaggedMeasurement(name: String, measurement: EquipmentMeasurement.FlaggedMeasurement) } @CasePathable public enum View { case addButtonTapped case destination(DestinationAction) case editButtonTapped(id: SharedPressureEstimationSettings.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 { ReceiveReducer { state, action in switch action { case let .existingFlaggedMeasurement(measurement): state.sharedSettings.flaggedEquipmentMeasurement = measurement return .none case let .estimatedFlaggedMeasurement(name: name, measurement: measurement): state.flaggedEstimations.append( .init( id: uuid(), name: name, flaggedMeasurement: measurement ) ) return .none } } .onFailure(.log(logger: logger)) Reduce { 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 { 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? = 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 public init(store: StoreOf) { 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.flaggedEstimations) { 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 budgets = BudgetedPercentEnvelope(equipmentType: .airHandler, fanType: .constantSpeed) private let flaggedMeasurements = IdentifiedArrayOf( uniqueElements: [ .init( id: UUID(0), name: "Existing", flaggedMeasurement: .init( budgets: budgets, measurement: .mock(type: .airHandler), ratedPressures: .init(), tons: .default ) ), ] ) #Preview { NavigationStack { FlaggedMeasurementListView( store: Store( initialState: FlaggedMeasurementsList.State( sharedSettings: Shared( SharedPressureEstimationSettings( budgets: budgets, equipmentMeasurement: .mock(type: .airHandler), flaggedEquipmentMeasurement: nil, flaggedEstimations: flaggedMeasurements ) ) ) ) { FlaggedMeasurementsList() } ) } } #endif