/* * DialerTab.jsx — Outbound Dialer Yönetim Paneli * Kampanya listesi, kişi tablosu, CSV/Excel yükleme, * Preview (manuel arama) ve Predictive (otomatik) modları. */ (function () { const { useState, useEffect, useRef, useCallback } = React; // Yardımcı: fetch wrapper async function api(path, opts = {}) { const res = await authFetch(path, { headers: { "Content-Type": "application/json", ...opts.headers }, ...opts, }); if (!res.ok) { let msg = res.statusText; try { const j = await res.json(); msg = j.detail || JSON.stringify(j); } catch {} throw new Error(msg); } return res.json(); } // Durum rozeti function StatusBadge({ status }) { const map = { idle: { color: "#6b7280", label: "Hazır" }, running: { color: "#22c55e", label: " Çalışıyor" }, paused: { color: "#f59e0b", label: " Duraklatıldı" }, completed: { color: "#3b82f6", label: " Tamamlandı" }, }; const s = map[status] || { color: "#6b7280", label: status }; return ( {s.label} ); } // Kişi durum rozeti function ContactBadge({ status }) { const map = { pending: { bg: "#374151", color: "#9ca3af", label: "Bekliyor" }, calling: { bg: "#1e3a5f", color: "#60a5fa", label: "Arıyor" }, answered: { bg: "#14532d", color: "#86efac", label: "Cevaplandı" }, completed: { bg: "#1e3a5f", color: "#3b82f6", label: "Tamamlandı" }, no_answer: { bg: "#422006", color: "#fbbf24", label: "Cevap Yok" }, busy: { bg: "#3b0764", color: "#c084fc", label: "Meşgul" }, failed: { bg: "#450a0a", color: "#f87171", label: "Başarısız" }, dnc: { bg: "#3f3f46", color: "#a1a1aa", label: "DNC" }, }; const s = map[status] || { bg: "#374151", color: "#9ca3af", label: status }; return ( {s.label} ); } // İstatistik kutusu function StatBox({ label, value, color = "var(--text)" }) { return (
{value ?? 0}
{label}
); } // Yeni Kampanya Modalı function NewCampaignModal({ agents, onClose, onSaved }) { const [form, setForm] = useState({ name: "", description: "", agent_type: "ai", agent_id: agents[0]?.id || "", queue_id: "", caller_id_number: "", caller_id_name: "", gateway: "default", mode: "preview", max_concurrent: 3, retry_attempts: 1, retry_delay_min: 30, leg_timeout: 30, }); const [queues, setQueues] = useState([]); const [saving, setSaving] = useState(false); const [err, setErr] = useState(""); const set = (k, v) => setForm(p => ({ ...p, [k]: v })); // Kuyruk listesini yukle useEffect(() => { api("/api/queues").then(setQueues).catch(() => {}); }, []); async function save() { if (!form.name || !form.caller_id_number) { setErr("Ad ve Caller ID zorunludur."); return; } if (form.agent_type === "ai" && !form.agent_id) { setErr("AI modu icin asistan secimi zorunludur."); return; } if (form.agent_type === "human" && !form.queue_id) { setErr("Insan modu icin kuyruk secimi zorunludur."); return; } setSaving(true); setErr(""); try { const body = { ...form, agent_id: form.agent_type === "ai" ? Number(form.agent_id) : null, queue_id: form.agent_type === "human" ? Number(form.queue_id) : null, }; const created = await api("/api/campaigns", { method: "POST", body: JSON.stringify(body), }); onSaved(created); } catch (e) { setErr(e.message); } finally { setSaving(false); } } const inp = { background: "var(--surface2)", border: "1px solid var(--border)", color: "var(--text)", padding: "7px 10px", borderRadius: 8, width: "100%", fontSize: 13, outline: "none", }; return (
Yeni Kampanya
{[ ["Kampanya Adı *", "name", "text"], ["Açıklama", "description", "text"], ["Caller ID Numarası *", "caller_id_number", "text"], ["Caller ID Adı", "caller_id_name", "text"], ["Gateway", "gateway", "text"], ].map(([label, key, type]) => (
{label}
set(key, e.target.value)} style={inp} />
))} {/* Arama Tipi */}
Arama Tipi
{[ { val: "ai", label: "AI Asistan", icon: "" }, { val: "human", label: "Insan Temsilci", icon: "" }, ].map(opt => ( ))}
{/* AI modu: Asistan secimi */} {form.agent_type === "ai" && (
AI Asistan *
)} {/* Human modu: Kuyruk secimi */} {form.agent_type === "human" && (
Kuyruk * (Bu kuyruktaki temsilciler kampanyayi gorebilir)
)}
Arama Modu
{form.mode === "predictive" && (
{[ ["Eş Zamanlı Hat", "max_concurrent"], ["Tekrar Denemesi", "retry_attempts"], ["Tekrar Aralığı (dk)", "retry_delay_min"], ].map(([label, key]) => (
{label}
set(key, Number(e.target.value))} style={inp} />
))}
)}
Zil Süresi (sn)
set("leg_timeout", Number(e.target.value))} style={{ ...inp, width: 100 }} />
{err &&
{err}
}
); } // CSV Yükleme bileşeni function UploadPanel({ campaign, onUploaded }) { const [dragging, setDragging] = useState(false); const [uploading, setUploading] = useState(false); const [msg, setMsg] = useState(""); const fileRef = useRef(); async function upload(file) { if (!file) return; setUploading(true); setMsg(""); const fd = new FormData(); fd.append("file", file); try { const res = await authFetch(`/api/campaigns/${campaign.id}/upload`, { method: "POST", body: fd }); const j = await res.json(); if (!res.ok) throw new Error(j.detail || "Yükleme hatası"); setMsg(` ${j.inserted} kişi yüklendi.`); onUploaded(); } catch (e) { setMsg(" " + e.message); } finally { setUploading(false); } } function onDrop(e) { e.preventDefault(); setDragging(false); const f = e.dataTransfer.files[0]; if (f) upload(f); } return (
fileRef.current?.click()} onDragOver={e => { e.preventDefault(); setDragging(true); }} onDragLeave={() => setDragging(false)} onDrop={onDrop} style={{ border: `2px dashed ${dragging ? "var(--primary)" : "var(--border)"}`, borderRadius: 10, padding: "24px 20px", textAlign: "center", cursor: "pointer", transition: "border-color .2s", background: dragging ? "var(--primary)11" : "var(--surface2)", }} >
CSV veya Excel dosyasını buraya sürükleyin
veya tıklayarak seçin • .csv, .xlsx, .xls
{uploading &&
Yükleniyor…
}
{ if (e.target.files[0]) upload(e.target.files[0]); }} /> {msg &&
{msg}
}
); } // Kişi tablosu function ContactsTable({ campaign, refreshKey, onRefresh }) { const [contacts, setContacts] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [filter, setFilter] = useState("all"); const [calling, setCalling] = useState({}); const PER_PAGE = 50; const load = useCallback(async () => { try { const offset = (page - 1) * PER_PAGE; const st = filter === "all" ? "" : `&status=${filter}`; const r = await api(`/api/campaigns/${campaign.id}/contacts?limit=${PER_PAGE}&offset=${offset}${st}`); setContacts(r.contacts || []); setTotal(r.total || 0); } catch {} }, [campaign.id, page, filter, refreshKey]); useEffect(() => { load(); }, [load]); async function callContact(contact) { setCalling(p => ({ ...p, [contact.id]: true })); try { await api(`/api/campaigns/${campaign.id}/contacts/${contact.id}/call`, { method: "POST" }); await load(); onRefresh(); } catch (e) { alert("Arama başlatılamadı: " + e.message); } finally { setCalling(p => ({ ...p, [contact.id]: false })); } } const pages = Math.ceil(total / PER_PAGE); const statusOptions = [ { v: "all", l: "Tümü" }, { v: "pending", l: "Bekliyor" }, { v: "calling", l: "Arıyor" }, { v: "answered", l: "Cevaplandı" }, { v: "completed", l: "Tamamlandı" }, { v: "no_answer", l: "Cevap Yok" }, { v: "failed", l: "Başarısız" }, { v: "dnc", l: "DNC" }, ]; return (
Kişiler ({total})
{contacts.length === 0 ? (
Kişi bulunamadı.
) : (
{["Telefon", "Ad", "Not", "Durum", "Sebep", "Deneme", "Son Arama", ""].map(h => ( ))} {contacts.map(c => ( ))}
{h}
{c.phone} {c.name || "—"} {c.note || "—"} {c.hangup_cause || "—"} {c.attempts ?? 0} {fmtDateTime(c.last_attempt_at)} {campaign.mode === "preview" && campaign.status === "running" && c.status === "pending" && ( )}
)} {pages > 1 && (
{page} / {pages}
)}
); } // Kampanya Detayı function CampaignDetail({ campaign: initCamp, agents, onBack, wsEvents }) { const [camp, setCamp] = useState(initCamp); const [stats, setStats] = useState(null); const [tab, setTab] = useState("contacts"); // "contacts" | "upload" const [contactsKey, setContactsKey] = useState(0); const [busy, setBusy] = useState(false); const loadCamp = useCallback(async () => { try { const r = await api(`/api/campaigns/${initCamp.id}`); setCamp(r); setStats(r.live_stats || null); } catch {} }, [initCamp.id]); useEffect(() => { loadCamp(); }, [loadCamp]); // WS events: kampanya / kişi güncellemelerini dinle useEffect(() => { if (!wsEvents) return; const last = wsEvents[wsEvents.length - 1]; if (!last) return; if ( (last.type === "dialer_campaign_update" || last.type === "dialer_contact_update") && last.campaign_id === camp.id ) { loadCamp(); setContactsKey(k => k + 1); } }, [wsEvents, camp.id, loadCamp]); async function action(endpoint) { setBusy(true); try { await api(`/api/campaigns/${camp.id}/${endpoint}`, { method: "POST" }); await loadCamp(); } catch (e) { alert(e.message); } finally { setBusy(false); } } const agentName = agents.find(a => a.id === camp.agent_id)?.name || `Agent #${camp.agent_id}`; const live = stats || {}; return (
{/* Başlık */}
{camp.name} {camp.mode === "predictive" ? " Predictive" : " Preview"} {camp.agent_type === "human" ? " Insan Temsilci" : ` ${agentName}`}
{camp.description && (
{camp.description}
)}
{/* Kontrol butonları */}
{camp.status === "idle" || camp.status === "paused" ? ( ) : null} {camp.status === "running" ? ( ) : null} {camp.status !== "idle" && camp.status !== "completed" ? ( ) : null} {camp.status !== "running" ? ( ) : null}
{/* İstatistikler */}
{/* Alt sekmeler */}
{[["contacts", " Kişiler"], ["upload", " Yükle"]].map(([v, l]) => ( ))}
{tab === "upload" && ( { loadCamp(); setContactsKey(k => k + 1); }} /> )} {tab === "contacts" && ( { loadCamp(); setContactsKey(k => k + 1); }} /> )}
); } // Kampanya listesi kartı function CampaignCard({ campaign, agents, selected, onClick }) { const agentName = agents.find(a => a.id === campaign.agent_id)?.name || `Agent #${campaign.agent_id}`; const pct = campaign.total_contacts > 0 ? Math.round(((campaign.called_count || 0) / campaign.total_contacts) * 100) : 0; return (
{campaign.name}
{agentName}  ·  {campaign.mode === "predictive" ? "Predictive" : "Preview"}
{campaign.total_contacts} kişi {campaign.called_count || 0} aranan {campaign.answered_count || 0} cevap
{campaign.total_contacts > 0 && (
)}
); } // Ana DialerTab bileşeni function DialerTab({ wsEvents }) { const [campaigns, setCampaigns] = useState([]); const [agents, setAgents] = useState([]); const [selected, setSelected] = useState(null); const [showNew, setShowNew] = useState(false); const [loading, setLoading] = useState(true); const loadAll = useCallback(async () => { try { const [camps, ags] = await Promise.all([ api("/api/campaigns"), api("/api/agents"), ]); setCampaigns(Array.isArray(camps) ? camps : (camps.campaigns || [])); setAgents(Array.isArray(ags) ? ags : (ags.agents || [])); } catch (e) { console.error("Dialer yükleme hatası:", e); } finally { setLoading(false); } }, []); useEffect(() => { loadAll(); }, [loadAll]); // WS events: kampanya güncellemelerini dinle (liste yenileme) useEffect(() => { if (!wsEvents) return; const last = wsEvents[wsEvents.length - 1]; if (!last) return; if (last.type === "dialer_campaign_update") { setCampaigns(prev => prev.map(c => c.id === last.campaign_id ? { ...c, status: last.status ?? c.status, answered_count: last.answered ?? c.answered_count } : c )); } }, [wsEvents]); async function deleteCampaign(camp) { if (!confirm(`"${camp.name}" kampanyası silinsin mi? Tüm kişi verileri de silinir.`)) return; try { await api(`/api/campaigns/${camp.id}`, { method: "DELETE" }); setCampaigns(prev => prev.filter(c => c.id !== camp.id)); if (selected?.id === camp.id) setSelected(null); } catch (e) { alert(e.message); } } if (loading) return (
Yükleniyor…
); return (
{/* Sol panel: Kampanya listesi */}
Dialer
{campaigns.length === 0 ? (
Henüz kampanya yok.
setShowNew(true)}>Oluştur →
) : ( campaigns.map(c => (
setSelected(c)} />
)) )}
{/* Sağ panel: Detay veya boş durum */}
{selected ? ( setSelected(null)} wsEvents={wsEvents} /> ) : (
Outbound Dialer
Sol panelden bir kampanya seçin veya yeni oluşturun.
)}
{showNew && ( setShowNew(false)} onSaved={(newCamp) => { setCampaigns(prev => [newCamp, ...prev]); setShowNew(false); setSelected(newCamp); }} /> )}
); } window.DialerTab = DialerTab; })();