Compare commits
23 Commits
0.1.1
...
c82f20bb60
| Author | SHA1 | Date | |
|---|---|---|---|
|
c82f20bb60
|
|||
|
458b3bd644
|
|||
|
58023c4dbc
|
|||
|
30241fec60
|
|||
|
273da46db2
|
|||
|
6064b5267a
|
|||
|
69e8acc5d8
|
|||
|
066b3003d0
|
|||
|
1663c0a514
|
|||
|
e08d896758
|
|||
|
5fa11ae584
|
|||
|
04a7405ca4
|
|||
|
0fe80d05c6
|
|||
|
3ec1ee2814
|
|||
|
761ba29c1e
|
|||
|
13c4bb33b5
|
|||
|
146baa7815
|
|||
|
b5436c2073
|
|||
|
59c1c9ec4a
|
|||
|
65fc8565b6
|
|||
|
d14477e97a
|
|||
|
dbec7fb920
|
|||
|
6b8cb73434
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,3 +9,5 @@ DerivedData/
|
||||
.swift-version
|
||||
node_modules/
|
||||
tailwindcss
|
||||
.envrc
|
||||
*.pdf
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "5d6dad57209ac74e3c47d8e8eb162768b81c9e63e15df87d29019d46a13cfec2",
|
||||
"originHash" : "c3efcfd33bc1490f59ae406e4e5292027b2d01cafee9fc625652213505df50fb",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "async-http-client",
|
||||
@@ -226,6 +226,15 @@
|
||||
"version" : "4.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-custom-dump",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-custom-dump",
|
||||
"state" : {
|
||||
"revision" : "93a8aa4937030b606de42f44b17870249f49af0b",
|
||||
"version" : "1.3.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-dependencies",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -361,6 +370,15 @@
|
||||
"version" : "2.9.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-snapshot-testing",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
||||
"state" : {
|
||||
"revision" : "a8b7c5e0ed33d8ab8887d1654d9b59f2cbad529b",
|
||||
"version" : "1.18.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-syntax",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -7,7 +7,13 @@ let package = Package(
|
||||
products: [
|
||||
.executable(name: "App", targets: ["App"]),
|
||||
.library(name: "ApiController", targets: ["ApiController"]),
|
||||
.library(name: "AuthClient", targets: ["AuthClient"]),
|
||||
.library(name: "DatabaseClient", targets: ["DatabaseClient"]),
|
||||
.library(name: "EnvClient", targets: ["EnvClient"]),
|
||||
.library(name: "FileClient", targets: ["FileClient"]),
|
||||
.library(name: "HTMLSnapshotTesting", targets: ["HTMLSnapshotTesting"]),
|
||||
.library(name: "PdfClient", targets: ["PdfClient"]),
|
||||
.library(name: "ProjectClient", targets: ["ProjectClient"]),
|
||||
.library(name: "ManualDCore", targets: ["ManualDCore"]),
|
||||
.library(name: "ManualDClient", targets: ["ManualDClient"]),
|
||||
.library(name: "Styleguide", targets: ["Styleguide"]),
|
||||
@@ -19,6 +25,7 @@ let package = Package(
|
||||
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
|
||||
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"),
|
||||
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.12.0"),
|
||||
.package(url: "https://github.com/pointfreeco/swift-url-routing.git", from: "0.6.2"),
|
||||
.package(url: "https://github.com/pointfreeco/vapor-routing.git", from: "0.1.3"),
|
||||
.package(url: "https://github.com/pointfreeco/swift-case-paths.git", from: "1.6.0"),
|
||||
@@ -31,6 +38,7 @@ let package = Package(
|
||||
name: "App",
|
||||
dependencies: [
|
||||
.target(name: "ApiController"),
|
||||
.target(name: "AuthClient"),
|
||||
.target(name: "DatabaseClient"),
|
||||
.target(name: "ViewController"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
@@ -53,6 +61,15 @@ let package = Package(
|
||||
.product(name: "Vapor", package: "vapor"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "AuthClient",
|
||||
dependencies: [
|
||||
.target(name: "DatabaseClient"),
|
||||
.target(name: "ManualDCore"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "DatabaseClient",
|
||||
dependencies: [
|
||||
@@ -63,6 +80,48 @@ let package = Package(
|
||||
.product(name: "Vapor", package: "vapor"),
|
||||
]
|
||||
),
|
||||
|
||||
.target(
|
||||
name: "EnvClient",
|
||||
dependencies: [
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "FileClient",
|
||||
dependencies: [
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "HTMLSnapshotTesting",
|
||||
dependencies: [
|
||||
.product(name: "Elementary", package: "elementary"),
|
||||
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "PdfClient",
|
||||
dependencies: [
|
||||
.target(name: "EnvClient"),
|
||||
.target(name: "FileClient"),
|
||||
.target(name: "ManualDCore"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
.product(name: "Elementary", package: "elementary"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "ProjectClient",
|
||||
dependencies: [
|
||||
.target(name: "DatabaseClient"),
|
||||
.target(name: "ManualDClient"),
|
||||
.target(name: "PdfClient"),
|
||||
.product(name: "Vapor", package: "vapor"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "ManualDCore",
|
||||
dependencies: [
|
||||
@@ -104,7 +163,10 @@ let package = Package(
|
||||
.target(
|
||||
name: "ViewController",
|
||||
dependencies: [
|
||||
.target(name: "AuthClient"),
|
||||
.target(name: "DatabaseClient"),
|
||||
.target(name: "PdfClient"),
|
||||
.target(name: "ProjectClient"),
|
||||
.target(name: "ManualDClient"),
|
||||
.target(name: "ManualDCore"),
|
||||
.target(name: "Styleguide"),
|
||||
@@ -115,5 +177,15 @@ let package = Package(
|
||||
.product(name: "Vapor", package: "vapor"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "ViewControllerTests",
|
||||
dependencies: [
|
||||
.target(name: "ViewController"),
|
||||
.target(name: "HTMLSnapshotTesting"),
|
||||
],
|
||||
resources: [
|
||||
.copy("__Snapshots__")
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
12
Public/css/htmx.css
Normal file
12
Public/css/htmx.css
Normal file
@@ -0,0 +1,12 @@
|
||||
.htmx-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.htmx-request .htmx-indicator {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.htmx-request.htmx-indicator {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@@ -2,4 +2,3 @@
|
||||
@plugin "daisyui" {
|
||||
themes: all;
|
||||
}
|
||||
|
||||
|
||||
109
Public/css/pdf.css
Normal file
109
Public/css/pdf.css
Normal file
@@ -0,0 +1,109 @@
|
||||
@media print {
|
||||
body {
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
table td, table th {
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
* {
|
||||
font-size: 12px;
|
||||
}
|
||||
h1 { font-size: 24px; }
|
||||
h2 { font-size: 16px; }
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
max-width: 100%;
|
||||
margin: 10px auto;
|
||||
border: none !important;
|
||||
border-style: none;
|
||||
}
|
||||
th, td {
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-style: none;
|
||||
}
|
||||
.table-bordered {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.table-bordered th, td {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.table-bordered 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 {
|
||||
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;
|
||||
}
|
||||
63
Public/js/htmx-download.js
Normal file
63
Public/js/htmx-download.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copied from: https://github.com/dakixr/htmx-download/blob/main/htmx-download.js
|
||||
htmx.defineExtension('htmx-download', {
|
||||
onEvent: function(name, evt) {
|
||||
|
||||
if (name === 'htmx:beforeRequest') {
|
||||
// Set the responseType to 'arraybuffer' to handle binary data
|
||||
evt.detail.xhr.responseType = 'arraybuffer';
|
||||
}
|
||||
|
||||
if (name === 'htmx:beforeSwap') {
|
||||
const xhr = evt.detail.xhr;
|
||||
|
||||
if (xhr.status === 200) {
|
||||
// Parse headers
|
||||
const headers = {};
|
||||
const headerStr = xhr.getAllResponseHeaders();
|
||||
const headerArr = headerStr.trim().split(/[\r\n]+/);
|
||||
headerArr.forEach((line) => {
|
||||
const parts = line.split(": ");
|
||||
const header = parts.shift().toLowerCase();
|
||||
const value = parts.join(": ");
|
||||
headers[header] = value;
|
||||
});
|
||||
|
||||
// Extract filename
|
||||
let filename = 'downloaded_file.xlsx';
|
||||
if (headers['content-disposition']) {
|
||||
const filenameMatch = headers['content-disposition'].match(/filename\*?=(?:UTF-8'')?"?([^;\n"]+)/i);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = decodeURIComponent(filenameMatch[1].replace(/['"]/g, ''));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine MIME type
|
||||
const mimetype = headers['content-type'] || 'application/octet-stream';
|
||||
|
||||
// Create Blob
|
||||
const blob = new Blob([xhr.response], { type: mimetype });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Trigger download
|
||||
const link = document.createElement("a");
|
||||
link.style.display = "none";
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// Cleanup
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
link.remove();
|
||||
}, 100);
|
||||
|
||||
} else {
|
||||
console.warn(`[htmx-download] Unexpected response status: ${xhr.status}`);
|
||||
}
|
||||
|
||||
// Prevent htmx from swapping content
|
||||
evt.detail.shouldSwap = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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()
|
||||
|
||||
@@ -12,11 +12,7 @@ extension ViewController {
|
||||
.init(
|
||||
route: route,
|
||||
isHtmxRequest: request.isHtmxRequest,
|
||||
logger: request.logger,
|
||||
authenticateUser: { request.session.authenticate($0) },
|
||||
currentUser: {
|
||||
try request.auth.require(User.self)
|
||||
}
|
||||
logger: request.logger
|
||||
)
|
||||
)
|
||||
return AnyHTMLResponse(value: html)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import ApiController
|
||||
import AuthClient
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import ManualDCore
|
||||
import PdfClient
|
||||
import Vapor
|
||||
import ViewController
|
||||
|
||||
// Taken from discussions page on `swift-dependencies`.
|
||||
|
||||
// FIX: Use live view controller.
|
||||
struct DependenciesMiddleware: AsyncMiddleware {
|
||||
|
||||
private let values: DependencyValues.Continuation
|
||||
@@ -29,9 +31,11 @@ struct DependenciesMiddleware: AsyncMiddleware {
|
||||
try await values.yield {
|
||||
try await withDependencies {
|
||||
$0.apiController = apiController
|
||||
$0.authClient = .live(on: request)
|
||||
$0.database = database
|
||||
// $0.dateFormatter = .liveValue
|
||||
$0.viewController = viewController
|
||||
$0.pdfClient = .liveValue
|
||||
} operation: {
|
||||
try await next.respond(to: request)
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ private let viewRouteMiddleware: [any Middleware] = [
|
||||
extension SiteRoute.View {
|
||||
var middleware: [any Middleware]? {
|
||||
switch self {
|
||||
case .project, .user:
|
||||
return viewRouteMiddleware
|
||||
case .login, .signup, .test:
|
||||
return nil
|
||||
case .project, .user:
|
||||
return viewRouteMiddleware
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Fluent
|
||||
import FluentSQLiteDriver
|
||||
import ManualDCore
|
||||
import NIOSSL
|
||||
import ProjectClient
|
||||
import Vapor
|
||||
import VaporElementary
|
||||
@preconcurrency import VaporRouting
|
||||
@@ -111,6 +112,45 @@ extension SiteRoute {
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizes: Content {}
|
||||
|
||||
// FIX: Move
|
||||
func handlePdf(_ projectID: Project.ID, on request: Request) async throws -> Response {
|
||||
@Dependency(\.projectClient) var projectClient
|
||||
return try await projectClient.generatePdf(projectID, request.fileio)
|
||||
|
||||
// let html = try await projectClient.toHTML(projectID)
|
||||
// let url = "/tmp/\(projectID)"
|
||||
// try await request.fileio.writeFile(.init(string: html.render()), at: "\(url).html")
|
||||
//
|
||||
// let process = Process()
|
||||
// let standardInput = Pipe()
|
||||
// let standardOutput = Pipe()
|
||||
// process.standardInput = standardInput
|
||||
// process.standardOutput = standardOutput
|
||||
// process.executableURL = URL(fileURLWithPath: "/bin/pandoc")
|
||||
// process.arguments = [
|
||||
// "\(url).html",
|
||||
// "--pdf-engine=weasyprint",
|
||||
// "--from=html",
|
||||
// "--css=Public/css/pdf.css",
|
||||
// "-o", "\(url).pdf",
|
||||
// ]
|
||||
// try process.run()
|
||||
// process.waitUntilExit()
|
||||
//
|
||||
// let response = try await request.fileio.asyncStreamFile(at: "\(url).pdf", mediaType: .pdf) { _ in
|
||||
// // Remove files here.
|
||||
// try FileManager.default.removeItem(atPath: "\(url).pdf")
|
||||
// try FileManager.default.removeItem(atPath: "\(url).html")
|
||||
// }
|
||||
// response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream")
|
||||
// response.headers.replaceOrAdd(
|
||||
// name: .contentDisposition, value: "attachment; filename=Duct-Calc.pdf"
|
||||
// )
|
||||
// return response
|
||||
}
|
||||
|
||||
@Sendable
|
||||
private func siteHandler(
|
||||
request: Request,
|
||||
@@ -118,12 +158,16 @@ private func siteHandler(
|
||||
) async throws -> any AsyncResponseEncodable {
|
||||
@Dependency(\.apiController) var apiController
|
||||
@Dependency(\.viewController) var viewController
|
||||
@Dependency(\.projectClient) var projectClient
|
||||
|
||||
switch route {
|
||||
case .api(let route):
|
||||
return try await apiController.respond(route, request: request)
|
||||
case .health:
|
||||
return HTTPStatus.ok
|
||||
// FIX: Move
|
||||
case .view(.project(.detail(let projectID, .pdf))):
|
||||
return try await handlePdf(projectID, on: request)
|
||||
case .view(let route):
|
||||
return try await viewController.respond(route: route, request: request)
|
||||
}
|
||||
|
||||
51
Sources/AuthClient/Interface.swift
Normal file
51
Sources/AuthClient/Interface.swift
Normal file
@@ -0,0 +1,51 @@
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import ManualDCore
|
||||
import Vapor
|
||||
|
||||
extension DependencyValues {
|
||||
public var authClient: AuthClient {
|
||||
get { self[AuthClient.self] }
|
||||
set { self[AuthClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@DependencyClient
|
||||
public struct AuthClient: Sendable {
|
||||
public var createAndLogin: @Sendable (User.Create) async throws -> User
|
||||
public var currentUser: @Sendable () throws -> User
|
||||
public var login: @Sendable (User.Login) async throws -> User
|
||||
public var logout: @Sendable () throws -> Void
|
||||
}
|
||||
|
||||
extension AuthClient: TestDependencyKey {
|
||||
public static let testValue = Self()
|
||||
|
||||
public static func live(on request: Request) -> Self {
|
||||
@Dependency(\.database) var database
|
||||
|
||||
return .init(
|
||||
createAndLogin: { createForm in
|
||||
let user = try await database.users.create(createForm)
|
||||
_ = try await database.users.login(
|
||||
.init(email: createForm.email, password: createForm.password)
|
||||
)
|
||||
request.auth.login(user)
|
||||
request.logger.debug("LOGGED IN: \(user.id)")
|
||||
return user
|
||||
},
|
||||
currentUser: {
|
||||
try request.auth.require(User.self)
|
||||
},
|
||||
login: { loginForm in
|
||||
let token = try await database.users.login(loginForm)
|
||||
let user = try await database.users.get(token.userID)!
|
||||
request.session.authenticate(user)
|
||||
request.logger.debug("LOGGED IN: \(user.id)")
|
||||
return user
|
||||
},
|
||||
logout: { request.auth.logout(User.self) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ extension DatabaseClient.Migrations: DependencyKey {
|
||||
EquipmentInfo.Migrate(),
|
||||
Room.Migrate(),
|
||||
EffectiveLength.Migrate(),
|
||||
DuctSizing.TrunkSize.Migrate(),
|
||||
TrunkSize.Migrate(),
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,44 @@ 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,
|
||||
{ trunk in
|
||||
trunk.with(
|
||||
\.$rooms,
|
||||
{
|
||||
$0.with(\.$room)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
.filter(\.$id == id)
|
||||
.first()
|
||||
else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
|
||||
// TODO: Different error ??
|
||||
guard let equipmentInfo = model.equipment else { return nil }
|
||||
|
||||
let trunks = try model.trunks.toDTO()
|
||||
|
||||
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() }
|
||||
},
|
||||
@@ -73,10 +112,16 @@ extension DatabaseClient.Projects: TestDependencyKey {
|
||||
)
|
||||
},
|
||||
getSensibleHeatRatio: { id in
|
||||
guard let model = try await ProjectModel.find(id, on: database) else {
|
||||
guard
|
||||
let shr = try await ProjectModel.query(on: database)
|
||||
.field(\.$id)
|
||||
.field(\.$sensibleHeatRatio)
|
||||
.filter(\.$id == id)
|
||||
.first()
|
||||
else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
return model.sensibleHeatRatio
|
||||
return shr.sensibleHeatRatio
|
||||
},
|
||||
fetch: { userID, request in
|
||||
try await ProjectModel.query(on: database)
|
||||
@@ -242,6 +287,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
|
||||
|
||||
|
||||
@@ -10,12 +10,11 @@ extension DatabaseClient {
|
||||
public var create: @Sendable (Room.Create) async throws -> Room
|
||||
public var delete: @Sendable (Room.ID) async throws -> Void
|
||||
public var deleteRectangularSize:
|
||||
@Sendable (Room.ID, DuctSizing.RectangularDuct.ID) async throws -> Room
|
||||
@Sendable (Room.ID, Room.RectangularSize.ID) async throws -> Room
|
||||
public var get: @Sendable (Room.ID) async throws -> Room?
|
||||
public var fetch: @Sendable (Project.ID) async throws -> [Room]
|
||||
public var update: @Sendable (Room.ID, Room.Update) async throws -> Room
|
||||
public var updateRectangularSize:
|
||||
@Sendable (Room.ID, DuctSizing.RectangularDuct) async throws -> Room
|
||||
public var updateRectangularSize: @Sendable (Room.ID, Room.RectangularSize) async throws -> Room
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +203,7 @@ final class RoomModel: Model, @unchecked Sendable {
|
||||
var registerCount: Int
|
||||
|
||||
@Field(key: "rectangularSizes")
|
||||
var rectangularSizes: [DuctSizing.RectangularDuct]?
|
||||
var rectangularSizes: [Room.RectangularSize]?
|
||||
|
||||
@Timestamp(key: "createdAt", on: .create, format: .iso8601)
|
||||
var createdAt: Date?
|
||||
@@ -224,7 +223,7 @@ final class RoomModel: Model, @unchecked Sendable {
|
||||
coolingTotal: Double,
|
||||
coolingSensible: Double? = nil,
|
||||
registerCount: Int,
|
||||
rectangularSizes: [DuctSizing.RectangularDuct]? = nil,
|
||||
rectangularSizes: [Room.RectangularSize]? = nil,
|
||||
createdAt: Date? = nil,
|
||||
updatedAt: Date? = nil,
|
||||
projectID: Project.ID
|
||||
|
||||
@@ -7,13 +7,13 @@ import ManualDCore
|
||||
extension DatabaseClient {
|
||||
@DependencyClient
|
||||
public struct TrunkSizes: Sendable {
|
||||
public var create: @Sendable (DuctSizing.TrunkSize.Create) async throws -> DuctSizing.TrunkSize
|
||||
public var delete: @Sendable (DuctSizing.TrunkSize.ID) async throws -> Void
|
||||
public var fetch: @Sendable (Project.ID) async throws -> [DuctSizing.TrunkSize]
|
||||
public var get: @Sendable (DuctSizing.TrunkSize.ID) async throws -> DuctSizing.TrunkSize?
|
||||
public var create: @Sendable (TrunkSize.Create) async throws -> TrunkSize
|
||||
public var delete: @Sendable (TrunkSize.ID) async throws -> Void
|
||||
public var fetch: @Sendable (Project.ID) async throws -> [TrunkSize]
|
||||
public var get: @Sendable (TrunkSize.ID) async throws -> TrunkSize?
|
||||
public var update:
|
||||
@Sendable (DuctSizing.TrunkSize.ID, DuctSizing.TrunkSize.Update) async throws ->
|
||||
DuctSizing.TrunkSize
|
||||
@Sendable (TrunkSize.ID, TrunkSize.Update) async throws ->
|
||||
TrunkSize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||
try request.validate()
|
||||
|
||||
let trunk = request.toModel()
|
||||
var roomProxies = [DuctSizing.TrunkSize.RoomProxy]()
|
||||
var roomProxies = [TrunkSize.RoomProxy]()
|
||||
|
||||
try await trunk.save(on: database)
|
||||
|
||||
@@ -41,7 +41,9 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||
type: request.type
|
||||
)
|
||||
try await model.save(on: database)
|
||||
try await roomProxies.append(model.toDTO(on: database))
|
||||
roomProxies.append(
|
||||
.init(room: try room.toDTO(), registers: registers)
|
||||
)
|
||||
}
|
||||
|
||||
return try .init(
|
||||
@@ -60,23 +62,30 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||
fetch: { projectID in
|
||||
try await TrunkModel.query(on: database)
|
||||
.with(\.$project)
|
||||
.with(\.$rooms)
|
||||
.with(\.$rooms, { $0.with(\.$room) })
|
||||
.filter(\.$project.$id == projectID)
|
||||
.all()
|
||||
.toDTO(on: database)
|
||||
.toDTO()
|
||||
},
|
||||
get: { id in
|
||||
guard let model = try await TrunkModel.find(id, on: database) else {
|
||||
guard
|
||||
let model =
|
||||
try await TrunkModel
|
||||
.query(on: database)
|
||||
.with(\.$rooms, { $0.with(\.$room) })
|
||||
.filter(\.$id == id)
|
||||
.first()
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return try await model.toDTO(on: database)
|
||||
return try model.toDTO()
|
||||
},
|
||||
update: { id, updates in
|
||||
guard
|
||||
let model =
|
||||
try await TrunkModel
|
||||
.query(on: database)
|
||||
.with(\.$rooms)
|
||||
.with(\.$rooms, { $0.with(\.$room) })
|
||||
.filter(\.$id == id)
|
||||
.first()
|
||||
else {
|
||||
@@ -84,13 +93,13 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||
}
|
||||
try updates.validate()
|
||||
try await model.applyUpdates(updates, on: database)
|
||||
return try await model.toDTO(on: database)
|
||||
return try model.toDTO()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizing.TrunkSize.Create {
|
||||
extension TrunkSize.Create {
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
guard rooms.count > 0 else {
|
||||
@@ -113,7 +122,7 @@ extension DuctSizing.TrunkSize.Create {
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizing.TrunkSize.Update {
|
||||
extension TrunkSize.Update {
|
||||
func validate() throws(ValidationError) {
|
||||
if let rooms {
|
||||
guard rooms.count > 0 else {
|
||||
@@ -128,7 +137,7 @@ extension DuctSizing.TrunkSize.Update {
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizing.TrunkSize {
|
||||
extension TrunkSize {
|
||||
|
||||
struct Migrate: AsyncMigration {
|
||||
let name = "CreateTrunkSize"
|
||||
@@ -192,7 +201,7 @@ final class TrunkRoomModel: Model, @unchecked Sendable {
|
||||
trunkID: TrunkModel.IDValue,
|
||||
roomID: RoomModel.IDValue,
|
||||
registers: [Int],
|
||||
type: DuctSizing.TrunkSize.TrunkType
|
||||
type: TrunkSize.TrunkType
|
||||
) {
|
||||
self.id = id
|
||||
$trunk.id = trunkID
|
||||
@@ -201,10 +210,10 @@ final class TrunkRoomModel: Model, @unchecked Sendable {
|
||||
self.type = type.rawValue
|
||||
}
|
||||
|
||||
func toDTO(on database: any Database) async throws -> DuctSizing.TrunkSize.RoomProxy {
|
||||
guard let room = try await RoomModel.find($room.id, on: database) else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
func toDTO() throws -> TrunkSize.RoomProxy {
|
||||
// guard let room = try await RoomModel.find($room.id, on: database) else {
|
||||
// throw NotFoundError()
|
||||
// }
|
||||
return .init(
|
||||
room: try room.toDTO(),
|
||||
registers: registers
|
||||
@@ -240,7 +249,7 @@ final class TrunkModel: Model, @unchecked Sendable {
|
||||
init(
|
||||
id: UUID? = nil,
|
||||
projectID: Project.ID,
|
||||
type: DuctSizing.TrunkSize.TrunkType,
|
||||
type: TrunkSize.TrunkType,
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
@@ -251,18 +260,22 @@ final class TrunkModel: Model, @unchecked Sendable {
|
||||
self.name = name
|
||||
}
|
||||
|
||||
func toDTO(on database: any Database) async throws -> DuctSizing.TrunkSize {
|
||||
let rooms = try await withThrowingTaskGroup(of: DuctSizing.TrunkSize.RoomProxy.self) { group in
|
||||
for room in self.rooms {
|
||||
group.addTask {
|
||||
try await room.toDTO(on: database)
|
||||
}
|
||||
}
|
||||
|
||||
return try await group.reduce(into: [DuctSizing.TrunkSize.RoomProxy]()) {
|
||||
$0.append($1)
|
||||
}
|
||||
func toDTO() throws -> TrunkSize {
|
||||
// let rooms = try await withThrowingTaskGroup(of: TrunkSize.RoomProxy.self) { group in
|
||||
// for room in self.rooms {
|
||||
// group.addTask {
|
||||
// try await room.toDTO(on: database)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return try await group.reduce(into: [TrunkSize.RoomProxy]()) {
|
||||
// $0.append($1)
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
let rooms = try rooms.reduce(into: [TrunkSize.RoomProxy]()) {
|
||||
$0.append(try $1.toDTO())
|
||||
}
|
||||
|
||||
return try .init(
|
||||
@@ -277,7 +290,7 @@ final class TrunkModel: Model, @unchecked Sendable {
|
||||
}
|
||||
|
||||
func applyUpdates(
|
||||
_ updates: DuctSizing.TrunkSize.Update,
|
||||
_ updates: TrunkSize.Update,
|
||||
on database: any Database
|
||||
) async throws {
|
||||
if let type = updates.type, type.rawValue != self.type {
|
||||
@@ -340,17 +353,17 @@ final class TrunkModel: Model, @unchecked Sendable {
|
||||
|
||||
extension Array where Element == TrunkModel {
|
||||
|
||||
func toDTO(on database: any Database) async throws -> [DuctSizing.TrunkSize] {
|
||||
try await withThrowingTaskGroup(of: DuctSizing.TrunkSize.self) { group in
|
||||
for model in self {
|
||||
group.addTask {
|
||||
try await model.toDTO(on: database)
|
||||
}
|
||||
}
|
||||
func toDTO() throws -> [TrunkSize] {
|
||||
// try await withThrowingTaskGroup(of: TrunkSize.self) { group in
|
||||
// for model in self {
|
||||
// group.addTask {
|
||||
// try await model.toDTO(on: database)
|
||||
// }
|
||||
// }
|
||||
|
||||
return try await group.reduce(into: [DuctSizing.TrunkSize]()) {
|
||||
$0.append($1)
|
||||
}
|
||||
return try reduce(into: [TrunkSize]()) {
|
||||
$0.append(try $1.toDTO())
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
74
Sources/EnvClient/Interface.swift
Normal file
74
Sources/EnvClient/Interface.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Foundation
|
||||
|
||||
extension DependencyValues {
|
||||
|
||||
/// Holds values defined in the process environment that are needed.
|
||||
///
|
||||
/// These are generally loaded from a `.env` file, but also have default values,
|
||||
/// if not found.
|
||||
public var env: @Sendable () throws -> EnvVars {
|
||||
get { self[EnvClient.self].env }
|
||||
set { self[EnvClient.self].env = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@DependencyClient
|
||||
struct EnvClient: Sendable {
|
||||
public var env: @Sendable () throws -> EnvVars
|
||||
}
|
||||
|
||||
/// Holds values defined in the process environment that are needed.
|
||||
///
|
||||
/// These are generally loaded from a `.env` file, but also have default values,
|
||||
/// if not found.
|
||||
public struct EnvVars: Codable, Equatable, Sendable {
|
||||
|
||||
/// The path to the pandoc executable on the system, used to generate pdf's.
|
||||
public let pandocPath: String
|
||||
|
||||
/// The pdf engine to use with pandoc when creating pdf's.
|
||||
public let pdfEngine: String
|
||||
|
||||
public init(
|
||||
pandocPath: String = "/bin/pandoc",
|
||||
pdfEngine: String = "weasyprint"
|
||||
) {
|
||||
self.pandocPath = pandocPath
|
||||
self.pdfEngine = pdfEngine
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case pandocPath = "PANDOC_PATH"
|
||||
case pdfEngine = "PDF_ENGINE"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension EnvClient: DependencyKey {
|
||||
static let testValue = Self()
|
||||
|
||||
static let liveValue = Self(env: {
|
||||
// Convert default values into a dictionary.
|
||||
let defaults =
|
||||
(try? encoder.encode(EnvVars()))
|
||||
.flatMap { try? decoder.decode([String: String].self, from: $0) }
|
||||
?? [:]
|
||||
|
||||
// Merge the default values with values found in process environment.
|
||||
let assigned = defaults.merging(ProcessInfo.processInfo.environment, uniquingKeysWith: { $1 })
|
||||
|
||||
return (try? JSONSerialization.data(withJSONObject: assigned))
|
||||
.flatMap { try? decoder.decode(EnvVars.self, from: $0) }
|
||||
?? .init()
|
||||
})
|
||||
}
|
||||
|
||||
private let encoder: JSONEncoder = {
|
||||
JSONEncoder()
|
||||
}()
|
||||
|
||||
private let decoder: JSONDecoder = {
|
||||
JSONDecoder()
|
||||
}()
|
||||
29
Sources/FileClient/Interface.swift
Normal file
29
Sources/FileClient/Interface.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Foundation
|
||||
|
||||
extension DependencyValues {
|
||||
public var fileClient: FileClient {
|
||||
get { self[FileClient.self] }
|
||||
set { self[FileClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@DependencyClient
|
||||
public struct FileClient: Sendable {
|
||||
public var writeFile: @Sendable (String, String) async throws -> Void
|
||||
public var removeFile: @Sendable (String) async throws -> Void
|
||||
}
|
||||
|
||||
extension FileClient: DependencyKey {
|
||||
public static let testValue = Self()
|
||||
|
||||
public static let liveValue = Self(
|
||||
writeFile: { contents, path in
|
||||
try contents.write(to: URL(fileURLWithPath: path), atomically: true, encoding: .utf8)
|
||||
},
|
||||
removeFile: { path in
|
||||
try FileManager.default.removeItem(atPath: path)
|
||||
}
|
||||
)
|
||||
}
|
||||
22
Sources/HTMLSnapshotTesting/Snapshotting.swift
Normal file
22
Sources/HTMLSnapshotTesting/Snapshotting.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
import Elementary
|
||||
import SnapshotTesting
|
||||
|
||||
extension Snapshotting where Value == (any HTML), Format == String {
|
||||
public static var html: Snapshotting {
|
||||
var snapshotting = SimplySnapshotting.lines
|
||||
.pullback { (html: any HTML) in html.renderFormatted() }
|
||||
|
||||
snapshotting.pathExtension = "html"
|
||||
return snapshotting
|
||||
}
|
||||
}
|
||||
|
||||
extension Snapshotting where Value == String, Format == String {
|
||||
public static var html: Snapshotting {
|
||||
var snapshotting = SimplySnapshotting.lines
|
||||
.pullback { $0 }
|
||||
|
||||
snapshotting.pathExtension = "html"
|
||||
return snapshotting
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ extension Room {
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizing.TrunkSize.RoomProxy {
|
||||
extension TrunkSize.RoomProxy {
|
||||
|
||||
// We need to make sure if registers got removed after a trunk
|
||||
// was already made / saved that we do not include registers that
|
||||
@@ -35,7 +35,7 @@ extension DuctSizing.TrunkSize.RoomProxy {
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizing.TrunkSize {
|
||||
extension TrunkSize {
|
||||
|
||||
var totalHeatingLoad: Double {
|
||||
rooms.reduce(into: 0) { $0 += $1.totalHeatingLoad }
|
||||
|
||||
129
Sources/ManualDClient/Interface.swift
Normal file
129
Sources/ManualDClient/Interface.swift
Normal file
@@ -0,0 +1,129 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Logging
|
||||
import ManualDCore
|
||||
|
||||
extension DependencyValues {
|
||||
public var manualD: ManualDClient {
|
||||
get { self[ManualDClient.self] }
|
||||
set { self[ManualDClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs manual-d duct sizing calculations.
|
||||
///
|
||||
///
|
||||
@DependencyClient
|
||||
public struct ManualDClient: Sendable {
|
||||
public var ductSize: @Sendable (DuctSizeRequest) async throws -> DuctSizeResponse
|
||||
public var frictionRate: @Sendable (FrictionRateRequest) async throws -> FrictionRate
|
||||
public var totalEquivalentLength: @Sendable (TotalEquivalentLengthRequest) async throws -> Int
|
||||
public var rectangularSize:
|
||||
@Sendable (RectangularSizeRequest) async throws -> RectangularSizeResponse
|
||||
|
||||
}
|
||||
|
||||
extension ManualDClient: TestDependencyKey {
|
||||
public static let testValue = Self()
|
||||
}
|
||||
|
||||
extension ManualDClient {
|
||||
|
||||
public struct DuctSizeRequest: Codable, Equatable, Sendable {
|
||||
public let designCFM: Int
|
||||
public let frictionRate: Double
|
||||
|
||||
public init(
|
||||
designCFM: Int,
|
||||
frictionRate: Double
|
||||
) {
|
||||
self.designCFM = designCFM
|
||||
self.frictionRate = frictionRate
|
||||
}
|
||||
}
|
||||
|
||||
public struct DuctSizeResponse: Codable, Equatable, Sendable {
|
||||
|
||||
public let calculatedSize: Double
|
||||
public let finalSize: Int
|
||||
public let flexSize: Int
|
||||
public let velocity: Int
|
||||
|
||||
public init(
|
||||
calculatedSize: Double,
|
||||
finalSize: Int,
|
||||
flexSize: Int,
|
||||
velocity: Int
|
||||
) {
|
||||
self.calculatedSize = calculatedSize
|
||||
self.finalSize = finalSize
|
||||
self.flexSize = flexSize
|
||||
self.velocity = velocity
|
||||
}
|
||||
}
|
||||
|
||||
public struct FrictionRateRequest: Codable, Equatable, Sendable {
|
||||
|
||||
public let externalStaticPressure: Double
|
||||
public let componentPressureLosses: [ComponentPressureLoss]
|
||||
public let totalEffectiveLength: Int
|
||||
|
||||
public init(
|
||||
externalStaticPressure: Double,
|
||||
componentPressureLosses: [ComponentPressureLoss],
|
||||
totalEffectiveLength: Int
|
||||
) {
|
||||
self.externalStaticPressure = externalStaticPressure
|
||||
self.componentPressureLosses = componentPressureLosses
|
||||
self.totalEffectiveLength = totalEffectiveLength
|
||||
}
|
||||
}
|
||||
|
||||
public struct FrictionRateResponse: Codable, Equatable, Sendable {
|
||||
|
||||
public let availableStaticPressure: Double
|
||||
public let frictionRate: Double
|
||||
|
||||
public init(availableStaticPressure: Double, frictionRate: Double) {
|
||||
self.availableStaticPressure = availableStaticPressure
|
||||
self.frictionRate = frictionRate
|
||||
}
|
||||
}
|
||||
|
||||
public struct TotalEquivalentLengthRequest: Codable, Equatable, Sendable {
|
||||
|
||||
public let trunkLengths: [Int]
|
||||
public let runoutLengths: [Int]
|
||||
public let effectiveLengthGroups: [EffectiveLengthGroup]
|
||||
|
||||
public init(
|
||||
trunkLengths: [Int],
|
||||
runoutLengths: [Int],
|
||||
effectiveLengthGroups: [EffectiveLengthGroup]
|
||||
) {
|
||||
self.trunkLengths = trunkLengths
|
||||
self.runoutLengths = runoutLengths
|
||||
self.effectiveLengthGroups = effectiveLengthGroups
|
||||
}
|
||||
}
|
||||
|
||||
public struct RectangularSizeRequest: Codable, Equatable, Sendable {
|
||||
public let roundSize: Int
|
||||
public let height: Int
|
||||
|
||||
public init(round roundSize: Int, height: Int) {
|
||||
self.roundSize = roundSize
|
||||
self.height = height
|
||||
}
|
||||
}
|
||||
|
||||
public struct RectangularSizeResponse: Codable, Equatable, Sendable {
|
||||
public let height: Int
|
||||
public let width: Int
|
||||
|
||||
public init(height: Int, width: Int) {
|
||||
self.height = height
|
||||
self.width = width
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ extension ManualDClient: DependencyKey {
|
||||
let finalSize = try roundSize(ductulatorSize)
|
||||
let flexSize = try flexSize(request)
|
||||
return .init(
|
||||
ductulatorSize: ductulatorSize,
|
||||
calculatedSize: ductulatorSize,
|
||||
finalSize: finalSize,
|
||||
flexSize: flexSize,
|
||||
velocity: velocity(cfm: request.designCFM, roundSize: finalSize)
|
||||
@@ -28,15 +28,15 @@ extension ManualDClient: DependencyKey {
|
||||
let totalComponentLosses = request.componentPressureLosses.total
|
||||
let availableStaticPressure = request.externalStaticPressure - totalComponentLosses
|
||||
let frictionRate = availableStaticPressure * 100.0 / Double(request.totalEffectiveLength)
|
||||
return .init(availableStaticPressure: availableStaticPressure, frictionRate: frictionRate)
|
||||
return .init(availableStaticPressure: availableStaticPressure, value: frictionRate)
|
||||
},
|
||||
totalEffectiveLength: { request in
|
||||
totalEquivalentLength: { request in
|
||||
let trunkLengths = request.trunkLengths.reduce(0) { $0 + $1 }
|
||||
let runoutLengths = request.runoutLengths.reduce(0) { $0 + $1 }
|
||||
let groupLengths = request.effectiveLengthGroups.totalEffectiveLength
|
||||
return trunkLengths + runoutLengths + groupLengths
|
||||
},
|
||||
equivalentRectangularDuct: { request in
|
||||
rectangularSize: { request in
|
||||
let width = (Double.pi * (pow(Double(request.roundSize) / 2.0, 2.0))) / Double(request.height)
|
||||
return .init(height: request.height, width: Int(width.rounded(.toNearestOrEven)))
|
||||
}
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Logging
|
||||
import ManualDCore
|
||||
|
||||
@DependencyClient
|
||||
public struct ManualDClient: Sendable {
|
||||
public var ductSize: @Sendable (DuctSizeRequest) async throws -> DuctSizeResponse
|
||||
public var frictionRate: @Sendable (FrictionRateRequest) async throws -> FrictionRateResponse
|
||||
public var totalEffectiveLength: @Sendable (TotalEffectiveLengthRequest) async throws -> Int
|
||||
public var equivalentRectangularDuct:
|
||||
@Sendable (EquivalentRectangularDuctRequest) async throws -> EquivalentRectangularDuctResponse
|
||||
|
||||
public func calculateSizes(
|
||||
rooms: [Room],
|
||||
trunks: [DuctSizing.TrunkSize],
|
||||
equipmentInfo: EquipmentInfo,
|
||||
maxSupplyLength: EffectiveLength,
|
||||
maxReturnLength: EffectiveLength,
|
||||
designFrictionRate: Double,
|
||||
projectSHR: Double,
|
||||
logger: Logger? = nil
|
||||
) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) {
|
||||
try await (
|
||||
calculateSizes(
|
||||
rooms: rooms, equipmentInfo: equipmentInfo,
|
||||
maxSupplyLength: maxSupplyLength, maxReturnLength: maxReturnLength,
|
||||
designFrictionRate: designFrictionRate, projectSHR: projectSHR
|
||||
),
|
||||
calculateSizes(
|
||||
rooms: rooms, trunks: trunks, equipmentInfo: equipmentInfo,
|
||||
maxSupplyLength: maxSupplyLength, maxReturnLength: maxReturnLength,
|
||||
designFrictionRate: designFrictionRate, projectSHR: projectSHR)
|
||||
)
|
||||
}
|
||||
|
||||
func calculateSizes(
|
||||
rooms: [Room],
|
||||
equipmentInfo: EquipmentInfo,
|
||||
maxSupplyLength: EffectiveLength,
|
||||
maxReturnLength: EffectiveLength,
|
||||
designFrictionRate: Double,
|
||||
projectSHR: Double,
|
||||
logger: Logger? = nil
|
||||
) async throws -> [DuctSizing.RoomContainer] {
|
||||
|
||||
var retval: [DuctSizing.RoomContainer] = []
|
||||
let totalHeatingLoad = rooms.totalHeatingLoad
|
||||
let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR)
|
||||
|
||||
for room in rooms {
|
||||
let heatingLoad = room.heatingLoadPerRegister
|
||||
let coolingLoad = room.coolingSensiblePerRegister(projectSHR: projectSHR)
|
||||
let heatingPercent = heatingLoad / totalHeatingLoad
|
||||
let coolingPercent = coolingLoad / totalCoolingSensible
|
||||
let heatingCFM = heatingPercent * Double(equipmentInfo.heatingCFM)
|
||||
let coolingCFM = coolingPercent * Double(equipmentInfo.coolingCFM)
|
||||
let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
|
||||
let sizes = try await self.ductSize(
|
||||
.init(designCFM: Int(designCFM.value), frictionRate: designFrictionRate)
|
||||
)
|
||||
|
||||
for n in 1...room.registerCount {
|
||||
|
||||
var rectangularWidth: Int? = nil
|
||||
let rectangularSize = room.rectangularSizes?
|
||||
.first(where: { $0.register == nil || $0.register == n })
|
||||
|
||||
if let rectangularSize {
|
||||
let response = try await self.equivalentRectangularDuct(
|
||||
.init(round: sizes.finalSize, height: rectangularSize.height)
|
||||
)
|
||||
rectangularWidth = response.width
|
||||
}
|
||||
|
||||
retval.append(
|
||||
.init(
|
||||
roomID: room.id,
|
||||
roomName: "\(room.name)-\(n)",
|
||||
roomRegister: n,
|
||||
heatingLoad: heatingLoad,
|
||||
coolingLoad: coolingLoad,
|
||||
heatingCFM: heatingCFM,
|
||||
coolingCFM: coolingCFM,
|
||||
designCFM: designCFM,
|
||||
roundSize: sizes.ductulatorSize,
|
||||
finalSize: sizes.finalSize,
|
||||
velocity: sizes.velocity,
|
||||
flexSize: sizes.flexSize,
|
||||
rectangularSize: rectangularSize,
|
||||
rectangularWidth: rectangularWidth
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
func calculateSizes(
|
||||
rooms: [Room],
|
||||
trunks: [DuctSizing.TrunkSize],
|
||||
equipmentInfo: EquipmentInfo,
|
||||
maxSupplyLength: EffectiveLength,
|
||||
maxReturnLength: EffectiveLength,
|
||||
designFrictionRate: Double,
|
||||
projectSHR: Double,
|
||||
logger: Logger? = nil
|
||||
) async throws -> [DuctSizing.TrunkContainer] {
|
||||
|
||||
var retval = [DuctSizing.TrunkContainer]()
|
||||
let totalHeatingLoad = rooms.totalHeatingLoad
|
||||
let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR)
|
||||
|
||||
for trunk in trunks {
|
||||
let heatingLoad = trunk.totalHeatingLoad
|
||||
let coolingLoad = trunk.totalCoolingSensible(projectSHR: projectSHR)
|
||||
let heatingPercent = heatingLoad / totalHeatingLoad
|
||||
let coolingPercent = coolingLoad / totalCoolingSensible
|
||||
let heatingCFM = heatingPercent * Double(equipmentInfo.heatingCFM)
|
||||
let coolingCFM = coolingPercent * Double(equipmentInfo.coolingCFM)
|
||||
let designCFM = DuctSizing.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
|
||||
let sizes = try await self.ductSize(
|
||||
.init(designCFM: Int(designCFM.value), frictionRate: designFrictionRate)
|
||||
)
|
||||
var width: Int? = nil
|
||||
if let height = trunk.height {
|
||||
let rectangularSize = try await self.equivalentRectangularDuct(
|
||||
.init(round: sizes.finalSize, height: height)
|
||||
)
|
||||
width = rectangularSize.width
|
||||
}
|
||||
|
||||
retval.append(
|
||||
.init(
|
||||
trunk: trunk,
|
||||
ductSize: .init(
|
||||
designCFM: designCFM,
|
||||
roundSize: sizes.ductulatorSize,
|
||||
finalSize: sizes.finalSize,
|
||||
velocity: sizes.velocity,
|
||||
flexSize: sizes.flexSize,
|
||||
height: trunk.height,
|
||||
width: width
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ManualDClient: TestDependencyKey {
|
||||
public static let testValue = Self()
|
||||
}
|
||||
|
||||
extension DependencyValues {
|
||||
public var manualD: ManualDClient {
|
||||
get { self[ManualDClient.self] }
|
||||
set { self[ManualDClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Duct Size
|
||||
extension ManualDClient {
|
||||
public struct DuctSizeRequest: Codable, Equatable, Sendable {
|
||||
public let designCFM: Int
|
||||
public let frictionRate: Double
|
||||
|
||||
public init(
|
||||
designCFM: Int,
|
||||
frictionRate: Double
|
||||
) {
|
||||
self.designCFM = designCFM
|
||||
self.frictionRate = frictionRate
|
||||
}
|
||||
}
|
||||
|
||||
public struct DuctSizeResponse: Codable, Equatable, Sendable {
|
||||
|
||||
public let ductulatorSize: Double
|
||||
public let finalSize: Int
|
||||
public let flexSize: Int
|
||||
public let velocity: Int
|
||||
|
||||
public init(
|
||||
ductulatorSize: Double,
|
||||
finalSize: Int,
|
||||
flexSize: Int,
|
||||
velocity: Int
|
||||
) {
|
||||
self.ductulatorSize = ductulatorSize
|
||||
self.finalSize = finalSize
|
||||
self.flexSize = flexSize
|
||||
self.velocity = velocity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Friction Rate
|
||||
extension ManualDClient {
|
||||
public struct FrictionRateRequest: Codable, Equatable, Sendable {
|
||||
|
||||
public let externalStaticPressure: Double
|
||||
public let componentPressureLosses: [ComponentPressureLoss]
|
||||
public let totalEffectiveLength: Int
|
||||
|
||||
public init(
|
||||
externalStaticPressure: Double,
|
||||
componentPressureLosses: [ComponentPressureLoss],
|
||||
totalEffectiveLength: Int
|
||||
) {
|
||||
self.externalStaticPressure = externalStaticPressure
|
||||
self.componentPressureLosses = componentPressureLosses
|
||||
self.totalEffectiveLength = totalEffectiveLength
|
||||
}
|
||||
}
|
||||
|
||||
public struct FrictionRateResponse: Codable, Equatable, Sendable {
|
||||
|
||||
public let availableStaticPressure: Double
|
||||
public let frictionRate: Double
|
||||
|
||||
public init(availableStaticPressure: Double, frictionRate: Double) {
|
||||
self.availableStaticPressure = availableStaticPressure
|
||||
self.frictionRate = frictionRate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Total Effective Length
|
||||
extension ManualDClient {
|
||||
public struct TotalEffectiveLengthRequest: Codable, Equatable, Sendable {
|
||||
|
||||
public let trunkLengths: [Int]
|
||||
public let runoutLengths: [Int]
|
||||
public let effectiveLengthGroups: [EffectiveLengthGroup]
|
||||
|
||||
public init(
|
||||
trunkLengths: [Int],
|
||||
runoutLengths: [Int],
|
||||
effectiveLengthGroups: [EffectiveLengthGroup]
|
||||
) {
|
||||
self.trunkLengths = trunkLengths
|
||||
self.runoutLengths = runoutLengths
|
||||
self.effectiveLengthGroups = effectiveLengthGroups
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Equivalent Rectangular Duct
|
||||
extension ManualDClient {
|
||||
public struct EquivalentRectangularDuctRequest: Codable, Equatable, Sendable {
|
||||
public let roundSize: Int
|
||||
public let height: Int
|
||||
|
||||
public init(round roundSize: Int, height: Int) {
|
||||
self.roundSize = roundSize
|
||||
self.height = height
|
||||
}
|
||||
}
|
||||
|
||||
public struct EquivalentRectangularDuctResponse: Codable, Equatable, Sendable {
|
||||
public let height: Int
|
||||
public let width: Int
|
||||
|
||||
public init(height: Int, width: Int) {
|
||||
self.height = height
|
||||
self.width = width
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
|
||||
public struct ComponentPressureLoss: Codable, Equatable, Identifiable, Sendable {
|
||||
@@ -89,7 +90,61 @@ public typealias ComponentPressureLosses = [String: Double]
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == ComponentPressureLoss {
|
||||
public static func mock(projectID: Project.ID) -> Self {
|
||||
ComponentPressureLoss.mock(projectID: projectID)
|
||||
}
|
||||
}
|
||||
|
||||
extension ComponentPressureLoss {
|
||||
public static func mock(projectID: Project.ID) -> [Self] {
|
||||
@Dependency(\.uuid) var uuid
|
||||
@Dependency(\.date.now) var now
|
||||
|
||||
return [
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "evaporator-coil",
|
||||
value: 0.2,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "filter",
|
||||
value: 0.1,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "supply-outlet",
|
||||
value: 0.03,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "return-grille",
|
||||
value: 0.03,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "balancing-damper",
|
||||
value: 0.03,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
public static var mock: [Self] {
|
||||
[
|
||||
.init(
|
||||
|
||||
256
Sources/ManualDCore/DuctSizes.swift
Normal file
256
Sources/ManualDCore/DuctSizes.swift
Normal file
@@ -0,0 +1,256 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
|
||||
public struct DuctSizes: Codable, Equatable, Sendable {
|
||||
|
||||
public let rooms: [RoomContainer]
|
||||
public let trunks: [TrunkContainer]
|
||||
|
||||
public init(
|
||||
rooms: [DuctSizes.RoomContainer],
|
||||
trunks: [DuctSizes.TrunkContainer]
|
||||
) {
|
||||
self.rooms = rooms
|
||||
self.trunks = trunks
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizes {
|
||||
|
||||
public struct SizeContainer: Codable, Equatable, Sendable {
|
||||
|
||||
public let rectangularID: Room.RectangularSize.ID?
|
||||
public let designCFM: DesignCFM
|
||||
public let roundSize: Double
|
||||
public let finalSize: Int
|
||||
public let velocity: Int
|
||||
public let flexSize: Int
|
||||
public let height: Int?
|
||||
public let width: Int?
|
||||
|
||||
public init(
|
||||
rectangularID: Room.RectangularSize.ID? = nil,
|
||||
designCFM: DuctSizes.DesignCFM,
|
||||
roundSize: Double,
|
||||
finalSize: Int,
|
||||
velocity: Int,
|
||||
flexSize: Int,
|
||||
height: Int? = nil,
|
||||
width: Int? = nil
|
||||
) {
|
||||
self.rectangularID = rectangularID
|
||||
self.designCFM = designCFM
|
||||
self.roundSize = roundSize
|
||||
self.finalSize = finalSize
|
||||
self.velocity = velocity
|
||||
self.flexSize = flexSize
|
||||
self.height = height
|
||||
self.width = width
|
||||
}
|
||||
}
|
||||
|
||||
@dynamicMemberLookup
|
||||
public struct RoomContainer: Codable, Equatable, Sendable {
|
||||
|
||||
public let roomID: Room.ID
|
||||
public let roomName: String
|
||||
public let roomRegister: Int
|
||||
public let heatingLoad: Double
|
||||
public let coolingLoad: Double
|
||||
public let heatingCFM: Double
|
||||
public let coolingCFM: Double
|
||||
public let ductSize: SizeContainer
|
||||
|
||||
public init(
|
||||
roomID: Room.ID,
|
||||
roomName: String,
|
||||
roomRegister: Int,
|
||||
heatingLoad: Double,
|
||||
coolingLoad: Double,
|
||||
heatingCFM: Double,
|
||||
coolingCFM: Double,
|
||||
ductSize: SizeContainer
|
||||
) {
|
||||
self.roomID = roomID
|
||||
self.roomName = roomName
|
||||
self.roomRegister = roomRegister
|
||||
self.heatingLoad = heatingLoad
|
||||
self.coolingLoad = coolingLoad
|
||||
self.heatingCFM = heatingCFM
|
||||
self.coolingCFM = coolingCFM
|
||||
self.ductSize = ductSize
|
||||
}
|
||||
|
||||
public subscript<T>(dynamicMember keyPath: KeyPath<DuctSizes.SizeContainer, T>) -> T {
|
||||
ductSize[keyPath: keyPath]
|
||||
}
|
||||
}
|
||||
|
||||
public enum DesignCFM: Codable, Equatable, Sendable {
|
||||
case heating(Double)
|
||||
case cooling(Double)
|
||||
|
||||
public init(heating: Double, cooling: Double) {
|
||||
if heating >= cooling {
|
||||
self = .heating(heating)
|
||||
} else {
|
||||
self = .cooling(cooling)
|
||||
}
|
||||
}
|
||||
|
||||
public var value: Double {
|
||||
switch self {
|
||||
case .heating(let value): return value
|
||||
case .cooling(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Represents the database model that the duct sizes have been calculated
|
||||
// for.
|
||||
@dynamicMemberLookup
|
||||
public struct TrunkContainer: Codable, Equatable, Identifiable, Sendable {
|
||||
public var id: TrunkSize.ID { trunk.id }
|
||||
|
||||
public let trunk: TrunkSize
|
||||
public let ductSize: SizeContainer
|
||||
|
||||
public init(
|
||||
trunk: TrunkSize,
|
||||
ductSize: SizeContainer
|
||||
) {
|
||||
self.trunk = trunk
|
||||
self.ductSize = ductSize
|
||||
}
|
||||
|
||||
public subscript<T>(dynamicMember keyPath: KeyPath<TrunkSize, T>) -> T {
|
||||
trunk[keyPath: keyPath]
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension DuctSizes {
|
||||
public static func mock(
|
||||
equipmentInfo: EquipmentInfo,
|
||||
rooms: [Room],
|
||||
trunks: [TrunkSize]
|
||||
) -> Self {
|
||||
|
||||
let totalHeatingLoad = rooms.totalHeatingLoad
|
||||
let totalCoolingLoad = rooms.totalCoolingLoad
|
||||
|
||||
let roomContainers = rooms.reduce(into: [RoomContainer]()) { array, room in
|
||||
array += RoomContainer.mock(
|
||||
room: room,
|
||||
totalHeatingLoad: totalHeatingLoad,
|
||||
totalCoolingLoad: totalCoolingLoad,
|
||||
totalHeatingCFM: Double(equipmentInfo.heatingCFM),
|
||||
totalCoolingCFM: Double(equipmentInfo.coolingCFM)
|
||||
)
|
||||
}
|
||||
|
||||
return .init(
|
||||
rooms: roomContainers,
|
||||
trunks: TrunkContainer.mock(
|
||||
trunks: trunks,
|
||||
totalHeatingLoad: totalHeatingLoad,
|
||||
totalCoolingLoad: totalCoolingLoad,
|
||||
totalHeatingCFM: Double(equipmentInfo.heatingCFM),
|
||||
totalCoolingCFM: Double(equipmentInfo.coolingCFM)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizes.RoomContainer {
|
||||
public static func mock(
|
||||
room: Room,
|
||||
totalHeatingLoad: Double,
|
||||
totalCoolingLoad: Double,
|
||||
totalHeatingCFM: Double,
|
||||
totalCoolingCFM: Double
|
||||
) -> [Self] {
|
||||
var retval = [DuctSizes.RoomContainer]()
|
||||
let heatingLoad = room.heatingLoad / Double(room.registerCount)
|
||||
let heatingFraction = heatingLoad / totalHeatingLoad
|
||||
let heatingCFM = totalHeatingCFM * heatingFraction
|
||||
// Not really accurate, but works for mocks.
|
||||
let coolingLoad = room.coolingTotal / Double(room.registerCount)
|
||||
let coolingFraction = coolingLoad / totalCoolingLoad
|
||||
let coolingCFM = totalCoolingCFM * coolingFraction
|
||||
|
||||
for n in 1...room.registerCount {
|
||||
|
||||
retval.append(
|
||||
.init(
|
||||
roomID: room.id,
|
||||
roomName: room.name,
|
||||
roomRegister: n,
|
||||
heatingLoad: heatingLoad,
|
||||
coolingLoad: coolingLoad,
|
||||
heatingCFM: heatingCFM,
|
||||
coolingCFM: coolingCFM,
|
||||
ductSize: .init(
|
||||
rectangularID: nil,
|
||||
designCFM: .init(heating: heatingCFM, cooling: coolingCFM),
|
||||
roundSize: 7,
|
||||
finalSize: 8,
|
||||
velocity: 489,
|
||||
flexSize: 8,
|
||||
height: nil,
|
||||
width: nil
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
return retval
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DuctSizes.TrunkContainer {
|
||||
|
||||
public static func mock(
|
||||
trunks: [TrunkSize],
|
||||
totalHeatingLoad: Double,
|
||||
totalCoolingLoad: Double,
|
||||
totalHeatingCFM: Double,
|
||||
totalCoolingCFM: Double
|
||||
) -> [Self] {
|
||||
trunks.reduce(into: []) { array, trunk in
|
||||
array.append(
|
||||
.init(
|
||||
trunk: trunk,
|
||||
ductSize: .init(
|
||||
designCFM: .init(heating: totalHeatingCFM, cooling: totalCoolingCFM),
|
||||
roundSize: 18,
|
||||
finalSize: 20,
|
||||
velocity: 987,
|
||||
flexSize: 20
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,248 +0,0 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
|
||||
public enum DuctSizing {
|
||||
|
||||
public struct RectangularDuct: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public let id: UUID
|
||||
public let register: Int?
|
||||
public let height: Int
|
||||
|
||||
public init(
|
||||
id: UUID = .init(),
|
||||
register: Int? = nil,
|
||||
height: Int,
|
||||
) {
|
||||
self.id = id
|
||||
self.register = register
|
||||
self.height = height
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct SizeContainer: Codable, Equatable, Sendable {
|
||||
|
||||
public let designCFM: DesignCFM
|
||||
public let roundSize: Double
|
||||
public let finalSize: Int
|
||||
public let velocity: Int
|
||||
public let flexSize: Int
|
||||
public let height: Int?
|
||||
public let width: Int?
|
||||
|
||||
public init(
|
||||
designCFM: DuctSizing.DesignCFM,
|
||||
roundSize: Double,
|
||||
finalSize: Int,
|
||||
velocity: Int,
|
||||
flexSize: Int,
|
||||
height: Int? = nil,
|
||||
width: Int? = nil
|
||||
) {
|
||||
self.designCFM = designCFM
|
||||
self.roundSize = roundSize
|
||||
self.finalSize = finalSize
|
||||
self.velocity = velocity
|
||||
self.flexSize = flexSize
|
||||
self.height = height
|
||||
self.width = width
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Uses SizeContainer
|
||||
|
||||
public struct RoomContainer: Codable, Equatable, Sendable {
|
||||
|
||||
public let roomID: Room.ID
|
||||
public let roomName: String
|
||||
public let roomRegister: Int
|
||||
public let heatingLoad: Double
|
||||
public let coolingLoad: Double
|
||||
public let heatingCFM: Double
|
||||
public let coolingCFM: Double
|
||||
public let designCFM: DesignCFM
|
||||
public let roundSize: Double
|
||||
public let finalSize: Int
|
||||
public let velocity: Int
|
||||
public let flexSize: Int
|
||||
public let rectangularSize: RectangularDuct?
|
||||
public let rectangularWidth: Int?
|
||||
|
||||
public init(
|
||||
roomID: Room.ID,
|
||||
roomName: String,
|
||||
roomRegister: Int,
|
||||
heatingLoad: Double,
|
||||
coolingLoad: Double,
|
||||
heatingCFM: Double,
|
||||
coolingCFM: Double,
|
||||
designCFM: DesignCFM,
|
||||
roundSize: Double,
|
||||
finalSize: Int,
|
||||
velocity: Int,
|
||||
flexSize: Int,
|
||||
rectangularSize: RectangularDuct? = nil,
|
||||
rectangularWidth: Int? = nil
|
||||
) {
|
||||
self.roomID = roomID
|
||||
self.roomName = roomName
|
||||
self.roomRegister = roomRegister
|
||||
self.heatingLoad = heatingLoad
|
||||
self.coolingLoad = coolingLoad
|
||||
self.heatingCFM = heatingCFM
|
||||
self.coolingCFM = coolingCFM
|
||||
self.designCFM = designCFM
|
||||
self.roundSize = roundSize
|
||||
self.finalSize = finalSize
|
||||
self.velocity = velocity
|
||||
self.flexSize = flexSize
|
||||
self.rectangularSize = rectangularSize
|
||||
self.rectangularWidth = rectangularWidth
|
||||
}
|
||||
}
|
||||
|
||||
public enum DesignCFM: Codable, Equatable, Sendable {
|
||||
case heating(Double)
|
||||
case cooling(Double)
|
||||
|
||||
public init(heating: Double, cooling: Double) {
|
||||
if heating >= cooling {
|
||||
self = .heating(heating)
|
||||
} else {
|
||||
self = .cooling(cooling)
|
||||
}
|
||||
}
|
||||
|
||||
public var value: Double {
|
||||
switch self {
|
||||
case .heating(let value): return value
|
||||
case .cooling(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DuctSizing {
|
||||
|
||||
// Represents the database model that the duct sizes have been calculated
|
||||
// for.
|
||||
@dynamicMemberLookup
|
||||
public struct TrunkContainer: Codable, Equatable, Identifiable, Sendable {
|
||||
public var id: TrunkSize.ID { trunk.id }
|
||||
|
||||
public let trunk: TrunkSize
|
||||
public let ductSize: SizeContainer
|
||||
|
||||
public init(
|
||||
trunk: TrunkSize,
|
||||
ductSize: SizeContainer
|
||||
) {
|
||||
self.trunk = trunk
|
||||
self.ductSize = ductSize
|
||||
}
|
||||
|
||||
public subscript<T>(dynamicMember keyPath: KeyPath<DuctSizing.TrunkSize, T>) -> T {
|
||||
trunk[keyPath: keyPath]
|
||||
}
|
||||
|
||||
public subscript<T>(dynamicMember keyPath: KeyPath<DuctSizing.SizeContainer, T>) -> T {
|
||||
ductSize[keyPath: keyPath]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add an optional label that the user can set.
|
||||
|
||||
// Represents the database model.
|
||||
public struct TrunkSize: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public let id: UUID
|
||||
public let projectID: Project.ID
|
||||
public let type: TrunkType
|
||||
public let rooms: [RoomProxy]
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
id: UUID,
|
||||
projectID: Project.ID,
|
||||
type: DuctSizing.TrunkSize.TrunkType,
|
||||
rooms: [DuctSizing.TrunkSize.RoomProxy],
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.projectID = projectID
|
||||
self.type = type
|
||||
self.rooms = rooms
|
||||
self.height = height
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DuctSizing.TrunkSize {
|
||||
public struct Create: Codable, Equatable, Sendable {
|
||||
|
||||
public let projectID: Project.ID
|
||||
public let type: TrunkType
|
||||
public let rooms: [Room.ID: [Int]]
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
projectID: Project.ID,
|
||||
type: DuctSizing.TrunkSize.TrunkType,
|
||||
rooms: [Room.ID: [Int]],
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.projectID = projectID
|
||||
self.type = type
|
||||
self.rooms = rooms
|
||||
self.height = height
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public struct Update: Codable, Equatable, Sendable {
|
||||
|
||||
public let type: TrunkType?
|
||||
public let rooms: [Room.ID: [Int]]?
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
type: DuctSizing.TrunkSize.TrunkType? = nil,
|
||||
rooms: [Room.ID: [Int]]? = nil,
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.type = type
|
||||
self.rooms = rooms
|
||||
self.height = height
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make registers non-optional
|
||||
public struct RoomProxy: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public var id: Room.ID { room.id }
|
||||
public let room: Room
|
||||
public let registers: [Int]
|
||||
|
||||
public init(room: Room, registers: [Int]) {
|
||||
self.room = room
|
||||
self.registers = registers
|
||||
}
|
||||
}
|
||||
|
||||
public enum TrunkType: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
case `return`
|
||||
case supply
|
||||
|
||||
public static let allCases = [Self.supply, .return]
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,44 @@ extension Array where Element == EffectiveLength.Group {
|
||||
#if DEBUG
|
||||
|
||||
extension EffectiveLength {
|
||||
|
||||
public static func mock(projectID: Project.ID) -> [Self] {
|
||||
@Dependency(\.uuid) var uuid
|
||||
@Dependency(\.date.now) var now
|
||||
|
||||
return [
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "Supply - 1",
|
||||
type: .supply,
|
||||
straightLengths: [10, 25],
|
||||
groups: [
|
||||
.init(group: 1, letter: "a", value: 20),
|
||||
.init(group: 2, letter: "b", value: 30, quantity: 1),
|
||||
.init(group: 3, letter: "a", value: 10, quantity: 1),
|
||||
.init(group: 12, letter: "a", value: 10, quantity: 1),
|
||||
],
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "Return - 1",
|
||||
type: .return,
|
||||
straightLengths: [10, 20, 5],
|
||||
groups: [
|
||||
.init(group: 5, letter: "a", value: 10),
|
||||
.init(group: 6, letter: "a", value: 15, quantity: 1),
|
||||
.init(group: 7, letter: "a", value: 20, quantity: 1),
|
||||
],
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
public static let mocks: [Self] = [
|
||||
.init(
|
||||
id: UUID(0),
|
||||
|
||||
@@ -70,6 +70,21 @@ extension EquipmentInfo {
|
||||
|
||||
#if DEBUG
|
||||
extension EquipmentInfo {
|
||||
|
||||
public static func mock(projectID: Project.ID) -> Self {
|
||||
@Dependency(\.uuid) var uuid
|
||||
@Dependency(\.date.now) var now
|
||||
|
||||
return .init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
heatingCFM: 900,
|
||||
coolingCFM: 1000,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
)
|
||||
}
|
||||
|
||||
public static let mock = Self(
|
||||
id: UUID(0),
|
||||
projectID: UUID(0),
|
||||
|
||||
57
Sources/ManualDCore/FrictionRate.swift
Normal file
57
Sources/ManualDCore/FrictionRate.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
/// Holds onto values returned when calculating the design
|
||||
/// friction rate for a project.
|
||||
public struct FrictionRate: Codable, Equatable, Sendable {
|
||||
public let availableStaticPressure: Double
|
||||
public let value: Double
|
||||
public var hasErrors: Bool { error != nil }
|
||||
|
||||
public init(
|
||||
availableStaticPressure: Double,
|
||||
value: Double
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension FrictionRate {
|
||||
public static let mock = Self(availableStaticPressure: 0.21, value: 0.11)
|
||||
}
|
||||
#endif
|
||||
24
Sources/ManualDCore/Numbers+string.swift
Normal file
24
Sources/ManualDCore/Numbers+string.swift
Normal 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)!
|
||||
}
|
||||
12
Sources/ManualDCore/PageRequest+extensions.swift
Normal file
12
Sources/ManualDCore/PageRequest+extensions.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
@@ -114,16 +140,22 @@ extension Project {
|
||||
#if DEBUG
|
||||
|
||||
extension Project {
|
||||
public static let mock = Self(
|
||||
id: UUID(0),
|
||||
name: "Testy McTestface",
|
||||
streetAddress: "1234 Sesame Street",
|
||||
city: "Monroe",
|
||||
state: "OH",
|
||||
zipCode: "55555",
|
||||
createdAt: Date(),
|
||||
updatedAt: Date()
|
||||
)
|
||||
|
||||
public static var mock: Self {
|
||||
@Dependency(\.uuid) var uuid
|
||||
@Dependency(\.date.now) var now
|
||||
|
||||
return .init(
|
||||
id: uuid(),
|
||||
name: "Testy McTestface",
|
||||
streetAddress: "1234 Sesame Street",
|
||||
city: "Monroe",
|
||||
state: "OH",
|
||||
zipCode: "55555",
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -9,7 +9,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
|
||||
public let coolingTotal: Double
|
||||
public let coolingSensible: Double?
|
||||
public let registerCount: Int
|
||||
public let rectangularSizes: [DuctSizing.RectangularDuct]?
|
||||
public let rectangularSizes: [RectangularSize]?
|
||||
public let createdAt: Date
|
||||
public let updatedAt: Date
|
||||
|
||||
@@ -21,7 +21,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
|
||||
coolingTotal: Double,
|
||||
coolingSensible: Double? = nil,
|
||||
registerCount: Int = 1,
|
||||
rectangularSizes: [DuctSizing.RectangularDuct]? = nil,
|
||||
rectangularSizes: [RectangularSize]? = nil,
|
||||
createdAt: Date,
|
||||
updatedAt: Date
|
||||
) {
|
||||
@@ -65,13 +65,30 @@ extension Room {
|
||||
}
|
||||
}
|
||||
|
||||
public struct RectangularSize: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public let id: UUID
|
||||
public let register: Int?
|
||||
public let height: Int
|
||||
|
||||
public init(
|
||||
id: UUID = .init(),
|
||||
register: Int? = nil,
|
||||
height: Int,
|
||||
) {
|
||||
self.id = id
|
||||
self.register = register
|
||||
self.height = height
|
||||
}
|
||||
}
|
||||
|
||||
public struct Update: Codable, Equatable, Sendable {
|
||||
public let name: String?
|
||||
public let heatingLoad: Double?
|
||||
public let coolingTotal: Double?
|
||||
public let coolingSensible: Double?
|
||||
public let registerCount: Int?
|
||||
public let rectangularSizes: [DuctSizing.RectangularDuct]?
|
||||
public let rectangularSizes: [RectangularSize]?
|
||||
|
||||
public init(
|
||||
name: String? = nil,
|
||||
@@ -89,7 +106,7 @@ extension Room {
|
||||
}
|
||||
|
||||
public init(
|
||||
rectangularSizes: [DuctSizing.RectangularDuct]
|
||||
rectangularSizes: [RectangularSize]
|
||||
) {
|
||||
self.name = nil
|
||||
self.heatingLoad = nil
|
||||
@@ -154,6 +171,86 @@ extension Array where Element == Room {
|
||||
updatedAt: Date()
|
||||
),
|
||||
]
|
||||
|
||||
public static func mock(projectID: Project.ID) -> [Self] {
|
||||
@Dependency(\.uuid) var uuid
|
||||
@Dependency(\.date.now) var now
|
||||
|
||||
return [
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "Bed-1",
|
||||
heatingLoad: 3913,
|
||||
coolingTotal: 2472,
|
||||
coolingSensible: nil,
|
||||
registerCount: 1,
|
||||
rectangularSizes: nil,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "Entry",
|
||||
heatingLoad: 8284,
|
||||
coolingTotal: 2916,
|
||||
coolingSensible: nil,
|
||||
registerCount: 2,
|
||||
rectangularSizes: nil,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "Family Room",
|
||||
heatingLoad: 9785,
|
||||
coolingTotal: 7446,
|
||||
coolingSensible: nil,
|
||||
registerCount: 3,
|
||||
rectangularSizes: nil,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "Kitchen",
|
||||
heatingLoad: 4518,
|
||||
coolingTotal: 5096,
|
||||
coolingSensible: nil,
|
||||
registerCount: 2,
|
||||
rectangularSizes: nil,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "Living Room",
|
||||
heatingLoad: 7553,
|
||||
coolingTotal: 6829,
|
||||
coolingSensible: nil,
|
||||
registerCount: 2,
|
||||
rectangularSizes: nil,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
.init(
|
||||
id: uuid(),
|
||||
projectID: projectID,
|
||||
name: "Master",
|
||||
heatingLoad: 8202,
|
||||
coolingTotal: 2076,
|
||||
coolingSensible: nil,
|
||||
registerCount: 2,
|
||||
rectangularSizes: nil,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -627,7 +632,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
}
|
||||
Method.delete
|
||||
Query {
|
||||
Field("rectangularSize") { DuctSizing.RectangularDuct.ID.parser() }
|
||||
Field("rectangularSize") { Room.RectangularSize.ID.parser() }
|
||||
Field("register") { Int.parser() }
|
||||
}
|
||||
.map(.memberwise(DeleteRectangularDuct.init))
|
||||
@@ -642,7 +647,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Body {
|
||||
FormData {
|
||||
Optionally {
|
||||
Field("id") { DuctSizing.RectangularDuct.ID.parser() }
|
||||
Field("id") { Room.RectangularSize.ID.parser() }
|
||||
}
|
||||
Field("register") { Int.parser() }
|
||||
Field("height") { Int.parser() }
|
||||
@@ -658,19 +663,19 @@ extension SiteRoute.View.ProjectRoute {
|
||||
|
||||
public struct DeleteRectangularDuct: Equatable, Sendable {
|
||||
|
||||
public let rectangularSizeID: DuctSizing.RectangularDuct.ID
|
||||
public let rectangularSizeID: Room.RectangularSize.ID
|
||||
public let register: Int
|
||||
|
||||
public init(rectangularSizeID: DuctSizing.RectangularDuct.ID, register: Int) {
|
||||
public init(rectangularSizeID: Room.RectangularSize.ID, register: Int) {
|
||||
self.rectangularSizeID = rectangularSizeID
|
||||
self.register = register
|
||||
}
|
||||
}
|
||||
|
||||
public enum TrunkRoute: Equatable, Sendable {
|
||||
case delete(DuctSizing.TrunkSize.ID)
|
||||
case delete(TrunkSize.ID)
|
||||
case submit(TrunkSizeForm)
|
||||
case update(DuctSizing.TrunkSize.ID, TrunkSizeForm)
|
||||
case update(TrunkSize.ID, TrunkSizeForm)
|
||||
|
||||
public static let rootPath = "trunk"
|
||||
|
||||
@@ -678,7 +683,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Route(.case(Self.delete)) {
|
||||
Path {
|
||||
rootPath
|
||||
DuctSizing.TrunkSize.ID.parser()
|
||||
TrunkSize.ID.parser()
|
||||
}
|
||||
Method.delete
|
||||
}
|
||||
@@ -690,7 +695,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Body {
|
||||
FormData {
|
||||
Field("projectID") { Project.ID.parser() }
|
||||
Field("type") { DuctSizing.TrunkSize.TrunkType.parser() }
|
||||
Field("type") { TrunkSize.TrunkType.parser() }
|
||||
Optionally {
|
||||
Field("height") { Int.parser() }
|
||||
|
||||
@@ -708,13 +713,13 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Route(.case(Self.update)) {
|
||||
Path {
|
||||
rootPath
|
||||
DuctSizing.TrunkSize.ID.parser()
|
||||
TrunkSize.ID.parser()
|
||||
}
|
||||
Method.patch
|
||||
Body {
|
||||
FormData {
|
||||
Field("projectID") { Project.ID.parser() }
|
||||
Field("type") { DuctSizing.TrunkSize.TrunkType.parser() }
|
||||
Field("type") { TrunkSize.TrunkType.parser() }
|
||||
Optionally {
|
||||
Field("height") { Int.parser() }
|
||||
}
|
||||
@@ -732,17 +737,43 @@ extension SiteRoute.View.ProjectRoute {
|
||||
}
|
||||
|
||||
public struct RoomRectangularForm: Equatable, Sendable {
|
||||
public let id: DuctSizing.RectangularDuct.ID?
|
||||
|
||||
public let id: Room.RectangularSize.ID?
|
||||
public let register: Int
|
||||
public let height: Int
|
||||
|
||||
public init(
|
||||
id: Room.RectangularSize.ID? = nil,
|
||||
register: Int,
|
||||
height: Int
|
||||
) {
|
||||
self.id = id
|
||||
self.register = register
|
||||
self.height = height
|
||||
}
|
||||
}
|
||||
|
||||
public struct TrunkSizeForm: Equatable, Sendable {
|
||||
|
||||
public let projectID: Project.ID
|
||||
public let type: DuctSizing.TrunkSize.TrunkType
|
||||
public let type: TrunkSize.TrunkType
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
public let rooms: [String]
|
||||
|
||||
public init(
|
||||
projectID: Project.ID,
|
||||
type: TrunkSize.TrunkType,
|
||||
height: Int? = nil,
|
||||
name: String? = nil,
|
||||
rooms: [String]
|
||||
) {
|
||||
self.projectID = projectID
|
||||
self.type = type
|
||||
self.height = height
|
||||
self.name = name
|
||||
self.rooms = rooms
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
114
Sources/ManualDCore/TrunkSize.swift
Normal file
114
Sources/ManualDCore/TrunkSize.swift
Normal file
@@ -0,0 +1,114 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
|
||||
// Represents the database model.
|
||||
public struct TrunkSize: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public let id: UUID
|
||||
public let projectID: Project.ID
|
||||
public let type: TrunkType
|
||||
public let rooms: [RoomProxy]
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
id: UUID,
|
||||
projectID: Project.ID,
|
||||
type: TrunkType,
|
||||
rooms: [RoomProxy],
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.projectID = projectID
|
||||
self.type = type
|
||||
self.rooms = rooms
|
||||
self.height = height
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
extension TrunkSize {
|
||||
public struct Create: Codable, Equatable, Sendable {
|
||||
|
||||
public let projectID: Project.ID
|
||||
public let type: TrunkType
|
||||
public let rooms: [Room.ID: [Int]]
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
projectID: Project.ID,
|
||||
type: TrunkType,
|
||||
rooms: [Room.ID: [Int]],
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.projectID = projectID
|
||||
self.type = type
|
||||
self.rooms = rooms
|
||||
self.height = height
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public struct Update: Codable, Equatable, Sendable {
|
||||
|
||||
public let type: TrunkType?
|
||||
public let rooms: [Room.ID: [Int]]?
|
||||
public let height: Int?
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
type: TrunkType? = nil,
|
||||
rooms: [Room.ID: [Int]]? = nil,
|
||||
height: Int? = nil,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.type = type
|
||||
self.rooms = rooms
|
||||
self.height = height
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public struct RoomProxy: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
public var id: Room.ID { room.id }
|
||||
public let room: Room
|
||||
public let registers: [Int]
|
||||
|
||||
public init(room: Room, registers: [Int]) {
|
||||
self.room = room
|
||||
self.registers = registers
|
||||
}
|
||||
}
|
||||
|
||||
public enum TrunkType: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
case `return`
|
||||
case supply
|
||||
|
||||
public static let allCases = [Self.supply, .return]
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension TrunkSize {
|
||||
public static func mock(projectID: Project.ID, rooms: [Room]) -> [Self] {
|
||||
@Dependency(\.uuid) var uuid
|
||||
|
||||
let allRooms = rooms.reduce(into: [TrunkSize.RoomProxy]()) { array, room in
|
||||
var registers = [Int]()
|
||||
for n in 1...room.registerCount {
|
||||
registers.append(n)
|
||||
}
|
||||
array.append(.init(room: room, registers: registers))
|
||||
}
|
||||
|
||||
return [
|
||||
.init(id: uuid(), projectID: projectID, type: .supply, rooms: allRooms),
|
||||
.init(id: uuid(), projectID: projectID, type: .return, rooms: allRooms),
|
||||
]
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -65,3 +65,15 @@ extension User {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
extension User {
|
||||
public static var mock: Self {
|
||||
@Dependency(\.uuid) var uuid
|
||||
@Dependency(\.date.now) var now
|
||||
return .init(id: uuid(), email: "testy@example.com", createdAt: now, updatedAt: now)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
|
||||
extension User {
|
||||
@@ -113,3 +114,27 @@ extension User.Profile {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension User.Profile {
|
||||
public static func mock(userID: User.ID) -> Self {
|
||||
@Dependency(\.uuid) var uuid
|
||||
@Dependency(\.date.now) var now
|
||||
|
||||
return .init(
|
||||
id: uuid(),
|
||||
userID: userID,
|
||||
firstName: "Testy",
|
||||
lastName: "McTestface",
|
||||
companyName: "Acme Co.",
|
||||
streetAddress: "1234 Sesame St",
|
||||
city: "Monroe",
|
||||
state: "OH",
|
||||
zipCode: "55555",
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
141
Sources/PdfClient/Interface.swift
Normal file
141
Sources/PdfClient/Interface.swift
Normal file
@@ -0,0 +1,141 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Elementary
|
||||
import EnvClient
|
||||
import FileClient
|
||||
import Foundation
|
||||
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 generatePdf: @Sendable (Project.ID, any HTML & Sendable) async throws -> Response
|
||||
|
||||
public func generatePdf(request: Request) async throws -> Response {
|
||||
let html = try await self.html(request)
|
||||
return try await self.generatePdf(request.project.id, html)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PdfClient: DependencyKey {
|
||||
public static let testValue = Self()
|
||||
|
||||
public static let liveValue = Self(
|
||||
html: { request in
|
||||
request.toHTML()
|
||||
},
|
||||
generatePdf: { projectID, html in
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.env) var env
|
||||
|
||||
let envVars = try env()
|
||||
let baseUrl = "/tmp/\(projectID)"
|
||||
try await fileClient.writeFile(html.render(), "\(baseUrl).html")
|
||||
|
||||
let process = Process()
|
||||
let standardInput = Pipe()
|
||||
let standardOutput = Pipe()
|
||||
process.standardInput = standardInput
|
||||
process.standardOutput = standardOutput
|
||||
process.executableURL = URL(fileURLWithPath: envVars.pandocPath)
|
||||
process.arguments = [
|
||||
"\(baseUrl).html",
|
||||
"--pdf-engine=\(envVars.pdfEngine)",
|
||||
"--from=html",
|
||||
"--css=Public/css/pdf.css",
|
||||
"--output=\(baseUrl).pdf",
|
||||
]
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
return .init(htmlPath: "\(baseUrl).html", pdfPath: "\(baseUrl).pdf")
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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 frictionRate: FrictionRate
|
||||
public let projectSHR: Double
|
||||
|
||||
var totalEquivalentLength: Double {
|
||||
maxReturnTEL.totalEquivalentLength + maxSupplyTEL.totalEquivalentLength
|
||||
}
|
||||
|
||||
public init(
|
||||
project: Project,
|
||||
rooms: [Room],
|
||||
componentLosses: [ComponentPressureLoss],
|
||||
ductSizes: DuctSizes,
|
||||
equipmentInfo: EquipmentInfo,
|
||||
maxSupplyTEL: EffectiveLength,
|
||||
maxReturnTEL: EffectiveLength,
|
||||
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.frictionRate = frictionRate
|
||||
self.projectSHR = projectSHR
|
||||
}
|
||||
}
|
||||
|
||||
public struct Response: Equatable, Sendable {
|
||||
|
||||
public let htmlPath: String
|
||||
public let pdfPath: String
|
||||
|
||||
public init(htmlPath: String, pdfPath: String) {
|
||||
self.htmlPath = htmlPath
|
||||
self.pdfPath = pdfPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension PdfClient.Request {
|
||||
public static func mock(project: Project = .mock) -> Self {
|
||||
let rooms = Room.mock(projectID: project.id)
|
||||
let trunks = TrunkSize.mock(projectID: project.id, rooms: rooms)
|
||||
let equipmentInfo = EquipmentInfo.mock(projectID: project.id)
|
||||
let equivalentLengths = EffectiveLength.mock(projectID: project.id)
|
||||
|
||||
return .init(
|
||||
project: project,
|
||||
rooms: rooms,
|
||||
componentLosses: ComponentPressureLoss.mock(projectID: project.id),
|
||||
ductSizes: .mock(equipmentInfo: equipmentInfo, rooms: rooms, trunks: trunks),
|
||||
equipmentInfo: equipmentInfo,
|
||||
maxSupplyTEL: equivalentLengths.first { $0.type == .supply }!,
|
||||
maxReturnTEL: equivalentLengths.first { $0.type == .return }!,
|
||||
frictionRate: .mock,
|
||||
projectSHR: 0.83
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
#endif
|
||||
107
Sources/PdfClient/Request+html.swift
Normal file
107
Sources/PdfClient/Request+html.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
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 {
|
||||
link(.rel(.stylesheet), .href("/css/pdf.css"))
|
||||
}
|
||||
|
||||
var body: some HTML {
|
||||
div {
|
||||
// h1(.class("headline")) { "Duct Calc" }
|
||||
|
||||
h2 { "Project" }
|
||||
|
||||
div(.class("flex")) {
|
||||
ProjectTable(project: request.project)
|
||||
// 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)
|
||||
}
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
37
Sources/PdfClient/Views/DuctSizeTable.swift
Normal file
37
Sources/PdfClient/Views/DuctSizeTable.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
import Elementary
|
||||
import ManualDCore
|
||||
|
||||
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() ?? "" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Sources/PdfClient/Views/EquipmentTable.swift
Normal file
38
Sources/PdfClient/Views/EquipmentTable.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import Elementary
|
||||
import ManualDCore
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
Sources/PdfClient/Views/EquivalentLengthTable.swift
Normal file
68
Sources/PdfClient/Views/EquivalentLengthTable.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
import Elementary
|
||||
import ManualDCore
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
Sources/PdfClient/Views/FrictionRateTable.swift
Normal file
47
Sources/PdfClient/Views/FrictionRateTable.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
import Elementary
|
||||
import ManualDCore
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Sources/PdfClient/Views/ProjectTable.swift
Normal file
33
Sources/PdfClient/Views/ProjectTable.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
import Elementary
|
||||
import ManualDCore
|
||||
|
||||
struct ProjectTable: HTML, Sendable {
|
||||
let project: Project
|
||||
|
||||
var body: some HTML<HTMLTag.table> {
|
||||
table {
|
||||
tbody {
|
||||
tr {
|
||||
td(.class("label")) { "Name" }
|
||||
td { project.name }
|
||||
}
|
||||
tr {
|
||||
td(.class("label")) { "Address" }
|
||||
td {
|
||||
p {
|
||||
project.streetAddress
|
||||
br()
|
||||
project.cityStateZipString
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Project {
|
||||
var cityStateZipString: String {
|
||||
return "\(city), \(state) \(zipCode)"
|
||||
}
|
||||
}
|
||||
33
Sources/PdfClient/Views/RegisterTable.swift
Normal file
33
Sources/PdfClient/Views/RegisterTable.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
import Elementary
|
||||
import ManualDCore
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Sources/PdfClient/Views/RoomTable.swift
Normal file
50
Sources/PdfClient/Views/RoomTable.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
import Elementary
|
||||
import ManualDCore
|
||||
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Sources/PdfClient/Views/TrunkTable.swift
Normal file
42
Sources/PdfClient/Views/TrunkTable.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
import Elementary
|
||||
import ManualDCore
|
||||
|
||||
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() ?? "" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Sources/ProjectClient/DuctCalcClientError.swift
Normal file
9
Sources/ProjectClient/DuctCalcClientError.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
public struct ProjectClientError: Error {
|
||||
public let reason: String
|
||||
|
||||
public init(_ reason: String) {
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
81
Sources/ProjectClient/Interface.swift
Normal file
81
Sources/ProjectClient/Interface.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Elementary
|
||||
import ManualDClient
|
||||
import ManualDCore
|
||||
import Vapor
|
||||
|
||||
extension DependencyValues {
|
||||
public var projectClient: ProjectClient {
|
||||
get { self[ProjectClient.self] }
|
||||
set { self[ProjectClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Useful helper utilities for project's.
|
||||
///
|
||||
/// This is primarily used for implementing logic required to get the needed data
|
||||
/// for the view controller client to render views.
|
||||
@DependencyClient
|
||||
public struct ProjectClient: Sendable {
|
||||
public var calculateDuctSizes: @Sendable (Project.ID) async throws -> DuctSizes
|
||||
public var calculateRoomDuctSizes:
|
||||
@Sendable (Project.ID) async throws -> [DuctSizes.RoomContainer]
|
||||
public var calculateTrunkDuctSizes:
|
||||
@Sendable (Project.ID) async throws -> [DuctSizes.TrunkContainer]
|
||||
|
||||
public var createProject:
|
||||
@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)
|
||||
|
||||
public var generatePdf: @Sendable (Project.ID, FileIO) async throws -> Response
|
||||
}
|
||||
|
||||
extension ProjectClient: TestDependencyKey {
|
||||
public static let testValue = Self()
|
||||
}
|
||||
|
||||
extension ProjectClient {
|
||||
|
||||
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 FrictionRateResponse: Codable, Equatable, Sendable {
|
||||
|
||||
public let componentLosses: [ComponentPressureLoss]
|
||||
public let equivalentLengths: EffectiveLength.MaxContainer
|
||||
public let frictionRate: FrictionRate?
|
||||
|
||||
public init(
|
||||
componentLosses: [ComponentPressureLoss],
|
||||
equivalentLengths: EffectiveLength.MaxContainer,
|
||||
frictionRate: FrictionRate? = nil
|
||||
) {
|
||||
self.componentLosses = componentLosses
|
||||
self.equivalentLengths = equivalentLengths
|
||||
self.frictionRate = frictionRate
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import ManualDClient
|
||||
import ManualDCore
|
||||
|
||||
extension DatabaseClient {
|
||||
|
||||
func calculateDuctSizes(
|
||||
details: Project.Detail
|
||||
) async throws -> (DuctSizes, DuctSizeSharedRequest) {
|
||||
let (rooms, shared) = try await calculateRoomDuctSizes(details: details)
|
||||
let (trunks, _) = try await calculateTrunkDuctSizes(details: details)
|
||||
return (.init(rooms: rooms, trunks: trunks), shared)
|
||||
}
|
||||
|
||||
func calculateDuctSizes(
|
||||
projectID: Project.ID
|
||||
) async throws -> (DuctSizes, DuctSizeSharedRequest, [Room]) {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
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(
|
||||
details: Project.Detail
|
||||
) async throws -> ([DuctSizes.RoomContainer], DuctSizeSharedRequest) {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
let shared = try sharedDuctRequest(details: details)
|
||||
let rooms = try await manualD.calculateRoomSizes(rooms: details.rooms, sharedRequest: shared)
|
||||
return (rooms, shared)
|
||||
}
|
||||
|
||||
func calculateRoomDuctSizes(
|
||||
projectID: Project.ID
|
||||
) async throws -> ([DuctSizes.RoomContainer], DuctSizeSharedRequest) {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
let shared = try await sharedDuctRequest(projectID)
|
||||
|
||||
return try await (
|
||||
manualD.calculateRoomSizes(
|
||||
rooms: rooms.fetch(projectID),
|
||||
sharedRequest: shared
|
||||
),
|
||||
shared
|
||||
)
|
||||
}
|
||||
|
||||
func calculateTrunkDuctSizes(
|
||||
details: Project.Detail
|
||||
) async throws -> ([DuctSizes.TrunkContainer], DuctSizeSharedRequest) {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
let shared = try sharedDuctRequest(details: details)
|
||||
let trunks = try await manualD.calculateTrunkSizes(
|
||||
rooms: details.rooms,
|
||||
trunks: details.trunks,
|
||||
sharedRequest: shared
|
||||
)
|
||||
return (trunks, shared)
|
||||
}
|
||||
|
||||
func calculateTrunkDuctSizes(
|
||||
projectID: Project.ID
|
||||
) async throws -> ([DuctSizes.TrunkContainer], DuctSizeSharedRequest) {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
let shared = try await sharedDuctRequest(projectID)
|
||||
|
||||
return try await (
|
||||
manualD.calculateTrunkSizes(
|
||||
rooms: rooms.fetch(projectID),
|
||||
trunks: trunkSizes.fetch(projectID),
|
||||
sharedRequest: shared
|
||||
),
|
||||
shared
|
||||
)
|
||||
}
|
||||
|
||||
func sharedDuctRequest(details: Project.Detail) throws -> DuctSizeSharedRequest {
|
||||
guard
|
||||
let dfrResponse = designFrictionRate(
|
||||
componentLosses: details.componentLosses,
|
||||
equipmentInfo: details.equipmentInfo,
|
||||
equivalentLengths: details.maxContainer
|
||||
)
|
||||
else {
|
||||
throw ProjectClientError("Project not complete.")
|
||||
}
|
||||
|
||||
guard let projectSHR = details.project.sensibleHeatRatio else {
|
||||
throw ProjectClientError("Project sensible heat ratio not set.")
|
||||
}
|
||||
|
||||
let ensuredTEL = try dfrResponse.ensureMaxContainer()
|
||||
|
||||
return .init(
|
||||
equipmentInfo: dfrResponse.equipmentInfo,
|
||||
maxSupplyLength: ensuredTEL.supply,
|
||||
maxReturnLenght: ensuredTEL.return,
|
||||
designFrictionRate: dfrResponse.designFrictionRate,
|
||||
projectSHR: projectSHR
|
||||
)
|
||||
}
|
||||
|
||||
func sharedDuctRequest(_ projectID: Project.ID) async throws -> DuctSizeSharedRequest {
|
||||
|
||||
guard let dfrResponse = try await designFrictionRate(projectID: projectID) else {
|
||||
throw ProjectClientError("Project not complete.")
|
||||
}
|
||||
|
||||
let ensuredTEL = try dfrResponse.ensureMaxContainer()
|
||||
|
||||
return try await .init(
|
||||
equipmentInfo: dfrResponse.equipmentInfo,
|
||||
maxSupplyLength: ensuredTEL.supply,
|
||||
maxReturnLenght: ensuredTEL.return,
|
||||
designFrictionRate: dfrResponse.designFrictionRate,
|
||||
projectSHR: ensuredSHR(projectID)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// Fetches the project sensible heat ratio or throws an error if it's nil.
|
||||
func ensuredSHR(_ projectID: Project.ID) async throws -> Double {
|
||||
guard let projectSHR = try await projects.getSensibleHeatRatio(projectID) else {
|
||||
throw ProjectClientError("Project sensible heat ratio not set.")
|
||||
}
|
||||
return projectSHR
|
||||
}
|
||||
|
||||
// Internal container.
|
||||
struct DesignFrictionRateResponse: Equatable, Sendable {
|
||||
|
||||
typealias EnsuredTEL = (supply: EffectiveLength, return: EffectiveLength)
|
||||
|
||||
let designFrictionRate: Double
|
||||
let equipmentInfo: EquipmentInfo
|
||||
let telMaxContainer: EffectiveLength.MaxContainer
|
||||
|
||||
func ensureMaxContainer() throws -> EnsuredTEL {
|
||||
|
||||
guard let maxSupplyLength = telMaxContainer.supply else {
|
||||
throw ProjectClientError("Max supply TEL not found")
|
||||
}
|
||||
guard let maxReturnLength = telMaxContainer.return else {
|
||||
throw ProjectClientError("Max supply TEL not found")
|
||||
}
|
||||
|
||||
return (maxSupplyLength, maxReturnLength)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func designFrictionRate(
|
||||
componentLosses: [ComponentPressureLoss],
|
||||
equipmentInfo: EquipmentInfo,
|
||||
equivalentLengths: EffectiveLength.MaxContainer
|
||||
) -> DesignFrictionRateResponse? {
|
||||
guard let tel = equivalentLengths.total,
|
||||
componentLosses.count > 0
|
||||
else { return nil }
|
||||
|
||||
let availableStaticPressure = equipmentInfo.staticPressure - componentLosses.total
|
||||
|
||||
return .init(
|
||||
designFrictionRate: (availableStaticPressure * 100) / tel,
|
||||
equipmentInfo: equipmentInfo,
|
||||
telMaxContainer: equivalentLengths
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func designFrictionRate(
|
||||
projectID: Project.ID
|
||||
) async throws -> DesignFrictionRateResponse? {
|
||||
|
||||
guard let equipmentInfo = try await equipment.fetch(projectID) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return try await designFrictionRate(
|
||||
componentLosses: componentLoss.fetch(projectID),
|
||||
equipmentInfo: equipmentInfo,
|
||||
equivalentLengths: effectiveLength.fetchMax(projectID)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
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 {
|
||||
|
||||
func calculateDuctSizes(
|
||||
rooms: [Room],
|
||||
trunks: [TrunkSize],
|
||||
sharedRequest: DuctSizeSharedRequest,
|
||||
logger: Logger? = nil
|
||||
) async throws -> DuctSizes {
|
||||
try await .init(
|
||||
rooms: calculateRoomSizes(
|
||||
rooms: rooms,
|
||||
sharedRequest: sharedRequest
|
||||
),
|
||||
trunks: calculateTrunkSizes(
|
||||
rooms: rooms,
|
||||
trunks: trunks,
|
||||
sharedRequest: sharedRequest
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func calculateRoomSizes(
|
||||
rooms: [Room],
|
||||
sharedRequest: DuctSizeSharedRequest,
|
||||
logger: Logger? = nil
|
||||
) async throws -> [DuctSizes.RoomContainer] {
|
||||
|
||||
var retval: [DuctSizes.RoomContainer] = []
|
||||
let totalHeatingLoad = rooms.totalHeatingLoad
|
||||
let totalCoolingSensible = rooms.totalCoolingSensible(shr: sharedRequest.projectSHR)
|
||||
|
||||
for room in rooms {
|
||||
let heatingLoad = room.heatingLoadPerRegister
|
||||
let coolingLoad = room.coolingSensiblePerRegister(projectSHR: sharedRequest.projectSHR)
|
||||
let heatingPercent = heatingLoad / totalHeatingLoad
|
||||
let coolingPercent = coolingLoad / totalCoolingSensible
|
||||
let heatingCFM = heatingPercent * Double(sharedRequest.equipmentInfo.heatingCFM)
|
||||
let coolingCFM = coolingPercent * Double(sharedRequest.equipmentInfo.coolingCFM)
|
||||
let designCFM = DuctSizes.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
|
||||
let sizes = try await self.ductSize(
|
||||
.init(designCFM: Int(designCFM.value), frictionRate: sharedRequest.designFrictionRate)
|
||||
)
|
||||
|
||||
for n in 1...room.registerCount {
|
||||
|
||||
var rectangularWidth: Int? = nil
|
||||
let rectangularSize = room.rectangularSizes?
|
||||
.first(where: { $0.register == nil || $0.register == n })
|
||||
|
||||
if let rectangularSize {
|
||||
let response = try await self.rectangularSize(
|
||||
.init(round: sizes.finalSize, height: rectangularSize.height)
|
||||
)
|
||||
rectangularWidth = response.width
|
||||
}
|
||||
|
||||
retval.append(
|
||||
.init(
|
||||
roomID: room.id,
|
||||
roomName: "\(room.name)-\(n)",
|
||||
roomRegister: n,
|
||||
heatingLoad: heatingLoad,
|
||||
coolingLoad: coolingLoad,
|
||||
heatingCFM: heatingCFM,
|
||||
coolingCFM: coolingCFM,
|
||||
ductSize: .init(
|
||||
designCFM: designCFM,
|
||||
sizes: sizes,
|
||||
rectangularSize: rectangularSize,
|
||||
width: rectangularWidth
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
func calculateTrunkSizes(
|
||||
rooms: [Room],
|
||||
trunks: [TrunkSize],
|
||||
sharedRequest: DuctSizeSharedRequest,
|
||||
logger: Logger? = nil
|
||||
) async throws -> [DuctSizes.TrunkContainer] {
|
||||
|
||||
var retval = [DuctSizes.TrunkContainer]()
|
||||
let totalHeatingLoad = rooms.totalHeatingLoad
|
||||
let totalCoolingSensible = rooms.totalCoolingSensible(shr: sharedRequest.projectSHR)
|
||||
|
||||
for trunk in trunks {
|
||||
let heatingLoad = trunk.totalHeatingLoad
|
||||
let coolingLoad = trunk.totalCoolingSensible(projectSHR: sharedRequest.projectSHR)
|
||||
let heatingPercent = heatingLoad / totalHeatingLoad
|
||||
let coolingPercent = coolingLoad / totalCoolingSensible
|
||||
let heatingCFM = heatingPercent * Double(sharedRequest.equipmentInfo.heatingCFM)
|
||||
let coolingCFM = coolingPercent * Double(sharedRequest.equipmentInfo.coolingCFM)
|
||||
let designCFM = DuctSizes.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
|
||||
let sizes = try await self.ductSize(
|
||||
.init(designCFM: Int(designCFM.value), frictionRate: sharedRequest.designFrictionRate)
|
||||
)
|
||||
var width: Int? = nil
|
||||
if let height = trunk.height {
|
||||
let rectangularSize = try await self.rectangularSize(
|
||||
.init(round: sizes.finalSize, height: height)
|
||||
)
|
||||
width = rectangularSize.width
|
||||
}
|
||||
|
||||
retval.append(
|
||||
.init(
|
||||
trunk: trunk,
|
||||
ductSize: .init(
|
||||
designCFM: designCFM,
|
||||
sizes: sizes,
|
||||
height: trunk.height,
|
||||
width: width
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DuctSizes.SizeContainer {
|
||||
init(
|
||||
designCFM: DuctSizes.DesignCFM,
|
||||
sizes: ManualDClient.DuctSizeResponse,
|
||||
height: Int?,
|
||||
width: Int?
|
||||
) {
|
||||
self.init(
|
||||
rectangularID: nil,
|
||||
designCFM: designCFM,
|
||||
roundSize: sizes.calculatedSize,
|
||||
finalSize: sizes.finalSize,
|
||||
velocity: sizes.velocity,
|
||||
flexSize: sizes.flexSize,
|
||||
height: height,
|
||||
width: width
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
designCFM: DuctSizes.DesignCFM,
|
||||
sizes: ManualDClient.DuctSizeResponse,
|
||||
rectangularSize: Room.RectangularSize?,
|
||||
width: Int?
|
||||
) {
|
||||
self.init(
|
||||
rectangularID: rectangularSize?.id,
|
||||
designCFM: designCFM,
|
||||
roundSize: sizes.calculatedSize,
|
||||
finalSize: sizes.finalSize,
|
||||
velocity: sizes.velocity,
|
||||
flexSize: sizes.flexSize,
|
||||
height: rectangularSize?.height,
|
||||
width: width
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Room {
|
||||
|
||||
var heatingLoadPerRegister: Double {
|
||||
|
||||
heatingLoad / Double(registerCount)
|
||||
}
|
||||
|
||||
func coolingSensiblePerRegister(projectSHR: Double) -> Double {
|
||||
let sensible = coolingSensible ?? (coolingTotal * projectSHR)
|
||||
return sensible / Double(registerCount)
|
||||
}
|
||||
}
|
||||
|
||||
extension TrunkSize.RoomProxy {
|
||||
|
||||
// We need to make sure if registers got removed after a trunk
|
||||
// was already made / saved that we do not include registers that
|
||||
// no longer exist.
|
||||
private var actualRegisterCount: Int {
|
||||
guard registers.count <= room.registerCount else {
|
||||
return room.registerCount
|
||||
}
|
||||
return registers.count
|
||||
}
|
||||
|
||||
var totalHeatingLoad: Double {
|
||||
room.heatingLoadPerRegister * Double(actualRegisterCount)
|
||||
}
|
||||
|
||||
func totalCoolingSensible(projectSHR: Double) -> Double {
|
||||
room.coolingSensiblePerRegister(projectSHR: projectSHR) * Double(actualRegisterCount)
|
||||
}
|
||||
}
|
||||
|
||||
extension TrunkSize {
|
||||
|
||||
var totalHeatingLoad: Double {
|
||||
rooms.reduce(into: 0) { $0 += $1.totalHeatingLoad }
|
||||
}
|
||||
|
||||
func totalCoolingSensible(projectSHR: Double) -> Double {
|
||||
rooms.reduce(into: 0) { $0 += $1.totalCoolingSensible(projectSHR: projectSHR) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import ManualDClient
|
||||
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 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)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
106
Sources/ProjectClient/Live.swift
Normal file
106
Sources/ProjectClient/Live.swift
Normal file
@@ -0,0 +1,106 @@
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import FileClient
|
||||
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
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
|
||||
return .init(
|
||||
calculateDuctSizes: { projectID in
|
||||
try await database.calculateDuctSizes(projectID: projectID).0
|
||||
},
|
||||
calculateRoomDuctSizes: { projectID in
|
||||
try await database.calculateRoomDuctSizes(projectID: projectID).0
|
||||
},
|
||||
calculateTrunkDuctSizes: { projectID in
|
||||
try await database.calculateTrunkDuctSizes(projectID: projectID).0
|
||||
},
|
||||
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
|
||||
try await manualD.frictionRate(projectID: projectID)
|
||||
},
|
||||
generatePdf: { projectID, fileIO in
|
||||
let pdfResponse = try await pdfClient.generatePdf(
|
||||
request: database.makePdfRequest(projectID))
|
||||
|
||||
let response = try await fileIO.asyncStreamFile(at: pdfResponse.pdfPath) { _ in
|
||||
try await fileClient.removeFile(pdfResponse.htmlPath)
|
||||
try await fileClient.removeFile(pdfResponse.pdfPath)
|
||||
}
|
||||
|
||||
response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream")
|
||||
response.headers.replaceOrAdd(
|
||||
name: .contentDisposition, value: "attachment; filename=Duct-Calc.pdf"
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DatabaseClient {
|
||||
|
||||
fileprivate func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
guard let projectDetails = try await projects.detail(projectID) else {
|
||||
throw ProjectClientError("Project not found. id: \(projectID)")
|
||||
}
|
||||
|
||||
let (ductSizes, shared) = try await calculateDuctSizes(details: projectDetails)
|
||||
|
||||
let frictionRateResponse = try await manualD.frictionRate(details: projectDetails)
|
||||
guard let frictionRate = frictionRateResponse.frictionRate else {
|
||||
throw ProjectClientError("Friction rate not found. id: \(projectID)")
|
||||
}
|
||||
|
||||
return .init(
|
||||
details: projectDetails,
|
||||
ductSizes: ductSizes,
|
||||
shared: shared,
|
||||
frictionRate: frictionRate
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PdfClient.Request {
|
||||
init(
|
||||
details: Project.Detail,
|
||||
ductSizes: DuctSizes,
|
||||
shared: DuctSizeSharedRequest,
|
||||
frictionRate: FrictionRate
|
||||
) {
|
||||
self.init(
|
||||
project: details.project,
|
||||
rooms: details.rooms,
|
||||
componentLosses: details.componentLosses,
|
||||
ductSizes: ductSizes,
|
||||
equipmentInfo: details.equipmentInfo,
|
||||
maxSupplyTEL: shared.maxSupplyLength,
|
||||
maxReturnTEL: shared.maxReturnLenght,
|
||||
frictionRate: frictionRate,
|
||||
projectSHR: shared.projectSHR
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,6 @@ extension HTMLAttribute.hx {
|
||||
extension HTMLAttribute.hx {
|
||||
@Sendable
|
||||
public static func indicator() -> HTMLAttribute {
|
||||
indicator(".hx-indicator")
|
||||
indicator(".htmx-indicator")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +1,76 @@
|
||||
import Elementary
|
||||
import Foundation
|
||||
|
||||
public struct ResultView<
|
||||
V: Sendable,
|
||||
E: Error,
|
||||
ValueView: HTML,
|
||||
ErrorView: HTML
|
||||
>: HTML {
|
||||
public struct ResultView<ValueView, ErrorView>: HTML where ValueView: HTML, ErrorView: HTML {
|
||||
|
||||
let onSuccess: @Sendable (V) -> ValueView
|
||||
let onError: @Sendable (E) -> ErrorView
|
||||
let result: Result<V, E>
|
||||
let result: Result<ValueView, any Error>
|
||||
let errorView: @Sendable (any Error) -> ErrorView
|
||||
|
||||
public init(
|
||||
result: Result<V, E>,
|
||||
@HTMLBuilder onSuccess: @escaping @Sendable (V) -> ValueView,
|
||||
@HTMLBuilder onError: @escaping @Sendable (E) -> ErrorView
|
||||
) {
|
||||
self.result = result
|
||||
self.onError = onError
|
||||
self.onSuccess = onSuccess
|
||||
_ content: @escaping @Sendable () async throws -> ValueView,
|
||||
onError: @escaping @Sendable (any Error) -> ErrorView
|
||||
) async {
|
||||
self.result = await Result(catching: content)
|
||||
self.errorView = onError
|
||||
}
|
||||
|
||||
public var body: some HTML {
|
||||
switch result {
|
||||
case .success(let value):
|
||||
onSuccess(value)
|
||||
case .success(let view):
|
||||
view
|
||||
case .failure(let error):
|
||||
onError(error)
|
||||
errorView(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ResultView {
|
||||
extension ResultView where ErrorView == Styleguide.ErrorView {
|
||||
|
||||
public init(
|
||||
result: Result<V, E>,
|
||||
@HTMLBuilder onSuccess: @escaping @Sendable (V) -> ValueView
|
||||
) where ErrorView == Styleguide.ErrorView<E> {
|
||||
self.init(result: result, onSuccess: onSuccess) { error in
|
||||
Styleguide.ErrorView(error: error)
|
||||
}
|
||||
_ content: @escaping @Sendable () async throws -> ValueView
|
||||
) async {
|
||||
await self.init(
|
||||
content,
|
||||
onError: { Styleguide.ErrorView(error: $0) }
|
||||
)
|
||||
}
|
||||
|
||||
public init<V: Sendable>(
|
||||
catching: @escaping @Sendable () async throws -> V,
|
||||
onSuccess content: @escaping @Sendable (V) -> ValueView
|
||||
) async where ValueView: Sendable {
|
||||
await self.init(
|
||||
{
|
||||
try await content(catching())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public init(
|
||||
catching: @escaping @Sendable () async throws(E) -> V,
|
||||
@HTMLBuilder onSuccess: @escaping @Sendable (V) -> ValueView
|
||||
) async where ErrorView == Styleguide.ErrorView<E> {
|
||||
catching: @escaping @Sendable () async throws -> Void
|
||||
) async where ValueView == EmptyHTML {
|
||||
await self.init(
|
||||
result: .init(catching: catching),
|
||||
onSuccess: onSuccess
|
||||
) { error in
|
||||
Styleguide.ErrorView(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
public init(
|
||||
catching: @escaping @Sendable () async throws(E) -> V,
|
||||
) async where ErrorView == Styleguide.ErrorView<E>, V == Void, ValueView == EmptyHTML {
|
||||
await self.init(
|
||||
result: .init(catching: catching),
|
||||
catching: catching,
|
||||
onSuccess: { EmptyHTML() }
|
||||
) { error in
|
||||
Styleguide.ErrorView(error: error)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension ResultView: Sendable where Error: Sendable, ValueView: Sendable, ErrorView: Sendable {}
|
||||
extension ResultView: Sendable where ValueView: Sendable, ErrorView: Sendable {}
|
||||
|
||||
public struct ErrorView<E: Error>: HTML, Sendable where Error: Sendable {
|
||||
public struct ErrorView: HTML, Sendable {
|
||||
let error: any Error
|
||||
|
||||
let error: E
|
||||
|
||||
public init(error: E) {
|
||||
public init(error: any Error) {
|
||||
self.error = error
|
||||
}
|
||||
|
||||
public var body: some HTML<HTMLTag.div> {
|
||||
div {
|
||||
h1(.class("text-2xl font-bold text-error")) { "Oops: Error" }
|
||||
h1(.class("text-xl font-bold text-error")) { "Oops: Error" }
|
||||
p {
|
||||
"\(error)"
|
||||
"\(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import ManualDCore
|
||||
|
||||
extension SiteRoute.View.ProjectRoute.DuctSizingRoute.TrunkSizeForm {
|
||||
|
||||
func toCreate(logger: Logger? = nil) throws -> DuctSizing.TrunkSize.Create {
|
||||
func toCreate(logger: Logger? = nil) throws -> TrunkSize.Create {
|
||||
try .init(
|
||||
projectID: projectID,
|
||||
type: type,
|
||||
@@ -14,7 +14,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute.TrunkSizeForm {
|
||||
)
|
||||
}
|
||||
|
||||
func toUpdate(logger: Logger? = nil) throws -> DuctSizing.TrunkSize.Update {
|
||||
func toUpdate(logger: Logger? = nil) throws -> TrunkSize.Update {
|
||||
try .init(
|
||||
type: type,
|
||||
rooms: makeRooms(logger: logger),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AuthClient
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Elementary
|
||||
@@ -15,10 +16,6 @@ public typealias AnySendableHTML = (any HTML & Sendable)
|
||||
|
||||
@DependencyClient
|
||||
public struct ViewController: Sendable {
|
||||
|
||||
public typealias AuthenticateHandler = @Sendable (User) -> Void
|
||||
public typealias CurrentUserHandler = @Sendable () throws -> User
|
||||
|
||||
public var view: @Sendable (Request) async throws -> AnySendableHTML
|
||||
}
|
||||
|
||||
@@ -29,21 +26,15 @@ extension ViewController {
|
||||
public let route: SiteRoute.View
|
||||
public let isHtmxRequest: Bool
|
||||
public let logger: Logger
|
||||
public let authenticateUser: AuthenticateHandler
|
||||
public let currentUser: CurrentUserHandler
|
||||
|
||||
public init(
|
||||
route: SiteRoute.View,
|
||||
isHtmxRequest: Bool,
|
||||
logger: Logger,
|
||||
authenticateUser: @escaping AuthenticateHandler,
|
||||
currentUser: @escaping CurrentUserHandler
|
||||
logger: Logger
|
||||
) {
|
||||
self.route = route
|
||||
self.isHtmxRequest = isHtmxRequest
|
||||
self.logger = logger
|
||||
self.authenticateUser = authenticateUser
|
||||
self.currentUser = currentUser
|
||||
}
|
||||
|
||||
}
|
||||
@@ -62,28 +53,23 @@ extension ViewController: DependencyKey {
|
||||
|
||||
extension ViewController.Request {
|
||||
|
||||
func currentUser() throws -> User {
|
||||
@Dependency(\.authClient.currentUser) var currentUser
|
||||
return try currentUser()
|
||||
}
|
||||
|
||||
func authenticate(
|
||||
_ login: User.Login
|
||||
) async throws -> User {
|
||||
@Dependency(\.database.users) var users
|
||||
let token = try await users.login(login)
|
||||
let user = try await users.get(token.userID)!
|
||||
authenticateUser(user)
|
||||
logger.debug("Logged in user: \(user.id)")
|
||||
return user
|
||||
@Dependency(\.authClient) var auth
|
||||
return try await auth.login(login)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func createAndAuthenticate(
|
||||
_ signup: User.Create
|
||||
) async throws -> User {
|
||||
@Dependency(\.database.users) var users
|
||||
let user = try await users.create(signup)
|
||||
let _ = try await users.login(
|
||||
.init(email: signup.email, password: signup.password)
|
||||
)
|
||||
authenticateUser(user)
|
||||
logger.debug("Created and logged in user: \(user.id)")
|
||||
return user
|
||||
@Dependency(\.authClient) var auth
|
||||
return try await auth.createAndLogin(signup)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import Dependencies
|
||||
import Elementary
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import PdfClient
|
||||
import ProjectClient
|
||||
import Styleguide
|
||||
|
||||
extension ViewController.Request {
|
||||
@@ -10,20 +12,27 @@ extension ViewController.Request {
|
||||
func render() async -> AnySendableHTML {
|
||||
|
||||
@Dependency(\.database) var database
|
||||
@Dependency(\.projectClient) var projectClient
|
||||
@Dependency(\.pdfClient) var pdfClient
|
||||
|
||||
switch route {
|
||||
case .test:
|
||||
let projectID = UUID(uuidString: "A9C20153-E2E5-4C65-B33F-4D8A29C63A7A")!
|
||||
return await view {
|
||||
await ResultView {
|
||||
return (
|
||||
try await database.projects.getCompletedSteps(projectID),
|
||||
try await database.calculateDuctSizes(projectID: projectID)
|
||||
)
|
||||
} onSuccess: { (_, result) in
|
||||
TestPage(trunks: result.trunks, rooms: result.rooms)
|
||||
}
|
||||
}
|
||||
// let projectID = UUID(uuidString: "E796C96C-F527-4753-A00A-EBCF25630663")!
|
||||
// return await view {
|
||||
// await ResultView {
|
||||
//
|
||||
// // return (
|
||||
// // try await database.projects.getCompletedSteps(projectID),
|
||||
// // try await projectClient.calculateDuctSizes(projectID)
|
||||
// // )
|
||||
// return try await pdfClient.html(.mock())
|
||||
// } onSuccess: {
|
||||
// $0
|
||||
// // TestPage()
|
||||
// // TestPage(trunks: result.trunks, rooms: result.rooms)
|
||||
// }
|
||||
// }
|
||||
return try! await pdfClient.html(.mock())
|
||||
case .login(let route):
|
||||
switch route {
|
||||
case .index(let next):
|
||||
@@ -116,6 +125,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:
|
||||
@@ -124,7 +134,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
|
||||
@@ -146,19 +156,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,6 +192,12 @@ 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:
|
||||
// FIX: This should return a pdf to download or be wrapped in a
|
||||
// result view.
|
||||
// return try! await projectClient.toHTML(projectID)
|
||||
// This get's handled elsewhere because it returns a response, not a view.
|
||||
fatalError()
|
||||
case .rooms(let route):
|
||||
return await route.renderView(on: request, projectID: projectID)
|
||||
}
|
||||
@@ -372,7 +383,7 @@ extension SiteRoute.View.ProjectRoute.FrictionRateRoute {
|
||||
FrictionRateView(
|
||||
componentLosses: losses,
|
||||
equivalentLengths: lengths,
|
||||
frictionRateResponse: frictionRate
|
||||
frictionRate: frictionRate
|
||||
)
|
||||
}
|
||||
|
||||
@@ -416,32 +427,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,
|
||||
frictionRate: response.frictionRate
|
||||
)
|
||||
}
|
||||
|
||||
@@ -555,6 +555,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
|
||||
) async -> AnySendableHTML {
|
||||
@Dependency(\.database) var database
|
||||
@Dependency(\.manualD) var manualD
|
||||
@Dependency(\.projectClient) var projectClient
|
||||
|
||||
switch self {
|
||||
case .index:
|
||||
@@ -563,8 +564,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 database.calculateDuctSizes(projectID: projectID)
|
||||
.rooms
|
||||
return try await projectClient.calculateRoomDuctSizes(projectID)
|
||||
.filter({ $0.roomID == room.id && $0.roomRegister == request.register })
|
||||
.first!
|
||||
} onSuccess: { room in
|
||||
@@ -577,8 +577,7 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
|
||||
roomID,
|
||||
.init(id: form.id ?? .init(), register: form.register, height: form.height)
|
||||
)
|
||||
return try await database.calculateDuctSizes(projectID: projectID)
|
||||
.rooms
|
||||
return try await projectClient.calculateRoomDuctSizes(projectID)
|
||||
.filter({ $0.roomID == room.id && $0.roomRegister == form.register })
|
||||
.first!
|
||||
} onSuccess: { room in
|
||||
@@ -612,17 +611,18 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
|
||||
catching: @escaping @Sendable () async throws -> Void = {}
|
||||
) async -> AnySendableHTML {
|
||||
@Dependency(\.database) var database
|
||||
@Dependency(\.projectClient) var project
|
||||
|
||||
return await request.view {
|
||||
await ResultView {
|
||||
try await catching()
|
||||
return (
|
||||
try await database.projects.getCompletedSteps(projectID),
|
||||
try await database.calculateDuctSizes(projectID: projectID)
|
||||
try await project.calculateDuctSizes(projectID)
|
||||
)
|
||||
} onSuccess: { (steps, ducts) in
|
||||
ProjectView(projectID: projectID, activeTab: .ductSizing, completedSteps: steps) {
|
||||
DuctSizingView(rooms: ducts.rooms, trunks: ducts.trunks)
|
||||
DuctSizingView(ductSizes: ducts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ struct DuctSizingView: HTML, Sendable {
|
||||
|
||||
@Environment(ProjectViewValue.$projectID) var projectID
|
||||
|
||||
let rooms: [DuctSizing.RoomContainer]
|
||||
let trunks: [DuctSizing.TrunkContainer]
|
||||
let ductSizes: DuctSizes
|
||||
|
||||
var body: some HTML {
|
||||
div(.class("space-y-4")) {
|
||||
@@ -21,13 +20,30 @@ struct DuctSizingView: HTML, Sendable {
|
||||
Must complete all the previous sections to display duct sizing calculations.
|
||||
"""
|
||||
)
|
||||
.hidden(when: rooms.count > 0)
|
||||
.hidden(when: ductSizes.rooms.count > 0)
|
||||
.attributes(.class("text-error font-bold italic mt-4"))
|
||||
}
|
||||
|
||||
div {
|
||||
button(
|
||||
.class("btn btn-primary"),
|
||||
.hx.get(route: .project(.detail(projectID, .pdf))),
|
||||
.hx.ext("htmx-download"),
|
||||
.hx.swap(.none),
|
||||
.hx.indicator()
|
||||
) {
|
||||
span { "PDF" }
|
||||
Indicator()
|
||||
}
|
||||
// div {
|
||||
// Indicator()
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if rooms.count != 0 {
|
||||
RoomsTable(rooms: rooms)
|
||||
if ductSizes.rooms.count != 0 {
|
||||
RoomsTable(rooms: ductSizes.rooms)
|
||||
|
||||
PageTitleRow {
|
||||
PageTitle {
|
||||
@@ -42,13 +58,13 @@ struct DuctSizingView: HTML, Sendable {
|
||||
.tooltip("Add trunk / runout")
|
||||
}
|
||||
|
||||
if trunks.count > 0 {
|
||||
TrunkTable(trunks: trunks, rooms: rooms)
|
||||
if ductSizes.trunks.count > 0 {
|
||||
TrunkTable(ductSizes: ductSizes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TrunkSizeForm(rooms: rooms, dismiss: true)
|
||||
TrunkSizeForm(rooms: ductSizes.rooms, dismiss: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import Styleguide
|
||||
|
||||
struct RectangularSizeForm: HTML, Sendable {
|
||||
|
||||
static func id(_ room: DuctSizing.RoomContainer) -> String {
|
||||
static func id(_ room: DuctSizes.RoomContainer) -> String {
|
||||
let base = "rectangularSize"
|
||||
return "\(base)_\(room.roomName.idString)"
|
||||
}
|
||||
@@ -13,12 +13,12 @@ struct RectangularSizeForm: HTML, Sendable {
|
||||
@Environment(ProjectViewValue.$projectID) var projectID
|
||||
|
||||
let id: String
|
||||
let room: DuctSizing.RoomContainer
|
||||
let room: DuctSizes.RoomContainer
|
||||
let dismiss: Bool
|
||||
|
||||
init(
|
||||
id: String? = nil,
|
||||
room: DuctSizing.RoomContainer,
|
||||
room: DuctSizes.RoomContainer,
|
||||
dismiss: Bool = true
|
||||
) {
|
||||
self.id = Self.id(room)
|
||||
@@ -40,7 +40,7 @@ struct RectangularSizeForm: HTML, Sendable {
|
||||
}
|
||||
|
||||
var height: Int? {
|
||||
room.rectangularSize?.height
|
||||
room.ductSize.height
|
||||
}
|
||||
|
||||
var body: some HTML<HTMLTag.dialog> {
|
||||
@@ -54,7 +54,7 @@ struct RectangularSizeForm: HTML, Sendable {
|
||||
.hx.swap(.outerHTML)
|
||||
) {
|
||||
input(.class("hidden"), .name("register"), .value(room.roomRegister))
|
||||
input(.class("hidden"), .name("id"), .value(room.rectangularSize?.id))
|
||||
input(.class("hidden"), .name("id"), .value(room.ductSize.rectangularID))
|
||||
|
||||
LabeledInput(
|
||||
"Height",
|
||||
|
||||
@@ -9,7 +9,7 @@ extension DuctSizingView {
|
||||
struct RoomsTable: HTML, Sendable {
|
||||
@Environment(ProjectViewValue.$projectID) var projectID
|
||||
|
||||
let rooms: [DuctSizing.RoomContainer]
|
||||
let rooms: [DuctSizes.RoomContainer]
|
||||
|
||||
var body: some HTML<HTMLTag.table> {
|
||||
|
||||
@@ -34,17 +34,17 @@ extension DuctSizingView {
|
||||
|
||||
struct RoomRow: HTML, Sendable {
|
||||
|
||||
static func id(_ room: DuctSizing.RoomContainer) -> String {
|
||||
static func id(_ room: DuctSizes.RoomContainer) -> String {
|
||||
"roomRow_\(room.roomName.idString)"
|
||||
}
|
||||
|
||||
@Environment(ProjectViewValue.$projectID) var projectID
|
||||
|
||||
let room: DuctSizing.RoomContainer
|
||||
let room: DuctSizes.RoomContainer
|
||||
let formID = UUID().idString
|
||||
|
||||
var deleteRoute: String {
|
||||
guard let id = room.rectangularSize?.id else { return "" }
|
||||
guard let id = room.rectangularID else { return "" }
|
||||
|
||||
return SiteRoute.View.router.path(
|
||||
for: .project(
|
||||
@@ -80,7 +80,7 @@ extension DuctSizingView {
|
||||
|
||||
span(.class("label")) { "Design" }
|
||||
div(.class("flex justify-center")) {
|
||||
Badge(number: room.designCFM.value, digits: 0)
|
||||
Badge(number: room.ductSize.designCFM.value, digits: 0)
|
||||
}
|
||||
|
||||
span(.class("label")) { "Heating" }
|
||||
@@ -103,28 +103,28 @@ extension DuctSizingView {
|
||||
|
||||
div(.class("label")) { "Calculated" }
|
||||
div(.class("flex justify-center")) {
|
||||
Badge(number: room.roundSize, digits: 2)
|
||||
Badge(number: room.ductSize.roundSize, digits: 2)
|
||||
}
|
||||
div {}
|
||||
|
||||
div(.class("label")) { "Final" }
|
||||
div(.class("flex justify-center")) {
|
||||
Badge(number: room.finalSize)
|
||||
Badge(number: room.ductSize.finalSize)
|
||||
.attributes(.class("badge-secondary"))
|
||||
}
|
||||
div {}
|
||||
|
||||
div(.class("label")) { "Flex" }
|
||||
div(.class("flex justify-center")) {
|
||||
Badge(number: room.flexSize)
|
||||
Badge(number: room.ductSize.flexSize)
|
||||
.attributes(.class("badge-primary"))
|
||||
}
|
||||
div {}
|
||||
|
||||
div(.class("label")) { "Rectangular" }
|
||||
div(.class("flex justify-center")) {
|
||||
if let width = room.rectangularWidth,
|
||||
let height = room.rectangularSize?.height
|
||||
if let width = room.ductSize.width,
|
||||
let height = room.ductSize.height
|
||||
{
|
||||
Badge {
|
||||
span { "\(width) x \(height)" }
|
||||
@@ -134,7 +134,7 @@ extension DuctSizingView {
|
||||
}
|
||||
div(.class("flex justify-end")) {
|
||||
div(.class("join")) {
|
||||
if room.rectangularSize != nil {
|
||||
if room.ductSize.width != nil {
|
||||
Tooltip("Delete Size", position: .bottom) {
|
||||
TrashButton()
|
||||
.attributes(.class("join-item btn-ghost"))
|
||||
@@ -142,7 +142,7 @@ extension DuctSizingView {
|
||||
.hx.delete(deleteRoute),
|
||||
.hx.target("#\(rowID)"),
|
||||
.hx.swap(.outerHTML),
|
||||
when: room.rectangularSize != nil
|
||||
when: room.ductSize.width != nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import Styleguide
|
||||
|
||||
struct TrunkSizeForm: HTML, Sendable {
|
||||
|
||||
static func id(_ trunk: DuctSizing.TrunkContainer? = nil) -> String {
|
||||
static func id(_ trunk: DuctSizes.TrunkContainer? = nil) -> String {
|
||||
let base = "trunkSizeForm"
|
||||
guard let trunk else { return base }
|
||||
return "\(base)_\(trunk.id.idString)"
|
||||
@@ -13,17 +13,17 @@ struct TrunkSizeForm: HTML, Sendable {
|
||||
|
||||
@Environment(ProjectViewValue.$projectID) var projectID
|
||||
|
||||
let container: DuctSizing.TrunkContainer?
|
||||
let rooms: [DuctSizing.RoomContainer]
|
||||
let container: DuctSizes.TrunkContainer?
|
||||
let rooms: [DuctSizes.RoomContainer]
|
||||
let dismiss: Bool
|
||||
|
||||
var trunk: DuctSizing.TrunkSize? {
|
||||
var trunk: TrunkSize? {
|
||||
container?.trunk
|
||||
}
|
||||
|
||||
init(
|
||||
trunk: DuctSizing.TrunkContainer? = nil,
|
||||
rooms: [DuctSizing.RoomContainer],
|
||||
trunk: DuctSizes.TrunkContainer? = nil,
|
||||
rooms: [DuctSizes.RoomContainer],
|
||||
dismiss: Bool = true
|
||||
) {
|
||||
self.container = trunk
|
||||
@@ -56,7 +56,7 @@ struct TrunkSizeForm: HTML, Sendable {
|
||||
label(.class("select w-full")) {
|
||||
span(.class("label")) { "Type" }
|
||||
select(.name("type")) {
|
||||
for type in DuctSizing.TrunkSize.TrunkType.allCases {
|
||||
for type in TrunkSize.TrunkType.allCases {
|
||||
option(.value(type.rawValue)) { type.rawValue.capitalized }
|
||||
.attributes(.selected, when: trunk?.type == type)
|
||||
}
|
||||
@@ -121,8 +121,8 @@ struct TrunkSizeForm: HTML, Sendable {
|
||||
|
||||
}
|
||||
|
||||
extension Array where Element == DuctSizing.TrunkSize.RoomProxy {
|
||||
func hasRoom(_ room: DuctSizing.RoomContainer) -> Bool {
|
||||
extension Array where Element == TrunkSize.RoomProxy {
|
||||
func hasRoom(_ room: DuctSizes.RoomContainer) -> Bool {
|
||||
first {
|
||||
$0.id == room.roomID
|
||||
&& $0.registers.contains(room.roomRegister)
|
||||
|
||||
@@ -7,11 +7,10 @@ extension DuctSizingView {
|
||||
|
||||
struct TrunkTable: HTML, Sendable {
|
||||
|
||||
let trunks: [DuctSizing.TrunkContainer]
|
||||
let rooms: [DuctSizing.RoomContainer]
|
||||
let ductSizes: DuctSizes
|
||||
|
||||
private var sortedTrunks: [DuctSizing.TrunkContainer] {
|
||||
trunks
|
||||
private var sortedTrunks: [DuctSizes.TrunkContainer] {
|
||||
ductSizes.trunks
|
||||
.sorted(by: { $0.designCFM.value > $1.designCFM.value })
|
||||
.sorted(by: { $0.type.rawValue > $1.type.rawValue })
|
||||
}
|
||||
@@ -29,7 +28,7 @@ extension DuctSizingView {
|
||||
}
|
||||
tbody {
|
||||
for trunk in sortedTrunks {
|
||||
TrunkRow(trunk: trunk, rooms: rooms)
|
||||
TrunkRow(trunk: trunk, rooms: ductSizes.rooms)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,8 +40,8 @@ extension DuctSizingView {
|
||||
|
||||
@Environment(ProjectViewValue.$projectID) var projectID
|
||||
|
||||
let trunk: DuctSizing.TrunkContainer
|
||||
let rooms: [DuctSizing.RoomContainer]
|
||||
let trunk: DuctSizes.TrunkContainer
|
||||
let rooms: [DuctSizes.RoomContainer]
|
||||
|
||||
var body: some HTML<HTMLTag.tr> {
|
||||
tr {
|
||||
@@ -135,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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,37 +9,33 @@ struct FrictionRateView: HTML, Sendable {
|
||||
|
||||
let componentLosses: [ComponentPressureLoss]
|
||||
let equivalentLengths: EffectiveLength.MaxContainer
|
||||
let frictionRateResponse: ManualDClient.FrictionRateResponse?
|
||||
let frictionRate: FrictionRate?
|
||||
|
||||
private var availableStaticPressure: Double? {
|
||||
frictionRateResponse?.availableStaticPressure
|
||||
}
|
||||
|
||||
private var frictionRateDesignValue: Double? {
|
||||
frictionRateResponse?.frictionRate
|
||||
frictionRate?.availableStaticPressure
|
||||
}
|
||||
|
||||
private var shouldShowBadges: Bool {
|
||||
frictionRateDesignValue != nil || availableStaticPressure != nil
|
||||
frictionRate != nil
|
||||
}
|
||||
|
||||
private var badgeColor: String {
|
||||
let base = "badge-info"
|
||||
guard let frictionRateDesignValue else { return base }
|
||||
if frictionRateDesignValue >= 0.18 || frictionRateDesignValue <= 0.02 {
|
||||
guard let frictionRate = frictionRate?.value else { return base }
|
||||
if frictionRate >= 0.18 || frictionRate <= 0.02 {
|
||||
return "badge-error"
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
private var showHighErrors: Bool {
|
||||
guard let frictionRateDesignValue else { return false }
|
||||
return frictionRateDesignValue >= 0.18
|
||||
guard let frictionRate = frictionRate?.value else { return false }
|
||||
return frictionRate >= 0.18
|
||||
}
|
||||
|
||||
private var showLowErrors: Bool {
|
||||
guard let frictionRateDesignValue else { return false }
|
||||
return frictionRateDesignValue <= 0.02
|
||||
guard let frictionRate = frictionRate?.value else { return false }
|
||||
return frictionRate <= 0.02
|
||||
}
|
||||
|
||||
private var showNoComponentLossesError: Bool {
|
||||
@@ -47,7 +43,7 @@ struct FrictionRateView: HTML, Sendable {
|
||||
}
|
||||
|
||||
private var showIncompleteSectionsError: Bool {
|
||||
availableStaticPressure == nil || frictionRateDesignValue == nil
|
||||
availableStaticPressure == nil || frictionRate?.value == nil
|
||||
}
|
||||
|
||||
private var hasAlerts: Bool {
|
||||
@@ -68,11 +64,11 @@ struct FrictionRateView: HTML, Sendable {
|
||||
div(.class("space-y-2 justify-end font-bold text-lg")) {
|
||||
if shouldShowBadges {
|
||||
|
||||
if let frictionRateDesignValue {
|
||||
if let frictionRate = frictionRate?.value {
|
||||
LabeledContent {
|
||||
span { "Friction Rate Design Value" }
|
||||
} content: {
|
||||
Badge(number: frictionRateDesignValue, digits: 2)
|
||||
Badge(number: frictionRate, digits: 2)
|
||||
.attributes(.class("\(badgeColor) badge-lg"))
|
||||
.bold()
|
||||
}
|
||||
|
||||
@@ -50,8 +50,10 @@ public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable
|
||||
meta(.content("1024"), .name("og:image:height"))
|
||||
meta(.content(keywords), .name(.keywords))
|
||||
script(.src("https://unpkg.com/htmx.org@2.0.8")) {}
|
||||
script(.src("/js/htmx-download.js")) {}
|
||||
script(.src("/js/main.js")) {}
|
||||
link(.rel(.stylesheet), .href("/css/output.css"))
|
||||
link(.rel(.stylesheet), .href("/css/htmx.css"))
|
||||
link(
|
||||
.rel(.icon),
|
||||
.href("/images/favicon.ico"),
|
||||
|
||||
@@ -234,7 +234,7 @@ extension ManualDClient {
|
||||
equipmentInfo: EquipmentInfo?,
|
||||
componentLosses: [ComponentPressureLoss],
|
||||
effectiveLength: EffectiveLength.MaxContainer
|
||||
) async throws -> FrictionRateResponse? {
|
||||
) async throws -> FrictionRate? {
|
||||
guard let staticPressure = equipmentInfo?.staticPressure else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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,23 +43,24 @@ 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")) {
|
||||
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"))
|
||||
}
|
||||
}
|
||||
@@ -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")) {
|
||||
@@ -237,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,27 +5,27 @@ import ManualDCore
|
||||
import Styleguide
|
||||
|
||||
struct TestPage: HTML, Sendable {
|
||||
let trunks: [DuctSizing.TrunkContainer]
|
||||
let rooms: [DuctSizing.RoomContainer]
|
||||
// let ductSizes: DuctSizes
|
||||
|
||||
var body: some HTML {
|
||||
div(.class("overflow-auto")) {
|
||||
DuctSizingView.TrunkTable(trunks: trunks, rooms: rooms)
|
||||
|
||||
Row {
|
||||
h2(.class("text-2xl font-bold")) { "Trunk Sizes" }
|
||||
|
||||
PlusButton()
|
||||
.attributes(
|
||||
.class("me-6"),
|
||||
.showModal(id: TrunkSizeForm.id())
|
||||
)
|
||||
}
|
||||
.attributes(.class("mt-6"))
|
||||
|
||||
div(.class("divider -mt-2")) {}
|
||||
|
||||
DuctSizingView.TrunkTable(trunks: trunks, rooms: rooms)
|
||||
}
|
||||
div {}
|
||||
// div(.class("overflow-auto")) {
|
||||
// DuctSizingView.TrunkTable(ductSizes: ductSizes)
|
||||
//
|
||||
// Row {
|
||||
// h2(.class("text-2xl font-bold")) { "Trunk Sizes" }
|
||||
//
|
||||
// PlusButton()
|
||||
// .attributes(
|
||||
// .class("me-6"),
|
||||
// .showModal(id: TrunkSizeForm.id())
|
||||
// )
|
||||
// }
|
||||
// .attributes(.class("mt-6"))
|
||||
//
|
||||
// div(.class("divider -mt-2")) {}
|
||||
//
|
||||
// DuctSizingView.TrunkTable(ductSizes: ductSizes)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
9
TODO.md
Normal file
9
TODO.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# TODO's
|
||||
|
||||
- [x] Fix theme not working when selected upon signup.
|
||||
- [ ] Pdf generation
|
||||
- [ ] Add postgres / mysql support
|
||||
- [ ] Opensource / license ??
|
||||
- [ ] Figure out domain to host (currently thinking ductcalc.pro)
|
||||
- [ ] Logo / navbar name may have to change if it's not duct-calc.
|
||||
- [ ] MainPage meta items will have to change also
|
||||
@@ -27,41 +27,41 @@ struct ManualDClientTests {
|
||||
let response = try await manualD.ductSize(
|
||||
.init(designCFM: 88, frictionRate: 0.06)
|
||||
)
|
||||
#expect(numberFormatter.string(for: response.ductulatorSize) == "6.07")
|
||||
#expect(numberFormatter.string(for: response.calculatedSize) == "6.07")
|
||||
#expect(response.finalSize == 7)
|
||||
#expect(response.flexSize == 7)
|
||||
#expect(response.velocity == 329)
|
||||
}
|
||||
|
||||
@Test
|
||||
func frictionRate() async throws {
|
||||
let response = try await manualD.frictionRate(
|
||||
.init(
|
||||
externalStaticPressure: 0.5,
|
||||
componentPressureLosses: .mock,
|
||||
totalEffectiveLength: 185
|
||||
)
|
||||
)
|
||||
#expect(numberFormatter.string(for: response.availableStaticPressure) == "0.11")
|
||||
#expect(numberFormatter.string(for: response.frictionRate) == "0.06")
|
||||
}
|
||||
// @Test
|
||||
// func frictionRate() async throws {
|
||||
// let response = try await manualD.frictionRate(
|
||||
// .init(
|
||||
// externalStaticPressure: 0.5,
|
||||
// componentPressureLosses: .mock,
|
||||
// totalEffectiveLength: 185
|
||||
// )
|
||||
// )
|
||||
// #expect(numberFormatter.string(for: response.availableStaticPressure) == "0.11")
|
||||
// #expect(numberFormatter.string(for: response.frictionRate) == "0.06")
|
||||
// }
|
||||
|
||||
@Test
|
||||
func frictionRateFails() async throws {
|
||||
await #expect(throws: ManualDError.self) {
|
||||
_ = try await manualD.frictionRate(
|
||||
.init(
|
||||
externalStaticPressure: 0.5,
|
||||
componentPressureLosses: .mock,
|
||||
totalEffectiveLength: 0
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
// @Test
|
||||
// func frictionRateFails() async throws {
|
||||
// await #expect(throws: ManualDError.self) {
|
||||
// _ = try await manualD.frictionRate(
|
||||
// .init(
|
||||
// externalStaticPressure: 0.5,
|
||||
// componentPressureLosses: .mock,
|
||||
// totalEffectiveLength: 0
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
@Test
|
||||
func totalEffectiveLength() async throws {
|
||||
let response = try await manualD.totalEffectiveLength(
|
||||
let response = try await manualD.totalEquivalentLength(
|
||||
.init(
|
||||
trunkLengths: [25],
|
||||
runoutLengths: [10],
|
||||
@@ -79,7 +79,7 @@ struct ManualDClientTests {
|
||||
|
||||
@Test
|
||||
func equivalentRectangularDuct() async throws {
|
||||
let response = try await manualD.equivalentRectangularDuct(.init(round: 7, height: 8))
|
||||
let response = try await manualD.rectangularSize(.init(round: 7, height: 8))
|
||||
#expect(response.height == 8)
|
||||
#expect(response.width == 5)
|
||||
}
|
||||
|
||||
208
Tests/ViewControllerTests/ViewControllerTests.swift
Normal file
208
Tests/ViewControllerTests/ViewControllerTests.swift
Normal file
@@ -0,0 +1,208 @@
|
||||
import AuthClient
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import HTMLSnapshotTesting
|
||||
import Logging
|
||||
import ManualDClient
|
||||
import ManualDCore
|
||||
import ProjectClient
|
||||
import SnapshotTesting
|
||||
import Testing
|
||||
import ViewController
|
||||
|
||||
@Suite(.snapshots(record: .missing))
|
||||
struct ViewControllerTests {
|
||||
|
||||
@Test
|
||||
func login() async throws {
|
||||
try await withDependencies {
|
||||
$0.viewController = .liveValue
|
||||
$0.authClient = .failing
|
||||
} operation: {
|
||||
@Dependency(\.viewController) var viewController
|
||||
|
||||
let login = try await viewController.view(.test(.login(.index())))
|
||||
assertSnapshot(of: login, as: .html)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func signup() async throws {
|
||||
try await withDependencies {
|
||||
$0.viewController = .liveValue
|
||||
$0.authClient = .failing
|
||||
} operation: {
|
||||
@Dependency(\.viewController) var viewController
|
||||
|
||||
let signup = try await viewController.view(.test(.login(.index())))
|
||||
assertSnapshot(of: signup, as: .html)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func userProfile() async throws {
|
||||
try await withDefaultDependencies {
|
||||
@Dependency(\.viewController) var viewController
|
||||
let html = try await viewController.view(.test(.user(.profile(.index))))
|
||||
assertSnapshot(of: html, as: .html)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func projectIndex() async throws {
|
||||
let project = withDependencies {
|
||||
$0.uuid = .incrementing
|
||||
$0.date = .constant(.mock)
|
||||
} operation: {
|
||||
Project.mock
|
||||
}
|
||||
|
||||
try await withDefaultDependencies {
|
||||
$0.database.projects.fetch = { _, _ in
|
||||
.init(items: [project], metadata: .init(page: 1, per: 25, total: 1))
|
||||
}
|
||||
} operation: {
|
||||
@Dependency(\.viewController) var viewController
|
||||
let html = try await viewController.view(.test(.project(.index)))
|
||||
assertSnapshot(of: html, as: .html)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func projectDetail() async throws {
|
||||
|
||||
let (
|
||||
project,
|
||||
rooms,
|
||||
equipment,
|
||||
tels,
|
||||
componentLosses,
|
||||
trunks
|
||||
) = withDependencies {
|
||||
$0.uuid = .incrementing
|
||||
$0.date = .constant(.mock)
|
||||
} operation: {
|
||||
let project = Project.mock
|
||||
let rooms = Room.mock(projectID: project.id)
|
||||
let equipment = EquipmentInfo.mock(projectID: project.id)
|
||||
let tels = EffectiveLength.mock(projectID: project.id)
|
||||
let componentLosses = ComponentPressureLoss.mock(projectID: project.id)
|
||||
let trunks = TrunkSize.mock(projectID: project.id, rooms: rooms)
|
||||
|
||||
return (
|
||||
project,
|
||||
rooms,
|
||||
equipment,
|
||||
tels,
|
||||
componentLosses,
|
||||
trunks
|
||||
)
|
||||
}
|
||||
|
||||
try await withDefaultDependencies {
|
||||
$0.database.projects.get = { _ in project }
|
||||
$0.database.projects.getCompletedSteps = { _ in
|
||||
.init(equipmentInfo: true, rooms: true, equivalentLength: true, frictionRate: true)
|
||||
}
|
||||
$0.database.projects.getSensibleHeatRatio = { _ in 0.83 }
|
||||
$0.database.rooms.fetch = { _ in rooms }
|
||||
$0.database.equipment.fetch = { _ in equipment }
|
||||
$0.database.effectiveLength.fetch = { _ in tels }
|
||||
$0.database.effectiveLength.fetchMax = { _ in
|
||||
.init(supply: tels.first, return: tels.last)
|
||||
}
|
||||
$0.database.componentLoss.fetch = { _ in componentLosses }
|
||||
$0.projectClient.calculateDuctSizes = { _ in
|
||||
.mock(equipmentInfo: equipment, rooms: rooms, trunks: trunks)
|
||||
}
|
||||
} operation: {
|
||||
@Dependency(\.viewController) var viewController
|
||||
|
||||
var html = try await viewController.view(.test(.project(.detail(project.id, .index))))
|
||||
assertSnapshot(of: html, as: .html)
|
||||
|
||||
html = try await viewController.view(.test(.project(.detail(project.id, .rooms(.index)))))
|
||||
assertSnapshot(of: html, as: .html)
|
||||
|
||||
html = try await viewController.view(.test(.project(.detail(project.id, .equipment(.index)))))
|
||||
assertSnapshot(of: html, as: .html)
|
||||
|
||||
html = try await viewController.view(
|
||||
.test(.project(.detail(project.id, .equivalentLength(.index)))))
|
||||
assertSnapshot(of: html, as: .html)
|
||||
|
||||
html = try await viewController.view(
|
||||
.test(.project(.detail(project.id, .frictionRate(.index)))))
|
||||
assertSnapshot(of: html, as: .html)
|
||||
|
||||
html = try await viewController.view(
|
||||
.test(.project(.detail(project.id, .ductSizing(.index)))))
|
||||
assertSnapshot(of: html, as: .html)
|
||||
}
|
||||
}
|
||||
|
||||
func createUserDependencies() -> (User, User.Profile) {
|
||||
withDependencies {
|
||||
$0.uuid = .incrementing
|
||||
$0.date = .constant(.mock)
|
||||
} operation: {
|
||||
let user = User.mock
|
||||
let profile = User.Profile.mock(userID: user.id)
|
||||
return (user, profile)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func withDefaultDependencies<R>(
|
||||
isolation: isolated (any Actor)? = #isolation,
|
||||
_ updateDependencies: (inout DependencyValues) async throws -> Void = { _ in },
|
||||
operation: () async throws -> R
|
||||
) async rethrows -> R {
|
||||
let (user, profile) = createUserDependencies()
|
||||
|
||||
return try await withDependencies {
|
||||
$0.viewController = .liveValue
|
||||
$0.authClient.currentUser = { user }
|
||||
$0.database.userProfile.fetch = { _ in profile }
|
||||
$0.manualD = .liveValue
|
||||
try await updateDependencies(&$0)
|
||||
} operation: {
|
||||
try await operation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Date {
|
||||
static let mock = Self(timeIntervalSince1970: 1_234_567_890)
|
||||
}
|
||||
|
||||
extension ViewController.Request {
|
||||
|
||||
static func test(
|
||||
_ route: SiteRoute.View,
|
||||
isHtmxRequest: Bool = false,
|
||||
logger: Logger = .init(label: "ViewControllerTests")
|
||||
) -> Self {
|
||||
.init(route: route, isHtmxRequest: isHtmxRequest, logger: logger)
|
||||
}
|
||||
}
|
||||
|
||||
extension AuthClient {
|
||||
static let failing = Self(
|
||||
createAndLogin: { _ in
|
||||
throw TestError()
|
||||
},
|
||||
currentUser: {
|
||||
throw TestError()
|
||||
},
|
||||
login: { _ in
|
||||
throw TestError()
|
||||
},
|
||||
logout: {
|
||||
throw TestError()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
struct TestError: Error {}
|
||||
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Duct Calc</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="ductcalc.com" name="og:site_name">
|
||||
<meta content="Duct Calc" name="og:title">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="description">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="og:description">
|
||||
<meta content="/images/mand_logo.png" name="og:image">
|
||||
<meta content="/images/mand_logo.png" name="twitter:image">
|
||||
<meta content="Duct Calc" name="twitter:image:alt">
|
||||
<meta content="summary_large_image" name="twitter:card">
|
||||
<meta content="1536" name="og:image:width">
|
||||
<meta content="1024" name="og:image:height">
|
||||
<meta content="duct, hvac, duct-design, duct design, manual-d, manual d, design" name="keywords">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<link rel="stylesheet" href="/css/output.css">
|
||||
<link rel="icon" href="/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="/images/favicon-32x32.png" type="image/png">
|
||||
<link rel="icon" href="/images/favicon-16x16.png" type="image/png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<script src="https://unpkg.com/htmx-remove@latest" crossorigin="anonymous" integrity="sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex flex-col min-h-screen min-w-full justify-between">
|
||||
<main class="flex flex-col min-h-screen min-w-full grow mb-auto">
|
||||
<dialog id="loginForm" class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h1 class="text-2xl font-bold mb-6">Login</h1>
|
||||
<form method="post" class="space-y-4">
|
||||
<div>
|
||||
<label class="input validator w-full"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
|
||||
</g>
|
||||
</svg>
|
||||
<input type="email" placeholder="Email" required name="email" id="email" autofocus></label>
|
||||
<div class="validator-hint hidden">Enter valid email address.</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input validator w-full"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"
|
||||
></path>
|
||||
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
<input type="password" placeholder="Password" required pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}" minlength="8" name="password" id="password"></label>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<button class="btn btn-secondary mt-4 w-full">Login</button>
|
||||
</div>
|
||||
<div class="flex justify-center"><a class="btn btn-link" href="/signup">Sign Up</a></div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</main>
|
||||
<div class="bottom-0 left-0 bg-error"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,251 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Duct Calc</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="ductcalc.com" name="og:site_name">
|
||||
<meta content="Duct Calc" name="og:title">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="description">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="og:description">
|
||||
<meta content="/images/mand_logo.png" name="og:image">
|
||||
<meta content="/images/mand_logo.png" name="twitter:image">
|
||||
<meta content="Duct Calc" name="twitter:image:alt">
|
||||
<meta content="summary_large_image" name="twitter:card">
|
||||
<meta content="1536" name="og:image:width">
|
||||
<meta content="1024" name="og:image:height">
|
||||
<meta content="duct, hvac, duct-design, duct design, manual-d, manual d, design" name="keywords">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<link rel="stylesheet" href="/css/output.css">
|
||||
<link rel="icon" href="/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="/images/favicon-32x32.png" type="image/png">
|
||||
<link rel="icon" href="/images/favicon-16x16.png" type="image/png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<script src="https://unpkg.com/htmx-remove@latest" crossorigin="anonymous" integrity="sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex flex-col min-h-screen min-w-full justify-between">
|
||||
<main class="flex flex-col min-h-screen min-w-full grow mb-auto">
|
||||
<div class="drawer lg:drawer-open h-full">
|
||||
<input id="my-drawer-1" type="checkbox" class="drawer-toggle">
|
||||
<div class="drawer-content overflow-auto">
|
||||
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
|
||||
<div class="flex flex-1 space-x-4 items-center">
|
||||
<div class="tooltip tooltip-right" data-tip="Open sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div>
|
||||
<div class="tooltip tooltip-right" data-tip="Home">
|
||||
<a class="flex w-fit h-fit text-xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
|
||||
<img src="/images/mand_logo_sm.webp">
|
||||
Duct Calc<span></span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<div class="tooltip tooltip-left" data-tip="Profile"><a href="/profile" class="btn btn-square btn-ghost hover:bg-neutral hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-icon lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg></a></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="p-4">
|
||||
<div>
|
||||
<div class="flex justify-between bg-secondary border-2 border-primary rounded-sm shadow-sm
|
||||
p-6 w-full">
|
||||
<h1 class="text-3xl font-bold">Project</h1>
|
||||
<div class="tooltip tooltip-left" data-tip="Edit project">
|
||||
<button class="btn btn-primary" type="button" onclick="projectForm.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-zebra text-lg">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label font-bold">Name</td>
|
||||
<td>
|
||||
<div class="flex justify-end">Testy McTestface</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label font-bold">Street Address</td>
|
||||
<td>
|
||||
<div class="flex justify-end">1234 Sesame Street</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label font-bold">City</td>
|
||||
<td>
|
||||
<div class="flex justify-end">Monroe</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label font-bold">State</td>
|
||||
<td>
|
||||
<div class="flex justify-end">OH</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label font-bold">Zip</td>
|
||||
<td>
|
||||
<div class="flex justify-end">55555</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<dialog id="projectForm" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="projectForm.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6 ps-2">Project</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000000">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" value="Testy McTestface" placeholder="Project Name" required autofocus>
|
||||
Address</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="streetAddress" type="text" value="1234 Sesame Street" placeholder="Street Address" required>
|
||||
City</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="city" type="text" value="Monroe" placeholder="City" required>
|
||||
State</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="state" type="text" value="OH" placeholder="State" required>
|
||||
Zip</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="zipCode" type="text" value="55555" placeholder="Zip Code" required></label>
|
||||
<button class="btn btn-secondary btn-block my-6" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-side is-drawer-close:overflow-visible grow">
|
||||
<label for="my-drawer-1" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<div class="flex grow h-full flex-col items-start bg-base-300 text-base-content
|
||||
is-drawer-close:min-w-[80px] is-drawer-open:max-w-[300px]">
|
||||
<ul class="w-full grow">
|
||||
<li class="flex w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000" hx-push-url="true" hx-target="body" hx-swap="outerHTML" data-active="true">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-icon lucide-map-pin"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Project</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-door-closed-icon lucide-door-closed"><path d="M10 12h.01"/><path d="M18 20V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"/><path d="M2 20h20"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Rooms</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="flex w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/equipment" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-fan-icon lucide-fan"><path d="M10.827 16.379a6.082 6.082 0 0 1-8.618-7.002l5.412 1.45a6.082 6.082 0 0 1 7.002-8.618l-1.45 5.412a6.082 6.082 0 0 1 8.618 7.002l-5.412-1.45a6.082 6.082 0 0 1-7.002 8.618l1.45-5.412Z"/><path d="M12 12v.01"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Equipment</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/effective-lengths" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ruler-dimension-line-icon lucide-ruler-dimension-line"><path d="M10 15v-3"/><path d="M14 15v-3"/><path d="M18 15v-3"/><path d="M2 8V4"/><path d="M22 6H2"/><path d="M22 8V4"/><path d="M6 15v-3"/><rect x="2" y="12" width="20" height="8" rx="2"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>T.E.L.</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/friction-rate" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-function-icon lucide-square-function"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"/><path d="M9 11.2h5.7"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Friction Rate</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/duct-sizing" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wind-icon lucide-wind"><path d="M12.8 19.6A2 2 0 1 0 14 16H2"/><path d="M17.5 8a2.5 2.5 0 1 1 2 4H2"/><path d="M9.8 4.4A2 2 0 1 1 11 8H2"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Duct Sizes</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div class="bottom-0 left-0 bg-error">
|
||||
<footer class="footer sm:footer-horizontal footer-center
|
||||
bg-base-300 text-base-content p-4">
|
||||
<aside>
|
||||
<p>Copyright © 2026 - All rights reserved by Michael Housh</p>
|
||||
</aside>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,593 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Duct Calc</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="ductcalc.com" name="og:site_name">
|
||||
<meta content="Duct Calc" name="og:title">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="description">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="og:description">
|
||||
<meta content="/images/mand_logo.png" name="og:image">
|
||||
<meta content="/images/mand_logo.png" name="twitter:image">
|
||||
<meta content="Duct Calc" name="twitter:image:alt">
|
||||
<meta content="summary_large_image" name="twitter:card">
|
||||
<meta content="1536" name="og:image:width">
|
||||
<meta content="1024" name="og:image:height">
|
||||
<meta content="duct, hvac, duct-design, duct design, manual-d, manual d, design" name="keywords">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<link rel="stylesheet" href="/css/output.css">
|
||||
<link rel="icon" href="/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="/images/favicon-32x32.png" type="image/png">
|
||||
<link rel="icon" href="/images/favicon-16x16.png" type="image/png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<script src="https://unpkg.com/htmx-remove@latest" crossorigin="anonymous" integrity="sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex flex-col min-h-screen min-w-full justify-between">
|
||||
<main class="flex flex-col min-h-screen min-w-full grow mb-auto">
|
||||
<div class="drawer lg:drawer-open h-full">
|
||||
<input id="my-drawer-1" type="checkbox" class="drawer-toggle">
|
||||
<div class="drawer-content overflow-auto">
|
||||
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
|
||||
<div class="flex flex-1 space-x-4 items-center">
|
||||
<div class="tooltip tooltip-right" data-tip="Open sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div>
|
||||
<div class="tooltip tooltip-right" data-tip="Home">
|
||||
<a class="flex w-fit h-fit text-xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
|
||||
<img src="/images/mand_logo_sm.webp">
|
||||
Duct Calc<span></span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<div class="tooltip tooltip-left" data-tip="Profile"><a href="/profile" class="btn btn-square btn-ghost hover:bg-neutral hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-icon lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg></a></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="p-4">
|
||||
<div class="flex w-full flex-col">
|
||||
<div class="flex justify-between bg-secondary border-2 border-primary rounded-sm shadow-sm
|
||||
p-6 w-full">
|
||||
<div class="flex grid grid-cols-3 w-full gap-y-4">
|
||||
<div class="col-span-2">
|
||||
<h1 class="text-3xl font-bold">Room Loads</h1>
|
||||
</div>
|
||||
<div class="flex justify-end grow">
|
||||
<div class="tooltip tooltip-left" data-tip="Set sensible heat ratio">
|
||||
<button class="btn btn-primary text-lg font-bold py-2 " onclick="shrForm.showModal()">
|
||||
<div class="flex grow justify-end items-end space-x-4">
|
||||
<span>Sensible Heat Ratio</span>
|
||||
<div class="badge badge-lg badge-outline"><span>0.83</span></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end space-x-4 font-bold">
|
||||
<span class="text-lg">Heating Total</span>
|
||||
<div class="badge badge-lg badge-outline badge-error"><span>42,255</span></div>
|
||||
</div>
|
||||
<div class="flex justify-center items-end space-x-4 my-auto font-bold">
|
||||
<span class="text-lg">Cooling Total</span>
|
||||
<div class="badge badge-lg badge-outline badge-success"><span>26,835</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end space-x-4 me-4 my-auto font-bold">
|
||||
<span class="text-lg">Cooling Sensible</span>
|
||||
<div class="badge badge-lg badge-outline badge-info"><span>22,273</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="shrForm" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="shrForm.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-xl font-bold mb-6">Sensible Heat Ratio</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/update-shr" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
SHR<label class="input w-full"><span class="label"></span>
|
||||
<input name="sensibleHeatRatio" type="number" value="0.83" placeholder="0.83" min="0" max="1" step="0.01" autofocus></label>
|
||||
<button class="btn btn-secondary btn-block my-6" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
<table class="table table-zebra text-lg" id="roomsTable">
|
||||
<thead>
|
||||
<tr class="text-lg font-bold">
|
||||
<th>Name</th>
|
||||
<th>
|
||||
<div class="flex justify-center">Heating Load</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex justify-center">Cooling Total</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex justify-center">Cooling Sensible</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex justify-center">Register Count</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex justify-end me-2">
|
||||
<div class="tooltip tooltip-left" data-tip="Add Room">
|
||||
<button type="button" class="btn btn-primary mx-auto tooltip-left" onclick="roomForm.showModal()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-plus-icon lucide-circle-plus"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/><path d="M12 8v8"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr id="roomRow_00000000000000000000000000000001">
|
||||
<td>Bed-1</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>3,913</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>2,472</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>2,052</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>1</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-end">
|
||||
<div class="join">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete room">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000001" hx-target="closest tr" hx-confirm="Are you sure?"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit room">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="roomForm_00000000000000000000000000000001.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="roomForm_00000000000000000000000000000001" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000001.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000001" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000001">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Bed-1">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="3913.0">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="2472.0">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="registerCount" type="number" min="1" required value="1"></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="roomRow_00000000000000000000000000000002">
|
||||
<td>Entry</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>8,284</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>2,916</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>2,420</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>2</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-end">
|
||||
<div class="join">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete room">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000002" hx-target="closest tr" hx-confirm="Are you sure?"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit room">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="roomForm_00000000000000000000000000000002.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="roomForm_00000000000000000000000000000002" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000002.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000002" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000002">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Entry">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="8284.0">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="2916.0">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="registerCount" type="number" min="1" required value="2"></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="roomRow_00000000000000000000000000000003">
|
||||
<td>Family Room</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>9,785</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>7,446</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>6,180</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>3</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-end">
|
||||
<div class="join">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete room">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000003" hx-target="closest tr" hx-confirm="Are you sure?"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit room">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="roomForm_00000000000000000000000000000003.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="roomForm_00000000000000000000000000000003" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000003.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000003" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000003">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Family Room">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="9785.0">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="7446.0">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="registerCount" type="number" min="1" required value="3"></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="roomRow_00000000000000000000000000000004">
|
||||
<td>Kitchen</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>4,518</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>5,096</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>4,230</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>2</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-end">
|
||||
<div class="join">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete room">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000004" hx-target="closest tr" hx-confirm="Are you sure?"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit room">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="roomForm_00000000000000000000000000000004.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="roomForm_00000000000000000000000000000004" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000004.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000004" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000004">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Kitchen">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="4518.0">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="5096.0">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="registerCount" type="number" min="1" required value="2"></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="roomRow_00000000000000000000000000000005">
|
||||
<td>Living Room</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>7,553</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>6,829</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>5,668</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>2</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-end">
|
||||
<div class="join">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete room">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000005" hx-target="closest tr" hx-confirm="Are you sure?"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit room">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="roomForm_00000000000000000000000000000005.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="roomForm_00000000000000000000000000000005" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000005.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000005" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000005">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Living Room">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="7553.0">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="6829.0">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="registerCount" type="number" min="1" required value="2"></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="roomRow_00000000000000000000000000000006">
|
||||
<td>Master</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>8,202</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>2,076</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>1,723</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center"><span>2</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-end">
|
||||
<div class="join">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete room">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000006" hx-target="closest tr" hx-confirm="Are you sure?"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit room">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="roomForm_00000000000000000000000000000006.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="roomForm_00000000000000000000000000000006" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000006.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000006" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000006">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Master">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="8202.0">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="2076.0">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="registerCount" type="number" min="1" required value="2"></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<dialog id="roomForm" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-post="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="registerCount" type="number" min="1" required value="1"></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-side is-drawer-close:overflow-visible grow">
|
||||
<label for="my-drawer-1" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<div class="flex grow h-full flex-col items-start bg-base-300 text-base-content
|
||||
is-drawer-close:min-w-[80px] is-drawer-open:max-w-[300px]">
|
||||
<ul class="w-full grow">
|
||||
<li class="flex w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-icon lucide-map-pin"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Project</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-push-url="true" hx-target="body" hx-swap="outerHTML" data-active="true">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-door-closed-icon lucide-door-closed"><path d="M10 12h.01"/><path d="M18 20V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"/><path d="M2 20h20"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Rooms</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="flex w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/equipment" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-fan-icon lucide-fan"><path d="M10.827 16.379a6.082 6.082 0 0 1-8.618-7.002l5.412 1.45a6.082 6.082 0 0 1 7.002-8.618l-1.45 5.412a6.082 6.082 0 0 1 8.618 7.002l-5.412-1.45a6.082 6.082 0 0 1-7.002 8.618l1.45-5.412Z"/><path d="M12 12v.01"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Equipment</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/effective-lengths" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ruler-dimension-line-icon lucide-ruler-dimension-line"><path d="M10 15v-3"/><path d="M14 15v-3"/><path d="M18 15v-3"/><path d="M2 8V4"/><path d="M22 6H2"/><path d="M22 8V4"/><path d="M6 15v-3"/><rect x="2" y="12" width="20" height="8" rx="2"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>T.E.L.</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/friction-rate" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-function-icon lucide-square-function"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"/><path d="M9 11.2h5.7"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Friction Rate</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/duct-sizing" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wind-icon lucide-wind"><path d="M12.8 19.6A2 2 0 1 0 14 16H2"/><path d="M17.5 8a2.5 2.5 0 1 1 2 4H2"/><path d="M9.8 4.4A2 2 0 1 1 11 8H2"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Duct Sizes</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div class="bottom-0 left-0 bg-error">
|
||||
<footer class="footer sm:footer-horizontal footer-center
|
||||
bg-base-300 text-base-content p-4">
|
||||
<aside>
|
||||
<p>Copyright © 2026 - All rights reserved by Michael Housh</p>
|
||||
</aside>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,236 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Duct Calc</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="ductcalc.com" name="og:site_name">
|
||||
<meta content="Duct Calc" name="og:title">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="description">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="og:description">
|
||||
<meta content="/images/mand_logo.png" name="og:image">
|
||||
<meta content="/images/mand_logo.png" name="twitter:image">
|
||||
<meta content="Duct Calc" name="twitter:image:alt">
|
||||
<meta content="summary_large_image" name="twitter:card">
|
||||
<meta content="1536" name="og:image:width">
|
||||
<meta content="1024" name="og:image:height">
|
||||
<meta content="duct, hvac, duct-design, duct design, manual-d, manual d, design" name="keywords">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<link rel="stylesheet" href="/css/output.css">
|
||||
<link rel="icon" href="/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="/images/favicon-32x32.png" type="image/png">
|
||||
<link rel="icon" href="/images/favicon-16x16.png" type="image/png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<script src="https://unpkg.com/htmx-remove@latest" crossorigin="anonymous" integrity="sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex flex-col min-h-screen min-w-full justify-between">
|
||||
<main class="flex flex-col min-h-screen min-w-full grow mb-auto">
|
||||
<div class="drawer lg:drawer-open h-full">
|
||||
<input id="my-drawer-1" type="checkbox" class="drawer-toggle">
|
||||
<div class="drawer-content overflow-auto">
|
||||
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
|
||||
<div class="flex flex-1 space-x-4 items-center">
|
||||
<div class="tooltip tooltip-right" data-tip="Open sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div>
|
||||
<div class="tooltip tooltip-right" data-tip="Home">
|
||||
<a class="flex w-fit h-fit text-xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
|
||||
<img src="/images/mand_logo_sm.webp">
|
||||
Duct Calc<span></span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<div class="tooltip tooltip-left" data-tip="Profile"><a href="/profile" class="btn btn-square btn-ghost hover:bg-neutral hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-icon lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg></a></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="p-4">
|
||||
<div class="space-y-4" id="equipmentInfo">
|
||||
<div class="flex justify-between bg-secondary border-2 border-primary rounded-sm shadow-sm
|
||||
p-6 w-full">
|
||||
<h1 class="text-3xl font-bold">Equipment Details</h1>
|
||||
<div class="tooltip tooltip-left" data-tip="Edit equipment details">
|
||||
<button class="btn btn-primary" type="button" onclick="equipmentForm.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-zebra">
|
||||
<tbody class="text-lg">
|
||||
<tr>
|
||||
<td><span class="text-lg label font-bold">Static Pressure</span></td>
|
||||
<td>
|
||||
<div class="flex justify-end"><span>0.5</span></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="text-lg label font-bold">Heating CFM</span></td>
|
||||
<td>
|
||||
<div class="flex justify-end"><span>900</span></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="text-lg label font-bold">Cooling CFM</span></td>
|
||||
<td>
|
||||
<div class="flex justify-end"><span>1,000</span></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<dialog id="equipmentForm" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="equipmentForm.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6 ps-2">Equipment Info</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/equipment/00000000-0000-0000-0000-000000000007" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000007">
|
||||
Static Pressure<label class="input w-full"><span class="label"></span>
|
||||
<input name="staticPressure" type="number" value="0.5" min="0" max="1.0" step="0.1" required>
|
||||
Heating CFM</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingCFM" type="number" value="900" placeholder="1000" min="0" required autofocus>
|
||||
Cooling CFM</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingCFM" type="number" value="1000" placeholder="1000" min="0" required></label>
|
||||
<button class="btn btn-secondary btn-block my-6" type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-side is-drawer-close:overflow-visible grow">
|
||||
<label for="my-drawer-1" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<div class="flex grow h-full flex-col items-start bg-base-300 text-base-content
|
||||
is-drawer-close:min-w-[80px] is-drawer-open:max-w-[300px]">
|
||||
<ul class="w-full grow">
|
||||
<li class="flex w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-icon lucide-map-pin"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Project</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-door-closed-icon lucide-door-closed"><path d="M10 12h.01"/><path d="M18 20V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"/><path d="M2 20h20"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Rooms</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="flex w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/equipment" hx-push-url="true" hx-target="body" hx-swap="outerHTML" data-active="true">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-fan-icon lucide-fan"><path d="M10.827 16.379a6.082 6.082 0 0 1-8.618-7.002l5.412 1.45a6.082 6.082 0 0 1 7.002-8.618l-1.45 5.412a6.082 6.082 0 0 1 8.618 7.002l-5.412-1.45a6.082 6.082 0 0 1-7.002 8.618l1.45-5.412Z"/><path d="M12 12v.01"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Equipment</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/effective-lengths" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ruler-dimension-line-icon lucide-ruler-dimension-line"><path d="M10 15v-3"/><path d="M14 15v-3"/><path d="M18 15v-3"/><path d="M2 8V4"/><path d="M22 6H2"/><path d="M22 8V4"/><path d="M6 15v-3"/><rect x="2" y="12" width="20" height="8" rx="2"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>T.E.L.</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/friction-rate" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-function-icon lucide-square-function"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"/><path d="M9 11.2h5.7"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Friction Rate</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/duct-sizing" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wind-icon lucide-wind"><path d="M12.8 19.6A2 2 0 1 0 14 16H2"/><path d="M17.5 8a2.5 2.5 0 1 1 2 4H2"/><path d="M9.8 4.4A2 2 0 1 1 11 8H2"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Duct Sizes</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div class="bottom-0 left-0 bg-error">
|
||||
<footer class="footer sm:footer-horizontal footer-center
|
||||
bg-base-300 text-base-content p-4">
|
||||
<aside>
|
||||
<p>Copyright © 2026 - All rights reserved by Michael Housh</p>
|
||||
</aside>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,366 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Duct Calc</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="ductcalc.com" name="og:site_name">
|
||||
<meta content="Duct Calc" name="og:title">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="description">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="og:description">
|
||||
<meta content="/images/mand_logo.png" name="og:image">
|
||||
<meta content="/images/mand_logo.png" name="twitter:image">
|
||||
<meta content="Duct Calc" name="twitter:image:alt">
|
||||
<meta content="summary_large_image" name="twitter:card">
|
||||
<meta content="1536" name="og:image:width">
|
||||
<meta content="1024" name="og:image:height">
|
||||
<meta content="duct, hvac, duct-design, duct design, manual-d, manual d, design" name="keywords">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<link rel="stylesheet" href="/css/output.css">
|
||||
<link rel="icon" href="/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="/images/favicon-32x32.png" type="image/png">
|
||||
<link rel="icon" href="/images/favicon-16x16.png" type="image/png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<script src="https://unpkg.com/htmx-remove@latest" crossorigin="anonymous" integrity="sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex flex-col min-h-screen min-w-full justify-between">
|
||||
<main class="flex flex-col min-h-screen min-w-full grow mb-auto">
|
||||
<div class="drawer lg:drawer-open h-full">
|
||||
<input id="my-drawer-1" type="checkbox" class="drawer-toggle">
|
||||
<div class="drawer-content overflow-auto">
|
||||
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
|
||||
<div class="flex flex-1 space-x-4 items-center">
|
||||
<div class="tooltip tooltip-right" data-tip="Open sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div>
|
||||
<div class="tooltip tooltip-right" data-tip="Home">
|
||||
<a class="flex w-fit h-fit text-xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
|
||||
<img src="/images/mand_logo_sm.webp">
|
||||
Duct Calc<span></span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<div class="tooltip tooltip-left" data-tip="Profile"><a href="/profile" class="btn btn-square btn-ghost hover:bg-neutral hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-icon lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg></a></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="p-4">
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between bg-secondary border-2 border-primary rounded-sm shadow-sm
|
||||
p-6 w-full pb-6">
|
||||
<h1 class="text-3xl font-bold">Equivalent Lengths</h1>
|
||||
<div class="tooltip tooltip-left" data-tip="Add equivalent length">
|
||||
<button type="button" class="btn btn-primary" onclick="equivalentLengthForm.showModal()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-plus-icon lucide-circle-plus"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/><path d="M12 8v8"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="equivalentLengthForm" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="equivalentLengthForm.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-2xl font-bold">Effective Length</h1>
|
||||
<div id="formStep_equivalentLengthForm" class="mt-4">
|
||||
<form class="space-y-4" hx-post="/projects/00000000-0000-0000-0000-000000000000/effective-lengths/stepOne" hx-target="#formStep_equivalentLengthForm" hx-swap="innerHTML">
|
||||
<label class="input w-full"><span class="label">Name</span>
|
||||
<input name="name" type="text" value="" required autofocus>
|
||||
Type</label><label class="select w-full"><span class="label"></span>
|
||||
<select name="type" id="type">
|
||||
<option value="return">Return</option>
|
||||
<option value="supply" selected>Supply</option>
|
||||
</select></label>
|
||||
<div class="flex justify-between">
|
||||
<div></div>
|
||||
<button class="btn btn-secondary" type="submit">Next</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<table class="table table-zebra text-lg">
|
||||
<thead>
|
||||
<tr class="text-lg">
|
||||
<th>Type</th>
|
||||
<th>Name</th>
|
||||
<th>Straight Lengths</th>
|
||||
<th>
|
||||
<div class="grid grid-cols-3 gap-2 min-w-[220px]">
|
||||
<div class="flex justify-center col-span-3">Groups</div>
|
||||
<div>Group</div>
|
||||
<div class="flex justify-center">T.E.L.</div>
|
||||
<div class="flex justify-end">Quantity</div>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex justify-end me-[140px]">T.E.L.</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr id="00000000000000000000000000000008">
|
||||
<td>
|
||||
<div class="badge badge-lg badge-outline badge-info"><span>supply</span></div>
|
||||
</td>
|
||||
<td>Supply - 1</td>
|
||||
<td>
|
||||
<div class="grid grid-cols-1 gap-2"><span>10</span><span>25</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="grid grid-cols-3 gap-2 min-w-[220px]">
|
||||
<span>1-a</span>
|
||||
<div class="flex justify-center"><span>20</span></div>
|
||||
<div class="flex justify-end"><span>1</span></div>
|
||||
2-b<span></span>
|
||||
<div class="flex justify-center"><span>30</span></div>
|
||||
<div class="flex justify-end"><span>1</span></div>
|
||||
3-a<span></span>
|
||||
<div class="flex justify-center"><span>10</span></div>
|
||||
<div class="flex justify-end"><span>1</span></div>
|
||||
12-a<span></span>
|
||||
<div class="flex justify-center"><span>10</span></div>
|
||||
<div class="flex justify-end"><span>1</span></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-end mx-auto space-x-4">
|
||||
<div class="badge badge-lg badge-outline badge-primary badge-lg pt-2"><span>105</span></div>
|
||||
<div class="flex justify-end -mt-2">
|
||||
<div class="join">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/effective-lengths/00000000-0000-0000-0000-000000000008" hx-confirm="Are you sure?" hx-target="#00000000000000000000000000000008" hx-swap="outerHTML"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="equivalentLengthForm_00000000000000000000000000000008.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="equivalentLengthForm_00000000000000000000000000000008" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="equivalentLengthForm_00000000000000000000000000000008.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-2xl font-bold">Effective Length</h1>
|
||||
<div id="formStep_equivalentLengthForm_00000000000000000000000000000008" class="mt-4">
|
||||
<form class="space-y-4" hx-post="/projects/00000000-0000-0000-0000-000000000000/effective-lengths/stepOne" hx-target="#formStep_equivalentLengthForm_00000000000000000000000000000008" hx-swap="innerHTML">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000008">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" value="Supply - 1" required autofocus>
|
||||
Type</label><label class="select w-full"><span class="label"></span>
|
||||
<select name="type" id="type">
|
||||
<option value="return">Return</option>
|
||||
<option value="supply" selected>Supply</option>
|
||||
</select></label>
|
||||
<div class="flex justify-between">
|
||||
<div></div>
|
||||
<button class="btn btn-secondary" type="submit">Next</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="00000000000000000000000000000009">
|
||||
<td>
|
||||
<div class="badge badge-lg badge-outline badge-error"><span>return</span></div>
|
||||
</td>
|
||||
<td>Return - 1</td>
|
||||
<td>
|
||||
<div class="grid grid-cols-1 gap-2"><span>10</span><span>20</span><span>5</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="grid grid-cols-3 gap-2 min-w-[220px]">
|
||||
<span>5-a</span>
|
||||
<div class="flex justify-center"><span>10</span></div>
|
||||
<div class="flex justify-end"><span>1</span></div>
|
||||
6-a<span></span>
|
||||
<div class="flex justify-center"><span>15</span></div>
|
||||
<div class="flex justify-end"><span>1</span></div>
|
||||
7-a<span></span>
|
||||
<div class="flex justify-center"><span>20</span></div>
|
||||
<div class="flex justify-end"><span>1</span></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-end mx-auto space-x-4">
|
||||
<div class="badge badge-lg badge-outline badge-primary badge-lg pt-2"><span>80</span></div>
|
||||
<div class="flex justify-end -mt-2">
|
||||
<div class="join">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/effective-lengths/00000000-0000-0000-0000-000000000009" hx-confirm="Are you sure?" hx-target="#00000000000000000000000000000009" hx-swap="outerHTML"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="equivalentLengthForm_00000000000000000000000000000009.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="equivalentLengthForm_00000000000000000000000000000009" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="equivalentLengthForm_00000000000000000000000000000009.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-2xl font-bold">Effective Length</h1>
|
||||
<div id="formStep_equivalentLengthForm_00000000000000000000000000000009" class="mt-4">
|
||||
<form class="space-y-4" hx-post="/projects/00000000-0000-0000-0000-000000000000/effective-lengths/stepOne" hx-target="#formStep_equivalentLengthForm_00000000000000000000000000000009" hx-swap="innerHTML">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000009">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" value="Return - 1" required autofocus>
|
||||
Type</label><label class="select w-full"><span class="label"></span>
|
||||
<select name="type" id="type">
|
||||
<option value="return" selected>Return</option>
|
||||
<option value="supply">Supply</option>
|
||||
</select></label>
|
||||
<div class="flex justify-between">
|
||||
<div></div>
|
||||
<button class="btn btn-secondary" type="submit">Next</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-side is-drawer-close:overflow-visible grow">
|
||||
<label for="my-drawer-1" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<div class="flex grow h-full flex-col items-start bg-base-300 text-base-content
|
||||
is-drawer-close:min-w-[80px] is-drawer-open:max-w-[300px]">
|
||||
<ul class="w-full grow">
|
||||
<li class="flex w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-icon lucide-map-pin"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Project</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-door-closed-icon lucide-door-closed"><path d="M10 12h.01"/><path d="M18 20V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"/><path d="M2 20h20"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Rooms</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="flex w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/equipment" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-fan-icon lucide-fan"><path d="M10.827 16.379a6.082 6.082 0 0 1-8.618-7.002l5.412 1.45a6.082 6.082 0 0 1 7.002-8.618l-1.45 5.412a6.082 6.082 0 0 1 8.618 7.002l-5.412-1.45a6.082 6.082 0 0 1-7.002 8.618l1.45-5.412Z"/><path d="M12 12v.01"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Equipment</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/effective-lengths" hx-push-url="true" hx-target="body" hx-swap="outerHTML" data-active="true">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ruler-dimension-line-icon lucide-ruler-dimension-line"><path d="M10 15v-3"/><path d="M14 15v-3"/><path d="M18 15v-3"/><path d="M2 8V4"/><path d="M22 6H2"/><path d="M22 8V4"/><path d="M6 15v-3"/><rect x="2" y="12" width="20" height="8" rx="2"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>T.E.L.</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/friction-rate" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-function-icon lucide-square-function"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"/><path d="M9 11.2h5.7"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Friction Rate</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/duct-sizing" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wind-icon lucide-wind"><path d="M12.8 19.6A2 2 0 1 0 14 16H2"/><path d="M17.5 8a2.5 2.5 0 1 1 2 4H2"/><path d="M9.8 4.4A2 2 0 1 1 11 8H2"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Duct Sizes</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div class="bottom-0 left-0 bg-error">
|
||||
<footer class="footer sm:footer-horizontal footer-center
|
||||
bg-base-300 text-base-content p-4">
|
||||
<aside>
|
||||
<p>Copyright © 2026 - All rights reserved by Michael Housh</p>
|
||||
</aside>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,442 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Duct Calc</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="ductcalc.com" name="og:site_name">
|
||||
<meta content="Duct Calc" name="og:title">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="description">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="og:description">
|
||||
<meta content="/images/mand_logo.png" name="og:image">
|
||||
<meta content="/images/mand_logo.png" name="twitter:image">
|
||||
<meta content="Duct Calc" name="twitter:image:alt">
|
||||
<meta content="summary_large_image" name="twitter:card">
|
||||
<meta content="1536" name="og:image:width">
|
||||
<meta content="1024" name="og:image:height">
|
||||
<meta content="duct, hvac, duct-design, duct design, manual-d, manual d, design" name="keywords">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<link rel="stylesheet" href="/css/output.css">
|
||||
<link rel="icon" href="/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="/images/favicon-32x32.png" type="image/png">
|
||||
<link rel="icon" href="/images/favicon-16x16.png" type="image/png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<script src="https://unpkg.com/htmx-remove@latest" crossorigin="anonymous" integrity="sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex flex-col min-h-screen min-w-full justify-between">
|
||||
<main class="flex flex-col min-h-screen min-w-full grow mb-auto">
|
||||
<div class="drawer lg:drawer-open h-full">
|
||||
<input id="my-drawer-1" type="checkbox" class="drawer-toggle">
|
||||
<div class="drawer-content overflow-auto">
|
||||
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
|
||||
<div class="flex flex-1 space-x-4 items-center">
|
||||
<div class="tooltip tooltip-right" data-tip="Open sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div>
|
||||
<div class="tooltip tooltip-right" data-tip="Home">
|
||||
<a class="flex w-fit h-fit text-xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
|
||||
<img src="/images/mand_logo_sm.webp">
|
||||
Duct Calc<span></span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<div class="tooltip tooltip-left" data-tip="Profile"><a href="/profile" class="btn btn-square btn-ghost hover:bg-neutral hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-icon lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg></a></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="p-4">
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between bg-secondary border-2 border-primary rounded-sm shadow-sm
|
||||
p-6 w-full">
|
||||
<div class="grid grid-cols-2 px-4 w-full">
|
||||
<h1 class="text-3xl font-bold">Friction Rate</h1>
|
||||
<div class="space-y-2 justify-end font-bold text-lg">
|
||||
<div class="flex space-x-4 justify-end mx-auto">
|
||||
<span>Friction Rate Design Value</span>
|
||||
<div class="badge badge-lg badge-outline badge-info badge-lg font-bold"><span>0.06</span></div>
|
||||
</div>
|
||||
<div class="flex space-x-4 justify-end mx-auto">
|
||||
<span>Available Static Pressure</span>
|
||||
<div class="badge badge-lg badge-outline"><span>0.11</span></div>
|
||||
</div>
|
||||
<div class="flex space-x-4 justify-end mx-auto">
|
||||
<span>Component Pressure Losses</span>
|
||||
<div class="badge badge-lg badge-outline"><span>0.39</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-error font-bold italic col-span-2">
|
||||
<div class="flex space-x-2 hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
||||
<p>Must complete previous sections.</p>
|
||||
</div>
|
||||
<div class="flex space-x-2 hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
||||
<p>No component pressures losses</p>
|
||||
</div>
|
||||
<div class="flex space-x-2 hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
||||
<p class="block">
|
||||
Calculated friction rate is below 0.02. The fan may not deliver the required CFM.
|
||||
<br>
|
||||
* Increase the blower speed
|
||||
<br>
|
||||
* Increase the blower size
|
||||
<br>
|
||||
* Decrease the Total Effective Length (TEL)
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex space-x-2 hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
||||
<p class="block">
|
||||
Calculated friction rate is above 0.18. The fan may deliver too many CFM.
|
||||
<br>
|
||||
* Decrease the blower speed
|
||||
<br>
|
||||
* Decreae the blower size
|
||||
<br>
|
||||
* Increase the Total Effective Length (TEL)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between px-4">
|
||||
<h1 class="text-2xl font-bold">Component Pressure Losses</h1>
|
||||
<div class="tooltip tooltip-left" data-tip="Add component loss">
|
||||
<button type="button" class="btn btn-primary text-2xl me-2" onclick="componentLossForm.showModal()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-plus-icon lucide-circle-plus"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/><path d="M12 8v8"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr class="text-xl font-bold">
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
<th class="min-w-[200px]"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="text-lg">
|
||||
<td>evaporator-coil</td>
|
||||
<td><span>0.2</span></td>
|
||||
<td>
|
||||
<div class="flex join items-end justify-end mx-auto">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/component-loss/00000000-0000-0000-0000-00000000000A" hx-target="body" hx-swap="outerHTML" hx-confirm="Are your sure?"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="componentLossForm_0000000000000000000000000000000A.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="componentLossForm_0000000000000000000000000000000A" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="componentLossForm_0000000000000000000000000000000A.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-2xl font-bold">Component Loss</h1>
|
||||
<form class="space-y-4 p-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/component-loss/00000000-0000-0000-0000-00000000000A" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-00000000000A">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" value="evaporator-coil" placeholder="Name" required autofocus>
|
||||
Value</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="value" type="number" value="0.2" placeholder="0.2" min="0.03" max="1.0" step="0.01" required></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="text-lg">
|
||||
<td>filter</td>
|
||||
<td><span>0.1</span></td>
|
||||
<td>
|
||||
<div class="flex join items-end justify-end mx-auto">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/component-loss/00000000-0000-0000-0000-00000000000B" hx-target="body" hx-swap="outerHTML" hx-confirm="Are your sure?"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="componentLossForm_0000000000000000000000000000000B.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="componentLossForm_0000000000000000000000000000000B" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="componentLossForm_0000000000000000000000000000000B.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-2xl font-bold">Component Loss</h1>
|
||||
<form class="space-y-4 p-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/component-loss/00000000-0000-0000-0000-00000000000B" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-00000000000B">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" value="filter" placeholder="Name" required autofocus>
|
||||
Value</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="value" type="number" value="0.1" placeholder="0.2" min="0.03" max="1.0" step="0.01" required></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="text-lg">
|
||||
<td>supply-outlet</td>
|
||||
<td><span>0.03</span></td>
|
||||
<td>
|
||||
<div class="flex join items-end justify-end mx-auto">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/component-loss/00000000-0000-0000-0000-00000000000C" hx-target="body" hx-swap="outerHTML" hx-confirm="Are your sure?"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="componentLossForm_0000000000000000000000000000000C.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="componentLossForm_0000000000000000000000000000000C" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="componentLossForm_0000000000000000000000000000000C.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-2xl font-bold">Component Loss</h1>
|
||||
<form class="space-y-4 p-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/component-loss/00000000-0000-0000-0000-00000000000C" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-00000000000C">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" value="supply-outlet" placeholder="Name" required autofocus>
|
||||
Value</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="value" type="number" value="0.03" placeholder="0.2" min="0.03" max="1.0" step="0.01" required></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="text-lg">
|
||||
<td>return-grille</td>
|
||||
<td><span>0.03</span></td>
|
||||
<td>
|
||||
<div class="flex join items-end justify-end mx-auto">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/component-loss/00000000-0000-0000-0000-00000000000D" hx-target="body" hx-swap="outerHTML" hx-confirm="Are your sure?"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="componentLossForm_0000000000000000000000000000000D.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="componentLossForm_0000000000000000000000000000000D" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="componentLossForm_0000000000000000000000000000000D.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-2xl font-bold">Component Loss</h1>
|
||||
<form class="space-y-4 p-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/component-loss/00000000-0000-0000-0000-00000000000D" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-00000000000D">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" value="return-grille" placeholder="Name" required autofocus>
|
||||
Value</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="value" type="number" value="0.03" placeholder="0.2" min="0.03" max="1.0" step="0.01" required></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="text-lg">
|
||||
<td>balancing-damper</td>
|
||||
<td><span>0.03</span></td>
|
||||
<td>
|
||||
<div class="flex join items-end justify-end mx-auto">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Delete">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000/component-loss/00000000-0000-0000-0000-00000000000E" hx-target="body" hx-swap="outerHTML" hx-confirm="Are your sure?"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Edit">
|
||||
<button class="btn join-item btn-ghost" type="button" onclick="componentLossForm_0000000000000000000000000000000E.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="componentLossForm_0000000000000000000000000000000E" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="componentLossForm_0000000000000000000000000000000E.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-2xl font-bold">Component Loss</h1>
|
||||
<form class="space-y-4 p-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/component-loss/00000000-0000-0000-0000-00000000000E" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-00000000000E">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" value="balancing-damper" placeholder="Name" required autofocus>
|
||||
Value</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="value" type="number" value="0.03" placeholder="0.2" min="0.03" max="1.0" step="0.01" required></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<dialog id="componentLossForm" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="componentLossForm.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-2xl font-bold">Component Loss</h1>
|
||||
<form class="space-y-4 p-4" hx-post="/projects/00000000-0000-0000-0000-000000000000/component-loss" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" value="" placeholder="Name" required autofocus>
|
||||
Value</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="value" type="number" value="" placeholder="0.2" min="0.03" max="1.0" step="0.01" required></label>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-side is-drawer-close:overflow-visible grow">
|
||||
<label for="my-drawer-1" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<div class="flex grow h-full flex-col items-start bg-base-300 text-base-content
|
||||
is-drawer-close:min-w-[80px] is-drawer-open:max-w-[300px]">
|
||||
<ul class="w-full grow">
|
||||
<li class="flex w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-icon lucide-map-pin"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Project</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-door-closed-icon lucide-door-closed"><path d="M10 12h.01"/><path d="M18 20V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"/><path d="M2 20h20"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Rooms</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="flex w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/equipment" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-fan-icon lucide-fan"><path d="M10.827 16.379a6.082 6.082 0 0 1-8.618-7.002l5.412 1.45a6.082 6.082 0 0 1 7.002-8.618l-1.45 5.412a6.082 6.082 0 0 1 8.618 7.002l-5.412-1.45a6.082 6.082 0 0 1-7.002 8.618l1.45-5.412Z"/><path d="M12 12v.01"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Equipment</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/effective-lengths" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ruler-dimension-line-icon lucide-ruler-dimension-line"><path d="M10 15v-3"/><path d="M14 15v-3"/><path d="M18 15v-3"/><path d="M2 8V4"/><path d="M22 6H2"/><path d="M22 8V4"/><path d="M6 15v-3"/><rect x="2" y="12" width="20" height="8" rx="2"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>T.E.L.</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/friction-rate" hx-push-url="true" hx-target="body" hx-swap="outerHTML" data-active="true">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center is-drawer-close:text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-function-icon lucide-square-function"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"/><path d="M9 11.2h5.7"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Friction Rate</span></div>
|
||||
</div>
|
||||
<div class="flex grow justify-end items-end is-drawer-close:hidden text-green-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<button class="w-full gap-1 py-2 border-b-1 border-gray-200
|
||||
hover:bg-neutral data-active:bg-neutral
|
||||
hover:text-white data-active:text-white
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1" hx-get="/projects/00000000-0000-0000-0000-000000000000/duct-sizing" hx-push-url="true" hx-target="body" hx-swap="outerHTML">
|
||||
<div class="w-full p-2 gap-1
|
||||
is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:grid-cols-1">
|
||||
<div class="items-center
|
||||
is-drawer-open:justify-start is-drawer-open:flex is-drawer-open:space-x-4
|
||||
is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2">
|
||||
<div class="flex items-center justify-center"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wind-icon lucide-wind"><path d="M12.8 19.6A2 2 0 1 0 14 16H2"/><path d="M17.5 8a2.5 2.5 0 1 1 2 4H2"/><path d="M9.8 4.4A2 2 0 1 1 11 8H2"/></svg></div>
|
||||
<div class="flex items-center justify-center"><span>Duct Sizes</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div class="bottom-0 left-0 bg-error">
|
||||
<footer class="footer sm:footer-horizontal footer-center
|
||||
bg-base-300 text-base-content p-4">
|
||||
<aside>
|
||||
<p>Copyright © 2026 - All rights reserved by Michael Housh</p>
|
||||
</aside>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Duct Calc</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="ductcalc.com" name="og:site_name">
|
||||
<meta content="Duct Calc" name="og:title">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="description">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="og:description">
|
||||
<meta content="/images/mand_logo.png" name="og:image">
|
||||
<meta content="/images/mand_logo.png" name="twitter:image">
|
||||
<meta content="Duct Calc" name="twitter:image:alt">
|
||||
<meta content="summary_large_image" name="twitter:card">
|
||||
<meta content="1536" name="og:image:width">
|
||||
<meta content="1024" name="og:image:height">
|
||||
<meta content="duct, hvac, duct-design, duct design, manual-d, manual d, design" name="keywords">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<link rel="stylesheet" href="/css/output.css">
|
||||
<link rel="icon" href="/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="/images/favicon-32x32.png" type="image/png">
|
||||
<link rel="icon" href="/images/favicon-16x16.png" type="image/png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<script src="https://unpkg.com/htmx-remove@latest" crossorigin="anonymous" integrity="sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex flex-col min-h-screen min-w-full justify-between">
|
||||
<main class="flex flex-col min-h-screen min-w-full grow mb-auto">
|
||||
<div>
|
||||
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
|
||||
<div class="flex flex-1 space-x-4 items-center">
|
||||
<div class="tooltip tooltip-right" data-tip="Home">
|
||||
<a class="flex w-fit h-fit text-xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
|
||||
<img src="/images/mand_logo_sm.webp">
|
||||
Duct Calc<span></span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<div class="tooltip tooltip-left" data-tip="Profile"><a href="/profile" class="btn btn-square btn-ghost hover:bg-neutral hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-icon lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg></a></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="m-6">
|
||||
<div class="flex justify-between bg-secondary border-2 border-primary rounded-sm shadow-sm
|
||||
p-6 w-full pb-6">
|
||||
<h1 class="text-3xl font-bold">Projects</h1>
|
||||
<div class="tooltip tooltip-left" data-tip="Add project">
|
||||
<button type="button" class="btn btn-primary" onclick="projectForm.showModal()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-plus-icon lucide-circle-plus"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/><path d="M12 8v8"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><span class="text-lg label font-bold">Date</span></th>
|
||||
<th><span class="text-lg label font-bold">Name</span></th>
|
||||
<th><span class="text-lg label font-bold">Address</span></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr id="00000000-0000-0000-0000-000000000000">
|
||||
<td><span>02/13/2009</span></td>
|
||||
<td>Testy McTestface</td>
|
||||
<td>1234 Sesame Street</td>
|
||||
<td>
|
||||
<div class="flex justify-end space-x-6">
|
||||
<div class="join">
|
||||
<div class="tooltip tooltip-left" data-tip="Delete project">
|
||||
<button type="button" class="btn btn-error join-item btn-ghost" hx-delete="/projects/00000000-0000-0000-0000-000000000000" hx-confirm="Are you sure?" hx-target="closest tr"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-left" data-tip="View project"><a class="join-item btn btn-success btn-ghost" href="/projects/00000000-0000-0000-0000-000000000000/rooms"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right-icon lucide-chevron-right"><path d="m9 18 6-6-6-6"/></svg></a></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<dialog id="projectForm" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="projectForm.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6 ps-2">Project</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-post="/projects" hx-target="body" hx-swap="outerHTML">
|
||||
<label class="input w-full"><span class="label">Name</span>
|
||||
<input name="name" type="text" value="" placeholder="Project Name" required autofocus>
|
||||
Address</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="streetAddress" type="text" value="" placeholder="Street Address" required>
|
||||
City</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="city" type="text" value="" placeholder="City" required>
|
||||
State</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="state" type="text" value="" placeholder="State" required>
|
||||
Zip</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="zipCode" type="text" value="" placeholder="Zip Code" required></label>
|
||||
<button class="btn btn-secondary btn-block my-6" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</main>
|
||||
<div class="bottom-0 left-0 bg-error">
|
||||
<footer class="footer sm:footer-horizontal footer-center
|
||||
bg-base-300 text-base-content p-4">
|
||||
<aside>
|
||||
<p>Copyright © 2026 - All rights reserved by Michael Housh</p>
|
||||
</aside>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Duct Calc</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="ductcalc.com" name="og:site_name">
|
||||
<meta content="Duct Calc" name="og:title">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="description">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="og:description">
|
||||
<meta content="/images/mand_logo.png" name="og:image">
|
||||
<meta content="/images/mand_logo.png" name="twitter:image">
|
||||
<meta content="Duct Calc" name="twitter:image:alt">
|
||||
<meta content="summary_large_image" name="twitter:card">
|
||||
<meta content="1536" name="og:image:width">
|
||||
<meta content="1024" name="og:image:height">
|
||||
<meta content="duct, hvac, duct-design, duct design, manual-d, manual d, design" name="keywords">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<link rel="stylesheet" href="/css/output.css">
|
||||
<link rel="icon" href="/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="/images/favicon-32x32.png" type="image/png">
|
||||
<link rel="icon" href="/images/favicon-16x16.png" type="image/png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<script src="https://unpkg.com/htmx-remove@latest" crossorigin="anonymous" integrity="sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex flex-col min-h-screen min-w-full justify-between">
|
||||
<main class="flex flex-col min-h-screen min-w-full grow mb-auto">
|
||||
<dialog id="loginForm" class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h1 class="text-2xl font-bold mb-6">Login</h1>
|
||||
<form method="post" class="space-y-4">
|
||||
<div>
|
||||
<label class="input validator w-full"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
|
||||
</g>
|
||||
</svg>
|
||||
<input type="email" placeholder="Email" required name="email" id="email" autofocus></label>
|
||||
<div class="validator-hint hidden">Enter valid email address.</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input validator w-full"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"
|
||||
></path>
|
||||
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
<input type="password" placeholder="Password" required pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}" minlength="8" name="password" id="password"></label>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<button class="btn btn-secondary mt-4 w-full">Login</button>
|
||||
</div>
|
||||
<div class="flex justify-center"><a class="btn btn-link" href="/signup">Sign Up</a></div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</main>
|
||||
<div class="bottom-0 left-0 bg-error"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,159 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Duct Calc</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="ductcalc.com" name="og:site_name">
|
||||
<meta content="Duct Calc" name="og:title">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="description">
|
||||
<meta content="Duct sizing based on ACCA, Manual-D." name="og:description">
|
||||
<meta content="/images/mand_logo.png" name="og:image">
|
||||
<meta content="/images/mand_logo.png" name="twitter:image">
|
||||
<meta content="Duct Calc" name="twitter:image:alt">
|
||||
<meta content="summary_large_image" name="twitter:card">
|
||||
<meta content="1536" name="og:image:width">
|
||||
<meta content="1024" name="og:image:height">
|
||||
<meta content="duct, hvac, duct-design, duct design, manual-d, manual d, design" name="keywords">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<link rel="stylesheet" href="/css/output.css">
|
||||
<link rel="icon" href="/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="/images/favicon-32x32.png" type="image/png">
|
||||
<link rel="icon" href="/images/favicon-16x16.png" type="image/png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<script src="https://unpkg.com/htmx-remove@latest" crossorigin="anonymous" integrity="sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex flex-col min-h-screen min-w-full justify-between">
|
||||
<main class="flex flex-col min-h-screen min-w-full grow mb-auto">
|
||||
<div>
|
||||
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
|
||||
<div class="flex flex-1 space-x-4 items-center">
|
||||
<div class="tooltip tooltip-right" data-tip="Home">
|
||||
<a class="flex w-fit h-fit text-xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
|
||||
<img src="/images/mand_logo_sm.webp">
|
||||
Duct Calc<span></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="p-4">
|
||||
<div class="flex justify-between">
|
||||
<h1 class="text-2xl font-bold">Account</h1>
|
||||
<button class="btn" type="button" onclick="userProfileForm_00000000000000000000000000000001.showModal()">
|
||||
<div class="flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<table class="table table-zebra border rounded-lg">
|
||||
<tr>
|
||||
<td><span class="text-lg label font-bold">Name</span></td>
|
||||
<td>Testy McTestface</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="text-lg label font-bold">Company</span></td>
|
||||
<td>Acme Co.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="text-lg label font-bold">Street Address</span></td>
|
||||
<td>1234 Sesame St</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="text-lg label font-bold">City</span></td>
|
||||
<td>Monroe</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="text-lg label font-bold">State</span></td>
|
||||
<td>OH</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="text-lg label font-bold">Zip Code</span></td>
|
||||
<td>55555</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="text-lg label font-bold">Theme</span></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
<dialog id="userProfileForm_00000000000000000000000000000001" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="userProfileForm_00000000000000000000000000000001.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-xl font-bold pb-6">Profile</h1>
|
||||
<form class="grid grid-cols-1 gap-4 p-4" hx-patch="/profile/00000000-0000-0000-0000-000000000001" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000001">
|
||||
<input class="hidden" name="userID" value="00000000-0000-0000-0000-000000000000">
|
||||
First Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="firstName" value="Testy" required autofocus>
|
||||
Last Name</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="lastName" value="McTestface" required>
|
||||
Company</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="companyName" value="Acme Co." required>
|
||||
Address</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="streetAddress" value="1234 Sesame St" required>
|
||||
City</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="city" value="Monroe" required>
|
||||
State</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="state" value="OH" required>
|
||||
Zip</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="zipCode" value="55555" required></label>
|
||||
<div class="dropdown dropdown-top">
|
||||
<div class="input btn m-1 w-full" tabindex="0" role="button">Theme<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg></div>
|
||||
<ul tabindex="-1" class="dropdown-content bg-base-300 rounded-box z-1 p-2 shadow-2xl">
|
||||
<li>
|
||||
<input type="radio" name="theme" class="theme-controller w-full btn btn-sm btn-block btn-ghost justify-start" aria-label="Default" value="default">
|
||||
</li>
|
||||
<li><span class="text-sm font-bold text-gray-400">Light</span></li>
|
||||
<li>
|
||||
<input type="radio" name="theme" class="theme-controller w-full btn btn-sm btn-block btn-ghost justify-start" aria-label="Cupcake" value="cupcake">
|
||||
</li>
|
||||
<li>
|
||||
<input type="radio" name="theme" class="theme-controller w-full btn btn-sm btn-block btn-ghost justify-start" aria-label="Light" value="light">
|
||||
</li>
|
||||
<li>
|
||||
<input type="radio" name="theme" class="theme-controller w-full btn btn-sm btn-block btn-ghost justify-start" aria-label="Nord" value="nord">
|
||||
</li>
|
||||
<li>
|
||||
<input type="radio" name="theme" class="theme-controller w-full btn btn-sm btn-block btn-ghost justify-start" aria-label="Retro" value="retro">
|
||||
</li>
|
||||
<li><span class="text-sm font-bold text-gray-400">Dark</span></li>
|
||||
<li>
|
||||
<input type="radio" name="theme" class="theme-controller w-full btn btn-sm btn-block btn-ghost justify-start" aria-label="Aqua" value="aqua">
|
||||
</li>
|
||||
<li>
|
||||
<input type="radio" name="theme" class="theme-controller w-full btn btn-sm btn-block btn-ghost justify-start" aria-label="Cyberpunk" value="cyberpunk">
|
||||
</li>
|
||||
<li>
|
||||
<input type="radio" name="theme" class="theme-controller w-full btn btn-sm btn-block btn-ghost justify-start" aria-label="Dark" value="dark">
|
||||
</li>
|
||||
<li>
|
||||
<input type="radio" name="theme" class="theme-controller w-full btn btn-sm btn-block btn-ghost justify-start" aria-label="Dracula" value="dracula">
|
||||
</li>
|
||||
<li>
|
||||
<input type="radio" name="theme" class="theme-controller w-full btn btn-sm btn-block btn-ghost justify-start" aria-label="Night" value="night">
|
||||
</li>
|
||||
<li>
|
||||
<input type="radio" name="theme" class="theme-controller w-full btn btn-sm btn-block btn-ghost justify-start" aria-label="Synthwave" value="synthwave">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-block" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div class="bottom-0 left-0 bg-error">
|
||||
<footer class="footer sm:footer-horizontal footer-center
|
||||
bg-base-300 text-base-content p-4">
|
||||
<aside>
|
||||
<p>Copyright © 2026 - All rights reserved by Michael Housh</p>
|
||||
</aside>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,6 +17,7 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
|
||||
npm \
|
||||
build-essential \
|
||||
curl \
|
||||
wkhtmltopdf \
|
||||
&& rm -r /var/lib/apt/lists/*
|
||||
|
||||
# Set up a build area
|
||||
|
||||
359
ductsizes.json
Normal file
359
ductsizes.json
Normal file
@@ -0,0 +1,359 @@
|
||||
{
|
||||
"trunks": [
|
||||
{
|
||||
"trunk": {
|
||||
"projectID": "E796C96C-F527-4753-A00A-EBCF25630663",
|
||||
"type": "supply",
|
||||
"name": "West",
|
||||
"id": "AFBC5264-8129-4383-97D8-F44CF5258A53",
|
||||
"rooms": [
|
||||
{
|
||||
"room": {
|
||||
"coolingTotal": 4567,
|
||||
"id": "FA2F107F-5DF0-4D69-B231-DE3C6EF64134",
|
||||
"registerCount": 3,
|
||||
"heatingLoad": 9876,
|
||||
"updatedAt": "2026-01-17T16:51:33Z",
|
||||
"createdAt": "2026-01-17T16:51:33Z",
|
||||
"projectID": "E796C96C-F527-4753-A00A-EBCF25630663",
|
||||
"name": "Kitchen"
|
||||
},
|
||||
"registers": [1, 2, 3]
|
||||
},
|
||||
{
|
||||
"room": {
|
||||
"coolingTotal": 4567,
|
||||
"id": "9A01CEE8-6A4B-4299-9353-58AFAD042903",
|
||||
"registerCount": 3,
|
||||
"heatingLoad": 9876,
|
||||
"updatedAt": "2026-01-17T16:51:21Z",
|
||||
"createdAt": "2026-01-17T16:51:21Z",
|
||||
"name": "Master",
|
||||
"projectID": "E796C96C-F527-4753-A00A-EBCF25630663"
|
||||
},
|
||||
"registers": [1, 2, 3]
|
||||
}
|
||||
],
|
||||
"height": 8
|
||||
},
|
||||
"ductSize": {
|
||||
"height": 8,
|
||||
"roundSize": 12.3660960368007,
|
||||
"width": 19,
|
||||
"velocity": 737,
|
||||
"finalSize": 14,
|
||||
"flexSize": 14,
|
||||
"designCFM": {
|
||||
"cooling": {
|
||||
"_0": 787.278055507671
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trunk": {
|
||||
"projectID": "E796C96C-F527-4753-A00A-EBCF25630663",
|
||||
"name": "East",
|
||||
"id": "C6A21594-0A28-4A20-BC15-628502010201",
|
||||
"type": "supply",
|
||||
"height": 8,
|
||||
"rooms": [
|
||||
{
|
||||
"room": {
|
||||
"coolingTotal": 1234,
|
||||
"id": "2420F9C7-0FCA-4E92-BAC9-8A054CB3201B",
|
||||
"registerCount": 1,
|
||||
"heatingLoad": 4567,
|
||||
"updatedAt": "2026-01-17T16:51:10Z",
|
||||
"createdAt": "2026-01-17T16:51:10Z",
|
||||
"name": "Entry",
|
||||
"projectID": "E796C96C-F527-4753-A00A-EBCF25630663"
|
||||
},
|
||||
"registers": [1]
|
||||
},
|
||||
{
|
||||
"room": {
|
||||
"coolingTotal": 1234,
|
||||
"id": "449FE324-2ACF-4C12-83A3-EB86FD45A991",
|
||||
"registerCount": 2,
|
||||
"heatingLoad": 4567,
|
||||
"updatedAt": "2026-01-17T16:51:02Z",
|
||||
"createdAt": "2026-01-17T16:51:02Z",
|
||||
"projectID": "E796C96C-F527-4753-A00A-EBCF25630663",
|
||||
"name": "Bed-1"
|
||||
},
|
||||
"registers": [1, 2]
|
||||
}
|
||||
]
|
||||
},
|
||||
"ductSize": {
|
||||
"height": 8,
|
||||
"roundSize": 8.39504804369037,
|
||||
"width": 8,
|
||||
"velocity": 643,
|
||||
"flexSize": 10,
|
||||
"finalSize": 9,
|
||||
"designCFM": {
|
||||
"heating": {
|
||||
"_0": 284.587689538185
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trunk": {
|
||||
"name": "Main",
|
||||
"projectID": "E796C96C-F527-4753-A00A-EBCF25630663",
|
||||
"id": "C65863A0-D2EE-4D90-8C71-B1FD2454A8DF",
|
||||
"type": "return",
|
||||
"rooms": [
|
||||
{
|
||||
"registers": [1, 2, 3],
|
||||
"room": {
|
||||
"coolingTotal": 4567,
|
||||
"id": "FA2F107F-5DF0-4D69-B231-DE3C6EF64134",
|
||||
"registerCount": 3,
|
||||
"heatingLoad": 9876,
|
||||
"updatedAt": "2026-01-17T16:51:33Z",
|
||||
"createdAt": "2026-01-17T16:51:33Z",
|
||||
"name": "Kitchen",
|
||||
"projectID": "E796C96C-F527-4753-A00A-EBCF25630663"
|
||||
}
|
||||
},
|
||||
{
|
||||
"registers": [1, 2, 3],
|
||||
"room": {
|
||||
"coolingTotal": 4567,
|
||||
"id": "9A01CEE8-6A4B-4299-9353-58AFAD042903",
|
||||
"registerCount": 3,
|
||||
"heatingLoad": 9876,
|
||||
"updatedAt": "2026-01-17T16:51:21Z",
|
||||
"createdAt": "2026-01-17T16:51:21Z",
|
||||
"name": "Master",
|
||||
"projectID": "E796C96C-F527-4753-A00A-EBCF25630663"
|
||||
}
|
||||
},
|
||||
{
|
||||
"registers": [1],
|
||||
"room": {
|
||||
"coolingTotal": 1234,
|
||||
"id": "2420F9C7-0FCA-4E92-BAC9-8A054CB3201B",
|
||||
"registerCount": 1,
|
||||
"heatingLoad": 4567,
|
||||
"updatedAt": "2026-01-17T16:51:10Z",
|
||||
"createdAt": "2026-01-17T16:51:10Z",
|
||||
"name": "Entry",
|
||||
"projectID": "E796C96C-F527-4753-A00A-EBCF25630663"
|
||||
}
|
||||
},
|
||||
{
|
||||
"registers": [1, 2],
|
||||
"room": {
|
||||
"coolingTotal": 1234,
|
||||
"id": "449FE324-2ACF-4C12-83A3-EB86FD45A991",
|
||||
"registerCount": 2,
|
||||
"heatingLoad": 4567,
|
||||
"updatedAt": "2026-01-17T16:51:02Z",
|
||||
"createdAt": "2026-01-17T16:51:02Z",
|
||||
"name": "Bed-1",
|
||||
"projectID": "E796C96C-F527-4753-A00A-EBCF25630663"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ductSize": {
|
||||
"designCFM": {
|
||||
"cooling": {
|
||||
"_0": 1000
|
||||
}
|
||||
},
|
||||
"roundSize": 13.539327773393,
|
||||
"velocity": 935,
|
||||
"finalSize": 14,
|
||||
"flexSize": 16
|
||||
}
|
||||
}
|
||||
],
|
||||
"rooms": [
|
||||
{
|
||||
"ductSize": {
|
||||
"velocity": 521,
|
||||
"flexSize": 6,
|
||||
"designCFM": {
|
||||
"heating": {
|
||||
"_0": 71.1469223845461
|
||||
}
|
||||
},
|
||||
"roundSize": 4.95724506597333,
|
||||
"finalSize": 5
|
||||
},
|
||||
"heatingCFM": 71.1469223845461,
|
||||
"roomRegister": 1,
|
||||
"heatingLoad": 2283.5,
|
||||
"roomName": "Bed-1-1",
|
||||
"roomID": "449FE324-2ACF-4C12-83A3-EB86FD45A991",
|
||||
"coolingLoad": 512.11,
|
||||
"coolingCFM": 53.1804861230822
|
||||
},
|
||||
{
|
||||
"ductSize": {
|
||||
"velocity": 521,
|
||||
"flexSize": 6,
|
||||
"designCFM": {
|
||||
"heating": {
|
||||
"_0": 71.1469223845461
|
||||
}
|
||||
},
|
||||
"roundSize": 4.95724506597333,
|
||||
"finalSize": 5
|
||||
},
|
||||
"heatingCFM": 71.1469223845461,
|
||||
"roomRegister": 2,
|
||||
"heatingLoad": 2283.5,
|
||||
"roomName": "Bed-1-2",
|
||||
"roomID": "449FE324-2ACF-4C12-83A3-EB86FD45A991",
|
||||
"coolingLoad": 512.11,
|
||||
"coolingCFM": 53.1804861230822
|
||||
},
|
||||
{
|
||||
"ductSize": {
|
||||
"velocity": 532,
|
||||
"flexSize": 7,
|
||||
"designCFM": {
|
||||
"heating": {
|
||||
"_0": 142.293844769092
|
||||
}
|
||||
},
|
||||
"roundSize": 6.4510704920341,
|
||||
"finalSize": 7
|
||||
},
|
||||
"heatingCFM": 142.293844769092,
|
||||
"roomRegister": 1,
|
||||
"heatingLoad": 4567,
|
||||
"roomName": "Entry-1",
|
||||
"roomID": "2420F9C7-0FCA-4E92-BAC9-8A054CB3201B",
|
||||
"coolingLoad": 1024.22,
|
||||
"coolingCFM": 106.360972246164
|
||||
},
|
||||
{
|
||||
"ductSize": {
|
||||
"velocity": 490,
|
||||
"flexSize": 7,
|
||||
"designCFM": {
|
||||
"cooling": {
|
||||
"_0": 131.213009251279
|
||||
}
|
||||
},
|
||||
"roundSize": 6.25641154872314,
|
||||
"finalSize": 7
|
||||
},
|
||||
"heatingCFM": 102.568718410303,
|
||||
"roomRegister": 1,
|
||||
"heatingLoad": 3292,
|
||||
"roomName": "Kitchen-1",
|
||||
"roomID": "FA2F107F-5DF0-4D69-B231-DE3C6EF64134",
|
||||
"coolingLoad": 1263.53666666667,
|
||||
"coolingCFM": 131.213009251279
|
||||
},
|
||||
{
|
||||
"ductSize": {
|
||||
"velocity": 490,
|
||||
"flexSize": 7,
|
||||
"designCFM": {
|
||||
"cooling": {
|
||||
"_0": 131.213009251279
|
||||
}
|
||||
},
|
||||
"roundSize": 6.25641154872314,
|
||||
"finalSize": 7
|
||||
},
|
||||
"heatingCFM": 102.568718410303,
|
||||
"roomRegister": 2,
|
||||
"heatingLoad": 3292,
|
||||
"roomName": "Kitchen-2",
|
||||
"roomID": "FA2F107F-5DF0-4D69-B231-DE3C6EF64134",
|
||||
"coolingLoad": 1263.53666666667,
|
||||
"coolingCFM": 131.213009251279
|
||||
},
|
||||
{
|
||||
"ductSize": {
|
||||
"velocity": 490,
|
||||
"flexSize": 7,
|
||||
"designCFM": {
|
||||
"cooling": {
|
||||
"_0": 131.213009251279
|
||||
}
|
||||
},
|
||||
"roundSize": 6.25641154872314,
|
||||
"finalSize": 7
|
||||
},
|
||||
"heatingCFM": 102.568718410303,
|
||||
"roomRegister": 3,
|
||||
"heatingLoad": 3292,
|
||||
"roomName": "Kitchen-3",
|
||||
"roomID": "FA2F107F-5DF0-4D69-B231-DE3C6EF64134",
|
||||
"coolingLoad": 1263.53666666667,
|
||||
"coolingCFM": 131.213009251279
|
||||
},
|
||||
{
|
||||
"ductSize": {
|
||||
"velocity": 490,
|
||||
"flexSize": 7,
|
||||
"designCFM": {
|
||||
"cooling": {
|
||||
"_0": 131.213009251279
|
||||
}
|
||||
},
|
||||
"roundSize": 6.25641154872314,
|
||||
"finalSize": 7
|
||||
},
|
||||
"heatingCFM": 102.568718410303,
|
||||
"roomRegister": 1,
|
||||
"heatingLoad": 3292,
|
||||
"roomName": "Master-1",
|
||||
"roomID": "9A01CEE8-6A4B-4299-9353-58AFAD042903",
|
||||
"coolingLoad": 1263.53666666667,
|
||||
"coolingCFM": 131.213009251279
|
||||
},
|
||||
{
|
||||
"ductSize": {
|
||||
"velocity": 490,
|
||||
"flexSize": 7,
|
||||
"designCFM": {
|
||||
"cooling": {
|
||||
"_0": 131.213009251279
|
||||
}
|
||||
},
|
||||
"roundSize": 6.25641154872314,
|
||||
"finalSize": 7
|
||||
},
|
||||
"heatingCFM": 102.568718410303,
|
||||
"roomRegister": 2,
|
||||
"heatingLoad": 3292,
|
||||
"roomName": "Master-2",
|
||||
"roomID": "9A01CEE8-6A4B-4299-9353-58AFAD042903",
|
||||
"coolingLoad": 1263.53666666667,
|
||||
"coolingCFM": 131.213009251279
|
||||
},
|
||||
{
|
||||
"ductSize": {
|
||||
"velocity": 490,
|
||||
"flexSize": 7,
|
||||
"designCFM": {
|
||||
"cooling": {
|
||||
"_0": 131.213009251279
|
||||
}
|
||||
},
|
||||
"roundSize": 6.25641154872314,
|
||||
"finalSize": 7
|
||||
},
|
||||
"heatingCFM": 102.568718410303,
|
||||
"roomRegister": 3,
|
||||
"heatingLoad": 3292,
|
||||
"roomName": "Master-3",
|
||||
"roomID": "9A01CEE8-6A4B-4299-9353-58AFAD042903",
|
||||
"coolingLoad": 1263.53666666667,
|
||||
"coolingCFM": 131.213009251279
|
||||
}
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user