// FSPM — micro CRM // // SQLite no soporta enums nativos en Prisma: los campos de estado son String // y su dominio vive en src/lib/crm/constantes.ts, que es la fuente de verdad. // No inventes valores fuera de esas constantes. generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } /// Usuario del panel. rol: "ADMIN" | "VENDEDOR" model User { id String @id @default(cuid()) nombre String email String @unique passwordHash String rol String @default("VENDEDOR") puesto String? telefono String? activo Boolean @default(true) creadoEn DateTime @default(now()) contactos Contact[] oportunidades Opportunity[] actividades Activity[] pedidos Order[] reglas AssignmentRule[] @@index([rol, activo]) } /// Persona. Un mismo contacto puede generar muchas oportunidades. model Contact { id String @id @default(cuid()) nombre String email String? telefono String? /// email en minúsculas y sin espacios — clave de deduplicación emailNorm String? /// solo dígitos, últimos 10 — clave de deduplicación telefonoNorm String? empresa String? puesto String? ciudad String? /// slug de industria, ver src/data/industrias.ts industria String? notas String? propietarioId String? propietario User? @relation(fields: [propietarioId], references: [id], onDelete: SetNull) // atribución de primer toque: nunca se sobreescribe primerUtmSource String? primerUtmMedium String? primerUtmCampaign String? primerLandingPath String? primerReferrer String? creadoEn DateTime @default(now()) actualizadoEn DateTime @updatedAt oportunidades Opportunity[] pedidos Order[] @@index([emailNorm]) @@index([telefonoNorm]) @@index([propietarioId]) @@index([creadoEn]) } /// Un envío de formulario o de widget de WhatsApp convertido en oportunidad. model Opportunity { id String @id @default(cuid()) /// OPP-2026-0001 folio String @unique contactoId String contacto Contact @relation(fields: [contactoId], references: [id], onDelete: Cascade) propietarioId String? propietario User? @relation(fields: [propietarioId], references: [id], onDelete: SetNull) /// "FORMULARIO" | "WHATSAPP" origen String /// dónde se envió: "home" | "catalogo" | "producto" | "solucion" | "contacto" | "widget" canal String? /// "NUEVO" | "CONTACTADO" | "DIAGNOSTICO" | "COTIZACION" | "GANADA" | "PERDIDA" etapa String @default("NUEVO") /// "ALTA" | "MEDIA" | "BAJA" prioridad String @default("MEDIA") // interés declarado productoSlug String? productoNombre String? /// URL del producto de interés, tal como la vio el usuario productoUrl String? categoriaSlug String? industria String? /// clases de fuego separadas por coma, ej. "B,K" clasesFuego String? superficie String? urgencia String? mensaje String? valorEstimado Float? // atribución de campaña utmSource String? utmMedium String? utmCampaign String? utmTerm String? utmContent String? gclid String? fbclid String? referrer String? landingPath String? paginaUrl String? dispositivo String? userAgent String? creadoEn DateTime @default(now()) actualizadoEn DateTime @updatedAt primeraRespuestaEn DateTime? cerradoEn DateTime? motivoPerdida String? actividades Activity[] pedidos Order[] @@index([etapa]) @@index([propietarioId]) @@index([contactoId]) @@index([creadoEn]) @@index([utmCampaign]) @@index([origen]) } /// Bitácora de la oportunidad. model Activity { id String @id @default(cuid()) oportunidadId String oportunidad Opportunity @relation(fields: [oportunidadId], references: [id], onDelete: Cascade) usuarioId String? usuario User? @relation(fields: [usuarioId], references: [id], onDelete: SetNull) /// "NOTA" | "LLAMADA" | "CORREO" | "WHATSAPP" | "REUNION" | "CAMBIO_ETAPA" | "ASIGNACION" | "SISTEMA" tipo String texto String creadoEn DateTime @default(now()) @@index([oportunidadId, creadoEn]) } /// Pedido derivado de una oportunidad ganada. model Order { id String @id @default(cuid()) /// PED-2026-0001 folio String @unique oportunidadId String? oportunidad Opportunity? @relation(fields: [oportunidadId], references: [id], onDelete: SetNull) contactoId String contacto Contact @relation(fields: [contactoId], references: [id], onDelete: Cascade) propietarioId String? propietario User? @relation(fields: [propietarioId], references: [id], onDelete: SetNull) /// "BORRADOR" | "CONFIRMADO" | "EN_PROCESO" | "ENVIADO" | "ENTREGADO" | "CANCELADO" estado String @default("BORRADOR") moneda String @default("MXN") subtotal Float @default(0) iva Float @default(0) total Float @default(0) /// JSON: [{ productoSlug, nombre, cantidad, precioUnitario }] items String @default("[]") notas String? entregaEstimada DateTime? creadoEn DateTime @default(now()) actualizadoEn DateTime @updatedAt @@index([estado]) @@index([propietarioId]) @@index([creadoEn]) } /// Regla de asignación de oportunidades. Se evalúan por prioridad ascendente, /// gana la primera que coincide; si ninguna coincide, se reparte por turno /// entre los vendedores activos. model AssignmentRule { id String @id @default(cuid()) nombre String /// menor número = se evalúa primero prioridad Int @default(100) /// "INDUSTRIA" | "CATEGORIA" | "ORIGEN" | "CLASE" | "CUALQUIERA" tipo String /// valor a comparar; null para "CUALQUIERA" valor String? usuarioId String usuario User @relation(fields: [usuarioId], references: [id], onDelete: Cascade) activa Boolean @default(true) creadoEn DateTime @default(now()) @@index([activa, prioridad]) } /// Contadores para folios legibles. name: "oportunidad" | "pedido" model Counter { name String @id valor Int @default(0) }