/* AAU CRM — Unified Inbox (3 columns, interactive) */
const INBOX_LABELS = [
{ id: 'hot', label: 'Hot', color: '#ef4444' },
{ id: 'proposal', label: 'Chờ proposal', color: '#f59e0b' },
{ id: 'callback', label: 'Hẹn gọi lại', color: '#3b82f6' },
{ id: 'franchise', label: 'Nhượng quyền', color: '#8b5cf6' },
{ id: 'care', label: 'Cần CSKH', color: '#0a9e6e' },
{ id: 'spam', label: 'Rác / Spam', color: '#9ca3af' },
];
const labelById = (id) => INBOX_LABELS.find(l => l.id === id);
const SENTIMENT = { cv1: 'pos', cv2: 'neu', cv3: 'neg', cv4: 'pos', cv5: 'neu', cv6: 'neg', cv7: 'neu', cv8: 'pos' };
const SENT_META = { pos: { l: 'Thiện chí', c: '#0a9e6e' }, neu: { l: 'Trung lập', c: '#8a8a8a' }, neg: { l: 'Do dự', c: '#c4320a' } };
const DUPES = { L1: 'Khoa BBQ — hồ sơ cũ (T9/2025)' };
const ME_ID = 'u5'; // người đang trực inbox (demo)
function ChannelAvatar({ ch, name, color, size = 40 }) {
const c = (AAU.channels && AAU.channels[ch]) || { color: '#8a8f98', short: (ch || '?').toUpperCase() };
return (
);
}
function LabelChip({ id, small, onRemove }) {
const l = labelById(id); if (!l) return null;
return (
{l.label}
{onRemove && { e.stopPropagation(); onRemove(); }}> }
);
}
function SentimentChip({ s }) {
const m = SENT_META[s] || SENT_META.neu;
return {m.l} ;
}
/* ── Call modal (click-to-call → log + disposition) ── */
function CallModal({ lead, onClose, onLog }) {
const [phase, setPhase] = useState('calling');
const [sec, setSec] = useState(0);
const [disp, setDisp] = useState('consulted');
const [note, setNote] = useState('');
useEffect(() => { if (phase !== 'calling') return; const t = setInterval(() => setSec(s => s + 1), 1000); return () => clearInterval(t); }, [phase]);
const mmss = `${String(Math.floor(sec / 60)).padStart(2, '0')}:${String(sec % 60).padStart(2, '0')}`;
const dispo = [
{ v: 'consulted', l: 'Đã tư vấn xong', tone: 'success' },
{ v: 'callback', l: 'Hẹn gọi lại', tone: 'info' },
{ v: 'noanswer', l: 'Không nghe máy', tone: 'warning' },
{ v: 'wrong', l: 'Sai số', tone: 'critical' },
{ v: 'refused', l: 'Từ chối', tone: 'critical' },
];
return (
setPhase('ended')} style={{ margin: '0 auto' }}>Kết thúc cuộc gọi
: Hủy onLog({ duration: mmss, disp, note })}>Lưu vào nhật ký
}>
{lead.name}
{lead.phone}
{phase === 'calling'
?
Đang kết nối · {mmss}
:
Đã kết thúc · {mmss} }
{phase === 'ended' && (
KẾT QUẢ CUỘC GỌI
{dispo.map(d => (
setDisp(d.v)}>
{d.l}
))}
Tự động ghi vào hội thoại + Timeline hồ sơ 360°. Chọn "Hẹn gọi lại" sẽ snooze hội thoại.
)}
);
}
function InboxEmptyState() {
return (
);
}
// Wrapper: chặn crash khi chưa có hội thoại nào (sau khi wipe / trước khi nối real data).
function UnifiedInbox() {
if (!AAU.conversations || Object.keys(AAU.conversations).length === 0) return ;
return ;
}
function UnifiedInboxBody() {
const convList = Object.values(AAU.conversations);
// Hội thoại thật từ CQA có thể CHƯA gắn lead (leadId rỗng) → trả lead mặc định an
// toàn để màn không crash. Khi lead được tạo/nối, lookup thật sẽ thay thế.
const leadOf = c => AAU.leadById(c.leadId) || {
id: '', name: c.name || 'Khách', company: '', role: '', phone: '', email: '',
bizModel: '', industry: '', courseInterest: '', source: c.channel || 'zalo', channel: c.channel || 'zalo',
stage: 's4', branch: '', grade: '', dealValue: 0, sla: 'ok', slaLeft: '', hot: false,
signal: { level: '', text: '' }, painpoints: [], region: '', assignedTo: '', qualify: {},
};
const [chFilter, setChFilter] = useState('all');
const [triage, setTriage] = useState('all');
const [q, setQ] = useState('');
const [selId, setSelId] = useState(() => {
const ni = window.NavIntent;
if (ni && ni.inboxConv && AAU.conversations[ni.inboxConv]) { window.NavIntent = null; return ni.inboxConv; }
return convList[0].id;
});
const [store, setStore] = useState(() => JSON.parse(JSON.stringify(AAU.conversations)));
const [meta, setMeta] = useState(() => {
const m = {};
convList.forEach(c => {
const l = leadOf(c);
const labels = [];
if (l.hot) labels.push('hot');
if (l.courseInterest === 'c10') labels.push('franchise');
if (['s6', 's7'].includes(l.stage)) labels.push('proposal');
m[c.id] = { owner: l.assignedTo || '', labels, status: 'open', snoozeLabel: '', notes: [] };
});
if (m.cv1) m.cv1.notes = [{ id: 'n0', by: ME_ID, at: new Date(Date.now() - 36e5).toISOString(), text: '@Lê Vy khách quyết nhanh, ưu tiên gọi xác nhận lịch trước 5h chiều nay nhé.' }];
return m;
});
const [draft, setDraft] = useState('');
const [composerTab, setComposerTab] = useState('reply');
const [aiMode, setAiMode] = useState(store[selId].aiMode);
const [copilot, setCopilot] = useState('');
const [playbook, setPlaybook] = useState(true);
const [slash, setSlash] = useState(false);
const [panel, setPanel] = useState(true);
const [botOff, setBotOff] = useState({});
const [labelMenu, setLabelMenu] = useState(false);
const [assignMenu, setAssignMenu] = useState(false);
const [attachMenu, setAttachMenu] = useState(false);
const [callOpen, setCallOpen] = useState(false);
const bodyRef = useRef(null);
const conv = store[selId];
const lead = leadOf(conv);
const cm = meta[selId];
const sales = AAU.users.filter(u => u.role === 'sales');
const playbookSteps = {
s7: { step: 'Bước: Chốt đàm phán', tip: 'Khách đã hỏi proposal & muốn chốt sớm → đưa ưu đãi early-bird có thời hạn, xác nhận lịch khai giảng.', asset: 'Brochure Nhượng quyền + Bảng giá Q2', reply: 'Dạ em xác nhận ưu đãi early-bird giảm 20% nếu anh đăng ký trước 15/06. Lớp Nhượng quyền F&B khai giảng 22/06, em giữ slot giúp anh nhé?' },
s6: { step: 'Bước: Theo dõi proposal', tip: 'Đã gửi proposal → hỏi phản hồi, xử lý phản đối về ngân sách.', asset: 'Case study + So sánh đối thủ', reply: 'Dạ chị xem proposal em gửi chưa ạ? Nếu cần em gửi thêm case study của chuỗi tương tự để chị tham khảo phần ROI nhé.' },
s5: { step: 'Bước: Tư vấn chuyên sâu', tip: 'Đào sâu painpoint, map đúng khóa, đặt lịch tư vấn 1-1.', asset: 'Pitch deck khóa quan tâm', reply: 'Dạ để tư vấn sát nhất, em xin phép đặt lịch gọi 15 phút trao đổi về tình hình quán mình hiện tại được không ạ?' },
s4: { step: 'Bước: Qualify SQL', tip: 'Xác nhận mô hình KD, ngân sách, nhu cầu → chấm điểm qualify.', asset: 'Brochure khóa + Bảng giá', reply: 'Dạ để em tư vấn đúng khóa, cho em hỏi hiện quán mình đang vận hành mấy điểm và anh/chị muốn cải thiện nhất điều gì ạ?' },
};
const pb = playbookSteps[lead.stage] || playbookSteps.s4;
const detectedMap = {
s7: { label: '"Đòi chốt nhanh / hỏi thanh toán"', cat: 'Tín hiệu mua', color: '#0e7c4a', win: 88 },
s6: { label: '"Im lặng > 3 ngày sau báo giá"', cat: 'Rủi ro mất deal', color: '#b42318', win: 33 },
s5: { label: '"So sánh AAU vs đối thủ X"', cat: 'So sánh đối thủ', color: '#6d28d9', win: 59 },
s4: { label: '"Hỏi lịch khai giảng"', cat: 'Câu hỏi lặp lại', color: '#1a4fa3', win: 72 },
};
const detected = detectedMap[lead.stage] || detectedMap.s4;
useEffect(() => { setAiMode(store[selId].aiMode); setComposerTab('reply'); }, [selId]);
useEffect(() => { setCopilot(aiMode === 'copilot' ? pb.reply : ''); }, [selId, aiMode]);
useEffect(() => { if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight; }, [selId, conv.messages.length, cm.notes.length]);
function patchMeta(id, p) { setMeta(m => ({ ...m, [id]: { ...m[id], ...p } })); }
const triageTabs = [
{ v: 'all', l: 'Tất cả', f: c => meta[c.id].status !== 'done' },
{ v: 'unread', l: 'Chưa đọc', f: c => c.unread > 0 && meta[c.id].status !== 'done' },
{ v: 'mine', l: 'Của tôi', f: c => meta[c.id].owner === ME_ID && meta[c.id].status !== 'done' },
{ v: 'unassigned', l: 'Chưa gán', f: c => !meta[c.id].owner && meta[c.id].status !== 'done' },
{ v: 'overdue', l: 'Quá hạn SLA', f: c => leadOf(c).sla === 'bad' && meta[c.id].status !== 'done' },
{ v: 'snoozed', l: 'Snooze', f: c => meta[c.id].status === 'snoozed' },
{ v: 'done', l: 'Xong', f: c => meta[c.id].status === 'done' },
];
const triF = triageTabs.find(t => t.v === triage).f;
const filtered = convList
.filter(triF)
.filter(c => (chFilter === 'all' || c.channel === chFilter) && (!q || leadOf(c).name.toLowerCase().includes(q.toLowerCase())))
.sort((a, b) => new Date(b.lastAt) - new Date(a.lastAt));
// keyboard shortcuts: J/K navigate, E mark done
useEffect(() => {
function onKeyG(e) {
if (/input|textarea|select/i.test(e.target.tagName)) return;
const ids = filtered.map(c => c.id); const i = ids.indexOf(selId);
if (e.key === 'j' && i < ids.length - 1) { setSelId(ids[i + 1]); }
else if (e.key === 'k' && i > 0) { setSelId(ids[i - 1]); }
else if (e.key === 'e') { patchMeta(selId, { status: 'done' }); }
}
window.addEventListener('keydown', onKeyG);
return () => window.removeEventListener('keydown', onKeyG);
}, [filtered, selId]);
// A — auto-refresh: định kỳ kéo /conversations, gộp tin mới (theo id) vào store +
// cập nhật danh sách trái. Không clobber tin optimistic/note đang có.
useEffect(() => {
if (!(window.API && window.API.enabled && window.API.get)) return;
let alive = true;
const pull = () => window.API.get('/conversations').then(server => {
if (!alive || !server) return;
const list = Array.isArray(server) ? server : Object.values(server);
list.forEach(sc => { AAU.conversations[sc.id] = { ...(AAU.conversations[sc.id] || {}), ...sc }; });
// lead mới (auto-tạo từ chat) chưa có trong AAU.leads → refresh để panel + kanban điền
const haveLead = new Set((AAU.leads || []).map(l => l.id));
if (list.some(sc => sc.leadId && !haveLead.has(sc.leadId)) && window.API.refreshLeads) window.API.refreshLeads().catch(() => { });
setMeta(m => { let ch = false; const nm = { ...m }; list.forEach(sc => { if (!nm[sc.id]) { const l = leadOf(sc); nm[sc.id] = { owner: l.assignedTo || '', labels: [], status: 'open', snoozeLabel: '', notes: [] }; ch = true; } }); return ch ? nm : m; });
setStore(prev => {
const next = { ...prev }; let ch = false;
list.forEach(sc => {
const loc = prev[sc.id];
if (!loc) { next[sc.id] = sc; ch = true; return; }
const have = new Set((loc.messages || []).map(m => m.id));
const add = (sc.messages || []).filter(m => !have.has(m.id));
if (add.length || sc.unread !== loc.unread || sc.lastAt !== loc.lastAt || sc.leadId !== loc.leadId) {
next[sc.id] = { ...loc, leadId: sc.leadId || loc.leadId, linked: !!(sc.leadId || loc.leadId), name: sc.name || loc.name, messages: [...(loc.messages || []), ...add].sort((a, b) => new Date(a.at) - new Date(b.at)), lastAt: sc.lastAt || loc.lastAt, unread: sc.unread };
ch = true;
}
});
return ch ? next : prev;
});
}).catch(() => { });
const iv = setInterval(pull, 30000);
return () => { alive = false; clearInterval(iv); };
}, []);
// Tạo Lead từ chat hiện tại → gắn vào pipeline + điền panel.
function createLeadFromChat() {
if (!(window.API && window.API.enabled)) { window.alert('Cần chế độ LIVE để tạo lead.'); return; }
const cid = selId;
window.API.createLeadFromConv(cid).then(lead => {
if (!lead || !lead.id) return;
if (Array.isArray(AAU.leads) && !AAU.leads.some(l => l.id === lead.id)) AAU.leads.unshift(lead);
if (typeof LeadStore !== 'undefined' && LeadStore.reload) LeadStore.reload();
AAU.conversations[cid] = { ...(AAU.conversations[cid] || {}), leadId: lead.id, linked: true };
setStore(s => { const c = s[cid]; if (!c) return s; return { ...s, [cid]: { ...c, leadId: lead.id, linked: true } }; });
}).catch(e => window.alert('Tạo lead lỗi: ' + (e && e.message || e)));
}
const lastOutId = [...conv.messages].reverse().find(m => m.dir === 'out')?.id;
// B — gửi ra khách: optimistic 'sending' → patch theo kết quả thật (delivered/failed).
function send(text) {
const t = (text ?? draft).trim(); if (!t) return;
const mid = 'x' + Date.now();
const liveSend = !!(window.API && window.API.enabled);
setStore(s => { const n = { ...s }; n[selId] = { ...n[selId], messages: [...n[selId].messages, { id: mid, dir: 'out', at: new Date().toISOString(), by: ME_ID, text: t, ai: aiMode === 'autopilot', status: liveSend ? 'sending' : 'sent' }], unread: 0 }; return n; });
setDraft(''); setSlash(false); setCopilot('');
if (liveSend) {
const cid = selId;
window.API.sendMessage(cid, t, ME_ID)
.then(r => setStore(s => { const c = s[cid]; if (!c) return s; return { ...s, [cid]: { ...c, messages: c.messages.map(m => m.id === mid ? { ...m, id: (r && r.id) || mid, status: (r && r.delivered) ? 'delivered' : 'sent', note: r && r.note } : m) } }; }))
.catch(e => { setStore(s => { const c = s[cid]; if (!c) return s; return { ...s, [cid]: { ...c, messages: c.messages.map(m => m.id === mid ? { ...m, status: 'failed' } : m) } }; }); window.alert('Gửi tới khách thất bại: ' + (e && e.message || e)); });
}
}
function attach(name) {
setStore(s => { const n = { ...s }; n[selId] = { ...n[selId], messages: [...n[selId].messages, { id: 'x' + Date.now(), dir: 'out', at: new Date().toISOString(), by: ME_ID, attachment: name, status: 'sent' }] }; return n; });
setAttachMenu(false);
}
function saveNote() {
const t = draft.trim(); if (!t) return;
patchMeta(selId, { notes: [...cm.notes, { id: 'n' + Date.now(), by: ME_ID, at: new Date().toISOString(), text: t }] });
setDraft('');
}
function onKey(e) { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); composerTab === 'note' ? saveNote() : send(); } }
function onDraft(v) { setDraft(v); setSlash(composerTab === 'reply' && v.startsWith('/')); }
function logCall({ duration, disp, note }) {
const dispMap = { consulted: 'Đã tư vấn xong', callback: 'Hẹn gọi lại', noanswer: 'Không nghe máy', wrong: 'Sai số', refused: 'Từ chối' };
setStore(s => { const n = { ...s }; n[selId] = { ...n[selId], messages: [...n[selId].messages, { id: 'c' + Date.now(), type: 'call', dir: 'out', at: new Date().toISOString(), by: ME_ID, duration, disp: dispMap[disp], note }] }; return n; });
if (window.API && window.API.enabled) { const lid = (store[selId] || {}).leadId; if (lid) window.API.logCall({ leadId: lid, duration, agent: ME_ID, verdict: disp }).catch(() => {}); }
if (disp === 'callback') patchMeta(selId, { status: 'snoozed', snoozeLabel: 'Hẹn gọi lại 3h', labels: [...new Set([...cm.labels, 'callback'])] });
setCallOpen(false);
}
// merged thread (messages + internal notes), sorted
const thread = [
...conv.messages.map(m => ({ ...m, _t: m.type === 'call' ? 'call' : 'msg' })),
...cm.notes.map(n => ({ ...n, _t: 'note', at: n.at })),
].sort((a, b) => new Date(a.at) - new Date(b.at));
const assets = ['Brochure Nhượng quyền F&B', 'Bảng giá Q2/2026', 'Case study chuỗi 6 chi nhánh', 'Lịch khai giảng tháng 6'];
const aiOpts = [{ value: 'off', label: 'Tắt' }, { value: 'copilot', label: 'Copilot' }, { value: 'autopilot', label: 'Autopilot' }];
return (
{/* LEFT */}
Hộp thư {convList.reduce((s, c) => s + c.unread, 0)} chưa đọc
{triageTabs.map(t => { const n = convList.filter(t.f).length; return (
setTriage(t.v)} className="row aic g4" style={{ flexShrink: 0, fontSize: 11.5, fontWeight: 600, padding: '4px 9px', borderRadius: 7, cursor: 'pointer', border: '1px solid ' + (triage === t.v ? '#1a4fa3' : 'var(--p-border)'), background: triage === t.v ? '#1a4fa3' : '#fff', color: triage === t.v ? '#fff' : 'var(--p-text)' }}>
{t.l}{n}
); })}
{filtered.length === 0 &&
Không có hội thoại nào.
}
{filtered.map(c => { const l = leadOf(c); const m2 = meta[c.id]; const last = c.messages[c.messages.length - 1]; const sel = c.id === selId; return (
setSelId(c.id)} style={{ display: 'flex', gap: 10, padding: '11px 14px', cursor: 'pointer', background: sel ? 'var(--p-bg-surface-selected)' : 'transparent', borderBottom: '1px solid var(--p-border-secondary)', borderLeft: sel ? '3px solid #1a4fa3' : '3px solid transparent', opacity: m2.status === 'done' ? 0.6 : 1 }}>
{l.name} {l.hot && }{relTime(c.lastAt)}
{!last ? '—' : last.type === 'call' ? '📞 Cuộc gọi' : (last.dir === 'out' ? 'Bạn: ' : '') + (last.attachment ? '📎 ' + last.attachment : (last.text || ''))}
{m2.status === 'snoozed' && {m2.snoozeLabel || 'Snooze'} }
{m2.status === 'done' && Xong }
{m2.labels.slice(0, 2).map(id => )}
{c.unread > 0 &&
{c.unread} }
); })}
{/* MIDDLE */}
{lead.name} {lead.hot && }
{lead.company} · {AAU.channels[conv.channel].label}
Cửa sổ {conv.windowLeft}
setCallOpen(true)} />
navigate('/leads/' + lead.id)} />
setPanel(!panel)} />
{/* Toolbar: owner · labels · snooze/done */}
{ setAssignMenu(!assignMenu); setLabelMenu(false); }} style={{ cursor: 'pointer', border: '1px solid var(--p-border)', borderRadius: 7, padding: '3px 8px 3px 4px', background: '#fff' }}>
{cm.owner ? <> u.id === cm.owner)?.name} color={AAU.users.find(u => u.id === cm.owner)?.color} size={20} />{AAU.users.find(u => u.id === cm.owner)?.name} > : Chưa gán }
{assignMenu && (
GÁN PHỤ TRÁCH
{sales.map(u => (
{ patchMeta(selId, { owner: u.id }); setAssignMenu(false); }} style={{ padding: '8px 12px', cursor: 'pointer', background: cm.owner === u.id ? 'var(--p-bg-surface-selected)' : '' }} onMouseEnter={e => { if (cm.owner !== u.id) e.currentTarget.style.background = 'var(--p-bg-surface-hover)'; }} onMouseLeave={e => { if (cm.owner !== u.id) e.currentTarget.style.background = ''; }}>
{u.name} {cm.owner === u.id &&
}
))}
)}
{cm.labels.map(id =>
patchMeta(selId, { labels: cm.labels.filter(x => x !== id) })} />)}
{ setLabelMenu(!labelMenu); setAssignMenu(false); }} style={{ width: 26, height: 26 }}>
{labelMenu && (
{INBOX_LABELS.map(l => { const on = cm.labels.includes(l.id); return (
patchMeta(selId, { labels: on ? cm.labels.filter(x => x !== l.id) : [...cm.labels, l.id] })} style={{ padding: '7px 8px', cursor: 'pointer', borderRadius: 7 }} onMouseEnter={e => e.currentTarget.style.background = 'var(--p-bg-surface-hover)'} onMouseLeave={e => e.currentTarget.style.background = ''}>
{}} />
); })}
)}
{cm.status === 'snoozed'
? patchMeta(selId, { status: 'open', snoozeLabel: '' })}>Bỏ snooze
: patchMeta(selId, { status: 'snoozed', snoozeLabel: 'Snooze 3h' })}>Snooze }
{cm.status === 'done'
? patchMeta(selId, { status: 'open' })}>Mở lại
: patchMeta(selId, { status: 'done' })}>Đánh dấu xong }
{['fb', 'instagram', 'zalo'].includes(conv.channel) && (
botOff[selId] ? (
Đã tắt bot tự động {AAU.channels[conv.channel].label} — hội thoại do CRM điều khiển, sales trả lời trực tiếp.
setBotOff(s => ({ ...s, [selId]: false }))}>Bật lại bot
) : (
Bot tự động {AAU.channels[conv.channel].label} đang trả lời. Tắt để chuyển hội thoại về hệ thống chat & sales tiếp quản.
{ setBotOff(s => ({ ...s, [selId]: true })); if (window.API && window.API.enabled) window.API.setAIMode(selId, 'off').catch(() => {}); }}>Tắt bot & nhận hội thoại
)
)}
{thread.map(m => {
if (m._t === 'note') return (
NỘI BỘ · {AAU.users.find(u => u.id === m.by)?.name} {relTime(m.at)} trước
{m.text.split(/(@[^\s]+(?:\s[A-ZÀ-Ỹ][^\s]*)?)/).map((p, i) => p.startsWith('@') ? {p} : p)}
);
if (m._t === 'call') return (
Cuộc gọi đi · {m.duration || '—'} · {m.disp}{m.note ?
— {m.note} : ''}
{relTime(m.at)} trước · đã ghi vào hồ sơ 360°
);
return (
{m.image ? (
{m.text ?
{m.text}
: null}
) : m.attachment ? (
{m.attachment}
PDF · đã gửi
) : (
{m.text}
)}
{m.ai && AI }
{relTime(m.at)} trước
{m.dir === 'out' && m.status === 'sending' && · Đang gửi… }
{m.dir === 'out' && m.status === 'failed' && · ⚠ Gửi lỗi }
{m.dir === 'out' && m.status === 'delivered' && · ✓ Đã gửi tới khách }
{m.dir === 'out' && m.id === lastOutId && !['sending', 'failed', 'delivered'].includes(m.status) && · {String(m.id).startsWith('x') || String(m.id).startsWith('c') ? 'Đã gửi' : (conv.linked ? '✓✓ Đã xem' : 'Đã gửi')} }
);
})}
{/* Playbook */}
setPlaybook(!playbook)}>
Playbook Assistant
{pb.step}
{playbook && (
AI PHÁT HIỆN PATTERN · {detected.cat}
{detected.label} · win-rate {detected.win}%
navigate('/enablement/intelligence')}>Xem
{pb.tip}
onDraft((draft ? draft + ' ' : '') + pb.reply)}>Chèn script
attach(pb.asset.split(' + ')[0])}> {pb.asset}
navigate('/enablement/assets')}>Mở Asset Library
)}
{/* Composer */}
{/* Copilot draft */}
{composerTab === 'reply' && aiMode === 'copilot' && copilot && (
AI GỢI Ý TRẢ LỜI Copilot — bạn duyệt trước khi gửi
{copilot}
send(copilot)}>Gửi ngay
{ setDraft(copilot); setCopilot(''); }}>Dùng & sửa
setCopilot('')}>Bỏ
)}
{slash && (
QUICK REPLY · gõ để lọc
{AAU.replyTemplates.filter(t => t.key.includes(draft.slice(1).toLowerCase()) || t.label.toLowerCase().includes(draft.slice(1).toLowerCase())).map(t => (
send(t.text)} style={{ padding: '9px 12px', cursor: 'pointer', borderTop: '1px solid var(--p-border-secondary)' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--p-bg-surface-hover)'} onMouseLeave={e => e.currentTarget.style.background = ''}>
/{t.key} · {t.label}
{t.text}
))}
)}
{attachMenu && (
GỬI TÀI LIỆU
{assets.map(a => (
attach(a)} style={{ padding: '9px 12px', cursor: 'pointer', borderTop: '1px solid var(--p-border-secondary)' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--p-bg-surface-hover)'} onMouseLeave={e => e.currentTarget.style.background = ''}>
{a}
))}
)}
{composerTab === 'reply' && <>
AI:
{ setAiMode(v); setStore(s => ({ ...s, [selId]: { ...s[selId], aiMode: v } })); if (window.API && window.API.enabled) window.API.setAIMode(selId, v).catch(() => {}); }} options={aiOpts} />
{aiMode === 'autopilot' && AI tự trả lời }
>}
{composerTab === 'note' && Chỉ nội bộ thấy · gõ @ để nhắc đồng đội }
{composerTab === 'reply' && setAttachMenu(!attachMenu)} />}
Phím tắt: J/K chuyển hội thoại · E đánh dấu xong
{/* RIGHT — Customer 360 */}
{panel &&
}
{callOpen &&
setCallOpen(false)} onLog={logCall} />}
);
}
function Section360({ title, children, action }) {
const [open, setOpen] = useState(true);
return (
setOpen(!open)}>
{title}
{action}
{open &&
{children}
}
);
}
function Row360({ k, v, auto }) {
return (
{k}{auto && auto }
{v}
);
}
/* Inline-editable key/value row */
function EditRow({ k, value, onChange, editing, type = 'text', suffix }) {
if (!editing) return ;
return (
{k}
onChange(e.target.value)} style={{ height: 28, fontSize: 12.5, textAlign: 'right', padding: '2px 8px' }} />
);
}
const LEARNER_ROLES = ['Chủ / Founder', 'CEO / Tổng GĐ', 'Quản lý vận hành', 'Quản lý cửa hàng', 'Bếp trưởng', 'Phụ trách Marketing', 'Phụ trách Nhân sự', 'Nhân viên cử đi học'];
/* Multi-value contact list (phone / email) */
function ContactList({ items, onChange, editing, type = 'text', icon }) {
const [val, setVal] = useState('');
function add() { const t = val.trim(); if (!t) return; onChange([...items, t]); setVal(''); }
return (
{items.map((it, i) => (
{i === 0 && chính }{it}
{editing && items.length > 1 && onChange(items.filter((_, j) => j !== i))}> }
))}
{editing && (
setVal(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') add(); }} style={{ height: 28, fontSize: 12.5 }} />
)}
);
}
/* Multi-select scrollable course picker */
function CourseMultiSelect({ value, onChange }) {
const [open, setOpen] = useState(false);
const courses = AAU.courses.filter(c => c.status === 'active');
function toggle(id) { onChange(value.includes(id) ? value.filter(x => x !== id) : [...value, id]); }
return (
{value.length === 0 &&
Chưa chọn khóa }
{value.map(id => { const c = AAU.courseById(id); if (!c) return null; return (
{c.name} toggle(id)}>
); })}
setOpen(!open)} style={{ width: '100%', justifyContent: 'space-between' }}>
Thêm khóa quan tâm
{open && (
{AAU.courseGroups.map(g => {
const list = courses.filter(c => c.group === g.id); if (!list.length) return null;
return (
{g.label.toUpperCase()}
{list.map(c => { const on = value.includes(c.id); return (
toggle(c.id)} className="row aic g8" style={{ padding: '7px 12px', cursor: 'pointer', background: on ? 'var(--p-bg-surface-selected)' : '' }} onMouseEnter={e => { if (!on) e.currentTarget.style.background = 'var(--p-bg-surface-hover)'; }} onMouseLeave={e => { if (!on) e.currentTarget.style.background = ''; }}>
toggle(c.id)} />
{c.name}
{c.code} · {AAU.fmtVNDm(c.price)}
); })}
);
})}
)}
);
}
function Customer360Panel({ lead, conv, onCreateLead }) {
const [editId, setEditId] = useState(false);
const [editBiz, setEditBiz] = useState(false);
const [editNeed, setEditNeed] = useState(false);
const [phones, setPhones] = useState([lead.phone]);
const [emails, setEmails] = useState([lead.email]);
const [biz, setBiz] = useState({ bizModel: lead.bizModel, industry: lead.industry, chainSize: lead.chainSize, revenue: AAU.fmtVNDm(lead.revenue) });
const [pains, setPains] = useState(lead.painpoints);
const [newPain, setNewPain] = useState('');
const [courses, setCourses] = useState(lead.courseInterest ? [lead.courseInterest] : []);
const [learner, setLearner] = useState('');
const [applied, setApplied] = useState({});
const [dupeDismissed, setDupeDismissed] = useState(false);
const aiSug = [{ id: 'budget', f: 'Ngân sách', v: '~20tr/khóa' }, { id: 'role', f: 'Vai trò', v: 'Người quyết định' }];
function addPain() { const t = newPain.trim(); if (!t) return; setPains([...pains, t]); setNewPain(''); }
return (
{lead.name}
{lead.role} · {lead.company}
{lead.id
? <> Đã link CRM navigate('/leads/' + lead.id)}>Xem dữ liệu 360° >
: Tạo Lead từ chat }
{DUPES[lead.id] && !dupeDismissed && (
Nghi ngờ trùng lặp
Trùng SĐT với {DUPES[lead.id]} .
Xem & gộp setDupeDismissed(true)}>Bỏ qua
)}
{ e.stopPropagation(); setEditId(!editId); }} />}>
SĐT {!editId && auto }
Email
{lead.fb}} auto />
{ e.stopPropagation(); setEditBiz(!editBiz); }} />}>
setBiz({ ...biz, bizModel: v })} />
setBiz({ ...biz, industry: v })} />
setBiz({ ...biz, chainSize: v })} />
setBiz({ ...biz, revenue: v })} />
{editBiz && Lưu vào hồ sơ 360° của khách.
}
{ e.stopPropagation(); setEditNeed(!editNeed); }} />}>
Painpoints
{pains.map((p, i) => (
{p}{editNeed && setPains(pains.filter((_, j) => j !== i))}> }
))}
{editNeed && (
setNewPain(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') addPain(); }} style={{ height: 30, fontSize: 12.5 }} />
)}
Khóa quan tâm (chọn 1 hoặc nhiều)
{editNeed
?
: {courses.length ? courses.map(id => {AAU.courseById(id)?.name} ) : — }
}
Người học
{editNeed
? ({ value: r, label: r }))]} style={{ width: '100%' }} />
: {learner || '—'} }
{aiSug.map(s => (
{s.f}: {s.v}
{applied[s.id]
?
Đã áp dụng
:
{ setApplied({ ...applied, [s.id]: true }); if (s.id === 'role') setLearner('Chủ / Founder'); }} />}
))}
} />
Có : 'Không'} />
} />
);
}
Object.assign(window, { UnifiedInbox, Customer360Panel, ChannelAvatar, CallModal });