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
+22
View File
@@ -612,6 +612,7 @@ let pollTimer = null;
let editModalOpen = false; // modal de edición/nuevo abierta
let editingId = null; // id del trámite siendo editado (null = nuevo)
let pendingRefresh = false; // poll trajo datos durante una edición
let firstLoadDone = false; // deep-link ?edit=ID solo aplica en la primera carga
function esc(s) { return (s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function fmtDate(dt) {
@@ -672,6 +673,10 @@ async function loadAll(showOverlay=false) {
} else {
render();
setSyncState('ok','Sincronizado');
if (!firstLoadDone) {
firstLoadDone = true;
checkDeepLink();
}
}
} catch(e) {
setSyncState('error','Sin conexión');
@@ -729,6 +734,23 @@ function doLogout() {
if (confirm('¿Cerrar sesión?')) window.location.href = 'api.php?action=logout';
}
// Si el usuario llegó desde el Calendario con ?edit=ID, abre la modal sobre esa fila
function checkDeepLink() {
const params = new URLSearchParams(location.search);
const raw = params.get('edit');
if (!raw) return;
const id = parseInt(raw, 10);
if (Number.isNaN(id)) return;
const r = allRows.find(x => x.id === id);
if (r) {
openEditModal(id);
} else {
toast('Trámite #' + id + ' no encontrado','error');
}
// Limpiar la URL para no re-disparar en refresh
history.replaceState({}, '', location.pathname);
}
// ── EDIT MODAL ───────────────────────────────────────────────────────────
function openEditModal(id) {
editingId = id;