+ >
+ );
+}
diff --git a/mvp/b2c/src/app/solicitud/actions.ts b/mvp/b2c/src/app/solicitud/actions.ts
index 2a4465a..aea15bb 100644
--- a/mvp/b2c/src/app/solicitud/actions.ts
+++ b/mvp/b2c/src/app/solicitud/actions.ts
@@ -8,8 +8,13 @@ import { db } from '@/db';
import { leads, leadFotos, leadNotas, leadPipelineEventos } from '@/db/schema';
import type { NewLeadFoto, NewLeadNota } from '@/db/schema';
import { getTenantBySlug } from '@/lib/funnel/public-queries';
+import { getTenantPerfilById } from '@/db/tenant-queries';
import { procesarLead } from '@/lib/funnel/orchestrator';
import { señalarPerfilCompleto } from '@/lib/funnel/perfil';
+import { iniciarLlamadaSaliente, construirVariablesLlamada } from '@/lib/voice/retell';
+import { iniciarConversacionWhatsapp } from '@/lib/webhooks';
+import { enviarEnlaceFormulario } from '@/lib/email/mailer';
+import { env } from '@/lib/env';
const MAX_ZONAS = 6;
const MAX_FOTOS_ZONA = 6;
@@ -207,3 +212,78 @@ export async function guardarDetallesYFotos(leadId: string, formData: FormData):
revalidatePath('/panel');
redirect(`/solicitud/${leadId}/estado`);
}
+
+// Canal llamada: el cliente pide que le llamen ahora o programa la llamada. "Ahora" dispara la
+// llamada saliente de Retell; "programar" registra la fecha y la señala (el dialing en hora lo
+// hace el flujo externo, la app no monta cron). Best-effort.
+export async function pedirLlamada(
+ leadId: string,
+ cuando: 'ahora' | string,
+): Promise<{ ok: boolean; programada?: string }> {
+ const [lead] = await db.select().from(leads).where(eq(leads.id, leadId)).limit(1);
+ if (!lead) return { ok: false };
+ const tenant = await getTenantPerfilById(lead.tenantId);
+
+ if (cuando === 'ahora') {
+ const llamada = await iniciarLlamadaSaliente({
+ telefono: lead.telefono,
+ variables: construirVariablesLlamada({ nombreEmpresa: tenant.nombreEmpresa }, lead),
+ });
+ await db.insert(leadPipelineEventos).values({
+ leadId,
+ stage: 'prellamada_enviada',
+ metadata: { via: 'llamada', cuando: 'ahora', real: Boolean(llamada), simulado: !llamada },
+ });
+ return { ok: true };
+ }
+
+ const fecha = new Date(cuando);
+ const programadaAt = Number.isNaN(fecha.getTime()) ? null : fecha.toISOString();
+ await db.insert(leadPipelineEventos).values({
+ leadId,
+ stage: 'prellamada_enviada',
+ metadata: { via: 'llamada', cuando: 'programada', programadaAt },
+ });
+ return { ok: true, programada: programadaAt ?? undefined };
+}
+
+// Canal llamada: envía al cliente un email con el enlace a su formulario para subir las imágenes.
+export async function enviarEnlaceFormularioEmail(leadId: string): Promise {
+ const [lead] = await db.select().from(leads).where(eq(leads.id, leadId)).limit(1);
+ if (!lead) return false;
+ const tenant = await getTenantPerfilById(lead.tenantId);
+ const url = `${env.APP_URL ?? ''}/solicitud/${leadId}/formulario`;
+ return enviarEnlaceFormulario({
+ to: lead.email,
+ nombre: lead.nombre,
+ empresa: tenant.nombreEmpresa,
+ url,
+ });
+}
+
+// Canal WhatsApp: arranca la conversación con el lead a través del flujo externo (que manda el
+// primer mensaje a su teléfono) y deja traza. El cliente confirma luego en la UI.
+export async function iniciarWhatsapp(leadId: string): Promise<{ ok: boolean; telefono: string }> {
+ const [lead] = await db.select().from(leads).where(eq(leads.id, leadId)).limit(1);
+ if (!lead) return { ok: false, telefono: '' };
+ const tenant = await getTenantPerfilById(lead.tenantId);
+ const ok = await iniciarConversacionWhatsapp({
+ leadId,
+ telefono: lead.telefono,
+ nombre: lead.nombre,
+ empresa: tenant.nombreEmpresa,
+ });
+ return { ok, telefono: lead.telefono };
+}
+
+// Canal WhatsApp: el cliente confirma que ha recibido el mensaje; seguimos por WhatsApp.
+export async function confirmarWhatsapp(leadId: string): Promise<{ ok: boolean }> {
+ const [lead] = await db.select({ id: leads.id }).from(leads).where(eq(leads.id, leadId)).limit(1);
+ if (!lead) return { ok: false };
+ await db.insert(leadPipelineEventos).values({
+ leadId,
+ stage: 'prellamada_enviada',
+ metadata: { via: 'whatsapp', confirmado: true },
+ });
+ return { ok: true };
+}
diff --git a/mvp/b2c/src/components/funnel/CanalLlamada.tsx b/mvp/b2c/src/components/funnel/CanalLlamada.tsx
new file mode 100644
index 0000000..a6ca70a
--- /dev/null
+++ b/mvp/b2c/src/components/funnel/CanalLlamada.tsx
@@ -0,0 +1,124 @@
+'use client';
+
+import { useState, useTransition } from 'react';
+import { pedirLlamada, enviarEnlaceFormularioEmail } from '@/app/solicitud/actions';
+
+export default function CanalLlamada({
+ leadId,
+ telefono,
+ email,
+}: {
+ leadId: string;
+ telefono: string;
+ email: string;
+}) {
+ const [pending, startTransition] = useTransition();
+ const [confirmacion, setConfirmacion] = useState(null);
+ const [mostrarProgramar, setMostrarProgramar] = useState(false);
+ const [cuando, setCuando] = useState('');
+ const [enlaceEnviado, setEnlaceEnviado] = useState(false);
+
+ const llamarAhora = () =>
+ startTransition(async () => {
+ await pedirLlamada(leadId, 'ahora');
+ setConfirmacion(`Te llamamos en menos de 2 minutos al ${telefono}. Tenlo a mano.`);
+ });
+
+ const programar = () =>
+ startTransition(async () => {
+ if (!cuando) return;
+ const r = await pedirLlamada(leadId, cuando);
+ const fecha = r.programada
+ ? new Date(r.programada).toLocaleString('es-ES', {
+ day: '2-digit',
+ month: 'long',
+ hour: '2-digit',
+ minute: '2-digit',
+ })
+ : '';
+ setConfirmacion(`Hecho. Te llamaremos el ${fecha} al ${telefono}.`);
+ });
+
+ const enviarEnlace = () =>
+ startTransition(async () => {
+ await enviarEnlaceFormularioEmail(leadId);
+ setEnlaceEnviado(true);
+ });
+
+ if (confirmacion) {
+ return (
+
+
+ ✅ {confirmacion}
+
+
+
+ Para el render necesitamos ver el espacio. Puedes mandarnos las fotos por WhatsApp
+ durante la llamada, o te enviamos un enlace al formulario por email para que las subas
+ cuando quieras.
+
+ {enlaceEnviado ? (
+
📧 Te hemos enviado el enlace a {email}.
+ ) : (
+
+ )}
+
+
+ );
+ }
+
+ return (
+
+
+ Llamarme ahora
+ Recibes la llamada en menos de 2 minutos.
+
+
+
+
+ Programar la llamada
+ Elige el día y la hora que mejor te venga.
+ {mostrarProgramar ? (
+