Rewrite frontend, added rooms, voltage selection per project, startet with todos
This commit is contained in:
+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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user