ixiclinicDocs
Desarrolladores

Multi-sucursal

Cómo un laboratorio opera varias sucursales en ixiclinic — modelo de datos, sucursal activa, scoping de datos, acceso por usuario y gestión.

Un laboratorio (tenant) puede tener varias sucursales (sedes). El multi-sucursal se apoya en el modelo multi-tenant (ver Multi-tenancy): todo sigue aislado por tenantId, y dentro del tenant los datos operativos se separan por branchId. Los datos maestros (pacientes, catálogo) son compartidos entre sucursales.

Modelo de datos

  • branches (apps/api/src/db/schema/branches.ts): id, tenantId, name, code (prefijo corto para numeración de caja, p. ej. SPMSPM-000001), address, isDefault (la sede principal), isActive.
  • user_branches: acceso de un usuario a sucursales concretas. Si un usuario no tiene filas, accede a todas las sucursales de su tenant (comportamiento heredado); en cuanto tiene al menos una, queda limitado a esas.
  • Cada laboratorio nace con una sucursal "Sede Principal" (isDefault: true), tanto en el registro público como al crear un cliente desde la consola.

Qué se separa por sucursal y qué se comparte

ÁmbitoPor sucursal (branchId)Compartido (tenant)
Órdenes (orders)
Facturas (invoices)
Caja (cash_registers, cash_sessions)
Cola/turnos, citas, equipos, recetas, vitales
Almacenes (warehouses)✅ (opcional)
Pacientes (patients)✅ (el mismo paciente se atiende en cualquier sede)
Catálogo (pruebas, precios), médicos, empresas

Sucursal activa

El JWT lleva branchId además de tenantId y role (apps/api/src/lib/jwt.ts). El plugin tenant-scope (apps/api/src/plugins/tenant-scope.ts) inyecta request.tenant = { id, branchId } en cada request. Los handlers leen request.tenant.branchId para fijar la sucursal al crear datos y para filtrar listados.

Cambiar de sucursal

El selector del topbar (apps/admin/src/components/layout/org-switcher.tsx) llama a POST /api/tenants/switch, que re-emite el token con el nuevo branchId. El cambio:

  1. Valida que el usuario tenga acceso al tenant.
  2. Valida que la sucursal exista en ese tenant.
  3. Valida el acceso a la sucursal contra user_branches (vacío = acceso a todas).

GET /api/tenants/:tenantId/branches (usado por el selector) devuelve solo las sucursales activas a las que el usuario tiene acceso.

Scoping de órdenes y facturación

  • Al crear, createOrder y createInvoice/createGroupedInvoice fijan branchId = request.tenant.branchId (la sucursal activa).
  • Al listar, GET /api/orders y GET /api/billing/invoices aceptan un filtro opcional ?branchId=. El admin lo usa con un selector que por defecto apunta a la sucursal activa.
  • La migración 0043_multi_branch agrega las columnas y rellena (backfill) las órdenes y facturas existentes con la sede por defecto de su tenant, de modo que el filtrado por sucursal no deja registros huérfanos.

Compatibilidad: todo es aditivo. Un laboratorio de una sola sucursal no nota cambios (todo su dato vive en la sede por defecto y los filtros son transparentes).

Numeración

  • Caja: las sesiones se numeran por sucursal usando el code de la sucursal (SPM-000001), con un lock por (tenantId, branchId) (apps/api/src/modules/cash/service.ts).
  • Órdenes y facturas/NCF: la numeración sigue siendo a nivel de tenant (los NCF fiscales se asignan por tenant). Numeración por sucursal de órdenes/NCF es un paso futuro.

Acceso por usuario y gestión

  • Gestión de sucursales: Settings → Sucursales en el admin (crear, editar nombre/ código/dirección, marcar principal, activar/desactivar). API: GET /api/tenants/branches/manage, POST /api/tenants/:tenantId/branches, PATCH /api/tenants/:tenantId/branches/:branchId (todas gateadas por requireModule("settings"), scopeadas al tenant activo del token para evitar IDOR).
  • Asignar usuarios a sucursales: en el formulario de usuario (Settings → Usuarios), un multiselector de sucursales. API: GET/PUT /api/tenants/users/:userId/branches. Sin selección = acceso a todas.

Cómo dar conciencia de sucursal a un módulo nuevo

  1. Añade branchId (uuid, nullable, FK a branches) a la tabla del módulo.
  2. En el handler de creación, pásale request.tenant.branchId al service y guárdalo.
  3. En el listado, acepta un branchId opcional en el schema y agrégalo a las condiciones (if (params.branchId) conditions.push(eq(tabla.branchId, params.branchId))).
  4. En la migración, rellena los registros existentes con la sede por defecto del tenant.

Pendientes

  • Filtro de sucursal en el dashboard y los reportes (el modelo ya lo permite).
  • Numeración de órdenes y NCF por sucursal (hoy es por tenant).
  • Ver Known issues para el estado general.

On this page