const { useState, useEffect, useRef, useCallback } = React; const { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, BarChart, Bar, Cell } = Recharts; function LiveTimer({ startTime }) { const [elapsed, setElapsed] = useState('0:00'); useEffect(() => { if (!startTime) return; // Backend naive UTC ISO gönderiyorsa parseUTC ile doğru çözülür, // yoksa LiveTimer 3 saatlik yanlış offset gösterirdi (TR için). const startObj = parseUTC(startTime); if (!startObj) return; const start = startObj.getTime(); const tick = () => { const diff = Math.floor((Date.now() - start) / 1000); const m = Math.floor(diff / 60); const s = diff % 60; setElapsed(`${m}:${s.toString().padStart(2, '0')}`); }; tick(); const id = setInterval(tick, 1000); return () => clearInterval(id); }, [startTime]); return {elapsed}; } const SENTIMENT_STYLE = { positive: { bg: 'rgba(34,197,94,.07)', border: 'rgba(34,197,94,.2)', color: 'var(--green)' }, neutral: { bg: 'rgba(107,114,128,.06)', border: 'var(--border)', color: 'var(--text-dim)' }, negative: { bg: 'rgba(245,158,11,.07)', border: 'rgba(245,158,11,.2)', color: 'var(--amber)' }, angry: { bg: 'rgba(239,68,68,.07)', border: 'rgba(239,68,68,.2)', color: 'var(--red)' }, }; function SentimentBadge({ sentiment }) { if (!sentiment) return null; const s = SENTIMENT_STYLE[sentiment.label] || SENTIMENT_STYLE.neutral; return ( {sentiment.tr} ); } function DashboardTab({ activeCalls, onTransfer, toast }) { const { data: stats } = window.useApi('/api/stats', [activeCalls.length]); // ── Son Çağrılar: filtre + sayfalama ───────────────────────────────────── const [calls, setCalls] = useState([]); const [callsTotal, setCallsTotal] = useState(0); const [callsPages, setCallsPages] = useState(1); const [callsPage, setCallsPage] = useState(1); const [filterPhone, setFilterPhone] = useState(''); const [filterStatus, setFilterStatus] = useState(''); const [phoneInput, setPhoneInput] = useState(''); // debounceable input const PER_PAGE = 10; const fetchCalls = useCallback(async () => { try { const params = new URLSearchParams({ page: callsPage, per_page: PER_PAGE }); if (filterPhone) params.set('phone', filterPhone); if (filterStatus) params.set('status', filterStatus); const r = await authFetch(`/api/calls?${params}`); if (!r.ok) return; const data = await r.json(); if (Array.isArray(data)) { setCalls(data); setCallsTotal(data.length); setCallsPages(1); } else { setCalls(data.calls || []); setCallsTotal(data.total || 0); setCallsPages(data.pages || 1); } } catch (_) {} }, [callsPage, filterPhone, filterStatus, activeCalls.length]); useEffect(() => { fetchCalls(); }, [fetchCalls]); // Filtre değişince 1. sayfaya dön const applyFilter = useCallback(() => { setFilterPhone(phoneInput); setCallsPage(1); }, [phoneInput]); const resetFilters = useCallback(() => { setPhoneInput(''); setFilterPhone(''); setFilterStatus(''); setCallsPage(1); }, []); const reloadCalls = fetchCalls; const [transcript, setTranscript] = useState(null); const [transferCall, setTransferCall] = useState(null); const [sentimentMap, setSentimentMap] = useState({}); const _EMOTION_META_FE = { positive: { emoji: '', tr: 'Olumlu' }, neutral: { emoji: '', tr: 'Nötr' }, negative: { emoji: '', tr: 'Olumsuz' }, angry: { emoji: '', tr: 'Sinirli' }, }; // ── Alarm Yönetimi ──────────────────────────────────────────────────────── const [activeAlarms, setActiveAlarms] = useState([]); // Sayfa açıldığında mevcut aktif alarmları API'den çek useEffect(() => { authFetch('/api/alarms/active') .then(r => r.ok ? r.json() : []) .then(data => setActiveAlarms(Array.isArray(data) ? data : [])) .catch(() => {}); }, []); // Alarm kapat const resolveAlarm = useCallback(async (alarm) => { try { await authFetch(`/api/alarms/${alarm.id}/resolve?resolved_by=supervisor`, { method: 'POST' }); } catch (_) {} setActiveAlarms(prev => prev.filter(a => (a.id || a.call_uuid) !== (alarm.id || alarm.call_uuid))); }, []); // ── Dashboard WebSocket ─────────────────────────────────────────────────── useEffect(() => { const proto = location.protocol === 'https:' ? 'wss' : 'ws'; const ws = new WebSocket(`${proto}://${location.host}/ws/dashboard`); ws.onmessage = (e) => { try { const ev = JSON.parse(e.data); // Duygu güncelleme (mevcut) if (ev.type === 'sentiment_update') { const { call_uuid, sentiment, text } = ev; setSentimentMap(prev => ({ ...prev, [call_uuid]: sentiment })); if (sentiment.label === 'angry' || sentiment.label === 'negative') { const preview = text ? ` — "${text.slice(0, 60)}${text.length > 60 ? '…' : ''}"` : ''; toast(`${sentiment.emoji} ${ev.phone}: Müşteri ${sentiment.tr.toLowerCase()}${preview}`, sentiment.label === 'angry' ? 'error' : 'warning'); } } // ── YENİ: Alarm tetiklendi ──────────────────────────────────── else if (ev.type === 'alarm_triggered') { setActiveAlarms(prev => { // Aynı çağrı + tip için son 10sn içinde eklenmişse tekrarlama const now = Date.now(); const duplicate = prev.find(a => { if (a.call_uuid !== ev.call_uuid || a.alarm_type !== ev.alarm_type) return false; const t = parseUTC(a.triggered_at); return t && (now - t.getTime() < 10000); }); if (duplicate) return prev; return [ev, ...prev]; }); // Ses çal if (window.playAlarmSound) window.playAlarmSound(); // Toast bildirimi const meta = { angry_detected: 'Sinirli müşteri tespit edildi', score_threshold: 'Yüksek olumsuzluk skoru', sustained_negative: 'Sürekli olumsuz geri bildirim', rapid_drop: 'Ani duygu düşüşü', }; toast(meta[ev.alarm_type] || 'Duygu alarmı', 'error'); } // ── Duygu iyileşti → alarmları otomatik kapat ──────────────── else if (ev.type === 'alarm_resolved') { setActiveAlarms(prev => prev.filter(a => a.call_uuid !== ev.call_uuid)); if (ev.auto) toast('Müşteri sakinleşti, alarmlar kapatıldı', 'success'); } // Çağrı bitti — temizlik else if (ev.type === 'call_ended') { setSentimentMap(prev => { const next = { ...prev }; delete next[ev.call_uuid]; return next; }); // Transkripti 30sn sonra temizle (son görüntüleme şansı) setTimeout(() => { setTranscriptMap(prev => { const next = { ...prev }; delete next[ev.call_uuid]; return next; }); }, 30000); // Genişletilmiş kart kapandıysa sıfırla setExpandedCall(prev => prev === ev.call_uuid ? null : prev); } } catch (_) {} }; return () => ws.close(); }, []); async function doTransfer(callUuid, dest) { const r = await authFetch('/api/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ call_uuid: callUuid, destination: dest }) }); const d = await r.json(); if (d.ok) { toast('Transfer başarılı', 'success'); setTransferCall(null); reloadCalls(); } else toast('Transfer başarısız', 'error'); } return (