diff --git a/Public/css/output.css b/Public/css/output.css index 36ea1fd..259c4eb 100644 --- a/Public/css/output.css +++ b/Public/css/output.css @@ -5231,6 +5231,9 @@ .m-1 { margin: calc(var(--spacing) * 1); } + .m-4 { + margin: calc(var(--spacing) * 4); + } .m-6 { margin: calc(var(--spacing) * 6); } @@ -5374,6 +5377,9 @@ .-my-2 { margin-block: calc(var(--spacing) * -2); } + .-my-4 { + margin-block: calc(var(--spacing) * -4); + } .my-1 { margin-block: calc(var(--spacing) * 1); } @@ -5646,6 +5652,9 @@ .mt-1 { margin-top: calc(var(--spacing) * 1); } + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } .mt-4 { margin-top: calc(var(--spacing) * 4); } @@ -6604,9 +6613,6 @@ .max-w-1\/3 { max-width: calc(1/3 * 100%); } - .max-w-\[300px\] { - max-width: 300px; - } .flex-1 { flex: 1; } @@ -6812,6 +6818,9 @@ .flex-col { flex-direction: column; } + .flex-row { + flex-direction: row; + } .flex-wrap { flex-wrap: wrap; } @@ -7905,6 +7914,12 @@ .py-2 { padding-block: calc(var(--spacing) * 2); } + .py-4 { + padding-block: calc(var(--spacing) * 4); + } + .py-6 { + padding-block: calc(var(--spacing) * 6); + } .ps-2 { padding-inline-start: calc(var(--spacing) * 2); } @@ -7982,6 +7997,14 @@ font-size: var(--text-3xl); line-height: var(--tw-leading, var(--text-3xl--line-height)); } + .text-4xl { + font-size: var(--text-4xl); + line-height: var(--tw-leading, var(--text-4xl--line-height)); + } + .text-base { + font-size: var(--text-base); + line-height: var(--tw-leading, var(--text-base--line-height)); + } .text-lg { font-size: var(--text-lg); line-height: var(--tw-leading, var(--text-lg--line-height)); diff --git a/Sources/DatabaseClient/TrunkSizes.swift b/Sources/DatabaseClient/TrunkSizes.swift index d96b4d5..e5c851b 100644 --- a/Sources/DatabaseClient/TrunkSizes.swift +++ b/Sources/DatabaseClient/TrunkSizes.swift @@ -11,6 +11,9 @@ extension DatabaseClient { public var delete: @Sendable (DuctSizing.TrunkSize.ID) async throws -> Void public var fetch: @Sendable (Project.ID) async throws -> [DuctSizing.TrunkSize] public var get: @Sendable (DuctSizing.TrunkSize.ID) async throws -> DuctSizing.TrunkSize? + public var update: + @Sendable (DuctSizing.TrunkSize.ID, DuctSizing.TrunkSize.Update) async throws -> + DuctSizing.TrunkSize } } @@ -82,6 +85,21 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey { return nil } return try await model.toDTO(on: database) + }, + update: { id, updates in + guard + let model = + try await TrunkModel + .query(on: database) + .with(\.$rooms) + .filter(\.$id == id) + .first() + else { + throw NotFoundError() + } + try updates.validate() + try await model.applyUpdates(updates, on: database) + return try await model.toDTO(on: database) } ) } @@ -107,7 +125,21 @@ extension DuctSizing.TrunkSize.Create { height: height ) } +} +extension DuctSizing.TrunkSize.Update { + func validate() throws(ValidationError) { + if let rooms { + guard rooms.count > 0 else { + throw ValidationError("Trunk size should have associated rooms / registers.") + } + } + if let height { + guard height > 0 else { + throw ValidationError("Trunk size height should be greater than 0.") + } + } + } } extension DuctSizing.TrunkSize { @@ -250,4 +282,62 @@ final class TrunkModel: Model, @unchecked Sendable { ) } + + func applyUpdates( + _ updates: DuctSizing.TrunkSize.Update, + on database: any Database + ) async throws { + if let type = updates.type, type.rawValue != self.type { + self.type = type.rawValue + } + if let height = updates.height, height != self.height { + self.height = height + } + if hasChanges { + try await self.save(on: database) + } + + guard let updateRooms = updates.rooms else { + return + } + + // Update rooms. + let rooms = try await TrunkRoomModel.query(on: database) + .with(\.$room) + .filter(\.$trunk.$id == requireID()) + .all() + + for (roomID, registers) in updateRooms { + if let currRoom = rooms.first(where: { $0.$room.id == roomID }) { + database.logger.debug("CURRENT ROOM: \(currRoom.room.name)") + if registers != currRoom.registers { + database.logger.debug("Updating registers for: \(currRoom.room.name)") + currRoom.registers = registers + } + if currRoom.hasChanges { + try await currRoom.save(on: database) + } + } else { + database.logger.debug("CREATING NEW TrunkRoomModel") + let newModel = try TrunkRoomModel( + trunkID: requireID(), + roomID: roomID, + registers: registers, + type: .init(rawValue: type)! + ) + try await newModel.save(on: database) + } + } + + let roomsToDelete = rooms.filter { + !updateRooms.keys.contains($0.$room.id) + } + + for room in roomsToDelete { + try await room.delete(on: database) + } + + database.logger.debug("DONE WITH UPDATES") + + } } diff --git a/Sources/ManualDCore/DuctSizing.swift b/Sources/ManualDCore/DuctSizing.swift index e165943..9b7b34f 100644 --- a/Sources/ManualDCore/DuctSizing.swift +++ b/Sources/ManualDCore/DuctSizing.swift @@ -126,6 +126,8 @@ public enum DuctSizing { extension DuctSizing { + // Represents the database model that the duct sizes have been calculated + // for. public struct TrunkContainer: Codable, Equatable, Identifiable, Sendable { public var id: TrunkSize.ID { trunk.id } @@ -141,6 +143,7 @@ extension DuctSizing { } } + // Represents the database model. public struct TrunkSize: Codable, Equatable, Identifiable, Sendable { public let id: UUID @@ -187,6 +190,23 @@ extension DuctSizing.TrunkSize { } } + public struct Update: Codable, Equatable, Sendable { + + public let type: TrunkType? + public let rooms: [Room.ID: [Int]]? + public let height: Int? + + public init( + type: DuctSizing.TrunkSize.TrunkType? = nil, + rooms: [Room.ID: [Int]]? = nil, + height: Int? = nil + ) { + self.type = type + self.rooms = rooms + self.height = height + } + } + // TODO: Make registers non-optional public struct RoomProxy: Codable, Equatable, Identifiable, Sendable { diff --git a/Sources/ViewController/Extensions/TrunkSizeForm+extensions.swift b/Sources/ViewController/Extensions/TrunkSizeForm+extensions.swift index 92b3ef7..2212b38 100644 --- a/Sources/ViewController/Extensions/TrunkSizeForm+extensions.swift +++ b/Sources/ViewController/Extensions/TrunkSizeForm+extensions.swift @@ -13,6 +13,14 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute.TrunkSizeForm { ) } + func toUpdate(logger: Logger? = nil) throws -> DuctSizing.TrunkSize.Update { + try .init( + 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 { diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index 3461af6..61c4933 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -581,8 +581,9 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute { } case .update(let id, let form): - // FIX: - fatalError() + return await view(on: request, projectID: projectID) { + _ = try await database.trunkSizes.update(id, form.toUpdate()) + } } } } diff --git a/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift b/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift index 89b91ff..7ba38d9 100644 --- a/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift +++ b/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift @@ -176,6 +176,7 @@ struct DuctSizingView: HTML, Sendable { } struct TrunkTable: HTML, Sendable { + let trunks: [DuctSizing.TrunkContainer] let rooms: [DuctSizing.RoomContainer] @@ -196,86 +197,89 @@ struct DuctSizingView: HTML, Sendable { } 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. - } - } + TrunkRow(trunk: trunk, rooms: rooms) } } } } } - func registerIDS(_ trunk: DuctSizing.TrunkSize) -> [String] { + } + + struct TrunkRow: HTML, Sendable { + + @Environment(ProjectViewValue.$projectID) var projectID + + let trunk: DuctSizing.TrunkContainer + let rooms: [DuctSizing.RoomContainer] + + var body: some HTML { + tr { + td(.class("space-x-2")) { + 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: deleteRoute), + .hx.target("closest tr"), + .hx.swap(.outerHTML) + ) + + EditButton() + .attributes( + .class("join-item btn-ghost"), + .showModal(id: TrunkSizeForm.id(trunk)) + ) + } + } + } + TrunkSizeForm(trunk: trunk, rooms: rooms, dismiss: true) + } + } + } + + private var deleteRoute: SiteRoute.View { + .project(.detail(projectID, .ductSizing(.trunk(.delete(trunk.id))))) + } + + private 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 = diff --git a/Sources/ViewController/Views/DuctSizing/TrunkSizeForm.swift b/Sources/ViewController/Views/DuctSizing/TrunkSizeForm.swift index c1aafd7..ea48368 100644 --- a/Sources/ViewController/Views/DuctSizing/TrunkSizeForm.swift +++ b/Sources/ViewController/Views/DuctSizing/TrunkSizeForm.swift @@ -5,27 +5,47 @@ import Styleguide struct TrunkSizeForm: HTML, Sendable { - static func id() -> String { - "trunkSizeForm" + static func id(_ trunk: DuctSizing.TrunkContainer? = nil) -> String { + let base = "trunkSizeForm" + guard let trunk else { return base } + return "\(base)_\(trunk.id.idString)" } @Environment(ProjectViewValue.$projectID) var projectID + let container: DuctSizing.TrunkContainer? let rooms: [DuctSizing.RoomContainer] let dismiss: Bool + var trunk: DuctSizing.TrunkSize? { + container?.trunk + } + + init( + trunk: DuctSizing.TrunkContainer? = nil, + rooms: [DuctSizing.RoomContainer], + dismiss: Bool = true + ) { + self.container = trunk + self.rooms = rooms + self.dismiss = dismiss + } + var route: String { SiteRoute.View.router .path(for: .project(.detail(projectID, .ductSizing(.index)))) .appendingPath(SiteRoute.View.ProjectRoute.DuctSizingRoute.TrunkRoute.rootPath) + .appendingPath(trunk?.id) } var body: some HTML { - ModalForm(id: Self.id(), dismiss: dismiss) { + ModalForm(id: Self.id(container), dismiss: dismiss) { h1(.class("text-lg font-bold mb-4")) { "Trunk Size" } form( .class("space-y-4"), - .hx.post(route), + trunk == nil + ? .hx.post(route) + : .hx.patch(route), .hx.target("body"), .hx.swap(.outerHTML) ) { @@ -38,6 +58,7 @@ struct TrunkSizeForm: HTML, Sendable { select(.name("type")) { for type in DuctSizing.TrunkSize.TrunkType.allCases { option(.value(type.rawValue)) { type.rawValue.capitalized } + .attributes(.selected, when: trunk?.type == type) } } } @@ -46,6 +67,7 @@ struct TrunkSizeForm: HTML, Sendable { "Height", .type(.text), .name("height"), + .value(trunk?.height), .placeholder("8 (Optional)"), ) } @@ -63,6 +85,10 @@ struct TrunkSizeForm: HTML, Sendable { .name("rooms"), .value("\(room.roomID)_\(room.roomRegister)") ) + .attributes( + .checked, + when: trunk == nil ? false : trunk!.rooms.hasRoom(room) + ) } } } @@ -75,3 +101,12 @@ struct TrunkSizeForm: HTML, Sendable { } } + +extension Array where Element == DuctSizing.TrunkSize.RoomProxy { + func hasRoom(_ room: DuctSizing.RoomContainer) -> Bool { + first { + $0.id == room.roomID + && $0.registers.contains(room.roomRegister) + } != nil + } +}