feat(calendario): app Calendario con 4 vistas + deep-link a apps origen

Cliente-side: el calendario hace fetch a /escrituras/api.php?action=list
y /tramites/api.php?action=list y agrega los eventos en memoria. Sin
backend nuevo (cero costo de mantenimiento, datos siempre frescos).

Eventos generados automáticamente:
* Escrituras → fecha_solicitud
* Trámites adm/corp/mig/lit → fecha_solicitud
* Trámites mig → fecha_vencimiento (resaltado como deadline)
* Trámites lit → fecha_termino (resaltado como deadline)

Vistas: Año (12 mini-meses con dots por evento + marca roja para
vencimientos), Mes (grid con chips ≤3 por día), Semana (7 columnas),
Día (lista de cards). Hoy resaltado en todas.

Resaltado de vencimientos:
* Vencido (días<0, status abierto) → rojo + animación flash
* ≤7d → naranja
* >7d → color por fuente

Funcionalidad adicional:
* Strip "Próximos vencimientos" con los 8 deadlines más urgentes
* KPIs: total · ≤7d · ≤30d · vencidos abiertos
* Filtros por fuente (chips) + búsqueda por texto
* Atajos: ←/→ mueve período, T hoy, Y/M/W/D cambia vista, Esc cierra
* Auto-refresh cada 60s en segundo plano (pausa si la modal está abierta)
* Misma barra superior estandarizada que Escrituras y Trámites

Popup ViewOnly al hacer click en un evento muestra ficha completa con
todos los campos relevantes según fuente y dept. Dos botones:
"Regresar al calendario" cierra · "Editar evento" navega a
/escrituras/?edit=ID o /tramites/?edit=CODIGO.

Deep-link: escrituras.html y tramites.html detectan ?edit=X al cargar
y auto-abren la modal de edición sobre esa fila. URL se limpia con
history.replaceState para que un refresh no re-abra la modal.

Apps.php: card placeholder "Agenda" reemplazada por "Calendario" live.
This commit is contained in:
Soporte ZH Consultores
2026-05-11 20:48:20 +00:00
parent 3f5027962a
commit cd9dae98bb
5 changed files with 899 additions and 8 deletions
+26 -3
View File
@@ -194,9 +194,10 @@ const SI_NO = ['Sí','No'];
const API = 'api.php';
let data = {adm:[],corp:[],mig:[],lit:[]};
let currentDept = 'adm';
let editingId = null;
let authUser = {email:'', name:''};
let currentDept = 'adm';
let editingId = null;
let authUser = {email:'', name:''};
let firstLoadDone = false; // deep-link ?edit=CODIGO solo aplica en la primera carga
function esc(s){return (s==null?'':String(s)).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
function setSync(state,label){
@@ -288,6 +289,10 @@ async function loadAll(){
setSync('ok', 'Sincronizado');
setUserChip();
renderAll();
if (!firstLoadDone) {
firstLoadDone = true;
checkDeepLink();
}
} catch(e){
setSync('error','Sin conexión');
toast('Error al cargar: '+e.message,'error');
@@ -304,6 +309,24 @@ function switchTab(dept){
renderPanel(dept);
}
// Si el usuario llegó desde el Calendario con ?edit=ADM-001, busca el dept y abre el modal
function checkDeepLink(){
const params = new URLSearchParams(location.search);
const code = params.get('edit');
if(!code) return;
for(const dept of ['adm','corp','mig','lit']){
const r = data[dept].find(x => x.id === code);
if(r){
switchTab(dept);
openModal(dept, code);
history.replaceState({}, '', location.pathname);
return;
}
}
toast('Trámite ' + code + ' no encontrado','error');
history.replaceState({}, '', location.pathname);
}
function renderAll(){['adm','corp','mig','lit'].forEach(renderPanel);}
function renderPanel(dept){