Antes solo miraba nombre/tipoTramite/tipoProceso/cliente/abogado/id,
así que buscar "Municipio" en la tab Administrativo no encontraba
nada aunque la columna Institución tuviera "Municipio de Panamá".
Nuevo rowHaystack(dept, r) concatena todos los campos que se
muestran en la tabla, excluyendo abogado y estatus (tienen sus
propios dropdowns al lado del buscador):
- Común: id/codigo, cliente, nombre, instrucciones, fecha solicitud
- adm: institución
- corp: tipo documento
- mig: tipo trámite, doc completa, permiso trabajo, dependientes,
vencimiento
- lit: tipo proceso, juzgado, circuito, esfera, instancia,
doc completa, incidentes, amparo, término
- Todos: involucrados (name + email)
Cada tecla en el input de búsqueda disparaba renderPanel() que
reescribía panel.innerHTML — incluyendo el input mismo — así que el
navegador descartaba el foco (y la posición del cursor). Los usuarios
tenían que hacer clic después de cada letra.
Cambios:
- Nuevo filterState por depto en memoria (no lee del DOM al filtrar)
- renderPanel() reconstruye todo solo cuando cambia el conjunto de
datos (carga inicial, guardar, borrar, cambio de tab)
- Nuevo renderPanelBody() que solo repinta contadores + tbody
- Los handlers de search/status/abogado/esfera actualizan
filterState y llaman renderPanelBody, dejando el toolbar intacto
Efecto: al tipear en el buscador, el input mantiene foco y cursor.
- Todos los strings de branding actualizados:
- "AALN" / "AALN Law" → "AAL"
- "Arias, Abrego, López & Noriega" → "Arias, Abrego & López"
- "M. Noriega" (placeholder en sample card del landing) → "M. Arias"
- Website link cambia de aalnlaw.com a aal-law.com en landing
y footer del portal
- Filenames de exportación a Excel/PDF: *_AALN → *_AAL
- Footer del PDF compartido (DonnaExport.toPDF) actualizado
- Mensajes de denegación de OAuth callback actualizados
- README refleja nuevo nombre y URL del firm
OAuth sigue aceptando @aal-law.com y @aalnlaw.com como dominios
válidos (configurado en /etc/donna/donna.env) para no romper logins
durante la transición de correos.
- Usuarios y permisos: exporta los rows visibles (respeta filtros
de rol, activos y búsqueda). Columnas: Email, Nombre, Rol,
Activo, Último login
- Registro de transacciones: exporta las entradas cargadas en
pantalla (respeta filtros de fecha/usuario/módulo/acción/texto).
Excel incluye Email e IP; el PDF los omite para que quepa
- Mismo módulo compartido DonnaExport — UI idéntica a los otros
módulos
- 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
- API: by_user_tramite agrupa horas por (abogado, trámite) para
socios y para el propio abogado (no-socio)
- API: nueva acción ?action=export que devuelve CSV con BOM UTF-8
(Fecha · Abogado · Departamento · Código · Trámite · Cliente ·
Estatus · Horas · Descripción), respetando los mismos filtros
- UI: tarjetas de "Por persona" ahora son expandibles (clic en
la tarjeta) y muestran un mini-listado de trámites con total
de horas y cantidad de entradas por trámite
- UI: cada trámite del desglose es clickable y abre la ficha
- UI: botón verde "Exportar a Excel" en la barra de herramientas
- Migración 012: ALTER TABLE users DROP COLUMN orden
- Lookups: abogadoNames y allUsers ordenan por name ASC
- API admin: create/update_user dejan de leer/escribir orden
- UI admin: removida la columna "Orden" de la tabla y el input
"Orden" del modal de usuario
- Apps.catalog() ahora recibe isSocio y emite "Reporte de Horas"
(todos) y "Panel de Control" (solo socios/admin)
- Eliminado el placeholder "Directorio de Clientes"
- Removidos los iconos de reloj y engranaje del header en home,
tramites, escrituras, calendario, horas y admin (la navegación
ahora pasa por las tarjetas del portal)
- Iconos clock y gear agregados a la función icon() del home
- Nueva tabla calendar_events (tipo, fecha, hora_inicio/fin, ubicación,
descripción, creador) — migración 011
- API CRUD /calendario/api.php (list/create/update/delete) con AuditLog
- Permisos: cualquiera autenticado crea; solo creador o Socio/Admin
edita y elimina
- UI calendario: chip "Eventos", botón "+ Nuevo evento" en toolbar y
en vista Día, modal con tipo/fecha/horario/ubicación, opción
"Todo el día"
- Detalle de evento manual muestra creador, horario y ubicación;
acciones Editar/Eliminar inline (sin navegación) si tiene permiso
- Colores por tipo: reserva sala, cumpleaños, recordatorio, evento
NUEVA INFRAESTRUCTURA
* SQL 010: tabla audit_log (id, fecha, user_email, user_name, modulo,
accion, entity_type, entity_id, entity_label, detalles JSON, ip_addr)
con índices por fecha/user/modulo/accion/entity.
* src/AuditLog.php: helper estático con dos métodos:
- log() lee actor de $_SESSION (uso normal post-auth)
- logAs() recibe email/name explícito (para login/logout y CLI)
Falla silenciosamente si la DB no responde — no rompe la operación
principal por un fallo de logging.
HOOKS EN MUTACIONES
* Auth.php: registra 'login' al completar OAuth y 'logout' antes de
destruir la sesión.
* escrituras/api.php: registra create/update/delete con id+nombre.
* tramites/api.php: registra 8 mutaciones — create/update/delete del
trámite, set_status (con nuevo estatus en detalles), add_obs,
add_hours/update_hours/delete_hours (con abogado y horas en
detalles), todos referenciados por codigo (ADM-001, etc.).
* admin/api.php: registra create_user/update_user/delete_user, con
diff de cambios en update_user.
* GETs (list_*) no se loguean — serían ruido.
LECTOR
* admin/api.php nuevo endpoint GET ?action=list_log que devuelve
entradas con filtros from/to/user/modulo/accion/search (LIKE)
+ catálogos para dropdowns. Límite 500 por query.
UI EN PANEL DE CONTROL
* admin.html ahora tiene tabs: "Usuarios" (existente) y "Registro de
transacciones" (nuevo).
* Tab Registro: toolbar de filtros (rango fechas, usuario, módulo,
acción, búsqueda libre, atajos Hoy/Semana/Mes/Todo) + tabla con
Fecha·Hora · Usuario · Módulo (pill coloreado) · Acción · Entidad
con label y tipo · Detalles JSON resumido.
Antes solo el autor original de la entrada (o un Socio/Admin) podía
editar/eliminar registros de horas. Ahora cualquiera con acceso al
trámite (creador, abogado solicitante, involucrado, socio, admin)
puede editar/eliminar horas de cualquier entrada — útil cuando un
involucrado nota un error en una hora registrada por otro.
* tramites/api.php: removido el check (autor_email == me OR isSocio)
de update_hours y delete_hours. canView() sigue siendo el único
gate, lo cual es consistente con add_hours.
* tramites.html: los botones ✎ y ✕ ahora siempre se muestran en cada
entrada de horas. El backend protege la operación.
TIMEZONE
* OS: timedatectl set-timezone America/Panama (era UTC). El reporte
de horas salía vacío para entries creados después de las 19:00 hora
Panama porque el filtro "este mes" del frontend usa local time pero
los datos se guardaban en UTC del día siguiente.
* MariaDB y PHP ahora corren en hora Panamá:
- src/Db.php hace SET time_zone='-05:00' por conexión.
- src/Config.php hace date_default_timezone_set('America/Panama').
* Migración 009: convierte timestamps existentes UTC → Panama usando
CONVERT_TZ en todas las tablas con DATETIME (users, escrituras_*,
tramites_*).
EDIT/DELETE DE HORAS
* Backend tramites/api.php: nuevos endpoints
- PUT ?action=update_hours: cambia abogado, hrs y descripción
- DELETE ?action=delete_hours
Permisos: solo el autor_email original (quien registró) o un
Socio/Admin puede editar/eliminar. También se valida canView().
* Frontend tramites.html: cada entrada de horas ahora muestra botones
✎ (editar) y ✕ (eliminar) al lado de la fecha, visibles solo para
quien tiene permiso. Editar abre un modal pequeño dedicado con los
mismos campos (abogado dropdown, horas dropdown 15-min, descripción).
NUEVA APP /horas/
* public/horas/index.php — Bootstrap::inject del view
* public/horas/api.php — endpoint GET ?action=list que retorna:
- entries: entradas detalladas con join a tramites_legales (link,
título, cliente, departamento, estatus)
- by_user: agregado por persona (solo socio/admin)
- avail_users: dropdown de usuarios para filtrar (solo socio/admin)
- total_hrs, is_socio, my_name
Filtros: ?from=YYYY-MM-DD, ?to=YYYY-MM-DD, ?user=ID
* views/horas.html — UI con:
- Header estandarizado (Portal, sync, user-chip, gear si socio, logout)
- Toolbar de filtros: rango de fechas + dropdown usuario (solo socio)
+ atajos rápidos (Hoy / Semana / Mes / Año / Todo)
- KPIs: Total horas, Entradas, Personas con horas (socio)
- Tarjetas "Por persona" (solo socio), click filtra por ese usuario
- Tabla detalle con link al trámite (?edit=CODIGO) + departamento +
horas en h:mm + descripción
VISIBILIDAD
* Abogado/usuario ve solo sus horas (match por h.abogado = mi_name).
* Socio/admin ve todas las horas. Puede filtrar por persona.
* Admin oculto del dropdown de personas (queries excluyen role=admin).
ICONO DE RELOJ EN HEADERS
* Agregado .btn-clock (alias visual de .btn-gear) en:
escrituras.html, tramites.html, calendario.html, admin.html,
horas.html, public/index.php (landing).
* Posición: al lado del user-chip, antes del gear/logout.
* Visible para todos los autenticados (no restringido por rol).
* 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.
Refinamiento del commit anterior: el dropdown de "Horas Incurridas"
ahora incluye al Abogado Solicitante del trámite + los Involucrados
(deduplicado por nombre).
Si el trámite no tiene ni solicitante asignado ni involucrados,
inputs/botón quedan disabled con mensaje "— Sin asignados —".
En la ficha detalle de cada trámite (los 4 departamentos), la sección
"Horas Incurridas" listaba TODOS los abogados del sistema. Cambia a
listar solo los involucrados del trámite — que son quienes pueden
realmente trabajar el caso.
Si no hay involucrados aún, dropdown queda deshabilitado con texto
"— Sin involucrados —" y los inputs + botón "+" también disabled,
para que el usuario primero agregue involucrados desde el modal de
edición antes de registrar horas.
JERARQUÍA DE ROLES
admin = programador. Modo Dios. Oculto de dropdowns y del Panel
de Control para non-admins. Acceso total.
socio = modo Dios visible. Acceso al Panel, ve y edita todo.
abogado = crea casos, ve los suyos + donde es involucrado.
usuario = solo ve/edita donde es involucrado.
* Migración 008: agrega 'admin' al ENUM users.role. Promueve a
ozerpa@aal-law.com como admin.
* Auth.php: isAdmin() nuevo. isSocio() y isAbogado() ahora incluyen
admin (admin hereda todos los poderes de socio). canCreate/canDelete
también heredan.
* Lookups.php:
- abogadoNames() ya filtraba a role IN (socio, abogado) → admins no
aparecen en el dropdown "Abogado Solicitante".
- involucrablesUsers() agrega filtro role != 'admin' → admins no
aparecen en el picker de Involucrados.
- allUsers($includeAdmins) parametrizada: socios reciben la lista
sin admins, admins reciben todo.
* Bootstrap.php: window.DONNA.isAdmin disponible al frontend.
* admin/api.php:
- list_users pasa Auth::isAdmin() para filtrar.
- create_user / update_user: rechazan role='admin' si quien llama no
es admin. Solo admins pueden editar a otros admins.
- delete_user: protege admins (extends al check existente de socio).
- Self-protection extendida: admin no puede demoverse por debajo de
socio (queda en god-mode tier).
* admin.html: opción 'admin' en role dropdown solo visible si el
usuario actual es admin. Role pill navy/blanca para admin.
* admin/api.php: nuevo endpoint DELETE ?action=delete_user con salvaguardas:
- No te puedes eliminar a ti mismo
- No puedes eliminar a otro Socio (cambiar su rol primero)
- CASCADE de tramites_involucrados limpia vínculos N:M automáticamente
- Strings históricos (creado_por, abogado, autor_email) permanecen como
referencia muerta para preservar trazabilidad de trámites antiguos
* admin.html: botón rojo "Borrar usuario" en el footer del modal, visible
solo en modo edición (no en crear). Confirmación contextual según si es
placeholder o usuario real. Pista en el hint del email sobre usar Borrar
cuando hay conflicto.
* Fix: removido onclick del modal-overlay que cerraba el modal al hacer
clic en el padding del backdrop. Ahora solo cierra con ✕, Cancelar,
Esc, o Guardar exitoso. Más predecible.
BUG FIX
* admin.html: el filtro <select id="f-activo"> y el modal <input id="f-activo">
tenían el MISMO id. document.getElementById devolvía el select (primer en
DOM order) y la lectura .checked siempre era undefined → JSON enviaba el
valor stale del select. Renombré el filtro a `f-activo-filter`.
REFACTOR: drop apellido, usar Nombre Completo
* Migración 007: copia apellido→name para placeholders, migra
escrituras_tramites.abogado y tramites_legales.abogado a usar nombre
completo, DROP COLUMN apellido + índice.
* Lookups: abogadoApellidos() → abogadoNames() filtra socios+abogados
activos con name no vacío.
* Auth.php: drop método apellido(), agregado userName().
* tramites/api.php: visibilidad usa users.name = t.abogado.
* admin/api.php: drop apellido en create/update payloads.
* admin.html: drop columna y campo Apellido del table y form.
* Bootstrap envía window.DONNA.abogados con nombres completos.
LANDING (index.php)
* Gear icon en dash-header visible solo si Auth::isSocio() (chequeo
server-side, sin flash de JS).
CLEANUP
* Eliminado logging de debug en admin/api.php que se agregó para
diagnosticar el bug del activo.
PERMISOS (sin cambios — confirmación)
* Trámites Legales: solo socio/abogado pueden crear (canCreate() ya lo
enforza desde 2db8f23).
* Escrituras: cualquier rol autenticado puede crear (escrituras/api.php
nunca tuvo restricción).
ESQUEMA (migraciones 005 + 006)
* users: agrega role ENUM('socio','abogado','usuario'), apellido, activo, orden
* Seed: cabrego@aal-law.com como socio + 15 placeholders por apellido para
poblar el dropdown "Abogado Solicitante" hasta que los reales se logueen.
* tramites_involucrados: tabla N:M (tramite_id, user_id) con CASCADE delete.
AUTH (src/Auth.php)
* userRow() cachea el row del usuario actual por request.
* role(), isSocio(), isAbogado(), canCreate(), canDelete(), requireSocio().
* completeLogin() retorna bool: false si user.activo=0 → callback muestra
pantalla "cuenta deshabilitada" en lugar de loguear.
BOOTSTRAP (src/Bootstrap.php + src/Lookups.php)
* Bootstrap::inject(view) reemplaza readfile en los wrappers; inyecta
<script>window.DONNA={me,name,role,isSocio,isAbogado,canCreate,canDelete,
abogados,users}</script> antes de </head> para que el frontend tenga
contexto sin requests extra.
TRAMITES — visibilidad + involucrados
* api.php: list filtra por (socio | creador | abogado solicitante mapped via
users.apellido | involucrado) excepto socios que ven todo. Todas las
mutaciones validan canView(). create/delete validan rol. Persiste
involucrados vía syncInvolucrados().
* tramites.html: nueva columna "Involucrados" después de "Abogado", chip
picker multi-select en el modal, gear icon (oculto a non-socios), oculta
botones "+ Nuevo" y ✕ para usuarios sin permisos. Auto-incluye al
creador como involucrado por defecto.
* Calendario y demás apps consumen el API filtrado → heredan la
visibilidad sin código nuevo.
PANEL DE CONTROL (/admin/)
* index.php: requireSocio() + Bootstrap injection.
* api.php: list_users, create_user, update_user. Salvaguardas: el socio
no puede demoverse ni desactivarse a sí mismo.
* admin.html: tabla unificada de usuarios con filtros, modal CRUD,
resaltado de fila propia, placeholders identificados visualmente.
GEAR ICON
* Agregado en escrituras.html, tramites.html, calendario.html con
display:none por defecto; JS lo revela si window.DONNA.isSocio.
* Link a /admin/.
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.
* Header pasa de "h1 + hdr-btns chiquitos" al patrón de Escrituras:
izq = botón "← Portal" + logo (mark + AALN Law + Control de Trámites
Legales), der = sync-status + user-chip con avatar e iniciales +
botón "Cerrar sesión".
* Mismo navy + 3px gold border-bottom, mismo height 58px, sticky top.
* Pulse animation en sync-dot (igual que Escrituras).
* Carga DM Serif Display + DM Sans para que la tipografía del header
coincida con Escrituras.
* setUserChip() pobla avatar (iniciales) y nombre tras loadAll.
* Pestañas de departamentos quedan como estaban — solo el header se
estandarizó.
* Reemplaza la edición inline (15 columnas con inputs editables) por
un modal popup tipo Control de Trámites:
- Click ✏️ o doble-click sobre la fila → abre el modal con el form
- "Nuevo trámite" abre el mismo modal vacío
- Duplicar abre el modal pre-poblado con "Copia — <nombre>"
- Esc cierra el modal (prioritizando edición sobre historial)
- Validación en el form: nombre requerido, patrón ########/####
para Entrada antes de POST/PUT
* La tabla queda read-only — más limpia visualmente y sin riesgo de
edits accidentales por click en una celda.
* Eliminadas funciones obsoletas: newRow/saveNewRow/cancelNewRow/
toggleEditRow/saveExistingRow/getRowData/onFieldChange/onFieldBlur/
onKeydown y el Set editingRows. Reemplazadas por openEditModal/
closeEditModal/buildEditForm/saveEditModal.
* maybeFlushRefresh ahora solo chequea editModalOpen — más simple.
* Header: botón "← Portal" arriba a la izquierda (igual estilo de
retorno que tiene Trámites Legales).
Migración del frontend localStorage a backend autenticado bajo Donna.
* SQL 004: tramites_legales (todos los campos por dept en columnas
nullable + ENUMs Sí/No), tramites_obs (bitácora N:1), tramites_hours
(registro de horas N:1), tramites_seq (contador por departamento).
IDs server-side tipo ADM-001 / CRP-001 / MIG-001 / LIT-001 con
SELECT FOR UPDATE para atomicidad.
* public/tramites/api.php: list/create/update/delete + set_status +
add_obs + add_hours. Valida departamento y estatus contra los enums
del frontend. Audita creado_por / actualizado_por con el email
autenticado.
* views/tramites.html: artifact original adaptado. Quitado backup/restore
localStorage (mysqldump diario ya hace ese trabajo). Cada mutación
va vía fetch con manejo de 401 → redirect a login. Mapeo
id<->codigo para preservar el contrato del artifact.
* Apps.php: la card "Gestión de Casos" placeholder se reemplaza por
"Control de Trámites Legales" en estado live.
Antes el poll de 15s llamaba render() y reconstruía toda la tbody,
borrando el modo edición en filas existentes (el guard isEditing solo
cubría filas nuevas).
Ahora:
* loadAll() detecta si hay edición en curso (isEditing || editingRows.size)
y guarda los datos en allRows sin rerenderizar.
* Muestra "Edición en curso · refresco pausado" en el sync-status.
* maybeFlushRefresh() aplica los cambios remotos pendientes al salir
del modo edición (cancelar o guardar la última fila).
* saveExistingRow ya llama a render() vía saveRow, así que limpia la
bandera explícitamente.
* SQL: nueva migración 003 que añade `entrada VARCHAR(13)` después de
`estatus` en `escrituras_tramites`. Aplicada en producción.
* API: INSERT/UPDATE persisten el campo; auditoría lo registra con
label "Entrada" en el historial.
* Frontend:
- Nueva columna en el header entre Estatus y Últ. Actualización
- Input con `pattern=^\d{1,8}/\d{4}$`, `maxlength=13`, placeholder
"########/####" y validación nativa del navegador
- newRow() y duplicateRow() incluyen `entrada:''` por defecto
- exportHeaders / rowToArray / Excel widths actualizados
- pdfCols renumerado para incluir Entrada en el PDF
Setup completo del portal interno donna.aal-law.com:
* Landing público con OAuth login + grid de apps autenticado
* Flujo OAuth2 contra Mailcow (mail.aal-law.com)
* Sesiones SSO compartidas en el dominio (cookie donna_session, 8h)
* Whitelist de dominios aal-law.com / aalnlaw.com
* App "Control de Escrituras" migrada desde ZH-Consultores/control-escrituras
(mismo schema, mismo frontend, ahora con shared Auth/Db/Config libs)
* Migraciones SQL para users, escrituras_tramites, escrituras_historial
* Caddy on :80 con rewrite extless → .php
* Backup diario MariaDB (systemd timer + script) con retención 14d
* Documentación de arquitectura en README + infra files en docs/infra