All first todos completed
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
## Project Context
|
## Project Context
|
||||||
|
|
||||||
This repository contains a web application for creating, editing, calculating, and documenting electrical power balances for building-services electrical planning.
|
This repository contains a web application for creating, editing, calculating, and documenting electrical power balances for building-services electrical planning.
|
||||||
|
|
||||||
The application is intended for small internal use by approximately 2–3 concurrent users. It should support practical planning workflows, not an over-engineered enterprise architecture.
|
The application is intended for small internal use by approximately 2–3 concurrent users. It should support practical planning workflows, not an over-engineered enterprise architecture.
|
||||||
@@ -447,19 +445,8 @@ Use proper German umlauts (�, �, �, �, �, �, �) in all new or chan
|
|||||||
- By default, the table should initially hide: power factor (cos phi), phase count, and current.
|
- By default, the table should initially hide: power factor (cos phi), phase count, and current.
|
||||||
- Users must be able to add any available attribute as a table column at any time, and must be able to reorder column positions.
|
- Users must be able to add any available attribute as a table column at any time, and must be able to reorder column positions.
|
||||||
|
|
||||||
|
## Encoding Rule
|
||||||
|
|
||||||
## Open TODOs from docs/electrical-load-balance-requirements-context-dump.md
|
- All text files must be saved as UTF-8.
|
||||||
|
- German UI text must never contain mojibake artifacts (for example geöffnet, wählen, Übernehmen, ←, →).
|
||||||
- [x] Extend `CircuitEntry` data model with missing fields from requirements, especially `circuitNumber`, `description`, `deviceType`, `phaseType`, `tradeOrCostGroup`, `group`, `protectionType`, `protectionRatedCurrent`, `protectionCharacteristic`, `cableType`, `cableCrossSection`, `cableLength` and `comment`.
|
- If such artifacts appear, they must be corrected immediately before merge or handoff.
|
||||||
- [x] Allow multiple entries with the same `circuitNumber` and make this visible/editable in the circuit-list table.
|
|
||||||
- [x] Implement project-specific device lists (`ProjectDeviceList`) in backend + UI.
|
|
||||||
- [ ] Implement copying devices both directions between global and project-specific device lists.
|
|
||||||
- [ ] Add separate device naming model with `Device.name` and `Device.displayName`.
|
|
||||||
- [ ] Add explicit entry description field (`CircuitEntry.description`) independent of linked device naming.
|
|
||||||
- [ ] Implement device-link lifecycle on entries: link, unlink/detach, and update propagation from device changes to linked entries.
|
|
||||||
- [ ] Add `addCount` when adding a device to a circuit list to create multiple entries in one action (`addCount != quantity`).
|
|
||||||
- [ ] Relax circuit-entry validation so incomplete entries are possible (currently several fields are required).
|
|
||||||
- [ ] Add duplicate-entry action within the same circuit list (separate from copy to another list).
|
|
||||||
- [ ] Add sorting/filtering/bulk-edit capabilities for circuit-list tables (beyond current copy-selection flow).
|
|
||||||
- [ ] Define and implement fixed selection lists for domain fields (`deviceType`, `phaseType`, `tradeOrCostGroup`, `group`, protection and cable fields).
|
|
||||||
- [ ] Extend tests beyond pure power formulas to cover new circuit-entry/device-link behaviors once implemented.
|
|
||||||
|
|||||||
+2
-2
@@ -11,8 +11,8 @@
|
|||||||
"build:api": "tsc -p tsconfig.json",
|
"build:api": "tsc -p tsconfig.json",
|
||||||
"build:web": "next build",
|
"build:web": "next build",
|
||||||
"start": "node dist/server/index.js",
|
"start": "node dist/server/index.js",
|
||||||
"test": "tsx --test tests/power-calculation.test.ts",
|
"test": "tsx --test tests/power-calculation.test.ts tests/consumer-linking.service.test.ts tests/consumer-schema-options.test.ts",
|
||||||
"test:watch": "tsx --watch --test tests/power-calculation.test.ts",
|
"test:watch": "tsx --watch --test tests/power-calculation.test.ts tests/consumer-linking.service.test.ts tests/consumer-schema-options.test.ts",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate"
|
"db:migrate": "drizzle-kit migrate"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams, useSearchParams } from "next/navigation";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
listProjectDevices,
|
listProjectDevices,
|
||||||
listRooms,
|
listRooms,
|
||||||
updateConsumer,
|
updateConsumer,
|
||||||
updateProjectSettings,
|
|
||||||
} from "../../../../frontend/utils/api";
|
} from "../../../../frontend/utils/api";
|
||||||
import type {
|
import type {
|
||||||
ConsumerWithCalculatedValues,
|
ConsumerWithCalculatedValues,
|
||||||
@@ -22,6 +21,16 @@ import type {
|
|||||||
ProjectDto,
|
ProjectDto,
|
||||||
RoomDto,
|
RoomDto,
|
||||||
} from "../../../../frontend/types";
|
} from "../../../../frontend/types";
|
||||||
|
import {
|
||||||
|
cableCrossSectionOptions,
|
||||||
|
cableTypeOptions,
|
||||||
|
consumerGroupOptions,
|
||||||
|
deviceTypeOptions,
|
||||||
|
phaseTypeOptions,
|
||||||
|
protectionCharacteristicOptions,
|
||||||
|
protectionTypeOptions,
|
||||||
|
tradeOrCostGroupOptions,
|
||||||
|
} from "../../../../shared/constants/consumer-option-lists";
|
||||||
|
|
||||||
interface SlotState {
|
interface SlotState {
|
||||||
boardId: string;
|
boardId: string;
|
||||||
@@ -35,6 +44,22 @@ interface QuickCreateFormState {
|
|||||||
installedPowerPerUnitKw: string;
|
installedPowerPerUnitKw: string;
|
||||||
demandFactor: string;
|
demandFactor: string;
|
||||||
totalPowerKw: 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 =
|
type ColumnKey =
|
||||||
@@ -42,9 +67,21 @@ type ColumnKey =
|
|||||||
| "circuitNumber"
|
| "circuitNumber"
|
||||||
| "description"
|
| "description"
|
||||||
| "name"
|
| "name"
|
||||||
|
| "projectDevice"
|
||||||
|
| "deviceLink"
|
||||||
| "room"
|
| "room"
|
||||||
| "floor"
|
| "floor"
|
||||||
| "category"
|
| "category"
|
||||||
|
| "deviceType"
|
||||||
|
| "phaseType"
|
||||||
|
| "tradeOrCostGroup"
|
||||||
|
| "group"
|
||||||
|
| "protectionType"
|
||||||
|
| "protectionRatedCurrent"
|
||||||
|
| "protectionCharacteristic"
|
||||||
|
| "cableType"
|
||||||
|
| "cableCrossSection"
|
||||||
|
| "comment"
|
||||||
| "quantity"
|
| "quantity"
|
||||||
| "installedPowerPerUnitKw"
|
| "installedPowerPerUnitKw"
|
||||||
| "installedPowerKw"
|
| "installedPowerKw"
|
||||||
@@ -70,6 +107,19 @@ const defaultCreateForm: QuickCreateFormState = {
|
|||||||
installedPowerPerUnitKw: "0.10",
|
installedPowerPerUnitKw: "0.10",
|
||||||
demandFactor: "1.00",
|
demandFactor: "1.00",
|
||||||
totalPowerKw: "0.10",
|
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 }> = [
|
const allColumns: Array<{ key: ColumnKey; label: string }> = [
|
||||||
@@ -77,9 +127,21 @@ const allColumns: Array<{ key: ColumnKey; label: string }> = [
|
|||||||
{ key: "circuitNumber", label: "Stromkreis-Nr." },
|
{ key: "circuitNumber", label: "Stromkreis-Nr." },
|
||||||
{ key: "description", label: "Beschreibung" },
|
{ key: "description", label: "Beschreibung" },
|
||||||
{ key: "name", label: "Verbraucher" },
|
{ key: "name", label: "Verbraucher" },
|
||||||
|
{ key: "projectDevice", label: "Projektgerät" },
|
||||||
|
{ key: "deviceLink", label: "Link" },
|
||||||
{ key: "room", label: "Raum" },
|
{ key: "room", label: "Raum" },
|
||||||
{ key: "floor", label: "Etage" },
|
{ key: "floor", label: "Etage" },
|
||||||
{ key: "category", label: "Kategorie" },
|
{ 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: "quantity", label: "Anzahl" },
|
||||||
{ key: "installedPowerPerUnitKw", label: "Einzelleistung [kW]" },
|
{ key: "installedPowerPerUnitKw", label: "Einzelleistung [kW]" },
|
||||||
{ key: "installedPowerKw", label: "Installierte Leistung [kW]" },
|
{ key: "installedPowerKw", label: "Installierte Leistung [kW]" },
|
||||||
@@ -87,7 +149,7 @@ const allColumns: Array<{ key: ColumnKey; label: string }> = [
|
|||||||
{ key: "demandPowerKw", label: "Gesamtleistung [kW]" },
|
{ key: "demandPowerKw", label: "Gesamtleistung [kW]" },
|
||||||
{ key: "voltageV", label: "Spannung [V]" },
|
{ key: "voltageV", label: "Spannung [V]" },
|
||||||
{ key: "phaseCount", label: "Phasen" },
|
{ key: "phaseCount", label: "Phasen" },
|
||||||
{ key: "powerFactor", label: "cos φ" },
|
{ key: "powerFactor", label: "cos f" },
|
||||||
{ key: "currentA", label: "Strom [A]" },
|
{ key: "currentA", label: "Strom [A]" },
|
||||||
{ key: "note", label: "Bemerkung" },
|
{ key: "note", label: "Bemerkung" },
|
||||||
{ key: "actions", label: "Aktionen" },
|
{ key: "actions", label: "Aktionen" },
|
||||||
@@ -98,6 +160,8 @@ const defaultVisibleColumns: ColumnKey[] = [
|
|||||||
"circuitNumber",
|
"circuitNumber",
|
||||||
"description",
|
"description",
|
||||||
"name",
|
"name",
|
||||||
|
"projectDevice",
|
||||||
|
"deviceLink",
|
||||||
"room",
|
"room",
|
||||||
"quantity",
|
"quantity",
|
||||||
"installedPowerPerUnitKw",
|
"installedPowerPerUnitKw",
|
||||||
@@ -139,8 +203,16 @@ export default function CircuitListsPage() {
|
|||||||
2: { ...defaultCreateForm },
|
2: { ...defaultCreateForm },
|
||||||
});
|
});
|
||||||
const [visibleColumns, setVisibleColumns] = useState<ColumnKey[]>(defaultVisibleColumns);
|
const [visibleColumns, setVisibleColumns] = useState<ColumnKey[]>(defaultVisibleColumns);
|
||||||
const [singlePhaseVoltageV, setSinglePhaseVoltageV] = useState("230");
|
const [listFilters, setListFilters] = useState<Record<number, ListFilterState>>({
|
||||||
const [threePhaseVoltageV, setThreePhaseVoltageV] = useState("400");
|
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 [showColumnManager, setShowColumnManager] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
@@ -163,8 +235,6 @@ export default function CircuitListsPage() {
|
|||||||
.then(([loadedProject, distributionBoards, loadedRooms, loadedProjectDevices, loadedConsumers]) => {
|
.then(([loadedProject, distributionBoards, loadedRooms, loadedProjectDevices, loadedConsumers]) => {
|
||||||
const initialBoardId = searchParams.get("boardId") ?? distributionBoards[0]?.id ?? "";
|
const initialBoardId = searchParams.get("boardId") ?? distributionBoards[0]?.id ?? "";
|
||||||
setProject(loadedProject);
|
setProject(loadedProject);
|
||||||
setSinglePhaseVoltageV(String(loadedProject.singlePhaseVoltageV));
|
|
||||||
setThreePhaseVoltageV(String(loadedProject.threePhaseVoltageV));
|
|
||||||
setBoards(distributionBoards);
|
setBoards(distributionBoards);
|
||||||
setRooms(loadedRooms);
|
setRooms(loadedRooms);
|
||||||
setProjectDevices(loadedProjectDevices);
|
setProjectDevices(loadedProjectDevices);
|
||||||
@@ -190,6 +260,39 @@ export default function CircuitListsPage() {
|
|||||||
return consumers.filter((item) => item.distributionBoardId === boardId);
|
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) {
|
function setSlotBoard(slotIndex: number, boardId: string) {
|
||||||
setSlots((current) =>
|
setSlots((current) =>
|
||||||
current.map((slot, index) => (index === slotIndex ? { boardId, selectedConsumerIds: [] } : slot))
|
current.map((slot, index) => (index === slotIndex ? { boardId, selectedConsumerIds: [] } : slot))
|
||||||
@@ -266,26 +369,6 @@ export default function CircuitListsPage() {
|
|||||||
setConsumers(await listConsumers(projectId));
|
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>) {
|
async function handleCreateManualConsumer(slotIndex: number, event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const slot = slots[slotIndex];
|
const slot = slots[slotIndex];
|
||||||
@@ -298,6 +381,7 @@ export default function CircuitListsPage() {
|
|||||||
const payload: CreateConsumerInput = {
|
const payload: CreateConsumerInput = {
|
||||||
projectId,
|
projectId,
|
||||||
distributionBoardId: slot.boardId,
|
distributionBoardId: slot.boardId,
|
||||||
|
isLinkedToDevice: false,
|
||||||
roomId: form.roomId || undefined,
|
roomId: form.roomId || undefined,
|
||||||
description: name,
|
description: name,
|
||||||
name,
|
name,
|
||||||
@@ -332,13 +416,17 @@ export default function CircuitListsPage() {
|
|||||||
if (!projectId || !slot.boardId || !projectDevice) {
|
if (!projectId || !slot.boardId || !projectDevice) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const addCountRaw = Number(form.addCount);
|
||||||
|
const addCount = Number.isFinite(addCountRaw) ? Math.max(1, Math.floor(addCountRaw)) : 1;
|
||||||
|
|
||||||
const payload: CreateConsumerInput = {
|
const payload: CreateConsumerInput = {
|
||||||
projectId,
|
projectId,
|
||||||
distributionBoardId: slot.boardId,
|
distributionBoardId: slot.boardId,
|
||||||
|
projectDeviceId: projectDevice.id,
|
||||||
|
isLinkedToDevice: true,
|
||||||
roomId: form.roomId || undefined,
|
roomId: form.roomId || undefined,
|
||||||
description: projectDevice.name,
|
description: projectDevice.displayName,
|
||||||
name: projectDevice.name,
|
name: projectDevice.displayName,
|
||||||
category: projectDevice.category ?? undefined,
|
category: projectDevice.category ?? undefined,
|
||||||
quantity: projectDevice.quantity,
|
quantity: projectDevice.quantity,
|
||||||
installedPowerPerUnitKw: projectDevice.installedPowerPerUnitKw,
|
installedPowerPerUnitKw: projectDevice.installedPowerPerUnitKw,
|
||||||
@@ -351,7 +439,7 @@ export default function CircuitListsPage() {
|
|||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await createConsumer(payload);
|
await Promise.all(Array.from({ length: addCount }, () => createConsumer(payload)));
|
||||||
await reloadConsumers();
|
await reloadConsumers();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Verbraucher konnte nicht aus Projektgerät erstellt werden.");
|
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 = {
|
const payload: CreateConsumerInput = {
|
||||||
projectId,
|
projectId,
|
||||||
distributionBoardId: targetBoardId,
|
distributionBoardId: targetBoardId,
|
||||||
|
projectDeviceId: sourceConsumer.projectDeviceId ?? undefined,
|
||||||
|
isLinkedToDevice: sourceConsumer.isLinkedToDevice,
|
||||||
circuitNumber: sourceConsumer.circuitNumber,
|
circuitNumber: sourceConsumer.circuitNumber,
|
||||||
description: sourceConsumer.description,
|
description: sourceConsumer.description,
|
||||||
roomId: sourceConsumer.roomId ?? undefined,
|
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) {
|
async function handleCopySelectionToSlot(sourceSlotIndex: number, targetSlotIndex: number) {
|
||||||
const sourceSlot = slots[sourceSlotIndex];
|
const sourceSlot = slots[sourceSlotIndex];
|
||||||
const targetBoardId = slots[targetSlotIndex].boardId;
|
const targetBoardId = slots[targetSlotIndex].boardId;
|
||||||
@@ -425,6 +561,8 @@ export default function CircuitListsPage() {
|
|||||||
createConsumer({
|
createConsumer({
|
||||||
projectId,
|
projectId,
|
||||||
distributionBoardId: targetBoardId,
|
distributionBoardId: targetBoardId,
|
||||||
|
projectDeviceId: sourceConsumer.projectDeviceId ?? undefined,
|
||||||
|
isLinkedToDevice: sourceConsumer.isLinkedToDevice,
|
||||||
circuitNumber: sourceConsumer.circuitNumber,
|
circuitNumber: sourceConsumer.circuitNumber,
|
||||||
description: sourceConsumer.description,
|
description: sourceConsumer.description,
|
||||||
roomId: sourceConsumer.roomId ?? undefined,
|
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(
|
async function handleInlineUpdateFields(
|
||||||
consumer: ConsumerWithCalculatedValues,
|
consumer: ConsumerWithCalculatedValues,
|
||||||
patch: Partial<CreateConsumerInput>
|
patch: Partial<CreateConsumerInput>
|
||||||
@@ -472,6 +663,10 @@ export default function CircuitListsPage() {
|
|||||||
? patch.distributionBoardId
|
? patch.distributionBoardId
|
||||||
: consumer.distributionBoardId ?? undefined,
|
: consumer.distributionBoardId ?? undefined,
|
||||||
circuitListId: patch.circuitListId !== undefined ? patch.circuitListId : consumer.circuitListId ?? 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,
|
circuitNumber: patch.circuitNumber !== undefined ? patch.circuitNumber : consumer.circuitNumber,
|
||||||
description: patch.description !== undefined ? patch.description : consumer.description,
|
description: patch.description !== undefined ? patch.description : consumer.description,
|
||||||
roomId: patch.roomId !== undefined ? patch.roomId : consumer.roomId ?? undefined,
|
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}
|
{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 ? (
|
{showColumnManager ? (
|
||||||
<section className="card shadow-sm mb-3">
|
<section className="card shadow-sm mb-3">
|
||||||
<div className="card-header">Spaltenauswahl und Reihenfolge</div>
|
<div className="card-header">Spaltenauswahl und Reihenfolge</div>
|
||||||
@@ -677,7 +830,7 @@ export default function CircuitListsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<small className="text-secondary d-block mt-2">
|
<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>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -685,7 +838,38 @@ export default function CircuitListsPage() {
|
|||||||
|
|
||||||
<div className="row g-3">
|
<div className="row g-3">
|
||||||
{slots.slice(0, activeListCount).map((slot, slotIndex) => {
|
{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];
|
const quickForm = quickCreateForms[slotIndex];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -797,10 +981,20 @@ export default function CircuitListsPage() {
|
|||||||
<option value="">Projektgerät wählen</option>
|
<option value="">Projektgerät wählen</option>
|
||||||
{projectDevices.map((item) => (
|
{projectDevices.map((item) => (
|
||||||
<option key={item.id} value={item.id}>
|
<option key={item.id} value={item.id}>
|
||||||
{item.name}
|
{item.displayName}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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
|
<button
|
||||||
className="btn btn-outline-primary"
|
className="btn btn-outline-primary"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -812,6 +1006,95 @@ export default function CircuitListsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="table-responsive border rounded">
|
<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">
|
<table className="table table-sm align-middle mb-0">
|
||||||
<thead className="table-light">
|
<thead className="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -882,6 +1165,45 @@ export default function CircuitListsPage() {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</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":
|
case "room":
|
||||||
return (
|
return (
|
||||||
<td key={column.key}>
|
<td key={column.key}>
|
||||||
@@ -921,6 +1243,217 @@ export default function CircuitListsPage() {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</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":
|
case "quantity":
|
||||||
return (
|
return (
|
||||||
<td key={column.key}>
|
<td key={column.key}>
|
||||||
@@ -1060,6 +1593,15 @@ export default function CircuitListsPage() {
|
|||||||
L{targetSlotIndex + 1}
|
L{targetSlotIndex + 1}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleDuplicateInSameList(consumer)}
|
||||||
|
disabled={isSaving}
|
||||||
|
title="In derselben Liste duplizieren"
|
||||||
|
>
|
||||||
|
Dupl.
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline-danger"
|
className="btn btn-outline-danger"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1119,3 +1661,5 @@ export default function CircuitListsPage() {
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
copyGlobalDeviceToProject,
|
||||||
|
copyProjectDeviceToGlobal,
|
||||||
createDistributionBoard,
|
createDistributionBoard,
|
||||||
createFloor,
|
createFloor,
|
||||||
createProjectDevice,
|
createProjectDevice,
|
||||||
@@ -11,6 +13,7 @@ import {
|
|||||||
deleteProjectDevice,
|
deleteProjectDevice,
|
||||||
listDistributionBoards,
|
listDistributionBoards,
|
||||||
listFloors,
|
listFloors,
|
||||||
|
listGlobalDevices,
|
||||||
listProjectDevices,
|
listProjectDevices,
|
||||||
listProjects,
|
listProjects,
|
||||||
listRooms,
|
listRooms,
|
||||||
@@ -21,6 +24,7 @@ import type {
|
|||||||
CreateProjectDeviceInput,
|
CreateProjectDeviceInput,
|
||||||
DistributionBoardDto,
|
DistributionBoardDto,
|
||||||
FloorDto,
|
FloorDto,
|
||||||
|
GlobalDeviceDto,
|
||||||
ProjectDeviceDto,
|
ProjectDeviceDto,
|
||||||
ProjectDto,
|
ProjectDto,
|
||||||
RoomDto,
|
RoomDto,
|
||||||
@@ -28,6 +32,7 @@ import type {
|
|||||||
|
|
||||||
const emptyProjectDevice: CreateProjectDeviceInput = {
|
const emptyProjectDevice: CreateProjectDeviceInput = {
|
||||||
name: "",
|
name: "",
|
||||||
|
displayName: "",
|
||||||
category: "",
|
category: "",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
installedPowerPerUnitKw: 0.1,
|
installedPowerPerUnitKw: 0.1,
|
||||||
@@ -54,6 +59,7 @@ export default function ProjectDetailPage() {
|
|||||||
const [floors, setFloors] = useState<FloorDto[]>([]);
|
const [floors, setFloors] = useState<FloorDto[]>([]);
|
||||||
const [rooms, setRooms] = useState<RoomDto[]>([]);
|
const [rooms, setRooms] = useState<RoomDto[]>([]);
|
||||||
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
||||||
|
const [globalDevices, setGlobalDevices] = useState<GlobalDeviceDto[]>([]);
|
||||||
const [boardName, setBoardName] = useState("");
|
const [boardName, setBoardName] = useState("");
|
||||||
const [floorName, setFloorName] = useState("");
|
const [floorName, setFloorName] = useState("");
|
||||||
const [roomNumber, setRoomNumber] = useState("");
|
const [roomNumber, setRoomNumber] = useState("");
|
||||||
@@ -63,6 +69,7 @@ export default function ProjectDetailPage() {
|
|||||||
const [threePhaseVoltageV, setThreePhaseVoltageV] = useState("400");
|
const [threePhaseVoltageV, setThreePhaseVoltageV] = useState("400");
|
||||||
const [projectDeviceForm, setProjectDeviceForm] = useState<Record<string, string>>({
|
const [projectDeviceForm, setProjectDeviceForm] = useState<Record<string, string>>({
|
||||||
name: "",
|
name: "",
|
||||||
|
displayName: "",
|
||||||
category: "",
|
category: "",
|
||||||
quantity: "1",
|
quantity: "1",
|
||||||
installedPowerPerUnitKw: "0.1",
|
installedPowerPerUnitKw: "0.1",
|
||||||
@@ -72,6 +79,7 @@ export default function ProjectDetailPage() {
|
|||||||
powerFactor: "1",
|
powerFactor: "1",
|
||||||
note: "",
|
note: "",
|
||||||
});
|
});
|
||||||
|
const [selectedGlobalDeviceId, setSelectedGlobalDeviceId] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
@@ -89,8 +97,9 @@ export default function ProjectDetailPage() {
|
|||||||
listFloors(projectId),
|
listFloors(projectId),
|
||||||
listRooms(projectId),
|
listRooms(projectId),
|
||||||
listProjectDevices(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;
|
const currentProject = projects.find((item) => item.id === projectId) ?? null;
|
||||||
setProject(currentProject);
|
setProject(currentProject);
|
||||||
if (currentProject) {
|
if (currentProject) {
|
||||||
@@ -101,6 +110,7 @@ export default function ProjectDetailPage() {
|
|||||||
setFloors(loadedFloors);
|
setFloors(loadedFloors);
|
||||||
setRooms(loadedRooms);
|
setRooms(loadedRooms);
|
||||||
setProjectDevices(loadedProjectDevices);
|
setProjectDevices(loadedProjectDevices);
|
||||||
|
setGlobalDevices(loadedGlobalDevices);
|
||||||
setError(null);
|
setError(null);
|
||||||
})
|
})
|
||||||
.catch((err: unknown) =>
|
.catch((err: unknown) =>
|
||||||
@@ -201,6 +211,7 @@ export default function ProjectDetailPage() {
|
|||||||
|
|
||||||
const payload: CreateProjectDeviceInput = {
|
const payload: CreateProjectDeviceInput = {
|
||||||
name: projectDeviceForm.name.trim(),
|
name: projectDeviceForm.name.trim(),
|
||||||
|
displayName: projectDeviceForm.displayName.trim() || projectDeviceForm.name.trim(),
|
||||||
category: projectDeviceForm.category.trim() || undefined,
|
category: projectDeviceForm.category.trim() || undefined,
|
||||||
quantity: Number(projectDeviceForm.quantity),
|
quantity: Number(projectDeviceForm.quantity),
|
||||||
installedPowerPerUnitKw: Number(projectDeviceForm.installedPowerPerUnitKw),
|
installedPowerPerUnitKw: Number(projectDeviceForm.installedPowerPerUnitKw),
|
||||||
@@ -218,6 +229,7 @@ export default function ProjectDetailPage() {
|
|||||||
setProjectDevices((current) => [...current, created]);
|
setProjectDevices((current) => [...current, created]);
|
||||||
setProjectDeviceForm({
|
setProjectDeviceForm({
|
||||||
name: emptyProjectDevice.name,
|
name: emptyProjectDevice.name,
|
||||||
|
displayName: emptyProjectDevice.displayName,
|
||||||
category: emptyProjectDevice.category ?? "",
|
category: emptyProjectDevice.category ?? "",
|
||||||
quantity: String(emptyProjectDevice.quantity),
|
quantity: String(emptyProjectDevice.quantity),
|
||||||
installedPowerPerUnitKw: String(emptyProjectDevice.installedPowerPerUnitKw),
|
installedPowerPerUnitKw: String(emptyProjectDevice.installedPowerPerUnitKw),
|
||||||
@@ -252,7 +264,7 @@ export default function ProjectDetailPage() {
|
|||||||
|
|
||||||
async function handleQuickUpdateProjectDevice(
|
async function handleQuickUpdateProjectDevice(
|
||||||
device: ProjectDeviceDto,
|
device: ProjectDeviceDto,
|
||||||
key: "name" | "category",
|
key: "name" | "displayName" | "category",
|
||||||
value: string
|
value: string
|
||||||
) {
|
) {
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
@@ -261,6 +273,7 @@ export default function ProjectDetailPage() {
|
|||||||
|
|
||||||
const payload: CreateProjectDeviceInput = {
|
const payload: CreateProjectDeviceInput = {
|
||||||
name: key === "name" ? value : device.name,
|
name: key === "name" ? value : device.name,
|
||||||
|
displayName: key === "displayName" ? value : device.displayName,
|
||||||
category: key === "category" ? value : device.category ?? undefined,
|
category: key === "category" ? value : device.category ?? undefined,
|
||||||
quantity: device.quantity,
|
quantity: device.quantity,
|
||||||
installedPowerPerUnitKw: device.installedPowerPerUnitKw,
|
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 (
|
return (
|
||||||
<main className="container py-4">
|
<main className="container py-4">
|
||||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
<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">
|
<div className="col-12 col-md-3">
|
||||||
<input
|
<input
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder="Bezeichnung"
|
placeholder="Interner Name"
|
||||||
value={projectDeviceForm.name}
|
value={projectDeviceForm.name}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setProjectDeviceForm((current) => ({ ...current, name: event.target.value }))
|
setProjectDeviceForm((current) => ({ ...current, name: event.target.value }))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<input
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder="Kategorie"
|
placeholder="Kategorie"
|
||||||
@@ -540,7 +597,7 @@ export default function ProjectDetailPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-6 col-md-2">
|
<div className="col-6 col-md-1">
|
||||||
<input
|
<input
|
||||||
className="form-control"
|
className="form-control"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -586,17 +643,40 @@ export default function ProjectDetailPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<table className="table table-sm table-striped align-middle mb-0">
|
<table className="table table-sm table-striped align-middle mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Bezeichnung</th>
|
<th>Interner Name</th>
|
||||||
|
<th>Anzeigename</th>
|
||||||
<th>Kategorie</th>
|
<th>Kategorie</th>
|
||||||
<th>Anzahl</th>
|
<th>Anzahl</th>
|
||||||
<th>Leistung je Stück [kW]</th>
|
<th>Leistung je Stück [kW]</th>
|
||||||
<th>GZF</th>
|
<th>GZF</th>
|
||||||
<th>Aktion</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -613,6 +693,17 @@ export default function ProjectDetailPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</td>
|
</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>
|
<td>
|
||||||
<input
|
<input
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
@@ -628,19 +719,30 @@ export default function ProjectDetailPage() {
|
|||||||
<td>{device.installedPowerPerUnitKw}</td>
|
<td>{device.installedPowerPerUnitKw}</td>
|
||||||
<td>{device.demandFactor}</td>
|
<td>{device.demandFactor}</td>
|
||||||
<td>
|
<td>
|
||||||
|
<div className="btn-group btn-group-sm">
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-outline-danger"
|
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"
|
type="button"
|
||||||
onClick={() => handleDeleteProjectDevice(device.id)}
|
onClick={() => handleDeleteProjectDevice(device.id)}
|
||||||
|
disabled={isSaving}
|
||||||
>
|
>
|
||||||
Löschen
|
Löschen
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{!projectDevices.length ? (
|
{!projectDevices.length ? (
|
||||||
<tr>
|
<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.
|
Noch keine Projektgeräte vorhanden.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -658,3 +760,5 @@ export default function ProjectDetailPage() {
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
|
|
||||||
const emptyGlobalDevice: CreateGlobalDeviceInput = {
|
const emptyGlobalDevice: CreateGlobalDeviceInput = {
|
||||||
name: "",
|
name: "",
|
||||||
|
displayName: "",
|
||||||
category: "",
|
category: "",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
installedPowerPerUnitKw: 0.1,
|
installedPowerPerUnitKw: 0.1,
|
||||||
@@ -42,6 +43,7 @@ export default function ProjectsPage() {
|
|||||||
const [projectName, setProjectName] = useState("");
|
const [projectName, setProjectName] = useState("");
|
||||||
const [globalDeviceForm, setGlobalDeviceForm] = useState<Record<string, string>>({
|
const [globalDeviceForm, setGlobalDeviceForm] = useState<Record<string, string>>({
|
||||||
name: "",
|
name: "",
|
||||||
|
displayName: "",
|
||||||
category: "",
|
category: "",
|
||||||
quantity: "1",
|
quantity: "1",
|
||||||
installedPowerPerUnitKw: "0.1",
|
installedPowerPerUnitKw: "0.1",
|
||||||
@@ -92,6 +94,7 @@ export default function ProjectsPage() {
|
|||||||
}
|
}
|
||||||
const payload: CreateGlobalDeviceInput = {
|
const payload: CreateGlobalDeviceInput = {
|
||||||
name: globalDeviceForm.name.trim(),
|
name: globalDeviceForm.name.trim(),
|
||||||
|
displayName: globalDeviceForm.displayName.trim() || globalDeviceForm.name.trim(),
|
||||||
category: globalDeviceForm.category.trim() || undefined,
|
category: globalDeviceForm.category.trim() || undefined,
|
||||||
quantity: Number(globalDeviceForm.quantity),
|
quantity: Number(globalDeviceForm.quantity),
|
||||||
installedPowerPerUnitKw: Number(globalDeviceForm.installedPowerPerUnitKw),
|
installedPowerPerUnitKw: Number(globalDeviceForm.installedPowerPerUnitKw),
|
||||||
@@ -108,6 +111,7 @@ export default function ProjectsPage() {
|
|||||||
setGlobalDevices((current) => [...current, created]);
|
setGlobalDevices((current) => [...current, created]);
|
||||||
setGlobalDeviceForm({
|
setGlobalDeviceForm({
|
||||||
name: emptyGlobalDevice.name,
|
name: emptyGlobalDevice.name,
|
||||||
|
displayName: emptyGlobalDevice.displayName,
|
||||||
category: emptyGlobalDevice.category ?? "",
|
category: emptyGlobalDevice.category ?? "",
|
||||||
quantity: String(emptyGlobalDevice.quantity),
|
quantity: String(emptyGlobalDevice.quantity),
|
||||||
installedPowerPerUnitKw: String(emptyGlobalDevice.installedPowerPerUnitKw),
|
installedPowerPerUnitKw: String(emptyGlobalDevice.installedPowerPerUnitKw),
|
||||||
@@ -139,11 +143,12 @@ export default function ProjectsPage() {
|
|||||||
|
|
||||||
async function handleQuickUpdateGlobalDevice(
|
async function handleQuickUpdateGlobalDevice(
|
||||||
device: GlobalDeviceDto,
|
device: GlobalDeviceDto,
|
||||||
key: "name" | "category",
|
key: "name" | "displayName" | "category",
|
||||||
value: string
|
value: string
|
||||||
) {
|
) {
|
||||||
const payload: CreateGlobalDeviceInput = {
|
const payload: CreateGlobalDeviceInput = {
|
||||||
name: key === "name" ? value : device.name,
|
name: key === "name" ? value : device.name,
|
||||||
|
displayName: key === "displayName" ? value : device.displayName,
|
||||||
category: key === "category" ? value : device.category ?? undefined,
|
category: key === "category" ? value : device.category ?? undefined,
|
||||||
quantity: device.quantity,
|
quantity: device.quantity,
|
||||||
installedPowerPerUnitKw: device.installedPowerPerUnitKw,
|
installedPowerPerUnitKw: device.installedPowerPerUnitKw,
|
||||||
@@ -238,12 +243,20 @@ export default function ProjectsPage() {
|
|||||||
<div className="col-12 col-md-3">
|
<div className="col-12 col-md-3">
|
||||||
<input
|
<input
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder="Bezeichnung"
|
placeholder="Interner Name"
|
||||||
value={globalDeviceForm.name}
|
value={globalDeviceForm.name}
|
||||||
onChange={(event) => setGlobalDeviceForm((current) => ({ ...current, name: event.target.value }))}
|
onChange={(event) => setGlobalDeviceForm((current) => ({ ...current, name: event.target.value }))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<input
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder="Kategorie"
|
placeholder="Kategorie"
|
||||||
@@ -264,7 +277,7 @@ export default function ProjectsPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-6 col-md-2">
|
<div className="col-6 col-md-1">
|
||||||
<input
|
<input
|
||||||
className="form-control"
|
className="form-control"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -315,7 +328,8 @@ export default function ProjectsPage() {
|
|||||||
<table className="table table-sm table-striped align-middle mb-0">
|
<table className="table table-sm table-striped align-middle mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Bezeichnung</th>
|
<th>Interner Name</th>
|
||||||
|
<th>Anzeigename</th>
|
||||||
<th>Kategorie</th>
|
<th>Kategorie</th>
|
||||||
<th>Anzahl</th>
|
<th>Anzahl</th>
|
||||||
<th>Leistung je Stück [kW]</th>
|
<th>Leistung je Stück [kW]</th>
|
||||||
@@ -337,6 +351,17 @@ export default function ProjectsPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</td>
|
</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>
|
<td>
|
||||||
<input
|
<input
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
@@ -364,7 +389,7 @@ export default function ProjectsPage() {
|
|||||||
))}
|
))}
|
||||||
{!globalDevices.length ? (
|
{!globalDevices.length ? (
|
||||||
<tr>
|
<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.
|
Noch keine globalen Geräte vorhanden.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE `global_devices` ADD COLUMN `display_name` text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE `global_devices` SET `display_name` = `name` WHERE `display_name` IS NULL;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `project_devices` ADD COLUMN `display_name` text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE `project_devices` SET `display_name` = `name` WHERE `display_name` IS NULL;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE `consumers` ADD COLUMN `project_device_id` text REFERENCES `project_devices`(`id`) ON UPDATE no action ON DELETE set null;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `consumers` ADD COLUMN `is_linked_to_device` integer NOT NULL DEFAULT 0;
|
||||||
@@ -43,6 +43,20 @@
|
|||||||
"when": 1777597000000,
|
"when": 1777597000000,
|
||||||
"tag": "0005_project_devices",
|
"tag": "0005_project_devices",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1777652000000,
|
||||||
|
"tag": "0006_device_display_name",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1777680000000,
|
||||||
|
"tag": "0007_consumer_device_link",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { db } from "../client.js";
|
import { db } from "../client.js";
|
||||||
import { consumers } from "../schema/consumers.js";
|
import { consumers } from "../schema/consumers.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -14,15 +14,21 @@ export class ConsumerRepository {
|
|||||||
|
|
||||||
async create(input: CreateConsumerInput) {
|
async create(input: CreateConsumerInput) {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
|
const normalizedName = input.name?.trim() || "Unbenannter Eintrag";
|
||||||
|
const normalizedQuantity = input.quantity ?? 0;
|
||||||
|
const normalizedInstalledPowerPerUnitKw = input.installedPowerPerUnitKw ?? 0;
|
||||||
|
const normalizedDemandFactor = input.demandFactor ?? 1;
|
||||||
await db.insert(consumers).values({
|
await db.insert(consumers).values({
|
||||||
id,
|
id,
|
||||||
projectId: input.projectId,
|
projectId: input.projectId,
|
||||||
distributionBoardId: input.distributionBoardId ?? null,
|
distributionBoardId: input.distributionBoardId ?? null,
|
||||||
circuitListId: input.circuitListId ?? null,
|
circuitListId: input.circuitListId ?? null,
|
||||||
|
projectDeviceId: input.projectDeviceId ?? null,
|
||||||
|
isLinkedToDevice: input.isLinkedToDevice ? 1 : 0,
|
||||||
roomId: input.roomId ?? null,
|
roomId: input.roomId ?? null,
|
||||||
circuitNumber: input.circuitNumber ?? null,
|
circuitNumber: input.circuitNumber ?? null,
|
||||||
description: input.description ?? null,
|
description: input.description ?? null,
|
||||||
name: input.name,
|
name: normalizedName,
|
||||||
category: input.category ?? null,
|
category: input.category ?? null,
|
||||||
deviceType: input.deviceType ?? null,
|
deviceType: input.deviceType ?? null,
|
||||||
phaseType: input.phaseType ?? null,
|
phaseType: input.phaseType ?? null,
|
||||||
@@ -34,28 +40,41 @@ export class ConsumerRepository {
|
|||||||
cableType: input.cableType ?? null,
|
cableType: input.cableType ?? null,
|
||||||
cableCrossSection: input.cableCrossSection ?? null,
|
cableCrossSection: input.cableCrossSection ?? null,
|
||||||
comment: input.comment ?? null,
|
comment: input.comment ?? null,
|
||||||
quantity: input.quantity,
|
quantity: normalizedQuantity,
|
||||||
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
installedPowerPerUnitKw: normalizedInstalledPowerPerUnitKw,
|
||||||
demandFactor: input.demandFactor,
|
demandFactor: normalizedDemandFactor,
|
||||||
voltageV: input.voltageV ?? null,
|
voltageV: input.voltageV ?? null,
|
||||||
phaseCount: input.phaseCount ?? null,
|
phaseCount: input.phaseCount ?? null,
|
||||||
powerFactor: input.powerFactor ?? null,
|
powerFactor: input.powerFactor ?? null,
|
||||||
note: input.note ?? null,
|
note: input.note ?? null,
|
||||||
});
|
});
|
||||||
return { id, ...input };
|
return {
|
||||||
|
id,
|
||||||
|
...input,
|
||||||
|
name: normalizedName,
|
||||||
|
quantity: normalizedQuantity,
|
||||||
|
installedPowerPerUnitKw: normalizedInstalledPowerPerUnitKw,
|
||||||
|
demandFactor: normalizedDemandFactor,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(consumerId: string, input: UpdateConsumerInput) {
|
async update(consumerId: string, input: UpdateConsumerInput) {
|
||||||
|
const normalizedName = input.name?.trim() || "Unbenannter Eintrag";
|
||||||
|
const normalizedQuantity = input.quantity ?? 0;
|
||||||
|
const normalizedInstalledPowerPerUnitKw = input.installedPowerPerUnitKw ?? 0;
|
||||||
|
const normalizedDemandFactor = input.demandFactor ?? 1;
|
||||||
await db
|
await db
|
||||||
.update(consumers)
|
.update(consumers)
|
||||||
.set({
|
.set({
|
||||||
projectId: input.projectId,
|
projectId: input.projectId,
|
||||||
distributionBoardId: input.distributionBoardId ?? null,
|
distributionBoardId: input.distributionBoardId ?? null,
|
||||||
circuitListId: input.circuitListId ?? null,
|
circuitListId: input.circuitListId ?? null,
|
||||||
|
projectDeviceId: input.projectDeviceId ?? null,
|
||||||
|
isLinkedToDevice: input.isLinkedToDevice ? 1 : 0,
|
||||||
roomId: input.roomId ?? null,
|
roomId: input.roomId ?? null,
|
||||||
circuitNumber: input.circuitNumber ?? null,
|
circuitNumber: input.circuitNumber ?? null,
|
||||||
description: input.description ?? null,
|
description: input.description ?? null,
|
||||||
name: input.name,
|
name: normalizedName,
|
||||||
category: input.category ?? null,
|
category: input.category ?? null,
|
||||||
deviceType: input.deviceType ?? null,
|
deviceType: input.deviceType ?? null,
|
||||||
phaseType: input.phaseType ?? null,
|
phaseType: input.phaseType ?? null,
|
||||||
@@ -67,9 +86,9 @@ export class ConsumerRepository {
|
|||||||
cableType: input.cableType ?? null,
|
cableType: input.cableType ?? null,
|
||||||
cableCrossSection: input.cableCrossSection ?? null,
|
cableCrossSection: input.cableCrossSection ?? null,
|
||||||
comment: input.comment ?? null,
|
comment: input.comment ?? null,
|
||||||
quantity: input.quantity,
|
quantity: normalizedQuantity,
|
||||||
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
installedPowerPerUnitKw: normalizedInstalledPowerPerUnitKw,
|
||||||
demandFactor: input.demandFactor,
|
demandFactor: normalizedDemandFactor,
|
||||||
voltageV: input.voltageV ?? null,
|
voltageV: input.voltageV ?? null,
|
||||||
phaseCount: input.phaseCount ?? null,
|
phaseCount: input.phaseCount ?? null,
|
||||||
powerFactor: input.powerFactor ?? null,
|
powerFactor: input.powerFactor ?? null,
|
||||||
@@ -86,4 +105,39 @@ export class ConsumerRepository {
|
|||||||
async delete(consumerId: string) {
|
async delete(consumerId: string) {
|
||||||
await db.delete(consumers).where(eq(consumers.id, consumerId));
|
await db.delete(consumers).where(eq(consumers.id, consumerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async syncLinkedConsumersFromProjectDevice(
|
||||||
|
projectId: string,
|
||||||
|
projectDeviceId: string,
|
||||||
|
data: {
|
||||||
|
displayName: string;
|
||||||
|
category?: string;
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnitKw: number;
|
||||||
|
demandFactor: number;
|
||||||
|
phaseCount?: 1 | 3;
|
||||||
|
powerFactor?: number;
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
await db
|
||||||
|
.update(consumers)
|
||||||
|
.set({
|
||||||
|
name: data.displayName,
|
||||||
|
category: data.category ?? null,
|
||||||
|
quantity: data.quantity,
|
||||||
|
installedPowerPerUnitKw: data.installedPowerPerUnitKw,
|
||||||
|
demandFactor: data.demandFactor,
|
||||||
|
phaseCount: data.phaseCount ?? null,
|
||||||
|
powerFactor: data.powerFactor ?? null,
|
||||||
|
note: data.note ?? null,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(consumers.projectId, projectId),
|
||||||
|
eq(consumers.projectDeviceId, projectDeviceId),
|
||||||
|
eq(consumers.isLinkedToDevice, 1)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export class GlobalDeviceRepository {
|
|||||||
await db.insert(globalDevices).values({
|
await db.insert(globalDevices).values({
|
||||||
id,
|
id,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
|
displayName: input.displayName,
|
||||||
category: input.category ?? null,
|
category: input.category ?? null,
|
||||||
quantity: input.quantity,
|
quantity: input.quantity,
|
||||||
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||||
@@ -34,6 +35,7 @@ export class GlobalDeviceRepository {
|
|||||||
.update(globalDevices)
|
.update(globalDevices)
|
||||||
.set({
|
.set({
|
||||||
name: input.name,
|
name: input.name,
|
||||||
|
displayName: input.displayName,
|
||||||
category: input.category ?? null,
|
category: input.category ?? null,
|
||||||
quantity: input.quantity,
|
quantity: input.quantity,
|
||||||
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export class ProjectDeviceRepository {
|
|||||||
id,
|
id,
|
||||||
projectId,
|
projectId,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
|
displayName: input.displayName,
|
||||||
category: input.category ?? null,
|
category: input.category ?? null,
|
||||||
quantity: input.quantity,
|
quantity: input.quantity,
|
||||||
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||||
@@ -46,6 +47,7 @@ export class ProjectDeviceRepository {
|
|||||||
.update(projectDevices)
|
.update(projectDevices)
|
||||||
.set({
|
.set({
|
||||||
name: input.name,
|
name: input.name,
|
||||||
|
displayName: input.displayName,
|
||||||
category: input.category ?? null,
|
category: input.category ?? null,
|
||||||
quantity: input.quantity,
|
quantity: input.quantity,
|
||||||
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
import { circuitLists } from "./circuit-lists.js";
|
import { circuitLists } from "./circuit-lists.js";
|
||||||
import { distributionBoards } from "./distribution-boards.js";
|
import { distributionBoards } from "./distribution-boards.js";
|
||||||
|
import { projectDevices } from "./project-devices.js";
|
||||||
import { projects } from "./projects.js";
|
import { projects } from "./projects.js";
|
||||||
import { rooms } from "./rooms.js";
|
import { rooms } from "./rooms.js";
|
||||||
|
|
||||||
@@ -15,6 +16,10 @@ export const consumers = sqliteTable("consumers", {
|
|||||||
circuitListId: text("circuit_list_id").references(() => circuitLists.id, {
|
circuitListId: text("circuit_list_id").references(() => circuitLists.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
projectDeviceId: text("project_device_id").references(() => projectDevices.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
isLinkedToDevice: integer("is_linked_to_device").notNull().default(0),
|
||||||
roomId: text("room_id").references(() => rooms.id, {
|
roomId: text("room_id").references(() => rooms.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|||||||
export const globalDevices = sqliteTable("global_devices", {
|
export const globalDevices = sqliteTable("global_devices", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
|
displayName: text("display_name").notNull(),
|
||||||
category: text("category"),
|
category: text("category"),
|
||||||
quantity: integer("quantity").notNull(),
|
quantity: integer("quantity").notNull(),
|
||||||
installedPowerPerUnitKw: real("installed_power_per_unit_kw").notNull(),
|
installedPowerPerUnitKw: real("installed_power_per_unit_kw").notNull(),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const projectDevices = sqliteTable("project_devices", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => projects.id, { onDelete: "cascade" }),
|
.references(() => projects.id, { onDelete: "cascade" }),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
|
displayName: text("display_name").notNull(),
|
||||||
category: text("category"),
|
category: text("category"),
|
||||||
quantity: integer("quantity").notNull(),
|
quantity: integer("quantity").notNull(),
|
||||||
installedPowerPerUnitKw: real("installed_power_per_unit_kw").notNull(),
|
installedPowerPerUnitKw: real("installed_power_per_unit_kw").notNull(),
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export interface Consumer {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
distributionBoardId?: string;
|
distributionBoardId?: string;
|
||||||
circuitListId?: string;
|
circuitListId?: string;
|
||||||
|
projectDeviceId?: string;
|
||||||
|
isLinkedToDevice?: boolean;
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
roomNumber?: string;
|
roomNumber?: string;
|
||||||
roomName?: string;
|
roomName?: string;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { CreateConsumerInput } from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
|
type ProjectDeviceForLinking = {
|
||||||
|
displayName: string;
|
||||||
|
category: string | null;
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnitKw: number;
|
||||||
|
demandFactor: number;
|
||||||
|
phaseCount: number | null;
|
||||||
|
powerFactor: number | null;
|
||||||
|
note: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function applyLinkedProjectDeviceValues(
|
||||||
|
input: CreateConsumerInput,
|
||||||
|
projectDevice: ProjectDeviceForLinking | null
|
||||||
|
): CreateConsumerInput {
|
||||||
|
if (!input.projectDeviceId || !input.isLinkedToDevice || !projectDevice) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...input,
|
||||||
|
name: projectDevice.displayName,
|
||||||
|
category: projectDevice.category ?? undefined,
|
||||||
|
quantity: projectDevice.quantity,
|
||||||
|
installedPowerPerUnitKw: projectDevice.installedPowerPerUnitKw,
|
||||||
|
demandFactor: projectDevice.demandFactor,
|
||||||
|
phaseCount:
|
||||||
|
projectDevice.phaseCount === 1 || projectDevice.phaseCount === 3
|
||||||
|
? (projectDevice.phaseCount as 1 | 3)
|
||||||
|
: undefined,
|
||||||
|
powerFactor: projectDevice.powerFactor ?? undefined,
|
||||||
|
note: projectDevice.note ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ export interface ConsumerWithCalculatedValues {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
distributionBoardId?: string | null;
|
distributionBoardId?: string | null;
|
||||||
circuitListId?: string | null;
|
circuitListId?: string | null;
|
||||||
|
projectDeviceId?: string | null;
|
||||||
|
isLinkedToDevice?: boolean;
|
||||||
roomId?: string | null;
|
roomId?: string | null;
|
||||||
roomNumber?: string;
|
roomNumber?: string;
|
||||||
roomName?: string;
|
roomName?: string;
|
||||||
@@ -73,6 +75,7 @@ export interface RoomDto {
|
|||||||
export interface GlobalDeviceDto {
|
export interface GlobalDeviceDto {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
displayName: string;
|
||||||
category: string | null;
|
category: string | null;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
installedPowerPerUnitKw: number;
|
installedPowerPerUnitKw: number;
|
||||||
@@ -87,6 +90,7 @@ export interface ProjectDeviceDto {
|
|||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
displayName: string;
|
||||||
category: string | null;
|
category: string | null;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
installedPowerPerUnitKw: number;
|
installedPowerPerUnitKw: number;
|
||||||
@@ -101,6 +105,8 @@ export interface CreateConsumerInput {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
distributionBoardId?: string;
|
distributionBoardId?: string;
|
||||||
circuitListId?: string;
|
circuitListId?: string;
|
||||||
|
projectDeviceId?: string;
|
||||||
|
isLinkedToDevice?: boolean;
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
circuitNumber?: string;
|
circuitNumber?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -139,6 +145,7 @@ export interface CreateRoomInput {
|
|||||||
|
|
||||||
export interface CreateGlobalDeviceInput {
|
export interface CreateGlobalDeviceInput {
|
||||||
name: string;
|
name: string;
|
||||||
|
displayName: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
installedPowerPerUnitKw: number;
|
installedPowerPerUnitKw: number;
|
||||||
@@ -151,6 +158,7 @@ export interface CreateGlobalDeviceInput {
|
|||||||
|
|
||||||
export interface CreateProjectDeviceInput {
|
export interface CreateProjectDeviceInput {
|
||||||
name: string;
|
name: string;
|
||||||
|
displayName: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
installedPowerPerUnitKw: number;
|
installedPowerPerUnitKw: number;
|
||||||
|
|||||||
@@ -143,6 +143,12 @@ export function deleteGlobalDevice(globalDeviceId: string) {
|
|||||||
return request<void>(`/api/global-devices/${globalDeviceId}`, { method: "DELETE" });
|
return request<void>(`/api/global-devices/${globalDeviceId}`, { method: "DELETE" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function copyProjectDeviceToGlobal(projectId: string, projectDeviceId: string) {
|
||||||
|
return request<GlobalDeviceDto>(`/api/global-devices/import-project/${projectId}/${projectDeviceId}`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function listProjectDevices(projectId: string) {
|
export function listProjectDevices(projectId: string) {
|
||||||
return request<ProjectDeviceDto[]>(`/api/project-devices/projects/${projectId}`);
|
return request<ProjectDeviceDto[]>(`/api/project-devices/projects/${projectId}`);
|
||||||
}
|
}
|
||||||
@@ -170,3 +176,9 @@ export function deleteProjectDevice(projectId: string, projectDeviceId: string)
|
|||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function copyGlobalDeviceToProject(projectId: string, globalDeviceId: string) {
|
||||||
|
return request<ProjectDeviceDto>(`/api/project-devices/projects/${projectId}/import-global/${globalDeviceId}`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import { CircuitListRepository } from "../../db/repositories/circuit-list.reposi
|
|||||||
import { ConsumerRepository } from "../../db/repositories/consumer.repository.js";
|
import { ConsumerRepository } from "../../db/repositories/consumer.repository.js";
|
||||||
import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
|
import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
|
||||||
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
||||||
|
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
||||||
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
||||||
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
||||||
import type { Consumer } from "../../domain/models/consumer.model.js";
|
import type { Consumer } from "../../domain/models/consumer.model.js";
|
||||||
|
import { applyLinkedProjectDeviceValues } from "../../domain/services/consumer-linking.service.js";
|
||||||
import { PowerBalanceService } from "../../domain/services/power-balance.service.js";
|
import { PowerBalanceService } from "../../domain/services/power-balance.service.js";
|
||||||
import {
|
import {
|
||||||
|
type CreateConsumerInput,
|
||||||
createConsumerSchema,
|
createConsumerSchema,
|
||||||
updateConsumerSchema,
|
updateConsumerSchema,
|
||||||
} from "../../shared/validation/consumer.schemas.js";
|
} from "../../shared/validation/consumer.schemas.js";
|
||||||
@@ -16,6 +19,7 @@ const circuitListRepository = new CircuitListRepository();
|
|||||||
const consumerRepository = new ConsumerRepository();
|
const consumerRepository = new ConsumerRepository();
|
||||||
const distributionBoardRepository = new DistributionBoardRepository();
|
const distributionBoardRepository = new DistributionBoardRepository();
|
||||||
const floorRepository = new FloorRepository();
|
const floorRepository = new FloorRepository();
|
||||||
|
const projectDeviceRepository = new ProjectDeviceRepository();
|
||||||
const projectRepository = new ProjectRepository();
|
const projectRepository = new ProjectRepository();
|
||||||
const roomRepository = new RoomRepository();
|
const roomRepository = new RoomRepository();
|
||||||
const powerBalanceService = new PowerBalanceService();
|
const powerBalanceService = new PowerBalanceService();
|
||||||
@@ -25,6 +29,8 @@ type ConsumerRow = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
distributionBoardId: string | null;
|
distributionBoardId: string | null;
|
||||||
circuitListId: string | null;
|
circuitListId: string | null;
|
||||||
|
projectDeviceId: string | null;
|
||||||
|
isLinkedToDevice: number;
|
||||||
roomId: string | null;
|
roomId: string | null;
|
||||||
circuitNumber: string | null;
|
circuitNumber: string | null;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
@@ -66,6 +72,22 @@ async function validateRoomOwnership(projectId: string, roomId: string | undefin
|
|||||||
return roomRepository.existsInProject(projectId, roomId);
|
return roomRepository.existsInProject(projectId, roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function validateProjectDeviceOwnership(projectId: string, projectDeviceId: string | undefined) {
|
||||||
|
if (!projectDeviceId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const row = await projectDeviceRepository.findById(projectId, projectDeviceId);
|
||||||
|
return Boolean(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyDeviceLinkIfNeeded(input: CreateConsumerInput): Promise<CreateConsumerInput> {
|
||||||
|
if (!input.projectDeviceId || !input.isLinkedToDevice) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
const device = await projectDeviceRepository.findById(input.projectId, input.projectDeviceId);
|
||||||
|
return applyLinkedProjectDeviceValues(input, device);
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveCircuitScope(input: {
|
async function resolveCircuitScope(input: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
distributionBoardId?: string;
|
distributionBoardId?: string;
|
||||||
@@ -125,6 +147,8 @@ function buildConsumerFromRow(
|
|||||||
projectId: row.projectId,
|
projectId: row.projectId,
|
||||||
distributionBoardId: row.distributionBoardId ?? undefined,
|
distributionBoardId: row.distributionBoardId ?? undefined,
|
||||||
circuitListId: row.circuitListId ?? undefined,
|
circuitListId: row.circuitListId ?? undefined,
|
||||||
|
projectDeviceId: row.projectDeviceId ?? undefined,
|
||||||
|
isLinkedToDevice: Boolean(row.isLinkedToDevice),
|
||||||
roomId: row.roomId ?? undefined,
|
roomId: row.roomId ?? undefined,
|
||||||
roomNumber: room?.roomNumber,
|
roomNumber: room?.roomNumber,
|
||||||
roomName: room?.roomName,
|
roomName: room?.roomName,
|
||||||
@@ -195,9 +219,18 @@ export async function createConsumer(req: Request, res: Response) {
|
|||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
}
|
}
|
||||||
|
|
||||||
const [hasValidDistributionBoard, hasValidRoom] = await Promise.all([
|
const normalizedData: CreateConsumerInput = {
|
||||||
validateDistributionBoardOwnership(parsed.data.projectId, parsed.data.distributionBoardId),
|
...parsed.data,
|
||||||
validateRoomOwnership(parsed.data.projectId, parsed.data.roomId),
|
name: parsed.data.name?.trim() || "Unbenannter Eintrag",
|
||||||
|
quantity: parsed.data.quantity ?? 0,
|
||||||
|
installedPowerPerUnitKw: parsed.data.installedPowerPerUnitKw ?? 0,
|
||||||
|
demandFactor: parsed.data.demandFactor ?? 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [hasValidDistributionBoard, hasValidRoom, hasValidProjectDevice] = await Promise.all([
|
||||||
|
validateDistributionBoardOwnership(normalizedData.projectId, normalizedData.distributionBoardId),
|
||||||
|
validateRoomOwnership(normalizedData.projectId, normalizedData.roomId),
|
||||||
|
validateProjectDeviceOwnership(normalizedData.projectId, normalizedData.projectDeviceId),
|
||||||
]);
|
]);
|
||||||
if (!hasValidDistributionBoard) {
|
if (!hasValidDistributionBoard) {
|
||||||
return res
|
return res
|
||||||
@@ -207,28 +240,34 @@ export async function createConsumer(req: Request, res: Response) {
|
|||||||
if (!hasValidRoom) {
|
if (!hasValidRoom) {
|
||||||
return res.status(400).json({ error: "Room does not belong to the provided project." });
|
return res.status(400).json({ error: "Room does not belong to the provided project." });
|
||||||
}
|
}
|
||||||
|
if (!hasValidProjectDevice) {
|
||||||
|
return res.status(400).json({ error: "Project device does not belong to the provided project." });
|
||||||
|
}
|
||||||
|
if (normalizedData.isLinkedToDevice && !normalizedData.projectDeviceId) {
|
||||||
|
return res.status(400).json({ error: "Linked entries require a projectDeviceId." });
|
||||||
|
}
|
||||||
|
|
||||||
const resolvedScope = await resolveCircuitScope({
|
const resolvedScope = await resolveCircuitScope({
|
||||||
projectId: parsed.data.projectId,
|
projectId: normalizedData.projectId,
|
||||||
distributionBoardId: parsed.data.distributionBoardId,
|
distributionBoardId: normalizedData.distributionBoardId,
|
||||||
circuitListId: parsed.data.circuitListId,
|
circuitListId: normalizedData.circuitListId,
|
||||||
});
|
});
|
||||||
if (!resolvedScope.ok) {
|
if (!resolvedScope.ok) {
|
||||||
return res.status(400).json({ error: resolvedScope.error });
|
return res.status(400).json({ error: resolvedScope.error });
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = await applyDeviceLinkIfNeeded({
|
||||||
...parsed.data,
|
...normalizedData,
|
||||||
distributionBoardId: resolvedScope.distributionBoardId,
|
distributionBoardId: resolvedScope.distributionBoardId,
|
||||||
circuitListId: resolvedScope.circuitListId,
|
circuitListId: resolvedScope.circuitListId,
|
||||||
description: parsed.data.description ?? parsed.data.name,
|
description: normalizedData.description ?? normalizedData.name,
|
||||||
};
|
});
|
||||||
|
|
||||||
const created = await consumerRepository.create(payload);
|
const created = await consumerRepository.create(payload);
|
||||||
const [project, floors, rooms] = await Promise.all([
|
const [project, floors, rooms] = await Promise.all([
|
||||||
projectRepository.findById(parsed.data.projectId),
|
projectRepository.findById(normalizedData.projectId),
|
||||||
floorRepository.listByProject(parsed.data.projectId),
|
floorRepository.listByProject(normalizedData.projectId),
|
||||||
roomRepository.listByProject(parsed.data.projectId),
|
roomRepository.listByProject(normalizedData.projectId),
|
||||||
]);
|
]);
|
||||||
const roomById = new Map(
|
const roomById = new Map(
|
||||||
rooms.map((room) => [room.id, { floorId: room.floorId, roomName: room.roomName, roomNumber: room.roomNumber }])
|
rooms.map((room) => [room.id, { floorId: room.floorId, roomName: room.roomName, roomNumber: room.roomNumber }])
|
||||||
@@ -269,9 +308,18 @@ export async function updateConsumer(req: Request, res: Response) {
|
|||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
}
|
}
|
||||||
|
|
||||||
const [hasValidDistributionBoard, hasValidRoom] = await Promise.all([
|
const normalizedData: CreateConsumerInput = {
|
||||||
validateDistributionBoardOwnership(parsed.data.projectId, parsed.data.distributionBoardId),
|
...parsed.data,
|
||||||
validateRoomOwnership(parsed.data.projectId, parsed.data.roomId),
|
name: parsed.data.name?.trim() || "Unbenannter Eintrag",
|
||||||
|
quantity: parsed.data.quantity ?? 0,
|
||||||
|
installedPowerPerUnitKw: parsed.data.installedPowerPerUnitKw ?? 0,
|
||||||
|
demandFactor: parsed.data.demandFactor ?? 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [hasValidDistributionBoard, hasValidRoom, hasValidProjectDevice] = await Promise.all([
|
||||||
|
validateDistributionBoardOwnership(normalizedData.projectId, normalizedData.distributionBoardId),
|
||||||
|
validateRoomOwnership(normalizedData.projectId, normalizedData.roomId),
|
||||||
|
validateProjectDeviceOwnership(normalizedData.projectId, normalizedData.projectDeviceId),
|
||||||
]);
|
]);
|
||||||
if (!hasValidDistributionBoard) {
|
if (!hasValidDistributionBoard) {
|
||||||
return res
|
return res
|
||||||
@@ -281,23 +329,31 @@ export async function updateConsumer(req: Request, res: Response) {
|
|||||||
if (!hasValidRoom) {
|
if (!hasValidRoom) {
|
||||||
return res.status(400).json({ error: "Room does not belong to the provided project." });
|
return res.status(400).json({ error: "Room does not belong to the provided project." });
|
||||||
}
|
}
|
||||||
|
if (!hasValidProjectDevice) {
|
||||||
|
return res.status(400).json({ error: "Project device does not belong to the provided project." });
|
||||||
|
}
|
||||||
|
if (normalizedData.isLinkedToDevice && !normalizedData.projectDeviceId) {
|
||||||
|
return res.status(400).json({ error: "Linked entries require a projectDeviceId." });
|
||||||
|
}
|
||||||
|
|
||||||
const resolvedScope = await resolveCircuitScope({
|
const resolvedScope = await resolveCircuitScope({
|
||||||
projectId: parsed.data.projectId,
|
projectId: normalizedData.projectId,
|
||||||
distributionBoardId: parsed.data.distributionBoardId,
|
distributionBoardId: normalizedData.distributionBoardId,
|
||||||
circuitListId: parsed.data.circuitListId,
|
circuitListId: normalizedData.circuitListId,
|
||||||
});
|
});
|
||||||
if (!resolvedScope.ok) {
|
if (!resolvedScope.ok) {
|
||||||
return res.status(400).json({ error: resolvedScope.error });
|
return res.status(400).json({ error: resolvedScope.error });
|
||||||
}
|
}
|
||||||
|
|
||||||
await consumerRepository.update(consumerId, {
|
const payload = await applyDeviceLinkIfNeeded({
|
||||||
...parsed.data,
|
...normalizedData,
|
||||||
distributionBoardId: resolvedScope.distributionBoardId,
|
distributionBoardId: resolvedScope.distributionBoardId,
|
||||||
circuitListId: resolvedScope.circuitListId,
|
circuitListId: resolvedScope.circuitListId,
|
||||||
description: parsed.data.description ?? parsed.data.name,
|
description: normalizedData.description ?? normalizedData.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await consumerRepository.update(consumerId, payload);
|
||||||
|
|
||||||
const row = await consumerRepository.findById(consumerId);
|
const row = await consumerRepository.findById(consumerId);
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return res.status(404).json({ error: "Consumer not found" });
|
return res.status(404).json({ error: "Consumer not found" });
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { GlobalDeviceRepository } from "../../db/repositories/global-device.repository.js";
|
import { GlobalDeviceRepository } from "../../db/repositories/global-device.repository.js";
|
||||||
|
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
||||||
import {
|
import {
|
||||||
createGlobalDeviceSchema,
|
createGlobalDeviceSchema,
|
||||||
updateGlobalDeviceSchema,
|
updateGlobalDeviceSchema,
|
||||||
} from "../../shared/validation/global-device.schemas.js";
|
} from "../../shared/validation/global-device.schemas.js";
|
||||||
|
|
||||||
const globalDeviceRepository = new GlobalDeviceRepository();
|
const globalDeviceRepository = new GlobalDeviceRepository();
|
||||||
|
const projectDeviceRepository = new ProjectDeviceRepository();
|
||||||
|
|
||||||
export async function listGlobalDevices(_req: Request, res: Response) {
|
export async function listGlobalDevices(_req: Request, res: Response) {
|
||||||
const rows = await globalDeviceRepository.list();
|
const rows = await globalDeviceRepository.list();
|
||||||
@@ -50,3 +52,30 @@ export async function deleteGlobalDevice(req: Request, res: Response) {
|
|||||||
await globalDeviceRepository.delete(globalDeviceId);
|
await globalDeviceRepository.delete(globalDeviceId);
|
||||||
return res.status(204).send();
|
return res.status(204).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function copyProjectDeviceToGlobal(req: Request, res: Response) {
|
||||||
|
const { projectId, projectDeviceId } = req.params;
|
||||||
|
if (typeof projectId !== "string" || typeof projectDeviceId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid parameters" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = await projectDeviceRepository.findById(projectId, projectDeviceId);
|
||||||
|
if (!source) {
|
||||||
|
return res.status(404).json({ error: "Project device not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await globalDeviceRepository.create({
|
||||||
|
name: source.name,
|
||||||
|
displayName: source.displayName,
|
||||||
|
category: source.category ?? undefined,
|
||||||
|
quantity: source.quantity,
|
||||||
|
installedPowerPerUnitKw: source.installedPowerPerUnitKw,
|
||||||
|
demandFactor: source.demandFactor,
|
||||||
|
voltageV: source.voltageV ?? undefined,
|
||||||
|
phaseCount: source.phaseCount === 1 || source.phaseCount === 3 ? source.phaseCount : undefined,
|
||||||
|
powerFactor: source.powerFactor ?? undefined,
|
||||||
|
note: source.note ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(201).json(created);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
|
import { GlobalDeviceRepository } from "../../db/repositories/global-device.repository.js";
|
||||||
|
import { ConsumerRepository } from "../../db/repositories/consumer.repository.js";
|
||||||
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
||||||
import {
|
import {
|
||||||
createProjectDeviceSchema,
|
createProjectDeviceSchema,
|
||||||
updateProjectDeviceSchema,
|
updateProjectDeviceSchema,
|
||||||
} from "../../shared/validation/project-device.schemas.js";
|
} from "../../shared/validation/project-device.schemas.js";
|
||||||
|
|
||||||
|
const globalDeviceRepository = new GlobalDeviceRepository();
|
||||||
|
const consumerRepository = new ConsumerRepository();
|
||||||
const projectDeviceRepository = new ProjectDeviceRepository();
|
const projectDeviceRepository = new ProjectDeviceRepository();
|
||||||
|
|
||||||
export async function listProjectDevicesByProject(req: Request, res: Response) {
|
export async function listProjectDevicesByProject(req: Request, res: Response) {
|
||||||
@@ -41,6 +45,16 @@ export async function updateProjectDevice(req: Request, res: Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await projectDeviceRepository.update(projectId, projectDeviceId, parsed.data);
|
await projectDeviceRepository.update(projectId, projectDeviceId, parsed.data);
|
||||||
|
await consumerRepository.syncLinkedConsumersFromProjectDevice(projectId, projectDeviceId, {
|
||||||
|
displayName: parsed.data.displayName,
|
||||||
|
category: parsed.data.category,
|
||||||
|
quantity: parsed.data.quantity,
|
||||||
|
installedPowerPerUnitKw: parsed.data.installedPowerPerUnitKw,
|
||||||
|
demandFactor: parsed.data.demandFactor,
|
||||||
|
phaseCount: parsed.data.phaseCount,
|
||||||
|
powerFactor: parsed.data.powerFactor,
|
||||||
|
note: parsed.data.note,
|
||||||
|
});
|
||||||
const row = await projectDeviceRepository.findById(projectId, projectDeviceId);
|
const row = await projectDeviceRepository.findById(projectId, projectDeviceId);
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return res.status(404).json({ error: "Project device not found" });
|
return res.status(404).json({ error: "Project device not found" });
|
||||||
@@ -57,3 +71,30 @@ export async function deleteProjectDevice(req: Request, res: Response) {
|
|||||||
await projectDeviceRepository.delete(projectId, projectDeviceId);
|
await projectDeviceRepository.delete(projectId, projectDeviceId);
|
||||||
return res.status(204).send();
|
return res.status(204).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function copyGlobalDeviceToProject(req: Request, res: Response) {
|
||||||
|
const { projectId, globalDeviceId } = req.params;
|
||||||
|
if (typeof projectId !== "string" || typeof globalDeviceId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid parameters" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = await globalDeviceRepository.findById(globalDeviceId);
|
||||||
|
if (!source) {
|
||||||
|
return res.status(404).json({ error: "Global device not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await projectDeviceRepository.create(projectId, {
|
||||||
|
name: source.name,
|
||||||
|
displayName: source.displayName,
|
||||||
|
category: source.category ?? undefined,
|
||||||
|
quantity: source.quantity,
|
||||||
|
installedPowerPerUnitKw: source.installedPowerPerUnitKw,
|
||||||
|
demandFactor: source.demandFactor,
|
||||||
|
voltageV: source.voltageV ?? undefined,
|
||||||
|
phaseCount: source.phaseCount === 1 || source.phaseCount === 3 ? source.phaseCount : undefined,
|
||||||
|
powerFactor: source.powerFactor ?? undefined,
|
||||||
|
note: source.note ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(201).json(created);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import {
|
import {
|
||||||
|
copyProjectDeviceToGlobal,
|
||||||
createGlobalDevice,
|
createGlobalDevice,
|
||||||
deleteGlobalDevice,
|
deleteGlobalDevice,
|
||||||
listGlobalDevices,
|
listGlobalDevices,
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
export const globalDeviceRouter = Router();
|
export const globalDeviceRouter = Router();
|
||||||
|
|
||||||
globalDeviceRouter.get("/", listGlobalDevices);
|
globalDeviceRouter.get("/", listGlobalDevices);
|
||||||
|
globalDeviceRouter.post("/import-project/:projectId/:projectDeviceId", copyProjectDeviceToGlobal);
|
||||||
globalDeviceRouter.post("/", createGlobalDevice);
|
globalDeviceRouter.post("/", createGlobalDevice);
|
||||||
globalDeviceRouter.put("/:globalDeviceId", updateGlobalDevice);
|
globalDeviceRouter.put("/:globalDeviceId", updateGlobalDevice);
|
||||||
globalDeviceRouter.delete("/:globalDeviceId", deleteGlobalDevice);
|
globalDeviceRouter.delete("/:globalDeviceId", deleteGlobalDevice);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import {
|
import {
|
||||||
|
copyGlobalDeviceToProject,
|
||||||
createProjectDevice,
|
createProjectDevice,
|
||||||
deleteProjectDevice,
|
deleteProjectDevice,
|
||||||
listProjectDevicesByProject,
|
listProjectDevicesByProject,
|
||||||
@@ -10,5 +11,6 @@ export const projectDeviceRouter = Router();
|
|||||||
|
|
||||||
projectDeviceRouter.get("/projects/:projectId", listProjectDevicesByProject);
|
projectDeviceRouter.get("/projects/:projectId", listProjectDevicesByProject);
|
||||||
projectDeviceRouter.post("/projects/:projectId", createProjectDevice);
|
projectDeviceRouter.post("/projects/:projectId", createProjectDevice);
|
||||||
|
projectDeviceRouter.post("/projects/:projectId/import-global/:globalDeviceId", copyGlobalDeviceToProject);
|
||||||
projectDeviceRouter.put("/projects/:projectId/:projectDeviceId", updateProjectDevice);
|
projectDeviceRouter.put("/projects/:projectId/:projectDeviceId", updateProjectDevice);
|
||||||
projectDeviceRouter.delete("/projects/:projectId/:projectDeviceId", deleteProjectDevice);
|
projectDeviceRouter.delete("/projects/:projectId/:projectDeviceId", deleteProjectDevice);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
export const deviceTypeOptions = [
|
||||||
|
"Beleuchtung",
|
||||||
|
"Steckdose",
|
||||||
|
"Heizung",
|
||||||
|
"Kühlung",
|
||||||
|
"Lüftung",
|
||||||
|
"Antrieb",
|
||||||
|
"Sicherheit",
|
||||||
|
"IT",
|
||||||
|
"Sonstiges",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const phaseTypeOptions = ["1-phasig", "3-phasig"] as const;
|
||||||
|
|
||||||
|
export const tradeOrCostGroupOptions = [
|
||||||
|
"KG 440 Starkstromanlagen",
|
||||||
|
"KG 450 Fernmelde- und informationstechnische Anlagen",
|
||||||
|
"KG 460 Förderanlagen",
|
||||||
|
"KG 470 Nutzungsspezifische Anlagen",
|
||||||
|
"KG 480 Gebäude- und Anlagenautomation",
|
||||||
|
"Sonstiges",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const consumerGroupOptions = [
|
||||||
|
"Allgemein",
|
||||||
|
"Notstrom",
|
||||||
|
"Sicherheitsstrom",
|
||||||
|
"USV",
|
||||||
|
"Technik",
|
||||||
|
"Reserve",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const protectionTypeOptions = ["LS", "Schmelzsicherung", "Leistungsschalter", "FI/LS"] as const;
|
||||||
|
|
||||||
|
export const protectionCharacteristicOptions = ["B", "C", "D", "K", "Z"] as const;
|
||||||
|
|
||||||
|
export const cableTypeOptions = ["NYM-J", "NYY-J", "H07RN-F", "NHXMH-J", "Sonstiges"] as const;
|
||||||
|
|
||||||
|
export const cableCrossSectionOptions = [
|
||||||
|
"1,5 mm²",
|
||||||
|
"2,5 mm²",
|
||||||
|
"4 mm²",
|
||||||
|
"6 mm²",
|
||||||
|
"10 mm²",
|
||||||
|
"16 mm²",
|
||||||
|
"25 mm²",
|
||||||
|
"35 mm²",
|
||||||
|
"50 mm²",
|
||||||
|
] as const;
|
||||||
@@ -1,27 +1,39 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
cableCrossSectionOptions,
|
||||||
|
cableTypeOptions,
|
||||||
|
consumerGroupOptions,
|
||||||
|
deviceTypeOptions,
|
||||||
|
phaseTypeOptions,
|
||||||
|
protectionCharacteristicOptions,
|
||||||
|
protectionTypeOptions,
|
||||||
|
tradeOrCostGroupOptions,
|
||||||
|
} from "../constants/consumer-option-lists.js";
|
||||||
|
|
||||||
export const createConsumerSchema = z.object({
|
export const createConsumerSchema = z.object({
|
||||||
projectId: z.string().min(1),
|
projectId: z.string().min(1),
|
||||||
distributionBoardId: z.string().min(1).optional(),
|
distributionBoardId: z.string().min(1).optional(),
|
||||||
circuitListId: z.string().min(1).optional(),
|
circuitListId: z.string().min(1).optional(),
|
||||||
|
projectDeviceId: z.string().min(1).optional(),
|
||||||
|
isLinkedToDevice: z.boolean().optional(),
|
||||||
roomId: z.string().min(1).optional(),
|
roomId: z.string().min(1).optional(),
|
||||||
circuitNumber: z.string().optional(),
|
circuitNumber: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
name: z.string().min(1),
|
name: z.string().optional(),
|
||||||
category: z.string().optional(),
|
category: z.string().optional(),
|
||||||
deviceType: z.string().optional(),
|
deviceType: z.enum(deviceTypeOptions).optional(),
|
||||||
phaseType: z.string().optional(),
|
phaseType: z.enum(phaseTypeOptions).optional(),
|
||||||
tradeOrCostGroup: z.string().optional(),
|
tradeOrCostGroup: z.enum(tradeOrCostGroupOptions).optional(),
|
||||||
group: z.string().optional(),
|
group: z.enum(consumerGroupOptions).optional(),
|
||||||
protectionType: z.string().optional(),
|
protectionType: z.enum(protectionTypeOptions).optional(),
|
||||||
protectionRatedCurrent: z.number().min(0).optional(),
|
protectionRatedCurrent: z.number().min(0).optional(),
|
||||||
protectionCharacteristic: z.string().optional(),
|
protectionCharacteristic: z.enum(protectionCharacteristicOptions).optional(),
|
||||||
cableType: z.string().optional(),
|
cableType: z.enum(cableTypeOptions).optional(),
|
||||||
cableCrossSection: z.string().optional(),
|
cableCrossSection: z.enum(cableCrossSectionOptions).optional(),
|
||||||
comment: z.string().optional(),
|
comment: z.string().optional(),
|
||||||
quantity: z.number().min(0),
|
quantity: z.number().min(0).optional(),
|
||||||
installedPowerPerUnitKw: z.number().min(0),
|
installedPowerPerUnitKw: z.number().min(0).optional(),
|
||||||
demandFactor: z.number().min(0).max(1),
|
demandFactor: z.number().min(0).max(1).optional(),
|
||||||
voltageV: z.number().positive().optional(),
|
voltageV: z.number().positive().optional(),
|
||||||
phaseCount: z.union([z.literal(1), z.literal(3)]).optional(),
|
phaseCount: z.union([z.literal(1), z.literal(3)]).optional(),
|
||||||
powerFactor: z.number().min(0).max(1).optional(),
|
powerFactor: z.number().min(0).max(1).optional(),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export const createGlobalDeviceSchema = z.object({
|
export const createGlobalDeviceSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
|
displayName: z.string().min(1),
|
||||||
category: z.string().optional(),
|
category: z.string().optional(),
|
||||||
quantity: z.number().min(0),
|
quantity: z.number().min(0),
|
||||||
installedPowerPerUnitKw: z.number().min(0),
|
installedPowerPerUnitKw: z.number().min(0),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export const createProjectDeviceSchema = z.object({
|
export const createProjectDeviceSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
|
displayName: z.string().min(1),
|
||||||
category: z.string().optional(),
|
category: z.string().optional(),
|
||||||
quantity: z.number().min(0),
|
quantity: z.number().min(0),
|
||||||
installedPowerPerUnitKw: z.number().min(0),
|
installedPowerPerUnitKw: z.number().min(0),
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { applyLinkedProjectDeviceValues } from "../src/domain/services/consumer-linking.service.js";
|
||||||
|
|
||||||
|
describe("consumer linking service", () => {
|
||||||
|
it("applies project-device values to linked consumers", () => {
|
||||||
|
const input = {
|
||||||
|
projectId: "p1",
|
||||||
|
projectDeviceId: "d1",
|
||||||
|
isLinkedToDevice: true,
|
||||||
|
name: "Alt",
|
||||||
|
quantity: 1,
|
||||||
|
installedPowerPerUnitKw: 0.2,
|
||||||
|
demandFactor: 0.8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = applyLinkedProjectDeviceValues(input, {
|
||||||
|
displayName: "Kaffeemaschine",
|
||||||
|
category: "Küche",
|
||||||
|
quantity: 3,
|
||||||
|
installedPowerPerUnitKw: 1.5,
|
||||||
|
demandFactor: 0.6,
|
||||||
|
phaseCount: 3,
|
||||||
|
powerFactor: 0.9,
|
||||||
|
note: "aus Vorlage",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.name, "Kaffeemaschine");
|
||||||
|
assert.equal(result.category, "Küche");
|
||||||
|
assert.equal(result.quantity, 3);
|
||||||
|
assert.equal(result.installedPowerPerUnitKw, 1.5);
|
||||||
|
assert.equal(result.demandFactor, 0.6);
|
||||||
|
assert.equal(result.phaseCount, 3);
|
||||||
|
assert.equal(result.powerFactor, 0.9);
|
||||||
|
assert.equal(result.note, "aus Vorlage");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps values unchanged when entry is not linked", () => {
|
||||||
|
const input = {
|
||||||
|
projectId: "p1",
|
||||||
|
projectDeviceId: "d1",
|
||||||
|
isLinkedToDevice: false,
|
||||||
|
name: "Eigener Name",
|
||||||
|
quantity: 2,
|
||||||
|
installedPowerPerUnitKw: 0.5,
|
||||||
|
demandFactor: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = applyLinkedProjectDeviceValues(input, {
|
||||||
|
displayName: "Vorlage",
|
||||||
|
category: "Test",
|
||||||
|
quantity: 9,
|
||||||
|
installedPowerPerUnitKw: 9,
|
||||||
|
demandFactor: 0.2,
|
||||||
|
phaseCount: 1,
|
||||||
|
powerFactor: 1,
|
||||||
|
note: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.name, "Eigener Name");
|
||||||
|
assert.equal(result.quantity, 2);
|
||||||
|
assert.equal(result.installedPowerPerUnitKw, 0.5);
|
||||||
|
assert.equal(result.demandFactor, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { createConsumerSchema } from "../src/shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
|
describe("consumer schema fixed option lists", () => {
|
||||||
|
it("accepts valid predefined domain values", () => {
|
||||||
|
const parsed = createConsumerSchema.safeParse({
|
||||||
|
projectId: "p1",
|
||||||
|
name: "Test",
|
||||||
|
quantity: 1,
|
||||||
|
installedPowerPerUnitKw: 1,
|
||||||
|
demandFactor: 1,
|
||||||
|
deviceType: "Beleuchtung",
|
||||||
|
phaseType: "3-phasig",
|
||||||
|
tradeOrCostGroup: "KG 440 Starkstromanlagen",
|
||||||
|
group: "Technik",
|
||||||
|
protectionType: "LS",
|
||||||
|
protectionCharacteristic: "C",
|
||||||
|
cableType: "NYM-J",
|
||||||
|
cableCrossSection: "2,5 mm²",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(parsed.success, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unknown domain values", () => {
|
||||||
|
const parsed = createConsumerSchema.safeParse({
|
||||||
|
projectId: "p1",
|
||||||
|
name: "Test",
|
||||||
|
quantity: 1,
|
||||||
|
installedPowerPerUnitKw: 1,
|
||||||
|
demandFactor: 1,
|
||||||
|
deviceType: "Irgendwas",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(parsed.success, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user