/* AAU CRM — Hàng đợi "Nghi trùng" (identity resolution). Gợi ý cặp lead có thể là 1 người (cùng SĐT/email, hoặc tên giống + khác kênh — vd MKT chat FB lấy SĐT → Sales add Zalo, tên FB ≠ tên Zalo). Sales QUYẾT: Gộp (1 contact, giữ alias) hoặc "Không phải" (không gợi lại). KHÔNG tự gộp ca mơ hồ. */ function DuplicateReview() { const live = !!(window.API && window.API.enabled); const [pairs, setPairs] = useState([]); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(''); const reload = () => { if (!live || !window.API.suspectedDuplicates) { setLoading(false); return; } setLoading(true); window.API.suspectedDuplicates().then(r => { setPairs((r && r.pairs) || []); }).catch(() => setPairs([])).finally(() => setLoading(false)); }; useEffect(() => { reload(); }, [live]); const chLabel = (ch) => (AAU.channels && AAU.channels[ch] || {}).label || ch; const chColor = (ch) => (AAU.channels && AAU.channels[ch] || {}).color || '#8a8f98'; const keyOf = (p) => p.a.id + '|' + p.b.id; const merge = async (p) => { if (!window.confirm(`Gộp "${p.a.name}" và "${p.b.name}" thành 1 contact? (giữ lead tiến xa hơn, tên kia thành bí danh)`)) return; setBusy('mg:' + keyOf(p)); try { await window.API.mergeLeads(p.a.id, p.b.id); if (window.API.refreshLeads) await window.API.refreshLeads(); reload(); } catch (e) { window.alert('Gộp lỗi: ' + ((e && e.message) || '')); } finally { setBusy(''); } }; const notDup = async (p) => { setBusy('nd:' + keyOf(p)); try { await window.API.markNotDuplicate(p.a.id, p.b.id); setPairs(ps => ps.filter(x => keyOf(x) !== keyOf(p))); } catch (e) { window.alert('Lỗi: ' + ((e && e.message) || '')); } finally { setBusy(''); } }; const LeadCol = ({ l }) => (
{(AAU.channels && AAU.channels[l.channel] || {}).short || '?'}
{l.name || '(chưa có tên)'}
{chLabel(l.channel)}{l.phone ? ' · ' + l.phone : ' · chưa có SĐT'}
Stage: {stageName(l.stage)}{l.assignedTo && · {(AAU.users || []).find(u => u.id === l.assignedTo)?.name || l.assignedTo}}
); return (
Tải lại} /> {!live ? (
Bật LIVE (mở app với ?api=…) để xem cặp nghi trùng từ backend.
) : loading ? (
Đang quét cặp nghi trùng…
) : pairs.length === 0 ? ( ) : ( <>
{pairs.length} cặp nghi trùng · {pairs.filter(p => p.confidence === 'cao').length} độ tin CAO
{pairs.map(p => (
{p.confidence === 'cao' ? 'Độ tin CAO' : 'Độ tin vừa'} {(p.signals || []).map((s, i) => {s})}
))}
)}
); }