309 lines
9.4 KiB
Swift
309 lines
9.4 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|