From 588aa4dc1c7fbab8773f9d8d111a40db13ff24d5 Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Sat, 30 May 2026 12:41:40 +0200 Subject: [PATCH] feat: wire computeBudget into recalcularPresupuesto and show desglose Co-Authored-By: Claude Sonnet 4.6 --- mvp/b2c/src/app/panel/[id]/page.tsx | 97 +++++++++++++++++++++++++++++ mvp/b2c/src/app/panel/actions.ts | 47 +++++++++++++- 2 files changed, 143 insertions(+), 1 deletion(-) diff --git a/mvp/b2c/src/app/panel/[id]/page.tsx b/mvp/b2c/src/app/panel/[id]/page.tsx index 49533fc..04d6763 100644 --- a/mvp/b2c/src/app/panel/[id]/page.tsx +++ b/mvp/b2c/src/app/panel/[id]/page.tsx @@ -10,6 +10,8 @@ import { formatEuros, formatFecha, } from '@/lib/funnel'; +import { recalcularPresupuesto } from '../actions'; +import type { BudgetResult } from '@/budget/types'; export const dynamic = 'force-dynamic'; @@ -30,6 +32,9 @@ export default async function LeadDetailPage({ params }: { params: Promise<{ id: const { lead, fotos, eventos, precision } = data; const reachedStages = new Set(eventos.map((e) => e.stage)); + const snapshot = lead.desgloseSnapshot as { result: BudgetResult } | null; + const desglose = snapshot?.result ?? null; + return (
@@ -208,6 +213,98 @@ export default async function LeadDetailPage({ params }: { params: Promise<{ id:
)} + + {/* Presupuesto desglosado */} +
+
+ +
+ + {desglose ? ( +
+ {/* Partidas */} + + + + + + + + + {desglose.partidas.map((partida) => ( + + + + + ))} + +
PartidaImporte
{partida.label} + {formatEuros(partida.importe)} +
+ + {/* Subtotal, factor zona, total */} +
+
+ Subtotal + {formatEuros(desglose.subtotal)} +
+
+ Factor de zona + ×{desglose.factorZona.toFixed(2)} +
+
+ Total estimado + {formatEuros(desglose.total)} +
+
+ + {/* Rango */} +
+ Rango orientativo + + {formatEuros(desglose.rango.min)} – {formatEuros(desglose.rango.max)} + +
+ + {/* Confianza */} +
+ Confianza del cálculo + + {desglose.confianza} + +
+ + {/* Avisos */} + {desglose.avisos.length > 0 && ( +
    + {desglose.avisos.map((aviso, i) => ( +
  • ⚠ {aviso}
  • + ))} +
+ )} + + {/* Disclaimer RF-B-09 */} +

+ Presupuesto orientativo. El precio final puede variar según la visita técnica. +

+
+ ) : ( +

Aún no se ha calculado el presupuesto.

+ )} +
); } diff --git a/mvp/b2c/src/app/panel/actions.ts b/mvp/b2c/src/app/panel/actions.ts index 35bac90..e0d8351 100644 --- a/mvp/b2c/src/app/panel/actions.ts +++ b/mvp/b2c/src/app/panel/actions.ts @@ -3,8 +3,11 @@ import { and, eq } from 'drizzle-orm'; import { revalidatePath } from 'next/cache'; import { db } from '@/db'; -import { leads, leadEstadoHistory, precisionHistory, tenants } from '@/db/schema'; +import { leads, leadEstadoHistory, leadPipelineEventos, precisionHistory, tenants } from '@/db/schema'; import { TENANT_SLUG } from '@/lib/funnel'; +import { getPricingConfig, getCatalog } from '@/db/pricing-queries'; +import { computeBudget } from '@/budget'; +import type { BudgetInputs } from '@/budget/types'; async function getTenantId(): Promise { const [tenant] = await db.select().from(tenants).where(eq(tenants.slug, TENANT_SLUG)).limit(1); @@ -60,3 +63,45 @@ export async function marcarGanado(leadId: string, precioFinalEuros: number) { revalidatePath('/panel'); revalidatePath(`/panel/${leadId}`); } + +export async function recalcularPresupuesto(leadId: string) { + const tenantId = await getTenantId(); + const [lead] = await db + .select() + .from(leads) + .where(and(eq(leads.id, leadId), eq(leads.tenantId, tenantId))) + .limit(1); + if (!lead) throw new Error('Lead no encontrado.'); + + const [config, catalog] = await Promise.all([getPricingConfig(), getCatalog()]); + + const inputs: BudgetInputs = { + tipoReforma: lead.tipoReforma ?? 'otro', + m2Suelo: lead.m2Suelo ?? null, + alturaTecho: lead.alturaTecho ?? null, + calidadGlobal: lead.calidadGlobal ?? 'media', + estructural: lead.estructural, + provincia: lead.provincia ?? null, + materialSelections: (lead.materialSelections as Record) ?? {}, + }; + + const result = computeBudget(inputs, config, catalog); + + await db + .update(leads) + .set({ + presupuestoEstimado: result.total, + desgloseSnapshot: { stage: lead.pipelineStage, result }, + updatedAt: new Date(), + }) + .where(eq(leads.id, leadId)); + + await db.insert(leadPipelineEventos).values({ + leadId, + stage: 'presupuesto_generado', + metadata: { total: result.total, confianza: result.confianza }, + }); + + revalidatePath('/panel'); + revalidatePath(`/panel/${leadId}`); +}