afcaf991b5
Editors can now control the order applications appear in, the same way they already reorder Hero slides. - Application gains an `order Int @default(0)` column (additive migration 20260605120000_add_application_order, IF NOT EXISTS, safe for deploy) plus an (isActive, order) index. - New reorderApplications(orderedSlugs) server action — single $transaction renumbering, mirrors reorderHeroSlides. - HQ applications panel: rows are now draggable by a grip handle (HTML5 DnD, optimistic local reorder, persisted on drop, toast feedback). - All public-facing queries now order by [order asc, createdAt asc]: home ApplicationsDashboard + GlobalOperations, the footer apps list, and the HQ list itself. Existing rows default to 0 so current order is preserved until the editor drags something. Verified: production build compiles, TypeScript clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
419 lines
16 KiB
Plaintext
419 lines
16 KiB
Plaintext
// This is your Prisma schema file,
|
||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
//url = env("DATABASE_URL")
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 1. BÓVEDA DE SEGURIDAD (Usuarios del CMS)
|
||
// ------------------------------------------------------
|
||
model AdminUser {
|
||
id String @id @default(cuid())
|
||
username String @unique
|
||
email String? // 🔥 NUEVO CAMPO: Correo del administrador
|
||
passwordHash String
|
||
twoFactorSecret String?
|
||
is2FAEnabled Boolean @default(false)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 2. EL GLOBO HOLOGRÁFICO (Nodos y Casos de Estudio Profundos)
|
||
// ------------------------------------------------------
|
||
model GlobalNode {
|
||
id String @id @default(cuid())
|
||
title String // Ej: "Toray Advanced Textiles"
|
||
location String
|
||
// Ej: "Tokyo, Japan"
|
||
lat Float // Ej: 35.6895
|
||
lon Float // Ej: 139.6917
|
||
|
||
// Taxonomía
|
||
nodeType String @default("installation") // "installation", "event", "hq"
|
||
application String // Ej: "textile-drying", "hq", "event"
|
||
stats String // Ej: "2,400 kg/h throughput"
|
||
isActive Boolean @default(true) // Permite ocultar un nodo sin borrarlo
|
||
|
||
// 📖 GEO-CHRONICLE (THE STORY)
|
||
projectOverview String? // El Artículo completo / Resumen del evento (Markdown)
|
||
energySavings String? // Métrica (Ej: "-45% vs Conventional" o "Stand 4B")
|
||
eventDate DateTime?// Fecha para controlar si el evento es pasado o futuro
|
||
|
||
// 🔥 NUEVOS CAMPOS FASE 1: MULTIMEDIA Y DATASHEET ESPECÍFICO 🔥
|
||
mediaFileName String? // Imagen de Portada principal
|
||
galleryJson String? @default("[]") // Array de imágenes extra
|
||
videosJson String? @default("[]") // Links a videos reales
|
||
specificDatasheetJson String? @default("[]") // Ficha técnica de ESTA máquina
|
||
model3DPath String? // Ruta al archivo GLB/USDZ
|
||
rendersJson String? @default("[]") // Renders 3D fotorrealistas
|
||
model3DDimsJson String? // Dimensiones físicas AR: { w, h, d, unit, weight }
|
||
|
||
// 🌍 MOTOR DE TRADUCCIONES
|
||
translationsJson String? @default("{}")
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([isActive])
|
||
@@index([nodeType])
|
||
@@index([nodeType, isActive])
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 3. LA BASE DE CONOCIMIENTO (Páginas de Aplicaciones)
|
||
// ------------------------------------------------------
|
||
model Application {
|
||
id String @id @default(cuid())
|
||
slug String @unique // Ej: "textile-drying" (Debe coincidir con la URL)
|
||
title String
|
||
subtitle String
|
||
category String
|
||
|
||
// 🔥 NUEVO: La descripción corta para las tarjetas de la página principal
|
||
shortDescription String @default("Learn more about this FLUX RF technology application.")
|
||
heroDescription String // Recibirá MARKDOWN para la teoría científica general
|
||
|
||
// JSONs para estructuras complejas
|
||
sectionsJson String
|
||
advantagesJson String
|
||
datasheetJson String
|
||
|
||
// Métricas Rápidas para el Dashboard
|
||
dashboardMetricsJson String? @default("[]")
|
||
isActive Boolean @default(true) // 🔥 NUEVO: Para poder ocultarlas
|
||
|
||
// 🔥 NUEVO: Orden manual para drag-to-reorder en el frontend (como HeroSlide)
|
||
order Int @default(0)
|
||
|
||
// 🌍 MOTOR DE TRADUCCIONES
|
||
translationsJson String? @default("{}")
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([isActive])
|
||
@@index([category])
|
||
@@index([isActive, order])
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 4. NUESTRA HISTORIA (Línea de tiempo de la empresa)
|
||
// ------------------------------------------------------
|
||
model TimelineEvent {
|
||
id String @id @default(cuid())
|
||
year String // Ej: "1978" o "1990s"
|
||
title String
|
||
description String
|
||
order Int @default(0) // Para ordenar cronológicamente
|
||
isActive Boolean @default(true)
|
||
|
||
// 🌍 MOTOR DE TRADUCCIONES
|
||
translationsJson String? @default("{}")
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 5. INSIDE FLUX (Motor de Noticias y Detrás de Cámaras)
|
||
// ------------------------------------------------------
|
||
model NewsArticle {
|
||
id String @id @default(cuid())
|
||
slug String @unique
|
||
title String
|
||
excerpt String // Resumen corto para la tarjeta
|
||
content String // El artículo completo (Markdown)
|
||
coverImage String? // Ej: "team-meeting.jpg"
|
||
category String @default("News")
|
||
publishedAt DateTime @default(now())
|
||
isActive Boolean @default(true)
|
||
|
||
// Editor avanzado
|
||
order Int @default(0) // Para ordenar las noticias
|
||
galleryJson String? @default("[]") // Galería de imágenes extra
|
||
linkedinUrl String? // Enlace oficial para LinkedIn
|
||
|
||
// 🌍 MOTOR DE TRADUCCIONES
|
||
translationsJson String? @default("{}")
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([isActive])
|
||
@@index([isActive, publishedAt(sort: Desc)])
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 6. OUR HERITAGE (La Historia Profunda de Patrizio)
|
||
// ------------------------------------------------------
|
||
model HeritageSection {
|
||
id String @id @default(cuid())
|
||
type String @default("text") // "text", "image", "video"
|
||
title String?
|
||
content String? // Párrafos de la historia
|
||
mediaUrl String? // Ej: "patrizio-1980.jpg" o enlace de YouTube
|
||
order Int @default(0) // Para ordenar cómo se lee la página
|
||
|
||
// 🌍 MOTOR DE TRADUCCIONES
|
||
translationsJson String? @default("{}")
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 7. COMPONENT MATRIX (Catálogo de Repuestos)
|
||
// ------------------------------------------------------
|
||
model SparePart {
|
||
id String @id @default(cuid())
|
||
sku String @unique // Identificador único / Referencia (Ej: "FLX-GEN-001")
|
||
title String // Nombre de la pieza en Inglés
|
||
description String // Descripción técnica / Función (Markdown)
|
||
|
||
// Multimedia & Ficha Técnica
|
||
mediaJson String? @default("[]") // Imágenes, videos, renders 3D
|
||
specsJson String? @default("[]") // Array de métricas [{label: "Voltage", value: "24V"}]
|
||
|
||
// Estrategia de Ventas
|
||
price Float? // Precio (Opcional)
|
||
showPrice Boolean @default(false) // Interruptor: true = mostrar precio, false = "Request Quote"
|
||
isActive Boolean @default(true) // Para ocultar repuestos descontinuados
|
||
|
||
// 🌍 MOTOR DE TRADUCCIONES (Integración con aiTranslator.ts)
|
||
translationsJson String? @default("{}")
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([isActive])
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 8. OPERATIONS INBOX (Signal Hub - Mesa de Ayuda y Órdenes)
|
||
// ------------------------------------------------------
|
||
model OperationsSignal {
|
||
id String @id @default(cuid())
|
||
ticketId String @unique
|
||
ticketNumber Int @default(autoincrement()) // Sequential for analytics
|
||
type String // "ORDER", "DIAGNOSTIC", "CONSULTATION"
|
||
status String @default("PENDING") // "PENDING", "REVIEWING", "RESOLVED"
|
||
|
||
// Client data
|
||
clientName String
|
||
clientEmail String
|
||
clientCompany String
|
||
clientPhone String?
|
||
message String?
|
||
|
||
// Payloads
|
||
cartPayload String? @default("[]")
|
||
attachedFiles String? @default("[]")
|
||
aiAnalysis String?
|
||
|
||
// Email delivery tracking
|
||
emailSentTo String? // Comma-separated list of emails that received notification
|
||
emailSentAt DateTime? // When the email was dispatched
|
||
emailError String? // Error message if email failed
|
||
|
||
// 🔥 NUEVO: Relación opcional con el Cliente Registrado (Para el futuro CRM)
|
||
clientId String?
|
||
client ClientUser? @relation(fields: [clientId], references: [id])
|
||
|
||
// FluxAI telemetry back-ref: which AI conversations converted into this ticket.
|
||
conversations AiConversation[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([type])
|
||
@@index([status])
|
||
@@index([createdAt(sort: Desc)])
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 9. RUTAS DE NOTIFICACIÓN (Gestión de Emails)
|
||
// ------------------------------------------------------
|
||
model NotificationRoute {
|
||
id String @id @default(cuid())
|
||
routeType String @unique // Ej: "ORDER", "DIAGNOSTIC", "CONSULTATION"
|
||
emails String // Correos separados por coma (Ej: "sales@fluxsrl.com, tech@fluxsrl.com")
|
||
isActive Boolean @default(true)
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 10. PAGE CONTENT (Metadata y Textos de Páginas)
|
||
// ------------------------------------------------------
|
||
model PageContent {
|
||
id String @id @default(cuid())
|
||
slug String @unique // Identificador de la página (Ej: "parts-catalog")
|
||
title String
|
||
subtitle String?
|
||
description String?
|
||
|
||
// 🌍 MOTOR DE TRADUCCIONES
|
||
translationsJson String? @default("{}")
|
||
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 11. HERO REEL (Carrusel principal del Home)
|
||
// ------------------------------------------------------
|
||
// Manages the rotating images/videos shown in the home hero section.
|
||
// Replaces the previous filesystem-scan approach (fs.readdirSync of
|
||
// /public/footage/main) with full CMS control: ordering, on/off toggle,
|
||
// focal-point per slide for proper responsive cropping on mobile/tablet,
|
||
// and per-slide alt text for SEO.
|
||
model HeroSlide {
|
||
id String @id @default(cuid())
|
||
mediaUrl String // Public path, e.g. "/footage/main/01_tifas.png"
|
||
mediaType String @default("image") // "image" | "video"
|
||
altText String? // For accessibility + SEO; falls back to title if null
|
||
|
||
order Int @default(0)
|
||
isActive Boolean @default(true)
|
||
|
||
// Focal point for object-position on mobile/tablet crops (0–1 range).
|
||
// Lets the editor pick "what should stay visible when the image is cropped".
|
||
focalPointX Float @default(0.5)
|
||
focalPointY Float @default(0.5)
|
||
|
||
// Optional per-slide caption that overrides the global Hero text.
|
||
// Stored as JSON keyed by locale: {"en":{"title":"...","subtitle":"..."}}
|
||
translationsJson String? @default("{}")
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 12. SITE SETTINGS (Favicon, Footer, Branding global)
|
||
// ------------------------------------------------------
|
||
// Single-row pattern (key-value) for global site config that doesn't
|
||
// fit any other model: favicon, logos, footer, OG image, social links.
|
||
model SiteSetting {
|
||
id String @id @default(cuid())
|
||
key String @unique // e.g. "favicon", "footer", "logo", "og_image", "hero_text"
|
||
valueJson String @default("{}") // Flexible JSON payload per key
|
||
|
||
// 🌍 Translation engine (used for things like footer link labels)
|
||
translationsJson String? @default("{}")
|
||
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 13b. FLUXAI TELEMETRY (Conversaciones + eventos del chat IA)
|
||
// ------------------------------------------------------
|
||
// Persiste cada conversación con FluxAI para análisis de funnel B2B.
|
||
// Una conversación se identifica por sessionId (UUID generado en cliente,
|
||
// persistido en localStorage). Los eventos individuales (mensajes,
|
||
// tool calls, errores) viven en AiEvent.
|
||
model AiConversation {
|
||
id String @id @default(cuid())
|
||
sessionId String @unique
|
||
visitorIp String? // sha256(ip + SESSION_SECRET) — pseudonymous
|
||
userAgent String?
|
||
locale String? // "it","en","es","fr","de"
|
||
pageUrl String? // entry page
|
||
industryLabel String? // SPIN-detected: "textile","food", etc.
|
||
funnelStage String @default("DISCOVERY") // DISCOVERY|QUALIFY|RECOMMEND|HANDOFF
|
||
outcome String @default("OPEN") // OPEN|CONSULTATION|ABANDONED
|
||
messageCount Int @default(0)
|
||
toolCallCount Int @default(0)
|
||
estimatedSavingsPercent Float?
|
||
productionVolume String?
|
||
signalId String?
|
||
signal OperationsSignal? @relation(fields: [signalId], references: [id])
|
||
startedAt DateTime @default(now())
|
||
lastMessageAt DateTime @default(now())
|
||
closedAt DateTime?
|
||
events AiEvent[]
|
||
|
||
@@index([funnelStage])
|
||
@@index([outcome])
|
||
@@index([startedAt(sort: Desc)])
|
||
@@index([industryLabel])
|
||
}
|
||
|
||
model AiEvent {
|
||
id String @id @default(cuid())
|
||
conversationId String
|
||
conversation AiConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
||
type String // "user_msg" | "ai_msg" | "tool_call" | "tool_result" | "error"
|
||
payloadJson String // truncated to 8KB at write time
|
||
toolName String?
|
||
latencyMs Int?
|
||
tokensIn Int?
|
||
tokensOut Int?
|
||
cachedTokens Int? // populated when OpenAI returns cached_tokens
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([conversationId, createdAt])
|
||
@@index([type])
|
||
@@index([toolName])
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 13. CLIENT PORTAL (Usuarios B2B Aprobados)
|
||
// ------------------------------------------------------
|
||
model ClientUser {
|
||
id String @id @default(cuid())
|
||
email String @unique
|
||
passwordHash String
|
||
fullName String
|
||
companyName String
|
||
phone String?
|
||
|
||
// Control de Acceso
|
||
isApproved Boolean @default(false) // Requiere aprobación del Admin
|
||
|
||
// Historial de Compras/Tickets
|
||
signals OperationsSignal[]
|
||
|
||
lastLoginAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 14. THE TEAM (Equipo de FLUX — página pública + CMS)
|
||
// ------------------------------------------------------
|
||
// Minimal LinkedIn-style profiles. Editable in the HQ Command Center with
|
||
// drag-to-reorder (same pattern as HeroSlide). Name stays as written; role
|
||
// and bio are translatable through the AI translation engine. Social links
|
||
// are all optional — only the ones filled in render on the public card.
|
||
model TeamMember {
|
||
id String @id @default(cuid())
|
||
name String // Proper name — never translated
|
||
role String // Job title, e.g. "Founder & CEO" — translatable
|
||
bio String? // Short biography (Markdown allowed) — translatable
|
||
photoUrl String? // Portrait, served from /team/ bucket
|
||
|
||
// Optional social links — render only when present
|
||
email String?
|
||
linkedinUrl String?
|
||
xUrl String? // X / Twitter
|
||
websiteUrl String?
|
||
|
||
order Int @default(0) // Drag-to-reorder
|
||
isActive Boolean @default(true)
|
||
|
||
// 🌍 Translation engine — holds localized role + bio per locale
|
||
translationsJson String? @default("{}")
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([isActive, order])
|
||
} |