Streamline project management UI
This commit is contained in:
@@ -44,6 +44,8 @@ import {
|
||||
ProjectSettingsModal,
|
||||
type ProjectSettingsInput,
|
||||
} from "../../../frontend/components/project-settings-modal";
|
||||
import { FormModal } from "../../../frontend/components/form-modal";
|
||||
import { ProjectDeviceModal } from "../../../frontend/components/project-device-modal";
|
||||
|
||||
const projectDeviceSyncFieldLabels: Record<ProjectDeviceSyncField, string> = {
|
||||
name: "Technischer Name",
|
||||
@@ -59,28 +61,6 @@ const projectDeviceSyncFieldLabels: Record<ProjectDeviceSyncField, string> = {
|
||||
remark: "Bemerkung",
|
||||
};
|
||||
|
||||
const emptyProjectDevice: CreateProjectDeviceInput = {
|
||||
name: "",
|
||||
displayName: "",
|
||||
phaseType: "single_phase",
|
||||
connectionKind: "",
|
||||
costGroup: "",
|
||||
category: "",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: 1,
|
||||
remark: "",
|
||||
};
|
||||
|
||||
function toOptionalNumber(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return Number(trimmed);
|
||||
}
|
||||
|
||||
export default function ProjectDetailPage() {
|
||||
const params = useParams<{ projectId: string }>();
|
||||
const [projectId, setProjectId] = useState("");
|
||||
@@ -97,20 +77,14 @@ export default function ProjectDetailPage() {
|
||||
const [roomName, setRoomName] = useState("");
|
||||
const [roomFloorId, setRoomFloorId] = useState("");
|
||||
const [isProjectSettingsOpen, setIsProjectSettingsOpen] = useState(false);
|
||||
const [projectDeviceForm, setProjectDeviceForm] = useState<Record<string, string>>({
|
||||
name: "",
|
||||
displayName: "",
|
||||
phaseType: "single_phase",
|
||||
connectionKind: "",
|
||||
costGroup: "",
|
||||
category: "",
|
||||
quantity: "1",
|
||||
powerPerUnit: "0.1",
|
||||
simultaneityFactor: "1",
|
||||
cosPhi: "1",
|
||||
remark: "",
|
||||
});
|
||||
const [selectedGlobalDeviceId, setSelectedGlobalDeviceId] = useState("");
|
||||
const [structureModal, setStructureModal] = useState<
|
||||
"board" | "floor" | "room" | null
|
||||
>(null);
|
||||
const [isProjectDeviceModalOpen, setIsProjectDeviceModalOpen] =
|
||||
useState(false);
|
||||
const [editingProjectDevice, setEditingProjectDevice] =
|
||||
useState<ProjectDeviceDto | null>(null);
|
||||
const [projectDeviceQuery, setProjectDeviceQuery] = useState("");
|
||||
const [syncPreview, setSyncPreview] = useState<ProjectDeviceSyncPreviewDto | null>(null);
|
||||
const [selectedSyncRowIds, setSelectedSyncRowIds] = useState<string[]>([]);
|
||||
const [selectedSyncFields, setSelectedSyncFields] = useState<ProjectDeviceSyncField[]>([]);
|
||||
@@ -162,6 +136,21 @@ export default function ProjectDetailPage() {
|
||||
const floorCount = useMemo(() => floors.length, [floors.length]);
|
||||
const roomCount = useMemo(() => rooms.length, [rooms.length]);
|
||||
const projectDeviceCount = useMemo(() => projectDevices.length, [projectDevices.length]);
|
||||
const visibleProjectDevices = useMemo(() => {
|
||||
const query = projectDeviceQuery.trim().toLocaleLowerCase("de");
|
||||
if (!query) {
|
||||
return projectDevices;
|
||||
}
|
||||
return projectDevices.filter((device) =>
|
||||
[
|
||||
device.name,
|
||||
device.displayName,
|
||||
device.category,
|
||||
device.connectionKind,
|
||||
device.costGroup,
|
||||
].some((value) => value?.toLocaleLowerCase("de").includes(query))
|
||||
);
|
||||
}, [projectDeviceQuery, projectDevices]);
|
||||
const floorById = useMemo(() => new Map(floors.map((item) => [item.id, item])), [floors]);
|
||||
const circuitListByBoardId = useMemo(
|
||||
() => new Map(circuitLists.map((circuitList) => [circuitList.distributionBoardId, circuitList])),
|
||||
@@ -194,6 +183,7 @@ export default function ProjectDetailPage() {
|
||||
setCircuitLists(await listCircuitLists(projectId));
|
||||
applyProjectRevision(result.history.currentRevision);
|
||||
setBoardName("");
|
||||
setStructureModal(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Verteilung konnte nicht erstellt werden.");
|
||||
} finally {
|
||||
@@ -217,6 +207,7 @@ export default function ProjectDetailPage() {
|
||||
setFloors((current) => [...current, result.floor]);
|
||||
applyProjectRevision(result.history.currentRevision);
|
||||
setFloorName("");
|
||||
setStructureModal(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Etage konnte nicht erstellt werden.");
|
||||
} finally {
|
||||
@@ -251,6 +242,7 @@ export default function ProjectDetailPage() {
|
||||
setRoomNumber("");
|
||||
setRoomName("");
|
||||
setRoomFloorId("");
|
||||
setStructureModal(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Raum konnte nicht erstellt werden.");
|
||||
} finally {
|
||||
@@ -279,26 +271,13 @@ export default function ProjectDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateProjectDevice(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!projectId || !project || !projectDeviceForm.name.trim()) {
|
||||
async function handleCreateProjectDevice(
|
||||
payload: CreateProjectDeviceInput
|
||||
) {
|
||||
if (!projectId || !project || !payload.name.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: CreateProjectDeviceInput = {
|
||||
name: projectDeviceForm.name.trim(),
|
||||
displayName: projectDeviceForm.displayName.trim() || projectDeviceForm.name.trim(),
|
||||
phaseType: projectDeviceForm.phaseType === "three_phase" ? "three_phase" : "single_phase",
|
||||
connectionKind: projectDeviceForm.connectionKind.trim() || undefined,
|
||||
costGroup: projectDeviceForm.costGroup.trim() || undefined,
|
||||
category: projectDeviceForm.category.trim() || undefined,
|
||||
quantity: Number(projectDeviceForm.quantity),
|
||||
powerPerUnit: Number(projectDeviceForm.powerPerUnit),
|
||||
simultaneityFactor: Number(projectDeviceForm.simultaneityFactor),
|
||||
cosPhi: toOptionalNumber(projectDeviceForm.cosPhi),
|
||||
remark: projectDeviceForm.remark.trim() || undefined,
|
||||
};
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -309,19 +288,7 @@ export default function ProjectDetailPage() {
|
||||
);
|
||||
setProjectDevices((current) => [...current, result.projectDevice]);
|
||||
applyProjectRevision(result.history.currentRevision);
|
||||
setProjectDeviceForm({
|
||||
name: emptyProjectDevice.name,
|
||||
displayName: emptyProjectDevice.displayName,
|
||||
phaseType: emptyProjectDevice.phaseType,
|
||||
connectionKind: emptyProjectDevice.connectionKind ?? "",
|
||||
costGroup: emptyProjectDevice.costGroup ?? "",
|
||||
category: emptyProjectDevice.category ?? "",
|
||||
quantity: String(emptyProjectDevice.quantity),
|
||||
powerPerUnit: String(emptyProjectDevice.powerPerUnit),
|
||||
simultaneityFactor: String(emptyProjectDevice.simultaneityFactor),
|
||||
cosPhi: String(emptyProjectDevice.cosPhi ?? ""),
|
||||
remark: emptyProjectDevice.remark ?? "",
|
||||
});
|
||||
setIsProjectDeviceModalOpen(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Projektgerät konnte nicht erstellt werden.");
|
||||
} finally {
|
||||
@@ -350,30 +317,16 @@ export default function ProjectDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleQuickUpdateProjectDevice(
|
||||
async function handleUpdateProjectDevice(
|
||||
device: ProjectDeviceDto,
|
||||
key: "name" | "displayName" | "category",
|
||||
value: string
|
||||
payload: CreateProjectDeviceInput
|
||||
) {
|
||||
if (!projectId || !project) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: CreateProjectDeviceInput = {
|
||||
name: key === "name" ? value : device.name,
|
||||
displayName: key === "displayName" ? value : device.displayName,
|
||||
phaseType: device.phaseType,
|
||||
connectionKind: device.connectionKind ?? undefined,
|
||||
costGroup: device.costGroup ?? undefined,
|
||||
category: key === "category" ? value : device.category ?? undefined,
|
||||
quantity: device.quantity,
|
||||
powerPerUnit: device.powerPerUnit,
|
||||
simultaneityFactor: device.simultaneityFactor,
|
||||
cosPhi: device.cosPhi ?? undefined,
|
||||
remark: device.remark ?? undefined,
|
||||
voltageV: device.voltageV ?? undefined,
|
||||
};
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await updateProjectDevice(
|
||||
projectId,
|
||||
@@ -387,9 +340,12 @@ export default function ProjectDetailPage() {
|
||||
)
|
||||
);
|
||||
applyProjectRevision(result.history.currentRevision);
|
||||
setEditingProjectDevice(null);
|
||||
await openProjectDeviceSyncPreview(result.projectDevice.id);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Projektgerät konnte nicht aktualisiert werden.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,8 +444,8 @@ export default function ProjectDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopyGlobalToProject() {
|
||||
if (!projectId || !project || !selectedGlobalDeviceId) {
|
||||
async function handleCopyGlobalToProject(globalDeviceId: string) {
|
||||
if (!projectId || !project || !globalDeviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -498,11 +454,12 @@ export default function ProjectDetailPage() {
|
||||
try {
|
||||
const result = await copyGlobalDeviceToProject(
|
||||
projectId,
|
||||
selectedGlobalDeviceId,
|
||||
globalDeviceId,
|
||||
project.currentRevision
|
||||
);
|
||||
setProjectDevices((current) => [...current, result.projectDevice]);
|
||||
applyProjectRevision(result.history.currentRevision);
|
||||
setIsProjectDeviceModalOpen(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Globales Gerät konnte nicht ins Projekt kopiert werden.");
|
||||
} finally {
|
||||
@@ -567,30 +524,20 @@ export default function ProjectDetailPage() {
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="col-12 col-lg-4">
|
||||
<section className="col-12">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-header">Neue Verteilung</div>
|
||||
<div className="card-body">
|
||||
<form className="vstack gap-3" onSubmit={handleCreateBoard}>
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="z. B. UV-01"
|
||||
value={boardName}
|
||||
onChange={(event) => setBoardName(event.target.value)}
|
||||
/>
|
||||
<button className="btn btn-primary" type="submit" disabled={isSaving}>
|
||||
Verteilung erstellen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="col-12 col-lg-8">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-header d-flex justify-content-between">
|
||||
<span>Alle Verteilungen</span>
|
||||
<span className="badge text-bg-secondary">{boardCount}</span>
|
||||
<div className="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span>Verteilungen</span>
|
||||
<span className="badge text-bg-secondary ms-2">{boardCount}</span>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={() => setStructureModal("board")}
|
||||
type="button"
|
||||
>
|
||||
Verteilung hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-hover align-middle mb-0">
|
||||
@@ -638,24 +585,20 @@ export default function ProjectDetailPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="col-12 col-xl-4">
|
||||
<section className="col-12 col-lg-5">
|
||||
<div className="card shadow-sm h-100">
|
||||
<div className="card-header d-flex justify-content-between">
|
||||
<span>Etagen</span>
|
||||
<span className="badge text-bg-secondary">{floorCount}</span>
|
||||
</div>
|
||||
<div className="card-body border-bottom">
|
||||
<form className="vstack gap-2" onSubmit={handleCreateFloor}>
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="z. B. EG"
|
||||
value={floorName}
|
||||
onChange={(event) => setFloorName(event.target.value)}
|
||||
/>
|
||||
<button className="btn btn-primary" type="submit" disabled={isSaving}>
|
||||
Etage hinzufügen
|
||||
</button>
|
||||
</form>
|
||||
<div className="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span>Etagen</span>
|
||||
<span className="badge text-bg-secondary ms-2">{floorCount}</span>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={() => setStructureModal("floor")}
|
||||
type="button"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
<ul className="list-group list-group-flush">
|
||||
{floors.map((floor) => (
|
||||
@@ -670,50 +613,20 @@ export default function ProjectDetailPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="col-12 col-xl-8">
|
||||
<section className="col-12 col-lg-7">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-header d-flex justify-content-between">
|
||||
<span>Räume</span>
|
||||
<span className="badge text-bg-secondary">{roomCount}</span>
|
||||
</div>
|
||||
<div className="card-body border-bottom">
|
||||
<form className="row g-2" onSubmit={handleCreateRoom}>
|
||||
<div className="col-12 col-lg-3">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Raumnummer"
|
||||
value={roomNumber}
|
||||
onChange={(event) => setRoomNumber(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-lg-4">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Raumname"
|
||||
value={roomName}
|
||||
onChange={(event) => setRoomName(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-lg-3">
|
||||
<select
|
||||
className="form-select"
|
||||
value={roomFloorId}
|
||||
onChange={(event) => setRoomFloorId(event.target.value)}
|
||||
>
|
||||
<option value="">Ohne Etage</option>
|
||||
{floors.map((floor) => (
|
||||
<option key={floor.id} value={floor.id}>
|
||||
{floor.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-12 col-lg-2">
|
||||
<button className="btn btn-primary w-100" type="submit" disabled={isSaving}>
|
||||
Raum anlegen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span>Räume</span>
|
||||
<span className="badge text-bg-secondary ms-2">{roomCount}</span>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={() => setStructureModal("room")}
|
||||
type="button"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-hover align-middle mb-0">
|
||||
@@ -747,167 +660,26 @@ export default function ProjectDetailPage() {
|
||||
</div>
|
||||
|
||||
<section className="card shadow-sm mt-4">
|
||||
<div className="card-header d-flex justify-content-between">
|
||||
<span>Projektgeräte / Verbraucher</span>
|
||||
<span className="badge text-bg-secondary">{projectDeviceCount}</span>
|
||||
</div>
|
||||
<div className="card-body border-bottom">
|
||||
<form className="row g-2" onSubmit={handleCreateProjectDevice}>
|
||||
<div className="col-12 col-md-2">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Interner Name"
|
||||
value={projectDeviceForm.name}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({ ...current, name: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-2">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Anzeigename"
|
||||
value={projectDeviceForm.displayName}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({ ...current, displayName: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-1">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Kategorie"
|
||||
value={projectDeviceForm.category}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({ ...current, category: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-1">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Anschlussart"
|
||||
value={projectDeviceForm.connectionKind}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({ ...current, connectionKind: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-1">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Kostengruppe"
|
||||
value={projectDeviceForm.costGroup}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({ ...current, costGroup: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-1">
|
||||
<input
|
||||
className="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
title="Anzahl"
|
||||
value={projectDeviceForm.quantity}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({ ...current, quantity: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-1">
|
||||
<input
|
||||
className="form-control"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
title="Leistung je Stück [kW]"
|
||||
value={projectDeviceForm.powerPerUnit}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({
|
||||
...current,
|
||||
powerPerUnit: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-1">
|
||||
<input
|
||||
className="form-control"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="1"
|
||||
title="Gleichzeitigkeitsfaktor"
|
||||
value={projectDeviceForm.simultaneityFactor}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({ ...current, simultaneityFactor: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-1">
|
||||
<input
|
||||
className="form-control"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="1"
|
||||
title="cos Phi"
|
||||
value={projectDeviceForm.cosPhi}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({ ...current, cosPhi: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-1">
|
||||
<select
|
||||
className="form-select"
|
||||
title="Phasenart"
|
||||
value={projectDeviceForm.phaseType}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({ ...current, phaseType: event.target.value }))
|
||||
}
|
||||
>
|
||||
<option value="single_phase">1-ph</option>
|
||||
<option value="three_phase">3-ph</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-12 col-md-10">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="Bemerkung"
|
||||
value={projectDeviceForm.remark}
|
||||
onChange={(event) =>
|
||||
setProjectDeviceForm((current) => ({ ...current, remark: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-2">
|
||||
<button className="btn btn-primary w-100" type="submit" disabled={isSaving}>
|
||||
Gerät anlegen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="input-group mt-3">
|
||||
<select
|
||||
className="form-select"
|
||||
value={selectedGlobalDeviceId}
|
||||
onChange={(event) => setSelectedGlobalDeviceId(event.target.value)}
|
||||
>
|
||||
<option value="">Globales Gerät auswählen</option>
|
||||
{globalDevices.map((device) => (
|
||||
<option key={device.id} value={device.id}>
|
||||
{device.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div>
|
||||
<span>Projektgeräte</span>
|
||||
<span className="badge text-bg-secondary ms-2">{projectDeviceCount}</span>
|
||||
</div>
|
||||
<div className="d-flex gap-2">
|
||||
<input
|
||||
aria-label="Projektgeräte durchsuchen"
|
||||
className="form-control form-control-sm"
|
||||
onChange={(event) => setProjectDeviceQuery(event.target.value)}
|
||||
placeholder="Geräte durchsuchen"
|
||||
type="search"
|
||||
value={projectDeviceQuery}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
className="btn btn-sm btn-primary text-nowrap"
|
||||
onClick={() => setIsProjectDeviceModalOpen(true)}
|
||||
type="button"
|
||||
onClick={handleCopyGlobalToProject}
|
||||
disabled={!selectedGlobalDeviceId || isSaving}
|
||||
>
|
||||
Nach Projekt kopieren
|
||||
Gerät hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -915,64 +687,51 @@ export default function ProjectDetailPage() {
|
||||
<table className="table table-sm table-striped align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Interner Name</th>
|
||||
<th>Anzeigename</th>
|
||||
<th>Phasenart</th>
|
||||
<th>Kategorie</th>
|
||||
<th>Gerät</th>
|
||||
<th>Anschluss</th>
|
||||
<th>Leistung</th>
|
||||
<th>Kostengruppe</th>
|
||||
<th>Anzahl</th>
|
||||
<th>Leistung je Stück [kW]</th>
|
||||
<th>GZF</th>
|
||||
<th>Gesamtleistung [kW]</th>
|
||||
<th>Aktionen</th>
|
||||
<th className="text-end">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{projectDevices.map((device) => (
|
||||
{visibleProjectDevices.map((device) => (
|
||||
<tr key={device.id}>
|
||||
<td>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
defaultValue={device.name}
|
||||
onBlur={(event) =>
|
||||
event.target.value !== device.name
|
||||
? handleQuickUpdateProjectDevice(device, "name", event.target.value)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<strong>{device.displayName}</strong>
|
||||
<div className="small text-secondary">
|
||||
{device.name}
|
||||
{device.category ? ` · ${device.category}` : ""}
|
||||
</div>
|
||||
</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
|
||||
}
|
||||
/>
|
||||
{device.phaseType === "three_phase" ? "3-phasig" : "1-phasig"}
|
||||
{device.connectionKind ? (
|
||||
<div className="small text-secondary">
|
||||
{device.connectionKind}
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td>{device.phaseType === "three_phase" ? "3-ph" : "1-ph"}</td>
|
||||
<td>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
defaultValue={device.category ?? ""}
|
||||
onBlur={(event) =>
|
||||
event.target.value !== (device.category ?? "")
|
||||
? handleQuickUpdateProjectDevice(device, "category", event.target.value)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{device.totalPower} kW
|
||||
<div className="small text-secondary">
|
||||
{device.quantity} × {device.powerPerUnit} kW · GZF{" "}
|
||||
{device.simultaneityFactor}
|
||||
</div>
|
||||
</td>
|
||||
<td>{device.costGroup ?? "-"}</td>
|
||||
<td>{device.quantity}</td>
|
||||
<td>{device.powerPerUnit}</td>
|
||||
<td>{device.simultaneityFactor}</td>
|
||||
<td>{device.totalPower}</td>
|
||||
<td>
|
||||
<div className="btn-group btn-group-sm">
|
||||
<td className="text-end">
|
||||
<div className="d-inline-flex flex-wrap justify-content-end gap-1">
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
type="button"
|
||||
onClick={() => setEditingProjectDevice(device)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
type="button"
|
||||
onClick={() => void openProjectDeviceSyncPreview(device.id)}
|
||||
disabled={isSaving}
|
||||
@@ -980,7 +739,7 @@ export default function ProjectDetailPage() {
|
||||
Verknüpfungen
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
type="button"
|
||||
onClick={() => handleCopyProjectToGlobal(device.id)}
|
||||
disabled={isSaving}
|
||||
@@ -988,7 +747,7 @@ export default function ProjectDetailPage() {
|
||||
Nach global kopieren
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
className="btn btn-sm btn-outline-danger"
|
||||
type="button"
|
||||
onClick={() => handleDeleteProjectDevice(device.id)}
|
||||
disabled={isSaving}
|
||||
@@ -999,10 +758,12 @@ export default function ProjectDetailPage() {
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!projectDevices.length ? (
|
||||
{!visibleProjectDevices.length ? (
|
||||
<tr>
|
||||
<td colSpan={10} className="text-center text-secondary py-4">
|
||||
Noch keine Projektgeräte vorhanden.
|
||||
<td colSpan={5} className="text-center text-secondary py-4">
|
||||
{projectDevices.length
|
||||
? "Keine Projektgeräte entsprechen der Suche."
|
||||
: "Noch keine Projektgeräte vorhanden."}
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
@@ -1121,6 +882,135 @@ export default function ProjectDetailPage() {
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
{structureModal === "board" ? (
|
||||
<FormModal
|
||||
description="Eine Stromkreisliste mit den vier Standardbereichen wird automatisch angelegt."
|
||||
isSaving={isSaving}
|
||||
onClose={() => setStructureModal(null)}
|
||||
onSubmit={handleCreateBoard}
|
||||
submitDisabled={!boardName.trim()}
|
||||
submitLabel="Verteilung erstellen"
|
||||
title="Verteilung hinzufügen"
|
||||
>
|
||||
<label className="form-label" htmlFor="distribution-board-name">
|
||||
Bezeichnung
|
||||
</label>
|
||||
<input
|
||||
autoFocus
|
||||
className="form-control"
|
||||
id="distribution-board-name"
|
||||
onChange={(event) => setBoardName(event.target.value)}
|
||||
placeholder="z. B. UV-01"
|
||||
required
|
||||
value={boardName}
|
||||
/>
|
||||
</FormModal>
|
||||
) : null}
|
||||
|
||||
{structureModal === "floor" ? (
|
||||
<FormModal
|
||||
isSaving={isSaving}
|
||||
onClose={() => setStructureModal(null)}
|
||||
onSubmit={handleCreateFloor}
|
||||
submitDisabled={!floorName.trim()}
|
||||
submitLabel="Etage hinzufügen"
|
||||
title="Etage hinzufügen"
|
||||
>
|
||||
<label className="form-label" htmlFor="floor-name">
|
||||
Bezeichnung
|
||||
</label>
|
||||
<input
|
||||
autoFocus
|
||||
className="form-control"
|
||||
id="floor-name"
|
||||
onChange={(event) => setFloorName(event.target.value)}
|
||||
placeholder="z. B. EG"
|
||||
required
|
||||
value={floorName}
|
||||
/>
|
||||
</FormModal>
|
||||
) : null}
|
||||
|
||||
{structureModal === "room" ? (
|
||||
<FormModal
|
||||
isSaving={isSaving}
|
||||
onClose={() => setStructureModal(null)}
|
||||
onSubmit={handleCreateRoom}
|
||||
submitDisabled={!roomNumber.trim() || !roomName.trim()}
|
||||
submitLabel="Raum anlegen"
|
||||
title="Raum hinzufügen"
|
||||
>
|
||||
<div className="row g-3">
|
||||
<div className="col-12 col-md-4">
|
||||
<label className="form-label" htmlFor="room-number">
|
||||
Raumnummer
|
||||
</label>
|
||||
<input
|
||||
autoFocus
|
||||
className="form-control"
|
||||
id="room-number"
|
||||
onChange={(event) => setRoomNumber(event.target.value)}
|
||||
required
|
||||
value={roomNumber}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-8">
|
||||
<label className="form-label" htmlFor="room-name">
|
||||
Raumname
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="room-name"
|
||||
onChange={(event) => setRoomName(event.target.value)}
|
||||
required
|
||||
value={roomName}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form-label" htmlFor="room-floor">
|
||||
Etage
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="room-floor"
|
||||
onChange={(event) => setRoomFloorId(event.target.value)}
|
||||
value={roomFloorId}
|
||||
>
|
||||
<option value="">Ohne Etage</option>
|
||||
{floors.map((floor) => (
|
||||
<option key={floor.id} value={floor.id}>
|
||||
{floor.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</FormModal>
|
||||
) : null}
|
||||
|
||||
{isProjectDeviceModalOpen ? (
|
||||
<ProjectDeviceModal
|
||||
globalDevices={globalDevices}
|
||||
isSaving={isSaving}
|
||||
onClose={() => setIsProjectDeviceModalOpen(false)}
|
||||
onImportGlobal={handleCopyGlobalToProject}
|
||||
onSave={handleCreateProjectDevice}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{editingProjectDevice ? (
|
||||
<ProjectDeviceModal
|
||||
globalDevices={globalDevices}
|
||||
initialDevice={editingProjectDevice}
|
||||
isSaving={isSaving}
|
||||
onClose={() => setEditingProjectDevice(null)}
|
||||
onImportGlobal={handleCopyGlobalToProject}
|
||||
onSave={(input) =>
|
||||
handleUpdateProjectDevice(editingProjectDevice, input)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{project && isProjectSettingsOpen ? (
|
||||
<ProjectSettingsModal
|
||||
isSaving={isSaving}
|
||||
|
||||
Reference in New Issue
Block a user