import Elementary import ElementaryHTMX import Routes import Styleguide struct HeatingBalancePointForm: HTML, Sendable { let mode: HeatingBalancePoint.Mode let heatLossMode: HeatingBalancePoint.HeatLoss.Mode let response: HeatingBalancePoint.Response? init( mode: HeatingBalancePoint.Mode?, heatLossMode: HeatingBalancePoint.HeatLoss.Mode? = nil, response: HeatingBalancePoint.Response? = nil ) { self.mode = mode ?? .thermal self.heatLossMode = heatLossMode ?? .estimated self.response = response } var content: some HTML { div(.class("flex flex-wrap justify-between")) { FormHeader(label: "Balance Point - \(mode.label)", svg: .scale) Toggle( isOn: mode == .thermal, onLabel: HeatingBalancePoint.Mode.thermal.label, onAttributes: .hxDefaults(get: .heatingBalancePoint(.index(mode: .thermal, heatLossMode: heatLossMode))), offLabel: HeatingBalancePoint.Mode.economic.label, offAttributes: .hxDefaults(get: .heatingBalancePoint(.index(mode: .economic, heatLossMode: heatLossMode))) ) .attributes(.class("mb-6")) } form( .hx.post(route: .heatingBalancePoint(.index)), .hx.target("#result") ) { div(.class("space-y-6")) { switch mode { case .thermal: ThermalFields(heatLossMode: heatLossMode) case .economic: EconomicFields() } div { SubmitButton(label: "Calculate Balance Point") } } } div(.id("result")) { if let response { HeatingBalancePointResponse(response: response) } } } struct EconomicFields: HTML, Sendable { var content: some HTML { div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) { div { InputLabel(for: "fuelType") { "Fuel Type" } Select(for: HeatingBalancePoint.FuelType.self, id: "fuelType") { $0.label } } LabeledContent(label: "AFUE (%)") { Input(id: "fuelAFUE", placeholder: "AFUE") .attributes(.type(.number), .min("1"), .max("100"), .step("0.5"), .value("90"), .required) } LabeledContent(label: "Fuel Cost (gallon or therm)") { Input(id: "fuelCostPerUnit", placeholder: "Fuel cost per unit") .attributes(.type(.number), .min("0.1"), .step("0.01"), .value("1.33"), .required) } LabeledContent(label: "Electric Cost (kw/h)") { Input(id: "costPerKW", placeholder: "Electric cost per kw/h") .attributes(.type(.number), .min("0.01"), .step("0.01"), .value("0.13"), .required) } } } } struct ThermalFields: HTML, Sendable { let heatLossMode: HeatingBalancePoint.HeatLoss.Mode var content: some HTML { LabeledContent(label: "System Size (Tons)") { Input(id: "systemSize", placeholder: "System size") .attributes(.type(.number), .min("1"), .step("0.5"), .autofocus, .required) } div { div(.class("mb-4")) { h4(.class("text-lg font-bold")) { "Capacities" } p(.class("text-xs text-blue-500")) { "Entering known capacities gives better results, otherwise capacities will be estimated." } } div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) { LabeledContent(label: "Capacity @ 47° (BTU/h)") { Input(id: "capacityAt47", placeholder: "Capacity @ 47° (optional)") .attributes(.type(.number), .min("1"), .step("0.5")) } LabeledContent(label: "Capacity @ 17° (BTU/h)") { Input(id: "capacityAt17", placeholder: "Capacity @ 17° (optional)") .attributes(.type(.number), .min("1"), .step("0.5")) } } } HeatLossFields(mode: heatLossMode) } } struct HeatLossFields: HTML, Sendable { let mode: HeatingBalancePoint.HeatLoss.Mode var content: some HTML { div(.id("heatLossFields")) { div(.class("flex flex-wrap justify-between")) { h4(.class("text-lg font-bold")) { "Heat Loss - \(mode.label)" } Toggle( isOn: mode == .estimated, onLabel: HeatingBalancePoint.HeatLoss.Mode.estimated.label, onAttributes: [ .hx.get(route: .heatingBalancePoint(.heatLossFields(mode: .estimated))), .hx.target("#heatLossFields") ], offLabel: HeatingBalancePoint.HeatLoss.Mode.known.label, offAttributes: [ .hx.get(route: .heatingBalancePoint(.heatLossFields(mode: .known))), .hx.target("#heatLossFields") ] ) .attributes(.class("mb-6")) } switch mode { case .known: knownFields case .estimated: simplifiedFields } } } private var simplifiedFields: some HTML { div(.class("grid grid-cols-1 lg:grid-cols-3 gap-4")) { LabeledContent(label: "Building Size (ft²)") { Input(id: "simplifiedHeatLoss", placeholder: "Square feet") .attributes(.type(.number), .min("1"), .step("0.5"), .required) } div { InputLabel(for: "climateZone") { "Climate Zone" } Select(for: ClimateZone.self, id: "climateZone") { $0.rawValue } } LabeledContent(label: "Outdoor Design Temperature (°F)") { Input(id: "heatingDesignTemperature", placeholder: "Design temperature (optional)") .attributes(.type(.number), .step("0.5")) } } } private var knownFields: some HTML { div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) { LabeledContent(label: "Heat Loss (BTU/h)") { Input(id: "knownHeatLoss", placeholder: "Heat loss") .attributes(.type(.number), .min("1"), .step("0.5"), .required) } LabeledContent(label: "Outdoor Design Temperature (°F)") { Input(id: "heatingDesignTemperature", placeholder: "Design temperature") .attributes(.type(.number), .step("0.5"), .required) } } } } } struct HeatingBalancePointResponse: HTML, Sendable { let response: HeatingBalancePoint.Response var content: some HTML { ResultContainer(reset: .heatingBalancePoint(.index(mode: response.mode, heatLossMode: nil))) { div( .class(""" w-full rounded-xl shadow-xl bg-blue-100 text-blue-600 border border-blue-600 py-4 """) ) { div(.class("flex")) { SVG(.scale, color: "text-blue-600") .attributes(.class("px-4")) p(.class("font-medium")) { "\(response.mode.label) Balance Point" } } switch response { case let .economic(result): economicResult(result) case let .thermal(result): thermalResult(result) } } WarningBox(warnings: response.warnings) } } func economicResult(_ result: HeatingBalancePoint.Response.Economic) -> some HTML { div { VerticalGroup( label: "Balance Point", value: "\(double: result.balancePointTemperature, fractionDigits: 1)", valueLabel: "°F" ) .attributes(.class("mb-8")) div(.class("grid grid-cols-2 space-y-6")) { div { VerticalGroup( label: "Electric Cost", value: "$\(double: result.electricCostPerMMBTU, fractionDigits: 2)", valueLabel: "/ MMBTU" ) } div { VerticalGroup( label: "Fuel Cost", value: "$\(double: result.fuelCostPerMMBTU, fractionDigits: 2)", valueLabel: "/ MMBTU" ) } } div { VerticalGroup( label: "COP at Balance Point", value: "\(double: result.copAtBalancePoint, fractionDigits: 2)" ) } } } func thermalResult(_ result: HeatingBalancePoint.Response.Thermal) -> some HTML { div { VerticalGroup( label: "Balance Point", value: "\(double: result.balancePointTemperature, fractionDigits: 1)", valueLabel: "°F" ) .attributes(.class("mb-8")) div(.class("grid grid-cols-2 space-y-6")) { div { VerticalGroup( label: "Heat Loss - \(result.heatLossMode.label)", value: "\(double: result.heatLoss, fractionDigits: 0)", valueLabel: "BTU/h" ) } div { VerticalGroup( label: "Heating Design Temperature", value: "\(double: result.heatingDesignTemperature, fractionDigits: 0)", valueLabel: "°F" ) } div { VerticalGroup( label: "Capacity @ 47°", value: "\(double: result.capacityAt47, fractionDigits: 0)", valueLabel: "BTU/h" ) } div { VerticalGroup( label: "Capacity @ 17°", value: "\(double: result.capacityAt17, fractionDigits: 0)", valueLabel: "BTU/h" ) } } } } } private extension HeatingBalancePoint.Mode { var label: String { rawValue.capitalized } } private extension HeatingBalancePoint.HeatLoss.Mode { var label: String { rawValue.capitalized } } private extension HeatingBalancePoint.Response { var mode: HeatingBalancePoint.Mode { switch self { case .economic: return .economic case .thermal: return .thermal } } var warnings: [String] { switch self { case .economic: return [] case let .thermal(result): return result.warnings } } }