// ═══════════════════════════════════════════════════════════════
// RoutingTab.jsx — AI Akıllı Yönlendirme
// Gelen çağrıları müşteri geçmişine göre AI ile yönlendirin
// ═══════════════════════════════════════════════════════════════
const { useState, useEffect, useCallback } = React;
// ── Koşul Şablonları ─────────────────────────────────────────
const CONDITION_TEMPLATES = [
{ key: 'customer_identified', label: 'Müşteri tanınıyor', type: 'bool' },
{ key: 'tags_include_any', label: 'Etiket içerir (herhangi biri)', type: 'tags' },
{ key: 'tags_include_all', label: 'Etiket içerir (hepsi)', type: 'tags' },
{ key: 'tags_exclude', label: 'Etiket içermez', type: 'tags' },
{ key: 'call_count_min', label: 'Min. çağrı sayısı', type: 'number' },
{ key: 'call_count_max', label: 'Max. çağrı sayısı', type: 'number' },
{ key: 'last_disposition_in', label: 'Son çağrı sonucu', type: 'tags' },
{ key: 'is_blacklist', label: 'Kara listede', type: 'bool' },
{ key: 'time_range', label: 'Saat aralığı', type: 'time_range' },
{ key: 'weekdays', label: 'Hafta günleri', type: 'weekdays' },
];
const ACTION_TYPES = [
{ value: 'route_queue', label: 'Kuyruğa Yönlendir', icon: '' },
{ value: 'route_ai_agent', label: 'AI Agent\'a Yönlendir', icon: '' },
{ value: 'route_ivr', label: 'IVR Menüye Yönlendir', icon: '' },
{ value: 'set_priority', label: 'Öncelik Ayarla', icon: '⬆' },
{ value: 'reject', label: 'Çağrıyı Reddet', icon: '' },
{ value: 'continue', label: 'Varsayılan Devam', icon: '▶' },
];
const WEEKDAY_NAMES = ['Pzt', 'Sal', 'Çar', 'Per', 'Cum', 'Cmt', 'Paz'];
// ═══════════════════════════════════════════════════════════════
// Koşul Editörü
// ═══════════════════════════════════════════════════════════════
function ConditionEditor({ conditions, onChange }) {
const conds = conditions || {};
const setValue = (key, val) => {
const next = { ...conds, [key]: val };
if (val === null || val === undefined || val === '' || (Array.isArray(val) && val.length === 0)) {
delete next[key];
}
onChange(next);
};
const addCondition = (key) => {
const tpl = CONDITION_TEMPLATES.find(t =>t.key === key);
if (!tpl) return;
const defaults = { bool: true, tags: [], number: 1, time_range: { start: '09:00', end: '18:00' }, weekdays: [1,2,3,4,5] };
setValue(key, defaults[tpl.type]);
};
const removeCondition = (key) => {
const next = { ...conds };
delete next[key];
onChange(next);
};
const activeKeys = Object.keys(conds);
const availableKeys = CONDITION_TEMPLATES.filter(t => !activeKeys.includes(t.key));
return (
{activeKeys.map(key => {
const tpl = CONDITION_TEMPLATES.find(t =>t.key === key);
if (!tpl) return null;
return (
{tpl.label}
{tpl.type === 'bool' && (
)}
{tpl.type === 'number' && (
setValue(key, parseInt(e.target.value) || 0)} />
)}
{tpl.type === 'tags' && (
setValue(key, e.target.value.split(',').map(s =>s.trim()).filter(Boolean))} />
)}
{tpl.type === 'time_range' && (
setValue(key, { ...conds[key], start: e.target.value })} />
—
setValue(key, { ...conds[key], end: e.target.value })} />
)}
{tpl.type === 'weekdays' && (
{WEEKDAY_NAMES.map((name, i) => {
const day = i + 1;
const active = (conds[key] || []).includes(day);
return (
);
})}
)}
);
})}
{availableKeys.length > 0 && (
)}
);
}
// ═══════════════════════════════════════════════════════════════
// Kural Düzenleme Modalı
// ═══════════════════════════════════════════════════════════════
function RuleModal({ rule, targets, onClose, onSave }) {
const empty = { name: '', description: '', priority: 10, is_active: true, conditions: {}, action_type: 'route_queue', target_id: null, priority_boost: 1, welcome_override: '', ai_hint: '' };
const [form, setForm] = useState(rule ? { ...empty, ...rule } : empty);
const [saving, setSaving] = useState(false);
const set = (k, v) =>setForm(f => ({ ...f, [k]: v }));
const handleSave = async () => {
if (!form.name) { alert('Kural adını girin'); return; }
setSaving(true);
onSave(form);
setSaving(false);
};
const targetOptions = () => {
switch (form.action_type) {
case 'route_queue': return (targets.queues || []).map(q => ({ id: q.id, label: `${q.name} (${q.extension})` }));
case 'route_ai_agent': return (targets.ai_agents || []).map(a => ({ id: a.id, label: `${a.name} (${a.department || '-'})` }));
case 'route_ivr': return (targets.ivr_menus || []).map(m => ({ id: m.id, label: m.name }));
default: return [];
}
};
const needsTarget = ['route_queue', 'route_ai_agent', 'route_ivr'].includes(form.action_type);
return (
e.stopPropagation()} style={{ maxWidth: 700 }}>
{rule ? 'Kuralı Düzenle' : 'Yeni Kural'}
{/* Temel Bilgiler */}
set('description', e.target.value)} placeholder="Bu kural ne zaman uygulanır?" />
{/* Koşullar */}
Koşullar
AI bu koşulları müşteri verisiyle karşılaştırarak kararını verir. Boş bırakılırsa kural her zaman geçerlidir.
set('conditions', v)} />
{/* Aksiyon */}
Aksiyon
{needsTarget && (
)}
{/* Özelleştirme */}
Özelleştirme
set('welcome_override', e.target.value)} placeholder="Değerli VIP müşterimiz, sizi öncelikli hatta yönlendiriyoruz." />
);
}
// ═══════════════════════════════════════════════════════════════
// Config Düzenleme Modalı (AI Prompt + Ayarlar)
// ═══════════════════════════════════════════════════════════════
function ConfigModal({ config, queues, agents, onClose, onSave }) {
const empty = { name: '', description: '', ai_prompt: '', fallback_queue_id: null, fallback_ai_agent_id: null, ai_timeout_ms: 3000, routing_enabled: true, is_active: true };
const [form, setForm] = useState(config ? { ...empty, ...config } : empty);
const [saving, setSaving] = useState(false);
const set = (k, v) =>setForm(f => ({ ...f, [k]: v }));
const handleSave = async () => {
if (!form.name) { alert('Profil adını girin'); return; }
setSaving(true);
try {
const url = config ? `/api/routing/configs/${config.id}` : '/api/routing/configs';
const method = config ? 'PUT' : 'POST';
const r = await authFetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) });
if (r.ok) { onSave(); }
else { const err = await r.json(); alert('Hata: ' + JSON.stringify(err)); }
} finally { setSaving(false); }
};
return (
e.stopPropagation()} style={{ maxWidth: 750 }}>
{config ? 'Profili Düzenle' : 'Yeni Routing Profili'}
set('description', e.target.value)} />
{/* AI Prompt */}
AI Yönlendirme Prompt'u
Yapay zekanın çağrıları nasıl yönlendireceğini belirleyen talimat. Boş bırakılırsa varsayılan prompt kullanılır.
{/* Fallback Ayarları */}
Fallback Ayarları
AI karar veremezse veya zaman aşımına uğrarsa çağrı nereye yönlendirilsin?
{/* Durum */}
);
}
// ═══════════════════════════════════════════════════════════════
// Test Modalı — Routing kararını test et
// ═══════════════════════════════════════════════════════════════
function TestModal({ onClose }) {
const [phone, setPhone] = useState('');
const [testing, setTesting] = useState(false);
const [result, setResult] = useState(null);
const handleTest = async () => {
if (!phone) { alert('Telefon numarası girin'); return; }
setTesting(true);
setResult(null);
try {
const r = await authFetch('/api/routing/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone }),
});
if (r.ok) setResult(await r.json());
else alert('Test hatası');
} finally { setTesting(false); }
};
const actionLabel = (action) => {
const a = ACTION_TYPES.find(t =>t.value === action);
return a ? `${a.icon} ${a.label}` : action;
};
return (
e.stopPropagation()} style={{ maxWidth: 550 }}>
Routing Testi
Bir telefon numarası girerek AI routing kararını test edin. Gerçek transfer yapılmaz.
setPhone(e.target.value)} placeholder="05321234567" style={{ flex: 1 }} onKeyDown={e =>e.key === 'Enter' && handleTest()} />
{result && (
MÜŞTERI
{result.customer_found ? `${result.customer_name || 'Adsız'}` : 'Tanınmıyor'}
ÇAĞRI GEÇMİŞİ
{result.call_count} çağrı
AI KARARI
{actionLabel(result.decision.action)}
{result.decision.target_id && Hedef ID: {result.decision.target_id}}
Öncelik: {result.decision.priority}
Gerekçe: {result.decision.reason}
{result.decision.matched_rules && result.decision.matched_rules.length > 0 && (
Eşleşen Kurallar: {result.decision.matched_rules.join(', ')}
)}
{result.decision.welcome_override && (
Karşılama: "{result.decision.welcome_override}"
)}
Güven: %{Math.round((result.decision.confidence || 0) * 100)}
)}
);
}
// ═══════════════════════════════════════════════════════════════
// Karar Logları Paneli
// ═══════════════════════════════════════════════════════════════
function LogsPanel() {
const [logs, setLogs] = useState([]);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const load = useCallback(async () => {
setLoading(true);
try {
const r = await authFetch(`/api/routing/logs?page=${page}&per_page=15`);
if (r.ok) { const d = await r.json(); setLogs(d.items); setTotal(d.total); }
} finally { setLoading(false); }
}, [page]);
useEffect(() => { load(); }, [load]);
const actionBadge = (action) => {
const colors = { route_queue: 'var(--green)', route_ai_agent: 'var(--blue)', route_ivr: 'var(--purple)', reject: 'var(--red)', continue: 'var(--text-dim)' };
return (
{action}
);
};
return (
Routing Karar Logları ({total})
{logs.length === 0 ? (
Henüz karar logu yok
) : (
| Zaman |
Telefon |
Müşteri |
Karar |
Hedef |
Öncelik |
Gerekçe |
Güven |
{logs.map(log => (
| {fmtDate(log.created_at)} |
{log.phone} |
{log.customer_name || '—'} |
{actionBadge(log.action)} |
{log.target_id || '—'} |
{log.priority} |
{log.reason || '—'} |
%{Math.round((log.confidence || 0) * 100)} |
))}
)}
{total > 15 && (
Sayfa {page}/{Math.ceil(total / 15)}
)}
);
}
// ═══════════════════════════════════════════════════════════════
// ANA TAB BİLEŞENİ
// ═══════════════════════════════════════════════════════════════
function RoutingTab({ toast }) {
const [configs, setConfigs] = useState([]);
const [selectedConfig, setSelectedConfig] = useState(null);
const [rules, setRules] = useState([]);
const [targets, setTargets] = useState({});
const [configModal, setConfigModal] = useState(null);
const [ruleModal, setRuleModal] = useState(null);
const [testModal, setTestModal] = useState(false);
const [activeView, setActiveView] = useState('rules'); // 'rules' | 'logs'
const [loading, setLoading] = useState(true);
// Hedefleri yükle (kuyruk, agent, IVR)
useEffect(() => {
authFetch('/api/routing/targets').then(r =>r.ok ? r.json() : {}).then(setTargets).catch(() => {});
}, []);
// Config listesini yükle
const loadConfigs = useCallback(async () => {
setLoading(true);
try {
const r = await authFetch('/api/routing/configs');
if (r.ok) {
const data = await r.json();
setConfigs(data);
if (data.length > 0 && !selectedConfig) setSelectedConfig(data[0]);
else if (selectedConfig) {
const updated = data.find(c =>c.id === selectedConfig.id);
if (updated) setSelectedConfig(updated);
}
}
} finally { setLoading(false); }
}, []);
useEffect(() => { loadConfigs(); }, [loadConfigs]);
// Kuralları yükle
const loadRules = useCallback(async () => {
if (!selectedConfig) { setRules([]); return; }
const r = await authFetch(`/api/routing/configs/${selectedConfig.id}/rules`);
if (r.ok) setRules(await r.json());
}, [selectedConfig?.id]);
useEffect(() => { loadRules(); }, [loadRules]);
// Config sil
const deleteConfig = async (id) => {
if (!confirm('Bu routing profilini ve tüm kurallarını silmek istediğinize emin misiniz?')) return;
await authFetch(`/api/routing/configs/${id}`, { method: 'DELETE' });
toast('Profil silindi', 'success');
setSelectedConfig(null);
loadConfigs();
};
// Kural kaydet (create/update)
const saveRule = async (form) => {
const isEdit = !!form.id;
const url = isEdit ? `/api/routing/rules/${form.id}` : `/api/routing/configs/${selectedConfig.id}/rules`;
const method = isEdit ? 'PUT' : 'POST';
const r = await authFetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) });
if (r.ok) {
setRuleModal(null);
loadRules();
toast(isEdit ? 'Kural güncellendi' : 'Kural eklendi', 'success');
} else { alert('Hata!'); }
};
// Kural sil
const deleteRule = async (id) => {
if (!confirm('Bu kuralı silmek istediğinize emin misiniz?')) return;
await authFetch(`/api/routing/rules/${id}`, { method: 'DELETE' });
toast('Kural silindi', 'success');
loadRules();
};
// Kural toggle
const toggleRule = async (rule) => {
await authFetch(`/api/routing/rules/${rule.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_active: !rule.is_active }),
});
loadRules();
};
const actionIcon = (type) => {
const a = ACTION_TYPES.find(t =>t.value === type);
return a ? a.icon : '?';
};
const actionLabel = (type) => {
const a = ACTION_TYPES.find(t =>t.value === type);
return a ? a.label : type;
};
const targetLabel = (rule) => {
if (!rule.target_id) return '';
if (rule.action_type === 'route_queue') {
const q = (targets.queues || []).find(q =>q.id === rule.target_id);
return q ? `→ ${q.name}` : `→ ID:${rule.target_id}`;
}
if (rule.action_type === 'route_ai_agent') {
const a = (targets.ai_agents || []).find(a =>a.id === rule.target_id);
return a ? `→ ${a.name}` : `→ ID:${rule.target_id}`;
}
if (rule.action_type === 'route_ivr') {
const m = (targets.ivr_menus || []).find(m =>m.id === rule.target_id);
return m ? `→ ${m.name}` : `→ ID:${rule.target_id}`;
}
return '';
};
const conditionSummary = (conditions) => {
if (!conditions || Object.keys(conditions).length === 0) return 'Her zaman';
const parts = [];
if (conditions.customer_identified !== undefined) parts.push(conditions.customer_identified ? 'Tanınan müşteri' : 'Tanınmayan müşteri');
if (conditions.tags_include_any?.length) parts.push(`Etiket: ${conditions.tags_include_any.join(', ')}`);
if (conditions.tags_exclude?.length) parts.push(`Hariç: ${conditions.tags_exclude.join(', ')}`);
if (conditions.call_count_min) parts.push(`Min ${conditions.call_count_min} çağrı`);
if (conditions.last_disposition_in?.length) parts.push(`Son sonuç: ${conditions.last_disposition_in.join(', ')}`);
if (conditions.is_blacklist) parts.push('Kara liste');
if (conditions.time_range) parts.push(`${conditions.time_range.start}-${conditions.time_range.end}`);
if (conditions.weekdays?.length && conditions.weekdays.length < 7) parts.push(`${conditions.weekdays.map(d =>WEEKDAY_NAMES[d-1]).join(',')}`);
return parts.join(' + ') || 'Her zaman';
};
return (
{/* Header */}
AI Akıllı Yönlendirme
Gelen çağrıları müşteri geçmişine göre yapay zeka ile yönlendirin
{/* Config seçici */}
{configs.length > 0 && (
{configs.map(c => (
))}
)}
{/* Boş durum */}
{configs.length === 0 && !loading && (
Henüz Routing Profili yok
AI yönlendirme kuralları tanımlayarak gelen çağrıları akıllıca yönlendirin.
)}
{/* Seçili config detayı */}
{selectedConfig && (
{/* Config bilgi kartı */}
{selectedConfig.routing_enabled ? '' : ''} {selectedConfig.name}
{selectedConfig.description &&
{selectedConfig.description}
}
AI Timeout: {selectedConfig.ai_timeout_ms}ms
Kurallar: {rules.length}
{selectedConfig.fallback_queue_id && Fallback Kuyruk: #{selectedConfig.fallback_queue_id}}
{/* Tab: Kurallar / Loglar */}
{activeView === 'rules' && (
{/* Kural listesi */}
{rules.length === 0 ? (
Henüz kural tanımlı değil. Yeni bir kural ekleyin.
) : (
{rules.map((rule, idx) => (
#{rule.priority}
{rule.name}
{!rule.is_active && Pasif}
{rule.description &&
{rule.description}
}
{conditionSummary(rule.conditions)}
{actionIcon(rule.action_type)} {actionLabel(rule.action_type)} {targetLabel(rule)}
{rule.priority_boost > 1 && (
Öncelik: {rule.priority_boost}
)}
))}
)}
)}
{activeView === 'logs' && (
)}
)}
{/* Modaller */}
{configModal && (
setConfigModal(null)}
onSave={() => { setConfigModal(null); loadConfigs(); toast('Profil kaydedildi', 'success'); }}
/>
)}
{ruleModal && (
setRuleModal(null)}
onSave={saveRule}
/>
)}
{testModal && setTestModal(false)} />}
);
}
window.RoutingTab = RoutingTab;