diff --git a/Public/css/output.css b/Public/css/output.css index 9dd3149..1d6c50b 100644 --- a/Public/css/output.css +++ b/Public/css/output.css @@ -4230,9 +4230,6 @@ .top-2 { top: calc(var(--spacing) * 2); } - .top-40 { - top: calc(var(--spacing) * 40); - } .right-2 { right: calc(var(--spacing) * 2); } @@ -4293,9 +4290,6 @@ } } } - .left-\[25vw\] { - left: 25vw; - } .join { display: inline-flex; align-items: stretch; @@ -4682,9 +4676,6 @@ } } } - .z-50 { - z-index: 50; - } .tab-content { @layer daisyui.l1.l2.l3 { order: var(--tabcontent-order); @@ -5276,12 +5267,6 @@ } } } - .mx-6 { - margin-inline: calc(var(--spacing) * 6); - } - .mx-10 { - margin-inline: calc(var(--spacing) * 10); - } .file-input-ghost { @layer daisyui.l1.l2 { background-color: transparent; @@ -5571,6 +5556,9 @@ border-width: var(--border, 1px) 0 var(--border, 1px) var(--border, 1px); } } + .me-4 { + margin-inline-end: calc(var(--spacing) * 4); + } .modal-action { @layer daisyui.l1.l2.l3 { margin-top: calc(0.25rem * 6); @@ -6537,21 +6525,12 @@ width: calc(var(--size-selector, 0.25rem) * 4); } } - .w-1 { - width: calc(var(--spacing) * 1); - } - .w-1\/2 { - width: calc(1/2 * 100%); - } .w-\[40px\] { width: 40px; } .w-full { width: 100%; } - .w-xl { - width: var(--container-xl); - } .max-w-\[280px\] { max-width: 280px; } @@ -7200,15 +7179,9 @@ border-color: currentColor; } } - .border-base-300 { - border-color: var(--color-base-300); - } .border-gray-200 { border-color: var(--color-gray-200); } - .border-gray-400 { - border-color: var(--color-gray-400); - } .menu-active { :where(:not(ul, details, .menu-title, .btn))& { @layer daisyui.l1.l2 { @@ -7360,15 +7333,6 @@ } } } - .bg-base-200 { - background-color: var(--color-base-200); - } - .bg-blue-500 { - background-color: var(--color-blue-500); - } - .bg-gray-200 { - background-color: var(--color-gray-200); - } .bg-red-500 { background-color: var(--color-red-500); } @@ -7786,21 +7750,12 @@ .px-6 { padding-inline: calc(var(--spacing) * 6); } - .py-1 { - padding-block: calc(var(--spacing) * 1); - } .py-1\.5 { padding-block: calc(var(--spacing) * 1.5); } .py-2 { padding-block: calc(var(--spacing) * 2); } - .py-4 { - padding-block: calc(var(--spacing) * 4); - } - .py-6 { - padding-block: calc(var(--spacing) * 6); - } .py-10 { padding-block: calc(var(--spacing) * 10); } @@ -7821,8 +7776,8 @@ .pe-2 { padding-inline-end: calc(var(--spacing) * 2); } - .pt-6 { - padding-top: calc(var(--spacing) * 6); + .pe-4 { + padding-inline-end: calc(var(--spacing) * 4); } .pb-4 { padding-bottom: calc(var(--spacing) * 4); @@ -8446,9 +8401,6 @@ .text-gray-400 { color: var(--color-gray-400); } - .text-gray-800 { - color: var(--color-gray-800); - } .text-slate-900 { color: var(--color-slate-900); } @@ -9324,13 +9276,6 @@ border-color: var(--color-red-500); } } - .hover\:bg-blue-600 { - &:hover { - @media (hover: hover) { - background-color: var(--color-blue-600); - } - } - } .hover\:bg-gray-300 { &:hover { @media (hover: hover) { diff --git a/Sources/DatabaseClient/Projects.swift b/Sources/DatabaseClient/Projects.swift index 43aa9ce..b685fde 100644 --- a/Sources/DatabaseClient/Projects.swift +++ b/Sources/DatabaseClient/Projects.swift @@ -10,6 +10,7 @@ extension DatabaseClient { public var create: @Sendable (User.ID, Project.Create) async throws -> Project public var delete: @Sendable (Project.ID) async throws -> Void public var get: @Sendable (Project.ID) async throws -> Project? + public var getSensibleHeatRatio: @Sendable (Project.ID) async throws -> Double? public var fetch: @Sendable (User.ID, PageRequest) async throws -> Page public var update: @Sendable (Project.Update) async throws -> Project } @@ -34,6 +35,12 @@ extension DatabaseClient.Projects: TestDependencyKey { get: { id in try await ProjectModel.find(id, on: database).map { try $0.toDTO() } }, + getSensibleHeatRatio: { id in + guard let model = try await ProjectModel.find(id, on: database) else { + throw NotFoundError() + } + return model.sensibleHeatRatio + }, fetch: { userID, request in try await ProjectModel.query(on: database) .sort(\.$createdAt, .descending) @@ -86,6 +93,14 @@ extension Project.Create { guard !zipCode.isEmpty else { throw ValidationError("Project zipCode should not be empty.") } + if let sensibleHeatRatio { + guard sensibleHeatRatio >= 0 else { + throw ValidationError("Project sensible heat ratio should be greater than 0.") + } + guard sensibleHeatRatio <= 1 else { + throw ValidationError("Project sensible heat ratio should be less than 1.") + } + } } } @@ -117,6 +132,14 @@ extension Project.Update { throw ValidationError("Project zipCode should not be empty.") } } + if let sensibleHeatRatio { + guard sensibleHeatRatio >= 0 else { + throw ValidationError("Project sensible heat ratio should be greater than 0.") + } + guard sensibleHeatRatio <= 1 else { + throw ValidationError("Project sensible heat ratio should be less than 1.") + } + } } } @@ -132,6 +155,7 @@ extension Project { .field("city", .string, .required) .field("state", .string, .required) .field("zipCode", .string, .required) + .field("sensibleHeatRatio", .double) .field("createdAt", .datetime) .field("updatedAt", .datetime) .field("userID", .uuid, .required, .references(UserModel.schema, "id")) @@ -168,6 +192,9 @@ final class ProjectModel: Model, @unchecked Sendable { @Field(key: "zipCode") var zipCode: String + @Field(key: "sensibleHeatRatio") + var sensibleHeatRatio: Double? + @Timestamp(key: "createdAt", on: .create, format: .iso8601) var createdAt: Date? @@ -189,6 +216,7 @@ final class ProjectModel: Model, @unchecked Sendable { city: String, state: String, zipCode: String, + sensibleHeatRatio: Double? = nil, userID: User.ID, createdAt: Date? = nil, updatedAt: Date? = nil @@ -199,6 +227,7 @@ final class ProjectModel: Model, @unchecked Sendable { self.city = city self.state = state self.zipCode = zipCode + self.sensibleHeatRatio = sensibleHeatRatio $user.id = userID self.createdAt = createdAt self.updatedAt = updatedAt @@ -212,6 +241,7 @@ final class ProjectModel: Model, @unchecked Sendable { city: city, state: state, zipCode: zipCode, + sensibleHeatRatio: sensibleHeatRatio, createdAt: createdAt!, updatedAt: updatedAt! ) @@ -239,6 +269,12 @@ final class ProjectModel: Model, @unchecked Sendable { hasUpdates = true self.zipCode = zipCode } + if let sensibleHeatRatio = updates.sensibleHeatRatio, + sensibleHeatRatio != self.sensibleHeatRatio + { + hasUpdates = true + self.sensibleHeatRatio = sensibleHeatRatio + } return hasUpdates } } diff --git a/Sources/ManualDCore/Project.swift b/Sources/ManualDCore/Project.swift index d3b11c4..06ec67e 100644 --- a/Sources/ManualDCore/Project.swift +++ b/Sources/ManualDCore/Project.swift @@ -9,6 +9,7 @@ public struct Project: Codable, Equatable, Identifiable, Sendable { public let city: String public let state: String public let zipCode: String + public let sensibleHeatRatio: Double? public let createdAt: Date public let updatedAt: Date @@ -19,6 +20,7 @@ public struct Project: Codable, Equatable, Identifiable, Sendable { city: String, state: String, zipCode: String, + sensibleHeatRatio: Double? = nil, createdAt: Date, updatedAt: Date ) { @@ -28,6 +30,7 @@ public struct Project: Codable, Equatable, Identifiable, Sendable { self.city = city self.state = state self.zipCode = zipCode + self.sensibleHeatRatio = sensibleHeatRatio self.createdAt = createdAt self.updatedAt = updatedAt } @@ -42,6 +45,7 @@ extension Project { public let city: String public let state: String public let zipCode: String + public let sensibleHeatRatio: Double? public init( name: String, @@ -49,12 +53,14 @@ extension Project { city: String, state: String, zipCode: String, + sensibleHeatRatio: Double? = nil, ) { self.name = name self.streetAddress = streetAddress self.city = city self.state = state self.zipCode = zipCode + self.sensibleHeatRatio = sensibleHeatRatio } } @@ -66,6 +72,7 @@ extension Project { public let city: String? public let state: String? public let zipCode: String? + public let sensibleHeatRatio: Double? public init( id: Project.ID, @@ -73,7 +80,8 @@ extension Project { streetAddress: String? = nil, city: String? = nil, state: String? = nil, - zipCode: String? = nil + zipCode: String? = nil, + sensibleHeatRatio: Double? = nil ) { self.id = id self.name = name @@ -81,6 +89,7 @@ extension Project { self.city = city self.state = state self.zipCode = zipCode + self.sensibleHeatRatio = sensibleHeatRatio } } } diff --git a/Sources/ManualDCore/Routes/ViewRoute.swift b/Sources/ManualDCore/Routes/ViewRoute.swift index f8c68f8..9a55273 100644 --- a/Sources/ManualDCore/Routes/ViewRoute.swift +++ b/Sources/ManualDCore/Routes/ViewRoute.swift @@ -57,6 +57,11 @@ extension SiteRoute.View { Field("city", .string) Field("state", .string) Field("zipCode", .string) + Optionally { + Field("sensibleHeatRatio", default: nil) { + Double.parser() + } + } } .map(.memberwise(Project.Create.init)) } @@ -125,6 +130,11 @@ extension SiteRoute.View { Optionally { Field("zipCode", .string) } + Optionally { + Field("sensibleHeatRatio", default: nil) { + Double.parser() + } + } } .map(.memberwise(Project.Update.init)) } @@ -178,6 +188,7 @@ extension SiteRoute.View.ProjectRoute { case index case submit(Room.Create) case update(Room.Update) + case updateSensibleHeatRatio(SHRUpdate) static let rootPath = "rooms" @@ -250,6 +261,27 @@ extension SiteRoute.View.ProjectRoute { .map(.memberwise(Room.Update.init)) } } + Route(.case(Self.updateSensibleHeatRatio)) { + Path { + rootPath + "update-shr" + } + Method.patch + Body { + FormData { + Field("projectID") { Project.ID.parser() } + Optionally { + Field("sensibleHeatRatio") { Double.parser() } + } + } + .map(.memberwise(SHRUpdate.init)) + } + } + } + + public struct SHRUpdate: Codable, Equatable, Sendable { + public let projectID: Project.ID + public let sensibleHeatRatio: Double? } } diff --git a/Sources/Styleguide/ElementaryExtensions.swift b/Sources/Styleguide/ElementaryExtensions.swift index d827146..20f78e3 100644 --- a/Sources/Styleguide/ElementaryExtensions.swift +++ b/Sources/Styleguide/ElementaryExtensions.swift @@ -29,3 +29,9 @@ extension HTMLAttribute where Tag == HTMLTag.input { value(double == nil ? "" : "\(double!)") } } + +extension HTMLAttribute where Tag == HTMLTag.button { + public static func showModal(id: String) -> Self { + .on(.click, "\(id).showModal()") + } +} diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index 9952b5c..b4c124b 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -187,6 +187,14 @@ extension SiteRoute.View.ProjectRoute.RoomRoute { case .update(let form): _ = try await database.rooms.update(form) return ProjectView(projectID: projectID, activeTab: .rooms) + + case .updateSensibleHeatRatio(let form): + let _ = try await database.projects.update( + .init(id: form.projectID, sensibleHeatRatio: form.sensibleHeatRatio) + ) + return request.view { + ProjectView(projectID: projectID, activeTab: .rooms) + } } } } diff --git a/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoForm.swift b/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoForm.swift index f2a924b..dd9d414 100644 --- a/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoForm.swift +++ b/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoForm.swift @@ -5,6 +5,8 @@ import Styleguide // TODO: Have form hold onto equipment info model to edit. struct EquipmentInfoForm: HTML, Sendable { + static let id = "equipmentForm" + let dismiss: Bool let projectID: Project.ID let equipmentInfo: EquipmentInfo? @@ -31,7 +33,7 @@ struct EquipmentInfoForm: HTML, Sendable { } var body: some HTML { - ModalForm(id: "equipmentForm", dismiss: dismiss) { + ModalForm(id: Self.id, dismiss: dismiss) { h1(.class("text-3xl font-bold pb-6 ps-2")) { "Equipment Info" } form( .class("space-y-4 p-4"), @@ -64,21 +66,9 @@ struct EquipmentInfoForm: HTML, Sendable { Input(id: "coolingCFM", placeholder: "CFM") .attributes(.type(.number), .min("0"), .value(coolingCFM)) } - Row { - div {} - div(.class("space-x-4")) { - CancelButton() - .attributes( - .hx.get( - route: .project( - .detail(projectID, .equipment(.form(dismiss: true))) - ) - ), - .hx.target("#equipmentForm"), - .hx.swap(.outerHTML) - ) - SubmitButton(title: "Save") - } + div { + SubmitButton(title: "Save") + .attributes(.class("btn-block")) } } } diff --git a/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoView.swift b/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoView.swift index 252d810..b196122 100644 --- a/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoView.swift +++ b/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoView.swift @@ -15,14 +15,10 @@ struct EquipmentInfoView: HTML, Sendable { Row { h1(.class("text-2xl font-bold")) { "Equipment Info" } - if equipmentInfo != nil { - EditButton() - .attributes( - .hx.get(route: .project(.detail(projectID, .equipment(.form(dismiss: false))))), - .hx.target("#equipmentForm"), - .hx.swap(.outerHTML) - ) - } + EditButton() + .attributes( + .on(.click, "\(EquipmentInfoForm.id).showModal()") + ) } if let equipmentInfo { @@ -45,10 +41,10 @@ struct EquipmentInfoView: HTML, Sendable { } .attributes(.class("border-b border-gray-200")) - EquipmentInfoForm(dismiss: true, projectID: projectID, equipmentInfo: nil) - } else { - EquipmentInfoForm(dismiss: false, projectID: projectID, equipmentInfo: nil) } + EquipmentInfoForm( + dismiss: true, projectID: projectID, equipmentInfo: equipmentInfo + ) } } } diff --git a/Sources/ViewController/Views/Project/ProjectView.swift b/Sources/ViewController/Views/Project/ProjectView.swift index 0613c07..fb188d3 100644 --- a/Sources/ViewController/Views/Project/ProjectView.swift +++ b/Sources/ViewController/Views/Project/ProjectView.swift @@ -36,7 +36,11 @@ struct ProjectView: HTML, Sendable { } } case .rooms: - try await RoomsView(projectID: projectID, rooms: database.rooms.fetch(projectID)) + try await RoomsView( + projectID: projectID, + rooms: database.rooms.fetch(projectID), + sensibleHeatRatio: database.projects.getSensibleHeatRatio(projectID) + ) case .effectiveLength: try await EffectiveLengthsView( diff --git a/Sources/ViewController/Views/Rooms/RoomForm.swift b/Sources/ViewController/Views/Rooms/RoomForm.swift index c222b5e..08f211c 100644 --- a/Sources/ViewController/Views/Rooms/RoomForm.swift +++ b/Sources/ViewController/Views/Rooms/RoomForm.swift @@ -8,12 +8,14 @@ import Styleguide // TODO: Need to hold the project ID in hidden input field. struct RoomForm: HTML, Sendable { + static let id = "roomForm" + let dismiss: Bool let projectID: Project.ID let room: Room? var body: some HTML { - ModalForm(id: "roomForm", dismiss: dismiss) { + ModalForm(id: Self.id, dismiss: dismiss) { h1(.class("text-3xl font-bold pb-6")) { "Room" } // TODO: Use htmx here. form( diff --git a/Sources/ViewController/Views/Rooms/RoomsView.swift b/Sources/ViewController/Views/Rooms/RoomsView.swift index ea7c543..7f11c47 100644 --- a/Sources/ViewController/Views/Rooms/RoomsView.swift +++ b/Sources/ViewController/Views/Rooms/RoomsView.swift @@ -10,6 +10,7 @@ import Styleguide struct RoomsView: HTML, Sendable { let projectID: Project.ID let rooms: [Room] + let sensibleHeatRatio: Double? var body: some HTML { div { @@ -20,10 +21,7 @@ struct RoomsView: HTML, Sendable { .data("tip", value: "Add room") ) { button( - // .hx.get(route: .project(.detail(projectID, .rooms(.form(dismiss: false))))), - // .hx.target("#roomForm"), - // .hx.swap(.outerHTML), - .on(.click, "roomForm.showModal()"), + .showModal(id: RoomForm.id), .class("btn btn-primary w-[40px] text-2xl") ) { "+" @@ -32,6 +30,23 @@ struct RoomsView: HTML, Sendable { } .attributes(.class("pb-6")) + div(.class("border rounded-lg mb-6")) { + Row { + div(.class("space-x-6")) { + Label("Sensible Heat Ratio") + if let sensibleHeatRatio { + Number(sensibleHeatRatio) + } + } + + EditButton() + .attributes(.showModal(id: SHRForm.id)) + } + .attributes(.class("m-4")) + + SHRForm(projectID: projectID, sensibleHeatRatio: sensibleHeatRatio) + } + div(.class("overflow-x-auto rounded-box border")) { table(.class("table table-zebra"), .id("roomsTable")) { thead { @@ -114,6 +129,34 @@ struct RoomsView: HTML, Sendable { } } + struct SHRForm: HTML, Sendable { + static let id = "shrForm" + + let projectID: Project.ID + let sensibleHeatRatio: Double? + + var body: some HTML { + ModalForm(id: Self.id, dismiss: true) { + form( + .class("space-y-6"), + .hx.patch("/projects/\(projectID)/rooms/update-shr"), + .hx.target("body"), + .hx.swap(.outerHTML) + ) { + input(.class("hidden"), .name("projectID"), .value("\(projectID)")) + div { + label(.for("sensibleHeatRatio")) { "Sensible Heat Ratio" } + Input(id: "sensibleHeatRatio", placeholder: "Sensible Heat Ratio") + .attributes(.min("0"), .max("1"), .step("0.01"), .value(sensibleHeatRatio)) + } + div { + SubmitButton() + .attributes(.class("btn-block")) + } + } + } + } + } } extension Array where Element == Room { diff --git a/input.css b/input.css deleted file mode 100644 index 8bc6dde..0000000 --- a/input.css +++ /dev/null @@ -1,6 +0,0 @@ -@import "tailwindcss"; - -@source not "./tailwindcss"; -@source not "./daisyui{,*}.mjs"; - -@plugin "./daisyui.mjs";