From eff590af47da466518e76c16d5a034a186d49536 Mon Sep 17 00:00:00 2001 From: Soporte ZH Consultores Date: Thu, 4 Jun 2026 22:30:37 -0500 Subject: [PATCH] =?UTF-8?q?feat(export):=20UI=20unificada=20de=20Excel=20+?= =?UTF-8?q?=20PDF=20en=20los=204=20m=C3=B3dulos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- public/assets/donna-export.css | 32 +++++ public/assets/donna-export.js | 179 ++++++++++++++++++++++++++ public/horas/api.php | 32 +---- views/calendario.html | 43 +++++++ views/escrituras.html | 225 +++++---------------------------- views/horas.html | 52 +++++--- views/tramites.html | 50 ++++++++ 7 files changed, 377 insertions(+), 236 deletions(-) create mode 100644 public/assets/donna-export.css create mode 100644 public/assets/donna-export.js diff --git a/public/assets/donna-export.css b/public/assets/donna-export.css new file mode 100644 index 0000000..0d4065a --- /dev/null +++ b/public/assets/donna-export.css @@ -0,0 +1,32 @@ +/* ============================================================ + * Donna — UI compartida para botón "Exportar" con menú dropdown + * ============================================================ */ +.export-wrap { position:relative; display:inline-block; } +.btn-export { + display:inline-flex; align-items:center; gap:7px; + padding:8px 16px; background:#fff; color:var(--navy, #1B3A6B); + border:1px solid var(--navy, #1B3A6B); border-radius:8px; + 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, #F7F6F3); } +.btn-export svg { width:14px; height:14px; } +.export-menu { + position:absolute; right:0; top:calc(100% + 6px); + background:#fff; border:1px solid var(--gray-200, #D6D4CC); + border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.1); + min-width:210px; z-index:300; overflow:hidden; display:none; +} +.export-menu.open { display:block; } +.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, #4A4946); + cursor:pointer; text-align:left; transition:background .12s; +} +.export-menu button:hover { background:var(--off, #F7F6F3); color:var(--navy, #1B3A6B); } +.export-menu button svg { width:15px; height:15px; flex-shrink:0; } +.export-menu button.xl { color:#1D6F42; } +.export-menu button.xl:hover { background:#EAF3DE; } +.export-menu button.pdf { color:#C0392B; } +.export-menu button.pdf:hover { background:#FCEBEB; } diff --git a/public/assets/donna-export.js b/public/assets/donna-export.js new file mode 100644 index 0000000..30e119f --- /dev/null +++ b/public/assets/donna-export.js @@ -0,0 +1,179 @@ +/* ============================================================ + * Donna — Helpers compartidos para exportación a Excel y PDF + * ============================================================ + * Requiere XLSX (https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js) + * cargado antes de este script. + * + * Uso: + *
…botón + menú…
+ * DonnaExport.attach('export-wrap', { onExcel, onPDF }); + * DonnaExport.toExcel({ filename, sheetName, headers, rows, columnWidths }); + * DonnaExport.toPDF({ filename, title, subtitle, headers, rows, + * pdfCols, orientation, statusColIndex, statusColors, footer }); + */ +(function(){ + 'use strict'; + + const NAVY = '#1B3A6B'; + const GOLD = '#C9A84C'; + const FIRM = 'AALN Law — Arias, Abrego, López & Noriega'; + const ADDR = 'P.H. Midtown, Piso 16, Of. 1605, Calle 74 Este, San Francisco, Ciudad de Panamá'; + + function escHtml(s){ + return (s==null?'':String(s)) + .replace(/&/g,'&').replace(/"/g,'"') + .replace(//g,'>'); + } + + function todayStamp(){ + const d = new Date(); + const pad = n => String(n).padStart(2,'0'); + return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; + } + + function attach(wrapId, handlers){ + const wrap = document.getElementById(wrapId); + if (!wrap) return; + const btn = wrap.querySelector('.btn-export'); + const menu = wrap.querySelector('.export-menu'); + const xl = wrap.querySelector('.export-menu button.xl'); + const pdf = wrap.querySelector('.export-menu button.pdf'); + if (btn && menu) { + btn.addEventListener('click', e => { + e.stopPropagation(); + menu.classList.toggle('open'); + }); + } + document.addEventListener('click', e => { + if (!wrap.contains(e.target)) menu && menu.classList.remove('open'); + }); + if (xl && handlers.onExcel) xl.addEventListener('click', () => { menu.classList.remove('open'); handlers.onExcel(); }); + if (pdf && handlers.onPDF) pdf.addEventListener('click', () => { menu.classList.remove('open'); handlers.onPDF(); }); + } + + function toExcel(opts){ + if (typeof XLSX === 'undefined') { alert('XLSX no está cargado'); return; } + const headers = opts.headers || []; + const rows = opts.rows || []; + const sheetName = opts.sheetName || 'Datos'; + const fileBase = opts.filename || 'Donna_Export'; + if (!rows.length) { opts.onEmpty && opts.onEmpty(); return; } + + const wb = XLSX.utils.book_new(); + const data = [headers, ...rows]; + const ws = XLSX.utils.aoa_to_sheet(data); + + const widths = opts.columnWidths || headers.map(() => 18); + ws['!cols'] = widths.map(w => ({ wch: w })); + + 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, sheetName); + XLSX.writeFile(wb, `${fileBase}_${todayStamp()}.xlsx`); + } + + function toPDF(opts){ + const headers = opts.headers || []; + const rows = opts.rows || []; + const title = opts.title || 'Reporte'; + const subtitle = opts.subtitle || ''; + const orientation = opts.orientation || 'landscape'; + const fileBase = opts.filename || 'Donna_Export'; + const footer = opts.footer != null ? opts.footer : ADDR; + const pdfCols = opts.pdfCols || headers.map((_,i) => i); + const statusCol = opts.statusColIndex; + const statusColors = opts.statusColors || {}; + if (!rows.length) { opts.onEmpty && opts.onEmpty(); return; } + + const pdfHeaders = pdfCols.map(i => headers[i]); + const styledRows = rows.map((r, ri) => { + const cells = pdfCols.map(ci => { + const val = r[ci] == null ? '' : String(r[ci]); + if (statusCol === ci && statusColors[val]) { + const [bg, fg] = statusColors[val]; + return `${escHtml(val)}`; + } + return `${escHtml(val)}`; + }).join(''); + return `${cells}`; + }).join(''); + + const fechaStr = new Date().toLocaleDateString('es-PA'); + const html = ` + +${escHtml(title)} — AALN Law + + +
+
+
A
+
+
${FIRM}
+
${escHtml(title)}${subtitle ? ' — ' + escHtml(subtitle) : ''}
+
+
+
+ Exportado: ${escHtml(fechaStr)}
+ Total registros: ${rows.length} +
+
+ + ${pdfHeaders.map(h => ``).join('')} + ${styledRows} +
${escHtml(h)}
+ + + + + +