From 04a7405ca42421aa7973a8f6a8fd7d475e166e5c Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Sat, 17 Jan 2026 20:40:49 -0500 Subject: [PATCH] WIP: Html view that prints to pdf ok. --- Package.swift | 2 + .../Middleware/DependenciesMiddleware.swift | 2 + Sources/App/configure.swift | 3 +- Sources/ManualDCore/DuctSizes.swift | 14 + Sources/ManualDCore/FrictionRate.swift | 37 ++ Sources/ManualDCore/Numbers+string.swift | 24 + Sources/PdfClient/Interface.swift | 30 +- Sources/PdfClient/Request+html.swift | 531 ++++++++++++++++++ Sources/PdfClient/Request+markdown.swift | 95 +++- Sources/ProjectClient/Interface.swift | 5 + .../DatabaseClient+calculateDuctSizes.swift | 45 +- .../Internal/ManualDClient+frictionRate.swift | 35 ++ Sources/ProjectClient/Live.swift | 68 ++- Sources/Styleguide/Number.swift | 17 +- .../Views/DuctSizing/TrunksTable.swift | 23 +- .../Views/Rooms/RoomsView.swift | 25 +- 16 files changed, 858 insertions(+), 98 deletions(-) create mode 100644 Sources/ManualDCore/Numbers+string.swift create mode 100644 Sources/PdfClient/Request+html.swift create mode 100644 Sources/ProjectClient/Internal/ManualDClient+frictionRate.swift diff --git a/Package.swift b/Package.swift index d1c899c..4213637 100644 --- a/Package.swift +++ b/Package.swift @@ -82,6 +82,7 @@ let package = Package( .target(name: "ManualDCore"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "DependenciesMacros", package: "swift-dependencies"), + .product(name: "Elementary", package: "elementary"), ] ), .target( @@ -89,6 +90,7 @@ let package = Package( dependencies: [ .target(name: "DatabaseClient"), .target(name: "ManualDClient"), + .target(name: "PdfClient"), ] ), .target( diff --git a/Sources/App/Middleware/DependenciesMiddleware.swift b/Sources/App/Middleware/DependenciesMiddleware.swift index 23b676c..4ef42a9 100644 --- a/Sources/App/Middleware/DependenciesMiddleware.swift +++ b/Sources/App/Middleware/DependenciesMiddleware.swift @@ -3,6 +3,7 @@ import AuthClient import DatabaseClient import Dependencies import ManualDCore +import PdfClient import Vapor import ViewController @@ -34,6 +35,7 @@ struct DependenciesMiddleware: AsyncMiddleware { $0.database = database // $0.dateFormatter = .liveValue $0.viewController = viewController + $0.pdfClient = .liveValue } operation: { try await next.respond(to: request) } diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 115153c..c3a99c3 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -132,7 +132,8 @@ private func siteHandler( // FIX: Remove. if route == .test { let projectID = UUID(uuidString: "E796C96C-F527-4753-A00A-EBCF25630663")! - return try await projectClient.calculateDuctSizes(projectID) + // return try await projectClient.toMarkdown(projectID) + return try await AnyHTMLResponse(value: projectClient.toHTML(projectID)) } return try await viewController.respond(route: route, request: request) } diff --git a/Sources/ManualDCore/DuctSizes.swift b/Sources/ManualDCore/DuctSizes.swift index e4b1c8a..9e56ee7 100644 --- a/Sources/ManualDCore/DuctSizes.swift +++ b/Sources/ManualDCore/DuctSizes.swift @@ -130,5 +130,19 @@ extension DuctSizes { public subscript(dynamicMember keyPath: KeyPath) -> T { ductSize[keyPath: keyPath] } + + public func registerIDS(rooms: [RoomContainer]) -> [String] { + trunk.rooms.reduce(into: []) { array, room in + array = room.registers.reduce(into: array) { array, register in + if let room = + rooms + .first(where: { $0.roomID == room.id && $0.roomRegister == register }) + { + array.append(room.roomName) + } + } + } + .sorted() + } } } diff --git a/Sources/ManualDCore/FrictionRate.swift b/Sources/ManualDCore/FrictionRate.swift index 7949765..2f26ff5 100644 --- a/Sources/ManualDCore/FrictionRate.swift +++ b/Sources/ManualDCore/FrictionRate.swift @@ -3,6 +3,7 @@ public struct FrictionRate: Codable, Equatable, Sendable { public let availableStaticPressure: Double public let value: Double + public var hasErrors: Bool { error != nil } public init( availableStaticPressure: Double, @@ -11,4 +12,40 @@ public struct FrictionRate: Codable, Equatable, Sendable { self.availableStaticPressure = availableStaticPressure self.value = value } + + public var error: FrictionRateError? { + if value >= 0.18 { + return .init( + "Friction rate should be lower than 0.18", + resolutions: [ + "Decrease the blower speed", + "Decrease the blower size", + "Increase the Total Equivalent Length", + ] + ) + } else if value <= 0.02 { + return .init( + "Friction rate should be higher than 0.02", + resolutions: [ + "Increase the blower speed", + "Increase the blower size", + "Decrease the Total Equivalent Length", + ] + ) + } + return nil + } +} + +public struct FrictionRateError: Error, Equatable, Sendable { + public let reason: String + public let resolutions: [String] + + public init( + _ reason: String, + resolutions: [String] + ) { + self.reason = reason + self.resolutions = resolutions + } } diff --git a/Sources/ManualDCore/Numbers+string.swift b/Sources/ManualDCore/Numbers+string.swift new file mode 100644 index 0000000..c633eb5 --- /dev/null +++ b/Sources/ManualDCore/Numbers+string.swift @@ -0,0 +1,24 @@ +import Foundation + +extension Double { + + public func string(digits: Int = 2) -> String { + numberString(self, digits: digits) + } +} + +extension Int { + + public func string() -> String { + numberString(Double(self), digits: 0) + } +} + +private func numberString(_ value: Double, digits: Int = 2) -> String { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = digits + formatter.groupingSize = 3 + formatter.groupingSeparator = "," + formatter.numberStyle = .decimal + return formatter.string(for: value)! +} diff --git a/Sources/PdfClient/Interface.swift b/Sources/PdfClient/Interface.swift index e495f1a..5900232 100644 --- a/Sources/PdfClient/Interface.swift +++ b/Sources/PdfClient/Interface.swift @@ -1,14 +1,33 @@ import Dependencies import DependenciesMacros +import Elementary import ManualDCore +extension DependencyValues { + + public var pdfClient: PdfClient { + get { self[PdfClient.self] } + set { self[PdfClient.self] = newValue } + } +} + @DependencyClient public struct PdfClient: Sendable { + public var html: @Sendable (Request) async throws -> (any HTML & Sendable) public var markdown: @Sendable (Request) async throws -> String } -extension PdfClient: TestDependencyKey { +extension PdfClient: DependencyKey { public static let testValue = Self() + + public static let liveValue = Self( + html: { request in + request.toHTML() + }, + markdown: { request in + request.toMarkdown() + } + ) } extension PdfClient { @@ -16,31 +35,34 @@ extension PdfClient { public struct Request: Codable, Equatable, Sendable { public let project: Project + public let rooms: [Room] public let componentLosses: [ComponentPressureLoss] public let ductSizes: DuctSizes public let equipmentInfo: EquipmentInfo public let maxSupplyTEL: EffectiveLength public let maxReturnTEL: EffectiveLength - public let designFrictionRate: FrictionRate + public let frictionRate: FrictionRate public let projectSHR: Double public init( project: Project, + rooms: [Room], componentLosses: [ComponentPressureLoss], ductSizes: DuctSizes, equipmentInfo: EquipmentInfo, maxSupplyTEL: EffectiveLength, maxReturnTEL: EffectiveLength, - designFrictionRate: FrictionRate, + frictionRate: FrictionRate, projectSHR: Double ) { self.project = project + self.rooms = rooms self.componentLosses = componentLosses self.ductSizes = ductSizes self.equipmentInfo = equipmentInfo self.maxSupplyTEL = maxSupplyTEL self.maxReturnTEL = maxReturnTEL - self.designFrictionRate = designFrictionRate + self.frictionRate = frictionRate self.projectSHR = projectSHR } } diff --git a/Sources/PdfClient/Request+html.swift b/Sources/PdfClient/Request+html.swift new file mode 100644 index 0000000..e8f803a --- /dev/null +++ b/Sources/PdfClient/Request+html.swift @@ -0,0 +1,531 @@ +import Elementary +import ManualDCore + +extension PdfClient.Request { + + func toHTML() -> (some HTML & Sendable) { + PdfDocument(request: self) + } +} + +struct PdfDocument: HTMLDocument { + + let title = "Duct Calc" + let lang = "en" + let request: PdfClient.Request + + var head: some HTML { + style { + """ + @media print { + body { + -webkit-print-color-adjust: exact; + color-adjust: exact; + print-color-adjust: exact; + } + table td, table th { + -webkit-print-color-adjust: exact; + } + } + table { + max-width: 100%; + border-collapse: collapse; + margin: 10px auto; + border: 1px solid #ccc; + } + th, td { + border: 1px solid #ccc; + padding: 10px; + } + tr:nth-child(even) { + background-color: #f2f2f2; + } + .w-full { + width: 100%; + } + .w-half { + width: 50%; + } + .table-footer { + background-color: #75af4c; + color: white; + font-weight: bold; + } + .bg-green { + background-color: #4CAF50; + color: white; + } + .heating { + color: red; + } + .coolingTotal { + color: blue; + } + .coolingSensible { + color: cyan; + } + .justify-end { + text-align: end; + } + .flex { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 10px; + } + .flex table { + border: 1px solid #ccc; + width: 50%; + margin: 0; + flex: 1 1 calc(50% - 10px); + } + .container { + display: flex; + width: 100%; + gap: 10px; + } + .table-container { + flex: 1; + min-width: 0; + } + .table-container table { + width: 100%; + border-collapse: collapse; + } + .customerTable { + width: 50%; + } + .section { + padding: 10px; + } + .label { + font-weight: bold; + } + .error { + color: red; + font-weight: bold; + } + .effectiveLengthGroupTable, .effectiveLengthGroupHeader { + background-color: white; + color: black; + font-weight: bold; + } + .headline { + padding: 10px 0; + } + """ + } + } + + var body: some HTML { + div { + h1(.class("headline")) { "Duct Calc" } + + h2 { "Project" } + + div(.class("flex")) { + table(.class("table customer-table")) { + tbody { + tr { + td { "Name" } + td { request.project.name } + } + tr { + td { "Street Address" } + td { + p { + request.project.streetAddress + br() + request.project.cityStateZipString + } + } + } + } + } + // HACK: + table {} + } + + div(.class("section")) { + div(.class("flex")) { + h2 { "Equipment" } + h2 { "Friction Rate" } + } + div(.class("flex")) { + div(.class("container")) { + div(.class("table-container")) { + EquipmentTable(title: "Equipment", equipmentInfo: request.equipmentInfo) + } + // .attributes(.style("height: 140px;")) + div(.class("table-container")) { + FrictionRateTable( + title: "Friction Rate", + componentLosses: request.componentLosses, + frictionRate: request.frictionRate, + totalEquivalentLength: request.totalEquivalentLength, + displayTotals: false + ) + } + } + } + if let error = request.frictionRate.error { + div(.class("section")) { + p(.class("error")) { + error.reason + for resolution in error.resolutions { + br() + " * \(resolution)" + } + } + } + } + } + + div(.class("section")) { + h2 { "Duct Sizes" } + DuctSizesTable(rooms: request.ductSizes.rooms) + .attributes(.class("w-full")) + } + + div(.class("section")) { + h2 { "Supply Trunk / Run Outs" } + TrunkTable(sizes: request.ductSizes, type: .supply) + .attributes(.class("w-full")) + } + + div(.class("section")) { + h2 { "Return Trunk / Run Outs" } + TrunkTable(sizes: request.ductSizes, type: .return) + .attributes(.class("w-full")) + } + + div(.class("section")) { + h2 { "Total Equivalent Lengths" } + EffectiveLengthsTable(effectiveLengths: [ + request.maxSupplyTEL, request.maxReturnTEL, + ]) + .attributes(.class("w-full")) + } + + div(.class("section")) { + h2 { "Register Detail" } + RegisterDetailTable(rooms: request.ductSizes.rooms) + .attributes(.class("w-full")) + } + + div(.class("section")) { + h2 { "Room Detail" } + RoomsTable(rooms: request.rooms, projectSHR: request.projectSHR) + .attributes(.class("w-full")) + } + } + + } + + struct EffectiveLengthsTable: HTML, Sendable { + let effectiveLengths: [EffectiveLength] + + var body: some HTML { + table { + thead { + tr(.class("bg-green")) { + th { "Name" } + th { "Type" } + th { "Straight Lengths" } + th { "Groups" } + th { "Total" } + } + } + tbody { + for row in effectiveLengths { + tr { + td { row.name } + td { row.type.rawValue } + td { + ul { + for length in row.straightLengths { + li { length.string() } + } + } + } + td { + EffectiveLengthGroupTable(groups: row.groups) + .attributes(.class("w-full")) + } + td { row.totalEquivalentLength.string(digits: 0) } + } + } + } + } + } + + } + + struct EffectiveLengthGroupTable: HTML, Sendable { + let groups: [EffectiveLength.Group] + + var body: some HTML { + table { + thead { + tr(.class("effectiveLengthGroupHeader")) { + th { "Name" } + th { "Length" } + th { "Quantity" } + th { "Total" } + } + } + tbody { + for row in groups { + tr { + td { "\(row.group)-\(row.letter)" } + td { row.value.string(digits: 0) } + td { row.quantity.string() } + td { (row.value * Double(row.quantity)).string(digits: 0) } + } + } + } + } + } + } + + struct RoomsTable: HTML, Sendable { + let rooms: [Room] + let projectSHR: Double + + var body: some HTML { + table { + thead { + tr(.class("bg-green")) { + th { "Name" } + th { "Heating BTU" } + th { "Cooling Total BTU" } + th { "Cooling Sensible BTU" } + th { "Register Count" } + } + } + tbody { + for room in rooms { + tr { + td { room.name } + td { room.heatingLoad.string(digits: 0) } + td { room.coolingTotal.string(digits: 0) } + td { + (room.coolingSensible + ?? (room.coolingTotal * projectSHR)).string(digits: 0) + } + td { room.registerCount.string() } + } + } + // Totals + // tr(.class("table-footer")) { + tr { + td(.class("label")) { "Totals" } + td(.class("heating label")) { + rooms.totalHeatingLoad.string(digits: 0) + } + td(.class("coolingTotal label")) { + rooms.totalCoolingLoad.string(digits: 0) + } + td(.class("coolingSensible label")) { + rooms.totalCoolingSensible(shr: projectSHR).string(digits: 0) + } + td {} + } + } + } + } + } + + struct RegisterDetailTable: HTML, Sendable { + let rooms: [DuctSizes.RoomContainer] + + var body: some HTML { + table { + thead { + tr(.class("bg-green")) { + th { "Name" } + th { "Heating BTU" } + th { "Cooling BTU" } + th { "Heating CFM" } + th { "Cooling CFM" } + th { "Design CFM" } + } + } + tbody { + for row in rooms { + tr { + td { row.roomName } + td { row.heatingLoad.string(digits: 0) } + td { row.coolingLoad.string(digits: 0) } + td { row.heatingCFM.string(digits: 0) } + td { row.coolingCFM.string(digits: 0) } + td { row.designCFM.value.string(digits: 0) } + } + } + } + } + } + } + + struct TrunkTable: HTML, Sendable { + public let sizes: DuctSizes + public let type: TrunkSize.TrunkType + + var trunks: [DuctSizes.TrunkContainer] { + sizes.trunks.filter { $0.type == type } + } + + var body: some HTML { + table { + thead(.class("bg-green")) { + tr { + th { "Name" } + th { "Dsn CFM" } + th { "Round Size" } + th { "Velocity" } + th { "Final Size" } + th { "Flex Size" } + th { "Height" } + th { "Width" } + } + } + tbody { + for row in trunks { + tr { + td { row.name ?? "" } + td { row.designCFM.value.string(digits: 0) } + td { row.ductSize.roundSize.string() } + td { row.velocity.string() } + td { row.finalSize.string() } + td { row.flexSize.string() } + td { row.ductSize.height?.string() ?? "" } + td { row.width?.string() ?? "" } + } + } + } + } + } + } + + struct DuctSizesTable: HTML, Sendable { + let rooms: [DuctSizes.RoomContainer] + + var body: some HTML { + table { + thead { + tr(.class("bg-green")) { + th { "Name" } + th { "Dsn CFM" } + th { "Round Size" } + th { "Velocity" } + th { "Final Size" } + th { "Flex Size" } + th { "Height" } + th { "Width" } + } + } + tbody { + for row in rooms { + tr { + td { row.roomName } + td { row.designCFM.value.string(digits: 0) } + td { row.roundSize.string() } + td { row.velocity.string() } + td { row.flexSize.string() } + td { row.finalSize.string() } + td { row.ductSize.height?.string() ?? "" } + td { row.width?.string() ?? "" } + } + } + } + } + } + } + + struct EquipmentTable: HTML, Sendable { + let title: String? + let equipmentInfo: EquipmentInfo + + init(title: String? = nil, equipmentInfo: EquipmentInfo) { + self.title = title + self.equipmentInfo = equipmentInfo + } + + var body: some HTML { + + table { + thead { + tr(.class("bg-green")) { + th { title ?? "" } + th(.class("justify-end")) { "Value" } + } + } + tbody { + tr { + td { "Static Pressure" } + td(.class("justify-end")) { equipmentInfo.staticPressure.string() } + } + tr { + td { "Heating CFM" } + td(.class("justify-end")) { equipmentInfo.heatingCFM.string() } + } + tr { + td { "Cooling CFM" } + td(.class("justify-end")) { equipmentInfo.coolingCFM.string() } + } + } + } + } + } + + struct FrictionRateTable: HTML, Sendable { + let title: String? + let componentLosses: [ComponentPressureLoss] + let frictionRate: FrictionRate + let totalEquivalentLength: Double + let displayTotals: Bool + + var sortedLosses: [ComponentPressureLoss] { + componentLosses.sorted { $0.value > $1.value } + } + + var body: some HTML { + table { + thead { + tr(.class("bg-green")) { + th { title ?? "" } + th(.class("justify-end")) { "Value" } + } + } + tbody { + for row in sortedLosses { + tr { + td { row.name } + td(.class("justify-end")) { row.value.string() } + } + } + if displayTotals { + tr { + td(.class("label justify-end")) { "Available Static Pressure" } + td(.class("justify-end")) { frictionRate.availableStaticPressure.string() } + } + tr { + td(.class("label justify-end")) { "Total Equivalent Length" } + td(.class("justify-end")) { totalEquivalentLength.string() } + } + tr { + td(.class("label justify-end")) { "Friction Rate Design Value" } + td(.class("justify-end")) { frictionRate.value.string() } + } + } + } + } + } + } +} + +extension Project { + var cityStateZipString: String { + return "\(city), \(state) \(zipCode)" + } +} diff --git a/Sources/PdfClient/Request+markdown.swift b/Sources/PdfClient/Request+markdown.swift index 6ec90d9..00f9a11 100644 --- a/Sources/PdfClient/Request+markdown.swift +++ b/Sources/PdfClient/Request+markdown.swift @@ -1,3 +1,4 @@ +import Foundation import ManualDCore extension PdfClient.Request { @@ -13,30 +14,100 @@ extension PdfClient.Request { ## Equipment | | Value | - |-----------------|---------------------------------| - | Static Pressure | \(equipmentInfo.staticPressure) | - | Heating CFM | \(equipmentInfo.heatingCFM) | - | Cooling CFM | \(equipmentInfo.coolingCFM) | + |:----------------|:--------------------------------| + | Static Pressure | \(equipmentInfo.staticPressure.string()) | + | Heating CFM | \(equipmentInfo.heatingCFM.string()) | + | Cooling CFM | \(equipmentInfo.coolingCFM.string()) | ## Friction Rate - | | Value | - |-----------------|---------------------------------| + | Component Loss | Value | + |:----------------|:--------------------------------| """ for row in componentLosses { - retval = """ - \(retval) - \(componentLossRow(row)) - """ + retval += "\(componentLossRow(row))\n" + } + + retval += """ + + + | Results | Value | + |:-----------------|:---------------------------------| + | Available Static Pressure | \(frictionRate.availableStaticPressure.string()) | + | Total Equivalent Length | \(totalEquivalentLength.string()) | + | Friction Rate Design Value | \(frictionRate.value.string()) | + + ## Duct Sizes + + | Register | Dsn CFM | Round Size | Velocity | Final Size | Flex Size | Height | Width | + |:---------|:--------|:----------------|:---------|:-----------|:----------|:-------|:------| + + """ + for row in ductSizes.rooms { + retval += "\(registerRow(row))\n" + } + + retval += """ + + ## Trunk Sizes + + ### Supply Trunks + + | Name | Associated Supplies | Dsn CFM | Velocity | Final Size | Flex Size | Height | Width | + |:---------|:--------------------|:--------|:---------|:-----------|:----------|:-------|:------| + + """ + for row in ductSizes.trunks.filter({ $0.type == .supply }) { + retval += "\(trunkRow(row))\n" + } + + retval += """ + + ### Return Trunks / Run Outs + + | Name | Associated Supplies | Dsn CFM | Velocity | Final Size | Flex Size | Height | Width | + |:---------|:--------------------|:--------|:---------|:-----------|:----------|:-------|:------| + + """ + for row in ductSizes.trunks.filter({ $0.type == .return }) { + retval += "\(trunkRow(row))\n" } return retval } - func componentLossRow(_ row: ComponentPressureLoss) -> String { + func registerRow(_ row: DuctSizes.RoomContainer) -> String { return """ - | \(row.name) | \(row.value) | + | \(row.roomName) | \(row.designCFM.value.string(digits: 0)) | \(row.roundSize.string()) | \(row.velocity.string()) | \(row.finalSize.string()) | \(row.flexSize.string()) | \(row.height?.string() ?? "") | \(row.width?.string() ?? "") | """ } + + func trunkRow(_ row: DuctSizes.TrunkContainer) -> String { + return """ + | \(row.name ?? "") | \(associatedSupplyString(row)) | \(row.designCFM.value.string(digits: 0)) | \(row.roundSize.string()) | \(row.velocity.string()) | \(row.finalSize.string()) | \(row.flexSize.string()) | \(row.ductSize.height?.string() ?? "") | \(row.width?.string() ?? "") | + """ + } + + func componentLossRow(_ row: ComponentPressureLoss) -> String { + return """ + | \(row.name) | \(row.value.string()) | + """ + } + + var totalEquivalentLength: Double { + maxSupplyTEL.totalEquivalentLength + maxReturnTEL.totalEquivalentLength + } + + func associatedSupplyString(_ row: DuctSizes.TrunkContainer) -> String { + row.associatedSupplyString(rooms: ductSizes.rooms) + } +} + +extension DuctSizes.TrunkContainer { + + func associatedSupplyString(rooms: [DuctSizes.RoomContainer]) -> String { + self.registerIDS(rooms: rooms) + .joined(separator: ", ") + } } diff --git a/Sources/ProjectClient/Interface.swift b/Sources/ProjectClient/Interface.swift index 4de8cbe..9fe0ea8 100644 --- a/Sources/ProjectClient/Interface.swift +++ b/Sources/ProjectClient/Interface.swift @@ -1,5 +1,6 @@ import Dependencies import DependenciesMacros +import Elementary import ManualDClient import ManualDCore @@ -26,6 +27,10 @@ public struct ProjectClient: Sendable { @Sendable (User.ID, Project.Create) async throws -> CreateProjectResponse public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse + + // FIX: Name to something to do with generating a pdf, just experimenting now. + public var toMarkdown: @Sendable (Project.ID) async throws -> String + public var toHTML: @Sendable (Project.ID) async throws -> (any HTML & Sendable) } extension ProjectClient: TestDependencyKey { diff --git a/Sources/ProjectClient/Internal/DatabaseClient+calculateDuctSizes.swift b/Sources/ProjectClient/Internal/DatabaseClient+calculateDuctSizes.swift index 1380283..b0cac46 100644 --- a/Sources/ProjectClient/Internal/DatabaseClient+calculateDuctSizes.swift +++ b/Sources/ProjectClient/Internal/DatabaseClient+calculateDuctSizes.swift @@ -7,36 +7,53 @@ extension DatabaseClient { func calculateDuctSizes( projectID: Project.ID - ) async throws -> DuctSizes { + ) async throws -> (DuctSizes, DuctSizeSharedRequest, [Room]) { @Dependency(\.manualD) var manualD - return try await manualD.calculateDuctSizes( - rooms: rooms.fetch(projectID), - trunks: trunkSizes.fetch(projectID), - sharedRequest: sharedDuctRequest(projectID) + let shared = try await sharedDuctRequest(projectID) + let rooms = try await rooms.fetch(projectID) + + return try await ( + manualD.calculateDuctSizes( + rooms: rooms, + trunks: trunkSizes.fetch(projectID), + sharedRequest: shared + ), + shared, + rooms ) } func calculateRoomDuctSizes( projectID: Project.ID - ) async throws -> [DuctSizes.RoomContainer] { + ) async throws -> ([DuctSizes.RoomContainer], DuctSizeSharedRequest) { @Dependency(\.manualD) var manualD - return try await manualD.calculateRoomSizes( - rooms: rooms.fetch(projectID), - sharedRequest: sharedDuctRequest(projectID) + let shared = try await sharedDuctRequest(projectID) + + return try await ( + manualD.calculateRoomSizes( + rooms: rooms.fetch(projectID), + sharedRequest: shared + ), + shared ) } func calculateTrunkDuctSizes( projectID: Project.ID - ) async throws -> [DuctSizes.TrunkContainer] { + ) async throws -> ([DuctSizes.TrunkContainer], DuctSizeSharedRequest) { @Dependency(\.manualD) var manualD - return try await manualD.calculateTrunkSizes( - rooms: rooms.fetch(projectID), - trunks: trunkSizes.fetch(projectID), - sharedRequest: sharedDuctRequest(projectID) + let shared = try await sharedDuctRequest(projectID) + + return try await ( + manualD.calculateTrunkSizes( + rooms: rooms.fetch(projectID), + trunks: trunkSizes.fetch(projectID), + sharedRequest: shared + ), + shared ) } diff --git a/Sources/ProjectClient/Internal/ManualDClient+frictionRate.swift b/Sources/ProjectClient/Internal/ManualDClient+frictionRate.swift new file mode 100644 index 0000000..15410ab --- /dev/null +++ b/Sources/ProjectClient/Internal/ManualDClient+frictionRate.swift @@ -0,0 +1,35 @@ +import DatabaseClient +import Dependencies +import ManualDClient +import ManualDCore + +extension ManualDClient { + + func frictionRate(projectID: Project.ID) async throws -> ProjectClient.FrictionRateResponse { + @Dependency(\.database) var database + + 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: frictionRate( + .init( + externalStaticPressure: staticPressure, + componentPressureLosses: database.componentLoss.fetch(projectID), + totalEffectiveLength: Int(totalEquivalentLength) + ) + ) + ) + } +} diff --git a/Sources/ProjectClient/Live.swift b/Sources/ProjectClient/Live.swift index 1c69e52..292c223 100644 --- a/Sources/ProjectClient/Live.swift +++ b/Sources/ProjectClient/Live.swift @@ -3,22 +3,24 @@ import Dependencies import Logging import ManualDClient import ManualDCore +import PdfClient extension ProjectClient: DependencyKey { public static var liveValue: Self { @Dependency(\.database) var database @Dependency(\.manualD) var manualD + @Dependency(\.pdfClient) var pdfClient return .init( calculateDuctSizes: { projectID in - try await database.calculateDuctSizes(projectID: projectID) + try await database.calculateDuctSizes(projectID: projectID).0 }, calculateRoomDuctSizes: { projectID in - try await database.calculateRoomDuctSizes(projectID: projectID) + try await database.calculateRoomDuctSizes(projectID: projectID).0 }, calculateTrunkDuctSizes: { projectID in - try await database.calculateTrunkDuctSizes(projectID: projectID) + try await database.calculateTrunkDuctSizes(projectID: projectID).0 }, createProject: { userID, request in let project = try await database.projects.create(userID, request) @@ -31,32 +33,44 @@ extension ProjectClient: DependencyKey { ) }, 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) - ) - ) - ) + try await manualD.frictionRate(projectID: projectID) + }, + toMarkdown: { projectID in + try await pdfClient.markdown(database.makePdfRequest(projectID)) + }, + toHTML: { projectID in + try await pdfClient.html(database.makePdfRequest(projectID)) } ) } } + +extension DatabaseClient { + + fileprivate func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request { + @Dependency(\.manualD) var manualD + + guard let project = try await projects.get(projectID) else { + throw ProjectClientError("Project not found. id: \(projectID)") + } + let frictionRateResponse = try await manualD.frictionRate(projectID: projectID) + guard let frictionRate = frictionRateResponse.frictionRate else { + throw ProjectClientError("Friction rate not found. id: \(projectID)") + } + let (ductSizes, sharedInfo, rooms) = try await calculateDuctSizes(projectID: projectID) + + return .init( + project: project, + rooms: rooms, + componentLosses: frictionRateResponse.componentLosses, + ductSizes: ductSizes, + equipmentInfo: sharedInfo.equipmentInfo, + maxSupplyTEL: sharedInfo.maxSupplyLength, + maxReturnTEL: sharedInfo.maxReturnLenght, + frictionRate: frictionRate, + projectSHR: sharedInfo.projectSHR + ) + } + +} diff --git a/Sources/Styleguide/Number.swift b/Sources/Styleguide/Number.swift index fe261dc..ecc466a 100644 --- a/Sources/Styleguide/Number.swift +++ b/Sources/Styleguide/Number.swift @@ -1,16 +1,19 @@ import Elementary import Foundation +import ManualDCore public struct Number: HTML, Sendable { let fractionDigits: Int let value: Double - private var formatter: NumberFormatter { - let formatter = NumberFormatter() - formatter.maximumFractionDigits = fractionDigits - formatter.numberStyle = .decimal - return formatter - } + // private var formatter: NumberFormatter { + // let formatter = NumberFormatter() + // formatter.maximumFractionDigits = fractionDigits + // formatter.numberStyle = .decimal + // formatter.groupingSize = 3 + // formatter.groupingSeparator = "," + // return formatter + // } public init( _ value: Double, @@ -27,6 +30,6 @@ public struct Number: HTML, Sendable { } public var body: some HTML { - span { formatter.string(for: value) ?? "N/A" } + span { value.string(digits: fractionDigits) } } } diff --git a/Sources/ViewController/Views/DuctSizing/TrunksTable.swift b/Sources/ViewController/Views/DuctSizing/TrunksTable.swift index ced8f52..3bf4e15 100644 --- a/Sources/ViewController/Views/DuctSizing/TrunksTable.swift +++ b/Sources/ViewController/Views/DuctSizing/TrunksTable.swift @@ -134,17 +134,18 @@ extension DuctSizingView { } private var registerIDS: [String] { - trunk.rooms.reduce(into: []) { array, room in - array = room.registers.reduce(into: array) { array, register in - if let room = - rooms - .first(where: { $0.roomID == room.id && $0.roomRegister == register }) - { - array.append(room.roomName) - } - } - } - .sorted() + trunk.registerIDS(rooms: rooms) + // trunk.rooms.reduce(into: []) { array, room in + // array = room.registers.reduce(into: array) { array, register in + // if let room = + // rooms + // .first(where: { $0.roomID == room.id && $0.roomRegister == register }) + // { + // array.append(room.roomName) + // } + // } + // } + // .sorted() } } diff --git a/Sources/ViewController/Views/Rooms/RoomsView.swift b/Sources/ViewController/Views/Rooms/RoomsView.swift index a0ed5b2..acc8085 100644 --- a/Sources/ViewController/Views/Rooms/RoomsView.swift +++ b/Sources/ViewController/Views/Rooms/RoomsView.swift @@ -48,19 +48,19 @@ struct RoomsView: HTML, Sendable { div(.class("flex items-end space-x-4 font-bold")) { span(.class("text-lg")) { "Heating Total" } - Badge(number: rooms.heatingTotal, digits: 0) + Badge(number: rooms.totalHeatingLoad, digits: 0) .attributes(.class("badge-error")) } div(.class("flex justify-center items-end space-x-4 my-auto font-bold")) { span(.class("text-lg")) { "Cooling Total" } - Badge(number: rooms.coolingTotal, digits: 0) + Badge(number: rooms.totalCoolingLoad, digits: 0) .attributes(.class("badge-success")) } div(.class("flex grow justify-end items-end space-x-4 me-4 my-auto font-bold")) { span(.class("text-lg")) { "Cooling Sensible" } - Badge(number: rooms.coolingSensible(shr: sensibleHeatRatio), digits: 0) + Badge(number: rooms.totalCoolingSensible(shr: sensibleHeatRatio ?? 1.0), digits: 0) .attributes(.class("badge-info")) } } @@ -238,22 +238,3 @@ struct RoomsView: HTML, Sendable { } } } - -extension Array where Element == Room { - var heatingTotal: Double { - reduce(into: 0) { $0 += $1.heatingLoad } - } - - var coolingTotal: Double { - reduce(into: 0) { $0 += $1.coolingTotal } - } - - func coolingSensible(shr: Double?) -> Double { - let shr = shr ?? 1.0 - - return reduce(into: 0) { - let sensible = $1.coolingSensible ?? ($1.coolingTotal * shr) - $0 += sensible - } - } -}