Files
Vorrania/web/src/App.jsx
Scarriffle 89598147ed Web: Dashboards in der Seitenleiste, Karten mehrfach, Buchen ohne Seitenwechsel
Drei Dinge in einem, weil sie dieselbe Umstellung brauchen.

Karten sind jetzt Instanzen: Die Anordnung merkt sich neben der Kennung auch
die Art und ein "props" je Karte. Dadurch laesst sich dieselbe Art mehrfach
auflegen, und "Verlauf eines Artikels" merkt sich endlich, welcher Artikel
gemeint ist - vorher lag diese Wahl nur in useState und war nach jedem
Neuladen wieder weg, was die Karte zum Beobachten unbrauchbar machte. Wer
mehrere Artikel im Blick haben will, legt jetzt einfach mehrere Karten an.

Mehrere Dashboards stehen als eingerueckte Eintraege unter "Übersicht" in der
Seitenleiste, anlegen und umbenennen laeuft ueber die Kopfzeile. Ab dem
zweiten werden sie aufgezaehlt; bei einem einzigen waere die Liste nur eine
Verdopplung.

Neu sind die Karten "Einlagern" und "Auslagern": Artikel suchen, Menge
eintragen, gebucht. MHD und Lagerort bleiben bewusst weg - wer die braucht,
ist auf der vollen Seite besser aufgehoben. Die alte Karte "Schnellzugriff"
bleibt als reine Verlinkung bestehen.

Der Bestaetigungsdialog kann jetzt auch nach Text fragen (prompt: true). Das
war noetig, weil Dashboards benannt werden - und window.prompt sieht aus wie
der Browser, nicht wie diese Anwendung; derselbe Grund, aus dem es hier schon
kein window.confirm gibt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:18:51 +02:00

149 lines
6.1 KiB
JavaScript

import { useEffect, useState } from "react";
import { NavLink, Navigate, Route, Routes, useNavigate } from "react-router-dom";
import { api } from "./api";
import { useAuth } from "./auth";
import Icon from "./components/Icon";
import BrandMark from "./components/BrandMark";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import Products from "./pages/Products";
import ProductForm from "./pages/ProductForm";
import CheckIn from "./pages/CheckIn";
import CheckOut from "./pages/CheckOut";
import Groups from "./pages/Groups";
import Categories from "./pages/Categories";
import Locations from "./pages/Locations";
import PackageTypes from "./pages/PackageTypes";
import Units from "./pages/Units";
import Users from "./pages/Users";
import ShoppingList from "./pages/ShoppingList";
import History from "./pages/History";
import Transfer from "./pages/Transfer";
import Settings from "./pages/Settings";
const navClass = ({ isActive }) => (isActive ? "nav-link active" : "nav-link");
function NavItem({ to, icon, label, end }) {
return (
<NavLink to={to} end={end} className={navClass}>
<Icon name={icon} size={17} />
<span className="label">{label}</span>
</NavLink>
);
}
function Sidebar() {
const { user, isAdmin, logout } = useAuth();
const navigate = useNavigate();
const [dashboards, setDashboards] = useState([]);
// Die Dashboards des Benutzers stehen als eigene Eintraege in der Leiste.
// Sie aendern sich beim Anlegen und Loeschen, deshalb horcht die Leiste auf
// das Ereignis, das die Dashboard-Seite dabei ausloest.
useEffect(() => {
function laden() {
api.dashboards().then((a) => setDashboards(a.dashboards)).catch(() => {});
}
laden();
window.addEventListener("dashboards-geaendert", laden);
return () => window.removeEventListener("dashboards-geaendert", laden);
}, []);
function handleLogout() {
logout();
navigate("/login");
}
return (
<aside className="sidebar">
<div className="sidebar-brand">
<BrandMark />
</div>
<nav className="sidebar-nav">
<NavItem to="/" end icon="dashboard" label="Übersicht" />
{/* Ab dem zweiten Dashboard lohnt die Aufzaehlung; bei einem einzigen
waere sie nur eine Verdopplung von "Übersicht". */}
{dashboards.length > 1 && dashboards.map((d, index) => (
<NavLink key={d.id} to={index === 0 ? "/" : `/d/${d.id}`} end
className={({ isActive }) => (isActive ? "nav-link sub active" : "nav-link sub")}>
<span className="label">{d.name}</span>
</NavLink>
))}
<NavItem to="/checkin" icon="checkin" label="Einlagern" />
<NavItem to="/checkout" icon="checkout" label="Auslagern" />
<NavItem to="/products" icon="package" label="Produkte" />
<NavItem to="/groups" icon="tag" label="Gruppen" />
<NavItem to="/categories" icon="package" label="Kategorien" />
<NavItem to="/shopping" icon="cart" label="Einkaufsliste" />
<NavItem to="/history" icon="history" label="Verlauf" />
{isAdmin && (
<>
<div className="nav-section">Verwaltung</div>
<NavItem to="/locations" icon="location" label="Lagerorte" />
<NavItem to="/units" icon="box" label="Einheiten" />
<NavItem to="/package-types" icon="package" label="Gebinde" />
<NavItem to="/users" icon="users" label="Benutzer" />
<NavItem to="/transfer" icon="history" label="Import / Export" />
<NavItem to="/settings" icon="settings" label="Einstellungen" />
</>
)}
</nav>
<div className="sidebar-foot">
<div className="sidebar-user">
<span className="name">{user?.username}</span>
<span className="role">{isAdmin ? "Administrator" : "Benutzer"}</span>
</div>
<button className="btn-icon" onClick={handleLogout} title="Abmelden">
<Icon name="logout" size={18} />
</button>
</div>
</aside>
);
}
function Protected({ children, adminOnly = false }) {
const { user, isAdmin, loading } = useAuth();
if (loading) return <div className="center muted" style={{ padding: 60 }}>Lädt</div>;
if (!user) return <Navigate to="/login" replace />;
if (adminOnly && !isAdmin) return <Navigate to="/" replace />;
return (
<div className="app-shell">
<Sidebar />
<div className="main">
<div className="content">{children}</div>
</div>
</div>
);
}
export default function App() {
const { user, loading } = useAuth();
return (
<Routes>
<Route
path="/login"
element={loading ? null : user ? <Navigate to="/" replace /> : <Login />}
/>
<Route path="/" element={<Protected><Dashboard /></Protected>} />
<Route path="/d/:id" element={<Protected><Dashboard /></Protected>} />
<Route path="/checkin" element={<Protected><CheckIn /></Protected>} />
<Route path="/checkout" element={<Protected><CheckOut /></Protected>} />
<Route path="/products" element={<Protected><Products /></Protected>} />
<Route path="/products/new" element={<Protected adminOnly><ProductForm /></Protected>} />
<Route path="/products/:id" element={<Protected><ProductForm /></Protected>} />
<Route path="/groups" element={<Protected><Groups /></Protected>} />
<Route path="/categories" element={<Protected><Categories /></Protected>} />
<Route path="/shopping" element={<Protected><ShoppingList /></Protected>} />
<Route path="/history" element={<Protected><History /></Protected>} />
<Route path="/locations" element={<Protected adminOnly><Locations /></Protected>} />
<Route path="/units" element={<Protected adminOnly><Units /></Protected>} />
<Route path="/package-types" element={<Protected adminOnly><PackageTypes /></Protected>} />
<Route path="/users" element={<Protected adminOnly><Users /></Protected>} />
<Route path="/transfer" element={<Protected adminOnly><Transfer /></Protected>} />
<Route path="/settings" element={<Protected adminOnly><Settings /></Protected>} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}