From f2c79ad56f7940c26eecadef18f073229c28a758 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 6 Feb 2026 12:11:01 -0500 Subject: [PATCH] WIP: Adds database field to delegate airflow to another room, adds select to room form. --- Sources/CSVParser/Internal/Room+parsing.swift | 8 +++ Sources/DatabaseClient/Internal/Rooms.swift | 8 +++ Sources/ManualDCore/Room.swift | 18 +++++- Sources/ManualDCore/Routes/ViewRoute.swift | 4 +- Sources/Styleguide/Select.swift | 61 +++++++++++++++++++ Sources/ViewController/Views/MainPage.swift | 1 + .../ViewController/Views/Rooms/RoomForm.swift | 27 +++++++- .../Views/Rooms/RoomsView.swift | 14 ++++- 8 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 Sources/Styleguide/Select.swift diff --git a/Sources/CSVParser/Internal/Room+parsing.swift b/Sources/CSVParser/Internal/Room+parsing.swift index 6a52db9..b5b65be 100644 --- a/Sources/CSVParser/Internal/Room+parsing.swift +++ b/Sources/CSVParser/Internal/Room+parsing.swift @@ -30,6 +30,10 @@ enum RoomRowType { struct RoomCreateParser: ParserPrinter { + // FIX: The delegated to field won't work here, as we potentially have not created + // the room yet, so we will need an intermediate representation for the csv data + // that uses a room's name or disregard and require user to delegate airflow in + // the ui. var body: some ParserPrinter { ParsePrint { Prefix { $0 != UInt8(ascii: ",") }.map(.string) @@ -45,6 +49,10 @@ struct RoomCreateParser: ParserPrinter { } ",".utf8 Int.parser() + ",".utf8 + Optionally { + Room.ID.parser() + } } .map(.memberwise(Room.Create.init)) } diff --git a/Sources/DatabaseClient/Internal/Rooms.swift b/Sources/DatabaseClient/Internal/Rooms.swift index d7d97e2..3602899 100644 --- a/Sources/DatabaseClient/Internal/Rooms.swift +++ b/Sources/DatabaseClient/Internal/Rooms.swift @@ -89,6 +89,7 @@ extension Room.Create { heatingLoad: heatingLoad, coolingLoad: coolingLoad, registerCount: registerCount, + delegetedToID: delegatedTo, projectID: projectID ) } @@ -105,6 +106,7 @@ extension Room { .field("heatingLoad", .double, .required) .field("coolingLoad", .dictionary, .required) .field("registerCount", .int8, .required) + .field("delegatedToID", .uuid, .references(RoomModel.schema, "id")) .field("rectangularSizes", .array) .field("createdAt", .datetime) .field("updatedAt", .datetime) @@ -140,6 +142,9 @@ final class RoomModel: Model, @unchecked Sendable, Validatable { @Field(key: "registerCount") var registerCount: Int + @OptionalParent(key: "delegatedToID") + var room: RoomModel? + @Field(key: "rectangularSizes") var rectangularSizes: [Room.RectangularSize]? @@ -160,6 +165,7 @@ final class RoomModel: Model, @unchecked Sendable, Validatable { heatingLoad: Double, coolingLoad: Room.CoolingLoad, registerCount: Int, + delegetedToID: UUID? = nil, rectangularSizes: [Room.RectangularSize]? = nil, createdAt: Date? = nil, updatedAt: Date? = nil, @@ -170,6 +176,7 @@ final class RoomModel: Model, @unchecked Sendable, Validatable { self.heatingLoad = heatingLoad self.coolingLoad = coolingLoad self.registerCount = registerCount + $room.id = delegetedToID self.rectangularSizes = rectangularSizes self.createdAt = createdAt self.updatedAt = updatedAt @@ -184,6 +191,7 @@ final class RoomModel: Model, @unchecked Sendable, Validatable { heatingLoad: heatingLoad, coolingLoad: coolingLoad, registerCount: registerCount, + delegatedTo: $room.id, rectangularSizes: rectangularSizes, createdAt: createdAt!, updatedAt: updatedAt! diff --git a/Sources/ManualDCore/Room.swift b/Sources/ManualDCore/Room.swift index 4567af7..4f7eb50 100644 --- a/Sources/ManualDCore/Room.swift +++ b/Sources/ManualDCore/Room.swift @@ -7,6 +7,7 @@ import Foundation /// room, the number of registers in the room, and any rectangular /// duct size calculations stored for the room. public struct Room: Codable, Equatable, Identifiable, Sendable { + /// The unique id of the room. public let id: UUID @@ -24,6 +25,10 @@ public struct Room: Codable, Equatable, Identifiable, Sendable { /// The number of registers for the room. public let registerCount: Int + + /// An optional room that the airflow is delegated to. + public let delegatedTo: Room.ID? + /// The rectangular duct size calculations for a room. /// /// **NOTE:** These are optionally set after the round sizes have been calculate @@ -43,6 +48,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable { heatingLoad: Double, coolingLoad: CoolingLoad, registerCount: Int = 1, + delegatedTo: Room.ID? = nil, rectangularSizes: [RectangularSize]? = nil, createdAt: Date, updatedAt: Date @@ -53,6 +59,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable { self.heatingLoad = heatingLoad self.coolingLoad = coolingLoad self.registerCount = registerCount + self.delegatedTo = delegatedTo self.rectangularSizes = rectangularSizes self.createdAt = createdAt self.updatedAt = updatedAt @@ -98,15 +105,22 @@ extension Room { public struct Create: Codable, Equatable, Sendable { /// A unique name for the room in the project. public let name: String + /// The heating load required for the room (from Manual-J). public let heatingLoad: Double + /// The total cooling load required for the room (from Manual-J). public let coolingTotal: Double? + /// An optional sensible cooling load for the room. public let coolingSensible: Double? + /// The number of registers for the room. public let registerCount: Int + /// An optional room that this room delegates it's airflow to. + public let delegatedTo: Room.ID? + public var coolingLoad: Room.CoolingLoad { .init(total: coolingTotal, sensible: coolingSensible) } @@ -116,13 +130,15 @@ extension Room { heatingLoad: Double, coolingTotal: Double? = nil, coolingSensible: Double? = nil, - registerCount: Int = 1 + registerCount: Int = 1, + delegatedTo: Room.ID? = nil ) { self.name = name self.heatingLoad = heatingLoad self.coolingTotal = coolingTotal self.coolingSensible = coolingSensible self.registerCount = registerCount + self.delegatedTo = delegatedTo } } diff --git a/Sources/ManualDCore/Routes/ViewRoute.swift b/Sources/ManualDCore/Routes/ViewRoute.swift index 11eb2d6..6b303c1 100644 --- a/Sources/ManualDCore/Routes/ViewRoute.swift +++ b/Sources/ManualDCore/Routes/ViewRoute.swift @@ -233,7 +233,6 @@ extension SiteRoute.View.ProjectRoute { Method.post Body { FormData { - // Field("projectID") { Project.ID.parser() } Field("name", .string) Field("heatingLoad") { Double.parser() } Optionally { @@ -243,6 +242,9 @@ extension SiteRoute.View.ProjectRoute { Field("coolingSensible") { Double.parser() } } Field("registerCount") { Digits() } + Optionally { + Field("delegatedTo") { Room.ID.parser() } + } } .map(.memberwise(Room.Create.init)) } diff --git a/Sources/Styleguide/Select.swift b/Sources/Styleguide/Select.swift new file mode 100644 index 0000000..4728a58 --- /dev/null +++ b/Sources/Styleguide/Select.swift @@ -0,0 +1,61 @@ +import Elementary +import Foundation + +/// NOTE: This does not have the 'select' class added to it, because it's generally +/// added to the label of the field. +public struct Select: HTML where Label: HTML { + + let label: @Sendable (Element) -> Label + let value: @Sendable (Element) -> String + let selected: @Sendable (Element) -> Bool + let items: [Element] + let placeholder: String? + + public init( + _ items: [Element], + placeholder: String? = nil, + value: @escaping @Sendable (Element) -> String, + selected: @escaping @Sendable (Element) -> Bool = { _ in false }, + @HTMLBuilder label: @escaping @Sendable (Element) -> Label + ) { + self.label = label + self.items = items + self.placeholder = placeholder + self.selected = selected + self.value = value + } + + public var body: some HTML { + select { + if let placeholder { + option(.selected, .disabled) { placeholder } + } + for item in items { + option(.value(value(item))) { label(item) } + .attributes(.selected, when: selected(item)) + } + } + } +} + +extension Select: Sendable where Element: Sendable, Label: Sendable {} + +extension Select where Element: Identifiable, Element.ID == UUID, Element: Sendable { + + public init( + _ items: [Element], + placeholder: String? = nil, + selected: @escaping @Sendable (Element) -> Bool = { _ in false }, + @HTMLBuilder label: @escaping @Sendable (Element) -> Label + ) { + self.init( + items, + placeholder: placeholder, + value: { $0.id.uuidString }, + selected: selected, + label: label + ) + + } + +} diff --git a/Sources/ViewController/Views/MainPage.swift b/Sources/ViewController/Views/MainPage.swift index 933a1ff..fef281b 100644 --- a/Sources/ViewController/Views/MainPage.swift +++ b/Sources/ViewController/Views/MainPage.swift @@ -108,6 +108,7 @@ public struct MainPage: SendableHTMLDocument where Inner: Sendable } } .attributes(.data("theme", value: theme?.rawValue ?? "default"), when: theme != nil) + } } diff --git a/Sources/ViewController/Views/Rooms/RoomForm.swift b/Sources/ViewController/Views/Rooms/RoomForm.swift index 3a6496f..5d80a27 100644 --- a/Sources/ViewController/Views/Rooms/RoomForm.swift +++ b/Sources/ViewController/Views/Rooms/RoomForm.swift @@ -5,7 +5,6 @@ import Foundation import ManualDCore import Styleguide -// TODO: Need to hold the project ID in hidden input field. struct RoomForm: HTML, Sendable { static func id(_ room: Room? = nil) -> String { @@ -17,14 +16,17 @@ struct RoomForm: HTML, Sendable { let dismiss: Bool let projectID: Project.ID let room: Room? + let rooms: [Room] init( dismiss: Bool, projectID: Project.ID, + rooms: [Room], room: Room? = nil ) { self.dismiss = dismiss self.projectID = projectID + self.rooms = rooms self.room = room } @@ -38,6 +40,7 @@ struct RoomForm: HTML, Sendable { var body: some HTML { ModalForm(id: Self.id(room), dismiss: dismiss) { h1(.class("text-3xl font-bold pb-6")) { "Room" } + form( .class("grid grid-cols-1 gap-4"), room == nil @@ -97,12 +100,32 @@ struct RoomForm: HTML, Sendable { .type(.number), .min("1"), .required, - .value(room?.registerCount ?? 1) + .value(room?.registerCount ?? 1), + .id("registerCount") ) + label(.class("select w-full"), .id("delegateToSelect")) { + span(.class("label")) { "Room" } + Select(rooms, placeholder: "Delegate Airflow") { + $0.name + } + .attributes(.name("delegatedTo")) + } SubmitButton() .attributes(.class("btn-block")) } } + + script { + """ + function myClick() { + console.log('clicked'); + const simple = document.getElementById('simple'); + console.log(simple.style.display); + simple.style.display = 'block'; + console.log(simple.style.display); + } + """ + } } } diff --git a/Sources/ViewController/Views/Rooms/RoomsView.swift b/Sources/ViewController/Views/Rooms/RoomsView.swift index b4b6d5b..b9dadcf 100644 --- a/Sources/ViewController/Views/Rooms/RoomsView.swift +++ b/Sources/ViewController/Views/Rooms/RoomsView.swift @@ -17,6 +17,11 @@ struct RoomsView: HTML, Sendable { var body: some HTML { div(.class("flex w-full flex-col")) { + input(.type(.checkbox), .name("delegateToCheckbox"), .on(.click, "showElement('simple');")) + div(.style("display: none;"), .id("simple"), .class("hidden")) { + "This is hidden" + } + PageTitleRow { div(.class("flex grid grid-cols-3 w-full gap-y-4")) { @@ -129,17 +134,18 @@ struct RoomsView: HTML, Sendable { } tbody { for room in rooms { - RoomRow(room: room, shr: sensibleHeatRatio) + RoomRow(room: room, shr: sensibleHeatRatio, rooms: rooms) } } } - RoomForm(dismiss: true, projectID: projectID, room: nil) + RoomForm(dismiss: true, projectID: projectID, rooms: rooms, room: nil) UploadCSVForm(dismiss: true) } } public struct RoomRow: HTML, Sendable { + let rooms: [Room] let room: Room let shr: Double @@ -151,9 +157,10 @@ struct RoomsView: HTML, Sendable { // return value } - init(room: Room, shr: Double?) { + init(room: Room, shr: Double?, rooms: [Room]) { self.room = room self.shr = shr ?? 1.0 + self.rooms = rooms } public var body: some HTML { @@ -208,6 +215,7 @@ struct RoomsView: HTML, Sendable { RoomForm( dismiss: true, projectID: room.projectID, + rooms: rooms, room: room ) }