Compare commits
18 Commits
432533c940
...
0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
f5afc6e32b
|
|||
|
9709eaaf8e
|
|||
|
4ecd4dba7b
|
|||
|
7471e11bd2
|
|||
|
1b88f81b5f
|
|||
|
86307dfa05
|
|||
|
356e020e3b
|
|||
|
9b5b891744
|
|||
|
658ea9f12e
|
|||
|
7f734e912b
|
|||
|
b5d1f87380
|
|||
|
450791b37e
|
|||
|
71848c607a
|
|||
|
62a82ed674
|
|||
|
dfee50de8e
|
|||
|
f990c4b6db
|
|||
|
930db145a8
|
|||
|
df600a5471
|
65
.gitea/workflows/release.yaml
Normal file
65
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
name: Create and publish a Docker image
|
||||||
|
|
||||||
|
# Configures this workflow to run every time a change is pushed to the branch called `release`.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# branches: ['main']
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Defines two custom environment variables for the workflow. These are used for the Container registry domain,
|
||||||
|
# and a name for the Docker image that this workflow builds.
|
||||||
|
env:
|
||||||
|
REGISTRY: git.housh.dev
|
||||||
|
IMAGE_NAME: ${{ gitea.repository }}
|
||||||
|
|
||||||
|
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
|
||||||
|
jobs:
|
||||||
|
build-and-push-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
attestations: write
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
# Uses the `docker/login-action` action to log in to the Container registry registry using the account
|
||||||
|
# and password that will publish the packages. Once published, the packages are scoped to the account defined here.
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.CONTAINER_TOKEN }}
|
||||||
|
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels
|
||||||
|
# that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a
|
||||||
|
# subsequent step. The `images` value provides the base name for the tags and labels.
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=sha
|
||||||
|
type=raw,value=prod
|
||||||
|
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If
|
||||||
|
# the build succeeds, it pushes the image to GitHub Packages. It uses the `context` parameter to define the build's context
|
||||||
|
# as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)"
|
||||||
|
# in the README of the `docker/build-push-action` repository.
|
||||||
|
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
|
||||||
|
- name: Build and push Docker image
|
||||||
|
id: push
|
||||||
|
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
@@ -20,8 +20,6 @@
|
|||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
--text-sm: 0.875rem;
|
--text-sm: 0.875rem;
|
||||||
--text-sm--line-height: calc(1.25 / 0.875);
|
--text-sm--line-height: calc(1.25 / 0.875);
|
||||||
--text-base: 1rem;
|
|
||||||
--text-base--line-height: calc(1.5 / 1);
|
|
||||||
--text-lg: 1.125rem;
|
--text-lg: 1.125rem;
|
||||||
--text-lg--line-height: calc(1.75 / 1.125);
|
--text-lg--line-height: calc(1.75 / 1.125);
|
||||||
--text-xl: 1.25rem;
|
--text-xl: 1.25rem;
|
||||||
@@ -30,9 +28,8 @@
|
|||||||
--text-2xl--line-height: calc(2 / 1.5);
|
--text-2xl--line-height: calc(2 / 1.5);
|
||||||
--text-3xl: 1.875rem;
|
--text-3xl: 1.875rem;
|
||||||
--text-3xl--line-height: calc(2.25 / 1.875);
|
--text-3xl--line-height: calc(2.25 / 1.875);
|
||||||
--text-4xl: 2.25rem;
|
|
||||||
--text-4xl--line-height: calc(2.5 / 2.25);
|
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
|
--radius-sm: 0.25rem;
|
||||||
--radius-md: 0.375rem;
|
--radius-md: 0.375rem;
|
||||||
--radius-lg: 0.5rem;
|
--radius-lg: 0.5rem;
|
||||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||||
@@ -4227,6 +4224,9 @@
|
|||||||
.top-2 {
|
.top-2 {
|
||||||
top: calc(var(--spacing) * 2);
|
top: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
.top-\[100vh\] {
|
||||||
|
top: 100vh;
|
||||||
|
}
|
||||||
.right-2 {
|
.right-2 {
|
||||||
right: calc(var(--spacing) * 2);
|
right: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@@ -4287,6 +4287,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.bottom-0 {
|
||||||
|
bottom: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
.left-0 {
|
||||||
|
left: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
.join {
|
.join {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@@ -4801,9 +4807,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.col-span-1 {
|
||||||
|
grid-column: span 1 / span 1;
|
||||||
|
}
|
||||||
.col-span-2 {
|
.col-span-2 {
|
||||||
grid-column: span 2 / span 2;
|
grid-column: span 2 / span 2;
|
||||||
}
|
}
|
||||||
|
.col-span-3 {
|
||||||
|
grid-column: span 3 / span 3;
|
||||||
|
}
|
||||||
|
.col-span-5 {
|
||||||
|
grid-column: span 5 / span 5;
|
||||||
|
}
|
||||||
.timeline-end {
|
.timeline-end {
|
||||||
@layer daisyui.l1.l2.l3 {
|
@layer daisyui.l1.l2.l3 {
|
||||||
grid-column-start: 1;
|
grid-column-start: 1;
|
||||||
@@ -5225,9 +5240,6 @@
|
|||||||
.m-1 {
|
.m-1 {
|
||||||
margin: calc(var(--spacing) * 1);
|
margin: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
.m-4 {
|
|
||||||
margin: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
.m-6 {
|
.m-6 {
|
||||||
margin: calc(var(--spacing) * 6);
|
margin: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
@@ -5279,6 +5291,9 @@
|
|||||||
.mx-2 {
|
.mx-2 {
|
||||||
margin-inline: calc(var(--spacing) * 2);
|
margin-inline: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
.mx-4 {
|
||||||
|
margin-inline: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
.mx-auto {
|
.mx-auto {
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
@@ -5368,18 +5383,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.-my-2 {
|
|
||||||
margin-block: calc(var(--spacing) * -2);
|
|
||||||
}
|
|
||||||
.-my-4 {
|
|
||||||
margin-block: calc(var(--spacing) * -4);
|
|
||||||
}
|
|
||||||
.my-1 {
|
.my-1 {
|
||||||
margin-block: calc(var(--spacing) * 1);
|
margin-block: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
.my-1\.5 {
|
.my-1\.5 {
|
||||||
margin-block: calc(var(--spacing) * 1.5);
|
margin-block: calc(var(--spacing) * 1.5);
|
||||||
}
|
}
|
||||||
|
.my-4 {
|
||||||
|
margin-block: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
.my-6 {
|
.my-6 {
|
||||||
margin-block: calc(var(--spacing) * 6);
|
margin-block: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
@@ -5589,12 +5601,18 @@
|
|||||||
border-width: var(--border, 1px) 0 var(--border, 1px) var(--border, 1px);
|
border-width: var(--border, 1px) 0 var(--border, 1px) var(--border, 1px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.me-2 {
|
||||||
|
margin-inline-end: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.me-4 {
|
.me-4 {
|
||||||
margin-inline-end: calc(var(--spacing) * 4);
|
margin-inline-end: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
.me-6 {
|
.me-6 {
|
||||||
margin-inline-end: calc(var(--spacing) * 6);
|
margin-inline-end: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.me-\[140px\] {
|
||||||
|
margin-inline-end: 140px;
|
||||||
|
}
|
||||||
.modal-action {
|
.modal-action {
|
||||||
@layer daisyui.l1.l2.l3 {
|
@layer daisyui.l1.l2.l3 {
|
||||||
margin-top: calc(0.25rem * 6);
|
margin-top: calc(0.25rem * 6);
|
||||||
@@ -5634,11 +5652,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.mt-1 {
|
.-mt-2 {
|
||||||
margin-top: calc(var(--spacing) * 1);
|
margin-top: calc(var(--spacing) * -2);
|
||||||
}
|
|
||||||
.mt-2 {
|
|
||||||
margin-top: calc(var(--spacing) * 2);
|
|
||||||
}
|
}
|
||||||
.mt-4 {
|
.mt-4 {
|
||||||
margin-top: calc(var(--spacing) * 4);
|
margin-top: calc(var(--spacing) * 4);
|
||||||
@@ -5725,12 +5740,18 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.mb-4 {
|
.mb-4 {
|
||||||
margin-bottom: calc(var(--spacing) * 4);
|
margin-bottom: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
.mb-6 {
|
.mb-6 {
|
||||||
margin-bottom: calc(var(--spacing) * 6);
|
margin-bottom: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.mb-auto {
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
.carousel-item {
|
.carousel-item {
|
||||||
@layer daisyui.l1.l2.l3 {
|
@layer daisyui.l1.l2.l3 {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
@@ -6458,6 +6479,9 @@
|
|||||||
.min-h-full {
|
.min-h-full {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
.min-h-screen {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
.btn-wide {
|
.btn-wide {
|
||||||
@layer daisyui.l1.l2 {
|
@layer daisyui.l1.l2 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -6586,12 +6610,36 @@
|
|||||||
width: calc(var(--size-selector, 0.25rem) * 4);
|
width: calc(var(--size-selector, 0.25rem) * 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.w-\[300px\] {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
.w-\[310px\] {
|
||||||
|
width: 310px;
|
||||||
|
}
|
||||||
|
.w-\[330px\] {
|
||||||
|
width: 330px;
|
||||||
|
}
|
||||||
.w-fit {
|
.w-fit {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.min-w-\[100px\] {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
.min-w-\[200px\] {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
.min-w-\[220px\] {
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
.min-w-\[300px\] {
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
.min-w-full {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
.flex-1 {
|
.flex-1 {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
@@ -6803,9 +6851,6 @@
|
|||||||
.flex-wrap {
|
.flex-wrap {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.items-baseline {
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
.items-center {
|
.items-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -6827,6 +6872,9 @@
|
|||||||
.justify-start {
|
.justify-start {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
.justify-items-end {
|
||||||
|
justify-items: end;
|
||||||
|
}
|
||||||
.gap-1 {
|
.gap-1 {
|
||||||
gap: calc(var(--spacing) * 1);
|
gap: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
@@ -6836,6 +6884,23 @@
|
|||||||
.gap-4 {
|
.gap-4 {
|
||||||
gap: calc(var(--spacing) * 4);
|
gap: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.gap-6 {
|
||||||
|
gap: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
|
.space-y-1 {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));
|
||||||
|
margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.space-y-2 {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));
|
||||||
|
margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
}
|
||||||
|
}
|
||||||
.space-y-4 {
|
.space-y-4 {
|
||||||
:where(& > :not(:last-child)) {
|
:where(& > :not(:last-child)) {
|
||||||
--tw-space-y-reverse: 0;
|
--tw-space-y-reverse: 0;
|
||||||
@@ -6871,8 +6936,14 @@
|
|||||||
margin-inline-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)));
|
margin-inline-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overflow-x-auto {
|
.gap-y-2 {
|
||||||
overflow-x: auto;
|
row-gap: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.gap-y-4 {
|
||||||
|
row-gap: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.overflow-auto {
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.timeline-box {
|
.timeline-box {
|
||||||
@layer daisyui.l1.l2.l3 {
|
@layer daisyui.l1.l2.l3 {
|
||||||
@@ -6980,6 +7051,9 @@
|
|||||||
.rounded-selector {
|
.rounded-selector {
|
||||||
border-radius: var(--radius-selector);
|
border-radius: var(--radius-selector);
|
||||||
}
|
}
|
||||||
|
.rounded-sm {
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
.rounded-t-box {
|
.rounded-t-box {
|
||||||
border-top-left-radius: var(--radius-box);
|
border-top-left-radius: var(--radius-box);
|
||||||
border-top-right-radius: var(--radius-box);
|
border-top-right-radius: var(--radius-box);
|
||||||
@@ -7152,6 +7226,10 @@
|
|||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
|
.border-2 {
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
.border-b {
|
.border-b {
|
||||||
border-bottom-style: var(--tw-border-style);
|
border-bottom-style: var(--tw-border-style);
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
@@ -7269,15 +7347,15 @@
|
|||||||
border-color: currentColor;
|
border-color: currentColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.border-error {
|
||||||
|
border-color: var(--color-error);
|
||||||
|
}
|
||||||
.border-gray-200 {
|
.border-gray-200 {
|
||||||
border-color: var(--color-gray-200);
|
border-color: var(--color-gray-200);
|
||||||
}
|
}
|
||||||
.border-primary {
|
.border-primary {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
.border-secondary {
|
|
||||||
border-color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
.menu-active {
|
.menu-active {
|
||||||
:where(:not(ul, details, .menu-title, .btn))& {
|
:where(:not(ul, details, .menu-title, .btn))& {
|
||||||
@layer daisyui.l1.l2 {
|
@layer daisyui.l1.l2 {
|
||||||
@@ -7435,9 +7513,15 @@
|
|||||||
.bg-base-300 {
|
.bg-base-300 {
|
||||||
background-color: var(--color-base-300);
|
background-color: var(--color-base-300);
|
||||||
}
|
}
|
||||||
|
.bg-error {
|
||||||
|
background-color: var(--color-error);
|
||||||
|
}
|
||||||
.bg-red-500 {
|
.bg-red-500 {
|
||||||
background-color: var(--color-red-500);
|
background-color: var(--color-red-500);
|
||||||
}
|
}
|
||||||
|
.bg-secondary {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
}
|
||||||
.bg-white {
|
.bg-white {
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
}
|
}
|
||||||
@@ -7729,6 +7813,9 @@
|
|||||||
.p-4 {
|
.p-4 {
|
||||||
padding: calc(var(--spacing) * 4);
|
padding: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.p-6 {
|
||||||
|
padding: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.menu-title {
|
.menu-title {
|
||||||
@layer daisyui.l1.l2.l3 {
|
@layer daisyui.l1.l2.l3 {
|
||||||
padding-inline: calc(0.25rem * 3);
|
padding-inline: calc(0.25rem * 3);
|
||||||
@@ -7861,12 +7948,6 @@
|
|||||||
.py-2 {
|
.py-2 {
|
||||||
padding-block: calc(var(--spacing) * 2);
|
padding-block: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
.py-4 {
|
|
||||||
padding-block: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
.py-6 {
|
|
||||||
padding-block: calc(var(--spacing) * 6);
|
|
||||||
}
|
|
||||||
.ps-2 {
|
.ps-2 {
|
||||||
padding-inline-start: calc(var(--spacing) * 2);
|
padding-inline-start: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@@ -7884,12 +7965,12 @@
|
|||||||
.pe-2 {
|
.pe-2 {
|
||||||
padding-inline-end: calc(var(--spacing) * 2);
|
padding-inline-end: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
.pt-2 {
|
||||||
|
padding-top: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.pt-6 {
|
.pt-6 {
|
||||||
padding-top: calc(var(--spacing) * 6);
|
padding-top: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
.pb-4 {
|
|
||||||
padding-bottom: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
.pb-6 {
|
.pb-6 {
|
||||||
padding-bottom: calc(var(--spacing) * 6);
|
padding-bottom: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
@@ -7941,14 +8022,6 @@
|
|||||||
font-size: var(--text-3xl);
|
font-size: var(--text-3xl);
|
||||||
line-height: var(--tw-leading, var(--text-3xl--line-height));
|
line-height: var(--tw-leading, var(--text-3xl--line-height));
|
||||||
}
|
}
|
||||||
.text-4xl {
|
|
||||||
font-size: var(--text-4xl);
|
|
||||||
line-height: var(--tw-leading, var(--text-4xl--line-height));
|
|
||||||
}
|
|
||||||
.text-base {
|
|
||||||
font-size: var(--text-base);
|
|
||||||
line-height: var(--tw-leading, var(--text-base--line-height));
|
|
||||||
}
|
|
||||||
.text-lg {
|
.text-lg {
|
||||||
font-size: var(--text-lg);
|
font-size: var(--text-lg);
|
||||||
line-height: var(--tw-leading, var(--text-lg--line-height));
|
line-height: var(--tw-leading, var(--text-lg--line-height));
|
||||||
@@ -8515,9 +8588,6 @@
|
|||||||
color: var(--color-warning);
|
color: var(--color-warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.text-base-100 {
|
|
||||||
color: var(--color-base-100);
|
|
||||||
}
|
|
||||||
.text-base-content {
|
.text-base-content {
|
||||||
color: var(--color-base-content);
|
color: var(--color-base-content);
|
||||||
}
|
}
|
||||||
@@ -8533,21 +8603,9 @@
|
|||||||
.text-info {
|
.text-info {
|
||||||
color: var(--color-info);
|
color: var(--color-info);
|
||||||
}
|
}
|
||||||
.text-neutral {
|
|
||||||
color: var(--color-neutral);
|
|
||||||
}
|
|
||||||
.text-neutral-content {
|
|
||||||
color: var(--color-neutral-content);
|
|
||||||
}
|
|
||||||
.text-primary {
|
.text-primary {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
.text-secondary {
|
|
||||||
color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
.text-secondary-content {
|
|
||||||
color: var(--color-secondary-content);
|
|
||||||
}
|
|
||||||
.text-slate-900 {
|
.text-slate-900 {
|
||||||
color: var(--color-slate-900);
|
color: var(--color-slate-900);
|
||||||
}
|
}
|
||||||
@@ -9434,13 +9492,6 @@
|
|||||||
border-color: var(--color-red-500);
|
border-color: var(--color-red-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hover\:rounded-lg {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hover\:bg-neutral {
|
.hover\:bg-neutral {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@@ -9462,16 +9513,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hover\:btn-success {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
@layer daisyui.l1.l2.l3 {
|
|
||||||
--btn-color: var(--color-success);
|
|
||||||
--btn-fg: var(--color-success-content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.focus\:outline {
|
.focus\:outline {
|
||||||
&:focus {
|
&:focus {
|
||||||
outline-style: var(--tw-outline-style);
|
outline-style: var(--tw-outline-style);
|
||||||
@@ -9498,11 +9539,26 @@
|
|||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sm\:footer-horizontal {
|
||||||
|
@media (width >= 40rem) {
|
||||||
|
@layer daisyui.l1.l2 {
|
||||||
|
grid-auto-flow: column;
|
||||||
|
&.footer-center {
|
||||||
|
grid-auto-flow: row dense;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.md\:grid-cols-2 {
|
.md\:grid-cols-2 {
|
||||||
@media (width >= 48rem) {
|
@media (width >= 48rem) {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.md\:grid-cols-3 {
|
||||||
|
@media (width >= 48rem) {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
.lg\:drawer-open {
|
.lg\:drawer-open {
|
||||||
@media (width >= 64rem) {
|
@media (width >= 64rem) {
|
||||||
@layer daisyui.l1.l2.l3 {
|
@layer daisyui.l1.l2.l3 {
|
||||||
@@ -9556,14 +9612,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.lg\:grid-cols-3 {
|
.lg\:grid-cols-4 {
|
||||||
@media (width >= 64rem) {
|
@media (width >= 64rem) {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
}
|
|
||||||
}
|
|
||||||
.\32 xl\:table-cell {
|
|
||||||
@media (width >= 96rem) {
|
|
||||||
display: table-cell;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.is-drawer-close\:mx-auto {
|
.is-drawer-close\:mx-auto {
|
||||||
|
|||||||
BIN
Public/files/ManD.Groups.pdf
Executable file
BIN
Public/files/ManD.Groups.pdf
Executable file
Binary file not shown.
BIN
Public/files/ManD.Groups.pdf_original
Executable file
BIN
Public/files/ManD.Groups.pdf_original
Executable file
Binary file not shown.
@@ -20,6 +20,7 @@ public struct DatabaseClient: Sendable {
|
|||||||
public var effectiveLength: EffectiveLengthClient
|
public var effectiveLength: EffectiveLengthClient
|
||||||
public var users: Users
|
public var users: Users
|
||||||
public var userProfile: UserProfile
|
public var userProfile: UserProfile
|
||||||
|
public var trunkSizes: TrunkSizes
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DatabaseClient: TestDependencyKey {
|
extension DatabaseClient: TestDependencyKey {
|
||||||
@@ -31,7 +32,8 @@ extension DatabaseClient: TestDependencyKey {
|
|||||||
componentLoss: .testValue,
|
componentLoss: .testValue,
|
||||||
effectiveLength: .testValue,
|
effectiveLength: .testValue,
|
||||||
users: .testValue,
|
users: .testValue,
|
||||||
userProfile: .testValue
|
userProfile: .testValue,
|
||||||
|
trunkSizes: .testValue
|
||||||
)
|
)
|
||||||
|
|
||||||
public static func live(database: any Database) -> Self {
|
public static func live(database: any Database) -> Self {
|
||||||
@@ -43,7 +45,8 @@ extension DatabaseClient: TestDependencyKey {
|
|||||||
componentLoss: .live(database: database),
|
componentLoss: .live(database: database),
|
||||||
effectiveLength: .live(database: database),
|
effectiveLength: .live(database: database),
|
||||||
users: .live(database: database),
|
users: .live(database: database),
|
||||||
userProfile: .live(database: database)
|
userProfile: .live(database: database),
|
||||||
|
trunkSizes: .live(database: database)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +74,7 @@ extension DatabaseClient.Migrations: DependencyKey {
|
|||||||
EquipmentInfo.Migrate(),
|
EquipmentInfo.Migrate(),
|
||||||
Room.Migrate(),
|
Room.Migrate(),
|
||||||
EffectiveLength.Migrate(),
|
EffectiveLength.Migrate(),
|
||||||
|
DuctSizing.TrunkSize.Migrate(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ extension DatabaseClient {
|
|||||||
public var get: @Sendable (Room.ID) async throws -> Room?
|
public var get: @Sendable (Room.ID) async throws -> Room?
|
||||||
public var fetch: @Sendable (Project.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 update: @Sendable (Room.ID, Room.Update) async throws -> Room
|
||||||
|
public var updateRectangularSize:
|
||||||
|
@Sendable (Room.ID, DuctSizing.RectangularDuct) async throws -> Room
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +69,19 @@ extension DatabaseClient.Rooms: TestDependencyKey {
|
|||||||
try await model.save(on: database)
|
try await model.save(on: database)
|
||||||
}
|
}
|
||||||
return try model.toDTO()
|
return try model.toDTO()
|
||||||
|
},
|
||||||
|
updateRectangularSize: { id, size in
|
||||||
|
guard let model = try await RoomModel.find(id, on: database) else {
|
||||||
|
throw NotFoundError()
|
||||||
|
}
|
||||||
|
var rectangularSizes = model.rectangularSizes ?? []
|
||||||
|
rectangularSizes.removeAll {
|
||||||
|
$0.id == size.id
|
||||||
|
}
|
||||||
|
rectangularSizes.append(size)
|
||||||
|
model.rectangularSizes = rectangularSizes
|
||||||
|
try await model.save(on: database)
|
||||||
|
return try model.toDTO()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
356
Sources/DatabaseClient/TrunkSizes.swift
Normal file
356
Sources/DatabaseClient/TrunkSizes.swift
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import Fluent
|
||||||
|
import Foundation
|
||||||
|
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 update:
|
||||||
|
@Sendable (DuctSizing.TrunkSize.ID, DuctSizing.TrunkSize.Update) async throws ->
|
||||||
|
DuctSizing.TrunkSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||||
|
public static let testValue = Self()
|
||||||
|
|
||||||
|
public static func live(database: any Database) -> Self {
|
||||||
|
.init(
|
||||||
|
create: { request in
|
||||||
|
try request.validate()
|
||||||
|
|
||||||
|
let trunk = request.toModel()
|
||||||
|
var roomProxies = [DuctSizing.TrunkSize.RoomProxy]()
|
||||||
|
|
||||||
|
try await trunk.save(on: database)
|
||||||
|
|
||||||
|
for (roomID, registers) in request.rooms {
|
||||||
|
guard let room = try await RoomModel.find(roomID, on: database) else {
|
||||||
|
throw NotFoundError()
|
||||||
|
}
|
||||||
|
let model = try TrunkRoomModel(
|
||||||
|
trunkID: trunk.requireID(),
|
||||||
|
roomID: room.requireID(),
|
||||||
|
registers: registers,
|
||||||
|
type: request.type
|
||||||
|
)
|
||||||
|
try await model.save(on: database)
|
||||||
|
try await roomProxies.append(model.toDTO(on: database))
|
||||||
|
}
|
||||||
|
|
||||||
|
return try .init(
|
||||||
|
id: trunk.requireID(),
|
||||||
|
projectID: trunk.$project.id,
|
||||||
|
type: .init(rawValue: trunk.type)!,
|
||||||
|
rooms: roomProxies
|
||||||
|
)
|
||||||
|
},
|
||||||
|
delete: { id in
|
||||||
|
guard let model = try await TrunkModel.find(id, on: database) else {
|
||||||
|
throw NotFoundError()
|
||||||
|
}
|
||||||
|
try await model.delete(on: database)
|
||||||
|
},
|
||||||
|
fetch: { projectID in
|
||||||
|
try await TrunkModel.query(on: database)
|
||||||
|
.with(\.$project)
|
||||||
|
.with(\.$rooms)
|
||||||
|
.filter(\.$project.$id == projectID)
|
||||||
|
.all()
|
||||||
|
.toDTO(on: database)
|
||||||
|
},
|
||||||
|
get: { id in
|
||||||
|
guard let model = try await TrunkModel.find(id, on: database) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return try await model.toDTO(on: database)
|
||||||
|
},
|
||||||
|
update: { id, updates in
|
||||||
|
guard
|
||||||
|
let model =
|
||||||
|
try await TrunkModel
|
||||||
|
.query(on: database)
|
||||||
|
.with(\.$rooms)
|
||||||
|
.filter(\.$id == id)
|
||||||
|
.first()
|
||||||
|
else {
|
||||||
|
throw NotFoundError()
|
||||||
|
}
|
||||||
|
try updates.validate()
|
||||||
|
try await model.applyUpdates(updates, on: database)
|
||||||
|
return try await model.toDTO(on: database)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DuctSizing.TrunkSize.Create {
|
||||||
|
|
||||||
|
func validate() throws(ValidationError) {
|
||||||
|
guard rooms.count > 0 else {
|
||||||
|
throw ValidationError("Trunk size should have associated rooms / registers.")
|
||||||
|
}
|
||||||
|
if let height {
|
||||||
|
guard height > 0 else {
|
||||||
|
throw ValidationError("Trunk size height should be greater than 0.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toModel() -> TrunkModel {
|
||||||
|
.init(
|
||||||
|
projectID: projectID,
|
||||||
|
type: type,
|
||||||
|
height: height,
|
||||||
|
name: name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DuctSizing.TrunkSize.Update {
|
||||||
|
func validate() throws(ValidationError) {
|
||||||
|
if let rooms {
|
||||||
|
guard rooms.count > 0 else {
|
||||||
|
throw ValidationError("Trunk size should have associated rooms / registers.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let height {
|
||||||
|
guard height > 0 else {
|
||||||
|
throw ValidationError("Trunk size height should be greater than 0.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DuctSizing.TrunkSize {
|
||||||
|
|
||||||
|
struct Migrate: AsyncMigration {
|
||||||
|
let name = "CreateTrunkSize"
|
||||||
|
|
||||||
|
func prepare(on database: any Database) async throws {
|
||||||
|
try await database.schema(TrunkModel.schema)
|
||||||
|
.id()
|
||||||
|
.field("height", .int8)
|
||||||
|
.field("name", .string)
|
||||||
|
.field("type", .string, .required)
|
||||||
|
.field(
|
||||||
|
"projectID", .uuid, .required, .references(ProjectModel.schema, "id", onDelete: .cascade)
|
||||||
|
)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
try await database.schema(TrunkRoomModel.schema)
|
||||||
|
.id()
|
||||||
|
.field("registers", .array(of: .int), .required)
|
||||||
|
.field("type", .string, .required)
|
||||||
|
.field(
|
||||||
|
"trunkID", .uuid, .required, .references(TrunkModel.schema, "id", onDelete: .cascade)
|
||||||
|
)
|
||||||
|
.field(
|
||||||
|
"roomID", .uuid, .required, .references(RoomModel.schema, "id", onDelete: .cascade)
|
||||||
|
)
|
||||||
|
.unique(on: "trunkID", "roomID", "type")
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any Database) async throws {
|
||||||
|
try await database.schema(TrunkRoomModel.schema).delete()
|
||||||
|
try await database.schema(TrunkModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pivot table for associating rooms and trunks.
|
||||||
|
final class TrunkRoomModel: Model, @unchecked Sendable {
|
||||||
|
|
||||||
|
static let schema = "room+trunk"
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
@Parent(key: "trunkID")
|
||||||
|
var trunk: TrunkModel
|
||||||
|
|
||||||
|
@Parent(key: "roomID")
|
||||||
|
var room: RoomModel
|
||||||
|
|
||||||
|
@Field(key: "registers")
|
||||||
|
var registers: [Int]
|
||||||
|
|
||||||
|
@Field(key: "type")
|
||||||
|
var type: String
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
init(
|
||||||
|
id: UUID? = nil,
|
||||||
|
trunkID: TrunkModel.IDValue,
|
||||||
|
roomID: RoomModel.IDValue,
|
||||||
|
registers: [Int],
|
||||||
|
type: DuctSizing.TrunkSize.TrunkType
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
$trunk.id = trunkID
|
||||||
|
$room.id = roomID
|
||||||
|
self.registers = registers
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
return .init(
|
||||||
|
room: try room.toDTO(),
|
||||||
|
registers: registers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TrunkModel: Model, @unchecked Sendable {
|
||||||
|
|
||||||
|
static let schema = "trunk"
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
@Parent(key: "projectID")
|
||||||
|
var project: ProjectModel
|
||||||
|
|
||||||
|
@OptionalField(key: "height")
|
||||||
|
var height: Int?
|
||||||
|
|
||||||
|
@Field(key: "type")
|
||||||
|
var type: String
|
||||||
|
|
||||||
|
@OptionalField(key: "name")
|
||||||
|
var name: String?
|
||||||
|
|
||||||
|
@Children(for: \.$trunk)
|
||||||
|
var rooms: [TrunkRoomModel]
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
init(
|
||||||
|
id: UUID? = nil,
|
||||||
|
projectID: Project.ID,
|
||||||
|
type: DuctSizing.TrunkSize.TrunkType,
|
||||||
|
height: Int? = nil,
|
||||||
|
name: String? = nil
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
$project.id = projectID
|
||||||
|
self.height = height
|
||||||
|
self.type = type.rawValue
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return try .init(
|
||||||
|
id: requireID(),
|
||||||
|
projectID: $project.id,
|
||||||
|
type: .init(rawValue: type)!,
|
||||||
|
rooms: rooms,
|
||||||
|
height: height,
|
||||||
|
name: name
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyUpdates(
|
||||||
|
_ updates: DuctSizing.TrunkSize.Update,
|
||||||
|
on database: any Database
|
||||||
|
) async throws {
|
||||||
|
if let type = updates.type, type.rawValue != self.type {
|
||||||
|
self.type = type.rawValue
|
||||||
|
}
|
||||||
|
if let height = updates.height, height != self.height {
|
||||||
|
self.height = height
|
||||||
|
}
|
||||||
|
if let name = updates.name, name != self.name {
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
if hasChanges {
|
||||||
|
try await self.save(on: database)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let updateRooms = updates.rooms else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update rooms.
|
||||||
|
let rooms = try await TrunkRoomModel.query(on: database)
|
||||||
|
.with(\.$room)
|
||||||
|
.filter(\.$trunk.$id == requireID())
|
||||||
|
.all()
|
||||||
|
|
||||||
|
for (roomID, registers) in updateRooms {
|
||||||
|
if let currRoom = rooms.first(where: { $0.$room.id == roomID }) {
|
||||||
|
database.logger.debug("CURRENT ROOM: \(currRoom.room.name)")
|
||||||
|
if registers != currRoom.registers {
|
||||||
|
database.logger.debug("Updating registers for: \(currRoom.room.name)")
|
||||||
|
currRoom.registers = registers
|
||||||
|
}
|
||||||
|
if currRoom.hasChanges {
|
||||||
|
try await currRoom.save(on: database)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
database.logger.debug("CREATING NEW TrunkRoomModel")
|
||||||
|
let newModel = try TrunkRoomModel(
|
||||||
|
trunkID: requireID(),
|
||||||
|
roomID: roomID,
|
||||||
|
registers: registers,
|
||||||
|
type: .init(rawValue: type)!
|
||||||
|
)
|
||||||
|
try await newModel.save(on: database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let roomsToDelete = rooms.filter {
|
||||||
|
!updateRooms.keys.contains($0.$room.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
for room in roomsToDelete {
|
||||||
|
try await room.delete(on: database)
|
||||||
|
}
|
||||||
|
|
||||||
|
database.logger.debug("DONE WITH UPDATES")
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return try await group.reduce(into: [DuctSizing.TrunkSize]()) {
|
||||||
|
$0.append($1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,38 @@ extension Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension DuctSizing.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 DuctSizing.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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ComponentPressureLosses {
|
extension ComponentPressureLosses {
|
||||||
var totalLosses: Double { values.reduce(0) { $0 + $1 } }
|
var totalLosses: Double { values.reduce(0) { $0 + $1 } }
|
||||||
}
|
}
|
||||||
@@ -32,6 +64,8 @@ func roundSize(_ size: Double) throws -> Int {
|
|||||||
throw ManualDError(message: "Size should be less than 24.")
|
throw ManualDError(message: "Size should be less than 24.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// let size = size.rounded(.toNearestOrEven)
|
||||||
|
|
||||||
switch size {
|
switch size {
|
||||||
case 0..<4:
|
case 0..<4:
|
||||||
return 4
|
return 4
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ extension ManualDClient: DependencyKey {
|
|||||||
throw ManualDError(message: "Total Effective Length should be greater than 0.")
|
throw ManualDError(message: "Total Effective Length should be greater than 0.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalComponentLosses = request.componentPressureLosses.totalComponentPressureLoss
|
let totalComponentLosses = request.componentPressureLosses.total
|
||||||
let availableStaticPressure = request.externalStaticPressure - totalComponentLosses
|
let availableStaticPressure = request.externalStaticPressure - totalComponentLosses
|
||||||
let frictionRate = availableStaticPressure * 100.0 / Double(request.totalEffectiveLength)
|
let frictionRate = availableStaticPressure * 100.0 / Double(request.totalEffectiveLength)
|
||||||
return .init(availableStaticPressure: availableStaticPressure, frictionRate: frictionRate)
|
return .init(availableStaticPressure: availableStaticPressure, frictionRate: frictionRate)
|
||||||
@@ -38,23 +38,7 @@ extension ManualDClient: DependencyKey {
|
|||||||
},
|
},
|
||||||
equivalentRectangularDuct: { request in
|
equivalentRectangularDuct: { request in
|
||||||
let width = (Double.pi * (pow(Double(request.roundSize) / 2.0, 2.0))) / Double(request.height)
|
let width = (Double.pi * (pow(Double(request.roundSize) / 2.0, 2.0))) / Double(request.height)
|
||||||
// Round the width up or fail (really should never fail since we know the input is a number).
|
return .init(height: request.height, width: Int(width.rounded(.toNearestOrEven)))
|
||||||
guard let widthStr = numberFormatter.string(for: width),
|
|
||||||
let widthInt = Int(widthStr)
|
|
||||||
else {
|
|
||||||
throw ManualDError(
|
|
||||||
message: "Failed to convert to to rectangular duct size, width: \(width)"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return .init(height: request.height, width: widthInt)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private let numberFormatter: NumberFormatter = {
|
|
||||||
let formatter = NumberFormatter()
|
|
||||||
formatter.maximumFractionDigits = 0
|
|
||||||
formatter.minimumFractionDigits = 0
|
|
||||||
formatter.roundingMode = .ceiling
|
|
||||||
return formatter
|
|
||||||
}()
|
|
||||||
|
|||||||
@@ -12,6 +12,29 @@ public struct ManualDClient: Sendable {
|
|||||||
@Sendable (EquivalentRectangularDuctRequest) async throws -> EquivalentRectangularDuctResponse
|
@Sendable (EquivalentRectangularDuctRequest) async throws -> EquivalentRectangularDuctResponse
|
||||||
|
|
||||||
public func calculateSizes(
|
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],
|
rooms: [Room],
|
||||||
equipmentInfo: EquipmentInfo,
|
equipmentInfo: EquipmentInfo,
|
||||||
maxSupplyLength: EffectiveLength,
|
maxSupplyLength: EffectiveLength,
|
||||||
@@ -21,7 +44,6 @@ public struct ManualDClient: Sendable {
|
|||||||
logger: Logger? = nil
|
logger: Logger? = nil
|
||||||
) async throws -> [DuctSizing.RoomContainer] {
|
) async throws -> [DuctSizing.RoomContainer] {
|
||||||
|
|
||||||
var registerIDCount = 1
|
|
||||||
var retval: [DuctSizing.RoomContainer] = []
|
var retval: [DuctSizing.RoomContainer] = []
|
||||||
let totalHeatingLoad = rooms.totalHeatingLoad
|
let totalHeatingLoad = rooms.totalHeatingLoad
|
||||||
let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR)
|
let totalCoolingSensible = rooms.totalCoolingSensible(shr: projectSHR)
|
||||||
@@ -53,9 +75,9 @@ public struct ManualDClient: Sendable {
|
|||||||
|
|
||||||
retval.append(
|
retval.append(
|
||||||
.init(
|
.init(
|
||||||
registerID: "SR-\(registerIDCount)",
|
|
||||||
roomID: room.id,
|
roomID: room.id,
|
||||||
roomName: "\(room.name)-\(n)",
|
roomName: "\(room.name)-\(n)",
|
||||||
|
roomRegister: n,
|
||||||
heatingLoad: heatingLoad,
|
heatingLoad: heatingLoad,
|
||||||
coolingLoad: coolingLoad,
|
coolingLoad: coolingLoad,
|
||||||
heatingCFM: heatingCFM,
|
heatingCFM: heatingCFM,
|
||||||
@@ -69,13 +91,65 @@ public struct ManualDClient: Sendable {
|
|||||||
rectangularWidth: rectangularWidth
|
rectangularWidth: rectangularWidth
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
registerIDCount += 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return retval
|
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 {
|
extension ManualDClient: TestDependencyKey {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ extension ComponentPressureLoss {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension Array where Element == ComponentPressureLoss {
|
extension Array where Element == ComponentPressureLoss {
|
||||||
public var totalComponentPressureLoss: Double {
|
public var total: Double {
|
||||||
reduce(into: 0) { $0 += $1.value }
|
reduce(into: 0) { $0 += $1.value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,42 @@ public enum DuctSizing {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 struct RoomContainer: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
public let registerID: String
|
|
||||||
public let roomID: Room.ID
|
public let roomID: Room.ID
|
||||||
public let roomName: String
|
public let roomName: String
|
||||||
|
public let roomRegister: Int
|
||||||
public let heatingLoad: Double
|
public let heatingLoad: Double
|
||||||
public let coolingLoad: Double
|
public let coolingLoad: Double
|
||||||
public let heatingCFM: Double
|
public let heatingCFM: Double
|
||||||
@@ -39,9 +70,9 @@ public enum DuctSizing {
|
|||||||
public let rectangularWidth: Int?
|
public let rectangularWidth: Int?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
registerID: String,
|
|
||||||
roomID: Room.ID,
|
roomID: Room.ID,
|
||||||
roomName: String,
|
roomName: String,
|
||||||
|
roomRegister: Int,
|
||||||
heatingLoad: Double,
|
heatingLoad: Double,
|
||||||
coolingLoad: Double,
|
coolingLoad: Double,
|
||||||
heatingCFM: Double,
|
heatingCFM: Double,
|
||||||
@@ -54,9 +85,9 @@ public enum DuctSizing {
|
|||||||
rectangularSize: RectangularDuct? = nil,
|
rectangularSize: RectangularDuct? = nil,
|
||||||
rectangularWidth: Int? = nil
|
rectangularWidth: Int? = nil
|
||||||
) {
|
) {
|
||||||
self.registerID = registerID
|
|
||||||
self.roomID = roomID
|
self.roomID = roomID
|
||||||
self.roomName = roomName
|
self.roomName = roomName
|
||||||
|
self.roomRegister = roomRegister
|
||||||
self.heatingLoad = heatingLoad
|
self.heatingLoad = heatingLoad
|
||||||
self.coolingLoad = coolingLoad
|
self.coolingLoad = coolingLoad
|
||||||
self.heatingCFM = heatingCFM
|
self.heatingCFM = heatingCFM
|
||||||
@@ -91,3 +122,127 @@ public enum DuctSizing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -607,9 +607,11 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
|
|
||||||
public enum DuctSizingRoute: Equatable, Sendable {
|
public enum DuctSizingRoute: Equatable, Sendable {
|
||||||
case index
|
case index
|
||||||
case deleteRectangularSize(Room.ID, DuctSizing.RectangularDuct.ID)
|
case deleteRectangularSize(Room.ID, DeleteRectangularDuct)
|
||||||
case roomRectangularForm(Room.ID, RoomRectangularForm)
|
case roomRectangularForm(Room.ID, RoomRectangularForm)
|
||||||
|
case trunk(TrunkRoute)
|
||||||
|
|
||||||
|
public static let roomPath = "room"
|
||||||
static let rootPath = "duct-sizing"
|
static let rootPath = "duct-sizing"
|
||||||
|
|
||||||
static let router = OneOf {
|
static let router = OneOf {
|
||||||
@@ -620,18 +622,20 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
Route(.case(Self.deleteRectangularSize)) {
|
Route(.case(Self.deleteRectangularSize)) {
|
||||||
Path {
|
Path {
|
||||||
rootPath
|
rootPath
|
||||||
"room"
|
roomPath
|
||||||
Room.ID.parser()
|
Room.ID.parser()
|
||||||
}
|
}
|
||||||
Method.delete
|
Method.delete
|
||||||
Query {
|
Query {
|
||||||
Field("rectangularSize") { DuctSizing.RectangularDuct.ID.parser() }
|
Field("rectangularSize") { DuctSizing.RectangularDuct.ID.parser() }
|
||||||
|
Field("register") { Int.parser() }
|
||||||
}
|
}
|
||||||
|
.map(.memberwise(DeleteRectangularDuct.init))
|
||||||
}
|
}
|
||||||
Route(.case(Self.roomRectangularForm)) {
|
Route(.case(Self.roomRectangularForm)) {
|
||||||
Path {
|
Path {
|
||||||
rootPath
|
rootPath
|
||||||
"room"
|
roomPath
|
||||||
Room.ID.parser()
|
Room.ID.parser()
|
||||||
}
|
}
|
||||||
Method.post
|
Method.post
|
||||||
@@ -646,6 +650,85 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
.map(.memberwise(RoomRectangularForm.init))
|
.map(.memberwise(RoomRectangularForm.init))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Route(.case(Self.trunk)) {
|
||||||
|
Path { rootPath }
|
||||||
|
TrunkRoute.router
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DeleteRectangularDuct: Equatable, Sendable {
|
||||||
|
|
||||||
|
public let rectangularSizeID: DuctSizing.RectangularDuct.ID
|
||||||
|
public let register: Int
|
||||||
|
|
||||||
|
public init(rectangularSizeID: DuctSizing.RectangularDuct.ID, register: Int) {
|
||||||
|
self.rectangularSizeID = rectangularSizeID
|
||||||
|
self.register = register
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TrunkRoute: Equatable, Sendable {
|
||||||
|
case delete(DuctSizing.TrunkSize.ID)
|
||||||
|
case submit(TrunkSizeForm)
|
||||||
|
case update(DuctSizing.TrunkSize.ID, TrunkSizeForm)
|
||||||
|
|
||||||
|
public static let rootPath = "trunk"
|
||||||
|
|
||||||
|
static let router = OneOf {
|
||||||
|
Route(.case(Self.delete)) {
|
||||||
|
Path {
|
||||||
|
rootPath
|
||||||
|
DuctSizing.TrunkSize.ID.parser()
|
||||||
|
}
|
||||||
|
Method.delete
|
||||||
|
}
|
||||||
|
Route(.case(Self.submit)) {
|
||||||
|
Path {
|
||||||
|
rootPath
|
||||||
|
}
|
||||||
|
Method.post
|
||||||
|
Body {
|
||||||
|
FormData {
|
||||||
|
Field("projectID") { Project.ID.parser() }
|
||||||
|
Field("type") { DuctSizing.TrunkSize.TrunkType.parser() }
|
||||||
|
Optionally {
|
||||||
|
Field("height") { Int.parser() }
|
||||||
|
|
||||||
|
}
|
||||||
|
Optionally {
|
||||||
|
Field("name", .string)
|
||||||
|
}
|
||||||
|
Many {
|
||||||
|
Field("rooms", .string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map(.memberwise(TrunkSizeForm.init))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Route(.case(Self.update)) {
|
||||||
|
Path {
|
||||||
|
rootPath
|
||||||
|
DuctSizing.TrunkSize.ID.parser()
|
||||||
|
}
|
||||||
|
Method.patch
|
||||||
|
Body {
|
||||||
|
FormData {
|
||||||
|
Field("projectID") { Project.ID.parser() }
|
||||||
|
Field("type") { DuctSizing.TrunkSize.TrunkType.parser() }
|
||||||
|
Optionally {
|
||||||
|
Field("height") { Int.parser() }
|
||||||
|
}
|
||||||
|
Optionally {
|
||||||
|
Field("name", .string)
|
||||||
|
}
|
||||||
|
Many {
|
||||||
|
Field("rooms", .string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map(.memberwise(TrunkSizeForm.init))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct RoomRectangularForm: Equatable, Sendable {
|
public struct RoomRectangularForm: Equatable, Sendable {
|
||||||
@@ -653,6 +736,14 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
public let register: Int
|
public let register: Int
|
||||||
public let height: Int
|
public let height: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct TrunkSizeForm: Equatable, Sendable {
|
||||||
|
public let projectID: Project.ID
|
||||||
|
public let type: DuctSizing.TrunkSize.TrunkType
|
||||||
|
public let height: Int?
|
||||||
|
public let name: String?
|
||||||
|
public let rooms: [String]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
Sources/Styleguide/Alert.swift
Normal file
28
Sources/Styleguide/Alert.swift
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import Elementary
|
||||||
|
|
||||||
|
public struct Alert<Content: HTML>: HTML {
|
||||||
|
|
||||||
|
let inner: Content
|
||||||
|
|
||||||
|
public init(@HTMLBuilder content: () -> Content) {
|
||||||
|
self.inner = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some HTML<HTMLTag.div> {
|
||||||
|
div(.class("flex space-x-2")) {
|
||||||
|
SVG(.triangleAlert)
|
||||||
|
inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Alert: Sendable where Content: Sendable {}
|
||||||
|
|
||||||
|
extension Alert where Content == p<HTMLText> {
|
||||||
|
|
||||||
|
public init(_ description: String) {
|
||||||
|
self.init {
|
||||||
|
p { description }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ public struct Badge<Inner: HTML>: HTML, Sendable where Inner: Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var body: some HTML<HTMLTag.div> {
|
public var body: some HTML<HTMLTag.div> {
|
||||||
div(.class("badge badge-lg badge-outline font-bold")) {
|
div(.class("badge badge-lg badge-outline")) {
|
||||||
inner
|
inner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ public struct EditButton: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var body: some HTML<HTMLTag.button> {
|
public var body: some HTML<HTMLTag.button> {
|
||||||
button(.class("btn hover:btn-success"), .type(type)) {
|
button(.class("btn"), .type(type)) {
|
||||||
div(.class("flex")) {
|
div(.class("flex")) {
|
||||||
if let title {
|
if let title {
|
||||||
span(.class("pe-2")) { title }
|
span(.class("pe-2")) { title }
|
||||||
@@ -83,7 +83,7 @@ public struct PlusButton: HTML, Sendable {
|
|||||||
public var body: some HTML<HTMLTag.button> {
|
public var body: some HTML<HTMLTag.button> {
|
||||||
button(
|
button(
|
||||||
.type(.button),
|
.type(.button),
|
||||||
.class("btn btn-primary btn-circle text-xl")
|
.class("btn")
|
||||||
) { SVG(.circlePlus) }
|
) { SVG(.circlePlus) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ extension HTMLAttribute where Tag == HTMLTag.button {
|
|||||||
|
|
||||||
extension HTML where Tag: HTMLTrait.Attributes.Global {
|
extension HTML where Tag: HTMLTrait.Attributes.Global {
|
||||||
public func badge() -> _AttributedElement<Self> {
|
public func badge() -> _AttributedElement<Self> {
|
||||||
attributes(.class("badge badge-lg badge-outline font-bold"))
|
attributes(.class("badge badge-lg badge-outline"))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hidden(when shouldHide: Bool) -> _AttributedElement<Self> {
|
||||||
|
attributes(.class("hidden"), when: shouldHide)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func bold(when shouldBeBold: Bool = true) -> _AttributedElement<Self> {
|
||||||
|
attributes(.class("font-bold"), when: shouldBeBold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public struct Label: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var body: some HTML<HTMLTag.span> {
|
public var body: some HTML<HTMLTag.span> {
|
||||||
span(.class("text-lg text-secondary font-bold")) {
|
span(.class("text-lg label font-bold")) {
|
||||||
title
|
title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public struct ModalForm<T: HTML>: HTML, Sendable where T: Sendable {
|
|||||||
self.inner = inner()
|
self.inner = inner()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some HTML {
|
public var body: some HTML<HTMLTag.dialog> {
|
||||||
dialog(.id(id), .class("modal")) {
|
dialog(.id(id), .class("modal")) {
|
||||||
div(.class("modal-box")) {
|
div(.class("modal-box")) {
|
||||||
if closeButton {
|
if closeButton {
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
import Elementary
|
import Elementary
|
||||||
|
|
||||||
|
public struct PageTitleRow<Content: HTML>: HTML, Sendable where Content: Sendable {
|
||||||
|
|
||||||
|
let inner: Content
|
||||||
|
|
||||||
|
public init(@HTMLBuilder content: () -> Content) {
|
||||||
|
self.inner = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some HTML<HTMLTag.div> {
|
||||||
|
div(
|
||||||
|
.class(
|
||||||
|
"""
|
||||||
|
flex justify-between bg-secondary border-2 border-primary rounded-sm shadow-sm
|
||||||
|
p-6 w-full
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct PageTitle: HTML, Sendable {
|
public struct PageTitle: HTML, Sendable {
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ extension SVG {
|
|||||||
case squareFunction
|
case squareFunction
|
||||||
case squarePen
|
case squarePen
|
||||||
case trash
|
case trash
|
||||||
|
case triangleAlert
|
||||||
case user
|
case user
|
||||||
case wind
|
case wind
|
||||||
|
|
||||||
@@ -135,6 +136,10 @@ extension SVG {
|
|||||||
return """
|
return """
|
||||||
<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>
|
<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>
|
||||||
"""
|
"""
|
||||||
|
case .triangleAlert:
|
||||||
|
return """
|
||||||
|
<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>
|
||||||
|
"""
|
||||||
case .user:
|
case .user:
|
||||||
return """
|
return """
|
||||||
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import Elementary
|
import Elementary
|
||||||
|
|
||||||
public struct Tooltip<Inner: HTML & Sendable>: HTML, Sendable {
|
extension HTML {
|
||||||
|
|
||||||
|
public func tooltip(
|
||||||
|
_ tip: String,
|
||||||
|
position: TooltipPosition = .default
|
||||||
|
) -> Tooltip<Self> {
|
||||||
|
Tooltip(tip, position: position) {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Tooltip<Inner: HTML>: HTML {
|
||||||
|
|
||||||
let tooltip: String
|
let tooltip: String
|
||||||
let position: TooltipPosition
|
let position: TooltipPosition
|
||||||
@@ -26,6 +38,8 @@ public struct Tooltip<Inner: HTML & Sendable>: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Tooltip: Sendable where Inner: Sendable {}
|
||||||
|
|
||||||
public enum TooltipPosition: String, CaseIterable, Sendable {
|
public enum TooltipPosition: String, CaseIterable, Sendable {
|
||||||
|
|
||||||
public static let `default` = Self.left
|
public static let `default` = Self.left
|
||||||
|
|||||||
@@ -26,11 +26,14 @@ extension DatabaseClient.Projects {
|
|||||||
|
|
||||||
extension DatabaseClient {
|
extension DatabaseClient {
|
||||||
|
|
||||||
func calculateDuctSizes(projectID: Project.ID) async throws -> [DuctSizing.RoomContainer] {
|
func calculateDuctSizes(
|
||||||
|
projectID: Project.ID
|
||||||
|
) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) {
|
||||||
@Dependency(\.manualD) var manualD
|
@Dependency(\.manualD) var manualD
|
||||||
|
|
||||||
return try await manualD.calculate(
|
return try await manualD.calculate(
|
||||||
rooms: rooms.fetch(projectID),
|
rooms: rooms.fetch(projectID),
|
||||||
|
trunks: trunkSizes.fetch(projectID),
|
||||||
designFrictionRateResult: designFrictionRate(projectID: projectID),
|
designFrictionRateResult: designFrictionRate(projectID: projectID),
|
||||||
projectSHR: projects.getSensibleHeatRatio(projectID)
|
projectSHR: projects.getSensibleHeatRatio(projectID)
|
||||||
)
|
)
|
||||||
@@ -50,7 +53,7 @@ extension DatabaseClient {
|
|||||||
guard componentLosses.count > 0 else { return nil }
|
guard componentLosses.count > 0 else { return nil }
|
||||||
|
|
||||||
let availableStaticPressure =
|
let availableStaticPressure =
|
||||||
equipmentInfo.staticPressure - componentLosses.totalComponentPressureLoss
|
equipmentInfo.staticPressure - componentLosses.total
|
||||||
|
|
||||||
let designFrictionRate = (availableStaticPressure * 100) / tel
|
let designFrictionRate = (availableStaticPressure * 100) / tel
|
||||||
|
|
||||||
|
|||||||
@@ -6,20 +6,22 @@ extension ManualDClient {
|
|||||||
|
|
||||||
func calculate(
|
func calculate(
|
||||||
rooms: [Room],
|
rooms: [Room],
|
||||||
|
trunks: [DuctSizing.TrunkSize],
|
||||||
designFrictionRateResult: (EquipmentInfo, EffectiveLength.MaxContainer, Double)?,
|
designFrictionRateResult: (EquipmentInfo, EffectiveLength.MaxContainer, Double)?,
|
||||||
projectSHR: Double?,
|
projectSHR: Double?,
|
||||||
logger: Logger? = nil
|
logger: Logger? = nil
|
||||||
) async throws -> [DuctSizing.RoomContainer] {
|
) async throws -> (rooms: [DuctSizing.RoomContainer], trunks: [DuctSizing.TrunkContainer]) {
|
||||||
guard let designFrictionRateResult else { return [] }
|
guard let designFrictionRateResult else { return ([], []) }
|
||||||
let equipmentInfo = designFrictionRateResult.0
|
let equipmentInfo = designFrictionRateResult.0
|
||||||
let effectiveLengths = designFrictionRateResult.1
|
let effectiveLengths = designFrictionRateResult.1
|
||||||
let designFrictionRate = designFrictionRateResult.2
|
let designFrictionRate = designFrictionRateResult.2
|
||||||
|
|
||||||
guard let maxSupply = effectiveLengths.supply else { return [] }
|
guard let maxSupply = effectiveLengths.supply else { return ([], []) }
|
||||||
guard let maxReturn = effectiveLengths.return else { return [] }
|
guard let maxReturn = effectiveLengths.return else { return ([], []) }
|
||||||
|
|
||||||
let ductRooms = try await self.calculateSizes(
|
let ductRooms = try await self.calculateSizes(
|
||||||
rooms: rooms,
|
rooms: rooms,
|
||||||
|
trunks: trunks,
|
||||||
equipmentInfo: equipmentInfo,
|
equipmentInfo: equipmentInfo,
|
||||||
maxSupplyLength: maxSupply,
|
maxSupplyLength: maxSupply,
|
||||||
maxReturnLength: maxReturn,
|
maxReturnLength: maxReturn,
|
||||||
|
|||||||
@@ -17,4 +17,9 @@ extension String {
|
|||||||
func appendingPath(_ id: UUID) -> Self {
|
func appendingPath(_ id: UUID) -> Self {
|
||||||
return appendingPath(id.uuidString)
|
return appendingPath(id.uuidString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var idString: Self {
|
||||||
|
replacing("-", with: "")
|
||||||
|
.replacing(" ", with: "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
import ManualDCore
|
||||||
|
|
||||||
|
extension SiteRoute.View.ProjectRoute.DuctSizingRoute.TrunkSizeForm {
|
||||||
|
|
||||||
|
func toCreate(logger: Logger? = nil) throws -> DuctSizing.TrunkSize.Create {
|
||||||
|
try .init(
|
||||||
|
projectID: projectID,
|
||||||
|
type: type,
|
||||||
|
rooms: makeRooms(logger: logger),
|
||||||
|
height: height,
|
||||||
|
name: name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUpdate(logger: Logger? = nil) throws -> DuctSizing.TrunkSize.Update {
|
||||||
|
try .init(
|
||||||
|
type: type,
|
||||||
|
rooms: makeRooms(logger: logger),
|
||||||
|
height: height,
|
||||||
|
name: name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRooms(logger: Logger?) throws -> [Room.ID: [Int]] {
|
||||||
|
var retval = [Room.ID: [Int]]()
|
||||||
|
for room in rooms {
|
||||||
|
let split = room.split(separator: "_")
|
||||||
|
guard let idString = split.first,
|
||||||
|
let id = UUID(uuidString: String(idString))
|
||||||
|
else {
|
||||||
|
logger?.error("Could not parse id from: \(room)")
|
||||||
|
throw RoomError()
|
||||||
|
}
|
||||||
|
guard let registerString = split.last,
|
||||||
|
let register = Int(registerString)
|
||||||
|
else {
|
||||||
|
logger?.error("Could not register number from: \(room)")
|
||||||
|
throw RoomError()
|
||||||
|
}
|
||||||
|
if var currRegisters = retval[id] {
|
||||||
|
currRegisters.append(register)
|
||||||
|
retval[id] = currRegisters
|
||||||
|
} else {
|
||||||
|
retval[id] = [register]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return retval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RoomError: Error {}
|
||||||
@@ -2,6 +2,6 @@ import Foundation
|
|||||||
|
|
||||||
extension UUID {
|
extension UUID {
|
||||||
var idString: String {
|
var idString: String {
|
||||||
uuidString.replacing("-", with: "")
|
uuidString.idString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,16 @@ extension ViewController.Request {
|
|||||||
|
|
||||||
switch route {
|
switch route {
|
||||||
case .test:
|
case .test:
|
||||||
|
let projectID = UUID(uuidString: "A9C20153-E2E5-4C65-B33F-4D8A29C63A7A")!
|
||||||
return await view {
|
return await view {
|
||||||
TestPage()
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .login(let route):
|
case .login(let route):
|
||||||
switch route {
|
switch route {
|
||||||
@@ -78,7 +86,7 @@ extension ViewController.Request {
|
|||||||
let inner = await inner()
|
let inner = await inner()
|
||||||
let theme = await self.theme
|
let theme = await self.theme
|
||||||
|
|
||||||
return MainPage(theme: theme) {
|
return MainPage(displayFooter: displayFooter, theme: theme) {
|
||||||
inner
|
inner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,6 +98,15 @@ extension ViewController.Request {
|
|||||||
return try? await database.userProfile.fetch(user.id)?.theme
|
return try? await database.userProfile.fetch(user.id)?.theme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var displayFooter: Bool {
|
||||||
|
switch route {
|
||||||
|
case .login, .signup:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SiteRoute.View.ProjectRoute {
|
extension SiteRoute.View.ProjectRoute {
|
||||||
@@ -123,7 +140,8 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case .create(let form):
|
case .create(let form):
|
||||||
return await ResultView {
|
return await request.view {
|
||||||
|
await ResultView {
|
||||||
let user = try request.currentUser()
|
let user = try request.currentUser()
|
||||||
let project = try await database.projects.create(user.id, form)
|
let project = try await database.projects.create(user.id, form)
|
||||||
try await database.componentLoss.createDefaults(projectID: project.id)
|
try await database.componentLoss.createDefaults(projectID: project.id)
|
||||||
@@ -140,6 +158,7 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
RoomsView(rooms: rooms, sensibleHeatRatio: shr)
|
RoomsView(rooms: rooms, sensibleHeatRatio: shr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case .delete(let id):
|
case .delete(let id):
|
||||||
return await ResultView {
|
return await ResultView {
|
||||||
@@ -211,10 +230,8 @@ extension SiteRoute.View.ProjectRoute.EquipmentInfoRoute {
|
|||||||
return await equipmentView(on: request, projectID: projectID)
|
return await equipmentView(on: request, projectID: projectID)
|
||||||
|
|
||||||
case .submit(let form):
|
case .submit(let form):
|
||||||
return await ResultView {
|
return await equipmentView(on: request, projectID: projectID) {
|
||||||
try await database.equipment.create(form)
|
_ = try await database.equipment.create(form)
|
||||||
} onSuccess: { equipment in
|
|
||||||
EquipmentInfoView(equipmentInfo: equipment, projectID: projectID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .update(let id, let updates):
|
case .update(let id, let updates):
|
||||||
@@ -540,31 +557,48 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
|
|||||||
case .index:
|
case .index:
|
||||||
return await view(on: request, projectID: projectID)
|
return await view(on: request, projectID: projectID)
|
||||||
|
|
||||||
case .deleteRectangularSize(let roomID, let rectangularSizeID):
|
case .deleteRectangularSize(let roomID, let request):
|
||||||
return await ResultView {
|
return await ResultView {
|
||||||
let room = try await database.rooms.deleteRectangularSize(roomID, rectangularSizeID)
|
let room = try await database.rooms.deleteRectangularSize(roomID, request.rectangularSizeID)
|
||||||
return try await database.calculateDuctSizes(projectID: projectID)
|
return try await database.calculateDuctSizes(projectID: projectID)
|
||||||
.filter({ $0.roomID == room.id })
|
.rooms
|
||||||
|
.filter({ $0.roomID == room.id && $0.roomRegister == request.register })
|
||||||
.first!
|
.first!
|
||||||
} onSuccess: { container in
|
} onSuccess: { room in
|
||||||
DuctSizingView.RoomRow(projectID: projectID, room: container)
|
DuctSizingView.RoomRow(room: room)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .roomRectangularForm(let roomID, let form):
|
case .roomRectangularForm(let roomID, let form):
|
||||||
return await ResultView {
|
return await ResultView {
|
||||||
let room = try await database.rooms.update(
|
let room = try await database.rooms.updateRectangularSize(
|
||||||
roomID,
|
roomID,
|
||||||
.init(
|
|
||||||
rectangularSizes: [
|
|
||||||
.init(id: form.id ?? .init(), register: form.register, height: form.height)
|
.init(id: form.id ?? .init(), register: form.register, height: form.height)
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return try await database.calculateDuctSizes(projectID: projectID)
|
return try await database.calculateDuctSizes(projectID: projectID)
|
||||||
.filter({ $0.roomID == room.id })
|
.rooms
|
||||||
|
.filter({ $0.roomID == room.id && $0.roomRegister == form.register })
|
||||||
.first!
|
.first!
|
||||||
} onSuccess: { container in
|
} onSuccess: { room in
|
||||||
DuctSizingView.RoomRow(projectID: projectID, room: container)
|
DuctSizingView.RoomRow(room: room)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .trunk(let route):
|
||||||
|
switch route {
|
||||||
|
case .delete(let id):
|
||||||
|
return await ResultView {
|
||||||
|
try await database.trunkSizes.delete(id)
|
||||||
|
}
|
||||||
|
case .submit(let form):
|
||||||
|
return await view(on: request, projectID: projectID) {
|
||||||
|
_ = try await database.trunkSizes.create(
|
||||||
|
form.toCreate(logger: request.logger)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .update(let id, let form):
|
||||||
|
return await view(on: request, projectID: projectID) {
|
||||||
|
_ = try await database.trunkSizes.update(id, form.toUpdate())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -576,15 +610,17 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
|
|||||||
) async -> AnySendableHTML {
|
) async -> AnySendableHTML {
|
||||||
@Dependency(\.database) var database
|
@Dependency(\.database) var database
|
||||||
|
|
||||||
return await ResultView {
|
return await request.view {
|
||||||
|
await ResultView {
|
||||||
try await catching()
|
try await catching()
|
||||||
return (
|
return (
|
||||||
try await database.projects.getCompletedSteps(projectID),
|
try await database.projects.getCompletedSteps(projectID),
|
||||||
try await database.calculateDuctSizes(projectID: projectID)
|
try await database.calculateDuctSizes(projectID: projectID)
|
||||||
)
|
)
|
||||||
} onSuccess: { (steps, rooms) in
|
} onSuccess: { (steps, ducts) in
|
||||||
ProjectView(projectID: projectID, activeTab: .ductSizing, completedSteps: steps) {
|
ProjectView(projectID: projectID, activeTab: .ductSizing, completedSteps: steps) {
|
||||||
DuctSizingView(rooms: rooms)
|
DuctSizingView(rooms: ducts.rooms, trunks: ducts.trunks)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,19 +42,27 @@ struct ComponentLossForm: HTML, Sendable {
|
|||||||
|
|
||||||
input(.class("hidden"), .name("projectID"), .value("\(projectID)"))
|
input(.class("hidden"), .name("projectID"), .value("\(projectID)"))
|
||||||
|
|
||||||
div {
|
LabeledInput(
|
||||||
label(.for("name")) { "Name" }
|
"Name",
|
||||||
Input(id: "name", placeholder: "Name")
|
.name("name"),
|
||||||
.attributes(.type(.text), .required, .autofocus, .value(componentLoss?.name))
|
.type(.text),
|
||||||
}
|
.value(componentLoss?.name),
|
||||||
div {
|
.placeholder("Name"),
|
||||||
label(.for("value")) { "Value" }
|
.required,
|
||||||
Input(id: "value", placeholder: "Pressure loss")
|
.autofocus
|
||||||
.attributes(
|
)
|
||||||
.type(.number), .min("0.03"), .max("1.0"), .step("0.01"), .required,
|
|
||||||
.value(componentLoss?.value)
|
LabeledInput(
|
||||||
|
"Value",
|
||||||
|
.name("value"),
|
||||||
|
.type(.number),
|
||||||
|
.value(componentLoss?.value),
|
||||||
|
.placeholder("0.2"),
|
||||||
|
.min("0.03"),
|
||||||
|
.max("1.0"),
|
||||||
|
.step("0.01"),
|
||||||
|
.required
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
SubmitButton()
|
SubmitButton()
|
||||||
.attributes(.class("btn-block"))
|
.attributes(.class("btn-block"))
|
||||||
|
|||||||
@@ -3,24 +3,31 @@ import ElementaryHTMX
|
|||||||
import ManualDCore
|
import ManualDCore
|
||||||
import Styleguide
|
import Styleguide
|
||||||
|
|
||||||
// TODO: Load component losses when view appears??
|
|
||||||
|
|
||||||
struct ComponentPressureLossesView: HTML, Sendable {
|
struct ComponentPressureLossesView: HTML, Sendable {
|
||||||
|
|
||||||
let componentPressureLosses: [ComponentPressureLoss]
|
let componentPressureLosses: [ComponentPressureLoss]
|
||||||
let projectID: Project.ID
|
let projectID: Project.ID
|
||||||
|
|
||||||
private var total: Double {
|
private var total: Double {
|
||||||
componentPressureLosses.reduce(into: 0) { $0 += $1.value }
|
componentPressureLosses.total
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sortedLosses: [ComponentPressureLoss] {
|
||||||
|
componentPressureLosses.sorted {
|
||||||
|
$0.value > $1.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
div(.class("space-y-4")) {
|
div(.class("space-y-4")) {
|
||||||
Row {
|
Row {
|
||||||
h1(.class("text-2xl font-bold")) { "Component Pressure Losses" }
|
h1(.class("text-2xl font-bold")) { "Component Pressure Losses" }
|
||||||
LabeledContent("Total") {
|
PlusButton()
|
||||||
Badge(number: total)
|
.attributes(
|
||||||
}
|
.class("btn-primary text-2xl me-2"),
|
||||||
|
.showModal(id: ComponentLossForm.id())
|
||||||
|
)
|
||||||
|
.tooltip("Add component loss")
|
||||||
}
|
}
|
||||||
.attributes(.class("px-4"))
|
.attributes(.class("px-4"))
|
||||||
|
|
||||||
@@ -29,25 +36,14 @@ struct ComponentPressureLossesView: HTML, Sendable {
|
|||||||
tr(.class("text-xl font-bold")) {
|
tr(.class("text-xl font-bold")) {
|
||||||
th { "Name" }
|
th { "Name" }
|
||||||
th { "Value" }
|
th { "Value" }
|
||||||
th {
|
th(.class("min-w-[200px]")) {}
|
||||||
div(.class("flex justify-end mx-auto")) {
|
|
||||||
Tooltip("Add Component Loss") {
|
|
||||||
PlusButton()
|
|
||||||
.attributes(
|
|
||||||
.class("btn-ghost text-2xl"),
|
|
||||||
.showModal(id: ComponentLossForm.id())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tbody {
|
tbody {
|
||||||
for row in componentPressureLosses {
|
for row in sortedLosses {
|
||||||
TableRow(row: row)
|
TableRow(row: row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ComponentLossForm(dismiss: true, projectID: projectID, componentLoss: nil)
|
ComponentLossForm(dismiss: true, projectID: projectID, componentLoss: nil)
|
||||||
|
|||||||
@@ -3,151 +3,53 @@ import ElementaryHTMX
|
|||||||
import ManualDCore
|
import ManualDCore
|
||||||
import Styleguide
|
import Styleguide
|
||||||
|
|
||||||
// TODO: Add error text if prior steps are not completed.
|
|
||||||
|
|
||||||
struct DuctSizingView: HTML, Sendable {
|
struct DuctSizingView: HTML, Sendable {
|
||||||
|
|
||||||
@Environment(ProjectViewValue.$projectID) var projectID
|
@Environment(ProjectViewValue.$projectID) var projectID
|
||||||
|
|
||||||
// let projectID: Project.ID
|
|
||||||
let rooms: [DuctSizing.RoomContainer]
|
let rooms: [DuctSizing.RoomContainer]
|
||||||
|
let trunks: [DuctSizing.TrunkContainer]
|
||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
|
div(.class("space-y-4")) {
|
||||||
|
PageTitleRow {
|
||||||
div {
|
div {
|
||||||
PageTitle { "Duct Sizes" }
|
PageTitle("Duct Sizes")
|
||||||
if rooms.count == 0 {
|
|
||||||
p(.class("text-error italic")) {
|
|
||||||
"Must complete all the previous sections to display duct sizing calculations."
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
RoomsTable(projectID: projectID, rooms: rooms)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RoomsTable: HTML, Sendable {
|
Alert(
|
||||||
let projectID: Project.ID
|
"""
|
||||||
let rooms: [DuctSizing.RoomContainer]
|
Must complete all the previous sections to display duct sizing calculations.
|
||||||
|
"""
|
||||||
var body: some HTML<HTMLTag.div> {
|
|
||||||
div(.class("overflow-x-auto")) {
|
|
||||||
table(.class("table table-zebra")) {
|
|
||||||
thead {
|
|
||||||
tr(.class("text-xl text-gray-400 font-bold")) {
|
|
||||||
th { "ID" }
|
|
||||||
th { "Name" }
|
|
||||||
th { "H-BTU" }
|
|
||||||
th { "C-BTU" }
|
|
||||||
th(.class("hidden 2xl:table-cell")) { "Htg CFM" }
|
|
||||||
th(.class("hidden 2xl:table-cell")) { "Clg CFM" }
|
|
||||||
th { "Dsn CFM" }
|
|
||||||
th(.class("hidden 2xl:table-cell")) { "Round Size" }
|
|
||||||
th { "Velocity" }
|
|
||||||
th { "Final Size" }
|
|
||||||
th { "Flex Size" }
|
|
||||||
th { "Width" }
|
|
||||||
th { "Height" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tbody {
|
|
||||||
for room in rooms {
|
|
||||||
RoomRow(projectID: projectID, room: room)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RoomRow: HTML, Sendable {
|
|
||||||
let projectID: Project.ID
|
|
||||||
let room: DuctSizing.RoomContainer
|
|
||||||
|
|
||||||
var route: String {
|
|
||||||
SiteRoute.View.router.path(
|
|
||||||
for: .project(.detail(projectID, .ductSizing(.index)))
|
|
||||||
)
|
)
|
||||||
.appendingPath("room")
|
.hidden(when: rooms.count > 0)
|
||||||
.appendingPath(room.roomID)
|
.attributes(.class("text-error font-bold italic mt-4"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some HTML<HTMLTag.tr> {
|
if rooms.count != 0 {
|
||||||
tr(.class("text-lg items-baseline"), .id(room.roomID.idString)) {
|
RoomsTable(rooms: rooms)
|
||||||
td { room.registerID }
|
|
||||||
td { room.roomName }
|
PageTitleRow {
|
||||||
td { Number(room.heatingLoad, digits: 0) }
|
PageTitle {
|
||||||
td { Number(room.coolingLoad, digits: 0) }
|
"Trunk / Runout Sizes"
|
||||||
td(.class("hidden 2xl:table-cell")) { Number(room.heatingCFM, digits: 0) }
|
|
||||||
td(.class("hidden 2xl:table-cell")) { Number(room.coolingCFM, digits: 0) }
|
|
||||||
td {
|
|
||||||
Badge(number: room.designCFM.value, digits: 0)
|
|
||||||
.attributes(.class("badge-\(room.designCFM.color)"))
|
|
||||||
}
|
}
|
||||||
td(.class("hidden 2xl:table-cell")) { Number(room.roundSize, digits: 1) }
|
|
||||||
td { Number(room.velocity) }
|
PlusButton()
|
||||||
td {
|
|
||||||
Badge(number: room.finalSize)
|
|
||||||
.attributes(.class("badge-secondary"))
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
Number(room.flexSize)
|
|
||||||
.attributes(.class("badge badge-outline badge-primary text-xl font-bold"))
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
if let width = room.rectangularWidth {
|
|
||||||
Number(width)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
div(.class("flex justify-between items-center space-x-4")) {
|
|
||||||
div(.id("height_\(room.roomID.idString)"), .class("h-full my-auto")) {
|
|
||||||
if let height = room.rectangularSize?.height {
|
|
||||||
Number(height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
div(.class("join")) {
|
|
||||||
// FIX: Delete rectangular size from room.
|
|
||||||
TrashButton()
|
|
||||||
.attributes(.class("join-item btn-ghost"))
|
|
||||||
.attributes(
|
.attributes(
|
||||||
.hx.delete(
|
.class("btn-primary"),
|
||||||
route: .project(
|
.showModal(id: TrunkSizeForm.id())
|
||||||
.detail(
|
|
||||||
projectID,
|
|
||||||
.ductSizing(
|
|
||||||
.deleteRectangularSize(
|
|
||||||
room.roomID,
|
|
||||||
room.rectangularSize?.id ?? .init())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
.hx.target("closest tr"),
|
|
||||||
.hx.swap(.outerHTML),
|
|
||||||
when: room.rectangularSize != nil
|
|
||||||
)
|
)
|
||||||
|
.tooltip("Add trunk / runout")
|
||||||
|
}
|
||||||
|
|
||||||
EditButton()
|
if trunks.count > 0 {
|
||||||
.attributes(
|
TrunkTable(trunks: trunks, rooms: rooms)
|
||||||
.class("join-item btn-ghost"),
|
|
||||||
.showModal(id: RectangularSizeForm.id(room))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RectangularSizeForm(projectID: projectID, room: room)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TrunkSizeForm(rooms: rooms, dismiss: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DuctSizing.DesignCFM {
|
|
||||||
var color: String {
|
|
||||||
switch self {
|
|
||||||
case .heating: return "error"
|
|
||||||
case .cooling: return "info"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,56 +5,25 @@ import Styleguide
|
|||||||
|
|
||||||
struct RectangularSizeForm: HTML, Sendable {
|
struct RectangularSizeForm: HTML, Sendable {
|
||||||
|
|
||||||
static func id(_ roomID: Room.ID? = nil) -> String {
|
|
||||||
let base = "rectangularSizeForm"
|
|
||||||
guard let roomID else { return base }
|
|
||||||
return "\(base)_\(roomID.idString)"
|
|
||||||
}
|
|
||||||
|
|
||||||
static func id(_ room: DuctSizing.RoomContainer) -> String {
|
static func id(_ room: DuctSizing.RoomContainer) -> String {
|
||||||
return id(room.roomID)
|
let base = "rectangularSize"
|
||||||
|
return "\(base)_\(room.roomName.idString)"
|
||||||
}
|
}
|
||||||
|
|
||||||
let projectID: Project.ID
|
@Environment(ProjectViewValue.$projectID) var projectID
|
||||||
let roomID: Room.ID
|
|
||||||
let rectangularSizeID: DuctSizing.RectangularDuct.ID?
|
let id: String
|
||||||
let register: Int
|
let room: DuctSizing.RoomContainer
|
||||||
let height: Int?
|
|
||||||
let dismiss: Bool
|
let dismiss: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
projectID: Project.ID,
|
id: String? = nil,
|
||||||
roomID: Room.ID,
|
|
||||||
rectangularSizeID: DuctSizing.RectangularDuct.ID? = nil,
|
|
||||||
register: Int,
|
|
||||||
height: Int? = nil,
|
|
||||||
dismiss: Bool = true
|
|
||||||
) {
|
|
||||||
self.projectID = projectID
|
|
||||||
self.roomID = roomID
|
|
||||||
self.rectangularSizeID = rectangularSizeID
|
|
||||||
self.register = register
|
|
||||||
self.height = height
|
|
||||||
self.dismiss = dismiss
|
|
||||||
}
|
|
||||||
|
|
||||||
init(
|
|
||||||
projectID: Project.ID,
|
|
||||||
room: DuctSizing.RoomContainer,
|
room: DuctSizing.RoomContainer,
|
||||||
dismiss: Bool = true
|
dismiss: Bool = true
|
||||||
) {
|
) {
|
||||||
let register =
|
self.id = Self.id(room)
|
||||||
room.rectangularSize?.register
|
self.room = room
|
||||||
?? (Int("\(room.roomName.last!)") ?? 1)
|
self.dismiss = dismiss
|
||||||
|
|
||||||
self.init(
|
|
||||||
projectID: projectID,
|
|
||||||
roomID: room.roomID,
|
|
||||||
rectangularSizeID: room.rectangularSize?.id,
|
|
||||||
register: register,
|
|
||||||
height: room.rectangularSize?.height,
|
|
||||||
dismiss: dismiss
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var route: String {
|
var route: String {
|
||||||
@@ -62,26 +31,41 @@ struct RectangularSizeForm: HTML, Sendable {
|
|||||||
for: .project(.detail(projectID, .ductSizing(.index)))
|
for: .project(.detail(projectID, .ductSizing(.index)))
|
||||||
)
|
)
|
||||||
.appendingPath("room")
|
.appendingPath("room")
|
||||||
.appendingPath(roomID)
|
.appendingPath(room.roomID)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some HTML {
|
var rowID: String {
|
||||||
ModalForm(id: Self.id(roomID), dismiss: dismiss) {
|
DuctSizingView.RoomRow.id(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
var height: Int? {
|
||||||
|
room.rectangularSize?.height
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some HTML<HTMLTag.dialog> {
|
||||||
|
ModalForm(id: id, dismiss: dismiss) {
|
||||||
h1(.class("text-lg pb-6")) { "Rectangular Size" }
|
h1(.class("text-lg pb-6")) { "Rectangular Size" }
|
||||||
|
|
||||||
form(
|
form(
|
||||||
.class("space-y-4"),
|
.class("space-y-4"),
|
||||||
.hx.post(route),
|
.hx.post(route),
|
||||||
.hx.target("closest tr"),
|
.hx.target("#\(rowID)"),
|
||||||
.hx.swap(.outerHTML)
|
.hx.swap(.outerHTML)
|
||||||
) {
|
) {
|
||||||
input(.class("hidden"), .name("register"), .value(register))
|
input(.class("hidden"), .name("register"), .value(room.roomRegister))
|
||||||
input(.class("hidden"), .name("id"), .value(rectangularSizeID))
|
input(.class("hidden"), .name("id"), .value(room.rectangularSize?.id))
|
||||||
|
|
||||||
Input(id: "height", placeholder: "Height")
|
LabeledInput(
|
||||||
.attributes(.type(.number), .min("0"), .value(height), .required, .autofocus)
|
"Height",
|
||||||
|
.name("height"),
|
||||||
|
.type(.number),
|
||||||
|
.value(height),
|
||||||
|
.placeholder("8"),
|
||||||
|
.min("0"),
|
||||||
|
.required,
|
||||||
|
.autofocus
|
||||||
|
)
|
||||||
|
|
||||||
SubmitButton()
|
SubmitButton()
|
||||||
.attributes(.class("btn-block"))
|
.attributes(.class("btn-block"))
|
||||||
|
|||||||
167
Sources/ViewController/Views/DuctSizing/RoomsTable.swift
Normal file
167
Sources/ViewController/Views/DuctSizing/RoomsTable.swift
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import Elementary
|
||||||
|
import ElementaryHTMX
|
||||||
|
import Foundation
|
||||||
|
import ManualDCore
|
||||||
|
import Styleguide
|
||||||
|
|
||||||
|
extension DuctSizingView {
|
||||||
|
// TODO: Remove register ID.
|
||||||
|
struct RoomsTable: HTML, Sendable {
|
||||||
|
@Environment(ProjectViewValue.$projectID) var projectID
|
||||||
|
|
||||||
|
let rooms: [DuctSizing.RoomContainer]
|
||||||
|
|
||||||
|
var body: some HTML<HTMLTag.table> {
|
||||||
|
|
||||||
|
table(.class("table table-zebra text-lg")) {
|
||||||
|
thead {
|
||||||
|
tr(.class("text-lg")) {
|
||||||
|
th { "Name" }
|
||||||
|
th { "BTU" }
|
||||||
|
th { "CFM" }
|
||||||
|
th { "Velocity" }
|
||||||
|
th(.class("w-[330px]")) { "Size" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
for room in rooms {
|
||||||
|
RoomRow(room: room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RoomRow: HTML, Sendable {
|
||||||
|
|
||||||
|
static func id(_ room: DuctSizing.RoomContainer) -> String {
|
||||||
|
"roomRow_\(room.roomName.idString)"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Environment(ProjectViewValue.$projectID) var projectID
|
||||||
|
|
||||||
|
let room: DuctSizing.RoomContainer
|
||||||
|
let formID = UUID().idString
|
||||||
|
|
||||||
|
var deleteRoute: String {
|
||||||
|
guard let id = room.rectangularSize?.id else { return "" }
|
||||||
|
|
||||||
|
return SiteRoute.View.router.path(
|
||||||
|
for: .project(
|
||||||
|
.detail(
|
||||||
|
projectID,
|
||||||
|
.ductSizing(
|
||||||
|
.deleteRectangularSize(
|
||||||
|
room.roomID,
|
||||||
|
.init(rectangularSizeID: id, register: room.roomRegister)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rowID: String { Self.id(room) }
|
||||||
|
|
||||||
|
var body: some HTML<HTMLTag.tr> {
|
||||||
|
tr(.class("text-lg"), .id(rowID)) {
|
||||||
|
td { room.roomName }
|
||||||
|
td {
|
||||||
|
div(.class("flex flex-wrap grid grid-cols-2 gap-2")) {
|
||||||
|
span(.class("label")) { "Heating" }
|
||||||
|
Number(room.heatingLoad, digits: 0)
|
||||||
|
|
||||||
|
span(.class("label")) { "Cooling" }
|
||||||
|
Number(room.coolingLoad, digits: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
div(.class("flex flex-wrap grid grid-cols-2 gap-2")) {
|
||||||
|
|
||||||
|
span(.class("label")) { "Design" }
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
Badge(number: room.designCFM.value, digits: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
span(.class("label")) { "Heating" }
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
Number(room.heatingCFM, digits: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
span(.class("label")) { "Cooling" }
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
Number(room.coolingCFM, digits: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td { Number(room.velocity) }
|
||||||
|
|
||||||
|
td {
|
||||||
|
div(.class("grid grid-cols-3 gap-2 w-[330px]")) {
|
||||||
|
|
||||||
|
div(.class("label")) { "Calculated" }
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
Badge(number: room.roundSize, digits: 2)
|
||||||
|
}
|
||||||
|
div {}
|
||||||
|
|
||||||
|
div(.class("label")) { "Final" }
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
Badge(number: room.finalSize)
|
||||||
|
.attributes(.class("badge-secondary"))
|
||||||
|
}
|
||||||
|
div {}
|
||||||
|
|
||||||
|
div(.class("label")) { "Flex" }
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
Badge(number: room.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
|
||||||
|
{
|
||||||
|
Badge {
|
||||||
|
span { "\(width) x \(height)" }
|
||||||
|
}
|
||||||
|
.attributes(.class("badge-info"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div(.class("flex justify-end")) {
|
||||||
|
div(.class("join")) {
|
||||||
|
if room.rectangularSize != nil {
|
||||||
|
Tooltip("Delete Size", position: .bottom) {
|
||||||
|
TrashButton()
|
||||||
|
.attributes(.class("join-item btn-ghost"))
|
||||||
|
.attributes(
|
||||||
|
.hx.delete(deleteRoute),
|
||||||
|
.hx.target("#\(rowID)"),
|
||||||
|
.hx.swap(.outerHTML),
|
||||||
|
when: room.rectangularSize != nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Tooltip("Edit Size", position: .bottom) {
|
||||||
|
EditButton()
|
||||||
|
.attributes(
|
||||||
|
.class("join-item btn-ghost"),
|
||||||
|
.showModal(id: RectangularSizeForm.id(room))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RectangularSizeForm(room: room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
131
Sources/ViewController/Views/DuctSizing/TrunkSizeForm.swift
Normal file
131
Sources/ViewController/Views/DuctSizing/TrunkSizeForm.swift
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import Elementary
|
||||||
|
import ElementaryHTMX
|
||||||
|
import ManualDCore
|
||||||
|
import Styleguide
|
||||||
|
|
||||||
|
struct TrunkSizeForm: HTML, Sendable {
|
||||||
|
|
||||||
|
static func id(_ trunk: DuctSizing.TrunkContainer? = nil) -> String {
|
||||||
|
let base = "trunkSizeForm"
|
||||||
|
guard let trunk else { return base }
|
||||||
|
return "\(base)_\(trunk.id.idString)"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Environment(ProjectViewValue.$projectID) var projectID
|
||||||
|
|
||||||
|
let container: DuctSizing.TrunkContainer?
|
||||||
|
let rooms: [DuctSizing.RoomContainer]
|
||||||
|
let dismiss: Bool
|
||||||
|
|
||||||
|
var trunk: DuctSizing.TrunkSize? {
|
||||||
|
container?.trunk
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
trunk: DuctSizing.TrunkContainer? = nil,
|
||||||
|
rooms: [DuctSizing.RoomContainer],
|
||||||
|
dismiss: Bool = true
|
||||||
|
) {
|
||||||
|
self.container = trunk
|
||||||
|
self.rooms = rooms
|
||||||
|
self.dismiss = dismiss
|
||||||
|
}
|
||||||
|
|
||||||
|
var route: String {
|
||||||
|
SiteRoute.View.router
|
||||||
|
.path(for: .project(.detail(projectID, .ductSizing(.index))))
|
||||||
|
.appendingPath(SiteRoute.View.ProjectRoute.DuctSizingRoute.TrunkRoute.rootPath)
|
||||||
|
.appendingPath(trunk?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some HTML {
|
||||||
|
ModalForm(id: Self.id(container), dismiss: dismiss) {
|
||||||
|
h1(.class("text-lg font-bold mb-4")) { "Trunk / Runout Size" }
|
||||||
|
form(
|
||||||
|
.class("space-y-4"),
|
||||||
|
trunk == nil
|
||||||
|
? .hx.post(route)
|
||||||
|
: .hx.patch(route),
|
||||||
|
.hx.target("body"),
|
||||||
|
.hx.swap(.outerHTML)
|
||||||
|
) {
|
||||||
|
|
||||||
|
input(.class("hidden"), .name("projectID"), .value(projectID))
|
||||||
|
|
||||||
|
div(.class("grid grid-cols-1 md:grid-cols-2 gap-4")) {
|
||||||
|
label(.class("select w-full")) {
|
||||||
|
span(.class("label")) { "Type" }
|
||||||
|
select(.name("type")) {
|
||||||
|
for type in DuctSizing.TrunkSize.TrunkType.allCases {
|
||||||
|
option(.value(type.rawValue)) { type.rawValue.capitalized }
|
||||||
|
.attributes(.selected, when: trunk?.type == type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LabeledInput(
|
||||||
|
"Height",
|
||||||
|
.type(.text),
|
||||||
|
.name("height"),
|
||||||
|
.value(trunk?.height),
|
||||||
|
.placeholder("8 (Optional)"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LabeledInput(
|
||||||
|
"Name",
|
||||||
|
.type(.text),
|
||||||
|
.name("name"),
|
||||||
|
.value(trunk?.name),
|
||||||
|
.placeholder("Trunk-1 (Optional)")
|
||||||
|
)
|
||||||
|
|
||||||
|
div {
|
||||||
|
h2(.class("label font-bold col-span-3 mb-6")) { "Associated Supply Runs" }
|
||||||
|
div(
|
||||||
|
.class(
|
||||||
|
"""
|
||||||
|
grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 justify-center items-center gap-4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
for room in rooms {
|
||||||
|
div(.class("block grow")) {
|
||||||
|
div(.class("grid grid-cols-1 space-y-1")) {
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
p(.class("label")) { room.roomName }
|
||||||
|
}
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
input(
|
||||||
|
.class("checkbox"),
|
||||||
|
.type(.checkbox),
|
||||||
|
.name("rooms"),
|
||||||
|
.value("\(room.roomID)_\(room.roomRegister)")
|
||||||
|
)
|
||||||
|
.attributes(
|
||||||
|
.checked,
|
||||||
|
when: trunk == nil ? false : trunk!.rooms.hasRoom(room)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SubmitButton()
|
||||||
|
.attributes(.class("btn-block mt-6"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Array where Element == DuctSizing.TrunkSize.RoomProxy {
|
||||||
|
func hasRoom(_ room: DuctSizing.RoomContainer) -> Bool {
|
||||||
|
first {
|
||||||
|
$0.id == room.roomID
|
||||||
|
&& $0.registers.contains(room.roomRegister)
|
||||||
|
} != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
152
Sources/ViewController/Views/DuctSizing/TrunksTable.swift
Normal file
152
Sources/ViewController/Views/DuctSizing/TrunksTable.swift
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import Elementary
|
||||||
|
import ElementaryHTMX
|
||||||
|
import ManualDCore
|
||||||
|
import Styleguide
|
||||||
|
|
||||||
|
extension DuctSizingView {
|
||||||
|
|
||||||
|
struct TrunkTable: HTML, Sendable {
|
||||||
|
|
||||||
|
let trunks: [DuctSizing.TrunkContainer]
|
||||||
|
let rooms: [DuctSizing.RoomContainer]
|
||||||
|
|
||||||
|
private var sortedTrunks: [DuctSizing.TrunkContainer] {
|
||||||
|
trunks
|
||||||
|
.sorted(by: { $0.designCFM.value > $1.designCFM.value })
|
||||||
|
.sorted(by: { $0.type.rawValue > $1.type.rawValue })
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some HTML {
|
||||||
|
table(.class("table table-zebra text-lg")) {
|
||||||
|
thead {
|
||||||
|
tr(.class("text-lg")) {
|
||||||
|
th { "Name / Type" }
|
||||||
|
th { "Associated Supplies" }
|
||||||
|
th { "Dsn CFM" }
|
||||||
|
th { "Velocity" }
|
||||||
|
th(.class("w-[330px]")) { "Size" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
for trunk in sortedTrunks {
|
||||||
|
TrunkRow(trunk: trunk, rooms: rooms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrunkRow: HTML, Sendable {
|
||||||
|
|
||||||
|
@Environment(ProjectViewValue.$projectID) var projectID
|
||||||
|
|
||||||
|
let trunk: DuctSizing.TrunkContainer
|
||||||
|
let rooms: [DuctSizing.RoomContainer]
|
||||||
|
|
||||||
|
var body: some HTML<HTMLTag.tr> {
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
div(.class("grid grid-cols-1 space-y-2")) {
|
||||||
|
if let name = trunk.name {
|
||||||
|
p(.class("w-fit")) { name }
|
||||||
|
}
|
||||||
|
|
||||||
|
Badge {
|
||||||
|
trunk.trunk.type.rawValue
|
||||||
|
}
|
||||||
|
.attributes(.class("badge-info"), when: trunk.type == .supply)
|
||||||
|
.attributes(.class("badge-error"), when: trunk.type == .return)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
div(.class("flex flex-wrap space-x-2 space-y-2")) {
|
||||||
|
for id in registerIDS {
|
||||||
|
Badge { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
Number(trunk.designCFM.value, digits: 0)
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
Number(trunk.velocity)
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
div(.class("grid grid-cols-3 gap-2 w-[330px]")) {
|
||||||
|
div(.class("label")) { "Calculated" }
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
Badge(number: trunk.roundSize, digits: 1)
|
||||||
|
}
|
||||||
|
div {}
|
||||||
|
|
||||||
|
div(.class("label")) { "Final" }
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
Badge(number: trunk.finalSize)
|
||||||
|
.attributes(.class("badge-secondary"))
|
||||||
|
}
|
||||||
|
div {}
|
||||||
|
|
||||||
|
div(.class("label")) { "Flex" }
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
Badge(number: trunk.flexSize)
|
||||||
|
.attributes(.class("badge-primary"))
|
||||||
|
}
|
||||||
|
div {}
|
||||||
|
|
||||||
|
div(.class("label")) { "Rectangular" }
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
if let width = trunk.width,
|
||||||
|
let height = trunk.ductSize.height
|
||||||
|
{
|
||||||
|
Badge {
|
||||||
|
span { "\(width) x \(height)" }
|
||||||
|
}
|
||||||
|
.attributes(.class("badge-info"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div(.class("flex justify-end items-end")) {
|
||||||
|
div(.class("join")) {
|
||||||
|
if trunk.width != nil {
|
||||||
|
TrashButton()
|
||||||
|
.attributes(.class("join-item btn-ghost"))
|
||||||
|
.attributes(
|
||||||
|
.hx.delete(route: deleteRoute),
|
||||||
|
.hx.target("closest tr"),
|
||||||
|
.hx.swap(.outerHTML)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
EditButton()
|
||||||
|
.attributes(
|
||||||
|
.class("join-item btn-ghost"),
|
||||||
|
.showModal(id: TrunkSizeForm.id(trunk))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrunkSizeForm(trunk: trunk, rooms: rooms, dismiss: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var deleteRoute: SiteRoute.View {
|
||||||
|
.project(.detail(projectID, .ductSizing(.trunk(.delete(trunk.id)))))
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ struct EffectiveLengthForm: HTML, Sendable {
|
|||||||
dismiss: dismiss
|
dismiss: dismiss
|
||||||
) {
|
) {
|
||||||
h1(.class("text-2xl font-bold")) { "Effective Length" }
|
h1(.class("text-2xl font-bold")) { "Effective Length" }
|
||||||
div(.id("formStep_\(id)")) {
|
div(.id("formStep_\(id)"), .class("mt-4")) {
|
||||||
StepOne(projectID: projectID, effectiveLength: effectiveLength)
|
StepOne(projectID: projectID, effectiveLength: effectiveLength)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,8 +74,15 @@ struct EffectiveLengthForm: HTML, Sendable {
|
|||||||
if let id = effectiveLength?.id {
|
if let id = effectiveLength?.id {
|
||||||
input(.class("hidden"), .name("id"), .value("\(id)"))
|
input(.class("hidden"), .name("id"), .value("\(id)"))
|
||||||
}
|
}
|
||||||
Input(id: "name", placeholder: "Name")
|
|
||||||
.attributes(.type(.text), .required, .autofocus, .value(effectiveLength?.name))
|
LabeledInput(
|
||||||
|
"Name",
|
||||||
|
.name("name"),
|
||||||
|
.type(.text),
|
||||||
|
.value(effectiveLength?.name),
|
||||||
|
.required,
|
||||||
|
.autofocus
|
||||||
|
)
|
||||||
|
|
||||||
GroupTypeSelect(projectID: projectID, selected: effectiveLength?.type ?? .supply)
|
GroupTypeSelect(projectID: projectID, selected: effectiveLength?.type ?? .supply)
|
||||||
|
|
||||||
@@ -193,12 +200,12 @@ struct EffectiveLengthForm: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div(.class("grid grid-cols-5 gap-2")) {
|
a(
|
||||||
Label("Group")
|
.href("/files/ManD.Groups.pdf"),
|
||||||
Label("Letter")
|
.target(.blank),
|
||||||
Label("Length")
|
.class("btn btn-link")
|
||||||
Label("Quantity")
|
) {
|
||||||
.attributes(.class("col-span-2"))
|
"Click here for Manual-D groups reference."
|
||||||
}
|
}
|
||||||
|
|
||||||
div(.id("groups"), .class("space-y-4")) {
|
div(.id("groups"), .class("space-y-4")) {
|
||||||
@@ -228,12 +235,16 @@ struct StraightLengthField: HTML, Sendable {
|
|||||||
|
|
||||||
var body: some HTML<HTMLTag.div> {
|
var body: some HTML<HTMLTag.div> {
|
||||||
Row {
|
Row {
|
||||||
Input(
|
LabeledInput(
|
||||||
name: "straightLengths",
|
"Length",
|
||||||
placeholder: "Length"
|
.name("straightLengths"),
|
||||||
|
.type(.number),
|
||||||
|
.value(value),
|
||||||
|
.placeholder("10"),
|
||||||
|
.min("0"),
|
||||||
|
.autofocus,
|
||||||
|
.required
|
||||||
)
|
)
|
||||||
.attributes(.type(.number), .min("0"), .autofocus, .required, .value(value))
|
|
||||||
|
|
||||||
TrashButton()
|
TrashButton()
|
||||||
.attributes(.data("remove", value: "true"))
|
.attributes(.data("remove", value: "true"))
|
||||||
}
|
}
|
||||||
@@ -252,18 +263,43 @@ struct GroupField: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
div(.class("grid grid-cols-5 gap-2")) {
|
div(.class("grid grid-cols-3 gap-2 p-2 border rounded-lg shadow-sm")) {
|
||||||
GroupSelect(style: style)
|
GroupSelect(style: style)
|
||||||
Input(name: "group[letter]", placeholder: "Letter")
|
|
||||||
.attributes(.type(.text), .autofocus, .required, .value(group?.letter))
|
LabeledInput(
|
||||||
Input(name: "group[length]", placeholder: "Length")
|
"Letter",
|
||||||
.attributes(.type(.number), .min("0"), .required, .value(group?.value))
|
.name("group[letter]"),
|
||||||
Input(name: "group[quantity]", placeholder: "Quantity")
|
.type(.text),
|
||||||
.attributes(.type(.number), .min("1"), .value("1"), .required, .value(group?.quantity ?? 1))
|
.value(group?.letter),
|
||||||
div(.class("flex justify-end")) {
|
.placeholder("a"),
|
||||||
|
.required
|
||||||
|
)
|
||||||
|
|
||||||
|
LabeledInput(
|
||||||
|
"Length",
|
||||||
|
.name("group[length]"),
|
||||||
|
.type(.number),
|
||||||
|
.value(group?.value),
|
||||||
|
.placeholder("10"),
|
||||||
|
.min("0"),
|
||||||
|
.required
|
||||||
|
)
|
||||||
|
|
||||||
|
LabeledInput(
|
||||||
|
"Quantity",
|
||||||
|
.name("group[quantity]"),
|
||||||
|
.type(.number),
|
||||||
|
.value(group?.quantity ?? 1),
|
||||||
|
.min("1"),
|
||||||
|
.required
|
||||||
|
)
|
||||||
|
.attributes(.class("col-span-2"))
|
||||||
|
|
||||||
TrashButton()
|
TrashButton()
|
||||||
.attributes(.data("remove", value: "true"), .class("mx-2"))
|
.attributes(
|
||||||
}
|
.data("remove", value: "true"),
|
||||||
|
.class("me-2 btn-block")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.attributes(.class("space-x-2"), .hx.ext("remove"))
|
.attributes(.class("space-x-2"), .hx.ext("remove"))
|
||||||
}
|
}
|
||||||
@@ -274,15 +310,18 @@ struct GroupSelect: HTML, Sendable {
|
|||||||
let style: EffectiveLength.EffectiveLengthType
|
let style: EffectiveLength.EffectiveLengthType
|
||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
|
label(.class("select")) {
|
||||||
|
span(.class("label")) { "Group" }
|
||||||
select(
|
select(
|
||||||
.name("group[group]"),
|
.name("group[group]"),
|
||||||
.class("select")
|
.autofocus
|
||||||
) {
|
) {
|
||||||
for value in style.selectOptions {
|
for value in style.selectOptions {
|
||||||
option(.value("\(value)")) { "\(value)" }
|
option(.value("\(value)")) { "\(value)" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,8 +330,10 @@ struct GroupTypeSelect: HTML, Sendable {
|
|||||||
let projectID: Project.ID
|
let projectID: Project.ID
|
||||||
let selected: EffectiveLength.EffectiveLengthType
|
let selected: EffectiveLength.EffectiveLengthType
|
||||||
|
|
||||||
var body: some HTML<HTMLTag.select> {
|
var body: some HTML<HTMLTag.label> {
|
||||||
select(.class("select"), .name("type"), .id("type")) {
|
label(.class("select w-full")) {
|
||||||
|
span(.class("label")) { "Type" }
|
||||||
|
select(.name("type"), .id("type")) {
|
||||||
for value in EffectiveLength.EffectiveLengthType.allCases {
|
for value in EffectiveLength.EffectiveLengthType.allCases {
|
||||||
option(
|
option(
|
||||||
.value("\(value.rawValue)"),
|
.value("\(value.rawValue)"),
|
||||||
@@ -302,6 +343,7 @@ struct GroupTypeSelect: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension EffectiveLength.EffectiveLengthType {
|
extension EffectiveLength.EffectiveLengthType {
|
||||||
|
|
||||||
@@ -312,7 +354,7 @@ extension EffectiveLength.EffectiveLengthType {
|
|||||||
case .return:
|
case .return:
|
||||||
return [5, 6, 7, 8, 10, 11, 12]
|
return [5, 6, 7, 8, 10, 11, 12]
|
||||||
case .supply:
|
case .supply:
|
||||||
return [1, 2, 4, 8, 9, 11, 12]
|
return [1, 2, 3, 4, 8, 9, 11, 12]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import Elementary
|
||||||
|
import ElementaryHTMX
|
||||||
|
import ManualDCore
|
||||||
|
import Styleguide
|
||||||
|
|
||||||
|
struct EffectiveLengthsTable: HTML, Sendable {
|
||||||
|
|
||||||
|
let effectiveLengths: [EffectiveLength]
|
||||||
|
|
||||||
|
private var sortedLengths: [EffectiveLength] {
|
||||||
|
effectiveLengths.sorted {
|
||||||
|
$0.totalEquivalentLength > $1.totalEquivalentLength
|
||||||
|
}
|
||||||
|
.sorted {
|
||||||
|
$0.type.rawValue > $1.type.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some HTML<HTMLTag.table> {
|
||||||
|
table(.class("table table-zebra text-lg")) {
|
||||||
|
thead {
|
||||||
|
tr(.class("text-lg")) {
|
||||||
|
th { "Type" }
|
||||||
|
th { "Name" }
|
||||||
|
th { "Straight Lengths" }
|
||||||
|
th {
|
||||||
|
div(.class("grid grid-cols-3 gap-2 min-w-[220px]")) {
|
||||||
|
div(.class("flex justify-center col-span-3")) {
|
||||||
|
"Groups"
|
||||||
|
}
|
||||||
|
div { "Group" }
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
"T.E.L."
|
||||||
|
}
|
||||||
|
div(.class("flex justify-end")) {
|
||||||
|
"Quantity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
div(.class("flex justify-end me-[140px]")) {
|
||||||
|
"T.E.L."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
for row in sortedLengths {
|
||||||
|
EffectiveLenghtRow(effectiveLength: row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EffectiveLenghtRow: HTML, Sendable {
|
||||||
|
|
||||||
|
let effectiveLength: EffectiveLength
|
||||||
|
|
||||||
|
private var deleteRoute: SiteRoute.View {
|
||||||
|
.project(
|
||||||
|
.detail(
|
||||||
|
effectiveLength.projectID,
|
||||||
|
.equivalentLength(.delete(id: effectiveLength.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some HTML<HTMLTag.tr> {
|
||||||
|
tr(.id(effectiveLength.id.idString)) {
|
||||||
|
td {
|
||||||
|
// Type
|
||||||
|
Badge {
|
||||||
|
span { effectiveLength.type.rawValue }
|
||||||
|
}
|
||||||
|
.attributes(.class("badge-info"), when: effectiveLength.type == .supply)
|
||||||
|
.attributes(.class("badge-error"), when: effectiveLength.type == .return)
|
||||||
|
|
||||||
|
}
|
||||||
|
td { effectiveLength.name }
|
||||||
|
td {
|
||||||
|
// Lengths
|
||||||
|
div(.class("grid grid-cols-1 gap-2")) {
|
||||||
|
for length in effectiveLength.straightLengths {
|
||||||
|
Number(length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
div(.class("grid grid-cols-3 gap-2 min-w-[220px]")) {
|
||||||
|
for group in effectiveLength.groups {
|
||||||
|
span { "\(group.group)-\(group.letter)" }
|
||||||
|
div(.class("flex justify-center")) {
|
||||||
|
Number(group.value)
|
||||||
|
}
|
||||||
|
div(.class("flex justify-end")) {
|
||||||
|
Number(group.quantity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
// Total
|
||||||
|
// Row {
|
||||||
|
div(.class("flex justify-end mx-auto space-x-4")) {
|
||||||
|
Badge(number: effectiveLength.totalEquivalentLength, digits: 0)
|
||||||
|
.attributes(.class("badge-primary badge-lg pt-2"))
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
div(.class("flex justify-end -mt-2")) {
|
||||||
|
div(.class("join")) {
|
||||||
|
TrashButton()
|
||||||
|
.attributes(
|
||||||
|
.class("join-item btn-ghost"),
|
||||||
|
.hx.delete(route: deleteRoute),
|
||||||
|
.hx.confirm("Are you sure?"),
|
||||||
|
.hx.target("#\(effectiveLength.id.idString)"),
|
||||||
|
.hx.swap(.outerHTML)
|
||||||
|
)
|
||||||
|
.tooltip("Delete", position: .bottom)
|
||||||
|
|
||||||
|
EditButton()
|
||||||
|
.attributes(
|
||||||
|
.class("join-item btn-ghost"),
|
||||||
|
.showModal(id: EffectiveLengthForm.id(effectiveLength))
|
||||||
|
)
|
||||||
|
.tooltip("Edit", position: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EffectiveLengthForm(effectiveLength: effectiveLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,13 +3,10 @@ import ElementaryHTMX
|
|||||||
import ManualDCore
|
import ManualDCore
|
||||||
import Styleguide
|
import Styleguide
|
||||||
|
|
||||||
// TODO: Group into grids of supply / return.
|
|
||||||
|
|
||||||
struct EffectiveLengthsView: HTML, Sendable {
|
struct EffectiveLengthsView: HTML, Sendable {
|
||||||
|
|
||||||
@Environment(ProjectViewValue.$projectID) var projectID
|
@Environment(ProjectViewValue.$projectID) var projectID
|
||||||
|
|
||||||
// let projectID: Project.ID
|
|
||||||
let effectiveLengths: [EffectiveLength]
|
let effectiveLengths: [EffectiveLength]
|
||||||
|
|
||||||
var supplies: [EffectiveLength] {
|
var supplies: [EffectiveLength] {
|
||||||
@@ -24,141 +21,21 @@ struct EffectiveLengthsView: HTML, Sendable {
|
|||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
div(.class("space-y-4")) {
|
div(.class("space-y-4")) {
|
||||||
Row {
|
PageTitleRow {
|
||||||
PageTitle { "Equivalent Lengths" }
|
PageTitle { "Equivalent Lengths" }
|
||||||
PlusButton()
|
PlusButton()
|
||||||
.attributes(
|
.attributes(
|
||||||
.class("btn-ghost"),
|
.class("btn-primary"),
|
||||||
.showModal(id: EffectiveLengthForm.id(nil))
|
.showModal(id: EffectiveLengthForm.id(nil))
|
||||||
)
|
)
|
||||||
|
.tooltip("Add equivalent length")
|
||||||
}
|
}
|
||||||
.attributes(.class("pb-6"))
|
.attributes(.class("pb-6"))
|
||||||
|
|
||||||
EffectiveLengthForm(projectID: projectID, dismiss: true)
|
EffectiveLengthForm(projectID: projectID, dismiss: true)
|
||||||
|
|
||||||
div {
|
EffectiveLengthsTable(effectiveLengths: effectiveLengths)
|
||||||
h2(.class("text-xl font-bold pb-4")) { "Supplies" }
|
|
||||||
.attributes(.class("hidden"), when: supplies.count == 0)
|
|
||||||
|
|
||||||
div(.class("grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4")) {
|
|
||||||
for (n, row) in supplies.enumerated() {
|
|
||||||
EffectiveLengthView(effectiveLength: row)
|
|
||||||
.attributes(.class(n == 0 ? "border-primary" : "border-gray-200"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
h2(.class("text-xl font-bold pb-4")) { "Returns" }
|
|
||||||
.attributes(.class("hidden"), when: returns.count == 0)
|
|
||||||
div(.class("grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 space-x-4 space-y-4")) {
|
|
||||||
for (n, row) in returns.enumerated() {
|
|
||||||
EffectiveLengthView(effectiveLength: row)
|
|
||||||
.attributes(.class(n == 0 ? "border-secondary" : "border-gray-200"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct EffectiveLengthView: HTML, Sendable {
|
|
||||||
|
|
||||||
let effectiveLength: EffectiveLength
|
|
||||||
|
|
||||||
var straightLengthsTotal: Int {
|
|
||||||
effectiveLength.straightLengths
|
|
||||||
.reduce(into: 0) { $0 += $1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
var groupsTotal: Double {
|
|
||||||
effectiveLength.groups.totalEquivalentLength
|
|
||||||
}
|
|
||||||
|
|
||||||
var id: String {
|
|
||||||
return "effectiveLenghtCard_\(effectiveLength.id.uuidString.replacing("-", with: ""))"
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some HTML<HTMLTag.div> {
|
|
||||||
div(
|
|
||||||
.class("card h-full bg-base-100 shadow-sm border rounded-lg"),
|
|
||||||
.id(id)
|
|
||||||
) {
|
|
||||||
div(.class("card-body")) {
|
|
||||||
Row {
|
|
||||||
h2 { effectiveLength.name }
|
|
||||||
div(
|
|
||||||
.class("space-x-4")
|
|
||||||
) {
|
|
||||||
span(.class("text-sm italic")) {
|
|
||||||
"Total"
|
|
||||||
}
|
|
||||||
.attributes(.class("text-primary"), when: effectiveLength.type == .supply)
|
|
||||||
.attributes(.class("text-secondary"), when: effectiveLength.type == .return)
|
|
||||||
|
|
||||||
Number(self.effectiveLength.totalEquivalentLength, digits: 0)
|
|
||||||
.attributes(.class("badge badge-outline text-lg"))
|
|
||||||
.attributes(
|
|
||||||
.class("badge-primary"), when: effectiveLength.type == .supply
|
|
||||||
)
|
|
||||||
.attributes(
|
|
||||||
.class("badge-secondary"), when: effectiveLength.type == .return
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.attributes(.class("card-title pb-6"))
|
|
||||||
|
|
||||||
Label("Straight Lengths")
|
|
||||||
|
|
||||||
for length in effectiveLength.straightLengths {
|
|
||||||
div(.class("flex justify-end")) {
|
|
||||||
Number(length)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
Label("Groups")
|
|
||||||
Label("Equivalent Length")
|
|
||||||
Label("Quantity")
|
|
||||||
}
|
|
||||||
.attributes(.class("border-b border-gray-200"))
|
|
||||||
|
|
||||||
for group in effectiveLength.groups {
|
|
||||||
Row {
|
|
||||||
span { "\(group.group)-\(group.letter)" }
|
|
||||||
Number(group.value)
|
|
||||||
Number(group.quantity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
div(.class("card-actions justify-end pt-6 space-y-4 mt-auto")) {
|
|
||||||
div(.class("join")) {
|
|
||||||
TrashButton()
|
|
||||||
.attributes(
|
|
||||||
.class("join-item btn-ghost"),
|
|
||||||
.hx.delete(
|
|
||||||
route: .project(
|
|
||||||
.detail(
|
|
||||||
effectiveLength.projectID,
|
|
||||||
.equivalentLength(.delete(id: effectiveLength.id))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
.hx.confirm("Are you sure?"),
|
|
||||||
.hx.target("#\(id)"),
|
|
||||||
.hx.swap(.outerHTML)
|
|
||||||
)
|
|
||||||
EditButton()
|
|
||||||
.attributes(
|
|
||||||
.class("join-item btn-ghost"),
|
|
||||||
.showModal(id: EffectiveLengthForm.id(effectiveLength))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EffectiveLengthForm(effectiveLength: effectiveLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ struct EquipmentInfoForm: HTML, Sendable {
|
|||||||
|
|
||||||
static let id = "equipmentForm"
|
static let id = "equipmentForm"
|
||||||
|
|
||||||
|
@Environment(ProjectViewValue.$projectID) var projectID
|
||||||
|
|
||||||
let dismiss: Bool
|
let dismiss: Bool
|
||||||
let projectID: Project.ID
|
|
||||||
let equipmentInfo: EquipmentInfo?
|
let equipmentInfo: EquipmentInfo?
|
||||||
|
|
||||||
var staticPressure: String {
|
var staticPressure: String {
|
||||||
@@ -33,7 +34,7 @@ struct EquipmentInfoForm: HTML, Sendable {
|
|||||||
equipmentInfo != nil
|
equipmentInfo != nil
|
||||||
? .hx.patch(route)
|
? .hx.patch(route)
|
||||||
: .hx.post(route),
|
: .hx.post(route),
|
||||||
.hx.target("#equipmentInfo"),
|
.hx.target("body"),
|
||||||
.hx.swap(.outerHTML)
|
.hx.swap(.outerHTML)
|
||||||
) {
|
) {
|
||||||
input(.class("hidden"), .name("projectID"), .value("\(projectID)"))
|
input(.class("hidden"), .name("projectID"), .value("\(projectID)"))
|
||||||
|
|||||||
@@ -12,16 +12,15 @@ struct EquipmentInfoView: HTML, Sendable {
|
|||||||
.id("equipmentInfo")
|
.id("equipmentInfo")
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Row {
|
PageTitleRow {
|
||||||
PageTitle { "Equipment Info" }
|
PageTitle { "Equipment Details" }
|
||||||
|
|
||||||
Tooltip("Edit equipment info") {
|
|
||||||
EditButton()
|
EditButton()
|
||||||
.attributes(
|
.attributes(
|
||||||
.class("btn-ghost"),
|
.class("btn-primary"),
|
||||||
.showModal(id: EquipmentInfoForm.id)
|
.showModal(id: EquipmentInfoForm.id)
|
||||||
)
|
)
|
||||||
}
|
.tooltip("Edit equipment details")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let equipmentInfo {
|
if let equipmentInfo {
|
||||||
@@ -29,7 +28,7 @@ struct EquipmentInfoView: HTML, Sendable {
|
|||||||
table(.class("table table-zebra")) {
|
table(.class("table table-zebra")) {
|
||||||
tbody(.class("text-lg")) {
|
tbody(.class("text-lg")) {
|
||||||
tr {
|
tr {
|
||||||
td { span { "Static Pressure" } }
|
td { Label { "Static Pressure" } }
|
||||||
td {
|
td {
|
||||||
div(.class("flex justify-end")) {
|
div(.class("flex justify-end")) {
|
||||||
Number(equipmentInfo.staticPressure)
|
Number(equipmentInfo.staticPressure)
|
||||||
@@ -37,7 +36,7 @@ struct EquipmentInfoView: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
td { span { "Heating CFM" } }
|
td { Label { "Heating CFM" } }
|
||||||
td {
|
td {
|
||||||
div(.class("flex justify-end")) {
|
div(.class("flex justify-end")) {
|
||||||
Number(equipmentInfo.heatingCFM)
|
Number(equipmentInfo.heatingCFM)
|
||||||
@@ -45,7 +44,7 @@ struct EquipmentInfoView: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
td { span { "Cooling CFM" } }
|
td { Label { "Cooling CFM" } }
|
||||||
td {
|
td {
|
||||||
div(.class("flex justify-end")) {
|
div(.class("flex justify-end")) {
|
||||||
Number(equipmentInfo.coolingCFM)
|
Number(equipmentInfo.coolingCFM)
|
||||||
@@ -56,7 +55,8 @@ struct EquipmentInfoView: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
EquipmentInfoForm(
|
EquipmentInfoForm(
|
||||||
dismiss: true, projectID: projectID, equipmentInfo: equipmentInfo
|
dismiss: equipmentInfo != nil,
|
||||||
|
equipmentInfo: equipmentInfo
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import ManualDClient
|
|||||||
import ManualDCore
|
import ManualDCore
|
||||||
import Styleguide
|
import Styleguide
|
||||||
|
|
||||||
// FIX: Need to update available static, etc. when equipment info is submitted.
|
|
||||||
|
|
||||||
struct FrictionRateView: HTML, Sendable {
|
struct FrictionRateView: HTML, Sendable {
|
||||||
|
|
||||||
@Environment(ProjectViewValue.$projectID) var projectID
|
@Environment(ProjectViewValue.$projectID) var projectID
|
||||||
@@ -13,16 +11,20 @@ struct FrictionRateView: HTML, Sendable {
|
|||||||
let equivalentLengths: EffectiveLength.MaxContainer
|
let equivalentLengths: EffectiveLength.MaxContainer
|
||||||
let frictionRateResponse: ManualDClient.FrictionRateResponse?
|
let frictionRateResponse: ManualDClient.FrictionRateResponse?
|
||||||
|
|
||||||
var availableStaticPressure: Double? {
|
private var availableStaticPressure: Double? {
|
||||||
frictionRateResponse?.availableStaticPressure
|
frictionRateResponse?.availableStaticPressure
|
||||||
}
|
}
|
||||||
|
|
||||||
var frictionRateDesignValue: Double? {
|
private var frictionRateDesignValue: Double? {
|
||||||
frictionRateResponse?.frictionRate
|
frictionRateResponse?.frictionRate
|
||||||
}
|
}
|
||||||
|
|
||||||
var badgeColor: String {
|
private var shouldShowBadges: Bool {
|
||||||
let base = "badge-primary"
|
frictionRateDesignValue != nil || availableStaticPressure != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private var badgeColor: String {
|
||||||
|
let base = "badge-info"
|
||||||
guard let frictionRateDesignValue else { return base }
|
guard let frictionRateDesignValue else { return base }
|
||||||
if frictionRateDesignValue >= 0.18 || frictionRateDesignValue <= 0.02 {
|
if frictionRateDesignValue >= 0.18 || frictionRateDesignValue <= 0.02 {
|
||||||
return "badge-error"
|
return "badge-error"
|
||||||
@@ -30,48 +32,88 @@ struct FrictionRateView: HTML, Sendable {
|
|||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
var showHighErrors: Bool {
|
private var showHighErrors: Bool {
|
||||||
guard let frictionRateDesignValue else { return false }
|
guard let frictionRateDesignValue else { return false }
|
||||||
return frictionRateDesignValue >= 0.18
|
return frictionRateDesignValue >= 0.18
|
||||||
}
|
}
|
||||||
|
|
||||||
var showLowErrors: Bool {
|
private var showLowErrors: Bool {
|
||||||
guard let frictionRateDesignValue else { return false }
|
guard let frictionRateDesignValue else { return false }
|
||||||
return frictionRateDesignValue <= 0.02
|
return frictionRateDesignValue <= 0.02
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var showNoComponentLossesError: Bool {
|
||||||
|
componentLosses.count == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private var showIncompleteSectionsError: Bool {
|
||||||
|
availableStaticPressure == nil || frictionRateDesignValue == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hasAlerts: Bool {
|
||||||
|
showLowErrors
|
||||||
|
|| showHighErrors
|
||||||
|
|| showNoComponentLossesError
|
||||||
|
|| showIncompleteSectionsError
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
div(.class("space-y-6")) {
|
div(.class("space-y-6")) {
|
||||||
div(.class("grid grid-cols-2 px-4")) {
|
PageTitleRow {
|
||||||
|
div(.class("grid grid-cols-2 px-4 w-full")) {
|
||||||
|
|
||||||
PageTitle { "Friction Rate" }
|
PageTitle { "Friction Rate" }
|
||||||
|
|
||||||
div(.class("space-y-4 justify-end")) {
|
div(.class("space-y-2 justify-end font-bold text-lg")) {
|
||||||
|
if shouldShowBadges {
|
||||||
|
|
||||||
if let frictionRateDesignValue {
|
if let frictionRateDesignValue {
|
||||||
LabeledContent("Friction Rate Design Value") {
|
LabeledContent {
|
||||||
|
span { "Friction Rate Design Value" }
|
||||||
|
} content: {
|
||||||
Badge(number: frictionRateDesignValue, digits: 2)
|
Badge(number: frictionRateDesignValue, digits: 2)
|
||||||
.attributes(.class("\(badgeColor)"))
|
.attributes(.class("\(badgeColor) badge-lg"))
|
||||||
|
.bold()
|
||||||
}
|
}
|
||||||
.attributes(.class("justify-end"))
|
.attributes(.class("justify-end mx-auto"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let availableStaticPressure {
|
if let availableStaticPressure {
|
||||||
LabeledContent("Available Static Pressure") {
|
LabeledContent {
|
||||||
|
span { "Available Static Pressure" }
|
||||||
|
} content: {
|
||||||
Badge(number: availableStaticPressure, digits: 2)
|
Badge(number: availableStaticPressure, digits: 2)
|
||||||
}
|
}
|
||||||
.attributes(.class("justify-end"))
|
.attributes(.class("justify-end mx-auto"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LabeledContent {
|
||||||
|
span { "Component Pressure Losses" }
|
||||||
|
} content: {
|
||||||
|
Badge(number: componentLosses.total, digits: 2)
|
||||||
|
}
|
||||||
|
.attributes(.class("justify-end mx-auto"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div(.class("text-error italic")) {
|
div(.class("text-error font-bold italic col-span-2")) {
|
||||||
|
Alert {
|
||||||
|
p {
|
||||||
|
"Must complete previous sections."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hidden(when: !showIncompleteSectionsError)
|
||||||
|
|
||||||
|
Alert {
|
||||||
p {
|
p {
|
||||||
"No component pressures losses"
|
"No component pressures losses"
|
||||||
}
|
}
|
||||||
.attributes(.class("hidden"), when: componentLosses.totalComponentPressureLoss > 0)
|
}
|
||||||
|
.hidden(when: !showNoComponentLossesError)
|
||||||
|
|
||||||
p {
|
Alert {
|
||||||
|
p(.class("block")) {
|
||||||
"Calculated friction rate is below 0.02. The fan may not deliver the required CFM."
|
"Calculated friction rate is below 0.02. The fan may not deliver the required CFM."
|
||||||
br()
|
br()
|
||||||
" * Increase the blower speed"
|
" * Increase the blower speed"
|
||||||
@@ -80,9 +122,11 @@ struct FrictionRateView: HTML, Sendable {
|
|||||||
br()
|
br()
|
||||||
" * Decrease the Total Effective Length (TEL)"
|
" * Decrease the Total Effective Length (TEL)"
|
||||||
}
|
}
|
||||||
.attributes(.class("hidden"), when: !showLowErrors)
|
}
|
||||||
|
.hidden(when: !showLowErrors)
|
||||||
|
|
||||||
p {
|
Alert {
|
||||||
|
p(.class("block")) {
|
||||||
"Calculated friction rate is above 0.18. The fan may deliver too many CFM."
|
"Calculated friction rate is above 0.18. The fan may deliver too many CFM."
|
||||||
br()
|
br()
|
||||||
" * Decrease the blower speed"
|
" * Decrease the blower speed"
|
||||||
@@ -91,10 +135,13 @@ struct FrictionRateView: HTML, Sendable {
|
|||||||
br()
|
br()
|
||||||
" * Increase the Total Effective Length (TEL)"
|
" * Increase the Total Effective Length (TEL)"
|
||||||
}
|
}
|
||||||
.attributes(.class("hidden"), when: !showHighErrors)
|
|
||||||
}
|
}
|
||||||
|
.hidden(when: !showHighErrors)
|
||||||
|
|
||||||
div(.class("divider")) {}
|
}
|
||||||
|
.attributes(.class("mt-4"), when: hasAlerts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ComponentPressureLossesView(
|
ComponentPressureLossesView(
|
||||||
componentPressureLosses: componentLosses, projectID: projectID
|
componentPressureLosses: componentLosses, projectID: projectID
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Elementary
|
import Elementary
|
||||||
import ElementaryHTMX
|
import ElementaryHTMX
|
||||||
|
import Foundation
|
||||||
import ManualDCore
|
import ManualDCore
|
||||||
import Styleguide
|
import Styleguide
|
||||||
|
|
||||||
@@ -10,18 +11,44 @@ public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable
|
|||||||
|
|
||||||
let inner: Inner
|
let inner: Inner
|
||||||
let theme: Theme?
|
let theme: Theme?
|
||||||
|
let displayFooter: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
displayFooter: Bool = true,
|
||||||
theme: Theme? = nil,
|
theme: Theme? = nil,
|
||||||
_ inner: () -> Inner
|
_ inner: () -> Inner
|
||||||
) {
|
) {
|
||||||
|
self.displayFooter = displayFooter
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.inner = inner()
|
self.inner = inner()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var summary: String {
|
||||||
|
"""
|
||||||
|
Duct sizing based on ACCA, Manual-D.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
private var keywords: String {
|
||||||
|
"""
|
||||||
|
duct, hvac, duct-design, duct design, manual-d, manual d, design
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
public var head: some HTML {
|
public var head: some HTML {
|
||||||
meta(.charset(.utf8))
|
meta(.charset(.utf8))
|
||||||
meta(.name(.viewport), .content("width=device-width, initial-scale=1.0"))
|
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(summary), .name("description"))
|
||||||
|
meta(.content(summary), .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(keywords), .name(.keywords))
|
||||||
script(.src("https://unpkg.com/htmx.org@2.0.8")) {}
|
script(.src("https://unpkg.com/htmx.org@2.0.8")) {}
|
||||||
script(.src("/js/main.js")) {}
|
script(.src("/js/main.js")) {}
|
||||||
link(.rel(.stylesheet), .href("/css/output.css"))
|
link(.rel(.stylesheet), .href("/css/output.css"))
|
||||||
@@ -54,9 +81,30 @@ public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var body: some HTML {
|
public var body: some HTML {
|
||||||
div(.class("h-screen w-full")) {
|
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")) {
|
||||||
inner
|
inner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div(.class("bottom-0 left-0 bg-error")) {
|
||||||
|
if displayFooter {
|
||||||
|
footer(
|
||||||
|
.class(
|
||||||
|
"""
|
||||||
|
footer sm:footer-horizontal footer-center
|
||||||
|
bg-base-300 text-base-content p-4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
aside {
|
||||||
|
p {
|
||||||
|
"Copyright © \(Date().description.prefix(4)) - All rights reserved by Michael Housh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.attributes(.data("theme", value: theme?.rawValue ?? "default"), when: theme != nil)
|
.attributes(.data("theme", value: theme?.rawValue ?? "default"), when: theme != nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ struct Navbar: HTML, Sendable {
|
|||||||
) {
|
) {
|
||||||
div(.class("flex flex-1 space-x-4 items-center")) {
|
div(.class("flex flex-1 space-x-4 items-center")) {
|
||||||
if sidebarToggle {
|
if sidebarToggle {
|
||||||
Tooltip("Open sidebar", position: .right) {
|
|
||||||
label(
|
label(
|
||||||
.for("my-drawer-1"),
|
.for("my-drawer-1"),
|
||||||
.class("size-7"),
|
.class("size-7"),
|
||||||
@@ -33,11 +32,9 @@ struct Navbar: HTML, Sendable {
|
|||||||
SVG(.sidebarToggle)
|
SVG(.sidebarToggle)
|
||||||
}
|
}
|
||||||
.navButton()
|
.navButton()
|
||||||
|
.tooltip("Open sidebar", position: .right)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Tooltip("Home", position: .right) {
|
|
||||||
a(
|
a(
|
||||||
.class("flex w-fit h-fit text-xl items-end px-4 py-2"),
|
.class("flex w-fit h-fit text-xl items-end px-4 py-2"),
|
||||||
.href(route: .project(.index))
|
.href(route: .project(.index))
|
||||||
@@ -48,7 +45,7 @@ struct Navbar: HTML, Sendable {
|
|||||||
span { "Duct Calc" }
|
span { "Duct Calc" }
|
||||||
}
|
}
|
||||||
.navButton()
|
.navButton()
|
||||||
}
|
.tooltip("Home", position: .right)
|
||||||
}
|
}
|
||||||
if userProfile {
|
if userProfile {
|
||||||
// TODO: Make dropdown
|
// TODO: Make dropdown
|
||||||
@@ -59,34 +56,7 @@ struct Navbar: HTML, Sendable {
|
|||||||
SVG(.circleUser)
|
SVG(.circleUser)
|
||||||
}
|
}
|
||||||
.navButton()
|
.navButton()
|
||||||
// details(.class("dropdown dropdown-left dropdown-bottom")) {
|
.tooltip("Profile")
|
||||||
// summary(.class("btn w-fit px-4 py-2")) {
|
|
||||||
// SVG(.circleUser)
|
|
||||||
// }
|
|
||||||
// .navButton()
|
|
||||||
//
|
|
||||||
// ul(
|
|
||||||
// .class(
|
|
||||||
// """
|
|
||||||
// menu dropdown-content bg-base-100
|
|
||||||
// rounded-box z-1 w-fit p-2 shadow-sm
|
|
||||||
// """
|
|
||||||
// )
|
|
||||||
// ) {
|
|
||||||
// li(.class("w-full")) {
|
|
||||||
// // TODO: Save theme to user profile ??
|
|
||||||
// div(.class("flex justify-between p-4 space-x-6")) {
|
|
||||||
// Label("Theme")
|
|
||||||
// input(.type(.checkbox), .class("toggle theme-controller"), .value("light"))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // button(.class("w-fit px-4 py-2")) {
|
|
||||||
// // SVG(.circleUser)
|
|
||||||
// // }
|
|
||||||
// // .navButton()
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,21 @@ struct ProjectDetail: HTML, Sendable {
|
|||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
div {
|
div {
|
||||||
Row {
|
PageTitleRow {
|
||||||
h1(.class("text-3xl font-bold")) { "Project" }
|
PageTitle { "Project" }
|
||||||
|
|
||||||
EditButton()
|
EditButton()
|
||||||
.attributes(
|
.attributes(
|
||||||
.class("btn-ghost"),
|
.class("btn-primary"),
|
||||||
.on(.click, "projectForm.showModal()")
|
.on(.click, "projectForm.showModal()")
|
||||||
)
|
)
|
||||||
|
.tooltip("Edit project", position: .left)
|
||||||
}
|
}
|
||||||
|
|
||||||
div(.class("overflow-x-auto")) {
|
|
||||||
table(.class("table table-zebra text-lg")) {
|
table(.class("table table-zebra text-lg")) {
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
td { "Name" }
|
td(.class("label font-bold")) { "Name" }
|
||||||
td {
|
td {
|
||||||
div(.class("flex justify-end")) {
|
div(.class("flex justify-end")) {
|
||||||
project.name
|
project.name
|
||||||
@@ -29,7 +30,7 @@ struct ProjectDetail: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
td { "Street Address" }
|
td(.class("label font-bold")) { "Street Address" }
|
||||||
td {
|
td {
|
||||||
div(.class("flex justify-end")) {
|
div(.class("flex justify-end")) {
|
||||||
project.streetAddress
|
project.streetAddress
|
||||||
@@ -37,7 +38,7 @@ struct ProjectDetail: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
td { "City" }
|
td(.class("label font-bold")) { "City" }
|
||||||
td {
|
td {
|
||||||
div(.class("flex justify-end")) {
|
div(.class("flex justify-end")) {
|
||||||
project.city
|
project.city
|
||||||
@@ -45,7 +46,7 @@ struct ProjectDetail: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
td { "State" }
|
td(.class("label font-bold")) { "State" }
|
||||||
td {
|
td {
|
||||||
div(.class("flex justify-end")) {
|
div(.class("flex justify-end")) {
|
||||||
project.state
|
project.state
|
||||||
@@ -53,7 +54,7 @@ struct ProjectDetail: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
td { "Zip" }
|
td(.class("label font-bold")) { "Zip" }
|
||||||
td {
|
td {
|
||||||
div(.class("flex justify-end")) {
|
div(.class("flex justify-end")) {
|
||||||
project.zipCode
|
project.zipCode
|
||||||
@@ -62,7 +63,6 @@ struct ProjectDetail: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ProjectForm(dismiss: true, project: project)
|
ProjectForm(dismiss: true, project: project)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,10 @@ struct ProjectView<Inner: HTML>: HTML, Sendable where Inner: Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
div(.class("h-screen w-full")) {
|
div(.class("drawer lg:drawer-open h-full")) {
|
||||||
|
|
||||||
div(.class("drawer lg:drawer-open")) {
|
|
||||||
input(.id("my-drawer-1"), .type(.checkbox), .class("drawer-toggle"))
|
input(.id("my-drawer-1"), .type(.checkbox), .class("drawer-toggle"))
|
||||||
|
|
||||||
div(.class("drawer-content")) {
|
div(.class("drawer-content overflow-auto")) {
|
||||||
Navbar(sidebarToggle: true)
|
Navbar(sidebarToggle: true)
|
||||||
div(.class("p-4")) {
|
div(.class("p-4")) {
|
||||||
inner
|
inner
|
||||||
@@ -51,7 +49,6 @@ struct ProjectView<Inner: HTML>: HTML, Sendable where Inner: Sendable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +62,7 @@ extension ProjectView {
|
|||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
|
|
||||||
div(.class("drawer-side is-drawer-close:overflow-visible")) {
|
div(.class("drawer-side is-drawer-close:overflow-visible grow")) {
|
||||||
label(
|
label(
|
||||||
.for("my-drawer-1"), .init(name: "aria-label", value: "close sidebar"),
|
.for("my-drawer-1"), .init(name: "aria-label", value: "close sidebar"),
|
||||||
.class("drawer-overlay")
|
.class("drawer-overlay")
|
||||||
@@ -74,14 +71,13 @@ extension ProjectView {
|
|||||||
div(
|
div(
|
||||||
.class(
|
.class(
|
||||||
"""
|
"""
|
||||||
flex min-h-full flex-col items-start bg-base-300 text-base-content
|
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]
|
is-drawer-close:min-w-[80px] is-drawer-open:max-w-[300px]
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
ul(.class("w-full")) {
|
ul(.class("w-full grow")) {
|
||||||
|
|
||||||
|
|
||||||
li(.class("flex w-full")) {
|
li(.class("flex w-full")) {
|
||||||
row(
|
row(
|
||||||
|
|||||||
@@ -19,19 +19,18 @@ struct ProjectsTable: HTML, Sendable {
|
|||||||
div {
|
div {
|
||||||
Navbar(sidebarToggle: false)
|
Navbar(sidebarToggle: false)
|
||||||
div(.class("m-6")) {
|
div(.class("m-6")) {
|
||||||
Row {
|
PageTitleRow {
|
||||||
PageTitle { "Projects" }
|
PageTitle { "Projects" }
|
||||||
Tooltip("Add project") {
|
Tooltip("Add project") {
|
||||||
PlusButton()
|
PlusButton()
|
||||||
.attributes(
|
.attributes(
|
||||||
.class("btn-ghost"),
|
.class("btn-primary"),
|
||||||
.showModal(id: ProjectForm.id)
|
.showModal(id: ProjectForm.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.attributes(.class("pb-6"))
|
.attributes(.class("pb-6"))
|
||||||
|
|
||||||
div(.class("overflow-x-auto")) {
|
|
||||||
table(.class("table table-zebra")) {
|
table(.class("table table-zebra")) {
|
||||||
thead {
|
thead {
|
||||||
tr {
|
tr {
|
||||||
@@ -51,7 +50,6 @@ struct ProjectsTable: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension ProjectsTable {
|
extension ProjectsTable {
|
||||||
struct Rows: HTML, Sendable {
|
struct Rows: HTML, Sendable {
|
||||||
|
|||||||
@@ -13,63 +13,63 @@ struct RoomsView: HTML, Sendable {
|
|||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
div(.class("flex w-full flex-col")) {
|
div(.class("flex w-full flex-col")) {
|
||||||
Row {
|
PageTitleRow {
|
||||||
PageTitle { "Room Loads" }
|
div(.class("flex grid grid-cols-3 w-full gap-y-4")) {
|
||||||
|
|
||||||
div(.class("flex justify-end items-end -my-2")) {
|
div(.class("col-span-2")) {
|
||||||
|
PageTitle { "Room Loads" }
|
||||||
|
}
|
||||||
|
|
||||||
|
div(.class("flex justify-end grow")) {
|
||||||
Tooltip("Project wide sensible heat ratio", position: .left) {
|
Tooltip("Project wide sensible heat ratio", position: .left) {
|
||||||
button(
|
button(
|
||||||
.class(
|
.class(
|
||||||
"""
|
"""
|
||||||
justify-end items-end p-4
|
btn btn-primary text-lg font-bold py-2
|
||||||
hover:bg-neutral hover:text-white hover:rounded-lg
|
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
.showModal(id: SHRForm.id)
|
.showModal(id: SHRForm.id)
|
||||||
) {
|
) {
|
||||||
LabeledContent {
|
div(.class("flex grow justify-end items-end space-x-4")) {
|
||||||
div(.class("flex justify-end items-end space-x-4")) {
|
span {
|
||||||
// SVG(.squarePen)
|
|
||||||
span(.class("font-bold")) {
|
|
||||||
"Sensible Heat Ratio"
|
"Sensible Heat Ratio"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} content: {
|
|
||||||
if let sensibleHeatRatio {
|
if let sensibleHeatRatio {
|
||||||
Badge(number: sensibleHeatRatio)
|
Badge(number: sensibleHeatRatio)
|
||||||
|
} else {
|
||||||
|
Badge { "set" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.attributes(.class("border border-error"), when: sensibleHeatRatio == nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div(.class("flex flex-wrap justify-between mt-6")) {
|
div(.class("flex items-end space-x-4 font-bold")) {
|
||||||
div(.class("flex items-end space-x-4")) {
|
span(.class("text-lg")) { "Heating Total" }
|
||||||
span(.class("font-bold")) { "Heating Total" }
|
|
||||||
Badge(number: rooms.heatingTotal, digits: 0)
|
Badge(number: rooms.heatingTotal, digits: 0)
|
||||||
.attributes(.class("badge-error"))
|
.attributes(.class("badge-error"))
|
||||||
}
|
}
|
||||||
|
|
||||||
div(.class("flex items-end space-x-4")) {
|
div(.class("flex justify-center items-end space-x-4 my-auto font-bold")) {
|
||||||
span(.class("font-bold")) { "Cooling Total" }
|
span(.class("text-lg")) { "Cooling Total" }
|
||||||
Badge(number: rooms.coolingTotal, digits: 0)
|
Badge(number: rooms.coolingTotal, digits: 0)
|
||||||
.attributes(.class("badge-success"))
|
.attributes(.class("badge-success"))
|
||||||
}
|
}
|
||||||
|
|
||||||
div(.class("flex justify-end items-end space-x-4 me-4")) {
|
div(.class("flex grow justify-end items-end space-x-4 me-4 my-auto font-bold")) {
|
||||||
span(.class("font-bold")) { "Cooling Sensible" }
|
span(.class("text-lg")) { "Cooling Sensible" }
|
||||||
Badge(number: rooms.coolingSensible(shr: sensibleHeatRatio), digits: 0)
|
Badge(number: rooms.coolingSensible(shr: sensibleHeatRatio), digits: 0)
|
||||||
.attributes(.class("badge-info"))
|
.attributes(.class("badge-info"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// .attributes(.class("mt-6 me-4"))
|
}
|
||||||
|
|
||||||
div(.class("divider")) {}
|
SHRForm(
|
||||||
|
sensibleHeatRatio: sensibleHeatRatio,
|
||||||
|
dismiss: sensibleHeatRatio != nil
|
||||||
|
)
|
||||||
|
|
||||||
SHRForm(projectID: projectID, sensibleHeatRatio: sensibleHeatRatio)
|
|
||||||
|
|
||||||
div(.class("overflow-x-auto")) {
|
|
||||||
table(.class("table table-zebra text-lg"), .id("roomsTable")) {
|
table(.class("table table-zebra text-lg"), .id("roomsTable")) {
|
||||||
thead {
|
thead {
|
||||||
tr(.class("text-lg font-bold")) {
|
tr(.class("text-lg font-bold")) {
|
||||||
@@ -95,11 +95,11 @@ struct RoomsView: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
div(.class("flex justify-end")) {
|
div(.class("flex justify-end me-2")) {
|
||||||
Tooltip("Add Room") {
|
Tooltip("Add Room") {
|
||||||
PlusButton()
|
PlusButton()
|
||||||
.attributes(
|
.attributes(
|
||||||
.class("btn-ghost mx-auto"),
|
.class("btn-primary mx-auto"),
|
||||||
.showModal(id: RoomForm.id())
|
.showModal(id: RoomForm.id())
|
||||||
)
|
)
|
||||||
.attributes(.class("tooltip-left"))
|
.attributes(.class("tooltip-left"))
|
||||||
@@ -114,7 +114,6 @@ struct RoomsView: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
RoomForm(dismiss: true, projectID: projectID, room: nil)
|
RoomForm(dismiss: true, projectID: projectID, room: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,29 +197,37 @@ struct RoomsView: HTML, Sendable {
|
|||||||
struct SHRForm: HTML, Sendable {
|
struct SHRForm: HTML, Sendable {
|
||||||
static let id = "shrForm"
|
static let id = "shrForm"
|
||||||
|
|
||||||
let projectID: Project.ID
|
@Environment(ProjectViewValue.$projectID) var projectID
|
||||||
let sensibleHeatRatio: Double?
|
let sensibleHeatRatio: Double?
|
||||||
|
let dismiss: Bool
|
||||||
|
|
||||||
|
var route: String {
|
||||||
|
SiteRoute.View.router
|
||||||
|
.path(for: .project(.detail(projectID, .rooms(.index))))
|
||||||
|
.appendingPath("update-shr")
|
||||||
|
}
|
||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
ModalForm(id: Self.id, dismiss: true) {
|
ModalForm(id: Self.id, dismiss: dismiss) {
|
||||||
h1(.class("text-xl font-bold mb-6")) {
|
h1(.class("text-xl font-bold mb-6")) {
|
||||||
"Sensible Heat Ratio"
|
"Sensible Heat Ratio"
|
||||||
}
|
}
|
||||||
form(
|
form(
|
||||||
.class("grid grid-cols-1 gap-4"),
|
.class("grid grid-cols-1 gap-4"),
|
||||||
.hx.patch("/projects/\(projectID)/rooms/update-shr"),
|
.hx.patch(route),
|
||||||
.hx.target("body"),
|
.hx.target("body"),
|
||||||
.hx.swap(.outerHTML)
|
.hx.swap(.outerHTML)
|
||||||
) {
|
) {
|
||||||
input(.class("hidden"), .name("projectID"), .value("\(projectID)"))
|
input(.class("hidden"), .name("projectID"), .value("\(projectID)"))
|
||||||
LabeledInput(
|
LabeledInput(
|
||||||
"SHR",
|
"SHR",
|
||||||
|
.name("sensibleHeatRatio"),
|
||||||
.type(.number),
|
.type(.number),
|
||||||
|
.value(sensibleHeatRatio),
|
||||||
.placeholder("0.83"),
|
.placeholder("0.83"),
|
||||||
.min("0"),
|
.min("0"),
|
||||||
.max("1"),
|
.max("1"),
|
||||||
.step("0.01"),
|
.step("0.01"),
|
||||||
.value(sensibleHeatRatio),
|
|
||||||
.autofocus
|
.autofocus
|
||||||
)
|
)
|
||||||
SubmitButton()
|
SubmitButton()
|
||||||
|
|||||||
@@ -2,9 +2,30 @@ import Dependencies
|
|||||||
import Elementary
|
import Elementary
|
||||||
import Foundation
|
import Foundation
|
||||||
import ManualDCore
|
import ManualDCore
|
||||||
|
import Styleguide
|
||||||
|
|
||||||
struct TestPage: HTML, Sendable {
|
struct TestPage: HTML, Sendable {
|
||||||
|
let trunks: [DuctSizing.TrunkContainer]
|
||||||
|
let rooms: [DuctSizing.RoomContainer]
|
||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
UserProfileForm(userID: UUID(0), profile: nil, dismiss: false)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
justfile
11
justfile
@@ -1,4 +1,5 @@
|
|||||||
docker_image := "manuald"
|
docker_image := "ductcalc"
|
||||||
|
docker_tag := "latest"
|
||||||
|
|
||||||
install-deps:
|
install-deps:
|
||||||
@curl -sL daisyui.com/fast | bash
|
@curl -sL daisyui.com/fast | bash
|
||||||
@@ -9,8 +10,8 @@ run-css:
|
|||||||
run:
|
run:
|
||||||
@swift run App serve --log debug
|
@swift run App serve --log debug
|
||||||
|
|
||||||
build-docker:
|
build-docker file="docker/Dockerfile":
|
||||||
@podman build -f docker/Dockerfile.dev -t {{docker_image}}:dev .
|
@podman build -f {{file}} -t {{docker_image}}:{{docker_tag}} .
|
||||||
|
|
||||||
run-dev:
|
run-docker:
|
||||||
@podman run -it --rm -v $PWD:/app -p 8080:8080 {{docker_image}}:dev
|
@podman run -it --rm -v $PWD:/app -p 8080:8080 {{docker_image}}:{{docker_tag}}
|
||||||
|
|||||||
Reference in New Issue
Block a user