/* ============================================================
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ışı */}
{/* Çağrı Kaydı */}
);
}
/* ── Yönlendirme seçici ── */
function RouteSelector({ type, target, ivrs, ivrFlows, queues, agents, qcScripts, onTypeChange, onTargetChange }) {
return (
);
}
/* ============================================================
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()}>
);
}
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 }; }
}