/* ============================================================ TrunksTab.jsx — SIP Trunk ve DID Hat Yönetimi ============================================================ */ window.TrunksTab = function TrunksTab({ toast }) { const { useState, useEffect, useCallback } = React; const [trunks, setTrunks] = useState([]); const [dids, setDids] = useState([]); const [ivrs, setIvrs] = useState([]); const [ivrFlows, setIvrFlows] = useState([]); const [queues, setQueues] = useState([]); const [agents, setAgents] = useState([]); const [qcScripts, setQcScripts] = useState([]); const [loading, setLoading] = useState(true); const [section, setSection] = useState('trunks'); // 'trunks' | 'dids' const [trunkModal, setTrunkModal] = useState(null); const [didModal, setDidModal] = useState(null); const [xmlPreview, setXmlPreview] = useState(null); const [testResult, setTestResult] = useState(null); const [saving, setSaving] = useState(false); /* ── Fetch ─────────────────────────────────────────────── */ const load = useCallback(async () => { setLoading(true); const [t, d, i, f, q, a, qc] = await Promise.all([ apiFetch('/api/trunks'), apiFetch('/api/trunks/dids'), apiFetch('/api/ivr'), apiFetch('/api/flows'), apiFetch('/api/queues'), apiFetch('/api/agents'), apiFetch('/api/qc/scripts'), ]); setTrunks(t || []); setDids(d || []); setIvrs(i || []); setIvrFlows((f || []).filter(fl => fl.status === 'published')); setQueues(q || []); setAgents(a || []); setQcScripts(qc || []); setLoading(false); }, []); useEffect(() => { load(); }, [load]); /* ── Trunk işlemleri ───────────────────────────────────── */ const saveTrunk = async (data) => { setSaving(true); const isEdit = !!data.id; const res = await apiFetch(isEdit ? `/api/trunks/${data.id}` : '/api/trunks', { method: isEdit ? 'PUT' : 'POST', body: JSON.stringify(data) }); setSaving(false); if (res?._error) { toast(res.detail || 'Trunk kaydedilemedi', 'error'); } else if (res?.id) { toast(isEdit ? 'Trunk güncellendi' : 'Trunk oluşturuldu', 'success'); setTrunkModal(null); load(); } else toast('Beklenmeyen bir hata oluştu', 'error'); }; const deleteTrunk = async (id) => { if (!confirm('Trunk silinsin mi?')) return; await apiFetch(`/api/trunks/${id}`, { method: 'DELETE' }); toast('Trunk silindi', 'success'); load(); }; const testTrunk = async (id, name) => { setTestResult({ loading: true, name }); const res = await apiFetch(`/api/trunks/${id}/test`, { method: 'POST' }); setTestResult({ ...res, name }); }; const previewXml = async (id, type) => { const url = type === 'trunk' ? `/api/trunks/${id}/xml-preview` : `/api/trunks/dids/${id}/xml-preview`; const res = await apiFetch(url); if (res?.xml) setXmlPreview({ xml: res.xml, title: type === 'trunk' ? 'Trunk Gateway XML' : 'DID Dialplan XML' }); }; /* ── DID işlemleri ─────────────────────────────────────── */ const saveDid = async (data) => { setSaving(true); const isEdit = !!data.id; const res = await apiFetch(isEdit ? `/api/trunks/dids/${data.id}` : '/api/trunks/dids', { method: isEdit ? 'PUT' : 'POST', body: JSON.stringify(data) }); setSaving(false); if (res?._error) { toast(res.detail || 'DID kaydedilemedi', 'error'); } else if (res?.id) { toast(isEdit ? 'DID güncellendi' : 'DID oluşturuldu', 'success'); setDidModal(null); load(); } else toast('Beklenmeyen bir hata oluştu', 'error'); }; const deleteDid = async (id) => { if (!confirm('DID numarası silinsin mi?')) return; await apiFetch(`/api/trunks/dids/${id}`, { method: 'DELETE' }); toast('DID silindi', 'success'); load(); }; /* ── Render ────────────────────────────────────────────── */ const STATUS_COLORS = { registered:'#10b981', no_register:'#6366f1', failed:'#ef4444', trying:'#f59e0b', unknown:'#9ca3af' }; const STATUS_LABELS = { registered:'Kayıtlı', no_register:'IP Tabanlı', failed:'Bağlantı Hatası', trying:'Bağlanıyor…', unknown:'Bilinmiyor' }; const ROUTE_ICONS = { ivr:'IVR', ivr_flow:'Akış', queue:'Kuyruk', agent:'AI', human:'Temsilci', qc_script:'KK' }; const routeLabel = (type, target) => { if (type === 'ivr') return `IVR: ${ivrs.find(i =>String(i.id) === String(target))?.name || 'IVR #'+target}`; if (type === 'ivr_flow') return `Akış: ${ivrFlows.find(f =>String(f.id) === String(target))?.name || 'Akış #'+target}`; if (type === 'queue') return `Kuyruk: ${queues.find(q =>q.extension === target)?.name || 'Kuyruk '+target}`; if (type === 'human') return `Temsilci: ${target}`; if (type === 'qc_script') return `KK: ${qcScripts.find(s =>String(s.id) === String(target))?.name || 'Script #'+target}`; return `AI: ${target}`; }; return (
{/* ── Sayfa başlığı ── */}

Hat Yönetimi

SIP Trunk bağlantıları ve DID numara yönlendirmeleri

{loading ?
Yükleniyor…
: ( section === 'trunks' ? ( /* ════ TRUNK SECTION ════ */
{trunks.length === 0 ? : (
{trunks.map(t => (
{/* Sol: Bilgiler */}
{t.name} {t.provider && {t.provider}} {t.direction==='both' ? '↕ İki Yönlü' : t.direction==='inbound' ? '↓ Gelen' : '↑ Giden'} {!t.is_active && Pasif}
{/* SIP Bilgileri */}
{/* Bağlı DID'ler */} {(() => { const bDids = dids.filter(d =>d.trunk_id === t.id); return bDids.length > 0 ? (
{bDids.map(d => ( {d.number} ))}
) : null; })()}
{/* Sağ: Butonlar */}
))}
)}
) : ( /* ════ DID SECTION ════ */
{dids.length === 0 ? : (
{dids.map(d => (
{d.number}
{d.description &&
{d.description}
}
Ana Yönlendirme: {routeLabel(d.route_type, d.route_target)}
{d.off_hours_route_type && (
Mesai dışı: {routeLabel(d.off_hours_route_type, d.off_hours_route_target)}
)} {d.trunk_name && (
Trunk: {d.trunk_name}
)}
))}
)}
) )} {/* ── Modals ── */} {trunkModal !== null && ( setTrunkModal(null)} saving={saving} /> )} {didModal !== null && ( setDidModal(null)} saving={saving} /> )} {xmlPreview && ( setXmlPreview(null)} /> )} {testResult && ( setTestResult(null)} statusColors={STATUS_COLORS} statusLabels={STATUS_LABELS} /> )}
); }; /* ============================================================ TRUNK MODAL ============================================================ */ function TrunkModal({ initial, onSave, onClose, saving }) { const { useState } = React; const [tab, setTab] = useState('basic'); const [form, setForm] = useState({ name:'', slug:'', description:'', provider:'', host:'', port:5060, transport:'udp', username:'', password:'', realm:'', register:true, direction:'both', caller_id_number:'', caller_id_name:'', outbound_prefix:'', max_channels:30, codec_string:'PCMU,PCMA,G729,opus', dtmf_mode:'rfc2833', ping_sec:25, extra_params:[], is_active:true, ...initial }); const set = (k, v) =>setForm(f => ({ ...f, [k]: v })); // Slug otomatik üret const handleNameChange = (v) => { set('name', v); if (!form.id) set('slug', v.toLowerCase().replace(/[^a-z0-9]/g,'_').replace(/__+/g,'_')); }; const TABS = [{ id:'basic', label:'Temel' }, { id:'auth', label:'Auth' }, { id:'routing', label:'Yönlendirme' }, { id:'advanced', label:'Gelişmiş' }]; return (
{TABS.map(t => ( ))}
{tab === 'basic' && (<> handleNameChange(e.target.value)} placeholder="Turk Telekom SIP Trunk" /> set('slug', e.target.value)} style={{ fontFamily:'monospace' }} placeholder="turk_telekom" />
Dosyada kullanılır: external/{form.slug || 'trunk_adi'}.xml
set('provider', e.target.value)} placeholder="Turk Telekom, Vodafone…" /> set('description', e.target.value)} />
set('host', e.target.value)} placeholder="sip.provider.com veya 1.2.3.4" style={{ fontFamily:'monospace' }} /> set('port', parseInt(e.target.value))} style={{ width:90 }} />
)} {tab === 'auth' && (<>
IP Tabanlı Trunk — Kullanıcı adı/şifre gerektirmeyen trunk'lar için "Kayıt Yap"ı kapatın.
{form.register && (<> set('username', e.target.value)} style={{ fontFamily:'monospace' }} placeholder="1001 veya username" /> set('password', e.target.value)} /> set('realm', e.target.value)} style={{ fontFamily:'monospace' }} placeholder="sip.provider.com" /> )} )} {tab === 'routing' && (<> set('caller_id_number', e.target.value)} style={{ fontFamily:'monospace' }} placeholder="+905321234567" /> set('caller_id_name', e.target.value)} placeholder="Şirket Adı" /> set('outbound_prefix', e.target.value)} placeholder="9 (dışarı çevirirken 9 ile başla)" /> set('max_channels', parseInt(e.target.value))} /> )} {tab === 'advanced' && (<> set('codec_string', e.target.value)} style={{ fontFamily:'monospace' }} placeholder="PCMU,PCMA,G729,opus" /> set('ping_sec', parseInt(e.target.value))} /> )}
); } /* ============================================================ DID MODAL ============================================================ */ function DidModal({ initial, trunks, ivrs, ivrFlows, queues, agents, qcScripts, onSave, onClose, saving }) { const { useState } = React; const [form, setForm] = useState({ number:'', description:'', trunk_id:null, route_type:'ivr', route_target:'', off_hours_route_type:null, off_hours_route_target:'', recording_enabled:true, recording_stereo:false, recording_path:'', is_active:true, ...initial }); const set = (k, v) =>setForm(f => ({ ...f, [k]: v })); return ( set('number', e.target.value)} style={{ fontFamily:'monospace', fontSize:18 }} placeholder="+905321234567 veya 08501234567" />
Operatörden gelen çağrıdaki hedef numara formatında girin
set('description', e.target.value)} placeholder="Satış Hattı, Destek Hattı…" /> {/* Ana Yönlendirme */}
Ana Yönlendirme (Mesai Saatleri)
set('route_type', v)} onTargetChange={v =>set('route_target', v)} />
{/* Mesai Dışı */}
Mesai Dışı Yönlendirme (Opsiyonel)
{form.off_hours_route_type && ( set('off_hours_route_type', v)} onTargetChange={v =>set('off_hours_route_target', v)} /> )}
{/* Çağrı Kaydı */}
Çağrı Kaydı
{form.recording_enabled !== false && ( <>
set('recording_path', e.target.value)} placeholder="/usr/local/freeswitch/recordings/${strftime(%Y-%m-%d-%H-%M-%S)}_${caller_id_number}.wav" />
)}
); } /* ── Yönlendirme seçici ── */ function RouteSelector({ type, target, ivrs, ivrFlows, queues, agents, qcScripts, onTypeChange, onTargetChange }) { return (
{type === 'ivr' ? ( ) : type === 'ivr_flow' ? ( ) : type === 'queue' ? ( ) : type === 'agent' ? ( ) : type === 'qc_script' ? ( ) : ( onTargetChange(e.target.value)} placeholder="Temsilci ext: 9001" style={{ fontFamily:'monospace' }} /> )}
); } /* ============================================================ XML PREVIEW MODAL ============================================================ */ function XmlPreviewModal({ title, xml, onClose }) { return (
{xml}
); } /* ============================================================ TEST RESULT MODAL ============================================================ */ function TestResultModal({ result, onClose, statusColors, statusLabels }) { if (result.loading) return (
Trunk test ediliyor…
); const color = statusColors[result.status] || '#9ca3af'; return (
{result.status === 'registered' ? 'Basarili' : result.status === 'failed' ? 'Basarisiz' : 'Uyari'}
{statusLabels[result.status] || result.status}
{result.raw && (
          {result.raw}
        
)}
); } /* ============================================================ SHARED HELPERS ============================================================ */ function ModalOverlay({ children, onClose, title, width=600 }) { return (
e.target===e.currentTarget && onClose()}>

{title}

{children}
); } function FRow({ label, children }) { return (
{children}
); } function Badge({ children, color='#6b7280' }) { return ( {children} ); } function InfoRow({ icon, label, value, mono }) { return (
{icon} {label}: {value}
); } function EmptyState({ icon, text, sub }) { return (
{icon &&
{icon}
}
{text}
{sub &&
{sub}
}
); } async function apiFetch(url, opts = {}) { // Content-Type header'ını her zaman ekle (body varsa) if (opts.body && !opts.headers?.['Content-Type']) { opts.headers = { 'Content-Type': 'application/json', ...(opts.headers || {}) }; } async function _parseError(res) { try { const body = await res.json(); return body.detail || body.message || null; } catch { return null; } } if (window.authFetch) { try { const res = await window.authFetch(url, opts); if (!res.ok) { const detail = await _parseError(res); return { _error: true, detail }; } return await res.json(); } catch { return { _error: true, detail: null }; } } const token = localStorage.getItem('ai_cc_access_token'); try { const res = await fetch(url, { headers: { 'Content-Type':'application/json', 'Authorization':`Bearer ${token}`, ...(opts.headers||{}) }, ...opts, }); if (!res.ok) { const detail = await _parseError(res); return { _error: true, detail }; } return await res.json(); } catch { return { _error: true, detail: null }; } }