From 30fddb9dcedcfd2d3b31eea70e7cf757dbf98edb Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 9 Jan 2026 09:25:37 -0500 Subject: [PATCH] feat: Updates form routes and database routes to use id's in the url path. --- Public/css/output.css | 46 +++- .../ComponentPressureLoss.swift | 41 +++ Sources/DatabaseClient/EffectiveLength.swift | 18 +- Sources/DatabaseClient/Equipment.swift | 18 +- Sources/DatabaseClient/Projects.swift | 19 +- Sources/DatabaseClient/Rooms.swift | 19 +- .../ManualDCore/ComponentPressureLosses.swift | 20 ++ Sources/ManualDCore/EffectiveLength.swift | 13 +- Sources/ManualDCore/EquipmentInfo.swift | 3 - Sources/ManualDCore/Project.swift | 3 - Sources/ManualDCore/Room.swift | 3 - Sources/ManualDCore/Routes/ViewRoute.swift | 88 ++++++- .../EquivalentLengthForm+extensions.swift | 4 - .../Extensions/String+appendingPath.swift | 20 ++ .../Extensions/UUID+idString.swift | 7 + Sources/ViewController/Live.swift | 51 +++- .../ComponentLoss/ComponentLossForm.swift | 64 +++-- .../ComponentLoss/ComponentLossesView.swift | 74 ++++-- .../EffectiveLength/EffectiveLengthForm.swift | 14 +- .../EffectiveLengthsView.swift | 36 ++- .../EquipmentInfo/EquipmentInfoForm.swift | 25 +- .../EquipmentInfo/EquipmentInfoView.swift | 38 +-- .../Views/FrictionRate/FrictionRateView.swift | 88 ++++++- .../Views/Project/ProjectForm.swift | 9 +- .../Views/Project/ProjectView.swift | 239 +++++++++--------- .../ViewController/Views/Rooms/RoomForm.swift | 11 +- .../Views/Rooms/RoomsView.swift | 28 +- 27 files changed, 677 insertions(+), 322 deletions(-) create mode 100644 Sources/ViewController/Extensions/String+appendingPath.swift create mode 100644 Sources/ViewController/Extensions/UUID+idString.swift diff --git a/Public/css/output.css b/Public/css/output.css index 9a60fa1..43a0cca 100644 --- a/Public/css/output.css +++ b/Public/css/output.css @@ -5271,8 +5271,8 @@ .mx-2 { margin-inline: calc(var(--spacing) * 2); } - .mx-4 { - margin-inline: calc(var(--spacing) * 4); + .mx-auto { + margin-inline: auto; } .file-input-ghost { @layer daisyui.l1.l2 { @@ -5569,6 +5569,15 @@ border-width: var(--border, 1px) 0 var(--border, 1px) var(--border, 1px); } } + .ms-4 { + margin-inline-start: calc(var(--spacing) * 4); + } + .ms-6 { + margin-inline-start: calc(var(--spacing) * 6); + } + .ms-8 { + margin-inline-start: calc(var(--spacing) * 8); + } .me-4 { margin-inline-end: calc(var(--spacing) * 4); } @@ -6745,6 +6754,9 @@ .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } + .grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } .grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); } @@ -6754,6 +6766,15 @@ .flex-wrap { flex-wrap: wrap; } + .items-baseline { + align-items: baseline; + } + .items-center { + align-items: center; + } + .items-end { + align-items: flex-end; + } .items-start { align-items: flex-start; } @@ -7793,6 +7814,9 @@ .ps-2 { padding-inline-start: calc(var(--spacing) * 2); } + .ps-8 { + padding-inline-start: calc(var(--spacing) * 8); + } .file-input-xl { @layer daisyui.l1.l2 { padding-inline-end: calc(0.25rem * 6); @@ -9357,9 +9381,9 @@ outline-color: var(--color-indigo-600); } } - .md\:hidden { + .md\:grid-cols-1 { @media (width >= 48rem) { - display: none; + grid-template-columns: repeat(1, minmax(0, 1fr)); } } .md\:grid-cols-2 { @@ -9420,9 +9444,9 @@ } } } - .lg\:hidden { + .lg\:grid-cols-2 { @media (width >= 64rem) { - display: none; + grid-template-columns: repeat(2, minmax(0, 1fr)); } } .lg\:grid-cols-3 { @@ -9430,6 +9454,16 @@ grid-template-columns: repeat(3, minmax(0, 1fr)); } } + .xl\:grid-cols-2 { + @media (width >= 80rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .xl\:grid-cols-3 { + @media (width >= 80rem) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } .dark\:text-white { @media (prefers-color-scheme: dark) { color: var(--color-white); diff --git a/Sources/DatabaseClient/ComponentPressureLoss.swift b/Sources/DatabaseClient/ComponentPressureLoss.swift index 8898556..28b996b 100644 --- a/Sources/DatabaseClient/ComponentPressureLoss.swift +++ b/Sources/DatabaseClient/ComponentPressureLoss.swift @@ -12,6 +12,9 @@ extension DatabaseClient { public var delete: @Sendable (ComponentPressureLoss.ID) async throws -> Void public var fetch: @Sendable (Project.ID) async throws -> [ComponentPressureLoss] public var get: @Sendable (ComponentPressureLoss.ID) async throws -> ComponentPressureLoss? + public var update: + @Sendable (ComponentPressureLoss.ID, ComponentPressureLoss.Update) async throws -> + ComponentPressureLoss } } @@ -43,6 +46,17 @@ extension DatabaseClient.ComponentLoss { }, get: { id in try await ComponentLossModel.find(id, on: database).map { try $0.toDTO() } + }, + update: { id, updates in + try updates.validate() + guard let model = try await ComponentLossModel.find(id, on: database) else { + throw NotFoundError() + } + model.applyUpdates(updates) + if model.hasChanges { + try await model.save(on: database) + } + return try model.toDTO() } ) } @@ -68,6 +82,24 @@ extension ComponentPressureLoss.Create { } } +extension ComponentPressureLoss.Update { + func validate() throws(ValidationError) { + if let name { + guard !name.isEmpty else { + throw ValidationError("Component loss name should not be empty.") + } + } + if let value { + guard value > 0 else { + throw ValidationError("Component loss value should be greater than 0.") + } + guard value < 1.0 else { + throw ValidationError("Component loss value should be less than 1.0.") + } + } + } +} + extension ComponentPressureLoss { struct Migrate: AsyncMigration { let name = "CreateComponentLoss" @@ -142,4 +174,13 @@ final class ComponentLossModel: Model, @unchecked Sendable { updatedAt: updatedAt! ) } + + func applyUpdates(_ updates: ComponentPressureLoss.Update) { + if let name = updates.name, name != self.name { + self.name = name + } + if let value = updates.value, value != self.value { + self.value = value + } + } } diff --git a/Sources/DatabaseClient/EffectiveLength.swift b/Sources/DatabaseClient/EffectiveLength.swift index 7868c6a..da5bd25 100644 --- a/Sources/DatabaseClient/EffectiveLength.swift +++ b/Sources/DatabaseClient/EffectiveLength.swift @@ -12,7 +12,8 @@ extension DatabaseClient { public var fetch: @Sendable (Project.ID) async throws -> [EffectiveLength] public var fetchMax: @Sendable (Project.ID) async throws -> EffectiveLength.MaxContainer public var get: @Sendable (EffectiveLength.ID) async throws -> EffectiveLength? - public var update: @Sendable (EffectiveLength.Update) async throws -> EffectiveLength + public var update: + @Sendable (EffectiveLength.ID, EffectiveLength.Update) async throws -> EffectiveLength } } @@ -59,11 +60,12 @@ extension DatabaseClient.EffectiveLengthClient: TestDependencyKey { get: { id in try await EffectiveLengthModel.find(id, on: database).map { try $0.toDTO() } }, - update: { updates in - guard let model = try await EffectiveLengthModel.find(updates.id, on: database) else { + update: { id, updates in + guard let model = try await EffectiveLengthModel.find(id, on: database) else { throw NotFoundError() } - if try model.applyUpdates(updates) { + try model.applyUpdates(updates) + if model.hasChanges { try await model.save(on: database) } return try model.toDTO() @@ -184,24 +186,18 @@ final class EffectiveLengthModel: Model, @unchecked Sendable { ) } - func applyUpdates(_ updates: EffectiveLength.Update) throws -> Bool { - var hasUpdates = false + func applyUpdates(_ updates: EffectiveLength.Update) throws { if let name = updates.name, name != self.name { - hasUpdates = true self.name = name } if let type = updates.type, type.rawValue != self.type { - hasUpdates = true self.type = type.rawValue } if let straightLengths = updates.straightLengths, straightLengths != self.straightLengths { - hasUpdates = true self.straightLengths = straightLengths } if let groups = updates.groups { - hasUpdates = true self.groups = try JSONEncoder().encode(groups) } - return hasUpdates } } diff --git a/Sources/DatabaseClient/Equipment.swift b/Sources/DatabaseClient/Equipment.swift index 0fb03ae..cdb776a 100644 --- a/Sources/DatabaseClient/Equipment.swift +++ b/Sources/DatabaseClient/Equipment.swift @@ -11,7 +11,8 @@ extension DatabaseClient { public var delete: @Sendable (EquipmentInfo.ID) async throws -> Void public var fetch: @Sendable (Project.ID) async throws -> EquipmentInfo? public var get: @Sendable (EquipmentInfo.ID) async throws -> EquipmentInfo? - public var update: @Sendable (EquipmentInfo.Update) async throws -> EquipmentInfo + public var update: + @Sendable (EquipmentInfo.ID, EquipmentInfo.Update) async throws -> EquipmentInfo } } @@ -46,13 +47,15 @@ extension DatabaseClient.Equipment { get: { id in try await EquipmentModel.find(id, on: database).map { try $0.toDTO() } }, - update: { request in - guard let model = try await EquipmentModel.find(request.id, on: database) else { + update: { id, updates in + guard let model = try await EquipmentModel.find(id, on: database) else { throw NotFoundError() } - guard request.hasUpdates else { return try model.toDTO() } - try model.applyUpdates(request) - try await model.save(on: database) + try updates.validate() + model.applyUpdates(updates) + if model.hasChanges { + try await model.save(on: database) + } return try model.toDTO() } ) @@ -196,8 +199,7 @@ final class EquipmentModel: Model, @unchecked Sendable { ) } - func applyUpdates(_ updates: EquipmentInfo.Update) throws { - try updates.validate() + func applyUpdates(_ updates: EquipmentInfo.Update) { if let staticPressure = updates.staticPressure { self.staticPressure = staticPressure } diff --git a/Sources/DatabaseClient/Projects.swift b/Sources/DatabaseClient/Projects.swift index 90ab1f8..d33ac61 100644 --- a/Sources/DatabaseClient/Projects.swift +++ b/Sources/DatabaseClient/Projects.swift @@ -13,7 +13,7 @@ extension DatabaseClient { public var getCompletedSteps: @Sendable (Project.ID) async throws -> Project.CompletedSteps 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 + public var update: @Sendable (Project.ID, Project.Update) async throws -> Project } } @@ -85,12 +85,13 @@ extension DatabaseClient.Projects: TestDependencyKey { .paginate(request) .map { try $0.toDTO() } }, - update: { updates in - guard let model = try await ProjectModel.find(updates.id, on: database) else { + update: { id, updates in + guard let model = try await ProjectModel.find(id, on: database) else { throw NotFoundError() } try updates.validate() - if model.applyUpdates(updates) { + model.applyUpdates(updates) + if model.hasChanges { try await model.save(on: database) } return try model.toDTO() @@ -283,34 +284,26 @@ final class ProjectModel: Model, @unchecked Sendable { ) } - func applyUpdates(_ updates: Project.Update) -> Bool { - var hasUpdates = false + func applyUpdates(_ updates: Project.Update) { if let name = updates.name, name != self.name { - hasUpdates = true self.name = name } if let streetAddress = updates.streetAddress, streetAddress != self.streetAddress { - hasUpdates = true self.streetAddress = streetAddress } if let city = updates.city, city != self.city { - hasUpdates = true self.city = city } if let state = updates.state, state != self.state { - hasUpdates = true self.state = state } if let zipCode = updates.zipCode, zipCode != self.zipCode { - hasUpdates = true self.zipCode = zipCode } if let sensibleHeatRatio = updates.sensibleHeatRatio, sensibleHeatRatio != self.sensibleHeatRatio { - hasUpdates = true self.sensibleHeatRatio = sensibleHeatRatio } - return hasUpdates } } diff --git a/Sources/DatabaseClient/Rooms.swift b/Sources/DatabaseClient/Rooms.swift index 7df966e..f2d6e17 100644 --- a/Sources/DatabaseClient/Rooms.swift +++ b/Sources/DatabaseClient/Rooms.swift @@ -11,7 +11,7 @@ extension DatabaseClient { public var delete: @Sendable (Room.ID) async throws -> Void public var get: @Sendable (Room.ID) async throws -> Room? public var fetch: @Sendable (Project.ID) async throws -> [Room] - public var update: @Sendable (Room.Update) async throws -> Room + public var update: @Sendable (Room.ID, Room.Update) async throws -> Room } } @@ -41,13 +41,14 @@ extension DatabaseClient.Rooms: TestDependencyKey { .all() .map { try $0.toDTO() } }, - update: { updates in - guard let model = try await RoomModel.find(updates.id, on: database) else { + update: { id, updates in + guard let model = try await RoomModel.find(id, on: database) else { throw NotFoundError() } try updates.validate() - if model.applyUpdates(updates) { + model.applyUpdates(updates) + if model.hasChanges { try await model.save(on: database) } return try model.toDTO() @@ -218,30 +219,24 @@ final class RoomModel: Model, @unchecked Sendable { ) } - func applyUpdates(_ updates: Room.Update) -> Bool { - var hasUpdates = false + func applyUpdates(_ updates: Room.Update) { if let name = updates.name, name != self.name { - hasUpdates = true self.name = name } if let heatingLoad = updates.heatingLoad, heatingLoad != self.heatingLoad { - hasUpdates = true self.heatingLoad = heatingLoad } if let coolingTotal = updates.coolingTotal, coolingTotal != self.coolingTotal { - hasUpdates = true self.coolingTotal = coolingTotal } if let coolingSensible = updates.coolingSensible, coolingSensible != self.coolingSensible { - hasUpdates = true self.coolingSensible = coolingSensible } if let registerCount = updates.registerCount, registerCount != self.registerCount { - hasUpdates = true self.registerCount = registerCount } - return hasUpdates + } } diff --git a/Sources/ManualDCore/ComponentPressureLosses.swift b/Sources/ManualDCore/ComponentPressureLosses.swift index 7d8181e..fa332d9 100644 --- a/Sources/ManualDCore/ComponentPressureLosses.swift +++ b/Sources/ManualDCore/ComponentPressureLosses.swift @@ -52,6 +52,26 @@ extension ComponentPressureLoss { ] } } + + public struct Update: Codable, Equatable, Sendable { + + public let name: String? + public let value: Double? + + public init( + name: String? = nil, + value: Double? = nil + ) { + self.name = name + self.value = value + } + } +} + +extension Array where Element == ComponentPressureLoss { + public var totalComponentPressureLoss: Double { + reduce(into: 0) { $0 += $1.value } + } } public typealias ComponentPressureLosses = [String: Double] diff --git a/Sources/ManualDCore/EffectiveLength.swift b/Sources/ManualDCore/EffectiveLength.swift index 6a99fc7..7056715 100644 --- a/Sources/ManualDCore/EffectiveLength.swift +++ b/Sources/ManualDCore/EffectiveLength.swift @@ -63,20 +63,17 @@ extension EffectiveLength { public struct Update: Codable, Equatable, Sendable { - public let id: EffectiveLength.ID public let name: String? public let type: EffectiveLengthType? public let straightLengths: [Int]? public let groups: [Group]? public init( - id: EffectiveLength.ID, - name: String? = nil, + name: String? = nil, type: EffectiveLength.EffectiveLengthType? = nil, - straightLengths: [Int]? = nil, + straightLengths: [Int]? = nil, groups: [EffectiveLength.Group]? = nil ) { - self.id = id self.name = name self.type = type self.straightLengths = straightLengths @@ -113,6 +110,12 @@ extension EffectiveLength { public let supply: EffectiveLength? public let `return`: EffectiveLength? + public var total: Double? { + guard let supply else { return nil } + guard let `return` else { return nil } + return supply.totalEquivalentLength + `return`.totalEquivalentLength + } + public init(supply: EffectiveLength? = nil, return: EffectiveLength? = nil) { self.supply = supply self.return = `return` diff --git a/Sources/ManualDCore/EquipmentInfo.swift b/Sources/ManualDCore/EquipmentInfo.swift index 347af39..e2b6fb6 100644 --- a/Sources/ManualDCore/EquipmentInfo.swift +++ b/Sources/ManualDCore/EquipmentInfo.swift @@ -52,18 +52,15 @@ extension EquipmentInfo { } public struct Update: Codable, Equatable, Sendable { - public let id: EquipmentInfo.ID public let staticPressure: Double? public let heatingCFM: Int? public let coolingCFM: Int? public init( - id: EquipmentInfo.ID, staticPressure: Double? = nil, heatingCFM: Int? = nil, coolingCFM: Int? = nil ) { - self.id = id self.staticPressure = staticPressure self.heatingCFM = heatingCFM self.coolingCFM = coolingCFM diff --git a/Sources/ManualDCore/Project.swift b/Sources/ManualDCore/Project.swift index 2c3c6ca..539c0a9 100644 --- a/Sources/ManualDCore/Project.swift +++ b/Sources/ManualDCore/Project.swift @@ -79,7 +79,6 @@ extension Project { public struct Update: Codable, Equatable, Sendable { - public let id: Project.ID public let name: String? public let streetAddress: String? public let city: String? @@ -88,7 +87,6 @@ extension Project { public let sensibleHeatRatio: Double? public init( - id: Project.ID, name: String? = nil, streetAddress: String? = nil, city: String? = nil, @@ -96,7 +94,6 @@ extension Project { zipCode: String? = nil, sensibleHeatRatio: Double? = nil ) { - self.id = id self.name = name self.streetAddress = streetAddress self.city = city diff --git a/Sources/ManualDCore/Room.swift b/Sources/ManualDCore/Room.swift index c8a4370..5b7147d 100644 --- a/Sources/ManualDCore/Room.swift +++ b/Sources/ManualDCore/Room.swift @@ -63,7 +63,6 @@ extension Room { } public struct Update: Codable, Equatable, Sendable { - public let id: Room.ID public let name: String? public let heatingLoad: Double? public let coolingTotal: Double? @@ -71,14 +70,12 @@ extension Room { public let registerCount: Int? public init( - id: Room.ID, name: String? = nil, heatingLoad: Double? = nil, coolingTotal: Double? = nil, coolingSensible: Double? = nil, registerCount: Int? = nil ) { - self.id = id self.name = name self.heatingLoad = heatingLoad self.coolingTotal = coolingTotal diff --git a/Sources/ManualDCore/Routes/ViewRoute.swift b/Sources/ManualDCore/Routes/ViewRoute.swift index 5eced86..944b263 100644 --- a/Sources/ManualDCore/Routes/ViewRoute.swift +++ b/Sources/ManualDCore/Routes/ViewRoute.swift @@ -40,7 +40,7 @@ extension SiteRoute.View { case form(id: Project.ID? = nil, dismiss: Bool = false) case index case page(PageRequest) - case update(Project.Update) + case update(Project.ID, Project.Update) public static func page(page: Int, per limit: Int) -> Self { .page(.init(page: page, per: limit)) @@ -112,11 +112,13 @@ extension SiteRoute.View { .map(.memberwise(PageRequest.init)) } Route(.case(Self.update)) { - Path { rootPath } + Path { + rootPath + Project.ID.parser() + } Method.patch Body { FormData { - Field("id") { Project.ID.parser() } Optionally { Field("name", .string) } @@ -149,6 +151,7 @@ extension SiteRoute.View.ProjectRoute { public enum DetailRoute: Equatable, Sendable { case index(tab: Tab = .default) + case componentLoss(ComponentLossRoute) case equipment(EquipmentInfoRoute) case equivalentLength(EquivalentLengthRoute) case frictionRate(FrictionRateRoute) @@ -163,6 +166,9 @@ extension SiteRoute.View.ProjectRoute { } } } + Route(.case(Self.componentLoss)) { + ComponentLossRoute.router + } Route(.case(Self.equipment)) { EquipmentInfoRoute.router } @@ -193,7 +199,7 @@ extension SiteRoute.View.ProjectRoute { case form(id: Room.ID? = nil, dismiss: Bool = false) case index case submit(Room.Create) - case update(Room.Update) + case update(Room.ID, Room.Update) case updateSensibleHeatRatio(SHRUpdate) static let rootPath = "rooms" @@ -243,11 +249,13 @@ extension SiteRoute.View.ProjectRoute { } } Route(.case(Self.update)) { - Path { rootPath } + Path { + rootPath + Room.ID.parser() + } Method.patch Body { FormData { - Field("id") { Room.ID.parser() } Optionally { Field("name", .string) } @@ -291,6 +299,59 @@ extension SiteRoute.View.ProjectRoute { } } + public enum ComponentLossRoute: Equatable, Sendable { + case index + case delete(ComponentPressureLoss.ID) + case submit(ComponentPressureLoss.Create) + case update(ComponentPressureLoss.ID, ComponentPressureLoss.Update) + + static let rootPath = "component-loss" + + static let router = OneOf { + Route(.case(Self.index)) { + Path { rootPath } + Method.get + } + Route(.case(Self.delete)) { + Path { + rootPath + ComponentPressureLoss.ID.parser() + } + Method.delete + } + Route(.case(Self.submit)) { + Path { rootPath } + Method.post + Body { + FormData { + Field("projectID") { Project.ID.parser() } + Field("name", .string) + Field("value") { Double.parser() } + } + .map(.memberwise(ComponentPressureLoss.Create.init)) + } + } + Route(.case(Self.update)) { + Path { + rootPath + ComponentPressureLoss.ID.parser() + } + Method.patch + Body { + FormData { + Optionally { + Field("name", .string) + } + Optionally { + Field("value") { Double.parser() } + } + } + .map(.memberwise(ComponentPressureLoss.Update.init)) + } + } + } + } + public enum FrictionRateRoute: Equatable, Sendable { case index // TODO: Remove form or move equipment / component losses routes here. @@ -326,7 +387,7 @@ extension SiteRoute.View.ProjectRoute { case index case form(dismiss: Bool) case submit(EquipmentInfo.Create) - case update(EquipmentInfo.Update) + case update(EquipmentInfo.ID, EquipmentInfo.Update) static let rootPath = "equipment" @@ -359,11 +420,13 @@ extension SiteRoute.View.ProjectRoute { } } Route(.case(Self.update)) { - Path { rootPath } + Path { + rootPath + EquipmentInfo.ID.parser() + } Method.patch Body { FormData { - Field("id") { EquipmentInfo.ID.parser() } Optionally { Field("staticPressure", default: nil) { Double.parser() } } @@ -386,7 +449,7 @@ extension SiteRoute.View.ProjectRoute { case form(dismiss: Bool = false) case index case submit(FormStep) - case update(StepThree) + case update(EffectiveLength.ID, StepThree) static let rootPath = "effective-lengths" @@ -433,7 +496,10 @@ extension SiteRoute.View.ProjectRoute { FormStep.router } Route(.case(Self.update)) { - Path { rootPath } + Path { + rootPath + EffectiveLength.ID.parser() + } Method.patch Body { FormData { diff --git a/Sources/ViewController/Extensions/EquivalentLengthForm+extensions.swift b/Sources/ViewController/Extensions/EquivalentLengthForm+extensions.swift index efccc90..2b46368 100644 --- a/Sources/ViewController/Extensions/EquivalentLengthForm+extensions.swift +++ b/Sources/ViewController/Extensions/EquivalentLengthForm+extensions.swift @@ -50,11 +50,7 @@ extension EffectiveLength.Update { form: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree, projectID: Project.ID ) throws { - guard let id = form.id else { - throw ValidationError("Id not found.") - } self.init( - id: id, name: form.name, type: form.type, straightLengths: form.straightLengths, diff --git a/Sources/ViewController/Extensions/String+appendingPath.swift b/Sources/ViewController/Extensions/String+appendingPath.swift new file mode 100644 index 0000000..882ee6c --- /dev/null +++ b/Sources/ViewController/Extensions/String+appendingPath.swift @@ -0,0 +1,20 @@ +import Foundation + +extension String { + + func appendingPath(_ string: String) -> Self { + guard string.starts(with: "/") else { + return self.appending("/\(string)") + } + return self.appending(string) + } + + func appendingPath(_ id: UUID?) -> Self { + guard let id else { return self } + return appendingPath(id.uuidString) + } + + func appendingPath(_ id: UUID) -> Self { + return appendingPath(id.uuidString) + } +} diff --git a/Sources/ViewController/Extensions/UUID+idString.swift b/Sources/ViewController/Extensions/UUID+idString.swift new file mode 100644 index 0000000..759974f --- /dev/null +++ b/Sources/ViewController/Extensions/UUID+idString.swift @@ -0,0 +1,7 @@ +import Foundation + +extension UUID { + var idString: String { + uuidString.replacing("-", with: "") + } +} diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index df5fb8e..d9d5890 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -105,8 +105,8 @@ extension SiteRoute.View.ProjectRoute { try await database.projects.delete(id) return EmptyHTML() - case .update(let form): - let project = try await database.projects.update(form) + case .update(let id, let form): + let project = try await database.projects.update(id, form) return ProjectView(projectID: project.id, activeTab: .project) case .detail(let projectID, let route): @@ -115,6 +115,8 @@ extension SiteRoute.View.ProjectRoute { return request.view { ProjectView(projectID: projectID, activeTab: tab) } + case .componentLoss(let route): + return try await route.renderView(on: request, projectID: projectID) case .equipment(let route): return try await route.renderView(on: request, projectID: projectID) case .equivalentLength(let route): @@ -147,8 +149,8 @@ extension SiteRoute.View.ProjectRoute.EquipmentInfoRoute { case .submit(let form): let equipment = try await database.equipment.create(form) return EquipmentInfoView(equipmentInfo: equipment, projectID: projectID) - case .update(let updates): - let equipment = try await database.equipment.update(updates) + case .update(let id, let updates): + let equipment = try await database.equipment.update(id, updates) return EquipmentInfoView(equipmentInfo: equipment, projectID: projectID) } } @@ -187,13 +189,14 @@ extension SiteRoute.View.ProjectRoute.RoomRoute { ProjectView(projectID: projectID, activeTab: .rooms) } - case .update(let form): - let _ = try await database.rooms.update(form) + case .update(let id, let form): + let _ = try await database.rooms.update(id, form) return ProjectView(projectID: projectID, activeTab: .rooms) case .updateSensibleHeatRatio(let form): let _ = try await database.projects.update( - .init(id: form.projectID, sensibleHeatRatio: form.sensibleHeatRatio) + form.projectID, + .init(sensibleHeatRatio: form.sensibleHeatRatio) ) return request.view { ProjectView(projectID: projectID, activeTab: .rooms) @@ -210,8 +213,8 @@ extension SiteRoute.View.ProjectRoute.FrictionRateRoute { switch self { case .index: - let equipment = try await database.equipment.fetch(projectID) - let componentLosses = try await database.componentLoss.fetch(projectID) + // let equipment = try await database.equipment.fetch(projectID) + // let componentLosses = try await database.componentLoss.fetch(projectID) return request.view { ProjectView(projectID: projectID, activeTab: .frictionRate) @@ -224,12 +227,36 @@ extension SiteRoute.View.ProjectRoute.FrictionRateRoute { return div { "REMOVE ME!" } // return EquipmentForm(dismiss: dismiss, projectID: projectID) case .componentPressureLoss: - return ComponentLossForm(dismiss: dismiss, projectID: projectID) + return ComponentLossForm(dismiss: dismiss, projectID: projectID, componentLoss: nil) } } } } +extension SiteRoute.View.ProjectRoute.ComponentLossRoute { + + func renderView( + on request: ViewController.Request, + projectID: Project.ID + ) async throws -> AnySendableHTML { + @Dependency(\.database) var database + + switch self { + case .index: + return EmptyHTML() + case .delete(let id): + _ = try await database.componentLoss.delete(id) + return EmptyHTML() + case .submit(let form): + _ = try await database.componentLoss.create(form) + return ProjectView(projectID: projectID, activeTab: .frictionRate) + case .update(let id, let form): + _ = try await database.componentLoss.update(id, form) + return ProjectView(projectID: projectID, activeTab: .frictionRate) + } + } +} + extension SiteRoute.View.ProjectRoute.FrictionRateRoute.FormType { var id: String { switch self { @@ -271,8 +298,8 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute { return GroupField(style: style ?? .supply) } - case .update(let form): - _ = try await database.effectiveLength.update(.init(form: form, projectID: projectID)) + case .update(let id, let form): + _ = try await database.effectiveLength.update(id, .init(form: form, projectID: projectID)) return ProjectView(projectID: projectID, activeTab: .equivalentLength) case .submit(let step): diff --git a/Sources/ViewController/Views/ComponentLoss/ComponentLossForm.swift b/Sources/ViewController/Views/ComponentLoss/ComponentLossForm.swift index bf4d2e1..fda17aa 100644 --- a/Sources/ViewController/Views/ComponentLoss/ComponentLossForm.swift +++ b/Sources/ViewController/Views/ComponentLoss/ComponentLossForm.swift @@ -4,38 +4,62 @@ import ManualDCore import Styleguide struct ComponentLossForm: HTML, Sendable { + + static func id(_ componentLoss: ComponentPressureLoss? = nil) -> String { + let base = "componentLossForm" + guard let componentLoss else { return base } + return "\(base)_\(componentLoss.id.idString)" + } + let dismiss: Bool let projectID: Project.ID + let componentLoss: ComponentPressureLoss? + + var route: String { + SiteRoute.View.router.path( + for: .project(.detail(projectID, .componentLoss(.index))) + ) + .appendingPath(componentLoss?.id) + // if let componentLoss { + // return baseRoute.appending("/\(componentLoss.id)") + // } + // return baseRoute + } var body: some HTML { - ModalForm(id: "componentLossForm", dismiss: dismiss) { + ModalForm(id: Self.id(componentLoss), dismiss: dismiss) { h1(.class("text-2xl font-bold")) { "Component Loss" } - form(.class("space-y-4 p-4")) { + form( + .class("space-y-4 p-4"), + componentLoss == nil + ? .hx.post(route) + : .hx.patch(route), + .hx.target("body"), + .hx.swap(.outerHTML) + ) { + + if let componentLoss { + input(.class("hidden"), .name("id"), .value("\(componentLoss.id)")) + } + + input(.class("hidden"), .name("projectID"), .value("\(projectID)")) + div { label(.for("name")) { "Name" } Input(id: "name", placeholder: "Name") - .attributes(.type(.text), .required, .autofocus) + .attributes(.type(.text), .required, .autofocus, .value(componentLoss?.name)) } div { label(.for("value")) { "Value" } - Input(id: "name", placeholder: "Pressure loss") - .attributes(.type(.number), .min("0"), .max("1"), .step("0.1"), .required) - } - Row { - div {} - div { - CancelButton() - .attributes( - .hx.get( - route: .project( - .detail(projectID, .frictionRate(.form(.componentPressureLoss, dismiss: true)))) - ), - .hx.target("#componentLossForm"), - .hx.swap(.outerHTML) - ) - SubmitButton() - } + Input(id: "value", placeholder: "Pressure loss") + .attributes( + .type(.number), .min("0.03"), .max("1.0"), .step("0.1"), .required, + .value(componentLoss?.value) + ) } + + SubmitButton() + .attributes(.class("btn-block")) } } } diff --git a/Sources/ViewController/Views/ComponentLoss/ComponentLossesView.swift b/Sources/ViewController/Views/ComponentLoss/ComponentLossesView.swift index 7fc0492..43714a9 100644 --- a/Sources/ViewController/Views/ComponentLoss/ComponentLossesView.swift +++ b/Sources/ViewController/Views/ComponentLoss/ComponentLossesView.swift @@ -23,33 +23,71 @@ struct ComponentPressureLossesView: HTML, Sendable { ) ) { Row { - h1(.class("text-2xl font-bold")) { "Component Pressure Losses" } + div(.class("flex space-x-4 items-center")) { + h1(.class("text-2xl font-bold")) { "Component Pressure Losses" } + div(.class("flex text-primary space-x-2 items-baseline")) { + Number(total) + .attributes(.class("text-xl font-bold badge badge-outline badge-primary")) + span(.class("text-sm italic")) { "Total" } + } + } PlusButton() .attributes( - .hx.get( - route: .project( - .detail(projectID, .frictionRate(.form(.componentPressureLoss, dismiss: false)))) - ), - .hx.target("#componentLossForm"), - .hx.swap(.outerHTML) + .showModal(id: ComponentLossForm.id()) ) } - for row in componentPressureLosses { - Row { - Label { row.name } - Number(row.value) + table(.class("table table-zebra")) { + thead { + tr(.class("text-xl font-bold")) { + th { "Name" } + th { "Value" } + th {} + } + } + tbody { + for row in componentPressureLosses { + TableRow(row: row) + } } - .attributes(.class("border-b border-gray-200")) - } - Row { - Label { "Total" } - Number(total) - .attributes(.class("text-xl font-bold")) } } - ComponentLossForm(dismiss: true, projectID: projectID) + ComponentLossForm(dismiss: true, projectID: projectID, componentLoss: nil) } + struct TableRow: HTML, Sendable { + let row: ComponentPressureLoss + + var body: some HTML { + tr(.class("text-lg")) { + td { row.name } + td { Number(row.value) } + td { + div(.class("flex join items-end justify-end mx-auto")) { + TrashButton() + .attributes( + .class("join-item"), + .hx.delete( + route: .project( + .detail(row.projectID, .componentLoss(.delete(row.id))) + ) + ), + .hx.target("body"), + .hx.swap(.outerHTML), + .hx.confirm("Are your sure?") + + ) + EditButton() + .attributes( + .class("join-item"), + .showModal(id: ComponentLossForm.id(row)) + ) + } + + ComponentLossForm(dismiss: true, projectID: row.projectID, componentLoss: row) + } + } + } + } } diff --git a/Sources/ViewController/Views/EffectiveLength/EffectiveLengthForm.swift b/Sources/ViewController/Views/EffectiveLength/EffectiveLengthForm.swift index edd2767..d8b1037 100644 --- a/Sources/ViewController/Views/EffectiveLength/EffectiveLengthForm.swift +++ b/Sources/ViewController/Views/EffectiveLength/EffectiveLengthForm.swift @@ -157,14 +157,14 @@ struct EffectiveLengthForm: HTML, Sendable { let stepTwo: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepTwo var route: String { - if effectiveLength != nil { - return SiteRoute.View.router.path( - for: .project(.detail(projectID, .equivalentLength(.index)))) + let baseRoute = SiteRoute.View.router.path( + for: .project(.detail(projectID, .equivalentLength(.index))) + ) + + if let effectiveLength { + return baseRoute.appendingPath(effectiveLength.id) } else { - let baseRoute = SiteRoute.View.router.path( - for: .project(.detail(projectID, .equivalentLength(.index))) - ) - return "\(baseRoute)/stepThree" + return baseRoute.appendingPath("stepThree") } } diff --git a/Sources/ViewController/Views/EffectiveLength/EffectiveLengthsView.swift b/Sources/ViewController/Views/EffectiveLength/EffectiveLengthsView.swift index f2e9d1b..f89f3ae 100644 --- a/Sources/ViewController/Views/EffectiveLength/EffectiveLengthsView.swift +++ b/Sources/ViewController/Views/EffectiveLength/EffectiveLengthsView.swift @@ -126,20 +126,28 @@ struct EffectiveLengthsView: HTML, Sendable { } div(.class("card-actions justify-end pt-6 space-y-4 mt-auto")) { - // TODO: Delete. - TrashButton() - .attributes( - .hx.delete( - route: .project( - .detail( - effectiveLength.projectID, .equivalentLength(.delete(id: effectiveLength.id))) - )), - .hx.confirm("Are you sure?"), - .hx.target("#\(id)"), - .hx.swap(.outerHTML) - ) - EditButton() - .attributes(.showModal(id: EffectiveLengthForm.id(effectiveLength))) + div(.class("join")) { + TrashButton() + .attributes( + .class("join-item"), + .hx.delete( + route: .project( + .detail( + effectiveLength.projectID, + .equivalentLength(.delete(id: effectiveLength.id)) + ) + ) + ), + .hx.confirm("Are you sure?"), + .hx.target("#\(id)"), + .hx.swap(.outerHTML) + ) + EditButton() + .attributes( + .class("join-item"), + .showModal(id: EffectiveLengthForm.id(effectiveLength)) + ) + } } EffectiveLengthForm(effectiveLength: effectiveLength) diff --git a/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoForm.swift b/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoForm.swift index dd9d414..34bbaf0 100644 --- a/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoForm.swift +++ b/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoForm.swift @@ -18,18 +18,11 @@ struct EquipmentInfoForm: HTML, Sendable { return "\(staticPressure)" } - var heatingCFM: String { - guard let heatingCFM = equipmentInfo?.heatingCFM else { - return "" - } - return "\(heatingCFM)" - } - - var coolingCFM: String { - guard let heatingCFM = equipmentInfo?.heatingCFM else { - return "" - } - return "\(heatingCFM)" + var route: String { + SiteRoute.View.router.path( + for: .project(.detail(projectID, .equipment(.index))) + ) + .appendingPath(equipmentInfo?.id) } var body: some HTML { @@ -38,8 +31,8 @@ struct EquipmentInfoForm: HTML, Sendable { form( .class("space-y-4 p-4"), equipmentInfo != nil - ? .hx.patch(route: .project(.detail(projectID, .equipment(.index)))) - : .hx.post(route: .project(.detail(projectID, .equipment(.index)))), + ? .hx.patch(route) + : .hx.post(route), .hx.target("#equipmentInfo"), .hx.swap(.outerHTML) ) { @@ -59,12 +52,12 @@ struct EquipmentInfoForm: HTML, Sendable { div { label(.for("heatingCFM")) { "Heating CFM" } Input(id: "heatingCFM", placeholder: "CFM") - .attributes(.type(.number), .min("0"), .value(heatingCFM)) + .attributes(.type(.number), .min("0"), .value(equipmentInfo?.heatingCFM)) } div { label(.for("coolingCFM")) { "Cooling CFM" } Input(id: "coolingCFM", placeholder: "CFM") - .attributes(.type(.number), .min("0"), .value(coolingCFM)) + .attributes(.type(.number), .min("0"), .value(equipmentInfo?.coolingCFM)) } div { SubmitButton(title: "Save") diff --git a/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoView.swift b/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoView.swift index b196122..3fc1073 100644 --- a/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoView.swift +++ b/Sources/ViewController/Views/EquipmentInfo/EquipmentInfoView.swift @@ -23,24 +23,28 @@ struct EquipmentInfoView: HTML, Sendable { if let equipmentInfo { - Row { - Label { "Static Pressure" } - Number(equipmentInfo.staticPressure) + table(.class("table table-zebra")) { + thead { + tr { + th { Label("Name") } + th { Label("Value") } + } + } + tbody(.class("text-lg")) { + tr { + td { "Static Pressure" } + td { Number(equipmentInfo.staticPressure) } + } + tr { + td { "Heating CFM" } + td { Number(equipmentInfo.heatingCFM) } + } + tr { + td { "Cooling CFM" } + td { Number(equipmentInfo.coolingCFM) } + } + } } - .attributes(.class("border-b border-gray-200")) - - Row { - Label { "Heating CFM" } - Number(equipmentInfo.heatingCFM) - } - .attributes(.class("border-b border-gray-200")) - - Row { - Label { "Cooling CFM" } - Number(equipmentInfo.coolingCFM) - } - .attributes(.class("border-b border-gray-200")) - } EquipmentInfoForm( dismiss: true, projectID: projectID, equipmentInfo: equipmentInfo diff --git a/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift b/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift index 0bdf612..65a61d7 100644 --- a/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift +++ b/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift @@ -6,15 +6,95 @@ struct FrictionRateView: HTML, Sendable { let equipmentInfo: EquipmentInfo? let componentLosses: [ComponentPressureLoss] + let equivalentLengths: EffectiveLength.MaxContainer let projectID: Project.ID + var availableStaticPressure: Double? { + guard let staticPressure = equipmentInfo?.staticPressure else { + return nil + } + return staticPressure - componentLosses.totalComponentPressureLoss + } + + var frictionRateDesignValue: Double? { + guard let availableStaticPressure, let tel = equivalentLengths.total else { + return nil + } + return (((availableStaticPressure * 100) / tel) * 100) / 100 + } + + var badgeColor: String { + let base = "badge-primary" + guard let frictionRateDesignValue else { return base } + if frictionRateDesignValue >= 0.18 || frictionRateDesignValue <= 0.02 { + return "badge-error" + } + return base + } + + var showHighErrors: Bool { + guard let frictionRateDesignValue else { return false } + return frictionRateDesignValue >= 0.18 + } + + var showLowErrors: Bool { + guard let frictionRateDesignValue else { return false } + return frictionRateDesignValue <= 0.02 + } + var body: some HTML { div(.class("p-4 space-y-6")) { h1(.class("text-4xl font-bold pb-6")) { "Friction Rate" } - EquipmentInfoView(equipmentInfo: equipmentInfo, projectID: projectID) - ComponentPressureLossesView( - componentPressureLosses: componentLosses, projectID: projectID - ) + div(.class("flex space-x-4")) { + Label("Available Static Pressure") + if let availableStaticPressure { + Number(availableStaticPressure, digits: 2) + .attributes(.class("badge badge-lg badge-outline font-bold ms-4")) + } + } + div(.class("flex space-x-4")) { + if let frictionRateDesignValue { + Label("Friction Rate Design Value") + Number(frictionRateDesignValue, digits: 2) + .attributes(.class("badge badge-lg badge-outline \(badgeColor) font-bold")) + } + } + + div(.class("text-error italic")) { + p { + "No component pressures losses" + } + .attributes(.class("hidden"), when: componentLosses.totalComponentPressureLoss > 0) + + p { + "Calculated friction rate is below 0.02. The fan may not deliver the required CFM." + br() + " * Increase the blower speed" + br() + " * Increase the blower size" + br() + " * Decrease the Total Effective Length (TEL)" + } + .attributes(.class("hidden"), when: !showLowErrors) + + p { + "Calculated friction rate is above 0.18. The fan may deliver too many CFM." + br() + " * Decrease the blower speed" + br() + " * Decreae the blower size" + br() + " * Increase the Total Effective Length (TEL)" + } + .attributes(.class("hidden"), when: !showHighErrors) + } + + div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) { + EquipmentInfoView(equipmentInfo: equipmentInfo, projectID: projectID) + ComponentPressureLossesView( + componentPressureLosses: componentLosses, projectID: projectID + ) + } } } } diff --git a/Sources/ViewController/Views/Project/ProjectForm.swift b/Sources/ViewController/Views/Project/ProjectForm.swift index a192585..339deda 100644 --- a/Sources/ViewController/Views/Project/ProjectForm.swift +++ b/Sources/ViewController/Views/Project/ProjectForm.swift @@ -18,14 +18,19 @@ struct ProjectForm: HTML, Sendable { self.project = project } + var route: String { + SiteRoute.View.router.path(for: .project(.index)) + .appendingPath(project?.id) + } + var body: some HTML { ModalForm(id: Self.id, dismiss: dismiss) { h1(.class("text-3xl font-bold pb-6 ps-2")) { "Project" } form( .class("space-y-4 p-4"), project == nil - ? .hx.post(route: .project(.index)) - : .hx.patch(route: .project(.index)), + ? .hx.post(route) + : .hx.patch(route), .hx.target("body"), .hx.swap(.outerHTML) ) { diff --git a/Sources/ViewController/Views/Project/ProjectView.swift b/Sources/ViewController/Views/Project/ProjectView.swift index e9b88f9..9f74ed9 100644 --- a/Sources/ViewController/Views/Project/ProjectView.swift +++ b/Sources/ViewController/Views/Project/ProjectView.swift @@ -5,8 +5,6 @@ import ElementaryHTMX import ManualDCore import Styleguide -// TODO: Make view async and load based on the active tab. - struct ProjectView: HTML, Sendable { @Dependency(\.database) var database @@ -58,7 +56,10 @@ struct ProjectView: HTML, Sendable { case .frictionRate: try await FrictionRateView( equipmentInfo: database.equipment.fetch(projectID), - componentLosses: database.componentLoss.fetch(projectID), projectID: projectID) + componentLosses: database.componentLoss.fetch(projectID), + equivalentLengths: database.effectiveLength.fetchMax(projectID), + projectID: projectID + ) case .ductSizing: div { "FIX ME!" } @@ -75,135 +76,135 @@ struct ProjectView: HTML, Sendable { } } -// TODO: Update to use DaisyUI drawer. -struct Sidebar: HTML { +extension ProjectView { - let active: SiteRoute.View.ProjectRoute.DetailRoute.Tab - let projectID: Project.ID - let completedSteps: Project.CompletedSteps + struct Sidebar: HTML { - var body: some HTML { + let active: SiteRoute.View.ProjectRoute.DetailRoute.Tab + let projectID: Project.ID + let completedSteps: Project.CompletedSteps - div(.class("drawer-side is-drawer-close:overflow-visible")) { - label( - .for("my-drawer-1"), .init(name: "aria-label", value: "close sidebar"), - .class("drawer-overlay") - ) {} + var body: some HTML { - div( - .class( - """ - flex min-h-full flex-col items-start bg-base-200 - is-drawer-close:min-w-[80px] is-drawer-open:min-w-[340px] - """ - ) - ) { + div(.class("drawer-side is-drawer-close:overflow-visible")) { + label( + .for("my-drawer-1"), .init(name: "aria-label", value: "close sidebar"), + .class("drawer-overlay") + ) {} - ul(.class("w-full")) { + div( + .class( + """ + flex min-h-full flex-col items-start bg-base-200 + is-drawer-close:min-w-[80px] is-drawer-open:min-w-[340px] + """ + ) + ) { - li(.class("w-full")) { - div( - .class("w-full is-drawer-close:tooltip is-drawer-close:tooltip-right"), - .data("tip", value: "All Projects") - ) { - a( - .class( - """ - flex btn btn-secondary btn-square btn-block - is-drawer-close:items-center - """ - ), - .hx.get(route: .project(.index)), - .hx.target("body"), - .hx.pushURL(true), - .hx.swap(.outerHTML), + ul(.class("w-full")) { + + li(.class("w-full")) { + div( + .class("w-full is-drawer-close:tooltip is-drawer-close:tooltip-right"), + .data("tip", value: "All Projects") ) { - div(.class("flex is-drawer-open:space-x-4")) { - span { "<" } - span(.class("is-drawer-close:hidden")) { "All Projects" } + a( + .class( + """ + flex btn btn-secondary btn-square btn-block + is-drawer-close:items-center + """ + ), + .hx.get(route: .project(.index)), + .hx.target("body"), + .hx.pushURL(true), + .hx.swap(.outerHTML), + ) { + div(.class("flex is-drawer-open:space-x-4")) { + span { "<" } + span(.class("is-drawer-close:hidden")) { "All Projects" } + } } } } - } - // FIX: Move to user profile / settings page. - li(.class("w-full is-drawer-close:hidden")) { - div(.class("flex justify-between p-4")) { - Label("Theme") - input(.type(.checkbox), .class("toggle theme-controller"), .value("light")) + // FIX: Move to user profile / settings page. + li(.class("w-full is-drawer-close:hidden")) { + div(.class("flex justify-between p-4")) { + Label("Theme") + input(.type(.checkbox), .class("toggle theme-controller"), .value("light")) + } } - } - li(.class("w-full")) { - row( - title: "Project", - icon: .mapPin, - route: .project(.detail(projectID, .index(tab: .project))), - isComplete: true - ) - .attributes(.class("btn-active"), when: active == .project) - } + li(.class("w-full")) { + row( + title: "Project", + icon: .mapPin, + route: .project(.detail(projectID, .index(tab: .project))), + isComplete: true + ) + .attributes(.class("btn-active"), when: active == .project) + } - li(.class("w-full")) { - row( - title: "Rooms", - icon: .doorClosed, - route: .project(.detail(projectID, .rooms(.index))), - isComplete: completedSteps.rooms - ) - .attributes(.class("btn-active"), when: active == .rooms) - } + li(.class("w-full")) { + row( + title: "Rooms", + icon: .doorClosed, + route: .project(.detail(projectID, .rooms(.index))), + isComplete: completedSteps.rooms + ) + .attributes(.class("btn-active"), when: active == .rooms) + } - li(.class("w-full")) { - row( - title: "Equivalent Lengths", - icon: .rulerDimensionLine, - route: .project(.detail(projectID, .equivalentLength(.index))), - isComplete: completedSteps.equivalentLength - ) - .attributes(.class("btn-active"), when: active == .equivalentLength) + li(.class("w-full")) { + row( + title: "Equivalent Lengths", + icon: .rulerDimensionLine, + route: .project(.detail(projectID, .equivalentLength(.index))), + isComplete: completedSteps.equivalentLength + ) + .attributes(.class("btn-active"), when: active == .equivalentLength) - } - li(.class("w-full")) { - row( - title: "Friction Rate", - icon: .squareFunction, - route: .project(.detail(projectID, .frictionRate(.index))), - isComplete: completedSteps.frictionRate - ) - .attributes(.class("btn-active"), when: active == .frictionRate) + } + li(.class("w-full")) { + row( + title: "Friction Rate", + icon: .squareFunction, + route: .project(.detail(projectID, .frictionRate(.index))), + isComplete: completedSteps.frictionRate + ) + .attributes(.class("btn-active"), when: active == .frictionRate) - } - li(.class("w-full")) { - row( - title: "Duct Sizes", icon: .wind, href: "#", isComplete: false, hideIsComplete: true - ) - .attributes(.class("btn-active"), when: active == .ductSizing) + } + li(.class("w-full")) { + row( + title: "Duct Sizes", icon: .wind, href: "#", isComplete: false, hideIsComplete: true + ) + .attributes(.class("btn-active"), when: active == .ductSizing) + } } } } } - } - // TODO: Use SiteRoute.View routes as href. - private func row( - title: String, - icon: SVG.Key, - href: String, - isComplete: Bool, - hideIsComplete: Bool = false - ) -> some HTML { - div( - .class( - "w-full is-drawer-close:tooltip is-drawer-close:tooltip-right" - ), - .data("tip", value: title) - ) { + // TODO: Use SiteRoute.View routes as href. + private func row( + title: String, + icon: SVG.Key, + href: String, + isComplete: Bool, + hideIsComplete: Bool = false + ) -> some HTML { a( .class( - "flex btn btn-soft btn-square btn-block is-drawer-open:justify-between is-drawer-close:items-center" + """ + flex w-full btn btn-soft btn-square btn-block + is-drawer-open:justify-between is-drawer-close:items-center + is-drawer-close:tooltip is-drawer-close:tooltip-right + """ ), - .href(href) + .href(href), + .data("tip", value: title) ) { div(.class("flex is-drawer-open:space-x-4")) { SVG(icon) @@ -226,18 +227,18 @@ struct Sidebar: HTML { .attributes(.class("is-drawer-close:text-green-400"), when: isComplete) .attributes(.class("is-drawer-close:text-error"), when: !isComplete && !hideIsComplete) } - } - private func row( - title: String, - icon: SVG.Key, - route: SiteRoute.View, - isComplete: Bool, - hideIsComplete: Bool = false - ) -> some HTML { - row( - title: title, icon: icon, href: SiteRoute.View.router.path(for: route), - isComplete: isComplete, hideIsComplete: hideIsComplete - ) + private func row( + title: String, + icon: SVG.Key, + route: SiteRoute.View, + isComplete: Bool, + hideIsComplete: Bool = false + ) -> some HTML { + row( + title: title, icon: icon, href: SiteRoute.View.router.path(for: route), + isComplete: isComplete, hideIsComplete: hideIsComplete + ) + } } } diff --git a/Sources/ViewController/Views/Rooms/RoomForm.swift b/Sources/ViewController/Views/Rooms/RoomForm.swift index 4b01083..0f135f8 100644 --- a/Sources/ViewController/Views/Rooms/RoomForm.swift +++ b/Sources/ViewController/Views/Rooms/RoomForm.swift @@ -27,6 +27,13 @@ struct RoomForm: HTML, Sendable { self.room = room } + var route: String { + SiteRoute.View.router.path( + for: .project(.detail(projectID, .rooms(.index))) + ) + .appendingPath(room?.id) + } + var body: some HTML { ModalForm(id: id, dismiss: dismiss) { h1(.class("text-3xl font-bold pb-6")) { "Room" } @@ -34,8 +41,8 @@ struct RoomForm: HTML, Sendable { .class("modal-backdrop"), .init(name: "method", value: "dialog"), room == nil - ? .hx.post(route: .project(.detail(projectID, .rooms(.index)))) - : .hx.patch(route: .project(.detail(projectID, .rooms(.index)))), + ? .hx.post(route) + : .hx.patch(route), .hx.target("body"), .hx.swap(.outerHTML) ) { diff --git a/Sources/ViewController/Views/Rooms/RoomsView.swift b/Sources/ViewController/Views/Rooms/RoomsView.swift index 6341874..db5e588 100644 --- a/Sources/ViewController/Views/Rooms/RoomsView.swift +++ b/Sources/ViewController/Views/Rooms/RoomsView.swift @@ -121,18 +121,22 @@ struct RoomsView: HTML, Sendable { Number(room.registerCount) } td { - div(.class("flex justify-end space-x-6")) { - TrashButton() - .attributes( - .hx.delete( - route: .project(.detail(room.projectID, .rooms(.delete(id: room.id))))), - .hx.target("closest tr"), - .hx.confirm("Are you sure?") - ) - EditButton() - .attributes( - .showModal(id: "roomForm_\(room.name)") - ) + div(.class("flex justify-end")) { + div(.class("join")) { + TrashButton() + .attributes( + .class("join-item"), + .hx.delete( + route: .project(.detail(room.projectID, .rooms(.delete(id: room.id))))), + .hx.target("closest tr"), + .hx.confirm("Are you sure?") + ) + EditButton() + .attributes( + .class("join-item"), + .showModal(id: "roomForm_\(room.name)") + ) + } } RoomForm( id: "roomForm_\(room.name)",