Web: druckbare QR-Etiketten fuer Lagerorte (Locations-Seite) und Route /l/<id> (zeigt den Ort im Browser). Geteilter QR-Helfer (web/src/qr.jsx). iOS: neuer Ablauf "Ort zuordnen" - erst Einzelstueck-QR (…/i/<UID>) scannen, dann Lagerort-QR (…/l/<ID>); das Stueck wird dem Ort zugewiesen, danach gleich das naechste. Plus die zuvor gebaute "Nachschlagen"-Kachel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
156 lines
6.6 KiB
JavaScript
156 lines
6.6 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 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="/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="/shops" icon="cart" label="Shops" />
|
|
<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="/i/:uid" element={<Protected><ItemResolve /></Protected>} />
|
|
<Route path="/l/:id" element={<Protected><LocationResolve /></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="/shops" element={<Protected adminOnly><Shops /></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>
|
|
);
|
|
}
|