Files
dogfather-universe/server-internal/routes/webdesign-suche.js
T
DogFatherGitandClaude Opus 5 2ae77fc54f Verwaltung: eine Suche ueber alles, mit Tastatur bedienbar
Bisher gab es genau ein Suchfeld, und es durchsuchte nur die
Anfragenliste. Wer den Namen eines Kunden im Kopf hatte, musste raten,
in welchem Reiter er nachsehen muss: War das eine Anfrage, ein laufendes
Projekt, eine offene Rechnung? Bei drei Vorgaengen merkt man sich das,
bei dreissig nicht mehr.

Jetzt: Strg+K von ueberall, auf dem Handy der Lupenknopf oben. Ein
Aufruf durchsucht Anfragen, Kunden, Projekte und Zahlungen; jeder
Treffer traegt seinen Zusammenhang (Nummer, Kunde, Betrag, Liefertermin)
und fuehrt per Enter in den passenden Reiter, bei einer Anfrage direkt in
die Detailansicht.

Gebaut nach dem ARIA-Muster 'Combobox mit Listbox-Popup' aus den W3C
Authoring Practices -- nachgeschlagen, nicht aus dem Gedaechtnis:
role=combobox am Eingabefeld, aria-expanded, aria-controls,
aria-activedescendant, role=listbox, role=option mit aria-selected. Der
Fokus bleibt dabei im Eingabefeld, damit man weitertippen kann; die
Auswahl wandert ueber aria-activedescendant. Ohne diese Auszeichnung
waere ein Feld, das Vorschlaege einblendet, fuer einen Screenreader
stumm -- das sieht man beim Testen mit den Augen nie.

Vier Fallen ausdruecklich behandelt:
- Nicht bei jedem Tastendruck suchen (180 ms Wartezeit): 'Musterbau'
  haette sonst neun Abfragen ausgeloest, acht davon veraltet.
- Das Wettrennen der Antworten: Jede Abfrage bekommt eine laufende
  Nummer, nur die neueste darf zeichnen. Sonst ueberschreibt eine spaet
  eintreffende alte Antwort die neue.
- Leer, laedt und Fehler sind eigene Zustaende. Ein Kasten, der bei
  einem Serverfehler leer bleibt, sieht aus wie 'nichts gefunden'.
- LIKE-Sonderzeichen: '%' waere ein Platzhalter, '_' ein beliebiges
  Zeichen -- und Unterstriche stehen regelmaessig in E-Mail-Adressen.
  Der Fehler ist tueckisch, weil die Suche trotzdem Treffer liefert, nur
  die falschen.

Ausserdem zum dritten Mal dieselbe Spezifitaetsfalle gefunden: '.wd p'
schlug die eigene Regel, der Tastaturhinweis erschien in 17,9px statt
11,5px -- so gross wie der Inhalt, den er erklaert. Jetzt festgenagelt
durch eine Pruefung, die Groessenverhaeltnisse vergleicht.

Geprueft: 28 gegen eine echte Datenbank (darunter alle
LIKE-Sonderzeichen), 64 im Browser auf Computer und Handy inkl. der
ARIA-Vorgaben, des Wettrennens und des Fehlerzustands.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-23 23:13:23 +02:00

157 lines
6.1 KiB
JavaScript

