/* ===================================================================== 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, }); }