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

93
mvp/b2c/src/lib/funnel.ts Normal file
View File

@@ -0,0 +1,93 @@
import type { leadEstado, pipelineStage, tipoReforma } from '@/db/schema';
export const TENANT_SLUG = 'reformas-ejemplo';
type Estado = (typeof leadEstado.enumValues)[number];
type Stage = (typeof pipelineStage.enumValues)[number];
type Tipo = (typeof tipoReforma.enumValues)[number];
export const ESTADOS: Estado[] = [
'nuevo',
'contactado',
'visita_agendada',
'presupuesto_enviado',
'ganado',
'perdido',
];
export const ESTADO_LABEL: Record<Estado, string> = {
nuevo: 'Nuevo',
contactado: 'Contactado',
visita_agendada: 'Visita agendada',
presupuesto_enviado: 'Presupuesto enviado',
ganado: 'Ganado',
perdido: 'Perdido',
};
// Clases Tailwind para el badge de cada estado (fondo + texto).
export const ESTADO_BADGE: Record<Estado, string> = {
nuevo: 'bg-blue-100 text-blue-700',
contactado: 'bg-amber-100 text-amber-700',
visita_agendada: 'bg-violet-100 text-violet-700',
presupuesto_enviado: 'bg-cyan-100 text-cyan-700',
ganado: 'bg-green-100 text-green-700',
perdido: 'bg-gray-200 text-gray-600',
};
export const PIPELINE_ORDER: Stage[] = [
'form_completado',
'fotos_subidas',
'prellamada_enviada',
'llamada_completada',
'render_generado',
'presupuesto_generado',
'whatsapp_entregado',
];
export const PIPELINE_LABEL: Record<Stage, string> = {
form_completado: 'Datos recibidos',
fotos_subidas: 'Fotos subidas',
prellamada_enviada: 'Pre-llamada enviada',
llamada_completada: 'Llamada completada',
render_generado: 'Render generado',
presupuesto_generado: 'Presupuesto generado',
whatsapp_entregado: 'Entregado por WhatsApp',
};
// Qué falta para que el lead avance. Permite analizar el siguiente paso.
export const PIPELINE_NEXT: Record<Stage, string> = {
form_completado: 'Esperando que suba fotos',
fotos_subidas: 'Lanzar pre-llamada (SMS + WhatsApp)',
prellamada_enviada: 'Llamada del agente IA',
llamada_completada: 'Generar render IA',
render_generado: 'Calcular presupuesto + PDF',
presupuesto_generado: 'Entregar al cliente por WhatsApp',
whatsapp_entregado: 'Lead listo: contactar al cliente',
};
export const TIPO_LABEL: Record<Tipo, string> = {
cocina: 'Cocina',
bano: 'Baño',
salon: 'Salón',
comedor: 'Comedor',
integral: 'Reforma integral',
otro: 'Otro',
};
export function formatEuros(cents: number | null): string {
if (cents == null) return '—';
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: 'EUR',
maximumFractionDigits: 0,
}).format(cents / 100);
}
export function formatFecha(date: Date): string {
return new Intl.DateTimeFormat('es-ES', {
day: '2-digit',
month: 'short',
hour: '2-digit',
minute: '2-digit',
}).format(date);
}