WIP: Html view that prints to pdf ok.

This commit is contained in:
2026-01-17 20:40:49 -05:00
parent 0fe80d05c6
commit 04a7405ca4
16 changed files with 858 additions and 98 deletions

View File

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

View File

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

View File

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

View File

@@ -130,5 +130,19 @@ extension DuctSizes {
public subscript<T>(dynamicMember keyPath: KeyPath<DuctSizes.SizeContainer, T>) -> 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()
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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<HTMLTag.table> {
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<HTMLTag.table> {
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<HTMLTag.table> {
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<HTMLTag.table> {
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<HTMLTag.table> {
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<HTMLTag.table> {
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<HTMLTag.table> {
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<HTMLTag.table> {
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)"
}
}

View File

@@ -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: ", ")
}
}

View File

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

View File

@@ -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),
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: sharedDuctRequest(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(
let shared = try await sharedDuctRequest(projectID)
return try await (
manualD.calculateRoomSizes(
rooms: rooms.fetch(projectID),
sharedRequest: sharedDuctRequest(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(
let shared = try await sharedDuctRequest(projectID)
return try await (
manualD.calculateTrunkSizes(
rooms: rooms.fetch(projectID),
trunks: trunkSizes.fetch(projectID),
sharedRequest: sharedDuctRequest(projectID)
sharedRequest: shared
),
shared
)
}

View File

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

View File

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

View File

@@ -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<HTMLTag.span> {
span { formatter.string(for: value) ?? "N/A" }
span { value.string(digits: fractionDigits) }
}
}

View File

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

View File

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