Rewrite frontend, added rooms, voltage selection per project, startet with todos
This commit is contained in:
@@ -322,6 +322,20 @@ Document:
|
|||||||
|
|
||||||
Prefer concise Markdown documentation.
|
Prefer concise Markdown documentation.
|
||||||
|
|
||||||
|
## Development Progress Documentation
|
||||||
|
|
||||||
|
Document the current implementation status in dedicated Markdown files whenever regular status updates are requested.
|
||||||
|
|
||||||
|
- Keep status tracking explicit with `TODO`, `WIP`, and `DONE` sections.
|
||||||
|
- For each relevant module or feature area, document:
|
||||||
|
- what it does,
|
||||||
|
- how it works,
|
||||||
|
- which files/functions are involved,
|
||||||
|
- why important design choices were made,
|
||||||
|
- current limitations or open points.
|
||||||
|
- Break larger features into small sub-sections so someone new can read through and understand the flow.
|
||||||
|
- Update these Markdown status documents incrementally during implementation so progress can be tracked clearly over time.
|
||||||
|
- Prefer understandable, step-by-step explanations over overly brief summaries when needed for clarity.
|
||||||
## Coding Style
|
## Coding Style
|
||||||
|
|
||||||
Write clear, explicit TypeScript.
|
Write clear, explicit TypeScript.
|
||||||
@@ -397,3 +411,55 @@ The first useful milestone should be:
|
|||||||
- Provide basic tests for calculation logic
|
- Provide basic tests for calculation logic
|
||||||
|
|
||||||
Do not start with advanced reporting before the core data and calculation workflow works.
|
Do not start with advanced reporting before the core data and calculation workflow works.
|
||||||
|
|
||||||
|
## Current UI Workflow Requirements
|
||||||
|
|
||||||
|
Implement and preserve the following navigation and workflow structure:
|
||||||
|
|
||||||
|
1. A dedicated project page that lists all projects and allows creating new projects.
|
||||||
|
2. On the same project page, users can configure global devices/consumers.
|
||||||
|
3. Inside a project, users first see all distribution boards and can open a selected board.
|
||||||
|
4. Circuit lists are edited in a dedicated view where up to 3 circuit lists can be opened in parallel.
|
||||||
|
5. Users must be able to copy circuit entries/consumers between the open circuit lists with minimal clicks.
|
||||||
|
6. In the circuit-list view, exactly 1 list is open by default. Users can add/remove list panels dynamically with a minimum of 1 and a maximum of 3 open lists.
|
||||||
|
|
||||||
|
When implementing frontend changes, keep this structure as the default interaction model unless the user explicitly requests a different UX.
|
||||||
|
|
||||||
|
## Language and Text Rules
|
||||||
|
|
||||||
|
Use proper German umlauts (ä, ö, ü, Ä, Ö, Ü, ß) in all new or changed German UI texts and documentation text, unless technical constraints explicitly prevent this.
|
||||||
|
|
||||||
|
## Responsiveness Rule
|
||||||
|
|
||||||
|
Frontend implementations must remain fully responsive by default across mobile, tablet, laptop, and wide desktop breakpoints.
|
||||||
|
Layouts with dynamic panel counts (for example 1-3 parallel circuit-list panels) must adapt so available horizontal space is used appropriately instead of leaving fixed empty columns.
|
||||||
|
|
||||||
|
## Language and Text Rules (Enforced)
|
||||||
|
|
||||||
|
Use proper German umlauts (�, �, �, �, �, �, �) in all new or changed German UI texts and documentation text, unless technical constraints explicitly prevent this.
|
||||||
|
|
||||||
|
|
||||||
|
## Project Voltage and Columns Rules
|
||||||
|
|
||||||
|
- Project properties must include default single-phase and three-phase voltages. Use 230 V for single-phase and 400 V for three-phase by default, and allow users to change both values in project settings.
|
||||||
|
- In circuit lists, when a consumer is single-phase and has no explicit voltage override, calculations use the project single-phase default voltage. When a consumer is three-phase and has no explicit voltage override, calculations use the project three-phase default voltage.
|
||||||
|
- When creating a new consumer/device entry, the standard input fields should be: consumer display name, quantity, unit power, demand factor, and total power.
|
||||||
|
- By default, the table should initially hide: power factor (cos phi), phase count, and current.
|
||||||
|
- Users must be able to add any available attribute as a table column at any time, and must be able to reorder column positions.
|
||||||
|
|
||||||
|
|
||||||
|
## Open TODOs from docs/electrical-load-balance-requirements-context-dump.md
|
||||||
|
|
||||||
|
- [x] Extend `CircuitEntry` data model with missing fields from requirements, especially `circuitNumber`, `description`, `deviceType`, `phaseType`, `tradeOrCostGroup`, `group`, `protectionType`, `protectionRatedCurrent`, `protectionCharacteristic`, `cableType`, `cableCrossSection`, `cableLength` and `comment`.
|
||||||
|
- [x] Allow multiple entries with the same `circuitNumber` and make this visible/editable in the circuit-list table.
|
||||||
|
- [x] Implement project-specific device lists (`ProjectDeviceList`) in backend + UI.
|
||||||
|
- [ ] Implement copying devices both directions between global and project-specific device lists.
|
||||||
|
- [ ] Add separate device naming model with `Device.name` and `Device.displayName`.
|
||||||
|
- [ ] Add explicit entry description field (`CircuitEntry.description`) independent of linked device naming.
|
||||||
|
- [ ] Implement device-link lifecycle on entries: link, unlink/detach, and update propagation from device changes to linked entries.
|
||||||
|
- [ ] Add `addCount` when adding a device to a circuit list to create multiple entries in one action (`addCount != quantity`).
|
||||||
|
- [ ] Relax circuit-entry validation so incomplete entries are possible (currently several fields are required).
|
||||||
|
- [ ] Add duplicate-entry action within the same circuit list (separate from copy to another list).
|
||||||
|
- [ ] Add sorting/filtering/bulk-edit capabilities for circuit-list tables (beyond current copy-selection flow).
|
||||||
|
- [ ] Define and implement fixed selection lists for domain fields (`deviceType`, `phaseType`, `tradeOrCostGroup`, `group`, protection and cable fields).
|
||||||
|
- [ ] Extend tests beyond pure power formulas to cover new circuit-entry/device-link behaviors once implemented.
|
||||||
|
|||||||
Generated
+29
@@ -10,6 +10,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.9.0",
|
"better-sqlite3": "^12.9.0",
|
||||||
|
"bootstrap": "^5.3.8",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"lucide-react": "^1.14.0",
|
"lucide-react": "^1.14.0",
|
||||||
@@ -1437,6 +1438,16 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@popperjs/core": {
|
||||||
|
"version": "2.11.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
|
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/popperjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
@@ -1656,6 +1667,24 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bootstrap": {
|
||||||
|
"version": "5.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
|
||||||
|
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/twbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"@popperjs/core": "^2.11.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.9.0",
|
"better-sqlite3": "^12.9.0",
|
||||||
|
"bootstrap": "^5.3.8",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"lucide-react": "^1.14.0",
|
"lucide-react": "^1.14.0",
|
||||||
|
|||||||
+6
-325
@@ -1,331 +1,12 @@
|
|||||||
:root {
|
|
||||||
color-scheme: light;
|
|
||||||
--bg: #f4f6f8;
|
|
||||||
--band: #ffffff;
|
|
||||||
--line: #d7dde5;
|
|
||||||
--line-strong: #b9c3d0;
|
|
||||||
--text: #17212f;
|
|
||||||
--muted: #647084;
|
|
||||||
--accent: #0f766e;
|
|
||||||
--accent-dark: #115e59;
|
|
||||||
--warn-bg: #fff4e5;
|
|
||||||
--warn-line: #f3b562;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
background-color: #f5f7fb;
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
.card {
|
||||||
input,
|
border-radius: 0.5rem;
|
||||||
select {
|
|
||||||
font: inherit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace {
|
.table td input.form-control-sm,
|
||||||
min-height: 100vh;
|
.table td select.form-select-sm {
|
||||||
padding: 20px;
|
min-width: 8rem;
|
||||||
display: grid;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar,
|
|
||||||
.toolbarBand,
|
|
||||||
.entryBand,
|
|
||||||
.tableBand {
|
|
||||||
background: var(--band);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar {
|
|
||||||
min-height: 82px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eyebrow {
|
|
||||||
margin: 0 0 4px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
letter-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 19px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconButton,
|
|
||||||
.primaryButton {
|
|
||||||
border: 1px solid var(--accent);
|
|
||||||
background: var(--accent);
|
|
||||||
color: #ffffff;
|
|
||||||
min-height: 36px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconButton {
|
|
||||||
width: 38px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconButton.small {
|
|
||||||
width: 30px;
|
|
||||||
min-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconButton.muted {
|
|
||||||
background: #64748b;
|
|
||||||
border-color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconButton.danger {
|
|
||||||
background: #b42318;
|
|
||||||
border-color: #b42318;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primaryButton {
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primaryButton:hover,
|
|
||||||
.iconButton:hover {
|
|
||||||
background: var(--accent-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.primaryButton:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert {
|
|
||||||
margin: 0;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid var(--warn-line);
|
|
||||||
background: var(--warn-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbarBand {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(360px, 1fr) minmax(360px, 1fr) minmax(320px, 0.9fr);
|
|
||||||
gap: 14px;
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projectForm,
|
|
||||||
.boardForm,
|
|
||||||
.consumerForm {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projectForm,
|
|
||||||
.boardForm {
|
|
||||||
grid-template-columns: minmax(180px, 1fr) minmax(220px, 1fr) auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.consumerForm {
|
|
||||||
grid-template-columns: minmax(210px, 1.6fr) minmax(150px, 1fr) 90px 140px 150px 110px 110px 100px minmax(180px, 1.4fr) auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
min-width: 0;
|
|
||||||
display: grid;
|
|
||||||
gap: 5px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
select {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 36px;
|
|
||||||
border: 1px solid var(--line-strong);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 6px 9px;
|
|
||||||
color: var(--text);
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summaryStrip {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(120px, 1fr));
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summaryStrip div {
|
|
||||||
padding: 11px 12px;
|
|
||||||
border-right: 1px solid var(--line);
|
|
||||||
display: grid;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summaryStrip div:last-child {
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summaryStrip span,
|
|
||||||
.statusPill {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summaryStrip strong {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entryBand,
|
|
||||||
.tableBand {
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableHeader {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 14px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardTotals {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardTotals h3 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 15px;
|
|
||||||
color: #344054;
|
|
||||||
}
|
|
||||||
|
|
||||||
.totalRow td {
|
|
||||||
font-weight: 700;
|
|
||||||
background: #eef4f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subline {
|
|
||||||
margin: 6px 0 0;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusPill,
|
|
||||||
.nameCell {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusPill {
|
|
||||||
min-height: 30px;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableScroll {
|
|
||||||
overflow-x: auto;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 1020px;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
padding: 9px 10px;
|
|
||||||
text-align: left;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background: #edf1f5;
|
|
||||||
color: #344054;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:hover {
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rowField {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 64px 1fr;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rowActions {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emptyState {
|
|
||||||
height: 92px;
|
|
||||||
color: var(--muted);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1180px) {
|
|
||||||
.toolbarBand,
|
|
||||||
.projectForm,
|
|
||||||
.boardForm,
|
|
||||||
.consumerForm {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summaryStrip,
|
|
||||||
.wideField,
|
|
||||||
.consumerForm .primaryButton {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
|
||||||
.workspace {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar,
|
|
||||||
.tableHeader {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbarBand,
|
|
||||||
.projectForm,
|
|
||||||
.boardForm,
|
|
||||||
.consumerForm,
|
|
||||||
.summaryStrip {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,9 +1,10 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Leistungsbilanz",
|
title: "Leistungsbilanz",
|
||||||
description: "Leistungsbilanz fuer elektrische Verbraucher",
|
description: "Leistungsbilanz für elektrische Verbraucher und Stromkreislisten",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { PowerBalanceWorkspace } from "../frontend/components/power-balance-workspace";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return <PowerBalanceWorkspace />;
|
redirect("/projects");
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,660 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
createDistributionBoard,
|
||||||
|
createFloor,
|
||||||
|
createProjectDevice,
|
||||||
|
createRoom,
|
||||||
|
deleteProjectDevice,
|
||||||
|
listDistributionBoards,
|
||||||
|
listFloors,
|
||||||
|
listProjectDevices,
|
||||||
|
listProjects,
|
||||||
|
listRooms,
|
||||||
|
updateProjectDevice,
|
||||||
|
updateProjectSettings,
|
||||||
|
} from "../../../frontend/utils/api";
|
||||||
|
import type {
|
||||||
|
CreateProjectDeviceInput,
|
||||||
|
DistributionBoardDto,
|
||||||
|
FloorDto,
|
||||||
|
ProjectDeviceDto,
|
||||||
|
ProjectDto,
|
||||||
|
RoomDto,
|
||||||
|
} from "../../../frontend/types";
|
||||||
|
|
||||||
|
const emptyProjectDevice: CreateProjectDeviceInput = {
|
||||||
|
name: "",
|
||||||
|
category: "",
|
||||||
|
quantity: 1,
|
||||||
|
installedPowerPerUnitKw: 0.1,
|
||||||
|
demandFactor: 1,
|
||||||
|
voltageV: 230,
|
||||||
|
phaseCount: 1,
|
||||||
|
powerFactor: 1,
|
||||||
|
note: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
function toOptionalNumber(value: string) {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return Number(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectDetailPage() {
|
||||||
|
const params = useParams<{ projectId: string }>();
|
||||||
|
const [projectId, setProjectId] = useState("");
|
||||||
|
const [project, setProject] = useState<ProjectDto | null>(null);
|
||||||
|
const [boards, setBoards] = useState<DistributionBoardDto[]>([]);
|
||||||
|
const [floors, setFloors] = useState<FloorDto[]>([]);
|
||||||
|
const [rooms, setRooms] = useState<RoomDto[]>([]);
|
||||||
|
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
||||||
|
const [boardName, setBoardName] = useState("");
|
||||||
|
const [floorName, setFloorName] = useState("");
|
||||||
|
const [roomNumber, setRoomNumber] = useState("");
|
||||||
|
const [roomName, setRoomName] = useState("");
|
||||||
|
const [roomFloorId, setRoomFloorId] = useState("");
|
||||||
|
const [singlePhaseVoltageV, setSinglePhaseVoltageV] = useState("230");
|
||||||
|
const [threePhaseVoltageV, setThreePhaseVoltageV] = useState("400");
|
||||||
|
const [projectDeviceForm, setProjectDeviceForm] = useState<Record<string, string>>({
|
||||||
|
name: "",
|
||||||
|
category: "",
|
||||||
|
quantity: "1",
|
||||||
|
installedPowerPerUnitKw: "0.1",
|
||||||
|
demandFactor: "1",
|
||||||
|
voltageV: "230",
|
||||||
|
phaseCount: "1",
|
||||||
|
powerFactor: "1",
|
||||||
|
note: "",
|
||||||
|
});
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setProjectId(params.projectId);
|
||||||
|
}, [params.projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Promise.all([
|
||||||
|
listProjects(),
|
||||||
|
listDistributionBoards(projectId),
|
||||||
|
listFloors(projectId),
|
||||||
|
listRooms(projectId),
|
||||||
|
listProjectDevices(projectId),
|
||||||
|
])
|
||||||
|
.then(([projects, distributionBoards, loadedFloors, loadedRooms, loadedProjectDevices]) => {
|
||||||
|
const currentProject = projects.find((item) => item.id === projectId) ?? null;
|
||||||
|
setProject(currentProject);
|
||||||
|
if (currentProject) {
|
||||||
|
setSinglePhaseVoltageV(String(currentProject.singlePhaseVoltageV));
|
||||||
|
setThreePhaseVoltageV(String(currentProject.threePhaseVoltageV));
|
||||||
|
}
|
||||||
|
setBoards(distributionBoards);
|
||||||
|
setFloors(loadedFloors);
|
||||||
|
setRooms(loadedRooms);
|
||||||
|
setProjectDevices(loadedProjectDevices);
|
||||||
|
setError(null);
|
||||||
|
})
|
||||||
|
.catch((err: unknown) =>
|
||||||
|
setError(err instanceof Error ? err.message : "Projektdaten konnten nicht geladen werden.")
|
||||||
|
);
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
|
const boardCount = useMemo(() => boards.length, [boards.length]);
|
||||||
|
const floorCount = useMemo(() => floors.length, [floors.length]);
|
||||||
|
const roomCount = useMemo(() => rooms.length, [rooms.length]);
|
||||||
|
const projectDeviceCount = useMemo(() => projectDevices.length, [projectDevices.length]);
|
||||||
|
const floorById = useMemo(() => new Map(floors.map((item) => [item.id, item])), [floors]);
|
||||||
|
|
||||||
|
async function handleCreateBoard(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!projectId || !boardName.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const created = await createDistributionBoard(projectId, boardName.trim());
|
||||||
|
setBoards((current) => [...current, created]);
|
||||||
|
setBoardName("");
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Verteilung konnte nicht erstellt werden.");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateFloor(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!projectId || !floorName.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const created = await createFloor(projectId, { name: floorName.trim() });
|
||||||
|
setFloors((current) => [...current, created]);
|
||||||
|
setFloorName("");
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Etage konnte nicht erstellt werden.");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateRoom(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!projectId || !roomNumber.trim() || !roomName.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const created = await createRoom(projectId, {
|
||||||
|
floorId: roomFloorId || undefined,
|
||||||
|
roomNumber: roomNumber.trim(),
|
||||||
|
roomName: roomName.trim(),
|
||||||
|
});
|
||||||
|
setRooms((current) => [...current, created]);
|
||||||
|
setRoomNumber("");
|
||||||
|
setRoomName("");
|
||||||
|
setRoomFloorId("");
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Raum konnte nicht erstellt werden.");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveProjectSettings() {
|
||||||
|
if (!projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const updated = await updateProjectSettings(projectId, {
|
||||||
|
singlePhaseVoltageV: Number(singlePhaseVoltageV),
|
||||||
|
threePhaseVoltageV: Number(threePhaseVoltageV),
|
||||||
|
});
|
||||||
|
setProject(updated);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Projekteigenschaften konnten nicht gespeichert werden.");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateProjectDevice(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!projectId || !projectDeviceForm.name.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: CreateProjectDeviceInput = {
|
||||||
|
name: projectDeviceForm.name.trim(),
|
||||||
|
category: projectDeviceForm.category.trim() || undefined,
|
||||||
|
quantity: Number(projectDeviceForm.quantity),
|
||||||
|
installedPowerPerUnitKw: Number(projectDeviceForm.installedPowerPerUnitKw),
|
||||||
|
demandFactor: Number(projectDeviceForm.demandFactor),
|
||||||
|
voltageV: toOptionalNumber(projectDeviceForm.voltageV),
|
||||||
|
phaseCount: projectDeviceForm.phaseCount === "3" ? 3 : 1,
|
||||||
|
powerFactor: toOptionalNumber(projectDeviceForm.powerFactor),
|
||||||
|
note: projectDeviceForm.note.trim() || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const created = await createProjectDevice(projectId, payload);
|
||||||
|
setProjectDevices((current) => [...current, created]);
|
||||||
|
setProjectDeviceForm({
|
||||||
|
name: emptyProjectDevice.name,
|
||||||
|
category: emptyProjectDevice.category ?? "",
|
||||||
|
quantity: String(emptyProjectDevice.quantity),
|
||||||
|
installedPowerPerUnitKw: String(emptyProjectDevice.installedPowerPerUnitKw),
|
||||||
|
demandFactor: String(emptyProjectDevice.demandFactor),
|
||||||
|
voltageV: String(emptyProjectDevice.voltageV ?? ""),
|
||||||
|
phaseCount: String(emptyProjectDevice.phaseCount ?? 1),
|
||||||
|
powerFactor: String(emptyProjectDevice.powerFactor ?? ""),
|
||||||
|
note: emptyProjectDevice.note ?? "",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Projektgerät konnte nicht erstellt werden.");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteProjectDevice(projectDeviceId: string) {
|
||||||
|
if (!projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await deleteProjectDevice(projectId, projectDeviceId);
|
||||||
|
setProjectDevices((current) => current.filter((item) => item.id !== projectDeviceId));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Projektgerät konnte nicht gelöscht werden.");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleQuickUpdateProjectDevice(
|
||||||
|
device: ProjectDeviceDto,
|
||||||
|
key: "name" | "category",
|
||||||
|
value: string
|
||||||
|
) {
|
||||||
|
if (!projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: CreateProjectDeviceInput = {
|
||||||
|
name: key === "name" ? value : device.name,
|
||||||
|
category: key === "category" ? value : device.category ?? undefined,
|
||||||
|
quantity: device.quantity,
|
||||||
|
installedPowerPerUnitKw: device.installedPowerPerUnitKw,
|
||||||
|
demandFactor: device.demandFactor,
|
||||||
|
voltageV: device.voltageV ?? undefined,
|
||||||
|
phaseCount: device.phaseCount ?? undefined,
|
||||||
|
powerFactor: device.powerFactor ?? undefined,
|
||||||
|
note: device.note ?? undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await updateProjectDevice(projectId, device.id, payload);
|
||||||
|
setProjectDevices((current) => current.map((item) => (item.id === device.id ? updated : item)));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Projektgerät konnte nicht aktualisiert werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="container py-4">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div>
|
||||||
|
<h1 className="h3 mb-1">{project?.name ?? "Projekt"}</h1>
|
||||||
|
<p className="text-secondary mb-0">Verteilerübersicht und Einstieg in die Stromkreislisten</p>
|
||||||
|
</div>
|
||||||
|
<Link className="btn btn-outline-secondary" href="/projects">
|
||||||
|
Zur Projektseite
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? <div className="alert alert-warning">{error}</div> : null}
|
||||||
|
|
||||||
|
<div className="row g-4">
|
||||||
|
<section className="col-12">
|
||||||
|
<div className="card shadow-sm">
|
||||||
|
<div className="card-header">Projekteigenschaften</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="row g-3 align-items-end">
|
||||||
|
<div className="col-12 col-md-4">
|
||||||
|
<label className="form-label">Standardspannung 1-phasig [V]</label>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={singlePhaseVoltageV}
|
||||||
|
onChange={(event) => setSinglePhaseVoltageV(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-4">
|
||||||
|
<label className="form-label">Standardspannung 3-phasig [V]</label>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={threePhaseVoltageV}
|
||||||
|
onChange={(event) => setThreePhaseVoltageV(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-4">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary w-100"
|
||||||
|
type="button"
|
||||||
|
onClick={handleSaveProjectSettings}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
Projekteigenschaften speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="col-12 col-lg-4">
|
||||||
|
<div className="card shadow-sm">
|
||||||
|
<div className="card-header">Neue Verteilung</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<form className="vstack gap-3" onSubmit={handleCreateBoard}>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
placeholder="z. B. UV-01"
|
||||||
|
value={boardName}
|
||||||
|
onChange={(event) => setBoardName(event.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-primary" type="submit" disabled={isSaving}>
|
||||||
|
Verteilung erstellen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="col-12 col-lg-8">
|
||||||
|
<div className="card shadow-sm">
|
||||||
|
<div className="card-header d-flex justify-content-between">
|
||||||
|
<span>Alle Verteilungen</span>
|
||||||
|
<span className="badge text-bg-secondary">{boardCount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-hover align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Verteilung</th>
|
||||||
|
<th className="text-end">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{boards.map((board) => (
|
||||||
|
<tr key={board.id}>
|
||||||
|
<td>{board.name}</td>
|
||||||
|
<td className="text-end">
|
||||||
|
<Link
|
||||||
|
className="btn btn-sm btn-outline-primary"
|
||||||
|
href={`/projects/${projectId}/circuit-lists?boardId=${board.id}`}
|
||||||
|
>
|
||||||
|
Stromkreisliste öffnen
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!boards.length ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={2} className="text-center text-secondary py-4">
|
||||||
|
Noch keine Verteilungen vorhanden.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="col-12 col-xl-4">
|
||||||
|
<div className="card shadow-sm h-100">
|
||||||
|
<div className="card-header d-flex justify-content-between">
|
||||||
|
<span>Etagen</span>
|
||||||
|
<span className="badge text-bg-secondary">{floorCount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-body border-bottom">
|
||||||
|
<form className="vstack gap-2" onSubmit={handleCreateFloor}>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
placeholder="z. B. EG"
|
||||||
|
value={floorName}
|
||||||
|
onChange={(event) => setFloorName(event.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-primary" type="submit" disabled={isSaving}>
|
||||||
|
Etage hinzufügen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<ul className="list-group list-group-flush">
|
||||||
|
{floors.map((floor) => (
|
||||||
|
<li className="list-group-item" key={floor.id}>
|
||||||
|
{floor.name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{!floors.length ? (
|
||||||
|
<li className="list-group-item text-secondary">Noch keine Etagen vorhanden.</li>
|
||||||
|
) : null}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="col-12 col-xl-8">
|
||||||
|
<div className="card shadow-sm">
|
||||||
|
<div className="card-header d-flex justify-content-between">
|
||||||
|
<span>Räume</span>
|
||||||
|
<span className="badge text-bg-secondary">{roomCount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-body border-bottom">
|
||||||
|
<form className="row g-2" onSubmit={handleCreateRoom}>
|
||||||
|
<div className="col-12 col-lg-3">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Raumnummer"
|
||||||
|
value={roomNumber}
|
||||||
|
onChange={(event) => setRoomNumber(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-lg-4">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Raumname"
|
||||||
|
value={roomName}
|
||||||
|
onChange={(event) => setRoomName(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-lg-3">
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={roomFloorId}
|
||||||
|
onChange={(event) => setRoomFloorId(event.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Ohne Etage</option>
|
||||||
|
{floors.map((floor) => (
|
||||||
|
<option key={floor.id} value={floor.id}>
|
||||||
|
{floor.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-lg-2">
|
||||||
|
<button className="btn btn-primary w-100" type="submit" disabled={isSaving}>
|
||||||
|
Raum anlegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-hover align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Raumnummer</th>
|
||||||
|
<th>Raumname</th>
|
||||||
|
<th>Etage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rooms.map((room) => (
|
||||||
|
<tr key={room.id}>
|
||||||
|
<td>{room.roomNumber}</td>
|
||||||
|
<td>{room.roomName}</td>
|
||||||
|
<td>{room.floorId ? floorById.get(room.floorId)?.name ?? "-" : "-"}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!rooms.length ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="text-center text-secondary py-4">
|
||||||
|
Noch keine Räume vorhanden.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="card shadow-sm mt-4">
|
||||||
|
<div className="card-header d-flex justify-content-between">
|
||||||
|
<span>Projektgeräte / Verbraucher</span>
|
||||||
|
<span className="badge text-bg-secondary">{projectDeviceCount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-body border-bottom">
|
||||||
|
<form className="row g-2" onSubmit={handleCreateProjectDevice}>
|
||||||
|
<div className="col-12 col-md-3">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Bezeichnung"
|
||||||
|
value={projectDeviceForm.name}
|
||||||
|
onChange={(event) =>
|
||||||
|
setProjectDeviceForm((current) => ({ ...current, name: event.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 col-md-2">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Kategorie"
|
||||||
|
value={projectDeviceForm.category}
|
||||||
|
onChange={(event) =>
|
||||||
|
setProjectDeviceForm((current) => ({ ...current, category: event.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 col-md-1">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={projectDeviceForm.quantity}
|
||||||
|
onChange={(event) =>
|
||||||
|
setProjectDeviceForm((current) => ({ ...current, quantity: event.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 col-md-2">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
value={projectDeviceForm.installedPowerPerUnitKw}
|
||||||
|
onChange={(event) =>
|
||||||
|
setProjectDeviceForm((current) => ({
|
||||||
|
...current,
|
||||||
|
installedPowerPerUnitKw: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 col-md-1">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
value={projectDeviceForm.demandFactor}
|
||||||
|
onChange={(event) =>
|
||||||
|
setProjectDeviceForm((current) => ({ ...current, demandFactor: event.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 col-md-1">
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={projectDeviceForm.phaseCount}
|
||||||
|
onChange={(event) =>
|
||||||
|
setProjectDeviceForm((current) => ({ ...current, phaseCount: event.target.value }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="1">1-ph</option>
|
||||||
|
<option value="3">3-ph</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 col-md-2">
|
||||||
|
<button className="btn btn-primary w-100" type="submit" disabled={isSaving}>
|
||||||
|
Gerät anlegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-sm table-striped align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Bezeichnung</th>
|
||||||
|
<th>Kategorie</th>
|
||||||
|
<th>Anzahl</th>
|
||||||
|
<th>Leistung je Stück [kW]</th>
|
||||||
|
<th>GZF</th>
|
||||||
|
<th>Aktion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{projectDevices.map((device) => (
|
||||||
|
<tr key={device.id}>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
defaultValue={device.name}
|
||||||
|
onBlur={(event) =>
|
||||||
|
event.target.value !== device.name
|
||||||
|
? handleQuickUpdateProjectDevice(device, "name", event.target.value)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
defaultValue={device.category ?? ""}
|
||||||
|
onBlur={(event) =>
|
||||||
|
event.target.value !== (device.category ?? "")
|
||||||
|
? handleQuickUpdateProjectDevice(device, "category", event.target.value)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{device.quantity}</td>
|
||||||
|
<td>{device.installedPowerPerUnitKw}</td>
|
||||||
|
<td>{device.demandFactor}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-outline-danger"
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleDeleteProjectDevice(device.id)}
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!projectDevices.length ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="text-center text-secondary py-4">
|
||||||
|
Noch keine Projektgeräte vorhanden.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<Link className="btn btn-primary" href={`/projects/${projectId}/circuit-lists`}>
|
||||||
|
3 parallele Stromkreislisten öffnen
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { FormEvent, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
createGlobalDevice,
|
||||||
|
createProject,
|
||||||
|
deleteGlobalDevice,
|
||||||
|
listGlobalDevices,
|
||||||
|
listProjects,
|
||||||
|
updateGlobalDevice,
|
||||||
|
} from "../../frontend/utils/api";
|
||||||
|
import type {
|
||||||
|
CreateGlobalDeviceInput,
|
||||||
|
GlobalDeviceDto,
|
||||||
|
ProjectDto,
|
||||||
|
} from "../../frontend/types";
|
||||||
|
|
||||||
|
const emptyGlobalDevice: CreateGlobalDeviceInput = {
|
||||||
|
name: "",
|
||||||
|
category: "",
|
||||||
|
quantity: 1,
|
||||||
|
installedPowerPerUnitKw: 0.1,
|
||||||
|
demandFactor: 1,
|
||||||
|
voltageV: 230,
|
||||||
|
phaseCount: 1,
|
||||||
|
powerFactor: 1,
|
||||||
|
note: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
function toOptionalNumber(value: string) {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return Number(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectsPage() {
|
||||||
|
const [projects, setProjects] = useState<ProjectDto[]>([]);
|
||||||
|
const [globalDevices, setGlobalDevices] = useState<GlobalDeviceDto[]>([]);
|
||||||
|
const [projectName, setProjectName] = useState("");
|
||||||
|
const [globalDeviceForm, setGlobalDeviceForm] = useState<Record<string, string>>({
|
||||||
|
name: "",
|
||||||
|
category: "",
|
||||||
|
quantity: "1",
|
||||||
|
installedPowerPerUnitKw: "0.1",
|
||||||
|
demandFactor: "1",
|
||||||
|
voltageV: "230",
|
||||||
|
phaseCount: "1",
|
||||||
|
powerFactor: "1",
|
||||||
|
note: "",
|
||||||
|
});
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
setError(null);
|
||||||
|
const [loadedProjects, loadedGlobalDevices] = await Promise.all([listProjects(), listGlobalDevices()]);
|
||||||
|
setProjects(loadedProjects);
|
||||||
|
setGlobalDevices(loadedGlobalDevices);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData().catch((err: unknown) =>
|
||||||
|
setError(err instanceof Error ? err.message : "Daten konnten nicht geladen werden.")
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleCreateProject(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!projectName.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const created = await createProject(projectName.trim());
|
||||||
|
setProjects((current) => [...current, created]);
|
||||||
|
setProjectName("");
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Projekt konnte nicht erstellt werden.");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateGlobalDevice(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!globalDeviceForm.name.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload: CreateGlobalDeviceInput = {
|
||||||
|
name: globalDeviceForm.name.trim(),
|
||||||
|
category: globalDeviceForm.category.trim() || undefined,
|
||||||
|
quantity: Number(globalDeviceForm.quantity),
|
||||||
|
installedPowerPerUnitKw: Number(globalDeviceForm.installedPowerPerUnitKw),
|
||||||
|
demandFactor: Number(globalDeviceForm.demandFactor),
|
||||||
|
voltageV: toOptionalNumber(globalDeviceForm.voltageV),
|
||||||
|
phaseCount: globalDeviceForm.phaseCount === "3" ? 3 : 1,
|
||||||
|
powerFactor: toOptionalNumber(globalDeviceForm.powerFactor),
|
||||||
|
note: globalDeviceForm.note.trim() || undefined,
|
||||||
|
};
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const created = await createGlobalDevice(payload);
|
||||||
|
setGlobalDevices((current) => [...current, created]);
|
||||||
|
setGlobalDeviceForm({
|
||||||
|
name: emptyGlobalDevice.name,
|
||||||
|
category: emptyGlobalDevice.category ?? "",
|
||||||
|
quantity: String(emptyGlobalDevice.quantity),
|
||||||
|
installedPowerPerUnitKw: String(emptyGlobalDevice.installedPowerPerUnitKw),
|
||||||
|
demandFactor: String(emptyGlobalDevice.demandFactor),
|
||||||
|
voltageV: String(emptyGlobalDevice.voltageV ?? ""),
|
||||||
|
phaseCount: String(emptyGlobalDevice.phaseCount ?? 1),
|
||||||
|
powerFactor: String(emptyGlobalDevice.powerFactor ?? ""),
|
||||||
|
note: emptyGlobalDevice.note ?? "",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Globales Gerät konnte nicht erstellt werden.");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteGlobalDevice(globalDeviceId: string) {
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await deleteGlobalDevice(globalDeviceId);
|
||||||
|
setGlobalDevices((current) => current.filter((item) => item.id !== globalDeviceId));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Globales Gerät konnte nicht gelöscht werden.");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleQuickUpdateGlobalDevice(
|
||||||
|
device: GlobalDeviceDto,
|
||||||
|
key: "name" | "category",
|
||||||
|
value: string
|
||||||
|
) {
|
||||||
|
const payload: CreateGlobalDeviceInput = {
|
||||||
|
name: key === "name" ? value : device.name,
|
||||||
|
category: key === "category" ? value : device.category ?? undefined,
|
||||||
|
quantity: device.quantity,
|
||||||
|
installedPowerPerUnitKw: device.installedPowerPerUnitKw,
|
||||||
|
demandFactor: device.demandFactor,
|
||||||
|
voltageV: device.voltageV ?? undefined,
|
||||||
|
phaseCount: device.phaseCount ?? undefined,
|
||||||
|
powerFactor: device.powerFactor ?? undefined,
|
||||||
|
note: device.note ?? undefined,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const updated = await updateGlobalDevice(device.id, payload);
|
||||||
|
setGlobalDevices((current) => current.map((item) => (item.id === device.id ? updated : item)));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Globales Gerät konnte nicht aktualisiert werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="container py-4">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="h3 mb-1">Projekte</h1>
|
||||||
|
<p className="text-secondary mb-0">Projektübersicht und globale Geräteverwaltung</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? <div className="alert alert-warning">{error}</div> : null}
|
||||||
|
|
||||||
|
<div className="row g-4">
|
||||||
|
<section className="col-12 col-lg-4">
|
||||||
|
<div className="card shadow-sm">
|
||||||
|
<div className="card-header">Neues Projekt</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<form className="vstack gap-3" onSubmit={handleCreateProject}>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Projektname</label>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
value={projectName}
|
||||||
|
onChange={(event) => setProjectName(event.target.value)}
|
||||||
|
placeholder="z. B. Neubau Schule"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-primary" type="submit" disabled={isSaving}>
|
||||||
|
Projekt erstellen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="col-12 col-lg-8">
|
||||||
|
<div className="card shadow-sm">
|
||||||
|
<div className="card-header">Alle Projekte</div>
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-hover align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Projekt</th>
|
||||||
|
<th className="text-end">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{projects.map((project) => (
|
||||||
|
<tr key={project.id}>
|
||||||
|
<td>{project.name}</td>
|
||||||
|
<td className="text-end">
|
||||||
|
<Link className="btn btn-sm btn-outline-primary" href={`/projects/${project.id}`}>
|
||||||
|
Projekt öffnen
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!projects.length ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={2} className="text-center text-secondary py-4">
|
||||||
|
Noch keine Projekte vorhanden.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="card shadow-sm mt-4">
|
||||||
|
<div className="card-header">Globale Geräte / Verbraucher</div>
|
||||||
|
<div className="card-body border-bottom">
|
||||||
|
<form className="row g-2" onSubmit={handleCreateGlobalDevice}>
|
||||||
|
<div className="col-12 col-md-3">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Bezeichnung"
|
||||||
|
value={globalDeviceForm.name}
|
||||||
|
onChange={(event) => setGlobalDeviceForm((current) => ({ ...current, name: event.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 col-md-2">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Kategorie"
|
||||||
|
value={globalDeviceForm.category}
|
||||||
|
onChange={(event) =>
|
||||||
|
setGlobalDeviceForm((current) => ({ ...current, category: event.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 col-md-1">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={globalDeviceForm.quantity}
|
||||||
|
onChange={(event) =>
|
||||||
|
setGlobalDeviceForm((current) => ({ ...current, quantity: event.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 col-md-2">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
value={globalDeviceForm.installedPowerPerUnitKw}
|
||||||
|
onChange={(event) =>
|
||||||
|
setGlobalDeviceForm((current) => ({
|
||||||
|
...current,
|
||||||
|
installedPowerPerUnitKw: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 col-md-1">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
value={globalDeviceForm.demandFactor}
|
||||||
|
onChange={(event) =>
|
||||||
|
setGlobalDeviceForm((current) => ({ ...current, demandFactor: event.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 col-md-1">
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={globalDeviceForm.phaseCount}
|
||||||
|
onChange={(event) =>
|
||||||
|
setGlobalDeviceForm((current) => ({ ...current, phaseCount: event.target.value }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="1">1-ph</option>
|
||||||
|
<option value="3">3-ph</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 col-md-2">
|
||||||
|
<button className="btn btn-primary w-100" type="submit" disabled={isSaving}>
|
||||||
|
Gerät anlegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-sm table-striped align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Bezeichnung</th>
|
||||||
|
<th>Kategorie</th>
|
||||||
|
<th>Anzahl</th>
|
||||||
|
<th>Leistung je Stück [kW]</th>
|
||||||
|
<th>GZF</th>
|
||||||
|
<th>Aktion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{globalDevices.map((device) => (
|
||||||
|
<tr key={device.id}>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
defaultValue={device.name}
|
||||||
|
onBlur={(event) =>
|
||||||
|
event.target.value !== device.name
|
||||||
|
? handleQuickUpdateGlobalDevice(device, "name", event.target.value)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
defaultValue={device.category ?? ""}
|
||||||
|
onBlur={(event) =>
|
||||||
|
event.target.value !== (device.category ?? "")
|
||||||
|
? handleQuickUpdateGlobalDevice(device, "category", event.target.value)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{device.quantity}</td>
|
||||||
|
<td>{device.installedPowerPerUnitKw}</td>
|
||||||
|
<td>{device.demandFactor}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-outline-danger"
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleDeleteGlobalDevice(device.id)}
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!globalDevices.length ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="text-center text-secondary py-4">
|
||||||
|
Noch keine globalen Geräte vorhanden.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE `global_devices` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`category` text,
|
||||||
|
`quantity` integer NOT NULL,
|
||||||
|
`installed_power_per_unit_kw` real NOT NULL,
|
||||||
|
`demand_factor` real NOT NULL,
|
||||||
|
`voltage_v` real,
|
||||||
|
`phase_count` integer,
|
||||||
|
`power_factor` real,
|
||||||
|
`note` text
|
||||||
|
);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE `projects` ADD COLUMN `single_phase_voltage_v` integer NOT NULL DEFAULT 230;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `projects` ADD COLUMN `three_phase_voltage_v` integer NOT NULL DEFAULT 400;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
CREATE TABLE `floors` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`project_id` text NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`sort_order` integer NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `rooms` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`project_id` text NOT NULL,
|
||||||
|
`floor_id` text,
|
||||||
|
`room_number` text NOT NULL,
|
||||||
|
`room_name` text NOT NULL,
|
||||||
|
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`floor_id`) REFERENCES `floors`(`id`) ON UPDATE no action ON DELETE set null
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `room_id` text REFERENCES `rooms`(`id`) ON UPDATE no action ON DELETE set null;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
CREATE TABLE `circuit_lists` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`project_id` text NOT NULL,
|
||||||
|
`distribution_board_id` text NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`distribution_board_id`) REFERENCES `distribution_boards`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `circuit_lists_distribution_board_id_unique` ON `circuit_lists` (`distribution_board_id`);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `circuit_lists` (`id`, `project_id`, `distribution_board_id`, `name`)
|
||||||
|
SELECT `id`, `project_id`, `id`, `name` || ' Stromkreisliste'
|
||||||
|
FROM `distribution_boards`;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `circuit_list_id` text REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE set null;
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE `consumers` SET `circuit_list_id` = `distribution_board_id` WHERE `distribution_board_id` IS NOT NULL;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `circuit_number` text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `description` text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE `consumers` SET `description` = `name` WHERE `description` IS NULL;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `device_type` text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `phase_type` text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `trade_or_cost_group` text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `group_name` text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `protection_type` text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `protection_rated_current` real;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `protection_characteristic` text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `cable_type` text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `cable_cross_section` text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `comment` text;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE `project_devices` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`project_id` text NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`category` text,
|
||||||
|
`quantity` integer NOT NULL,
|
||||||
|
`installed_power_per_unit_kw` real NOT NULL,
|
||||||
|
`demand_factor` real NOT NULL,
|
||||||
|
`voltage_v` real,
|
||||||
|
`phase_count` integer,
|
||||||
|
`power_factor` real,
|
||||||
|
`note` text,
|
||||||
|
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
@@ -8,6 +8,41 @@
|
|||||||
"when": 1777565414148,
|
"when": 1777565414148,
|
||||||
"tag": "0000_bizarre_colossus",
|
"tag": "0000_bizarre_colossus",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1777577000000,
|
||||||
|
"tag": "0001_global_devices",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1777580000000,
|
||||||
|
"tag": "0002_project_voltage_defaults",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1777589000000,
|
||||||
|
"tag": "0003_project_floors_rooms",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1777594000000,
|
||||||
|
"tag": "0004_circuit_lists_and_entry_fields",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1777597000000,
|
||||||
|
"tag": "0005_project_devices",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { db } from "../client.js";
|
||||||
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
|
|
||||||
|
export class CircuitListRepository {
|
||||||
|
async listByProject(projectId: string) {
|
||||||
|
return db.select().from(circuitLists).where(eq(circuitLists.projectId, projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async createForDistributionBoard(input: {
|
||||||
|
projectId: string;
|
||||||
|
distributionBoardId: string;
|
||||||
|
name: string;
|
||||||
|
}) {
|
||||||
|
const entry = {
|
||||||
|
id: input.distributionBoardId,
|
||||||
|
projectId: input.projectId,
|
||||||
|
distributionBoardId: input.distributionBoardId,
|
||||||
|
name: input.name,
|
||||||
|
};
|
||||||
|
await db.insert(circuitLists).values(entry);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByDistributionBoardId(projectId: string, distributionBoardId: string) {
|
||||||
|
const [row] = await db
|
||||||
|
.select()
|
||||||
|
.from(circuitLists)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(circuitLists.projectId, projectId),
|
||||||
|
eq(circuitLists.distributionBoardId, distributionBoardId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async existsInProject(projectId: string, circuitListId: string) {
|
||||||
|
const [row] = await db
|
||||||
|
.select({ id: circuitLists.id })
|
||||||
|
.from(circuitLists)
|
||||||
|
.where(and(eq(circuitLists.projectId, projectId), eq(circuitLists.id, circuitListId)))
|
||||||
|
.limit(1);
|
||||||
|
return Boolean(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(projectId: string, circuitListId: string) {
|
||||||
|
const [row] = await db
|
||||||
|
.select()
|
||||||
|
.from(circuitLists)
|
||||||
|
.where(and(eq(circuitLists.projectId, projectId), eq(circuitLists.id, circuitListId)))
|
||||||
|
.limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,22 @@ export class ConsumerRepository {
|
|||||||
id,
|
id,
|
||||||
projectId: input.projectId,
|
projectId: input.projectId,
|
||||||
distributionBoardId: input.distributionBoardId ?? null,
|
distributionBoardId: input.distributionBoardId ?? null,
|
||||||
|
circuitListId: input.circuitListId ?? null,
|
||||||
|
roomId: input.roomId ?? null,
|
||||||
|
circuitNumber: input.circuitNumber ?? null,
|
||||||
|
description: input.description ?? null,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
category: input.category ?? null,
|
category: input.category ?? null,
|
||||||
|
deviceType: input.deviceType ?? null,
|
||||||
|
phaseType: input.phaseType ?? null,
|
||||||
|
tradeOrCostGroup: input.tradeOrCostGroup ?? null,
|
||||||
|
group: input.group ?? null,
|
||||||
|
protectionType: input.protectionType ?? null,
|
||||||
|
protectionRatedCurrent: input.protectionRatedCurrent ?? null,
|
||||||
|
protectionCharacteristic: input.protectionCharacteristic ?? null,
|
||||||
|
cableType: input.cableType ?? null,
|
||||||
|
cableCrossSection: input.cableCrossSection ?? null,
|
||||||
|
comment: input.comment ?? null,
|
||||||
quantity: input.quantity,
|
quantity: input.quantity,
|
||||||
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||||
demandFactor: input.demandFactor,
|
demandFactor: input.demandFactor,
|
||||||
@@ -37,8 +51,22 @@ export class ConsumerRepository {
|
|||||||
.set({
|
.set({
|
||||||
projectId: input.projectId,
|
projectId: input.projectId,
|
||||||
distributionBoardId: input.distributionBoardId ?? null,
|
distributionBoardId: input.distributionBoardId ?? null,
|
||||||
|
circuitListId: input.circuitListId ?? null,
|
||||||
|
roomId: input.roomId ?? null,
|
||||||
|
circuitNumber: input.circuitNumber ?? null,
|
||||||
|
description: input.description ?? null,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
category: input.category ?? null,
|
category: input.category ?? null,
|
||||||
|
deviceType: input.deviceType ?? null,
|
||||||
|
phaseType: input.phaseType ?? null,
|
||||||
|
tradeOrCostGroup: input.tradeOrCostGroup ?? null,
|
||||||
|
group: input.group ?? null,
|
||||||
|
protectionType: input.protectionType ?? null,
|
||||||
|
protectionRatedCurrent: input.protectionRatedCurrent ?? null,
|
||||||
|
protectionCharacteristic: input.protectionCharacteristic ?? null,
|
||||||
|
cableType: input.cableType ?? null,
|
||||||
|
cableCrossSection: input.cableCrossSection ?? null,
|
||||||
|
comment: input.comment ?? null,
|
||||||
quantity: input.quantity,
|
quantity: input.quantity,
|
||||||
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||||
demandFactor: input.demandFactor,
|
demandFactor: input.demandFactor,
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import { and, asc, eq } from "drizzle-orm";
|
||||||
|
import { db } from "../client.js";
|
||||||
|
import { floors } from "../schema/floors.js";
|
||||||
|
|
||||||
|
export class FloorRepository {
|
||||||
|
async listByProject(projectId: string) {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(floors)
|
||||||
|
.where(eq(floors.projectId, projectId))
|
||||||
|
.orderBy(asc(floors.sortOrder), asc(floors.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(projectId: string, name: string) {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const existing = await this.listByProject(projectId);
|
||||||
|
const floor = {
|
||||||
|
id,
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
sortOrder: existing.length,
|
||||||
|
};
|
||||||
|
await db.insert(floors).values(floor);
|
||||||
|
return floor;
|
||||||
|
}
|
||||||
|
|
||||||
|
async existsInProject(projectId: string, floorId: string) {
|
||||||
|
const [row] = await db
|
||||||
|
.select({ id: floors.id })
|
||||||
|
.from(floors)
|
||||||
|
.where(and(eq(floors.projectId, projectId), eq(floors.id, floorId)))
|
||||||
|
.limit(1);
|
||||||
|
return Boolean(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "../client.js";
|
||||||
|
import { globalDevices } from "../schema/global-devices.js";
|
||||||
|
import type {
|
||||||
|
CreateGlobalDeviceInput,
|
||||||
|
UpdateGlobalDeviceInput,
|
||||||
|
} from "../../shared/validation/global-device.schemas.js";
|
||||||
|
|
||||||
|
export class GlobalDeviceRepository {
|
||||||
|
async list() {
|
||||||
|
return db.select().from(globalDevices);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(input: CreateGlobalDeviceInput) {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
await db.insert(globalDevices).values({
|
||||||
|
id,
|
||||||
|
name: input.name,
|
||||||
|
category: input.category ?? null,
|
||||||
|
quantity: input.quantity,
|
||||||
|
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||||
|
demandFactor: input.demandFactor,
|
||||||
|
voltageV: input.voltageV ?? null,
|
||||||
|
phaseCount: input.phaseCount ?? null,
|
||||||
|
powerFactor: input.powerFactor ?? null,
|
||||||
|
note: input.note ?? null,
|
||||||
|
});
|
||||||
|
return { id, ...input };
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(globalDeviceId: string, input: UpdateGlobalDeviceInput) {
|
||||||
|
await db
|
||||||
|
.update(globalDevices)
|
||||||
|
.set({
|
||||||
|
name: input.name,
|
||||||
|
category: input.category ?? null,
|
||||||
|
quantity: input.quantity,
|
||||||
|
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||||
|
demandFactor: input.demandFactor,
|
||||||
|
voltageV: input.voltageV ?? null,
|
||||||
|
phaseCount: input.phaseCount ?? null,
|
||||||
|
powerFactor: input.powerFactor ?? null,
|
||||||
|
note: input.note ?? null,
|
||||||
|
})
|
||||||
|
.where(eq(globalDevices.id, globalDeviceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(globalDeviceId: string) {
|
||||||
|
const [row] = await db.select().from(globalDevices).where(eq(globalDevices.id, globalDeviceId)).limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(globalDeviceId: string) {
|
||||||
|
await db.delete(globalDevices).where(eq(globalDevices.id, globalDeviceId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { db } from "../client.js";
|
||||||
|
import { projectDevices } from "../schema/project-devices.js";
|
||||||
|
import type {
|
||||||
|
CreateProjectDeviceInput,
|
||||||
|
UpdateProjectDeviceInput,
|
||||||
|
} from "../../shared/validation/project-device.schemas.js";
|
||||||
|
|
||||||
|
export class ProjectDeviceRepository {
|
||||||
|
async listByProject(projectId: string) {
|
||||||
|
return db.select().from(projectDevices).where(eq(projectDevices.projectId, projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(projectId: string, input: CreateProjectDeviceInput) {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
await db.insert(projectDevices).values({
|
||||||
|
id,
|
||||||
|
projectId,
|
||||||
|
name: input.name,
|
||||||
|
category: input.category ?? null,
|
||||||
|
quantity: input.quantity,
|
||||||
|
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||||
|
demandFactor: input.demandFactor,
|
||||||
|
voltageV: input.voltageV ?? null,
|
||||||
|
phaseCount: input.phaseCount ?? null,
|
||||||
|
powerFactor: input.powerFactor ?? null,
|
||||||
|
note: input.note ?? null,
|
||||||
|
});
|
||||||
|
return { id, projectId, ...input };
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(projectId: string, projectDeviceId: string) {
|
||||||
|
const [row] = await db
|
||||||
|
.select()
|
||||||
|
.from(projectDevices)
|
||||||
|
.where(
|
||||||
|
and(eq(projectDevices.id, projectDeviceId), eq(projectDevices.projectId, projectId))
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(projectId: string, projectDeviceId: string, input: UpdateProjectDeviceInput) {
|
||||||
|
await db
|
||||||
|
.update(projectDevices)
|
||||||
|
.set({
|
||||||
|
name: input.name,
|
||||||
|
category: input.category ?? null,
|
||||||
|
quantity: input.quantity,
|
||||||
|
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||||
|
demandFactor: input.demandFactor,
|
||||||
|
voltageV: input.voltageV ?? null,
|
||||||
|
phaseCount: input.phaseCount ?? null,
|
||||||
|
powerFactor: input.powerFactor ?? null,
|
||||||
|
note: input.note ?? null,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(eq(projectDevices.id, projectDeviceId), eq(projectDevices.projectId, projectId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(projectId: string, projectDeviceId: string) {
|
||||||
|
await db
|
||||||
|
.delete(projectDevices)
|
||||||
|
.where(
|
||||||
|
and(eq(projectDevices.id, projectDeviceId), eq(projectDevices.projectId, projectId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,20 +2,44 @@ import crypto from "node:crypto";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "../client.js";
|
import { db } from "../client.js";
|
||||||
import { projects } from "../schema/projects.js";
|
import { projects } from "../schema/projects.js";
|
||||||
|
import type {
|
||||||
|
CreateProjectInput,
|
||||||
|
UpdateProjectSettingsInput,
|
||||||
|
} from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
export class ProjectRepository {
|
export class ProjectRepository {
|
||||||
async list() {
|
async list() {
|
||||||
return db.select().from(projects);
|
return db.select().from(projects);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(name: string) {
|
async create(input: CreateProjectInput) {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
await db.insert(projects).values({ id, name });
|
const project = {
|
||||||
return { id, name };
|
id,
|
||||||
|
name: input.name,
|
||||||
|
singlePhaseVoltageV: input.singlePhaseVoltageV ?? 230,
|
||||||
|
threePhaseVoltageV: input.threePhaseVoltageV ?? 400,
|
||||||
|
};
|
||||||
|
await db.insert(projects).values(project);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(projectId: string) {
|
||||||
|
const [row] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSettings(projectId: string, input: UpdateProjectSettingsInput) {
|
||||||
|
await db
|
||||||
|
.update(projects)
|
||||||
|
.set({
|
||||||
|
singlePhaseVoltageV: input.singlePhaseVoltageV,
|
||||||
|
threePhaseVoltageV: input.threePhaseVoltageV,
|
||||||
|
})
|
||||||
|
.where(eq(projects.id, projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(projectId: string) {
|
async delete(projectId: string) {
|
||||||
await db.delete(projects).where(eq(projects.id, projectId));
|
await db.delete(projects).where(eq(projects.id, projectId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import { and, asc, eq } from "drizzle-orm";
|
||||||
|
import { db } from "../client.js";
|
||||||
|
import { rooms } from "../schema/rooms.js";
|
||||||
|
import type { CreateRoomInput } from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
|
export class RoomRepository {
|
||||||
|
async listByProject(projectId: string) {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(rooms)
|
||||||
|
.where(eq(rooms.projectId, projectId))
|
||||||
|
.orderBy(asc(rooms.roomNumber), asc(rooms.roomName));
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(projectId: string, input: CreateRoomInput) {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const room = {
|
||||||
|
id,
|
||||||
|
projectId,
|
||||||
|
floorId: input.floorId ?? null,
|
||||||
|
roomNumber: input.roomNumber,
|
||||||
|
roomName: input.roomName,
|
||||||
|
};
|
||||||
|
await db.insert(rooms).values(room);
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
async existsInProject(projectId: string, roomId: string) {
|
||||||
|
const [row] = await db
|
||||||
|
.select({ id: rooms.id })
|
||||||
|
.from(rooms)
|
||||||
|
.where(and(eq(rooms.projectId, projectId), eq(rooms.id, roomId)))
|
||||||
|
.limit(1);
|
||||||
|
return Boolean(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(roomId: string) {
|
||||||
|
const [row] = await db.select().from(rooms).where(eq(rooms.id, roomId)).limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
||||||
|
import { distributionBoards } from "./distribution-boards.js";
|
||||||
|
import { projects } from "./projects.js";
|
||||||
|
|
||||||
|
export const circuitLists = sqliteTable(
|
||||||
|
"circuit_lists",
|
||||||
|
{
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
projectId: text("project_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => projects.id, { onDelete: "cascade" }),
|
||||||
|
distributionBoardId: text("distribution_board_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => distributionBoards.id, { onDelete: "cascade" }),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
},
|
||||||
|
(table) => [unique("circuit_lists_distribution_board_id_unique").on(table.distributionBoardId)]
|
||||||
|
);
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
import { circuitLists } from "./circuit-lists.js";
|
||||||
import { distributionBoards } from "./distribution-boards.js";
|
import { distributionBoards } from "./distribution-boards.js";
|
||||||
import { projects } from "./projects.js";
|
import { projects } from "./projects.js";
|
||||||
|
import { rooms } from "./rooms.js";
|
||||||
|
|
||||||
export const consumers = sqliteTable("consumers", {
|
export const consumers = sqliteTable("consumers", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
@@ -10,8 +12,26 @@ export const consumers = sqliteTable("consumers", {
|
|||||||
distributionBoardId: text("distribution_board_id").references(() => distributionBoards.id, {
|
distributionBoardId: text("distribution_board_id").references(() => distributionBoards.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
circuitListId: text("circuit_list_id").references(() => circuitLists.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
roomId: text("room_id").references(() => rooms.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
circuitNumber: text("circuit_number"),
|
||||||
|
description: text("description"),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
category: text("category"),
|
category: text("category"),
|
||||||
|
deviceType: text("device_type"),
|
||||||
|
phaseType: text("phase_type"),
|
||||||
|
tradeOrCostGroup: text("trade_or_cost_group"),
|
||||||
|
group: text("group_name"),
|
||||||
|
protectionType: text("protection_type"),
|
||||||
|
protectionRatedCurrent: real("protection_rated_current"),
|
||||||
|
protectionCharacteristic: text("protection_characteristic"),
|
||||||
|
cableType: text("cable_type"),
|
||||||
|
cableCrossSection: text("cable_cross_section"),
|
||||||
|
comment: text("comment"),
|
||||||
quantity: integer("quantity").notNull(),
|
quantity: integer("quantity").notNull(),
|
||||||
installedPowerPerUnitKw: real("installed_power_per_unit_kw").notNull(),
|
installedPowerPerUnitKw: real("installed_power_per_unit_kw").notNull(),
|
||||||
demandFactor: real("demand_factor").notNull(),
|
demandFactor: real("demand_factor").notNull(),
|
||||||
@@ -20,4 +40,3 @@ export const consumers = sqliteTable("consumers", {
|
|||||||
powerFactor: real("power_factor"),
|
powerFactor: real("power_factor"),
|
||||||
note: text("note"),
|
note: text("note"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
import { projects } from "./projects.js";
|
||||||
|
|
||||||
|
export const floors = sqliteTable("floors", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
projectId: text("project_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => projects.id, { onDelete: "cascade" }),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
sortOrder: integer("sort_order").notNull().default(0),
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
export const globalDevices = sqliteTable("global_devices", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
category: text("category"),
|
||||||
|
quantity: integer("quantity").notNull(),
|
||||||
|
installedPowerPerUnitKw: real("installed_power_per_unit_kw").notNull(),
|
||||||
|
demandFactor: real("demand_factor").notNull(),
|
||||||
|
voltageV: real("voltage_v"),
|
||||||
|
phaseCount: integer("phase_count"),
|
||||||
|
powerFactor: real("power_factor"),
|
||||||
|
note: text("note"),
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
import { projects } from "./projects.js";
|
||||||
|
|
||||||
|
export const projectDevices = sqliteTable("project_devices", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
projectId: text("project_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => projects.id, { onDelete: "cascade" }),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
category: text("category"),
|
||||||
|
quantity: integer("quantity").notNull(),
|
||||||
|
installedPowerPerUnitKw: real("installed_power_per_unit_kw").notNull(),
|
||||||
|
demandFactor: real("demand_factor").notNull(),
|
||||||
|
voltageV: real("voltage_v"),
|
||||||
|
phaseCount: integer("phase_count"),
|
||||||
|
powerFactor: real("power_factor"),
|
||||||
|
note: text("note"),
|
||||||
|
});
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
export const projects = sqliteTable("projects", {
|
export const projects = sqliteTable("projects", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
|
singlePhaseVoltageV: integer("single_phase_voltage_v").notNull().default(230),
|
||||||
|
threePhaseVoltageV: integer("three_phase_voltage_v").notNull().default(400),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
import { floors } from "./floors.js";
|
||||||
|
import { projects } from "./projects.js";
|
||||||
|
|
||||||
|
export const rooms = sqliteTable("rooms", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
projectId: text("project_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => projects.id, { onDelete: "cascade" }),
|
||||||
|
floorId: text("floor_id").references(() => floors.id, { onDelete: "set null" }),
|
||||||
|
roomNumber: text("room_number").notNull(),
|
||||||
|
roomName: text("room_name").notNull(),
|
||||||
|
});
|
||||||
@@ -2,8 +2,26 @@ export interface Consumer {
|
|||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
distributionBoardId?: string;
|
distributionBoardId?: string;
|
||||||
|
circuitListId?: string;
|
||||||
|
roomId?: string;
|
||||||
|
roomNumber?: string;
|
||||||
|
roomName?: string;
|
||||||
|
floorId?: string;
|
||||||
|
floorName?: string;
|
||||||
|
circuitNumber?: string;
|
||||||
|
description?: string;
|
||||||
name: string;
|
name: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
|
deviceType?: string;
|
||||||
|
phaseType?: string;
|
||||||
|
tradeOrCostGroup?: string;
|
||||||
|
group?: string;
|
||||||
|
protectionType?: string;
|
||||||
|
protectionRatedCurrent?: number;
|
||||||
|
protectionCharacteristic?: string;
|
||||||
|
cableType?: string;
|
||||||
|
cableCrossSection?: string;
|
||||||
|
comment?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
installedPowerPerUnitKw: number;
|
installedPowerPerUnitKw: number;
|
||||||
demandFactor: number;
|
demandFactor: number;
|
||||||
@@ -12,4 +30,3 @@ export interface Consumer {
|
|||||||
powerFactor?: number;
|
powerFactor?: number;
|
||||||
note?: string;
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,20 @@ import type { Consumer } from "../models/consumer.model.js";
|
|||||||
export interface ConsumerWithCalculatedValues extends Consumer {
|
export interface ConsumerWithCalculatedValues extends Consumer {
|
||||||
installedPowerKw: number;
|
installedPowerKw: number;
|
||||||
demandPowerKw: number;
|
demandPowerKw: number;
|
||||||
|
effectiveVoltageV?: number;
|
||||||
currentA?: number;
|
currentA?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectVoltageDefaults {
|
||||||
|
singlePhaseVoltageV: number;
|
||||||
|
threePhaseVoltageV: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class PowerBalanceService {
|
export class PowerBalanceService {
|
||||||
enrichConsumer(consumer: Consumer): ConsumerWithCalculatedValues {
|
enrichConsumer(
|
||||||
|
consumer: Consumer,
|
||||||
|
projectVoltageDefaults?: ProjectVoltageDefaults
|
||||||
|
): ConsumerWithCalculatedValues {
|
||||||
const installedPowerKw = calculateInstalledPowerKw({
|
const installedPowerKw = calculateInstalledPowerKw({
|
||||||
quantity: consumer.quantity,
|
quantity: consumer.quantity,
|
||||||
installedPowerPerUnitKw: consumer.installedPowerPerUnitKw,
|
installedPowerPerUnitKw: consumer.installedPowerPerUnitKw,
|
||||||
@@ -24,11 +33,19 @@ export class PowerBalanceService {
|
|||||||
demandFactor: consumer.demandFactor,
|
demandFactor: consumer.demandFactor,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const effectiveVoltageV =
|
||||||
|
consumer.voltageV ??
|
||||||
|
(consumer.phaseCount === 1
|
||||||
|
? projectVoltageDefaults?.singlePhaseVoltageV
|
||||||
|
: consumer.phaseCount === 3
|
||||||
|
? projectVoltageDefaults?.threePhaseVoltageV
|
||||||
|
: undefined);
|
||||||
|
|
||||||
let currentA: number | undefined;
|
let currentA: number | undefined;
|
||||||
if (consumer.voltageV && consumer.phaseCount && consumer.powerFactor) {
|
if (effectiveVoltageV && consumer.phaseCount && consumer.powerFactor) {
|
||||||
currentA = calculateCurrentA({
|
currentA = calculateCurrentA({
|
||||||
demandPowerKw,
|
demandPowerKw,
|
||||||
voltageV: consumer.voltageV,
|
voltageV: effectiveVoltageV,
|
||||||
phaseCount: consumer.phaseCount,
|
phaseCount: consumer.phaseCount,
|
||||||
powerFactor: consumer.powerFactor,
|
powerFactor: consumer.powerFactor,
|
||||||
});
|
});
|
||||||
@@ -38,8 +55,8 @@ export class PowerBalanceService {
|
|||||||
...consumer,
|
...consumer,
|
||||||
installedPowerKw,
|
installedPowerKw,
|
||||||
demandPowerKw,
|
demandPowerKw,
|
||||||
|
effectiveVoltageV,
|
||||||
currentA,
|
currentA,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,34 @@
|
|||||||
export interface ProjectDto {
|
export interface ProjectDto {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
singlePhaseVoltageV: number;
|
||||||
|
threePhaseVoltageV: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConsumerWithCalculatedValues {
|
export interface ConsumerWithCalculatedValues {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
distributionBoardId?: string | null;
|
distributionBoardId?: string | null;
|
||||||
|
circuitListId?: string | null;
|
||||||
|
roomId?: string | null;
|
||||||
|
roomNumber?: string;
|
||||||
|
roomName?: string;
|
||||||
|
floorId?: string;
|
||||||
|
floorName?: string;
|
||||||
|
circuitNumber?: string;
|
||||||
|
description?: string;
|
||||||
name: string;
|
name: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
|
deviceType?: string;
|
||||||
|
phaseType?: string;
|
||||||
|
tradeOrCostGroup?: string;
|
||||||
|
group?: string;
|
||||||
|
protectionType?: string;
|
||||||
|
protectionRatedCurrent?: number;
|
||||||
|
protectionCharacteristic?: string;
|
||||||
|
cableType?: string;
|
||||||
|
cableCrossSection?: string;
|
||||||
|
comment?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
installedPowerPerUnitKw: number;
|
installedPowerPerUnitKw: number;
|
||||||
demandFactor: number;
|
demandFactor: number;
|
||||||
@@ -18,6 +38,7 @@ export interface ConsumerWithCalculatedValues {
|
|||||||
note?: string;
|
note?: string;
|
||||||
installedPowerKw: number;
|
installedPowerKw: number;
|
||||||
demandPowerKw: number;
|
demandPowerKw: number;
|
||||||
|
effectiveVoltageV?: number;
|
||||||
currentA?: number;
|
currentA?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,11 +48,74 @@ export interface DistributionBoardDto {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CircuitListDto {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
distributionBoardId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FloorDto {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoomDto {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
floorId: string | null;
|
||||||
|
roomNumber: string;
|
||||||
|
roomName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GlobalDeviceDto {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: string | null;
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnitKw: number;
|
||||||
|
demandFactor: number;
|
||||||
|
voltageV: number | null;
|
||||||
|
phaseCount: 1 | 3 | null;
|
||||||
|
powerFactor: number | null;
|
||||||
|
note: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectDeviceDto {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
category: string | null;
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnitKw: number;
|
||||||
|
demandFactor: number;
|
||||||
|
voltageV: number | null;
|
||||||
|
phaseCount: 1 | 3 | null;
|
||||||
|
powerFactor: number | null;
|
||||||
|
note: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateConsumerInput {
|
export interface CreateConsumerInput {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
distributionBoardId?: string;
|
distributionBoardId?: string;
|
||||||
|
circuitListId?: string;
|
||||||
|
roomId?: string;
|
||||||
|
circuitNumber?: string;
|
||||||
|
description?: string;
|
||||||
name: string;
|
name: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
|
deviceType?: string;
|
||||||
|
phaseType?: string;
|
||||||
|
tradeOrCostGroup?: string;
|
||||||
|
group?: string;
|
||||||
|
protectionType?: string;
|
||||||
|
protectionRatedCurrent?: number;
|
||||||
|
protectionCharacteristic?: string;
|
||||||
|
cableType?: string;
|
||||||
|
cableCrossSection?: string;
|
||||||
|
comment?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
installedPowerPerUnitKw: number;
|
installedPowerPerUnitKw: number;
|
||||||
demandFactor: number;
|
demandFactor: number;
|
||||||
@@ -42,3 +126,37 @@ export interface CreateConsumerInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateConsumerInput extends CreateConsumerInput {}
|
export interface UpdateConsumerInput extends CreateConsumerInput {}
|
||||||
|
|
||||||
|
export interface CreateFloorInput {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateRoomInput {
|
||||||
|
floorId?: string;
|
||||||
|
roomNumber: string;
|
||||||
|
roomName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateGlobalDeviceInput {
|
||||||
|
name: string;
|
||||||
|
category?: string;
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnitKw: number;
|
||||||
|
demandFactor: number;
|
||||||
|
voltageV?: number;
|
||||||
|
phaseCount?: 1 | 3;
|
||||||
|
powerFactor?: number;
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateProjectDeviceInput {
|
||||||
|
name: string;
|
||||||
|
category?: string;
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnitKw: number;
|
||||||
|
demandFactor: number;
|
||||||
|
voltageV?: number;
|
||||||
|
phaseCount?: 1 | 3;
|
||||||
|
powerFactor?: number;
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|||||||
+106
-6
@@ -1,8 +1,17 @@
|
|||||||
import type {
|
import type {
|
||||||
|
CircuitListDto,
|
||||||
|
CreateFloorInput,
|
||||||
|
CreateProjectDeviceInput,
|
||||||
|
CreateRoomInput,
|
||||||
ConsumerWithCalculatedValues,
|
ConsumerWithCalculatedValues,
|
||||||
CreateConsumerInput,
|
CreateConsumerInput,
|
||||||
|
CreateGlobalDeviceInput,
|
||||||
DistributionBoardDto,
|
DistributionBoardDto,
|
||||||
|
FloorDto,
|
||||||
|
GlobalDeviceDto,
|
||||||
|
ProjectDeviceDto,
|
||||||
ProjectDto,
|
ProjectDto,
|
||||||
|
RoomDto,
|
||||||
UpdateConsumerInput,
|
UpdateConsumerInput,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
@@ -13,6 +22,7 @@ async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...init?.headers,
|
...init?.headers,
|
||||||
},
|
},
|
||||||
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -20,6 +30,10 @@ async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
|||||||
throw new Error(details || `Request failed with ${response.status}`);
|
throw new Error(details || `Request failed with ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
return undefined as T;
|
||||||
|
}
|
||||||
|
|
||||||
return response.json() as Promise<T>;
|
return response.json() as Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +41,10 @@ export function listProjects() {
|
|||||||
return request<ProjectDto[]>("/api/projects");
|
return request<ProjectDto[]>("/api/projects");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getProject(projectId: string) {
|
||||||
|
return request<ProjectDto>(`/api/projects/${projectId}`);
|
||||||
|
}
|
||||||
|
|
||||||
export function createProject(name: string) {
|
export function createProject(name: string) {
|
||||||
return request<ProjectDto>("/api/projects", {
|
return request<ProjectDto>("/api/projects", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -34,6 +52,16 @@ export function createProject(name: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateProjectSettings(
|
||||||
|
projectId: string,
|
||||||
|
input: { singlePhaseVoltageV: number; threePhaseVoltageV: number }
|
||||||
|
) {
|
||||||
|
return request<ProjectDto>(`/api/projects/${projectId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function listDistributionBoards(projectId: string) {
|
export function listDistributionBoards(projectId: string) {
|
||||||
return request<DistributionBoardDto[]>(`/api/projects/${projectId}/distribution-boards`);
|
return request<DistributionBoardDto[]>(`/api/projects/${projectId}/distribution-boards`);
|
||||||
}
|
}
|
||||||
@@ -45,6 +73,32 @@ export function createDistributionBoard(projectId: string, name: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listCircuitLists(projectId: string) {
|
||||||
|
return request<CircuitListDto[]>(`/api/projects/${projectId}/circuit-lists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listFloors(projectId: string) {
|
||||||
|
return request<FloorDto[]>(`/api/projects/${projectId}/floors`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFloor(projectId: string, input: CreateFloorInput) {
|
||||||
|
return request<FloorDto>(`/api/projects/${projectId}/floors`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listRooms(projectId: string) {
|
||||||
|
return request<RoomDto[]>(`/api/projects/${projectId}/rooms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRoom(projectId: string, input: CreateRoomInput) {
|
||||||
|
return request<RoomDto>(`/api/projects/${projectId}/rooms`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function listConsumers(projectId: string) {
|
export function listConsumers(projectId: string) {
|
||||||
return request<ConsumerWithCalculatedValues[]>(`/api/consumers/projects/${projectId}`);
|
return request<ConsumerWithCalculatedValues[]>(`/api/consumers/projects/${projectId}`);
|
||||||
}
|
}
|
||||||
@@ -63,10 +117,56 @@ export function updateConsumer(consumerId: string, input: UpdateConsumerInput) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteConsumer(consumerId: string) {
|
export function deleteConsumer(consumerId: string) {
|
||||||
const response = await fetch(`/api/consumers/${consumerId}`, { method: "DELETE" });
|
return request<void>(`/api/consumers/${consumerId}`, { method: "DELETE" });
|
||||||
if (!response.ok) {
|
}
|
||||||
const details = await response.text();
|
|
||||||
throw new Error(details || `Request failed with ${response.status}`);
|
export function listGlobalDevices() {
|
||||||
}
|
return request<GlobalDeviceDto[]>("/api/global-devices");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGlobalDevice(input: CreateGlobalDeviceInput) {
|
||||||
|
return request<GlobalDeviceDto>("/api/global-devices", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateGlobalDevice(globalDeviceId: string, input: CreateGlobalDeviceInput) {
|
||||||
|
return request<GlobalDeviceDto>(`/api/global-devices/${globalDeviceId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteGlobalDevice(globalDeviceId: string) {
|
||||||
|
return request<void>(`/api/global-devices/${globalDeviceId}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listProjectDevices(projectId: string) {
|
||||||
|
return request<ProjectDeviceDto[]>(`/api/project-devices/projects/${projectId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProjectDevice(projectId: string, input: CreateProjectDeviceInput) {
|
||||||
|
return request<ProjectDeviceDto>(`/api/project-devices/projects/${projectId}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateProjectDevice(
|
||||||
|
projectId: string,
|
||||||
|
projectDeviceId: string,
|
||||||
|
input: CreateProjectDeviceInput
|
||||||
|
) {
|
||||||
|
return request<ProjectDeviceDto>(`/api/project-devices/projects/${projectId}/${projectDeviceId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteProjectDevice(projectId: string, projectDeviceId: string) {
|
||||||
|
return request<void>(`/api/project-devices/projects/${projectId}/${projectDeviceId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
||||||
|
|
||||||
|
const circuitListRepository = new CircuitListRepository();
|
||||||
|
|
||||||
|
export async function listCircuitListsByProject(req: Request, res: Response) {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
if (typeof projectId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid projectId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await circuitListRepository.listByProject(projectId);
|
||||||
|
return res.json(result);
|
||||||
|
}
|
||||||
@@ -1,16 +1,54 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
|
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
||||||
import { ConsumerRepository } from "../../db/repositories/consumer.repository.js";
|
import { ConsumerRepository } from "../../db/repositories/consumer.repository.js";
|
||||||
import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
|
import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
|
||||||
|
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
||||||
|
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
||||||
|
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
||||||
|
import type { Consumer } from "../../domain/models/consumer.model.js";
|
||||||
import { PowerBalanceService } from "../../domain/services/power-balance.service.js";
|
import { PowerBalanceService } from "../../domain/services/power-balance.service.js";
|
||||||
import {
|
import {
|
||||||
createConsumerSchema,
|
createConsumerSchema,
|
||||||
updateConsumerSchema,
|
updateConsumerSchema,
|
||||||
} from "../../shared/validation/consumer.schemas.js";
|
} from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
|
const circuitListRepository = new CircuitListRepository();
|
||||||
const consumerRepository = new ConsumerRepository();
|
const consumerRepository = new ConsumerRepository();
|
||||||
const distributionBoardRepository = new DistributionBoardRepository();
|
const distributionBoardRepository = new DistributionBoardRepository();
|
||||||
|
const floorRepository = new FloorRepository();
|
||||||
|
const projectRepository = new ProjectRepository();
|
||||||
|
const roomRepository = new RoomRepository();
|
||||||
const powerBalanceService = new PowerBalanceService();
|
const powerBalanceService = new PowerBalanceService();
|
||||||
|
|
||||||
|
type ConsumerRow = {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
distributionBoardId: string | null;
|
||||||
|
circuitListId: string | null;
|
||||||
|
roomId: string | null;
|
||||||
|
circuitNumber: string | null;
|
||||||
|
description: string | null;
|
||||||
|
name: string;
|
||||||
|
category: string | null;
|
||||||
|
deviceType: string | null;
|
||||||
|
phaseType: string | null;
|
||||||
|
tradeOrCostGroup: string | null;
|
||||||
|
group: string | null;
|
||||||
|
protectionType: string | null;
|
||||||
|
protectionRatedCurrent: number | null;
|
||||||
|
protectionCharacteristic: string | null;
|
||||||
|
cableType: string | null;
|
||||||
|
cableCrossSection: string | null;
|
||||||
|
comment: string | null;
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnitKw: number;
|
||||||
|
demandFactor: number;
|
||||||
|
voltageV: number | null;
|
||||||
|
phaseCount: number | null;
|
||||||
|
powerFactor: number | null;
|
||||||
|
note: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
async function validateDistributionBoardOwnership(
|
async function validateDistributionBoardOwnership(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
distributionBoardId: string | undefined
|
distributionBoardId: string | undefined
|
||||||
@@ -21,19 +59,91 @@ async function validateDistributionBoardOwnership(
|
|||||||
return distributionBoardRepository.existsInProject(projectId, distributionBoardId);
|
return distributionBoardRepository.existsInProject(projectId, distributionBoardId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listConsumersByProject(req: Request, res: Response) {
|
async function validateRoomOwnership(projectId: string, roomId: string | undefined) {
|
||||||
const { projectId } = req.params;
|
if (!roomId) {
|
||||||
if (typeof projectId !== "string") {
|
return true;
|
||||||
return res.status(400).json({ error: "Invalid projectId" });
|
|
||||||
}
|
}
|
||||||
const rows = await consumerRepository.listByProject(projectId);
|
return roomRepository.existsInProject(projectId, roomId);
|
||||||
const enriched = rows.map((row) =>
|
}
|
||||||
powerBalanceService.enrichConsumer({
|
|
||||||
|
async function resolveCircuitScope(input: {
|
||||||
|
projectId: string;
|
||||||
|
distributionBoardId?: string;
|
||||||
|
circuitListId?: string;
|
||||||
|
}) {
|
||||||
|
let distributionBoardId = input.distributionBoardId;
|
||||||
|
let circuitListId = input.circuitListId;
|
||||||
|
|
||||||
|
if (distributionBoardId) {
|
||||||
|
const linkedList = await circuitListRepository.findByDistributionBoardId(
|
||||||
|
input.projectId,
|
||||||
|
distributionBoardId
|
||||||
|
);
|
||||||
|
if (!linkedList) {
|
||||||
|
return { ok: false as const, error: "No circuit list found for the provided distribution board." };
|
||||||
|
}
|
||||||
|
if (circuitListId && circuitListId !== linkedList.id) {
|
||||||
|
return {
|
||||||
|
ok: false as const,
|
||||||
|
error: "Circuit list does not match the provided distribution board.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
circuitListId = linkedList.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (circuitListId) {
|
||||||
|
const list = await circuitListRepository.findById(input.projectId, circuitListId);
|
||||||
|
if (!list) {
|
||||||
|
return { ok: false as const, error: "Circuit list does not belong to the provided project." };
|
||||||
|
}
|
||||||
|
if (distributionBoardId && distributionBoardId !== list.distributionBoardId) {
|
||||||
|
return {
|
||||||
|
ok: false as const,
|
||||||
|
error: "Circuit list does not match the provided distribution board.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
distributionBoardId = list.distributionBoardId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true as const,
|
||||||
|
distributionBoardId,
|
||||||
|
circuitListId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConsumerFromRow(
|
||||||
|
row: ConsumerRow,
|
||||||
|
roomById: Map<string, { floorId: string | null; roomName: string; roomNumber: string }>,
|
||||||
|
floorById: Map<string, { name: string }>
|
||||||
|
): Consumer {
|
||||||
|
const room = row.roomId ? roomById.get(row.roomId) : undefined;
|
||||||
|
const floor = room?.floorId ? floorById.get(room.floorId) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
projectId: row.projectId,
|
projectId: row.projectId,
|
||||||
distributionBoardId: row.distributionBoardId ?? undefined,
|
distributionBoardId: row.distributionBoardId ?? undefined,
|
||||||
|
circuitListId: row.circuitListId ?? undefined,
|
||||||
|
roomId: row.roomId ?? undefined,
|
||||||
|
roomNumber: room?.roomNumber,
|
||||||
|
roomName: room?.roomName,
|
||||||
|
floorId: room?.floorId ?? undefined,
|
||||||
|
floorName: floor?.name,
|
||||||
|
circuitNumber: row.circuitNumber ?? undefined,
|
||||||
|
description: row.description ?? undefined,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
category: row.category ?? undefined,
|
category: row.category ?? undefined,
|
||||||
|
deviceType: row.deviceType ?? undefined,
|
||||||
|
phaseType: row.phaseType ?? undefined,
|
||||||
|
tradeOrCostGroup: row.tradeOrCostGroup ?? undefined,
|
||||||
|
group: row.group ?? undefined,
|
||||||
|
protectionType: row.protectionType ?? undefined,
|
||||||
|
protectionRatedCurrent: row.protectionRatedCurrent ?? undefined,
|
||||||
|
protectionCharacteristic: row.protectionCharacteristic ?? undefined,
|
||||||
|
cableType: row.cableType ?? undefined,
|
||||||
|
cableCrossSection: row.cableCrossSection ?? undefined,
|
||||||
|
comment: row.comment ?? undefined,
|
||||||
quantity: row.quantity,
|
quantity: row.quantity,
|
||||||
installedPowerPerUnitKw: row.installedPowerPerUnitKw,
|
installedPowerPerUnitKw: row.installedPowerPerUnitKw,
|
||||||
demandFactor: row.demandFactor,
|
demandFactor: row.demandFactor,
|
||||||
@@ -41,9 +151,42 @@ export async function listConsumersByProject(req: Request, res: Response) {
|
|||||||
phaseCount: row.phaseCount === 1 || row.phaseCount === 3 ? row.phaseCount : undefined,
|
phaseCount: row.phaseCount === 1 || row.phaseCount === 3 ? row.phaseCount : undefined,
|
||||||
powerFactor: row.powerFactor ?? undefined,
|
powerFactor: row.powerFactor ?? undefined,
|
||||||
note: row.note ?? undefined,
|
note: row.note ?? undefined,
|
||||||
})
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listConsumersByProject(req: Request, res: Response) {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
if (typeof projectId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid projectId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows, project, floors, rooms] = await Promise.all([
|
||||||
|
consumerRepository.listByProject(projectId),
|
||||||
|
projectRepository.findById(projectId),
|
||||||
|
floorRepository.listByProject(projectId),
|
||||||
|
roomRepository.listByProject(projectId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const roomById = new Map(
|
||||||
|
rooms.map((room) => [room.id, { floorId: room.floorId, roomName: room.roomName, roomNumber: room.roomNumber }])
|
||||||
);
|
);
|
||||||
res.json(enriched);
|
const floorById = new Map(floors.map((floor) => [floor.id, { name: floor.name }]));
|
||||||
|
|
||||||
|
const projectVoltageDefaults = project
|
||||||
|
? {
|
||||||
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||||
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const enriched = rows.map((row) =>
|
||||||
|
powerBalanceService.enrichConsumer(
|
||||||
|
buildConsumerFromRow(row as ConsumerRow, roomById, floorById),
|
||||||
|
projectVoltageDefaults
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json(enriched);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createConsumer(req: Request, res: Response) {
|
export async function createConsumer(req: Request, res: Response) {
|
||||||
@@ -52,18 +195,66 @@ export async function createConsumer(req: Request, res: Response) {
|
|||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasValidDistributionBoard = await validateDistributionBoardOwnership(
|
const [hasValidDistributionBoard, hasValidRoom] = await Promise.all([
|
||||||
parsed.data.projectId,
|
validateDistributionBoardOwnership(parsed.data.projectId, parsed.data.distributionBoardId),
|
||||||
parsed.data.distributionBoardId
|
validateRoomOwnership(parsed.data.projectId, parsed.data.roomId),
|
||||||
);
|
]);
|
||||||
if (!hasValidDistributionBoard) {
|
if (!hasValidDistributionBoard) {
|
||||||
return res
|
return res
|
||||||
.status(400)
|
.status(400)
|
||||||
.json({ error: "Distribution board does not belong to the provided project." });
|
.json({ error: "Distribution board does not belong to the provided project." });
|
||||||
}
|
}
|
||||||
|
if (!hasValidRoom) {
|
||||||
|
return res.status(400).json({ error: "Room does not belong to the provided project." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedScope = await resolveCircuitScope({
|
||||||
|
projectId: parsed.data.projectId,
|
||||||
|
distributionBoardId: parsed.data.distributionBoardId,
|
||||||
|
circuitListId: parsed.data.circuitListId,
|
||||||
|
});
|
||||||
|
if (!resolvedScope.ok) {
|
||||||
|
return res.status(400).json({ error: resolvedScope.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...parsed.data,
|
||||||
|
distributionBoardId: resolvedScope.distributionBoardId,
|
||||||
|
circuitListId: resolvedScope.circuitListId,
|
||||||
|
description: parsed.data.description ?? parsed.data.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
const created = await consumerRepository.create(payload);
|
||||||
|
const [project, floors, rooms] = await Promise.all([
|
||||||
|
projectRepository.findById(parsed.data.projectId),
|
||||||
|
floorRepository.listByProject(parsed.data.projectId),
|
||||||
|
roomRepository.listByProject(parsed.data.projectId),
|
||||||
|
]);
|
||||||
|
const roomById = new Map(
|
||||||
|
rooms.map((room) => [room.id, { floorId: room.floorId, roomName: room.roomName, roomNumber: room.roomNumber }])
|
||||||
|
);
|
||||||
|
const floorById = new Map(floors.map((floor) => [floor.id, { name: floor.name }]));
|
||||||
|
|
||||||
|
const enriched = powerBalanceService.enrichConsumer(
|
||||||
|
{
|
||||||
|
...(created as Consumer),
|
||||||
|
description: created.description ?? created.name,
|
||||||
|
roomNumber: created.roomId ? roomById.get(created.roomId)?.roomNumber : undefined,
|
||||||
|
roomName: created.roomId ? roomById.get(created.roomId)?.roomName : undefined,
|
||||||
|
floorId: created.roomId ? roomById.get(created.roomId)?.floorId ?? undefined : undefined,
|
||||||
|
floorName:
|
||||||
|
created.roomId && roomById.get(created.roomId)?.floorId
|
||||||
|
? floorById.get(roomById.get(created.roomId)!.floorId as string)?.name
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
project
|
||||||
|
? {
|
||||||
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||||
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
const created = await consumerRepository.create(parsed.data);
|
|
||||||
const enriched = powerBalanceService.enrichConsumer(created);
|
|
||||||
return res.status(201).json(enriched);
|
return res.status(201).json(enriched);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,36 +269,60 @@ export async function updateConsumer(req: Request, res: Response) {
|
|||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasValidDistributionBoard = await validateDistributionBoardOwnership(
|
const [hasValidDistributionBoard, hasValidRoom] = await Promise.all([
|
||||||
parsed.data.projectId,
|
validateDistributionBoardOwnership(parsed.data.projectId, parsed.data.distributionBoardId),
|
||||||
parsed.data.distributionBoardId
|
validateRoomOwnership(parsed.data.projectId, parsed.data.roomId),
|
||||||
);
|
]);
|
||||||
if (!hasValidDistributionBoard) {
|
if (!hasValidDistributionBoard) {
|
||||||
return res
|
return res
|
||||||
.status(400)
|
.status(400)
|
||||||
.json({ error: "Distribution board does not belong to the provided project." });
|
.json({ error: "Distribution board does not belong to the provided project." });
|
||||||
}
|
}
|
||||||
|
if (!hasValidRoom) {
|
||||||
|
return res.status(400).json({ error: "Room does not belong to the provided project." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedScope = await resolveCircuitScope({
|
||||||
|
projectId: parsed.data.projectId,
|
||||||
|
distributionBoardId: parsed.data.distributionBoardId,
|
||||||
|
circuitListId: parsed.data.circuitListId,
|
||||||
|
});
|
||||||
|
if (!resolvedScope.ok) {
|
||||||
|
return res.status(400).json({ error: resolvedScope.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
await consumerRepository.update(consumerId, {
|
||||||
|
...parsed.data,
|
||||||
|
distributionBoardId: resolvedScope.distributionBoardId,
|
||||||
|
circuitListId: resolvedScope.circuitListId,
|
||||||
|
description: parsed.data.description ?? parsed.data.name,
|
||||||
|
});
|
||||||
|
|
||||||
await consumerRepository.update(consumerId, parsed.data);
|
|
||||||
const row = await consumerRepository.findById(consumerId);
|
const row = await consumerRepository.findById(consumerId);
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return res.status(404).json({ error: "Consumer not found" });
|
return res.status(404).json({ error: "Consumer not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const enriched = powerBalanceService.enrichConsumer({
|
const [project, floors, rooms] = await Promise.all([
|
||||||
id: row.id,
|
projectRepository.findById(row.projectId),
|
||||||
projectId: row.projectId,
|
floorRepository.listByProject(row.projectId),
|
||||||
distributionBoardId: row.distributionBoardId ?? undefined,
|
roomRepository.listByProject(row.projectId),
|
||||||
name: row.name,
|
]);
|
||||||
category: row.category ?? undefined,
|
const roomById = new Map(
|
||||||
quantity: row.quantity,
|
rooms.map((room) => [room.id, { floorId: room.floorId, roomName: room.roomName, roomNumber: room.roomNumber }])
|
||||||
installedPowerPerUnitKw: row.installedPowerPerUnitKw,
|
);
|
||||||
demandFactor: row.demandFactor,
|
const floorById = new Map(floors.map((floor) => [floor.id, { name: floor.name }]));
|
||||||
voltageV: row.voltageV ?? undefined,
|
|
||||||
phaseCount: row.phaseCount === 1 || row.phaseCount === 3 ? row.phaseCount : undefined,
|
const enriched = powerBalanceService.enrichConsumer(
|
||||||
powerFactor: row.powerFactor ?? undefined,
|
buildConsumerFromRow(row as ConsumerRow, roomById, floorById),
|
||||||
note: row.note ?? undefined,
|
project
|
||||||
});
|
? {
|
||||||
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||||
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
return res.json(enriched);
|
return res.json(enriched);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
|
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
||||||
import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
|
import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
|
||||||
import { createDistributionBoardSchema } from "../../shared/validation/consumer.schemas.js";
|
import { createDistributionBoardSchema } from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
|
const circuitListRepository = new CircuitListRepository();
|
||||||
const distributionBoardRepository = new DistributionBoardRepository();
|
const distributionBoardRepository = new DistributionBoardRepository();
|
||||||
|
|
||||||
export async function listDistributionBoardsByProject(req: Request, res: Response) {
|
export async function listDistributionBoardsByProject(req: Request, res: Response) {
|
||||||
@@ -26,5 +28,10 @@ export async function createDistributionBoard(req: Request, res: Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const board = await distributionBoardRepository.create(projectId, parsed.data.name);
|
const board = await distributionBoardRepository.create(projectId, parsed.data.name);
|
||||||
|
await circuitListRepository.createForDistributionBoard({
|
||||||
|
projectId,
|
||||||
|
distributionBoardId: board.id,
|
||||||
|
name: `${board.name} Stromkreisliste`,
|
||||||
|
});
|
||||||
return res.status(201).json(board);
|
return res.status(201).json(board);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
||||||
|
import { createFloorSchema } from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
|
const floorRepository = new FloorRepository();
|
||||||
|
|
||||||
|
export async function listFloorsByProject(req: Request, res: Response) {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
if (typeof projectId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid projectId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await floorRepository.listByProject(projectId);
|
||||||
|
return res.json(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createFloor(req: Request, res: Response) {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
if (typeof projectId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid projectId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = createFloorSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
|
}
|
||||||
|
|
||||||
|
const floor = await floorRepository.create(projectId, parsed.data.name);
|
||||||
|
return res.status(201).json(floor);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import { GlobalDeviceRepository } from "../../db/repositories/global-device.repository.js";
|
||||||
|
import {
|
||||||
|
createGlobalDeviceSchema,
|
||||||
|
updateGlobalDeviceSchema,
|
||||||
|
} from "../../shared/validation/global-device.schemas.js";
|
||||||
|
|
||||||
|
const globalDeviceRepository = new GlobalDeviceRepository();
|
||||||
|
|
||||||
|
export async function listGlobalDevices(_req: Request, res: Response) {
|
||||||
|
const rows = await globalDeviceRepository.list();
|
||||||
|
return res.json(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createGlobalDevice(req: Request, res: Response) {
|
||||||
|
const parsed = createGlobalDeviceSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await globalDeviceRepository.create(parsed.data);
|
||||||
|
return res.status(201).json(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateGlobalDevice(req: Request, res: Response) {
|
||||||
|
const { globalDeviceId } = req.params;
|
||||||
|
if (typeof globalDeviceId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid globalDeviceId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = updateGlobalDeviceSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
|
}
|
||||||
|
|
||||||
|
await globalDeviceRepository.update(globalDeviceId, parsed.data);
|
||||||
|
const row = await globalDeviceRepository.findById(globalDeviceId);
|
||||||
|
if (!row) {
|
||||||
|
return res.status(404).json({ error: "Global device not found" });
|
||||||
|
}
|
||||||
|
return res.json(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteGlobalDevice(req: Request, res: Response) {
|
||||||
|
const { globalDeviceId } = req.params;
|
||||||
|
if (typeof globalDeviceId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid globalDeviceId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await globalDeviceRepository.delete(globalDeviceId);
|
||||||
|
return res.status(204).send();
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
||||||
|
import {
|
||||||
|
createProjectDeviceSchema,
|
||||||
|
updateProjectDeviceSchema,
|
||||||
|
} from "../../shared/validation/project-device.schemas.js";
|
||||||
|
|
||||||
|
const projectDeviceRepository = new ProjectDeviceRepository();
|
||||||
|
|
||||||
|
export async function listProjectDevicesByProject(req: Request, res: Response) {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
if (typeof projectId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid projectId" });
|
||||||
|
}
|
||||||
|
const rows = await projectDeviceRepository.listByProject(projectId);
|
||||||
|
return res.json(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createProjectDevice(req: Request, res: Response) {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
if (typeof projectId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid projectId" });
|
||||||
|
}
|
||||||
|
const parsed = createProjectDeviceSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
|
}
|
||||||
|
const created = await projectDeviceRepository.create(projectId, parsed.data);
|
||||||
|
return res.status(201).json(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateProjectDevice(req: Request, res: Response) {
|
||||||
|
const { projectId, projectDeviceId } = req.params;
|
||||||
|
if (typeof projectId !== "string" || typeof projectDeviceId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid parameters" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = updateProjectDeviceSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectDeviceRepository.update(projectId, projectDeviceId, parsed.data);
|
||||||
|
const row = await projectDeviceRepository.findById(projectId, projectDeviceId);
|
||||||
|
if (!row) {
|
||||||
|
return res.status(404).json({ error: "Project device not found" });
|
||||||
|
}
|
||||||
|
return res.json(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteProjectDevice(req: Request, res: Response) {
|
||||||
|
const { projectId, projectDeviceId } = req.params;
|
||||||
|
if (typeof projectId !== "string" || typeof projectDeviceId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid parameters" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectDeviceRepository.delete(projectId, projectDeviceId);
|
||||||
|
return res.status(204).send();
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
||||||
import { createProjectSchema } from "../../shared/validation/consumer.schemas.js";
|
import {
|
||||||
|
createProjectSchema,
|
||||||
|
updateProjectSettingsSchema,
|
||||||
|
} from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
const projectRepository = new ProjectRepository();
|
const projectRepository = new ProjectRepository();
|
||||||
|
|
||||||
@@ -14,7 +17,37 @@ export async function createProject(req: Request, res: Response) {
|
|||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
}
|
}
|
||||||
const project = await projectRepository.create(parsed.data.name);
|
const project = await projectRepository.create(parsed.data);
|
||||||
return res.status(201).json(project);
|
return res.status(201).json(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getProject(req: Request, res: Response) {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
if (typeof projectId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid projectId" });
|
||||||
|
}
|
||||||
|
const row = await projectRepository.findById(projectId);
|
||||||
|
if (!row) {
|
||||||
|
return res.status(404).json({ error: "Project not found" });
|
||||||
|
}
|
||||||
|
return res.json(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateProjectSettings(req: Request, res: Response) {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
if (typeof projectId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid projectId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = updateProjectSettingsSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectRepository.updateSettings(projectId, parsed.data);
|
||||||
|
const row = await projectRepository.findById(projectId);
|
||||||
|
if (!row) {
|
||||||
|
return res.status(404).json({ error: "Project not found" });
|
||||||
|
}
|
||||||
|
return res.json(row);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
||||||
|
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
||||||
|
import { createRoomSchema } from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
|
const floorRepository = new FloorRepository();
|
||||||
|
const roomRepository = new RoomRepository();
|
||||||
|
|
||||||
|
export async function listRoomsByProject(req: Request, res: Response) {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
if (typeof projectId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid projectId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await roomRepository.listByProject(projectId);
|
||||||
|
return res.json(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRoom(req: Request, res: Response) {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
if (typeof projectId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid projectId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = createRoomSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.data.floorId) {
|
||||||
|
const hasValidFloor = await floorRepository.existsInProject(projectId, parsed.data.floorId);
|
||||||
|
if (!hasValidFloor) {
|
||||||
|
return res.status(400).json({ error: "Floor does not belong to the provided project." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await roomRepository.create(projectId, parsed.data);
|
||||||
|
return res.status(201).json(room);
|
||||||
|
}
|
||||||
+4
-1
@@ -1,5 +1,7 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { consumerRouter } from "./routes/consumer.routes.js";
|
import { consumerRouter } from "./routes/consumer.routes.js";
|
||||||
|
import { globalDeviceRouter } from "./routes/global-device.routes.js";
|
||||||
|
import { projectDeviceRouter } from "./routes/project-device.routes.js";
|
||||||
import { projectRouter } from "./routes/project.routes.js";
|
import { projectRouter } from "./routes/project.routes.js";
|
||||||
import { errorMiddleware } from "./middleware/error.middleware.js";
|
import { errorMiddleware } from "./middleware/error.middleware.js";
|
||||||
|
|
||||||
@@ -14,10 +16,11 @@ app.get("/health", (_req, res) => {
|
|||||||
|
|
||||||
app.use("/api/projects", projectRouter);
|
app.use("/api/projects", projectRouter);
|
||||||
app.use("/api/consumers", consumerRouter);
|
app.use("/api/consumers", consumerRouter);
|
||||||
|
app.use("/api/global-devices", globalDeviceRouter);
|
||||||
|
app.use("/api/project-devices", projectDeviceRouter);
|
||||||
|
|
||||||
app.use(errorMiddleware);
|
app.use(errorMiddleware);
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Server running on http://localhost:${port}`);
|
console.log(`Server running on http://localhost:${port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import {
|
||||||
|
createGlobalDevice,
|
||||||
|
deleteGlobalDevice,
|
||||||
|
listGlobalDevices,
|
||||||
|
updateGlobalDevice,
|
||||||
|
} from "../controllers/global-device.controller.js";
|
||||||
|
|
||||||
|
export const globalDeviceRouter = Router();
|
||||||
|
|
||||||
|
globalDeviceRouter.get("/", listGlobalDevices);
|
||||||
|
globalDeviceRouter.post("/", createGlobalDevice);
|
||||||
|
globalDeviceRouter.put("/:globalDeviceId", updateGlobalDevice);
|
||||||
|
globalDeviceRouter.delete("/:globalDeviceId", deleteGlobalDevice);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import {
|
||||||
|
createProjectDevice,
|
||||||
|
deleteProjectDevice,
|
||||||
|
listProjectDevicesByProject,
|
||||||
|
updateProjectDevice,
|
||||||
|
} from "../controllers/project-device.controller.js";
|
||||||
|
|
||||||
|
export const projectDeviceRouter = Router();
|
||||||
|
|
||||||
|
projectDeviceRouter.get("/projects/:projectId", listProjectDevicesByProject);
|
||||||
|
projectDeviceRouter.post("/projects/:projectId", createProjectDevice);
|
||||||
|
projectDeviceRouter.put("/projects/:projectId/:projectDeviceId", updateProjectDevice);
|
||||||
|
projectDeviceRouter.delete("/projects/:projectId/:projectDeviceId", deleteProjectDevice);
|
||||||
@@ -1,13 +1,28 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { createProject, listProjects } from "../controllers/project.controller.js";
|
import {
|
||||||
|
createProject,
|
||||||
|
getProject,
|
||||||
|
listProjects,
|
||||||
|
updateProjectSettings,
|
||||||
|
} from "../controllers/project.controller.js";
|
||||||
import {
|
import {
|
||||||
createDistributionBoard,
|
createDistributionBoard,
|
||||||
listDistributionBoardsByProject,
|
listDistributionBoardsByProject,
|
||||||
} from "../controllers/distribution-board.controller.js";
|
} from "../controllers/distribution-board.controller.js";
|
||||||
|
import { listCircuitListsByProject } from "../controllers/circuit-list.controller.js";
|
||||||
|
import { createFloor, listFloorsByProject } from "../controllers/floor.controller.js";
|
||||||
|
import { createRoom, listRoomsByProject } from "../controllers/room.controller.js";
|
||||||
|
|
||||||
export const projectRouter = Router();
|
export const projectRouter = Router();
|
||||||
|
|
||||||
projectRouter.get("/", listProjects);
|
projectRouter.get("/", listProjects);
|
||||||
projectRouter.post("/", createProject);
|
projectRouter.post("/", createProject);
|
||||||
|
projectRouter.get("/:projectId", getProject);
|
||||||
|
projectRouter.put("/:projectId", updateProjectSettings);
|
||||||
projectRouter.get("/:projectId/distribution-boards", listDistributionBoardsByProject);
|
projectRouter.get("/:projectId/distribution-boards", listDistributionBoardsByProject);
|
||||||
projectRouter.post("/:projectId/distribution-boards", createDistributionBoard);
|
projectRouter.post("/:projectId/distribution-boards", createDistributionBoard);
|
||||||
|
projectRouter.get("/:projectId/circuit-lists", listCircuitListsByProject);
|
||||||
|
projectRouter.get("/:projectId/floors", listFloorsByProject);
|
||||||
|
projectRouter.post("/:projectId/floors", createFloor);
|
||||||
|
projectRouter.get("/:projectId/rooms", listRoomsByProject);
|
||||||
|
projectRouter.post("/:projectId/rooms", createRoom);
|
||||||
|
|||||||
@@ -13,8 +13,22 @@ export interface ConsumerDto {
|
|||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
distributionBoardId: string | null;
|
distributionBoardId: string | null;
|
||||||
|
circuitListId: string | null;
|
||||||
|
roomId: string | null;
|
||||||
|
circuitNumber: string | null;
|
||||||
|
description: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
category: string | null;
|
category: string | null;
|
||||||
|
deviceType: string | null;
|
||||||
|
phaseType: string | null;
|
||||||
|
tradeOrCostGroup: string | null;
|
||||||
|
group: string | null;
|
||||||
|
protectionType: string | null;
|
||||||
|
protectionRatedCurrent: number | null;
|
||||||
|
protectionCharacteristic: string | null;
|
||||||
|
cableType: string | null;
|
||||||
|
cableCrossSection: string | null;
|
||||||
|
comment: string | null;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
installedPowerPerUnitKw: number;
|
installedPowerPerUnitKw: number;
|
||||||
demandFactor: number;
|
demandFactor: number;
|
||||||
@@ -23,4 +37,3 @@ export interface ConsumerDto {
|
|||||||
powerFactor: number | null;
|
powerFactor: number | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,22 @@ import { z } from "zod";
|
|||||||
export const createConsumerSchema = z.object({
|
export const createConsumerSchema = z.object({
|
||||||
projectId: z.string().min(1),
|
projectId: z.string().min(1),
|
||||||
distributionBoardId: z.string().min(1).optional(),
|
distributionBoardId: z.string().min(1).optional(),
|
||||||
|
circuitListId: z.string().min(1).optional(),
|
||||||
|
roomId: z.string().min(1).optional(),
|
||||||
|
circuitNumber: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
category: z.string().optional(),
|
category: z.string().optional(),
|
||||||
|
deviceType: z.string().optional(),
|
||||||
|
phaseType: z.string().optional(),
|
||||||
|
tradeOrCostGroup: z.string().optional(),
|
||||||
|
group: z.string().optional(),
|
||||||
|
protectionType: z.string().optional(),
|
||||||
|
protectionRatedCurrent: z.number().min(0).optional(),
|
||||||
|
protectionCharacteristic: z.string().optional(),
|
||||||
|
cableType: z.string().optional(),
|
||||||
|
cableCrossSection: z.string().optional(),
|
||||||
|
comment: z.string().optional(),
|
||||||
quantity: z.number().min(0),
|
quantity: z.number().min(0),
|
||||||
installedPowerPerUnitKw: z.number().min(0),
|
installedPowerPerUnitKw: z.number().min(0),
|
||||||
demandFactor: z.number().min(0).max(1),
|
demandFactor: z.number().min(0).max(1),
|
||||||
@@ -18,13 +32,33 @@ export const updateConsumerSchema = createConsumerSchema;
|
|||||||
|
|
||||||
export const createProjectSchema = z.object({
|
export const createProjectSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
|
singlePhaseVoltageV: z.number().positive().optional(),
|
||||||
|
threePhaseVoltageV: z.number().positive().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateProjectSettingsSchema = z.object({
|
||||||
|
singlePhaseVoltageV: z.number().positive(),
|
||||||
|
threePhaseVoltageV: z.number().positive(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createDistributionBoardSchema = z.object({
|
export const createDistributionBoardSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const createFloorSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createRoomSchema = z.object({
|
||||||
|
floorId: z.string().min(1).optional(),
|
||||||
|
roomNumber: z.string().min(1),
|
||||||
|
roomName: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
export type CreateConsumerInput = z.infer<typeof createConsumerSchema>;
|
export type CreateConsumerInput = z.infer<typeof createConsumerSchema>;
|
||||||
export type CreateProjectInput = z.infer<typeof createProjectSchema>;
|
export type CreateProjectInput = z.infer<typeof createProjectSchema>;
|
||||||
|
export type UpdateProjectSettingsInput = z.infer<typeof updateProjectSettingsSchema>;
|
||||||
export type CreateDistributionBoardInput = z.infer<typeof createDistributionBoardSchema>;
|
export type CreateDistributionBoardInput = z.infer<typeof createDistributionBoardSchema>;
|
||||||
export type UpdateConsumerInput = z.infer<typeof updateConsumerSchema>;
|
export type UpdateConsumerInput = z.infer<typeof updateConsumerSchema>;
|
||||||
|
export type CreateFloorInput = z.infer<typeof createFloorSchema>;
|
||||||
|
export type CreateRoomInput = z.infer<typeof createRoomSchema>;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const createGlobalDeviceSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
category: z.string().optional(),
|
||||||
|
quantity: z.number().min(0),
|
||||||
|
installedPowerPerUnitKw: z.number().min(0),
|
||||||
|
demandFactor: z.number().min(0).max(1),
|
||||||
|
voltageV: z.number().positive().optional(),
|
||||||
|
phaseCount: z.union([z.literal(1), z.literal(3)]).optional(),
|
||||||
|
powerFactor: z.number().min(0).max(1).optional(),
|
||||||
|
note: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateGlobalDeviceSchema = createGlobalDeviceSchema;
|
||||||
|
|
||||||
|
export type CreateGlobalDeviceInput = z.infer<typeof createGlobalDeviceSchema>;
|
||||||
|
export type UpdateGlobalDeviceInput = z.infer<typeof updateGlobalDeviceSchema>;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const createProjectDeviceSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
category: z.string().optional(),
|
||||||
|
quantity: z.number().min(0),
|
||||||
|
installedPowerPerUnitKw: z.number().min(0),
|
||||||
|
demandFactor: z.number().min(0).max(1),
|
||||||
|
voltageV: z.number().positive().optional(),
|
||||||
|
phaseCount: z.union([z.literal(1), z.literal(3)]).optional(),
|
||||||
|
powerFactor: z.number().min(0).max(1).optional(),
|
||||||
|
note: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateProjectDeviceSchema = createProjectDeviceSchema;
|
||||||
|
|
||||||
|
export type CreateProjectDeviceInput = z.infer<typeof createProjectDeviceSchema>;
|
||||||
|
export type UpdateProjectDeviceInput = z.infer<typeof updateProjectDeviceSchema>;
|
||||||
Reference in New Issue
Block a user