AgendaPro DevandClaude Opus 5 6d67b23e55 feat(platform): multi-tenancy con credenciales por negocio y sincronización por id
El backend Postgres de `platform/` asumía un solo negocio con un solo token del
CRM. Este cambio lo convierte en una plataforma multi-cuenta y añade la
sincronización selectiva de las cinco entidades del encargo.

## Multi-tenancy

El `locationId` ya era por negocio, pero el token vivía en la variable de entorno
`CRM_TOKEN`, una sola para todo el proceso. Con dos negocios eso usaba el token
del primero contra la subcuenta del segundo: 401 en el mejor caso, escritura en
la subcuenta equivocada en el peor.

- `lib/crypto.ts` — AES-256-GCM para los tokens. Autenticado a propósito: una
  fila manipulada hace que el descifrado FALLE, en vez de devolver basura que
  acabaríamos mandando como credencial al CRM. La clave maestra vive en
  `CRM_MASTER_KEY`, fuera de la base.
- `crm/ctx.ts` — `CrmCtx { businessId, locationId, token }` sustituye al
  `locationId: string` suelto que viajaba por once firmas. Es un objeto y no dos
  parámetros porque dos `string` seguidos se cruzan sin que el compilador diga
  nada, y cruzarlos aquí manda el token de un cliente a la subcuenta de otro. Es
  el único sitio donde el token existe descifrado, y solo en memoria.
- `crm/client.ts` — `CrmOptions.token` pasa a ser OBLIGATORIO, sin valor por
  defecto: olvidarlo es ahora un error de compilación. El estrangulador pasa a
  ser por token y aprende la cuota de las cabeceras `x-ratelimit-*`, que declaran
  100 peticiones por 10 s — el cliente iba 6,5x por debajo con una estimación.
- Migración 003: credencial cifrada, calendario y la red de seguridad de mensajes
  POR NEGOCIO. Como variable global decidía por todas las cuentas a la vez.

Lo único de la credencial que sale del servidor es la huella de 6 caracteres.

## Consola de superadministración

`/api/admin`, solo para el rol `admin`: alta de cuentas con su dueña en una
transacción, vínculo, desvínculo y suspensión. Las credenciales se COMPRUEBAN
contra el CRM antes de guardarse — un token sin validar traslada el fallo al
primer intento de sincronizar, lejos de donde se cometió. El error distingue
«token inválido» de «subcuenta inexistente» de «token de otra subcuenta».

Pantalla en `/admin/cuentas`, verificada en navegador: el campo del token es de
contraseña y viene vacío, porque no hay valor que traer.

## Sincronización por identificador

`POST /api/crm/sync/:entidad/:id` para contacto, conversación, mensaje, cita y
servicio. La dirección la decide la entidad: las tres primeras se TRAEN porque el
CRM es su dueño; las dos últimas se EMPUJAN, porque el calendario del CRM tiene
una sola cita en dos años y su catálogo de servicios está vacío.

- `crm/conversations.ts` — lectura por id de conversaciones y mensajes sueltos.
- `crm/syncConversations.ts` — el espejo persistido. Las tablas existían desde
  002_crm.sql y nadie escribía en ellas: la bandeja consultaba el CRM en vivo.
- `crm/calendars.ts` — escritura de citas al calendario. `isoConDesplazamiento`
  escribe la hora de pared del negocio con su desplazamiento; `toISOString()`
  habría movido la hora que el CRM enseña en su interfaz.
- `crm/services.ts` — publicación de servicios al catálogo.

## Verificado contra la subcuenta real, no deducido

Las cinco entidades se ejercieron contra el CRM del cliente. Las escrituras van
en un ciclo crear → releer → borrar → confirmar borrado, con la limpieza en un
`finally`, y antes se comprobó que el borrado existe: preguntar si se puede
deshacer ANTES de escribir en el CRM de un cliente, no después. La subcuenta
quedó como estaba.

47 hallazgos medidos en `crm/HALLAZGOS.md`, y la referencia de endpoints en
`crm/API.md`, con la lista explícita de dónde la documentación oficial falla.

110 pruebas de plataforma en verde, typecheck limpio, build correcto. El backend
de demo de `server/` no se ha tocado y sigue con sus 43 pruebas.

## Deuda conocida, dicha sin rodeos

- La bandeja de mensajes todavía lee en vivo del CRM, no del espejo.
- La autenticación sigue siendo el id del usuario en texto plano, también para el
  rol admin. Esta consola crea cuentas y guarda credenciales de clientes encima
  de esa base: no debe quedar expuesta a internet hasta endurecerla.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-30 15:07:20 -06:00

AgendaMax

