Enlace del email = subir solo fotos (sin re-preguntar ni re-llamar)
Arregla 2 problemas del flujo de subir fotos desde el email: - El enlace iba a /formulario (form completo) y al enviarlo re-ejecutaba procesarLead, que VOLVÍA a llamar. Ahora el email apunta a /solicitud/[id]/fotos, una página ligera (SubirFotos): solo sube fotos (+ nota opcional) al lead de la URL, re-señala perfilCompleto y NO llama. - Guarda en procesarLead: si el lead ya tiene llamada_completada, no se vuelve a llamar (ni se pisa la transcripción real del webhook). Copy de la página en COPY-GUIDE §3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { and, desc, eq } from 'drizzle-orm';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { db } from '@/db';
|
||||
@@ -257,13 +257,66 @@ export async function pedirLlamada(
|
||||
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.
|
||||
// Página ligera del enlace del email: el cliente solo sube fotos del espacio. NO ejecuta
|
||||
// procesarLead, así que NO vuelve a llamar (la llamada, si tocaba, ya se hizo). Solo guarda las
|
||||
// fotos en ESTE lead (id de la URL) y re-señala el perfil para regenerar el render con ellas.
|
||||
export async function subirFotos(leadId: string, formData: FormData): Promise<void> {
|
||||
const [lead] = await db.select().from(leads).where(eq(leads.id, leadId)).limit(1);
|
||||
if (!lead) throw new Error('Solicitud no encontrada.');
|
||||
|
||||
const archivos = formData
|
||||
.getAll('fotos')
|
||||
.filter((f): f is File => f instanceof File)
|
||||
.slice(0, MAX_FOTOS_ZONA);
|
||||
const dataUris: string[] = [];
|
||||
for (const file of archivos) {
|
||||
const uri = await fileToDataUri(file);
|
||||
if (uri) dataUris.push(uri);
|
||||
}
|
||||
const nota = String(formData.get('nota') ?? '').trim() || null;
|
||||
|
||||
if (dataUris.length > 0) {
|
||||
const [ultimo] = await db
|
||||
.select({ orden: leadFotos.orden })
|
||||
.from(leadFotos)
|
||||
.where(eq(leadFotos.leadId, leadId))
|
||||
.orderBy(desc(leadFotos.orden))
|
||||
.limit(1);
|
||||
let orden = (ultimo?.orden ?? -1) + 1;
|
||||
const filas: NewLeadFoto[] = dataUris.map((url) => ({
|
||||
leadId,
|
||||
url,
|
||||
momento: 'antes',
|
||||
zona: lead.tipoReforma ?? null,
|
||||
orden: orden++,
|
||||
}));
|
||||
await db.insert(leadFotos).values(filas);
|
||||
}
|
||||
if (nota) {
|
||||
await db
|
||||
.insert(leadNotas)
|
||||
.values({ leadId, texto: nota, zona: lead.tipoReforma ?? null, origen: 'funnel' });
|
||||
}
|
||||
await db.insert(leadPipelineEventos).values({
|
||||
leadId,
|
||||
stage: 'fotos_subidas',
|
||||
metadata: { origen: 'email', fotos: dataUris.length, notas: nota ? 1 : 0 },
|
||||
});
|
||||
|
||||
// Re-señala el perfil para que el flujo externo regenere el render con las fotos nuevas.
|
||||
await señalarPerfilCompleto(leadId);
|
||||
|
||||
revalidatePath('/panel');
|
||||
redirect(`/solicitud/${leadId}/estado`);
|
||||
}
|
||||
|
||||
// Canal llamada: envía al cliente un email con el enlace para subir las imágenes (página ligera).
|
||||
export async function enviarEnlaceFormularioEmail(leadId: string): Promise<boolean> {
|
||||
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 theme = resolveTheme(tenant.themePreset, tenant.themeColor);
|
||||
const url = `${env.APP_URL ?? ''}/solicitud/${leadId}/formulario`;
|
||||
const url = `${env.APP_URL ?? ''}/solicitud/${leadId}/fotos`;
|
||||
return enviarEnlaceFormulario({
|
||||
to: lead.email,
|
||||
nombre: lead.nombre,
|
||||
|
||||
Reference in New Issue
Block a user