GNL CRM — Diseño de Base de Datos Documento técnico — Referencia de esquema y arquitectura de datos

Diseño de Base de Datos — GNL CRM

Documento técnico de referencia — Describe el esquema completo, las relaciones entre tablas, los índices, la generación de códigos, el sistema de migraciones, la importación masiva desde Excel y la API REST expuesta por el servidor.

Contenido

  1. Arquitectura general
  2. Modelo Entidad-Relación
  3. Descripción detallada de cada tabla
    1. Pais
    2. Entidades
    3. Contactos
    4. Oportunidades
    5. Documentos
    6. Usuarios
  4. Índices y rendimiento
  5. Códigos auto-generados (CON / OPO / DOC)
  6. Migraciones de esquema
  7. Importación masiva desde Excel
  8. API REST — tabla de endpoints
  9. Estructura de ficheros del proyecto
  10. Consideraciones para reimplementación

1. Arquitectura General

GNL CRM es una aplicación web de una sola página (Single-Page Application) con arquitectura de tres capas:

+------------------------------------------------------------------+ | NAVEGADOR (cliente) | | index.html + /js/app.js + /css/style.css | | - Autenticacion (login/logout) | | - Vistas: Dashboard, Paises, Entidades, Contactos, etc. | | - Mapa mundial (Leaflet.js) | | - Chat IA | | - Formularios CRUD + importacion Excel | +---------------------------+--------------------------------------+ | HTTP / JSON (fetch API) | Sesion: cookie express-session +---------------------------v--------------------------------------+ | SERVIDOR (Node.js) | | server.js -- Express.js v5 | | - Middleware: express-session, express.json, multer | | - Rutas autenticadas (requireAuth) | | - Rutas de administrador (requireAdmin) | | - Sirve /public como archivos estaticos | +---------------------------+--------------------------------------+ | better-sqlite3 (sincrono) +---------------------------v--------------------------------------+ | BASE DE DATOS (SQLite 3) | | crm.db -- fichero unico en disco | | WAL mode | foreign_keys = ON | sin servidor externo | | | | Pais <---- Entidades <---- Contactos | | <---- Oportunidades | | <---- Documentos | | Usuarios (tabla independiente, gestion de acceso) | +------------------------------------------------------------------+

Tecnologías y versiones

ComponenteTecnologíaNotas
Motor de BDSQLite 3Incluido en better-sqlite3; sin proceso separado
Driver de BDbetter-sqlite3API síncrona; no usa callbacks ni promesas
Servidor HTTPExpress.js v5Manejo de rutas, middleware, sesiones
Sesionesexpress-sessionAlmacenamiento en memoria del proceso Node.js
Subida de ficherosmulterUsado exclusivamente para POST /api/import
Lectura de Excelxlsx (SheetJS)Convierte hojas Excel a arrays de objetos
FrontendHTML5 + CSS3 + JS VanillaSin React, Vue ni Angular; sin npm en el cliente
MapasLeaflet.js + OpenStreetMapCargados desde CDN en el navegador
IconosFont Awesome 6Cargado desde CDN en el navegador
Puerto3000 (por defecto)Configurable con variable de entorno PORT

Modo WAL y claves foráneas

Al iniciar la aplicación, database.js ejecuta los siguientes pragmas antes de cualquier otra operación:

db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');

2. Modelo Entidad-Relación

El modelo de datos gira en torno a la entidad central Entidades (empresas / contrapartes). El resto de tablas de negocio dependen de ella mediante claves foráneas. La tabla Pais actúa como catálogo geográfico referenciado desde Entidades. La tabla Usuarios es completamente independiente y gestiona el control de acceso.

+-------------+ +-------------------+ | Pais | 1 | Entidades | |-------------|<---------|-------------------| | CodigoPais | N | CodigoEntidad PK | | Nombre | | Compania | | Region | | CodigoPais FK | | ... | | Tipo | +-------------+ | LEI, Ticker, ... | +---------+---------+ | 1 ---------------+--------------- | | | | N | N | N +---------v------+ +-----v-------+ +----v--------+ | Contactos | |Oportunidades| | Documentos | |----------------| |-------------| |-------------| | id PK | | id PK | | id PK | | CodigoContacto | | CodigoOpor. | | CodigoDOC | | CodigoEntidad FK| |CodigoEnt.FK | |CodigoEnt.FK | | Nombre | | Contraparte | | KYC_S_N | | Cargo, Email | | Volumen | | NDA_S_N | | Telefono1/2 | | Precio | | MSPA S_N | | ... | | Timing, ... | | Links, ... | +----------------+ +-------------+ +-------------+ +-------------------+ | Usuarios | (tabla independiente) |-------------------| | id PK | | username UNIQUE | | password (hash) | | nombre | | rol (admin/user) | +-------------------+

Cardinalidades

