- 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
495 lines
23 KiB
HTML
495 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Reporte de Horas — AALN Donna</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--navy:#1B3A6B; --navy-d:#112446; --navy-l:#2D5090;
|
|
--gold:#C9A84C; --gold-l:#E8C96A; --gold-d:#9E7C2E;
|
|
--off:#F7F6F3;
|
|
--gray-100:#EEEDE9; --gray-200:#D6D4CC; --gray-400:#9B9A96;
|
|
--gray-700:#4A4946; --gray-900:#1A1916;
|
|
--red:#c0392b; --orange:#e67e22; --green:#27ae60;
|
|
--shadow:0 1px 3px rgba(0,0,0,.06),0 4px 16px rgba(0,0,0,.06);
|
|
}
|
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
|
body{font-family:'DM Sans',system-ui,sans-serif;color:var(--gray-900);background:var(--off);font-size:13.5px;}
|
|
a{color:inherit;text-decoration:none;}
|
|
button{font-family:inherit;cursor:pointer;}
|
|
|
|
header{background:var(--navy);border-bottom:3px solid var(--gold);padding:0 24px;
|
|
display:flex;align-items:center;justify-content:space-between;height:58px;
|
|
position:sticky;top:0;z-index:200;}
|
|
.header-left{display:flex;align-items:center;gap:14px;}
|
|
.btn-back{display:inline-flex;align-items:center;gap:6px;
|
|
background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.18);
|
|
color:rgba(255,255,255,.9);padding:6px 12px 6px 9px;border-radius:8px;
|
|
font-size:12px;transition:all .15s;white-space:nowrap;}
|
|
.btn-back:hover{background:var(--gold);border-color:var(--gold);color:var(--navy);}
|
|
.btn-back svg{width:14px;height:14px;}
|
|
.logo{display:flex;align-items:center;gap:14px;}
|
|
.logo-mark{width:32px;height:32px;background:var(--gold);border-radius:6px;
|
|
display:flex;align-items:center;justify-content:center;
|
|
font-family:'DM Serif Display',serif;font-size:15px;color:var(--navy);}
|
|
.logo-text{display:flex;flex-direction:column;line-height:1.2;}
|
|
.logo-firm{font-size:11px;font-weight:600;color:var(--gold);letter-spacing:.08em;text-transform:uppercase;}
|
|
.logo-tool{font-size:13px;font-weight:400;color:rgba(255,255,255,.85);}
|
|
.header-right{display:flex;align-items:center;gap:12px;}
|
|
.user-chip{display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.1);
|
|
border:1px solid rgba(255,255,255,.15);border-radius:20px;padding:5px 12px 5px 6px;}
|
|
.user-avatar{width:24px;height:24px;border-radius:50%;background:var(--gold);color:var(--navy);
|
|
display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;}
|
|
.user-name{font-size:12px;color:rgba(255,255,255,.85);max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
.btn-logout{font-size:11px;padding:4px 10px;border:1px solid rgba(255,255,255,.2);border-radius:6px;
|
|
background:transparent;color:rgba(255,255,255,.7);transition:all .15s;text-decoration:none;}
|
|
.btn-logout:hover{background:rgba(255,255,255,.1);color:#fff;}
|
|
.btn-gear{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;
|
|
border:1px solid rgba(255,255,255,0.18);border-radius:8px;color:rgba(255,255,255,0.85);
|
|
transition:all .15s;text-decoration:none;}
|
|
.btn-gear:hover{background:var(--gold);border-color:var(--gold);color:var(--navy);}
|
|
.btn-gear svg{width:16px;height:16px;}
|
|
|
|
.page{max-width:1180px;margin:0 auto;padding:24px;}
|
|
.intro h1{font-family:'DM Serif Display',serif;font-size:30px;color:var(--navy);font-weight:400;margin-bottom:6px;}
|
|
.intro p{font-size:13.5px;color:var(--gray-700);line-height:1.55;max-width:780px;margin-bottom:14px;}
|
|
|
|
.toolbar{background:#fff;border:1px solid var(--gray-100);border-radius:10px;padding:14px 18px;
|
|
display:flex;gap:14px;flex-wrap:wrap;align-items:flex-end;box-shadow:var(--shadow);margin-bottom:18px;}
|
|
.tb-group{display:flex;flex-direction:column;gap:4px;}
|
|
.tb-group label{font-size:10.5px;font-weight:600;color:var(--gray-400);
|
|
text-transform:uppercase;letter-spacing:.08em;}
|
|
.tb-group input,.tb-group select{padding:7px 11px;border:1px solid var(--gray-200);
|
|
border-radius:8px;font-family:inherit;font-size:12.5px;background:#fff;}
|
|
.tb-group input:focus,.tb-group select:focus{outline:none;border-color:var(--navy-l);box-shadow:0 0 0 3px rgba(27,58,107,.08);}
|
|
.tb-quick{display:flex;gap:6px;flex-wrap:wrap;align-items:flex-end;}
|
|
.quick-btn{font-size:11px;padding:6px 10px;background:var(--off);border:1px solid var(--gray-200);
|
|
border-radius:6px;color:var(--gray-700);transition:all .15s;}
|
|
.quick-btn:hover{background:var(--navy);color:#fff;border-color:var(--navy);}
|
|
.tb-spacer{flex:1;}
|
|
.btn-primary{padding:8px 18px;background:var(--navy);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:500;}
|
|
.btn-primary:hover{background:var(--navy-d);}
|
|
|
|
.kpi-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin-bottom:18px;}
|
|
.kpi{background:#fff;border:1px solid var(--gray-100);border-radius:10px;padding:14px 18px;
|
|
border-left:4px solid var(--navy);box-shadow:var(--shadow);}
|
|
.kpi-label{font-size:10.5px;font-weight:600;color:var(--gray-400);text-transform:uppercase;letter-spacing:.07em;}
|
|
.kpi-val{font-family:'DM Serif Display',serif;font-size:30px;color:var(--navy);line-height:1.1;margin-top:2px;}
|
|
.kpi-sub{font-size:11px;color:var(--gray-400);margin-top:2px;}
|
|
|
|
.section-label{font-size:11px;font-weight:600;color:var(--gray-400);letter-spacing:.12em;
|
|
text-transform:uppercase;margin:18px 0 10px;display:flex;align-items:center;gap:14px;}
|
|
.section-label::after{content:'';flex:1;height:1px;background:var(--gray-200);}
|
|
|
|
.by-user{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;margin-bottom:18px;align-items:start;}
|
|
.by-user-card{background:#fff;border:1px solid var(--gray-100);border-radius:10px;
|
|
box-shadow:var(--shadow);transition:all .15s;border-left:4px solid var(--gold);overflow:hidden;}
|
|
.by-user-card.expanded{border-left-color:var(--navy);background:#FFFBEB;}
|
|
.by-user-head{padding:12px 16px;cursor:pointer;display:flex;align-items:center;gap:10px;transition:background .15s;}
|
|
.by-user-head:hover{background:#F9FAFC;}
|
|
.by-user-card.expanded .by-user-head{background:#FFFBEB;}
|
|
.by-user-info{flex:1;min-width:0;}
|
|
.by-user-name{font-family:'DM Serif Display',serif;font-size:16px;color:var(--navy);line-height:1.2;}
|
|
.by-user-total{font-size:20px;font-weight:700;color:var(--gray-900);margin-top:4px;}
|
|
.by-user-entries{font-size:11px;color:var(--gray-400);margin-top:2px;}
|
|
.by-user-chev{width:14px;height:14px;color:var(--gray-400);transition:transform .15s;flex-shrink:0;}
|
|
.by-user-card.expanded .by-user-chev{transform:rotate(90deg);color:var(--navy);}
|
|
.by-user-tramites{display:none;border-top:1px solid var(--gray-100);background:#fff;}
|
|
.by-user-card.expanded .by-user-tramites{display:block;}
|
|
.but-row{display:grid;grid-template-columns:1fr auto;gap:8px;padding:9px 16px;border-bottom:1px solid var(--gray-100);
|
|
font-size:12px;cursor:pointer;transition:background .12s;align-items:center;}
|
|
.but-row:hover{background:#F9FAFC;}
|
|
.but-row:last-child{border-bottom:none;}
|
|
.but-tcode{font-weight:600;color:var(--navy);font-size:11.5px;}
|
|
.but-ttitle{color:var(--gray-700);font-size:11.5px;margin-top:1px;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;}
|
|
.but-tcli{font-size:10.5px;color:var(--gray-400);margin-top:1px;}
|
|
.but-hrs{font-family:'DM Serif Display',serif;font-size:14px;color:var(--navy);font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap;}
|
|
.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;}
|
|
|
|
|
|
.tbl-card{background:#fff;border:1px solid var(--gray-100);border-radius:10px;
|
|
overflow:hidden;box-shadow:var(--shadow);}
|
|
table{width:100%;border-collapse:collapse;font-size:12.5px;}
|
|
th{background:var(--navy);color:rgba(255,255,255,.9);padding:10px 12px;text-align:left;
|
|
font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.07em;
|
|
white-space:nowrap;border-right:1px solid rgba(255,255,255,.06);}
|
|
th:last-child{border-right:none;}
|
|
td{padding:9px 12px;border-bottom:1px solid var(--gray-100);color:var(--gray-700);vertical-align:top;}
|
|
tr:last-child td{border-bottom:none;}
|
|
tr:hover td{background:#F9FAFC;}
|
|
tr a.tramite-link{color:var(--navy);font-weight:600;}
|
|
tr a.tramite-link:hover{color:var(--gold-d);text-decoration:underline;}
|
|
.dept-tag{display:inline-block;padding:1px 7px;border-radius:10px;font-size:9.5px;font-weight:700;
|
|
text-transform:uppercase;letter-spacing:.06em;color:#fff;margin-left:4px;}
|
|
.dept-adm{background:#6c5b95;} .dept-corp{background:#9E7C2E;}
|
|
.dept-mig{background:#2980b9;} .dept-lit{background:#722f37;}
|
|
.hrs-cell{font-family:'DM Serif Display',serif;font-size:14px;color:var(--navy);font-variant-numeric:tabular-nums;white-space:nowrap;}
|
|
.user-cell{font-weight:500;color:var(--gray-900);}
|
|
.desc-cell{color:var(--gray-700);max-width:300px;}
|
|
.fecha-cell{font-size:11px;color:var(--gray-400);white-space:nowrap;}
|
|
.empty{text-align:center;padding:60px 20px;color:var(--gray-400);font-size:13px;}
|
|
|
|
.toast{position:fixed;bottom:24px;right:24px;background:var(--navy-d);color:#fff;
|
|
padding:11px 18px;border-radius:8px;font-size:12.5px;border-left:3px solid var(--gold);
|
|
box-shadow:0 4px 20px rgba(0,0,0,.2);transform:translateY(20px);opacity:0;
|
|
transition:all .25s;z-index:2000;}
|
|
.toast.show{transform:translateY(0);opacity:1;}
|
|
.toast.error{border-left-color:#ef5350;}
|
|
</style>
|
|
<link rel="stylesheet" href="/assets/donna-export.css">
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<div class="header-left">
|
|
<a class="btn-back" href="/" title="Volver al portal">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
|
Portal
|
|
</a>
|
|
<div class="logo">
|
|
<div class="logo-mark">A</div>
|
|
<div class="logo-text">
|
|
<span class="logo-firm">AALN Law</span>
|
|
<span class="logo-tool">Reporte de Horas</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="header-right">
|
|
<div class="user-chip">
|
|
<div class="user-avatar" id="user-avatar">?</div>
|
|
<span class="user-name" id="user-name">…</span>
|
|
</div>
|
|
<a class="btn-logout" href="/auth/logout.php" onclick="return confirm('¿Cerrar sesión?');">Cerrar sesión</a>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="page">
|
|
|
|
<div class="intro">
|
|
<h1>Reporte de Horas</h1>
|
|
<p id="intro-text">…</p>
|
|
</div>
|
|
|
|
<div class="toolbar">
|
|
<div class="tb-group">
|
|
<label>Desde</label>
|
|
<input type="date" id="f-from">
|
|
</div>
|
|
<div class="tb-group">
|
|
<label>Hasta</label>
|
|
<input type="date" id="f-to">
|
|
</div>
|
|
<div class="tb-group" id="user-filter-wrap" style="display:none;">
|
|
<label>Usuario</label>
|
|
<select id="f-user">
|
|
<option value="0">— Todos —</option>
|
|
</select>
|
|
</div>
|
|
<div class="tb-group">
|
|
<label> </label>
|
|
<div class="tb-quick">
|
|
<button class="quick-btn" onclick="setQuick('today')">Hoy</button>
|
|
<button class="quick-btn" onclick="setQuick('week')">Esta semana</button>
|
|
<button class="quick-btn" onclick="setQuick('month')">Este mes</button>
|
|
<button class="quick-btn" onclick="setQuick('year')">Este año</button>
|
|
<button class="quick-btn" onclick="setQuick('all')">Todo</button>
|
|
</div>
|
|
</div>
|
|
<div class="tb-spacer"></div>
|
|
<div id="export-mount"></div>
|
|
<button class="btn-primary" onclick="loadAll()">Aplicar</button>
|
|
</div>
|
|
|
|
<div class="kpi-strip">
|
|
<div class="kpi"><div class="kpi-label">Total horas</div><div class="kpi-val" id="k-total">—</div><div class="kpi-sub" id="k-total-sub">—</div></div>
|
|
<div class="kpi"><div class="kpi-label">Entradas</div><div class="kpi-val" id="k-entries">—</div><div class="kpi-sub">registros de horas</div></div>
|
|
<div class="kpi" id="kpi-users" style="display:none;"><div class="kpi-label">Personas con horas</div><div class="kpi-val" id="k-users">—</div><div class="kpi-sub">en el período</div></div>
|
|
</div>
|
|
|
|
<div id="by-user-section" style="display:none;">
|
|
<div class="section-label">Por persona</div>
|
|
<div class="by-user" id="by-user"></div>
|
|
</div>
|
|
|
|
<div class="section-label">Detalle de entradas <span id="detail-count" style="margin-left:auto;font-size:11px;color:var(--gray-400);"></span></div>
|
|
<div class="tbl-card">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Fecha</th>
|
|
<th>Trámite</th>
|
|
<th>Cliente</th>
|
|
<th id="th-abogado">Abogado</th>
|
|
<th>Horas</th>
|
|
<th>Descripción</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="tbody"></tbody>
|
|
</table>
|
|
<div id="empty" class="empty" style="display:none;">Sin entradas de horas en el período seleccionado.</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<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) || '';
|
|
const IS_SOCIO = !!(window.DONNA && window.DONNA.isSocio);
|
|
|
|
let data = null;
|
|
let authUser = { email: MY_EMAIL, name: MY_NAME };
|
|
|
|
const DEPT_LABEL = { adm:'Adm', corp:'Corp', mig:'Mig', lit:'Lit' };
|
|
|
|
function $(id){ return document.getElementById(id); }
|
|
function esc(s){ return (s==null?'':String(s)).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
|
function toast(msg, type='ok'){
|
|
const el=$('toast'); el.textContent=msg;
|
|
el.className='toast show'+(type==='error'?' error':'');
|
|
setTimeout(()=>el.className='toast'+(type==='error'?' error':''),3000);
|
|
}
|
|
function initials(text){
|
|
const parts=String(text||'').replace(/[@._-]/g,' ').trim().split(/\s+/);
|
|
return ((parts[0]||'').charAt(0)+(parts[1]||'').charAt(0)).toUpperCase()||'?';
|
|
}
|
|
function fmtHours(decimal){
|
|
const n = Math.round((parseFloat(decimal)||0) * 4) / 4;
|
|
const h = Math.floor(n);
|
|
const m = Math.round((n - h) * 60);
|
|
return m === 0 ? String(h) : `${h}:${String(m).padStart(2,'0')}`;
|
|
}
|
|
function fmtDate(dt){
|
|
if(!dt) return '—';
|
|
const d=new Date(dt); if(isNaN(d)) return dt;
|
|
return d.toLocaleDateString('es-PA',{day:'2-digit',month:'2-digit',year:'numeric'})
|
|
+' '+d.toLocaleTimeString('es-PA',{hour:'2-digit',minute:'2-digit'});
|
|
}
|
|
function ymd(d){
|
|
const y=d.getFullYear(), m=String(d.getMonth()+1).padStart(2,'0'), dd=String(d.getDate()).padStart(2,'0');
|
|
return `${y}-${m}-${dd}`;
|
|
}
|
|
|
|
function setUserChip(){
|
|
$('user-avatar').textContent = initials(authUser.name || authUser.email);
|
|
$('user-name').textContent = authUser.name || authUser.email || '—';
|
|
$('user-name').title = authUser.email;
|
|
}
|
|
|
|
function defaultDates(){
|
|
const now = new Date();
|
|
const first = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
$('f-from').value = ymd(first);
|
|
$('f-to').value = ymd(now);
|
|
}
|
|
|
|
function setQuick(period){
|
|
const now = new Date();
|
|
let from, to;
|
|
if (period === 'today') { from = now; to = now; }
|
|
if (period === 'week') { const d = now.getDay() || 7; from = new Date(now); from.setDate(now.getDate() - d + 1); to = now; }
|
|
if (period === 'month') { from = new Date(now.getFullYear(), now.getMonth(), 1); to = now; }
|
|
if (period === 'year') { from = new Date(now.getFullYear(), 0, 1); to = now; }
|
|
if (period === 'all') { $('f-from').value = ''; $('f-to').value = ''; loadAll(); return; }
|
|
$('f-from').value = ymd(from);
|
|
$('f-to').value = ymd(to);
|
|
loadAll();
|
|
}
|
|
|
|
async function apiCall(qs){
|
|
const url = '/horas/api.php?action=list' + (qs?'&'+qs:'');
|
|
const res = await fetch(url, {headers:{Accept:'application/json'}, credentials:'same-origin'});
|
|
let j;
|
|
try{ j = await res.json(); } catch(e){ throw new Error('JSON inválido'); }
|
|
if(res.status===401 && j.auth_required){
|
|
window.location.href = j.login_url || '/auth/login.php?return_to=/horas/';
|
|
return null;
|
|
}
|
|
if(!res.ok || !j.ok) throw new Error(j.error || ('HTTP '+res.status));
|
|
if(j.user) authUser = j.user;
|
|
return j.data;
|
|
}
|
|
|
|
async function loadAll(){
|
|
const from = $('f-from').value;
|
|
const to = $('f-to').value;
|
|
const user = IS_SOCIO ? ($('f-user') ? $('f-user').value : '0') : '0';
|
|
const params = new URLSearchParams();
|
|
if (from) params.set('from', from);
|
|
if (to) params.set('to', to);
|
|
if (user && user !== '0') params.set('user', user);
|
|
try {
|
|
data = await apiCall(params.toString());
|
|
if (!data) return;
|
|
setUserChip();
|
|
render();
|
|
} catch(e){
|
|
toast('Error: '+e.message, 'error');
|
|
}
|
|
}
|
|
|
|
function populateUserFilter(){
|
|
if (!data || !data.is_socio) return;
|
|
const wrap = $('user-filter-wrap');
|
|
const sel = $('f-user');
|
|
wrap.style.display = '';
|
|
const prev = sel.value;
|
|
sel.innerHTML = '<option value="0">— Todos —</option>' +
|
|
(data.avail_users||[]).map(u => `<option value="${u.id}">${esc(u.name)}</option>`).join('');
|
|
sel.value = prev;
|
|
}
|
|
|
|
function render(){
|
|
if (!data) return;
|
|
populateUserFilter();
|
|
|
|
const introText = data.is_socio
|
|
? 'Visualiza las horas registradas por todos los abogados y usuarios. Filtra por rango y por persona.'
|
|
: ('Estas son tus horas registradas (donde figuras como ' + esc(data.my_name||'—') + ' en el registro de horas).');
|
|
$('intro-text').textContent = data.note || introText;
|
|
|
|
$('k-total').textContent = fmtHours(data.total_hrs || 0);
|
|
$('k-total-sub').textContent = (data.total_hrs || 0).toFixed(2) + ' h decimal';
|
|
$('k-entries').textContent = (data.entries||[]).length;
|
|
if (data.is_socio) {
|
|
$('kpi-users').style.display = '';
|
|
$('k-users').textContent = (data.by_user||[]).length;
|
|
}
|
|
|
|
const sectByUser = $('by-user-section');
|
|
if (data.is_socio && (data.by_user||[]).length) {
|
|
sectByUser.style.display = '';
|
|
$('by-user').innerHTML = data.by_user.map(bu => byUserCardHtml(bu, bu.name)).join('');
|
|
} else if (!data.is_socio && (data.by_user_tramite||[]).length) {
|
|
// Abogado/usuario regular: muestra su propia tarjeta colapsable
|
|
sectByUser.style.display = '';
|
|
const myTotal = (data.by_user_tramite||[]).reduce((a,r)=>a+(+r.total||0),0);
|
|
const myEntries = (data.by_user_tramite||[]).reduce((a,r)=>a+(+r.entries||0),0);
|
|
$('by-user').innerHTML = byUserCardHtml({name:data.my_name, total:myTotal, entries:myEntries}, data.my_name);
|
|
} else {
|
|
sectByUser.style.display = 'none';
|
|
}
|
|
|
|
$('detail-count').textContent = (data.entries||[]).length + ' ' + ((data.entries||[]).length === 1 ? 'entrada' : 'entradas');
|
|
$('th-abogado').style.display = data.is_socio ? '' : 'none';
|
|
const tbody = $('tbody');
|
|
if (!(data.entries||[]).length) {
|
|
tbody.innerHTML = '';
|
|
$('empty').style.display = 'block';
|
|
return;
|
|
}
|
|
$('empty').style.display = 'none';
|
|
tbody.innerHTML = data.entries.map(e => {
|
|
const dept = e.tramite_departamento;
|
|
const tramiteLink = e.tramite_codigo
|
|
? `<a class="tramite-link" href="/tramites/?edit=${encodeURIComponent(e.tramite_codigo)}" title="Abrir en Trámites">${esc(e.tramite_codigo)}</a>`
|
|
: '—';
|
|
const abogadoCol = data.is_socio
|
|
? `<td class="user-cell">${esc(e.abogado||'—')}</td>`
|
|
: '';
|
|
return `<tr>
|
|
<td class="fecha-cell">${esc(fmtDate(e.fecha))}</td>
|
|
<td>${tramiteLink} <span class="dept-tag dept-${dept}">${DEPT_LABEL[dept]||dept}</span><br><small style="color:var(--gray-400)">${esc(e.tramite_title||'—')}</small></td>
|
|
<td>${esc(e.tramite_cliente||'—')}</td>
|
|
${abogadoCol}
|
|
<td class="hrs-cell">${fmtHours(e.hrs)}</td>
|
|
<td class="desc-cell">${esc(e.descripcion||'—')}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
function selectUser(id){
|
|
const sel = $('f-user');
|
|
if (sel) { sel.value = String(id); loadAll(); }
|
|
}
|
|
|
|
function byUserCardHtml(bu, abogadoName){
|
|
const tramites = ((data && data.by_user_tramite)||[]).filter(r => r.name === abogadoName);
|
|
const safeName = (abogadoName||'').replace(/[^a-zA-Z0-9_]/g,'_');
|
|
const trHtml = tramites.length
|
|
? tramites.map(t => `
|
|
<div class="but-row" onclick="event.stopPropagation();openTramite('${esc(t.tramite_codigo)}')">
|
|
<div>
|
|
<div class="but-tcode">${esc(t.tramite_codigo)} <span class="dept-tag dept-${t.departamento}">${DEPT_LABEL[t.departamento]||t.departamento}</span></div>
|
|
<div class="but-ttitle">${esc(t.tramite_title||'—')}</div>
|
|
<div class="but-tcli">${esc(t.tramite_cliente||'—')}</div>
|
|
</div>
|
|
<div>
|
|
<div class="but-hrs">${fmtHours(t.total)} h</div>
|
|
<div class="but-n">${t.entries} ${t.entries===1?'entrada':'entradas'}</div>
|
|
</div>
|
|
</div>`).join('')
|
|
: `<div class="but-empty">Sin desglose por trámite en este período.</div>`;
|
|
return `<div class="by-user-card" id="buc-${safeName}">
|
|
<div class="by-user-head" onclick="toggleByUser('${safeName}')">
|
|
<div class="by-user-info">
|
|
<div class="by-user-name">${esc(bu.name)}</div>
|
|
<div class="by-user-total">${fmtHours(bu.total)} h</div>
|
|
<div class="by-user-entries">${bu.entries} ${bu.entries===1?'entrada':'entradas'} · ${tramites.length} ${tramites.length===1?'trámite':'trámites'}</div>
|
|
</div>
|
|
<svg class="by-user-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
|
</div>
|
|
<div class="by-user-tramites">${trHtml}</div>
|
|
</div>`;
|
|
}
|
|
function toggleByUser(safeName){
|
|
const card = $('buc-'+safeName);
|
|
if (card) card.classList.toggle('expanded');
|
|
}
|
|
function openTramite(codigo){
|
|
window.location.href = '/tramites/?edit=' + encodeURIComponent(codigo);
|
|
}
|
|
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);
|
|
});
|
|
$('f-user') && $('f-user').addEventListener('change', loadAll);
|
|
|
|
defaultDates();
|
|
loadAll();
|
|
</script>
|
|
</body>
|
|
</html>
|