Añade personalización SEO/Quiénes somos y testimonios gestionables por reformista
- Panel/empresa: title y meta description SEO personalizables; foto, texto y años de experiencia para el bloque "Quiénes somos" (toggle on/off). - Funnel por slug: metadata SEO desde el tenant, bloque "Quiénes somos" y testimonios servidos desde DB (sustituye los hardcodeados). - Flujo de opiniones: el reformista solicita la opinión desde la ficha de un lead ganado; el cliente la deja en un funnel dedicado /opinion/[id] con estrellas + texto + fotos; entra como pendiente y el reformista la modera (publicar/ocultar/eliminar) en /panel/opiniones antes de mostrarla. - Schema: columnas SEO/about en tenants, testimonioSolicitadoAt en leads, enum testimonio_estado, tablas testimonios + testimonio_fotos (migración 0006). - Seed: opiniones demo (2 publicadas, 1 pendiente) y contenido "Quiénes somos". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,10 @@ export const subscriptionStatus = pgEnum('subscription_status', [
|
||||
// 'automatico' = el funnel lo envía solo; 'revision' = se para para que el reformista lo revise/edite antes de enviar.
|
||||
export const envioPresupuestoMode = pgEnum('envio_presupuesto_mode', ['automatico', 'revision']);
|
||||
|
||||
// Estado de moderación de un testimonio. El reformista aprueba antes de publicar.
|
||||
// 'pendiente' = recién enviado por el cliente; 'publicado' = visible en la landing; 'oculto' = retirado.
|
||||
export const testimonioEstado = pgEnum('testimonio_estado', ['pendiente', 'publicado', 'oculto']);
|
||||
|
||||
// Reformista (cliente del SaaS). MVP = "Reformas Ejemplo" hardcoded.
|
||||
// Multi-tenant real es F1.5; la tabla ya queda lista para ello.
|
||||
export const tenants = pgTable('tenants', {
|
||||
@@ -80,6 +84,14 @@ export const tenants = pgTable('tenants', {
|
||||
logoUrl: text('logo_url'), // data URI base64 del logo (no hay storage externo aún)
|
||||
provincia: text('provincia'),
|
||||
whatsappBusiness: text('whatsapp_business'),
|
||||
// SEO personalizable de la landing del reformista (RF-A / funnel público).
|
||||
seoTitle: text('seo_title'),
|
||||
seoDescription: text('seo_description'),
|
||||
// Bloque "Quiénes somos" opcional en el funnel del reformista.
|
||||
aboutEnabled: boolean('about_enabled').notNull().default(false),
|
||||
aboutFotoUrl: text('about_foto_url'), // data URI base64 de la foto del reformista
|
||||
aboutTexto: text('about_texto'),
|
||||
aniosExperiencia: integer('anios_experiencia'),
|
||||
// Datos de empresa para la cabecera del presupuesto (RF-D-07).
|
||||
cif: text('cif'),
|
||||
direccion: text('direccion'),
|
||||
@@ -172,6 +184,9 @@ export const leads = pgTable(
|
||||
|
||||
notas: text('notas'),
|
||||
|
||||
// Cuándo el reformista pidió la opinión al cliente (RF: recogida de testimonios).
|
||||
testimonioSolicitadoAt: timestamp('testimonio_solicitado_at', { withTimezone: true }),
|
||||
|
||||
// Inputs del motor de presupuesto (capturados de menos a más en el funnel)
|
||||
m2Suelo: doublePrecision('m2_suelo'),
|
||||
alturaTecho: doublePrecision('altura_techo'),
|
||||
@@ -206,6 +221,40 @@ export const leadFotos = pgTable('lead_fotos', {
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
// Opiniones del cliente final, recogidas en el funnel de review (/opinion/[id]).
|
||||
// El reformista las solicita desde el panel y aprueba antes de que salgan en su landing.
|
||||
export const testimonios = pgTable(
|
||||
'testimonios',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
tenantId: uuid('tenant_id')
|
||||
.notNull()
|
||||
.references(() => tenants.id, { onDelete: 'cascade' }),
|
||||
leadId: uuid('lead_id').references(() => leads.id, { onDelete: 'set null' }),
|
||||
nombre: text('nombre').notNull(),
|
||||
contexto: text('contexto'), // p.ej. "Reforma de cocina · Madrid"
|
||||
rating: integer('rating').notNull(), // 1-5 estrellas
|
||||
texto: text('texto').notNull(),
|
||||
estado: testimonioEstado('estado').notNull().default('pendiente'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
index('testimonios_tenant_estado_idx').on(table.tenantId, table.estado),
|
||||
index('testimonios_lead_idx').on(table.leadId),
|
||||
]
|
||||
);
|
||||
|
||||
// Fotos adjuntas a un testimonio (el cliente sube fotos del resultado).
|
||||
export const testimonioFotos = pgTable('testimonio_fotos', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
testimonioId: uuid('testimonio_id')
|
||||
.notNull()
|
||||
.references(() => testimonios.id, { onDelete: 'cascade' }),
|
||||
url: text('url').notNull(),
|
||||
orden: integer('orden').notNull().default(0),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
// Histórico de cambios de estado comercial (RF-D-03: persistir y reflejar)
|
||||
export const leadEstadoHistory = pgTable('lead_estado_history', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
@@ -281,6 +330,9 @@ export type Tenant = typeof tenants.$inferSelect;
|
||||
export type Lead = typeof leads.$inferSelect;
|
||||
export type NewLead = typeof leads.$inferInsert;
|
||||
export type LeadFoto = typeof leadFotos.$inferSelect;
|
||||
export type Testimonio = typeof testimonios.$inferSelect;
|
||||
export type NewTestimonio = typeof testimonios.$inferInsert;
|
||||
export type TestimonioFoto = typeof testimonioFotos.$inferSelect;
|
||||
export type LeadEstadoHistory = typeof leadEstadoHistory.$inferSelect;
|
||||
export type LeadPipelineEvento = typeof leadPipelineEventos.$inferSelect;
|
||||
export type PrecisionHistory = typeof precisionHistory.$inferSelect;
|
||||
|
||||
Reference in New Issue
Block a user