feat(tramites): Horas Incurridas en bloques de 15 min con formato h:mm
* Input de horas pasa de <input type=number step=0.1> a un <select> con 48 opciones de 0:15 a 12:00 en bloques de 15 minutos. Valor enviado al backend sigue siendo decimal (0.25 = 15 min, 1.5 = 1h30m). * fmtHours() formatea decimal → "h" para horas enteras o "h:mm" para fraccionarias. Snap a 15 min para tolerar datos legacy. * Display unificado: - Columna "Horas" en la tabla: muestra total en formato h:mm - Lista de entradas en detalle: cada entrada con su tiempo formateado - "Total" del card de Horas: en formato h:mm * Aplica a los 4 departamentos.
This commit is contained in:
+27
-9
@@ -438,9 +438,27 @@ function statusSlug(s){return (s||'').toLowerCase().replace(/\s+/g,'').replace(/
|
||||
function statusBadge(s){return `<span class="badge b-${statusSlug(s)}">${esc(s)}</span>`;}
|
||||
function yesnoBadge(v){return v?`<span class="badge ${v==='Sí'?'b-si':'b-no'}">${esc(v)}</span>`:'—';}
|
||||
|
||||
// Formatea decimal de horas como "h" o "h:mm" (bloques de 15 min).
|
||||
// Ej: 0.25 → "0:15", 1 → "1", 1.5 → "1:30", 2.75 → "2:45"
|
||||
function fmtHours(decimal){
|
||||
const n = Math.round((parseFloat(decimal)||0) * 4) / 4; // snap a 15 min
|
||||
const h = Math.floor(n);
|
||||
const m = Math.round((n - h) * 60);
|
||||
return m === 0 ? String(h) : `${h}:${String(m).padStart(2,'0')}`;
|
||||
}
|
||||
function totalHours(r){
|
||||
if(!r.hours||!r.hours.length) return 0;
|
||||
return r.hours.reduce((s,h)=>s+(parseFloat(h.hrs)||0),0).toFixed(1);
|
||||
if(!r.hours||!r.hours.length) return '0';
|
||||
const total = r.hours.reduce((s,h)=>s+(parseFloat(h.hrs)||0),0);
|
||||
return fmtHours(total);
|
||||
}
|
||||
// Opciones del dropdown de horas: bloques de 15 min de 0:15 a 12:00
|
||||
function hourBlockOptions(){
|
||||
let out = '';
|
||||
for (let q = 1; q <= 48; q++) {
|
||||
const value = q * 0.25;
|
||||
out += `<option value="${value}">${fmtHours(value)}</option>`;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function venceAlert(fecha){
|
||||
@@ -454,13 +472,13 @@ function venceAlert(fecha){
|
||||
function buildRow(dept,r){
|
||||
let cells='';
|
||||
const invCell = `<td>${involucradosCell(r.involucrados)}</td>`;
|
||||
if(dept==='adm') cells=`<td>${esc(r.institucion)||'—'}</td><td>${esc(r.nombre)||'—'}</td><td>${esc(r.cliente)||'—'}</td><td>${esc(r.abogado)||'—'}</td>${invCell}<td>${statusBadge(r.estatus)}</td><td>${esc(r.fechaSolicitud)||'—'}</td><td>${totalHours(r)} h</td>`;
|
||||
if(dept==='corp') cells=`<td>${esc(r.tipoDocumento)||'—'}</td><td>${esc(r.nombre)||'—'}</td><td>${esc(r.cliente)||'—'}</td><td>${esc(r.abogado)||'—'}</td>${invCell}<td>${statusBadge(r.estatus)}</td><td>${esc(r.fechaSolicitud)||'—'}</td><td>${totalHours(r)} h</td>`;
|
||||
if(dept==='mig') cells=`<td>${esc(r.tipoTramite)||'—'}</td><td>${esc(r.cliente)||'—'}</td><td>${esc(r.abogado)||'—'}</td>${invCell}<td>${yesnoBadge(r.docCompleta)}</td><td>${yesnoBadge(r.permisoTrabajo)}</td><td>${yesnoBadge(r.dependientes)}</td><td>${statusBadge(r.estatus)}</td><td>${esc(r.fechaSolicitud)||'—'}</td><td>${fmtDateOnly(r.fechaVencimiento)||'—'}${venceAlert(r.fechaVencimiento)}</td><td>${totalHours(r)} h</td>`;
|
||||
if(dept==='adm') cells=`<td>${esc(r.institucion)||'—'}</td><td>${esc(r.nombre)||'—'}</td><td>${esc(r.cliente)||'—'}</td><td>${esc(r.abogado)||'—'}</td>${invCell}<td>${statusBadge(r.estatus)}</td><td>${esc(r.fechaSolicitud)||'—'}</td><td>${totalHours(r)}</td>`;
|
||||
if(dept==='corp') cells=`<td>${esc(r.tipoDocumento)||'—'}</td><td>${esc(r.nombre)||'—'}</td><td>${esc(r.cliente)||'—'}</td><td>${esc(r.abogado)||'—'}</td>${invCell}<td>${statusBadge(r.estatus)}</td><td>${esc(r.fechaSolicitud)||'—'}</td><td>${totalHours(r)}</td>`;
|
||||
if(dept==='mig') cells=`<td>${esc(r.tipoTramite)||'—'}</td><td>${esc(r.cliente)||'—'}</td><td>${esc(r.abogado)||'—'}</td>${invCell}<td>${yesnoBadge(r.docCompleta)}</td><td>${yesnoBadge(r.permisoTrabajo)}</td><td>${yesnoBadge(r.dependientes)}</td><td>${statusBadge(r.estatus)}</td><td>${esc(r.fechaSolicitud)||'—'}</td><td>${fmtDateOnly(r.fechaVencimiento)||'—'}${venceAlert(r.fechaVencimiento)}</td><td>${totalHours(r)}</td>`;
|
||||
if(dept==='lit'){
|
||||
const esfera=r.esfera||'';
|
||||
const inst=r.instancia||'';
|
||||
cells=`<td><b>${esc(r.tipoProceso)||'—'}</b><br><small>${esc(r.juzgado)||'—'} · ${esc(r.circuito)||'—'}</small></td><td>${esc(r.cliente)||'—'}</td><td>${esc(r.abogado)||'—'}</td>${invCell}<td>${esfera?`<span class="badge b-${esfera.toLowerCase()}">${esfera}</span>`:'—'}</td><td>${inst?`<span class="badge b-${statusSlug(inst)}">${inst}</span>`:'—'}</td><td>${yesnoBadge(r.docCompleta)}</td><td>${yesnoBadge(r.incidentes)}</td><td>${yesnoBadge(r.amparo)}</td><td>${statusBadge(r.estatus)}</td><td>${esc(r.fechaSolicitud)||'—'}</td><td>${fmtDateOnly(r.fechaTermino)||'—'}${venceAlert(r.fechaTermino)}</td><td>${totalHours(r)} h</td>`;
|
||||
cells=`<td><b>${esc(r.tipoProceso)||'—'}</b><br><small>${esc(r.juzgado)||'—'} · ${esc(r.circuito)||'—'}</small></td><td>${esc(r.cliente)||'—'}</td><td>${esc(r.abogado)||'—'}</td>${invCell}<td>${esfera?`<span class="badge b-${esfera.toLowerCase()}">${esfera}</span>`:'—'}</td><td>${inst?`<span class="badge b-${statusSlug(inst)}">${inst}</span>`:'—'}</td><td>${yesnoBadge(r.docCompleta)}</td><td>${yesnoBadge(r.incidentes)}</td><td>${yesnoBadge(r.amparo)}</td><td>${statusBadge(r.estatus)}</td><td>${esc(r.fechaSolicitud)||'—'}</td><td>${fmtDateOnly(r.fechaTermino)||'—'}${venceAlert(r.fechaTermino)}</td><td>${totalHours(r)}</td>`;
|
||||
}
|
||||
const delBtn = CAN_DELETE ? `<button class="btn btn-danger btn-sm" onclick="deleteRow('${dept}','${r.id}');event.stopPropagation()">✕</button>` : '';
|
||||
const actions=`<td><button class="btn btn-gold btn-sm" onclick="openModal('${dept}','${r.id}');event.stopPropagation()">✎</button> ${delBtn}</td>`;
|
||||
@@ -495,7 +513,7 @@ function buildDetail(dept,r){
|
||||
|
||||
const obs=(r.obs||[]).map(o=>`<li>${esc(o.texto)}<br><span class="obs-meta">${esc(o.abogado)} · ${esc(o.fecha)}${o.autor_email?` · <i>${esc(o.autor_email.split('@')[0])}</i>`:''}</span></li>`).join('');
|
||||
const lastUpd=r.obs&&r.obs.length?`<div class="last-upd">Última actualización: ${esc(r.obs[r.obs.length-1].fecha)}</div>`:'';
|
||||
const hrs=(r.hours||[]).map(h=>`<li><span class="hl-who">${esc(h.abogado)}</span><span>${h.hrs} h · ${esc(h.fecha)}</span></li>`).join('');
|
||||
const hrs=(r.hours||[]).map(h=>`<li><span class="hl-who">${esc(h.abogado)}</span><span>${fmtHours(h.hrs)} · ${esc(h.fecha)}</span></li>`).join('');
|
||||
const totalH=totalHours(r);
|
||||
|
||||
const statusSel=`<select onchange="changeStatus('${dept}','${r.id}',this.value)" onclick="event.stopPropagation()">
|
||||
@@ -528,7 +546,7 @@ function buildDetail(dept,r){
|
||||
<div class="detail-card">
|
||||
<h4>Horas Incurridas</h4>
|
||||
${hrs?`<ul class="hours-list">${hrs}</ul>`:'<p style="color:var(--gray);font-size:11px">Sin horas registradas.</p>'}
|
||||
<div class="total-hrs">Total: ${totalH} h</div>
|
||||
<div class="total-hrs">Total: ${totalH}</div>
|
||||
${(()=>{
|
||||
// Opciones del dropdown de Horas: abogado solicitante + todos los involucrados,
|
||||
// deduplicados por nombre. Si el trámite no tiene ni solicitante ni involucrados,
|
||||
@@ -542,7 +560,7 @@ function buildDetail(dept,r){
|
||||
${enabled
|
||||
? `<select id="hr-abog-${r.id}">${list.map(n=>`<option>${esc(n)}</option>`).join('')}</select>`
|
||||
: `<select id="hr-abog-${r.id}" disabled title="Agrega un abogado solicitante o involucrado primero"><option>— Sin asignados —</option></select>`}
|
||||
<input type="number" id="hr-val-${r.id}" min="0.1" step="0.1" placeholder="Horas" style="width:70px" ${enabled?'':'disabled'}>
|
||||
<select id="hr-val-${r.id}" style="width:80px" ${enabled?'':'disabled'} title="Tiempo en bloques de 15 min"><option value="">Horas</option>${hourBlockOptions()}</select>
|
||||
<input type="text" id="hr-desc-${r.id}" placeholder="Descripción" style="flex:1" ${enabled?'':'disabled'}>
|
||||
<button class="btn btn-primary btn-sm" onclick="addHours('${dept}','${r.id}')" ${enabled?'':'disabled'} title="${enabled?'Registrar horas':'Agrega un abogado solicitante o involucrado primero'}">+</button>
|
||||
</div>`;
|
||||
|
||||
Reference in New Issue
Block a user