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:
@@ -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; }
|
||||
@@ -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:
|
||||
* <div class="export-wrap" id="export-wrap">…botón + menú…</div>
|
||||
* 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,'<').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 `<td><span style="background:${bg};color:${fg};padding:2px 8px;border-radius:12px;font-size:9px;font-weight:600;white-space:nowrap;">${escHtml(val)}</span></td>`;
|
||||
}
|
||||
return `<td>${escHtml(val)}</td>`;
|
||||
}).join('');
|
||||
return `<tr class="${ri%2===0?'even':''}">${cells}</tr>`;
|
||||
}).join('');
|
||||
|
||||
const fechaStr = new Date().toLocaleDateString('es-PA');
|
||||
const html = `<!DOCTYPE html><html lang="es"><head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${escHtml(title)} — AALN Law</title>
|
||||
<style>
|
||||
@page { size: A4 ${orientation}; 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 ${GOLD}; padding-bottom: 8px; }
|
||||
.header-left { display: flex; align-items: center; gap: 10px; }
|
||||
.logo-mark { width: 28px; height: 28px; background: ${NAVY}; border-radius: 5px; display: flex; align-items: center; justify-content: center; color: ${GOLD}; font-weight: 700; font-size: 14px; }
|
||||
.firm-name { font-size: 13px; font-weight: 700; color: ${NAVY}; }
|
||||
.doc-title { font-size: 9px; color: #666; }
|
||||
.meta { font-size: 8px; color: #888; text-align: right; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { background: ${NAVY}; 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; border-bottom: 1px solid #EEEDE9; font-size: 8.5px; vertical-align: middle; }
|
||||
tr.even td { background: #F7F6F3; }
|
||||
.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">${FIRM}</div>
|
||||
<div class="doc-title">${escHtml(title)}${subtitle ? ' — ' + escHtml(subtitle) : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
Exportado: ${escHtml(fechaStr)}<br>
|
||||
Total registros: ${rows.length}
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr>${pdfHeaders.map(h => `<th>${escHtml(h)}</th>`).join('')}</tr></thead>
|
||||
<tbody>${styledRows}</tbody>
|
||||
</table>
|
||||
<div class="footer">${footer}</div>
|
||||
<script>window.onload=()=>{ document.title=${JSON.stringify(fileBase + '_' + todayStamp())}; window.print(); }<\/script>
|
||||
</body></html>`;
|
||||
|
||||
const win = window.open('', '_blank');
|
||||
win.document.write(html);
|
||||
win.document.close();
|
||||
}
|
||||
|
||||
function buttonHtml(id){
|
||||
const wrapId = id || 'export-wrap';
|
||||
return `<div class="export-wrap" id="${wrapId}">
|
||||
<button class="btn-export" type="button" title="Exportar resultados visibles">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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">
|
||||
<button class="xl" type="button">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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 class="pdf" type="button">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>`;
|
||||
}
|
||||
|
||||
window.DonnaExport = { attach, toExcel, toPDF, buttonHtml };
|
||||
})();
|
||||
+1
-31
@@ -19,7 +19,7 @@ $method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
$pdo = Db::pdo();
|
||||
|
||||
if ($method !== 'GET' || !in_array($action, ['list','export'], true)) err('Acción no reconocida', 404);
|
||||
if ($method !== 'GET' || $action !== 'list') err('Acción no reconocida', 404);
|
||||
|
||||
try {
|
||||
$from = clean_date($_GET['from'] ?? null);
|
||||
@@ -175,36 +175,6 @@ try {
|
||||
unset($u);
|
||||
}
|
||||
|
||||
if ($action === 'export') {
|
||||
// CSV con BOM UTF-8 para que Excel lea acentos correctamente
|
||||
$filename = 'horas_' . date('Y-m-d_His') . '.csv';
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
$deptLabels = ['adm'=>'Administrativo', 'corp'=>'Corp · Com · Civil', 'mig'=>'Migración', 'lit'=>'Litigios'];
|
||||
$out = fopen('php://output', 'w');
|
||||
fwrite($out, "\xEF\xBB\xBF");
|
||||
fputcsv($out, ['Fecha', 'Abogado', 'Departamento', 'Código', 'Trámite', 'Cliente', 'Estatus', 'Horas', 'Descripción']);
|
||||
foreach ($entries as $e) {
|
||||
$hrs = (float)$e['hrs'];
|
||||
$full = floor($hrs);
|
||||
$mins = (int)round(($hrs - $full) * 60);
|
||||
$hrsFmt = $mins === 0 ? (string)$full : sprintf('%d:%02d', $full, $mins);
|
||||
fputcsv($out, [
|
||||
date('Y-m-d H:i', strtotime($e['fecha'])),
|
||||
$e['abogado'],
|
||||
$deptLabels[$e['tramite_departamento']] ?? $e['tramite_departamento'],
|
||||
$e['tramite_codigo'],
|
||||
$e['tramite_title'],
|
||||
$e['tramite_cliente'],
|
||||
$e['tramite_estatus'],
|
||||
$hrsFmt,
|
||||
$e['descripcion'],
|
||||
]);
|
||||
}
|
||||
fclose($out);
|
||||
exit;
|
||||
}
|
||||
|
||||
ok([
|
||||
'entries' => $entries,
|
||||
'by_user' => $byUser,
|
||||
|
||||
@@ -283,6 +283,7 @@ footer.legend { text-align:center; padding:14px 24px 24px; font-size:11px; color
|
||||
footer.legend .pill { display:inline-block; padding:2px 8px; border-radius:20px; color:#fff;
|
||||
font-weight:600; margin:0 2px; font-size:10.5px; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="/assets/donna-export.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -342,6 +343,7 @@ footer.legend .pill { display:inline-block; padding:2px 8px; border-radius:20px;
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
|
||||
Nuevo evento
|
||||
</button>
|
||||
<div id="export-mount"></div>
|
||||
</div>
|
||||
|
||||
<div class="cal-kpis">
|
||||
@@ -444,6 +446,8 @@ footer.legend .pill { display:inline-block; padding:2px 8px; border-radius:20px;
|
||||
|
||||
<div class="toast" id="toast"></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 DEPT_LABELS = {adm:'Administrativo', corp:'Corporativo', mig:'Migración', lit:'Litigios'};
|
||||
const SRC_LABELS = {esc:'Escrituras', adm:'Trámites Admin', corp:'Trámites Corp', mig:'Trámites Mig', lit:'Trámites Lit', manual:'Evento'};
|
||||
@@ -1094,6 +1098,45 @@ function renderEventBody(e){
|
||||
return rows.map(([k,v])=>`<div class="row"><div class="k">${k}</div><div class="v">${v}</div></div>`).join('');
|
||||
}
|
||||
|
||||
// ── EXPORT (Calendario) ────────────────────────────────────────────────
|
||||
const CAL_HEADERS = ['Fecha','Hora','Origen','Tipo','Título','Cliente / Ubicación','Abogado / Creador','Estatus','Código'];
|
||||
function calRowToArray(e){
|
||||
const tipoLabel = (e.source === 'manual')
|
||||
? (MANUAL_TIPO_LABELS[e.type] || 'Evento')
|
||||
: (({solicitud:'Solicitud', vencimiento:'Vencimiento', termino:'Término procesal'})[e.type] || e.type);
|
||||
const hora = (e.source === 'manual' && e.manual_hi)
|
||||
? (e.manual_hi.substring(0,5) + (e.manual_hf ? '–'+e.manual_hf.substring(0,5) : ''))
|
||||
: '';
|
||||
const titleBase = (e.source === 'manual') ? (e.manual_titulo || e.title) : e.title;
|
||||
return [
|
||||
e.date, hora,
|
||||
SRC_LABELS[e.source] || e.source,
|
||||
tipoLabel, titleBase,
|
||||
e.cliente || '', e.abogado || '',
|
||||
e.estatus || '', e.codigo || '',
|
||||
];
|
||||
}
|
||||
function calExportRows(){ return visibleEvents().map(calRowToArray); }
|
||||
function calExportExcel(){
|
||||
DonnaExport.toExcel({
|
||||
filename: 'Calendario_AALN', sheetName: 'Eventos',
|
||||
headers: CAL_HEADERS, rows: calExportRows(),
|
||||
columnWidths: [12,14,16,18,38,28,22,16,12],
|
||||
onEmpty: () => toast('No hay eventos para exportar','error'),
|
||||
});
|
||||
}
|
||||
function calExportPDF(){
|
||||
DonnaExport.toPDF({
|
||||
filename: 'Calendario_AALN',
|
||||
title: 'Calendario de Eventos',
|
||||
headers: CAL_HEADERS, rows: calExportRows(),
|
||||
pdfCols: [0,1,2,3,4,5,6,7],
|
||||
onEmpty: () => toast('No hay eventos para exportar','error'),
|
||||
});
|
||||
}
|
||||
document.getElementById('export-mount').innerHTML = DonnaExport.buttonHtml();
|
||||
DonnaExport.attach('export-wrap', { onExcel: calExportExcel, onPDF: calExportPDF });
|
||||
|
||||
document.querySelectorAll('#view-tabs button').forEach(b=>{
|
||||
b.addEventListener('click', ()=> setView(b.dataset.view));
|
||||
});
|
||||
|
||||
+35
-190
@@ -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 & 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á · 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); });
|
||||
|
||||
+37
-15
@@ -111,9 +111,6 @@ header{background:var(--navy);border-bottom:3px solid var(--gold);padding:0 24px
|
||||
.but-n{font-size:9.5px;color:var(--gray-400);text-align:right;}
|
||||
.but-empty{padding:14px 16px;font-size:11.5px;color:var(--gray-400);text-align:center;}
|
||||
|
||||
.btn-export{padding:8px 14px;background:#1e7e3e;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:500;display:inline-flex;align-items:center;gap:6px;}
|
||||
.btn-export:hover{background:#15692f;}
|
||||
.btn-export svg{width:14px;height:14px;}
|
||||
|
||||
.tbl-card{background:#fff;border:1px solid var(--gray-100);border-radius:10px;
|
||||
overflow:hidden;box-shadow:var(--shadow);}
|
||||
@@ -144,6 +141,7 @@ tr a.tramite-link:hover{color:var(--gold-d);text-decoration:underline;}
|
||||
.toast.show{transform:translateY(0);opacity:1;}
|
||||
.toast.error{border-left-color:#ef5350;}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/assets/donna-export.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -203,10 +201,7 @@ tr a.tramite-link:hover{color:var(--gold-d);text-decoration:underline;}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tb-spacer"></div>
|
||||
<button class="btn-export" onclick="exportExcel()" title="Descargar CSV (compatible con Excel)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><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 a Excel
|
||||
</button>
|
||||
<div id="export-mount"></div>
|
||||
<button class="btn-primary" onclick="loadAll()">Aplicar</button>
|
||||
</div>
|
||||
|
||||
@@ -243,6 +238,8 @@ tr a.tramite-link:hover{color:var(--gold-d);text-decoration:underline;}
|
||||
|
||||
<div class="toast" id="toast"></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 MY_EMAIL = (window.DONNA && window.DONNA.me) || '';
|
||||
const MY_NAME = (window.DONNA && window.DONNA.name) || '';
|
||||
@@ -450,15 +447,40 @@ function toggleByUser(safeName){
|
||||
function openTramite(codigo){
|
||||
window.location.href = '/tramites/?edit=' + encodeURIComponent(codigo);
|
||||
}
|
||||
function exportExcel(){
|
||||
const params = new URLSearchParams({
|
||||
action: 'export',
|
||||
from: $('f-from').value || '',
|
||||
to: $('f-to').value || '',
|
||||
user: ($('f-user') && $('f-user').value) || '0',
|
||||
});
|
||||
window.location.href = '/horas/api.php?' + params.toString();
|
||||
const HORAS_HEADERS = ['Fecha','Abogado','Departamento','Código','Trámite','Cliente','Estatus','Horas','Descripción'];
|
||||
const HORAS_WIDTHS = [16,22,18,12,30,22,16,8,38];
|
||||
const DEPT_LONG = { adm:'Administrativo', corp:'Corp · Com · Civil', mig:'Migración', lit:'Litigios' };
|
||||
function horasExportRows(){
|
||||
return (data && data.entries || []).map(e => [
|
||||
(e.fecha || '').substring(0,16).replace('T',' '),
|
||||
e.abogado || '',
|
||||
DEPT_LONG[e.tramite_departamento] || e.tramite_departamento || '',
|
||||
e.tramite_codigo || '',
|
||||
e.tramite_title || '',
|
||||
e.tramite_cliente || '',
|
||||
e.tramite_estatus || '',
|
||||
fmtHours(e.hrs),
|
||||
e.descripcion || '',
|
||||
]);
|
||||
}
|
||||
function exportExcel(){
|
||||
DonnaExport.toExcel({
|
||||
filename: 'Reporte_Horas_AALN', sheetName: 'Horas',
|
||||
headers: HORAS_HEADERS, rows: horasExportRows(), columnWidths: HORAS_WIDTHS,
|
||||
onEmpty: () => toast('No hay entradas para exportar','error'),
|
||||
});
|
||||
}
|
||||
function exportPDF(){
|
||||
DonnaExport.toPDF({
|
||||
filename: 'Reporte_Horas_AALN',
|
||||
title: 'Reporte de Horas',
|
||||
headers: HORAS_HEADERS, rows: horasExportRows(),
|
||||
pdfCols: [0,1,3,4,5,7,8], // Fecha, Abogado, Código, Trámite, Cliente, Horas, Descripción
|
||||
onEmpty: () => toast('No hay entradas para exportar','error'),
|
||||
});
|
||||
}
|
||||
document.getElementById('export-mount').innerHTML = DonnaExport.buttonHtml();
|
||||
DonnaExport.attach('export-wrap', { onExcel: exportExcel, onPDF: exportPDF });
|
||||
|
||||
['f-from','f-to'].forEach(id => {
|
||||
$(id).addEventListener('change', loadAll);
|
||||
|
||||
@@ -140,6 +140,7 @@ tr.expanded>td{background:#fffdf5;}
|
||||
.toast.show{transform:translateY(0);opacity:1;}
|
||||
.toast.error{border-left-color:#ef5350;}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/assets/donna-export.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -213,6 +214,8 @@ tr.expanded>td{background:#fffdf5;}
|
||||
|
||||
<div id="toast" class="toast"></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 USERS = (window.DONNA && window.DONNA.users) || [];
|
||||
@@ -391,6 +394,8 @@ function renderPanel(dept){
|
||||
const activos=rows.filter(r=>r.estatus!=='Culminado'&&r.estatus!=='Facturado').length;
|
||||
const culminados=rows.filter(r=>r.estatus==='Culminado'||r.estatus==='Facturado').length;
|
||||
|
||||
window.__tramFiltered = window.__tramFiltered || {};
|
||||
window.__tramFiltered[dept] = filtered;
|
||||
panel.innerHTML=`
|
||||
<div class="counts">
|
||||
<div class="count-card"><b>${total}</b>Total</div>
|
||||
@@ -412,6 +417,7 @@ function renderPanel(dept){
|
||||
<option${fEsfera==='Civil'?' selected':''}>Civil</option><option${fEsfera==='Penal'?' selected':''}>Penal</option>
|
||||
</select>`:''}
|
||||
${CAN_CREATE ? `<button class="btn btn-primary" onclick="openModal('${dept}',null)">+ Nuevo</button>` : ''}
|
||||
${DonnaExport.buttonHtml('export-wrap-'+dept)}
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table>
|
||||
@@ -421,6 +427,50 @@ function renderPanel(dept){
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
DonnaExport.attach('export-wrap-'+dept, {
|
||||
onExcel: () => tramExportExcel(dept),
|
||||
onPDF: () => tramExportPDF(dept),
|
||||
});
|
||||
}
|
||||
|
||||
// ── EXPORT (Trámites) ────────────────────────────────────────────────────
|
||||
const TRAM_HEADERS = {
|
||||
adm: ['Código','Institución','Nombre Trámite','Cliente','Abogado','Involucrados','Estatus','Fecha Solicitud','Horas'],
|
||||
corp: ['Código','Tipo Documento','Nombre Trámite','Cliente','Abogado','Involucrados','Estatus','Fecha Solicitud','Horas'],
|
||||
mig: ['Código','Tipo Trámite','Cliente','Abogado','Involucrados','Doc. Completa','Permiso Trabajo','Dependientes','Estatus','Fecha Solicitud','Vencimiento','Horas'],
|
||||
lit: ['Código','Proceso','Juzgado','Cliente','Abogado','Involucrados','Esfera','Instancia','Doc. Completa','Incidentes','Amparo','Estatus','Fecha Solicitud','Término','Horas'],
|
||||
};
|
||||
function fmtDateOnly(s){ return s ? String(s).substring(0,10) : ''; }
|
||||
function invsCSV(invs){ return (invs||[]).map(u => u.name || u.email).join(', '); }
|
||||
function tramRowToArray(dept, r){
|
||||
const inv = invsCSV(r.involucrados);
|
||||
if (dept === 'adm') return [r.codigo, r.institucion||'', r.nombre||'', r.cliente||'', r.abogado||'', inv, r.estatus||'', fmtDateOnly(r.fecha_solicitud), totalHours(r)];
|
||||
if (dept === 'corp')return [r.codigo, r.tipo_documento||'', r.nombre||'', r.cliente||'', r.abogado||'', inv, r.estatus||'', fmtDateOnly(r.fecha_solicitud), totalHours(r)];
|
||||
if (dept === 'mig') return [r.codigo, r.tipo_tramite||'', r.cliente||'', r.abogado||'', inv, r.doc_completa||'', r.permiso_trabajo||'', r.dependientes||'', r.estatus||'', fmtDateOnly(r.fecha_solicitud), fmtDateOnly(r.fecha_vencimiento), totalHours(r)];
|
||||
if (dept === 'lit') return [r.codigo, r.tipo_proceso||'', r.juzgado||'', r.cliente||'', r.abogado||'', inv, r.esfera||'', r.instancia||'', r.doc_completa||'', r.incidentes||'', r.amparo||'', r.estatus||'', fmtDateOnly(r.fecha_solicitud), fmtDateOnly(r.fecha_termino), totalHours(r)];
|
||||
}
|
||||
function tramExportExcel(dept){
|
||||
const rows = (window.__tramFiltered && window.__tramFiltered[dept]) || data[dept] || [];
|
||||
DonnaExport.toExcel({
|
||||
filename: 'Tramites_'+dept.toUpperCase()+'_AALN',
|
||||
sheetName: DEPT_LABELS[dept] || dept,
|
||||
headers: TRAM_HEADERS[dept],
|
||||
rows: rows.map(r => tramRowToArray(dept, r)),
|
||||
onEmpty: () => toast('No hay datos para exportar','error'),
|
||||
});
|
||||
}
|
||||
function tramExportPDF(dept){
|
||||
const rows = (window.__tramFiltered && window.__tramFiltered[dept]) || data[dept] || [];
|
||||
// En PDF excluye Involucrados (lista larga) para que quepa
|
||||
const cols = TRAM_HEADERS[dept].map((_,i) => i).filter(i => TRAM_HEADERS[dept][i] !== 'Involucrados');
|
||||
DonnaExport.toPDF({
|
||||
filename: 'Tramites_'+dept.toUpperCase()+'_AALN',
|
||||
title: 'Control de Trámites Legales · ' + (DEPT_LABELS[dept] || dept),
|
||||
headers: TRAM_HEADERS[dept],
|
||||
rows: rows.map(r => tramRowToArray(dept, r)),
|
||||
pdfCols: cols,
|
||||
onEmpty: () => toast('No hay datos para exportar','error'),
|
||||
});
|
||||
}
|
||||
|
||||
function buildHeaders(dept){
|
||||
|
||||
Reference in New Issue
Block a user