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:
62
mvp/b2c/src/app/panel/actions.ts
Normal file
62
mvp/b2c/src/app/panel/actions.ts
Normal 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}`);
|
||||
}
|
||||
Reference in New Issue
Block a user