Anruf-Popup auch für Kuriere: Rufnummernsuche findet Kunden und Kuriere

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 15:21:31 +02:00
co-authored by Claude Fable 5.1
parent ab5e402ee9
commit afd6d03a71
4 changed files with 49 additions and 27 deletions
+26 -8
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest' 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 * SwyxIt! meldet Rufnummern mit Amtsholung ("001602107449"), die Webhooks
@@ -32,7 +32,7 @@ describe('sameNumber', () => {
}) })
}) })
describe('pickCustomer', () => { describe('pickCaller', () => {
const entries: StoredAddress[] = [ const entries: StoredAddress[] = [
{ number: '5215', name: 'Abdul, Rokhsareh', description: 'Globales Telefonbuch' }, { number: '5215', name: 'Abdul, Rokhsareh', description: 'Globales Telefonbuch' },
{ {
@@ -43,21 +43,39 @@ describe('pickCustomer', () => {
cscId: 100164, cscId: 100164,
url: 'https://test.sb.assecutor.de/admin/tapi_wrapper.php?phoneNo=01602107449', 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', () => { 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?.name).toBe('SYSGEN GmbH')
expect(match?.url).toContain('tapi_wrapper') expect(match?.url).toContain('tapi_wrapper')
}) })
it('übergeht Telefonbuch-Einträge und Kuriere', () => { it('findet auch den Kurier zur gemeldeten Rufnummer', () => {
expect(pickCustomer(entries, '5215')).toBeNull() const match = pickCaller(entries, '+49171677xxxx')
expect(pickCustomer(entries, '+49171677xxxx')).toBeNull() 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', () => { it('liefert null, wenn keine Rufnummer passt', () => {
expect(pickCustomer(entries, '+49404711')).toBeNull() expect(pickCaller(entries, '+49404711')).toBeNull()
}) })
}) })
+15 -11
View File
@@ -63,25 +63,29 @@ export function sameNumber(a: string, b: string): boolean {
return left.slice(-tail) === right.slice(-tail) 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 { * Der erste Kunde oder Kurier aus `entries`, dessen Rufnummer passt Einträge
return ( * 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( entries.find(
(entry) => entry.role === 'customer' && entry.number && sameNumber(entry.number, number), (entry) => entry.role === role && entry.number && sameNumber(entry.number, number),
) ?? null )
) return withRole('customer') ?? withRole('courier') ?? null
} }
/** /**
* Sucht in der Ablage den Kunden zur Rufnummer eines Anrufs. Die Datenbank * Sucht in der Ablage den Kunden oder Kurier zur Rufnummer eines Anrufs. Die
* sucht als Teilzeichenkette abgefragt werden deshalb die letzten Ziffern, * Datenbank sucht als Teilzeichenkette abgefragt werden deshalb die letzten
* das genaue Passen prüft {@link sameNumber} hier im Browser. * Ziffern, das genaue Passen prüft {@link sameNumber} hier im Browser.
*/ */
export async function findCustomerByNumber(number: string): Promise<StoredAddress | null> { export async function findCallerByNumber(number: string): Promise<StoredAddress | null> {
const digits = significantDigits(number) const digits = significantDigits(number)
if (!digits) return null if (!digits) return null
const entries = await fetchAddressEntries(digits.slice(-9)) const entries = await fetchAddressEntries(digits.slice(-9))
return pickCustomer(entries, number) return pickCaller(entries, number)
} }
/** /**
@@ -3,7 +3,7 @@ import type { StoredAddress } from '../addresses'
import { fetchJob } from '../jobs' import { fetchJob } from '../jobs'
interface Props { 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 customer: StoredAddress
/** /**
* Nimmt den Anruf an; solange er klingelt gesetzt, danach `undefined` * 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 * Popup zu einem eingehenden Anruf: zeigt die Daten des Anrufers aus der
* Adress-Ablage gehört: zeigt die Kundendaten, dazu je Job-Kennung des Kunden * Adress-Ablage (Kunde oder Kurier), dazu je Job-Kennung des Eintrags
* (`job_ids`) einen Knopf, der den Job aus der Ablage holt und dessen * (`job_ids`) einen Knopf, der den Job aus der Ablage holt und dessen
* Sprungadresse (`url`) in einem neuen Tab öffnet. Gleiches Overlay wie der * Sprungadresse (`url`) in einem neuen Tab öffnet. Gleiches Overlay wie der
* Wartedialog (siehe LoadingDialog zur Begründung gegen `<dialog>`). * Wartedialog (siehe LoadingDialog zur Begründung gegen `<dialog>`).
+5 -5
View File
@@ -5,7 +5,7 @@ import { useCallNotifications } from '../hooks/useCallNotifications'
import { useWebhook } from '../hooks/useWebhook' import { useWebhook } from '../hooks/useWebhook'
import { countUnseen } from '../webhook' import { countUnseen } from '../webhook'
import { applyBrand, loadBrand, type Brand } from '../brand' 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 StatusBadge from '../components/StatusBadge'
import CustomerCallPopup from '../components/CustomerCallPopup' import CustomerCallPopup from '../components/CustomerCallPopup'
import StadtboteSignet from '../components/StadtboteSignet' import StadtboteSignet from '../components/StadtboteSignet'
@@ -53,9 +53,9 @@ export default function HomePage() {
const { permission, requestPermission } = useCallNotifications(ringingCall) const { permission, requestPermission } = useCallNotifications(ringingCall)
// Meldet SwyxIt! einen eingehenden Anruf, geht das Popup auf mit den // Meldet SwyxIt! einen eingehenden Anruf, geht das Popup auf mit den
// Kundendaten, wenn die Rufnummer in der Adress-Ablage einem Kunden gehört, // Daten aus der Adress-Ablage, wenn die Rufnummer einem Kunden oder Kurier
// sonst nur mit der Rufnummer. Je Anruf nur eine Abfrage der Snapshot // gehört, sonst nur mit der Rufnummer. Je Anruf nur eine Abfrage der
// kommt mehrfach. // Snapshot kommt mehrfach.
const [caller, setCaller] = useState<StoredAddress | null>(null) const [caller, setCaller] = useState<StoredAddress | null>(null)
const lookedUpRef = useRef<string | null>(null) const lookedUpRef = useRef<string | null>(null)
useEffect(() => { useEffect(() => {
@@ -70,7 +70,7 @@ export default function HomePage() {
if (lookedUpRef.current === key) return if (lookedUpRef.current === key) return
lookedUpRef.current = key lookedUpRef.current = key
let stale = false let stale = false
findCustomerByNumber(number) findCallerByNumber(number)
.then((match) => { .then((match) => {
if (!stale) setCaller(match ?? { number }) if (!stale) setCaller(match ?? { number })
}) })