leistungsbilanz-ts/src/app/projects/page.tsx

403 lines
14 KiB
TypeScript

"use client";
import Link from "next/link";
import { FormEvent, useEffect, useState } from "react";
import {
createGlobalDevice,
createProject,
deleteGlobalDevice,
listGlobalDevices,
listProjects,
updateGlobalDevice,
} from "../../frontend/utils/api";
import type {
CreateGlobalDeviceInput,
GlobalDeviceDto,
ProjectDto,
} from "../../frontend/types";
const emptyGlobalDevice: CreateGlobalDeviceInput = {
name: "",
displayName: "",
category: "",
quantity: 1,
installedPowerPerUnitKw: 0.1,
demandFactor: 1,
voltageV: 230,
phaseCount: 1,
powerFactor: 1,
note: "",
};
function toOptionalNumber(value: string) {
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
return Number(trimmed);
}
export default function ProjectsPage() {
const [projects, setProjects] = useState<ProjectDto[]>([]);
const [globalDevices, setGlobalDevices] = useState<GlobalDeviceDto[]>([]);
const [projectName, setProjectName] = useState("");
const [globalDeviceForm, setGlobalDeviceForm] = useState<Record<string, string>>({
name: "",
displayName: "",
category: "",
quantity: "1",
installedPowerPerUnitKw: "0.1",
demandFactor: "1",
voltageV: "230",
phaseCount: "1",
powerFactor: "1",
note: "",
});
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
async function loadData() {
setError(null);
const [loadedProjects, loadedGlobalDevices] = await Promise.all([listProjects(), listGlobalDevices()]);
setProjects(loadedProjects);
setGlobalDevices(loadedGlobalDevices);
}
useEffect(() => {
loadData().catch((err: unknown) =>
setError(err instanceof Error ? err.message : "Daten konnten nicht geladen werden.")
);
}, []);
async function handleCreateProject(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!projectName.trim()) {
return;
}
setIsSaving(true);
setError(null);
try {
const created = await createProject(projectName.trim());
setProjects((current) => [...current, created]);
setProjectName("");
} catch (err) {
setError(err instanceof Error ? err.message : "Projekt konnte nicht erstellt werden.");
} finally {
setIsSaving(false);
}
}
async function handleCreateGlobalDevice(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!globalDeviceForm.name.trim()) {
return;
}
const payload: CreateGlobalDeviceInput = {
name: globalDeviceForm.name.trim(),
displayName: globalDeviceForm.displayName.trim() || globalDeviceForm.name.trim(),
category: globalDeviceForm.category.trim() || undefined,
quantity: Number(globalDeviceForm.quantity),
installedPowerPerUnitKw: Number(globalDeviceForm.installedPowerPerUnitKw),
demandFactor: Number(globalDeviceForm.demandFactor),
voltageV: toOptionalNumber(globalDeviceForm.voltageV),
phaseCount: globalDeviceForm.phaseCount === "3" ? 3 : 1,
powerFactor: toOptionalNumber(globalDeviceForm.powerFactor),
note: globalDeviceForm.note.trim() || undefined,
};
setIsSaving(true);
setError(null);
try {
const created = await createGlobalDevice(payload);
setGlobalDevices((current) => [...current, created]);
setGlobalDeviceForm({
name: emptyGlobalDevice.name,
displayName: emptyGlobalDevice.displayName,
category: emptyGlobalDevice.category ?? "",
quantity: String(emptyGlobalDevice.quantity),
installedPowerPerUnitKw: String(emptyGlobalDevice.installedPowerPerUnitKw),
demandFactor: String(emptyGlobalDevice.demandFactor),
voltageV: String(emptyGlobalDevice.voltageV ?? ""),
phaseCount: String(emptyGlobalDevice.phaseCount ?? 1),
powerFactor: String(emptyGlobalDevice.powerFactor ?? ""),
note: emptyGlobalDevice.note ?? "",
});
} catch (err) {
setError(err instanceof Error ? err.message : "Globales Gerät konnte nicht erstellt werden.");
} finally {
setIsSaving(false);
}
}
async function handleDeleteGlobalDevice(globalDeviceId: string) {
setIsSaving(true);
setError(null);
try {
await deleteGlobalDevice(globalDeviceId);
setGlobalDevices((current) => current.filter((item) => item.id !== globalDeviceId));
} catch (err) {
setError(err instanceof Error ? err.message : "Globales Gerät konnte nicht gelöscht werden.");
} finally {
setIsSaving(false);
}
}
async function handleQuickUpdateGlobalDevice(
device: GlobalDeviceDto,
key: "name" | "displayName" | "category",
value: string
) {
const payload: CreateGlobalDeviceInput = {
name: key === "name" ? value : device.name,
displayName: key === "displayName" ? value : device.displayName,
category: key === "category" ? value : device.category ?? undefined,
quantity: device.quantity,
installedPowerPerUnitKw: device.installedPowerPerUnitKw,
demandFactor: device.demandFactor,
voltageV: device.voltageV ?? undefined,
phaseCount: device.phaseCount ?? undefined,
powerFactor: device.powerFactor ?? undefined,
note: device.note ?? undefined,
};
try {
const updated = await updateGlobalDevice(device.id, payload);
setGlobalDevices((current) => current.map((item) => (item.id === device.id ? updated : item)));
} catch (err) {
setError(err instanceof Error ? err.message : "Globales Gerät konnte nicht aktualisiert werden.");
}
}
return (
<main className="container py-4">
<div className="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 className="h3 mb-1">Projekte</h1>
<p className="text-secondary mb-0">Projektübersicht und globale Geräteverwaltung</p>
</div>
</div>
{error ? <div className="alert alert-warning">{error}</div> : null}
<div className="row g-4">
<section className="col-12 col-lg-4">
<div className="card shadow-sm">
<div className="card-header">Neues Projekt</div>
<div className="card-body">
<form className="vstack gap-3" onSubmit={handleCreateProject}>
<div>
<label className="form-label">Projektname</label>
<input
className="form-control"
value={projectName}
onChange={(event) => setProjectName(event.target.value)}
placeholder="z. B. Neubau Schule"
/>
</div>
<button className="btn btn-primary" type="submit" disabled={isSaving}>
Projekt erstellen
</button>
</form>
</div>
</div>
</section>
<section className="col-12 col-lg-8">
<div className="card shadow-sm">
<div className="card-header">Alle Projekte</div>
<div className="table-responsive">
<table className="table table-hover align-middle mb-0">
<thead>
<tr>
<th>Projekt</th>
<th className="text-end">Aktionen</th>
</tr>
</thead>
<tbody>
{projects.map((project) => (
<tr key={project.id}>
<td>{project.name}</td>
<td className="text-end">
<Link className="btn btn-sm btn-outline-primary" href={`/projects/${project.id}`}>
Projekt öffnen
</Link>
</td>
</tr>
))}
{!projects.length ? (
<tr>
<td colSpan={2} className="text-center text-secondary py-4">
Noch keine Projekte vorhanden.
</td>
</tr>
) : null}
</tbody>
</table>
</div>
</div>
</section>
</div>
<section className="card shadow-sm mt-4">
<div className="card-header">Globale Geräte / Verbraucher</div>
<div className="card-body border-bottom">
<form className="row g-2" onSubmit={handleCreateGlobalDevice}>
<div className="col-12 col-md-3">
<input
className="form-control"
placeholder="Interner Name"
value={globalDeviceForm.name}
onChange={(event) => setGlobalDeviceForm((current) => ({ ...current, name: event.target.value }))}
/>
</div>
<div className="col-12 col-md-3">
<input
className="form-control"
placeholder="Anzeigename"
value={globalDeviceForm.displayName}
onChange={(event) => setGlobalDeviceForm((current) => ({ ...current, displayName: event.target.value }))}
/>
</div>
<div className="col-6 col-md-1">
<input
className="form-control"
placeholder="Kategorie"
value={globalDeviceForm.category}
onChange={(event) =>
setGlobalDeviceForm((current) => ({ ...current, category: event.target.value }))
}
/>
</div>
<div className="col-6 col-md-1">
<input
className="form-control"
type="number"
min="0"
value={globalDeviceForm.quantity}
onChange={(event) =>
setGlobalDeviceForm((current) => ({ ...current, quantity: event.target.value }))
}
/>
</div>
<div className="col-6 col-md-1">
<input
className="form-control"
type="number"
step="0.01"
min="0"
value={globalDeviceForm.installedPowerPerUnitKw}
onChange={(event) =>
setGlobalDeviceForm((current) => ({
...current,
installedPowerPerUnitKw: event.target.value,
}))
}
/>
</div>
<div className="col-6 col-md-1">
<input
className="form-control"
type="number"
step="0.01"
min="0"
max="1"
value={globalDeviceForm.demandFactor}
onChange={(event) =>
setGlobalDeviceForm((current) => ({ ...current, demandFactor: event.target.value }))
}
/>
</div>
<div className="col-6 col-md-1">
<select
className="form-select"
value={globalDeviceForm.phaseCount}
onChange={(event) =>
setGlobalDeviceForm((current) => ({ ...current, phaseCount: event.target.value }))
}
>
<option value="1">1-ph</option>
<option value="3">3-ph</option>
</select>
</div>
<div className="col-6 col-md-2">
<button className="btn btn-primary w-100" type="submit" disabled={isSaving}>
Gerät anlegen
</button>
</div>
</form>
</div>
<div className="table-responsive">
<table className="table table-sm table-striped align-middle mb-0">
<thead>
<tr>
<th>Interner Name</th>
<th>Anzeigename</th>
<th>Kategorie</th>
<th>Anzahl</th>
<th>Leistung je Stück [kW]</th>
<th>GZF</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
{globalDevices.map((device) => (
<tr key={device.id}>
<td>
<input
className="form-control form-control-sm"
defaultValue={device.name}
onBlur={(event) =>
event.target.value !== device.name
? handleQuickUpdateGlobalDevice(device, "name", event.target.value)
: undefined
}
/>
</td>
<td>
<input
className="form-control form-control-sm"
defaultValue={device.displayName}
onBlur={(event) =>
event.target.value !== device.displayName
? handleQuickUpdateGlobalDevice(device, "displayName", event.target.value)
: undefined
}
/>
</td>
<td>
<input
className="form-control form-control-sm"
defaultValue={device.category ?? ""}
onBlur={(event) =>
event.target.value !== (device.category ?? "")
? handleQuickUpdateGlobalDevice(device, "category", event.target.value)
: undefined
}
/>
</td>
<td>{device.quantity}</td>
<td>{device.installedPowerPerUnitKw}</td>
<td>{device.demandFactor}</td>
<td>
<button
className="btn btn-sm btn-outline-danger"
type="button"
onClick={() => handleDeleteGlobalDevice(device.id)}
>
Löschen
</button>
</td>
</tr>
))}
{!globalDevices.length ? (
<tr>
<td colSpan={7} className="text-center text-secondary py-4">
Noch keine globalen Geräte vorhanden.
</td>
</tr>
) : null}
</tbody>
</table>
</div>
</section>
</main>
);
}