diff --git a/Sources/ManualDCore/PageRequest+extensions.swift b/Sources/ManualDCore/PageRequest+extensions.swift new file mode 100644 index 0000000..9fb8a38 --- /dev/null +++ b/Sources/ManualDCore/PageRequest+extensions.swift @@ -0,0 +1,12 @@ +import Fluent + +extension PageRequest { + + public static var first: Self { + .init(page: 1, per: 25) + } + + public static func next(_ currentPage: Page) -> Self { + .init(page: currentPage.metadata.page + 1, per: currentPage.metadata.per) + } +} diff --git a/Sources/ProjectClient/Interface.swift b/Sources/ProjectClient/Interface.swift index ee173b1..a4e0ea2 100644 --- a/Sources/ProjectClient/Interface.swift +++ b/Sources/ProjectClient/Interface.swift @@ -1,5 +1,6 @@ import Dependencies import DependenciesMacros +import ManualDClient import ManualDCore extension DependencyValues { @@ -11,7 +12,16 @@ extension DependencyValues { @DependencyClient public struct ProjectClient: Sendable { - public var calculateDuctSizes: @Sendable (Project.ID) async throws -> ProjectResponse + public var calculateDuctSizes: @Sendable (Project.ID) async throws -> DuctSizeResponse + public var calculateRoomDuctSizes: + @Sendable (Project.ID) async throws -> [DuctSizing.RoomContainer] + public var calculateTrunkDuctSizes: + @Sendable (Project.ID) async throws -> [DuctSizing.TrunkContainer] + + public var createProject: + @Sendable (User.ID, Project.Create) async throws -> CreateProjectResponse + + public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse } extension ProjectClient: TestDependencyKey { @@ -20,7 +30,27 @@ extension ProjectClient: TestDependencyKey { extension ProjectClient { - public struct ProjectResponse: Codable, Equatable, Sendable { + public struct CreateProjectResponse: Codable, Equatable, Sendable { + + public let projectID: Project.ID + public let rooms: [Room] + public let sensibleHeatRatio: Double? + public let completedSteps: Project.CompletedSteps + + public init( + projectID: Project.ID, + rooms: [Room], + sensibleHeatRatio: Double? = nil, + completedSteps: Project.CompletedSteps + ) { + self.projectID = projectID + self.rooms = rooms + self.sensibleHeatRatio = sensibleHeatRatio + self.completedSteps = completedSteps + } + } + + public struct DuctSizeResponse: Codable, Equatable, Sendable { public let rooms: [DuctSizing.RoomContainer] public let trunks: [DuctSizing.TrunkContainer] @@ -32,4 +62,21 @@ extension ProjectClient { self.trunks = trunks } } + + public struct FrictionRateResponse: Codable, Equatable, Sendable { + + public let componentLosses: [ComponentPressureLoss] + public let equivalentLengths: EffectiveLength.MaxContainer + public let frictionRate: ManualDClient.FrictionRateResponse? + + public init( + componentLosses: [ComponentPressureLoss], + equivalentLengths: EffectiveLength.MaxContainer, + frictionRate: ManualDClient.FrictionRateResponse? = nil + ) { + self.componentLosses = componentLosses + self.equivalentLengths = equivalentLengths + self.frictionRate = frictionRate + } + } } diff --git a/Sources/ProjectClient/Internal/ComponentLoss+createDefaults.swift b/Sources/ProjectClient/Internal/ComponentLoss+createDefaults.swift new file mode 100644 index 0000000..14ea334 --- /dev/null +++ b/Sources/ProjectClient/Internal/ComponentLoss+createDefaults.swift @@ -0,0 +1,12 @@ +import DatabaseClient +import ManualDCore + +extension DatabaseClient.ComponentLoss { + + func createDefaults(projectID: Project.ID) async throws { + let defaults = ComponentPressureLoss.Create.default(projectID: projectID) + for loss in defaults { + _ = try await create(loss) + } + } +} diff --git a/Sources/ProjectClient/Internal/DatabaseClient+calculateDuctSizes.swift b/Sources/ProjectClient/Internal/DatabaseClient+calculateDuctSizes.swift index 984d77c..6e9d123 100644 --- a/Sources/ProjectClient/Internal/DatabaseClient+calculateDuctSizes.swift +++ b/Sources/ProjectClient/Internal/DatabaseClient+calculateDuctSizes.swift @@ -7,24 +7,55 @@ extension DatabaseClient { func calculateDuctSizes( projectID: Project.ID - ) async throws -> ProjectClient.ProjectResponse { + ) async throws -> ProjectClient.DuctSizeResponse { @Dependency(\.manualD) var manualD + return try await manualD.calculateDuctSizes( + rooms: rooms.fetch(projectID), + trunks: trunkSizes.fetch(projectID), + sharedRequest: sharedDuctRequest(projectID) + ) + } + + func calculateRoomDuctSizes( + projectID: Project.ID + ) async throws -> [DuctSizing.RoomContainer] { + @Dependency(\.manualD) var manualD + + return try await manualD.calculateRoomSizes( + rooms: rooms.fetch(projectID), + sharedRequest: sharedDuctRequest(projectID) + ) + } + + func calculateTrunkDuctSizes( + projectID: Project.ID + ) async throws -> [DuctSizing.TrunkContainer] { + @Dependency(\.manualD) var manualD + + return try await manualD.calculateTrunkSizes( + rooms: rooms.fetch(projectID), + trunks: trunkSizes.fetch(projectID), + sharedRequest: sharedDuctRequest(projectID) + ) + } + + func sharedDuctRequest(_ projectID: Project.ID) async throws -> DuctSizeSharedRequest { + guard let dfrResponse = try await designFrictionRate(projectID: projectID) else { throw DuctCalcClientError("Project not complete.") } let ensuredTEL = try dfrResponse.ensureMaxContainer() - return try await manualD.calculateDuctSizes( - rooms: rooms.fetch(projectID), - trunks: trunkSizes.fetch(projectID), + return try await .init( equipmentInfo: dfrResponse.equipmentInfo, maxSupplyLength: ensuredTEL.supply, - maxReturnLength: ensuredTEL.return, + maxReturnLenght: ensuredTEL.return, designFrictionRate: dfrResponse.designFrictionRate, projectSHR: ensuredSHR(projectID) ) + } // Fetches the project sensible heat ratio or throws an error if it's nil. diff --git a/Sources/ProjectClient/Internal/ManualDClient+calculateDuctSizes.swift b/Sources/ProjectClient/Internal/ManualDClient+calculateDuctSizes.swift index 5c22fa5..45fc6e7 100644 --- a/Sources/ProjectClient/Internal/ManualDClient+calculateDuctSizes.swift +++ b/Sources/ProjectClient/Internal/ManualDClient+calculateDuctSizes.swift @@ -2,6 +2,14 @@ import Logging import ManualDClient import ManualDCore +struct DuctSizeSharedRequest { + let equipmentInfo: EquipmentInfo + let maxSupplyLength: EffectiveLength + let maxReturnLenght: EffectiveLength + let designFrictionRate: Double + let projectSHR: Double +} + // TODO: Remove Logger and use depedency logger. extension ManualDClient { @@ -9,58 +17,42 @@ extension ManualDClient { func calculateDuctSizes( rooms: [Room], trunks: [DuctSizing.TrunkSize], - equipmentInfo: EquipmentInfo, - maxSupplyLength: EffectiveLength, - maxReturnLength: EffectiveLength, - designFrictionRate: Double, - projectSHR: Double, + sharedRequest: DuctSizeSharedRequest, logger: Logger? = nil - ) async throws -> ProjectClient.ProjectResponse { + ) async throws -> ProjectClient.DuctSizeResponse { try await .init( rooms: calculateRoomSizes( rooms: rooms, - equipmentInfo: equipmentInfo, - maxSupplyLength: maxSupplyLength, - maxReturnLength: maxReturnLength, - designFrictionRate: designFrictionRate, - projectSHR: projectSHR + sharedRequest: sharedRequest ), trunks: calculateTrunkSizes( rooms: rooms, trunks: trunks, - equipmentInfo: equipmentInfo, - maxSupplyLength: maxSupplyLength, - maxReturnLength: maxReturnLength, - designFrictionRate: designFrictionRate, - projectSHR: projectSHR + sharedRequest: sharedRequest ) ) } func calculateRoomSizes( rooms: [Room], - equipmentInfo: EquipmentInfo, - maxSupplyLength: EffectiveLength, - maxReturnLength: EffectiveLength, - designFrictionRate: Double, - projectSHR: Double, + sharedRequest: DuctSizeSharedRequest, logger: Logger? = nil ) async throws -> [DuctSizing.RoomContainer] { var retval: [DuctSizing.RoomContainer] = [] let totalHeatingLoad = rooms.totalHeatingLoad - let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR) + let totalCoolingSensible = rooms.totalCoolingSensible(shr: sharedRequest.projectSHR) for room in rooms { let heatingLoad = room.heatingLoadPerRegister - let coolingLoad = room.coolingSensiblePerRegister(projectSHR: projectSHR) + let coolingLoad = room.coolingSensiblePerRegister(projectSHR: sharedRequest.projectSHR) let heatingPercent = heatingLoad / totalHeatingLoad let coolingPercent = coolingLoad / totalCoolingSensible - let heatingCFM = heatingPercent * Double(equipmentInfo.heatingCFM) - let coolingCFM = coolingPercent * Double(equipmentInfo.coolingCFM) + let heatingCFM = heatingPercent * Double(sharedRequest.equipmentInfo.heatingCFM) + let coolingCFM = coolingPercent * Double(sharedRequest.equipmentInfo.coolingCFM) let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM) let sizes = try await self.ductSize( - .init(designCFM: Int(designCFM.value), frictionRate: designFrictionRate) + .init(designCFM: Int(designCFM.value), frictionRate: sharedRequest.designFrictionRate) ) for n in 1...room.registerCount { @@ -103,28 +95,24 @@ extension ManualDClient { func calculateTrunkSizes( rooms: [Room], trunks: [DuctSizing.TrunkSize], - equipmentInfo: EquipmentInfo, - maxSupplyLength: EffectiveLength, - maxReturnLength: EffectiveLength, - designFrictionRate: Double, - projectSHR: Double, + sharedRequest: DuctSizeSharedRequest, logger: Logger? = nil ) async throws -> [DuctSizing.TrunkContainer] { var retval = [DuctSizing.TrunkContainer]() let totalHeatingLoad = rooms.totalHeatingLoad - let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR) + let totalCoolingSensible = rooms.totalCoolingSensible(shr: sharedRequest.projectSHR) for trunk in trunks { let heatingLoad = trunk.totalHeatingLoad - let coolingLoad = trunk.totalCoolingSensible(projectSHR: projectSHR) + let coolingLoad = trunk.totalCoolingSensible(projectSHR: sharedRequest.projectSHR) let heatingPercent = heatingLoad / totalHeatingLoad let coolingPercent = coolingLoad / totalCoolingSensible - let heatingCFM = heatingPercent * Double(equipmentInfo.heatingCFM) - let coolingCFM = coolingPercent * Double(equipmentInfo.coolingCFM) + let heatingCFM = heatingPercent * Double(sharedRequest.equipmentInfo.heatingCFM) + let coolingCFM = coolingPercent * Double(sharedRequest.equipmentInfo.coolingCFM) let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM) let sizes = try await self.ductSize( - .init(designCFM: Int(designCFM.value), frictionRate: designFrictionRate) + .init(designCFM: Int(designCFM.value), frictionRate: sharedRequest.designFrictionRate) ) var width: Int? = nil if let height = trunk.height { diff --git a/Sources/ProjectClient/Live.swift b/Sources/ProjectClient/Live.swift index 28fbd85..1c69e52 100644 --- a/Sources/ProjectClient/Live.swift +++ b/Sources/ProjectClient/Live.swift @@ -8,10 +8,53 @@ extension ProjectClient: DependencyKey { public static var liveValue: Self { @Dependency(\.database) var database + @Dependency(\.manualD) var manualD return .init( calculateDuctSizes: { projectID in try await database.calculateDuctSizes(projectID: projectID) + }, + calculateRoomDuctSizes: { projectID in + try await database.calculateRoomDuctSizes(projectID: projectID) + }, + calculateTrunkDuctSizes: { projectID in + try await database.calculateTrunkDuctSizes(projectID: projectID) + }, + createProject: { userID, request in + let project = try await database.projects.create(userID, request) + try await database.componentLoss.createDefaults(projectID: project.id) + return try await .init( + projectID: project.id, + rooms: database.rooms.fetch(project.id), + sensibleHeatRatio: database.projects.getSensibleHeatRatio(project.id), + completedSteps: database.projects.getCompletedSteps(project.id) + ) + }, + frictionRate: { projectID in + + let componentLosses = try await database.componentLoss.fetch(projectID) + let lengths = try await database.effectiveLength.fetchMax(projectID) + + let equipmentInfo = try await database.equipment.fetch(projectID) + guard let staticPressure = equipmentInfo?.staticPressure else { + return .init(componentLosses: componentLosses, equivalentLengths: lengths) + } + + guard let totalEquivalentLength = lengths.total else { + return .init(componentLosses: componentLosses, equivalentLengths: lengths) + } + + return try await .init( + componentLosses: componentLosses, + equivalentLengths: lengths, + frictionRate: manualD.frictionRate( + .init( + externalStaticPressure: staticPressure, + componentPressureLosses: database.componentLoss.fetch(projectID), + totalEffectiveLength: Int(totalEquivalentLength) + ) + ) + ) } ) } diff --git a/Sources/ViewController/Extensions/DatabaseExtensions.swift b/Sources/ViewController/Extensions/DatabaseExtensions.swift deleted file mode 100644 index 15f09c0..0000000 --- a/Sources/ViewController/Extensions/DatabaseExtensions.swift +++ /dev/null @@ -1,78 +0,0 @@ -import DatabaseClient -import Dependencies -import Fluent -import ManualDClient -import ManualDCore -import Vapor - -// FIX: Remove these, not used currently. -extension DatabaseClient.Projects { - - func fetchPage( - userID: User.ID, - page: Int = 1, - limit: Int = 25 - ) async throws -> Page { - try await fetch(userID, .init(page: page, per: limit)) - } - - func fetchPage( - userID: User.ID, - page: PageRequest - ) async throws -> Page { - try await fetch(userID, page) - } -} - -// extension DatabaseClient { -// -// func calculateDuctSizes( -// projectID: Project.ID -// ) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) { -// @Dependency(\.manualD) var manualD -// -// return try await manualD.calculate( -// rooms: rooms.fetch(projectID), -// trunks: trunkSizes.fetch(projectID), -// designFrictionRateResult: designFrictionRate(projectID: projectID), -// projectSHR: projects.getSensibleHeatRatio(projectID) -// ) -// } -// -// func designFrictionRate( -// projectID: Project.ID -// ) async throws -> (EquipmentInfo, EffectiveLength.MaxContainer, Double)? { -// guard let equipmentInfo = try await equipment.fetch(projectID) else { -// return nil -// } -// -// let equivalentLengths = try await effectiveLength.fetchMax(projectID) -// guard let tel = equivalentLengths.total else { return nil } -// -// let componentLosses = try await componentLoss.fetch(projectID) -// guard componentLosses.count > 0 else { return nil } -// -// let availableStaticPressure = -// equipmentInfo.staticPressure - componentLosses.total -// -// let designFrictionRate = (availableStaticPressure * 100) / tel -// -// return (equipmentInfo, equivalentLengths, designFrictionRate) -// } -// } - -extension DatabaseClient.ComponentLoss { - - func createDefaults(projectID: Project.ID) async throws { - let defaults = ComponentPressureLoss.Create.default(projectID: projectID) - for loss in defaults { - _ = try await create(loss) - } - } -} - -extension PageRequest { - static func next(_ currentPage: Page) -> Self { - .init(page: currentPage.metadata.page + 1, per: currentPage.metadata.per) - } -} diff --git a/Sources/ViewController/Extensions/ManualDClient+extensions.swift b/Sources/ViewController/Extensions/ManualDClient+extensions.swift deleted file mode 100644 index 7b56262..0000000 --- a/Sources/ViewController/Extensions/ManualDClient+extensions.swift +++ /dev/null @@ -1,38 +0,0 @@ -// import Logging -// import ManualDClient -// import ManualDCore -// -// extension ManualDClient { -// -// func calculate( -// rooms: [Room], -// trunks: [DuctSizing.TrunkSize], -// designFrictionRateResult: (EquipmentInfo, EffectiveLength.MaxContainer, Double)?, -// projectSHR: Double?, -// logger: Logger? = nil -// ) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) { -// guard let designFrictionRateResult else { return ([], []) } -// let equipmentInfo = designFrictionRateResult.0 -// let effectiveLengths = designFrictionRateResult.1 -// let designFrictionRate = designFrictionRateResult.2 -// -// guard let maxSupply = effectiveLengths.supply else { return ([], []) } -// guard let maxReturn = effectiveLengths.return else { return ([], []) } -// -// let ductRooms = try await self.calculateSizes( -// rooms: rooms, -// trunks: trunks, -// equipmentInfo: equipmentInfo, -// maxSupplyLength: maxSupply, -// maxReturnLength: maxReturn, -// designFrictionRate: designFrictionRate, -// projectSHR: projectSHR ?? 1.0, -// logger: logger -// ) -// -// // logger?.debug("Rooms: \(ductRooms)") -// -// return ductRooms -// -// } -// } diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index c5134d0..dd46031 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -118,6 +118,7 @@ extension SiteRoute.View.ProjectRoute { func renderView(on request: ViewController.Request) async -> AnySendableHTML { @Dependency(\.database) var database + @Dependency(\.projectClient) var projectClient switch self { case .index: @@ -126,7 +127,7 @@ extension SiteRoute.View.ProjectRoute { let user = try request.currentUser() return try await ( user.id, - database.projects.fetchPage(userID: user.id) + database.projects.fetch(user.id, .first) ) } onSuccess: { (userID, projects) in @@ -148,19 +149,14 @@ extension SiteRoute.View.ProjectRoute { return await request.view { await ResultView { let user = try request.currentUser() - let project = try await database.projects.create(user.id, form) - try await database.componentLoss.createDefaults(projectID: project.id) - let rooms = try await database.rooms.fetch(project.id) - let shr = try await database.projects.getSensibleHeatRatio(project.id) - let completedSteps = try await database.projects.getCompletedSteps(project.id) - return (project.id, rooms, shr, completedSteps) - } onSuccess: { (projectID, rooms, shr, completedSteps) in + return try await projectClient.createProject(user.id, form) + } onSuccess: { response in ProjectView( - projectID: projectID, + projectID: response.projectID, activeTab: .rooms, - completedSteps: completedSteps + completedSteps: response.completedSteps ) { - RoomsView(rooms: rooms, sensibleHeatRatio: shr) + RoomsView(rooms: response.rooms, sensibleHeatRatio: response.sensibleHeatRatio) } } } @@ -418,32 +414,21 @@ extension SiteRoute.View.ProjectRoute.ComponentLossRoute { ) async -> AnySendableHTML { @Dependency(\.database) var database - @Dependency(\.manualD) var manualD + @Dependency(\.projectClient) var projectClient return await request.view { await ResultView { try await catching() - - let equipment = try await database.equipment.fetch(projectID) - let componentLosses = try await database.componentLoss.fetch(projectID) - let lengths = try await database.effectiveLength.fetchMax(projectID) - return ( try await database.projects.getCompletedSteps(projectID), - componentLosses, - lengths, - try await manualD.frictionRate( - equipmentInfo: equipment, - componentLosses: componentLosses, - effectiveLength: lengths - ) + try await projectClient.frictionRate(projectID) ) - } onSuccess: { (steps, losses, lengths, frictionRate) in + } onSuccess: { (steps, response) in ProjectView(projectID: projectID, activeTab: .frictionRate, completedSteps: steps) { FrictionRateView( - componentLosses: losses, - equivalentLengths: lengths, - frictionRateResponse: frictionRate + componentLosses: response.componentLosses, + equivalentLengths: response.equivalentLengths, + frictionRateResponse: response.frictionRate ) } @@ -566,8 +551,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute { case .deleteRectangularSize(let roomID, let request): return await ResultView { let room = try await database.rooms.deleteRectangularSize(roomID, request.rectangularSizeID) - return try await projectClient.calculateDuctSizes(projectID) - .rooms + return try await projectClient.calculateRoomDuctSizes(projectID) .filter({ $0.roomID == room.id && $0.roomRegister == request.register }) .first! } onSuccess: { room in @@ -580,8 +564,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute { roomID, .init(id: form.id ?? .init(), register: form.register, height: form.height) ) - return try await projectClient.calculateDuctSizes(projectID) - .rooms + return try await projectClient.calculateRoomDuctSizes(projectID) .filter({ $0.roomID == room.id && $0.roomRegister == form.register }) .first! } onSuccess: { room in diff --git a/Sources/ViewController/Views/Rooms/RoomsView.swift b/Sources/ViewController/Views/Rooms/RoomsView.swift index ed86c4c..a0ed5b2 100644 --- a/Sources/ViewController/Views/Rooms/RoomsView.swift +++ b/Sources/ViewController/Views/Rooms/RoomsView.swift @@ -21,7 +21,7 @@ struct RoomsView: HTML, Sendable { } div(.class("flex justify-end grow")) { - Tooltip("Project wide sensible heat ratio", position: .left) { + Tooltip("Set sensible heat ratio", position: .left) { button( .class( """ @@ -43,6 +43,7 @@ struct RoomsView: HTML, Sendable { } .attributes(.class("border border-error"), when: sensibleHeatRatio == nil) } + .attributes(.class("tooltip-open"), when: sensibleHeatRatio == nil) } div(.class("flex items-end space-x-4 font-bold")) { @@ -67,7 +68,7 @@ struct RoomsView: HTML, Sendable { SHRForm( sensibleHeatRatio: sensibleHeatRatio, - dismiss: sensibleHeatRatio != nil + dismiss: true ) table(.class("table table-zebra text-lg"), .id("roomsTable")) {