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:
66
mvp/b2c/src/db/queries.ts
Normal file
66
mvp/b2c/src/db/queries.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { and, asc, desc, eq } from 'drizzle-orm';
|
||||
import { db } from './index';
|
||||
import {
|
||||
leads,
|
||||
leadFotos,
|
||||
leadEstadoHistory,
|
||||
leadPipelineEventos,
|
||||
precisionHistory,
|
||||
tenants,
|
||||
} from './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 "${TENANT_SLUG}" no existe. ¿Has corrido npm run db:seed?`);
|
||||
return tenant.id;
|
||||
}
|
||||
|
||||
export type LeadFiltro = (typeof leads.estado.enumValues)[number] | 'todos';
|
||||
|
||||
export async function getLeads(filtro: LeadFiltro = 'todos') {
|
||||
const tenantId = await getTenantId();
|
||||
const where =
|
||||
filtro === 'todos'
|
||||
? eq(leads.tenantId, tenantId)
|
||||
: and(eq(leads.tenantId, tenantId), eq(leads.estado, filtro));
|
||||
|
||||
return db.select().from(leads).where(where).orderBy(desc(leads.createdAt));
|
||||
}
|
||||
|
||||
export async function getLead(id: string) {
|
||||
const tenantId = await getTenantId();
|
||||
const [lead] = await db
|
||||
.select()
|
||||
.from(leads)
|
||||
.where(and(eq(leads.id, id), eq(leads.tenantId, tenantId)))
|
||||
.limit(1);
|
||||
|
||||
if (!lead) return null;
|
||||
|
||||
const [fotos, eventos, historial, precision] = await Promise.all([
|
||||
db.select().from(leadFotos).where(eq(leadFotos.leadId, id)).orderBy(asc(leadFotos.orden)),
|
||||
db
|
||||
.select()
|
||||
.from(leadPipelineEventos)
|
||||
.where(eq(leadPipelineEventos.leadId, id))
|
||||
.orderBy(asc(leadPipelineEventos.occurredAt)),
|
||||
db
|
||||
.select()
|
||||
.from(leadEstadoHistory)
|
||||
.where(eq(leadEstadoHistory.leadId, id))
|
||||
.orderBy(asc(leadEstadoHistory.changedAt)),
|
||||
db.select().from(precisionHistory).where(eq(precisionHistory.leadId, id)),
|
||||
]);
|
||||
|
||||
return { lead, fotos, eventos, historial, precision: precision[0] ?? null };
|
||||
}
|
||||
|
||||
export async function getResumen() {
|
||||
const all = await getLeads('todos');
|
||||
const porEstado = all.reduce<Record<string, number>>((acc, l) => {
|
||||
acc[l.estado] = (acc[l.estado] ?? 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
return { total: all.length, porEstado };
|
||||
}
|
||||
Reference in New Issue
Block a user