Added power sum per distributionboard
This commit is contained in:
@@ -91,6 +91,21 @@ h2 {
|
|||||||
border-radius: 6px;
|
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 {
|
.primaryButton {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
@@ -197,6 +212,21 @@ select {
|
|||||||
margin-bottom: 12px;
|
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 {
|
.subline {
|
||||||
margin: 6px 0 0;
|
margin: 6px 0 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -247,6 +277,18 @@ tbody tr:hover {
|
|||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rowField {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 64px 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rowActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.emptyState {
|
.emptyState {
|
||||||
height: 92px;
|
height: 92px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import crypto from "node:crypto";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "../client.js";
|
import { db } from "../client.js";
|
||||||
import { consumers } from "../schema/consumers.js";
|
import { consumers } from "../schema/consumers.js";
|
||||||
import type { CreateConsumerInput } from "../../shared/validation/consumer.schemas.js";
|
import type {
|
||||||
|
CreateConsumerInput,
|
||||||
|
UpdateConsumerInput,
|
||||||
|
} from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
export class ConsumerRepository {
|
export class ConsumerRepository {
|
||||||
async listByProject(projectId: string) {
|
async listByProject(projectId: string) {
|
||||||
@@ -27,5 +30,32 @@ export class ConsumerRepository {
|
|||||||
});
|
});
|
||||||
return { id, ...input };
|
return { id, ...input };
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
async update(consumerId: string, input: UpdateConsumerInput) {
|
||||||
|
await db
|
||||||
|
.update(consumers)
|
||||||
|
.set({
|
||||||
|
projectId: input.projectId,
|
||||||
|
distributionBoardId: input.distributionBoardId ?? null,
|
||||||
|
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(consumers.id, consumerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(consumerId: string) {
|
||||||
|
const [row] = await db.select().from(consumers).where(eq(consumers.id, consumerId)).limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(consumerId: string) {
|
||||||
|
await db.delete(consumers).where(eq(consumers.id, consumerId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { db } from "../client.js";
|
import { db } from "../client.js";
|
||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
|
|
||||||
@@ -14,4 +14,18 @@ export class DistributionBoardRepository {
|
|||||||
await db.insert(distributionBoards).values(board);
|
await db.insert(distributionBoards).values(board);
|
||||||
return board;
|
return board;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async existsInProject(projectId: string, distributionBoardId: string) {
|
||||||
|
const [row] = await db
|
||||||
|
.select({ id: distributionBoards.id })
|
||||||
|
.from(distributionBoards)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(distributionBoards.projectId, projectId),
|
||||||
|
eq(distributionBoards.id, distributionBoardId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return Boolean(row);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Activity, FolderPlus, Plus, RefreshCw, Zap } from "lucide-react";
|
import { Activity, FolderPlus, Pencil, Plus, RefreshCw, Save, Trash2, X, Zap } from "lucide-react";
|
||||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
createConsumer,
|
createConsumer,
|
||||||
createDistributionBoard,
|
createDistributionBoard,
|
||||||
createProject,
|
createProject,
|
||||||
|
deleteConsumer,
|
||||||
listConsumers,
|
listConsumers,
|
||||||
listDistributionBoards,
|
listDistributionBoards,
|
||||||
listProjects,
|
listProjects,
|
||||||
|
updateConsumer,
|
||||||
} from "../utils/api";
|
} from "../utils/api";
|
||||||
import type {
|
import type {
|
||||||
ConsumerWithCalculatedValues,
|
ConsumerWithCalculatedValues,
|
||||||
CreateConsumerInput,
|
CreateConsumerInput,
|
||||||
DistributionBoardDto,
|
DistributionBoardDto,
|
||||||
ProjectDto,
|
ProjectDto,
|
||||||
|
UpdateConsumerInput,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
const initialConsumerForm = {
|
const initialConsumerForm = {
|
||||||
@@ -29,6 +32,19 @@ const initialConsumerForm = {
|
|||||||
note: "",
|
note: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface EditConsumerForm {
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
distributionBoardId: string;
|
||||||
|
quantity: string;
|
||||||
|
installedPowerPerUnitKw: string;
|
||||||
|
demandFactor: string;
|
||||||
|
voltageV: string;
|
||||||
|
phaseCount: "1" | "3";
|
||||||
|
powerFactor: string;
|
||||||
|
note: string;
|
||||||
|
}
|
||||||
|
|
||||||
type ConsumerForm = typeof initialConsumerForm;
|
type ConsumerForm = typeof initialConsumerForm;
|
||||||
|
|
||||||
function toOptionalNumber(value: string) {
|
function toOptionalNumber(value: string) {
|
||||||
@@ -48,6 +64,21 @@ function formatNumber(value: number | undefined, digits = 2) {
|
|||||||
}).format(value);
|
}).format(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createEditForm(consumer: ConsumerWithCalculatedValues): EditConsumerForm {
|
||||||
|
return {
|
||||||
|
name: consumer.name,
|
||||||
|
category: consumer.category ?? "",
|
||||||
|
distributionBoardId: consumer.distributionBoardId ?? "",
|
||||||
|
quantity: String(consumer.quantity),
|
||||||
|
installedPowerPerUnitKw: String(consumer.installedPowerPerUnitKw),
|
||||||
|
demandFactor: String(consumer.demandFactor),
|
||||||
|
voltageV: consumer.voltageV !== undefined ? String(consumer.voltageV) : "",
|
||||||
|
phaseCount: consumer.phaseCount === 3 ? "3" : "1",
|
||||||
|
powerFactor: consumer.powerFactor !== undefined ? String(consumer.powerFactor) : "",
|
||||||
|
note: consumer.note ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function PowerBalanceWorkspace() {
|
export function PowerBalanceWorkspace() {
|
||||||
const [projects, setProjects] = useState<ProjectDto[]>([]);
|
const [projects, setProjects] = useState<ProjectDto[]>([]);
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState("");
|
const [selectedProjectId, setSelectedProjectId] = useState("");
|
||||||
@@ -57,6 +88,8 @@ export function PowerBalanceWorkspace() {
|
|||||||
const [projectName, setProjectName] = useState("");
|
const [projectName, setProjectName] = useState("");
|
||||||
const [boardName, setBoardName] = useState("");
|
const [boardName, setBoardName] = useState("");
|
||||||
const [consumerForm, setConsumerForm] = useState<ConsumerForm>(initialConsumerForm);
|
const [consumerForm, setConsumerForm] = useState<ConsumerForm>(initialConsumerForm);
|
||||||
|
const [editingConsumerId, setEditingConsumerId] = useState<string | null>(null);
|
||||||
|
const [editConsumerForm, setEditConsumerForm] = useState<EditConsumerForm | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -79,6 +112,49 @@ export function PowerBalanceWorkspace() {
|
|||||||
),
|
),
|
||||||
[visibleConsumers]
|
[visibleConsumers]
|
||||||
);
|
);
|
||||||
|
const totalsByBoard = useMemo(() => {
|
||||||
|
const bucket = new Map<string, { consumerCount: number; installedPowerKw: number; demandPowerKw: number }>();
|
||||||
|
for (const board of distributionBoards) {
|
||||||
|
bucket.set(board.id, { consumerCount: 0, installedPowerKw: 0, demandPowerKw: 0 });
|
||||||
|
}
|
||||||
|
bucket.set("__unassigned__", { consumerCount: 0, installedPowerKw: 0, demandPowerKw: 0 });
|
||||||
|
|
||||||
|
for (const consumer of consumers) {
|
||||||
|
const key = consumer.distributionBoardId ?? "__unassigned__";
|
||||||
|
const entry = bucket.get(key);
|
||||||
|
if (!entry) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entry.consumerCount += 1;
|
||||||
|
entry.installedPowerKw += consumer.installedPowerKw;
|
||||||
|
entry.demandPowerKw += consumer.demandPowerKw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...distributionBoards.map((board) => ({
|
||||||
|
key: board.id,
|
||||||
|
boardName: board.name,
|
||||||
|
...bucket.get(board.id)!,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
key: "__unassigned__",
|
||||||
|
boardName: "Ohne Verteilung",
|
||||||
|
...bucket.get("__unassigned__")!,
|
||||||
|
},
|
||||||
|
].filter((item) => item.consumerCount > 0);
|
||||||
|
}, [consumers, distributionBoards]);
|
||||||
|
const projectTotals = useMemo(
|
||||||
|
() =>
|
||||||
|
consumers.reduce(
|
||||||
|
(sum, consumer) => ({
|
||||||
|
consumerCount: sum.consumerCount + 1,
|
||||||
|
installedPowerKw: sum.installedPowerKw + consumer.installedPowerKw,
|
||||||
|
demandPowerKw: sum.demandPowerKw + consumer.demandPowerKw,
|
||||||
|
}),
|
||||||
|
{ consumerCount: 0, installedPowerKw: 0, demandPowerKw: 0 }
|
||||||
|
),
|
||||||
|
[consumers]
|
||||||
|
);
|
||||||
|
|
||||||
async function refreshProjects() {
|
async function refreshProjects() {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -116,6 +192,8 @@ export function PowerBalanceWorkspace() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setEditingConsumerId(null);
|
||||||
|
setEditConsumerForm(null);
|
||||||
refreshDistributionBoards(selectedProjectId).catch((err: unknown) =>
|
refreshDistributionBoards(selectedProjectId).catch((err: unknown) =>
|
||||||
setError(err instanceof Error ? err.message : "Verteilungen konnten nicht geladen werden.")
|
setError(err instanceof Error ? err.message : "Verteilungen konnten nicht geladen werden.")
|
||||||
);
|
);
|
||||||
@@ -199,10 +277,74 @@ export function PowerBalanceWorkspace() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSaveConsumer(consumerId: string) {
|
||||||
|
if (!selectedProjectId || !editConsumerForm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input: UpdateConsumerInput = {
|
||||||
|
projectId: selectedProjectId,
|
||||||
|
distributionBoardId: editConsumerForm.distributionBoardId || undefined,
|
||||||
|
name: editConsumerForm.name.trim(),
|
||||||
|
category: editConsumerForm.category.trim() || undefined,
|
||||||
|
quantity: Number(editConsumerForm.quantity),
|
||||||
|
installedPowerPerUnitKw: Number(editConsumerForm.installedPowerPerUnitKw),
|
||||||
|
demandFactor: Number(editConsumerForm.demandFactor),
|
||||||
|
voltageV: toOptionalNumber(editConsumerForm.voltageV),
|
||||||
|
phaseCount: editConsumerForm.phaseCount === "3" ? 3 : 1,
|
||||||
|
powerFactor: toOptionalNumber(editConsumerForm.powerFactor),
|
||||||
|
note: editConsumerForm.note.trim() || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const updated = await updateConsumer(consumerId, input);
|
||||||
|
setConsumers((current) => current.map((item) => (item.id === consumerId ? updated : item)));
|
||||||
|
setEditingConsumerId(null);
|
||||||
|
setEditConsumerForm(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Verbraucher konnte nicht gespeichert werden.");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteConsumer(consumerId: string) {
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await deleteConsumer(consumerId);
|
||||||
|
setConsumers((current) => current.filter((item) => item.id !== consumerId));
|
||||||
|
if (editingConsumerId === consumerId) {
|
||||||
|
setEditingConsumerId(null);
|
||||||
|
setEditConsumerForm(null);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Verbraucher konnte nicht geloescht werden.");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEditConsumer(consumer: ConsumerWithCalculatedValues) {
|
||||||
|
setEditingConsumerId(consumer.id);
|
||||||
|
setEditConsumerForm(createEditForm(consumer));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEditConsumer() {
|
||||||
|
setEditingConsumerId(null);
|
||||||
|
setEditConsumerForm(null);
|
||||||
|
}
|
||||||
|
|
||||||
function updateConsumerForm(field: keyof ConsumerForm, value: string) {
|
function updateConsumerForm(field: keyof ConsumerForm, value: string) {
|
||||||
setConsumerForm((current) => ({ ...current, [field]: value }));
|
setConsumerForm((current) => ({ ...current, [field]: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateEditConsumerForm(field: keyof EditConsumerForm, value: string) {
|
||||||
|
setEditConsumerForm((current) => (current ? { ...current, [field]: value } : current));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="workspace">
|
<main className="workspace">
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
@@ -282,7 +424,7 @@ export function PowerBalanceWorkspace() {
|
|||||||
<form className="consumerForm" onSubmit={handleCreateConsumer}>
|
<form className="consumerForm" onSubmit={handleCreateConsumer}>
|
||||||
<label>
|
<label>
|
||||||
Verbraucher
|
Verbraucher
|
||||||
<input value={consumerForm.name} onChange={(event) => updateConsumerForm("name", event.target.value)} placeholder="z. B. Steckdosen Büro" />
|
<input value={consumerForm.name} onChange={(event) => updateConsumerForm("name", event.target.value)} placeholder="z. B. Steckdosen Buero" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Kategorie
|
Kategorie
|
||||||
@@ -293,7 +435,7 @@ export function PowerBalanceWorkspace() {
|
|||||||
<input min="0" type="number" value={consumerForm.quantity} onChange={(event) => updateConsumerForm("quantity", event.target.value)} />
|
<input min="0" type="number" value={consumerForm.quantity} onChange={(event) => updateConsumerForm("quantity", event.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Leistung je Stück [kW]
|
Leistung je Stueck [kW]
|
||||||
<input min="0" step="0.01" type="number" value={consumerForm.installedPowerPerUnitKw} onChange={(event) => updateConsumerForm("installedPowerPerUnitKw", event.target.value)} />
|
<input min="0" step="0.01" type="number" value={consumerForm.installedPowerPerUnitKw} onChange={(event) => updateConsumerForm("installedPowerPerUnitKw", event.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
@@ -321,7 +463,7 @@ export function PowerBalanceWorkspace() {
|
|||||||
</label>
|
</label>
|
||||||
<button className="primaryButton" type="submit" disabled={!selectedProjectId || isSaving}>
|
<button className="primaryButton" type="submit" disabled={!selectedProjectId || isSaving}>
|
||||||
<Plus size={17} />
|
<Plus size={17} />
|
||||||
Hinzufügen
|
Hinzufuegen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
@@ -330,12 +472,51 @@ export function PowerBalanceWorkspace() {
|
|||||||
<div className="tableHeader">
|
<div className="tableHeader">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Aktuelles Projekt</p>
|
<p className="eyebrow">Aktuelles Projekt</p>
|
||||||
<h2>{selectedProject?.name || "Noch kein Projekt ausgewählt"}</h2>
|
<h2>{selectedProject?.name || "Noch kein Projekt ausgewaehlt"}</h2>
|
||||||
<p className="subline">{selectedBoard ? `Verteilung: ${selectedBoard.name}` : "Alle Verteilungen"}</p>
|
<p className="subline">{selectedBoard ? `Verteilung: ${selectedBoard.name}` : "Alle Verteilungen"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="statusPill">
|
<div className="statusPill">
|
||||||
<Activity size={16} />
|
<Activity size={16} />
|
||||||
{isLoading ? "Lädt" : "Bereit"}
|
{isLoading ? "Laedt" : "Bereit"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="boardTotals">
|
||||||
|
<h3>Summen nach Verteilung</h3>
|
||||||
|
<div className="tableScroll">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Verteilung</th>
|
||||||
|
<th>Verbraucher</th>
|
||||||
|
<th>Installierte Leistung [kW]</th>
|
||||||
|
<th>Berechnete Leistung [kW]</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{totalsByBoard.map((item) => (
|
||||||
|
<tr key={item.key}>
|
||||||
|
<td>{item.boardName}</td>
|
||||||
|
<td>{item.consumerCount}</td>
|
||||||
|
<td>{formatNumber(item.installedPowerKw)}</td>
|
||||||
|
<td>{formatNumber(item.demandPowerKw)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!totalsByBoard.length ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4} className="emptyState">
|
||||||
|
Noch keine Verbraucher fuer eine Summenbildung vorhanden.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
<tr className="totalRow">
|
||||||
|
<td>Projekt gesamt</td>
|
||||||
|
<td>{projectTotals.consumerCount}</td>
|
||||||
|
<td>{formatNumber(projectTotals.installedPowerKw)}</td>
|
||||||
|
<td>{formatNumber(projectTotals.demandPowerKw)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -347,35 +528,131 @@ export function PowerBalanceWorkspace() {
|
|||||||
<th>Verteilung</th>
|
<th>Verteilung</th>
|
||||||
<th>Kategorie</th>
|
<th>Kategorie</th>
|
||||||
<th>Anzahl</th>
|
<th>Anzahl</th>
|
||||||
<th>Leistung je Stück [kW]</th>
|
<th>Leistung je Stueck [kW]</th>
|
||||||
<th>Installierte Leistung [kW]</th>
|
<th>Installierte Leistung [kW]</th>
|
||||||
<th>GZF</th>
|
<th>GZF</th>
|
||||||
<th>Berechnete Leistung [kW]</th>
|
<th>Berechnete Leistung [kW]</th>
|
||||||
<th>Strom [A]</th>
|
<th>Strom [A]</th>
|
||||||
<th>Bemerkung</th>
|
<th>Bemerkung</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{visibleConsumers.map((consumer) => (
|
{visibleConsumers.map((consumer) => {
|
||||||
<tr key={consumer.id}>
|
const isEditing = editingConsumerId === consumer.id && editConsumerForm;
|
||||||
<td className="nameCell">
|
return (
|
||||||
<Zap size={15} />
|
<tr key={consumer.id}>
|
||||||
{consumer.name}
|
<td className="nameCell">
|
||||||
</td>
|
<Zap size={15} />
|
||||||
<td>{consumer.distributionBoardId ? boardNames.get(consumer.distributionBoardId) || "-" : "-"}</td>
|
{isEditing ? (
|
||||||
<td>{consumer.category || "-"}</td>
|
<input value={editConsumerForm.name} onChange={(event) => updateEditConsumerForm("name", event.target.value)} />
|
||||||
<td>{consumer.quantity}</td>
|
) : (
|
||||||
<td>{formatNumber(consumer.installedPowerPerUnitKw)}</td>
|
consumer.name
|
||||||
<td>{formatNumber(consumer.installedPowerKw)}</td>
|
)}
|
||||||
<td>{formatNumber(consumer.demandFactor, 2)}</td>
|
</td>
|
||||||
<td>{formatNumber(consumer.demandPowerKw)}</td>
|
<td>
|
||||||
<td>{formatNumber(consumer.currentA)}</td>
|
{isEditing ? (
|
||||||
<td>{consumer.note || "-"}</td>
|
<select
|
||||||
</tr>
|
value={editConsumerForm.distributionBoardId}
|
||||||
))}
|
onChange={(event) => updateEditConsumerForm("distributionBoardId", event.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">-</option>
|
||||||
|
{distributionBoards.map((board) => (
|
||||||
|
<option key={board.id} value={board.id}>
|
||||||
|
{board.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : consumer.distributionBoardId ? (
|
||||||
|
boardNames.get(consumer.distributionBoardId) || "-"
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{isEditing ? (
|
||||||
|
<input value={editConsumerForm.category} onChange={(event) => updateEditConsumerForm("category", event.target.value)} />
|
||||||
|
) : (
|
||||||
|
consumer.category || "-"
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{isEditing ? (
|
||||||
|
<input min="0" type="number" value={editConsumerForm.quantity} onChange={(event) => updateEditConsumerForm("quantity", event.target.value)} />
|
||||||
|
) : (
|
||||||
|
consumer.quantity
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
value={editConsumerForm.installedPowerPerUnitKw}
|
||||||
|
onChange={(event) => updateEditConsumerForm("installedPowerPerUnitKw", event.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
formatNumber(consumer.installedPowerPerUnitKw)
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>{formatNumber(consumer.installedPowerKw)}</td>
|
||||||
|
<td>
|
||||||
|
{isEditing ? (
|
||||||
|
<input min="0" max="1" step="0.01" type="number" value={editConsumerForm.demandFactor} onChange={(event) => updateEditConsumerForm("demandFactor", event.target.value)} />
|
||||||
|
) : (
|
||||||
|
formatNumber(consumer.demandFactor)
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>{formatNumber(consumer.demandPowerKw)}</td>
|
||||||
|
<td>
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="rowField">
|
||||||
|
<input min="1" type="number" value={editConsumerForm.voltageV} onChange={(event) => updateEditConsumerForm("voltageV", event.target.value)} />
|
||||||
|
<select value={editConsumerForm.phaseCount} onChange={(event) => updateEditConsumerForm("phaseCount", event.target.value)}>
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
</select>
|
||||||
|
<input min="0" max="1" step="0.01" type="number" value={editConsumerForm.powerFactor} onChange={(event) => updateEditConsumerForm("powerFactor", event.target.value)} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
formatNumber(consumer.currentA)
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{isEditing ? (
|
||||||
|
<input value={editConsumerForm.note} onChange={(event) => updateEditConsumerForm("note", event.target.value)} />
|
||||||
|
) : (
|
||||||
|
consumer.note || "-"
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="rowActions">
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<button className="iconButton small" type="button" title="Speichern" onClick={() => handleSaveConsumer(consumer.id)} disabled={isSaving}>
|
||||||
|
<Save size={14} />
|
||||||
|
</button>
|
||||||
|
<button className="iconButton small muted" type="button" title="Abbrechen" onClick={cancelEditConsumer}>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button className="iconButton small" type="button" title="Bearbeiten" onClick={() => startEditConsumer(consumer)}>
|
||||||
|
<Pencil size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button className="iconButton small danger" type="button" title="Loeschen" onClick={() => handleDeleteConsumer(consumer.id)} disabled={isSaving}>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{!visibleConsumers.length ? (
|
{!visibleConsumers.length ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={10} className="emptyState">
|
<td colSpan={11} className="emptyState">
|
||||||
Lege eine Verteilung an oder erfasse den ersten Verbraucher.
|
Lege eine Verteilung an oder erfasse den ersten Verbraucher.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -40,3 +40,5 @@ export interface CreateConsumerInput {
|
|||||||
powerFactor?: number;
|
powerFactor?: number;
|
||||||
note?: string;
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateConsumerInput extends CreateConsumerInput {}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
CreateConsumerInput,
|
CreateConsumerInput,
|
||||||
DistributionBoardDto,
|
DistributionBoardDto,
|
||||||
ProjectDto,
|
ProjectDto,
|
||||||
|
UpdateConsumerInput,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||||
@@ -54,3 +55,18 @@ export function createConsumer(input: CreateConsumerInput) {
|
|||||||
body: JSON.stringify(input),
|
body: JSON.stringify(input),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateConsumer(consumerId: string, input: UpdateConsumerInput) {
|
||||||
|
return request<ConsumerWithCalculatedValues>(`/api/consumers/${consumerId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,26 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { ConsumerRepository } from "../../db/repositories/consumer.repository.js";
|
import { ConsumerRepository } from "../../db/repositories/consumer.repository.js";
|
||||||
|
import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
|
||||||
import { PowerBalanceService } from "../../domain/services/power-balance.service.js";
|
import { PowerBalanceService } from "../../domain/services/power-balance.service.js";
|
||||||
import { createConsumerSchema } from "../../shared/validation/consumer.schemas.js";
|
import {
|
||||||
|
createConsumerSchema,
|
||||||
|
updateConsumerSchema,
|
||||||
|
} from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
const consumerRepository = new ConsumerRepository();
|
const consumerRepository = new ConsumerRepository();
|
||||||
|
const distributionBoardRepository = new DistributionBoardRepository();
|
||||||
const powerBalanceService = new PowerBalanceService();
|
const powerBalanceService = new PowerBalanceService();
|
||||||
|
|
||||||
|
async function validateDistributionBoardOwnership(
|
||||||
|
projectId: string,
|
||||||
|
distributionBoardId: string | undefined
|
||||||
|
) {
|
||||||
|
if (!distributionBoardId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return distributionBoardRepository.existsInProject(projectId, distributionBoardId);
|
||||||
|
}
|
||||||
|
|
||||||
export async function listConsumersByProject(req: Request, res: Response) {
|
export async function listConsumersByProject(req: Request, res: Response) {
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
if (typeof projectId !== "string") {
|
if (typeof projectId !== "string") {
|
||||||
@@ -36,7 +51,72 @@ export async function createConsumer(req: Request, res: Response) {
|
|||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasValidDistributionBoard = await validateDistributionBoardOwnership(
|
||||||
|
parsed.data.projectId,
|
||||||
|
parsed.data.distributionBoardId
|
||||||
|
);
|
||||||
|
if (!hasValidDistributionBoard) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "Distribution board does not belong to the provided project." });
|
||||||
|
}
|
||||||
|
|
||||||
const created = await consumerRepository.create(parsed.data);
|
const created = await consumerRepository.create(parsed.data);
|
||||||
const enriched = powerBalanceService.enrichConsumer(created);
|
const enriched = powerBalanceService.enrichConsumer(created);
|
||||||
return res.status(201).json(enriched);
|
return res.status(201).json(enriched);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateConsumer(req: Request, res: Response) {
|
||||||
|
const { consumerId } = req.params;
|
||||||
|
if (typeof consumerId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid consumerId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = updateConsumerSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasValidDistributionBoard = await validateDistributionBoardOwnership(
|
||||||
|
parsed.data.projectId,
|
||||||
|
parsed.data.distributionBoardId
|
||||||
|
);
|
||||||
|
if (!hasValidDistributionBoard) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "Distribution board does not belong to the provided project." });
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
return res.json(enriched);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteConsumer(req: Request, res: Response) {
|
||||||
|
const { consumerId } = req.params;
|
||||||
|
if (typeof consumerId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid consumerId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await consumerRepository.delete(consumerId);
|
||||||
|
return res.status(204).send();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { createConsumer, listConsumersByProject } from "../controllers/consumer.controller.js";
|
import {
|
||||||
|
createConsumer,
|
||||||
|
deleteConsumer,
|
||||||
|
listConsumersByProject,
|
||||||
|
updateConsumer,
|
||||||
|
} from "../controllers/consumer.controller.js";
|
||||||
|
|
||||||
export const consumerRouter = Router();
|
export const consumerRouter = Router();
|
||||||
|
|
||||||
consumerRouter.get("/projects/:projectId", listConsumersByProject);
|
consumerRouter.get("/projects/:projectId", listConsumersByProject);
|
||||||
consumerRouter.post("/", createConsumer);
|
consumerRouter.post("/", createConsumer);
|
||||||
|
consumerRouter.put("/:consumerId", updateConsumer);
|
||||||
|
consumerRouter.delete("/:consumerId", deleteConsumer);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export const createConsumerSchema = z.object({
|
|||||||
note: z.string().optional(),
|
note: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const updateConsumerSchema = createConsumerSchema;
|
||||||
|
|
||||||
export const createProjectSchema = z.object({
|
export const createProjectSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
});
|
});
|
||||||
@@ -25,3 +27,4 @@ export const createDistributionBoardSchema = z.object({
|
|||||||
export type CreateConsumerInput = z.infer<typeof createConsumerSchema>;
|
export type CreateConsumerInput = z.infer<typeof createConsumerSchema>;
|
||||||
export type CreateProjectInput = z.infer<typeof createProjectSchema>;
|
export type CreateProjectInput = z.infer<typeof createProjectSchema>;
|
||||||
export type CreateDistributionBoardInput = z.infer<typeof createDistributionBoardSchema>;
|
export type CreateDistributionBoardInput = z.infer<typeof createDistributionBoardSchema>;
|
||||||
|
export type UpdateConsumerInput = z.infer<typeof updateConsumerSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user