Aplicación web multi-tenant (SaaS) para la gestión de negocios de servicios (estética, spa, barbería, clínicas, etc.): calendario de citas con arrastrar y soltar, gestión de empleados, servicios, clientes y tickets, y un tablero con ingresos, mejores empleados, servicios más rentables y clientes top.

Tres roles: Administrador de plataforma (crea y gestiona todos los negocios desde la consola admin), Dueño (ve su negocio y el tablero) y Empleado (agenda citas que se auto-asignan a él).

Stack

  • Frontend: React 18 + TypeScript + Vite + Tailwind CSS
  • Calendario: FullCalendar (drag & drop, resize, day/time grid)
  • Gráficas: Recharts
  • Backend: Node.js + Express
  • Base de datos: SQLite (vía node:sqlite, nativo de Node — sin compilación nativa)

Requisitos

  • Node.js 22.5+ (recomendado 24+). Este proyecto usa el módulo nativo node:sqlite.

Instalación

npm install

La base de datos se siembra automáticamente con datos demo la primera vez que arranca el servidor. Para regenerarla desde cero:

# Borra la DB y vuelve a sembrar
npm run seed
# o, forzando reset desde el servidor:
$env:RESET_DB="1"; npm run dev:server   # PowerShell
RESET_DB=1 npm run dev:server           # bash

Desarrollo

npm run dev

Abre http://localhost:5173 (el frontend proxya /api al backend en el puerto 3000).

Ruta Qué es
/ Landing pública. Con sesión abierta redirige al panel según el rol.
/login Acceso. En fase demo ofrece entrar de un clic con las cuentas de abajo.
/b/:slug Reserva pública del negocio, sin sesión.

Producción

npm run build     # compila TS y empaqueta el frontend en dist/
npm start         # sirve la API y el frontend estático en http://localhost:3000

El acceso de un clic y el selector «Ver como…» del panel viven detrás de la constante de build DEMO, que en un build de producción solo se enciende con VITE_DEMO_UI=1. El Dockerfile la declara con ese valor por defecto porque el despliegue es la demostración; pasar --build-arg VITE_DEMO_UI=0 los elimina del bundle.

Instalar AgendaMax como app

  • Android/Chrome: abre la URL HTTPS y selecciona la acción de instalación mostrada por AgendaMax o por la barra de direcciones del navegador.
  • iPhone/iPad Safari: usa Compartir -> Añadir a pantalla de inicio; iOS no expone el aviso de instalación dentro de la página de Android.
  • La app instalada puede abrirse sin la interfaz del navegador, pero los datos del negocio siguen requiriendo conexión a internet.
  • Validación local: ejecuta npm.cmd run build, inicia producción con npm.cmd start y después ejecuta npm.cmd run test:pwa.
  • La validación de producción requiere el dominio HTTPS de Coolify, no una dirección IP HTTP.

npm.cmd run test:pwa usa estas variables opcionales para los flujos autenticados; si no se definen, usa las cuentas demo locales:

[email protected]
[email protected]
PWA_PASSWORD=demo1234

Tests y auditoría visual

npm run test:e2e         # 22 pruebas end-to-end de la API (login, CRUD, dashboard, auto-asignación, roles, multi-tenant)
npm run test:admin       # 17 pruebas del admin (crear negocio + plantilla, aislamiento, reset-demo, delete cascade)
npm run test:landing     # WebKit: ruteo landing/login, acceso de un clic y prefers-reduced-motion
npm run audit:visual     # Playwright: navega 10 páginas × 4 viewports (móvil/tablet/desktop/wide),
                         # mide overflow horizontal, captura errores de consola y guarda capturas en screenshots/
npm run audit:responsive # WebKit: iPhone/iPad reales — auto-zoom, safe areas, objetivos táctiles y canaletas

La auditoría visual verifica responsividad en 375 / 768 / 1280 / 1536 px, detecta overflow, errores de consola y confirma que el calendario, el modal de cita y el cambio de sesión funcionan en cada tamaño.

Cuentas demo

Todas usan la contraseña demo1234. En /login se puede entrar con un clic, sin escribirlas.

Rol Email Contraseña Nombre
Admin [email protected] demo1234 Administrador
Dueño [email protected] demo1234 Daniela Reyes
Empleado [email protected] demo1234 Valentina Cruz
Empleado [email protected] demo1234 Mateo Herrera
Empleado sofía.ramí[email protected] demo1234 Sofía Ramírez
Empleado [email protected] demo1234 Diego Castillo
Empleado [email protected] demo1234 Isabela Torres
Empleado carolina.mé[email protected] demo1234 Carolina Méndez

En la pantalla de login aparecen botones de acceso rápido a las cuentas demo, y desde la barra lateral puedes cambiar de cuenta con “Ver como…”.

