/* AAU CRM — Call Log + QC, Inbox Daily Report, Data Sync Explorer */
/* ── Date range + period comparison ── */
const REPORT_TODAY = new Date('2026-06-02T00:00:00');
const CMP_PROFILE = {
none: null,
prev: { label: 'kỳ trước liền kề', shift: 'adjacent', f: 0.9, respond: 89, frt: 4.7, qc: 7.6, callQc: 6.6, pass: 44 },
lastmonth: { label: 'cùng kỳ tháng trước', shift: 30, f: 0.8, respond: 86, frt: 5.3, qc: 7.3, callQc: 6.4, pass: 41 },
};
function fmtDM(d) { return ('0' + d.getDate()).slice(-2) + '/' + ('0' + (d.getMonth() + 1)).slice(-2); }
function rangeDays(range, custom) {
if (range === 'custom') {
const p = s => { const [d, m, y] = s.split('/').map(Number); return new Date(y, m - 1, d); };
try { return Math.max(1, Math.round((p(custom.to) - p(custom.from)) / 864e5) + 1); } catch (e) { return 14; }
}
return { '1d': 1, '7d': 7, '30d': 30 }[range] || 1;
}
function spanForDays(days, endOffset = 0) {
const end = new Date(REPORT_TODAY); end.setDate(end.getDate() - endOffset);
const start = new Date(end); start.setDate(start.getDate() - (days - 1));
return { label: fmtDM(start) + ' – ' + fmtDM(end) };
}
function compareSpan(days, cmpKey) {
const p = CMP_PROFILE[cmpKey]; if (!p) return null;
return p.shift === 'adjacent' ? spanForDays(days, days) : spanForDays(days, p.shift);
}
function deltaPct(cur, cmp) { return cmp ? Math.round(((cur - cmp) / cmp) * 100) : 0; }
function DateRangeBar({ range, setRange, custom, setCustom, cmp, setCmp }) {
const days = rangeDays(range, custom);
const cur = range === 'custom' ? { label: custom.from + ' – ' + custom.to } : spanForDays(days);
const cs = compareSpan(days, cmp);
return (
{range === 'custom' && (
setCustom({ ...custom, from: v })} style={{ width: 132 }} />
→
setCustom({ ...custom, to: v })} style={{ width: 132 }} />
)}
{cur.label} · {days} ngày
So sánh với:
{cs && ({cs.label})}
);
}
function InboxHeatmap() {
const days = ['T2', 'T3', 'T4', 'T5', 'T6', 'T7', 'CN'];
const bands = ['8–10h', '10–12h', '12–14h', '14–16h', '16–18h', '18–20h'];
// [day][band]
const grid = [
[14, 28, 16, 30, 38, 22],
[18, 32, 18, 34, 40, 26],
[16, 30, 14, 36, 44, 30],
[20, 34, 20, 38, 46, 28],
[22, 36, 18, 32, 42, 34],
[28, 40, 24, 30, 36, 44],
[24, 30, 20, 22, 28, 38],
];
const max = Math.max(...grid.flat());
return (
{bands.map(b =>
{b}
)}
{days.map((d, di) => (
{d}
{grid[di].map((v, bi) => { const a = 0.12 + 0.88 * (v / max); return (
0.55 ? '#fff' : '#573b8a', fontSize: 11, fontWeight: 600, cursor: 'default' }}>{v}
); })}
))}
Ít
{[0.18, 0.4, 0.6, 0.8, 1].map(a => )}
Nhiều
Khung 16–18h T4–T6 là giờ vàng — nên xếp đủ trực.
);
}
const DISPO = { consulted: { l: 'Đã tư vấn', tone: 'success', c: '#0a9e6e' }, callback: { l: 'Hẹn gọi lại', tone: 'info', c: '#3b82f6' }, noanswer: { l: 'Không nghe máy', tone: 'warning', c: '#d97706' }, refused: { l: 'Từ chối', tone: 'critical', c: '#ef4444' }, wrong: { l: 'Sai số', tone: 'neutral', c: '#9ca3af' } };
const CALLTYPE = { new: 'Tư vấn mới', followup: 'Follow-up', care: 'CSKH' };
const CALL_DATA = [
{ id: 'c1', leadId: 'L1', dir: 'out', dur: '8:42', agent: 'u5', mins: 35, qcA: 8.2, qcH: 8.5, status: 'reviewed', disp: 'consulted', type: 'followup', talk: { a: 58, c: 42, int: 1, sil: 6 }, kw: ['nhượng quyền', 'học phí'], viol: [] },
{ id: 'c2', leadId: 'L2', dir: 'out', dur: '12:10', agent: 'u4', mins: 80, qcA: 9.0, qcH: 9.1, status: 'reviewed', disp: 'consulted', type: 'followup', talk: { a: 62, c: 38, int: 0, sil: 4 }, kw: ['proposal', 'vận hành'], viol: [] },
{ id: 'c3', leadId: 'L3', dir: 'in', dur: '3:24', agent: 'u4', mins: 120, qcA: 5.8, qcH: 5.5, status: 'disputed', disp: 'noanswer', type: 'new', talk: { a: 70, c: 30, int: 3, sil: 18 }, kw: ['marketing'], viol: ['Báo giá khi chưa qualify', 'Không chốt lịch hẹn'] },
{ id: 'c4', leadId: 'L6', dir: 'out', dur: '1:05', agent: 'u4', mins: 200, qcA: 4.2, qcH: null, status: 'auto', disp: 'noanswer', type: 'followup', talk: { a: 88, c: 12, int: 0, sil: 40 }, kw: [], viol: ['Không lắng nghe nhu cầu'] },
{ id: 'c5', leadId: 'L4', dir: 'out', dur: '6:30', agent: 'u9', mins: 240, qcA: 6.6, qcH: null, status: 'auto', disp: 'callback', type: 'new', talk: { a: 55, c: 45, int: 2, sil: 8 }, kw: ['AI', 'online'], viol: ['Không chốt lịch hẹn'] },
{ id: 'c6', leadId: 'L5', dir: 'out', dur: '9:12', agent: 'u5', mins: 300, qcA: 7.4, qcH: 7.6, status: 'reviewed', disp: 'consulted', type: 'new', talk: { a: 60, c: 40, int: 1, sil: 5 }, kw: ['nhượng quyền', 'trà sữa'], viol: [] },
{ id: 'c7', leadId: 'L8', dir: 'out', dur: '4:48', agent: 'u4', mins: 360, qcA: 8.1, qcH: null, status: 'auto', disp: 'consulted', type: 'new', talk: { a: 57, c: 43, int: 0, sil: 7 }, kw: ['chi phí', 'hải sản'], viol: [] },
{ id: 'c8', leadId: 'L7', dir: 'in', dur: '2:10', agent: 'u9', mins: 420, qcA: 5.0, qcH: 4.8, status: 'reviewed', disp: 'refused', type: 'new', talk: { a: 64, c: 36, int: 4, sil: 12 }, kw: ['giảm giá'], viol: ['Báo giá khi chưa qualify', 'Thái độ chưa chuẩn'] },
{ id: 'c9', leadId: 'L9', dir: 'out', dur: '0:48', agent: 'u9', mins: 480, qcA: 3.6, qcH: null, status: 'auto', disp: 'wrong', type: 'new', talk: { a: 90, c: 10, int: 0, sil: 30 }, kw: [], viol: ['Sai số'] },
{ id: 'c10', leadId: 'L10', dir: 'out', dur: '7:20', agent: 'u4', mins: 540, qcA: 8.8, qcH: 9.0, status: 'reviewed', disp: 'consulted', type: 'care', talk: { a: 61, c: 39, int: 0, sil: 3 }, kw: ['nâng cao', 'gia hạn'], viol: [] },
{ id: 'c11', leadId: 'L2', dir: 'out', dur: '5:55', agent: 'u5', mins: 600, qcA: 7.0, qcH: null, status: 'auto', disp: 'callback', type: 'followup', talk: { a: 59, c: 41, int: 1, sil: 9 }, kw: ['proposal'], viol: ['Không chốt lịch hẹn'] },
{ id: 'c12', leadId: 'L3', dir: 'out', dur: '3:02', agent: 'u5', mins: 660, qcA: 6.2, qcH: null, status: 'auto', disp: 'consulted', type: 'followup', talk: { a: 66, c: 34, int: 2, sil: 11 }, kw: ['đối thủ'], viol: ['Xử lý phản đối yếu'] },
{ id: 'c13', leadId: 'L1', dir: 'in', dur: '10:05', agent: 'u5', mins: 720, qcA: 8.4, qcH: 8.6, status: 'reviewed', disp: 'consulted', type: 'followup', talk: { a: 54, c: 46, int: 0, sil: 5 }, kw: ['chốt', 'early-bird'], viol: [] },
{ id: 'c14', leadId: 'L5', dir: 'out', dur: '2:40', agent: 'u9', mins: 800, qcA: 5.4, qcH: null, status: 'auto', disp: 'noanswer', type: 'new', talk: { a: 80, c: 20, int: 1, sil: 22 }, kw: [], viol: [] },
{ id: 'c15', leadId: 'L4', dir: 'out', dur: '6:14', agent: 'u4', mins: 880, qcA: 7.8, qcH: null, status: 'auto', disp: 'callback', type: 'followup', talk: { a: 58, c: 42, int: 0, sil: 6 }, kw: ['lịch khai giảng'], viol: [] },
{ id: 'c16', leadId: 'L8', dir: 'out', dur: '4:30', agent: 'u5', mins: 960, qcA: 6.9, qcH: null, status: 'auto', disp: 'consulted', type: 'new', talk: { a: 63, c: 37, int: 2, sil: 8 }, kw: ['chi phí'], viol: ['Không chốt lịch hẹn'] },
];
function callScore(c) { return c.qcH != null ? c.qcH : c.qcA; }
function callVerdict(c) { return callScore(c) >= 7 ? 'pass' : 'fail'; }
const SAMPLE_TRANSCRIPT = [
{ t: '00:03', who: 'agent', s: 'pos', text: 'Dạ em chào anh, em gọi từ AAU Academy về khóa anh đang quan tâm ạ.' },
{ t: '00:11', who: 'cust', s: 'neu', text: 'Ừ, mà anh đang hơi bận, học phí nhiêu vậy em?' },
{ t: '00:18', who: 'agent', s: 'neg', text: 'Dạ khóa học phí 18.9 triệu anh ạ.', viol: 'Báo giá khi chưa qualify' },
{ t: '00:27', who: 'cust', s: 'neg', text: 'Đắt vậy, bên đối thủ rẻ hơn mà.' },
{ t: '00:35', who: 'agent', s: 'neu', text: 'Dạ để em gửi anh so sánh giá trị và case study chuỗi tương tự nhé.' },
{ t: '00:48', who: 'cust', s: 'pos', text: 'Ừ em gửi đi, mà chốt lịch khai giảng giúp anh luôn.' },
];
function hl(text, kws) {
if (!kws || !kws.length) return text;
let nodes = [text];
kws.forEach((k, ki) => {
const next = [];
nodes.forEach(node => {
if (typeof node !== 'string') { next.push(node); return; }
const low = node.toLowerCase(), kl = k.toLowerCase();
let idx = 0, pos;
while ((pos = low.indexOf(kl, idx)) !== -1) {
if (pos > idx) next.push(node.slice(idx, pos));
next.push({node.slice(pos, pos + k.length)});
idx = pos + k.length;
}
if (idx < node.length) next.push(node.slice(idx));
});
nodes = next;
});
return nodes;
}
function TalkBar({ talk }) {
const over = talk.a > 65;
return (
Agent {talk.a}%Khách {talk.c}%
Ngắt lời: {talk.int}Khoảng lặng dài: {talk.sil}s{over && Agent nói quá nhiều}
);
}
function CoachItem({ who, at, text, ack }) {
return (
{who}{at}{ack ? Agent đã đọc : Chờ xác nhận}
{text}
);
}
// Chuẩn hoá 1 cuộc gọi từ backend (AAU.calls: {id,leadId,dir,duration,agent,at,qc,verdict})
// về shape mà UI QC cần — các field QC sâu (talk/viol/kw) để mặc định rỗng cho tới khi
// engine QC chấm (CQA). KHÔNG còn dùng CALL_DATA hardcode.
function normCall(c) {
const dispMap = { pass: 'consulted', fail: 'refused', callback: 'callback', 'no-answer': 'noanswer', noanswer: 'noanswer' };
return {
id: c.id, leadId: c.leadId, dir: c.dir || 'out',
dur: c.duration || c.dur || '—', agent: c.agent, mins: c.mins || 0,
qcA: c.qc != null ? c.qc : (c.qcA != null ? c.qcA : null),
qcH: c.qcH != null ? c.qcH : null, status: c.status || 'auto',
disp: c.disp || dispMap[c.verdict] || 'consulted', type: c.type || 'new',
talk: c.talk || { a: 0, c: 0, int: 0, sil: 0 }, kw: c.kw || [], viol: c.viol || [], at: c.at,
};
}
function CallLogEmpty() {
return (
);
}
// Wrapper: rỗng → EmptyState (tránh chia 0 / crash); có data → body đọc AAU.calls thật.
function CallLogQC() {
if (!AAU.calls || AAU.calls.length === 0) return ;
return ;
}
function CallLogQCBody() {
const [open, setOpen] = useState(null);
const [playing, setPlaying] = useState(false);
const [cbSet, setCbSet] = useState(false);
const [range, setRange] = useState('1d');
const [custom, setCustom] = useState({ from: '19/05/2026', to: '02/06/2026' });
const [cmp, setCmp] = useState('prev');
const [fAgent, setFAgent] = useState('all');
const [fVerdict, setFVerdict] = useState('all');
const [fDisp, setFDisp] = useState('all');
const [fType, setFType] = useState('all');
const [tab, setTab] = useState('qc');
const [coach, setCoach] = useState('');
const [coachSent, setCoachSent] = useState(false);
const [cbDone, setCbDone] = useState({});
useEffect(() => { setCbSet(false); setPlaying(false); setTab('qc'); setCoach(''); setCoachSent(false); }, [open]);
const ruleset = AAU.qcRulesets[1];
const sales = AAU.users.filter(u => u.role === 'sales');
const enrich = c => { const u = AAU.users.find(x => x.id === c.agent); return { ...c, lead: AAU.leadById(c.leadId) || { name: c.leadId || 'Khách', company: '' }, agentName: u?.name, agentColor: u?.color, score: callScore(c), verdict: callVerdict(c), at: c.at || new Date(Date.now() - (c.mins || 0) * 60000).toISOString() }; };
const all = AAU.calls.map(normCall).map(enrich);
const rows = all.filter(c => (fAgent === 'all' || c.agent === fAgent) && (fVerdict === 'all' || c.verdict === fVerdict) && (fDisp === 'all' || c.disp === fDisp) && (fType === 'all' || c.type === fType));
const days = rangeDays(range, custom);
const prof = CMP_PROFILE[cmp];
const cmpOn = !!prof;
const perDay = 16;
const calls = perDay * days;
const totalMin = 96 * days;
const durLabel = totalMin >= 60 ? Math.floor(totalMin / 60) + ' giờ ' + (totalMin % 60) + ' phút' : totalMin + ' phút';
const passRate = Math.round(all.filter(c => c.verdict === 'pass').length / all.length * 100);
const qcAvg = (all.reduce((s, c) => s + c.score, 0) / all.length).toFixed(1).replace('.', ',');
const dCalls = cmpOn ? deltaPct(calls, perDay * days * prof.f) : null;
const dQc = cmpOn ? deltaPct(6.9, prof.callQc) : null;
const dPass = cmpOn ? passRate - prof.pass : null;
const periodLabel = range === 'custom' ? custom.from + ' – ' + custom.to : spanForDays(days).label;
const cmpLabel = cmpOn ? compareSpan(days, cmp).label : '';
const byAgent = sales.map(u => { const cs = rows.filter(c => c.agent === u.id); const n = cs.length; const avg = n ? cs.reduce((s, c) => s + c.score, 0) / n : 0; return { u, n, avg, pass: n ? Math.round(cs.filter(c => c.verdict === 'pass').length / n * 100) : 0 }; }).sort((a, b) => b.avg - a.avg);
const violCounts = {};
rows.forEach(c => c.viol.forEach(v => { violCounts[v] = (violCounts[v] || 0) + 1; }));
const topViol = Object.entries(violCounts).map(([l, v]) => ({ l, v })).sort((a, b) => b.v - a.v).slice(0, 5);
const violMax = Math.max(1, ...topViol.map(t => t.v));
const dispAgg = Object.keys(DISPO).map(k => ({ l: DISPO[k].l, v: rows.filter(c => c.disp === k).length, color: DISPO[k].c })).filter(d => d.v > 0);
const queue = all.filter(c => ['callback', 'noanswer'].includes(c.disp));
const trendDays = Math.min(Math.max(days, 7), 30);
const trend = Array.from({ length: trendDays }, (_, i) => { const d = new Date(REPORT_TODAY); d.setDate(d.getDate() - (trendDays - 1 - i)); return { l: fmtDM(d), v: 12 + ((i * 7 + 5) % 9) }; });
const qcTrend = Array.from({ length: trendDays }, (_, i) => { const d = new Date(REPORT_TODAY); d.setDate(d.getDate() - (trendDays - 1 - i)); return { l: fmtDM(d), v: +(6.2 + ((i * 3 + 2) % 16) / 10).toFixed(1) }; });
const cols = [
{ key: 'lead', label: 'Khách hàng', render: c => { e.stopPropagation(); navigate('/leads/' + c.leadId); }}>{c.lead.name}
{c.lead.company}
, csv: c => c.lead.name },
{ key: 'type', label: 'Loại', render: c => {CALLTYPE[c.type]} },
{ key: 'dir', label: 'Hướng', render: c => {c.dir === 'out' ? 'Gọi đi' : 'Gọi đến'} },
{ key: 'disp', label: 'Kết quả', render: c => {DISPO[c.disp].l} },
{ key: 'dur', label: 'Thời lượng', num: true },
{ key: 'agentName', label: 'Agent' },
{ key: 'at', label: 'Thời gian', render: c => relTime(c.at) + ' trước', csv: c => c.mins + 'm' },
{ key: 'score', label: 'QC', num: true, render: c => = 7 ? '#0e7c4a' : '#c4320a' }}>{c.score}{c.qcH != null ? QC : AI} },
{ key: 'verdict', label: 'Verdict', render: c => {c.verdict.toUpperCase()} },
{ key: 'review', label: 'Review', render: c => c.status === 'reviewed' ? Đã duyệt : c.status === 'disputed' ? Khiếu nại : AI tự động },
{ key: 'act', label: '', sortable: false, render: c => },
];
const toolbar = (
);
return (
>} />
{cmpOn && (
Đang so sánh {periodLabel} với {cmpLabel} ({prof.label}).
)}
{queue.length > 0 && (
{queue.map(c => cbDone[c.id] ? (
Đã xử lý — {c.lead.name}
) : (
navigate('/leads/' + c.leadId)}>{c.lead.name} · {c.lead.company}
{DISPO[c.disp].l} · {relTime(c.at)} trước · phụ trách {c.agentName}
Hạn: hôm nay
))}
)}
v.toFixed(1)} />
| Agent | Cuộc | QC TB | PASS |
{byAgent.map((a, i) => (
| {i + 1}{a.u.name} |
{a.n} |
= 7 ? '#0e7c4a' : '#c4320a' }} className="fw7 tnum">{a.n ? a.avg.toFixed(1) : '—'} |
{a.n ? a.pass + '%' : '—'} |
))}
{topViol.length === 0 ? Không có vi phạm trong bộ lọc hiện tại.
: (
)}
{dispAgg.length === 0 ? Không có dữ liệu.
: (
{dispAgg.map(d =>
{d.l}{d.v}
)}
)}
Nhật ký cuộc gọi
c.id} searchKeys={['agentName']} toolbar={toolbar} onRowClick={c => setOpen(c)} exportName="calls" pageSize={8} />
{open && (
setOpen(null)}>
navigate('/leads/' + open.leadId)}>{open.lead.name}
{open.lead.company} · {open.agentName} · {CALLTYPE[open.type]}
{open.verdict.toUpperCase()}
AI chấm{open.qcA}
QC người{open.qcH != null ? open.qcH : '—'}
Review{open.status === 'reviewed' ? 'Đã duyệt' : open.status === 'disputed' ? 'Khiếu nại' : 'AI tự động'}
{tab === 'qc' && (
PHÂN TÍCH HỘI THOẠI
CHẤM ĐIỂM THEO TIÊU CHÍ
{ruleset.criteria.map((cr, i) => { const sc = [8, 6, 7, 4][i] ?? 6; return (
{cr.name} ({cr.weight}%){sc}/10
); })}
{open.viol.length > 0 && (
VI PHẠM PHÁT HIỆN
{open.viol.map((v, i) =>
{v}
)}
)}
KẾT QUẢ & FOLLOW-UP
Kết quả cuộc gọi{DISPO[open.disp].l}
)}
{tab === 'transcript' && (
Click một dòng để tua tới đoạn ghi âm tương ứng. Từ khóa quan trọng được tô sáng.
{SAMPLE_TRANSCRIPT.map((l, i) => { const sc = { pos: '#0a9e6e', neu: '#8a8a8a', neg: '#c4320a' }[l.s]; return (
setPlaying(true)} className="row g10" style={{ cursor: 'pointer' }}>
{l.t}
{l.who === 'agent' ? 'Agent' : 'Khách'}
{hl(l.text, open.kw)}
{l.viol &&
Vi phạm: {l.viol}}
); })}
{open.kw.length > 0 &&
{open.kw.map(k => {k})}
}
)}
{tab === 'coaching' && (
GỬI NHẬN XÉT CHO {(open.agentName || '').toUpperCase()}
LỊCH SỬ COACHING
{coachSent && }
)}
)}
);
}
function InboxReportEmpty() {
return (
);
}
// Wrapper: chưa có hội thoại thật → EmptyState (không hiện báo cáo giả).
function InboxDailyReport() {
if (!AAU.conversations || Object.keys(AAU.conversations).length === 0) return ;
return ;
}
function InboxDailyReportBody() {
const bySource = [
{ page: 'Fanpage F&B Academy', ch: 'fb', sync: true, data: 96, replied: 92, frt: '3,1 ph', qc: 8.1 },
{ page: 'Fanpage Nhượng quyền', ch: 'fb', sync: true, data: 46, replied: 40, frt: '6,4 ph', qc: 7.4 },
{ page: 'Zalo OA Học viện', ch: 'zalo', sync: true, data: 86, replied: 82, frt: '2,8 ph', qc: 8.4 },
{ page: '@aau.academy', ch: 'instagram', sync: true, data: 54, replied: 47, frt: '5,2 ph', qc: 7.6 },
{ page: 'TikTok @aaufnb', ch: 'tiktok', sync: false, data: 38, replied: 33, frt: '7,8 ph', qc: 7.0 },
];
const byChannel = [
{ l: 'FB', v: 142, color: '#0084ff' }, { l: 'Zalo', v: 86, color: '#0068ff' }, { l: 'IG', v: 54, color: '#d62976' }, { l: 'TikTok', v: 38, color: '#111' },
];
const hourly = ['8h', '9h', '10h', '11h', '12h', '13h', '14h', '15h', '16h', '17h', '18h', '19h'].map((l, i) => ({ l, v: [12, 28, 34, 30, 18, 14, 26, 38, 42, 36, 22, 16][i] }));
const leaderboard = [
{ name: 'Phạm Tuấn', color: '#0a9e6e', convos: 64, respond: 95, frt: '2,6 ph', qc: 8.4, closes: 3 },
{ name: 'Ngô Hùng', color: '#d97706', convos: 58, respond: 91, frt: '3,4 ph', qc: 7.9, closes: 2 },
{ name: 'Lê Vy', color: '#d62976', convos: 47, respond: 86, frt: '5,1 ph', qc: 7.3, closes: 1 },
];
const lostReasons = [
{ l: 'Ngân sách chưa sẵn sàng', v: 34, color: '#ef4444' },
{ l: 'Chọn đối thủ', v: 21, color: '#f59e0b' },
{ l: 'Chưa đúng thời điểm', v: 18, color: '#3b82f6' },
{ l: 'Không phản hồi tiếp', v: 15, color: '#8a8a8a' },
{ l: 'Sai nhu cầu / không phù hợp', v: 9, color: '#8b5cf6' },
];
const lostTotal = lostReasons.reduce((s, r) => s + r.v, 0);
const [range, setRange] = useState('1d');
const [custom, setCustom] = useState({ from: '19/05/2026', to: '02/06/2026' });
const [cmp, setCmp] = useState('prev');
const days = rangeDays(range, custom);
const prof = CMP_PROFILE[cmp];
const cmpOn = !!prof;
const fmtN = n => Math.round(n).toLocaleString('vi-VN');
const data = 320 * days, replied = Math.round(data * 0.92);
const respondRate = 92, frtMin = '4,2', qc = '7,8';
const dData = cmpOn ? deltaPct(data, 320 * days * prof.f) : null;
const dResp = cmpOn ? respondRate - prof.respond : null;
const dFrt = cmpOn ? deltaPct(4.2, prof.frt) : null;
const dQc = cmpOn ? deltaPct(7.8, prof.qc) : null;
const periodLabel = range === 'custom' ? custom.from + ' – ' + custom.to : spanForDays(days).label;
const cmpLabel = cmpOn ? compareSpan(days, cmp).label : '';
const srcRows = bySource.map(s => ({ ...s, data: s.data * days, replied: s.replied * days }));
const chanRows = byChannel.map(d => ({ ...d, v: d.v * days }));
const totalChan = chanRows.reduce((a, b) => a + b.v, 0);
const lb = leaderboard.map(r => ({ ...r, convos: r.convos * days, closes: r.closes * days }));
const funnelSteps = [{ l: 'Tổng data', v: 320, color: '#8a8a8a' }, { l: 'Bỏ rác', v: 58, color: '#c4320a' }, { l: 'Lead mới', v: 198, color: '#3b82f6' }, { l: 'Đang chat', v: 142, color: '#0a9e6e' }].map(s => ({ ...s, v: s.v * days }));
return (
Xuất báo cáo} />
{cmpOn && (
Đang so sánh {periodLabel} với {cmpLabel} ({prof.label}). Δ bên dưới là thay đổi so với kỳ đối chiếu.
)}
navigate('/settings/ai-chat')}>Quản lý kết nối}>
| Nguồn / Trang |
Kênh |
Trạng thái sync |
Data về |
Đã phản hồi |
Tỉ lệ respond |
First response |
QC chat |
{srcRows.map((s, i) => { const rate = Math.round((s.replied / s.data) * 100); return (
| {s.page} |
|
{s.sync ? Đang sync : Tạm ngắt} |
{fmtN(s.data)} |
{fmtN(s.replied)} |
= 90 ? '#0e7c4a' : rate >= 80 ? '#b06f00' : '#c4320a' }}>{rate}%
|
{s.frt} |
= 8 ? '#0e7c4a' : '#b06f00' }} className="fw7 tnum">{s.qc} |
); })}
{chanRows.map(d =>
{d.l}{fmtN(d.v)}
)}
| Sales |
Hội thoại |
Tỉ lệ respond |
First response |
QC chat |
Chốt |
{lb.map((r, i) => (
| {i + 1}{r.name} |
{fmtN(r.convos)} |
= 90 ? '#0e7c4a' : '#b06f00' }} className="fw7 tnum">{r.respond}% |
{r.frt} |
= 8 ? '#0e7c4a' : '#b06f00' }} className="fw7 tnum">{r.qc} |
{r.closes} |
))}
{lostReasons.map(r => { const pct = Math.round((r.v / lostTotal) * 100); return (
); })}
);
}
function DataExplorer() {
const [view, setView] = useState('all');
const rows = AAU.leads;
const cols = [
{ key: 'name', label: 'Tên', render: l => {l.name} },
{ key: 'company', label: 'Công ty' },
{ key: 'phone', label: 'SĐT' },
{ key: 'email', label: 'Email' },
{ key: 'source', label: 'Nguồn', render: l => AAU.sourceMeta[l.source]?.label, csv: l => AAU.sourceMeta[l.source]?.label },
{ key: 'channel', label: 'Kênh', render: l => AAU.channels[l.channel]?.label },
{ key: 'stage', label: 'Stage', render: l => AAU.stageById(l.stage)?.name, csv: l => AAU.stageById(l.stage)?.name },
{ key: 'industry', label: 'Ngành' },
{ key: 'region', label: 'Khu vực' },
{ key: 'revenue', label: 'Doanh thu/th', num: true, render: l => AAU.fmtVNDm(l.revenue) },
{ key: 'grade', label: 'Grade' },
{ key: 'createdAt', label: 'Ngày tạo', render: l => AAU.fmtDate(l.createdAt) },
];
const health = [
{ icon: 'phone', t: 'Thiếu SĐT', v: 0, tone: 'success' },
{ icon: 'users', t: 'Trùng lặp nghi ngờ', v: 2, tone: 'warning' },
{ icon: 'tag', t: 'Chưa gán nguồn', v: 0, tone: 'success' },
{ icon: 'user', t: 'Chưa gán phụ trách', v: 3, tone: 'critical' },
];
return (
Lưu view hiện tại} />
{health.map((h, i) => (
))}
l.id} searchKeys={['name', 'company', 'phone', 'email']} selectable bulkActions={[{ icon: 'tag', label: 'Gán nguồn', onClick: () => {} }, { icon: 'user', label: 'Gán sales', onClick: () => {} }]} onRowClick={l => navigate('/leads/' + l.id)} exportName="data-explorer" pageSize={9} />
);
}
Object.assign(window, { CallLogQC, InboxDailyReport, DataExplorer });