feat: Cleans up / moves helpers for view controller to project client.

This commit is contained in:
2026-01-16 09:31:35 -05:00
parent dbec7fb920
commit d14477e97a
10 changed files with 194 additions and 193 deletions

View File

@@ -0,0 +1,12 @@
import Fluent
extension PageRequest {
public static var first: Self {
.init(page: 1, per: 25)
}
public static func next<T>(_ currentPage: Page<T>) -> Self {
.init(page: currentPage.metadata.page + 1, per: currentPage.metadata.per)
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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)
)
)
)
}
)
}

View File

@@ -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<Project> {
try await fetch(userID, .init(page: page, per: limit))
}
func fetchPage(
userID: User.ID,
page: PageRequest
) async throws -> Page<Project> {
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<T>(_ currentPage: Page<T>) -> Self {
.init(page: currentPage.metadata.page + 1, per: currentPage.metadata.per)
}
}

View File

@@ -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
//
// }
// }

View File

@@ -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

View File

@@ -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")) {