diff --git a/Public/css/output.css b/Public/css/output.css index d958a0f..36ea1fd 100644 --- a/Public/css/output.css +++ b/Public/css/output.css @@ -4801,9 +4801,15 @@ } } } + .col-span-1 { + grid-column: span 1 / span 1; + } .col-span-2 { grid-column: span 2 / span 2; } + .col-span-5 { + grid-column: span 5 / span 5; + } .timeline-end { @layer daisyui.l1.l2.l3 { grid-column-start: 1; @@ -5368,6 +5374,9 @@ .-my-2 { margin-block: calc(var(--spacing) * -2); } + .my-1 { + margin-block: calc(var(--spacing) * 1); + } .my-1\.5 { margin-block: calc(var(--spacing) * 1.5); } @@ -5586,6 +5595,9 @@ .me-4 { margin-inline-end: calc(var(--spacing) * 4); } + .me-6 { + margin-inline-end: calc(var(--spacing) * 6); + } .me-8 { margin-inline-end: calc(var(--spacing) * 8); } @@ -5628,6 +5640,12 @@ } } } + .-mt-2 { + margin-top: calc(var(--spacing) * -2); + } + .mt-1 { + margin-top: calc(var(--spacing) * 1); + } .mt-4 { margin-top: calc(var(--spacing) * 4); } @@ -6580,6 +6598,12 @@ .w-full { width: 100%; } + .max-w-1 { + max-width: calc(var(--spacing) * 1); + } + .max-w-1\/3 { + max-width: calc(1/3 * 100%); + } .max-w-\[300px\] { max-width: 300px; } @@ -6824,6 +6848,23 @@ .gap-4 { gap: calc(var(--spacing) * 4); } + .gap-6 { + gap: calc(var(--spacing) * 6); + } + .space-y-1 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-2 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); + } + } .space-y-4 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -6838,6 +6879,9 @@ margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); } } + .gap-x-4 { + column-gap: calc(var(--spacing) * 4); + } .space-x-2 { :where(& > :not(:last-child)) { --tw-space-x-reverse: 0; @@ -6859,6 +6903,9 @@ margin-inline-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse))); } } + .gap-y-6 { + row-gap: calc(var(--spacing) * 6); + } .overflow-x-auto { overflow-x: auto; } @@ -7840,12 +7887,18 @@ } } } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } .px-3 { padding-inline: calc(var(--spacing) * 3); } .px-4 { padding-inline: calc(var(--spacing) * 4); } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } .py-1\.5 { padding-block: calc(var(--spacing) * 1.5); } diff --git a/Sources/DatabaseClient/Trunk.swift b/Sources/DatabaseClient/TrunkSizes.swift similarity index 73% rename from Sources/DatabaseClient/Trunk.swift rename to Sources/DatabaseClient/TrunkSizes.swift index 10a01a5..d96b4d5 100644 --- a/Sources/DatabaseClient/Trunk.swift +++ b/Sources/DatabaseClient/TrunkSizes.swift @@ -34,10 +34,11 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey { let model = try TrunkRoomModel( trunkID: trunk.requireID(), roomID: room.requireID(), - registers: registers + registers: registers, + type: request.type ) try await model.save(on: database) - try roomProxies.append(model.toDTO()) + try await roomProxies.append(model.toDTO(on: database)) } return try .init( @@ -54,16 +55,33 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey { try await model.delete(on: database) }, fetch: { projectID in - try await TrunkModel.query(on: database) - .with(\.$rooms) + let models = try await TrunkModel.query(on: database) .with(\.$project) + .with(\.$rooms) .filter(\.$project.$id == projectID) .all() - .map { try $0.toDTO() } + + return try await withThrowingTaskGroup(of: DuctSizing.TrunkSize.self) { group in + for model in models { + group.addTask { + try await model.toDTO(on: database) + } + } + + return try await group.reduce(into: [DuctSizing.TrunkSize]()) { + $0.append($1) + } + } + + // return try await models.map { + // try await $0.toDTO(on: database) + // } }, get: { id in - try await TrunkModel.find(id, on: database) - .map { try $0.toDTO() } + guard let model = try await TrunkModel.find(id, on: database) else { + return nil + } + return try await model.toDTO(on: database) } ) } @@ -110,12 +128,14 @@ extension DuctSizing.TrunkSize { try await database.schema(TrunkRoomModel.schema) .id() .field("registers", .array(of: .int), .required) + .field("type", .string, .required) .field( "trunkID", .uuid, .required, .references(TrunkModel.schema, "id", onDelete: .cascade) ) .field( "roomID", .uuid, .required, .references(RoomModel.schema, "id", onDelete: .cascade) ) + .unique(on: "trunkID", "roomID", "type") .create() } @@ -143,22 +163,30 @@ final class TrunkRoomModel: Model, @unchecked Sendable { @Field(key: "registers") var registers: [Int] + @Field(key: "type") + var type: String + init() {} init( id: UUID? = nil, trunkID: TrunkModel.IDValue, roomID: RoomModel.IDValue, - registers: [Int] + registers: [Int], + type: DuctSizing.TrunkSize.TrunkType ) { self.id = id $trunk.id = trunkID $room.id = roomID self.registers = registers + self.type = type.rawValue } - func toDTO() throws -> DuctSizing.TrunkSize.RoomProxy { - .init( + func toDTO(on database: any Database) async throws -> DuctSizing.TrunkSize.RoomProxy { + guard let room = try await RoomModel.find($room.id, on: database) else { + throw NotFoundError() + } + return .init( room: try room.toDTO(), registers: registers ) @@ -199,12 +227,25 @@ final class TrunkModel: Model, @unchecked Sendable { self.type = type.rawValue } - func toDTO() throws -> DuctSizing.TrunkSize { - try .init( + func toDTO(on database: any Database) async throws -> DuctSizing.TrunkSize { + let rooms = try await withThrowingTaskGroup(of: DuctSizing.TrunkSize.RoomProxy.self) { group in + for room in self.rooms { + group.addTask { + try await room.toDTO(on: database) + } + } + + return try await group.reduce(into: [DuctSizing.TrunkSize.RoomProxy]()) { + $0.append($1) + } + + } + + return try .init( id: requireID(), projectID: $project.id, type: .init(rawValue: type)!, - rooms: rooms.map { try $0.toDTO() }, + rooms: rooms, height: height ) diff --git a/Sources/ManualDClient/Helpers.swift b/Sources/ManualDClient/Helpers.swift index 71f048e..6b78ee2 100644 --- a/Sources/ManualDClient/Helpers.swift +++ b/Sources/ManualDClient/Helpers.swift @@ -14,6 +14,28 @@ extension Room { } } +extension DuctSizing.TrunkSize.RoomProxy { + + var totalHeatingLoad: Double { + room.heatingLoadPerRegister * Double(registers.count) + } + + func totalCoolingSensible(projectSHR: Double) -> Double { + room.coolingSensiblePerRegister(projectSHR: projectSHR) * Double(registers.count) + } +} + +extension DuctSizing.TrunkSize { + + var totalHeatingLoad: Double { + rooms.reduce(into: 0) { $0 += $1.totalHeatingLoad } + } + + func totalCoolingSensible(projectSHR: Double) -> Double { + rooms.reduce(into: 0) { $0 += $1.totalCoolingSensible(projectSHR: projectSHR) } + } +} + extension ComponentPressureLosses { var totalLosses: Double { values.reduce(0) { $0 + $1 } } } diff --git a/Sources/ManualDClient/ManualDClient.swift b/Sources/ManualDClient/ManualDClient.swift index e7ee433..271e64a 100644 --- a/Sources/ManualDClient/ManualDClient.swift +++ b/Sources/ManualDClient/ManualDClient.swift @@ -12,6 +12,29 @@ public struct ManualDClient: Sendable { @Sendable (EquivalentRectangularDuctRequest) async throws -> EquivalentRectangularDuctResponse public func calculateSizes( + rooms: [Room], + trunks: [DuctSizing.TrunkSize], + equipmentInfo: EquipmentInfo, + maxSupplyLength: EffectiveLength, + maxReturnLength: EffectiveLength, + designFrictionRate: Double, + projectSHR: Double, + logger: Logger? = nil + ) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) { + try await ( + calculateSizes( + rooms: rooms, equipmentInfo: equipmentInfo, + maxSupplyLength: maxSupplyLength, maxReturnLength: maxReturnLength, + designFrictionRate: designFrictionRate, projectSHR: projectSHR + ), + calculateSizes( + rooms: rooms, trunks: trunks, equipmentInfo: equipmentInfo, + maxSupplyLength: maxSupplyLength, maxReturnLength: maxReturnLength, + designFrictionRate: designFrictionRate, projectSHR: projectSHR) + ) + } + + func calculateSizes( rooms: [Room], equipmentInfo: EquipmentInfo, maxSupplyLength: EffectiveLength, @@ -56,6 +79,7 @@ public struct ManualDClient: Sendable { registerID: "SR-\(registerIDCount)", roomID: room.id, roomName: "\(room.name)-\(n)", + roomRegister: n, heatingLoad: heatingLoad, coolingLoad: coolingLoad, heatingCFM: heatingCFM, @@ -76,6 +100,56 @@ public struct ManualDClient: Sendable { return retval } + func calculateSizes( + rooms: [Room], + trunks: [DuctSizing.TrunkSize], + equipmentInfo: EquipmentInfo, + maxSupplyLength: EffectiveLength, + maxReturnLength: EffectiveLength, + designFrictionRate: Double, + projectSHR: Double, + logger: Logger? = nil + ) async throws -> [DuctSizing.TrunkContainer] { + + var retval = [DuctSizing.TrunkContainer]() + let totalHeatingLoad = rooms.totalHeatingLoad + let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR) + + for trunk in trunks { + let heatingLoad = trunk.totalHeatingLoad + let coolingLoad = trunk.totalCoolingSensible(projectSHR: projectSHR) + let heatingPercent = heatingLoad / totalHeatingLoad + let coolingPercent = coolingLoad / totalCoolingSensible + let heatingCFM = heatingPercent * Double(equipmentInfo.heatingCFM) + let coolingCFM = coolingPercent * Double(equipmentInfo.coolingCFM) + let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM) + let sizes = try await self.ductSize( + .init(designCFM: Int(designCFM.value), frictionRate: designFrictionRate) + ) + var width: Int? = nil + if let height = trunk.height { + let rectangularSize = try await self.equivalentRectangularDuct( + .init(round: sizes.finalSize, height: height) + ) + width = rectangularSize.width + } + + retval.append( + .init( + trunk: trunk, + ductSize: .init( + designCFM: designCFM, + roundSize: sizes.ductulatorSize, + finalSize: sizes.finalSize, + velocity: sizes.velocity, + flexSize: sizes.flexSize) + ) + ) + } + + return retval + } + } extension ManualDClient: TestDependencyKey { diff --git a/Sources/ManualDCore/DuctSizing.swift b/Sources/ManualDCore/DuctSizing.swift index 4f88632..e165943 100644 --- a/Sources/ManualDCore/DuctSizing.swift +++ b/Sources/ManualDCore/DuctSizing.swift @@ -21,11 +21,41 @@ public enum DuctSizing { } + public struct SizeContainer: Codable, Equatable, Sendable { + + public let designCFM: DesignCFM + public let roundSize: Double + public let finalSize: Int + public let velocity: Int + public let flexSize: Int + public let height: Int? + public let width: Int? + + public init( + designCFM: DuctSizing.DesignCFM, + roundSize: Double, + finalSize: Int, + velocity: Int, + flexSize: Int, + height: Int? = nil, + width: Int? = nil + ) { + self.designCFM = designCFM + self.roundSize = roundSize + self.finalSize = finalSize + self.velocity = velocity + self.flexSize = flexSize + self.height = height + self.width = width + } + } + public struct RoomContainer: Codable, Equatable, Sendable { public let registerID: String public let roomID: Room.ID public let roomName: String + public let roomRegister: Int public let heatingLoad: Double public let coolingLoad: Double public let heatingCFM: Double @@ -42,6 +72,7 @@ public enum DuctSizing { registerID: String, roomID: Room.ID, roomName: String, + roomRegister: Int, heatingLoad: Double, coolingLoad: Double, heatingCFM: Double, @@ -57,6 +88,7 @@ public enum DuctSizing { self.registerID = registerID self.roomID = roomID self.roomName = roomName + self.roomRegister = roomRegister self.heatingLoad = heatingLoad self.coolingLoad = coolingLoad self.heatingCFM = heatingCFM @@ -94,6 +126,21 @@ public enum DuctSizing { extension DuctSizing { + public struct TrunkContainer: Codable, Equatable, Identifiable, Sendable { + public var id: TrunkSize.ID { trunk.id } + + public let trunk: TrunkSize + public let ductSize: SizeContainer + + public init( + trunk: TrunkSize, + ductSize: SizeContainer + ) { + self.trunk = trunk + self.ductSize = ductSize + } + } + public struct TrunkSize: Codable, Equatable, Identifiable, Sendable { public let id: UUID @@ -145,16 +192,18 @@ extension DuctSizing.TrunkSize { public var id: Room.ID { room.id } public let room: Room - public let registers: [Int]? + public let registers: [Int] - public init(room: Room, registers: [Int]? = nil) { + public init(room: Room, registers: [Int]) { self.room = room self.registers = registers } } - public enum TrunkType: String, Codable, Equatable, Sendable { + public enum TrunkType: String, CaseIterable, Codable, Equatable, Sendable { case `return` case supply + + public static let allCases = [Self.supply, .return] } } diff --git a/Sources/ManualDCore/Routes/ViewRoute.swift b/Sources/ManualDCore/Routes/ViewRoute.swift index 07e3b50..1117ddf 100644 --- a/Sources/ManualDCore/Routes/ViewRoute.swift +++ b/Sources/ManualDCore/Routes/ViewRoute.swift @@ -609,7 +609,9 @@ extension SiteRoute.View.ProjectRoute { case index case deleteRectangularSize(Room.ID, DuctSizing.RectangularDuct.ID) case roomRectangularForm(Room.ID, RoomRectangularForm) + case trunk(TrunkRoute) + public static let roomPath = "room" static let rootPath = "duct-sizing" static let router = OneOf { @@ -620,7 +622,7 @@ extension SiteRoute.View.ProjectRoute { Route(.case(Self.deleteRectangularSize)) { Path { rootPath - "room" + roomPath Room.ID.parser() } Method.delete @@ -631,7 +633,7 @@ extension SiteRoute.View.ProjectRoute { Route(.case(Self.roomRectangularForm)) { Path { rootPath - "room" + roomPath Room.ID.parser() } Method.post @@ -646,6 +648,67 @@ extension SiteRoute.View.ProjectRoute { .map(.memberwise(RoomRectangularForm.init)) } } + Route(.case(Self.trunk)) { + Path { rootPath } + TrunkRoute.router + } + } + + public enum TrunkRoute: Equatable, Sendable { + case delete(DuctSizing.TrunkSize.ID) + case submit(TrunkSizeForm) + case update(DuctSizing.TrunkSize.ID, TrunkSizeForm) + + public static let rootPath = "trunk" + + static let router = OneOf { + Route(.case(Self.delete)) { + Path { + rootPath + DuctSizing.TrunkSize.ID.parser() + } + Method.delete + } + Route(.case(Self.submit)) { + Path { + rootPath + } + Method.post + Body { + FormData { + Field("projectID") { Project.ID.parser() } + Field("type") { DuctSizing.TrunkSize.TrunkType.parser() } + Optionally { + Field("height") { Int.parser() } + } + Many { + Field("rooms", .string) + } + } + .map(.memberwise(TrunkSizeForm.init)) + } + } + Route(.case(Self.update)) { + Path { + rootPath + DuctSizing.TrunkSize.ID.parser() + } + Method.patch + Body { + FormData { + Field("projectID") { Project.ID.parser() } + Field("type") { DuctSizing.TrunkSize.TrunkType.parser() } + Optionally { + Field("height") { Int.parser() } + } + Many { + Field("rooms", .string) + } + } + .map(.memberwise(TrunkSizeForm.init)) + } + } + } } public struct RoomRectangularForm: Equatable, Sendable { @@ -653,6 +716,13 @@ extension SiteRoute.View.ProjectRoute { public let register: Int public let height: Int } + + public struct TrunkSizeForm: Equatable, Sendable { + public let projectID: Project.ID + public let type: DuctSizing.TrunkSize.TrunkType + public let height: Int? + public let rooms: [String] + } } } diff --git a/Sources/ViewController/Extensions/DatabaseExtensions.swift b/Sources/ViewController/Extensions/DatabaseExtensions.swift index 1546f9a..872f981 100644 --- a/Sources/ViewController/Extensions/DatabaseExtensions.swift +++ b/Sources/ViewController/Extensions/DatabaseExtensions.swift @@ -26,11 +26,14 @@ extension DatabaseClient.Projects { extension DatabaseClient { - func calculateDuctSizes(projectID: Project.ID) async throws -> [DuctSizing.RoomContainer] { + func calculateDuctSizes( + projectID: Project.ID + ) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) { @Dependency(\.manualD) var manualD return try await manualD.calculate( rooms: rooms.fetch(projectID), + trunks: trunkSizes.fetch(projectID), designFrictionRateResult: designFrictionRate(projectID: projectID), projectSHR: projects.getSensibleHeatRatio(projectID) ) diff --git a/Sources/ViewController/Extensions/ManualDClient+extensions.swift b/Sources/ViewController/Extensions/ManualDClient+extensions.swift index 821916c..e554b4b 100644 --- a/Sources/ViewController/Extensions/ManualDClient+extensions.swift +++ b/Sources/ViewController/Extensions/ManualDClient+extensions.swift @@ -6,20 +6,22 @@ extension ManualDClient { func calculate( rooms: [Room], + trunks: [DuctSizing.TrunkSize], designFrictionRateResult: (EquipmentInfo, EffectiveLength.MaxContainer, Double)?, projectSHR: Double?, logger: Logger? = nil - ) async throws -> [DuctSizing.RoomContainer] { - guard let designFrictionRateResult else { return [] } + ) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) { + guard let designFrictionRateResult else { return ([], []) } let equipmentInfo = designFrictionRateResult.0 let effectiveLengths = designFrictionRateResult.1 let designFrictionRate = designFrictionRateResult.2 - guard let maxSupply = effectiveLengths.supply else { return [] } - guard let maxReturn = effectiveLengths.return else { return [] } + guard let maxSupply = effectiveLengths.supply else { return ([], []) } + guard let maxReturn = effectiveLengths.return else { return ([], []) } let ductRooms = try await self.calculateSizes( rooms: rooms, + trunks: trunks, equipmentInfo: equipmentInfo, maxSupplyLength: maxSupply, maxReturnLength: maxReturn, diff --git a/Sources/ViewController/Extensions/TrunkSizeForm+extensions.swift b/Sources/ViewController/Extensions/TrunkSizeForm+extensions.swift new file mode 100644 index 0000000..92b3ef7 --- /dev/null +++ b/Sources/ViewController/Extensions/TrunkSizeForm+extensions.swift @@ -0,0 +1,44 @@ +import Foundation +import Logging +import ManualDCore + +extension SiteRoute.View.ProjectRoute.DuctSizingRoute.TrunkSizeForm { + + func toCreate(logger: Logger? = nil) throws -> DuctSizing.TrunkSize.Create { + try .init( + projectID: projectID, + type: type, + rooms: makeRooms(logger: logger), + height: height + ) + } + + func makeRooms(logger: Logger?) throws -> [Room.ID: [Int]] { + var retval = [Room.ID: [Int]]() + for room in rooms { + let split = room.split(separator: "_") + guard let idString = split.first, + let id = UUID(uuidString: String(idString)) + else { + logger?.error("Could not parse id from: \(room)") + throw RoomError() + } + guard let registerString = split.last, + let register = Int(registerString) + else { + logger?.error("Could not register number from: \(room)") + throw RoomError() + } + if var currRegisters = retval[id] { + currRegisters.append(register) + retval[id] = currRegisters + } else { + retval[id] = [register] + } + + } + return retval + } +} + +struct RoomError: Error {} diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index b9b1c44..3461af6 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -542,6 +542,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute { return await ResultView { let room = try await database.rooms.deleteRectangularSize(roomID, rectangularSizeID) return try await database.calculateDuctSizes(projectID: projectID) + .rooms .filter({ $0.roomID == room.id }) .first! } onSuccess: { container in @@ -559,11 +560,30 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute { ) ) return try await database.calculateDuctSizes(projectID: projectID) + .rooms .filter({ $0.roomID == room.id }) .first! } onSuccess: { container in DuctSizingView.RoomRow(projectID: projectID, room: container) } + + case .trunk(let route): + switch route { + case .delete(let id): + return await ResultView { + try await database.trunkSizes.delete(id) + } + case .submit(let form): + return await view(on: request, projectID: projectID) { + _ = try await database.trunkSizes.create( + form.toCreate(logger: request.logger) + ) + } + + case .update(let id, let form): + // FIX: + fatalError() + } } } @@ -581,9 +601,9 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute { try await database.projects.getCompletedSteps(projectID), try await database.calculateDuctSizes(projectID: projectID) ) - } onSuccess: { (steps, rooms) in + } onSuccess: { (steps, ducts) in ProjectView(projectID: projectID, activeTab: .ductSizing, completedSteps: steps) { - DuctSizingView(rooms: rooms) + DuctSizingView(rooms: ducts.rooms, trunks: ducts.trunks) } } } diff --git a/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift b/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift index 4e5e5b9..89b91ff 100644 --- a/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift +++ b/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift @@ -3,14 +3,22 @@ import ElementaryHTMX import ManualDCore import Styleguide -// TODO: Add error text if prior steps are not completed. +// TODO: Add trunk size table. struct DuctSizingView: HTML, Sendable { @Environment(ProjectViewValue.$projectID) var projectID - // let projectID: Project.ID let rooms: [DuctSizing.RoomContainer] + let trunks: [DuctSizing.TrunkContainer] + + var supplyTrunks: [DuctSizing.TrunkContainer] { + trunks.filter { $0.trunk.type == .supply } + } + + var returnTrunks: [DuctSizing.TrunkContainer] { + trunks.filter { $0.trunk.type == .return } + } var body: some HTML { div(.class("space-y-4")) { @@ -22,6 +30,31 @@ struct DuctSizingView: HTML, Sendable { } else { RoomsTable(projectID: projectID, rooms: rooms) } + + Row { + h2(.class("text-2xl font-bold")) { "Trunk Sizes" } + + PlusButton() + .attributes( + .class("me-6"), + .showModal(id: TrunkSizeForm.id()) + ) + } + .attributes(.class("mt-6")) + + div(.class("divider -mt-2")) {} + + if supplyTrunks.count > 0 { + h2(.class("text-lg font-bold text-info")) { "Supply Trunks" } + TrunkTable(trunks: supplyTrunks, rooms: rooms) + } + + if returnTrunks.count > 0 { + h2(.class("text-lg font-bold text-error")) { "Return Trunks" } + TrunkTable(trunks: returnTrunks, rooms: rooms) + } + + TrunkSizeForm(rooms: rooms, dismiss: true) } } @@ -90,8 +123,8 @@ struct DuctSizingView: HTML, Sendable { .attributes(.class("badge-secondary")) } td { - Number(room.flexSize) - .attributes(.class("badge badge-outline badge-primary text-xl font-bold")) + Badge(number: room.flexSize) + .attributes(.class("badge-primary")) } td { if let width = room.rectangularWidth { @@ -141,6 +174,122 @@ struct DuctSizingView: HTML, Sendable { } } } + + struct TrunkTable: HTML, Sendable { + let trunks: [DuctSizing.TrunkContainer] + let rooms: [DuctSizing.RoomContainer] + + var body: some HTML { + div(.class("overflow-x-auto")) { + table(.class("table table-zebra text-lg")) { + thead { + tr(.class("text-lg")) { + th { "Associated Supplies" } + th { "Dsn CFM" } + th { "Round Size" } + th { "Velocity" } + th { "Final Size" } + th { "Flex Size" } + th { "Width" } + th { "Height" } + } + } + tbody { + for trunk in trunks { + tr { + td(.class("space-x-2")) { + // div(.class("flex flex-wrap space-x-2 max-w-1/3")) { + for id in registerIDS(trunk.trunk) { + Badge { id } + } + // } + } + td { + Number(trunk.ductSize.designCFM.value, digits: 0) + } + td { + Number(trunk.ductSize.roundSize, digits: 1) + } + td { + Number(trunk.ductSize.velocity) + } + td { + Badge(number: trunk.ductSize.finalSize) + .attributes(.class("badge-secondary")) + } + td { + Badge(number: trunk.ductSize.flexSize) + .attributes(.class("badge-primary")) + } + td { + if let width = trunk.ductSize.width { + Number(width) + } + + } + td { + + div(.class("flex justify-between items-center space-x-4")) { + div { + if let height = trunk.ductSize.height { + Number(height) + } + } + + div { + div(.class("join")) { + TrashButton() + .attributes(.class("join-item btn-ghost")) + .attributes( + // .hx.delete( + // route: .project( + // .detail( + // projectID, + // .ductSizing( + // .deleteRectangularSize( + // room.roomID, + // room.rectangularSize?.id ?? .init()) + // ) + // ) + // ) + // ), + .hx.target("closest tr"), + .hx.swap(.outerHTML) + // when: room.rectangularSize != nil + ) + + EditButton() + .attributes( + .class("join-item btn-ghost"), + // .showModal(id: RectangularSizeForm.id(room)) + ) + } + } + } + // FIX: Add Trunk form. + } + } + } + } + } + } + } + + func registerIDS(_ trunk: DuctSizing.TrunkSize) -> [String] { + trunk.rooms.reduce(into: []) { array, room in + array = room.registers.reduce(into: array) { array, register in + if let room = + rooms + .first(where: { $0.roomID == room.id && $0.roomRegister == register }) + { + array.append(room.registerID) + } + } + } + .sorted() + } + + } } extension DuctSizing.DesignCFM { diff --git a/Sources/ViewController/Views/DuctSizing/TrunkSizeForm.swift b/Sources/ViewController/Views/DuctSizing/TrunkSizeForm.swift new file mode 100644 index 0000000..c1aafd7 --- /dev/null +++ b/Sources/ViewController/Views/DuctSizing/TrunkSizeForm.swift @@ -0,0 +1,77 @@ +import Elementary +import ElementaryHTMX +import ManualDCore +import Styleguide + +struct TrunkSizeForm: HTML, Sendable { + + static func id() -> String { + "trunkSizeForm" + } + + @Environment(ProjectViewValue.$projectID) var projectID + + let rooms: [DuctSizing.RoomContainer] + let dismiss: Bool + + var route: String { + SiteRoute.View.router + .path(for: .project(.detail(projectID, .ductSizing(.index)))) + .appendingPath(SiteRoute.View.ProjectRoute.DuctSizingRoute.TrunkRoute.rootPath) + } + + var body: some HTML { + ModalForm(id: Self.id(), dismiss: dismiss) { + h1(.class("text-lg font-bold mb-4")) { "Trunk Size" } + form( + .class("space-y-4"), + .hx.post(route), + .hx.target("body"), + .hx.swap(.outerHTML) + ) { + + input(.class("hidden"), .name("projectID"), .value(projectID)) + + div(.class("grid grid-cols-1 md:grid-cols-2 gap-4")) { + label(.class("select w-full")) { + span(.class("label")) { "Type" } + select(.name("type")) { + for type in DuctSizing.TrunkSize.TrunkType.allCases { + option(.value(type.rawValue)) { type.rawValue.capitalized } + } + } + } + + LabeledInput( + "Height", + .type(.text), + .name("height"), + .placeholder("8 (Optional)"), + ) + } + + // Add room select here. + div(.class("grid grid-cols-5 gap-6")) { + h2(.class("label font-bold col-span-5")) { "Associated Supply Runs" } + for room in rooms { + div(.class("flex justify-center items-center col-span-1")) { + div(.class("space-y-1")) { + p(.class("label block")) { room.registerID } + input( + .class("checkbox"), + .type(.checkbox), + .name("rooms"), + .value("\(room.roomID)_\(room.roomRegister)") + ) + } + } + } + } + + SubmitButton() + .attributes(.class("btn-block")) + } + } + } + +} diff --git a/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoForm.swift b/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoForm.swift index 444a835..a5b8e79 100644 --- a/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoForm.swift +++ b/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoForm.swift @@ -33,7 +33,7 @@ struct EquipmentInfoForm: HTML, Sendable { equipmentInfo != nil ? .hx.patch(route) : .hx.post(route), - .hx.target("#equipmentInfo"), + .hx.target("body"), .hx.swap(.outerHTML) ) { input(.class("hidden"), .name("projectID"), .value("\(projectID)"))