feat(export): UI unificada de Excel + PDF en los 4 módulos

- Nuevos assets compartidos /assets/donna-export.{css,js}: botón
  "Exportar ▾" + menú dropdown Excel/PDF y helpers DonnaExport.toExcel
  / .toPDF / .attach / .buttonHtml
- escrituras: refactor de exportExcel/exportPDF para usar el módulo
  compartido (sin cambio de comportamiento)
- tramites: nuevo botón en la toolbar de cada tab (adm/corp/mig/lit),
  exporta los rows filtrados de ese tab; PDF excluye Involucrados
- calendario: nuevo botón al final de la toolbar, exporta visibleEvents()
  (respeta chips de origen y búsqueda)
- horas: reemplaza CSV server-side por XLSX client-side + agrega PDF;
  removida la acción ?action=export del api.php
This commit is contained in:
Soporte ZH Consultores
2026-06-04 22:30:37 -05:00
parent 012c4e1c66
commit eff590af47
7 changed files with 377 additions and 236 deletions
+35 -190
View File
@@ -411,35 +411,8 @@ tr.editing-row td textarea.editable {
background:var(--gold); margin-right:4px; flex-shrink:0;
animation:pulse 1.5s ease-in-out infinite;
}
.export-wrap { position:relative; }
.btn-export {
display:flex; align-items:center; gap:7px;
padding:8px 16px; background:var(--white); color:var(--navy);
border:1px solid var(--navy); border-radius:var(--radius);
font-family:'DM Sans',sans-serif; font-size:13px; font-weight:500;
cursor:pointer; transition:background .15s; white-space:nowrap;
}
.btn-export:hover { background:var(--off); }
.btn-export svg { width:14px; height:14px; }
.export-menu {
position:absolute; right:0; top:calc(100% + 6px);
background:var(--white); border:1px solid var(--gray-200);
border-radius:var(--radius); box-shadow:0 4px 20px rgba(0,0,0,0.1);
min-width:210px; z-index:300; overflow:hidden;
}
.export-menu button {
width:100%; display:flex; align-items:center; gap:10px;
padding:11px 16px; background:transparent; border:none;
font-family:'DM Sans',sans-serif; font-size:13px; color:var(--gray-700);
cursor:pointer; text-align:left; transition:background .12s;
}
.export-menu button:hover { background:var(--off); color:var(--navy); }
.export-menu button svg { width:15px; height:15px; flex-shrink:0; }
.export-menu button:first-child { color:#1D6F42; }
.export-menu button:first-child:hover { background:#EAF3DE; }
.export-menu button:last-child { color:#C0392B; }
.export-menu button:last-child:hover { background:#FCEBEB; }
</style>
<link rel="stylesheet" href="/assets/donna-export.css">
</head>
<body>
@@ -512,23 +485,7 @@ tr.editing-row td textarea.editable {
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
Nuevo trámite
</button>
<div class="export-wrap" id="export-wrap">
<button class="btn-export" onclick="toggleExportMenu()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Exportar
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width:12px;height:12px;margin-left:2px;"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div class="export-menu" id="export-menu" style="display:none;">
<button onclick="exportExcel()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="m9 9 6 6M15 9l-6 6"/></svg>
Exportar a Excel (.xlsx)
</button>
<button onclick="exportPDF()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
Exportar a PDF
</button>
</div>
</div>
<div id="export-mount"></div>
</div>
<div class="kpi-strip">
@@ -609,6 +566,7 @@ tr.editing-row td textarea.editable {
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script src="/assets/donna-export.js"></script>
<script>
const ABOGADOS = (window.DONNA && window.DONNA.abogados) || ['Arias','Abrego','Lopez','Altafulla','Álvarez','Gutiérrez','Cedeño','Mariscal','Ávila','Agudo','Martínez','De Gracia','Góndola','González','Valdés'];
const ESTATUS = ['Solicitado','Recibido','En Proceso','Protocolizado','En Registro','Inscrito'];
@@ -1053,159 +1011,46 @@ document.addEventListener('keydown', e => {
});
// ── EXPORT ───────────────────────────────────────────────────────────────
function toggleExportMenu() {
const menu = document.getElementById('export-menu');
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
}
document.addEventListener('click', e => {
if (!document.getElementById('export-wrap').contains(e.target)) {
document.getElementById('export-menu').style.display = 'none';
}
});
document.getElementById('export-mount').innerHTML = DonnaExport.buttonHtml();
DonnaExport.attach('export-wrap', { onExcel: exportExcel, onPDF: exportPDF });
function getExportRows() {
return getFiltered(); // respeta filtros activos
}
function exportHeaders() {
return ['#','Nombre del Trámite','Cliente','Abogado Solicitante','Notaría',
'Se Inscribe','Alt. de Turno','Instrucciones Especiales',
'Fecha de Solicitud','Estatus','Entrada','Últ. Actualización','Observaciones','Creado por'];
}
function rowToArray(r, i) {
return [
const EXP_HEADERS = ['#','Nombre del Trámite','Cliente','Abogado Solicitante','Notaría',
'Se Inscribe','Alt. de Turno','Instrucciones Especiales',
'Fecha de Solicitud','Estatus','Entrada','Últ. Actualización','Observaciones','Creado por'];
const EXP_WIDTHS = [4,30,22,18,12,10,12,35,18,16,14,18,30,22];
const EXP_PDF_COLS = [0,1,2,3,4,5,6,9,10,12];
const EXP_STATUS_COLORS = {
'Solicitado': ['#F1EFE8','#5F5E5A'],
'Recibido': ['#E6F1FB','#0C447C'],
'En Proceso': ['#FAEEDA','#7A4F10'],
'Protocolizado': ['#EAF3DE','#2E5E0E'],
'En Registro': ['#E1F5EE','#085041'],
'Inscrito': ['#EEEDFE','#3C3489'],
};
function expRows() {
return getFiltered().map((r, i) => [
i+1, r.tramite, r.cliente, r.abogado, r.notaria,
r.inscribe, r.altera_turno, r.instrucciones,
fmtDate(r.fecha_solicitud), r.estatus, r.entrada || '',
fmtDate(r.ultima_actualizacion), r.observaciones, r.creado_por
];
]);
}
function exportExcel() {
document.getElementById('export-menu').style.display = 'none';
const rows = getExportRows();
if (!rows.length) { toast('No hay datos para exportar','error'); return; }
const wb = XLSX.utils.book_new();
const data = [exportHeaders(), ...rows.map(rowToArray)];
const ws = XLSX.utils.aoa_to_sheet(data);
// Column widths (#, Trámite, Cliente, Abogado, Notaría,
// Inscribe, Altera, Instrucciones, Fecha, Estatus,
// Entrada, Últ.Act., Observaciones, Creado por)
ws['!cols'] = [
{wch:4},{wch:30},{wch:22},{wch:18},{wch:12},
{wch:10},{wch:12},{wch:35},{wch:18},{wch:16},
{wch:14},{wch:18},{wch:30},{wch:22}
];
// Header style (row 1)
const range = XLSX.utils.decode_range(ws['!ref']);
for (let C = range.s.c; C <= range.e.c; C++) {
const addr = XLSX.utils.encode_cell({r:0, c:C});
if (!ws[addr]) continue;
ws[addr].s = {
fill: { fgColor: { rgb: '1B3A6B' } },
font: { bold: true, color: { rgb: 'FFFFFF' }, sz: 11 },
alignment: { horizontal: 'center', vertical: 'center', wrapText: true }
};
}
XLSX.utils.book_append_sheet(wb, ws, 'Escrituras');
const fecha = new Date().toLocaleDateString('es-PA').replace(/\//g,'-');
XLSX.writeFile(wb, `Control_Escrituras_AALN_${fecha}.xlsx`);
toast('Excel exportado correctamente');
function exportExcel(){
DonnaExport.toExcel({
filename: 'Control_Escrituras_AALN', sheetName: 'Escrituras',
headers: EXP_HEADERS, rows: expRows(), columnWidths: EXP_WIDTHS,
onEmpty: () => toast('No hay datos para exportar','error'),
});
}
function exportPDF() {
document.getElementById('export-menu').style.display = 'none';
const rows = getExportRows();
if (!rows.length) { toast('No hay datos para exportar','error'); return; }
const fecha = new Date().toLocaleDateString('es-PA');
const headers = exportHeaders();
// Columnas a mostrar en PDF (subset para que quepa en landscape)
// [#, Trámite, Cliente, Abogado, Notaría, Inscribe, Altera, Estatus, Entrada, Observaciones]
const pdfCols = [0,1,2,3,4,5,6,9,10,12];
const pdfHeaders = pdfCols.map(i=>headers[i]);
const win = window.open('','_blank');
const tableRows = rows.map((r,i) => {
const arr = rowToArray(r,i);
return '<tr>' + pdfCols.map(ci => `<td>${esc(String(arr[ci]||''))}</td>`).join('') + '</tr>';
}).join('');
const statusColors = {
'Solicitado': ['#F1EFE8','#5F5E5A'],
'Recibido': ['#E6F1FB','#0C447C'],
'En Proceso': ['#FAEEDA','#7A4F10'],
'Protocolizado': ['#EAF3DE','#2E5E0E'],
'En Registro': ['#E1F5EE','#085041'],
'Inscrito': ['#EEEDFE','#3C3489'],
};
const styledRows = rows.map((r,i) => {
const arr = rowToArray(r,i);
const cells = pdfCols.map(ci => {
let val = String(arr[ci]||'');
if (ci === 9) { // estatus
const [bg,fg] = statusColors[r.estatus] || ['#f5f5f5','#333'];
return `<td><span style="background:${bg};color:${fg};padding:2px 8px;border-radius:12px;font-size:9px;font-weight:600;white-space:nowrap;">${esc(val)}</span></td>`;
}
return `<td>${esc(val)}</td>`;
}).join('');
return `<tr class="${i%2===0?'even':''}">${cells}</tr>`;
}).join('');
win.document.write(`<!DOCTYPE html><html lang="es"><head>
<meta charset="UTF-8">
<title>Control de Escrituras — AALN Law</title>
<style>
@page { size: A4 landscape; margin: 12mm 10mm; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; font-size: 9px; color: #1a1916; }
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; border-bottom: 3px solid #C9A84C; padding-bottom: 8px; }
.header-left { display: flex; align-items: center; gap: 10px; }
.logo-mark { width: 28px; height: 28px; background: #1B3A6B; border-radius: 5px; display: flex; align-items: center; justify-content: center; color: #C9A84C; font-weight: 700; font-size: 14px; }
.firm-name { font-size: 13px; font-weight: 700; color: #1B3A6B; }
.doc-title { font-size: 9px; color: #666; }
.meta { font-size: 8px; color: #888; text-align: right; }
table { width: 100%; border-collapse: collapse; }
th { background: #1B3A6B; color: #fff; padding: 6px 5px; text-align: left; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; white-space: nowrap; }
td { padding: 5px 5px; border-bottom: 1px solid #EEEDE9; font-size: 8.5px; vertical-align: middle; }
tr.even td { background: #F7F6F3; }
tr:hover td { background: #EEF1F8; }
.footer { margin-top: 10px; font-size: 7.5px; color: #aaa; text-align: center; border-top: 1px solid #eee; padding-top: 6px; }
@media print { .no-print { display:none; } }
</style>
</head><body>
<div class="header">
<div class="header-left">
<div class="logo-mark">A</div>
<div>
<div class="firm-name">AALN Law — Arias, Abrego, López &amp; Noriega</div>
<div class="doc-title">Control de Estatus de Escrituras Públicas</div>
</div>
</div>
<div class="meta">
Exportado: ${fecha}<br>
Total registros: ${rows.length}
</div>
</div>
<table>
<thead><tr>${pdfHeaders.map(h=>`<th>${h}</th>`).join('')}</tr></thead>
<tbody>${styledRows}</tbody>
</table>
<div class="footer">
P.H. Midtown, Piso 16, Of. 1605, Calle 74 Este, San Francisco, Ciudad de Panamá &nbsp;·&nbsp; SPECTER Legal Assistant
</div>
<script>window.onload=()=>{ window.print(); }<\/script>
</body></html>`);
win.document.close();
toast('PDF generado — revisa la ventana de impresión');
function exportPDF(){
DonnaExport.toPDF({
filename: 'Control_Escrituras_AALN',
title: 'Control de Estatus de Escrituras Públicas',
headers: EXP_HEADERS, rows: expRows(),
pdfCols: EXP_PDF_COLS, statusColIndex: 9, statusColors: EXP_STATUS_COLORS,
onEmpty: () => toast('No hay datos para exportar','error'),
});
}
loadAll(true).then(()=>{ pollTimer=setInterval(()=>loadAll(false), POLL_MS); });