Add B2B reformista panel with Postgres/Drizzle data layer

Modela el funnel del lead en dos dimensiones (pipeline_stage técnico
de 7 pasos + estado comercial de 6 estados) y siembra 11 leads demo,
uno por cada momento del funnel, para analizar el siguiente paso.
Incluye panel /panel (lista + detalle RF-D-01/02) y wiring de deploy
(Dockerfile multi-stage + entrypoint migrate+seed).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-05-29 15:51:10 +02:00
parent 9020c24e68
commit f09024f753
20 changed files with 3630 additions and 2 deletions

View File

@@ -0,0 +1,62 @@
'use server';
import { and, eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
import { db } from '@/db';
import { leads, leadEstadoHistory, precisionHistory, tenants } from '@/db/schema';
import { TENANT_SLUG } from '@/lib/funnel';
async function getTenantId(): Promise<string> {
const [tenant] = await db.select().from(tenants).where(eq(tenants.slug, TENANT_SLUG)).limit(1);
if (!tenant) throw new Error('Tenant no encontrado.');
return tenant.id;
}
type Estado = (typeof leads.estado.enumValues)[number];
export async function cambiarEstado(leadId: string, estado: Estado) {
const tenantId = await getTenantId();
const [updated] = await db
.update(leads)
.set({ estado, updatedAt: new Date() })
.where(and(eq(leads.id, leadId), eq(leads.tenantId, tenantId)))
.returning();
if (!updated) throw new Error('Lead no encontrado.');
await db.insert(leadEstadoHistory).values({ leadId, estado });
revalidatePath('/panel');
revalidatePath(`/panel/${leadId}`);
}
export async function marcarGanado(leadId: string, precioFinalEuros: number) {
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.');
if (lead.presupuestoEstimado == null) {
throw new Error('El lead no tiene presupuesto estimado, no se puede calcular la precisión.');
}
const finalCents = Math.round(precioFinalEuros * 100);
const deltaPct = ((finalCents - lead.presupuestoEstimado) / lead.presupuestoEstimado) * 100;
await db.update(leads).set({ estado: 'ganado', updatedAt: new Date() }).where(eq(leads.id, leadId));
await db.insert(leadEstadoHistory).values({ leadId, estado: 'ganado' });
await db.insert(precisionHistory).values({
leadId,
estimated: lead.presupuestoEstimado,
final: finalCents,
deltaPct: deltaPct.toFixed(2),
});
revalidatePath('/panel');
revalidatePath(`/panel/${leadId}`);
}