<?php
/**
 * DokuWiki Plugin docmaster (Admin Component)
 *
 * Panel de administración con dos vistas:
 *  - Listado Maestro de Documentos
 *  - Registro de Formatos
 *
 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
 * @author  SIE Systems <soporte@systemsie.site>
 */

class admin_plugin_docmaster extends DokuWiki_Admin_Plugin
{
    /**
     * Acceso restringido a administradores
     */
    public function forAdminOnly()
    {
        return false;
    }

    /**
     * Texto del menú
     */
    public function getMenuText($language)
    {
        return 'Listado Maestro de Documentos';
    }

    /**
     * Orden en el menú de administración
     */
    public function getMenuSort()
    {
        return 50;
    }

    /**
     * Maneja las peticiones del admin
     */
    public function handle()
    {
        // No se requiere procesamiento especial aquí;
        // la lógica se maneja vía AJAX en action.php
    }

    /**
     * Renderiza la interfaz de administración
     */
    public function html()
    {
        /** @var helper_plugin_docmaster $helper */
        $helper = plugin_load('helper', 'docmaster');
        if (!$helper || !$helper->getDB()) {
            echo '<div class="error">' . $this->getLang('db_error') . '</div>';
            return;
        }

        // Obtener datos iniciales
        $stats = $helper->getStats();
        $namespaces = $helper->getNamespaces();
        $documents = $helper->getDocuments(['active_only' => true]);

        echo '<div id="docmaster-admin">';

        // 1. Título
        echo '<h1 class="docmaster-title">' . $this->getLang('admin_title') . '</h1>';

        // 2. Definición
        echo '<p class="docmaster-definition">' . $this->getLang('definition') . '</p>';

        // 3. Tabs de navegación
        $this->_renderTabs();

        // 3.5 Barra de edición masiva (se muestra cuando hay items seleccionados)
        echo '<div id="dm-bulk-toolbar" class="dm-bulk-toolbar" style="display:none;">';
        echo '<div class="dm-bulk-toolbar-inner">';
        echo '<span id="dm-bulk-count" class="dm-bulk-count">&#9745; 0 seleccionados</span>';
        echo '<select id="dm-bulk-field" class="dm-bulk-field">';
        echo '<option value="">-- Cambiar campo --</option>';
        echo '<option value="version">Versión</option>';
        echo '<option value="validity_date">Vigencia</option>';
        echo '<option value="code">Código</option>';
        echo '</select>';
        echo '<input type="text" id="dm-bulk-value" class="dm-bulk-value" placeholder="Nuevo valor..." disabled>';
        echo '<button type="button" id="dm-bulk-apply" class="dm-bulk-apply" disabled>&#10003; Aplicar a seleccionados</button>';
        echo '<button type="button" id="dm-bulk-cancel" class="dm-bulk-cancel">&#10005; Cancelar</button>';
        echo '</div>';
        echo '</div>';

        // 4. Tablas (dentro de cada tab)
        $this->_renderDocumentsTab($documents, $namespaces);
        $this->_renderFormatsTab();

        // 5-6. Botón Escanear + Stats inline
        $this->_renderScanAndStats($stats);

        // 7. Ubicaciones con documentos registrados
        $this->_renderLocations($documents, $namespaces);

        echo '</div>'; // #docmaster-admin

        // JavaScript inline - no depende del sistema de carga de scripts de DokuWiki
        $this->_renderInlineScript();
    }