Multi-tenant y consola de administrador

AgendaMax es multi-tenant: cada negocio es un tenant aislado (todos los datos llevan business_id y las consultas se filtran por el negocio del usuario). El administrador de plataforma gestiona todos los negocios desde /admin:

  • Resumen: KPIs globales (negocios activos, usuarios, ingresos y citas de toda la plataforma).
  • Negocios: lista buscable con stats por negocio (usuarios, citas, ingresos).
  • Crear negocio (2 pasos): eliges una plantilla → se cargan empleados, servicios y clientes demo con historial de citas/tickets listos para usar; defines los datos del negocio y la cuenta del dueño.
  • Detalle de negocio: editar nombre/industria, cambiar plan (Prueba/Pro/Free) y estado (Activo/Suspendido), recargar datos demo con otra plantilla (preserva las cuentas de usuario) o eliminar el negocio por completo (con cascade de todos sus datos).

Plantillas disponibles: estetica-spa (Lumière), barberia (Urban Cut), clinica (Dental Sonrisa) y blank (vacío, para configurar desde cero). Defínelas en server/lib/templates.ts.

El aislamiento entre tenants se valida: un dueño solo ve los datos de su negocio, y los empleados no pueden acceder al tablero ni a la gestión (403).

Funcionalidades

Calendario (Dueño y Empleado)

  • Vistas de mes / semana / día, con indicador de hora actual.
  • En móvil se abre por defecto en vista Día y se ofrece una vista Lista (agenda) — la vista Semana/Mes queda reservada para tablet/desktop para evitar columnas aplastadas.
  • Arrastra para reagendar y redimensiona para cambiar duración.
  • Clic en un hueco o en Nueva cita para abrir el formulario.
  • Clic en una cita para editarla: cambiar servicio, especialista, cliente, hora, precio, notas.
  • Marcar como completada (genera un ticket con propina y método de pago).
  • Cancelar o eliminar citas.
  • Filtra por empleado y por estado (programada / completada / cancelada).

Formulario de cita

  • Servicio desplegable (menú del negocio), con precio y duración.
  • Especialista: el empleado lo tiene auto-asignado por defecto (filtrado a quienes ofrecen el servicio).
  • Cliente: buscar existente o crear uno nuevo al vuelo.
  • Duración, precio y notas editables.

Tablero (Dueño)

  • KPIs: ingresos del período, citas programadas, ticket promedio, ocupación y % de cancelación.
  • Gráfica de ingresos diarios y distribución por categoría.
  • Mejores empleados por ingreso, con rating y utilización.
  • Servicios más rentables.
  • Tickets más altos recientes.
  • Top clientes (por gasto) y clientes frecuentes (por visitas).
  • Rango de tiempo ajustable (7 / 30 / 60 días).

Gestión (Dueño)

  • Empleados: crear, editar, asignar servicios, ver ingresos/citas/rating.
  • Servicios: catálogo agrupado por categoría, con especialistas asignados.
  • Tickets: historial de ventas con filtros por método de pago.

Clientes (Dueño y Empleado)

  • Buscador por nombre / teléfono / correo.
  • Ficha del cliente con historial de citas, gasto total y visitas.

Estructura

.
├── server/            # API Express + SQLite
│   ├── db.ts          # esquema multi-tenant + sistema de migraciones versionado
│   ├── index.ts       # app Express (auto-siembla admin + negocio demo en primer arranque)
│   ├── lib/auth.ts    # middlewares: authRequired, ownerOnly, adminOnly
│   ├── lib/templates.ts  # plantillas: estetica-spa, barberia, clinica, blank
│   ├── routes/        # auth, business, employees, services, clients, appointments, dashboard, admin
│   └── scripts/seed.ts# seedBusiness(businessId, template) reutilizable + script standalone
├── shared/types.ts    # tipos TS compartidos cliente/servidor
├── src/               # frontend React
│   ├── components/    # AppShell, AdminShell, Modal, AppointmentModal, ui, DemoSwitcher, admin/CreateBusinessModal
│   ├── lib/           # api, auth, format
│   └── pages/         # Login, Dashboard, Calendar, Employees, Services, Clients, Tickets + admin/*
└── data/              # SQLite (generado, .gitignored)

Notas de seguridad

Es una demo. La autenticación usa un token trivial (el id del usuario) sin hashing de contraseña ni sesiones reales — no usar en producción tal cual. Antes de un despliegue real habría que añadir hashing (bcrypt), JWT firmados, validación estricta con zod, rate-limiting y HTTPS.

S
Description
No description provided
Readme
1 MiB
Languages
TypeScript 80.9%
JavaScript 15.2%
CSS 3.3%
Dockerfile 0.4%
HTML 0.2%