Der Adressdaten-Bereich verliert den Neu-laden-Knopf samt Telefonbuch-Abgleich und Wartedialog – die Ablage hält sich über Cache-Push und die Abfragen der Suche aktuell. Anrufen erscheint nur noch bei bestehender Verbindung zur Tray-App, eine Suche ohne Treffer zeigt nichts weiter an. Erklärtexte in Adressdaten und Tabs entfernt, ebenso das Rohnachricht-Formular im Diagnose-Bereich. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
165 lines
6.5 KiB
TypeScript
165 lines
6.5 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react'
|
||
import { describeContact, type Contact } from '../swyx/protocol'
|
||
import { fetchAddresses } from '../addresses'
|
||
|
||
interface Props {
|
||
/** Panel ist sichtbar – erst dann wird abgefragt. */
|
||
active: boolean
|
||
/** Keine Verbindung zur SwyxTray-App – Anrufen entfällt. */
|
||
disabled: boolean
|
||
/**
|
||
* Adress-Cache, den die App unaufgefordert schickt. Er wird hier nicht
|
||
* angezeigt, sondern ist das Signal, die Ablage neu abzufragen – das Backend
|
||
* hat ihn gerade hineingeschrieben.
|
||
*/
|
||
cache: Contact[] | null
|
||
onDial: (number: string) => void
|
||
}
|
||
|
||
/** So lange bekommt das Backend Zeit, den Telefonbuch-Push wegzuschreiben. */
|
||
const ARCHIVE_WRITE_DELAY_MS = 1500
|
||
|
||
/** Entprellung der Eingabe – erst dann geht die Abfrage an die Datenbank. */
|
||
const SEARCH_DEBOUNCE_MS = 300
|
||
|
||
/**
|
||
* Adressdaten aus der **Adress-Ablage des Backends** (MongoDB-Sammlung
|
||
* `addresses`) – dem zusammengeführten Bestand aus den Webhooks (Kunden,
|
||
* Kuriere) und dem Telefonbuch des Swyx-Clients.
|
||
*
|
||
* Es gibt keinen Bestand im Browser: Jede Eingabe im Suchfeld fragt –
|
||
* entprellt – die Datenbank ab, gesucht wird dort als Teilzeichenkette in
|
||
* Name, Rufnummer und Beschreibung. Angezeigt wird immer das frische
|
||
* Abfrageergebnis; der Bereich funktioniert damit auch ohne Verbindung zur
|
||
* SwyxTray-App.
|
||
*
|
||
* Das Telefonbuch der App fließt über den **Cache-Push** beim Verbinden
|
||
* (Protokoll 8) in die Ablage – die Startseite meldet ihn ans Backend; das
|
||
* Panel fragt die Ablage danach neu ab.
|
||
*/
|
||
export default function ContactsPanel({ active, disabled, cache, onDial }: Props) {
|
||
// Das jeweils letzte Abfrageergebnis – kein Bestand, nur die Anzeige.
|
||
const [results, setResults] = useState<Contact[] | null>(null)
|
||
const [loadError, setLoadError] = useState<string | null>(null)
|
||
// Bewusst nicht gemerkt: Nach einem Neustart beginnt die Suche leer.
|
||
const [filter, setFilter] = useState('')
|
||
|
||
// Zählt die Abfragen: Eine überholte darf das Ergebnis einer neueren nicht
|
||
// mehr überschreiben – Antworten kommen nicht zwingend in Reihenfolge.
|
||
const queryIdRef = useRef(0)
|
||
// Der aktuelle Suchbegriff für Abfragen außerhalb des Eingabe-Effekts
|
||
// (Cache-Push) – ohne den Effekt neu anzustoßen.
|
||
const filterRef = useRef(filter)
|
||
filterRef.current = filter
|
||
|
||
/** Fragt die Ablage mit dem aktuellen Suchbegriff ab. */
|
||
const search = useCallback(async () => {
|
||
const ticket = ++queryIdRef.current
|
||
try {
|
||
const entries = await fetchAddresses(filterRef.current)
|
||
if (ticket !== queryIdRef.current) return
|
||
setResults(entries)
|
||
setLoadError(null)
|
||
} catch (e) {
|
||
if (ticket !== queryIdRef.current) return
|
||
setLoadError(e instanceof Error ? e.message : String(e))
|
||
}
|
||
}, [])
|
||
|
||
// Jede Eingabe fragt die Datenbank ab – entprellt; die erste Abfrage nach
|
||
// dem Öffnen läuft sofort. Beim erneuten Öffnen des Bereichs ebenfalls
|
||
// frisch abfragen, damit nie ein alter Stand stehen bleibt.
|
||
useEffect(() => {
|
||
if (!active) return
|
||
const timer = setTimeout(() => void search(), results === null ? 0 : SEARCH_DEBOUNCE_MS)
|
||
return () => clearTimeout(timer)
|
||
// `results` absichtlich nicht in den Abhängigkeiten: Es würde nach jeder
|
||
// Antwort eine weitere Abfrage anstoßen.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [active, filter, search])
|
||
|
||
// Telefonbuch-Push der App: Die Startseite hat ihn ans Backend gemeldet;
|
||
// nach einer kurzen Schreibfrist die Ablage neu abfragen.
|
||
useEffect(() => {
|
||
if (!cache || cache.length === 0) return
|
||
const timer = setTimeout(() => void search(), ARCHIVE_WRITE_DELAY_MS)
|
||
return () => clearTimeout(timer)
|
||
}, [cache, search])
|
||
|
||
return (
|
||
<>
|
||
<h2>Adressdaten</h2>
|
||
|
||
{/* Die `.row` ist nicht bloß Zierde: `.field` wächst (`flex: 1 1 260px`)
|
||
und würde als direktes Kind der Karte – einer Spalte – in die *Höhe*
|
||
wachsen und den freien Platz aufsaugen. In der Zeile wächst es in
|
||
die Breite, wie in allen anderen Bereichen auch. */}
|
||
<div className="row">
|
||
<label className="field">
|
||
<span className="field__label">Suchen</span>
|
||
<input
|
||
className="field__input"
|
||
type="search"
|
||
value={filter}
|
||
spellCheck={false}
|
||
autoComplete="off"
|
||
placeholder="Name, Rufnummer oder Kürzel"
|
||
onChange={(e) => setFilter(e.target.value)}
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
{loadError && <p className="note note--error">{loadError}</p>}
|
||
|
||
{results === null ? (
|
||
!loadError && <p className="note">Adressdaten werden abgefragt …</p>
|
||
) : (
|
||
<>
|
||
<p className="note">
|
||
{filter.trim() ? `${results.length} Treffer` : `${results.length} Einträge`}
|
||
</p>
|
||
|
||
{results.length === 0 ? (
|
||
!filter.trim() && (
|
||
<p className="note">
|
||
Die Ablage ist noch leer – sie füllt sich über die Webhooks und über das
|
||
Telefonbuch der SwyxTray-App.
|
||
</p>
|
||
)
|
||
) : (
|
||
<ul className="contacts">
|
||
{results.map((contact, index) => (
|
||
// Namen sind nicht eindeutig – dieselbe Person kommt mit
|
||
// mehreren Durchwahlen vor; deshalb die Position mit hinein.
|
||
<li
|
||
key={`${contact.number ?? ''}-${contact.name ?? ''}-${index}`}
|
||
className="contacts__item"
|
||
>
|
||
<span className="contacts__info">
|
||
<span className="contacts__name">{describeContact(contact)}</span>
|
||
{contact.description && (
|
||
<span className="contacts__description">{contact.description}</span>
|
||
)}
|
||
</span>
|
||
{contact.number && <span className="contacts__number">{contact.number}</span>}
|
||
{/* Ohne Verbindung zur Tray-App gibt es nichts zu wählen –
|
||
der Knopf erscheint dann gar nicht erst. */}
|
||
{contact.number && !disabled && (
|
||
<button
|
||
type="button"
|
||
className="button button--small"
|
||
onClick={() => onDial(contact.number!)}
|
||
>
|
||
Anrufen
|
||
</button>
|
||
)}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</>
|
||
)}
|
||
</>
|
||
)
|
||
}
|