Remove legacy consumer UI
This commit is contained in:
@@ -10,4 +10,4 @@
|
||||
- No full norm-compliant voltage-drop and protection-dimensioning calculation flow yet.
|
||||
- Sorting is view-only until users explicitly apply sorted order.
|
||||
- Cross-section circuit drag-reorder is intentionally blocked.
|
||||
- Legacy consumer and circuit-first paths coexist; migration is transitional and still requires operational discipline.
|
||||
- The legacy consumer UI has been removed after complete migration; its server endpoints and retained database records are still pending final cleanup.
|
||||
|
||||
@@ -46,7 +46,8 @@ Run in this order for local database workflows:
|
||||
The migration command finishes with a cutover verification across the complete
|
||||
database. It exits with an error while any legacy consumer lacks a migration
|
||||
mapping, including consumers that cannot be migrated because they have no circuit
|
||||
list assignment. Do not remove or disable the legacy path until this check passes.
|
||||
list assignment. The legacy multi-list UI was removed only after this check passed;
|
||||
old `/projects/:projectId/circuit-lists` bookmarks redirect to the project page.
|
||||
|
||||
## Validation Checks
|
||||
|
||||
@@ -77,6 +78,6 @@ Actions:
|
||||
|
||||
Do not delete legacy consumers yet.
|
||||
|
||||
- legacy endpoints/views remain enabled only until the cutover verification passes
|
||||
- legacy write endpoints remain temporarily available for the next server cutover step
|
||||
- migration trace tables reference old/new mapping
|
||||
- keeping legacy rows allows comparison and rollback validation during transition
|
||||
|
||||
@@ -24,9 +24,6 @@ export default function CircuitTreeEditPage() {
|
||||
<Link href={`/projects/${params.projectId}`} className="btn btn-outline-secondary btn-sm">
|
||||
Zum Projekt
|
||||
</Link>
|
||||
<Link href={`/projects/${params.projectId}/circuit-lists`} className="btn btn-outline-secondary btn-sm">
|
||||
Bisherige Listenansicht
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<CircuitTreeEditor projectId={params.projectId} circuitListId={params.circuitListId} />
|
||||
|
||||
@@ -25,10 +25,10 @@ export default function CircuitTreePreviewPage() {
|
||||
Editor öffnen
|
||||
</Link>
|
||||
<Link
|
||||
href={`/projects/${projectId}/circuit-lists`}
|
||||
href={`/projects/${projectId}`}
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
>
|
||||
Zurück zur bisherigen Listenansicht
|
||||
Zum Projekt
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -607,12 +607,6 @@ export default function ProjectDetailPage() {
|
||||
Keine Stromkreisliste vorhanden
|
||||
</span>
|
||||
)}
|
||||
<Link
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
href={`/projects/${projectId}/circuit-lists?boardId=${board.id}`}
|
||||
>
|
||||
Bisherige Ansicht
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1124,11 +1118,6 @@ export default function ProjectDetailPage() {
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<div className="mt-4">
|
||||
<Link className="btn btn-primary" href={`/projects/${projectId}/circuit-lists`}>
|
||||
3 parallele Stromkreislisten öffnen
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# Components Placeholder
|
||||
|
||||
Reusable UI components for project, distribution board, and consumer tables.
|
||||
|
||||
@@ -1,666 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Activity, FolderPlus, Pencil, Plus, RefreshCw, Save, Trash2, X, Zap } from "lucide-react";
|
||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
createConsumer,
|
||||
createDistributionBoard,
|
||||
createProject,
|
||||
deleteConsumer,
|
||||
listConsumers,
|
||||
listDistributionBoards,
|
||||
listProjects,
|
||||
updateConsumer,
|
||||
} from "../utils/api";
|
||||
import type {
|
||||
ConsumerWithCalculatedValues,
|
||||
CreateConsumerInput,
|
||||
DistributionBoardDto,
|
||||
ProjectDto,
|
||||
UpdateConsumerInput,
|
||||
} from "../types";
|
||||
|
||||
const initialConsumerForm = {
|
||||
name: "",
|
||||
category: "",
|
||||
quantity: "1",
|
||||
installedPowerPerUnitKw: "0.1",
|
||||
demandFactor: "1",
|
||||
voltageV: "230",
|
||||
phaseCount: "1",
|
||||
powerFactor: "1",
|
||||
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;
|
||||
|
||||
function toOptionalNumber(value: string) {
|
||||
if (value.trim() === "") {
|
||||
return undefined;
|
||||
}
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
function formatNumber(value: number | undefined, digits = 2) {
|
||||
if (value === undefined || Number.isNaN(value)) {
|
||||
return "-";
|
||||
}
|
||||
return new Intl.NumberFormat("de-DE", {
|
||||
maximumFractionDigits: digits,
|
||||
minimumFractionDigits: digits,
|
||||
}).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() {
|
||||
const [projects, setProjects] = useState<ProjectDto[]>([]);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState("");
|
||||
const [distributionBoards, setDistributionBoards] = useState<DistributionBoardDto[]>([]);
|
||||
const [selectedBoardId, setSelectedBoardId] = useState("");
|
||||
const [consumers, setConsumers] = useState<ConsumerWithCalculatedValues[]>([]);
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const [boardName, setBoardName] = useState("");
|
||||
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 [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const selectedProject = projects.find((project) => project.id === selectedProjectId);
|
||||
const selectedBoard = distributionBoards.find((board) => board.id === selectedBoardId);
|
||||
const boardNames = new Map(distributionBoards.map((board) => [board.id, board.name]));
|
||||
const visibleConsumers = selectedBoardId
|
||||
? consumers.filter((consumer) => consumer.distributionBoardId === selectedBoardId)
|
||||
: consumers;
|
||||
|
||||
const totals = useMemo(
|
||||
() =>
|
||||
visibleConsumers.reduce(
|
||||
(sum, consumer) => ({
|
||||
installedPowerKw: sum.installedPowerKw + consumer.installedPowerKw,
|
||||
demandPowerKw: sum.demandPowerKw + consumer.demandPowerKw,
|
||||
}),
|
||||
{ installedPowerKw: 0, demandPowerKw: 0 }
|
||||
),
|
||||
[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() {
|
||||
setError(null);
|
||||
const loadedProjects = await listProjects();
|
||||
setProjects(loadedProjects);
|
||||
setSelectedProjectId((current) => current || loadedProjects[0]?.id || "");
|
||||
}
|
||||
|
||||
async function refreshConsumers(projectId: string) {
|
||||
if (!projectId) {
|
||||
setConsumers([]);
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
setConsumers(await listConsumers(projectId));
|
||||
}
|
||||
|
||||
async function refreshDistributionBoards(projectId: string) {
|
||||
if (!projectId) {
|
||||
setDistributionBoards([]);
|
||||
setSelectedBoardId("");
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
const boards = await listDistributionBoards(projectId);
|
||||
setDistributionBoards(boards);
|
||||
setSelectedBoardId((current) => (boards.some((board) => board.id === current) ? current : boards[0]?.id || ""));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshProjects()
|
||||
.catch((err: unknown) => setError(err instanceof Error ? err.message : "Projekte konnten nicht geladen werden."))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setEditingConsumerId(null);
|
||||
setEditConsumerForm(null);
|
||||
refreshDistributionBoards(selectedProjectId).catch((err: unknown) =>
|
||||
setError(err instanceof Error ? err.message : "Verteilungen konnten nicht geladen werden.")
|
||||
);
|
||||
refreshConsumers(selectedProjectId).catch((err: unknown) =>
|
||||
setError(err instanceof Error ? err.message : "Verbraucher konnten nicht geladen werden.")
|
||||
);
|
||||
}, [selectedProjectId]);
|
||||
|
||||
async function handleCreateProject(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const name = projectName.trim();
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const project = await createProject(name);
|
||||
setProjects((current) => [...current, project]);
|
||||
setSelectedProjectId(project.id);
|
||||
setProjectName("");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Projekt konnte nicht angelegt werden.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateDistributionBoard(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const name = boardName.trim();
|
||||
if (!selectedProjectId || !name) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const board = await createDistributionBoard(selectedProjectId, name);
|
||||
setDistributionBoards((current) => [...current, board]);
|
||||
setSelectedBoardId(board.id);
|
||||
setBoardName("");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Verteilung konnte nicht angelegt werden.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateConsumer(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!selectedProjectId || !consumerForm.name.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input: CreateConsumerInput = {
|
||||
projectId: selectedProjectId,
|
||||
distributionBoardId: selectedBoardId || undefined,
|
||||
name: consumerForm.name.trim(),
|
||||
category: consumerForm.category.trim() || undefined,
|
||||
quantity: Number(consumerForm.quantity),
|
||||
installedPowerPerUnitKw: Number(consumerForm.installedPowerPerUnitKw),
|
||||
demandFactor: Number(consumerForm.demandFactor),
|
||||
voltageV: toOptionalNumber(consumerForm.voltageV),
|
||||
phaseCount: consumerForm.phaseCount === "3" ? 3 : 1,
|
||||
powerFactor: toOptionalNumber(consumerForm.powerFactor),
|
||||
note: consumerForm.note.trim() || undefined,
|
||||
};
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const created = await createConsumer(input);
|
||||
setConsumers((current) => [...current, created]);
|
||||
setConsumerForm(initialConsumerForm);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Verbraucher konnte nicht angelegt werden.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
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 gelöscht 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) {
|
||||
setConsumerForm((current) => ({ ...current, [field]: value }));
|
||||
}
|
||||
|
||||
function updateEditConsumerForm(field: keyof EditConsumerForm, value: string) {
|
||||
setEditConsumerForm((current) => (current ? { ...current, [field]: value } : current));
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="workspace">
|
||||
<header className="topbar">
|
||||
<div>
|
||||
<p className="eyebrow">TGA / ELT Planung</p>
|
||||
<h1>Leistungsbilanz</h1>
|
||||
</div>
|
||||
<button className="iconButton" type="button" onClick={() => refreshConsumers(selectedProjectId)} title="Aktualisieren">
|
||||
<RefreshCw size={18} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{error ? <p className="alert">{error}</p> : null}
|
||||
|
||||
<section className="toolbarBand">
|
||||
<form className="projectForm" onSubmit={handleCreateProject}>
|
||||
<label>
|
||||
Projekt
|
||||
<select value={selectedProjectId} onChange={(event) => setSelectedProjectId(event.target.value)}>
|
||||
<option value="">Kein Projekt</option>
|
||||
{projects.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Neues Projekt
|
||||
<input value={projectName} onChange={(event) => setProjectName(event.target.value)} placeholder="z. B. BV Neubau" />
|
||||
</label>
|
||||
<button className="primaryButton" type="submit" disabled={isSaving}>
|
||||
<FolderPlus size={17} />
|
||||
Anlegen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form className="boardForm" onSubmit={handleCreateDistributionBoard}>
|
||||
<label>
|
||||
Verteilung
|
||||
<select value={selectedBoardId} onChange={(event) => setSelectedBoardId(event.target.value)}>
|
||||
<option value="">Alle Verteilungen</option>
|
||||
{distributionBoards.map((board) => (
|
||||
<option key={board.id} value={board.id}>
|
||||
{board.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Neue Verteilung
|
||||
<input value={boardName} onChange={(event) => setBoardName(event.target.value)} placeholder="z. B. UV-01" />
|
||||
</label>
|
||||
<button className="primaryButton" type="submit" disabled={!selectedProjectId || isSaving}>
|
||||
<Plus size={17} />
|
||||
Anlegen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="summaryStrip" aria-label="Summen">
|
||||
<div>
|
||||
<span>Verbraucher</span>
|
||||
<strong>{visibleConsumers.length}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Installierte Leistung</span>
|
||||
<strong>{formatNumber(totals.installedPowerKw)} kW</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Berechnete Leistung</span>
|
||||
<strong>{formatNumber(totals.demandPowerKw)} kW</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="entryBand">
|
||||
<form className="consumerForm" onSubmit={handleCreateConsumer}>
|
||||
<label>
|
||||
Verbraucher
|
||||
<input value={consumerForm.name} onChange={(event) => updateConsumerForm("name", event.target.value)} placeholder="z. B. Steckdosen Büro" />
|
||||
</label>
|
||||
<label>
|
||||
Kategorie
|
||||
<input value={consumerForm.category} onChange={(event) => updateConsumerForm("category", event.target.value)} placeholder="Allgemein" />
|
||||
</label>
|
||||
<label>
|
||||
Anzahl
|
||||
<input min="0" type="number" value={consumerForm.quantity} onChange={(event) => updateConsumerForm("quantity", event.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Leistung je Stück [kW]
|
||||
<input min="0" step="0.01" type="number" value={consumerForm.installedPowerPerUnitKw} onChange={(event) => updateConsumerForm("installedPowerPerUnitKw", event.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Gleichzeitigkeitsfaktor
|
||||
<input min="0" max="1" step="0.01" type="number" value={consumerForm.demandFactor} onChange={(event) => updateConsumerForm("demandFactor", event.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Spannung [V]
|
||||
<input min="1" type="number" value={consumerForm.voltageV} onChange={(event) => updateConsumerForm("voltageV", event.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Phasen
|
||||
<select value={consumerForm.phaseCount} onChange={(event) => updateConsumerForm("phaseCount", event.target.value)}>
|
||||
<option value="1">1-phasig</option>
|
||||
<option value="3">3-phasig</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
cos phi
|
||||
<input min="0" max="1" step="0.01" type="number" value={consumerForm.powerFactor} onChange={(event) => updateConsumerForm("powerFactor", event.target.value)} />
|
||||
</label>
|
||||
<label className="wideField">
|
||||
Bemerkung
|
||||
<input value={consumerForm.note} onChange={(event) => updateConsumerForm("note", event.target.value)} />
|
||||
</label>
|
||||
<button className="primaryButton" type="submit" disabled={!selectedProjectId || isSaving}>
|
||||
<Plus size={17} />
|
||||
Hinzufügen
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className="tableBand">
|
||||
<div className="tableHeader">
|
||||
<div>
|
||||
<p className="eyebrow">Aktuelles Projekt</p>
|
||||
<h2>{selectedProject?.name || "Noch kein Projekt ausgewählt"}</h2>
|
||||
<p className="subline">{selectedBoard ? `Verteilung: ${selectedBoard.name}` : "Alle Verteilungen"}</p>
|
||||
</div>
|
||||
<div className="statusPill">
|
||||
<Activity size={16} />
|
||||
{isLoading ? "Lädt" : "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 für 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 className="tableScroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Verbraucher</th>
|
||||
<th>Verteilung</th>
|
||||
<th>Kategorie</th>
|
||||
<th>Anzahl</th>
|
||||
<th>Leistung je Stück [kW]</th>
|
||||
<th>Installierte Leistung [kW]</th>
|
||||
<th>GZF</th>
|
||||
<th>Berechnete Leistung [kW]</th>
|
||||
<th>Strom [A]</th>
|
||||
<th>Bemerkung</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visibleConsumers.map((consumer) => {
|
||||
const isEditing = editingConsumerId === consumer.id && editConsumerForm;
|
||||
return (
|
||||
<tr key={consumer.id}>
|
||||
<td className="nameCell">
|
||||
<Zap size={15} />
|
||||
{isEditing ? (
|
||||
<input value={editConsumerForm.name} onChange={(event) => updateEditConsumerForm("name", event.target.value)} />
|
||||
) : (
|
||||
consumer.name
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{isEditing ? (
|
||||
<select
|
||||
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="Löschen" onClick={() => handleDeleteConsumer(consumer.id)} disabled={isSaving}>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{!visibleConsumers.length ? (
|
||||
<tr>
|
||||
<td colSpan={11} className="emptyState">
|
||||
Lege eine Verteilung an oder erfasse den ersten Verbraucher.
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -5,45 +5,6 @@ export interface ProjectDto {
|
||||
threePhaseVoltageV: number;
|
||||
}
|
||||
|
||||
export interface ConsumerWithCalculatedValues {
|
||||
id: string;
|
||||
projectId: string;
|
||||
distributionBoardId?: string | null;
|
||||
circuitListId?: string | null;
|
||||
projectDeviceId?: string | null;
|
||||
isLinkedToDevice?: boolean;
|
||||
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;
|
||||
voltageV?: number;
|
||||
phaseCount?: 1 | 3;
|
||||
powerFactor?: number;
|
||||
note?: string;
|
||||
installedPowerKw: number;
|
||||
demandPowerKw: number;
|
||||
effectiveVoltageV?: number;
|
||||
currentA?: number;
|
||||
}
|
||||
|
||||
export interface DistributionBoardDto {
|
||||
id: string;
|
||||
projectId: string;
|
||||
@@ -101,47 +62,9 @@ export interface ProjectDeviceDto {
|
||||
totalPower: number;
|
||||
cosPhi: number | null;
|
||||
remark: string | null;
|
||||
// Legacy aliases retained until the old Consumer editor is removed.
|
||||
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;
|
||||
projectDeviceId?: string;
|
||||
isLinkedToDevice?: boolean;
|
||||
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;
|
||||
voltageV?: number;
|
||||
phaseCount?: 1 | 3;
|
||||
powerFactor?: number;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface UpdateConsumerInput extends CreateConsumerInput {}
|
||||
|
||||
export interface CreateFloorInput {
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ import type {
|
||||
CreateFloorInput,
|
||||
CreateProjectDeviceInput,
|
||||
CreateRoomInput,
|
||||
ConsumerWithCalculatedValues,
|
||||
CreateConsumerInput,
|
||||
CreateGlobalDeviceInput,
|
||||
DistributionBoardDto,
|
||||
FloorDto,
|
||||
@@ -16,7 +14,6 @@ import type {
|
||||
ProjectDeviceSyncResultDto,
|
||||
ProjectDto,
|
||||
RoomDto,
|
||||
UpdateConsumerInput,
|
||||
CircuitTreeResponseDto,
|
||||
CircuitTreeCircuitDto,
|
||||
CircuitTreeDeviceRowDto,
|
||||
@@ -229,28 +226,6 @@ export function createRoom(projectId: string, input: CreateRoomInput) {
|
||||
});
|
||||
}
|
||||
|
||||
export function listConsumers(projectId: string) {
|
||||
return request<ConsumerWithCalculatedValues[]>(`/api/consumers/projects/${projectId}`);
|
||||
}
|
||||
|
||||
export function createConsumer(input: CreateConsumerInput) {
|
||||
return request<ConsumerWithCalculatedValues>("/api/consumers", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function updateConsumer(consumerId: string, input: UpdateConsumerInput) {
|
||||
return request<ConsumerWithCalculatedValues>(`/api/consumers/${consumerId}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteConsumer(consumerId: string) {
|
||||
return request<void>(`/api/consumers/${consumerId}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export function listGlobalDevices() {
|
||||
return request<GlobalDeviceDto[]>("/api/global-devices");
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
export interface ProjectDto {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DistributionBoardDto {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
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;
|
||||
voltageV: number | null;
|
||||
phaseCount: 1 | 3 | null;
|
||||
powerFactor: number | null;
|
||||
note: string | null;
|
||||
}
|
||||
Reference in New Issue
Block a user