/* ============================================================
WebServicesTab.jsx — Web Servis Entegrasyonu Yönetim Ekranı
============================================================ */
window.WebServicesTab = function WebServicesTab({ toast }) {
const { useState, useEffect, useCallback } = React;
/* ── State ─────────────────────────────────────────────── */
const [authList, setAuthList] = useState([]);
const [serviceList, setServiceList] = useState([]);
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(true);
// Modal state
const [authModal, setAuthModal] = useState(null); // null | 'new' | {id,...}
const [svcModal, setSvcModal] = useState(null); // null | 'new' | {id,...}
const [testPanel, setTestPanel] = useState(null); // null | service obj
const [authTestPanel, setAuthTestPanel] = useState(null); // null | auth obj
const [logsPanel, setLogsPanel] = useState(null); // null | service obj
const [activeSection, setActiveSection] = useState('services'); // 'services' | 'auth'
/* ── Fetch ─────────────────────────────────────────────── */
const load = useCallback(async () => {
setLoading(true);
const [a, s] = await Promise.all([
apiFetch('/api/ws/auth'),
apiFetch('/api/ws/services'),
]);
setAuthList(a || []);
setServiceList(s || []);
setLoading(false);
}, []);
useEffect(() => { load(); }, [load]);
/* ── Auth CRUD ─────────────────────────────────────────── */
const saveAuth = async (data) => {
const isEdit = !!data.id;
const url = isEdit ? `/api/ws/auth/${data.id}` : '/api/ws/auth';
const method = isEdit ? 'PUT' : 'POST';
const res = await apiFetch(url, { method, body: JSON.stringify(data) });
if (res?.id) {
toast(isEdit ? 'Auth güncellendi' : 'Auth oluşturuldu', 'success');
setAuthModal(null);
load();
}
};
const deleteAuth = async (id) => {
if (!confirm('Auth konfigürasyonu silinsin mi?')) return;
await apiFetch(`/api/ws/auth/${id}`, { method: 'DELETE' });
toast('Auth silindi', 'success');
load();
};
/* ── Service CRUD ──────────────────────────────────────── */
const saveSvc = async (data) => {
const isEdit = !!data.id;
const url = isEdit ? `/api/ws/services/${data.id}` : '/api/ws/services';
const method = isEdit ? 'PUT' : 'POST';
const res = await apiFetch(url, { method, body: JSON.stringify(data) });
if (res?.id) {
toast(isEdit ? 'Servis güncellendi' : 'Servis oluşturuldu', 'success');
setSvcModal(null);
load();
}
};
const deleteSvc = async (id) => {
if (!confirm('Servis konfigürasyonu silinsin mi?')) return;
await apiFetch(`/api/ws/services/${id}`, { method: 'DELETE' });
toast('Servis silindi', 'success');
load();
};
const toggleActive = async (svc) => {
await apiFetch(`/api/ws/services/${svc.id}`, {
method: 'PUT',
body: JSON.stringify({ ...svc, is_active: !svc.is_active }),
});
load();
};
/* ── Logs ──────────────────────────────────────────────── */
const openLogs = async (svc) => {
setLogsPanel(svc);
const data = await apiFetch(`/api/ws/logs?svc_id=${svc.id}&limit=50`);
setLogs(data || []);
};
/* ── Render ────────────────────────────────────────────── */
const AUTH_TYPE_LABELS = {
oauth2_client_credentials: 'OAuth2 Client Credentials',
oauth2_password: 'OAuth2 Password',
api_key_header: 'API Key (Header)',
api_key_query: 'API Key (Query)',
basic_auth: 'Basic Auth',
bearer_static: 'Static Bearer Token',
custom_header: 'Custom Header',
none: 'Auth Yok',
};
const METHOD_COLORS = { GET:'#10b981', POST:'#6366f1', PUT:'#f59e0b', PATCH:'#8b5cf6' };
const CT_LABELS = { json:'REST/JSON', soap:'SOAP/XML', xml:'REST/XML', form:'Form', multipart:'Multipart' };
return (
{/* ── Page Header ── */}
Web Servis Entegrasyonları
IVR akışlarından dış servislere bağlantı kurun — kod yazmadan
setActiveSection('services')}
>Servisler ({serviceList.length})
setActiveSection('auth')}
>Auth Konfigürasyonları ({authList.length})
{loading ? (
Yükleniyor…
) : activeSection === 'auth' ? (
/* ════════════ AUTH SECTION ════════════ */
setAuthModal({})}>
+ Yeni Auth Konfigürasyonu
{authList.length === 0 ? (
) : (
{authList.map(a => (
setAuthModal(a)}
onDelete={() =>deleteAuth(a.id)}
onTest={() =>setAuthTestPanel(a)}
labels={AUTH_TYPE_LABELS}
/>
))}
)}
) : (
/* ════════════ SERVICES SECTION ════════════ */
setSvcModal({})}>
+ Yeni Servis
{serviceList.length === 0 ? (
) : (
{serviceList.map(s => (
a.id === s.auth_config_id)?.name}
methodColors={METHOD_COLORS}
ctLabels={CT_LABELS}
onEdit={() =>setSvcModal(s)}
onDelete={() =>deleteSvc(s.id)}
onTest={() =>setTestPanel(s)}
onLogs={() =>openLogs(s)}
onToggle={() =>toggleActive(s)}
/>
))}
)}
)}
{/* ── Modals ── */}
{authModal !== null && (
setAuthModal(null)}
typeLabels={AUTH_TYPE_LABELS}
/>
)}
{svcModal !== null && (
setSvcModal(null)}
/>
)}
{testPanel && (
setTestPanel(null)}
toast={toast}
/>
)}
{authTestPanel && (
setAuthTestPanel(null)}
toast={toast}
/>
)}
{logsPanel && (
{ setLogsPanel(null); setLogs([]); }}
/>
)}
);
};
/* ============================================================
AUTH CARD
============================================================ */
function AuthCard({ auth, onEdit, onDelete, onTest, labels }) {
const TYPE_ICONS = {
oauth2_client_credentials: 'OAuth2',
oauth2_password: 'OAuth2',
api_key_header: 'Key',
api_key_query: 'Key',
basic_auth: 'Basic',
bearer_static: 'Bearer',
custom_header: 'Custom',
none: 'None',
};
return (
{auth.name}
{labels[auth.auth_type] || auth.auth_type}
{auth.description && (
{auth.description}
)}
{auth.token_url && (
{auth.token_url}
)}
Test Et
Düzenle
Sil
);
}
/* ============================================================
SERVICE CARD
============================================================ */
function ServiceCard({ svc, authName, methodColors, ctLabels, onEdit, onDelete, onTest, onLogs, onToggle }) {
return (
{/* Sol: Bilgiler */}
{svc.name}
{/* Method badge */}
{svc.method}
{/* Content type badge */}
{ctLabels[svc.content_type] || svc.content_type}
{/* Auth badge */}
{authName && (
{authName}
)}
{svc.trigger_on_call_start && !svc.background_lookup && (
Çağrı başı sorgula
)}
{svc.trigger_on_call_start && svc.background_lookup && (
Arka planda sorgula
)}
{!svc.is_active && (
Pasif
)}
{/* URL */}
{svc.url}
{/* Response mappings özeti */}
{svc.response_mappings?.length > 0 && (
{svc.response_mappings.map((m, i) => (
{m.path} → {m.variable}
))}
)}
{svc.description && (
{svc.description}
)}
{/* Sağ: Butonlar */}
Test Et
Düzenle
Loglar
{svc.is_active ? 'Pasif' : 'Aktif'}
Sil
);
}
/* ============================================================
AUTH MODAL
============================================================ */
function AuthModal({ initial, onSave, onClose, typeLabels }) {
const { useState } = React;
const [form, setForm] = useState({
name: '', description: '', auth_type: 'oauth2_client_credentials',
token_url: '', client_id: '', client_secret: '', scope: '',
username: '', password: '',
api_key: '', api_key_header_name: 'X-API-Key', api_key_query_name: 'api_key',
static_token: '',
custom_headers: [],
token_ttl_override_sec: '',
is_active: true,
...initial
});
const set = (k, v) =>setForm(f => ({ ...f, [k]: v }));
const atype = form.auth_type;
return (
set('name', e.target.value)} placeholder="Şirket CRM Token" />
set('description', e.target.value)} />
set('auth_type', e.target.value)}>
{Object.entries(typeLabels).map(([k,v]) => {v} )}
{/* OAuth2 alanları */}
{(atype === 'oauth2_client_credentials' || atype === 'oauth2_password') && (<>
set('token_url', e.target.value)} placeholder="https://api.sirket.com/oauth/token" />
set('client_id', e.target.value)} />
set('client_secret', e.target.value)} />
set('scope', e.target.value)} placeholder="read write" />
>)}
{/* Password ek alanlar */}
{(atype === 'oauth2_password' || atype === 'basic_auth') && (<>
set('username', e.target.value)} />
set('password', e.target.value)} />
>)}
{/* API Key */}
{atype === 'api_key_header' && (<>
set('api_key_header_name', e.target.value)} />
set('api_key', e.target.value)} />
>)}
{atype === 'api_key_query' && (<>
set('api_key_query_name', e.target.value)} />
set('api_key', e.target.value)} />
>)}
{/* Static Bearer */}
{atype === 'bearer_static' && (
)}
{/* Token TTL override */}
{(atype === 'oauth2_client_credentials' || atype === 'oauth2_password') && (
set('token_ttl_override_sec', e.target.value ? parseInt(e.target.value) : null)} placeholder="3600" />
)}
İptal
onSave(form)}>Kaydet
);
}
/* ============================================================
SERVICE MODAL — sekmeli yapı
============================================================ */
function ServiceModal({ initial, authList, ctLabels, onSave, onClose }) {
const { useState, useRef } = React;
const [tab, setTab] = useState('basic');
const [form, setForm] = useState({
name: '', description: '',
url: '', method: 'POST', content_type: 'json',
request_body_template: '', query_params_template: null,
extra_headers: null, soap_action: '',
auth_config_id: null,
response_mappings: [],
timeout_sec: 5, on_error: 'continue', on_error_transfer_ext: '',
success_status_codes: null, log_enabled: true,
trigger_on_call_start: false, background_lookup: false, prompt_context_template: '',
allow_private_network: false, is_active: true,
...initial,
response_mappings: initial?.response_mappings || [],
});
// Değişken paneli için son odaklanan alan
const lastFocusedRef = useRef(null);
const trackFocus = (e) => { lastFocusedRef.current = e.target; };
const onInsertVar = (name) => {
const el = lastFocusedRef.current;
if (el && window.insertAtCursor) window.insertAtCursor(el, `{${name}}`);
};
const set = (k, v) =>setForm(f => ({ ...f, [k]: v }));
/* Response mapping yönetimi */
const addMapping = () =>set('response_mappings', [...form.response_mappings, { path:'', variable:'', default:'' }]);
const updMapping = (i, k, v) =>set('response_mappings', form.response_mappings.map((m, idx) =>idx===i ? {...m,[k]:v} : m));
const delMapping = (i) =>set('response_mappings', form.response_mappings.filter((_,idx) =>idx!==i));
/* Extra headers (JSON edit) */
const [headersJson, setHeadersJson] = useState(
form.extra_headers ? JSON.stringify(form.extra_headers, null, 2) : ''
);
const [queryJson, setQueryJson] = useState(
form.query_params_template ? JSON.stringify(form.query_params_template, null, 2) : ''
);
const handleSave = () => {
const data = { ...form };
// JSON alanlarını parse et
try { data.extra_headers = headersJson.trim() ? JSON.parse(headersJson) : null; } catch { alert('Extra Headers geçersiz JSON'); return; }
try { data.query_params_template = queryJson.trim() ? JSON.parse(queryJson) : null; } catch { alert('Query Params geçersiz JSON'); return; }
if (data.auth_config_id === '' || data.auth_config_id === 'null') data.auth_config_id = null;
if (data.auth_config_id) data.auth_config_id = parseInt(data.auth_config_id);
onSave(data);
};
const TABS = [
{ id:'basic', label:'Temel' },
{ id:'request', label:'İstek' },
{ id:'mapping', label:'Yanıt Mapping' },
{ id:'behavior', label:'Davranış' },
];
return (
{/* Sekmeler */}
{TABS.map(t => (
setTab(t.id)}
style={{
padding:'5px 14px', borderRadius:8, border:'none', cursor:'pointer', fontSize:13,
background: tab===t.id ? 'var(--primary)' : 'transparent',
color: tab===t.id ? '#fff' : 'var(--text-dim)', fontWeight: tab===t.id ? 600 : 400
}}
>{t.label}
))}
{/* ── Temel ── */}
{tab === 'basic' && (<>
set('name', e.target.value)} placeholder="Müşteri Sorgulama" />
set('description', e.target.value)} />
set('url', e.target.value)}
onFocus={trackFocus}
placeholder="https://api.sirket.com/customers/{caller_id}" />
{'{caller_id}'}gibi IVR değişkenlerini kullanabilirsiniz
{window.VariableHintPanel && (
)}
set('method', e.target.value)}>
{['GET','POST','PUT','PATCH'].map(m => {m} )}
set('content_type', e.target.value)}>
{Object.entries(ctLabels).map(([k,v]) => {v} )}
set('timeout_sec', parseInt(e.target.value))} />
set('auth_config_id', e.target.value || null)}>
— Auth Yok —
{authList.map(a => {a.name} )}
{form.content_type === 'soap' && (
set('soap_action', e.target.value)} placeholder="urn:GetCustomer" />
)}
set('trigger_on_call_start', e.target.checked)}
/>
Çağrı gelir gelmez caller_id ile otomatik sorgula
{form.trigger_on_call_start && (
Bu servis çağrı başlar başlamaz caller_id ile otomatik çalışır.
Yanıt Mapping'de tanımladığınız değişkenler AI asistanın sistemine eklenir.
{!form.background_lookup && <> customer_name değişkeni varsa karşılama mesajında isim kullanılır.>}
)}
{form.trigger_on_call_start && (
set('background_lookup', e.target.checked)}
/>
Hoşgeldin mesajı oynatılırken arka planda sorgu yap (hız kazanımı)
{form.background_lookup ? (
Hoşgeldin mesajı ve WS sorgusu aynı anda çalışır. Müşteri konuşmaya başlamadan önce sonuç hazır olur.
Karşılama metninde müşteri adı kullanılamaz; ancak konuşma boyunca tüm WS verileri AI'a aktarılır.
) : (
WS yanıtı beklendikten sonra hoşgeldin oynatılır. Müşteri adını karşılama metninde kullanabilirsiniz.
)}
)}
{form.trigger_on_call_start && (
)}
>)}
{/* ── İstek ── */}
{tab === 'request' && (<>
{window.VariableHintPanel && (
Değişken ekleneceği alana önce tıklayın (imleç konumu), sonra yukarıdan değişkene tıklayın.
)}
{form.method !== 'GET' && (
)}
>)}
{/* ── Yanıt Mapping ── */}
{tab === 'mapping' && (
REST/JSON için JSONPath ($.data.name) | SOAP/XML için XPath (//CustomerName/text())
+ Mapping Ekle
{form.response_mappings.length === 0 ? (
Henüz mapping yok. Yanıttan IVR değişkenine veri aktarmak için ekleyin.
) : (
)}
{/* Örnek */}
Örnek Kullanım
REST/JSON yanıtı:
{`{
"data": {
"name": "Ahmet Yılmaz",
"segment": "VIP"
}
}`}
Mapping:
{`path: $.data.name
variable: crm_name
path: $.data.segment
variable: crm_segment`}
)}
{/* ── Davranış ── */}
{tab === 'behavior' && (<>
set('on_error', e.target.value)}>
Akışa devam et
Çağrıyı kapat
Extension'a transfer et
{form.on_error === 'transfer' && (
set('on_error_transfer_ext', e.target.value)} placeholder="8001" />
)}
set('log_enabled', e.target.checked)} />
Her çağrıyı logla (KVKK hassas veri içeriyorsa kapatın)
set('allow_private_network', e.target.checked)} style={{ marginTop: 3 }} />
Private/localhost adreslerine izin ver
Yalnızca test/dev ortamı için. Production'da KAPALI tutun — aksi halde dahili ağ adreslerine SSRF saldırısına açık olur.
set('is_active', e.target.checked)} />
Servis aktif
>)}
İptal
Kaydet
);
}
/* ============================================================
TEST PANEL
============================================================ */
function TestPanel({ svc, onClose, toast }) {
const { useState } = React;
const [vars, setVars] = useState([{ key:'caller_id', value:'' }]);
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const addVar = () =>setVars(v => [...v, { key:'', value:'' }]);
const updVar = (i, k, v) =>setVars(list =>list.map((row, idx) =>idx===i ? {...row,[k]:v} : row));
const delVar = (i) =>setVars(list =>list.filter((_,idx) =>idx!==i));
const run = async () => {
setLoading(true);
setResult(null);
const variables = {};
vars.forEach(({ key, value }) => { if (key.trim()) variables[key.trim()] = value; });
const res = await apiFetch(`/api/ws/test/${svc.id}`, {
method: 'POST',
body: JSON.stringify({ call_uuid:'TEST', variables }),
});
setResult(res);
setLoading(false);
if (res?.success) toast('Test başarılı ', 'success');
else toast('Test başarısız ', 'error');
};
return (
IVR'dan gönderilecek kanal değişkenlerini girin ve servisi test edin.
{/* Değişkenler */}
{loading ? 'Çağrılıyor…' : 'Test Çalıştır'}
{/* Sonuç */}
{result && (
{result.success ? '✅' : '❌'}
{result.success ? 'Başarılı' : 'Başarısız'}
{result.response_status && (
HTTP {result.response_status}
)}
{result.error && ({result.error}) }
{/* Auth Bilgisi */}
{result.auth_info && (
AUTH SERVİSİ
Konfigürasyon
{result.auth_info.name}
Tip
{result.auth_info.type}
{result.auth_info.token_url && (
Token URL
{result.auth_info.token_url}
)}
Durum
{result.auth_info.status === 'cache_hit' && (
Önbellekten kullanıldı
)}
{result.auth_info.status === 'fetched' && (
Token alındı
)}
{result.auth_info.status === 'static' && (
Statik kimlik
)}
Süre
{result.auth_info.duration_ms} ms
)}
{/* REQUEST */}
{(result.request_url || result.request_headers || result.request_body) && (
REQUEST
{result.request_url && (
{svc.method || 'GET'}
{result.request_url}
)}
{result.request_headers && Object.keys(result.request_headers).length > 0 && (
HEADERS
{Object.entries(result.request_headers).map(([k, v]) => `${k}: ${v}`).join('\n')}
)}
{result.request_body && (
BODY
{result.request_body}
)}
)}
{result.success && Object.keys(result.variables || {}).length > 0 && (
Eşlenen Değişkenler:
{Object.entries(result.variables).map(([k, v]) => (
{k}
{v || boş }
))}
)}
{result.success && Object.keys(result.variables || {}).length === 0 && (
Servis yanıt verdi fakat eşlenen değişken yok. Mapping ayarlarını kontrol edin.
)}
)}
);
}
/* ============================================================
AUTH TEST PANEL
============================================================ */
function AuthTestPanel({ auth, onClose, toast }) {
const { useState } = React;
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const run = async () => {
setLoading(true);
setResult(null);
const res = await apiFetch(`/api/ws/auth/test/${auth.id}`, { method: 'POST' });
setResult(res);
setLoading(false);
if (res?.success) toast('Auth testi başarılı', 'success');
else toast('Auth testi başarısız', 'error');
};
return (
Auth konfigürasyonunu test edin. Token alınabiliyorsa veya kimlik doğrulama geçerliyse başarılı sayılır.
Konfigürasyon
{auth.name}
Auth Tipi
{auth.auth_type}
{auth.token_url && (
Token URL
{auth.token_url}
)}
{loading ? 'Test ediliyor…' : 'Auth Testini Çalıştır'}
{result && (
{result.success ? '✅' : '❌'}
{result.success ? 'Başarılı' : 'Başarısız'}
{result.response_status && (
HTTP {result.response_status}
)}
{result.error && (
{result.error}
)}
{result.success && !result.error && (
Kimlik doğrulama başarıyla tamamlandı. Bu auth konfigürasyonu servislerde kullanılabilir.
)}
)}
);
}
/* ============================================================
LOGS PANEL
============================================================ */
function WsLogsPanel({ svc, logs, onClose }) {
const { useState } = React;
const [expanded, setExpanded] = useState({});
const toggle = (id) => setExpanded(s => ({ ...s, [id]: !s[id] }));
const sectionStyle = (color) => ({
marginTop:6, borderRadius:6, padding:'6px 10px',
background: `rgba(${color},0.06)`, borderLeft:`3px solid rgba(${color},0.35)`
});
const labelStyle = (color) => ({
fontSize:11, fontWeight:700, color:`rgb(${color})`,
marginBottom:4, letterSpacing:'0.04em'
});
const monoBox = {
fontFamily:'monospace', fontSize:11, whiteSpace:'pre-wrap',
wordBreak:'break-all', color:'var(--text-dim)', marginTop:4,
background:'var(--bg-hover)', borderRadius:4, padding:'4px 8px'
};
return (
{logs.length === 0 ? (
Henüz log kaydı yok.
) : (
{logs.map(log => (
{/* ── Başlık satırı ── */}
{log.success ? 'OK' : 'FAIL'}
{log.call_uuid}
{log.response_status && (
HTTP {log.response_status}
)}
{log.duration_ms && (
{log.duration_ms}ms
)}
{fmtDateTime(log.called_at, { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' })}
toggle(log.id)}
>{expanded[log.id] ? 'Kapat' : 'Detay'}
{log.error_message && (
Hata: {log.error_message}
)}
{/* Eşlenen değişkenler — her zaman göster */}
{log.mapped_variables && Object.keys(log.mapped_variables).length > 0 && (
{Object.entries(log.mapped_variables).map(([k, v]) => (
{k}: {v}
))}
)}
{/* ── Detay bölümü ── */}
{expanded[log.id] && (
{/* Auth */}
{log.auth_info && (
AUTH
{log.auth_info.name}
{log.auth_info.type}
{log.auth_info.status === 'cache_hit' && (
önbellekten
)}
{log.auth_info.status === 'fetched' && (
token alındı
)}
{log.auth_info.status === 'static' && (
statik
)}
{log.auth_info.token_preview && (
token: {log.auth_info.token_preview}
)}
{log.auth_info.duration_ms != null && (
{log.auth_info.duration_ms}ms
)}
{log.auth_info.token_url && (
Token URL: {log.auth_info.token_url}
)}
)}
{/* Request */}
REQUEST
{log.request_method}
{' '}
{log.request_url}
{log.request_headers && Object.keys(log.request_headers).length > 0 && (
HEADERS
{Object.entries(log.request_headers).map(([k, v]) =>
`${k}: ${v}`
).join('\n')}
)}
{log.request_body && (
)}
{/* Response body */}
{log.response_body && (
RESPONSE BODY
{log.response_body}
)}
)}
))}
)}
);
}
/* ============================================================
SHARED HELPERS
============================================================ */
function ModalOverlay({ children, onClose, title, width=600 }) {
return (
e.target===e.currentTarget && onClose()}>
);
}
function FormRow({ label, children }) {
return (
{label}
{children}
);
}
function EmptyState({ icon, text, sub }) {
return (
{icon &&
{icon}
}
{text}
{sub &&
{sub}
}
);
}
async function apiFetch(url, opts = {}) {
const token = localStorage.getItem('access_token');
try {
const res = await fetch(url, {
headers: { 'Content-Type':'application/json', 'Authorization': `Bearer ${token}`, ...(opts.headers||{}) },
...opts,
});
if (!res.ok) return null;
return await res.json();
} catch { return null; }
}