import { toast } from 'bulma-toast' /** * Click-to-dial (FreePBX/Asterisk) client gate + action. * * `capability` is resolved ONCE per session from /api/telephony/me (server- * authoritative: system user + SIP_Extension) and shared through useState, so * the many s on a page cost no extra requests. `dial()` POSTs to * /api/telephony/dial, which returns as soon as the PBX accepted the call — * the user's own phone rings first, then the target is dialed. */ export interface DialCapability { loaded: boolean enabled: boolean canDial: boolean extension: string | null } export interface DialTarget { number: string /** AD_User the number belongs to → the call activity is written on this user. */ userId?: number | string | null /** Fallback: C_BPartner → the activity goes to the partner's first contact user. */ partnerId?: number | string | null /** Display name shown on the agent's phone + in the activity. */ label?: string | null /** Per-call extension override (dial popover chooser); default = the user's own. */ extension?: string | null } export interface DirectoryExtension { ext: string; state: string; stateLabel: string; pbxName: string; name: string; userId: number | null; isMine: boolean } /** Same rules as the server (callTracker.normalizePhoneNumber) — used for the visibility check. */ export const normalizeDialNumber = (raw: any): string => { let s = String(raw ?? '').trim() if (!s) return '' if (/^\*\d{2,3}$/.test(s)) return s s = s.replace(/\(0\)/g, '').replace(/^\+/, '00').replace(/[\s\-/().]/g, '') return /^\d{3,20}$/.test(s) ? s : '' } export const useDialCapability = () => useState('dialCapability', () => ({ loaded: false, enabled: false, canDial: false, extension: null })) let loadingPromise: Promise | null = null export const useClickToDial = () => { const capability = useDialCapability() const config = useRuntimeConfig() const { t } = useTranslation() const load = async (force = false) => { if (import.meta.server) return if (config.public.clickToDialEnabled !== true) { capability.value = { loaded: true, enabled: false, canDial: false, extension: null }; return } if (capability.value.loaded && !force) return if (loadingPromise && !force) return loadingPromise loadingPromise = (async () => { try { const res: any = await $fetch('/api/telephony/me', { headers: useRequestHeaders(['cookie']) }) capability.value = { loaded: true, enabled: res?.enabled === true, canDial: res?.canDial === true, extension: res?.extension || null } } catch { capability.value = { loaded: true, enabled: false, canDial: false, extension: null } } finally { loadingPromise = null } })() return loadingPromise } const canDial = computed(() => capability.value.canDial === true) const dialing = ref(false) /** PBX extension directory (cached per session in state; refreshed on demand). */ const directory = useState<{ loaded: boolean; records: DirectoryExtension[] }>('dialDirectory', () => ({ loaded: false, records: [] })) const loadDirectory = async (force = false) => { if (directory.value.loaded && !force) return directory.value.records try { const res: any = await $fetch('/api/telephony/extensions', { headers: useRequestHeaders(['cookie']) }) directory.value = { loaded: true, records: Array.isArray(res?.records) ? res.records : [] } } catch { directory.value = { loaded: true, records: [] } } return directory.value.records } const dial = async (target: DialTarget) => { const number = normalizeDialNumber(target?.number) if (!number) { toast({ message: t.value?.dial_invalid_number || 'Ungültige Telefonnummer', type: 'is-warning', duration: 2500 }) return null } if (dialing.value) return null dialing.value = true try { const res: any = await $fetch('/api/telephony/dial', { method: 'POST', headers: useRequestHeaders(['cookie']), body: { number, targetUserId: target?.userId || null, targetPartnerId: target?.partnerId || null, targetLabel: target?.label || '', ...(target?.extension ? { extension: String(target.extension) } : {}) } }) if (Number(res?.status) === 200) { const who = target?.label ? `${target.label} (${number})` : number const ext = res?.extension ? ` [${t.value?.dial_extension_short || 'Nst.'} ${res.extension}]` : '' toast({ message: `${t.value?.dial_ringing || 'Ihr Telefon klingelt – Anruf an'} ${who}${ext}`, type: 'is-success', duration: 4000 }) return res } toast({ message: res?.message || t.value?.dial_failed || 'Anruf fehlgeschlagen', type: 'is-danger', duration: 4000 }) return res } catch (err: any) { const status = Number(err?.status || err?.statusCode || 0) if (status === 403) capability.value = { ...capability.value, canDial: false } toast({ message: err?.data?.statusMessage || err?.data?.message || t.value?.dial_failed || 'Anruf fehlgeschlagen', type: 'is-danger', duration: 4000 }) return null } finally { dialing.value = false } } return { capability, canDial, dialing, load, dial, normalizeDialNumber, directory, loadDirectory } }