RelaciónCardinalidadClave foránea
Pais → Entidades1 Pais tiene N Entidades (0..N)Entidades.CodigoPaisNormalizadoPais.CodigoPaisNormalizado
Entidades → Contactos1 Entidad tiene N Contactos (0..N)Contactos.CodigoEntidadEntidades.CodigoEntidad
Entidades → Oportunidades1 Entidad tiene N Oportunidades (0..N)Oportunidades.CodigoEntidadEntidades.CodigoEntidad
Entidades → Documentos1 Entidad tiene N Documentos (0..N)Documentos.CodigoEntidadEntidades.CodigoEntidad
No existe una relación directa entre Contactos y Oportunidades en el esquema actual. Si en el futuro se requiere asociar personas de contacto a oportunidades específicas, sería necesario añadir una tabla de relación N:M o una columna CodigoContacto en Oportunidades.

3. Descripción Detallada de Cada Tabla

Leyenda de etiquetas:

3a. Tabla Pais

Catálogo de países con información geopolítica y comercial. Actúa como tabla de referencia (lookup table) para la columna CodigoPaisNormalizado de Entidades. La clave primaria es el código ISO 3166-1 alpha-3 (tres letras), que garantiza unicidad y compatibilidad internacional.

ColumnaTipo SQLiteNulableEtiquetaDescripción
CodigoPaisNormalizadoTEXTNOPKCódigo ISO 3166-1 alpha-3. Ejemplos: ESP, QAT, USA, NGA. Identificador único del país.
NombreTEXTNombre completo del país en español. Ej.: «España», «Qatar», «Nigeria».
RegionTEXTRegión geopolítica o comercial. Ej.: «Europa», «Oriente Medio», «África Subsahariana».
ReferenciaIndiceTEXTReferencia a índice de riesgo país u otras métricas comerciales internas.
LinkFichaPaisTEXTURL o ruta a la ficha de análisis del país (informe interno, SharePoint, etc.).
PersonaReferenciaOportunTEXTNombre del responsable interno de oportunidades en ese país.
ComentariosTEXTNotas libres sobre el país: contexto político, restricciones de mercado, etc.
CREATE TABLE IF NOT EXISTS Pais (
  CodigoPaisNormalizado    TEXT PRIMARY KEY,
  Nombre                   TEXT,
  Region                   TEXT,
  ReferenciaIndice         TEXT,
  LinkFichaPais            TEXT,
  PersonaReferenciaOportun TEXT,
  Comentarios              TEXT
);

3b. Tabla Entidades

Tabla central del CRM. Representa empresas, grupos empresariales o contrapartes con las que GNL trabaja o pretende trabajar. Cada entidad tiene un código único de formato libre asignado manualmente (ej. ENT-00001). Todas las tablas de detalle (Contactos, Oportunidades, Documentos) apuntan a esta tabla.

ColumnaTipo SQLiteNulableEtiquetaDescripción
CodigoEntidadTEXTNOPKCódigo único de la entidad, asignado manualmente. Formato convencional: ENT-00001.
CompaniaTEXTNORazón social o nombre comercial de la empresa. Campo NOT NULL.
RegionTEXTRegión operativa de la entidad. Puede diferir de la región del país.
TipoTEXTClasificación jerárquica de la empresa. Valores habituales: Matriz, Filial, Joint Venture.
CodigoPaisNormalizadoTEXTFKPaís de la entidad. Referencia Pais.CodigoPaisNormalizado. Puede ser NULL si el país no está catalogado.
FiscalCodeTEXTCódigo fiscal o NIF/VAT number de la empresa en su país de registro.
LEITEXTLegal Entity Identifier (norma ISO 17442). Código de 20 caracteres alfanuméricos para identificación global de entidades financieras.
TickerTEXTSímbolo bursátil si la empresa cotiza en bolsa. Ej.: SHEL, TTE.
DunsNumberTEXTNúmero D-U-N-S (Dun & Bradstreet). Identificador de 9 dígitos para verificación de empresas.
DireccionTEXTDirección postal de la sede o delegación de la entidad.
ComentariosTEXTNotas libres sobre la entidad: historial, estrategia, alertas, etc.
CREATE TABLE IF NOT EXISTS Entidades (
  CodigoEntidad         TEXT PRIMARY KEY,
  Compania              TEXT NOT NULL,
  Region                TEXT,
  Tipo                  TEXT,
  CodigoPaisNormalizado TEXT REFERENCES Pais(CodigoPaisNormalizado),
  FiscalCode            TEXT,
  LEI                   TEXT,
  Ticker                TEXT,
  DunsNumber            TEXT,
  Direccion             TEXT,
  Comentarios           TEXT
);

3c. Tabla Contactos

Personas de contacto vinculadas a una entidad. Cada contacto recibe un código correlativo auto-generado con formato CON-00001. Una entidad puede tener múltiples contactos. La columna ProbabilidadExito permite priorizar el seguimiento comercial.

