/* ═══════════════════════════════════════════════════════════════════════════════ ReportBuilderTab — Yap-Boz Tarzı Rapor Oluşturucu ═══════════════════════════════════════════════════════════════════════════════ */ const { useState, useEffect, useMemo, useCallback, useRef } = React; const { ResponsiveContainer, AreaChart, Area, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, Tooltip, CartesianGrid, Legend, } = Recharts; /* ── Widget Registry ─────────────────────────────────────────────────────── */ const WIDGET_REGISTRY = { kpi_total_calls: { id: 'kpi_total_calls', label: 'Toplam Çağrı', icon: '', category: 'kpi', defaultSize: 'small' }, kpi_today_calls: { id: 'kpi_today_calls', label: 'Bugünkü Çağrı', icon: '', category: 'kpi', defaultSize: 'small' }, kpi_avg_duration: { id: 'kpi_avg_duration', label: 'Ort. Süre', icon: '', category: 'kpi', defaultSize: 'small' }, kpi_transfer_rate: { id: 'kpi_transfer_rate', label: 'Transfer Oranı', icon: '', category: 'kpi', defaultSize: 'small' }, kpi_qc_rate: { id: 'kpi_qc_rate', label: 'QC Başarı Oranı', icon: '', category: 'kpi', defaultSize: 'small' }, kpi_active_calls: { id: 'kpi_active_calls', label: 'Aktif Çağrı', icon: '', category: 'kpi', defaultSize: 'small' }, kpi_aht: { id: 'kpi_aht', label: 'Ort. İşlem Süresi (AHT)', icon: '', category: 'kpi', defaultSize: 'small' }, kpi_abandon_rate: { id: 'kpi_abandon_rate', label: 'Terk Oranı', icon: '', category: 'kpi', defaultSize: 'small' }, kpi_avg_wait_time: { id: 'kpi_avg_wait_time', label: 'Ort. Bekleme Süresi', icon: '', category: 'kpi', defaultSize: 'small' }, kpi_calls_per_hour: { id: 'kpi_calls_per_hour', label: 'Saatlik Çağrı Hızı', icon: '', category: 'kpi', defaultSize: 'small' }, chart_daily_trend: { id: 'chart_daily_trend', label: 'Günlük Trend (Alan)', icon: '', category: 'chart', defaultSize: 'large' }, chart_status_dist: { id: 'chart_status_dist', label: 'Durum Dağılımı (Bar)', icon: '', category: 'chart', defaultSize: 'medium' }, chart_status_pie: { id: 'chart_status_pie', label: 'Durum Dağılımı (Pasta)', icon: '', category: 'chart', defaultSize: 'medium' }, chart_duration_dist: { id: 'chart_duration_dist', label: 'Süre Dağılımı (Bar)', icon: '', category: 'chart', defaultSize: 'medium' }, chart_dept_dist: { id: 'chart_dept_dist', label: 'Departman Dağılımı', icon: '', category: 'chart', defaultSize: 'medium' }, chart_hourly_heatmap: { id: 'chart_hourly_heatmap', label: 'Saatlik Yoğunluk Haritası', icon: '', category: 'chart', defaultSize: 'large' }, chart_aht_trend: { id: 'chart_aht_trend', label: 'AHT Günlük Trend', icon: '', category: 'chart', defaultSize: 'large' }, chart_abandon_trend: { id: 'chart_abandon_trend', label: 'Terk Oranı Trendi', icon: '', category: 'chart', defaultSize: 'large' }, chart_disposition_dist: { id: 'chart_disposition_dist', label: 'Sonuç Kodu Dağılımı', icon: '', category: 'chart', defaultSize: 'medium' }, table_agent_perf: { id: 'table_agent_perf', label: 'Sanal Temsilci (Basit)', icon: '', category: 'table', defaultSize: 'large' }, table_ai_agent_perf: { id: 'table_ai_agent_perf', label: 'Sanal Temsilci Performansı', icon: '', category: 'table', defaultSize: 'large' }, table_human_agent_perf: { id: 'table_human_agent_perf', label: 'İnsan Temsilci Performansı', icon: '', category: 'table', defaultSize: 'large' }, table_human_agent_status: { id: 'table_human_agent_status', label: 'Temsilci Statü Analizi', icon: '', category: 'table', defaultSize: 'large' }, table_dept_dist: { id: 'table_dept_dist', label: 'Departman Tablosu', icon: '', category: 'table', defaultSize: 'medium' }, table_calls: { id: 'table_calls', label: 'Çağrı Listesi (Müşteri Bilgili)', icon: '', category: 'table', defaultSize: 'large' }, summary_qc: { id: 'summary_qc', label: 'QC Özet Paneli', icon: '', category: 'summary', defaultSize: 'medium' }, }; const WIDGET_CATEGORIES = [ { id: 'kpi', label: 'KPI Kartları', icon: '' }, { id: 'chart', label: 'Grafikler', icon: '' }, { id: 'table', label: 'Tablolar', icon: '' }, { id: 'summary', label: 'Özet Paneller', icon: '' }, ]; const CHART_COLORS = ['#3b6df0', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#ec4899', '#6b7280']; const STATUS_COLORS = { ended: '#22c55e', transferred: '#3b6df0', active: '#f59e0b', abandoned: '#ef4444', failed: '#6b7280' }; function fmtDur(s) { if (!s) return '0s'; const m = Math.floor(s / 60); const sec = Math.round(s % 60); return m > 0 ? `${m}dk ${sec}s` : `${sec}s`; } // Local timezone'a göre YYYY-MM-DD üretir. // toISOString() UTC verdiği için TR'de gece 00:00–02:59 arası yanlış gün gönderiyordu. function localDateISO(d) { const pad = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; } // Widget cache anahtarı — table_calls için instance bazlı (sütun seçimi farklı // olabilir), diğer widget tiplerinde type-bazlı (aynı tip aynı veriyi paylaşır). function widgetCacheKey(w) { if (w.type === 'table_calls') { const cols = (w.variable_columns || []).slice().sort().join(','); return `${w.type}::${w.id}::${cols}::p${w.page || 1}`; } return w.type; } /* ── CSV Export ──────────────────────────────────────────────────────────── */ function exportToCSV(sortedWidgets, widgetData, dateRange, templateName, startDate, endDate) { const rows = []; const now = new Date().toLocaleString('tr-TR'); // Başlık bilgisi rows.push(['Rapor', templateName || 'Rapor Oluşturucu']); const rangeLabel = (startDate || endDate) ? ((startDate || endDate) === (endDate || startDate) ? (startDate || endDate) // tek gün : `${startDate || endDate} → ${endDate || startDate}`) : `Son ${dateRange} gün`; rows.push(['Tarih Aralığı', rangeLabel]); rows.push(['Oluşturulma', now]); rows.push([]); for (const w of sortedWidgets) { const data = widgetData[widgetCacheKey(w)]; const reg = WIDGET_REGISTRY[w.type]; rows.push([`=== ${reg?.icon || ''} ${w.title} ===`]); if (!data) { rows.push(['Veri yok']); rows.push([]); continue; } const t = w.type; if (t.startsWith('kpi_')) { rows.push(['Değer', 'Alt Bilgi']); if (t === 'kpi_avg_duration') { rows.push([fmtDur(data.value), `Maks: ${fmtDur(data.max)}`]); } else if (t === 'kpi_transfer_rate') { rows.push([`%${data.value}`, `${data.count}/${data.total}`]); } else if (t === 'kpi_qc_rate') { rows.push([`%${data.value}`, `${data.completed}/${data.total}`]); } else { rows.push([data.value ?? 0, '']); } } else if (t === 'chart_daily_trend') { rows.push(['Tarih', 'Çağrı Sayısı']); (data || []).forEach(d =>rows.push([d.date, d.count])); } else if (t === 'chart_status_dist' || t === 'chart_status_pie') { rows.push(['Durum', 'Adet']); (data || []).forEach(d =>rows.push([d.status, d.count])); } else if (t === 'chart_duration_dist') { rows.push(['Süre Aralığı', 'Adet']); (data || []).forEach(d =>rows.push([d.range, d.count])); } else if (t === 'chart_dept_dist') { rows.push(['Departman', 'Adet']); (data || []).forEach(d =>rows.push([d.dept, d.count])); } else if (t === 'chart_hourly_heatmap') { rows.push(['Saat', 'Çağrı Sayısı']); (data || []).forEach(d =>rows.push([`${String(d.hour).padStart(2, '0')}:00`, d.count])); } else if (t === 'table_agent_perf') { rows.push(['Asistan', 'Toplam', 'Tamamlanan', 'Transfer', 'Ort. Süre (sn)']); (data || []).forEach(d =>rows.push([d.name, d.total, d.ended, d.transferred, Math.round(d.avg_duration || 0)])); } else if (t === 'table_ai_agent_perf') { rows.push(['Asistan', 'Toplam', 'Tamamlanan', 'Transfer', 'Ort. Süre', 'Toplam Süre', 'Maks Süre']); (data || []).forEach(d =>rows.push([d.name, d.total, d.ended, d.transferred, fmtDur(d.avg_duration), fmtDur(d.total_duration), fmtDur(d.max_dur)])); } else if (t === 'table_human_agent_perf') { rows.push(['Temsilci', 'Uzantı', 'Dept.', 'Toplam', 'Tamamlanan', 'Terk', 'Konuşma', 'Ort.Konuşma', 'Ort.Bekleme', 'Hold', 'ACW', 'Kap.Süresi', 'Doluluk']); (data || []).forEach(d => { rows.push([d.name, d.extension, d.department, d.total_calls, d.completed_calls, d.abandoned_calls, fmtDur(d.total_talk_sec), fmtDur(d.avg_talk_sec), fmtDur(d.avg_wait_sec), fmtDur(d.hold_time_sec), fmtDur(d.acw_time_sec), fmtDur(d.handle_time_sec), `%${d.occupancy_pct}`]); if (d.queue_stats && d.queue_stats.length > 0) { rows.push([' → Kuyruk', 'Çağrı', 'Tamamlanan', 'Konuşma Süresi']); d.queue_stats.forEach(q =>rows.push([` → ${q.queue_name}`, q.calls, q.completed, fmtDur(q.talk_sec)])); } }); } else if (t === 'table_human_agent_status') { const statusSet = new Set(); (data || []).forEach(a =>Object.keys(a.statuses || {}).forEach(s =>statusSet.add(s))); const statuses = Array.from(statusSet).sort(); rows.push(['Temsilci', 'Uzantı', 'Dept.', ...statuses, 'Toplam']); (data || []).forEach(a =>rows.push([ a.agent_name, a.extension, a.department, ...statuses.map(s =>a.statuses[s] ? fmtDur(a.statuses[s]) : '—'), fmtDur(a.total_sec), ])); } else if (t === 'table_dept_dist') { rows.push(['Departman', 'Çağrı Sayısı']); (data || []).forEach(d =>rows.push([d.dept, d.count])); } else if (t === 'summary_qc') { rows.push(['Metrik', 'Değer']); rows.push(['Toplam Değerlendirme', data.total]); rows.push(['Başarılı', data.completed]); rows.push(['Başarısız', data.failed]); rows.push(['Yarıda Kalan', data.abandoned]); rows.push(['Başarı Oranı', `%${data.rate}`]); } else if (t === 'chart_aht_trend') { rows.push(['Tarih', 'Ort. Süre (sn)']); (data || []).forEach(d => rows.push([d.date, d.avg_duration])); } else if (t === 'chart_abandon_trend') { rows.push(['Tarih', 'Terk Oranı (%)', 'Terk Sayısı']); (data || []).forEach(d => rows.push([d.date, d.rate, d.count])); } else if (t === 'chart_disposition_dist') { rows.push(['Sonuç Kodu', 'Adet']); (data || []).forEach(d => rows.push([d.disposition, d.count])); } rows.push([]); } const csv = rows.map(r => r.map(v => `"${String(v ?? '').replace(/"/g, '""')}"`).join(',') ).join('\n'); const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `rapor_${new Date().toISOString().slice(0, 10)}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /* ── Widget Content Renderers ────────────────────────────────────────────── */ function WidgetKPI({ data, type }) { if (!data) return
Yükleniyor...
; let value = data.value ?? 0; let sub = ''; if (type === 'kpi_total_calls') { sub = 'toplam çağrı'; } if (type === 'kpi_today_calls') { sub = `dün: ${data.yesterday ?? 0}`; } if (type === 'kpi_avg_duration') { value = fmtDur(data.value); sub = `max: ${fmtDur(data.max)}`; } if (type === 'kpi_transfer_rate') { value = `%${data.value}`; sub = `${data.count}/${data.total}`; } if (type === 'kpi_qc_rate') { value = `%${data.value}`; sub = `${data.completed}/${data.total}`; } if (type === 'kpi_active_calls') { sub = 'anlık'; } if (type === 'kpi_aht') { value = fmtDur(data.value); sub = `${data.count ?? 0} çağrı`; } if (type === 'kpi_abandon_rate') { value = `%${data.value}`; sub = `${data.count}/${data.total}`; } if (type === 'kpi_avg_wait_time') { value = fmtDur(data.value); sub = `max: ${fmtDur(data.max)}`; } if (type === 'kpi_calls_per_hour') { value = data.value; sub = `${data.total ?? 0} çağrı / ${data.hours ?? 0} saat`; } return (
{value}
{sub}
); } function WidgetAreaChartComp({ data }) { if (!data || !data.length) return
Veri yok
; return ( v.slice(5)} /> ); } function WidgetBarChartComp({ data, dataKeyX, dataKeyY }) { if (!data || !data.length) return
Veri yok
; return ( {data.map((_, i) => )} ); } function WidgetPieChartComp({ data }) { if (!data || !data.length) return
Veri yok
; const pieData = data.map(d => ({ name: d.status, value: d.count })); return ( `${name} %${(percent * 100).toFixed(0)}`} labelLine={false}> {pieData.map((d, i) => )} ); } function WidgetHeatmap({ data }) { if (!data || !data.length) return
Veri yok
; const max = Math.max(...data.map(d =>d.count), 1); return (
{data.map(d => { const intensity = d.count / max; const bg = `rgba(59,109,240,${0.08 + intensity * 0.72})`; const color = intensity > 0.5 ? '#fff' : 'var(--text-dim)'; return (
{d.count > 0 ? d.count : ''}
); })}
00:0006:0012:0018:0023:00
); } function useSortable(data, defaultKey = null, defaultDir = 'desc') { const [sortKey, setSortKey] = useState(defaultKey); const [sortDir, setSortDir] = useState(defaultDir); const sorted = useMemo(() => { if (!data || !sortKey) return data || []; return [...data].sort((a, b) => { const va = a[sortKey] ?? 0, vb = b[sortKey] ?? 0; if (typeof va === 'string') return sortDir === 'asc' ? va.localeCompare(vb) : vb.localeCompare(va); return sortDir === 'asc' ? va - vb : vb - va; }); }, [data, sortKey, sortDir]); const toggle = (key) => { if (sortKey === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc'); else { setSortKey(key); setSortDir('desc'); } }; const indicator = (key) => sortKey === key ? (sortDir === 'asc' ? ' ▲' : ' ▼') : ''; return { sorted, toggle, indicator }; } function SortableTh({ label, field, toggle, indicator, style }) { return ( toggle(field)}> {label}{indicator(field)} ); } function WidgetAgentTable({ data }) { const { sorted, toggle, indicator } = useSortable(data, 'total'); if (!data || !data.length) return
Veri yok
; return ( {sorted.map((a, i) => ( ))}
{a.name} {a.total} {a.ended} {a.transferred} {fmtDur(a.avg_duration)}
); } function WidgetDeptTable({ data }) { const { sorted, toggle, indicator } = useSortable(data, 'count'); if (!data || !data.length) return
Veri yok
; const total = data.reduce((s, d) =>s + d.count, 0); return ( {sorted.map((d, i) => ( ))}
Oran
{d.dept} {d.count} %{total > 0 ? Math.round(d.count / total * 100) : 0}
); } function WidgetQCSummary({ data }) { if (!data) return
Yükleniyor...
; return (
Toplam Değerlendirme {data.total}
Başarılı {data.completed}
Başarısız {data.failed}
Yarıda Kalan {data.abandoned}
Başarı Oranı %{data.rate}
); } function WidgetAIAgentPerf({ data }) { const { sorted, toggle, indicator } = useSortable(data, 'total'); if (!data || !data.length) return
Veri yok
; return (
{sorted.map((a, i) => ( ))}
{a.name} {a.total} {a.ended} {a.transferred} {fmtDur(a.avg_duration)} {fmtDur(a.total_duration)} {fmtDur(a.max_dur)}
); } const HUMAN_STATUS_LABELS = { available: 'Müsait', busy: 'Meşgul', acw: 'ACW', offline: 'Çevrimdışı', hold: 'Hold', on_hold: 'Hold', }; const HUMAN_STATUS_COLORS = { available: 'var(--green)', busy: 'var(--amber)', acw: 'var(--blue)', offline: 'var(--text-dim)', hold: '#f59e0b', on_hold: '#f59e0b', }; function WidgetHumanAgentPerf({ data }) { const [expanded, setExpanded] = useState({}); const { sorted, toggle: sortToggle, indicator } = useSortable(data, 'total_calls'); if (!data || !data.length) return
Veri yok
; const toggleRow = name =>setExpanded(prev => ({ ...prev, [name]: !prev[name] })); return (
{sorted.map((a, i) => ( {expanded[a.name] && (a.queue_stats || []).map((q, j) => ( ))} ))}
Uzantı HoldACW
{a.name} {a.extension} {a.department} {a.total_calls} {a.completed_calls} {a.abandoned_calls} {fmtDur(a.total_talk_sec)} {fmtDur(a.avg_talk_sec)} {fmtDur(a.avg_wait_sec)} {fmtDur(a.hold_time_sec)} {fmtDur(a.acw_time_sec)} {fmtDur(a.handle_time_sec)} 80 ? 'var(--red)' : 'var(--green)', fontWeight:600 }}> %{a.occupancy_pct} {a.queue_stats && a.queue_stats.length > 0 && ( )}
↳ {q.queue_name} {q.calls} {q.completed} {fmtDur(q.talk_sec)}
); } function WidgetHumanAgentStatus({ data }) { const { sorted, toggle, indicator } = useSortable(data, 'total_sec'); if (!data || !data.length) return
Veri yok
; const statusSet = new Set(); data.forEach(a =>Object.keys(a.statuses || {}).forEach(s =>statusSet.add(s))); const statuses = Array.from(statusSet).sort(); return (
{statuses.map(s => )} {sorted.map((a, i) => ( {statuses.map(s => ( ))} ))}
Uzantı {HUMAN_STATUS_LABELS[s] || s}
{a.agent_name} {a.extension} {a.department} {a.statuses[s] ? fmtDur(a.statuses[s]) : '—'} {fmtDur(a.total_sec)}
); } // ─── Çağrı Listesi (drill-down: WS metadata sütunlarıyla) ─────────────────── function WidgetCallsTable({ data, widget, onUpdate, variableKeyOptions }) { const items = (data && data.items) || []; const total = (data && data.total) || 0; const page = (data && data.page) || 1; const pages = (data && data.pages) || 1; const cols = (widget && widget.variable_columns) || []; const [pickerOpen, setPickerOpen] = useState(false); const allKeys = (variableKeyOptions || []).slice().sort(); function toggleCol(k) { if (!onUpdate) return; const next = cols.includes(k) ? cols.filter(c => c !== k) : [...cols, k]; onUpdate({ ...widget, variable_columns: next, page: 1 }); } function setPage(p) { if (!onUpdate) return; onUpdate({ ...widget, page: p }); } return (
Toplam: {total} çağrı
{pickerOpen && (
{allKeys.length === 0 ? (
WS değişkeni bulunamadı. WebServis Entegrasyonlarınızı kontrol edin.
) : allKeys.map(k => )}
)}
{cols.map(k => )} {items.length === 0 ? ( ) : items.map(c => {cols.map(k => )} )}
Tarih Telefon Agent Süre Durum{k}
Bu kriterlerle çağrı bulunamadı.
{fmtDateTime(c.start_time)} {c.phone_number || '—'} {c.agent_name || '—'} {Math.round(c.duration || 0)}s {c.status || '—'} {(c.variables && c.variables[k]) || '—'}
{pages > 1 && (
{page} / {pages}
)}
); } function renderWidgetContent(widget, data, ctx) { const t = widget.type; if (t.startsWith('kpi_')) return ; if (t === 'chart_daily_trend') return ; if (t === 'chart_status_dist') return ; if (t === 'chart_status_pie') return ; if (t === 'chart_duration_dist') return ; if (t === 'chart_dept_dist') return ; if (t === 'chart_hourly_heatmap') return ; if (t === 'chart_aht_trend') return ({date:d.date, count:d.avg_duration}))} />; if (t === 'chart_abandon_trend') return ({date:d.date, count:d.rate}))} />; if (t === 'chart_disposition_dist') return ; if (t === 'table_agent_perf') return ; if (t === 'table_ai_agent_perf') return ; if (t === 'table_human_agent_perf') return ; if (t === 'table_human_agent_status') return ; if (t === 'table_dept_dist') return ; if (t === 'table_calls') return ; if (t === 'summary_qc') return ; return
Bilinmeyen widget
; } /* ── Main Component ──────────────────────────────────────────────────────── */ function ReportBuilderTab({ toast }) { // ── Templates const [templates, setTemplates] = useState([]); const [activeTemplateId, setActiveTemplateId] = useState(null); const [templateName, setTemplateName] = useState(''); // ── Canvas state const [widgets, setWidgets] = useState([]); const [dateRange, setDateRange] = useState(14); const [startDate, setStartDate] = useState(''); // YYYY-MM-DD — özel aralık başlangıç const [endDate, setEndDate] = useState(''); // YYYY-MM-DD — özel aralık bitiş (dahil) const [widgetData, setWidgetData] = useState({}); const [loadingData, setLoadingData] = useState(false); // ── Palette const [paletteOpen, setPaletteOpen] = useState(true); // ── Gelişmiş filtreler const [filterOptions, setFilterOptions] = useState({ agents: [], departments: [], dispositions: [], statuses: [] }); const [filters, setFilters] = useState({ agent_id: '', department: '', disposition: '', status: '', phone: '' }); const [filtersOpen, setFiltersOpen] = useState(false); const activeFilterCount = Object.values(filters).filter(v => v).length; // ── Load templates and filter options on mount useEffect(() => { loadTemplates(); authFetch('/api/reports/filter-options') .then(r => r.json()) .then(d => setFilterOptions(d)) .catch(() => {}); }, []); async function loadTemplates() { try { const r = await authFetch('/api/reports/templates'); const data = await r.json(); setTemplates(data); // Auto-load default template const def = data.find(t =>t.is_default); if (def) loadTemplate(def); } catch (e) { console.error('Template load error:', e); } } function loadTemplate(tpl) { setActiveTemplateId(tpl.id); setTemplateName(tpl.name); const cfg = tpl.config || {}; setWidgets(cfg.widgets || []); if (cfg.dateRange) { const d = parseInt(cfg.dateRange); if (!isNaN(d)) setDateRange(d); } setStartDate(cfg.startDate || ''); setEndDate(cfg.endDate || ''); } // ── Build filter query string const filterQS = useMemo(() => { let qs = ''; if (filters.agent_id) qs += `&agent_id=${filters.agent_id}`; if (filters.department) qs += `&department=${encodeURIComponent(filters.department)}`; if (filters.disposition) qs += `&disposition=${encodeURIComponent(filters.disposition)}`; if (filters.status) qs += `&status=${encodeURIComponent(filters.status)}`; if (filters.phone) qs += `&phone=${encodeURIComponent(filters.phone)}`; return qs; }, [filters]); // ── Özel tarih aralığı query parçası // // Sektör standardı (Datadog/Grafana/Mixpanel): tek tarih = tek gün. // Kullanıcı yalnızca başlangıç ya da bitiş seçerse, backend single-day // olarak yorumlar. Ayrıca kullanıcının IANA timezone'u backend'e gönderilir // ki gün sınırları doğru hesaplansın (UTC kaymasından kaynaklı kayıp/eksik // kayıt görüntüleme önlenir). const userTz = useMemo(() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch (e) { return ''; } }, []); const dateQS = useMemo(() => { const tzPart = userTz ? `&tz=${encodeURIComponent(userTz)}` : ''; if (startDate || endDate) { const s = startDate || endDate; const e = endDate || startDate; return `&start_date=${s}&end_date=${e}${tzPart}`; } return `&days=${dateRange}${tzPart}`; }, [startDate, endDate, dateRange, userTz]); // ── Fetch widget data when widgets, dateRange/özel tarih or filters change // // Çoğu widget aynı type için aynı veriyi döndürür (ör. iki kpi_total_calls // identik veri çeker), o yüzden type-bazlı cache kullanırız. table_calls ise // her instance'ın kendi sütun seçimi (variable_columns) olduğu için instance // bazlı (widget.id) cache key kullanılır. function _widgetQS(w) { let qs = ''; if (w.type === 'table_calls') { const cols = (w.variable_columns || []).join(','); if (cols) qs += `&variable_columns=${encodeURIComponent(cols)}`; qs += `&page=${w.page || 1}&per_page=${w.per_page || 25}`; } return qs; } useEffect(() => { if (widgets.length === 0) { setWidgetData({}); return; } const seen = new Set(); const targets = widgets.filter(w => { const k = widgetCacheKey(w); if (seen.has(k)) return false; seen.add(k); return true; }); setLoadingData(true); Promise.all( targets.map(w => authFetch(`/api/reports/widget-data?widget_type=${w.type}${dateQS}${filterQS}${_widgetQS(w)}`) .then(r =>r.json()) .then(data => ({ key: widgetCacheKey(w), data })) .catch(() => ({ key: widgetCacheKey(w), data: null })) ) ).then(results => { const map = {}; results.forEach(r => { map[r.key] = r.data; }); setWidgetData(map); setLoadingData(false); }); }, [widgets.length, dateQS, filterQS, widgets.map(w => `${w.id}:${w.type}:${(w.variable_columns||[]).join(',')}:${w.page||1}`).join('|')]); // ── Widget operations function addWidget(typeId) { const reg = WIDGET_REGISTRY[typeId]; if (!reg) return; const w = { id: 'w_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6), type: typeId, title: reg.label, size: reg.defaultSize, position: widgets.length, }; setWidgets(prev => [...prev, w]); } function removeWidget(id) { setWidgets(prev =>prev.filter(w =>w.id !== id).map((w, i) => ({ ...w, position: i }))); } function moveWidget(id, dir) { setWidgets(prev => { const list = [...prev].sort((a, b) =>a.position - b.position); const idx = list.findIndex(w =>w.id === id); if (idx < 0) return prev; const target = idx + dir; if (target < 0 || target >= list.length) return prev; [list[idx], list[target]] = [list[target], list[idx]]; return list.map((w, i) => ({ ...w, position: i })); }); } function resizeWidget(id) { const sizes = ['small', 'medium', 'large']; setWidgets(prev =>prev.map(w => { if (w.id !== id) return w; const idx = sizes.indexOf(w.size); return { ...w, size: sizes[(idx + 1) % sizes.length] }; })); } // ── Save/Delete template async function saveTemplate() { if (!templateName.trim()) { toast('Şablon adı gerekli', 'error'); return; } const config = { dateRange: String(dateRange), startDate, endDate, widgets }; try { if (activeTemplateId) { await authFetch(`/api/reports/templates/${activeTemplateId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: templateName, config }), }); toast('Şablon güncellendi', 'success'); } else { const r = await authFetch('/api/reports/templates', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: templateName, config }), }); const data = await r.json(); setActiveTemplateId(data.id); toast('Şablon kaydedildi', 'success'); } loadTemplates(); } catch (e) { toast('Kaydetme hatası: ' + e.message, 'error'); } } async function deleteTemplate() { if (!activeTemplateId) return; if (!confirm('Bu şablonu silmek istediğinize emin misiniz?')) return; try { await authFetch(`/api/reports/templates/${activeTemplateId}`, { method: 'DELETE' }); toast('Şablon silindi', 'success'); setActiveTemplateId(null); setTemplateName(''); setWidgets([]); loadTemplates(); } catch (e) { toast('Silme hatası', 'error'); } } function newTemplate() { setActiveTemplateId(null); setTemplateName(''); setWidgets([]); } // ── Sorted widgets const sortedWidgets = useMemo(() => [...widgets].sort((a, b) =>a.position - b.position), [widgets] ); const sizeLabel = { small: 'S', medium: 'M', large: 'L' }; // ═══════════════════════════ RENDER ═══════════════════════════ return (
{/* ── Header ── */}

Rapor Oluşturucu

Widget'ları seçerek kendi raporunuzu tasarlayın, şablon olarak kaydedin.

{/* ── Toolbar ── */}
setTemplateName(e.target.value)} /> {activeTemplateId && ( )}
{/* Export butonu — sadece canvas'ta widget varsa göster */} {sortedWidgets.length > 0 && (
)}
{[7, 14, 30, 60, 90].map(d => ( ))}
{/* Preset'ler kullanıcının LOCAL timezone'una göre hesaplanır. Önceki kod toISOString() ile UTC günü kullanıyordu; TR'de gece 00:00–02:59 arası "Bugün" butonu dünü gönderiyordu. */}
setStartDate(e.target.value)} title="Başlangıç tarihi" /> setEndDate(e.target.value)} title="Bitiş tarihi (dahil)" /> {(startDate || endDate) && ( )}
{/* ── Filtre Çubuğu ── */}
{activeFilterCount > 0 && ( )}
{filtersOpen && (
setFilters(f => ({ ...f, phone: e.target.value }))} />
)} {/* ── Layout: Palette + Canvas ── */}
{/* ── Palette ── */}
Widget Paleti
{WIDGET_CATEGORIES.map(cat => { const items = Object.values(WIDGET_REGISTRY).filter(w =>w.category === cat.id); return (
{cat.label}
{items.map(w => (
addWidget(w.id)}> {w.icon && {w.icon}} {w.label} {w.defaultSize[0].toUpperCase()}
))}
); })}
{/* ── Canvas ── */}
{sortedWidgets.length === 0 ? (
Soldaki paletten widget'ları tıklayarak raporunuzu oluşturun.
) : sortedWidgets.map((w, idx) => { const reg = WIDGET_REGISTRY[w.type]; const data = widgetData[widgetCacheKey(w)]; return (
{w.title}
{loadingData && !data ?
Yükleniyor...
: renderWidgetContent(w, data) }
); })}
); } window.ReportBuilderTab = ReportBuilderTab;