Files
Vorrania/web/src/App.jsx
Scarriffle 8cf7cef6e1 Web: Artikelseite nutzt volle Breite, Chargen-Tabelle bekommt mehr Platz
Die Bearbeiten-Seite war auf 1400px gedeckelt (viel Rand links/rechts) und teilte
Formular und Chargen 50/50. Jetzt nutzt sie die volle Breite (wie die Listen) und
das Grid ist gespiegelt zu .wide-aside: links das schmale Formular (clamp bis
640px), rechts die Chargen-/Bestandstabelle mit dem Restplatz - so passen
Menge/MHD/Lagerort ohne Seitwaerts-Scrollen. Neuanlage bleibt einspaltig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 18:52:30 +02:00

172 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 Charges from "./pages/Charges";
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="/chargen" icon="box" label="Chargen" />
<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="/chargen" element={<Protected wide><Charges /></Protected>} />
<Route path="/products/new" element={<Protected adminOnly><ProductForm /></Protected>} />
<Route path="/products/:id" element={<Protected wide><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>
);
}