feat(ui): UI-002 Command Palette und Deep-Links statt Nav-Umbau
Globale Suche/Quick Actions per Strg/Cmd+K (CommandPalette): Objekt- Suche nach Name/Code, Sprung zu QR-Scan/Material/Lager/Personal/ Verwaltung. AdminPage-Tab jetzt im URL-Query (?tab=...) statt nur lokalem State, damit die Palette gezielt verlinken kann, ohne Routen zu duplizieren. Bewusst keine neuen Sidebar-Punkte fuer Fahrzeuge/ Kontrollen/Pruefungen - dafuer gibt es keine eigenstaendigen Listen- Seiten in MABEA. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { apiRequest } from "../api/client";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import type { Objekt } from "../api/types";
|
||||
|
||||
interface Eintrag {
|
||||
id: string;
|
||||
label: string;
|
||||
hinweis?: string;
|
||||
gehe: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI-002 (Epic 20): globale Suche/Quick Actions per Ctrl+K bzw. ⌘+K.
|
||||
* Öffnet sich über der ganzen App (AppShell), navigiert nur - keine eigene
|
||||
* Fachlogik, nur Zusammenfassung bereits vorhandener Routen/Aktionen.
|
||||
*/
|
||||
export function CommandPalette() {
|
||||
const navigate = useNavigate();
|
||||
const { istVerantwortlich, istAdmin } = useAuth();
|
||||
const [offen, setOffen] = useState(false);
|
||||
const [suche, setSuche] = useState("");
|
||||
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
||||
e.preventDefault();
|
||||
setOffen((v) => !v);
|
||||
}
|
||||
if (e.key === "Escape") setOffen(false);
|
||||
}
|
||||
function onOeffnenAnfrage() {
|
||||
setOffen(true);
|
||||
}
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("mabea:open-command-palette", onOeffnenAnfrage);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("mabea:open-command-palette", onOeffnenAnfrage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (offen && objekte.length === 0) {
|
||||
apiRequest<Objekt[]>("/objekte")
|
||||
.then(setObjekte)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
if (!offen) setSuche("");
|
||||
}, [offen, objekte.length]);
|
||||
|
||||
const aktionen = useMemo<Eintrag[]>(() => {
|
||||
const liste: Eintrag[] = [
|
||||
{ id: "nav-objekte", label: "Objekte", gehe: () => navigate("/objekte") },
|
||||
{ id: "nav-maengel", label: "Mängel", gehe: () => navigate("/maengel") },
|
||||
{ id: "action-scan", label: "QR scannen", hinweis: "Quick Action", gehe: () => navigate("/objekte?scan=1") },
|
||||
];
|
||||
if (istVerantwortlich) {
|
||||
liste.push(
|
||||
{ id: "nav-dashboard", label: "Dashboard", gehe: () => navigate("/dashboard") },
|
||||
{ id: "nav-fehlbestaende", label: "Fehlbestände", gehe: () => navigate("/fehlbestaende") },
|
||||
{
|
||||
id: "action-material",
|
||||
label: "Material buchen (Lager)",
|
||||
hinweis: "Quick Action",
|
||||
gehe: () => navigate("/admin?tab=lager"),
|
||||
},
|
||||
{ id: "nav-lager", label: "Lager", gehe: () => navigate("/admin?tab=lager") },
|
||||
{ id: "nav-personal", label: "Personal", gehe: () => navigate("/admin?tab=personal") },
|
||||
{
|
||||
id: "nav-material",
|
||||
label: "Material (Stammdaten)",
|
||||
gehe: () => navigate(`/admin?tab=${istAdmin ? "material" : "objekte"}`),
|
||||
},
|
||||
{ id: "nav-admin", label: istAdmin ? "Administration" : "Verwaltung", gehe: () => navigate("/admin") }
|
||||
);
|
||||
}
|
||||
return liste;
|
||||
}, [navigate, istVerantwortlich, istAdmin]);
|
||||
|
||||
const objektTreffer = useMemo<Eintrag[]>(() => {
|
||||
if (suche.trim().length === 0) return [];
|
||||
const s = suche.toLowerCase();
|
||||
return objekte
|
||||
.filter((o) => o.name.toLowerCase().includes(s) || o.code.toLowerCase().includes(s))
|
||||
.slice(0, 8)
|
||||
.map((o) => ({
|
||||
id: `objekt-${o.id}`,
|
||||
label: o.name,
|
||||
hinweis: o.code,
|
||||
gehe: () => navigate(`/akte/objekt/${o.id}`),
|
||||
}));
|
||||
}, [suche, objekte, navigate]);
|
||||
|
||||
const gefilterteAktionen = useMemo(() => {
|
||||
if (suche.trim().length === 0) return aktionen;
|
||||
const s = suche.toLowerCase();
|
||||
return aktionen.filter((a) => a.label.toLowerCase().includes(s));
|
||||
}, [suche, aktionen]);
|
||||
|
||||
if (!offen) return null;
|
||||
|
||||
const treffer = [...objektTreffer, ...gefilterteAktionen];
|
||||
|
||||
function auswaehlen(eintrag: Eintrag) {
|
||||
eintrag.gehe();
|
||||
setOffen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="command-palette-overlay"
|
||||
onClick={() => setOffen(false)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Globale Suche"
|
||||
>
|
||||
<div className="command-palette" onClick={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
autoFocus
|
||||
className="input input-search"
|
||||
placeholder="Objekt suchen oder Aktion wählen…"
|
||||
value={suche}
|
||||
onChange={(e) => setSuche(e.target.value)}
|
||||
/>
|
||||
<ul className="command-palette-list">
|
||||
{treffer.length === 0 && <li className="text-muted command-palette-empty">Keine Treffer.</li>}
|
||||
{treffer.map((eintrag) => (
|
||||
<li key={eintrag.id}>
|
||||
<button className="command-palette-item" onClick={() => auswaehlen(eintrag)}>
|
||||
<span>{eintrag.label}</span>
|
||||
{eintrag.hinweis && <span className="text-muted">{eintrag.hinweis}</span>}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user