Files
Vorrania/web/src/confirm.jsx
Scarriffle 89598147ed Web: Dashboards in der Seitenleiste, Karten mehrfach, Buchen ohne Seitenwechsel
Drei Dinge in einem, weil sie dieselbe Umstellung brauchen.

Karten sind jetzt Instanzen: Die Anordnung merkt sich neben der Kennung auch
die Art und ein "props" je Karte. Dadurch laesst sich dieselbe Art mehrfach
auflegen, und "Verlauf eines Artikels" merkt sich endlich, welcher Artikel
gemeint ist - vorher lag diese Wahl nur in useState und war nach jedem
Neuladen wieder weg, was die Karte zum Beobachten unbrauchbar machte. Wer
mehrere Artikel im Blick haben will, legt jetzt einfach mehrere Karten an.

Mehrere Dashboards stehen als eingerueckte Eintraege unter "Übersicht" in der
Seitenleiste, anlegen und umbenennen laeuft ueber die Kopfzeile. Ab dem
zweiten werden sie aufgezaehlt; bei einem einzigen waere die Liste nur eine
Verdopplung.

Neu sind die Karten "Einlagern" und "Auslagern": Artikel suchen, Menge
eintragen, gebucht. MHD und Lagerort bleiben bewusst weg - wer die braucht,
ist auf der vollen Seite besser aufgehoben. Die alte Karte "Schnellzugriff"
bleibt als reine Verlinkung bestehen.

Der Bestaetigungsdialog kann jetzt auch nach Text fragen (prompt: true). Das
war noetig, weil Dashboards benannt werden - und window.prompt sieht aus wie
der Browser, nicht wie diese Anwendung; derselbe Grund, aus dem es hier schon
kein window.confirm gibt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:18:51 +02:00

99 lines
3.7 KiB
JavaScript

import { createContext, useCallback, useContext, useRef, useState } from "react";
import Icon from "./components/Icon";
/**
* Rückfragen innerhalb der Anwendung statt über den Browser.
*
* `window.confirm` reißt den Benutzer aus der Oberfläche, zeigt den Hostnamen
* und lässt sich weder gestalten noch beschriften. Der Dialog hier sieht aus wie
* der Rest der Anwendung, kann eine Aktion benennen ("Löschen" statt "OK") und
* gefährliche Schritte rot hervorheben.
*
* Aufruf bleibt so einfach wie vorher:
*
* const confirm = useConfirm();
* if (!(await confirm({ title: "Gruppe löschen?", message: "…" }))) return;
*
* Mit `prompt: true` wird daraus eine Eingabe; aufgeloest wird dann der
* eingetippte Text (oder `null` beim Abbrechen). Auch dafuer gilt der Grund von
* oben: `window.prompt` sieht aus wie der Browser, nicht wie diese Anwendung.
*
* const name = await confirm({ title: "Name?", prompt: true, defaultValue: "…" });
*/
const ConfirmContext = createContext(async () => false);
export function ConfirmProvider({ children }) {
const [frage, setFrage] = useState(null);
const [eingabe, setEingabe] = useState("");
// Die Zusage des offenen Dialogs, damit `await confirm(...)` funktioniert.
const antwort = useRef(null);
const confirm = useCallback((optionen) => {
const gestellt = typeof optionen === "string" ? { message: optionen } : optionen;
setFrage(gestellt);
setEingabe(gestellt.defaultValue ?? "");
return new Promise((resolve) => {
antwort.current = resolve;
});
}, []);
function schliessen(ergebnis) {
setFrage(null);
antwort.current?.(ergebnis);
antwort.current = null;
}
/** Bei einer Eingabe zaehlt der Text, sonst wie bisher true/false. */
function bestaetigen() {
if (!frage?.prompt) { schliessen(true); return; }
const text = eingabe.trim();
if (!text) return; // Leer bestaetigen ergibt keinen Sinn
schliessen(text);
}
return (
<ConfirmContext.Provider value={confirm}>
{children}
{frage && (
<div className="modal-backdrop"
onClick={(e) => e.target === e.currentTarget && schliessen(frage.prompt ? null : false)}>
<div className="modal" role="dialog" aria-modal="true">
<div className="modal-head">
<Icon name={frage.prompt ? "edit" : "alert"} size={18}
className={frage.danger ? "icon-danger" : ""} />
<h2>{frage.title || "Bitte bestätigen"}</h2>
</div>
{frage.message && <p className="modal-body">{frage.message}</p>}
{frage.prompt && (
<div className="modal-body">
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
<input autoFocus value={eingabe} onChange={(e) => setEingabe(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") bestaetigen();
if (e.key === "Escape") schliessen(null);
}} />
</div>
)}
<div className="btn-pair">
<button type="button" className="btn" autoFocus={!frage.prompt}
onClick={() => schliessen(frage.prompt ? null : false)}>
{frage.cancelLabel || "Abbrechen"}
</button>
<button type="button"
className={frage.danger ? "btn danger-solid" : "btn primary"}
disabled={frage.prompt && !eingabe.trim()}
onClick={bestaetigen}>
{frage.confirmLabel || "Fortfahren"}
</button>
</div>
</div>
</div>
)}
</ConfirmContext.Provider>
);
}
export function useConfirm() {
return useContext(ConfirmContext);
}