// ============ AcademIA8 · Consola — Campañas · Vista CONTACTOS ============
// Tabla paginada + buscar/filtrar + importar (CSV o pegar) + editar etiquetas + eliminar.
const CONTACT_COLS = '1.7fr 1.9fr 1.8fr 128px 62px';
function estadoPillClass(e){ return e==='activo'?'pill-activo':e==='dado_de_baja'?'pill-baja':'pill-rebotado'; }
// ---------- parser de filas (CSV o texto pegado) ----------
function parseContactRows(text){
const lines = text.split(/\r?\n/).map(l=>l.trim()).filter(Boolean);
if(!lines.length) return [];
const delim = (lines[0].includes(';') && !lines[0].includes(',')) ? ';' : ',';
const first = lines[0].toLowerCase();
const hasHeader = first.includes('email') || first.includes('correo') || first.includes('mail');
const body = hasHeader ? lines.slice(1) : lines;
return body.map(l=>{
const cells = l.split(delim).map(c=>c.trim().replace(/^["']|["']$/g,''));
// heurística: la celda que parece email
let email = cells.find(c=>/@/.test(c)) || cells[0] || '';
const rest = cells.filter(c=>c!==email);
const [nombre='', apellido=''] = rest;
const valido = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
return { email, nombre, apellido, valido };
});
}
// ---------- editor de etiquetas de UN contacto ----------
function EditTagsModal({ contact, tags, onClose, onSave, onCreateTag }){
const [sel, setSel] = useState(contact.etiquetas.slice());
const [nueva, setNueva] = useState('');
const [allTags, setAllTags] = useState(tags);
const toggle = (id)=> setSel(s=> s.includes(id) ? s.filter(x=>x!==id) : [...s, id]);
const crear = async ()=>{
const n = nueva.trim(); if(!n) return;
const t = await onCreateTag(n);
setAllTags(a=> a.find(x=>x.id===t.id)?a:[...a,t]);
setSel(s=> s.includes(t.id)?s:[...s,t.id]);
setNueva('');
};
return (
Cancelar onSave(sel)}>Guardar >}>
{allTags.map(t=>(
toggle(t.id)}>
{sel.includes(t.id) && }
{t.nombre}
))}
setNueva(e.target.value)} onKeyDown={e=>{ if(e.key==='Enter'){ e.preventDefault(); crear(); } }} />
Crear
);
}
// ---------- importador ----------
function ImportModal({ tags, onClose, onImported, onCreateTag, notify }){
const [mode, setMode] = useState('csv'); // csv | pegar
const [rows, setRows] = useState([]);
const [fileName, setFileName] = useState('');
const [pasted, setPasted] = useState('');
const [selTags, setSelTags] = useState([]);
const [allTags, setAllTags] = useState(tags);
const [nueva, setNueva] = useState('');
const [over, setOver] = useState(false);
const [busy, setBusy] = useState(false);
const [result, setResult] = useState(null);
const fileRef = useRef(null);
const readFile = (file)=>{
if(!file) return;
setFileName(file.name);
const r = new FileReader();
r.onload = ()=> setRows(parseContactRows(String(r.target.result||'')));
r.readAsText(file);
};
useEffect(()=>{ if(mode==='pegar') setRows(parseContactRows(pasted)); },[pasted, mode]);
const toggleTag = (id)=> setSelTags(s=> s.includes(id)?s.filter(x=>x!==id):[...s,id]);
const crearTag = async ()=>{
const n = nueva.trim(); if(!n) return;
const t = await onCreateTag(n);
setAllTags(a=> a.find(x=>x.id===t.id)?a:[...a,t]);
setSelTags(s=> s.includes(t.id)?s:[...s,t.id]);
setNueva('');
};
const validas = rows.filter(r=>r.valido);
const invalidas = rows.length - validas.length;
// Resultado del import: creados/actualizados + filas omitidas (nuevo respecto del mock).
if (result){
const skipped = result.skipped || [];
const skpCols = '70px 1.4fr 1.6fr';
return (
Listo >}>
{[['creados',result.creados],['actualizados',result.actualizados],['omitidos',skipped.length]].map(([l,n])=>(
{Number(n||0).toLocaleString('es-AR')}
{l}
))}
{skipped.length>0 && (
Línea Email Motivo
{skipped.slice(0,100).map((s,i)=>(
{s.line!=null?s.line:'—'}
{s.email||'—'}
{s.reason||'—'}
))}
)}
);
}
const importar = async ()=>{
if(!validas.length){ notify('No hay filas válidas para importar'); return; }
if(!selTags.length){ notify('Elegí al menos una etiqueta para el lote'); return; }
setBusy(true);
try{
const res = await CampAPI.importarContactos(validas, selTags);
setResult(res);
onImported(res); // refresca la lista de fondo sin cerrar el modal
}catch(e){ notify(e.message||'No se pudo importar'); }
setBusy(false);
};
return (
Cancelar
{busy?'Importando…':`Importar ${validas.length||''} contacto${validas.length===1?'':'s'}`}
>}>
setMode('csv')}>
Subir CSV
setMode('pegar')}>
Pegar lista
{mode==='csv' && !fileName && (
fileRef.current?.click()}
onDragOver={e=>{e.preventDefault();setOver(true);}}
onDragLeave={()=>setOver(false)}
onDrop={e=>{e.preventDefault();setOver(false);readFile(e.dataTransfer.files[0]);}}>
Arrastrá tu CSV o hacé clic para elegirlo
Columnas: email, nombre, apellido · con o sin encabezado
readFile(e.target.files[0])} />
)}
{mode==='csv' && fileName && (
{fileName}
{rows.length} fila{rows.length===1?'':'s'} leída{rows.length===1?'':'s'}
{setFileName('');setRows([]);}} />
)}
{mode==='pegar' && (
)}
{rows.length>0 && (
Vista previa
{validas.length} válida{validas.length===1?'':'s'}{invalidas>0 && ` · ${invalidas} con error`}
Email Nombre Apellido
{rows.slice(0,60).map((r,i)=>(
{r.email||'—'}
{r.nombre||'—'}
{r.apellido||'—'}
))}
)}
{allTags.map(t=>(
toggleTag(t.id)}>
{selTags.includes(t.id) && }
{t.nombre}
))}
setNueva(e.target.value)} onKeyDown={e=>{ if(e.key==='Enter'){ e.preventDefault(); crearTag(); } }} />
Crear
{selTags.length===0 && Elegí o creá al menos una etiqueta — el backend la exige para importar.
}
);
}
// ---------- vista principal ----------
function ContactsView({ notify }){
const [tags, setTags] = useState([]);
const [tagMap, setTagMap] = useState({});
const [resumen, setResumen] = useState(null);
const [data, setData] = useState({ items:[], nextCursor:null, prevCursor:null, pageSize:25 });
const [loading, setLoading] = useState(true);
const [etq, setEtq] = useState('todas');
const curRef = useRef(null);
const stackRef = useRef([]); // pila de cursores visitados (el backend no da prevCursor)
const [hasPrev, setHasPrev] = useState(false);
const [editing, setEditing] = useState(null);
const [deleting, setDeleting] = useState(null);
const [importing, setImporting] = useState(false);
const loadTags = async ()=>{ try{ const t = await CampAPI.listarEtiquetas(); setTags(t); const m={}; t.forEach(x=>m[x.id]=x.nombre); setTagMap(m); }catch(e){ /* no crítico */ } };
const loadResumen = async ()=>{ try{ setResumen(await CampAPI.resumenContactos()); }catch(e){ /* no crítico */ } };
const loadList = async (cursor=curRef.current)=>{
curRef.current = cursor;
setLoading(true);
try{
const res = await CampAPI.listarContactos({ etiqueta:etq, cursor, pageSize:25 });
setData(res);
}catch(e){ notify(e.message||'No se pudieron cargar los contactos'); }
setLoading(false);
};
useEffect(()=>{ loadTags(); loadResumen(); },[]);
// el filtro por etiqueta (soportado por el backend) resetea el cursor y la pila
useEffect(()=>{ stackRef.current=[]; setHasPrev(false); loadList(null); },[etq]);
const goNext = ()=>{ if(!data.nextCursor) return; stackRef.current.push(curRef.current); setHasPrev(true); loadList(data.nextCursor); };
const goPrev = ()=>{ const prev = stackRef.current.pop(); setHasPrev(stackRef.current.length>0); loadList(prev||null); };
const refreshAll = ()=>{ loadList(); loadResumen(); };
const saveTags = async (etiquetas)=>{
await CampAPI.actualizarContacto(editing.id, { etiquetas });
setEditing(null); notify('Etiquetas actualizadas'); refreshAll();
};
const doDelete = async ()=>{
await CampAPI.eliminarContacto(deleting.id);
setDeleting(null); notify('Contacto eliminado'); refreshAll();
};
const createTag = async (n)=>{ const t = await CampAPI.crearEtiqueta(n); await loadTags(); return t; };
return (
{/* resumen */}
{resumen && (
{resumen.total.toLocaleString('es-AR')} contactos
{resumen.activos.toLocaleString('es-AR')} activos
{resumen.baja} dados de baja
{resumen.rebote} rebotados
)}
{/* toolbar — solo filtro por etiqueta (lo único que soporta el backend).
Búsqueda de texto y filtro por estado se ocultaron: eran page-local con
paginación (resultado engañoso). Si el uso real los pide, irán al backend. */}
setEtq(e.target.value)}>
Todas las etiquetas
{tags.map(t=>{t.nombre} )}
setImporting(true)}>Importar contactos
{/* tabla */}
Contacto
Email
Etiquetas
Estado
Acciones
{loading ? (
Cargando contactos…
) : data.items.length===0 ? (
Sin resultados
No hay contactos con esta etiqueta. Probá con otra o importá contactos nuevos.
) : data.items.map(c=>(
{c.etiquetas.slice(0,2).map(id=>(
{tagMap[id]||'—'}
))}
{c.etiquetas.length>2 && +{c.etiquetas.length-2} }
{c.etiquetas.length===0 && sin etiquetas }
{CampAPI.ESTADO_CONTACTO[c.estado]}
setEditing(c)} />
setDeleting(c)} />
))}
{/* paginación por cursor (Anterior / Siguiente — sin número de página) */}
{!loading && data.items.length>0 && (data.nextCursor || hasPrev) && (
Mostrando {data.items.length} contacto{data.items.length===1?'':'s'} en esta página
Anterior
Siguiente
)}
{editing &&
setEditing(null)} onSave={saveTags} onCreateTag={createTag} />}
{deleting && setDeleting(null)} onConfirm={doDelete} />}
{importing && setImporting(false)}
onImported={(res)=>{ notify(`${res.creados} creados · ${res.actualizados} actualizados${res.skipped&&res.skipped.length?` · ${res.skipped.length} omitidos`:''}`); loadTags(); refreshAll(); }} />}
);
}
Object.assign(window, { ContactsView, ImportModal, EditTagsModal, parseContactRows });