From afd6d03a71b57ca9ef969b0580057c4a1d2f6f37 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Thu, 3 Sep 2026 15:21:31 +0200 Subject: [PATCH] =?UTF-8?q?Anruf-Popup=20auch=20f=C3=BCr=20Kuriere:=20Rufn?= =?UTF-8?q?ummernsuche=20findet=20Kunden=20und=20Kuriere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- frontend/src/addresses.test.ts | 34 ++++++++++++++----- frontend/src/addresses.ts | 26 ++++++++------ frontend/src/components/CustomerCallPopup.tsx | 6 ++-- frontend/src/pages/HomePage.tsx | 10 +++--- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/frontend/src/addresses.test.ts b/frontend/src/addresses.test.ts index c4fa0a1..b1cec6e 100644 --- a/frontend/src/addresses.test.ts +++ b/frontend/src/addresses.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { pickCustomer, sameNumber, type StoredAddress } from './addresses' +import { pickCaller, sameNumber, type StoredAddress } from './addresses' /** * SwyxIt! meldet Rufnummern mit Amtsholung ("001602107449"), die Webhooks @@ -32,7 +32,7 @@ describe('sameNumber', () => { }) }) -describe('pickCustomer', () => { +describe('pickCaller', () => { const entries: StoredAddress[] = [ { number: '5215', name: 'Abdul, Rokhsareh', description: 'Globales Telefonbuch' }, { @@ -43,21 +43,39 @@ describe('pickCustomer', () => { cscId: 100164, url: 'https://test.sb.assecutor.de/admin/tapi_wrapper.php?phoneNo=01602107449', }, - { number: '+49171677xxxx', name: 'Rainer Peters (HH1003)', role: 'courier' }, + { + number: '+49171677xxxx', + name: 'Rainer Peters (HH1003)', + role: 'courier', + jobIds: [21891233], + }, ] it('findet den Kunden zur gemeldeten Rufnummer', () => { - const match = pickCustomer(entries, '001602107449') + const match = pickCaller(entries, '001602107449') expect(match?.name).toBe('SYSGEN GmbH') expect(match?.url).toContain('tapi_wrapper') }) - it('übergeht Telefonbuch-Einträge und Kuriere', () => { - expect(pickCustomer(entries, '5215')).toBeNull() - expect(pickCustomer(entries, '+49171677xxxx')).toBeNull() + it('findet auch den Kurier zur gemeldeten Rufnummer', () => { + const match = pickCaller(entries, '+49171677xxxx') + expect(match?.name).toBe('Rainer Peters (HH1003)') + expect(match?.jobIds).toEqual([21891233]) + }) + + it('übergeht Telefonbuch-Einträge (ohne Rolle)', () => { + expect(pickCaller(entries, '5215')).toBeNull() + }) + + it('bevorzugt bei doppelter Rufnummer den Kunden vor dem Kurier', () => { + const both: StoredAddress[] = [ + { number: '+49404711', name: 'Kurier', role: 'courier' }, + { number: '+49404711', name: 'Kunde', role: 'customer' }, + ] + expect(pickCaller(both, '+49404711')?.name).toBe('Kunde') }) it('liefert null, wenn keine Rufnummer passt', () => { - expect(pickCustomer(entries, '+49404711')).toBeNull() + expect(pickCaller(entries, '+49404711')).toBeNull() }) }) diff --git a/frontend/src/addresses.ts b/frontend/src/addresses.ts index 5c797a0..128110b 100644 --- a/frontend/src/addresses.ts +++ b/frontend/src/addresses.ts @@ -63,25 +63,29 @@ export function sameNumber(a: string, b: string): boolean { return left.slice(-tail) === right.slice(-tail) } -/** Der erste Kunde (Rolle `customer`) aus `entries`, dessen Rufnummer passt. */ -export function pickCustomer(entries: StoredAddress[], number: string): StoredAddress | null { - return ( +/** + * Der erste Kunde oder Kurier aus `entries`, dessen Rufnummer passt – Einträge + * ohne Rolle (SwyxTray-Telefonbuch) zählen nicht. Passen beide Rollen, gewinnt + * der Kunde. + */ +export function pickCaller(entries: StoredAddress[], number: string): StoredAddress | null { + const withRole = (role: string) => entries.find( - (entry) => entry.role === 'customer' && entry.number && sameNumber(entry.number, number), - ) ?? null - ) + (entry) => entry.role === role && entry.number && sameNumber(entry.number, number), + ) + return withRole('customer') ?? withRole('courier') ?? null } /** - * Sucht in der Ablage den Kunden zur Rufnummer eines Anrufs. Die Datenbank - * sucht als Teilzeichenkette – abgefragt werden deshalb die letzten Ziffern, - * das genaue Passen prüft {@link sameNumber} hier im Browser. + * Sucht in der Ablage den Kunden oder Kurier zur Rufnummer eines Anrufs. Die + * Datenbank sucht als Teilzeichenkette – abgefragt werden deshalb die letzten + * Ziffern, das genaue Passen prüft {@link sameNumber} hier im Browser. */ -export async function findCustomerByNumber(number: string): Promise { +export async function findCallerByNumber(number: string): Promise { const digits = significantDigits(number) if (!digits) return null const entries = await fetchAddressEntries(digits.slice(-9)) - return pickCustomer(entries, number) + return pickCaller(entries, number) } /** diff --git a/frontend/src/components/CustomerCallPopup.tsx b/frontend/src/components/CustomerCallPopup.tsx index 38749fe..7687b2e 100644 --- a/frontend/src/components/CustomerCallPopup.tsx +++ b/frontend/src/components/CustomerCallPopup.tsx @@ -3,7 +3,7 @@ import type { StoredAddress } from '../addresses' import { fetchJob } from '../jobs' interface Props { - /** Der Kunde aus der Adress-Ablage, dessen Rufnummer zum Anruf passt. */ + /** Der Kunde oder Kurier aus der Adress-Ablage, dessen Rufnummer zum Anruf passt. */ customer: StoredAddress /** * Nimmt den Anruf an; solange er klingelt gesetzt, danach `undefined` – @@ -14,8 +14,8 @@ interface Props { } /** - * Popup zu einem eingehenden Anruf, dessen Rufnummer zu einem Kunden in der - * Adress-Ablage gehört: zeigt die Kundendaten, dazu je Job-Kennung des Kunden + * Popup zu einem eingehenden Anruf: zeigt die Daten des Anrufers aus der + * Adress-Ablage (Kunde oder Kurier), dazu je Job-Kennung des Eintrags * (`job_ids`) einen Knopf, der den Job aus der Ablage holt und dessen * Sprungadresse (`url`) in einem neuen Tab öffnet. Gleiches Overlay wie der * Wartedialog (siehe LoadingDialog zur Begründung gegen ``). diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 75c3553..b619902 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -5,7 +5,7 @@ import { useCallNotifications } from '../hooks/useCallNotifications' import { useWebhook } from '../hooks/useWebhook' import { countUnseen } from '../webhook' import { applyBrand, loadBrand, type Brand } from '../brand' -import { findCustomerByNumber, type StoredAddress } from '../addresses' +import { findCallerByNumber, type StoredAddress } from '../addresses' import StatusBadge from '../components/StatusBadge' import CustomerCallPopup from '../components/CustomerCallPopup' import StadtboteSignet from '../components/StadtboteSignet' @@ -53,9 +53,9 @@ export default function HomePage() { const { permission, requestPermission } = useCallNotifications(ringingCall) // Meldet SwyxIt! einen eingehenden Anruf, geht das Popup auf – mit den - // Kundendaten, wenn die Rufnummer in der Adress-Ablage einem Kunden gehört, - // sonst nur mit der Rufnummer. Je Anruf nur eine Abfrage – der Snapshot - // kommt mehrfach. + // Daten aus der Adress-Ablage, wenn die Rufnummer einem Kunden oder Kurier + // gehört, sonst nur mit der Rufnummer. Je Anruf nur eine Abfrage – der + // Snapshot kommt mehrfach. const [caller, setCaller] = useState(null) const lookedUpRef = useRef(null) useEffect(() => { @@ -70,7 +70,7 @@ export default function HomePage() { if (lookedUpRef.current === key) return lookedUpRef.current = key let stale = false - findCustomerByNumber(number) + findCallerByNumber(number) .then((match) => { if (!stale) setCaller(match ?? { number }) })