import { useEffect, useRef, useState } from 'react' import EmptyState from '../ui/EmptyState' import { ROOM_FILTERS, botleadRow, pageInfo } from '../../lib/botleadsView' const SEARCH_DELAY_MS = 300 // so lange wartet die Suche nach dem letzten Tastendruck, bevor sie den Dienst fragt const SEARCH_MAX = 100 // der Dienst kürzt die Suche auf 100 Zeichen const UNKNOWN_ERROR = 'Unbekannter Fehler' const COLUMNS = ['Betrieb', 'Raum', 'Punkte', 'Grund', 'Telefon', 'Quelle', 'Gefunden'] const START_VIEW = Object.freeze({ room: '', q: '', page: 1 }) // Raumfilter, geltende Suche, Seite: ein Wert, damit eine Änderung genau eine Anfrage auslöst // Reiter „Bot-Leads“: Betriebe, die nicht angeschrieben werden (z. B. keine veröffentlichte Mail), mit Grund. Reine Liste, nichts wird gesendet oder angerufen. // Die Liste holt sich ihre Daten bei jeder Änderung von Raum, Suche, Seite oder bot selbst; es gibt kein Polling. Nur die jüngste Anfrage zählt: eine späte // Antwort (auch ein später Fehler) einer älteren Anfrage oder nach dem Abbau wird verworfen. // bot (aus ?bot= der Adresse, Link aus dem Ereignisstrom): zeigt genau diesen Eintrag, hervorgehoben, mit „Alle anzeigen“ (onClearBot nimmt bot aus der Adresse). // Alle Texte der Zeilen sind Textknoten; Links entstehen nur aus row.sourceHref (botleadRow prüft die Adresse mit safeHref: nur http und https, keine Zugangsdaten). export default function BotLeadList({ api, bot, onClearBot }) { const botId = typeof bot === 'string' ? bot : '' const single = botId !== '' const [view, setView] = useState(START_VIEW) const [draft, setDraft] = useState('') // Inhalt des Suchfelds; in view.q steht erst die Suche, die nach der Verzögerung gilt const [result, setResult] = useState({ key: null, items: [], total: 0, pageSize: undefined, error: null }) const timerRef = useRef(null) const seqRef = useRef(0) // Tastaturfokus: Wer „Alle anzeigen“ oder „Zurück“/„Weiter“ drückt, verliert den Knopf mit dem Fokus (Einzelansicht und Seitenzeile werden ausgetauscht oder // hängen beim Laden aus); ohne Zutun fiele der Fokus auf
, an den Seitenanfang. Gemerkt wird nur, was der Nutzer als Letztes bedient hat. const selectRef = useRef(null) // Raumfilter: Ziel nach „Alle anzeigen“ const clearedRef = useRef(false) // „Alle anzeigen“ gedrückt: erst dann (nie schon beim Einhängen, das öffnete am Handy die Auswahl) bekommt der Raumfilter den Fokus const prevRef = useRef(null) const nextRef = useRef(null) const pagedRef = useRef('') // 'prev' oder 'next': welcher Blätterknopf zuletzt gedrückt wurde; nach dem Laden bekommt die Seitenzeile den Fokus zurück const key = JSON.stringify(single ? [botId] : [view.room, view.q, view.page]) useEffect(() => { seqRef.current += 1 const mine = seqRef.current // Ein früheres Ergebnis zu genau diesem Schlüssel (hin und zurück gewechselt, bevor der andere Schlüssel antwortete) ist veraltet: erst die neue Antwort zählt setResult((prev) => (prev.key === key ? { ...prev, key: null } : prev)) const load = async () => { try { const data = await api.botleads(single ? { id: botId } : { room: view.room, q: view.q, page: view.page }) if (mine !== seqRef.current) return const items = Array.isArray(data?.items) ? data.items : [] const total = Number.isFinite(data?.total) ? data.total : items.length // Die Liste ist inzwischen kürzer (Kürzen über Nacht): statt einer leeren Seite auf die letzte Seite springen const { pages } = pageInfo({ total, page: view.page, pageSize: data?.pageSize }) if (!single && items.length === 0 && total > 0 && view.page > pages) { setView((v) => ({ ...v, page: pages })) return } setResult({ key, items, total, pageSize: data?.pageSize, error: null }) } catch (err) { if (mine !== seqRef.current) return setResult({ key, items: [], total: 0, pageSize: undefined, error: err?.message || UNKNOWN_ERROR }) } } load() return () => { seqRef.current += 1 } // eine noch offene Anfrage zählt nicht mehr }, [api, key]) useEffect(() => () => window.clearTimeout(timerRef.current), []) const onClear = () => { clearedRef.current = true onClearBot() } const onPage = (dir) => { pagedRef.current = dir setView((v) => ({ ...v, page: v.page + (dir === 'next' ? 1 : -1) })) } const onRoom = (e) => { const room = e.target.value pagedRef.current = '' // wer den Raum wählt, hat den Blätterknopf verlassen: der Fokus springt nicht zurück setView((v) => ({ ...v, room, page: 1 })) } const onSearch = (e) => { const value = e.target.value pagedRef.current = '' setDraft(value) window.clearTimeout(timerRef.current) timerRef.current = window.setTimeout(() => setView((v) => ({ ...v, q: value.trim(), page: 1 })), SEARCH_DELAY_MS) } const loading = result.key !== key // Die Einzelansicht ist weg (die Seite hat bot aus der Adresse genommen): der Raumfilter ersetzt den gedrückten Knopf und bekommt den Fokus. // Ohne den Knopf (Zurück-Taste des Browsers) bleibt der Fokus, wo er ist. useEffect(() => { const wanted = clearedRef.current clearedRef.current = false if (wanted && !single) selectRef.current?.focus() }, [single]) // Die neue Seite ist da und die Seitenzeile wieder eingehängt: der Fokus geht an den gedrückten Knopf, ist er gesperrt (erste oder letzte Seite) an den anderen. // Scheiterte die Anfrage, gibt es keine Seitenzeile (die Refs sind null) und nichts geschieht. useEffect(() => { if (loading) return const dir = pagedRef.current pagedRef.current = '' if (dir === '') return const [first, second] = dir === 'next' ? [nextRef.current, prevRef.current] : [prevRef.current, nextRef.current] const target = first && !first.disabled ? first : second target?.focus() }, [loading]) const rows = result.items.map((item) => botleadRow(item)) const info = pageInfo({ total: result.total, page: view.page, pageSize: result.pageSize }) const filtered = view.room !== '' || view.q !== '' let body if (loading) { body =| {name} | )}||||||
|---|---|---|---|---|---|---|
|
{row.title}{row.gekuerzt && gekürzt}
{row.place && {row.place} }
|
{row.roomLabel} | {row.scoreLabel} | {row.reason} | {row.phone} | {row.sourceHref ? Quelle öffnen : '–'} | {row.found} |
Reine Liste: keine Anrufe, kein Mailversand.
{single ? (