// ============================================================
// TablaGenerica.js — Componente reutilizable de tabla CRUD
// Creado para el módulo de Maestras y Proveedores de NexusERP
// Permite: listar, paginar, ordenar, filtrar por columna,
// buscar, exportar (CSV/Excel/TXT/PDF/Print) y CRUD básico.
// Cada módulo hereda de este objeto vía Object.assign() y
// puede sobreescribir los métodos que necesite (ej: abrirModal).
// ============================================================

var TablaGenerica = {
    // URL base del API (debe coincidir con el backend)
    API_URL: (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')
        ? 'http://localhost:3000/api'
        : 'https://api.sabha.pe/api',

    // ============================================================
    // init(config) — Inicializa el módulo con la configuración
    // Recibe un objeto config con:
    //   title, icon, idSingular, columns, endpoint, idField,
    //   limite, renderFormulario, onGuardar, etc.
    // ============================================================
    init: function(config) {
        this.config = config;
        this.title = config.title || 'Módulo';
        this.icon = config.icon || 'fa-table';
        this.idSingular = config.idSingular || 'registro';
        this.columns = config.columns || [];           // Definición de columnas [{field, label, width, sortable, filterable, render}]
        this.endpoint = config.endpoint || '';         // Ruta del API (ej: /bancos)
        this.idField = config.idField || 'id';         // Campo ID de la tabla
        this.sortDefault = config.sortDefault || (this.columns[0] ? this.columns[0].field : null);
        this.limite = config.limite || 10;             // Registros por página
        this.searchPlaceholder = config.searchPlaceholder || 'Buscar...';
        this.filtroEstado = config.filtroEstado !== undefined ? config.filtroEstado : true;
        this.showEstado = config.showEstado !== undefined ? config.showEstado : true;
        this.showAcciones = config.showAcciones !== undefined ? config.showAcciones : true;
        this.showCrear = config.showCrear !== undefined ? config.showCrear : true;
        this.exportFileName = config.exportFileName || this.title;
        this.estadoOpciones = config.estadoOpciones || [
            { value: '1', text: 'Activos' },
            { value: '0', text: 'Inactivos' },
            { value: '', text: 'Todos' }
        ];

        // Valor para buscar el link en el menú lateral via [data-page="..."],
        // por si el title tiene caracteres que no coinciden con el data-page del HTML
        this.menuPage = config.menuPage || this.title;

        // Callbacks que el módulo puede sobreescribir
        this.renderFormulario = config.renderFormulario || function() { return '<p>Formulario no definido</p>'; };
        this.onGuardar = config.onGuardar || function() {};
        this.onEliminar = config.onEliminar || function() {};
        this.onCargarFormulario = config.onCargarFormulario || function() {};
        this.abrirModal = config.abrirModal || this.abrirModal;
        this.verDetalle = config.verDetalle || this.verDetalle;
        this.columnFilters = config.columnFilters || {};
        this.estadoFiltroActivo = '1';                 // Por defecto solo activos
        this.filtrosColumna = {};                      // Filtros activos por columna {campo: "valor1,valor2"}
        this.sortCol = this.sortDefault;               // Columna de ordenamiento actual
        this.sortOrder = 'ASC';                        // Dirección: ASC o DESC
        this.paginaActual = 1;                         // Página actual
        this.elementId = config.elementId || 'page' + this.title.replace(/\s/g, '');  // ID del contenedor en el DOM
        this.dataKey = config.dataKey || 'data';       // Clave donde viene el array en la respuesta del API
        this.currentData = [];                         // Datos de la página actual (para acciones)
        this.sesion = null;                            // Sesión del usuario logueado

        this.cargarSesion();
        this.enlazarMenu();

        if (typeof ResponsiveHelper !== 'undefined') {
            var colsCount = this.columns ? this.columns.length : 0;
            ResponsiveHelper.registrar(this.elementId, {
                hideCols: colsCount > 5 ? [4, 5, 6, 7, 8] : null
            });
        }
    },

    // Lee la sesión del usuario desde localStorage o sessionStorage
    cargarSesion: function() {
        var saved = localStorage.getItem('erp_session') || sessionStorage.getItem('erp_session');
        if (saved) {
            try { this.sesion = JSON.parse(saved); } catch(e) { this.sesion = null; }
        }
        return this.sesion;
    },

    ensureSesion: function() {
        if (!this.sesion || !this.sesion.id_empresa) {
            var saved = localStorage.getItem('erp_session') || sessionStorage.getItem('erp_session');
            if (saved) { try { this.sesion = JSON.parse(saved); } catch(e) { this.sesion = null; } }
        }
        return this.sesion;
    },

    // ============================================================
    // enlazarMenu() — Vincula los clicks del menú lateral
    // Busca elementos .nav-link con data-page = menuPage (o title)
    // y al hacer click ejecuta mostrarVista()
    // ============================================================
    enlazarMenu: function() {
        var self = this;
        var links = document.querySelectorAll('.nav-link[data-page="' + this.menuPage + '"]');
        links.forEach(function(link) {
            link.addEventListener('click', function(e) {
                e.preventDefault();
                self.mostrarVista();
            });
        });
    },

    // ============================================================
    // mostrarVista() — Renderiza el HTML completo del módulo
    // Crea o muestra el contenedor #elementId dentro de contentArea
    // Incluye: header con ícono/título, toolbar (nuevo, buscar,
    // filtro estado, imprimir, exportar dropdown), tabla y paginación
    // ============================================================
    mostrarVista: function() {
        var self = this;
        var content = document.getElementById(this.elementId);
        if (!content) {
            var area = document.getElementById('contentArea');
            if (!area) return;
            var div = document.createElement('div');
            div.id = this.elementId;
            area.appendChild(div);
            content = div;
        }

        document.getElementById('contentArea').classList.add('modulo-activo');
        content.style.display = 'flex';

        // Combo de filtro por estado (Activos/Inactivos/Todos)
        var estadoHtml = '';
        if (this.filtroEstado) {
            estadoHtml = '<select class="prov-filtro" id="' + this.elementId + 'FiltroEstado">';
            this.estadoOpciones.forEach(function(o) {
                var sel = o.value === self.estadoFiltroActivo ? ' selected' : '';
                estadoHtml += '<option value="' + o.value + '"' + sel + '>' + o.text + '</option>';
            });
            estadoHtml += '</select>';
        }

        // Estructura HTML completa de la vista
        content.innerHTML =
            '<div class="prov-header">' +
                '<div class="prov-header-left">' +
                    '<div class="prov-header-icon"><i class="fas ' + this.icon + '"></i></div>' +
                    '<div class="prov-header-titulo">' +
                        '<h2>' + this.title + '</h2>' +
                        '<p>Gestión de ' + this.title.toLowerCase() + '</p>' +
                    '</div>' +
                '</div>' +
                '<span class="prov-contador" id="' + this.elementId + 'Contador"></span>' +
            '</div>' +
            '<div class="prov-toolbar">' +
                '<div class="prov-toolbar-left">' +
                    (this.showCrear ? '<button class="btn-prov btn-prov-primario" id="' + this.elementId + 'BtnNuevo"><i class="fas fa-plus"></i> Nuevo ' + this.idSingular + '</button>' : '') +
                    '<div class="prov-buscador"><i class="fas fa-search"></i><input type="text" id="' + this.elementId + 'Buscar" placeholder="' + this.searchPlaceholder + '"></div>' +
                '</div>' +
                '<div class="prov-toolbar-right">' +
                    estadoHtml +
                    '<button class="btn-prov btn-prov-icono" id="' + this.elementId + 'BtnPrint" title="Imprimir"><i class="fas fa-print"></i></button>' +
                    '<div class="prov-export-wrap">' +
                        '<button class="btn-prov btn-prov-icono" id="' + this.elementId + 'BtnExport" title="Exportar"><i class="fas fa-download"></i></button>' +
                        '<div class="prov-export-dd" id="' + this.elementId + 'ExportDD">' +
                            '<button class="prov-export-dd-item" data-formato="xlsx"><i class="fas fa-file-excel"></i> Excel</button>' +
                            '<button class="prov-export-dd-item" data-formato="csv"><i class="fas fa-file-csv"></i> CSV</button>' +
                            '<button class="prov-export-dd-item" data-formato="pdf"><i class="fas fa-file-pdf"></i> PDF</button>' +
                            '<button class="prov-export-dd-item" data-formato="txt"><i class="fas fa-file-lines"></i> TXT</button>' +
                        '</div>' +
                    '</div>' +
                    '<span id="' + this.elementId + 'FiltrosBadge" style="display:none"></span>' +
                '</div>' +
            '</div>' +
            '<div class="prov-tabla-wrap" id="' + this.elementId + 'TablaWrap"></div>' +
            '<div class="prov-paginacion" id="' + this.elementId + 'Paginacion"></div>';

        this.paginaActual = 1;
        this.cargarTabla();
        this.enlazarEventos();
    },

    // ============================================================
    // enlazarEventos() — Vincula eventos a los controles del toolbar
    // Botón Nuevo → abrirModal(null)
    // Búsqueda con debounce de 400ms
    // Combo estado → recarga tabla
    // Botón Imprimir → imprimir()
    // Botón Exportar → toggle del dropdown con formatos
    // ============================================================
    enlazarEventos: function() {
        var self = this;
        var prefix = this.elementId;

        var btnNuevo = document.getElementById(prefix + 'BtnNuevo');
        var buscar = document.getElementById(prefix + 'Buscar');
        var filtro = document.getElementById(prefix + 'FiltroEstado');
        var btnPrint = document.getElementById(prefix + 'BtnPrint');

        if (btnNuevo) {
            btnNuevo.addEventListener('click', function() {
                self.abrirModal();
            });
        }

        // Búsqueda con debounce: espera 400ms después de escribir
        if (buscar) {
            var timer = null;
            buscar.addEventListener('input', function() {
                clearTimeout(timer);
                timer = setTimeout(function() {
                    self.paginaActual = 1;
                    self.cargarTabla();
                }, 400);
            });
        }

        // Filtro por estado: recarga al cambiar
        if (filtro) {
            filtro.addEventListener('change', function() {
                self.estadoFiltroActivo = this.value;
                self.paginaActual = 1;
                self.cargarTabla();
            });
        }

        if (btnPrint) {
            btnPrint.addEventListener('click', function() {
                self.imprimir();
            });
        }

        // Toggle del menú desplegable de exportación
        var btnExport = document.getElementById(prefix + 'BtnExport');
        if (btnExport) {
            btnExport.addEventListener('click', function(e) {
                e.stopPropagation();
                var dd = document.getElementById(prefix + 'ExportDD');
                var visible = dd.classList.contains('visible');
                self.cerrarExportDD();
                if (!visible) dd.classList.add('visible');
            });
        }

        // Cierra el dropdown de exportación al hacer click fuera
        document.addEventListener('click', function(e) {
            if (!e.target.closest('.prov-export-wrap')) self.cerrarExportDD();
        });

        // Maneja clicks en los items del dropdown (Excel, CSV, PDF, TXT)
        document.querySelectorAll('.prov-export-dd-item').forEach(function(item) {
            item.addEventListener('click', function() {
                var fmt = this.dataset.formato;
                self.cerrarExportDD();
                if (fmt === 'xlsx') self.exportarExcel();
                else if (fmt === 'csv') self.exportarCSV();
                else if (fmt === 'pdf') self.exportarPDF();
                else if (fmt === 'txt') self.exportarTXT();
            });
        });
    },

    // Cierra el menú desplegable de exportación
    cerrarExportDD: function() {
        var dd = document.getElementById(this.elementId + 'ExportDD');
        if (dd) dd.classList.remove('visible');
    },

    // ============================================================
    // cargarTabla() — Obtiene datos del API y renderiza la tabla
    // Soporta: paginación, ordenamiento, búsqueda, filtros por
    // columna y filtro por estado. Construye el HTML de la tabla
    // con encabezados, filas, botones de ordenar/filtrar/editar/eliminar
    // ============================================================
    cargarTabla: async function() {
        var self = this;
        this.ensureSesion();
        if (!this.sesion || !this.sesion.id_empresa) {
            var wrap = document.getElementById(this.elementId + 'TablaWrap');
            if (wrap) wrap.innerHTML = '<div class="prov-vacio"><p>Sesión no disponible. Inicie sesión.</p></div>';
            return;
        }
        var wrap = document.getElementById(this.elementId + 'TablaWrap');
        var pag = document.getElementById(this.elementId + 'Paginacion');
        var contador = document.getElementById(this.elementId + 'Contador');
        if (!wrap) return;

        // Muestra spinner mientras carga
        wrap.innerHTML = '<div class="prov-vacio"><i class="fas fa-spinner fa-spin"></i><p>Cargando...</p></div>';
        pag.innerHTML = '';

        try {
            // Serializa filtros de columna activos como JSON en query string
            var filtrosStr = Object.keys(this.filtrosColumna).length > 0
                ? '&filtros=' + encodeURIComponent(JSON.stringify(this.filtrosColumna))
                : '';

            var buscarVal = document.getElementById(this.elementId + 'Buscar');
            // Arma la URL con todos los parámetros
            var url = this.API_URL + this.endpoint + '?id_empresa=' + (this.sesion ? this.sesion.id_empresa : '') +
                '&buscar=' + encodeURIComponent(buscarVal ? buscarVal.value : '') +
                '&page=' + this.paginaActual +
                '&limit=' + this.limite +
                '&sort=' + this.sortCol +
                '&order=' + this.sortOrder +
                filtrosStr;

            if (this.sesion && this.sesion.id_sucursal) {
                url += '&id_sucursal=' + this.sesion.id_sucursal;
            }

            if (this.filtroEstado && this.estadoFiltroActivo !== '') {
                url += '&estado=' + this.estadoFiltroActivo;
            }

            if (typeof this.obtenerParamsExtra === 'function') {
                url += this.obtenerParamsExtra();
            }

            var response = await fetch(url);
            var res = await response.json();

            if (!res.ok) {
                wrap.innerHTML = '<div class="prov-vacio"><i class="fas fa-exclamation-triangle"></i><p>' + res.mensaje + '</p></div>';
                return;
            }

            // Guarda los datos de la página actual para operaciones (editar/eliminar)
            this.currentData = res[this.dataKey] || [];

            // Estado vacío: muestra mensaje y botón "Limpiar filtros" si hay filtros activos
            if (this.currentData.length === 0) {
                var filtrosActivos = Object.keys(this.filtrosColumna).length;
                var htmlVacio = filtrosActivos > 0
                    ? '<div class="prov-vacio"><i class="fas fa-filter-circle-xmark"></i><p>No hay resultados con los filtros actuales</p>' +
                      '<button class="btn-prov btn-prov-limpiar" id="' + this.elementId + 'BtnLimpiarFiltros"><i class="fas fa-times"></i> Limpiar filtros (' + filtrosActivos + ')</button></div>'
                    : '<div class="prov-vacio"><i class="fas fa-box-open"></i><p>No se encontraron ' + this.title.toLowerCase() + '</p></div>';
                wrap.innerHTML = htmlVacio;
                contador.textContent = '';
                this.actualizarBadgeFiltros();
                var btnLimpiar = document.getElementById(this.elementId + 'BtnLimpiarFiltros');
                if (btnLimpiar) btnLimpiar.addEventListener('click', function() { self.limpiarFiltros(); });
                return;
            }

            var colSort = this.sortCol;
            var colOrder = this.sortOrder;

            // Genera ícono de orden: ⇅ (neutro), ▲ (ASC) o ▼ (DESC)
            function iconoSort(col) {
                if (colSort !== col) return '<span class="prov-sort-icon">⇅</span>';
                return colOrder === 'ASC' ? '<span class="prov-sort-icon activo asc">▲</span>' : '<span class="prov-sort-icon activo desc">▼</span>';
            }

            // Marca visual si la columna tiene filtro activo
            function filtroActivo(col) {
                return self.filtrosColumna[col] ? ' activo' : '';
            }

            // Construye el HTML de la tabla
            var html = '<table class="prov-tabla"><thead><tr>';
            this.columns.forEach(function(col, idx) {
                var sortAttrib = col.sortable !== false ? ' data-sort="' + col.field + '"' : '';
                var thClass = col.sortable !== false ? 'prov-th-sort' : '';
                if (col.width) thClass += ' col-' + col.field;

                html += '<th class="' + thClass + '"' + sortAttrib + ' style="' + (col.width ? 'width:' + col.width : '') + '">' +
                    '<div class="prov-th-content"><span>' + col.label + '</span>' +
                    '<span class="prov-th-controls">';

                // Botón de ordenar (aparece en todas las columnas sorteables)
                if (col.sortable !== false) {
                    html += '<button class="prov-btn-sort" data-sort="' + col.field + '" title="Ordenar">' + iconoSort(col.field) + '</button>';
                }
                // Botón de filtro (solo en columnas con filterable: true)
                if (col.filterable) {
                    html += '<button class="prov-btn-filtro' + filtroActivo(col.field) + '" data-col="' + col.field + '" title="Filtrar"><i class="fas fa-filter"></i></button>';
                }

                html += '</span></div></th>';
            });
            // Columna de acciones (Editar / Eliminar)
            if (this.showAcciones !== false) {
                html += '<th style="text-align:center;width:100px">Acciones</th>';
            }
            html += '</tr></thead><tbody>';

            // Renderiza las filas con los datos
            this.currentData.forEach(function(row) {
                html += '<tr>';
                self.columns.forEach(function(col) {
                    var raw = row[col.field];
                    var val;
                    if (col.render) {
                        val = col.render(row, raw);  // Render personalizado (ej: badge estado)
                    } else {
                        val = (raw !== null && raw !== undefined) ? String(raw) : '';
                    }
                    var tdClass = col.className || 'col-' + col.field;
                    var tdStyle = col.align ? ' style="text-align:' + col.align + '"' : '';
                    var dataVal = (raw !== null && raw !== undefined) ? (raw === true ? '1' : raw === false ? '0' : String(raw)).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;') : '';
                    html += '<td class="' + tdClass + '"' + tdStyle + ' data-val="' + dataVal + '">' + val + '</td>';
                });
                if (self.showAcciones !== false) {
                    var idVal = row[self.idField];
                    html += '<td style="text-align:center"><div class="prov-acciones">' +
                        '<button class="prov-btn-accion" title="Editar" data-id="' + idVal + '"><i class="fas fa-pen"></i></button>' +
                        '<button class="prov-btn-accion prov-btn-eliminar" title="Eliminar" data-id="' + idVal + '"><i class="fas fa-trash-can"></i></button>' +
                    '</div></td>';
                }
                html += '</tr>';
            });

            html += '</tbody></table>';
            wrap.innerHTML = html;
            contador.textContent = res.total + ' registro(s)';

            this.actualizarBadgeFiltros();
            this.renderPaginacion(res.total, res.totalPaginas);

            // Evento: botón Editar → abrirModal con ID
            wrap.querySelectorAll('.prov-btn-accion:not(.prov-btn-eliminar)').forEach(function(btn) {
                btn.addEventListener('click', function() {
                    self.abrirModal(this.dataset.id);
                });
            });

            // Evento: botón Eliminar → confirmar y eliminar
            wrap.querySelectorAll('.prov-btn-eliminar').forEach(function(btn) {
                btn.addEventListener('click', function() {
                    self.eliminar(this.dataset.id);
                });
            });

            // Column resize handles (right neighbor compensates, fixed delta)
            var allTh = wrap.querySelectorAll('table thead th');
            var visibleTh = [];
            allTh.forEach(function(th) {
                if (th.textContent.trim() === 'Acciones') return;
                visibleTh.push(th);
            });
            visibleTh.forEach(function(th, idx) {
                th.style.position = 'relative';
                var grip = document.createElement('div');
                grip.className = 'tg-col-resize';
                grip.style.cssText = 'position:absolute;top:0;right:-2px;width:6px;height:100%;cursor:col-resize;z-index:2';
                th.appendChild(grip);
                grip.addEventListener('mousedown', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    var next = visibleTh[idx + 1];
                    var startX = e.clientX;
                    var startW = th.getBoundingClientRect().width;
                    var nextStartW = next ? next.getBoundingClientRect().width : 0;
                    var MIN = 30;
                    function setW(el, w) {
                        el.style.setProperty('width', w + 'px', 'important');
                        el.style.setProperty('max-width', w + 'px', 'important');
                        el.style.setProperty('min-width', w + 'px', 'important');
                    }
                    function onMove(ev) {
                        var delta = ev.clientX - startX;
                        var newW = Math.max(MIN, startW + delta);
                        setW(th, newW);
                        if (next) {
                            var nextW = Math.max(MIN, nextStartW - delta);
                            setW(next, nextW);
                        }
                    }
                    function onUp() {
                        document.removeEventListener('mousemove', onMove);
                        document.removeEventListener('mouseup', onUp);
                        document.body.style.cursor = '';
                        document.body.style.userSelect = '';
                    }
                    document.addEventListener('mousemove', onMove);
                    document.addEventListener('mouseup', onUp);
                    document.body.style.cursor = 'col-resize';
                    document.body.style.userSelect = 'none';
                });
                // Double-click restores auto width
                grip.addEventListener('dblclick', function() {
                    ['width', 'max-width', 'min-width'].forEach(function(p) {
                        th.style.removeProperty(p);
                        var nx = visibleTh[idx + 1];
                        if (nx) nx.style.removeProperty(p);
                    });
                });
            });

            // Evento: clicks en botones de ordenar (toggle ASC/DESC)
            wrap.querySelectorAll('.prov-btn-sort').forEach(function(btn) {
                btn.addEventListener('click', function(e) {
                    e.stopPropagation();
                    var col = this.dataset.sort;
                    if (self.sortCol === col) {
                        self.sortOrder = self.sortOrder === 'ASC' ? 'DESC' : 'ASC';
                    } else {
                        self.sortCol = col;
                        self.sortOrder = 'ASC';
                    }
                    self.paginaActual = 1;
                    self.cargarTabla();
                });
            });

            // Evento: clicks en botones de filtro → muestra dropdown de filtros
            wrap.querySelectorAll('.prov-btn-filtro').forEach(function(btn) {
                btn.addEventListener('click', function(e) {
                    e.stopPropagation();
                    self.cerrarTodosLosFiltros();
                    var col = this.dataset.col;
                    var rect = this.getBoundingClientRect();
                    var wrapRect = wrap.getBoundingClientRect();
                    self.mostrarFiltro(col, rect.left - wrapRect.left + wrap.scrollLeft, rect.bottom - wrapRect.top + wrap.scrollTop, this);
                });
            });

            // Evento: doble clic en fila → abre vista detalle
            wrap.querySelectorAll('.prov-tabla tbody tr').forEach(function(tr) {
                tr.addEventListener('dblclick', function(e) {
                    if (e.target.closest('.prov-btn-accion, .prov-btn-eliminar')) return;
                    var id = this.querySelector('.prov-btn-accion')?.dataset.id;
                    if (id) self.verDetalle(id);
                });
            });

        } catch (error) {
            console.error('[TABLA] Error cargar:', error);
            wrap.innerHTML = '<div class="prov-vacio"><i class="fas fa-exclamation-triangle"></i><p>Error de conexión con el servidor</p></div>';
        }
    },

    // ============================================================
    // renderPaginacion() — Genera los botones de paginación
    // Muestra máximo 5 botones (actual ± 2), con anterior/siguiente
    // ============================================================
    renderPaginacion: function(total, totalPaginas) {
        var pag = document.getElementById(this.elementId + 'Paginacion');
        if (!pag) return;

        if (totalPaginas <= 1) {
            pag.innerHTML = '<span>Total: ' + total + ' registro(s)</span>';
            return;
        }

        var self = this;
        var html = '<div class="prov-pag-btns">';
        html += '<button class="prov-pag-btn" data-page="1" ' + (this.paginaActual <= 1 ? 'disabled' : '') + ' title="Primera página"><i class="fas fa-angle-double-left"></i></button>';
        html += '<button class="prov-pag-btn" data-page="' + (this.paginaActual - 1) + '" ' + (this.paginaActual <= 1 ? 'disabled' : '') + ' title="Anterior"><i class="fas fa-chevron-left"></i></button>';

        var inicio = Math.max(1, this.paginaActual - 2);
        var fin = Math.min(totalPaginas, this.paginaActual + 2);

        for (var i = inicio; i <= fin; i++) {
            var activa = i === this.paginaActual ? ' activo' : '';
            html += '<button class="prov-pag-btn' + activa + '" data-page="' + i + '">' + i + '</button>';
        }

        html += '<button class="prov-pag-btn" data-page="' + (this.paginaActual + 1) + '" ' + (this.paginaActual >= totalPaginas ? 'disabled' : '') + ' title="Siguiente"><i class="fas fa-chevron-right"></i></button>';
        html += '<button class="prov-pag-btn" data-page="' + totalPaginas + '" ' + (this.paginaActual >= totalPaginas ? 'disabled' : '') + ' title="Última página"><i class="fas fa-angle-double-right"></i></button>';
        html += '</div>';
        html += '<span>Página ' + this.paginaActual + ' de ' + totalPaginas + ' (' + total + ' registros)</span>';

        pag.innerHTML = html;

        pag.querySelectorAll('.prov-pag-btn[data-page]').forEach(function(btn) {
            btn.addEventListener('click', function() {
                if (this.disabled) return;
                self.paginaActual = parseInt(this.dataset.page);
                self.cargarTabla();
                var contentArea = document.querySelector('.content-area');
                if (contentArea) contentArea.scrollTop = 0;
            });
        });
    },

    // ============================================================
    // FILTROS POR COLUMNA
    // Permite filtrar datos seleccionando valores del dropdown
    // Soporta 3 orígenes de opciones:
    //   1. filterOptions — array estático [{valor, texto}]
    //   2. filterEndpoint — URL del API para obtener opciones
    //   3. Por defecto — extrae valores únicos de las filas visibles
    // ============================================================

    // Cierra el dropdown de filtro si está abierto
    cerrarTodosLosFiltros: function() {
        var dd = document.getElementById('provFilterDropdown');
        if (dd) dd.remove();
    },

    // Muestra el dropdown de filtro para una columna
    mostrarFiltro: async function(col, left, top, btnRef) {
        this.cerrarTodosLosFiltros();
        var self = this;

        var dd = document.createElement('div');
        dd.id = 'provFilterDropdown';
        dd.className = 'prov-filter-dd';
        dd.style.left = left + 'px';
        dd.style.top = top + 'px';
        dd.dataset.col = col;

        dd.innerHTML = '<div class="prov-filter-dd-header">' +
            '<input type="text" class="prov-filter-dd-search" placeholder="Buscar...">' +
            '</div>' +
            '<div class="prov-filter-dd-body"><div class="prov-filter-dd-loading"><i class="fas fa-spinner fa-spin"></i> Cargando...</div></div>' +
            '<div class="prov-filter-dd-footer">' +
            '<button class="prov-filter-dd-btn prov-filter-dd-ok">Aceptar</button>' +
            '<button class="prov-filter-dd-btn prov-filter-dd-clear">Limpiar</button>' +
            '</div>';

        var wrap = document.getElementById(this.elementId + 'TablaWrap');
        if (wrap) wrap.appendChild(dd);

        // Posicionamiento inteligente: si no hay espacio a la derecha, abre hacia la izquierda
        requestAnimationFrame(function() {
            var ddRect = dd.getBoundingClientRect();
            var wrapRect2 = wrap.getBoundingClientRect();
            var overflowRight = ddRect.left + ddRect.width - wrapRect2.right;
            if (overflowRight > 0) {
                dd.style.left = (left - ddRect.width - 10) + 'px';
                dd.style.maxWidth = Math.min(400, wrapRect2.width - 20) + 'px';
            }
        });

        setTimeout(function() {
            function cerrar(e) {
                var ddActual = document.getElementById('provFilterDropdown');
                if (!ddActual) { document.removeEventListener('click', cerrar); return; }
                if (e.target.closest('.prov-btn-filtro')) return;
                if (ddActual.contains(e.target)) return;
                self.cerrarTodosLosFiltros();
                document.removeEventListener('click', cerrar);
            }
            document.addEventListener('click', cerrar);
        }, 10);

        var filtros = this.filtrosColumna[col] || '';
        var colDef = null;
        for (var i = 0; i < this.columns.length; i++) {
            if (this.columns[i].field === col) { colDef = this.columns[i]; break; }
        }

        // Determina las opciones según la configuración de la columna
        if (colDef && colDef.filterType === 'range') {
            // Filtro por rango numérico (inputs Desde / Hasta)
            dd.querySelector('.prov-filter-dd-search').style.display = 'none';
            this.renderFiltroRango(dd, col, filtros);
        } else if (colDef && colDef.filterOptions) {
            // Opciones estáticas definidas en configuración
            this.renderFiltroOpciones(dd, colDef.filterOptions, filtros);
        } else if (colDef && colDef.filterEndpoint) {
            // Opciones desde el API
            try {
                var res = await fetch(this.API_URL + colDef.filterEndpoint);
                var data = await res.json();
                var opts = data.ok ? data.data.map(function(r) { return { valor: String(r.id || r.valor), texto: r.nombre || r.texto }; }) : [];
                this.renderFiltroOpciones(dd, opts, filtros);
            } catch (_) {
                dd.querySelector('.prov-filter-dd-body').innerHTML = '<div class="prov-filter-dd-empty">Error al cargar</div>';
            }
        } else {
            // Consulta al backend para valores distintos (todas las páginas)
            var distinctUrl = this.API_URL + this.endpoint + '?distinct=' + encodeURIComponent(col) +
                '&id_empresa=' + (this.sesion ? this.sesion.id_empresa : '');

            if (this.sesion && this.sesion.id_sucursal) {
                distinctUrl += '&id_sucursal=' + this.sesion.id_sucursal;
            }

            if (this.filtroEstado && this.estadoFiltroActivo !== '') {
                distinctUrl += '&estado=' + this.estadoFiltroActivo;
            }

            var buscarVal = document.getElementById(this.elementId + 'Buscar');
            if (buscarVal && buscarVal.value) {
                distinctUrl += '&buscar=' + encodeURIComponent(buscarVal.value);
            }

            var filtrosStr = Object.keys(this.filtrosColumna).length > 0
                ? '&filtros=' + encodeURIComponent(JSON.stringify(this.filtrosColumna))
                : '';
            distinctUrl += filtrosStr;

            if (typeof this.obtenerParamsExtra === 'function') {
                distinctUrl += this.obtenerParamsExtra();
            }

            try {
                var res = await fetch(distinctUrl);
                var data = await res.json();

                if (data.ok && data.data && data.total === undefined) {
                    var opts = data.data.map(function(item) {
                        var raw = String(item.valor);
                        var txt = raw;
                        if (colDef && colDef.render) {
                            var mockRow = {};
                            mockRow[col] = raw === '1' ? true : raw === '0' ? false : raw;
                            var rendered = colDef.render(mockRow, item.valor);
                            var div = document.createElement('div');
                            div.innerHTML = rendered;
                            txt = div.textContent || div.innerText || raw;
                        }
                        return { valor: raw, texto: txt };
                    });
                    this.renderFiltroOpciones(dd, opts, filtros);
                } else {
                    // Fallback: extraer del DOM (backend no soporta distinct)
                    var valoresUnicos = {};
                    var filas = document.querySelectorAll('#' + this.elementId + 'TablaWrap .prov-tabla tbody tr:not([class*="total-row"])');
                    filas.forEach(function(tr) {
                        var celdas = tr.querySelectorAll('td');
                        var idx = self.columns.findIndex(function(c) { return c.field === col; });
                        if (idx >= 0 && celdas[idx]) {
                            var raw = celdas[idx].getAttribute('data-val');
                            var txt = celdas[idx].textContent.trim();
                            var key = raw || txt;
                            if (key) {
                                if (!valoresUnicos[key]) valoresUnicos[key] = { valor: raw || txt, texto: txt };
                            }
                        }
                    });
                    var opts2 = Object.keys(valoresUnicos).sort().map(function(k) {
                        return valoresUnicos[k];
                    });
                    this.renderFiltroOpciones(dd, opts2, filtros);
                }
            } catch (_) {
                // Fallback: extraer del DOM
                var valoresUnicos = {};
                var filas = document.querySelectorAll('#' + this.elementId + 'TablaWrap .prov-tabla tbody tr:not([class*="total-row"])');
                filas.forEach(function(tr) {
                    var celdas = tr.querySelectorAll('td');
                    var idx = self.columns.findIndex(function(c) { return c.field === col; });
                    if (idx >= 0 && celdas[idx]) {
                        var raw = celdas[idx].getAttribute('data-val');
                        var txt = celdas[idx].textContent.trim();
                        var key = raw || txt;
                        if (key) {
                            if (!valoresUnicos[key]) valoresUnicos[key] = { valor: raw || txt, texto: txt };
                        }
                    }
                });
                var opts2 = Object.keys(valoresUnicos).sort().map(function(k) {
                    return valoresUnicos[k];
                });
                this.renderFiltroOpciones(dd, opts2, filtros);
            }
        }

        // Botón Aceptar: aplica los valores seleccionados como filtro
        dd.querySelector('.prov-filter-dd-ok').addEventListener('click', function() {
            try {
                var rangeFrom = dd.querySelector('.prov-filter-range-from');
                var vals = [];
                if (rangeFrom) {
                    // Modo rango numérico
                    var fromVal = rangeFrom.value.trim();
                    var toVal = dd.querySelector('.prov-filter-range-to').value.trim();
                    if (fromVal) vals.push('>=' + fromVal);
                    if (toVal) vals.push('<=' + toVal);
                } else {
                    // Modo checkboxes (existente)
                    var body = dd.querySelector('.prov-filter-dd-body');
                    body.querySelectorAll('input:checked').forEach(function(c) {
                        var parent = c.closest('.prov-filter-dd-item');
                        if (parent && parent.style.display !== 'none') {
                            vals.push(c.value);
                        }
                    });
                }
                if (vals.length > 0) {
                    self.filtrosColumna[col] = vals.join('\x01');
                } else {
                    delete self.filtrosColumna[col];
                }
                self.paginaActual = 1;
                self.cargarTabla();
                self.cerrarTodosLosFiltros();
            } catch (e) {
                console.error('[FILTRO] Error al aplicar filtro:', e);
            }
        });

        // Botón Limpiar: quita el filtro de esta columna
        dd.querySelector('.prov-filter-dd-clear').addEventListener('click', function() {
            delete self.filtrosColumna[col];
            self.paginaActual = 1;
            self.cargarTabla();
            self.cerrarTodosLosFiltros();
        });
    },

    // Renderiza las opciones del filtro como checkboxes con "Seleccionar todo"
    renderFiltroOpciones: function(dd, opciones, seleccionados) {
        var selSet = {};
        if (seleccionados) {
            seleccionados.split('\x01').forEach(function(v) { selSet[String(v).trim()] = true; });
        }

        var body = dd.querySelector('.prov-filter-dd-body');
        if (!opciones || opciones.length === 0) {
            body.innerHTML = '<div class="prov-filter-dd-empty">Sin opciones</div>';
            return;
        }

        function escapar(s) { return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }

        var html = '';
        opciones.forEach(function(o) {
            var checked = selSet.hasOwnProperty(o.valor) && selSet[o.valor] ? ' checked' : '';
            html += '<label class="prov-filter-dd-item">' +
                '<input type="checkbox" value="' + escapar(o.valor) + '"' + checked + '>' +
                '<span>' + escapar(o.texto) + '</span></label>';
        });
        body.innerHTML = html;

        // Checkbox "Seleccionar todo / Deseleccionar todo"
        var header = dd.querySelector('.prov-filter-dd-header');
        var allChecked = opciones.every(function(o) { return selSet.hasOwnProperty(o.valor) && selSet[o.valor]; });
        var selectAllHtml = '<label class="prov-filter-dd-item prov-filter-dd-selectall">' +
            '<input type="checkbox" class="prov-filter-dd-toggleall"' + (allChecked ? ' checked' : '') + '>' +
            '<span>' + (allChecked ? 'Deseleccionar todo' : 'Seleccionar todo') + '</span></label>';
        header.insertAdjacentHTML('afterend', selectAllHtml);

        dd.querySelector('.prov-filter-dd-toggleall').addEventListener('change', function() {
            var checkAll = this.checked;
            dd.querySelectorAll('.prov-filter-dd-body .prov-filter-dd-item').forEach(function(item) {
                if (item.style.display !== 'none') {
                    item.querySelector('input[type="checkbox"]').checked = checkAll;
                }
            });
            var span = this.closest('.prov-filter-dd-item').querySelector('span');
            if (span) span.textContent = checkAll ? 'Deseleccionar todo' : 'Seleccionar todo';
        });

        // Búsqueda en el dropdown de filtros
        var searchInput = dd.querySelector('.prov-filter-dd-search');
        if (searchInput) {
            searchInput.addEventListener('input', function() {
                var term = this.value.toLowerCase().trim();
                var hasFilter = term.length > 0;
                var anyVisible = false;
                dd.querySelectorAll('.prov-filter-dd-body .prov-filter-dd-item').forEach(function(item) {
                    var text = item.querySelector('span').textContent.toLowerCase();
                    var match = !hasFilter || text.includes(term);
                    item.style.display = match ? '' : 'none';
                    if (match) anyVisible = true;
                });
                var selectAllItem = dd.querySelector('.prov-filter-dd-selectall');
                if (selectAllItem) selectAllItem.style.display = anyVisible ? '' : 'none';
            });
        }
    },

    // Renderiza el filtro de rango numérico (inputs Desde / Hasta)
    renderFiltroRango: function(dd, col, seleccionados) {
        var desde = '';
        var hasta = '';
        if (seleccionados) {
            seleccionados.split('\x01').forEach(function(v) {
                v = String(v).trim();
                if (v.indexOf('>=') === 0) desde = v.substring(2).trim();
                else if (v.indexOf('<=') === 0) hasta = v.substring(2).trim();
            });
        }
        var html = '<div class="prov-filter-range">' +
            '<label class="prov-filter-range-label">Desde</label>' +
            '<input type="number" class="prov-filter-range-from prov-input" step="0.01" value="' + desde + '">' +
            '<label class="prov-filter-range-label" style="margin-top:8px">Hasta</label>' +
            '<input type="number" class="prov-filter-range-to prov-input" step="0.01" value="' + hasta + '">' +
            '</div>';
        dd.querySelector('.prov-filter-dd-body').innerHTML = html;
    },

    // Muestra/oculta el badge con el número de filtros activos en el toolbar
    actualizarBadgeFiltros: function() {
        var total = Object.keys(this.filtrosColumna).length;
        var badge = document.getElementById(this.elementId + 'FiltrosBadge');
        if (!badge) return;
        if (total > 0) {
            badge.style.display = '';
            badge.innerHTML = '<button class="prov-badge-filtros" id="' + this.elementId + 'BtnLimpiarFiltrosToolbar"><i class="fas fa-filter"></i> ' + total + ' filtro(s) <i class="fas fa-times"></i></button>';
            var btn = document.getElementById(this.elementId + 'BtnLimpiarFiltrosToolbar');
            if (btn) btn.addEventListener('click', function() { this.limpiarFiltros(); }.bind(this));
        } else {
            badge.style.display = 'none';
            badge.innerHTML = '';
        }
    },

    // Limpia todos los filtros de columna y recarga
    limpiarFiltros: function() {
        this.filtrosColumna = {};
        this.paginaActual = 1;
        this.cargarTabla();
    },

    // ============================================================
    // CRUD — Crear, Leer, Actualizar, Eliminar
    // abrirModal(id): abre modal de creación (sin ID) o edición (con ID)
    // cargarDatos(id): obtiene datos del API para edición
    // eliminar(id): confirma y elimina vía DELETE
    // verDetalle(id): muestra detalles en modal (doble click en fila)
    // ============================================================

    abrirModal: function(id) {
        var self = this;
        var titulo = id ? 'Editar ' + this.idSingular : 'Nuevo ' + this.idSingular;
        var esNuevo = !id;

        // Renderiza el formulario dentro del modal
        var contenido = '<div class="prov-modal-form" id="' + this.elementId + 'Form">';
        contenido += '<input type="hidden" id="' + this.elementId + 'FormId" value="' + (id || '') + '">';
        contenido += this.renderFormulario(null);
        contenido += '</div>';

        Modal.abrir({
            titulo: titulo,
            ancho: '500px',
            contenido: contenido,
            botones: [
                { texto: 'Cancelar', tipo: 'secundario', accion: function() { Modal.cerrar(); } },
                { texto: 'Guardar', tipo: 'primario', accion: function() { self.onGuardar(); } }
            ]
        });

        if (esNuevo) {
            this.onCargarFormulario(null);   // Inicializa campos vacíos
        } else {
            this.cargarDatos(id);            // Carga datos existentes
        }
    },

    // Obtiene los datos del registro desde el API y los pasa al callback onCargarFormulario
    cargarDatos: async function(id) {
        var self = this;
        this.ensureSesion();
        try {
            var sucParam = (this.sesion && this.sesion.id_sucursal) ? '&id_sucursal=' + this.sesion.id_sucursal : '';
            var res = await fetch(this.API_URL + this.endpoint + '/' + id + '?id_empresa=' + (this.sesion ? this.sesion.id_empresa : '') + sucParam);
            var data = await res.json();
            if (data.ok) {
                self.onCargarFormulario(data.data);
            }
        } catch (error) {
            Mensaje.error({ titulo: 'Error', texto: 'No se pudo cargar el ' + this.idSingular });
        }
    },

    // Elimina un registro previa confirmación del usuario
    eliminar: function(id) {
        var self = this;
        this.ensureSesion();
        var row = this.currentData.find(function(r) { return String(r[self.idField]) === String(id); });
        var nombre = row ? (row.razon_social || row.nombre || 'ID ' + id) : 'ID ' + id;

        Mensaje.confirmacion({
            titulo: 'Eliminar ' + this.idSingular,
            texto: '¿Estás seguro de eliminar "' + nombre + '"?',
            onAceptar: function() {
                var sucDel = (self.sesion && self.sesion.id_sucursal) ? '&id_sucursal=' + self.sesion.id_sucursal : '';
                fetch(self.API_URL + self.endpoint + '/' + id + '?id_empresa=' + (self.sesion ? self.sesion.id_empresa : '') + sucDel, { method: 'DELETE' })
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    if (data.ok) {
                        var titulo = data.mensaje && data.mensaje.indexOf('desactivado') >= 0 ? 'Desactivado' : 'Eliminado';
                        Mensaje.exito({
                            titulo: titulo,
                            texto: data.mensaje,
                            onAceptar: function() {
                                self.paginaActual = 1;
                                self.cargarTabla();
                            }
                        });
                    } else {
                        Mensaje.error({ titulo: 'Error', texto: data.mensaje });
                    }
                })
                .catch(function() {
                    Mensaje.error({ titulo: 'Error', texto: 'Error al eliminar ' + self.idSingular });
                });
            }
        });
    },

    // Muestra una ventana de detalle con todos los campos (doble click en fila)
    verDetalle: async function(id) {
        var self = this;
        try {
            var res = await fetch(this.API_URL + this.endpoint + '/' + id + '?id_empresa=' + (this.sesion ? this.sesion.id_empresa : ''));
            var data = await res.json();
            if (!data.ok) return;

            var html = '<div class="prov-detalle">';
            this.columns.forEach(function(col) {
                var val = data.data[col.field];
                if (col.render) val = col.render(data.data, val);
                else val = (val !== null && val !== undefined) ? String(val) : '';
                html += '<div class="prov-detalle-row"><span class="prov-detalle-label">' + col.label + ':</span><span class="prov-detalle-valor">' + val + '</span></div>';
            });
            html += '</div>';

            Modal.abrir({
                titulo: 'Detalle de ' + this.idSingular,
                ancho: '500px',
                contenido: html,
                botones: [
                    {
                        texto: 'Imprimir', tipo: 'secundario', accion: function() {
                            var empresa = self.sesion && self.sesion.empresa_razon ? self.sesion.empresa_razon : '';
                            var ahora = new Date();
                            var fechaHora = ahora.toLocaleDateString('es-PE', { day: '2-digit', month: '2-digit', year: 'numeric' }) + ' ' + ahora.toLocaleTimeString('es-PE', { hour: '2-digit', minute: '2-digit' });
                            var lineas = '';
                            self.columns.forEach(function(col) {
                                var val = data.data[col.field];
                                if (col.render) val = col.render(data.data, val);
                                else val = (val !== null && val !== undefined) ? String(val) : '-';
                                lineas += '<tr><td style="padding:6px 10px;border-bottom:1px solid #E2E8F0;font-weight:600;color:#475569;width:140px;font-size:0.75rem">' + col.label + '</td><td style="padding:6px 10px;border-bottom:1px solid #E2E8F0;color:#1E293B;font-size:0.78rem">' + val + '</td></tr>';
                            });
                            var fullHtml = '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>' + self.idSingular + '</title>' +
                                '<link rel="preconnect" href="https://fonts.googleapis.com"><link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">' +
                                '<style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:"Plus Jakarta Sans",sans-serif;padding:30px 40px;color:#1E293B}.header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px;padding-bottom:12px;border-bottom:2px solid #1A4480}.header-left{}.empresa{font-size:0.65rem;font-weight:600;color:#475569;margin-top:2px}.titulo{font-size:1.2rem;font-weight:800;color:#1A4480;text-transform:uppercase;letter-spacing:0.5px}.fecha{font-size:0.65rem;color:#64748B;text-align:right;margin-bottom:4px}table{width:100%;border-collapse:collapse;margin-top:12px}.footer{text-align:center;font-size:0.65rem;color:#94A3B8;margin-top:20px;border-top:1px solid #E2E8F0;padding-top:8px}@page{margin:12mm}</style></head><body>' +
                                '<div class="header"><div class="header-left"><div class="titulo">' + self.idSingular + '</div>' + (empresa ? '<div class="empresa">' + empresa + '</div>' : '') + '</div><div class="fecha">' + fechaHora + '</div></div>' +
                                '<table>' + lineas + '</table>' +
                                '<div class="footer">NexusERP</div></body></html>';
                            var oldFrame = document.getElementById('tg-print-frame');
                            if (oldFrame) document.body.removeChild(oldFrame);
                            var iframe = document.createElement('iframe');
                            iframe.id = 'tg-print-frame';
                            iframe.style.cssText = 'position:absolute;width:1px;height:1px;opacity:0.01;top:-9999px;left:-9999px';
                            iframe.srcdoc = fullHtml;
                            document.body.appendChild(iframe);
                            iframe.onload = function() {
                                setTimeout(function() {
                                    try { iframe.contentWindow.print(); } catch(e) {}
                                }, 1500);
                            };
                        }
                    },
                    { texto: 'Cerrar', tipo: 'primario', accion: function() { Modal.cerrar(); } }
                ]
            });
        } catch (error) {
            Mensaje.error({ titulo: 'Error', texto: 'Error al cargar detalle' });
        }
    },

    // ============================================================
    // EXPORTAR — Exporta todos los registros filtrados a varios
    // formatos: CSV, Excel (XLSX), PDF (imprimir), TXT
    //
    // obtenerTodosDatos(): Obtiene TODOS los registros del API
    //   (no solo la página actual) usando limit=0 en el backend.
    //   Aplica los mismos filtros activos (búsqueda, columna, estado).
    // ============================================================

    // Obtiene todos los registros (sin paginación) respetando filtros activos
    obtenerTodosDatos: async function() {
        this.ensureSesion();
        var filtrosStr = Object.keys(this.filtrosColumna).length > 0
            ? '&filtros=' + encodeURIComponent(JSON.stringify(this.filtrosColumna))
            : '';

        var buscarVal = document.getElementById(this.elementId + 'Buscar');
        var url = this.API_URL + this.endpoint + '?id_empresa=' + (this.sesion ? this.sesion.id_empresa : '') +
            '&buscar=' + encodeURIComponent(buscarVal ? buscarVal.value : '') +
            '&limit=0' +   // ← flag para que el backend devuelva TODOS los registros
            '&sort=' + this.sortCol +
            '&order=' + this.sortOrder +
            filtrosStr;

        if (this.sesion && this.sesion.id_sucursal) {
            url += '&id_sucursal=' + this.sesion.id_sucursal;
        }

        if (this.filtroEstado && this.estadoFiltroActivo !== '') {
            url += '&estado=' + this.estadoFiltroActivo;
        }

        if (typeof this.obtenerParamsExtra === 'function') {
            url += this.obtenerParamsExtra();
        }

        var response = await fetch(url);
        var res = await response.json();
        if (!res.ok || !res[this.dataKey]) return [];
        return res[this.dataKey];
    },

    // Exporta a CSV (separado por punto y coma, BOM UTF-8 al inicio)
    exportarCSV: async function() {
        var datos = await this.obtenerTodosDatos();
        if (!datos || datos.length === 0) {
            Mensaje.error({ titulo: 'Exportar', texto: 'No hay datos para exportar' });
            return;
        }

        // Escapa valores para CSV: duplica comillas dobles, envuelve en comillas si contiene ; " \n
        function esc(val) {
            if (val == null) return '';
            var t = String(val).replace(/"/g, '""');
            if (t.indexOf(';') >= 0 || t.indexOf('"') >= 0 || t.indexOf('\n') >= 0) t = '"' + t + '"';
            return t;
        }

        var cabeceras = this.columns.map(function(c) { return c.label; });
        var lineas = [cabeceras.join(';')];

        datos.forEach(function(row) {
            var vals = [];
            this.columns.forEach(function(col) {
                var val = row[col.field];
                if (col.render) val = col.render(row, val);
                vals.push(esc(val));
            }, this);
            lineas.push(vals.join(';'));
        }, this);

        // \uFEFF = BOM para que Excel reconozca UTF-8
        var csv = '\uFEFF' + lineas.join('\r\n');
        var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
        var link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.download = this.exportFileName + '_' + new Date().toISOString().slice(0, 10) + '.csv';
        link.click();
        URL.revokeObjectURL(link.href);
    },

    // Exporta a Excel (XLSX) usando SheetJS (carga CDN bajo demanda)
    exportarExcel: async function() {
        var datos = await this.obtenerTodosDatos();
        if (!datos || datos.length === 0) {
            Mensaje.error({ titulo: 'Exportar', texto: 'No hay datos para exportar' });
            return;
        }

        var self = this;
        if (typeof XLSX === 'undefined') {
            // Carga la librería SheetJS desde CDN solo si no está disponible
            var script = document.createElement('script');
            script.src = 'https://cdn.sheetjs.com/xlsx-0.20.2/package/dist/xlsx.full.min.js';
            script.onload = function() { self._generarExcel(datos); };
            script.onerror = function() {
                Mensaje.error({ titulo: 'Exportar', texto: 'Error al cargar la librería de Excel' });
            };
            document.head.appendChild(script);
        } else {
            this._generarExcel(datos);
        }
    },

    // Genera el libro de Excel con una hoja única
    _generarExcel: function(datos) {
        var cabeceras = this.columns.map(function(c) { return c.label; });
        var filas = [cabeceras];

        datos.forEach(function(row) {
            var vals = [];
            this.columns.forEach(function(col) {
                var val = row[col.field];
                if (col.render) val = col.render(row, val);
                vals.push((val !== null && val !== undefined) ? String(val) : '');
            }, this);
            filas.push(vals);
        }, this);

        var wb = XLSX.utils.book_new();
        var ws = XLSX.utils.aoa_to_sheet(filas);
        XLSX.utils.book_append_sheet(wb, ws, this.title);
        ws['!cols'] = this.columns.map(function() { return { wch: 25 }; });  // Ancho de columna 25 caracteres
        XLSX.writeFile(wb, this.exportFileName + '_' + new Date().toISOString().slice(0, 10) + '.xlsx');
    },

    // Exportar PDF redirige a imprimir()
    exportarPDF: function() {
        this.imprimir();
    },

    // im prime() — Abre ventana de impresión con diseño profesional
    // Obtiene todos los datos, construye HTML con estilos y llama window.print()
    imprimir: async function() {
        var datos = await this.obtenerTodosDatos();
        if (!datos || datos.length === 0) {
            Mensaje.error({ titulo: 'Imprimir', texto: 'No hay datos para imprimir' });
            return;
        }

        // Recopila filtros activos para mostrarlos en el encabezado
        var filtros = [];
        if (Object.keys(this.filtrosColumna).length > 0) {
            for (var k in this.filtrosColumna) {
                filtros.push(k + ': ' + String(this.filtrosColumna[k]).replace(/\x01/g, ', '));
            }
        }
        var buscarVal = document.getElementById(this.elementId + 'Buscar');
        if (buscarVal && buscarVal.value.trim()) filtros.push('búsqueda: ' + buscarVal.value.trim());
        if (this.filtroEstado && this.estadoFiltroActivo !== '1') {
            var sel = document.getElementById(this.elementId + 'FiltroEstado');
            if (sel) filtros.push('estado: ' + sel.options[sel.selectedIndex].text);
        }

        var ahora = new Date();
        var fechaHora = ahora.toLocaleDateString('es-PE', { day: '2-digit', month: '2-digit', year: 'numeric' }) +
            ' ' + ahora.toLocaleTimeString('es-PE', { hour: '2-digit', minute: '2-digit' });

        var filasHtml = '';
        datos.forEach(function(row) {
            filasHtml += '<tr>';
            this.columns.forEach(function(col) {
                var val = row[col.field];
                if (col.render) val = col.render(row, val);
                else val = (val !== null && val !== undefined) ? String(val) : '';
                filasHtml += '<td>' + val + '</td>';
            }, this);
            filasHtml += '</tr>';
        }, this);

        var sesionActual = this.sesion;
        try {
            var saved = localStorage.getItem('erp_session') || sessionStorage.getItem('erp_session');
            if (saved) sesionActual = JSON.parse(saved);
        } catch(e) {}
        var empresa = sesionActual && sesionActual.empresa_razon ? sesionActual.empresa_razon : '';
        var logo = sesionActual && sesionActual.empresa_logo ? sesionActual.empresa_logo : '';
        if (!logo && sesionActual && sesionActual.id_empresa) {
            try {
                var r = await fetch(this.API_URL + '/empresas/' + sesionActual.id_empresa + '?id_empresa=' + sesionActual.id_empresa);
                var j = await r.json();
                if (j.ok && j.data && j.data.logo) {
                    logo = j.data.logo;
                    sesionActual.empresa_logo = logo;
                    localStorage.setItem('erp_session', JSON.stringify(sesionActual));
                }
            } catch(e) {}
        }
        var logoData = '';
        if (logo) {
            try {
                var respLogo = await fetch(logo);
                var blobLogo = await respLogo.blob();
                logoData = await new Promise(function(resolve) {
                    var reader = new FileReader();
                    reader.onloadend = function() { resolve(reader.result); };
                    reader.readAsDataURL(blobLogo);
                });
            } catch(e) {}
        }
        var logoHtml = logoData ? '<div style="display:inline-block;vertical-align:middle;margin-right:10px"><img src="' + logoData + '" style="height:36px;width:auto"></div>' : '';
        var sucursal = '';
        try {
            var s = sesionActual && sesionActual.sucursales && sesionActual.id_sucursal ? sesionActual.sucursales.find(function(su) { return su.id_sucursal == sesionActual.id_sucursal; }) : null;
            if (s && s.nombre) sucursal = s.nombre;
        } catch(e) {}

        var numCols = this.columns.length;
        var cabecerasHtml = this.columns.map(function(c) { return '<th style="white-space:nowrap">' + c.label + '</th>'; }).join('');
        var headerRowHtml = '<tr style="background:none;border:none">' +
            '<th colspan="' + numCols + '" style="background:none;border:none;text-transform:none;letter-spacing:normal;padding:0 0 12px 0;font-weight:400;color:#1E293B">' +
                '<div style="width:100%">' +
                    '<div style="float:right;font-size:0.65rem;color:#64748B;text-align:right">' + fechaHora + '</div>' +
                    logoHtml +
                    '<div style="display:inline-block;vertical-align:middle">' +
                        '<div style="font-size:1.2rem;font-weight:800;color:#1A4480;text-transform:uppercase;letter-spacing:0.5px">' + this.title + '</div>' +
                        (empresa ? '<div style="font-size:0.65rem;font-weight:600;color:#475569;margin-top:2px">' + empresa + '</div>' : '') +
                        (sucursal ? '<div style="font-size:0.6rem;color:#64748B;margin-top:1px">' + sucursal + '</div>' : '') +
                    '</div>' +
                    '<div style="clear:both"></div>' +
                '</div>' +
            '</th>' +
        '</tr>';

        var tablaHtml = '<div style="border-radius:8px">' +
            '<table style="border-collapse:collapse">' +
                '<thead>' +
                    headerRowHtml +
                    '<tr>' + cabecerasHtml + '</tr>' +
                '</thead>' +
                '<tbody>' + filasHtml + '</tbody>' +
            '</table></div>';

        if (filtros.length > 0) {
            var filtrosHtml = '<div style="font-size:0.72rem;color:#475569;background:#F1F5F9;padding:5px 10px;border-radius:6px;margin-bottom:14px">Filtros: ' + filtros.join(' | ') + '</div>';
            tablaHtml = filtrosHtml + tablaHtml;
        }

        var contenido = '<div id="provPrintContent">' +
            tablaHtml +
            '<div style="font-size:0.75rem;color:#1A4480;font-weight:600;margin-top:12px">Total: ' + datos.length + ' registro(s)</div>' +
            '</div>';

        var fullHtml = '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>' + this.title + '</title>' +
            '<link rel="preconnect" href="https://fonts.googleapis.com"><link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">' +
            '<style>' +
            '*{margin:0;padding:0;box-sizing:border-box}' +
            'body{font-family:"Plus Jakarta Sans",sans-serif;padding:30px 40px;color:#1E293B}' +
            '.prov-print-header{display:block;margin-bottom:16px;padding-bottom:12px;border-bottom:2px solid #1A4480}' +
            '.prov-print-titulo{font-size:1.2rem;font-weight:800;color:#1A4480;text-transform:uppercase;letter-spacing:0.5px}' +
            '.prov-print-empresa{font-size:0.65rem;font-weight:600;color:#475569;margin-top:2px}' +
            '.prov-print-info{text-align:right}' +
            '.prov-print-fecha{font-size:0.65rem;color:#64748B;text-align:right;margin-bottom:4px}' +
            '.prov-print-filtros{font-size:0.72rem;color:#475569;background:#F1F5F9;padding:5px 10px;border-radius:6px;margin-bottom:14px}' +
            'table{width:100%;border-collapse:collapse;font-size:0.72rem}' +
            'thead{display:table-header-group}' +
            'thead th:first-child{border-radius:6px 0 0 0}' +
            'thead th:last-child{border-radius:0 6px 0 0}' +
            'tbody tr:last-child td:first-child{border-radius:0 0 0 6px}' +
            'tbody tr:last-child td:last-child{border-radius:0 0 6px 0}' +
            'thead th{background:#1A4480;color:#FFF;padding:7px 8px;text-align:left;font-weight:700;text-transform:uppercase;font-size:0.6rem;letter-spacing:0.5px;border-bottom:2px solid #0F2847}' +
            'table tbody td{padding:5px 8px;border-bottom:1px solid #E2E8F0;color:#334155}' +
            'tbody tr:nth-child(even){background:#F8FAFC}' +
            '.prov-print-total{font-size:0.75rem;color:#1A4480;font-weight:600;margin-top:12px}' +
            '.prov-print-footer{text-align:center;font-size:0.65rem;color:#94A3B8;margin-top:20px;border-top:1px solid #E2E8F0;padding-top:8px}' +
            '@page{margin:12mm;@bottom-center{content:counter(page);font-family:"Plus Jakarta Sans",sans-serif;font-size:0.65rem;color:#94A3B8}}' +
            '<' + 'script' + '>window.onload=function(){setTimeout(function(){window.print()},800)};<' + '/' + 'script' + '>' +
            '</style></head><body>' + contenido + '<div class="prov-print-footer">NexusERP</div></body></html>';

        var oldFrame = document.getElementById('tg-print-frame');
        if (oldFrame) document.body.removeChild(oldFrame);
        var iframe = document.createElement('iframe');
        iframe.id = 'tg-print-frame';
        iframe.style.cssText = 'position:absolute;width:1px;height:1px;opacity:0.01;top:-9999px;left:-9999px';
        iframe.srcdoc = fullHtml;
        document.body.appendChild(iframe);
        iframe.onload = function() {
            setTimeout(function() {
                try { iframe.contentWindow.print(); } catch(e) {}
            }, 1500);
        };
    },

    // ============================================================
    // exportarTXT() — Exporta a archivo de texto con columnas alineadas
    // Usa padEnd() para alinear columnas dinámicamente según el ancho
    // máximo de cada columna. Incluye separadores, encabezado con
    // título/fecha/total, y caracteres Unicode para bordes.
    // ============================================================
    exportarTXT: async function() {
        var datos = await this.obtenerTodosDatos();
        if (!datos || datos.length === 0) {
            Mensaje.error({ titulo: 'Exportar', texto: 'No hay datos para exportar' });
            return;
        }

        var cabeceras = this.columns.map(function(c) { return c.label; });

        function armarFila(row, cols) {
            return cols.map(function(col) {
                var val = row[col.field];
                if (col.render) val = col.render(row, val);
                return (val !== null && val !== undefined) ? String(val) : '';
            });
        }

        var filas = [cabeceras];
        datos.forEach(function(row) { filas.push(armarFila(row, this.columns)); }, this);

        // Calcula ancho de cada columna basado en el contenido más largo
        var anchos = [];
        for (var c = 0; c < cabeceras.length; c++) {
            var maxLen = 0;
            for (var r = 0; r < filas.length; r++) {
                if (filas[r][c].length > maxLen) maxLen = filas[r][c].length;
            }
            anchos.push(Math.min(Math.max(maxLen + 2, 12), 35));  // Mín 12, máx 35 caracteres
        }

        // Formatea una fila completa con padEnd según los anchos calculados
        function formatearFila(fila) {
            var partes = [];
            for (var c = 0; c < fila.length; c++) {
                var val = fila[c];
                if (val.length > anchos[c] - 1) val = val.substring(0, anchos[c] - 2) + '\u2026';  // Trunca con …
                partes.push(val.padEnd(anchos[c]));
            }
            return '  ' + partes.join(' \u2502 ');  // │ como separador de columnas
        }

        // Genera una línea de separación con el carácter dado
        function lineaLarga(car) {
            var p = [];
            for (var c = 0; c < cabeceras.length; c++) p.push(car.repeat(anchos[c]));
            return '  ' + p.join(' \u2502 ');
        }

        var sepTotal = anchos.reduce(function(a, b) { return a + b; }, 0) + (anchos.length - 1) * 3;

        var lineas = [];
        lineas.push('='.repeat(sepTotal));
        lineas.push((this.title).padStart(Math.floor(this.title.length / 2) + 14));
        var ahora = new Date();
        lineas.push('  Fecha: ' + ahora.toLocaleDateString('es-PE') + '  ' + ahora.toLocaleTimeString('es-PE', { hour: '2-digit', minute: '2-digit' }));
        lineas.push('  Total: ' + (filas.length - 1) + ' registro(s)');
        lineas.push('='.repeat(sepTotal));
        lineas.push(formatearFila(cabeceras));
        lineas.push('-'.repeat(sepTotal));

        for (var r = 1; r < filas.length; r++) lineas.push(formatearFila(filas[r]));
        lineas.push('='.repeat(sepTotal));

        var txt = lineas.join('\r\n');
        var blob = new Blob([txt], { type: 'text/plain;charset=utf-8;' });
        var link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.download = this.exportFileName + '_' + new Date().toISOString().slice(0, 10) + '.txt';
        link.click();
        URL.revokeObjectURL(link.href);
    }
};

