All first todos completed
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
listProjectDevices,
|
||||
listRooms,
|
||||
updateConsumer,
|
||||
updateProjectSettings,
|
||||
} from "../../../../frontend/utils/api";
|
||||
import type {
|
||||
ConsumerWithCalculatedValues,
|
||||
@@ -22,6 +21,16 @@ import type {
|
||||
ProjectDto,
|
||||
RoomDto,
|
||||
} from "../../../../frontend/types";
|
||||
import {
|
||||
cableCrossSectionOptions,
|
||||
cableTypeOptions,
|
||||
consumerGroupOptions,
|
||||
deviceTypeOptions,
|
||||
phaseTypeOptions,
|
||||
protectionCharacteristicOptions,
|
||||
protectionTypeOptions,
|
||||
tradeOrCostGroupOptions,
|
||||
} from "../../../../shared/constants/consumer-option-lists";
|
||||
|
||||
interface SlotState {
|
||||
boardId: string;
|
||||
@@ -35,6 +44,22 @@ interface QuickCreateFormState {
|
||||
installedPowerPerUnitKw: string;
|
||||
demandFactor: string;
|
||||
totalPowerKw: string;
|
||||
addCount: string;
|
||||
}
|
||||
|
||||
type SortField = "name" | "circuitNumber" | "quantity" | "installedPowerPerUnitKw" | "demandFactor" | "demandPowerKw";
|
||||
type SortDirection = "asc" | "desc";
|
||||
|
||||
interface ListFilterState {
|
||||
query: string;
|
||||
sortField: SortField;
|
||||
sortDirection: SortDirection;
|
||||
}
|
||||
|
||||
interface BulkEditState {
|
||||
quantity: string;
|
||||
installedPowerPerUnitKw: string;
|
||||
demandFactor: string;
|
||||
}
|
||||
|
||||
type ColumnKey =
|
||||
@@ -42,9 +67,21 @@ type ColumnKey =
|
||||
| "circuitNumber"
|
||||
| "description"
|
||||
| "name"
|
||||
| "projectDevice"
|
||||
| "deviceLink"
|
||||
| "room"
|
||||
| "floor"
|
||||
| "category"
|
||||
| "deviceType"
|
||||
| "phaseType"
|
||||
| "tradeOrCostGroup"
|
||||
| "group"
|
||||
| "protectionType"
|
||||
| "protectionRatedCurrent"
|
||||
| "protectionCharacteristic"
|
||||
| "cableType"
|
||||
| "cableCrossSection"
|
||||
| "comment"
|
||||
| "quantity"
|
||||
| "installedPowerPerUnitKw"
|
||||
| "installedPowerKw"
|
||||
@@ -70,6 +107,19 @@ const defaultCreateForm: QuickCreateFormState = {
|
||||
installedPowerPerUnitKw: "0.10",
|
||||
demandFactor: "1.00",
|
||||
totalPowerKw: "0.10",
|
||||
addCount: "1",
|
||||
};
|
||||
|
||||
const defaultFilterState: ListFilterState = {
|
||||
query: "",
|
||||
sortField: "name",
|
||||
sortDirection: "asc",
|
||||
};
|
||||
|
||||
const defaultBulkEditState: BulkEditState = {
|
||||
quantity: "",
|
||||
installedPowerPerUnitKw: "",
|
||||
demandFactor: "",
|
||||
};
|
||||
|
||||
const allColumns: Array<{ key: ColumnKey; label: string }> = [
|
||||
@@ -77,9 +127,21 @@ const allColumns: Array<{ key: ColumnKey; label: string }> = [
|
||||
{ key: "circuitNumber", label: "Stromkreis-Nr." },
|
||||
{ key: "description", label: "Beschreibung" },
|
||||
{ key: "name", label: "Verbraucher" },
|
||||
{ key: "projectDevice", label: "Projektgerät" },
|
||||
{ key: "deviceLink", label: "Link" },
|
||||
{ key: "room", label: "Raum" },
|
||||
{ key: "floor", label: "Etage" },
|
||||
{ key: "category", label: "Kategorie" },
|
||||
{ key: "deviceType", label: "Geräteart" },
|
||||
{ key: "phaseType", label: "Phasenart" },
|
||||
{ key: "tradeOrCostGroup", label: "Kostengruppe" },
|
||||
{ key: "group", label: "Gruppe" },
|
||||
{ key: "protectionType", label: "Schutzart" },
|
||||
{ key: "protectionRatedCurrent", label: "Schutznennstrom [A]" },
|
||||
{ key: "protectionCharacteristic", label: "Schutzkennlinie" },
|
||||
{ key: "cableType", label: "Kabeltyp" },
|
||||
{ key: "cableCrossSection", label: "Querschnitt" },
|
||||
{ key: "comment", label: "Kommentar" },
|
||||
{ key: "quantity", label: "Anzahl" },
|
||||
{ key: "installedPowerPerUnitKw", label: "Einzelleistung [kW]" },
|
||||
{ key: "installedPowerKw", label: "Installierte Leistung [kW]" },
|
||||
@@ -87,7 +149,7 @@ const allColumns: Array<{ key: ColumnKey; label: string }> = [
|
||||
{ key: "demandPowerKw", label: "Gesamtleistung [kW]" },
|
||||
{ key: "voltageV", label: "Spannung [V]" },
|
||||
{ key: "phaseCount", label: "Phasen" },
|
||||
{ key: "powerFactor", label: "cos φ" },
|
||||
{ key: "powerFactor", label: "cos f" },
|
||||
{ key: "currentA", label: "Strom [A]" },
|
||||
{ key: "note", label: "Bemerkung" },
|
||||
{ key: "actions", label: "Aktionen" },
|
||||
@@ -98,6 +160,8 @@ const defaultVisibleColumns: ColumnKey[] = [
|
||||
"circuitNumber",
|
||||
"description",
|
||||
"name",
|
||||
"projectDevice",
|
||||
"deviceLink",
|
||||
"room",
|
||||
"quantity",
|
||||
"installedPowerPerUnitKw",
|
||||
@@ -139,8 +203,16 @@ export default function CircuitListsPage() {
|
||||
2: { ...defaultCreateForm },
|
||||
});
|
||||
const [visibleColumns, setVisibleColumns] = useState<ColumnKey[]>(defaultVisibleColumns);
|
||||
const [singlePhaseVoltageV, setSinglePhaseVoltageV] = useState("230");
|
||||
const [threePhaseVoltageV, setThreePhaseVoltageV] = useState("400");
|
||||
const [listFilters, setListFilters] = useState<Record<number, ListFilterState>>({
|
||||
0: { ...defaultFilterState },
|
||||
1: { ...defaultFilterState },
|
||||
2: { ...defaultFilterState },
|
||||
});
|
||||
const [bulkEditForms, setBulkEditForms] = useState<Record<number, BulkEditState>>({
|
||||
0: { ...defaultBulkEditState },
|
||||
1: { ...defaultBulkEditState },
|
||||
2: { ...defaultBulkEditState },
|
||||
});
|
||||
const [showColumnManager, setShowColumnManager] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -163,8 +235,6 @@ export default function CircuitListsPage() {
|
||||
.then(([loadedProject, distributionBoards, loadedRooms, loadedProjectDevices, loadedConsumers]) => {
|
||||
const initialBoardId = searchParams.get("boardId") ?? distributionBoards[0]?.id ?? "";
|
||||
setProject(loadedProject);
|
||||
setSinglePhaseVoltageV(String(loadedProject.singlePhaseVoltageV));
|
||||
setThreePhaseVoltageV(String(loadedProject.threePhaseVoltageV));
|
||||
setBoards(distributionBoards);
|
||||
setRooms(loadedRooms);
|
||||
setProjectDevices(loadedProjectDevices);
|
||||
@@ -190,6 +260,39 @@ export default function CircuitListsPage() {
|
||||
return consumers.filter((item) => item.distributionBoardId === boardId);
|
||||
}
|
||||
|
||||
function updateListFilter(slotIndex: number, patch: Partial<ListFilterState>) {
|
||||
setListFilters((current) => ({
|
||||
...current,
|
||||
[slotIndex]: { ...current[slotIndex], ...patch },
|
||||
}));
|
||||
}
|
||||
|
||||
function updateBulkEditForm(slotIndex: number, patch: Partial<BulkEditState>) {
|
||||
setBulkEditForms((current) => ({
|
||||
...current,
|
||||
[slotIndex]: { ...current[slotIndex], ...patch },
|
||||
}));
|
||||
}
|
||||
|
||||
function getComparableSortValue(consumer: ConsumerWithCalculatedValues, sortField: SortField): number | string {
|
||||
switch (sortField) {
|
||||
case "name":
|
||||
return (consumer.name ?? "").toLocaleLowerCase("de-DE");
|
||||
case "circuitNumber":
|
||||
return (consumer.circuitNumber ?? "").toLocaleLowerCase("de-DE");
|
||||
case "quantity":
|
||||
return consumer.quantity ?? 0;
|
||||
case "installedPowerPerUnitKw":
|
||||
return consumer.installedPowerPerUnitKw ?? 0;
|
||||
case "demandFactor":
|
||||
return consumer.demandFactor ?? 0;
|
||||
case "demandPowerKw":
|
||||
return consumer.demandPowerKw ?? 0;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function setSlotBoard(slotIndex: number, boardId: string) {
|
||||
setSlots((current) =>
|
||||
current.map((slot, index) => (index === slotIndex ? { boardId, selectedConsumerIds: [] } : slot))
|
||||
@@ -266,26 +369,6 @@ export default function CircuitListsPage() {
|
||||
setConsumers(await listConsumers(projectId));
|
||||
}
|
||||
|
||||
async function handleSaveProjectSettings() {
|
||||
if (!projectId) {
|
||||
return;
|
||||
}
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const updated = await updateProjectSettings(projectId, {
|
||||
singlePhaseVoltageV: Number(singlePhaseVoltageV),
|
||||
threePhaseVoltageV: Number(threePhaseVoltageV),
|
||||
});
|
||||
setProject(updated);
|
||||
await reloadConsumers();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Projekteigenschaften konnten nicht gespeichert werden.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateManualConsumer(slotIndex: number, event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const slot = slots[slotIndex];
|
||||
@@ -298,6 +381,7 @@ export default function CircuitListsPage() {
|
||||
const payload: CreateConsumerInput = {
|
||||
projectId,
|
||||
distributionBoardId: slot.boardId,
|
||||
isLinkedToDevice: false,
|
||||
roomId: form.roomId || undefined,
|
||||
description: name,
|
||||
name,
|
||||
@@ -332,13 +416,17 @@ export default function CircuitListsPage() {
|
||||
if (!projectId || !slot.boardId || !projectDevice) {
|
||||
return;
|
||||
}
|
||||
const addCountRaw = Number(form.addCount);
|
||||
const addCount = Number.isFinite(addCountRaw) ? Math.max(1, Math.floor(addCountRaw)) : 1;
|
||||
|
||||
const payload: CreateConsumerInput = {
|
||||
projectId,
|
||||
distributionBoardId: slot.boardId,
|
||||
projectDeviceId: projectDevice.id,
|
||||
isLinkedToDevice: true,
|
||||
roomId: form.roomId || undefined,
|
||||
description: projectDevice.name,
|
||||
name: projectDevice.name,
|
||||
description: projectDevice.displayName,
|
||||
name: projectDevice.displayName,
|
||||
category: projectDevice.category ?? undefined,
|
||||
quantity: projectDevice.quantity,
|
||||
installedPowerPerUnitKw: projectDevice.installedPowerPerUnitKw,
|
||||
@@ -351,7 +439,7 @@ export default function CircuitListsPage() {
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
await createConsumer(payload);
|
||||
await Promise.all(Array.from({ length: addCount }, () => createConsumer(payload)));
|
||||
await reloadConsumers();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Verbraucher konnte nicht aus Projektgerät erstellt werden.");
|
||||
@@ -369,6 +457,8 @@ export default function CircuitListsPage() {
|
||||
const payload: CreateConsumerInput = {
|
||||
projectId,
|
||||
distributionBoardId: targetBoardId,
|
||||
projectDeviceId: sourceConsumer.projectDeviceId ?? undefined,
|
||||
isLinkedToDevice: sourceConsumer.isLinkedToDevice,
|
||||
circuitNumber: sourceConsumer.circuitNumber,
|
||||
description: sourceConsumer.description,
|
||||
roomId: sourceConsumer.roomId ?? undefined,
|
||||
@@ -405,6 +495,52 @@ export default function CircuitListsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDuplicateInSameList(sourceConsumer: ConsumerWithCalculatedValues) {
|
||||
if (!projectId || !sourceConsumer.distributionBoardId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: CreateConsumerInput = {
|
||||
projectId,
|
||||
distributionBoardId: sourceConsumer.distributionBoardId,
|
||||
projectDeviceId: sourceConsumer.projectDeviceId ?? undefined,
|
||||
isLinkedToDevice: sourceConsumer.isLinkedToDevice,
|
||||
circuitNumber: sourceConsumer.circuitNumber,
|
||||
description: sourceConsumer.description,
|
||||
roomId: sourceConsumer.roomId ?? undefined,
|
||||
name: sourceConsumer.name,
|
||||
category: sourceConsumer.category,
|
||||
deviceType: sourceConsumer.deviceType,
|
||||
phaseType: sourceConsumer.phaseType,
|
||||
tradeOrCostGroup: sourceConsumer.tradeOrCostGroup,
|
||||
group: sourceConsumer.group,
|
||||
protectionType: sourceConsumer.protectionType,
|
||||
protectionRatedCurrent: sourceConsumer.protectionRatedCurrent,
|
||||
protectionCharacteristic: sourceConsumer.protectionCharacteristic,
|
||||
cableType: sourceConsumer.cableType,
|
||||
cableCrossSection: sourceConsumer.cableCrossSection,
|
||||
comment: sourceConsumer.comment,
|
||||
quantity: sourceConsumer.quantity,
|
||||
installedPowerPerUnitKw: sourceConsumer.installedPowerPerUnitKw,
|
||||
demandFactor: sourceConsumer.demandFactor,
|
||||
voltageV: sourceConsumer.voltageV,
|
||||
phaseCount: sourceConsumer.phaseCount,
|
||||
powerFactor: sourceConsumer.powerFactor,
|
||||
note: sourceConsumer.note,
|
||||
};
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
await createConsumer(payload);
|
||||
await reloadConsumers();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Duplizieren in derselben Liste fehlgeschlagen.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopySelectionToSlot(sourceSlotIndex: number, targetSlotIndex: number) {
|
||||
const sourceSlot = slots[sourceSlotIndex];
|
||||
const targetBoardId = slots[targetSlotIndex].boardId;
|
||||
@@ -425,6 +561,8 @@ export default function CircuitListsPage() {
|
||||
createConsumer({
|
||||
projectId,
|
||||
distributionBoardId: targetBoardId,
|
||||
projectDeviceId: sourceConsumer.projectDeviceId ?? undefined,
|
||||
isLinkedToDevice: sourceConsumer.isLinkedToDevice,
|
||||
circuitNumber: sourceConsumer.circuitNumber,
|
||||
description: sourceConsumer.description,
|
||||
roomId: sourceConsumer.roomId ?? undefined,
|
||||
@@ -461,6 +599,59 @@ export default function CircuitListsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBulkEditSelection(slotIndex: number) {
|
||||
const slot = slots[slotIndex];
|
||||
if (!slot.selectedConsumerIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const form = bulkEditForms[slotIndex];
|
||||
const patch: Partial<CreateConsumerInput> = {};
|
||||
const quantity = form.quantity.trim();
|
||||
const installedPowerPerUnitKw = form.installedPowerPerUnitKw.trim();
|
||||
const demandFactor = form.demandFactor.trim();
|
||||
|
||||
if (quantity) {
|
||||
const value = Number(quantity);
|
||||
if (!Number.isNaN(value) && value >= 0) {
|
||||
patch.quantity = value;
|
||||
}
|
||||
}
|
||||
if (installedPowerPerUnitKw) {
|
||||
const value = Number(installedPowerPerUnitKw);
|
||||
if (!Number.isNaN(value) && value >= 0) {
|
||||
patch.installedPowerPerUnitKw = value;
|
||||
}
|
||||
}
|
||||
if (demandFactor) {
|
||||
const value = Number(demandFactor);
|
||||
if (!Number.isNaN(value) && value >= 0 && value <= 1) {
|
||||
patch.demandFactor = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(patch).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = consumers.filter((consumer) => slot.selectedConsumerIds.includes(consumer.id));
|
||||
if (!selected.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
await Promise.all(selected.map((consumer) => handleInlineUpdateFields(consumer, patch)));
|
||||
await reloadConsumers();
|
||||
setBulkEditForms((current) => ({ ...current, [slotIndex]: { ...defaultBulkEditState } }));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Bulk-Änderung fehlgeschlagen.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleInlineUpdateFields(
|
||||
consumer: ConsumerWithCalculatedValues,
|
||||
patch: Partial<CreateConsumerInput>
|
||||
@@ -472,6 +663,10 @@ export default function CircuitListsPage() {
|
||||
? patch.distributionBoardId
|
||||
: consumer.distributionBoardId ?? undefined,
|
||||
circuitListId: patch.circuitListId !== undefined ? patch.circuitListId : consumer.circuitListId ?? undefined,
|
||||
projectDeviceId:
|
||||
patch.projectDeviceId !== undefined ? patch.projectDeviceId : consumer.projectDeviceId ?? undefined,
|
||||
isLinkedToDevice:
|
||||
patch.isLinkedToDevice !== undefined ? patch.isLinkedToDevice : consumer.isLinkedToDevice ?? false,
|
||||
circuitNumber: patch.circuitNumber !== undefined ? patch.circuitNumber : consumer.circuitNumber,
|
||||
description: patch.description !== undefined ? patch.description : consumer.description,
|
||||
roomId: patch.roomId !== undefined ? patch.roomId : consumer.roomId ?? undefined,
|
||||
@@ -593,48 +788,6 @@ export default function CircuitListsPage() {
|
||||
|
||||
{error ? <div className="alert alert-warning">{error}</div> : null}
|
||||
|
||||
<section className="card shadow-sm mb-3">
|
||||
<div className="card-header">Projekteigenschaften</div>
|
||||
<div className="card-body">
|
||||
<div className="row g-2 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>
|
||||
<small className="text-secondary d-block mt-2">
|
||||
Ohne explizite Verbraucherspannung gilt automatisch 1-phasig = {singlePhaseVoltageV || "230"} V und
|
||||
3-phasig = {threePhaseVoltageV || "400"} V.
|
||||
</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{showColumnManager ? (
|
||||
<section className="card shadow-sm mb-3">
|
||||
<div className="card-header">Spaltenauswahl und Reihenfolge</div>
|
||||
@@ -677,7 +830,7 @@ export default function CircuitListsPage() {
|
||||
))}
|
||||
</div>
|
||||
<small className="text-secondary d-block mt-2">
|
||||
Standardmäßig sind cos φ, Phasen und Strom ausgeblendet. Du kannst sie hier jederzeit einblenden.
|
||||
Standardmäßig sind cos f, Phasen und Strom ausgeblendet. Du kannst sie hier jederzeit einblenden.
|
||||
</small>
|
||||
</div>
|
||||
</section>
|
||||
@@ -685,7 +838,38 @@ export default function CircuitListsPage() {
|
||||
|
||||
<div className="row g-3">
|
||||
{slots.slice(0, activeListCount).map((slot, slotIndex) => {
|
||||
const listConsumersForSlot = slot.boardId ? consumersForBoard(slot.boardId) : [];
|
||||
const currentFilter = listFilters[slotIndex];
|
||||
const bulkEditForm = bulkEditForms[slotIndex];
|
||||
const baseConsumersForSlot = slot.boardId ? consumersForBoard(slot.boardId) : [];
|
||||
const query = currentFilter.query.trim().toLocaleLowerCase("de-DE");
|
||||
const filteredConsumersForSlot = query
|
||||
? baseConsumersForSlot.filter((consumer) => {
|
||||
const searchable = [
|
||||
consumer.circuitNumber ?? "",
|
||||
consumer.description ?? "",
|
||||
consumer.name ?? "",
|
||||
consumer.category ?? "",
|
||||
consumer.note ?? "",
|
||||
consumer.roomNumber ?? "",
|
||||
consumer.roomName ?? "",
|
||||
consumer.floorName ?? "",
|
||||
]
|
||||
.join(" ")
|
||||
.toLocaleLowerCase("de-DE");
|
||||
return searchable.includes(query);
|
||||
})
|
||||
: baseConsumersForSlot;
|
||||
const listConsumersForSlot = [...filteredConsumersForSlot].sort((a, b) => {
|
||||
const aValue = getComparableSortValue(a, currentFilter.sortField);
|
||||
const bValue = getComparableSortValue(b, currentFilter.sortField);
|
||||
let result = 0;
|
||||
if (typeof aValue === "number" && typeof bValue === "number") {
|
||||
result = aValue - bValue;
|
||||
} else {
|
||||
result = String(aValue).localeCompare(String(bValue), "de-DE", { sensitivity: "base" });
|
||||
}
|
||||
return currentFilter.sortDirection === "asc" ? result : result * -1;
|
||||
});
|
||||
const quickForm = quickCreateForms[slotIndex];
|
||||
|
||||
return (
|
||||
@@ -797,10 +981,20 @@ export default function CircuitListsPage() {
|
||||
<option value="">Projektgerät wählen</option>
|
||||
{projectDevices.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
{item.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
className="form-control"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={quickForm.addCount}
|
||||
onChange={(event) => updateQuickCreateForm(slotIndex, { addCount: event.target.value })}
|
||||
title="Anzahl Einträge"
|
||||
style={{ maxWidth: "8rem" }}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
type="button"
|
||||
@@ -812,6 +1006,95 @@ export default function CircuitListsPage() {
|
||||
</div>
|
||||
|
||||
<div className="table-responsive border rounded">
|
||||
<div className="border-bottom p-2 bg-light-subtle">
|
||||
<div className="row g-2">
|
||||
<div className="col-12 col-xl-4">
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Filter (z. B. Name, Raum, Nummer)"
|
||||
value={currentFilter.query}
|
||||
onChange={(event) => updateListFilter(slotIndex, { query: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-xl-3">
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={currentFilter.sortField}
|
||||
onChange={(event) =>
|
||||
updateListFilter(slotIndex, { sortField: event.target.value as SortField })
|
||||
}
|
||||
>
|
||||
<option value="name">Sortierung: Verbraucher</option>
|
||||
<option value="circuitNumber">Sortierung: Stromkreis-Nr.</option>
|
||||
<option value="quantity">Sortierung: Anzahl</option>
|
||||
<option value="installedPowerPerUnitKw">Sortierung: Einzelleistung</option>
|
||||
<option value="demandFactor">Sortierung: Gleichzeitigkeitsfaktor</option>
|
||||
<option value="demandPowerKw">Sortierung: Gesamtleistung</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-6 col-xl-2">
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={currentFilter.sortDirection}
|
||||
onChange={(event) =>
|
||||
updateListFilter(slotIndex, { sortDirection: event.target.value as SortDirection })
|
||||
}
|
||||
>
|
||||
<option value="asc">Aufsteigend</option>
|
||||
<option value="desc">Absteigend</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-4 col-xl-1">
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
placeholder="Anzahl"
|
||||
value={bulkEditForm.quantity}
|
||||
onChange={(event) => updateBulkEditForm(slotIndex, { quantity: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-4 col-xl-1">
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="kW"
|
||||
value={bulkEditForm.installedPowerPerUnitKw}
|
||||
onChange={(event) =>
|
||||
updateBulkEditForm(slotIndex, { installedPowerPerUnitKw: event.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-4 col-xl-1">
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
placeholder="GZF"
|
||||
value={bulkEditForm.demandFactor}
|
||||
onChange={(event) => updateBulkEditForm(slotIndex, { demandFactor: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between align-items-center mt-2">
|
||||
<small className="text-secondary">
|
||||
Gefiltert: {listConsumersForSlot.length} von {baseConsumersForSlot.length}
|
||||
</small>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
type="button"
|
||||
onClick={() => handleBulkEditSelection(slotIndex)}
|
||||
disabled={!slot.selectedConsumerIds.length || isSaving}
|
||||
>
|
||||
Auswahl ändern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table className="table table-sm align-middle mb-0">
|
||||
<thead className="table-light">
|
||||
<tr>
|
||||
@@ -882,6 +1165,45 @@ export default function CircuitListsPage() {
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "projectDevice":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
defaultValue={consumer.projectDeviceId ?? ""}
|
||||
onChange={(event) =>
|
||||
handleInlineUpdateFields(consumer, {
|
||||
projectDeviceId: event.target.value || undefined,
|
||||
isLinkedToDevice: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Kein Projektgerät</option>
|
||||
{projectDevices.map((device) => (
|
||||
<option key={device.id} value={device.id}>
|
||||
{device.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
);
|
||||
case "deviceLink":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
type="button"
|
||||
disabled={!consumer.projectDeviceId}
|
||||
onClick={() =>
|
||||
handleInlineUpdateFields(consumer, {
|
||||
isLinkedToDevice: !consumer.isLinkedToDevice,
|
||||
})
|
||||
}
|
||||
>
|
||||
{consumer.isLinkedToDevice ? "Verknüpft" : "Entkoppelt"}
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
case "room":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
@@ -921,6 +1243,217 @@ export default function CircuitListsPage() {
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "deviceType":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
defaultValue={consumer.deviceType ?? ""}
|
||||
onChange={(event) =>
|
||||
handleInlineUpdateFields(consumer, {
|
||||
deviceType: event.target.value || undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Bitte wählen</option>
|
||||
{deviceTypeOptions.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
);
|
||||
case "phaseType":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
defaultValue={consumer.phaseType ?? ""}
|
||||
onChange={(event) =>
|
||||
handleInlineUpdateFields(consumer, {
|
||||
phaseType: event.target.value || undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Bitte wählen</option>
|
||||
{phaseTypeOptions.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
);
|
||||
case "tradeOrCostGroup":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
defaultValue={consumer.tradeOrCostGroup ?? ""}
|
||||
onChange={(event) =>
|
||||
handleInlineUpdateFields(consumer, {
|
||||
tradeOrCostGroup: event.target.value || undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Bitte wählen</option>
|
||||
{tradeOrCostGroupOptions.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
);
|
||||
case "group":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
defaultValue={consumer.group ?? ""}
|
||||
onChange={(event) =>
|
||||
handleInlineUpdateFields(consumer, {
|
||||
group: event.target.value || undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Bitte wählen</option>
|
||||
{consumerGroupOptions.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
);
|
||||
case "protectionType":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
defaultValue={consumer.protectionType ?? ""}
|
||||
onChange={(event) =>
|
||||
handleInlineUpdateFields(consumer, {
|
||||
protectionType: event.target.value || undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Bitte wählen</option>
|
||||
{protectionTypeOptions.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
);
|
||||
case "protectionRatedCurrent":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
defaultValue={consumer.protectionRatedCurrent ?? ""}
|
||||
onBlur={(event) => {
|
||||
const raw = event.target.value.trim();
|
||||
const value = raw ? Number(raw) : undefined;
|
||||
if (raw === "" && consumer.protectionRatedCurrent !== undefined) {
|
||||
void handleInlineUpdateFields(consumer, {
|
||||
protectionRatedCurrent: undefined,
|
||||
});
|
||||
} else if (
|
||||
value !== undefined &&
|
||||
!Number.isNaN(value) &&
|
||||
value !== consumer.protectionRatedCurrent
|
||||
) {
|
||||
void handleInlineUpdateFields(consumer, { protectionRatedCurrent: value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "protectionCharacteristic":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
defaultValue={consumer.protectionCharacteristic ?? ""}
|
||||
onChange={(event) =>
|
||||
handleInlineUpdateFields(consumer, {
|
||||
protectionCharacteristic: event.target.value || undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Bitte wählen</option>
|
||||
{protectionCharacteristicOptions.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
);
|
||||
case "cableType":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
defaultValue={consumer.cableType ?? ""}
|
||||
onChange={(event) =>
|
||||
handleInlineUpdateFields(consumer, {
|
||||
cableType: event.target.value || undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Bitte wählen</option>
|
||||
{cableTypeOptions.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
);
|
||||
case "cableCrossSection":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
defaultValue={consumer.cableCrossSection ?? ""}
|
||||
onChange={(event) =>
|
||||
handleInlineUpdateFields(consumer, {
|
||||
cableCrossSection: event.target.value || undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Bitte wählen</option>
|
||||
{cableCrossSectionOptions.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
);
|
||||
case "comment":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
defaultValue={consumer.comment ?? ""}
|
||||
onBlur={(event) =>
|
||||
event.target.value !== (consumer.comment ?? "")
|
||||
? handleInlineUpdateFields(consumer, {
|
||||
comment: event.target.value || undefined,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
case "quantity":
|
||||
return (
|
||||
<td key={column.key}>
|
||||
@@ -1060,6 +1593,15 @@ export default function CircuitListsPage() {
|
||||
L{targetSlotIndex + 1}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
type="button"
|
||||
onClick={() => handleDuplicateInSameList(consumer)}
|
||||
disabled={isSaving}
|
||||
title="In derselben Liste duplizieren"
|
||||
>
|
||||
Dupl.
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
type="button"
|
||||
@@ -1119,3 +1661,5 @@ export default function CircuitListsPage() {
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
copyGlobalDeviceToProject,
|
||||
copyProjectDeviceToGlobal,
|
||||
createDistributionBoard,
|
||||
createFloor,
|
||||
createProjectDevice,
|
||||
@@ -11,6 +13,7 @@ import {
|
||||
deleteProjectDevice,
|
||||
listDistributionBoards,
|
||||
listFloors,
|
||||
listGlobalDevices,
|
||||
listProjectDevices,
|
||||
listProjects,
|
||||
listRooms,
|
||||
@@ -21,6 +24,7 @@ import type {
|
||||
CreateProjectDeviceInput,
|
||||
DistributionBoardDto,
|
||||
FloorDto,
|
||||
GlobalDeviceDto,
|
||||
ProjectDeviceDto,
|
||||
ProjectDto,
|
||||
RoomDto,
|
||||
@@ -28,6 +32,7 @@ import type {
|
||||
|
||||
const emptyProjectDevice: CreateProjectDeviceInput = {
|
||||
name: "",
|
||||
displayName: "",
|
||||
category: "",
|
||||
quantity: 1,
|
||||
installedPowerPerUnitKw: 0.1,
|
||||
@@ -54,6 +59,7 @@ export default function ProjectDetailPage() {
|
||||
const [floors, setFloors] = useState<FloorDto[]>([]);
|
||||
const [rooms, setRooms] = useState<RoomDto[]>([]);
|
||||
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
||||
const [globalDevices, setGlobalDevices] = useState<GlobalDeviceDto[]>([]);
|
||||
const [boardName, setBoardName] = useState("");
|
||||
const [floorName, setFloorName] = useState("");
|
||||
const [roomNumber, setRoomNumber] = useState("");
|
||||
@@ -63,6 +69,7 @@ export default function ProjectDetailPage() {
|
||||
const [threePhaseVoltageV, setThreePhaseVoltageV] = useState("400");
|
||||
const [projectDeviceForm, setProjectDeviceForm] = useState<Record<string, string>>({
|
||||
name: "",
|
||||
displayName: "",
|
||||
category: "",
|
||||
quantity: "1",
|
||||
installedPowerPerUnitKw: "0.1",
|
||||
@@ -72,6 +79,7 @@ export default function ProjectDetailPage() {
|
||||
powerFactor: "1",
|
||||
note: "",
|
||||
});
|
||||
const [selectedGlobalDeviceId, setSelectedGlobalDeviceId] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
@@ -89,8 +97,9 @@ export default function ProjectDetailPage() {
|
||||
listFloors(projectId),
|
||||
listRooms(projectId),
|
||||
listProjectDevices(projectId),
|
||||
listGlobalDevices(),
|
||||
])
|
||||
.then(([projects, distributionBoards, loadedFloors, loadedRooms, loadedProjectDevices]) => {
|
||||
.then(([projects, distributionBoards, loadedFloors, loadedRooms, loadedProjectDevices, loadedGlobalDevices]) => {
|
||||
const currentProject = projects.find((item) => item.id === projectId) ?? null;
|
||||
setProject(currentProject);
|
||||
if (currentProject) {
|
||||
@@ -101,6 +110,7 @@ export default function ProjectDetailPage() {
|
||||
setFloors(loadedFloors);
|
||||
setRooms(loadedRooms);
|
||||
setProjectDevices(loadedProjectDevices);
|
||||
setGlobalDevices(loadedGlobalDevices);
|
||||
setError(null);
|
||||
})
|
||||
.catch((err: unknown) =>
|
||||
@@ -201,6 +211,7 @@ export default function ProjectDetailPage() {
|
||||
|
||||
const payload: CreateProjectDeviceInput = {
|
||||
name: projectDeviceForm.name.trim(),
|
||||
displayName: projectDeviceForm.displayName.trim() || projectDeviceForm.name.trim(),
|
||||
category: projectDeviceForm.category.trim() || undefined,
|
||||
quantity: Number(projectDeviceForm.quantity),
|
||||
installedPowerPerUnitKw: Number(projectDeviceForm.installedPowerPerUnitKw),
|
||||
@@ -218,6 +229,7 @@ export default function ProjectDetailPage() {
|
||||
setProjectDevices((current) => [...current, created]);
|
||||
setProjectDeviceForm({
|
||||
name: emptyProjectDevice.name,
|
||||
displayName: emptyProjectDevice.displayName,
|
||||
category: emptyProjectDevice.category ?? "",
|
||||
quantity: String(emptyProjectDevice.quantity),
|
||||
installedPowerPerUnitKw: String(emptyProjectDevice.installedPowerPerUnitKw),
|
||||
@@ -252,7 +264,7 @@ export default function ProjectDetailPage() {
|
||||
|
||||
async function handleQuickUpdateProjectDevice(
|
||||
device: ProjectDeviceDto,
|
||||
key: "name" | "category",
|
||||
key: "name" | "displayName" | "category",
|
||||
value: string
|
||||
) {
|
||||
if (!projectId) {
|
||||
@@ -261,6 +273,7 @@ export default function ProjectDetailPage() {
|
||||
|
||||
const payload: CreateProjectDeviceInput = {
|
||||
name: key === "name" ? value : device.name,
|
||||
displayName: key === "displayName" ? value : device.displayName,
|
||||
category: key === "category" ? value : device.category ?? undefined,
|
||||
quantity: device.quantity,
|
||||
installedPowerPerUnitKw: device.installedPowerPerUnitKw,
|
||||
@@ -279,6 +292,40 @@ export default function ProjectDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopyGlobalToProject() {
|
||||
if (!projectId || !selectedGlobalDeviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const created = await copyGlobalDeviceToProject(projectId, selectedGlobalDeviceId);
|
||||
setProjectDevices((current) => [...current, created]);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Globales Gerät konnte nicht ins Projekt kopiert werden.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopyProjectToGlobal(projectDeviceId: string) {
|
||||
if (!projectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const created = await copyProjectDeviceToGlobal(projectId, projectDeviceId);
|
||||
setGlobalDevices((current) => [...current, created]);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Projektgerät konnte nicht global kopiert werden.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="container py-4">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
@@ -512,14 +559,24 @@ export default function ProjectDetailPage() {
|
||||
<div className="col-12 col-md-3">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Bezeichnung"
|
||||
placeholder="Interner Name"
|
||||
value={projectDeviceForm.name}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({ ...current, name: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-2">
|
||||
<div className="col-12 col-md-3">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Anzeigename"
|
||||
value={projectDeviceForm.displayName}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({ ...current, displayName: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-1">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Kategorie"
|
||||
@@ -540,7 +597,7 @@ export default function ProjectDetailPage() {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-2">
|
||||
<div className="col-6 col-md-1">
|
||||
<input
|
||||
className="form-control"
|
||||
type="number"
|
||||
@@ -586,17 +643,40 @@ export default function ProjectDetailPage() {
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="input-group mt-3">
|
||||
<select
|
||||
className="form-select"
|
||||
value={selectedGlobalDeviceId}
|
||||
onChange={(event) => setSelectedGlobalDeviceId(event.target.value)}
|
||||
>
|
||||
<option value="">Globales Gerät auswählen</option>
|
||||
{globalDevices.map((device) => (
|
||||
<option key={device.id} value={device.id}>
|
||||
{device.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
type="button"
|
||||
onClick={handleCopyGlobalToProject}
|
||||
disabled={!selectedGlobalDeviceId || isSaving}
|
||||
>
|
||||
Nach Projekt kopieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm table-striped align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bezeichnung</th>
|
||||
<th>Interner Name</th>
|
||||
<th>Anzeigename</th>
|
||||
<th>Kategorie</th>
|
||||
<th>Anzahl</th>
|
||||
<th>Leistung je Stück [kW]</th>
|
||||
<th>GZF</th>
|
||||
<th>Aktion</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -613,6 +693,17 @@ export default function ProjectDetailPage() {
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
defaultValue={device.displayName}
|
||||
onBlur={(event) =>
|
||||
event.target.value !== device.displayName
|
||||
? handleQuickUpdateProjectDevice(device, "displayName", event.target.value)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
@@ -628,19 +719,30 @@ export default function ProjectDetailPage() {
|
||||
<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>
|
||||
<div className="btn-group btn-group-sm">
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
type="button"
|
||||
onClick={() => handleCopyProjectToGlobal(device.id)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Nach global kopieren
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
type="button"
|
||||
onClick={() => handleDeleteProjectDevice(device.id)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!projectDevices.length ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center text-secondary py-4">
|
||||
<td colSpan={7} className="text-center text-secondary py-4">
|
||||
Noch keine Projektgeräte vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -658,3 +760,5 @@ export default function ProjectDetailPage() {
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
|
||||
const emptyGlobalDevice: CreateGlobalDeviceInput = {
|
||||
name: "",
|
||||
displayName: "",
|
||||
category: "",
|
||||
quantity: 1,
|
||||
installedPowerPerUnitKw: 0.1,
|
||||
@@ -42,6 +43,7 @@ export default function ProjectsPage() {
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const [globalDeviceForm, setGlobalDeviceForm] = useState<Record<string, string>>({
|
||||
name: "",
|
||||
displayName: "",
|
||||
category: "",
|
||||
quantity: "1",
|
||||
installedPowerPerUnitKw: "0.1",
|
||||
@@ -92,6 +94,7 @@ export default function ProjectsPage() {
|
||||
}
|
||||
const payload: CreateGlobalDeviceInput = {
|
||||
name: globalDeviceForm.name.trim(),
|
||||
displayName: globalDeviceForm.displayName.trim() || globalDeviceForm.name.trim(),
|
||||
category: globalDeviceForm.category.trim() || undefined,
|
||||
quantity: Number(globalDeviceForm.quantity),
|
||||
installedPowerPerUnitKw: Number(globalDeviceForm.installedPowerPerUnitKw),
|
||||
@@ -108,6 +111,7 @@ export default function ProjectsPage() {
|
||||
setGlobalDevices((current) => [...current, created]);
|
||||
setGlobalDeviceForm({
|
||||
name: emptyGlobalDevice.name,
|
||||
displayName: emptyGlobalDevice.displayName,
|
||||
category: emptyGlobalDevice.category ?? "",
|
||||
quantity: String(emptyGlobalDevice.quantity),
|
||||
installedPowerPerUnitKw: String(emptyGlobalDevice.installedPowerPerUnitKw),
|
||||
@@ -139,11 +143,12 @@ export default function ProjectsPage() {
|
||||
|
||||
async function handleQuickUpdateGlobalDevice(
|
||||
device: GlobalDeviceDto,
|
||||
key: "name" | "category",
|
||||
key: "name" | "displayName" | "category",
|
||||
value: string
|
||||
) {
|
||||
const payload: CreateGlobalDeviceInput = {
|
||||
name: key === "name" ? value : device.name,
|
||||
displayName: key === "displayName" ? value : device.displayName,
|
||||
category: key === "category" ? value : device.category ?? undefined,
|
||||
quantity: device.quantity,
|
||||
installedPowerPerUnitKw: device.installedPowerPerUnitKw,
|
||||
@@ -238,12 +243,20 @@ export default function ProjectsPage() {
|
||||
<div className="col-12 col-md-3">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Bezeichnung"
|
||||
placeholder="Interner Name"
|
||||
value={globalDeviceForm.name}
|
||||
onChange={(event) => setGlobalDeviceForm((current) => ({ ...current, name: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-2">
|
||||
<div className="col-12 col-md-3">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Anzeigename"
|
||||
value={globalDeviceForm.displayName}
|
||||
onChange={(event) => setGlobalDeviceForm((current) => ({ ...current, displayName: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-1">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Kategorie"
|
||||
@@ -264,7 +277,7 @@ export default function ProjectsPage() {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-2">
|
||||
<div className="col-6 col-md-1">
|
||||
<input
|
||||
className="form-control"
|
||||
type="number"
|
||||
@@ -315,7 +328,8 @@ export default function ProjectsPage() {
|
||||
<table className="table table-sm table-striped align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bezeichnung</th>
|
||||
<th>Interner Name</th>
|
||||
<th>Anzeigename</th>
|
||||
<th>Kategorie</th>
|
||||
<th>Anzahl</th>
|
||||
<th>Leistung je Stück [kW]</th>
|
||||
@@ -337,6 +351,17 @@ export default function ProjectsPage() {
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
defaultValue={device.displayName}
|
||||
onBlur={(event) =>
|
||||
event.target.value !== device.displayName
|
||||
? handleQuickUpdateGlobalDevice(device, "displayName", event.target.value)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
@@ -364,7 +389,7 @@ export default function ProjectsPage() {
|
||||
))}
|
||||
{!globalDevices.length ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center text-secondary py-4">
|
||||
<td colSpan={7} className="text-center text-secondary py-4">
|
||||
Noch keine globalen Geräte vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user