Files
swyxweb/frontend/src/components/ContactsPanel.tsx
T
SvenandClaude Fable 5 0fa690a67d Oberfläche entschlackt: Adressdaten ohne Neu-laden und Erklärtexte, Diagnose ohne Rohnachricht
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>
2026-08-27 17:31:13 +02:00

165 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)}
</>
)}
</>
)
}