Apply Petrol/Volt/Kupfer design system and project sidebar
Bring the Econsult-family design tokens (see design-system-econsult.md) into leistungsbilanz-ts and add a persistent sidebar for the projects area, mirroring elt-planung-suite / Einsatz-Orga. The sidebar lists all projects and, inside a project, jumps to its sections; the tree editor stays sidebar-free since it needs the full width. The projects overview page is decluttered to a single stacked column (create/import/global devices) now that the project list lives in the sidebar.
This commit is contained in:
parent
cfd4778305
commit
704f4df2a8
6 changed files with 568 additions and 127 deletions
|
|
@ -8,15 +8,15 @@ import {
|
|||
deleteGlobalDevice,
|
||||
importProjectTransferAsNew,
|
||||
listGlobalDevices,
|
||||
listProjects,
|
||||
updateGlobalDevice,
|
||||
} from "../../frontend/utils/api";
|
||||
import type {
|
||||
CreateGlobalDeviceInput,
|
||||
GlobalDeviceDto,
|
||||
ProjectDto,
|
||||
} from "../../frontend/types";
|
||||
|
||||
const PROJECTS_CHANGED_EVENT = "leistungsbilanz:projects-changed";
|
||||
|
||||
const emptyGlobalDevice: CreateGlobalDeviceInput = {
|
||||
name: "",
|
||||
displayName: "",
|
||||
|
|
@ -38,7 +38,6 @@ function toOptionalNumber(value: string) {
|
|||
}
|
||||
|
||||
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>>({
|
||||
|
|
@ -64,12 +63,14 @@ export default function ProjectsPage() {
|
|||
projectId: string;
|
||||
name: string;
|
||||
} | null>(null);
|
||||
const [createdProject, setCreatedProject] = useState<{
|
||||
id: string;
|
||||
name: string;
|
||||
} | null>(null);
|
||||
|
||||
async function loadData() {
|
||||
setError(null);
|
||||
const [loadedProjects, loadedGlobalDevices] = await Promise.all([listProjects(), listGlobalDevices()]);
|
||||
setProjects(loadedProjects);
|
||||
setGlobalDevices(loadedGlobalDevices);
|
||||
setGlobalDevices(await listGlobalDevices());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -85,10 +86,12 @@ export default function ProjectsPage() {
|
|||
}
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
setCreatedProject(null);
|
||||
try {
|
||||
const created = await createProject(projectName.trim());
|
||||
setProjects((current) => [...current, created]);
|
||||
setCreatedProject({ id: created.id, name: created.name });
|
||||
setProjectName("");
|
||||
window.dispatchEvent(new Event(PROJECTS_CHANGED_EVENT));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Projekt konnte nicht erstellt werden.");
|
||||
} finally {
|
||||
|
|
@ -123,11 +126,11 @@ export default function ProjectsPage() {
|
|||
setImportedProject(null);
|
||||
try {
|
||||
const imported = await importProjectTransferAsNew(projectImport);
|
||||
setProjects(await listProjects());
|
||||
setImportedProject(imported);
|
||||
setProjectImport(null);
|
||||
setProjectImportFilename("");
|
||||
setProjectImportInputKey((current) => current + 1);
|
||||
window.dispatchEvent(new Event(PROJECTS_CHANGED_EVENT));
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
|
|
@ -222,130 +225,107 @@ export default function ProjectsPage() {
|
|||
|
||||
return (
|
||||
<main className="container py-4">
|
||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="h3 mb-1">Projekte</h1>
|
||||
<div className="kicker">Leistungsbilanz</div>
|
||||
<h1>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 mb-4">
|
||||
<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>
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-header">Projekt importieren</div>
|
||||
<div className="card-body">
|
||||
<p className="text-secondary small">
|
||||
Eine exportierte JSON-Projektdatei wird als eigenständige Kopie
|
||||
mit neuen internen IDs angelegt.
|
||||
</p>
|
||||
<label className="form-label" htmlFor="project-overview-import">
|
||||
Projektdatei auswählen
|
||||
</label>
|
||||
<input
|
||||
accept=".json,application/json"
|
||||
className="form-control"
|
||||
disabled={isSaving}
|
||||
id="project-overview-import"
|
||||
key={projectImportInputKey}
|
||||
onChange={(event) =>
|
||||
void handleProjectImportFile(event.target.files?.[0])
|
||||
}
|
||||
type="file"
|
||||
/>
|
||||
{projectImportFilename ? (
|
||||
<div className="form-text">
|
||||
{projectImportFilename} ist bereit.
|
||||
</div>
|
||||
) : null}
|
||||
{projectImportFileError ? (
|
||||
<div className="text-danger small mt-1">
|
||||
{projectImportFileError}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
className="btn btn-outline-primary mt-3 w-100"
|
||||
disabled={isSaving || projectImport === null}
|
||||
onClick={() => void handleImportProject()}
|
||||
type="button"
|
||||
>
|
||||
Als neues Projekt importieren
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<section className="card shadow-sm">
|
||||
<div className="card-header">Neues Projekt</div>
|
||||
<div className="card-body" style={{ maxWidth: 480 }}>
|
||||
<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>
|
||||
{importedProject ? (
|
||||
<div className="alert alert-success mt-3 mb-0">
|
||||
<div className="small mb-2">
|
||||
„{importedProject.name}“ wurde angelegt.
|
||||
</div>
|
||||
<Link
|
||||
className="btn btn-sm btn-success"
|
||||
href={`/projects/${importedProject.projectId}`}
|
||||
>
|
||||
Importiertes Projekt öffnen
|
||||
</Link>
|
||||
</form>
|
||||
{createdProject ? (
|
||||
<div className="alert alert-success mt-3 mb-0">
|
||||
<div className="small mb-2">
|
||||
„{createdProject.name}“ wurde angelegt.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Link
|
||||
className="btn btn-sm btn-success"
|
||||
href={`/projects/${createdProject.id}`}
|
||||
>
|
||||
Projekt öffnen
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</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>
|
||||
<section className="card shadow-sm">
|
||||
<div className="card-header">Projekt importieren</div>
|
||||
<div className="card-body" style={{ maxWidth: 480 }}>
|
||||
<p className="text-secondary small">
|
||||
Eine exportierte JSON-Projektdatei wird als eigenständige Kopie
|
||||
mit neuen internen IDs angelegt.
|
||||
</p>
|
||||
<label className="form-label" htmlFor="project-overview-import">
|
||||
Projektdatei auswählen
|
||||
</label>
|
||||
<input
|
||||
accept=".json,application/json"
|
||||
className="form-control"
|
||||
disabled={isSaving}
|
||||
id="project-overview-import"
|
||||
key={projectImportInputKey}
|
||||
onChange={(event) =>
|
||||
void handleProjectImportFile(event.target.files?.[0])
|
||||
}
|
||||
type="file"
|
||||
/>
|
||||
{projectImportFilename ? (
|
||||
<div className="form-text">
|
||||
{projectImportFilename} ist bereit.
|
||||
</div>
|
||||
) : null}
|
||||
{projectImportFileError ? (
|
||||
<div className="text-danger small mt-1">
|
||||
{projectImportFileError}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
className="btn btn-outline-primary mt-3 w-100"
|
||||
disabled={isSaving || projectImport === null}
|
||||
onClick={() => void handleImportProject()}
|
||||
type="button"
|
||||
>
|
||||
Als neues Projekt importieren
|
||||
</button>
|
||||
{importedProject ? (
|
||||
<div className="alert alert-success mt-3 mb-0">
|
||||
<div className="small mb-2">
|
||||
„{importedProject.name}“ wurde angelegt.
|
||||
</div>
|
||||
<Link
|
||||
className="btn btn-sm btn-success"
|
||||
href={`/projects/${importedProject.projectId}`}
|
||||
>
|
||||
Importiertes Projekt öffnen
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="card shadow-sm mt-4">
|
||||
<div className="card-header">Globale Geräte / Verbraucher</div>
|
||||
<section className="card shadow-sm">
|
||||
<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">
|
||||
|
|
@ -524,6 +504,7 @@ export default function ProjectsPage() {
|
|||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue