- agruparPorZona (lib/funnel/fotos.ts): helper puro que agrupa fotos
(antes/después) y notas por zona, con fallback al tipo del lead. 5 tests.
- getLead trae también lead_notas.
- LeadFotosGaleria: galería por zona (Antes/Después + notas) que sustituye el
grid plano de la ficha del panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Llamada (/llamada): "Llamar ahora" dispara la llamada saliente de Retell;
"Programar" registra fecha/hora. Tras pedir, ofrece enviar por email el
enlace al formulario para subir las imágenes.
- WhatsApp (/whatsapp): arranca la conversación vía webhook al flujo externo
(con el teléfono del lead) y pide confirmación; al confirmar, agradece y
sigue por WhatsApp.
- actions: pedirLlamada, enviarEnlaceFormularioEmail, iniciarWhatsapp,
confirmarWhatsapp.
Verificado en navegador: las 3 pantallas y sus transiciones.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Paso intermedio /solicitud/[id]: el cliente elige llamada, WhatsApp o
formulario (crearLead ahora redirige aquí, no a /fotos).
- /formulario: FormularioZonas permite añadir varias zonas, cada una con tipo,
m², acabado, notas y fotos; /fotos queda como redirect.
- guardarDetallesYFotos: guarda fotos (antes, por zona) y notas (por zona),
agrega los campos del lead (m² suma, tipo único o 'integral', calidad más
alta, tasteText concatenado) para el presupuesto orientativo inmediato, y
señala perfilCompleto al flujo externo.
- Elimina FotosUploader (sustituido por FormularioZonas).
Verificado en navegador: 2 zonas → presupuesto al instante + notas por zona +
evento de perfil en DB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
api-docs/README.md: método/URL, auth Bearer, esquema del cuerpo (items
foto/texto, zona, momento, flags), respuesta y errores (401/404/422), ejemplos
curl de los 5 casos, y los payloads de los 3 webhooks salientes (perfil,
entrega WhatsApp, arranque WhatsApp) con cómo el flujo externo devuelve las
"después" por el mismo EP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /api/leads/:id/ingesta (Bearer FUNNEL_API_KEY): acepta items foto/texto
etiquetados por zona y momento, más flags perfilCompleto y finalizar.
- ingesta-schema.ts: zod del cuerpo (union discriminada foto|texto), exportado
para test; rechaza llamadas vacías.
- route.ts: auth 401, valida lead (404), inserta fotos (orden continúa el máx)
y notas, traza fotos_subidas; perfilCompleto→señalarPerfilCompleto,
finalizar→finalizarYEntregar.
- 10 tests del schema. Verificado por HTTP: 401/200/422/404 y finalizar genera
el pdf_url y avanza el lead a whatsapp_entregado.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- webhooks.ts: postWebhook best-effort + señalarGeneracionPerfil,
notificarFlujoWhatsapp (entrega) e iniciarConversacionWhatsapp (arranque).
- perfil.ts: señalarPerfilCompleto arma el JSON por zona (notas + fotos
antes/después) y lo manda al flujo externo; deja traza render_generado.
- finalizar.ts: finalizarYEntregar construye el PDF, persiste pdf_url, envía
el email (siempre) y la señal de WhatsApp, y avanza el lead a entregado.
- orchestrator: comentario en Paso 7 apuntando a la entrega real en finalizar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- mailer.ts: transport nodemailer perezoso desde env; enviarPresupuestoEmail
(adjunta el PDF) y enviarEnlaceFormulario. Best-effort: sin SMTP configurado
o ante error devuelven false sin lanzar.
- COPY-GUIDE §6.b: copy literal de ambos emails al cliente final.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- build-presupuesto.ts: construirPresupuestoPdf(leadId) agrupa fotos y notas
por zona (fallback al tipoReforma del lead), convierte las imágenes con
resolverImagenPdf y arma el PDF. Carga el lead por id sin scoping (uso
interno desde el route del panel y desde la finalización pública).
- tenant-queries: getTenantPerfilById(tenantId) sin auth; getTenantPerfil lo
reutiliza con el tenant de la sesión.
- PresupuestoDoc: prop zonas + sección "Imágenes de tu reforma" (antes/después
lado a lado + notas por zona).
- route del panel: refactor para reutilizar construirPresupuestoPdf (DRY),
manteniendo getLead como guardia de auth/404.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Variables nuevas (todas opcionales vía zod, sin romper build/demo):
- FUNNEL_API_KEY: clave Bearer del EP de ingesta.
- SMTP_* + EMAIL_FROM: envío de email del presupuesto/enlace.
- PERFIL/WHATSAPP/WHATSAPP_START webhook URLs: señales al flujo externo.
- APP_URL: base para enlaces absolutos.
Helpers emailConfigurado()/perfilWebhookConfigurado()/whatsappWebhookConfigurado()/
whatsappStartConfigurado(). nodemailer como dep directa (stack: Email SMTP).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prepara el modelo de datos para la ingesta multicanal del perfil del lead:
- lead_fotos: columnas momento (foto_momento antes/despues, default antes) y
zona (tipo_reforma, nullable con fallback al tipoReforma del lead).
- lead_notas: tabla append-only de datos de texto por zona (ej. "suelo
premium"), con origen (ep|funnel|panel) para auditar quién los aportó.
- Migración 0008 + regenerado db-schema/schema.sql.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Genera db-schema/schema.sql (DDL completo: 12 enums, 14 tablas, FKs e
índices) a partir del schema Drizzle, para que el equipo pueda consultar y
proponer cambios sobre el modelo de datos sin leer las migraciones una a una.
- db-schema/schema.sql: foto del esquema actual (drizzle-kit export)
- db-schema/README.md: qué es cada tabla, cómo cambiar el modelo y cómo
regenerar/levantar la BD desde este SQL
- package.json: script db:export para regenerar la foto
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Incluye una imagen WebP optimizada (48caf530-fa5f-476b-8120-195cfce7a9ec.webp) y una imagen PNG de ejemplo de reformas (reformas-ejemplo.png) para usar en la galería de trabajos y demostraciones del funnel.
Integra el agente de voz de Retell (Arquitectura A para la demo): tras la
pre-llamada, el orquestador lanza una llamada saliente real con override de
agente y dynamic variables (empresa + cliente), mientras el render y el
presupuesto se siguen generando con los datos del formulario.
- src/lib/env.ts: esquema zod de RETELL_* (claves opcionales -> sin ellas el
funnel sigue en modo simulado y el build no se rompe)
- src/lib/voice/retell.ts: cliente fino con fetch a create-phone-call
(sin nueva dependencia), normalización E.164 y builder de variables
- orchestrator: dispara la llamada best-effort y guarda el callId
- tests del normalizador de teléfono y del builder de variables
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Añade bajo el título un párrafo orientativo: el precio final se fija tras
la visita gratuita y la estimación se basa en datos estadísticos ajustados
para acercarse lo máximo posible al importe definitivo.
- Añade una sección con el render del resultado y una descripción generada a
partir de los materiales (materialesRender) y el estilo de la llamada, con
fallback elegante cuando faltan datos.
- @react-pdf solo incrusta PNG/JPEG: convierte con sharp los WebP/SVG (render
y logo) que antes se descartaban en silencio dejando el PDF sin imagen. El
render va a JPEG redimensionado (PDF ~360 KB en vez de ~2,7 MB) y el logo a
PNG para conservar transparencia.
- Fija sharp como dependencia directa (ya venía como transitiva de Next).
- Copy nuevo añadido primero a COPY-GUIDE.md (sección entrega del presupuesto).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Los tres uploaders del panel (logo, "quiénes somos" y galería) ahora abren
un recorte interactivo (zoom + encuadre) y reescalan en el navegador antes
de subir: logo y "quiénes somos" a máx. 500 px, galería a máx. 1200 px,
reencodando a WebP (calidad 0.82). El SVG del logo se sube tal cual para no
rasterizar el vector. Esto reduce el peso de los data URIs base64 que se
guardan en Postgres y se inlinean en el funnel.
Nueva dependencia react-easy-crop: librería de recorte ligera, sin estado
global, compatible con React 19; el reescalado y reencodado se hacen con
canvas nativo (lib/image/crop.ts), sin dependencias extra.
Sustituye la paleta negra/azul B2C del panel del reformista por el verde
de marca, neutros cálidos y titulares en Instrument Serif de la landing B2B.
Añade tokens --color-primary-*, --color-stone-50 y --font-display al @theme.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Vista de leads en tarjetas + tabla con toggle (tarjetas por defecto, preferencia persistida)
- Galería de trabajos: gestión en /panel/galeria y bloque público en el funnel
- Selector de tema por reformista (presets + color de marca opcional) aplicado a la landing
- Login y registro rediseñados a pantalla partida 50/50 con foto de reforma
- Enlace "Entrar" funcional en la cabecera del funnel; elimina Navbar muerto
- Unifica tipografía y botones del panel con los tokens de la landing
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
- / y /b2b sirven la landing B2B estática (rewrites beforeFiles)
- /{slug} resuelve el funnel del reformista (app/[slug]/page.tsx) con
branding propio (TenantBrand) y atribución de leads por tenant
- crearLead(slug) y páginas /solicitud usan el tenant del lead
- Panel: edición del slug del funnel + URL pública en /panel/empresa
- Helper de slugs reservados para evitar colisiones con rutas reales
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reemplaza public/b2b.html (bundle estático viejo) por la versión con
reveals al scroll, hover-lift, count-up de stats e imágenes antes/después.
Assets servidos desde /b2b-assets para evitar colisiones. Sirve en el
dominio único reformix.dv3.com.es vía el rewrite /b2b existente.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sirve index.html + assets (fuentes woff2 e imágenes webp) para
desplegar la landing como app estática en Dokploy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Desempaqueta el bundle autocontenido a index.html + assets servidos
(17 fuentes woff2, pares antes/después webp). Añade scroll-reveal sutil
con stagger, hover-lift en tarjetas, counters animados en stats y dos
imágenes reales: render en la burbuja de WhatsApp y comparativa
antes/después en "Cómo funciona". Respeta prefers-reduced-motion.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tarjeta 03 de la landing B2B pasa de "PDF lento" a regalar trabajo:
horas de oficina que no factura nadie cuando la obra no se ejecuta.
Reflejado en COPY-GUIDE.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
El formulario de la landing ahora crea un lead real en BD y redirige a
/solicitud/[id]/fotos, donde el cliente sube fotos y datos de la reforma.
El orquestador simula los pasos de IA (pre-llamada, llamada, render) y
calcula el presupuesto DE VERDAD con el catálogo del reformista, dejando
el lead listo en el panel con render y desglose.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
La sección Presupuesto (PDF) usaba lead.pdfUrl, que nunca se rellena en el
MVP, así que siempre mostraba "Aún no generado". Ahora apunta a la ruta
on-demand /panel/[id]/presupuesto cuando existe desglose, con un parámetro
?download=1 que fuerza Content-Disposition: attachment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Las filas del catálogo, la configuración general y el formulario de alta
no estaban pensados para móvil (input de precio descolgado, etiquetas que
desalineaban la cuadrícula, campos sueltos). Ahora las filas apilan
nombre + metadatos + controles de forma limpia en móvil y mantienen la
fila única en escritorio; la cuadrícula usa etiquetas legibles y alinea
los inputs; el alta es una cuadrícula de 2 columnas en móvil.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Las filas del catálogo y la cabecera CSV se desbordaban horizontalmente
en móvil (botones Guardar/Borrar fuera de pantalla), y ese overflow
horizontal desestabilizaba la barra de navegación fija. Las filas ahora
hacen wrap y el bloque <code> rompe palabra.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>