b9201a437c
Replaces the filesystem-scan hero (fs.readdirSync of /public/footage/main) with a fully CMS-driven HeroSlide model. Editors can now drag-drop reorder, toggle slides on/off, set focal points for proper mobile cropping, and auto-translate per-slide captions. NEW SCHEMA (additive — does not touch existing tables) - HeroSlide: mediaUrl, mediaType, altText, order, isActive, focalPointX, focalPointY, translationsJson, timestamps - SiteSetting: key-value JSON store for site-wide config (favicon, logo, footer, OG image) — wired up in next commit - Migration 20260504120000_add_hero_slides_and_site_settings/migration.sql uses CREATE TABLE IF NOT EXISTS, additive only HERO REEL REFACTOR (Bug #4 — responsive mobile/iPad) - Switches from `images: string[]` to `slides: HeroSlideData[]` while keeping a backwards-compat path so legacy callers still work - w-screen → w-full max-w-[100vw] (no horizontal scroll on iOS) - h-[100vh] → h-[100svh] so iOS Safari URL bar doesn't push content - Reduces title font sizes on small viewports (text-3xl → text-4xl → text-5xl → text-[5.5rem]) so the headline stays inside the canvas - objectPosition driven by focal-point fields per slide - Native <video> support for video slides HQ COMMAND — /hq-command/dashboard/hero - Drag-drop reorder, click-to-set-focal-point, inline alt-text editing - Auto-save with "Saving…" / "Saved ✓" indicators - Per-slide caption overrides (title, subtitle, descriptions) - Optional one-click AI translation to IT, VEC, ES, DE - Drop-zone uploader → /api/assets (scope=footage, flat folder) API — /api/assets - New flat scopes: "footage" (writes to /public/footage/main) and "branding" (writes to /public/branding) — slug-less for site-wide assets - New buildPublicUrl helper centralises the URL convention - Revalidate helper expanded with branding + settings scopes HOME PAGE - Reads hero slides from DB first; falls back to filesystem scan when HeroSlide table is empty (so production keeps working immediately after migration runs but before the editor populates rows) DEPLOY NOTES - After git pull on VPS, run the migration ONCE: docker compose exec app npx prisma migrate deploy Then: docker compose up -d --build app Existing data (AdminUser w/ 2FA, ClientUser, GlobalNode, Application, TimelineEvent, NewsArticle, HeritageSection, SparePart, OperationsSignal, NotificationRoute, PageContent) is NOT touched. Migration only creates two new tables.
316 lines
12 KiB
Plaintext
316 lines
12 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
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 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
|
||
|
||
// 🌍 MOTOR DE TRADUCCIONES
|
||
translationsJson String? @default("{}")
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 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
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 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
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 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])
|
||
|
||
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
|
||
}
|
||
|
||
// ------------------------------------------------------
|
||
// 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
|
||
} |