diff --git a/Sources/DatabaseClient/Interface.swift b/Sources/DatabaseClient/Interface.swift index 02246af..45f9bcd 100644 --- a/Sources/DatabaseClient/Interface.swift +++ b/Sources/DatabaseClient/Interface.swift @@ -18,6 +18,7 @@ public struct DatabaseClient: Sendable { public var equipment: Equipment public var componentLoss: ComponentLoss public var effectiveLength: EffectiveLengthClient + // public var rectangularDuct: RectangularDuct public var users: Users } @@ -29,6 +30,7 @@ extension DatabaseClient: TestDependencyKey { equipment: .testValue, componentLoss: .testValue, effectiveLength: .testValue, + // rectangularDuct: .testValue, users: .testValue ) @@ -40,6 +42,7 @@ extension DatabaseClient: TestDependencyKey { equipment: .live(database: database), componentLoss: .live(database: database), effectiveLength: .live(database: database), + // rectangularDuct: .live(database: database), users: .live(database: database) ) } @@ -67,6 +70,7 @@ extension DatabaseClient.Migrations: DependencyKey { EquipmentInfo.Migrate(), Room.Migrate(), EffectiveLength.Migrate(), + // DuctSizing.RectangularDuct.Migrate(), ] } ) diff --git a/Sources/DatabaseClient/RectangularDuct.swift b/Sources/DatabaseClient/RectangularDuct.swift new file mode 100644 index 0000000..1f66de3 --- /dev/null +++ b/Sources/DatabaseClient/RectangularDuct.swift @@ -0,0 +1,175 @@ +// import Dependencies +// import DependenciesMacros +// import Fluent +// import Foundation +// import ManualDCore +// +// extension DatabaseClient { +// @DependencyClient +// public struct RectangularDuct: Sendable { +// public var create: +// @Sendable (DuctSizing.RectangularDuct.Create) async throws -> DuctSizing.RectangularDuct +// public var delete: @Sendable (DuctSizing.RectangularDuct.ID) async throws -> Void +// public var fetch: @Sendable (Room.ID) async throws -> [DuctSizing.RectangularDuct] +// public var get: +// @Sendable (DuctSizing.RectangularDuct.ID) async throws -> DuctSizing.RectangularDuct? +// public var update: +// @Sendable (DuctSizing.RectangularDuct.ID, DuctSizing.RectangularDuct.Update) async throws -> +// DuctSizing.RectangularDuct +// } +// } +// +// extension DatabaseClient.RectangularDuct: TestDependencyKey { +// public static let testValue = Self() +// +// public static func live(database: any Database) -> Self { +// .init( +// create: { request in +// try request.validate() +// let model = request.toModel() +// try await model.save(on: database) +// return try model.toDTO() +// }, +// delete: { id in +// guard let model = try await RectangularDuctModel.find(id, on: database) else { +// throw NotFoundError() +// } +// try await model.delete(on: database) +// }, +// fetch: { roomID in +// try await RectangularDuctModel.query(on: database) +// .with(\.$room) +// .filter(\.$room.$id == roomID) +// .all() +// .map { try $0.toDTO() } +// }, +// get: { id in +// try await RectangularDuctModel.find(id, on: database) +// .map { try $0.toDTO() } +// }, +// update: { id, updates in +// guard let model = try await RectangularDuctModel.find(id, on: database) else { +// throw NotFoundError() +// } +// try updates.validate() +// model.applyUpdates(updates) +// if model.hasChanges { +// try await model.save(on: database) +// } +// return try model.toDTO() +// } +// ) +// } +// } +// +// extension DuctSizing.RectangularDuct.Create { +// +// func validate() throws(ValidationError) { +// guard height > 0 else { +// throw ValidationError("Rectangular duct size height should be greater than 0.") +// } +// if let register { +// guard register > 0 else { +// throw ValidationError("Rectangular duct size register should be greater than 0.") +// } +// } +// } +// +// func toModel() -> RectangularDuctModel { +// .init(roomID: roomID, height: height) +// } +// } +// +// extension DuctSizing.RectangularDuct.Update { +// +// func validate() throws(ValidationError) { +// if let height { +// guard height > 0 else { +// throw ValidationError("Rectangular duct size height should be greater than 0.") +// } +// } +// if let register { +// guard register > 0 else { +// throw ValidationError("Rectangular duct size register should be greater than 0.") +// } +// } +// } +// } +// +// extension DuctSizing.RectangularDuct { +// struct Migrate: AsyncMigration { +// let name = "CreateRectangularDuct" +// +// func prepare(on database: any Database) async throws { +// try await database.schema(RectangularDuctModel.schema) +// .id() +// .field("register", .int8) +// .field("height", .int8, .required) +// .field("roomID", .uuid, .required, .references(RoomModel.schema, "id", onDelete: .cascade)) +// .field("createdAt", .datetime) +// .field("updatedAt", .datetime) +// .create() +// } +// +// func revert(on database: any Database) async throws { +// try await database.schema(RectangularDuctModel.schema).delete() +// } +// } +// } +// +// final class RectangularDuctModel: Model, @unchecked Sendable { +// +// static let schema = "rectangularDuct" +// +// @ID(key: .id) +// var id: UUID? +// +// @Parent(key: "roomID") +// var room: RoomModel +// +// @Field(key: "height") +// var height: Int +// +// @Field(key: "register") +// var register: Int? +// +// @Timestamp(key: "createdAt", on: .create, format: .iso8601) +// var createdAt: Date? +// +// @Timestamp(key: "updatedAt", on: .update, format: .iso8601) +// var updatedAt: Date? +// +// init() {} +// +// init( +// id: UUID? = nil, +// roomID: Room.ID, +// register: Int? = nil, +// height: Int +// ) { +// self.id = id +// $room.id = roomID +// self.register = register +// self.height = height +// } +// +// func toDTO() throws -> DuctSizing.RectangularDuct { +// return try .init( +// id: requireID(), +// roomID: $room.id, +// register: register, +// height: height, +// createdAt: createdAt!, +// updatedAt: updatedAt! +// ) +// } +// +// func applyUpdates(_ updates: DuctSizing.RectangularDuct.Update) { +// if let height = updates.height, height != self.height { +// self.height = height +// } +// if let register = updates.register, register != self.register { +// self.register = register +// } +// } +// } diff --git a/Sources/DatabaseClient/Rooms.swift b/Sources/DatabaseClient/Rooms.swift index f2d6e17..b2ec33f 100644 --- a/Sources/DatabaseClient/Rooms.swift +++ b/Sources/DatabaseClient/Rooms.swift @@ -38,6 +38,7 @@ extension DatabaseClient.Rooms: TestDependencyKey { try await RoomModel.query(on: database) .with(\.$project) .filter(\.$project.$id, .equal, projectID) + .sort(\.$name, .ascending) .all() .map { try $0.toDTO() } }, @@ -135,6 +136,7 @@ extension Room { .field("coolingTotal", .double, .required) .field("coolingSensible", .double) .field("registerCount", .int8, .required) + .field("rectangularSizes", .array) .field("createdAt", .datetime) .field("updatedAt", .datetime) .field( @@ -172,6 +174,9 @@ final class RoomModel: Model, @unchecked Sendable { @Field(key: "registerCount") var registerCount: Int + @Field(key: "rectangularSizes") + var rectangularSizes: [DuctSizing.RectangularDuct]? + @Timestamp(key: "createdAt", on: .create, format: .iso8601) var createdAt: Date? @@ -190,6 +195,7 @@ final class RoomModel: Model, @unchecked Sendable { coolingTotal: Double, coolingSensible: Double? = nil, registerCount: Int, + rectangularSizes: [DuctSizing.RectangularDuct]? = nil, createdAt: Date? = nil, updatedAt: Date? = nil, projectID: Project.ID @@ -200,6 +206,7 @@ final class RoomModel: Model, @unchecked Sendable { self.coolingTotal = coolingTotal self.coolingSensible = coolingSensible self.registerCount = registerCount + self.rectangularSizes = rectangularSizes self.createdAt = createdAt self.updatedAt = updatedAt $project.id = projectID @@ -214,6 +221,7 @@ final class RoomModel: Model, @unchecked Sendable { coolingTotal: coolingTotal, coolingSensible: coolingSensible, registerCount: registerCount, + rectangularSizes: rectangularSizes, createdAt: createdAt!, updatedAt: updatedAt! ) @@ -236,6 +244,9 @@ final class RoomModel: Model, @unchecked Sendable { if let registerCount = updates.registerCount, registerCount != self.registerCount { self.registerCount = registerCount } + if let rectangularSizes = updates.rectangularSizes, rectangularSizes != self.rectangularSizes { + self.rectangularSizes = rectangularSizes + } } diff --git a/Sources/ManualDClient/ManualDClient.swift b/Sources/ManualDClient/ManualDClient.swift index adb193c..4a411d6 100644 --- a/Sources/ManualDClient/ManualDClient.swift +++ b/Sources/ManualDClient/ManualDClient.swift @@ -20,6 +20,7 @@ public struct ManualDClient: Sendable { projectSHR: Double, logger: Logger? = nil ) async throws -> [DuctSizing.RoomContainer] { + var registerIDCount = 1 var retval: [DuctSizing.RoomContainer] = [] let totalHeatingLoad = rooms.totalHeatingLoad @@ -38,6 +39,18 @@ public struct ManualDClient: Sendable { ) for n in 1...room.registerCount { + + var rectangularWidth: Int? = nil + let rectangularSize = room.rectangularSizes? + .first(where: { $0.register == nil || $0.register == n }) + + if let rectangularSize { + let response = try await self.equivalentRectangularDuct( + .init(round: sizes.finalSize, height: rectangularSize.height) + ) + rectangularWidth = response.width + } + retval.append( .init( registerID: "SR-\(registerIDCount)", @@ -51,7 +64,9 @@ public struct ManualDClient: Sendable { roundSize: sizes.ductulatorSize, finalSize: sizes.finalSize, velocity: sizes.velocity, - flexSize: sizes.flexSize + flexSize: sizes.flexSize, + rectangularSize: rectangularSize, + rectangularWidth: rectangularWidth ) ) registerIDCount += 1 diff --git a/Sources/ManualDCore/DuctSizing.swift b/Sources/ManualDCore/DuctSizing.swift index 52325d9..06ef7cc 100644 --- a/Sources/ManualDCore/DuctSizing.swift +++ b/Sources/ManualDCore/DuctSizing.swift @@ -3,6 +3,21 @@ import Foundation public enum DuctSizing { + public struct RectangularDuct: Codable, Equatable, Sendable { + + public let register: Int? + public let height: Int + + public init( + register: Int? = nil, + height: Int, + ) { + self.register = register + self.height = height + } + + } + public struct RoomContainer: Codable, Equatable, Sendable { public let registerID: String @@ -17,6 +32,8 @@ public enum DuctSizing { public let finalSize: Int public let velocity: Int public let flexSize: Int + public let rectangularSize: RectangularDuct? + public let rectangularWidth: Int? public init( registerID: String, @@ -30,7 +47,9 @@ public enum DuctSizing { roundSize: Double, finalSize: Int, velocity: Int, - flexSize: Int + flexSize: Int, + rectangularSize: RectangularDuct? = nil, + rectangularWidth: Int? = nil ) { self.registerID = registerID self.roomID = roomID @@ -44,6 +63,8 @@ public enum DuctSizing { self.finalSize = finalSize self.velocity = velocity self.flexSize = flexSize + self.rectangularSize = rectangularSize + self.rectangularWidth = rectangularWidth } } diff --git a/Sources/ManualDCore/Room.swift b/Sources/ManualDCore/Room.swift index a19aec0..a1793eb 100644 --- a/Sources/ManualDCore/Room.swift +++ b/Sources/ManualDCore/Room.swift @@ -9,6 +9,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable { public let coolingTotal: Double public let coolingSensible: Double? public let registerCount: Int + public let rectangularSizes: [DuctSizing.RectangularDuct]? public let createdAt: Date public let updatedAt: Date @@ -20,6 +21,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable { coolingTotal: Double, coolingSensible: Double? = nil, registerCount: Int = 1, + rectangularSizes: [DuctSizing.RectangularDuct]? = nil, createdAt: Date, updatedAt: Date ) { @@ -30,6 +32,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable { self.coolingTotal = coolingTotal self.coolingSensible = coolingSensible self.registerCount = registerCount + self.rectangularSizes = rectangularSizes self.createdAt = createdAt self.updatedAt = updatedAt } @@ -68,6 +71,7 @@ extension Room { public let coolingTotal: Double? public let coolingSensible: Double? public let registerCount: Int? + public let rectangularSizes: [DuctSizing.RectangularDuct]? public init( name: String? = nil, @@ -81,6 +85,18 @@ extension Room { self.coolingTotal = coolingTotal self.coolingSensible = coolingSensible self.registerCount = registerCount + self.rectangularSizes = nil + } + + public init( + rectangularSizes: [DuctSizing.RectangularDuct] + ) { + self.name = nil + self.heatingLoad = nil + self.coolingTotal = nil + self.coolingSensible = nil + self.registerCount = nil + self.rectangularSizes = rectangularSizes } } } diff --git a/Sources/ManualDCore/Routes/ViewRoute.swift b/Sources/ManualDCore/Routes/ViewRoute.swift index dbb0766..7ba0248 100644 --- a/Sources/ManualDCore/Routes/ViewRoute.swift +++ b/Sources/ManualDCore/Routes/ViewRoute.swift @@ -677,6 +677,7 @@ extension SiteRoute.View.ProjectRoute { public enum DuctSizingRoute: Equatable, Sendable { case index + case roomRectangularForm(Room.ID, RoomRectangularForm) static let rootPath = "duct-sizing" @@ -685,6 +686,26 @@ extension SiteRoute.View.ProjectRoute { Path { rootPath } Method.get } + Route(.case(Self.roomRectangularForm)) { + Path { + rootPath + "room" + Room.ID.parser() + } + Method.post + Body { + FormData { + Field("register") { Int.parser() } + Field("height") { Int.parser() } + } + .map(.memberwise(RoomRectangularForm.init)) + } + } + } + + public struct RoomRectangularForm: Equatable, Sendable { + public let register: Int + public let height: Int } } } diff --git a/Sources/ViewController/Extensions/ManualDClient+extensions.swift b/Sources/ViewController/Extensions/ManualDClient+extensions.swift index 76879d1..821916c 100644 --- a/Sources/ViewController/Extensions/ManualDClient+extensions.swift +++ b/Sources/ViewController/Extensions/ManualDClient+extensions.swift @@ -28,7 +28,7 @@ extension ManualDClient { logger: logger ) - logger?.debug("Rooms: \(ductRooms)") + // logger?.debug("Rooms: \(ductRooms)") return ductRooms diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index 8f46dca..97995d8 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -342,11 +342,31 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute { func renderView(on request: ViewController.Request, projectID: Project.ID) async throws -> AnySendableHTML { + @Dependency(\.database) var database + @Dependency(\.manualD) var manualD + switch self { case .index: return request.view { ProjectView(projectID: projectID, activeTab: .ductSizing, logger: request.logger) } + case .roomRectangularForm(let roomID, let form): + let _ = try await database.rooms.update( + roomID, + .init(rectangularSizes: [.init(register: form.register, height: form.height)]) + ) + // request.logger.debug("Got room rectangular form: \(roomID)") + // + // let containers = try await manualD.calculate( + // rooms: [room], + // designFrictionRateResult: database.designFrictionRate(projectID: projectID), + // projectSHR: database.projects.getSensibleHeatRatio(projectID) + // ) + // request.logger.debug("Room Containers: \(containers)") + // let container = containers.first(where: { $0.roomName == "\(room.name)-\(form.register)" })! + // request.logger.debug("Room Container: \(container)") + // return DuctSizingView.RoomRow(projectID: projectID, room: container) + return ProjectView(projectID: projectID, activeTab: .ductSizing, logger: request.logger) } } } diff --git a/Sources/ViewController/Views/ComponentLoss/ComponentLossForm.swift b/Sources/ViewController/Views/ComponentLoss/ComponentLossForm.swift index fe3d28a..fc229f6 100644 --- a/Sources/ViewController/Views/ComponentLoss/ComponentLossForm.swift +++ b/Sources/ViewController/Views/ComponentLoss/ComponentLossForm.swift @@ -3,6 +3,8 @@ import ElementaryHTMX import ManualDCore import Styleguide +// FIX: The value field is sometimes wonky as far as what values it accepts. + struct ComponentLossForm: HTML, Sendable { static func id(_ componentLoss: ComponentPressureLoss? = nil) -> String { @@ -49,7 +51,7 @@ struct ComponentLossForm: HTML, Sendable { label(.for("value")) { "Value" } Input(id: "value", placeholder: "Pressure loss") .attributes( - .type(.number), .min("0.03"), .max("1.0"), .step("0.1"), .required, + .type(.number), .min("0.03"), .max("1.0"), .step("0.01"), .required, .value(componentLoss?.value) ) } diff --git a/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift b/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift index 8b23b39..dee2058 100644 --- a/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift +++ b/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift @@ -7,12 +7,27 @@ import Styleguide struct DuctSizingView: HTML, Sendable { + let projectID: Project.ID let rooms: [DuctSizing.RoomContainer] var body: some HTML { div { h1(.class("text-2xl py-4")) { "Duct Sizes" } + if rooms.count == 0 { + p(.class("text-error italic")) { + "Must complete all the previous sections to display duct sizing calculations." + } + } else { + RoomsTable(projectID: projectID, rooms: rooms) + } + } + } + struct RoomsTable: HTML, Sendable { + let projectID: Project.ID + let rooms: [DuctSizing.RoomContainer] + + var body: some HTML { div(.class("overflow-x-auto")) { table(.class("table table-zebra")) { thead { @@ -27,12 +42,14 @@ struct DuctSizingView: HTML, Sendable { th(.class("hidden xl:table-cell")) { "Round Size" } th { "Velocity" } th { "Final Size" } + th { "Height" } + th { "Width" } th { "Flex Size" } } } tbody { for room in rooms { - RoomRow(room: room) + RoomRow(projectID: projectID, room: room) } } } @@ -41,10 +58,19 @@ struct DuctSizingView: HTML, Sendable { } struct RoomRow: HTML, Sendable { + let projectID: Project.ID let room: DuctSizing.RoomContainer + var route: String { + SiteRoute.View.router.path( + for: .project(.detail(projectID, .ductSizing(.index))) + ) + .appendingPath("room") + .appendingPath(room.roomID) + } + var body: some HTML { - tr(.class("text-lg")) { + tr(.class("text-lg"), .id(room.roomID.idString)) { td { room.registerID } td { room.roomName } td { Number(room.heatingLoad, digits: 0) } @@ -62,6 +88,32 @@ struct DuctSizingView: HTML, Sendable { Number(room.finalSize) .attributes(.class("badge badge-outline badge-secondary text-xl font-bold")) } + td { + form( + .hx.post(route), + .hx.target("body"), + .hx.swap(.outerHTML) + // .hx.trigger( + // .event(.change).from("#rectangularSize_\(room.roomID.idString)") + // ) + ) { + input(.class("hidden"), .name("register"), .value("\(room.roomName.last!)")) + Row { + Input( + id: "height", + name: "height", + placeholder: "Height" + ) + .attributes(.type(.number), .min("0"), .value(room.rectangularSize?.height)) + SubmitButton() + } + } + } + td { + if let width = room.rectangularWidth { + Number(width) + } + } td { Number(room.flexSize) .attributes(.class("badge badge-outline badge-primary text-xl font-bold")) diff --git a/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift b/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift index 65a61d7..4c0b02e 100644 --- a/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift +++ b/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift @@ -2,6 +2,8 @@ import Elementary import ManualDCore import Styleguide +// FIX: Need to update available static, etc. when equipment info is submitted. + struct FrictionRateView: HTML, Sendable { let equipmentInfo: EquipmentInfo? @@ -46,8 +48,8 @@ struct FrictionRateView: HTML, Sendable { div(.class("p-4 space-y-6")) { h1(.class("text-4xl font-bold pb-6")) { "Friction Rate" } div(.class("flex space-x-4")) { - Label("Available Static Pressure") if let availableStaticPressure { + Label("Available Static Pressure") Number(availableStaticPressure, digits: 2) .attributes(.class("badge badge-lg badge-outline font-bold ms-4")) } diff --git a/Sources/ViewController/Views/Project/ProjectView.swift b/Sources/ViewController/Views/Project/ProjectView.swift index a39ef5a..083f9e6 100644 --- a/Sources/ViewController/Views/Project/ProjectView.swift +++ b/Sources/ViewController/Views/Project/ProjectView.swift @@ -68,6 +68,7 @@ struct ProjectView: HTML, Sendable { ) case .ductSizing: try await DuctSizingView( + projectID: projectID, rooms: manualD.calculate( rooms: database.rooms.fetch(projectID), designFrictionRateResult: database.designFrictionRate(projectID: projectID),