    /**
     * JavaScript embebido directamente en la página.
     * Esto garantiza que funcione sin depender de js.php o caché.
     */
    protected function _renderInlineScript()
    {
        echo '<script>
(function(){
    "use strict";
    var wrap = document.getElementById("docmaster-admin");
    if (!wrap) return;

    var AJAX_URL = DOKU_BASE + "lib/exe/ajax.php";

    // === TABS ===
    var tabs = wrap.querySelectorAll(".docmaster-tab");
    tabs.forEach(function(tab){
        tab.addEventListener("click", function(){
            var target = this.getAttribute("data-tab");
            tabs.forEach(function(t){ t.classList.remove("active"); });
            wrap.querySelectorAll(".docmaster-tab-content").forEach(function(c){
                c.classList.remove("active"); c.style.display = "none";
            });
            this.classList.add("active");
            var content = document.getElementById("dm-tab-" + target);
            if (content){ content.classList.add("active"); content.style.display = "block"; }
            if (target === "formats") loadFormats();
            // Reset selecciones masivas al cambiar de tab
            clearBulkSelections();
        });
    });

    // === FILTROS ===
    var nsFilter = document.getElementById("dm-filter-ns");
    var versionFilter = document.getElementById("dm-filter-version");
    var dateFrom = document.getElementById("dm-filter-date-from");
    var dateTo = document.getElementById("dm-filter-date-to");
    var dateClear = document.getElementById("dm-filter-date-clear");
    var searchFilter = document.getElementById("dm-filter-search");
    var fmtSearch = document.getElementById("dm-filter-format-search");
    var filterTimer = null;

    if (nsFilter) nsFilter.addEventListener("change", filterDocs);
    if (versionFilter) versionFilter.addEventListener("change", filterDocs);
    if (searchFilter) searchFilter.addEventListener("input", function(){
        clearTimeout(filterTimer); filterTimer = setTimeout(filterDocs, 300);
    });
    if (fmtSearch) fmtSearch.addEventListener("input", function(){
        clearTimeout(filterTimer); filterTimer = setTimeout(filterFmts, 300);
    });

    // Filtro de fechas: auto-formato y filtrado al escribir
    [dateFrom, dateTo].forEach(function(input){
        if (!input) return;
        input.addEventListener("input", function(){
            autoFormatDate(this);
            clearTimeout(filterTimer);
            filterTimer = setTimeout(filterDocs, 500);
        });
        input.addEventListener("change", filterDocs);
    });
    if (dateClear) dateClear.addEventListener("click", function(){
        if (dateFrom) dateFrom.value = "";
        if (dateTo) dateTo.value = "";
        filterDocs();
    });

    // Auto-formato: inserta / al escribir dd/mm/aaaa
    function autoFormatDate(input){
        var v = input.value.replace(/[^\d\/]/g, "");
        var digits = v.replace(/\//g, "");
        if (digits.length > 8) digits = digits.substring(0, 8);
        var formatted = "";
        for (var i = 0; i < digits.length; i++){
            if (i === 2 || i === 4) formatted += "/";
            formatted += digits[i];
        }
        input.value = formatted;
    }

    // Convierte dd/mm/yyyy a número comparable (yyyymmdd)
    function dateToNum(dateStr){
        if (!dateStr) return 0;
        // Soporta dd/mm/yyyy y dd-mm-yyyy y dd.mm.yyyy
        var parts = dateStr.split(/[\/\-\.]/);
        if (parts.length !== 3) return 0;
        var d = parseInt(parts[0], 10);
        var m = parseInt(parts[1], 10);
        var y = parseInt(parts[2], 10);
        if (isNaN(d) || isNaN(m) || isNaN(y)) return 0;
        if (y < 100) y += 2000;
        return y * 10000 + m * 100 + d;
    }

    function filterDocs(){
        var ns = document.getElementById("dm-filter-ns").value;
        var ver = document.getElementById("dm-filter-version").value;
        var dFrom = dateFrom ? dateFrom.value.trim() : "";
        var dTo = dateTo ? dateTo.value.trim() : "";
        var numFrom = dateToNum(dFrom);
        var numTo = dateToNum(dTo);
        var search = document.getElementById("dm-filter-search").value.toLowerCase();
        var rows = document.querySelectorAll("#dm-documents-body > tr:not(.dm-formats-row)");
        var visible = 0;
        rows.forEach(function(row){
            var rowNs = row.getAttribute("data-ns") || "";
            var rowVer = row.getAttribute("data-version") || "0";
            var rowValidity = row.getAttribute("data-validity") || "";
            var text = row.textContent.toLowerCase();

            var show = true;

            // Filtro ubicación
            if (ns && rowNs !== ns) show = false;

            // Filtro versión
            if (show && ver && rowVer !== ver) show = false;

            // Filtro vigencia (rango de fechas)
            if (show && (numFrom > 0 || numTo > 0)) {
                var rowDate = dateToNum(rowValidity);
                if (rowDate === 0) {
                    show = false; // Sin fecha = no coincide con rango
                } else {
                    if (numFrom > 0 && rowDate < numFrom) show = false;
                    if (numTo > 0 && rowDate > numTo) show = false;
                }
            }

            // Filtro texto
            if (show && search && text.indexOf(search) === -1) show = false;

            row.style.display = show ? "" : "none";
            if (show) visible++;
            if (!show){
                var did = row.getAttribute("data-doc-id");
                var fr = document.querySelector(".dm-formats-row[data-doc-id=\"" + did + "\"]");
                if (fr) fr.style.display = "none";
            }
        });
        var nr = document.getElementById("dm-no-results");
        if (nr) nr.style.display = visible === 0 ? "block" : "none";

        // Actualizar contador visible
        var counter = document.getElementById("dm-filter-count");
        if (counter) counter.textContent = visible;
        // Desmarcar filas ocultas por filtro
        var docSelAll = document.getElementById("dm-doc-select-all");
        if (docSelAll) docSelAll.checked = false;
        wrap.querySelectorAll(".dm-doc-check").forEach(function(cb){
            var r = cb.closest("tr");
            if (r && r.style.display === "none"){ cb.checked = false; r.classList.remove("dm-selected"); }
        });
        if (typeof updateBulkToolbar === "function") updateBulkToolbar();
    }

    // === ORDENAMIENTO ===
    var currentSort = {field:"", dir:"asc"};
    wrap.querySelectorAll(".sortable").forEach(function(th){
        th.addEventListener("click", function(){
            var field = this.getAttribute("data-sort");
            if (!field) return;
            if (currentSort.field === field) currentSort.dir = currentSort.dir === "asc" ? "desc" : "asc";
            else { currentSort.field = field; currentSort.dir = "asc"; }
            wrap.querySelectorAll(".sortable").forEach(function(h){ h.classList.remove("sort-asc","sort-desc"); });
            this.classList.add("sort-" + currentSort.dir);
            sortTable(field, currentSort.dir);
        });
    });

    function sortTable(field, dir){
        var tbody = document.getElementById("dm-documents-body");
        if (!tbody) return;
        var pairs = [];
        tbody.querySelectorAll("tr:not(.dm-formats-row)").forEach(function(row){
            var did = row.getAttribute("data-doc-id");
            var fr = tbody.querySelector(".dm-formats-row[data-doc-id=\"" + did + "\"]");
            var v = "";
            if (field === "title"){ var a = row.querySelector(".dm-col-title a"); v = a ? a.textContent : ""; }
            else if (field === "namespace") v = row.getAttribute("data-ns") || "";
            else { var c = row.querySelector("[data-field=\"" + field + "\"]"); v = c ? (c.getAttribute("data-value") || "") : ""; }
            pairs.push({row:row, fr:fr, v:v});
        });
        pairs.sort(function(a,b){
            var va = a.v.toLowerCase(), vb = b.v.toLowerCase();
            var na = parseFloat(va), nb = parseFloat(vb);
            if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na-nb : nb-na;
            if (va < vb) return dir === "asc" ? -1 : 1;
            if (va > vb) return dir === "asc" ? 1 : -1;
            return 0;
        });
        pairs.forEach(function(p){ tbody.appendChild(p.row); if (p.fr) tbody.appendChild(p.fr); });
    }

    // === EDICIÓN INLINE ===
    var docTable = document.getElementById("dm-documents-table");
    var fmtTable = document.getElementById("dm-formats-table");
    if (docTable) docTable.addEventListener("dblclick", function(e){
        var cell = e.target.closest(".dm-editable");
        if (cell && !cell.classList.contains("editing")) startEdit(cell, "plugin_docmaster_update");
    });
    if (fmtTable) fmtTable.addEventListener("dblclick", function(e){
        var cell = e.target.closest(".dm-fmt-editable");
        if (cell && !cell.classList.contains("editing")) startEdit(cell, "plugin_docmaster_update_format");
    });

    function startEdit(cell, ajaxCall){
        var field = cell.getAttribute("data-field");
        var curVal = cell.getAttribute("data-value") || "";
        var row = cell.closest("tr");
        var id = row.getAttribute("data-doc-id") || row.getAttribute("data-fmt-id");
        cell.classList.add("editing");
        var span = cell.querySelector(".dm-cell-value");
        if (span) span.style.display = "none";
        var input = document.createElement("input");
        input.type = "text"; input.className = "dm-edit-input"; input.value = curVal;
        cell.appendChild(input); input.focus(); input.select();
        function save(){
            var nv = input.value.trim();
            cell.classList.remove("editing");
            if (input.parentNode) input.parentNode.removeChild(input);
            if (span) span.style.display = "";
            if (nv !== curVal){
                cell.classList.add("saving");
                ajaxPost(ajaxCall, {id:id, field:field, value:nv}, function(d){
                    cell.classList.remove("saving");
                    if (d.success){
                        cell.setAttribute("data-value", nv); if (span) span.textContent = nv;
                        var isDoc = (ajaxCall === "plugin_docmaster_update");
                        if (field === "version") row.setAttribute(isDoc ? "data-version" : "data-fmt-version", nv);
                        if (field === "validity_date") row.setAttribute(isDoc ? "data-validity" : "data-fmt-validity", nv);
                        // Re-detectar duplicados si se editó el campo código
                        if (field === "code" && typeof detectDuplicates === "function") detectDuplicates();
                    }
                    else { if (span) span.textContent = curVal; }
                }, function(){ cell.classList.remove("saving"); if (span) span.textContent = curVal; });
            }
        }
        input.addEventListener("keydown", function(e){
            if (e.key === "Enter"){ e.preventDefault(); save(); }
            else if (e.key === "Escape"){ e.preventDefault(); cell.classList.remove("editing"); if (input.parentNode) input.parentNode.removeChild(input); if (span) span.style.display = ""; }
        });
        input.addEventListener("blur", function(){ setTimeout(save, 150); });
    }

    // === EXPANDIR FORMATOS ===
    if (docTable) docTable.addEventListener("click", function(e){
        var btn = e.target.closest(".dm-btn-formats");
        if (!btn) return;
        var did = btn.getAttribute("data-doc-id");
        var fr = document.querySelector(".dm-formats-row[data-doc-id=\"" + did + "\"]");
        if (!fr) return;
        if (fr.style.display === "none" || !fr.style.display){
            fr.style.display = "";
            var container = fr.querySelector(".dm-formats-list");
            container.innerHTML = "<em>Cargando...</em>";
            ajaxPost("plugin_docmaster_doc_formats", {doc_id: did}, function(d){
                if (!d.formats || d.formats.length === 0){ container.innerHTML = "<em>Sin formatos</em>"; return; }
                var h = "<table><tr><th>Nombre</th><th>Archivo</th><th>Código</th><th>Versión</th></tr>";
                d.formats.forEach(function(f){
                    var u = DOKU_BASE + "lib/exe/fetch.php?media=" + encodeURIComponent(f.media_id);
                    h += "<tr><td>" + esc(f.display_name||"-") + "</td><td><a href=\"" + u + "\" target=\"_blank\">" + esc(f.media_id) + "</a></td><td>" + esc(f.code||"-") + "</td><td>" + (f.version||"-") + "</td></tr>";
                });
                container.innerHTML = h + "</table>";
            }, function(){ container.innerHTML = "<em style=\"color:#dc2626;\">Error</em>"; });
        } else { fr.style.display = "none"; }
    });

    // === DROPDOWN ACCIÓN FORMATOS ===
    // Delegación de eventos para botones con múltiples procedimientos
    if (fmtTable) fmtTable.addEventListener("click", function(e){
        var btn = e.target.closest(".dm-btn-edit-multi");
        if (!btn) return;
        e.preventDefault();
        e.stopPropagation();
        var menu = btn.nextElementSibling;
        if (!menu || !menu.classList.contains("dm-fmt-action-menu")) return;
        // Cerrar otros menus abiertos
        wrap.querySelectorAll(".dm-fmt-action-menu.open").forEach(function(m){
            if (m !== menu) m.classList.remove("open");
        });
        menu.classList.toggle("open");
    });
    // Cerrar menú al hacer click fuera
    document.addEventListener("click", function(e){
        if (!e.target.closest(".dm-fmt-action-wrap")){
            wrap.querySelectorAll(".dm-fmt-action-menu.open").forEach(function(m){
                m.classList.remove("open");
            });
        }
    });

    // === CARGAR FORMATOS (TAB) ===
    var fmtData = []; // Cache de formatos cargados

    function loadFormats(){
        var tbody = document.getElementById("dm-formats-body");
        if (!tbody) return;
        tbody.innerHTML = "<tr><td colspan=\"9\">Cargando formatos...</td></tr>";
        ajaxPost("plugin_docmaster_formats", {}, function(d){
            if (!d.formats || d.formats.length === 0){ tbody.innerHTML = "<tr><td colspan=\"9\">No se encontraron formatos</td></tr>"; return; }
            fmtData = d.formats;
            renderFormats(d.formats);
            populateFmtFilters(d.formats);
            filterFmts();
        }, function(){ tbody.innerHTML = "<tr><td colspan=\"9\" style=\"color:#dc2626;\">Error al cargar</td></tr>"; });
    }

    function renderFormats(formats){
        var tbody = document.getElementById("dm-formats-body");
        if (!tbody) return;
        var h = "";
        formats.forEach(function(f){
            var dc = f.document_count || 0;
            var sh = dc > 1 ? " style=\"font-weight:600;color:#2563eb;\"" : "";
            var u = DOKU_BASE + "lib/exe/fetch.php?media=" + encodeURIComponent(f.media_id);
            // Documentos relacionados (procedimientos)
            var titles = f.document_titles ? f.document_titles.split("||") : [];
            var nss = f.document_namespaces ? f.document_namespaces.split("||") : [];
            var pageIds = f.document_page_ids ? f.document_page_ids.split("||") : [];
            var procHtml = titles.length > 0 ? titles.map(function(t, idx){
                var pid = pageIds[idx] || "";
                if (pid) {
                    var pUrl = DOKU_BASE + "doku.php?id=" + encodeURIComponent(pid);
                    return "<a href=\"" + pUrl + "\" title=\"" + escA(pid) + "\">" + esc(t) + "</a>";
                }
                return esc(t);
            }).join("<br>") : "<em style=\"color:#94a3b8;\">-</em>";
            var firstNs = nss.length > 0 ? nss[0] : "";
            var firstProc = titles.length > 0 ? titles[0] : "";

            h += "<tr data-fmt-id=\"" + f.id + "\" data-fmt-ns=\"" + escA(firstNs) + "\" data-fmt-proc=\"" + escA(firstProc) + "\" data-fmt-version=\"" + (f.version||"0") + "\" data-fmt-validity=\"" + escA(f.validity_date||"") + "\" data-fmt-nss=\"" + escA(nss.join("||")) + "\" data-fmt-procs=\"" + escA(titles.join("||")) + "\">";
            h += "<td class=\"dm-col-check\"><input type=\"checkbox\" class=\"dm-fmt-check\" value=\"" + f.id + "\"></td>";
            h += "<td class=\"dm-fmt-editable\" data-field=\"display_name\" data-value=\"" + escA(f.display_name||"") + "\"><span class=\"dm-cell-value\">" + esc(f.display_name||"-") + "</span></td>";
            h += "<td><a href=\"" + u + "\" target=\"_blank\">" + esc(f.media_id) + "</a></td>";
            h += "<td class=\"dm-col-fproc\">" + procHtml + "</td>";
            h += "<td class=\"dm-fmt-editable\" data-field=\"code\" data-value=\"" + escA(f.code||"") + "\"><span class=\"dm-cell-value\">" + esc(f.code||"-") + "</span></td>";
            h += "<td class=\"dm-fmt-editable\" data-field=\"version\" data-value=\"" + (f.version||"") + "\"><span class=\"dm-cell-value\">" + (f.version||"-") + "</span></td>";
            h += "<td class=\"dm-fmt-editable\" data-field=\"validity_date\" data-value=\"" + escA(f.validity_date||"") + "\"><span class=\"dm-cell-value\">" + esc(f.validity_date||"-") + "</span></td>";
            h += "<td" + sh + ">" + dc + "</td>";

            // Columna Acción: editar procedimiento donde está el formato + botón fix duplicado
            h += "<td class=\"dm-col-factions\">";
            if (pageIds.length === 0) {
                h += "<span class=\"dm-no-formats\">-</span>";
            } else if (pageIds.length === 1) {
                var editUrl = DOKU_BASE + "doku.php?id=" + encodeURIComponent(pageIds[0]) + "&do=edit";
                h += "<a href=\"" + editUrl + "\" class=\"dm-btn-edit\" title=\"Editar " + escA(titles[0]||pageIds[0]) + "\">&#9998;</a>";
            } else {
                h += "<div class=\"dm-fmt-action-wrap\">";
                h += "<button class=\"dm-btn-edit dm-btn-edit-multi\" title=\"Editar procedimiento\">&#9998;</button>";
                h += "<div class=\"dm-fmt-action-menu\">";
                for (var pi = 0; pi < pageIds.length; pi++) {
                    var pUrl = DOKU_BASE + "doku.php?id=" + encodeURIComponent(pageIds[pi]) + "&do=edit";
                    var pTitle = titles[pi] || pageIds[pi];
                    h += "<a href=\"" + pUrl + "\" class=\"dm-fmt-action-item\" title=\"" + escA(pageIds[pi]) + "\">" + esc(pTitle) + "</a>";
                }
                h += "</div></div>";
            }
            // Botón sincronizar formato al procedimiento (actualiza link en la wiki)
            if (pageIds.length > 0) {
                h += " <button class=\"dm-btn-sync-fmt\" data-fmt-id=\"" + f.id + "\" title=\"Sincronizar datos al procedimiento\">&#128221;</button>";
            }
            // Botón fix duplicado (oculto por defecto, mostrado por detectDuplicates)
            h += " <button class=\"dm-btn-fix-dup\" data-fmt-id=\"" + f.id + "\" title=\"Corregir código duplicado\" style=\"display:none;\">&#128260;</button>";
            h += "</td>";

            h += "</tr>";
        });
        tbody.innerHTML = h;
        // Re-aplicar resaltado de duplicados después de renderizar formatos
        applyDuplicateHighlight();
    }

    // Llenar selects dinámicos del tab formatos
    function populateFmtFilters(formats){
        var nsSet = {}, procSet = {}, verSet = {};
        formats.forEach(function(f){
            if (f.document_namespaces){
                f.document_namespaces.split("||").forEach(function(n){ if (n) nsSet[n] = true; });
            }
            if (f.document_titles){
                f.document_titles.split("||").forEach(function(t){ if (t) procSet[t] = true; });
            }
            var v = f.version || 0;
            if (v > 0) verSet[v] = true;
        });
        fillSelect("dm-fmt-filter-ns", nsSet, "Todas las ubicaciones");
        fillSelect("dm-fmt-filter-proc", procSet, "Todos los procedimientos");
        fillSelect("dm-fmt-filter-version", verSet, "Todas las versiones", true);
    }

    function fillSelect(id, items, defaultText, isNum){
        var sel = document.getElementById(id);
        if (!sel) return;
        var current = sel.value;
        sel.innerHTML = "";
        var opt0 = document.createElement("option"); opt0.value = ""; opt0.textContent = defaultText; sel.appendChild(opt0);
        var keys = Object.keys(items);
        if (isNum) keys.sort(function(a,b){ return parseInt(a)-parseInt(b); });
        else keys.sort();
        keys.forEach(function(k){
            var opt = document.createElement("option"); opt.value = k;
            opt.textContent = isNum ? "Versión " + k : k;
            sel.appendChild(opt);
        });
        if (current) sel.value = current;
    }

    // === FILTROS FORMATOS ===
    var fmtNsFilter = document.getElementById("dm-fmt-filter-ns");
    var fmtProcFilter = document.getElementById("dm-fmt-filter-proc");
    var fmtVerFilter = document.getElementById("dm-fmt-filter-version");
    var fmtDateFrom = document.getElementById("dm-fmt-filter-date-from");
    var fmtDateTo = document.getElementById("dm-fmt-filter-date-to");
    var fmtDateClear = document.getElementById("dm-fmt-filter-date-clear");

    if (fmtNsFilter) fmtNsFilter.addEventListener("change", filterFmts);
    if (fmtProcFilter) fmtProcFilter.addEventListener("change", filterFmts);
    if (fmtVerFilter) fmtVerFilter.addEventListener("change", filterFmts);
    [fmtDateFrom, fmtDateTo].forEach(function(input){
        if (!input) return;
        input.addEventListener("input", function(){
            autoFormatDate(this);
            clearTimeout(filterTimer);
            filterTimer = setTimeout(filterFmts, 500);
        });
        input.addEventListener("change", filterFmts);
    });
    if (fmtDateClear) fmtDateClear.addEventListener("click", function(){
        if (fmtDateFrom) fmtDateFrom.value = "";
        if (fmtDateTo) fmtDateTo.value = "";
        filterFmts();
    });

    function filterFmts(){
        var fns = fmtNsFilter ? fmtNsFilter.value : "";
        var fproc = fmtProcFilter ? fmtProcFilter.value : "";
        var fver = fmtVerFilter ? fmtVerFilter.value : "";
        var dfrom = fmtDateFrom ? fmtDateFrom.value.trim() : "";
        var dto = fmtDateTo ? fmtDateTo.value.trim() : "";
        var numFrom = dateToNum(dfrom);
        var numTo = dateToNum(dto);
        var fsearch = fmtSearch ? fmtSearch.value.toLowerCase().trim() : "";
        var rows = document.querySelectorAll("#dm-formats-body > tr");
        var visible = 0;
        rows.forEach(function(row){
            if (!row.hasAttribute("data-fmt-id")) return;
            var rowNss = row.getAttribute("data-fmt-nss") || "";
            var rowProcs = row.getAttribute("data-fmt-procs") || "";
            var rowVer = row.getAttribute("data-fmt-version") || "0";
            var rowVal = row.getAttribute("data-fmt-validity") || "";
            var show = true;

            // Filtro ubicación: el formato pertenece a algún doc de ese namespace
            if (fns && rowNss.indexOf(fns) === -1) show = false;

            // Filtro procedimiento
            if (show && fproc && rowProcs.indexOf(fproc) === -1) show = false;

            // Filtro versión
            if (show && fver && rowVer !== fver) show = false;

            // Filtro vigencia
            if (show && (numFrom > 0 || numTo > 0)){
                var rd = dateToNum(rowVal);
                if (rd === 0) show = false;
                else {
                    if (numFrom > 0 && rd < numFrom) show = false;
                    if (numTo > 0 && rd > numTo) show = false;
                }
            }

            // Filtro búsqueda texto
            if (show && fsearch && row.textContent.toLowerCase().indexOf(fsearch) === -1) show = false;

            row.style.display = show ? "" : "none";
            if (show) visible++;
        });
        // Desmarcar filas ocultas por filtro
        var fmtSelAll = document.getElementById("dm-fmt-select-all");
        if (fmtSelAll) fmtSelAll.checked = false;
        wrap.querySelectorAll(".dm-fmt-check").forEach(function(cb){
            var r = cb.closest("tr");
            if (r && r.style.display === "none"){ cb.checked = false; r.classList.remove("dm-selected"); }
        });
        if (typeof updateBulkToolbar === "function") updateBulkToolbar();
    }

    // === BOTÓN ESCANEAR ===
    var scanBtn = document.getElementById("dm-btn-scan");
    if (scanBtn) {
        scanBtn.addEventListener("click", function(){
            if (scanBtn.disabled) return;
            if (!confirm("¿Escanear todas las páginas de las ubicaciones calificadas?\\nEsto puede tomar algunos minutos.")) return;

            scanBtn.disabled = true;
            scanBtn.textContent = "Escaneando...";
            var status = document.getElementById("dm-scan-status");
            if (status){ status.textContent = "Procesando..."; status.className = "docmaster-scan-status"; }

            ajaxPost("plugin_docmaster_scan", {}, function(data){
                scanBtn.disabled = false;
                scanBtn.textContent = "Escanear Documentación";
                var cv = data._v || 0;

                if (data.success){
                    if (status){
                        var msg = "Completado: " + data.processed + " procesados, " + data.skipped + " omitidos, " + data.errors + " errores";
                        if (data.removed > 0) msg += ", " + data.removed + " eliminados (excluidos)";
                        if (data.files_found !== undefined) msg += "\\n" + data.files_found + " archivos en " + data.dirs_found + "/" + data.dirs_checked + " ubicaciones";
                        status.innerText = msg;
                        status.className = "docmaster-scan-status " + (data.processed > 0 ? "success" : "warning");
                    }
                    if (data.skipped_pages && data.skipped_pages.length > 0) console.log("Omitidas:", data.skipped_pages);
                    if (data.processed > 0) setTimeout(function(){ window.location.reload(); }, 4000);
                } else {
                    if (status){
                        status.innerText = "Error: " + (data.error || "desconocido") + " (v" + cv + ")";
                        status.className = "docmaster-scan-status error";
                    }
                }
            }, function(err){
                scanBtn.disabled = false;
                scanBtn.textContent = "Escanear Documentación";
                if (status){
                    var m = "Error de conexión";
                    if (typeof err === "number") m += " (HTTP " + err + ")";
                    status.innerText = m;
                    status.className = "docmaster-scan-status error";
                }
            });
        });
    }

    // === DETECCIÓN DE CÓDIGOS DUPLICADOS ===
    var dupData = {documents:{}, formats:{}};

    function detectDuplicates(){
        ajaxPost("plugin_docmaster_duplicates", {}, function(d){
            dupData = d;
            applyDuplicateHighlight();
        });
    }

    function applyDuplicateHighlight(){
        // Documentos
        var docRows = document.querySelectorAll("#dm-documents-body > tr:not(.dm-formats-row)");
        docRows.forEach(function(row){
            var codeCell = row.querySelector("[data-field=\'code\']");
            var code = codeCell ? (codeCell.getAttribute("data-value") || "").trim() : "";
            var isDup = code && dupData.documents && dupData.documents[code];
            if (isDup) {
                row.classList.add("dm-duplicate");
                var fixBtn = row.querySelector(".dm-btn-fix-dup");
                if (fixBtn) fixBtn.style.display = "";
            } else {
                row.classList.remove("dm-duplicate");
                var fixBtn = row.querySelector(".dm-btn-fix-dup");
                if (fixBtn) fixBtn.style.display = "none";
            }
        });
        // Formatos
        var fmtRows = document.querySelectorAll("#dm-formats-body > tr");
        fmtRows.forEach(function(row){
            var codeCell = row.querySelector("[data-field=\'code\']");
            var code = codeCell ? (codeCell.getAttribute("data-value") || "").trim() : "";
            var isDup = code && dupData.formats && dupData.formats[code];
            if (isDup) {
                row.classList.add("dm-duplicate");
                // Mostrar botón fix si existe
                var fixBtn = row.querySelector(".dm-btn-fix-dup");
                if (fixBtn) fixBtn.style.display = "";
            } else {
                row.classList.remove("dm-duplicate");
                var fixBtn = row.querySelector(".dm-btn-fix-dup");
                if (fixBtn) fixBtn.style.display = "none";
            }
        });
    }

    // Detectar al cargar la página
    detectDuplicates();

    // Handler: corregir duplicado en documentos
    if (docTable) docTable.addEventListener("click", function(e){
        var fixBtn = e.target.closest(".dm-btn-fix-dup");
        if (!fixBtn) return;
        e.preventDefault();
        var row = fixBtn.closest("tr");
        var id = row.getAttribute("data-doc-id");
        var codeCell = row.querySelector("[data-field=\'code\']");
        var currentCode = codeCell ? codeCell.getAttribute("data-value") : "";
        if (!confirm("¿Corregir código duplicado [" + currentCode + "]?\\nSe asignará el siguiente consecutivo disponible.")) return;
        fixBtn.disabled = true;
        fixBtn.textContent = "...";
        ajaxPost("plugin_docmaster_fix_duplicate", {id: id, type: "document"}, function(d){
            fixBtn.disabled = false;
            fixBtn.innerHTML = "&#128260;";
            if (d.success){
                // Actualizar UI
                if (codeCell){
                    codeCell.setAttribute("data-value", d.new_code);
                    var span = codeCell.querySelector(".dm-cell-value");
                    if (span) span.textContent = d.new_code;
                }
                detectDuplicates();
                alert("Código corregido: " + d.old_code + " → " + d.new_code);
            } else {
                alert("Error: " + (d.error || "No se pudo corregir"));
            }
        }, function(){
            fixBtn.disabled = false;
            fixBtn.innerHTML = "&#128260;";
            alert("Error de conexión al corregir duplicado");
        });
    });

    // Handler: corregir duplicado en formatos
    if (fmtTable) fmtTable.addEventListener("click", function(e){
        var fixBtn = e.target.closest(".dm-btn-fix-dup");
        if (!fixBtn) return;
        e.preventDefault();
        e.stopPropagation();
        var row = fixBtn.closest("tr");
        var id = row.getAttribute("data-fmt-id");
        var codeCell = row.querySelector("[data-field=\'code\']");
        var currentCode = codeCell ? codeCell.getAttribute("data-value") : "";
        if (!confirm("¿Corregir código duplicado [" + currentCode + "]?\\nSe asignará el siguiente consecutivo disponible.")) return;
        fixBtn.disabled = true;
        fixBtn.textContent = "...";
        ajaxPost("plugin_docmaster_fix_duplicate", {id: id, type: "format"}, function(d){
            fixBtn.disabled = false;
            fixBtn.innerHTML = "&#128260;";
            if (d.success){
                if (codeCell){
                    codeCell.setAttribute("data-value", d.new_code);
                    var span = codeCell.querySelector(".dm-cell-value");
                    if (span) span.textContent = d.new_code;
                }
                detectDuplicates();
                alert("Código corregido: " + d.old_code + " → " + d.new_code);
            } else {
                alert("Error: " + (d.error || "No se pudo corregir"));
            }
        }, function(){
            fixBtn.disabled = false;
            fixBtn.innerHTML = "&#128260;";
            alert("Error de conexión al corregir duplicado");
        });
    });

    // Handler: sincronizar formato al procedimiento (actualizar link en wiki)
    if (fmtTable) fmtTable.addEventListener("click", function(e){
        var syncBtn = e.target.closest(".dm-btn-sync-fmt");
        if (!syncBtn) return;
        e.preventDefault();
        e.stopPropagation();
        var row = syncBtn.closest("tr");
        var id = row.getAttribute("data-fmt-id");
        if (!confirm("¿Sincronizar los datos de este formato (código, versión, vigencia) al procedimiento?")) return;
        syncBtn.disabled = true;
        syncBtn.textContent = "...";
        ajaxPost("plugin_docmaster_sync_format", {id: id}, function(d){
            syncBtn.disabled = false;
            syncBtn.innerHTML = "&#128221;";
            if (d.success){
                var msg = "Formato sincronizado en " + d.updated_pages + " página(s)";
                if (d.errors && d.errors.length > 0) msg += "\nErrores:\n" + d.errors.join("\n");
                if (d.debug && d.debug.length > 0) msg += "\n\nDiagnóstico:\n" + d.debug.join("\n");
                alert(msg);
            } else {
                var errMsg = "Error: " + (d.error || "No se pudo sincronizar");
                if (d.errors && d.errors.length > 0) errMsg += "\n" + d.errors.join("\n");
                if (d.debug && d.debug.length > 0) errMsg += "\n\nDiagnóstico:\n" + d.debug.join("\n");
                alert(errMsg);
            }
        }, function(){
            syncBtn.disabled = false;
            syncBtn.innerHTML = "&#128221;";
            alert("Error de conexión");
        });
    });

    // === EDICIÓN MASIVA (BULK EDIT) ===
    var bulkToolbar = document.getElementById("dm-bulk-toolbar");
    var bulkCount   = document.getElementById("dm-bulk-count");
    var bulkField   = document.getElementById("dm-bulk-field");
    var bulkValue   = document.getElementById("dm-bulk-value");
    var bulkApply   = document.getElementById("dm-bulk-apply");
    var bulkCancel  = document.getElementById("dm-bulk-cancel");

    function getActiveTab(){
        var activeTab = wrap.querySelector(".docmaster-tab.active");
        return activeTab ? activeTab.getAttribute("data-tab") : "documents";
    }

    function getCheckboxClass(){ return getActiveTab() === "formats" ? "dm-fmt-check" : "dm-doc-check"; }
    function getSelectAllId(){ return getActiveTab() === "formats" ? "dm-fmt-select-all" : "dm-doc-select-all"; }

    function getSelectedIds(){
        var cls = getCheckboxClass();
        var ids = [];
        wrap.querySelectorAll("." + cls + ":checked").forEach(function(cb){
            var row = cb.closest("tr");
            if (row && row.style.display !== "none") ids.push(cb.value);
        });
        return ids;
    }

    function updateBulkToolbar(){
        if (!bulkToolbar) { console.warn("dm-bulk-toolbar not found"); return; }
        var ids = getSelectedIds();
        var n = ids.length;
        if (n > 0){
            bulkToolbar.style.display = "block";
            bulkCount.innerHTML = "&#9745; " + n + " seleccionado" + (n > 1 ? "s" : "");
        } else {
            bulkToolbar.style.display = "none";
        }
    }

    function clearBulkSelections(){
        wrap.querySelectorAll(".dm-doc-check:checked, .dm-fmt-check:checked").forEach(function(cb){
            cb.checked = false;
            var row = cb.closest("tr");
            if (row) row.classList.remove("dm-selected");
        });
        var dsa = document.getElementById("dm-doc-select-all");
        var fsa = document.getElementById("dm-fmt-select-all");
        if (dsa) dsa.checked = false;
        if (fsa) fsa.checked = false;
        if (bulkField) bulkField.value = "";
        if (bulkValue){ bulkValue.value = ""; bulkValue.disabled = true; }
        if (bulkApply) bulkApply.disabled = true;
        if (bulkToolbar) { bulkToolbar.style.display = "none"; }
    }

    // Select All: solo marca filas VISIBLES
    function handleSelectAll(e){
        var checked = e.target.checked;
        var cls = getCheckboxClass();
        wrap.querySelectorAll("." + cls).forEach(function(cb){
            var row = cb.closest("tr");
            if (row && row.style.display !== "none"){
                cb.checked = checked;
                row.classList.toggle("dm-selected", checked);
            }
        });
        updateBulkToolbar();
    }

    // Checkbox individual
    function handleCheckboxChange(e){
        if (!e.target.classList.contains("dm-doc-check") && !e.target.classList.contains("dm-fmt-check")) return;
        var row = e.target.closest("tr");
        if (row) row.classList.toggle("dm-selected", e.target.checked);
        // Actualizar estado del select-all
        var cls = getCheckboxClass();
        var allId = getSelectAllId();
        var selAll = document.getElementById(allId);
        if (selAll){
            var visibleArr = [];
            wrap.querySelectorAll("." + cls).forEach(function(cb){
                var r = cb.closest("tr");
                if (r && r.style.display !== "none") visibleArr.push(cb);
            });
            selAll.checked = visibleArr.length > 0 && visibleArr.every(function(cb){ return cb.checked; });
        }
        updateBulkToolbar();
    }

    // Listeners en tablas (delegación)
    var docSelAllCb = document.getElementById("dm-doc-select-all");
    if (docSelAllCb) docSelAllCb.addEventListener("change", handleSelectAll);
    if (docTable) docTable.addEventListener("change", handleCheckboxChange);
    if (fmtTable) fmtTable.addEventListener("change", function(e){
        if (e.target.id === "dm-fmt-select-all") handleSelectAll(e);
        else handleCheckboxChange(e);
    });

    // Campo: habilitar input al seleccionar campo
    if (bulkField) bulkField.addEventListener("change", function(){
        var hasField = bulkField.value !== "";
        bulkValue.disabled = !hasField;
        bulkApply.disabled = !hasField;
        if (hasField && bulkField.value === "validity_date"){
            bulkValue.placeholder = "dd/mm/aaaa";
            bulkValue.maxLength = 10;
        } else if (hasField && bulkField.value === "version"){
            bulkValue.placeholder = "Ej: 3";
            bulkValue.maxLength = 5;
        } else {
            bulkValue.placeholder = "Nuevo valor...";
            bulkValue.maxLength = 100;
        }
        bulkValue.value = "";
        bulkValue.focus();
    });

    // Autoformato fecha en input masivo
    if (bulkValue) bulkValue.addEventListener("input", function(){
        if (bulkField && bulkField.value === "validity_date") autoFormatDate(bulkValue);
    });

    // Cancelar: desmarcar todo
    if (bulkCancel) bulkCancel.addEventListener("click", function(){ clearBulkSelections(); });

    // Aplicar masivamente
    if (bulkApply) bulkApply.addEventListener("click", function(){
        var ids = getSelectedIds();
        var field = bulkField.value;
        var value = bulkValue.value.trim();
        if (ids.length === 0 || !field) return;

        var tab = getActiveTab();
        var typeLabel = tab === "formats" ? "formato(s)" : "documento(s)";
        var msg = "¿Aplicar " + field + " = [" + value + "] a " + ids.length + " " + typeLabel + "?";
        if (field === "code") msg += "\\nNota: si todos reciben el mismo código, se marcarán como duplicados.";
        if (!confirm(msg)) return;

        var ajaxCall = tab === "formats" ? "plugin_docmaster_bulk_update_format" : "plugin_docmaster_bulk_update";
        bulkApply.disabled = true;
        bulkApply.textContent = "Aplicando...";

        ajaxPost(ajaxCall, { ids: JSON.stringify(ids), field: field, value: value }, function(d){
            bulkApply.disabled = false;
            bulkApply.textContent = "Aplicar a seleccionados";
            if (d.success){
                var idAttr = tab === "formats" ? "data-fmt-id" : "data-doc-id";
                var editClass = tab === "formats" ? "dm-fmt-editable" : "dm-editable";
                ids.forEach(function(id){
                    var row = wrap.querySelector("tr[" + idAttr + "=\\\'" + id + "\\\']");
                    if (!row) return;
                    var cell = row.querySelector("." + editClass + "[data-field=\\\'" + field + "\\\']");
                    if (cell){
                        cell.setAttribute("data-value", value);
                        var span = cell.querySelector(".dm-cell-value");
                        if (span) span.textContent = value || "-";
                    }
                    if (field === "version") row.setAttribute(tab === "formats" ? "data-fmt-version" : "data-version", value);
                    if (field === "validity_date") row.setAttribute(tab === "formats" ? "data-fmt-validity" : "data-validity", value);
                });
                if (field === "code") detectDuplicates();
                clearBulkSelections();
                var resMsg = "Actualización masiva: " + d.updated + " de " + d.total + " actualizados en BD.";
                if (d.wiki_updated > 0) resMsg += "\\n" + d.wiki_updated + " página(s) wiki actualizadas.";
                alert(resMsg);
            } else {
                alert("Error: " + (d.error || "No se pudo aplicar"));
            }
        }, function(){
            bulkApply.disabled = false;
            bulkApply.textContent = "Aplicar a seleccionados";
            alert("Error de conexión al aplicar cambios masivos");
        });
    });

    // === SCROLLBAR SUPERIOR SINCRONIZADA ===
    wrap.querySelectorAll(".docmaster-table-wrap").forEach(function(tw){
        var scrollTop = document.createElement("div");
        scrollTop.className = "dm-scroll-top";
        var inner = document.createElement("div");
        scrollTop.appendChild(inner);
        tw.parentNode.insertBefore(scrollTop, tw);
        function syncWidth(){ inner.style.width = tw.scrollWidth + "px"; }
        syncWidth();
        var obs = new MutationObserver(syncWidth);
        obs.observe(tw, {childList:true, subtree:true});
        var syncing = false;
        scrollTop.addEventListener("scroll", function(){ if (!syncing){ syncing = true; tw.scrollLeft = scrollTop.scrollLeft; syncing = false; } });
        tw.addEventListener("scroll", function(){ if (!syncing){ syncing = true; scrollTop.scrollLeft = tw.scrollLeft; syncing = false; } });
        // También sincronizar al redimensionar
        window.addEventListener("resize", syncWidth);
    });

    // === AJAX ===
    function ajaxPost(call, params, onOk, onErr){
        var fd = new FormData();
        fd.append("call", call);
        fd.append("sectok", (typeof JSINFO !== "undefined" && JSINFO.sectok) ? JSINFO.sectok : "");
        for (var k in params) if (params.hasOwnProperty(k)) fd.append(k, params[k]);
        var xhr = new XMLHttpRequest();
        xhr.open("POST", AJAX_URL, true);
        xhr.onreadystatechange = function(){
            if (xhr.readyState !== 4) return;
            if (xhr.status === 200){ try { var d = JSON.parse(xhr.responseText); if (onOk) onOk(d); } catch(e){ if (onErr) onErr(e); } }
            else { if (onErr) onErr(xhr.status); }
        };
        xhr.send(fd);
    }

    function esc(s){ var d = document.createElement("div"); d.textContent = s; return d.innerHTML; }
    function escA(s){ return String(s).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/\'/g,"&#39;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); }

    // === BOTÓN MASIVO: CORREGIR TODOS LOS DUPLICADOS ===
    var fixAllBtn = document.getElementById("dm-btn-fix-all");
    var dupCountSpan = document.getElementById("dm-dup-count");

    // Actualizar visibilidad del botón masivo según duplicados detectados
    function updateFixAllButton(){
        var totalDups = 0;
        if (dupData.documents) for (var k in dupData.documents) if (dupData.documents.hasOwnProperty(k)) totalDups += (dupData.documents[k] - 1);
        if (dupData.formats) for (var k in dupData.formats) if (dupData.formats.hasOwnProperty(k)) totalDups += (dupData.formats[k] - 1);
        if (fixAllBtn){
            fixAllBtn.style.display = totalDups > 0 ? "" : "none";
            if (dupCountSpan) dupCountSpan.textContent = totalDups;
        }
    }

    // Extender detectDuplicates para actualizar botón masivo
    var _origDetectDuplicates = detectDuplicates;
    detectDuplicates = function(){
        ajaxPost("plugin_docmaster_duplicates", {}, function(d){
            dupData = d;
            applyDuplicateHighlight();
            updateFixAllButton();
        });
    };
    // Re-ejecutar detección con la versión extendida
    detectDuplicates();

    if (fixAllBtn) fixAllBtn.addEventListener("click", function(){
        // Obtener IDs seleccionados (documentos y formatos por separado)
        var selDocIds = [];
        var selFmtIds = [];
        wrap.querySelectorAll(".dm-doc-check:checked").forEach(function(cb){
            var r = cb.closest("tr"); if (r && r.style.display !== "none") selDocIds.push(cb.value);
        });
        wrap.querySelectorAll(".dm-fmt-check:checked").forEach(function(cb){
            var r = cb.closest("tr"); if (r && r.style.display !== "none") selFmtIds.push(cb.value);
        });
        var hasSelection = selDocIds.length > 0 || selFmtIds.length > 0;

        // Calcular duplicados totales
        var totalDups = 0;
        if (dupData.documents) for (var k in dupData.documents) if (dupData.documents.hasOwnProperty(k)) totalDups += (dupData.documents[k] - 1);
        if (dupData.formats) for (var k in dupData.formats) if (dupData.formats.hasOwnProperty(k)) totalDups += (dupData.formats[k] - 1);

        var msg;
        if (hasSelection) {
            var parts = [];
            if (selDocIds.length > 0) parts.push(selDocIds.length + " documento(s)");
            if (selFmtIds.length > 0) parts.push(selFmtIds.length + " formato(s)");
            msg = "¿Corregir duplicados en los " + parts.join(" y ") + " seleccionados?\\nSolo se corregirán los que tengan códigos duplicados.";
        } else {
            msg = "¿Corregir TODOS los " + totalDups + " código(s) duplicado(s)?\\nNo hay items seleccionados, se corregirán todos.";
        }
        msg += "\\nLos documentos se actualizan automáticamente en la wiki.\\nLos formatos requieren revisión manual en sus archivos.";
        if (!confirm(msg)) return;

        fixAllBtn.disabled = true;
        fixAllBtn.textContent = "Corrigiendo...";

        var params = {};
        if (hasSelection) {
            params.doc_ids = JSON.stringify(selDocIds);
            params.fmt_ids = JSON.stringify(selFmtIds);
        }

        ajaxPost("plugin_docmaster_fix_all_duplicates", params, function(d){
            fixAllBtn.disabled = false;
            fixAllBtn.innerHTML = "&#128260; Corregir Duplicados <span class=\"dm-dup-count\" id=\"dm-dup-count\">0</span>";
            dupCountSpan = document.getElementById("dm-dup-count");

            if (d.success){
                // Actualizar UI de documentos y formatos corregidos
                if (d.corrections && d.corrections.length > 0){
                    d.corrections.forEach(function(c){
                        if (c.type === "document"){
                            var row = document.querySelector("#dm-documents-body tr[data-doc-id=\"" + c.id + "\"]");
                            if (row){
                                var cell = row.querySelector("[data-field=\'code\']");
                                if (cell){
                                    cell.setAttribute("data-value", c.new_code);
                                    var span = cell.querySelector(".dm-cell-value");
                                    if (span) span.textContent = c.new_code;
                                }
                            }
                        } else if (c.type === "format"){
                            var row = document.querySelector("#dm-formats-body tr[data-fmt-id=\"" + c.id + "\"]");
                            if (row){
                                var cell = row.querySelector("[data-field=\'code\']");
                                if (cell){
                                    cell.setAttribute("data-value", c.new_code);
                                    var span = cell.querySelector(".dm-cell-value");
                                    if (span) span.textContent = c.new_code;
                                }
                            }
                        }
                    });
                    renderPendingReview(d.corrections);
                }
                detectDuplicates();
                clearBulkSelections();
                alert("Corrección completada: " + d.total_fixed + " código(s) corregidos" + (d.total_errors > 0 ? ", " + d.total_errors + " error(es)" : ""));
            } else {
                alert("Error al corregir duplicados");
            }
        }, function(){
            fixAllBtn.disabled = false;
            fixAllBtn.innerHTML = "&#128260; Corregir Duplicados <span class=\"dm-dup-count\" id=\"dm-dup-count\">0</span>";
            dupCountSpan = document.getElementById("dm-dup-count");
            alert("Error de conexión al corregir duplicados");
        });
    });

    // === BOTÓN MASIVO: REPARADOR GENERAL ===
    var repairAllBtn = document.getElementById("dm-btn-repair-all");

    if (repairAllBtn) repairAllBtn.addEventListener("click", function(){
        // Obtener IDs de formatos seleccionados (si hay)
        var selFmtIds = [];
        wrap.querySelectorAll(".dm-fmt-check:checked").forEach(function(cb){
            var r = cb.closest("tr");
            if (r && r.style.display !== "none") selFmtIds.push(cb.value);
        });
        var hasSelection = selFmtIds.length > 0;

        var totalFmts = fmtData ? fmtData.length : 0;

        var msg;
        if (hasSelection) {
            msg = "Reparar los " + selFmtIds.length + " formato(s) seleccionados?\n"
                + "Se sincronizaran los datos (codigo, version, vigencia) en los hipervinculos de las paginas wiki vinculadas.";
        } else {
            msg = "Reparar TODOS los " + totalFmts + " formatos?\n"
                + "No hay formatos seleccionados, se procesaran todos.\n"
                + "Se sincronizaran los datos (codigo, version, vigencia) en los hipervinculos de las paginas wiki vinculadas.\n\n"
                + "Esta operacion puede tomar varios minutos.";
        }
        if (!confirm(msg)) return;

        repairAllBtn.disabled = true;
        repairAllBtn.innerHTML = "&#128736; Reparando... <span class=\"dm-repair-spinner\"></span>";

        var params = {};
        if (hasSelection) {
            params.fmt_ids = JSON.stringify(selFmtIds);
        }

        ajaxPost("plugin_docmaster_repair_all_formats", params, function(d){
            repairAllBtn.disabled = false;
            repairAllBtn.innerHTML = "&#128736; Reparador General";

            if (d.success){
                renderRepairResults(d);
                // Recargar tab de formatos para reflejar cambios
                loadFormats();
                clearBulkSelections();
                alert("Reparacion completada:\n"
                    + d.total_repaired + " formato(s) reparados\n"
                    + d.total_pages_updated + " pagina(s) actualizadas\n"
                    + d.total_skipped + " omitido(s) (sin paginas vinculadas)\n"
                    + (d.total_errors > 0 ? d.total_errors + " error(es)" : "Sin errores"));
            } else {
                alert("Error al ejecutar el Reparador General");
            }
        }, function(){
            repairAllBtn.disabled = false;
            repairAllBtn.innerHTML = "&#128736; Reparador General";
            alert("Error de conexion al ejecutar el Reparador General");
        });
    });

    // === TABLA DE RESULTADOS DEL REPARADOR GENERAL ===
    function renderRepairResults(data){
        var container = document.getElementById("dm-repair-results-wrap");
        if (!container) return;

        var repaired = data.repaired || [];
        var skipped  = data.skipped || [];
        var errors   = data.errors || [];

        if (repaired.length === 0 && skipped.length === 0 && errors.length === 0) {
            container.innerHTML = "";
            return;
        }

        var h = "<div class=\"dm-repair-results\">";
        h += "<div class=\"dm-repair-results-header\">";
        h += "<h3>&#128736; Resultados del Reparador General</h3>";
        h += "<div class=\"dm-repair-summary\">";
        h += "<span class=\"dm-repair-stat dm-repair-ok\">" + repaired.length + " reparados</span>";
        h += "<span class=\"dm-repair-stat dm-repair-skip\">" + skipped.length + " omitidos</span>";
        h += "<span class=\"dm-repair-stat dm-repair-err\">" + errors.length + " errores</span>";
        h += "<span class=\"dm-repair-stat dm-repair-pages\">" + data.total_pages_updated + " paginas actualizadas</span>";
        h += "</div>";
        h += "<button class=\"dm-btn-clear-repair\" id=\"dm-btn-clear-repair\">Limpiar resultados</button>";
        h += "</div>";

        // Tabla de reparados
        if (repaired.length > 0){
            h += "<table><thead><tr>";
            h += "<th>Estado</th><th>Formato</th><th>Archivo</th><th>Paginas</th><th>Notas</th>";
            h += "</tr></thead><tbody>";
            repaired.forEach(function(r){
                var notes = (r.errors && r.errors.length > 0) ? r.errors.join("; ") : "OK";
                var noteStyle = (r.errors && r.errors.length > 0) ? "color:#b45309;" : "color:#16a34a;";
                h += "<tr>";
                h += "<td><span class=\"dm-repair-badge dm-repair-badge-ok\">Reparado</span></td>";
                h += "<td>" + esc(r.display_name || "-") + "</td>";
                h += "<td>" + esc(r.media_id || "-") + "</td>";
                h += "<td style=\"text-align:center;font-weight:600;\">" + r.updated_pages + "</td>";
                h += "<td style=\"" + noteStyle + "font-size:0.75rem;\">" + esc(notes) + "</td>";
                h += "</tr>";
            });
            h += "</tbody></table>";
        }

        // Tabla de errores
        if (errors.length > 0){
            h += "<table><thead><tr>";
            h += "<th>Estado</th><th>Formato</th><th>Archivo</th><th colspan=\"2\">Error</th>";
            h += "</tr></thead><tbody>";
            errors.forEach(function(e){
                var errMsgs = (e.errors && e.errors.length > 0) ? e.errors.join("; ") : "Error desconocido";
                h += "<tr style=\"background:#fef2f2;\">";
                h += "<td><span class=\"dm-repair-badge dm-repair-badge-err\">Error</span></td>";
                h += "<td>" + esc(e.display_name || "-") + "</td>";
                h += "<td>" + esc(e.media_id || "-") + "</td>";
                h += "<td colspan=\"2\" style=\"color:#dc2626;font-size:0.75rem;\">" + esc(errMsgs) + "</td>";
                h += "</tr>";
            });
            h += "</tbody></table>";
        }

        // Tabla de omitidos
        if (skipped.length > 0){
            h += "<table><thead><tr>";
            h += "<th>Estado</th><th>Formato</th><th>Archivo</th><th colspan=\"2\">Razon</th>";
            h += "</tr></thead><tbody>";
            skipped.forEach(function(s){
                h += "<tr style=\"background:#fffbeb;\">";
                h += "<td><span class=\"dm-repair-badge dm-repair-badge-skip\">Omitido</span></td>";
                h += "<td>" + esc(s.display_name || "-") + "</td>";
                h += "<td>" + esc(s.media_id || "-") + "</td>";
                h += "<td colspan=\"2\" style=\"color:#92400e;font-size:0.75rem;\">" + esc(s.reason || "-") + "</td>";
                h += "</tr>";
            });
            h += "</tbody></table>";
        }

        h += "</div>";
        container.innerHTML = h;

        // Handler para limpiar resultados
        var clearBtn = document.getElementById("dm-btn-clear-repair");
        if (clearBtn) clearBtn.addEventListener("click", function(){
            container.innerHTML = "";
        });
    }

    // === TABLA DE PENDIENTES DE REVISIÓN MANUAL ===
    function renderPendingReview(corrections){
        var container = document.getElementById("dm-pending-review-wrap");
        if (!container) return;

        // Filtrar solo formatos (requieren revisión manual en archivos Office)
        var formatCorrections = corrections.filter(function(c){ return c.type === "format"; });
        // Incluir también documentos para referencia
        var allCorrections = corrections;

        if (allCorrections.length === 0){
            container.innerHTML = "";
            return;
        }

        var h = "<div class=\"dm-pending-review\">";
        h += "<div class=\"dm-pending-review-header\">";
        h += "<h3>&#128221; Códigos corregidos — Pendientes de revisión</h3>";
        h += "<button class=\"dm-btn-clear-pending\" id=\"dm-btn-clear-pending\">Limpiar lista</button>";
        h += "</div>";
        h += "<table>";
        h += "<thead><tr>";
        h += "<th>Tipo</th><th>Nombre / Título</th><th>Código anterior</th><th>Código nuevo</th><th>Tarea pendiente</th><th></th>";
        h += "</tr></thead><tbody>";

        allCorrections.forEach(function(c, idx){
            var tipo = c.type === "format" ? "Formato" : "Documento";
            var tarea = c.type === "format"
                ? "Abrir archivo y actualizar código manualmente"
                : "Actualizado automáticamente en la página wiki";
            var tareaClass = c.type === "format" ? "color:#dc2626;font-weight:600;" : "color:#16a34a;";

            h += "<tr data-pending-idx=\"" + idx + "\">";
            h += "<td><span style=\"font-size:0.75rem;padding:2px 8px;border-radius:10px;background:" + (c.type==="format"?"#fef3c7;color:#92400e":"#dbeafe;color:#1e40af") + ";\">" + tipo + "</span></td>";
            h += "<td>" + esc(c.title || c.page_id || "-") + "</td>";
            h += "<td><span class=\"dm-old-code\">" + esc(c.old_code) + "</span></td>";
            h += "<td><span class=\"dm-new-code\">" + esc(c.new_code) + "</span></td>";
            h += "<td style=\"" + tareaClass + "\">" + tarea + "</td>";
            h += "<td><button class=\"dm-btn-dismiss\" data-pending-idx=\"" + idx + "\" title=\"Marcar como revisado\">✓</button></td>";
            h += "</tr>";
        });

        h += "</tbody></table></div>";
        container.innerHTML = h;

        // Handler limpiar toda la lista
        var clearBtn = document.getElementById("dm-btn-clear-pending");
        if (clearBtn) clearBtn.addEventListener("click", function(){
            if (confirm("¿Limpiar la lista de pendientes?\\nEsto indica que ya revisó todos los cambios.")) container.innerHTML = "";
        });

        // Handler descartar individual
        container.addEventListener("click", function(e){
            var btn = e.target.closest(".dm-btn-dismiss");
            if (!btn) return;
            var row = btn.closest("tr");
            if (row) {
                row.style.transition = "opacity 0.3s";
                row.style.opacity = "0";
                setTimeout(function(){
                    row.remove();
                    // Si no quedan filas, limpiar todo
                    var remaining = container.querySelectorAll("tbody tr");
                    if (remaining.length === 0) container.innerHTML = "";
                }, 300);
            }
        });
    }
})();
</script>';
    }

    /**
     * Renderiza el botón de escaneo + estadísticas inline
     */
    protected function _renderScanAndStats($stats)
    {
        echo '<div class="docmaster-scan-bar">';

        // Botón de escaneo
        echo '<button id="dm-btn-scan" class="button docmaster-btn-scan" type="button">';
        echo $this->getLang('btn_scan');
        echo '</button>';

        // Botón corregir todos los duplicados
        echo '<button id="dm-btn-fix-all" class="dm-btn-fix-all" type="button" style="display:none;">';
        echo '&#128260; Corregir Duplicados <span class="dm-dup-count" id="dm-dup-count">0</span>';
        echo '</button>';

        // Botón Reparador General de formatos
        echo '<button id="dm-btn-repair-all" class="dm-btn-repair-all" type="button">';
        echo '&#128736; Reparador General';
        echo '</button>';

        echo '<span id="dm-scan-status" class="docmaster-scan-status"></span>';

        // Stats inline
        echo '<div class="docmaster-stats-inline">';
        echo '<span class="dm-stat-inline"><strong id="dm-stat-docs">' . ($stats['total_documents'] ?? 0) . '</strong> ' . $this->getLang('stat_documents') . '</span>';
        echo '<span class="dm-stat-sep">|</span>';
        echo '<span class="dm-stat-inline"><strong id="dm-stat-formats">' . ($stats['total_formats'] ?? 0) . '</strong> ' . $this->getLang('stat_formats') . '</span>';
        echo '<span class="dm-stat-sep">|</span>';
        echo '<span class="dm-stat-inline"><strong id="dm-stat-ns">' . ($stats['total_namespaces'] ?? 0) . '</strong> ' . $this->getLang('stat_namespaces') . '</span>';
        echo '<span class="dm-stat-sep">|</span>';
        echo '<span class="dm-stat-inline"><strong id="dm-stat-shared">' . ($stats['shared_formats'] ?? 0) . '</strong> ' . $this->getLang('stat_shared') . '</span>';
        echo '</div>';

        echo '</div>'; // .docmaster-scan-bar

        // Contenedor para resultados del Reparador General
        echo '<div id="dm-repair-results-wrap"></div>';
    }

    /**
     * Renderiza la sección de ubicaciones con documentos + explicación de estructura
     */
    protected function _renderLocations($documents, $namespaces)
    {
        // Sección: Ubicaciones con documentos registrados
        if (!empty($namespaces)) {
            echo '<div class="docmaster-locations-ref">';
            echo '<h4>Ubicaciones con documentos registrados</h4>';
            echo '<div class="dm-locations-list">';
            // Ordenar alfabéticamente
            $sortedNs = $namespaces;
            sort($sortedNs);
            foreach ($sortedNs as $ns) {
                $count = 0;
                foreach ($documents as $doc) {
                    if ($doc['namespace'] === $ns) $count++;
                }
                echo '<span class="dm-location-badge">' . hsc($ns) . ' <em>(' . $count . ')</em></span>';
            }
            echo '</div>';
            echo '</div>';
        }

        // Sección: Estructura de referencias documentales
        echo '<div class="docmaster-structure-ref">';
        echo '<h4>Estructura de referencias documentales</h4>';
        echo '<p class="dm-structure-desc">Los nombres de formatos en la sección <em>Anexos y Registros</em> pueden incluir la siguiente información:</p>';
        echo '<div class="dm-structure-format">';
        echo '<code>CÓDIGO, NOMBRE DEL FORMATO, ver. #, DD/MM/AAAA</code>';
        echo '</div>';
        echo '<div class="dm-structure-legend">';
        echo '<span><strong>CÓDIGO</strong> = Código del formato (ej: M03 F21, OPE P04 F1)</span>';
        echo '<span><strong>NOMBRE</strong> = Nombre descriptivo del formato</span>';
        echo '<span><strong>ver. #</strong> = Número de versión</span>';
        echo '<span><strong>DD/MM/AAAA</strong> = Fecha de vigencia</span>';
        echo '</div>';
        echo '<p class="dm-structure-note">Si un formato no incluye código, versión o vigencia, estos campos se registran como vacíos y pueden editarse manualmente.</p>';
        echo '</div>';

        // Sección: Lista de carpetas válidas (qualified namespaces) en orden alfabético
        /** @var helper_plugin_docmaster $helper */
        $helper = plugin_load('helper', 'docmaster');
        if ($helper) {
            $raw = $helper->getConf('qualified_namespaces');
            $allFolders = array_map('trim', explode(',', $raw));
            $allFolders = array_filter($allFolders);
            sort($allFolders);

            if (!empty($allFolders)) {
                echo '<div class="docmaster-folders-ref">';
                echo '<h4>Carpetas documentales válidas</h4>';
                echo '<div class="dm-locations-list">';
                foreach ($allFolders as $folder) {
                    // Resaltar las que tienen documentos
                    $hasDoc = in_array($folder, $namespaces);
                    $class = $hasDoc ? 'dm-location-badge dm-folder-active' : 'dm-location-badge dm-folder-empty';
                    echo '<span class="' . $class . '">' . hsc($folder) . '</span>';
                }
                echo '</div>';
                echo '</div>';
            }
        }
    }

    /**
     * Renderiza las tabs de navegación
     */
    protected function _renderTabs()
    {
        echo '<div class="docmaster-tabs">';
        echo '<button class="docmaster-tab active" data-tab="documents">' . $this->getLang('tab_documents') . '</button>';
        echo '<button class="docmaster-tab" data-tab="formats">' . $this->getLang('tab_formats') . '</button>';
        echo '</div>';
    }

    /**
     * Renderiza la vista de documentos
     */
    protected function _renderDocumentsTab($documents, $namespaces)
    {
        echo '<div id="dm-tab-documents" class="docmaster-tab-content active">';

        // Extraer versiones únicas para el filtro
        $versions = [];
        foreach ($documents as $doc) {
            $v = (int) $doc['version'];
            if ($v > 0) $versions[$v] = true;
        }
        ksort($versions);

        // Filtros - fila 1: ubicación + versión + búsqueda
        echo '<div class="docmaster-filters">';
        echo '<select id="dm-filter-ns" class="docmaster-select">';
        echo '<option value="">' . $this->getLang('filter_all_ns') . '</option>';
        foreach ($namespaces as $ns) {
            echo '<option value="' . hsc($ns) . '">' . hsc($ns) . '</option>';
        }
        echo '</select>';

        // Filtro por versión
        echo '<select id="dm-filter-version" class="docmaster-select">';
        echo '<option value="">' . $this->getLang('filter_all_versions') . '</option>';
        foreach (array_keys($versions) as $ver) {
            echo '<option value="' . $ver . '">Versión ' . $ver . '</option>';
        }
        echo '</select>';

        echo '<input type="text" id="dm-filter-search" class="docmaster-input" placeholder="' . hsc($this->getLang('filter_search')) . '">';
        echo '</div>';

        // Filtros - fila 2: rango de vigencia
        echo '<div class="docmaster-filters docmaster-filters-dates">';
        echo '<label class="dm-filter-label">' . $this->getLang('filter_validity_label') . '</label>';
        echo '<input type="text" id="dm-filter-date-from" class="docmaster-input docmaster-input-date" placeholder="dd/mm/aaaa" maxlength="10">';
        echo '<span class="dm-filter-sep">—</span>';
        echo '<input type="text" id="dm-filter-date-to" class="docmaster-input docmaster-input-date" placeholder="dd/mm/aaaa" maxlength="10">';
        echo '<button type="button" id="dm-filter-date-clear" class="dm-btn-clear" title="Limpiar fechas">✕</button>';
        echo '</div>';

        // Tabla de documentos
        echo '<div class="docmaster-table-wrap">';
        echo '<table class="docmaster-table" id="dm-documents-table">';
        echo '<thead>';
        echo '<tr>';
        echo '<th class="dm-col-check"><input type="checkbox" id="dm-doc-select-all" title="Seleccionar todos los visibles"></th>';
        echo '<th class="dm-col-title sortable" data-sort="title">' . $this->getLang('col_title') . '</th>';
        echo '<th class="dm-col-code sortable" data-sort="code">' . $this->getLang('col_code') . '</th>';
        echo '<th class="dm-col-version sortable" data-sort="version">' . $this->getLang('col_version') . '</th>';
        echo '<th class="dm-col-validity sortable" data-sort="validity_date">' . $this->getLang('col_validity') . '</th>';
        echo '<th class="dm-col-ns sortable" data-sort="namespace">' . $this->getLang('col_namespace') . '</th>';
        echo '<th class="dm-col-formats">' . $this->getLang('col_formats') . '</th>';
        echo '<th class="dm-col-actions">' . $this->getLang('col_actions') . '</th>';
        echo '</tr>';
        echo '</thead>';
        echo '<tbody id="dm-documents-body">';

        foreach ($documents as $doc) {
            $this->_renderDocumentRow($doc);
        }

        echo '</tbody>';
        echo '</table>';
        echo '</div>'; // .docmaster-table-wrap

        // Mensaje cuando no hay resultados
        echo '<div id="dm-no-results" class="docmaster-no-results" style="display:none;">';
        echo $this->getLang('no_results');
        echo '</div>';

        // Contenedor para tabla de pendientes de revisión manual (generada por JS)
        echo '<div id="dm-pending-review-wrap"></div>';

        echo '</div>'; // #dm-tab-documents
    }

    /**
     * Renderiza una fila de documento
     */
    protected function _renderDocumentRow($doc)
    {
        $pageUrl = wl($doc['page_id']);
        $formatCount = isset($doc['format_count']) ? (int) $doc['format_count'] : 0;

        $validityYear = '';
        if (!empty($doc['validity_date']) && preg_match('/(\d{4})/', $doc['validity_date'], $ym)) {
            $validityYear = $ym[1];
        }

        echo '<tr data-doc-id="' . (int) $doc['id'] . '" data-ns="' . hsc($doc['namespace']) . '" data-version="' . (int) $doc['version'] . '" data-validity="' . hsc($doc['validity_date']) . '" data-validity-year="' . $validityYear . '">';

        // Checkbox de selección
        echo '<td class="dm-col-check"><input type="checkbox" class="dm-doc-check" value="' . (int) $doc['id'] . '"></td>';

        // Título (enlace al documento)
        echo '<td class="dm-col-title">';
        echo '<a href="' . $pageUrl . '" title="' . hsc($doc['page_id']) . '">';
        echo hsc($doc['title'] ?: $doc['page_id']);
        echo '</a>';
        echo '</td>';

        // Código (editable)
        echo '<td class="dm-col-code dm-editable" data-field="code" data-value="' . hsc($doc['code']) . '">';
        echo '<span class="dm-cell-value">' . hsc($doc['code']) . '</span>';
        echo '</td>';

        // Versión (editable)
        echo '<td class="dm-col-version dm-editable" data-field="version" data-value="' . (int) $doc['version'] . '">';
        echo '<span class="dm-cell-value">' . (int) $doc['version'] . '</span>';
        echo '</td>';

        // Vigencia (editable)
        echo '<td class="dm-col-validity dm-editable" data-field="validity_date" data-value="' . hsc($doc['validity_date']) . '">';
        echo '<span class="dm-cell-value">' . hsc($doc['validity_date']) . '</span>';
        echo '</td>';

        // Namespace
        echo '<td class="dm-col-ns">';
        echo '<span class="dm-ns-badge">' . hsc($doc['namespace']) . '</span>';
        echo '</td>';

        // Formatos
        echo '<td class="dm-col-formats">';
        if ($formatCount > 0) {
            echo '<button class="dm-btn-formats" data-doc-id="' . (int) $doc['id'] . '" title="' . $this->getLang('btn_show_formats') . '">';
            echo '<span class="dm-format-count">' . $formatCount . '</span>';
            echo '</button>';
        } else {
            echo '<span class="dm-no-formats">0</span>';
        }
        echo '</td>';

        // Acciones
        echo '<td class="dm-col-actions">';
        echo '<a href="' . $pageUrl . '&do=edit" class="dm-btn-edit" title="' . $this->getLang('btn_edit_page') . '">&#9998;</a>';
        echo '<button class="dm-btn-fix-dup" data-doc-id="' . (int) $doc['id'] . '" title="Corregir código duplicado" style="display:none;">&#128260;</button>';
        echo '</td>';

        echo '</tr>';

        // Fila expandible para formatos (oculta)
        echo '<tr class="dm-formats-row" data-doc-id="' . (int) $doc['id'] . '" style="display:none;">';
        echo '<td colspan="8" class="dm-formats-cell">';
        echo '<div class="dm-formats-list"></div>';
        echo '</td>';
        echo '</tr>';
    }

    /**
     * Renderiza la vista de formatos
     */
    protected function _renderFormatsTab()
    {
        echo '<div id="dm-tab-formats" class="docmaster-tab-content" style="display:none;">';

        // Filtros - fila 1: ubicación + procedimiento + versión + búsqueda
        echo '<div class="docmaster-filters">';

        // Filtro ubicación (se llena dinámicamente vía JS)
        echo '<select id="dm-fmt-filter-ns" class="docmaster-select">';
        echo '<option value="">' . $this->getLang('filter_all_ns') . '</option>';
        echo '</select>';

        // Filtro procedimiento (se llena dinámicamente vía JS)
        echo '<select id="dm-fmt-filter-proc" class="docmaster-select">';
        echo '<option value="">' . $this->getLang('filter_all_procedures') . '</option>';
        echo '</select>';

        // Filtro versión (se llena dinámicamente vía JS)
        echo '<select id="dm-fmt-filter-version" class="docmaster-select">';
        echo '<option value="">' . $this->getLang('filter_all_versions') . '</option>';
        echo '</select>';

        echo '<input type="text" id="dm-filter-format-search" class="docmaster-input" placeholder="' . hsc($this->getLang('filter_search_format')) . '">';
        echo '</div>';

        // Filtros - fila 2: rango de vigencia
        echo '<div class="docmaster-filters docmaster-filters-dates">';
        echo '<label class="dm-filter-label">' . $this->getLang('filter_validity_label') . '</label>';
        echo '<input type="text" id="dm-fmt-filter-date-from" class="docmaster-input docmaster-input-date" placeholder="dd/mm/aaaa" maxlength="10">';
        echo '<span class="dm-filter-sep">—</span>';
        echo '<input type="text" id="dm-fmt-filter-date-to" class="docmaster-input docmaster-input-date" placeholder="dd/mm/aaaa" maxlength="10">';
        echo '<button type="button" id="dm-fmt-filter-date-clear" class="dm-btn-clear" title="Limpiar fechas">✕</button>';
        echo '</div>';

        // Tabla de formatos
        echo '<div class="docmaster-table-wrap">';
        echo '<table class="docmaster-table" id="dm-formats-table">';
        echo '<thead>';
        echo '<tr>';
        echo '<th class="dm-col-check"><input type="checkbox" id="dm-fmt-select-all" title="Seleccionar todos los visibles"></th>';
        echo '<th class="dm-col-fname">' . $this->getLang('col_format_name') . '</th>';
        echo '<th class="dm-col-fmedia">' . $this->getLang('col_format_media') . '</th>';
        echo '<th class="dm-col-fproc">' . $this->getLang('col_format_procedure') . '</th>';
        echo '<th class="dm-col-fcode">' . $this->getLang('col_format_code') . '</th>';
        echo '<th class="dm-col-fversion">' . $this->getLang('col_format_version') . '</th>';
        echo '<th class="dm-col-fvalidity">' . $this->getLang('col_format_validity') . '</th>';
        echo '<th class="dm-col-fdocs">' . $this->getLang('col_format_docs') . '</th>';
        echo '<th class="dm-col-factions">' . $this->getLang('col_actions') . '</th>';
        echo '</tr>';
        echo '</thead>';
        echo '<tbody id="dm-formats-body">';
        echo '<tr><td colspan="9" class="dm-loading">' . $this->getLang('loading') . '</td></tr>';
        echo '</tbody>';
        echo '</table>';
        echo '</div>'; // .docmaster-table-wrap

        echo '</div>'; // #dm-tab-formats
    }
}
