Neue Seite 'Mindestbestaende' (Sidebar): filterbare Tabelle mit je einer Zeile pro Mindestbestand - Gesamt je Produkt/Gruppe und je Lagerort - inline editierbar. Produkt-Gesamtwert wird nicht-destruktiv umgerechnet (Faktor aus dem bestehenden Wert abgeleitet, sonst Artikeleinheit); Lagerort-Bedarfe und Gruppenwerte ueber die vorhandenen Endpunkte. Nur-Lesen fuer Nicht-Admins. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
169 lines
7.7 KiB
JavaScript
169 lines
7.7 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 ItemList from "./pages/ItemList";
|
||
import MinStock from "./pages/MinStock";
|
||
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 Shops from "./pages/Shops";
|
||
import ItemResolve from "./pages/ItemResolve";
|
||
import LocationResolve from "./pages/LocationResolve";
|
||
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="/lebensmittel" icon="food" label="Lebensmittel" />
|
||
<NavItem to="/gegenstaende" icon="package" label="Gegenstände" />
|
||
<NavItem to="/items" icon="tag" label="Einzelstücke" />
|
||
<NavItem to="/shopping" icon="cart" label="Einkaufsliste" />
|
||
<NavItem to="/mindestbestaende" icon="check" label="Mindestbestände" />
|
||
<NavItem to="/history" icon="history" label="Verlauf" />
|
||
{isAdmin && (
|
||
<>
|
||
{/* Stammdaten strukturieren den Katalog und werden nur von Admins
|
||
gepflegt – deshalb alle zusammen hier, wie in der iOS-App. */}
|
||
<div className="nav-section">Stammdaten</div>
|
||
<NavItem to="/categories" icon="package" label="Kategorien" />
|
||
<NavItem to="/groups" icon="tag" label="Gruppen" />
|
||
<NavItem to="/locations" icon="location" label="Lagerorte" />
|
||
<NavItem to="/shops" icon="cart" label="Shops" />
|
||
<NavItem to="/units" icon="box" label="Einheiten" />
|
||
<NavItem to="/package-types" icon="package" label="Gebinde" />
|
||
<div className="nav-section">System</div>
|
||
<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, wide = 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">
|
||
{/* `wide` hebt die Lesebreite auf – nur fürs Dashboard-Raster sinnvoll. */}
|
||
<div className={wide ? "content content--wide" : "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 wide><Dashboard /></Protected>} />
|
||
<Route path="/d/:id" element={<Protected wide><Dashboard /></Protected>} />
|
||
<Route path="/checkin" element={<Protected><CheckIn /></Protected>} />
|
||
<Route path="/checkout" element={<Protected><CheckOut /></Protected>} />
|
||
<Route path="/lebensmittel" element={<Protected wide><Products fixedType="food" /></Protected>} />
|
||
<Route path="/gegenstaende" element={<Protected wide><Products fixedType="object" /></Protected>} />
|
||
<Route path="/products" element={<Protected wide><Products /></Protected>} />
|
||
<Route path="/items" element={<Protected wide><ItemList /></Protected>} />
|
||
<Route path="/products/new" element={<Protected adminOnly><ProductForm /></Protected>} />
|
||
<Route path="/products/:id" element={<Protected><ProductForm /></Protected>} />
|
||
<Route path="/i/:uid" element={<Protected><ItemResolve /></Protected>} />
|
||
<Route path="/l/:id" element={<Protected><LocationResolve /></Protected>} />
|
||
<Route path="/groups" element={<Protected adminOnly wide><Groups /></Protected>} />
|
||
<Route path="/categories" element={<Protected adminOnly wide><Categories /></Protected>} />
|
||
<Route path="/shopping" element={<Protected><ShoppingList /></Protected>} />
|
||
<Route path="/mindestbestaende" element={<Protected wide><MinStock /></Protected>} />
|
||
<Route path="/history" element={<Protected><History /></Protected>} />
|
||
<Route path="/shops" element={<Protected adminOnly><Shops /></Protected>} />
|
||
<Route path="/locations" element={<Protected adminOnly><Locations /></Protected>} />
|
||
<Route path="/units" element={<Protected adminOnly wide><Units /></Protected>} />
|
||
<Route path="/package-types" element={<Protected adminOnly wide><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>
|
||
);
|
||
}
|