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:
Carlos Narro
2026-06-01 12:26:13 +02:00
parent 1a1caaf0df
commit a91fe5ce2c
25 changed files with 2638 additions and 66 deletions

View File

@@ -284,7 +284,7 @@ async function main() {
console.log('Limpiando datos previos...');
await db.execute(
sql`TRUNCATE TABLE ${schema.precisionHistory}, ${schema.leadPipelineEventos}, ${schema.leadEstadoHistory}, ${schema.leadFotos}, ${schema.leads}, ${schema.sessions}, ${schema.users}, ${schema.plans}, ${schema.tenants} RESTART IDENTITY CASCADE`
sql`TRUNCATE TABLE ${schema.testimonioFotos}, ${schema.testimonios}, ${schema.precisionHistory}, ${schema.leadPipelineEventos}, ${schema.leadEstadoHistory}, ${schema.leadFotos}, ${schema.leads}, ${schema.sessions}, ${schema.users}, ${schema.plans}, ${schema.tenants} RESTART IDENTITY CASCADE`
);
console.log('Sembrando planes...');
@@ -341,6 +341,13 @@ async function main() {
planId: pro.id,
subscriptionStatus: 'trial',
trialEndsAt: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000),
seoTitle: 'Reformas Ejemplo · Reformas integrales en Madrid',
seoDescription:
'Pide tu presupuesto de reforma con render IA en minutos. Reformas de cocina, baño y vivienda completa en Madrid.',
aboutEnabled: true,
aboutTexto:
'Somos un equipo de Madrid especializado en reformas integrales de cocinas, baños y viviendas completas. Cuidamos cada detalle y te acompañamos desde la primera idea hasta la entrega de llaves, con presupuestos claros y sin sorpresas.',
aniosExperiencia: 15,
})
.returning();
@@ -430,6 +437,66 @@ async function main() {
}
}
// --- Opiniones demo (recogidas en el funnel de review, ya moderadas) ---
console.log('Sembrando opiniones demo...');
const leadsPorEmail = await db
.select({ id: schema.leads.id, email: schema.leads.email })
.from(schema.leads)
.where(eq(schema.leads.tenantId, tenant.id));
const leadIdPorEmail = new Map(leadsPorEmail.map((l) => [l.email, l.id]));
// Diego (ganado) ya tiene la opinión solicitada desde el panel.
const diegoId = leadIdPorEmail.get('diego.romero@example.com');
if (diegoId) {
await db
.update(schema.leads)
.set({ testimonioSolicitadoAt: new Date(Date.now() - 20 * 24 * 60 * 60 * 1000) })
.where(eq(schema.leads.id, diegoId));
}
const [testiDiego] = await db
.insert(schema.testimonios)
.values({
tenantId: tenant.id,
leadId: diegoId ?? null,
nombre: 'Diego Romero',
contexto: 'Reforma integral · Valencia',
rating: 5,
texto:
'Reformaron un piso heredado de 85 metros de arriba a abajo. El presupuesto inicial cuadró casi al detalle con el final y los plazos se cumplieron. Repetiría sin dudarlo.',
estado: 'publicado',
})
.returning();
if (testiDiego) {
await db
.insert(schema.testimonioFotos)
.values({ testimonioId: testiDiego.id, url: '/despues.webp', orden: 0 });
}
await db.insert(schema.testimonios).values([
{
tenantId: tenant.id,
leadId: leadIdPorEmail.get('carmen.ibanez@example.com') ?? null,
nombre: 'Carmen Ibáñez',
contexto: 'Comedor · Madrid',
rating: 5,
texto:
'Abrieron el comedor al salón y pusieron tarima nueva. El render que me enseñaron al principio era casi idéntico al resultado final. Trato impecable.',
estado: 'publicado',
},
{
// Pendiente de aprobar: aparece en el panel pero aún no en la landing.
tenantId: tenant.id,
leadId: leadIdPorEmail.get('tomas.herrero@example.com') ?? null,
nombre: 'Tomás Herrero',
contexto: 'Baño · Bilbao',
rating: 4,
texto:
'Cambiaron todo el alicatado y los sanitarios del baño. Buen acabado y limpios. Solo se retrasaron un par de días por los materiales.',
estado: 'pendiente',
},
]);
// --- Precios + catálogo demo (motor de presupuesto) ---
const [tenantRow] = await db
.select()