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,77 @@
CREATE TYPE "public"."lead_estado" AS ENUM('nuevo', 'contactado', 'visita_agendada', 'presupuesto_enviado', 'ganado', 'perdido');--> statement-breakpoint
CREATE TYPE "public"."pipeline_stage" AS ENUM('form_completado', 'fotos_subidas', 'prellamada_enviada', 'llamada_completada', 'render_generado', 'presupuesto_generado', 'whatsapp_entregado');--> statement-breakpoint
CREATE TYPE "public"."tipo_reforma" AS ENUM('cocina', 'bano', 'salon', 'comedor', 'integral', 'otro');--> statement-breakpoint
CREATE TABLE "lead_estado_history" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"lead_id" uuid NOT NULL,
"estado" "lead_estado" NOT NULL,
"changed_at" timestamp with time zone DEFAULT now() NOT NULL,
"changed_by" text
);
--> statement-breakpoint
CREATE TABLE "lead_fotos" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"lead_id" uuid NOT NULL,
"url" text NOT NULL,
"orden" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "lead_pipeline_eventos" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"lead_id" uuid NOT NULL,
"stage" "pipeline_stage" NOT NULL,
"occurred_at" timestamp with time zone DEFAULT now() NOT NULL,
"metadata" jsonb
);
--> statement-breakpoint
CREATE TABLE "leads" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"nombre" text NOT NULL,
"telefono" text NOT NULL,
"email" text NOT NULL,
"provincia" text,
"tipo_reforma" "tipo_reforma",
"consent_privacidad" boolean DEFAULT false NOT NULL,
"consent_contratacion" boolean DEFAULT false NOT NULL,
"pipeline_stage" "pipeline_stage" DEFAULT 'form_completado' NOT NULL,
"estado" "lead_estado" DEFAULT 'nuevo' NOT NULL,
"presupuesto_estimado" integer,
"transcripcion" text,
"entidades" jsonb,
"render_url" text,
"pdf_url" text,
"audio_url" text,
"notas" text
);
--> statement-breakpoint
CREATE TABLE "precision_history" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"lead_id" uuid NOT NULL,
"estimated" integer NOT NULL,
"final" integer NOT NULL,
"delta_pct" numeric(6, 2) NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "tenants" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"slug" text NOT NULL,
"nombre_empresa" text NOT NULL,
"logo_url" text,
"provincia" text,
"whatsapp_business" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "tenants_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
ALTER TABLE "lead_estado_history" ADD CONSTRAINT "lead_estado_history_lead_id_leads_id_fk" FOREIGN KEY ("lead_id") REFERENCES "public"."leads"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "lead_fotos" ADD CONSTRAINT "lead_fotos_lead_id_leads_id_fk" FOREIGN KEY ("lead_id") REFERENCES "public"."leads"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "lead_pipeline_eventos" ADD CONSTRAINT "lead_pipeline_eventos_lead_id_leads_id_fk" FOREIGN KEY ("lead_id") REFERENCES "public"."leads"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "leads" ADD CONSTRAINT "leads_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "precision_history" ADD CONSTRAINT "precision_history_lead_id_leads_id_fk" FOREIGN KEY ("lead_id") REFERENCES "public"."leads"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "leads_tenant_created_idx" ON "leads" USING btree ("tenant_id","created_at");--> statement-breakpoint
CREATE INDEX "leads_estado_idx" ON "leads" USING btree ("estado");

View File

