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

@@ -0,0 +1,36 @@
'use server';
import { and, eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
import { db } from '@/db';
import { testimonios } from '@/db/schema';
import { getCurrentTenantId as getTenantId } from '@/lib/auth/current-user';
type TestimonioEstado = (typeof testimonios.estado.enumValues)[number];
async function setEstado(testimonioId: string, estado: TestimonioEstado) {
const tenantId = await getTenantId();
const [updated] = await db
.update(testimonios)
.set({ estado })
.where(and(eq(testimonios.id, testimonioId), eq(testimonios.tenantId, tenantId)))
.returning({ id: testimonios.id });
if (!updated) throw new Error('Opinión no encontrada.');
revalidatePath('/panel/opiniones');
}
export async function publicarTestimonio(testimonioId: string) {
await setEstado(testimonioId, 'publicado');
}
export async function ocultarTestimonio(testimonioId: string) {
await setEstado(testimonioId, 'oculto');
}
export async function eliminarTestimonio(testimonioId: string) {
const tenantId = await getTenantId();
await db
.delete(testimonios)
.where(and(eq(testimonios.id, testimonioId), eq(testimonios.tenantId, tenantId)));
revalidatePath('/panel/opiniones');
}