`;
}
function renderPagination() {
const total = State.filtered.length;
const pages = Math.max(1, Math.ceil(total / State.pageSize));
const p = State.page;
const start = (p-1)*State.pageSize + 1;
const end = Math.min(p*State.pageSize, total);
document.getElementById('pagination-info').textContent =
total === 0 ? 'Sin resultados' : `Mostrando ${start}–${end} de ${total} registros`;
const wrap = document.getElementById('page-btns');
let html = ``;
const maxBtn = 7;
let start2 = Math.max(1, p - Math.floor(maxBtn/2));
let end2 = Math.min(pages, start2 + maxBtn - 1);
if (end2 - start2 < maxBtn - 1) start2 = Math.max(1, end2 - maxBtn + 1);
for (let i = start2; i <= end2; i++) {
html += ``;
}
html += ``;
wrap.innerHTML = html;
}
window.changePage = function(p) {
State.page = p;
renderTable();
document.getElementById('results-table-section').scrollIntoView({ behavior: 'smooth', block: 'start' });
};
/* ── Reporte ejecutivo ──────────────────────── */
function renderReport() {
const r = State.results;
const na = State.dataA.length;
const nb = State.dataB.length;
const coincTotal = r.filter(x => x.tipo === 'total').length;
const coincParc = r.filter(x => x.tipo === 'parcial2' || x.tipo === 'parcial1').length;
const sinCoinc = r.filter(x => x.tipo === 'none').length;
const soloB = r.filter(x => x.tipo === 'solo-b').length;
const pct = na > 0 ? Math.round(coincTotal/na*100) : 0;
const fecha = new Date().toLocaleDateString('es-MX',{year:'numeric',month:'long',day:'numeric'});
document.getElementById('report-text').innerHTML = `
Se analizaron ${na.toLocaleString()} registros del Archivo A
(${State.fileA.name}) y ${nb.toLocaleString()} registros
del Archivo B (${State.fileB.name}) el día ${fecha}.
El motor de comparación identificó ${coincTotal.toLocaleString()} coincidencias completas
(FFM + Nombre + ID),
representando un ${pct}% de efectividad de conciliación.
Adicionalmente, se detectaron ${coincParc.toLocaleString()} coincidencias parciales
(uno o dos campos coincidentes) y ${sinCoinc.toLocaleString()} registros de Archivo A
sin correspondencia en Archivo B.
Por su parte, Archivo B contiene ${soloB.toLocaleString()} registros exclusivos
no presentes en Archivo A.
Se recomienda revisar manualmente los registros con coincidencia parcial y los registros exclusivos
de cada archivo para determinar si corresponden a errores de captura, registros duplicados
o diferencias legítimas de información.
`;
}
/* ── Exportar Excel ─────────────────────────── */
function exportExcel() {
const wb = XLSX.utils.book_new();
// Hoja de resumen
const summary = [
['RESUME'],
['Generado:', new Date().toLocaleString('es-MX')],
['Archivo A:', State.fileA.name],
['Archivo B:', State.fileB.name],
['Archivo Distrito:', State.fileC ? State.fileC.name : 'No proporcionado'], // NUEVO
[],
['RESUMEN'],
['Total Archivo A', State.dataA.length],
['Total Archivo B', State.dataB.length],
['Coincidencia Total', State.results.filter(x => x.tipo==='total').length],
['Coincidencia Parcial', State.results.filter(x => x.tipo==='parcial2'||x.tipo==='parcial1').length],
['Sin Coincidencia', State.results.filter(x => x.tipo==='none').length],
['Solo en Archivo B', State.results.filter(x => x.tipo==='solo-b').length],
];
const wsSummary = XLSX.utils.aoa_to_sheet(summary);
XLSX.utils.book_append_sheet(wb, wsSummary, 'Resumen');
// Hoja de detalle
const headers = ['FFM Archivo A','FFM Archivo B','Nombre Archivo A','Nombre Archivo B',
'ID Archivo A','ID Archivo B','Campos Coincidentes','% Coincidencia','Estado',
'Distrito','Estatus','Observación']; // NUEVO: Distrito, Estatus
const rows = [headers, ...State.results.map(r => [
r.ffmA, r.ffmB, r.nombreA, r.nombreB, r.idA, r.idB,
r.matches.join(', '),
r.porcentaje + '%',
{ total:'Coincidencia Total', parcial2:'Parcial 2/3', parcial1:'Parcial 1/3',
none:'Sin Coincidencia', 'solo-b':'Solo en B' }[r.tipo] || r.tipo,
r.distrito || '', // NUEVO
r.estatus || '', // NUEVO
r.observacion
])];
const wsDetail = XLSX.utils.aoa_to_sheet(rows);
// Ancho de columnas
wsDetail['!cols'] = [14,14,24,24,14,14,18,12,18,16,18,32].map(w => ({ wch: w })); // NUEVO: anchos para Distrito/Estatus
XLSX.utils.book_append_sheet(wb, wsDetail, 'Detalle de Comparación');
// Hoja solo coincidencias totales
const totales = State.results.filter(x => x.tipo==='total');
const wsTot = XLSX.utils.aoa_to_sheet([
headers,
...totales.map(r => [r.ffmA,r.ffmB,r.nombreA,r.nombreB,r.idA,r.idB,r.matches.join(', '),'100%','Coincidencia Total',
r.distrito || '', r.estatus || '', r.observacion]) // NUEVO
]);
XLSX.utils.book_append_sheet(wb, wsTot, 'Coincidencias Totales');
XLSX.writeFile(wb, `Comparacion_${Date.now()}.xlsx`);
showToast('Excel exportado correctamente', 'success');
}
/* ── Exportar PDF ───────────────────────────── */
async function exportPDF() {
showToast('Generando PDF… un momento', 'success');
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' });
const colors = {
azul: [18, 65, 109],
claro: [45, 133, 152],
verdoso: [136, 201, 185],
naranja: [248, 173, 42],
rojo: [223, 37, 42],
gris: [107, 124, 147],
blanco: [255, 255, 255],
fondo: [244, 247, 250]
};
const W = 210, M = 16;
let y = 0;
// ── Header ──
doc.setFillColor(...colors.azul);
doc.rect(0, 0, W, 32, 'F');
// Logo placeholder
try {
const logoSrc = localStorage.getItem('logoSrc');
if (logoSrc) doc.addImage(logoSrc, 'JPEG', M, 8, 18, 16);
} catch(e) {}
doc.setFont('helvetica', 'bold');
doc.setFontSize(14);
doc.setTextColor(...colors.blanco);
doc.text('RESUME', M + 22, 15);
doc.setFont('helvetica', 'normal');
doc.setFontSize(8);
doc.setTextColor(136, 201, 185);
doc.text('Reporte Ejecutivo de Conciliación', M + 22, 21);
doc.setTextColor(200, 220, 235);
doc.text('Generado: ' + new Date().toLocaleString('es-MX'), M + 22, 27);
y = 40;
// ── Archivos ──
doc.setFillColor(...colors.fondo);
doc.rect(M, y, (W-2*M)/2 - 4, 18, 'F');
doc.rect(M + (W-2*M)/2 + 4, y, (W-2*M)/2 - 4, 18, 'F');
doc.setFont('helvetica','bold'); doc.setFontSize(8); doc.setTextColor(...colors.gris);
doc.text('ARCHIVO A', M+3, y+6);
doc.text('ARCHIVO B', M+(W-2*M)/2+7, y+6);
doc.setFont('helvetica','normal'); doc.setFontSize(9); doc.setTextColor(...colors.azul);
doc.text(State.fileA.name.substring(0,35), M+3, y+13);
doc.text(State.fileB.name.substring(0,35), M+(W-2*M)/2+7, y+13);
y += 24;
// ── KPIs ──
const r = State.results;
const na = State.dataA.length;
const nb = State.dataB.length;
const ct = r.filter(x=>x.tipo==='total').length;
const cp = r.filter(x=>x.tipo==='parcial2'||x.tipo==='parcial1').length;
const sa = r.filter(x=>x.tipo==='none').length;
const sb = r.filter(x=>x.tipo==='solo-b').length;
const pct = na>0 ? Math.round(ct/na*100) : 0;
const kpis = [
{ label:'Total A', val:na.toLocaleString(), color:colors.azul },
{ label:'Total B', val:nb.toLocaleString(), color:colors.claro },
{ label:'Coincidencias', val:ct.toLocaleString(), color:colors.verdoso },
{ label:'Parciales', val:cp.toLocaleString(), color:colors.naranja },
{ label:'Solo en A', val:sa.toLocaleString(), color:colors.azul },
{ label:'Solo en B', val:sb.toLocaleString(), color:colors.claro },
{ label:'% Efectividad', val:pct+'%', color:colors.verdoso },
];
const kW = (W - 2*M) / 4 - 3;
kpis.slice(0,4).forEach((k,i) => {
const x = M + i*(kW+4);
doc.setFillColor(...colors.blanco);
doc.rect(x, y, kW, 22, 'F');
doc.setDrawColor(...k.color);
doc.setLineWidth(0.5);
doc.line(x, y, x, y+22);
doc.setFont('helvetica','normal'); doc.setFontSize(7); doc.setTextColor(...colors.gris);
doc.text(k.label.toUpperCase(), x+3, y+8);
doc.setFont('helvetica','bold'); doc.setFontSize(14); doc.setTextColor(...k.color);
doc.text(k.val, x+3, y+18);
});
y += 28;
kpis.slice(4).forEach((k,i) => {
const kW2 = (W - 2*M) / 3 - 3;
const x = M + i*(kW2+4.5);
doc.setFillColor(...colors.blanco);
doc.rect(x, y, kW2, 22, 'F');
doc.setDrawColor(...k.color);
doc.setLineWidth(0.5);
doc.line(x, y, x, y+22);
doc.setFont('helvetica','normal'); doc.setFontSize(7); doc.setTextColor(...colors.gris);
doc.text(k.label.toUpperCase(), x+3, y+8);
doc.setFont('helvetica','bold'); doc.setFontSize(14); doc.setTextColor(...k.color);
doc.text(k.val, x+3, y+18);
});
y += 30;
// ── Reporte ejecutivo ──
doc.setFillColor(...colors.azul);
doc.rect(M, y, W-2*M, 7, 'F');
doc.setFont('helvetica','bold'); doc.setFontSize(9); doc.setTextColor(...colors.blanco);
doc.text('RESUMEN EJECUTIVO', M+3, y+5);
y += 10;
const fecha = new Date().toLocaleDateString('es-MX',{year:'numeric',month:'long',day:'numeric'});
const reportLines = doc.splitTextToSize(
`Se analizaron ${na.toLocaleString()} registros del Archivo A y ${nb.toLocaleString()} registros del Archivo B el día ${fecha}. Se encontraron ${ct.toLocaleString()} coincidencias completas, representando un ${pct}% de efectividad. Se detectaron ${cp.toLocaleString()} coincidencias parciales y ${sa.toLocaleString()} registros sin correspondencia en Archivo B. Archivo B contiene ${sb.toLocaleString()} registros exclusivos.`,
W - 2*M - 6
);
doc.setFont('helvetica','normal'); doc.setFontSize(9); doc.setTextColor(...colors.azul);
doc.text(reportLines, M+3, y+5);
y += reportLines.length * 5 + 10;
// ── Tabla de detalle (primeras 40 filas) ──
doc.setFillColor(...colors.azul);
doc.rect(M, y, W-2*M, 7, 'F');
doc.setFont('helvetica','bold'); doc.setFontSize(9); doc.setTextColor(...colors.blanco);
doc.text('DETALLE DE COMPARACIÓN (primeros 40 registros)', M+3, y+5);
y += 10;
// NUEVO: columnas Distrito y Estatus agregadas, anchos ajustados para mantener la tabla dentro del ancho de página
const cols = ['FFM-A','Nombre-A','ID-A','FFM-B','Nombre-B','ID-B','Coinciden','%','Estado','Distrito','Estatus'];
const colWs = [14,22,11,14,22,11,14,8,18,16,18];
let xc = M;
doc.setFillColor(232, 241, 248);
doc.rect(M, y, W-2*M, 7, 'F');
doc.setFont('helvetica','bold'); doc.setFontSize(7); doc.setTextColor(...colors.azul);
cols.forEach((c,i) => { doc.text(c, xc+1, y+5); xc += colWs[i]; });
y += 8;
const tipoLabel = { total:'Total', parcial2:'Parcial 2/3', parcial1:'Parcial 1/3', none:'Sin Coinc.', 'solo-b':'Solo B' };
const tipoColor = { total:colors.verdoso, parcial2:colors.naranja, parcial1:colors.naranja, none:colors.rojo, 'solo-b':colors.claro };
State.results.slice(0,40).forEach((r, ri) => {
if (y > 265) { doc.addPage(); y = 20; }
if (ri % 2 === 0) { doc.setFillColor(...colors.fondo); doc.rect(M, y-1, W-2*M, 7, 'F'); }
doc.setFont('helvetica','normal'); doc.setFontSize(6.5); doc.setTextColor(...colors.azul);
const vals = [
r.ffmA.substring(0,10), r.nombreA.substring(0,14), r.idA.substring(0,8),
r.ffmB.substring(0,10), r.nombreB.substring(0,14), r.idB.substring(0,8),
r.matches.join('+').substring(0,12), r.porcentaje+'%',
tipoLabel[r.tipo] || r.tipo,
(r.distrito || '—').substring(0,14), // NUEVO
(r.estatus || '—').substring(0,16) // NUEVO
];
xc = M;
vals.forEach((v, i) => {
if (i === 8) doc.setTextColor(...(tipoColor[r.tipo] || colors.gris));
else doc.setTextColor(...colors.azul);
doc.text(v, xc+1, y+4);
xc += colWs[i];
});
y += 7;
});
if (State.results.length > 40) {
doc.setFont('helvetica','italic'); doc.setFontSize(8); doc.setTextColor(...colors.gris);
doc.text(`… y ${(State.results.length - 40).toLocaleString()} registros más. Exporte a Excel para el detalle completo.`, M, y+8);
}
// ── Footer ──
const pageCount = doc.internal.getNumberOfPages();
for (let p = 1; p <= pageCount; p++) {
doc.setPage(p);
doc.setFillColor(...colors.azul);
doc.rect(0, 287, W, 10, 'F');
doc.setFont('helvetica','normal'); doc.setFontSize(7); doc.setTextColor(...colors.blanco);
doc.text('Conciliacion Colaboradores', M, 293);
doc.text(`Página ${p} de ${pageCount}`, W - M - 20, 293);
}
doc.save(`Reporte_Comparacion_${Date.now()}.pdf`);
showToast('PDF generado correctamente', 'success');
}
/* ── Eventos generales ──────────────────────── */
function bindEvents() {
document.getElementById('btn-compare').addEventListener('click', compareFiles);
document.getElementById('btn-export-excel').addEventListener('click', exportExcel);
document.getElementById('btn-export-pdf').addEventListener('click', exportPDF);
document.getElementById('btn-reset').addEventListener('click', resetApp);
document.getElementById('search-input').addEventListener('input', e => {
State.searchTerm = e.target.value;
applyFilters();
});
}
function resetApp() {
State.fileA = State.fileB = State.fileC = null; // NUEVO: incluir fileC
State.dataA = State.dataB = State.dataC = State.results = State.filtered = [];
State.indexC_ffm = new Map(); // NUEVO
State.indexC_curp = new Map(); // NUEVO
State.indexC_nombre = new Map(); // NUEVO
State.page = 1;
State.currentFilter = 'all';
State.searchTerm = '';
State.filesC = [];
State.dataC = [];
State.indexC_ffm = State.indexC_curp = State.indexC_nombre = new Map();
const list = document.getElementById('distrito-files-list');
if (list) list.innerHTML = '';
const badge = document.getElementById('distrito-count-badge');
if (badge) badge.textContent = '0 archivos cargados';
const inputC = document.getElementById('input-c');
if (inputC) inputC.value = '';
document.getElementById('screen-results').style.display = 'none';
document.getElementById('screen-upload').style.display = 'block';
['dz-a','dz-b','dz-c'].forEach(id => { // NUEVO: incluir dz-c si existe
const dz = document.getElementById(id);
if (!dz) return;
dz.classList.remove('file-loaded');
const input = dz.querySelector('input[type="file"]');
if (input) input.value = '';
});
document.getElementById('btn-compare').disabled = true;
document.querySelectorAll('.chip').forEach(c => {
c.classList.remove('active');
if (c.dataset.filter === 'all') c.classList.add('active');
});
document.getElementById('search-input').value = '';
window.scrollTo(0, 0);
showToast('Aplicación reiniciada', 'success');
}
/* ── Toast notifications ────────────────────── */
function showToast(msg, type = 'success') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icon = type === 'success' ? 'ti-circle-check' : 'ti-alert-circle';
toast.innerHTML = `${msg}`;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(20px)';
toast.style.transition = 'all 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 3500);
}