Commit Graph
24 Commits
Author SHA1 Message Date
Soporte ZH Consultores 3f922bc877 feat(horas): desglose por trámite en cada abogado + exportar a Excel
- 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
2026-06-04 22:10:05 -05:00
Soporte ZH Consultores 8ff56d3d78 refactor(users): eliminar columna orden, listar alfabéticamente
- 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
2026-06-04 21:31:02 -05:00
Soporte ZH Consultores d9adfbae09 ui(home): horas y admin como módulos; quitar Directorio de Clientes
- 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
2026-06-04 21:25:47 -05:00
Soporte ZH Consultores d75a2a5fda feat(home): eventos de hoy debajo de los módulos
- Lista con eventos del día agrupados: manuales, escrituras, trámites
- Visibilidad: eventos manuales y escrituras → todos los usuarios;
  trámites legales → solo creador, abogado nombrado o involucrado
  (mismo canView() que en el módulo de trámites)
- Cada evento es clickable y navega al módulo correspondiente
  (/calendario, /escrituras/?edit=ID, /tramites/?edit=CODIGO)
- Cards muestran: título + cliente + abogado + estatus, con tags
  de tipo y departamento; eventos manuales muestran rango horario
  y creador
2026-06-04 21:05:28 -05:00
Soporte ZH Consultores d26ab5e855 feat(calendario): eventos manuales — reservas, cumpleaños, recordatorios
- 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
2026-06-04 20:49:23 -05:00
Soporte ZH Consultores cd7595832f feat(admin): Registro de transacciones — log de auditoría de todo el sistema
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.
2026-06-04 20:30:46 -05:00
Soporte ZH Consultores 2f2d20604b feat(tramites): involucrados pueden editar y eliminar horas también
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.
2026-06-04 20:17:10 -05:00
Soporte ZH Consultores e98d791cb8 fix(tz): servidor en America/Panama + edit/delete de horas registradas
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).
2026-06-04 20:10:49 -05:00
Soporte ZH Consultores 72c04ad594 feat(horas): nuevo módulo Reporte de Horas + icono reloj en headers
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).
2026-06-05 00:56:10 +00:00
Soporte ZH Consultores bbf91c7bec 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.
2026-06-05 00:45:15 +00:00
Soporte ZH Consultores a01d1b29d0 fix(tramites): incluir también al Abogado Solicitante en dropdown de Horas
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 —".
2026-06-05 00:36:36 +00:00
Soporte ZH Consultores 729f1b4164 fix(tramites): dropdown de Horas Incurridas filtra solo a involucrados
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.
2026-06-05 00:36:02 +00:00
Soporte ZH Consultores 510304872c feat(auth): nuevo rol 'admin' (programador, modo Dios oculto)
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.
2026-06-05 00:27:54 +00:00
Soporte ZH Consultores 25f3d7461b fix(bootstrap): no duplicar inyección en </head> dentro de template literals
ESCRITURAS.HTML tiene una función exportPDF que genera HTML para una ventana
de impresión usando un template literal. Ese literal contiene `</head>`,
`</body>`, `</script>`, etc. como cadenas dentro de \`...\`.

Bootstrap::inject hacía str_replace('</head>', ...) que matchea TODAS las
ocurrencias — incluyendo las que están dentro del template literal del JS.
Resultado: inyectaba `<script>window.DONNA=...</script>` dentro del literal
del exportPDF, y ese `</script>` cerraba prematuramente el <script>
exterior. Todo el JS después del corte se renderizaba como texto plano en
la página (visible al final del view: `${fecha}`, `${rows.length}`, etc.).

Efectos colaterales del bug: el gear de Panel de Control no se mostraba en
escrituras porque el JS que lo activa (`if isSocio show gear`) estaba
después del corte y nunca corría.

Fix: usar strpos+substr para inyectar SOLO antes de la primera ocurrencia
de </head> (la legítima del <head> del documento).
2026-06-05 00:19:36 +00:00
Soporte ZH Consultores 684c83f2c9 feat(admin): botón Borrar usuario + fix modal cierra al clic fondo
* 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.
2026-06-05 00:01:02 +00:00
Soporte ZH Consultores 072cc0b6e3 fix(admin): bug del activo + simplificar a Nombre Completo + gear en landing
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).
2026-06-04 23:42:06 +00:00
Soporte ZH Consultores 2db8f23ba5 feat(perms): permisología completa con 3 roles (Socio/Abogado/Usuario) + Panel de Control
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/.
2026-06-04 23:14:07 +00:00
Soporte ZH Consultores cd9dae98bb 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.
2026-05-11 20:48:20 +00:00
Soporte ZH Consultores 3f5027962a style(tramites): estandarizar barra superior con Control de Escrituras
* 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ó.
2026-05-11 20:22:50 +00:00
Soporte ZH Consultores 3a342cb6a8 feat(escrituras): edición por modal + botón "← Portal"
* 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).
2026-05-11 20:12:46 +00:00
Soporte ZH Consultores 1bb63f12f6 feat(tramites): app "Control de Trámites Legales" + backend
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.
2026-05-11 20:03:28 +00:00
Soporte ZH Consultores da08e01f28 fix(escrituras): no interrumpir edición con el auto-refresh
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.
2026-05-11 19:40:32 +00:00
Soporte ZH Consultores 81a909a1ec feat(escrituras): añadir columna "Entrada" (########/####)
* 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
2026-05-11 19:31:22 +00:00
Soporte ZH Consultores 4ee32aec6d Initial commit — Donna portal v1
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
2026-05-11 19:10:36 +00:00