From cd9dae98bb82682d8c2a07b06dbb93d0551f1b3b Mon Sep 17 00:00:00 2001 From: Soporte ZH Consultores Date: Mon, 11 May 2026 20:48:20 +0000 Subject: [PATCH] feat(calendario): app Calendario con 4 vistas + deep-link a apps origen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- public/calendario/index.php | 8 + src/Apps.php | 10 +- views/calendario.html | 838 ++++++++++++++++++++++++++++++++++++ views/escrituras.html | 22 + views/tramites.html | 29 +- 5 files changed, 899 insertions(+), 8 deletions(-) create mode 100644 public/calendario/index.php create mode 100644 views/calendario.html diff --git a/public/calendario/index.php b/public/calendario/index.php new file mode 100644 index 0000000..60adb35 --- /dev/null +++ b/public/calendario/index.php @@ -0,0 +1,8 @@ + 'live', ], [ - 'slug' => 'agenda', - 'title' => 'Agenda', - 'description' => 'Calendario compartido de audiencias, reuniones y vencimientos.', + 'slug' => 'calendario', + 'title' => 'Calendario', + 'description' => 'Vista visual de eventos de Escrituras y Trámites: solicitudes y vencimientos por año, mes, semana y día.', 'icon' => 'calendar', - 'href' => '#', - 'status' => 'coming-soon', + 'href' => '/calendario/', + 'status' => 'live', ], [ 'slug' => 'clientes', diff --git a/views/calendario.html b/views/calendario.html new file mode 100644 index 0000000..4e3a609 --- /dev/null +++ b/views/calendario.html @@ -0,0 +1,838 @@ + + + + + +Calendario — AALN Donna + + + + + + + +
+
+ + + Portal + + +
+
+
+
+ Conectando… +
+
+
?
+ … +
+ Cerrar sesión +
+
+ +
+
+ + + + +
+
+ + + +
+ — + +
+ Escrituras + Admin + Corporativo + Migración + Litigios +
+
+ +
+
Total eventos
—
+
Vencen ≤ 7 días
—
+
Vencen ≤ 30 días
—
+
Vencidos abiertos
—
+
+ +
+

Próximos vencimientos

+
Cargando…
+
+ +
+ + + + + +
+ + + + diff --git a/views/escrituras.html b/views/escrituras.html index 6f91d4b..4ab8b4b 100644 --- a/views/escrituras.html +++ b/views/escrituras.html @@ -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,'&').replace(/"/g,'"').replace(//g,'>'); } 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; diff --git a/views/tramites.html b/views/tramites.html index 1e9ffea..49844f4 100644 --- a/views/tramites.html +++ b/views/tramites.html @@ -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,'&').replace(/"/g,'"').replace(//g,'>');} 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){