TablaGenerica._obtenerDatosImpresion = async function() {
    var sesionActual = {};
    try {
        var saved = localStorage.getItem('erp_session') || sessionStorage.getItem('erp_session');
        if (saved) sesionActual = JSON.parse(saved);
    } catch(e) {}
    var empresa = sesionActual.empresa_razon || '';
    var sucursal = '';
    try {
        var s = (sesionActual.sucursales || []).find(function(su) { return su.id_sucursal == sesionActual.id_sucursal; });
        if (s && s.nombre) sucursal = s.nombre;
    } catch(e) {}
    var logo = sesionActual.empresa_logo || '';
    if (!logo && sesionActual.id_empresa) {
        try {
            var r = await fetch('/api/empresas/' + sesionActual.id_empresa + '?id_empresa=' + sesionActual.id_empresa);
            var j = await r.json();
            if (j.ok && j.data && j.data.logo) {
                logo = j.data.logo;
                sesionActual.empresa_logo = logo;
                localStorage.setItem('erp_session', JSON.stringify(sesionActual));
            }
        } catch(e) {}
    }
    var logoHtml = '';
    if (logo) {
        try {
            var resp = await fetch(logo);
            var blob = await resp.blob();
            var logoData = await new Promise(function(resolve) {
                var reader = new FileReader();
                reader.onloadend = function() { resolve(reader.result); };
                reader.readAsDataURL(blob);
            });
            logoHtml = '<div style="display:inline-block;vertical-align:middle;margin-right:10px"><img src="' + logoData + '" style="height:36px;width:auto"></div>';
        } catch(e) {}
    }
    return { empresa: empresa, sucursal: sucursal, logoHtml: logoHtml };
};
