- 5 apps live (escrituras, tramites, calendario, horas, admin) con descripción de cada una - 4 roles (admin, socio, abogado, usuario) y matriz de permisos - Patrón Bootstrap::inject + window.DONNA - AuditLog y canView() documentados - FK user_id en hours/obs - Módulo compartido DonnaExport (Excel + PDF) con API JS - Eventos de hoy en home - 14 migraciones SQL listadas en orden - Setup en server nuevo con aplicación batch de migraciones - Guía "agregar una app nueva" actualizada
Donna — Portal interno AALN Law
Portal interno de la firma Arias, Abrego, López & Noriega (aalnlaw.com) que aloja las herramientas operativas del equipo: control de escrituras y trámites legales, registro de horas, calendario, panel de administración y auditoría.
Producción: https://donna.aal-law.com
Arquitectura
Navegador
│ HTTPS
▼
Cloudflare → Caddy infra (SSL termination)
│ HTTP :80
▼
┌──────────────────────┐
│ Caddy (este server) │ /opt/donna/public
│ rewrite *.php │
└──────┬───────────────┘
│ unix socket
▼
┌──────────────────────┐
│ PHP-FPM 8.4 │
└──────┬───────────────┘
▼
┌──────────────────────┐
│ MariaDB 11.8 │ DB: donna
└──────────────────────┘
Stack:
- PHP 8.4-fpm (
/run/php/php8.4-fpm.sock) - Caddy 2.6 en
:80(terminación SSL en Cloudflare → infra Caddy) - MariaDB 11.8 (
127.0.0.1:3306), zona horariaAmerica/Panama(UTC-5) - Autenticación OAuth2 contra Mailcow (
mail.aal-law.com) - Frontend: HTML + JS vanilla con
window.DONNAinyectado porBootstrap::inject() - Exportación a Excel/PDF cliente-side con SheetJS y
window.print() - Backups diarios automáticos vía systemd timer
Roles y permisos
| Rol | Descripción | Visibilidad |
|---|---|---|
admin |
Programador / modo Dios oculto | No aparece en dropdowns ni en listados para socios |
socio |
Acceso total al sistema | Ve todo, gestiona usuarios y auditoría |
abogado |
Puede crear trámites y escrituras | Solo ve trámites donde es creador, abogado solicitante o involucrado |
usuario |
Apoyo administrativo (pasantes, asistentes) | Solo ve trámites donde está involucrado; crea escrituras |
Helpers en src/Auth.php:
Auth::isSocio()— true paraadminysocioAuth::isAdmin()— soloadminAuth::canCreate()—socio,abogado,admin(trámites)Auth::canDelete()— igual quecanCreate
Usuarios nuevos al loguearse por primera vez nacen con rol usuario.
Estructura del repo
donna/
├── public/ ← DocumentRoot
│ ├── index.php ← Landing + grid de apps + "Eventos de hoy"
│ ├── assets/
│ │ ├── donna-export.css ← UI compartida del botón Exportar
│ │ └── donna-export.js ← Helpers DonnaExport.toExcel / toPDF / attach
│ ├── auth/
│ │ ├── login.php ← Inicia OAuth flow
│ │ ├── callback.php ← Recibe code de Mailcow + crea/actualiza user
│ │ └── logout.php
│ ├── admin/
│ │ ├── index.php ← Auth-guard + view (solo socios/admin)
│ │ └── api.php ← CRUD usuarios + query del audit log
│ ├── calendario/
│ │ ├── index.php
│ │ └── api.php ← CRUD calendar_events (manuales)
│ ├── escrituras/
│ │ ├── index.php
│ │ └── api.php ← CRUD escrituras + historial
│ ├── horas/
│ │ ├── index.php
│ │ └── api.php ← Reporte de horas con filtros + agregaciones
│ └── tramites/
│ ├── index.php
│ └── api.php ← CRUD trámites + observaciones + horas
├── src/ ← Librerías PHP compartidas (fuera de DocumentRoot)
│ ├── Apps.php ← Catálogo de apps del portal (filtra por isSocio)
│ ├── AuditLog.php ← Helper para registrar acciones en audit_log
│ ├── Auth.php ← Sesión + OAuth guard + roles + whitelist
│ ├── Bootstrap.php ← Inyecta window.DONNA en cada view HTML
│ ├── Config.php ← Lee /etc/donna/donna.env, fija TZ a Panama
│ ├── Db.php ← PDO singleton, fija session TZ a -05:00
│ ├── Lookups.php ← Queries para dropdowns (abogados, usuarios)
│ └── Oauth.php ← Cliente HTTP a Mailcow
├── views/ ← Templates HTML servidos por PHP tras auth
│ ├── admin.html ← Panel de Control (2 pestañas: Usuarios + Log)
│ ├── calendario.html ← Vista año/mes/semana/día con eventos manuales
│ ├── escrituras.html
│ ├── horas.html ← Reporte de horas con desglose por trámite
│ └── tramites.html ← 4 tabs: Admin / Corp / Mig / Litigios
├── sql/ ← Migraciones (aplicar manualmente, ordenadas)
│ ├── 001_core.sql ← users
│ ├── 002_escrituras.sql ← escrituras_tramites + escrituras_historial
│ ├── 003_escrituras_entrada.sql
│ ├── 004_tramites.sql ← tramites_legales + tramites_seq
│ ├── 005_users_roles.sql ← Roles socio/abogado/usuario
│ ├── 006_tramites_involucrados.sql
│ ├── 007_drop_apellido.sql ← Nombre Completo en toda la app
│ ├── 008_admin_role.sql ← Rol admin (programador)
│ ├── 009_panama_timezone.sql ← Convertir timestamps históricos UTC → -05:00
│ ├── 010_audit_log.sql ← Tabla audit_log
│ ├── 011_calendar_events.sql ← Eventos manuales del calendario
│ ├── 012_drop_users_orden.sql ← Simplificación: orden alfabético siempre
│ ├── 013_normalize_abogado_names.sql ← Backfill nombres legacy
│ └── 014_add_user_id_to_hours_obs.sql ← FK user_id en hours/obs
├── bin/
│ └── donna-backup.sh ← mysqldump diario, retención 14 días
├── docs/ ← Diseños y archivos de infra de referencia
├── donna.env.example ← Plantilla del archivo de secrets
└── README.md
Fuera del repo (en el servidor):
/etc/donna/donna.env— secrets reales (640 root:www-data)/etc/caddy/Caddyfile— config del Caddy local/etc/systemd/system/donna-backup.{service,timer}— backups/var/log/donna/— access logs/var/backups/donna/— dumps SQL
Apps
Registradas en src/Apps.php::catalog($isSocio). El landing las renderiza como cards.
| Slug | Estado | Visible a | Ruta | Tablas DB |
|---|---|---|---|---|
escrituras |
Live | Todos | /escrituras/ |
escrituras_tramites, escrituras_historial |
tramites |
Live | Todos (filtrado por canView()) |
/tramites/ |
tramites_legales, tramites_involucrados, tramites_obs, tramites_hours, tramites_seq |
calendario |
Live | Todos | /calendario/ |
calendar_events (+ deriva de escrituras/trámites) |
horas |
Live | Todos (no-socios solo sus propias) | /horas/ |
tramites_hours (vista) |
admin |
Live | Solo socios/admin | /admin/ |
users, audit_log |
Control de Escrituras (escrituras)
Seguimiento de trámites notariales: estado, abogado, notaría, alteración de turno, historial y auditoría. Estatus: Solicitado → Recibido → En Proceso → Protocolizado → En Registro → Inscrito.
Control de Trámites Legales (tramites)
4 departamentos (Administrativo, Corporativo/Civil, Migración, Litigios) con campos específicos por depto. Cada trámite tiene:
- Involucrados (M:N con users): determinan acceso y dropdowns
- Observaciones (
tramites_obs): timeline de comentarios - Horas Incurridas (
tramites_hours): registro en bloques de 15 min (0:15, 0:30… 12:00). Editable/eliminable por autor, involucrados o socios. Almacenaabogado(string) +user_id(FK) para que el filtro siga funcionando si renombras a un usuario.
Calendario (calendario)
Vista año/mes/semana/día. Eventos derivados automáticamente de Escrituras (fecha de solicitud) y Trámites (solicitud, vencimiento migración, término procesal). Más eventos manuales (calendar_events): reserva de sala, cumpleaños, recordatorio, evento general — con horario, ubicación y creador.
Reporte de Horas (horas)
Filtros por rango de fechas y por usuario. Tarjetas por abogado expandibles → muestran el desglose de trámites con total de horas por trámite. KPIs (Total horas, Entradas, Personas). No-socios ven solo sus propias horas.
Panel de Control (admin)
Pestaña Usuarios: CRUD, asignación de roles, activación/desactivación. La columna placeholder original se va reemplazando conforme los abogados se loguean. Listas siempre alfabéticas.
Pestaña Registro de Transacciones: query de audit_log con filtros por fecha, usuario, módulo, acción y búsqueda libre. Cada login, logout, create, update, delete, set_status, add_obs, add_hours y similares queda registrado.
Exportación a Excel + PDF
Módulo compartido en public/assets/donna-export.{js,css} con la misma UI (botón "Exportar ▾" + dropdown verde Excel / rojo PDF) en los 6 puntos del sistema:
| Módulo | Qué exporta |
|---|---|
| Escrituras | Trámites filtrados |
| Trámites (cada tab) | Filas filtradas del tab activo |
| Calendario | visibleEvents() (respeta chips de origen + búsqueda) |
| Reporte de Horas | Entradas filtradas |
| Panel · Usuarios | Lista visible (respeta filtros) |
| Panel · Registro de Transacciones | Entradas cargadas |
API JS:
DonnaExport.buttonHtml(id?) // returns HTML del botón + menú
DonnaExport.attach(wrapId, { onExcel, onPDF })
DonnaExport.toExcel({ filename, sheetName, headers, rows, columnWidths, onEmpty })
DonnaExport.toPDF ({ filename, title, headers, rows, pdfCols?, orientation?,
statusColIndex?, statusColors?, footer?, onEmpty })
- Excel:
.xlsxcon header en navy, ancho de columnas personalizable, UTF-8 nativo. - PDF: abre ventana nueva con HTML estilizado +
window.print()(paisaje A4 por defecto).
Eventos de hoy (Home)
Debajo de las tarjetas de módulos, el portal muestra los eventos del día:
- Eventos manuales (cumpleaños, sala de conferencias, recordatorios): visibles a todos
- Escrituras con
fecha_solicitud = hoy: visibles a todos - Trámites con
fecha_solicitud / vencimiento / término = hoy: solo a creador, abogado solicitante o involucrado (mismocanView()que el módulo)
Cada card es clickable y abre la ficha correspondiente.
Setup en un servidor nuevo
# 1. Paquetes del SO (Debian 13)
apt install -y caddy php8.4-fpm php8.4-mysql php8.4-curl \
php8.4-mbstring php8.4-intl php8.4-zip mariadb-server
# 2. Zona horaria del SO
timedatectl set-timezone America/Panama
# 3. Base de datos
mariadb -e "CREATE DATABASE donna CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mariadb -e "CREATE USER 'donna_app'@'localhost' IDENTIFIED BY 'XXX';
GRANT ALL ON donna.* TO 'donna_app'@'localhost';"
# 4. Código
git clone https://git.zhconsultores.com/ZH-Consultores/donna.git /opt/donna
chown -R www-data:www-data /opt/donna
# 5. Migraciones (en orden)
for f in /opt/donna/sql/0*.sql; do mariadb donna < "$f"; done
# 6. Secrets
mkdir -p /etc/donna
cp /opt/donna/donna.env.example /etc/donna/donna.env
# Editar /etc/donna/donna.env con valores reales (OAuth, DB_PASS, APP_SECRET)
chown root:www-data /etc/donna/donna.env
chmod 640 /etc/donna/donna.env
# 7. Caddy y backups: ver docs/infra/ para Caddyfile y systemd units
systemctl reload caddy
systemctl enable --now donna-backup.timer
OAuth2 con Mailcow
- En
https://mail.aal-law.com/admin→ Access → OAuth2 Apps - Crear app con
Redirect URI = https://donna.aal-law.com/auth/callback - Copiar
Client IDyClient Secreta/etc/donna/donna.env
El flujo:
/ ──login──▶ /auth/login.php ──redirect──▶ mail.aal-law.com/oauth/authorize
│
▼ ?code=...&state=...
/auth/callback.php ◀── redirect ◀── usuario autorizado
│
├── valida state (anti-CSRF)
├── intercambia code → token
├── lee profile (email, nombre)
├── valida dominio @aal-law.com o @aalnlaw.com
├── inserta/actualiza fila en `users` (rol=usuario si es nuevo)
├── registra login en audit_log
└── redirige a /
Sesión: cookie donna_session, path=/, Secure, HttpOnly, SameSite=Lax, lifetime 8h. SSO automático entre todas las apps del portal.
Patrones de código
Bootstrap::inject($viewPath)
Cada <app>/index.php hace:
require_once __DIR__ . '/../../src/Bootstrap.php';
Auth::requireAuth();
header('Content-Type: text/html; charset=utf-8');
Bootstrap::inject(__DIR__ . '/../../views/<app>.html');
Esto inyecta antes del primer </head>:
<script>window.DONNA = {
me: 'email@aal-law.com',
name: 'Nombre Completo',
role: 'socio',
isAdmin: false,
isSocio: true,
canCreate: true,
canDelete: true,
abogados: ['Mario Arias', ...],
users: [{id, email, name, role}, ...]
};</script>
Importante: usa strpos/substr para reemplazar solo el primer </head> (no str_replace), porque algunos JS embebidos contienen </head> dentro de template literals que rompen el HTML si se inyecta ahí.
AuditLog::log($modulo, $accion, $entityType, $entityId, $entityLabel, $detalles?)
Falla silenciosamente (try/catch) para no romper el flujo principal. Llamado desde cada API después de operaciones de escritura. Los detalles van como JSON.
Visibilidad de trámites legales (canView())
Socio/admin → siempre
Resto → creador, o abogado solicitante (match por users.name = t.abogado),
o involucrado (M:N en tramites_involucrados)
FK user_id en hours/obs
Tanto tramites_hours como tramites_obs tienen abogado VARCHAR(100) (display) y user_id INT NULL (FK canónica ON DELETE SET NULL). Esto evita que renombrar un usuario en el Panel de Control desconecte sus entradas históricas de los filtros. Si lo borras, user_id queda NULL y el string abogado permanece como historia.
Identidad visual
| Token | Valor |
|---|---|
--navy |
#1B3A6B |
--navy-d |
#112446 |
--navy-l |
#2D5090 |
--gold |
#C9A84C |
--gold-l |
#E8C96A |
--gold-d |
#9E7C2E |
--off |
#F7F6F3 |
| Fuente headlines | DM Serif Display |
| Fuente UI | DM Sans |
Gradient hero del landing: #0E1F3F → #1B3A6B → #2D5090.
Colores por departamento de trámites:
- Admin:
#6c5b95· Corp:#9E7C2E· Mig:#2980b9· Lit:#722f37
Colores por tipo de evento manual:
- Reserva sala:
#2e7d8f· Cumpleaños:#c2185b· Recordatorio:#e67e22· Evento:#27ae60
Backups
donna-backup.timer ejecuta diariamente a las 03:00 (jitter ±5min) y vuelca donna comprimido a /var/backups/donna/donna-YYYY-MM-DD_HHMMSS.sql.gz. Retención 14 días.
Restaurar:
gunzip -c /var/backups/donna/donna-YYYY-MM-DD_HHMMSS.sql.gz | mariadb donna
Operación
| Acción | Comando |
|---|---|
| Reload Caddy | systemctl reload caddy |
| Reload PHP-FPM | systemctl reload php8.4-fpm |
| Ver logs Caddy | tail -f /var/log/donna/access.log |
| Ver errores PHP | journalctl -u php8.4-fpm -f |
| Backup manual | /opt/donna/bin/donna-backup.sh |
| Próximo backup | systemctl list-timers donna-backup.timer |
| Aplicar migración | mariadb donna < /opt/donna/sql/0NN_<nombre>.sql |
| Consultar audit log | Panel de Control → Registro de Transacciones |
Agregar una app nueva
- Crear
/opt/donna/public/<slug>/index.phpconBootstrap::inject(__DIR__ . '/../../views/<slug>.html')precedido deAuth::requireAuth(). - Crear
/opt/donna/public/<slug>/api.phpconAuth::requireAuth()(orequireSocio()). - Crear
/opt/donna/views/<slug>.htmlcon el HTML + JS del módulo. - Prefijar todas las tablas DB con
<slug>_y crear migración numerada ensql/0NN_<slug>.sql. - Registrar en
src/Apps.php::catalog()(con flagisSociosi aplica). - Si tiene exportación, incluir:
<link rel="stylesheet" href="/assets/donna-export.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> <script src="/assets/donna-export.js"></script> - Para escritura de auditoría, llamar
AuditLog::log(...)después de cada operación. - Commit + push.
Licencia
Software interno — sin licencia pública. Propiedad de Arias, Abrego, López & Noriega.