@@ -0,0 +1,561 @@
{
"id": "66acce06-f292-49db-adc1-fa9cfcc7d2a9",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.lead_estado_history": {
"name": "lead_estado_history",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"lead_id": {
"name": "lead_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"estado": {
"name": "estado",
"type": "lead_estado",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"changed_at": {
"name": "changed_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"changed_by": {
"name": "changed_by",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"lead_estado_history_lead_id_leads_id_fk": {
"name": "lead_estado_history_lead_id_leads_id_fk",
"tableFrom": "lead_estado_history",
"tableTo": "leads",
"columnsFrom": [
"lead_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.lead_fotos": {
"name": "lead_fotos",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"lead_id": {
"name": "lead_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"orden": {
"name": "orden",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"lead_fotos_lead_id_leads_id_fk": {
"name": "lead_fotos_lead_id_leads_id_fk",
"tableFrom": "lead_fotos",
"tableTo": "leads",
"columnsFrom": [
"lead_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.lead_pipeline_eventos": {
"name": "lead_pipeline_eventos",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"lead_id": {
"name": "lead_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"stage": {
"name": "stage",
"type": "pipeline_stage",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"occurred_at": {
"name": "occurred_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"lead_pipeline_eventos_lead_id_leads_id_fk": {
"name": "lead_pipeline_eventos_lead_id_leads_id_fk",
"tableFrom": "lead_pipeline_eventos",
"tableTo": "leads",
"columnsFrom": [
"lead_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.leads": {
"name": "leads",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"tenant_id": {
"name": "tenant_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"nombre": {
"name": "nombre",
"type": "text",
"primaryKey": false,
"notNull": true
},
"telefono": {
"name": "telefono",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provincia": {
"name": "provincia",
"type": "text",
"primaryKey": false,
"notNull": false
},
"tipo_reforma": {
"name": "tipo_reforma",
"type": "tipo_reforma",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"consent_privacidad": {
"name": "consent_privacidad",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"consent_contratacion": {
"name": "consent_contratacion",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"pipeline_stage": {
"name": "pipeline_stage",
"type": "pipeline_stage",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'form_completado'"
},
"estado": {
"name": "estado",
"type": "lead_estado",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'nuevo'"
},
"presupuesto_estimado": {
"name": "presupuesto_estimado",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"transcripcion": {
"name": "transcripcion",
"type": "text",
"primaryKey": false,
"notNull": false
},
"entidades": {
"name": "entidades",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"render_url": {
"name": "render_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"pdf_url": {
"name": "pdf_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"audio_url": {
"name": "audio_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"notas": {
"name": "notas",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"leads_tenant_created_idx": {
"name": "leads_tenant_created_idx",
"columns": [
{
"expression": "tenant_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"leads_estado_idx": {
"name": "leads_estado_idx",
"columns": [
{
"expression": "estado",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"leads_tenant_id_tenants_id_fk": {
"name": "leads_tenant_id_tenants_id_fk",
"tableFrom": "leads",
"tableTo": "tenants",
"columnsFrom": [
"tenant_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.precision_history": {
"name": "precision_history",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"lead_id": {
"name": "lead_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"estimated": {
"name": "estimated",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"final": {
"name": "final",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"delta_pct": {
"name": "delta_pct",
"type": "numeric(6, 2)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"precision_history_lead_id_leads_id_fk": {
"name": "precision_history_lead_id_leads_id_fk",
"tableFrom": "precision_history",
"tableTo": "leads",
"columnsFrom": [
"lead_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.tenants": {
"name": "tenants",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"nombre_empresa": {
"name": "nombre_empresa",
"type": "text",
"primaryKey": false,
"notNull": true
},
"logo_url": {
"name": "logo_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"provincia": {
"name": "provincia",
"type": "text",
"primaryKey": false,
"notNull": false
},
"whatsapp_business": {
"name": "whatsapp_business",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"tenants_slug_unique": {
"name": "tenants_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.lead_estado": {
"name": "lead_estado",
"schema": "public",
"values": [
"nuevo",
"contactado",
"visita_agendada",
"presupuesto_enviado",
"ganado",
"perdido"
]
},
"public.pipeline_stage": {
"name": "pipeline_stage",
"schema": "public",
"values": [
"form_completado",
"fotos_subidas",
"prellamada_enviada",
"llamada_completada",
"render_generado",
"presupuesto_generado",
"whatsapp_entregado"
]
},
"public.tipo_reforma": {
"name": "tipo_reforma",
"schema": "public",
"values": [
"cocina",
"bano",
"salon",
"comedor",
"integral",
"otro"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1780056789929,
"tag": "0000_motionless_jackpot",
"breakpoints": true
}
]
}