4 Commits

23 changed files with 436 additions and 169 deletions

1
.gitignore vendored
View File

@@ -14,3 +14,4 @@ tailwindcss
.env .env
.env* .env*
default.profraw default.profraw
rooms.csv

View File

@@ -7,6 +7,7 @@ let package = Package(
products: [ products: [
.executable(name: "App", targets: ["App"]), .executable(name: "App", targets: ["App"]),
.library(name: "AuthClient", targets: ["AuthClient"]), .library(name: "AuthClient", targets: ["AuthClient"]),
.library(name: "CSVParser", targets: ["CSVParser"]),
.library(name: "DatabaseClient", targets: ["DatabaseClient"]), .library(name: "DatabaseClient", targets: ["DatabaseClient"]),
.library(name: "EnvVars", targets: ["EnvVars"]), .library(name: "EnvVars", targets: ["EnvVars"]),
.library(name: "FileClient", targets: ["FileClient"]), .library(name: "FileClient", targets: ["FileClient"]),
@@ -62,6 +63,20 @@ let package = Package(
.product(name: "DependenciesMacros", package: "swift-dependencies"), .product(name: "DependenciesMacros", package: "swift-dependencies"),
] ]
), ),
.target(
name: "CSVParser",
dependencies: [
.target(name: "ManualDCore"),
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"),
]
),
.testTarget(
name: "CSVParsingTests",
dependencies: [
.target(name: "CSVParser")
]
),
.target( .target(
name: "DatabaseClient", name: "DatabaseClient",
dependencies: [ dependencies: [
@@ -172,6 +187,7 @@ let package = Package(
name: "ViewController", name: "ViewController",
dependencies: [ dependencies: [
.target(name: "AuthClient"), .target(name: "AuthClient"),
.target(name: "CSVParser"),
.target(name: "DatabaseClient"), .target(name: "DatabaseClient"),
.target(name: "PdfClient"), .target(name: "PdfClient"),
.target(name: "ProjectClient"), .target(name: "ProjectClient"),

View File

@@ -0,0 +1,43 @@
import Dependencies
import DependenciesMacros
import ManualDCore
import Parsing
extension DependencyValues {
public var csvParser: CSVParser {
get { self[CSVParser.self] }
set { self[CSVParser.self] = newValue }
}
}
@DependencyClient
public struct CSVParser: Sendable {
public var parseRooms: @Sendable (Room.CSV) async throws -> [Room.Create]
}
extension CSVParser: DependencyKey {
public static let testValue = Self()
public static let liveValue = Self(
parseRooms: { csv in
guard let string = String(data: csv.file, encoding: .utf8) else {
throw CSVParsingError("Unreadable file data")
}
let rows = try RoomCSVParser().parse(string[...].utf8)
let rooms = rows.reduce(into: [Room.Create]()) {
if case .room(let room) = $1 {
$0.append(room)
}
}
return rooms
}
)
}
public struct CSVParsingError: Error {
let reason: String
public init(_ reason: String) {
self.reason = reason
}
}

View File

@@ -0,0 +1,51 @@
import ManualDCore
import Parsing
struct RoomCSVParser: Parser {
var body: some Parser<Substring.UTF8View, [RoomRowType]> {
Many {
RoomRowParser()
} separator: {
"\n".utf8
}
}
}
struct RoomRowParser: Parser {
var body: some Parser<Substring.UTF8View, RoomRowType> {
OneOf {
RoomCreateParser().map { RoomRowType.room($0) }
Prefix { $0 != UInt8(ascii: "\n") }
.map(.string)
.map { RoomRowType.header($0) }
}
}
}
enum RoomRowType {
case header(String)
case room(Room.Create)
}
struct RoomCreateParser: ParserPrinter {
var body: some ParserPrinter<Substring.UTF8View, Room.Create> {
ParsePrint {
Prefix { $0 != UInt8(ascii: ",") }.map(.string)
",".utf8
Double.parser()
",".utf8
Optionally {
Double.parser()
}
",".utf8
Optionally {
Double.parser()
}
",".utf8
Int.parser()
}
.map(.memberwise(Room.Create.init))
}
}

View File

@@ -89,8 +89,8 @@ public struct DatabaseClient: Sendable {
@DependencyClient @DependencyClient
public struct Rooms: Sendable { public struct Rooms: Sendable {
public var create: @Sendable (Room.Create) async throws -> Room public var create: @Sendable (Project.ID, Room.Create) async throws -> Room
public var createMany: @Sendable ([Room.Create]) async throws -> [Room] public var createMany: @Sendable (Project.ID, [Room.Create]) async throws -> [Room]
public var delete: @Sendable (Room.ID) async throws -> Void public var delete: @Sendable (Room.ID) async throws -> Void
public var deleteRectangularSize: public var deleteRectangularSize:
@Sendable (Room.ID, Room.RectangularSize.ID) async throws -> Room @Sendable (Room.ID, Room.RectangularSize.ID) async throws -> Room

View File

@@ -10,14 +10,14 @@ extension DatabaseClient.Rooms: TestDependencyKey {
public static func live(database: any Database) -> Self { public static func live(database: any Database) -> Self {
.init( .init(
create: { request in create: { projectID, request in
let model = try request.toModel() let model = try request.toModel(projectID: projectID)
try await model.validateAndSave(on: database) try await model.validateAndSave(on: database)
return try model.toDTO() return try model.toDTO()
}, },
createMany: { rooms in createMany: { projectID, rooms in
try await rooms.asyncMap { request in try await rooms.asyncMap { request in
let model = try request.toModel() let model = try request.toModel(projectID: projectID)
try await model.validateAndSave(on: database) try await model.validateAndSave(on: database)
return try model.toDTO() return try model.toDTO()
} }
@@ -83,12 +83,11 @@ extension DatabaseClient.Rooms: TestDependencyKey {
extension Room.Create { extension Room.Create {
func toModel() throws -> RoomModel { func toModel(projectID: Project.ID) throws -> RoomModel {
return .init( return .init(
name: name, name: name,
heatingLoad: heatingLoad, heatingLoad: heatingLoad,
coolingTotal: coolingTotal, coolingLoad: coolingLoad,
coolingSensible: coolingSensible,
registerCount: registerCount, registerCount: registerCount,
projectID: projectID projectID: projectID
) )
@@ -104,8 +103,7 @@ extension Room {
.id() .id()
.field("name", .string, .required) .field("name", .string, .required)
.field("heatingLoad", .double, .required) .field("heatingLoad", .double, .required)
.field("coolingTotal", .double, .required) .field("coolingLoad", .dictionary, .required)
.field("coolingSensible", .double)
.field("registerCount", .int8, .required) .field("registerCount", .int8, .required)
.field("rectangularSizes", .array) .field("rectangularSizes", .array)
.field("createdAt", .datetime) .field("createdAt", .datetime)
@@ -136,11 +134,8 @@ final class RoomModel: Model, @unchecked Sendable, Validatable {
@Field(key: "heatingLoad") @Field(key: "heatingLoad")
var heatingLoad: Double var heatingLoad: Double
@Field(key: "coolingTotal") @Field(key: "coolingLoad")
var coolingTotal: Double var coolingLoad: Room.CoolingLoad
@Field(key: "coolingSensible")
var coolingSensible: Double?
@Field(key: "registerCount") @Field(key: "registerCount")
var registerCount: Int var registerCount: Int
@@ -163,8 +158,7 @@ final class RoomModel: Model, @unchecked Sendable, Validatable {
id: UUID? = nil, id: UUID? = nil,
name: String, name: String,
heatingLoad: Double, heatingLoad: Double,
coolingTotal: Double, coolingLoad: Room.CoolingLoad,
coolingSensible: Double? = nil,
registerCount: Int, registerCount: Int,
rectangularSizes: [Room.RectangularSize]? = nil, rectangularSizes: [Room.RectangularSize]? = nil,
createdAt: Date? = nil, createdAt: Date? = nil,
@@ -174,8 +168,7 @@ final class RoomModel: Model, @unchecked Sendable, Validatable {
self.id = id self.id = id
self.name = name self.name = name
self.heatingLoad = heatingLoad self.heatingLoad = heatingLoad
self.coolingTotal = coolingTotal self.coolingLoad = coolingLoad
self.coolingSensible = coolingSensible
self.registerCount = registerCount self.registerCount = registerCount
self.rectangularSizes = rectangularSizes self.rectangularSizes = rectangularSizes
self.createdAt = createdAt self.createdAt = createdAt
@@ -189,8 +182,7 @@ final class RoomModel: Model, @unchecked Sendable, Validatable {
projectID: $project.id, projectID: $project.id,
name: name, name: name,
heatingLoad: heatingLoad, heatingLoad: heatingLoad,
coolingTotal: coolingTotal, coolingLoad: coolingLoad,
coolingSensible: coolingSensible,
registerCount: registerCount, registerCount: registerCount,
rectangularSizes: rectangularSizes, rectangularSizes: rectangularSizes,
createdAt: createdAt!, createdAt: createdAt!,
@@ -206,11 +198,8 @@ final class RoomModel: Model, @unchecked Sendable, Validatable {
if let heatingLoad = updates.heatingLoad, heatingLoad != self.heatingLoad { if let heatingLoad = updates.heatingLoad, heatingLoad != self.heatingLoad {
self.heatingLoad = heatingLoad self.heatingLoad = heatingLoad
} }
if let coolingTotal = updates.coolingTotal, coolingTotal != self.coolingTotal { if let coolingLoad = updates.coolingLoad, coolingLoad != self.coolingLoad {
self.coolingTotal = coolingTotal self.coolingLoad = coolingLoad
}
if let coolingSensible = updates.coolingSensible, coolingSensible != self.coolingSensible {
self.coolingSensible = coolingSensible
} }
if let registerCount = updates.registerCount, registerCount != self.registerCount { if let registerCount = updates.registerCount, registerCount != self.registerCount {
self.registerCount = registerCount self.registerCount = registerCount
@@ -229,11 +218,8 @@ final class RoomModel: Model, @unchecked Sendable, Validatable {
Validator.validate(\.heatingLoad, with: .greaterThanOrEquals(0)) Validator.validate(\.heatingLoad, with: .greaterThanOrEquals(0))
.errorLabel("Heating Load", inline: true) .errorLabel("Heating Load", inline: true)
Validator.validate(\.coolingTotal, with: .greaterThanOrEquals(0)) Validator.validate(\.coolingLoad)
.errorLabel("Cooling Total", inline: true) .errorLabel("Cooling Load", inline: true)
Validator.validate(\.coolingSensible, with: Double.greaterThanOrEquals(0).optional())
.errorLabel("Cooling Sensible", inline: true)
Validator.validate(\.registerCount, with: .greaterThanOrEquals(1)) Validator.validate(\.registerCount, with: .greaterThanOrEquals(1))
.errorLabel("Register Count", inline: true) .errorLabel("Register Count", inline: true)
@@ -244,6 +230,25 @@ final class RoomModel: Model, @unchecked Sendable, Validatable {
} }
} }
extension Room.CoolingLoad: Validatable {
public var body: some Validation<Self> {
Validator.accumulating {
// Ensure that at least one of the values is not nil.
Validator.oneOf {
Validator.validate(\.total, with: .notNil())
.errorLabel("Total or Sensible", inline: true)
Validator.validate(\.sensible, with: .notNil())
.errorLabel("Total or Sensible", inline: true)
}
Validator.validate(\.total, with: Double.greaterThan(0).optional())
Validator.validate(\.sensible, with: Double.greaterThan(0).optional())
}
}
}
extension Room.RectangularSize: Validatable { extension Room.RectangularSize: Validatable {
public var body: some Validation<Self> { public var body: some Validation<Self> {

View File

@@ -3,13 +3,12 @@ import ManualDCore
extension Room { extension Room {
var heatingLoadPerRegister: Double { public var heatingLoadPerRegister: Double {
heatingLoad / Double(registerCount) heatingLoad / Double(registerCount)
} }
func coolingSensiblePerRegister(projectSHR: Double) -> Double { public func coolingSensiblePerRegister(projectSHR: Double) throws -> Double {
let sensible = coolingSensible ?? (coolingTotal * projectSHR) let sensible = try coolingLoad.ensured(shr: projectSHR).sensible
return sensible / Double(registerCount) return sensible / Double(registerCount)
} }
} }
@@ -30,8 +29,8 @@ extension TrunkSize.RoomProxy {
room.heatingLoadPerRegister * Double(actualRegisterCount) room.heatingLoadPerRegister * Double(actualRegisterCount)
} }
func totalCoolingSensible(projectSHR: Double) -> Double { func totalCoolingSensible(projectSHR: Double) throws -> Double {
room.coolingSensiblePerRegister(projectSHR: projectSHR) * Double(actualRegisterCount) try room.coolingSensiblePerRegister(projectSHR: projectSHR) * Double(actualRegisterCount)
} }
} }
@@ -41,8 +40,8 @@ extension TrunkSize {
rooms.reduce(into: 0) { $0 += $1.totalHeatingLoad } rooms.reduce(into: 0) { $0 += $1.totalHeatingLoad }
} }
func totalCoolingSensible(projectSHR: Double) -> Double { func totalCoolingSensible(projectSHR: Double) throws -> Double {
rooms.reduce(into: 0) { $0 += $1.totalCoolingSensible(projectSHR: projectSHR) } try rooms.reduce(into: 0) { $0 += try $1.totalCoolingSensible(projectSHR: projectSHR) }
} }
} }

View File

@@ -1,13 +1,14 @@
import Foundation // import Foundation
public struct CoolingLoad: Codable, Equatable, Sendable { //
public let total: Double // public struct CoolingLoad: Codable, Equatable, Sendable {
public let sensible: Double // public let total: Double
public var latent: Double { total - sensible } // public let sensible: Double
public var shr: Double { sensible / total } // public var latent: Double { total - sensible }
// public var shr: Double { sensible / total }
public init(total: Double, sensible: Double) { //
self.total = total // public init(total: Double, sensible: Double) {
self.sensible = sensible // self.total = total
} // self.sensible = sensible
} // }
// }

View File

@@ -152,11 +152,12 @@ extension DuctSizes {
public static func mock( public static func mock(
equipmentInfo: EquipmentInfo, equipmentInfo: EquipmentInfo,
rooms: [Room], rooms: [Room],
trunks: [TrunkSize] trunks: [TrunkSize],
shr: Double
) -> Self { ) -> Self {
let totalHeatingLoad = rooms.totalHeatingLoad let totalHeatingLoad = rooms.totalHeatingLoad
let totalCoolingLoad = rooms.totalCoolingLoad let totalCoolingLoad = try! rooms.totalCoolingLoad(shr: shr)
let roomContainers = rooms.reduce(into: [RoomContainer]()) { array, room in let roomContainers = rooms.reduce(into: [RoomContainer]()) { array, room in
array += RoomContainer.mock( array += RoomContainer.mock(
@@ -164,7 +165,8 @@ extension DuctSizes {
totalHeatingLoad: totalHeatingLoad, totalHeatingLoad: totalHeatingLoad,
totalCoolingLoad: totalCoolingLoad, totalCoolingLoad: totalCoolingLoad,
totalHeatingCFM: Double(equipmentInfo.heatingCFM), totalHeatingCFM: Double(equipmentInfo.heatingCFM),
totalCoolingCFM: Double(equipmentInfo.coolingCFM) totalCoolingCFM: Double(equipmentInfo.coolingCFM),
shr: shr
) )
} }
@@ -187,14 +189,15 @@ extension DuctSizes {
totalHeatingLoad: Double, totalHeatingLoad: Double,
totalCoolingLoad: Double, totalCoolingLoad: Double,
totalHeatingCFM: Double, totalHeatingCFM: Double,
totalCoolingCFM: Double totalCoolingCFM: Double,
shr: Double
) -> [Self] { ) -> [Self] {
var retval = [DuctSizes.RoomContainer]() var retval = [DuctSizes.RoomContainer]()
let heatingLoad = room.heatingLoad / Double(room.registerCount) let heatingLoad = room.heatingLoad / Double(room.registerCount)
let heatingFraction = heatingLoad / totalHeatingLoad let heatingFraction = heatingLoad / totalHeatingLoad
let heatingCFM = totalHeatingCFM * heatingFraction let heatingCFM = totalHeatingCFM * heatingFraction
// Not really accurate, but works for mocks. // Not really accurate, but works for mocks.
let coolingLoad = room.coolingTotal / Double(room.registerCount) let coolingLoad = (try! room.coolingLoad.ensured(shr: shr).total) / Double(room.registerCount)
let coolingFraction = coolingLoad / totalCoolingLoad let coolingFraction = coolingLoad / totalCoolingLoad
let coolingCFM = totalCoolingCFM * coolingFraction let coolingCFM = totalCoolingCFM * coolingFraction

View File

@@ -9,19 +9,19 @@ import Foundation
public struct Room: Codable, Equatable, Identifiable, Sendable { public struct Room: Codable, Equatable, Identifiable, Sendable {
/// The unique id of the room. /// The unique id of the room.
public let id: UUID public let id: UUID
/// The project this room is associated with. /// The project this room is associated with.
public let projectID: Project.ID public let projectID: Project.ID
/// A unique name for the room in the project. /// A unique name for the room in the project.
public let name: String public let name: String
/// The heating load required for the room (from Manual-J). /// The heating load required for the room (from Manual-J).
public let heatingLoad: Double public let heatingLoad: Double
/// The total cooling load required for the room (from Manual-J).
public let coolingTotal: Double /// The cooling load required for the room (from Manual-J).
/// An optional sensible cooling load for the room. public let coolingLoad: CoolingLoad
///
/// **NOTE:** This is generally not set, but calculated from the project wide
/// sensible heat ratio.
public let coolingSensible: Double?
/// The number of registers for the room. /// The number of registers for the room.
public let registerCount: Int public let registerCount: Int
/// The rectangular duct size calculations for a room. /// The rectangular duct size calculations for a room.
@@ -29,8 +29,10 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
/// **NOTE:** These are optionally set after the round sizes have been calculate /// **NOTE:** These are optionally set after the round sizes have been calculate
/// for a room. /// for a room.
public let rectangularSizes: [RectangularSize]? public let rectangularSizes: [RectangularSize]?
/// When the room was created in the database. /// When the room was created in the database.
public let createdAt: Date public let createdAt: Date
/// When the room was updated in the database. /// When the room was updated in the database.
public let updatedAt: Date public let updatedAt: Date
@@ -39,8 +41,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
projectID: Project.ID, projectID: Project.ID,
name: String, name: String,
heatingLoad: Double, heatingLoad: Double,
coolingTotal: Double, coolingLoad: CoolingLoad,
coolingSensible: Double? = nil,
registerCount: Int = 1, registerCount: Int = 1,
rectangularSizes: [RectangularSize]? = nil, rectangularSizes: [RectangularSize]? = nil,
createdAt: Date, createdAt: Date,
@@ -50,40 +51,73 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
self.projectID = projectID self.projectID = projectID
self.name = name self.name = name
self.heatingLoad = heatingLoad self.heatingLoad = heatingLoad
self.coolingTotal = coolingTotal self.coolingLoad = coolingLoad
self.coolingSensible = coolingSensible
self.registerCount = registerCount self.registerCount = registerCount
self.rectangularSizes = rectangularSizes self.rectangularSizes = rectangularSizes
self.createdAt = createdAt self.createdAt = createdAt
self.updatedAt = updatedAt self.updatedAt = updatedAt
} }
/// Represents the cooling load of a room.
///
/// Generally only one of the values is provided by a Manual-J room x room
/// calculation.
///
public struct CoolingLoad: Codable, Equatable, Sendable {
public let total: Double?
public let sensible: Double?
public init(total: Double? = nil, sensible: Double? = nil) {
self.total = total
self.sensible = sensible
}
/// Calculates the cooling load based on the shr.
///
/// Generally Manual-J room x room loads provide either the total load or the
/// sensible load, so this allows us to calculate whichever is not provided.
public func ensured(shr: Double) throws -> (total: Double, sensible: Double) {
switch (total, sensible) {
case (.none, .none):
throw CoolingLoadError("Both the total and sensible loads are nil.")
case (.some(let total), .some(let sensible)):
return (total, sensible)
case (.some(let total), .none):
return (total, total * shr)
case (.none, .some(let sensible)):
return (sensible / shr, sensible)
}
}
}
} }
extension Room { extension Room {
/// Represents the data required to create a new room for a project. /// Represents the data required to create a new room for a project.
public struct Create: Codable, Equatable, Sendable { public struct Create: Codable, Equatable, Sendable {
/// The project this room is associated with.
public let projectID: Project.ID
/// A unique name for the room in the project. /// A unique name for the room in the project.
public let name: String public let name: String
/// The heating load required for the room (from Manual-J). /// The heating load required for the room (from Manual-J).
public let heatingLoad: Double public let heatingLoad: Double
/// The total cooling load required for the room (from Manual-J). /// The total cooling load required for the room (from Manual-J).
public let coolingTotal: Double public let coolingTotal: Double?
/// An optional sensible cooling load for the room. /// An optional sensible cooling load for the room.
public let coolingSensible: Double? public let coolingSensible: Double?
/// The number of registers for the room. /// The number of registers for the room.
public let registerCount: Int public let registerCount: Int
public var coolingLoad: Room.CoolingLoad {
.init(total: coolingTotal, sensible: coolingSensible)
}
public init( public init(
projectID: Project.ID,
name: String, name: String,
heatingLoad: Double, heatingLoad: Double,
coolingTotal: Double, coolingTotal: Double? = nil,
coolingSensible: Double? = nil, coolingSensible: Double? = nil,
registerCount: Int = 1 registerCount: Int = 1
) { ) {
self.projectID = projectID
self.name = name self.name = name
self.heatingLoad = heatingLoad self.heatingLoad = heatingLoad
self.coolingTotal = coolingTotal self.coolingTotal = coolingTotal
@@ -92,6 +126,14 @@ extension Room {
} }
} }
public struct CSV: Equatable, Sendable {
public let file: Data
public init(file: Data) {
self.file = file
}
}
/// Represents a rectangular size calculation that is stored in the /// Represents a rectangular size calculation that is stored in the
/// database for a given room. /// database for a given room.
/// ///
@@ -118,7 +160,7 @@ extension Room {
/// Represents field that can be updated on a room after it's been created in the database. /// Represents field that can be updated on a room after it's been created in the database.
/// ///
/// Only fields that are supplied get updated. /// Onlly fields that are supplied get updated.
public struct Update: Codable, Equatable, Sendable { public struct Update: Codable, Equatable, Sendable {
/// A unique name for the room in the project. /// A unique name for the room in the project.
public let name: String? public let name: String?
@@ -133,6 +175,13 @@ extension Room {
/// The rectangular duct size calculations for a room. /// The rectangular duct size calculations for a room.
public let rectangularSizes: [RectangularSize]? public let rectangularSizes: [RectangularSize]?
public var coolingLoad: CoolingLoad? {
guard coolingTotal != nil || coolingSensible != nil else {
return nil
}
return .init(total: coolingTotal, sensible: coolingSensible)
}
public init( public init(
name: String? = nil, name: String? = nil,
heatingLoad: Double? = nil, heatingLoad: Double? = nil,
@@ -169,22 +218,30 @@ extension Array where Element == Room {
} }
/// The sum of total cooling loads for an array of rooms. /// The sum of total cooling loads for an array of rooms.
public var totalCoolingLoad: Double { public func totalCoolingLoad(shr: Double) throws -> Double {
reduce(into: 0) { $0 += $1.coolingTotal } try reduce(into: 0) { $0 += try $1.coolingLoad.ensured(shr: shr).total }
} }
/// The sum of sensible cooling loads for an array of rooms. /// The sum of sensible cooling loads for an array of rooms.
/// ///
/// - Parameters: /// - Parameters:
/// - shr: The project wide sensible heat ratio. /// - shr: The project wide sensible heat ratio.
public func totalCoolingSensible(shr: Double) -> Double { public func totalCoolingSensible(shr: Double) throws -> Double {
reduce(into: 0) { try reduce(into: 0) {
let sensible = $1.coolingSensible ?? ($1.coolingTotal * shr) // let sensible = $1.coolingSensible ?? ($1.coolingTotal * shr)
$0 += sensible $0 += try $1.coolingLoad.ensured(shr: shr).sensible
} }
} }
} }
public struct CoolingLoadError: Error, Equatable, Sendable {
public let reason: String
public init(_ reason: String) {
self.reason = reason
}
}
#if DEBUG #if DEBUG
extension Room { extension Room {
@@ -199,8 +256,8 @@ extension Array where Element == Room {
projectID: projectID, projectID: projectID,
name: "Bed-1", name: "Bed-1",
heatingLoad: 3913, heatingLoad: 3913,
coolingTotal: 2472, coolingLoad: .init(total: 2472),
coolingSensible: nil, // coolingSensible: nil,
registerCount: 1, registerCount: 1,
rectangularSizes: nil, rectangularSizes: nil,
createdAt: now, createdAt: now,
@@ -211,8 +268,8 @@ extension Array where Element == Room {
projectID: projectID, projectID: projectID,
name: "Entry", name: "Entry",
heatingLoad: 8284, heatingLoad: 8284,
coolingTotal: 2916, coolingLoad: .init(total: 2916),
coolingSensible: nil, // coolingSensible: nil,
registerCount: 2, registerCount: 2,
rectangularSizes: nil, rectangularSizes: nil,
createdAt: now, createdAt: now,
@@ -223,8 +280,8 @@ extension Array where Element == Room {
projectID: projectID, projectID: projectID,
name: "Family Room", name: "Family Room",
heatingLoad: 9785, heatingLoad: 9785,
coolingTotal: 7446, coolingLoad: .init(total: 7446),
coolingSensible: nil, // coolingSensible: nil,
registerCount: 3, registerCount: 3,
rectangularSizes: nil, rectangularSizes: nil,
createdAt: now, createdAt: now,
@@ -235,8 +292,8 @@ extension Array where Element == Room {
projectID: projectID, projectID: projectID,
name: "Kitchen", name: "Kitchen",
heatingLoad: 4518, heatingLoad: 4518,
coolingTotal: 5096, coolingLoad: .init(total: 5096),
coolingSensible: nil, // coolingSensible: nil,
registerCount: 2, registerCount: 2,
rectangularSizes: nil, rectangularSizes: nil,
createdAt: now, createdAt: now,
@@ -247,8 +304,8 @@ extension Array where Element == Room {
projectID: projectID, projectID: projectID,
name: "Living Room", name: "Living Room",
heatingLoad: 7553, heatingLoad: 7553,
coolingTotal: 6829, coolingLoad: .init(total: 6829),
coolingSensible: nil, // coolingSensible: nil,
registerCount: 2, registerCount: 2,
rectangularSizes: nil, rectangularSizes: nil,
createdAt: now, createdAt: now,
@@ -259,8 +316,8 @@ extension Array where Element == Room {
projectID: projectID, projectID: projectID,
name: "Master", name: "Master",
heatingLoad: 8202, heatingLoad: 8202,
coolingTotal: 2076, coolingLoad: .init(total: 2076),
coolingSensible: nil, // coolingSensible: nil,
registerCount: 2, registerCount: 2,
rectangularSizes: nil, rectangularSizes: nil,
createdAt: now, createdAt: now,

View File

@@ -188,6 +188,7 @@ extension SiteRoute.View.ProjectRoute {
} }
public enum RoomRoute: Equatable, Sendable { public enum RoomRoute: Equatable, Sendable {
case csv(Room.CSV)
case delete(id: Room.ID) case delete(id: Room.ID)
case index case index
case submit(Room.Create) case submit(Room.Create)
@@ -197,6 +198,23 @@ extension SiteRoute.View.ProjectRoute {
static let rootPath = "rooms" static let rootPath = "rooms"
public static let router = OneOf { public static let router = OneOf {
Route(.case(Self.csv)) {
Path {
rootPath
"csv"
}
Headers {
Field("Content-Type") { "multipart/form-data" }
}
Method.post
Body().map(.memberwise(Room.CSV.init))
// Body {
// FormData {
//
// }
// .map(.memberwise(Room.CSV.init))
// }
}
Route(.case(Self.delete)) { Route(.case(Self.delete)) {
Path { Path {
rootPath rootPath
@@ -215,12 +233,14 @@ extension SiteRoute.View.ProjectRoute {
Method.post Method.post
Body { Body {
FormData { FormData {
Field("projectID") { Project.ID.parser() } // Field("projectID") { Project.ID.parser() }
Field("name", .string) Field("name", .string)
Field("heatingLoad") { Double.parser() } Field("heatingLoad") { Double.parser() }
Field("coolingTotal") { Double.parser() }
Optionally { Optionally {
Field("coolingSensible", default: nil) { Double.parser() } Field("coolingTotal") { Double.parser() }
}
Optionally {
Field("coolingSensible") { Double.parser() }
} }
Field("registerCount") { Digits() } Field("registerCount") { Digits() }
} }

View File

@@ -150,7 +150,12 @@ extension PdfClient {
project: project, project: project,
rooms: rooms, rooms: rooms,
componentLosses: ComponentPressureLoss.mock(projectID: project.id), componentLosses: ComponentPressureLoss.mock(projectID: project.id),
ductSizes: .mock(equipmentInfo: equipmentInfo, rooms: rooms, trunks: trunks), ductSizes: .mock(
equipmentInfo: equipmentInfo,
rooms: rooms,
trunks: trunks,
shr: project.sensibleHeatRatio ?? 0.83
),
equipmentInfo: equipmentInfo, equipmentInfo: equipmentInfo,
maxSupplyTEL: equivalentLengths.first { $0.type == .supply }!, maxSupplyTEL: equivalentLengths.first { $0.type == .supply }!,
maxReturnTEL: equivalentLengths.first { $0.type == .return }!, maxReturnTEL: equivalentLengths.first { $0.type == .return }!,

View File

@@ -21,10 +21,9 @@ struct RoomsTable: HTML, Sendable {
tr { tr {
td { room.name } td { room.name }
td { room.heatingLoad.string(digits: 0) } td { room.heatingLoad.string(digits: 0) }
td { room.coolingTotal.string(digits: 0) } td { try! room.coolingLoad.ensured(shr: projectSHR).total.string(digits: 0) }
td { td {
(room.coolingSensible try! room.coolingLoad.ensured(shr: projectSHR).sensible.string(digits: 0)
?? (room.coolingTotal * projectSHR)).string(digits: 0)
} }
td { room.registerCount.string() } td { room.registerCount.string() }
} }
@@ -37,10 +36,10 @@ struct RoomsTable: HTML, Sendable {
rooms.totalHeatingLoad.string(digits: 0) rooms.totalHeatingLoad.string(digits: 0)
} }
td(.class("coolingTotal label")) { td(.class("coolingTotal label")) {
rooms.totalCoolingLoad.string(digits: 0) try! rooms.totalCoolingLoad(shr: projectSHR).string(digits: 0)
} }
td(.class("coolingSensible label")) { td(.class("coolingSensible label")) {
rooms.totalCoolingSensible(shr: projectSHR).string(digits: 0) try! rooms.totalCoolingSensible(shr: projectSHR).string(digits: 0)
} }
td {} td {}
} }

View File

@@ -41,11 +41,11 @@ extension ManualDClient {
var retval: [DuctSizes.RoomContainer] = [] var retval: [DuctSizes.RoomContainer] = []
let totalHeatingLoad = rooms.totalHeatingLoad let totalHeatingLoad = rooms.totalHeatingLoad
let totalCoolingSensible = rooms.totalCoolingSensible(shr: sharedRequest.projectSHR) let totalCoolingSensible = try rooms.totalCoolingSensible(shr: sharedRequest.projectSHR)
for room in rooms { for room in rooms {
let heatingLoad = room.heatingLoadPerRegister let heatingLoad = room.heatingLoadPerRegister
let coolingLoad = room.coolingSensiblePerRegister(projectSHR: sharedRequest.projectSHR) let coolingLoad = try room.coolingSensiblePerRegister(projectSHR: sharedRequest.projectSHR)
let heatingPercent = heatingLoad / totalHeatingLoad let heatingPercent = heatingLoad / totalHeatingLoad
let coolingPercent = coolingLoad / totalCoolingSensible let coolingPercent = coolingLoad / totalCoolingSensible
let heatingCFM = heatingPercent * Double(sharedRequest.equipmentInfo.heatingCFM) let heatingCFM = heatingPercent * Double(sharedRequest.equipmentInfo.heatingCFM)
@@ -102,11 +102,11 @@ extension ManualDClient {
var retval = [DuctSizes.TrunkContainer]() var retval = [DuctSizes.TrunkContainer]()
let totalHeatingLoad = rooms.totalHeatingLoad let totalHeatingLoad = rooms.totalHeatingLoad
let totalCoolingSensible = rooms.totalCoolingSensible(shr: sharedRequest.projectSHR) let totalCoolingSensible = try rooms.totalCoolingSensible(shr: sharedRequest.projectSHR)
for trunk in trunks { for trunk in trunks {
let heatingLoad = trunk.totalHeatingLoad let heatingLoad = trunk.totalHeatingLoad
let coolingLoad = trunk.totalCoolingSensible(projectSHR: sharedRequest.projectSHR) let coolingLoad = try trunk.totalCoolingSensible(projectSHR: sharedRequest.projectSHR)
let heatingPercent = heatingLoad / totalHeatingLoad let heatingPercent = heatingLoad / totalHeatingLoad
let coolingPercent = coolingLoad / totalCoolingSensible let coolingPercent = coolingLoad / totalCoolingSensible
let heatingCFM = heatingPercent * Double(sharedRequest.equipmentInfo.heatingCFM) let heatingCFM = heatingPercent * Double(sharedRequest.equipmentInfo.heatingCFM)
@@ -181,18 +181,18 @@ extension DuctSizes.SizeContainer {
} }
} }
extension Room { // extension Room {
//
var heatingLoadPerRegister: Double { // var heatingLoadPerRegister: Double {
//
heatingLoad / Double(registerCount) // heatingLoad / Double(registerCount)
} // }
//
func coolingSensiblePerRegister(projectSHR: Double) -> Double { // func coolingSensiblePerRegister(projectSHR: Double) -> Double {
let sensible = coolingSensible ?? (coolingTotal * projectSHR) // let sensible = coolingSensible ?? (coolingTotal * projectSHR)
return sensible / Double(registerCount) // return sensible / Double(registerCount)
} // }
} // }
extension TrunkSize.RoomProxy { extension TrunkSize.RoomProxy {
@@ -210,8 +210,8 @@ extension TrunkSize.RoomProxy {
room.heatingLoadPerRegister * Double(actualRegisterCount) room.heatingLoadPerRegister * Double(actualRegisterCount)
} }
func totalCoolingSensible(projectSHR: Double) -> Double { func totalCoolingSensible(projectSHR: Double) throws -> Double {
room.coolingSensiblePerRegister(projectSHR: projectSHR) * Double(actualRegisterCount) try room.coolingSensiblePerRegister(projectSHR: projectSHR) * Double(actualRegisterCount)
} }
} }
@@ -221,7 +221,7 @@ extension TrunkSize {
rooms.reduce(into: 0) { $0 += $1.totalHeatingLoad } rooms.reduce(into: 0) { $0 += $1.totalHeatingLoad }
} }
func totalCoolingSensible(projectSHR: Double) -> Double { func totalCoolingSensible(projectSHR: Double) throws -> Double {
rooms.reduce(into: 0) { $0 += $1.totalCoolingSensible(projectSHR: projectSHR) } try rooms.reduce(into: 0) { $0 += try $1.totalCoolingSensible(projectSHR: projectSHR) }
} }
} }

View File

@@ -1,3 +1,4 @@
import CSVParser
import DatabaseClient import DatabaseClient
import Dependencies import Dependencies
import Elementary import Elementary
@@ -284,10 +285,18 @@ extension SiteRoute.View.ProjectRoute.RoomRoute {
on request: ViewController.Request, on request: ViewController.Request,
projectID: Project.ID projectID: Project.ID
) async -> AnySendableHTML { ) async -> AnySendableHTML {
@Dependency(\.csvParser) var csvParser
@Dependency(\.database) var database @Dependency(\.database) var database
switch self { switch self {
case .csv(let csv):
return await roomsView(on: request, projectID: projectID) {
let rooms = try await csvParser.parseRooms(csv)
_ = try await database.rooms.createMany(projectID, rooms)
}
// return EmptyHTML()
case .delete(let id): case .delete(let id):
return await ResultView { return await ResultView {
try await database.rooms.delete(id) try await database.rooms.delete(id)
@@ -298,7 +307,7 @@ extension SiteRoute.View.ProjectRoute.RoomRoute {
case .submit(let form): case .submit(let form):
return await roomsView(on: request, projectID: projectID) { return await roomsView(on: request, projectID: projectID) {
_ = try await database.rooms.create(form) _ = try await database.rooms.create(projectID, form)
} }
case .update(let id, let form): case .update(let id, let form):

View File

@@ -47,8 +47,6 @@ struct RoomForm: HTML, Sendable {
.hx.swap(.outerHTML) .hx.swap(.outerHTML)
) { ) {
input(.class("hidden"), .name("projectID"), .value("\(projectID)"))
if let id = room?.id { if let id = room?.id {
input(.class("hidden"), .name("id"), .value("\(id)")) input(.class("hidden"), .name("id"), .value("\(id)"))
} }
@@ -73,14 +71,15 @@ struct RoomForm: HTML, Sendable {
.value(room?.heatingLoad) .value(room?.heatingLoad)
) )
// TODO: Add description that only one is required (cooling total or sensible)
LabeledInput( LabeledInput(
"Cooling Total", "Cooling Total",
.name("coolingTotal"), .name("coolingTotal"),
.type(.number), .type(.number),
.placeholder("1234"), .placeholder("1234 (Optional)"),
.required,
.min("0"), .min("0"),
.value(room?.coolingTotal) .value(room?.coolingLoad.total)
) )
LabeledInput( LabeledInput(
@@ -89,7 +88,7 @@ struct RoomForm: HTML, Sendable {
.type(.number), .type(.number),
.placeholder("1234 (Optional)"), .placeholder("1234 (Optional)"),
.min("0"), .min("0"),
.value(room?.coolingSensible) .value(room?.coolingLoad.sensible)
) )
LabeledInput( LabeledInput(

View File

@@ -11,6 +11,11 @@ struct RoomsView: HTML, Sendable {
let rooms: [Room] let rooms: [Room]
let sensibleHeatRatio: Double? let sensibleHeatRatio: Double?
private var csvRoute: String {
SiteRoute.router.path(for: .view(.project(.detail(projectID, .rooms(.index)))))
.appendingPath("csv")
}
var body: some HTML { var body: some HTML {
div(.class("flex w-full flex-col")) { div(.class("flex w-full flex-col")) {
PageTitleRow { PageTitleRow {
@@ -44,6 +49,18 @@ struct RoomsView: HTML, Sendable {
.attributes(.class("border border-error"), when: sensibleHeatRatio == nil) .attributes(.class("border border-error"), when: sensibleHeatRatio == nil)
} }
.attributes(.class("tooltip-open"), when: sensibleHeatRatio == nil) .attributes(.class("tooltip-open"), when: sensibleHeatRatio == nil)
Tooltip("Upload csv file", position: .left) {
form(
.hx.post(csvRoute),
.hx.target("body"),
.hx.swap(.outerHTML),
.custom(name: "enctype", value: "multipart/form-data")
) {
input(.type(.file), .name("file"), .accept(".csv"))
SubmitButton()
}
}
} }
div(.class("flex items-end space-x-4 font-bold")) { div(.class("flex items-end space-x-4 font-bold")) {
@@ -54,13 +71,15 @@ struct RoomsView: HTML, Sendable {
div(.class("flex justify-center items-end space-x-4 my-auto font-bold")) { div(.class("flex justify-center items-end space-x-4 my-auto font-bold")) {
span(.class("text-lg")) { "Cooling Total" } span(.class("text-lg")) { "Cooling Total" }
Badge(number: rooms.totalCoolingLoad, digits: 0) // TODO: ResultView ??
Badge(number: try! rooms.totalCoolingLoad(shr: sensibleHeatRatio ?? 1.0), digits: 0)
.attributes(.class("badge-success")) .attributes(.class("badge-success"))
} }
div(.class("flex grow justify-end items-end space-x-4 me-4 my-auto font-bold")) { div(.class("flex grow justify-end items-end space-x-4 me-4 my-auto font-bold")) {
span(.class("text-lg")) { "Cooling Sensible" } span(.class("text-lg")) { "Cooling Sensible" }
Badge(number: rooms.totalCoolingSensible(shr: sensibleHeatRatio ?? 1.0), digits: 0) // TODO: ResultView ??
Badge(number: try! rooms.totalCoolingSensible(shr: sensibleHeatRatio ?? 1.0), digits: 0)
.attributes(.class("badge-info")) .attributes(.class("badge-info"))
} }
} }
@@ -125,10 +144,11 @@ struct RoomsView: HTML, Sendable {
let shr: Double let shr: Double
var coolingSensible: Double { var coolingSensible: Double {
guard let value = room.coolingSensible else { try! room.coolingLoad.ensured(shr: shr).sensible
return room.coolingTotal * shr // guard let value = room.coolingSensible else {
} // return room.coolingTotal * shr
return value // }
// return value
} }
init(room: Room, shr: Double?) { init(room: Room, shr: Double?) {
@@ -147,7 +167,7 @@ struct RoomsView: HTML, Sendable {
} }
td { td {
div(.class("flex justify-center")) { div(.class("flex justify-center")) {
Number(room.coolingTotal, digits: 0) Number(try! room.coolingLoad.ensured(shr: shr).total, digits: 0)
// .attributes(.class("text-success")) // .attributes(.class("text-success"))
} }
} }

View File

@@ -0,0 +1,22 @@
import CSVParser
import Foundation
import Testing
@Suite
struct CSVParsingTests {
@Test
func roomParsing() async throws {
let parser = CSVParser.liveValue
let input = """
Name,Heating Load,Cooling Total,Cooling Sensible,Register Count
Bed-1,12345,12345,,2
Bed-2,1223,,1123,1
"""
let rooms = try await parser.parseRooms(.init(file: Data(input.utf8)))
#expect(rooms.count == 2)
}
}

View File

@@ -85,7 +85,8 @@ struct ProjectTests {
#expect(completed.frictionRate == true) #expect(completed.frictionRate == true)
_ = try await database.rooms.create( _ = try await database.rooms.create(
.init(projectID: project.id, name: "Test", heatingLoad: 12345, coolingTotal: 12345) project.id,
.init(name: "Test", heatingLoad: 12345, coolingTotal: 12345)
) )
completed = try await database.projects.getCompletedSteps(project.id) completed = try await database.projects.getCompletedSteps(project.id)
#expect(completed.rooms == true) #expect(completed.rooms == true)
@@ -130,7 +131,8 @@ struct ProjectTests {
.init(projectID: project.id, name: "Test", value: 0.2) .init(projectID: project.id, name: "Test", value: 0.2)
) )
let room = try await database.rooms.create( let room = try await database.rooms.create(
.init(projectID: project.id, name: "Test", heatingLoad: 12345, coolingTotal: 12345) project.id,
.init(name: "Test", heatingLoad: 12345, coolingTotal: 12345)
) )
let supplyLength = try await database.equivalentLengths.create( let supplyLength = try await database.equivalentLengths.create(
.init( .init(

View File

@@ -1,8 +1,8 @@
import Dependencies import Dependencies
import Foundation import Foundation
import ManualDCore import ManualDCore
import Parsing
import Testing import Testing
import Validations
@testable import DatabaseClient @testable import DatabaseClient
@@ -15,7 +15,8 @@ struct RoomTests {
@Dependency(\.database.rooms) var rooms @Dependency(\.database.rooms) var rooms
let room = try await rooms.create( let room = try await rooms.create(
.init(projectID: project.id, name: "Test", heatingLoad: 1234, coolingTotal: 1234) project.id,
.init(name: "Test", heatingLoad: 1234, coolingTotal: 1234)
) )
let fetched = try await rooms.fetch(project.id) let fetched = try await rooms.fetch(project.id)
@@ -48,10 +49,13 @@ struct RoomTests {
try await withTestUserAndProject { _, project in try await withTestUserAndProject { _, project in
@Dependency(\.database.rooms) var rooms @Dependency(\.database.rooms) var rooms
let created = try await rooms.createMany([ let created = try await rooms.createMany(
.init(projectID: project.id, name: "Test 1", heatingLoad: 1234, coolingTotal: 1234), project.id,
.init(projectID: project.id, name: "Test 2", heatingLoad: 1234, coolingTotal: 1234), [
]) .init(name: "Test 1", heatingLoad: 1234, coolingTotal: 1234),
.init(name: "Test 2", heatingLoad: 1234, coolingTotal: 1234),
]
)
#expect(created.count == 2) #expect(created.count == 2)
#expect(created[0].name == "Test 1") #expect(created[0].name == "Test 1")
@@ -85,7 +89,7 @@ struct RoomTests {
@Test( @Test(
arguments: [ arguments: [
Room.Create( Room.Create(
projectID: UUID(0), // projectID: UUID(0),
name: "", name: "",
heatingLoad: 12345, heatingLoad: 12345,
coolingTotal: 12344, coolingTotal: 12344,
@@ -93,7 +97,7 @@ struct RoomTests {
registerCount: 1 registerCount: 1
), ),
Room.Create( Room.Create(
projectID: UUID(0), // projectID: UUID(0),
name: "Test", name: "Test",
heatingLoad: -12345, heatingLoad: -12345,
coolingTotal: 12344, coolingTotal: 12344,
@@ -101,7 +105,7 @@ struct RoomTests {
registerCount: 1 registerCount: 1
), ),
Room.Create( Room.Create(
projectID: UUID(0), // projectID: UUID(0),
name: "Test", name: "Test",
heatingLoad: 12345, heatingLoad: 12345,
coolingTotal: -12344, coolingTotal: -12344,
@@ -109,7 +113,7 @@ struct RoomTests {
registerCount: 1 registerCount: 1
), ),
Room.Create( Room.Create(
projectID: UUID(0), // projectID: UUID(0),
name: "Test", name: "Test",
heatingLoad: 12345, heatingLoad: 12345,
coolingTotal: 12344, coolingTotal: 12344,
@@ -117,7 +121,7 @@ struct RoomTests {
registerCount: 1 registerCount: 1
), ),
Room.Create( Room.Create(
projectID: UUID(0), // projectID: UUID(0),
name: "Test", name: "Test",
heatingLoad: 12345, heatingLoad: 12345,
coolingTotal: 12344, coolingTotal: 12344,
@@ -125,19 +129,27 @@ struct RoomTests {
registerCount: -1 registerCount: -1
), ),
Room.Create( Room.Create(
projectID: UUID(0), // projectID: UUID(0),
name: "", name: "",
heatingLoad: -12345, heatingLoad: -12345,
coolingTotal: -12344, coolingTotal: -12344,
coolingSensible: -1, coolingSensible: -1,
registerCount: -1 registerCount: -1
), ),
Room.Create(
// projectID: UUID(0),
name: "Test",
heatingLoad: 12345,
coolingTotal: nil,
coolingSensible: nil,
registerCount: 1
),
] ]
) )
func validations(room: Room.Create) throws { func validations(room: Room.Create) throws {
#expect(throws: (any Error).self) { #expect(throws: (any Error).self) {
// do { // do {
try room.toModel().validate() try room.toModel(projectID: UUID(0)).validate()
// } catch { // } catch {
// print("\(error)") // print("\(error)")
// throw error // throw error
@@ -145,3 +157,4 @@ struct RoomTests {
} }
} }
} }

View File

@@ -14,9 +14,11 @@ struct TrunkSizeTests {
@Dependency(\.database) var database @Dependency(\.database) var database
let room = try await database.rooms.create( let room = try await database.rooms.create(
project.id,
.init( .init(
projectID: project.id, name: "Test", heatingLoad: 12345, coolingTotal: 12345, name: "Test", heatingLoad: 12345, coolingTotal: 12345,
coolingSensible: nil, registerCount: 5) coolingSensible: nil, registerCount: 5
)
) )
let trunk = try await database.trunkSizes.create( let trunk = try await database.trunkSizes.create(

View File

@@ -103,7 +103,8 @@ struct ViewControllerTests {
let mockDuctSizes = DuctSizes.mock( let mockDuctSizes = DuctSizes.mock(
equipmentInfo: equipment, equipmentInfo: equipment,
rooms: rooms, rooms: rooms,
trunks: trunks trunks: trunks,
shr: project.sensibleHeatRatio ?? 0.83
) )
try await withDefaultDependencies { try await withDefaultDependencies {

View File

@@ -63,6 +63,12 @@ p-6 w-full">
</div> </div>
</button> </button>
</div> </div>
<div class="tooltip tooltip-left" data-tip="Upload csv file">
<form hx-post="/projects/00000000-0000-0000-0000-000000000000/rooms/csv" hx-target="body" hx-swap="outerHTML" enctype="multipart/form-data">
<input type="file" name="file" accept=".csv">
<button class="btn btn-secondary" type="submit">Submit</button>
</form>
</div>
</div> </div>
<div class="flex items-end space-x-4 font-bold"> <div class="flex items-end space-x-4 font-bold">
<span class="text-lg">Heating Total</span> <span class="text-lg">Heating Total</span>
@@ -151,14 +157,13 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000001.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000001.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1> <h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000001" hx-target="body" hx-swap="outerHTML"> <form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000001" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000001"> <input class="hidden" name="id" value="00000000-0000-0000-0000-000000000001">
Name<label class="input w-full"><span class="label"></span> Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Bed-1"> <input name="name" type="text" placeholder="Name" required autofocus value="Bed-1">
Heating Load</label><label class="input w-full"><span class="label"></span> Heating Load</label><label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="3913.0"> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="3913.0">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="2472.0"> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="2472.0">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
Registers</label><label class="input w-full"><span class="label"></span> Registers</label><label class="input w-full"><span class="label"></span>
@@ -204,14 +209,13 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000002.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000002.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1> <h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000002" hx-target="body" hx-swap="outerHTML"> <form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000002" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000002"> <input class="hidden" name="id" value="00000000-0000-0000-0000-000000000002">
Name<label class="input w-full"><span class="label"></span> Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Entry"> <input name="name" type="text" placeholder="Name" required autofocus value="Entry">
Heating Load</label><label class="input w-full"><span class="label"></span> Heating Load</label><label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="8284.0"> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="8284.0">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="2916.0"> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="2916.0">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
Registers</label><label class="input w-full"><span class="label"></span> Registers</label><label class="input w-full"><span class="label"></span>
@@ -257,14 +261,13 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000003.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000003.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1> <h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000003" hx-target="body" hx-swap="outerHTML"> <form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000003" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000003"> <input class="hidden" name="id" value="00000000-0000-0000-0000-000000000003">
Name<label class="input w-full"><span class="label"></span> Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Family Room"> <input name="name" type="text" placeholder="Name" required autofocus value="Family Room">
Heating Load</label><label class="input w-full"><span class="label"></span> Heating Load</label><label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="9785.0"> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="9785.0">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="7446.0"> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="7446.0">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
Registers</label><label class="input w-full"><span class="label"></span> Registers</label><label class="input w-full"><span class="label"></span>
@@ -310,14 +313,13 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000004.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000004.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1> <h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000004" hx-target="body" hx-swap="outerHTML"> <form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000004" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000004"> <input class="hidden" name="id" value="00000000-0000-0000-0000-000000000004">
Name<label class="input w-full"><span class="label"></span> Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Kitchen"> <input name="name" type="text" placeholder="Name" required autofocus value="Kitchen">
Heating Load</label><label class="input w-full"><span class="label"></span> Heating Load</label><label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="4518.0"> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="4518.0">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="5096.0"> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="5096.0">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
Registers</label><label class="input w-full"><span class="label"></span> Registers</label><label class="input w-full"><span class="label"></span>
@@ -363,14 +365,13 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000005.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000005.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1> <h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000005" hx-target="body" hx-swap="outerHTML"> <form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000005" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000005"> <input class="hidden" name="id" value="00000000-0000-0000-0000-000000000005">
Name<label class="input w-full"><span class="label"></span> Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Living Room"> <input name="name" type="text" placeholder="Name" required autofocus value="Living Room">
Heating Load</label><label class="input w-full"><span class="label"></span> Heating Load</label><label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="7553.0"> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="7553.0">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="6829.0"> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="6829.0">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
Registers</label><label class="input w-full"><span class="label"></span> Registers</label><label class="input w-full"><span class="label"></span>
@@ -416,14 +417,13 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000006.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000006.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1> <h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000006" hx-target="body" hx-swap="outerHTML"> <form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000006" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000006"> <input class="hidden" name="id" value="00000000-0000-0000-0000-000000000006">
Name<label class="input w-full"><span class="label"></span> Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Master"> <input name="name" type="text" placeholder="Name" required autofocus value="Master">
Heating Load</label><label class="input w-full"><span class="label"></span> Heating Load</label><label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="8202.0"> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="8202.0">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="2076.0"> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="2076.0">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
Registers</label><label class="input w-full"><span class="label"></span> Registers</label><label class="input w-full"><span class="label"></span>
@@ -441,13 +441,12 @@ p-6 w-full">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<h1 class="text-3xl font-bold pb-6">Room</h1> <h1 class="text-3xl font-bold pb-6">Room</h1>
<form class="grid grid-cols-1 gap-4" hx-post="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-target="body" hx-swap="outerHTML"> <form class="grid grid-cols-1 gap-4" hx-post="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-target="body" hx-swap="outerHTML">
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000"> <label class="input w-full"><span class="label">Name</span>
Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value=""> <input name="name" type="text" placeholder="Name" required autofocus value="">
Heating Load</label><label class="input w-full"><span class="label"></span> Heating Load</label><label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value=""> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value=""> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
Registers</label><label class="input w-full"><span class="label"></span> Registers</label><label class="input w-full"><span class="label"></span>