Etiqueta fotos por zona/momento y añade tabla lead_notas
Prepara el modelo de datos para la ingesta multicanal del perfil del lead: - lead_fotos: columnas momento (foto_momento antes/despues, default antes) y zona (tipo_reforma, nullable con fallback al tipoReforma del lead). - lead_notas: tabla append-only de datos de texto por zona (ej. "suelo premium"), con origen (ep|funnel|panel) para auditar quién los aportó. - Migración 0008 + regenerado db-schema/schema.sql. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
CREATE TYPE "public"."calidad" AS ENUM('basica', 'media', 'premium');
|
CREATE TYPE "public"."calidad" AS ENUM('basica', 'media', 'premium');
|
||||||
CREATE TYPE "public"."categoria_material" AS ENUM('suelo', 'pared', 'pintura', 'mobiliario');
|
CREATE TYPE "public"."categoria_material" AS ENUM('suelo', 'pared', 'pintura', 'mobiliario');
|
||||||
CREATE TYPE "public"."envio_presupuesto_mode" AS ENUM('automatico', 'revision');
|
CREATE TYPE "public"."envio_presupuesto_mode" AS ENUM('automatico', 'revision');
|
||||||
|
CREATE TYPE "public"."foto_momento" AS ENUM('antes', 'despues');
|
||||||
CREATE TYPE "public"."lead_estado" AS ENUM('nuevo', 'contactado', 'visita_agendada', 'presupuesto_enviado', 'ganado', 'perdido');
|
CREATE TYPE "public"."lead_estado" AS ENUM('nuevo', 'contactado', 'visita_agendada', 'presupuesto_enviado', 'ganado', 'perdido');
|
||||||
CREATE TYPE "public"."pipeline_stage" AS ENUM('form_completado', 'fotos_subidas', 'prellamada_enviada', 'llamada_completada', 'render_generado', 'presupuesto_generado', 'whatsapp_entregado');
|
CREATE TYPE "public"."pipeline_stage" AS ENUM('form_completado', 'fotos_subidas', 'prellamada_enviada', 'llamada_completada', 'render_generado', 'presupuesto_generado', 'whatsapp_entregado');
|
||||||
CREATE TYPE "public"."subscription_status" AS ENUM('trial', 'activo', 'cancelado', 'vencido');
|
CREATE TYPE "public"."subscription_status" AS ENUM('trial', 'activo', 'cancelado', 'vencido');
|
||||||
@@ -44,10 +45,21 @@ CREATE TABLE "lead_fotos" (
|
|||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
"lead_id" uuid NOT NULL,
|
"lead_id" uuid NOT NULL,
|
||||||
"url" text NOT NULL,
|
"url" text NOT NULL,
|
||||||
|
"momento" "foto_momento" DEFAULT 'antes' NOT NULL,
|
||||||
|
"zona" "tipo_reforma",
|
||||||
"orden" integer DEFAULT 0 NOT NULL,
|
"orden" integer DEFAULT 0 NOT NULL,
|
||||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "lead_notas" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"lead_id" uuid NOT NULL,
|
||||||
|
"zona" "tipo_reforma",
|
||||||
|
"texto" text NOT NULL,
|
||||||
|
"origen" text DEFAULT 'ep' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE "lead_pipeline_eventos" (
|
CREATE TABLE "lead_pipeline_eventos" (
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
"lead_id" uuid NOT NULL,
|
"lead_id" uuid NOT NULL,
|
||||||
@@ -195,6 +207,7 @@ ALTER TABLE "catalog_items" ADD CONSTRAINT "catalog_items_tenant_id_tenants_id_f
|
|||||||
ALTER TABLE "galeria_fotos" ADD CONSTRAINT "galeria_fotos_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;
|
ALTER TABLE "galeria_fotos" ADD CONSTRAINT "galeria_fotos_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
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;
|
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;
|
||||||
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;
|
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;
|
||||||
|
ALTER TABLE "lead_notas" ADD CONSTRAINT "lead_notas_lead_id_leads_id_fk" FOREIGN KEY ("lead_id") REFERENCES "public"."leads"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
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;
|
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;
|
||||||
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;
|
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;
|
||||||
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;
|
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;
|
||||||
|
|||||||
13
mvp/b2c/drizzle/0008_sharp_bloodaxe.sql
Normal file
13
mvp/b2c/drizzle/0008_sharp_bloodaxe.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TYPE "public"."foto_momento" AS ENUM('antes', 'despues');--> statement-breakpoint
|
||||||
|
CREATE TABLE "lead_notas" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"lead_id" uuid NOT NULL,
|
||||||
|
"zona" "tipo_reforma",
|
||||||
|
"texto" text NOT NULL,
|
||||||
|
"origen" text DEFAULT 'ep' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "lead_fotos" ADD COLUMN "momento" "foto_momento" DEFAULT 'antes' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "lead_fotos" ADD COLUMN "zona" "tipo_reforma";--> statement-breakpoint
|
||||||
|
ALTER TABLE "lead_notas" ADD CONSTRAINT "lead_notas_lead_id_leads_id_fk" FOREIGN KEY ("lead_id") REFERENCES "public"."leads"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
1673
mvp/b2c/drizzle/meta/0008_snapshot.json
Normal file
1673
mvp/b2c/drizzle/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,13 @@
|
|||||||
"when": 1780313493522,
|
"when": 1780313493522,
|
||||||
"tag": "0007_pale_chat",
|
"tag": "0007_pale_chat",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780505942614,
|
||||||
|
"tag": "0008_sharp_bloodaxe",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,9 @@ export const tipoReforma = pgEnum('tipo_reforma', [
|
|||||||
|
|
||||||
export const calidad = pgEnum('calidad', ['basica', 'media', 'premium']);
|
export const calidad = pgEnum('calidad', ['basica', 'media', 'premium']);
|
||||||
|
|
||||||
|
// Momento de una foto del lead: el estado antes de la reforma o el render del después.
|
||||||
|
export const fotoMomento = pgEnum('foto_momento', ['antes', 'despues']);
|
||||||
|
|
||||||
export const urgencia = pgEnum('urgencia', ['alta', 'media', 'baja']);
|
export const urgencia = pgEnum('urgencia', ['alta', 'media', 'baja']);
|
||||||
|
|
||||||
export const categoriaMaterial = pgEnum('categoria_material', [
|
export const categoriaMaterial = pgEnum('categoria_material', [
|
||||||
@@ -214,17 +217,34 @@ export const leads = pgTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fotos subidas por el cliente (paso 3, 2-4 fotos)
|
// Fotos del lead, etiquetadas por zona y momento. Las "antes" las sube el cliente (funnel o EP);
|
||||||
|
// las "despues" (renders) las devuelve el flujo de generación externo por el mismo EP de ingesta.
|
||||||
|
// zona es nullable por compatibilidad con filas antiguas (fallback al tipoReforma del lead).
|
||||||
export const leadFotos = pgTable('lead_fotos', {
|
export const leadFotos = pgTable('lead_fotos', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
leadId: uuid('lead_id')
|
leadId: uuid('lead_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => leads.id, { onDelete: 'cascade' }),
|
.references(() => leads.id, { onDelete: 'cascade' }),
|
||||||
url: text('url').notNull(),
|
url: text('url').notNull(),
|
||||||
|
momento: fotoMomento('momento').notNull().default('antes'),
|
||||||
|
zona: tipoReforma('zona'),
|
||||||
orden: integer('orden').notNull().default(0),
|
orden: integer('orden').notNull().default(0),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Datos de texto que enriquecen el perfil del lead por zona (ej. "Baño: suelo premium").
|
||||||
|
// Append-only: cada llamada al EP de ingesta puede añadir notas que el agente externo homologará.
|
||||||
|
export const leadNotas = pgTable('lead_notas', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
leadId: uuid('lead_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => leads.id, { onDelete: 'cascade' }),
|
||||||
|
zona: tipoReforma('zona'),
|
||||||
|
texto: text('texto').notNull(),
|
||||||
|
origen: text('origen').notNull().default('ep'), // ep | funnel | panel
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
// Opiniones del cliente final, recogidas en el funnel de review (/opinion/[id]).
|
// Opiniones del cliente final, recogidas en el funnel de review (/opinion/[id]).
|
||||||
// El reformista las solicita desde el panel y aprueba antes de que salgan en su landing.
|
// El reformista las solicita desde el panel y aprueba antes de que salgan en su landing.
|
||||||
export const testimonios = pgTable(
|
export const testimonios = pgTable(
|
||||||
@@ -350,6 +370,9 @@ export type Tenant = typeof tenants.$inferSelect;
|
|||||||
export type Lead = typeof leads.$inferSelect;
|
export type Lead = typeof leads.$inferSelect;
|
||||||
export type NewLead = typeof leads.$inferInsert;
|
export type NewLead = typeof leads.$inferInsert;
|
||||||
export type LeadFoto = typeof leadFotos.$inferSelect;
|
export type LeadFoto = typeof leadFotos.$inferSelect;
|
||||||
|
export type NewLeadFoto = typeof leadFotos.$inferInsert;
|
||||||
|
export type LeadNota = typeof leadNotas.$inferSelect;
|
||||||
|
export type NewLeadNota = typeof leadNotas.$inferInsert;
|
||||||
export type Testimonio = typeof testimonios.$inferSelect;
|
export type Testimonio = typeof testimonios.$inferSelect;
|
||||||
export type NewTestimonio = typeof testimonios.$inferInsert;
|
export type NewTestimonio = typeof testimonios.$inferInsert;
|
||||||
export type TestimonioFoto = typeof testimonioFotos.$inferSelect;
|
export type TestimonioFoto = typeof testimonioFotos.$inferSelect;
|
||||||
|
|||||||
Reference in New Issue
Block a user