diff --git a/Sources/ApiController/Live.swift b/Sources/ApiController/Live.swift index 097586b..62f4db5 100644 --- a/Sources/ApiController/Live.swift +++ b/Sources/ApiController/Live.swift @@ -33,6 +33,8 @@ extension SiteRoute.Api.ProjectRoute { return nil case .detail(let id, let route): switch route { + case .index: + return try await database.projects.detail(id) case .completedSteps: // FIX: fatalError() diff --git a/Sources/DatabaseClient/Projects.swift b/Sources/DatabaseClient/Projects.swift index 0b746f2..0d845e1 100644 --- a/Sources/DatabaseClient/Projects.swift +++ b/Sources/DatabaseClient/Projects.swift @@ -9,6 +9,7 @@ extension DatabaseClient { public struct Projects: Sendable { public var create: @Sendable (User.ID, Project.Create) async throws -> Project public var delete: @Sendable (Project.ID) async throws -> Void + public var detail: @Sendable (Project.ID) async throws -> Project.Detail? public var get: @Sendable (Project.ID) async throws -> Project? public var getCompletedSteps: @Sendable (Project.ID) async throws -> Project.CompletedSteps public var getSensibleHeatRatio: @Sendable (Project.ID) async throws -> Double? @@ -33,6 +34,34 @@ extension DatabaseClient.Projects: TestDependencyKey { } try await model.delete(on: database) }, + detail: { id in + guard + let model = try await ProjectModel.query(on: database) + .with(\.$componentLosses) + .with(\.$equipment) + .with(\.$equivalentLengths) + .with(\.$rooms) + .with(\.$trunks, { $0.with(\.$rooms) }) + .filter(\.$id == id) + .first() + else { + throw NotFoundError() + } + + // TODO: Different error ?? + guard let equipmentInfo = model.equipment else { return nil } + + let trunks = try await model.trunks.toDTO(on: database) + + return try .init( + project: model.toDTO(), + componentLosses: model.componentLosses.map { try $0.toDTO() }, + equipmentInfo: equipmentInfo.toDTO(), + equivalentLengths: model.equivalentLengths.map { try $0.toDTO() }, + rooms: model.rooms.map { try $0.toDTO() }, + trunks: trunks + ) + }, get: { id in try await ProjectModel.find(id, on: database).map { try $0.toDTO() } }, @@ -248,6 +277,18 @@ final class ProjectModel: Model, @unchecked Sendable { @Children(for: \.$project) var componentLosses: [ComponentLossModel] + @OptionalChild(for: \.$project) + var equipment: EquipmentModel? + + @Children(for: \.$project) + var equivalentLengths: [EffectiveLengthModel] + + @Children(for: \.$project) + var rooms: [RoomModel] + + @Children(for: \.$project) + var trunks: [TrunkModel] + @Parent(key: "userID") var user: UserModel diff --git a/Sources/ManualDCore/Project.swift b/Sources/ManualDCore/Project.swift index eeb49f9..548ef39 100644 --- a/Sources/ManualDCore/Project.swift +++ b/Sources/ManualDCore/Project.swift @@ -84,6 +84,32 @@ extension Project { } } + public struct Detail: Codable, Equatable, Sendable { + + public let project: Project + public let componentLosses: [ComponentPressureLoss] + public let equipmentInfo: EquipmentInfo + public let equivalentLengths: [EffectiveLength] + public let rooms: [Room] + public let trunks: [TrunkSize] + + public init( + project: Project, + componentLosses: [ComponentPressureLoss], + equipmentInfo: EquipmentInfo, + equivalentLengths: [EffectiveLength], + rooms: [Room], + trunks: [TrunkSize] + ) { + self.project = project + self.componentLosses = componentLosses + self.equipmentInfo = equipmentInfo + self.equivalentLengths = equivalentLengths + self.rooms = rooms + self.trunks = trunks + } + } + public struct Update: Codable, Equatable, Sendable { public let name: String? diff --git a/Sources/ManualDCore/Routes/ApiRoute.swift b/Sources/ManualDCore/Routes/ApiRoute.swift index 97ae36f..afaa5fe 100644 --- a/Sources/ManualDCore/Routes/ApiRoute.swift +++ b/Sources/ManualDCore/Routes/ApiRoute.swift @@ -88,11 +88,16 @@ extension SiteRoute.Api { extension SiteRoute.Api.ProjectRoute { public enum DetailRoute: Equatable, Sendable { + case index case completedSteps static let rootPath = "details" static let router = OneOf { + Route(.case(Self.index)) { + Path { rootPath } + Method.get + } Route(.case(Self.completedSteps)) { Path { rootPath diff --git a/Sources/ManualDCore/Routes/ViewRoute.swift b/Sources/ManualDCore/Routes/ViewRoute.swift index ca5be24..8414ec3 100644 --- a/Sources/ManualDCore/Routes/ViewRoute.swift +++ b/Sources/ManualDCore/Routes/ViewRoute.swift @@ -146,6 +146,7 @@ extension SiteRoute.View.ProjectRoute { case equipment(EquipmentInfoRoute) case equivalentLength(EquivalentLengthRoute) case frictionRate(FrictionRateRoute) + case pdf case rooms(RoomRoute) static let router = OneOf { @@ -167,6 +168,10 @@ extension SiteRoute.View.ProjectRoute { Route(.case(Self.frictionRate)) { FrictionRateRoute.router } + Route(.case(Self.pdf)) { + Path { "pdf" } + Method.get + } Route(.case(Self.rooms)) { RoomRoute.router } diff --git a/Sources/ProjectClient/Internal/ManualDClient+frictionRate.swift b/Sources/ProjectClient/Internal/ManualDClient+frictionRate.swift index 15410ab..3447c27 100644 --- a/Sources/ProjectClient/Internal/ManualDClient+frictionRate.swift +++ b/Sources/ProjectClient/Internal/ManualDClient+frictionRate.swift @@ -5,31 +5,51 @@ import ManualDCore extension ManualDClient { + func frictionRate(details: Project.Detail) async throws -> ProjectClient.FrictionRateResponse { + + let maxContainer = details.maxContainer + guard let totalEquivalentLength = maxContainer.total else { + return .init(componentLosses: details.componentLosses, equivalentLengths: maxContainer) + } + + return try await .init( + componentLosses: details.componentLosses, + equivalentLengths: maxContainer, + frictionRate: frictionRate( + .init( + externalStaticPressure: details.equipmentInfo.staticPressure, + componentPressureLosses: details.componentLosses, + totalEffectiveLength: Int(totalEquivalentLength) + ) + ) + ) + } + 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 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) - } + 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) - } + 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) - ) - ) + 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/Internal/ProjectDetail+maxContainer.swift b/Sources/ProjectClient/Internal/ProjectDetail+maxContainer.swift new file mode 100644 index 0000000..ef0e8a1 --- /dev/null +++ b/Sources/ProjectClient/Internal/ProjectDetail+maxContainer.swift @@ -0,0 +1,14 @@ +import ManualDCore + +extension Project.Detail { + var maxContainer: EffectiveLength.MaxContainer { + .init( + supply: equivalentLengths.filter({ $0.type == .supply }) + .sorted(by: { $0.totalEquivalentLength > $1.totalEquivalentLength }) + .first, + return: equivalentLengths.filter({ $0.type == .return }) + .sorted(by: { $0.totalEquivalentLength > $1.totalEquivalentLength }) + .first + ) + } +} diff --git a/Sources/Styleguide/Date.swift b/Sources/Styleguide/Date.swift index e679e4e..bf10d51 100644 --- a/Sources/Styleguide/Date.swift +++ b/Sources/Styleguide/Date.swift @@ -6,7 +6,7 @@ public struct DateView: HTML, Sendable { var formatter: DateFormatter { let formatter = DateFormatter() - formatter.dateStyle = .short + formatter.dateFormat = "MM/dd/yyyy" return formatter } diff --git a/Sources/Styleguide/ResultView.swift b/Sources/Styleguide/ResultView.swift index 227c4aa..5067fb6 100644 --- a/Sources/Styleguide/ResultView.swift +++ b/Sources/Styleguide/ResultView.swift @@ -67,6 +67,19 @@ extension ResultView { } } +extension ResultView where V == ValueView { + + public init( + catching: @escaping @Sendable () async throws(E) -> V + ) async where ErrorView == Styleguide.ErrorView { + await self.init(result: .init(catching: catching)) { + $0 + } onError: { error in + Styleguide.ErrorView(error: error) + } + } +} + extension ResultView: Sendable where Error: Sendable, ValueView: Sendable, ErrorView: Sendable {} public struct ErrorView: HTML, Sendable where Error: Sendable { diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index 0fdf10e..60ef3aa 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -192,6 +192,11 @@ extension SiteRoute.View.ProjectRoute { return await route.renderView(on: request, projectID: projectID) case .frictionRate(let route): return await route.renderView(on: request, projectID: projectID) + case .pdf: + return await ResultView { + try await projectClient.toHTML(projectID) + } + // fatalError() case .rooms(let route): return await route.renderView(on: request, projectID: projectID) } diff --git a/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift b/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift index 9963508..f499e50 100644 --- a/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift +++ b/Sources/ViewController/Views/DuctSizing/DuctSizingView.swift @@ -23,6 +23,14 @@ struct DuctSizingView: HTML, Sendable { .hidden(when: ductSizes.rooms.count > 0) .attributes(.class("text-error font-bold italic mt-4")) } + + a( + .class("btn btn-primary"), + .href(route: .project(.detail(projectID, .pdf))) + ) { + "PDF" + } + } if ductSizes.rooms.count != 0 {