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:
93
mvp/b2c/src/lib/funnel.ts
Normal file
93
mvp/b2c/src/lib/funnel.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user