/* =====================================================================
webdesign-suche.js — eine Suche über alles
WARUM ES DIE BRAUCHT
Bisher gab es genau ein Suchfeld, und es durchsuchte nur die
Anfragenliste. Wer den Namen eines Kunden im Kopf hatte, musste
raten, in welchem Reiter er nachsehen muss: War das eine Anfrage,
ein laufendes Projekt, eine offene Rechnung? Bei drei Vorgängen
merkt man sich das. Bei dreissig nicht mehr.
DREI ENTSCHEIDUNGEN
1. EIN AUFRUF FÜR ALLES. Vier einzelne Abfragen würden die Liste
ruckelnd aufbauen und bei jedem getippten Buchstaben viermal
losgeschickt. Hier kommt alles zusammen zurück, sortiert.
2. GESUCHT WIRD, WAS MAN IM KOPF HAT. Also Name, E-Mail, Firma,
Projekttitel, Vorgangsnummer -- nicht Beschreibungstexte. Wer
"Muster" tippt, meint den Kunden Muster, nicht jede Anfrage, in
deren Fliesstext das Wort vorkommt. Volltextsuche über alles
klingt mächtig und liefert vor allem Rauschen.
3. TREFFER TRAGEN IHREN ZUSAMMENHANG MIT. Ein Ergebnis "Anna Muster"
allein hilft nicht -- es steht dabei, ob das eine Anfrage von
vorgestern oder ein laufendes Projekt ist. Sonst muss man jeden
Treffer anklicken, um zu wissen, ob er gemeint war.
===================================================================== */
import { db } from "../db.js";
import { json } from "../lib/http.js";
import { darfWebdesign } from "../lib/webdesign-ausweis.js";
import { requireSession } from "../lib/auth.js";
import { hasPermission } from "../lib/permissions.js";
import { text, centZuText } from "../lib/webdesign-helfer.js";
async function darf(req, recht) {
return darfWebdesign(req, recht, async () => {
const sitzung = await requireSession(req);
if (!sitzung) return { erlaubt: false, grund: 401, sitzung: null };
if (sitzung.isOwner) return { erlaubt: true, sitzung };
const ok = hasPermission(sitzung.permissions, sitzung.overrides, recht);
return { erlaubt: ok, grund: ok ? null : 403, sitzung };
});
}
/* Sonderzeichen von LIKE unschädlich machen.
Ein Prozentzeichen im Suchbegriff wäre sonst ein Platzhalter: Die
Suche nach "50%" fände alles, was mit "50" beginnt. Der Unterstrich
steht für ein beliebiges Zeichen -- eine Suche nach "a_b" fände
"axb". Beides ist kein theoretischer Fall: E-Mail-Adressen enthalten
regelmässig Unterstriche.
Der Backslash als Fluchtzeichen muss ZUERST ersetzt werden, sonst
verdoppelt der zweite Durchlauf die eben eingefügten. */
function fuerLike(begriff) {
return begriff
.replace(/\\/g, "\\\\")
.replace(/%/g, "\\%")
.replace(/_/g, "\\_");
}
const GRENZE = 6; // je Bereich -- mehr passt nicht auf einen Blick
export async function suche(req, res) {
const p = await darf(req, "WD_ANFRAGEN_VIEW");
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
const roh = text(req.query?.q, 80) || "";
const begriff = roh.trim();
/* Unter zwei Zeichen wird nicht gesucht. Ein einzelner Buchstabe
trifft fast alles und ist damit keine Auskunft, sondern eine
Liste -- und er belastet die Datenbank bei jedem Tastendruck. */
if (begriff.length < 2) {
return json(res, { ok: true, begriff, zuKurz: true, treffer: [], anzahl: 0 });
}
const muster = "%" + fuerLike(begriff) + "%";
const treffer = [];
/* ---- Anfragen ---- */
for (const a of db.prepare(
`SELECT id, nummer, name, email, paket, status, erstellt_am
FROM wd_anfragen
WHERE archiviert = 0
AND (name LIKE ? ESCAPE '\\' OR email LIKE ? ESCAPE '\\' OR nummer LIKE ? ESCAPE '\\')
ORDER BY erstellt_am DESC LIMIT ?`
).all(muster, muster, muster, GRENZE)) {
treffer.push({
art: "anfrage", id: a.id, titel: a.name,
unten: a.nummer + " · " + (a.email || ""),
marke: a.status, ziel: "anfragen",
});
}
/* ---- Kunden ---- */
for (const k of db.prepare(
`SELECT id, name, email, firma, gesperrt, freigeschaltet
FROM wd_kunden
WHERE archiviert = 0
AND (name LIKE ? ESCAPE '\\' OR email LIKE ? ESCAPE '\\' OR firma LIKE ? ESCAPE '\\')
ORDER BY name LIMIT ?`
).all(muster, muster, muster, GRENZE)) {
treffer.push({
art: "kunde", id: k.id, titel: k.name,
unten: (k.firma ? k.firma + " · " : "") + (k.email || ""),
marke: k.gesperrt ? "gesperrt" : (k.freigeschaltet ? "aktiv" : "noch nicht frei"),
ziel: "kunden",
});
}
/* ---- Projekte ---- */
for (const pr of db.prepare(
`SELECT p.id, p.nummer, p.titel, p.status, p.termin_am, k.name AS kunde_name
FROM wd_projekte p JOIN wd_kunden k ON k.id = p.kunde_id
WHERE p.archiviert = 0
AND (p.titel LIKE ? ESCAPE '\\' OR p.nummer LIKE ? ESCAPE '\\' OR k.name LIKE ? ESCAPE '\\')
ORDER BY p.erstellt_am DESC LIMIT ?`
).all(muster, muster, muster, GRENZE)) {
treffer.push({
art: "projekt", id: pr.id, titel: pr.titel,
unten: pr.nummer + " · " + pr.kunde_name
+ (pr.termin_am ? " · bis " + pr.termin_am : ""),
marke: pr.status, ziel: "projekte",
});
}
/* ---- Zahlungen ---- */
for (const z of db.prepare(
`SELECT z.id, z.nummer, z.zweck_text, z.betrag_cent, z.waehrung, z.status, k.name AS kunde_name
FROM wd_zahlungen z LEFT JOIN wd_kunden k ON k.id = z.kunde_id
WHERE z.nummer LIKE ? ESCAPE '\\' OR z.zweck_text LIKE ? ESCAPE '\\'
OR k.name LIKE ? ESCAPE '\\'
ORDER BY z.erstellt_am DESC LIMIT ?`
).all(muster, muster, muster, GRENZE)) {
treffer.push({
art: "zahlung", id: z.id, titel: z.zweck_text || z.nummer,
unten: z.nummer + " · " + (z.kunde_name || "—") + " · " + centZuText(z.betrag_cent, z.waehrung),
marke: z.status, ziel: "zahlungen",
});
}
return json(res, {
ok: true,
begriff,
zuKurz: false,
treffer,
anzahl: treffer.length,
/* Ehrlich sagen, wenn abgeschnitten wurde. Eine Liste, die stillschweigend
bei sechs endet, liest sich wie "mehr gibt es nicht" -- und dann sucht
man den siebten Kunden vergeblich. */
begrenzt: treffer.length >= GRENZE,
grenzeJeBereich: GRENZE,
});
}