ColumnaTipo SQLiteNulableEtiquetaDescripción
idINTEGERNOPKIdentificador numérico interno, auto-incrementado por SQLite. No se expone directamente en la interfaz.
CodigoContactoTEXTIDXCódigo de negocio auto-generado. Formato CON-NNNNN. Índice único idx_contactos_codigo. Se genera en database.js antes del INSERT.
CodigoEntidadTEXTNOFKEntidad a la que pertenece este contacto. NOT NULL. Referencia Entidades.CodigoEntidad.
NombreTEXTNombre completo de la persona de contacto.
CargoTEXTPuesto o cargo en la empresa. Ej.: «CEO», «Head of LNG Trading», «Commercial Manager».
EmailTEXTDirección de correo electrónico profesional.
Telefono1TEXTTeléfono principal (puede incluir prefijo internacional).
Telefono2TEXTTeléfono alternativo o móvil.
ViaTEXTCanal preferido de contacto. Ej.: «Email», «WhatsApp», «LinkedIn», «Teléfono».
FechaUltimoContactoTEXTFecha del último contacto realizado (formato libre TEXT, recomendado ISO 8601: YYYY-MM-DD).
DemorarContactoAfechaTEXTFecha hasta la que se debe posponer el contacto. Permite gestionar contactos en modo «en espera».
ProbabilidadExitoTEXTValoración cualitativa del potencial comercial. Valores controlados: Muy Alta, Alta, Media, Baja.
LinkedinTEXTURL del perfil de LinkedIn del contacto.
ComentariosTEXTNotas libres: historial de conversaciones, intereses, alertas de seguimiento.
CREATE TABLE IF NOT EXISTS Contactos (
  id                    INTEGER PRIMARY KEY AUTOINCREMENT,
  CodigoContacto        TEXT,
  CodigoEntidad         TEXT NOT NULL REFERENCES Entidades(CodigoEntidad),
  Nombre                TEXT,
  Cargo                 TEXT,
  Email                 TEXT,
  Telefono1             TEXT,
  Telefono2             TEXT,
  Via                   TEXT,
  FechaUltimoContacto   TEXT,
  DemorarContactoAfecha TEXT,
  ProbabilidadExito     TEXT,
  Linkedin              TEXT,
  Comentarios           TEXT
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_contactos_codigo
  ON Contactos(CodigoContacto);

3d. Tabla Oportunidades

Registra oportunidades comerciales de compra/venta de GNL asociadas a una entidad. Cada oportunidad recibe un código correlativo auto-generado OPO-00001. Contiene información operativa y comercial específica del negocio de gas natural licuado.

ColumnaTipo SQLiteNulableEtiquetaDescripción
idINTEGERNOPKIdentificador numérico interno auto-incrementado.
CodigoOportunidadTEXTIDXCódigo de negocio auto-generado. Formato OPO-NNNNN. Índice único idx_oportunidades_codigo.
CodigoEntidadTEXTNOFKEntidad asociada a la oportunidad. NOT NULL. Referencia Entidades.CodigoEntidad.
ContraparteTEXTNombre de la contraparte en la operación (puede diferir del nombre de la entidad registrada).
OwnerAccountTEXTResponsable interno de la cuenta o la oportunidad dentro de GNL.
EntregaTEXTCondiciones o lugar de entrega del GNL (término Incoterm o terminal). Ej.: DES Bilbao, FOB Sabine Pass.
PeriodoTEXTVentana temporal de la oportunidad o del contrato. Ej.: «2025-2030», «Q1 2026».
VolumenTEXTVolumen de GNL negociado (TEXT para flexibilidad de unidades: TBtu, mmBtu, TM, cargamentos).
PrecioTEXTPrecio o fórmula de precio acordada o propuesta. Ej.: «JKM + 0.5», «TTF - 2%».
SpecsContrapartePCSTEXTEspecificaciones de calidad (Power Content Specifications) requeridas por la contraparte.
ProximosPasosNTGYTEXTPróximas acciones a realizar por el equipo interno de GNL (Naturgy).
ProximosPasosContraparteTEXTPróximas acciones pendientes de la contraparte.
TimingTEXTEstado de urgencia o plazo clave de la oportunidad. Ej.: «Urgente», «Q2 2026», «En negociación».
OrigenTEXTCómo se originó la oportunidad. Ej.: «Inbound», «Prospección», «Referido», «Feria sectorial».
ComentariosTEXTNotas libres sobre la oportunidad: contexto, riesgos, condiciones especiales.
CREATE TABLE IF NOT EXISTS Oportunidades (
  id                        INTEGER PRIMARY KEY AUTOINCREMENT,
  CodigoOportunidad         TEXT,
  CodigoEntidad             TEXT NOT NULL REFERENCES Entidades(CodigoEntidad),
  Contraparte               TEXT,
  OwnerAccount              TEXT,
  Entrega                   TEXT,
  Periodo                   TEXT,
  Volumen                   TEXT,
  Precio                    TEXT,
  SpecsContrapartePCS       TEXT,
  ProximosPasosNTGY         TEXT,
  ProximosPasosContraparte  TEXT,
  Timing                    TEXT,
  Origen                    TEXT,
  Comentarios               TEXT
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_oportunidades_codigo
  ON Oportunidades(CodigoOportunidad);

3e. Tabla Documentos

Registro del estado documental y de cumplimiento (compliance) de cada entidad. Almacena si existen los acuerdos legales fundamentales (KYC, NDA, MSPA) y enlaces a los mismos. Una entidad puede tener múltiples registros de documentos, aunque lo habitual es uno por entidad.

ColumnaTipo SQLiteNulableEtiquetaDescripción
idINTEGERNOPKIdentificador numérico interno auto-incrementado.
CodigoDocumentoTEXTIDXCódigo de negocio auto-generado. Formato DOC-NNNNN. Índice único idx_documentos_codigo.
CodigoEntidadTEXTNOFKEntidad a la que corresponden estos documentos. NOT NULL. Referencia Entidades.CodigoEntidad.
KYC_S_NTEXTIndica si existe documentación KYC (Know Your Customer). Valores: / No.
KYC_linkTEXTURL o ruta al fichero de KYC en el repositorio documental.
NDA_S_NTEXTIndica si existe un NDA (Non-Disclosure Agreement) firmado. Valores: / No.
FechaExpiracionNDATEXTFecha de vencimiento del NDA. Formato recomendado ISO 8601 (YYYY-MM-DD).
NDALinkTEXTURL o ruta al NDA firmado en el repositorio documental.
MSPASNTEXTIndica si existe un MSPA (Master Sales and Purchase Agreement) firmado. Valores: / No.
LinkMSPATEXTURL o ruta al MSPA firmado.
ComentariosTEXTNotas sobre el estado documental: documentos en negociación, renovaciones pendientes, etc.
CREATE TABLE IF NOT EXISTS Documentos (
  id                 INTEGER PRIMARY KEY AUTOINCREMENT,
  CodigoDocumento    TEXT,
  CodigoEntidad      TEXT NOT NULL REFERENCES Entidades(CodigoEntidad),
  KYC_S_N            TEXT,
  KYC_link           TEXT,
  NDA_S_N            TEXT,
  FechaExpiracionNDA TEXT,
  NDALink            TEXT,
  MSPASN             TEXT,
  LinkMSPA           TEXT,
  Comentarios        TEXT
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_documentos_codigo
  ON Documentos(CodigoDocumento);

3f. Tabla Usuarios

Tabla de control de acceso. Completamente independiente del modelo de negocio. Los usuarios se autentican contra esta tabla; las contraseñas deben almacenarse hasheadas (nunca en texto plano). El campo rol determina los permisos de la sesión.

ColumnaTipo SQLiteNulableEtiquetaDescripción
idINTEGERNOPKIdentificador numérico interno auto-incrementado.
usernameTEXTNOUNIQUENombre de usuario para el login. Único en la tabla. NOT NULL.
passwordTEXTNOContraseña hasheada. NOT NULL. Nunca se almacena ni se devuelve en texto plano.
nombreTEXTNombre visible del usuario en la interfaz (nombre real o alias).
rolTEXTNORol del usuario. DEFAULT 'user'. Valores en uso: admin (acceso total, gestión de usuarios), comercial (acceso normal sin administración). NOT NULL.
CREATE TABLE IF NOT EXISTS Usuarios (
  id       INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT UNIQUE NOT NULL,
  password TEXT NOT NULL,
  nombre   TEXT,
  rol      TEXT NOT NULL DEFAULT 'user'
);
Las contraseñas deben hashearse antes de almacenarse. La API de login compara el hash almacenado con el hash del password recibido; nunca devuelve el campo password en ninguna respuesta de la API.

4. Índices y Rendimiento

SQLite crea automáticamente un índice B-tree para cada PRIMARY KEY y para cada columna declarada como UNIQUE. Adicionalmente, la aplicación define los siguientes índices explícitos:

Nombre del índiceTablaColumna(s)TipoPropósito
idx_contactos_codigoContactosCodigoContactoUNIQUEBúsqueda y unicidad del código de negocio. Evita duplicados al generar CON-NNNNN.
idx_oportunidades_codigoOportunidadesCodigoOportunidadUNIQUEBúsqueda y unicidad del código de negocio OPO-NNNNN.
idx_documentos_codigoDocumentosCodigoDocumentoUNIQUEBúsqueda y unicidad del código de negocio DOC-NNNNN.

Índices implícitos (creados por SQLite automáticamente)

TablaColumnaMotivo
PaisCodigoPaisNormalizadoPRIMARY KEY TEXT
EntidadesCodigoEntidadPRIMARY KEY TEXT
ContactosidPRIMARY KEY INTEGER (rowid alias)
OportunidadesidPRIMARY KEY INTEGER (rowid alias)
DocumentosidPRIMARY KEY INTEGER (rowid alias)
UsuariosidPRIMARY KEY INTEGER
UsuariosusernameUNIQUE constraint

Consideraciones de rendimiento con SQLite en WAL


5. Códigos Auto-Generados (CON / OPO / DOC)

Tres tablas usan un identificador de negocio legible por humanos además del id numérico interno: Contactos (CON-NNNNN), Oportunidades (OPO-NNNNN) y Documentos (DOC-NNNNN).

Algoritmo de generación

La lógica reside en database.js y se ejecuta en el mismo proceso Node.js (síncrono, gracias a better-sqlite3) justo antes de cada INSERT. El algoritmo es:

  1. Obtener el código máximo existente en la tabla con una consulta SELECT MAX(CodigoContacto) (o equivalente para OPO/DOC).
  2. Extraer el número del código (los últimos 5 dígitos tras el guion).
  3. Incrementar en 1.
  4. Formatear con ceros a la izquierda hasta 5 dígitos.
  5. Componer el nuevo código: prefijo + '-' + número formateado.
// Ejemplo conceptual en database.js (Contactos)
function generateCodigoContacto() {
  const row = db.prepare(
    "SELECT MAX(CodigoContacto) as max FROM Contactos"
  ).get();

  if (!row.max) return 'CON-00001';

  // Extraer numero: 'CON-00042' -> 42
  const num = parseInt(row.max.split('-')[1], 10);
  const next = num + 1;

  // Formatear a 5 digitos: 43 -> '00043'
  return 'CON-' + String(next).padStart(5, '0');
}
Este mecanismo es seguro para la aplicación actual porque better-sqlite3 ejecuta las operaciones de forma síncrona y Node.js es de un solo hilo. No existe una race condition entre la lectura del MAX y el INSERT. En una reimplementación con drivers asíncronos (pg, mysql2), sería necesario usar una transacción explícita o una secuencia de base de datos.

Características del formato

TablaPrefijoEjemploCapacidad máxima
ContactosCON-CON-00001, CON-0004299.999 contactos
OportunidadesOPO-OPO-00001, OPO-0001599.999 oportunidades
DocumentosDOC-DOC-00001, DOC-0000799.999 documentos

Si el número de registros superara los 99.999, sería suficiente ampliar el padStart(5, '0') a padStart(6, '0') o más, sin cambiar el esquema de la base de datos.


6. Migraciones de Esquema

La aplicación no usa un framework de migraciones externo (como Flyway o Liquibase). En su lugar, database.js implementa una función runMigrations() que se ejecuta al arrancar el servidor, después de la creación inicial de tablas.

Estrategia de migración

Cada migración consiste en un ALTER TABLE ... ADD COLUMN ... envuelto en un bloque try/catch. Si la columna ya existe, SQLite lanza un error que se captura y se ignora silenciosamente. Esto hace que las migraciones sean idempotentes: se pueden ejecutar múltiples veces sin efectos secundarios.

function runMigrations() {
  const migrations = [
    `ALTER TABLE Entidades ADD COLUMN DunsNumber TEXT`,
    `ALTER TABLE Contactos ADD COLUMN DemorarContactoAfecha TEXT`,
    `ALTER TABLE Oportunidades ADD COLUMN SpecsContrapartePCS TEXT`,
  ];

  for (const sql of migrations) {
    try {
      db.prepare(sql).run();
      console.log('[DB] Migration OK:', sql);
    } catch (e) {
      // Column already exists -- safe to ignore
      if (!e.message.includes('duplicate column name')) {
        console.warn('[DB] Migration warning:', e.message);
      }
    }
  }
}

Flujo de arranque de la base de datos

  1. Abrir la conexión: new Database('crm.db')
  2. Activar pragmas: WAL + foreign_keys = ON
  3. Crear tablas: sentencias CREATE TABLE IF NOT EXISTS para las 6 tablas
  4. Crear índices: sentencias CREATE UNIQUE INDEX IF NOT EXISTS
  5. Ejecutar migraciones: runMigrations() — añade columnas nuevas si no existen
  6. Crear usuario admin por defecto: si la tabla Usuarios está vacía, inserta un usuario inicial
Limitación de SQLite en migraciones: SQLite no soporta ALTER TABLE ... DROP COLUMN en versiones anteriores a la 3.35.0, ni ALTER TABLE ... MODIFY COLUMN. Para renombrar o eliminar columnas en versiones antiguas, la estrategia es: crear tabla nueva, copiar datos, eliminar tabla antigua y renombrar. En la versión actual de la aplicación no se ha necesitado este procedimiento.

Cómo añadir una nueva columna

  1. Añadir la sentencia ALTER TABLE NombreTabla ADD COLUMN NuevaColumna TEXT al array migrations en database.js.
  2. Actualizar los helpers CRUD de database.js para incluir la nueva columna.
  3. Actualizar los formularios HTML en index.html y la lógica de formularios en app.js.
  4. Reiniciar la aplicación. La migración se aplicará automáticamente en el arranque.

7. Importación Masiva desde Excel

El endpoint POST /api/import permite cargar datos masivamente desde un fichero Excel (.xlsx). La lógica de importación reside en database.js (función bulkImport) y el servidor usa multer para la recepción del fichero.

Flujo de importación

  1. El usuario selecciona un fichero .xlsx en la interfaz y hace clic en «Importar».
  2. El frontend envía el fichero al servidor mediante POST /api/import con multipart/form-data.
  3. Multer guarda el fichero temporalmente en la carpeta /uploads.
  4. El servidor llama a xlsx.readFile() para parsear el Excel.
  5. Se itera sobre las hojas del fichero Excel, cada una correspondiente a una tabla.
  6. La función bulkImport() procesa los datos y ejecuta las operaciones de base de datos.
  7. El fichero temporal en /uploads se elimina automáticamente al finalizar la importación.
  8. El servidor devuelve un resumen de las operaciones realizadas (insertados, actualizados, errores).

Estrategia UPSERT por tipo de tabla

TablaEstrategiaComportamiento
PaisUPSERTSi el CodigoPaisNormalizado ya existe, actualiza los campos. Si no existe, inserta.
EntidadesUPSERTSi el CodigoEntidad ya existe, actualiza los campos. Si no existe, inserta.
ContactosINSERTSiempre inserta nuevas filas. Genera un nuevo CodigoContacto automáticamente.
OportunidadesINSERTSiempre inserta nuevas filas. Genera un nuevo CodigoOportunidad automáticamente.
DocumentosINSERTSiempre inserta nuevas filas. Genera un nuevo CodigoDocumento automáticamente.

Implementación del UPSERT para tablas padre

SQLite soporta la sintaxis INSERT OR REPLACE. La implementación usa esta forma:

// UPSERT para Pais
const upsertPais = db.prepare(`
  INSERT OR REPLACE INTO Pais
    (CodigoPaisNormalizado, Nombre, Region, ReferenciaIndice,
     LinkFichaPais, PersonaReferenciaOportun, Comentarios)
  VALUES
    (@CodigoPaisNormalizado, @Nombre, @Region, @ReferenciaIndice,
     @LinkFichaPais, @PersonaReferenciaOportun, @Comentarios)
`);

// UPSERT para Entidades
const upsertEntidad = db.prepare(`
  INSERT OR REPLACE INTO Entidades
    (CodigoEntidad, Compania, Region, Tipo, CodigoPaisNormalizado,
     FiscalCode, LEI, Ticker, DunsNumber, Direccion, Comentarios)
  VALUES
    (@CodigoEntidad, @Compania, @Region, @Tipo, @CodigoPaisNormalizado,
     @FiscalCode, @LEI, @Ticker, @DunsNumber, @Direccion, @Comentarios)
`);
INSERT OR REPLACE en SQLite elimina la fila existente y la reinserta. Para Pais y Entidades, cuya PK es TEXT y está definida externamente, esto no representa ningún problema. Para tablas con PK INTEGER AUTOINCREMENT, no usar INSERT OR REPLACE si se desea conservar el id original.

Estructura esperada del fichero Excel

El fichero Excel debe tener una hoja por tabla, con los nombres de columna exactos en la primera fila (cabecera). El orden recomendado de hojas es:

  1. Pais — con columna CodigoPaisNormalizado como primera columna.
  2. Entidades — con columna CodigoEntidad como primera columna.
  3. Contactos — con columna CodigoEntidad (referencia a entidad existente).
  4. Oportunidades — con columna CodigoEntidad.
  5. Documentos — con columna CodigoEntidad.
Las hojas de Pais y Entidades deben importarse antes que las hojas hijas para que las claves foráneas se puedan resolver. El orden de las hojas en el fichero Excel es determinante.

Carpeta /uploads

Multer usa la carpeta /uploads como almacenamiento temporal. El fichero se elimina inmediatamente tras ser procesado mediante fs.unlinkSync(). La carpeta debe existir en el servidor y tener permisos de escritura para el proceso Node.js. No se almacenan ficheros de datos del usuario de forma permanente en el servidor.


8. API REST — Tabla de Endpoints

Todos los endpoints (salvo POST /api/login) requieren sesión activa. Los marcados como Admin requieren adicionalmente rol = 'admin'. Las respuestas son siempre JSON.

Autenticación y sesión

MétodoRutaDescripciónAuth
POST/api/loginAutenticar usuario. Body: {username, password}. Crea sesión y devuelve datos del usuario.Pública
POST/api/logoutDestruir la sesión activa.Usuario
GET/api/meDevuelve datos del usuario autenticado (id, username, nombre, rol). Sin campo password.Usuario

Dashboard y búsqueda

MétodoRutaDescripciónAuth
GET/api/dashboardEstadísticas del CRM: conteos por tabla, distribución por región/tipo, datos para gráficas.Usuario
GET/api/search?q=&pais=Búsqueda libre entre Entidades, Contactos y Oportunidades. Parámetros: q (texto libre) y pais (filtro de país opcional).Usuario

Países

MétodoRutaDescripción
GET/api/paisesListado de todos los países.
POST/api/paisesCrear un nuevo país.
GET/api/paises/:idObtener un país por CodigoPaisNormalizado.
PUT/api/paises/:idActualizar un país existente.
DELETE/api/paises/:idEliminar un país (falla si tiene entidades asociadas, por integridad referencial).

Entidades

MétodoRutaDescripción
GET/api/entidadesListado de todas las entidades (con JOIN a Pais para nombre de país).
POST/api/entidadesCrear una nueva entidad.
GET/api/entidades/:idObtener una entidad por CodigoEntidad.
PUT/api/entidades/:idActualizar una entidad existente.
DELETE/api/entidades/:idEliminar una entidad (falla si tiene registros hijos asociados, por integridad referencial).
GET/api/entidades/:codigo/detailObtener la entidad con todos sus Contactos, Oportunidades y Documentos en un único objeto JSON.
GET/api/entidades-listListado ligero para selectores desplegables en formularios (solo CodigoEntidad + Compania).

Contactos

MétodoRutaDescripción
GET/api/contactosListado de todos los contactos.
POST/api/contactosCrear un nuevo contacto (genera CodigoContacto automáticamente).
GET/api/contactos/:idObtener un contacto por id.
PUT/api/contactos/:idActualizar un contacto existente.
DELETE/api/contactos/:idEliminar un contacto.

Oportunidades

MétodoRutaDescripción
GET/api/oportunidadesListado de todas las oportunidades.
POST/api/oportunidadesCrear una nueva oportunidad (genera CodigoOportunidad automáticamente).
GET/api/oportunidades/:idObtener una oportunidad por id.
PUT/api/oportunidades/:idActualizar una oportunidad existente.
DELETE/api/oportunidades/:idEliminar una oportunidad.

Documentos

MétodoRutaDescripción
GET/api/documentosListado de todos los documentos.
POST/api/documentosCrear un nuevo registro de documentos (genera CodigoDocumento automáticamente).
GET/api/documentos/:idObtener un registro de documentos por id.
PUT/api/documentos/:idActualizar un registro de documentos.
DELETE/api/documentos/:idEliminar un registro de documentos.

Usuarios (requiere rol admin)

MétodoRutaDescripciónAuth
GET/api/usuariosListado de todos los usuarios (sin campo password).Admin
POST/api/usuariosCrear un nuevo usuario.Admin
PUT/api/usuarios/:idActualizar datos de un usuario (nombre, rol, password).Admin
DELETE/api/usuarios/:idEliminar un usuario.Admin

Perfil propio e importación

MétodoRutaDescripción
GET/api/mi-perfilDatos del usuario en sesión (sin password).
PUT/api/mi-perfilActualizar nombre y/o contraseña del usuario en sesión.
POST/api/importImportación masiva desde fichero Excel (multipart/form-data, campo file). Devuelve resumen de operaciones.
POST/api/ai/chatProcesador de consultas en lenguaje natural. Recibe {message} y devuelve respuesta generada con datos de la BD.

9. Estructura de Ficheros del Proyecto

/CRM
├── server.js                  # Servidor Express.js: rutas API, middleware auth,
│                              # gestion de sesiones, multer para uploads
├── database.js                # Modulo SQLite: inicializacion del esquema,
│                              # runMigrations(), helpers CRUD, generacion de
│                              # codigos, bulkImport(), procesador AI
├── crm.db                     # Fichero de base de datos SQLite (WAL mode)
│                              # Contiene todos los datos. Se genera en el
│                              # primer arranque si no existe.
├── package.json               # Dependencias Node.js y scripts de arranque
├── GUIA_IMPLEMENTACION.md     # Documentacion de despliegue para IT
│
├── /public                    # Ficheros estaticos servidos por Express
│   ├── index.html             # SPA shell: estructura HTML, sidebar de navegacion,
│   │                          # todos los divs de vista, modal generico
│   ├── /js
│   │   └── app.js             # Toda la logica frontend: autenticacion, navegacion,
│   │                          # operaciones CRUD, mapa Leaflet, chat IA, formularios,
│   │                          # importacion Excel, gestion de toasts y modales
│   ├── /css
│   │   └── style.css          # Estilos completos: sidebar, tablas, modales,
│   │                          # toasts, mapa, formularios, responsive
│   ├── guia.html              # Guia de implementacion IT (documento estatico)
│   ├── guia-usuario.html      # Manual de usuario (documento estatico)
│   ├── diseno-bbdd.html       # Este documento: diseno de BD (documento estatico)
│   └── presentacion.html      # Presentacion del proyecto (documento estatico)
│
└── /uploads                   # Carpeta temporal para Excel importados
                               # Los ficheros se eliminan tras cada importacion.

Descripción de los ficheros principales

server.js

Punto de entrada de la aplicación. Define y registra todos los middlewares de Express (sesión, JSON parser, ficheros estáticos, multer) y todas las rutas de la API REST. Contiene los middlewares requireAuth y requireAdmin que protegen las rutas privadas. Llama a las funciones de database.js para las operaciones de datos.

database.js

Módulo central de acceso a datos. Abre la conexión SQLite, inicializa el esquema, ejecuta las migraciones y exporta funciones CRUD para cada tabla. También contiene la función bulkImport() para la importación desde Excel y el procesador de consultas en lenguaje natural para el chat IA.

crm.db

Fichero binario de SQLite. Es la base de datos completa de la aplicación. Se genera automáticamente en el primer arranque. En modo WAL, SQLite puede crear también los ficheros auxiliares crm.db-wal y crm.db-shm durante las operaciones de escritura; estos ficheros se consolidan en el .db principal cuando se cierra correctamente la conexión.

public/js/app.js

Fichero JavaScript de frontend más importante. Contiene toda la lógica del cliente: gestión del estado de autenticación, navegación entre vistas (sin router), llamadas fetch() a la API, renderizado dinámico de tablas, gestión del formulario modal, inicialización del mapa Leaflet, interfaz del chat IA y lógica de importación Excel.


10. Consideraciones para Reimplementación

Esta sección recoge los aspectos críticos a tener en cuenta si se desea portar la aplicación a otro lenguaje de programación, framework web o sistema de base de datos.

10.1 Modo WAL de SQLite

Si se mantiene SQLite como motor de base de datos, el pragma journal_mode = WAL debe activarse en cada apertura de conexión. En frameworks que usan connection pooling, esto implica ejecutar el pragma en el evento de creación de cada conexión del pool (hook on_connect o equivalente), no una sola vez al arrancar el servidor.

Si se migra a PostgreSQL o MySQL, el WAL es gestionado internamente por el motor y no requiere configuración explícita. La migración de datos puede realizarse con sqlite3 crm.db .dump y adaptación del SQL al dialecto destino.

10.2 Activación de Claves Foráneas en SQLite

SQLite no activa las claves foráneas por defecto por compatibilidad hacia atrás. El pragma PRAGMA foreign_keys = ON es obligatorio en cada conexión. Sin él, los REFERENCES declarados en el esquema son ignorados silenciosamente y es posible insertar datos huérfanos. En PostgreSQL y MySQL, las claves foráneas están activas por defecto.

-- Siempre ejecutar al abrir la conexion SQLite:
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;

10.3 Lógica de UPSERT en la Importación Masiva

La sintaxis exacta del UPSERT varía según el motor de base de datos:

MotorSintaxis UPSERT
SQLite (≥ 3.24)INSERT INTO ... ON CONFLICT(col) DO UPDATE SET ...
SQLite (cualquier versión)INSERT OR REPLACE INTO ... (reemplaza toda la fila)
PostgreSQLINSERT INTO ... ON CONFLICT(col) DO UPDATE SET ...
MySQL / MariaDBINSERT INTO ... ON DUPLICATE KEY UPDATE ...
SQL ServerSentencia MERGE

10.4 Generación de Códigos Correlativos (CON / OPO / DOC)

La implementación actual usa SELECT MAX(CodigoCampo) + incremento sincrónico. Esto funciona porque better-sqlite3 es síncrono y Node.js es de un único hilo. Al reimplementar en un entorno con drivers asíncronos o múltiples hilos/procesos, se necesita alguna de estas alternativas:

10.5 Gestión de Sesiones

La aplicación usa express-session con almacenamiento en memoria del proceso Node.js. Esto implica:

Al reimplementar, evaluar el uso de JWT (JSON Web Tokens) si se prefiere una arquitectura sin estado (stateless), aunque esto requiere una estrategia de revocación de tokens.

10.6 Campos de Fecha como TEXT

Los campos de fecha en todas las tablas son de tipo TEXT en SQLite. La aplicación no valida el formato de las fechas: el usuario puede introducir texto libre en campos como FechaUltimoContacto. Al migrar a PostgreSQL o MySQL, considerar cambiar estos campos a DATE o TIMESTAMP y añadir validación en el servidor.

10.7 Restricciones de Integridad No Modeladas en BD

Algunas reglas de negocio se aplican a nivel de código (no a nivel de base de datos mediante CHECK CONSTRAINT):

Al reimplementar, se recomienda añadir CHECK CONSTRAINT en la base de datos para reforzar estas restricciones a nivel de motor, eliminando la dependencia de la validación de capa de aplicación.

10.8 Seguridad a Considerar

10.9 Backup y Recuperación

Al ser la base de datos un único fichero (crm.db), la estrategia de backup es sencilla:

En Azure App Service, configurar un backup automático diario incluirá el fichero crm.db. En una VM, usar un cron job (Linux) o Tarea Programada (Windows) para ejecutar el backup periódicamente.

GNL CRM — Documento de Diseño de Base de Datos — Generado el 01/03/2026
Referencia técnica interna — No para distribución externa