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.
|
||||
|
||||
## 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
|
||||
|
||||
Write clear, explicit TypeScript.
|
||||
@@ -397,3 +411,55 @@ The first useful milestone should be:
|
||||
- Provide basic tests for calculation logic
|
||||
|
||||
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",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.9.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"express": "^5.2.1",
|
||||
"lucide-react": "^1.14.0",
|
||||
@@ -1437,6 +1438,16 @@
|
||||
"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": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
@@ -1656,6 +1667,24 @@
|
||||
"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": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.9.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"express": "^5.2.1",
|
||||
"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 {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: #f5f7fb;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font: inherit;
|
||||
.card {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
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;
|
||||
}
|
||||
.table td input.form-control-sm,
|
||||
.table td select.form-select-sm {
|
||||
min-width: 8rem;
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Leistungsbilanz",
|
||||
description: "Leistungsbilanz fuer elektrische Verbraucher",
|
||||
description: "Leistungsbilanz für elektrische Verbraucher und Stromkreislisten",
|
||||
};
|
||||
|
||||
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() {
|
||||
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,
|
||||
"tag": "0000_bizarre_colossus",
|
||||
"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,
|
||||
projectId: input.projectId,
|
||||
distributionBoardId: input.distributionBoardId ?? null,
|
||||
circuitListId: input.circuitListId ?? null,
|
||||
roomId: input.roomId ?? null,
|
||||
circuitNumber: input.circuitNumber ?? null,
|
||||
description: input.description ?? null,
|
||||
name: input.name,
|
||||
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,
|
||||
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||
demandFactor: input.demandFactor,
|
||||
@@ -37,8 +51,22 @@ export class ConsumerRepository {
|
||||
.set({
|
||||
projectId: input.projectId,
|
||||
distributionBoardId: input.distributionBoardId ?? null,
|
||||
circuitListId: input.circuitListId ?? null,
|
||||
roomId: input.roomId ?? null,
|
||||
circuitNumber: input.circuitNumber ?? null,
|
||||
description: input.description ?? null,
|
||||
name: input.name,
|
||||
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,
|
||||
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||
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 { db } from "../client.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import type {
|
||||
CreateProjectInput,
|
||||
UpdateProjectSettingsInput,
|
||||
} from "../../shared/validation/consumer.schemas.js";
|
||||
|
||||
export class ProjectRepository {
|
||||
async list() {
|
||||
return db.select().from(projects);
|
||||
}
|
||||
|
||||
async create(name: string) {
|
||||
async create(input: CreateProjectInput) {
|
||||
const id = crypto.randomUUID();
|
||||
await db.insert(projects).values({ id, name });
|
||||
return { id, name };
|
||||
const project = {
|
||||
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) {
|
||||
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 { circuitLists } from "./circuit-lists.js";
|
||||
import { distributionBoards } from "./distribution-boards.js";
|
||||
import { projects } from "./projects.js";
|
||||
import { rooms } from "./rooms.js";
|
||||
|
||||
export const consumers = sqliteTable("consumers", {
|
||||
id: text("id").primaryKey(),
|
||||
@@ -10,8 +12,26 @@ export const consumers = sqliteTable("consumers", {
|
||||
distributionBoardId: text("distribution_board_id").references(() => distributionBoards.id, {
|
||||
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(),
|
||||
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(),
|
||||
installedPowerPerUnitKw: real("installed_power_per_unit_kw").notNull(),
|
||||
demandFactor: real("demand_factor").notNull(),
|
||||
@@ -20,4 +40,3 @@ export const consumers = sqliteTable("consumers", {
|
||||
powerFactor: real("power_factor"),
|
||||
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", {
|
||||
id: text("id").primaryKey(),
|
||||
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;
|
||||
projectId: string;
|
||||
distributionBoardId?: string;
|
||||
circuitListId?: string;
|
||||
roomId?: string;
|
||||
roomNumber?: string;
|
||||
roomName?: string;
|
||||
floorId?: string;
|
||||
floorName?: string;
|
||||
circuitNumber?: string;
|
||||
description?: string;
|
||||
name: 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;
|
||||
installedPowerPerUnitKw: number;
|
||||
demandFactor: number;
|
||||
@@ -12,4 +30,3 @@ export interface Consumer {
|
||||
powerFactor?: number;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,20 @@ import type { Consumer } from "../models/consumer.model.js";
|
||||
export interface ConsumerWithCalculatedValues extends Consumer {
|
||||
installedPowerKw: number;
|
||||
demandPowerKw: number;
|
||||
effectiveVoltageV?: number;
|
||||
currentA?: number;
|
||||
}
|
||||
|
||||
export interface ProjectVoltageDefaults {
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
}
|
||||
|
||||
export class PowerBalanceService {
|
||||
enrichConsumer(consumer: Consumer): ConsumerWithCalculatedValues {
|
||||
enrichConsumer(
|
||||
consumer: Consumer,
|
||||
projectVoltageDefaults?: ProjectVoltageDefaults
|
||||
): ConsumerWithCalculatedValues {
|
||||
const installedPowerKw = calculateInstalledPowerKw({
|
||||
quantity: consumer.quantity,
|
||||
installedPowerPerUnitKw: consumer.installedPowerPerUnitKw,
|
||||
@@ -24,11 +33,19 @@ export class PowerBalanceService {
|
||||
demandFactor: consumer.demandFactor,
|
||||
});
|
||||
|
||||
const effectiveVoltageV =
|
||||
consumer.voltageV ??
|
||||
(consumer.phaseCount === 1
|
||||
? projectVoltageDefaults?.singlePhaseVoltageV
|
||||
: consumer.phaseCount === 3
|
||||
? projectVoltageDefaults?.threePhaseVoltageV
|
||||
: undefined);
|
||||
|
||||
let currentA: number | undefined;
|
||||
if (consumer.voltageV && consumer.phaseCount && consumer.powerFactor) {
|
||||
if (effectiveVoltageV && consumer.phaseCount && consumer.powerFactor) {
|
||||
currentA = calculateCurrentA({
|
||||
demandPowerKw,
|
||||
voltageV: consumer.voltageV,
|
||||
voltageV: effectiveVoltageV,
|
||||
phaseCount: consumer.phaseCount,
|
||||
powerFactor: consumer.powerFactor,
|
||||
});
|
||||
@@ -38,8 +55,8 @@ export class PowerBalanceService {
|
||||
...consumer,
|
||||
installedPowerKw,
|
||||
demandPowerKw,
|
||||
effectiveVoltageV,
|
||||
currentA,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
export interface ProjectDto {
|
||||
id: string;
|
||||
name: string;
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
}
|
||||
|
||||
export interface ConsumerWithCalculatedValues {
|
||||
id: string;
|
||||
projectId: string;
|
||||
distributionBoardId?: string | null;
|
||||
circuitListId?: string | null;
|
||||
roomId?: string | null;
|
||||
roomNumber?: string;
|
||||
roomName?: string;
|
||||
floorId?: string;
|
||||
floorName?: string;
|
||||
circuitNumber?: string;
|
||||
description?: string;
|
||||
name: 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;
|
||||
installedPowerPerUnitKw: number;
|
||||
demandFactor: number;
|
||||
@@ -18,6 +38,7 @@ export interface ConsumerWithCalculatedValues {
|
||||
note?: string;
|
||||
installedPowerKw: number;
|
||||
demandPowerKw: number;
|
||||
effectiveVoltageV?: number;
|
||||
currentA?: number;
|
||||
}
|
||||
|
||||
@@ -27,11 +48,74 @@ export interface DistributionBoardDto {
|
||||
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 {
|
||||
projectId: string;
|
||||
distributionBoardId?: string;
|
||||
circuitListId?: string;
|
||||
roomId?: string;
|
||||
circuitNumber?: string;
|
||||
description?: string;
|
||||
name: 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;
|
||||
installedPowerPerUnitKw: number;
|
||||
demandFactor: number;
|
||||
@@ -42,3 +126,37 @@ export interface 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 {
|
||||
CircuitListDto,
|
||||
CreateFloorInput,
|
||||
CreateProjectDeviceInput,
|
||||
CreateRoomInput,
|
||||
ConsumerWithCalculatedValues,
|
||||
CreateConsumerInput,
|
||||
CreateGlobalDeviceInput,
|
||||
DistributionBoardDto,
|
||||
FloorDto,
|
||||
GlobalDeviceDto,
|
||||
ProjectDeviceDto,
|
||||
ProjectDto,
|
||||
RoomDto,
|
||||
UpdateConsumerInput,
|
||||
} from "../types";
|
||||
|
||||
@@ -13,6 +22,7 @@ async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
"Content-Type": "application/json",
|
||||
...init?.headers,
|
||||
},
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
@@ -27,6 +41,10 @@ export function listProjects() {
|
||||
return request<ProjectDto[]>("/api/projects");
|
||||
}
|
||||
|
||||
export function getProject(projectId: string) {
|
||||
return request<ProjectDto>(`/api/projects/${projectId}`);
|
||||
}
|
||||
|
||||
export function createProject(name: string) {
|
||||
return request<ProjectDto>("/api/projects", {
|
||||
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) {
|
||||
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) {
|
||||
return request<ConsumerWithCalculatedValues[]>(`/api/consumers/projects/${projectId}`);
|
||||
}
|
||||
@@ -63,10 +117,56 @@ export function updateConsumer(consumerId: string, input: UpdateConsumerInput) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteConsumer(consumerId: string) {
|
||||
const response = await fetch(`/api/consumers/${consumerId}`, { method: "DELETE" });
|
||||
if (!response.ok) {
|
||||
const details = await response.text();
|
||||
throw new Error(details || `Request failed with ${response.status}`);
|
||||
}
|
||||
export function deleteConsumer(consumerId: string) {
|
||||
return request<void>(`/api/consumers/${consumerId}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
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 { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
||||
import { ConsumerRepository } from "../../db/repositories/consumer.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 {
|
||||
createConsumerSchema,
|
||||
updateConsumerSchema,
|
||||
} from "../../shared/validation/consumer.schemas.js";
|
||||
|
||||
const circuitListRepository = new CircuitListRepository();
|
||||
const consumerRepository = new ConsumerRepository();
|
||||
const distributionBoardRepository = new DistributionBoardRepository();
|
||||
const floorRepository = new FloorRepository();
|
||||
const projectRepository = new ProjectRepository();
|
||||
const roomRepository = new RoomRepository();
|
||||
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(
|
||||
projectId: string,
|
||||
distributionBoardId: string | undefined
|
||||
@@ -21,19 +59,91 @@ async function validateDistributionBoardOwnership(
|
||||
return distributionBoardRepository.existsInProject(projectId, distributionBoardId);
|
||||
}
|
||||
|
||||
export async function listConsumersByProject(req: Request, res: Response) {
|
||||
const { projectId } = req.params;
|
||||
if (typeof projectId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid projectId" });
|
||||
async function validateRoomOwnership(projectId: string, roomId: string | undefined) {
|
||||
if (!roomId) {
|
||||
return true;
|
||||
}
|
||||
const rows = await consumerRepository.listByProject(projectId);
|
||||
const enriched = rows.map((row) =>
|
||||
powerBalanceService.enrichConsumer({
|
||||
return roomRepository.existsInProject(projectId, roomId);
|
||||
}
|
||||
|
||||
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,
|
||||
projectId: row.projectId,
|
||||
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,
|
||||
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,
|
||||
installedPowerPerUnitKw: row.installedPowerPerUnitKw,
|
||||
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,
|
||||
powerFactor: row.powerFactor ?? 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) {
|
||||
@@ -52,18 +195,66 @@ export async function createConsumer(req: Request, res: Response) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
const hasValidDistributionBoard = await validateDistributionBoardOwnership(
|
||||
parsed.data.projectId,
|
||||
parsed.data.distributionBoardId
|
||||
);
|
||||
const [hasValidDistributionBoard, hasValidRoom] = await Promise.all([
|
||||
validateDistributionBoardOwnership(parsed.data.projectId, parsed.data.distributionBoardId),
|
||||
validateRoomOwnership(parsed.data.projectId, parsed.data.roomId),
|
||||
]);
|
||||
if (!hasValidDistributionBoard) {
|
||||
return res
|
||||
.status(400)
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -78,36 +269,60 @@ export async function updateConsumer(req: Request, res: Response) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
const hasValidDistributionBoard = await validateDistributionBoardOwnership(
|
||||
parsed.data.projectId,
|
||||
parsed.data.distributionBoardId
|
||||
);
|
||||
const [hasValidDistributionBoard, hasValidRoom] = await Promise.all([
|
||||
validateDistributionBoardOwnership(parsed.data.projectId, parsed.data.distributionBoardId),
|
||||
validateRoomOwnership(parsed.data.projectId, parsed.data.roomId),
|
||||
]);
|
||||
if (!hasValidDistributionBoard) {
|
||||
return res
|
||||
.status(400)
|
||||
.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);
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: "Consumer not found" });
|
||||
}
|
||||
|
||||
const enriched = powerBalanceService.enrichConsumer({
|
||||
id: row.id,
|
||||
projectId: row.projectId,
|
||||
distributionBoardId: row.distributionBoardId ?? undefined,
|
||||
name: row.name,
|
||||
category: row.category ?? undefined,
|
||||
quantity: row.quantity,
|
||||
installedPowerPerUnitKw: row.installedPowerPerUnitKw,
|
||||
demandFactor: row.demandFactor,
|
||||
voltageV: row.voltageV ?? undefined,
|
||||
phaseCount: row.phaseCount === 1 || row.phaseCount === 3 ? row.phaseCount : undefined,
|
||||
powerFactor: row.powerFactor ?? undefined,
|
||||
note: row.note ?? undefined,
|
||||
});
|
||||
const [project, floors, rooms] = await Promise.all([
|
||||
projectRepository.findById(row.projectId),
|
||||
floorRepository.listByProject(row.projectId),
|
||||
roomRepository.listByProject(row.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(
|
||||
buildConsumerFromRow(row as ConsumerRow, roomById, floorById),
|
||||
project
|
||||
? {
|
||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
|
||||
return res.json(enriched);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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 { createDistributionBoardSchema } from "../../shared/validation/consumer.schemas.js";
|
||||
|
||||
const circuitListRepository = new CircuitListRepository();
|
||||
const distributionBoardRepository = new DistributionBoardRepository();
|
||||
|
||||
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);
|
||||
await circuitListRepository.createForDistributionBoard({
|
||||
projectId,
|
||||
distributionBoardId: board.id,
|
||||
name: `${board.name} Stromkreisliste`,
|
||||
});
|
||||
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 { 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();
|
||||
|
||||
@@ -14,7 +17,37 @@ export async function createProject(req: Request, res: Response) {
|
||||
if (!parsed.success) {
|
||||
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);
|
||||
}
|
||||
|
||||
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 { 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 { errorMiddleware } from "./middleware/error.middleware.js";
|
||||
|
||||
@@ -14,10 +16,11 @@ app.get("/health", (_req, res) => {
|
||||
|
||||
app.use("/api/projects", projectRouter);
|
||||
app.use("/api/consumers", consumerRouter);
|
||||
app.use("/api/global-devices", globalDeviceRouter);
|
||||
app.use("/api/project-devices", projectDeviceRouter);
|
||||
|
||||
app.use(errorMiddleware);
|
||||
|
||||
app.listen(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 { createProject, listProjects } from "../controllers/project.controller.js";
|
||||
import {
|
||||
createProject,
|
||||
getProject,
|
||||
listProjects,
|
||||
updateProjectSettings,
|
||||
} from "../controllers/project.controller.js";
|
||||
import {
|
||||
createDistributionBoard,
|
||||
listDistributionBoardsByProject,
|
||||
} 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();
|
||||
|
||||
projectRouter.get("/", listProjects);
|
||||
projectRouter.post("/", createProject);
|
||||
projectRouter.get("/:projectId", getProject);
|
||||
projectRouter.put("/:projectId", updateProjectSettings);
|
||||
projectRouter.get("/:projectId/distribution-boards", listDistributionBoardsByProject);
|
||||
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;
|
||||
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;
|
||||
@@ -23,4 +37,3 @@ export interface ConsumerDto {
|
||||
powerFactor: number | null;
|
||||
note: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,22 @@ import { z } from "zod";
|
||||
export const createConsumerSchema = z.object({
|
||||
projectId: z.string().min(1),
|
||||
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),
|
||||
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),
|
||||
installedPowerPerUnitKw: z.number().min(0),
|
||||
demandFactor: z.number().min(0).max(1),
|
||||
@@ -18,13 +32,33 @@ export const updateConsumerSchema = createConsumerSchema;
|
||||
|
||||
export const createProjectSchema = z.object({
|
||||
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({
|
||||
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 CreateProjectInput = z.infer<typeof createProjectSchema>;
|
||||
export type UpdateProjectSettingsInput = z.infer<typeof updateProjectSettingsSchema>;
|
||||
export type CreateDistributionBoardInput = z.infer<typeof createDistributionBoardSchema>;
|
||||
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