import { useEffect, useState } from "react"; import { NavLink, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"; import { api } from "./api"; import { useAuth } from "./auth"; import Icon from "./components/Icon"; import ErrorBoundary from "./components/ErrorBoundary"; import BrandMark from "./components/BrandMark"; import Login from "./pages/Login"; import OidcCallback from "./pages/OidcCallback"; import SsoNeu from "./pages/SsoNeu"; 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 Masse from "./pages/Masse"; import Units from "./pages/Units"; import Users from "./pages/Users"; import ShoppingList from "./pages/ShoppingList"; import Rezepte from "./pages/Rezepte"; import History from "./pages/History"; import Verliehen from "./pages/Verliehen"; import Transfer from "./pages/Transfer"; import Settings from "./pages/Settings"; import Konto from "./pages/Konto"; const navClass = ({ isActive }) => (isActive ? "nav-link active" : "nav-link"); // Die vier Bestandslisten stehen unter einer zuklappbaren Gruppe. Sie sind der // laengste Block der Leiste und wird im Alltag selten gebraucht – wer einlagert, // nutzt die Knoepfe darueber. const LISTEN = [ { to: "/lebensmittel", icon: "food", label: "Lebensmittel" }, { to: "/gegenstaende", icon: "package", label: "Gegenstände" }, { to: "/items", icon: "tag", label: "Einzelstücke" }, { to: "/chargen", icon: "box", label: "Chargen" }, ]; const LISTEN_OFFEN_KEY = "nav:listen-offen"; function NavItem({ to, icon, label, end }) { return ( {label} ); } function Sidebar() { const { user, isAdmin, logout } = useAuth(); const navigate = useNavigate(); const { pathname } = useLocation(); const [dashboards, setDashboards] = useState([]); // Der Rezept-Eintrag erscheint nur, wenn Mealie eingerichtet ist. Ein Fehler // hier darf die Leiste nicht stoeren, deshalb still abfangen. const [mealie, setMealie] = useState(false); // Zugeklappt als Vorgabe; der Zustand bleibt im Browser, wie die // Spalteneinstellungen der Tabellen. const [listenOffen, setListenOffen] = useState( () => localStorage.getItem(LISTEN_OFFEN_KEY) === "1", ); // Steht man auf einer dieser Seiten, ist die Gruppe offen – ohne den // gemerkten Zustand zu ueberschreiben. Sonst saehe man nicht, wo man ist. const aufEinerListe = LISTEN.some((e) => pathname.startsWith(e.to)); function klappeListen() { setListenOffen((alt) => { const neu = !alt; localStorage.setItem(LISTEN_OFFEN_KEY, neu ? "1" : "0"); return neu; }); } // 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); }, []); useEffect(() => { api.mealieVerfuegbar().then((a) => setMealie(!!a.available)).catch(() => {}); }, []); function handleLogout() { logout(); navigate("/login"); } return ( ); } /** Server nicht erreichbar – die Anmeldung bleibt bestehen, nur der Abruf nicht. */ function StartFehler({ text, erneut }) { return (
{text}
Deine Anmeldung ist noch gültig – sobald der Server wieder antwortet, geht es weiter.
); } function Protected({ children, adminOnly = false, wide = false }) { const { user, isAdmin, loading, startFehler, erneutVersuchen } = useAuth(); const { pathname: pfad } = useLocation(); if (loading) return
Lädt…
; if (startFehler) return ; if (!user) return ; if (adminOnly && !isAdmin) return ; return (
{/* `wide` hebt die Lesebreite auf – nur fürs Dashboard-Raster sinnvoll. */}
{/* Je Seite eine eigene Grenze: stuerzt eine ab, bleibt wenigstens die Navigation bedienbar. `key` setzt sie beim Seitenwechsel zurueck, sonst haenge man nach einem Fehler dauerhaft fest. Nur das ERSTE Pfadsegment: Sonst gilt /home/edit als andere Seite als /home, der ganze Baum wird neu eingehaengt und laedt beim Umschalten in den Bearbeitungsmodus alles noch einmal. */} {children}
); } export default function App() { const { user, loading } = useAuth(); return ( : } /> {/* Die Startseite hat einen Namen, damit in der Adresszeile steht, wo man ist; "/" leitet sichtbar dorthin um. Der Bearbeitungsmodus ist ein eigener Pfad statt eines Zustands im Speicher: So ueberlebt er ein Neuladen, laesst sich verschicken, und die Zurueck-Taste verlaesst ihn. Den Modus gibt die Prop vor, damit der Pfad nicht ein zweites Mal in der Komponente steht. */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Ohne adminOnly: Verleihen und Zurücknehmen ist eine Buchung wie Ein- und Auslagern, keine Stammdatenänderung. */} } /> } /> {/* Bewusst ohne adminOnly: die SSO-Verknuepfung braucht jeder selbst. */} } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Rueckweg vom Single-Sign-on: MUSS oeffentlich sein (der Nutzer ist in diesem Moment noch nicht angemeldet) und MUSS vor dem Auffangmuster unten stehen – sonst schluckt "*" die Weiterleitung samt Token. */} } /> {/* Ebenfalls oeffentlich: hier ist man noch nicht angemeldet. */} } /> } /> ); }