feat(ui): 24-Spalten-Dashboard-Raster, einklappbare Sidebar
CI / backend-tests (push) Failing after 2m7s
CI / frontend-build (push) Successful in 20s

Dashboard-Raster von 12 auf 24 Spalten - bei 12 waren Reihen sofort randvoll (3 Kacheln à w:4), kein Spielraum zum Breiter-Ziehen ohne Nachbarkachel zu verkleinern. Layout-localStorage-Key auf v3 gehoben (alte v2-Koordinaten passen nicht mehr zum neuen Raster). Sidebar links jetzt einklappbar (Toggle-Button bleibt sichtbar, Zustand in localStorage) - mehr Platz für breite Dashboard-Raster auf Desktop-Monitoren.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-07 14:42:19 +02:00
co-authored by Claude Sonnet 5
parent e6eda26eb2
commit e5ddfde52f
5 changed files with 145 additions and 46 deletions
+74 -35
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from "react";
import { useState, type ReactNode } from "react";
import { NavLink, useNavigate } from "react-router-dom";
import { useAuth } from "../auth/AuthContext";
@@ -6,6 +6,24 @@ import { CommandPalette } from "./CommandPalette";
import { SyncStatus } from "./status/SyncStatus";
import { ThemeToggle } from "./ThemeToggle";
const SPEICHER_KEY = "mabea-sidebar-eingeklappt";
function eingeklapptLaden(): boolean {
try {
return localStorage.getItem(SPEICHER_KEY) === "1";
} catch {
return false;
}
}
function eingeklapptSpeichern(wert: boolean): void {
try {
localStorage.setItem(SPEICHER_KEY, wert ? "1" : "0");
} catch {
// Speichern optional
}
}
function paletteOeffnen() {
window.dispatchEvent(new Event("mabea:open-command-palette"));
}
@@ -14,6 +32,15 @@ function paletteOeffnen() {
export function AppShell({ children }: { children: ReactNode }) {
const { istAdmin, istVerantwortlich, logout } = useAuth();
const navigate = useNavigate();
const [eingeklappt, setEingeklappt] = useState(eingeklapptLaden);
function umschalten() {
setEingeklappt((bisherig) => {
const neu = !bisherig;
eingeklapptSpeichern(neu);
return neu;
});
}
function handleLogout() {
logout();
@@ -22,41 +49,53 @@ export function AppShell({ children }: { children: ReactNode }) {
return (
<div className="app-shell">
<header className="topbar">
<NavLink to="/objekte" className="topbar-brand">
MABEA
</NavLink>
<nav className="topbar-nav">
<NavLink to="/objekte" className={({ isActive }) => (isActive ? "active" : "")}>
Objekte
</NavLink>
<NavLink to="/maengel" className={({ isActive }) => (isActive ? "active" : "")}>
Mängel
</NavLink>
{istVerantwortlich && (
<NavLink to="/dashboard" className={({ isActive }) => (isActive ? "active" : "")}>
Dashboard
<header className={`topbar${eingeklappt ? " topbar-eingeklappt" : ""}`}>
<button
className="topbar-toggle"
onClick={umschalten}
title={eingeklappt ? "Navigation einblenden" : "Navigation ausblenden"}
aria-label={eingeklappt ? "Navigation einblenden" : "Navigation ausblenden"}
>
{eingeklappt ? "»" : "«"}
</button>
{!eingeklappt && (
<>
<NavLink to="/objekte" className="topbar-brand">
MABEA
</NavLink>
)}
{istVerantwortlich && (
<NavLink to="/fehlbestaende" className={({ isActive }) => (isActive ? "active" : "")}>
Fehlbestände
</NavLink>
)}
{istVerantwortlich && (
<NavLink to="/admin" className={({ isActive }) => (isActive ? "active" : "")}>
{istAdmin ? "Administration" : "Verwaltung"}
</NavLink>
)}
<SyncStatus />
<button className="btn btn-secondary" onClick={paletteOeffnen} title="Globale Suche (Strg/⌘+K)">
🔍 Suche
</button>
<ThemeToggle />
<button className="btn btn-secondary" onClick={handleLogout}>
Abmelden
</button>
</nav>
<nav className="topbar-nav">
<NavLink to="/objekte" className={({ isActive }) => (isActive ? "active" : "")}>
Objekte
</NavLink>
<NavLink to="/maengel" className={({ isActive }) => (isActive ? "active" : "")}>
Mängel
</NavLink>
{istVerantwortlich && (
<NavLink to="/dashboard" className={({ isActive }) => (isActive ? "active" : "")}>
Dashboard
</NavLink>
)}
{istVerantwortlich && (
<NavLink to="/fehlbestaende" className={({ isActive }) => (isActive ? "active" : "")}>
Fehlbestände
</NavLink>
)}
{istVerantwortlich && (
<NavLink to="/admin" className={({ isActive }) => (isActive ? "active" : "")}>
{istAdmin ? "Administration" : "Verwaltung"}
</NavLink>
)}
<SyncStatus />
<button className="btn btn-secondary" onClick={paletteOeffnen} title="Globale Suche (Strg/⌘+K)">
🔍 Suche
</button>
<ThemeToggle />
<button className="btn btn-secondary" onClick={handleLogout}>
Abmelden
</button>
</nav>
</>
)}
</header>
<CommandPalette />
{children}
@@ -205,7 +205,7 @@ export function DashboardPage() {
const sichtbareIds = ALLE_TILE_IDS.filter((id) => bearbeitenModus || !layout.ausgeblendet.includes(id));
const rasterLayout: Layout = useMemo(
() => sichtbareIds.map((id) => ({ i: id, ...layout.raster[id], minW: 2, minH: 2 })),
() => sichtbareIds.map((id) => ({ i: id, ...layout.raster[id], minW: 4, minH: 2 })),
[sichtbareIds, layout.raster]
);
+14 -10
View File
@@ -19,7 +19,11 @@ export const ALLE_TILE_IDS: DashboardTileId[] = [
"aktivitaet",
];
export const GRID_SPALTEN = 12;
// 24 statt 12 Spalten - feineres Raster, mehr Kontrolle beim Breiter/Schmaler
// ziehen auf großen Desktop-Monitoren (Nutzer-Feedback: bei 12 Spalten waren
// Reihen sofort randvoll, kein Spielraum zum Vergrößern ohne Nachbarkachel
// anzufassen).
export const GRID_SPALTEN = 24;
export const GRID_ZEILENHOEHE = 56;
export interface RasterPosition {
@@ -32,14 +36,14 @@ export interface RasterPosition {
// Freie Grid-Positionen als Startzustand - grob nach Wichtigkeit sortiert,
// Aufgaben-Kachel volle Breite oben.
export const STANDARD_RASTER: Record<DashboardTileId, RasterPosition> = {
aufgaben: { x: 0, y: 0, w: 12, h: 3 },
einsatzbereitschaft: { x: 0, y: 3, w: 4, h: 5 },
prueftermine: { x: 4, y: 3, w: 4, h: 5 },
ablaufdaten: { x: 8, y: 3, w: 4, h: 5 },
kennzahlen: { x: 0, y: 8, w: 4, h: 5 },
maengel: { x: 4, y: 8, w: 4, h: 5 },
qualifikation: { x: 8, y: 8, w: 4, h: 4 },
aktivitaet: { x: 0, y: 13, w: 8, h: 5 },
aufgaben: { x: 0, y: 0, w: 24, h: 3 },
einsatzbereitschaft: { x: 0, y: 3, w: 8, h: 5 },
prueftermine: { x: 8, y: 3, w: 8, h: 5 },
ablaufdaten: { x: 16, y: 3, w: 8, h: 5 },
kennzahlen: { x: 0, y: 8, w: 8, h: 5 },
maengel: { x: 8, y: 8, w: 8, h: 5 },
qualifikation: { x: 16, y: 8, w: 8, h: 4 },
aktivitaet: { x: 0, y: 13, w: 16, h: 5 },
};
export interface DashboardLayout {
@@ -47,7 +51,7 @@ export interface DashboardLayout {
ausgeblendet: DashboardTileId[];
}
const SPEICHER_KEY = "mabea-dashboard-layout-v2";
const SPEICHER_KEY = "mabea-dashboard-layout-v3";
function istGueltigeId(wert: unknown): wert is DashboardTileId {
return typeof wert === "string" && (ALLE_TILE_IDS as string[]).includes(wert);
+32
View File
@@ -193,6 +193,38 @@ a {
top: 0;
height: 100vh;
overflow-y: auto;
transition: width 0.18s ease, flex-basis 0.18s ease, padding 0.18s ease;
}
/* Sidebar einklappbar - Nutzer-Wunsch (mehr Platz für breite Dashboard-Raster
auf Desktop-Monitoren). Nur der Toggle-Button bleibt sichtbar. */
.topbar-eingeklappt {
width: 48px;
flex: 0 0 48px;
padding: 1.25rem 0.5rem;
align-items: center;
overflow: visible;
}
.topbar-toggle {
background: none;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
color: var(--color-text-muted);
cursor: pointer;
font-size: 0.9rem;
line-height: 1;
padding: 0.4rem 0.55rem;
align-self: flex-end;
}
.topbar-eingeklappt .topbar-toggle {
align-self: center;
}
.topbar-toggle:hover {
background: var(--color-surface-sunken);
color: var(--color-text);
}
.topbar-brand {