/*
* 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 (
);
}
// 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]) => (
))}
{/* 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]) => (
))}
)}
{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 => (
| {h} |
))}
{contacts.map(c => (
| {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 (
{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;
})();