From b815b0532bf7ea2922340a8ea4bcca43ec061638 Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Thu, 11 Jun 2026 16:58:55 +0200 Subject: [PATCH 1/6] =?UTF-8?q?A=C3=B1ade=20baremo=20de=20rentabilidad=20(?= =?UTF-8?q?valor=20de=20panel)=20e=20indicador=20en=20la=20ficha=20del=20l?= =?UTF-8?q?ead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parte C del plan: el baremo mínimo de rentabilidad es ahora un valor configurable del reformista, solo informativo. Los agentes NO lo usan para decidir nada. - schema: pricing_config.baremo_minimo (céntimos, nullable) + migración 0012. - pricing-queries / budget types: exponen baremoMinimo. - panel/precios: sección "Baremo de rentabilidad" + action actualizarBaremo (vacío = sin baremo). - panel/[id]: el presupuesto estimado se muestra en rojo con aviso "Por debajo de tu baremo (X €)" cuando no alcanza el baremo del tenant. Co-Authored-By: Claude Opus 4.8 --- mvp/b2c/db-schema/schema.sql | 1 + mvp/b2c/drizzle/0012_lame_sentinel.sql | 1 + mvp/b2c/drizzle/meta/0012_snapshot.json | 2458 ++++++++++++++++++++++ mvp/b2c/drizzle/meta/_journal.json | 7 + mvp/b2c/src/app/panel/[id]/page.tsx | 18 +- mvp/b2c/src/app/panel/precios/actions.ts | 13 + mvp/b2c/src/app/panel/precios/page.tsx | 29 + mvp/b2c/src/budget/types.ts | 1 + mvp/b2c/src/db/pricing-queries.ts | 2 + mvp/b2c/src/db/schema.ts | 4 + 10 files changed, 2533 insertions(+), 1 deletion(-) create mode 100644 mvp/b2c/drizzle/0012_lame_sentinel.sql create mode 100644 mvp/b2c/drizzle/meta/0012_snapshot.json diff --git a/mvp/b2c/db-schema/schema.sql b/mvp/b2c/db-schema/schema.sql index 7ff6b82..3712f9a 100644 --- a/mvp/b2c/db-schema/schema.sql +++ b/mvp/b2c/db-schema/schema.sql @@ -186,6 +186,7 @@ CREATE TABLE "pricing_config" ( "factor_zona" jsonb DEFAULT '{}'::jsonb NOT NULL, "mano_obra" jsonb DEFAULT '{}'::jsonb NOT NULL, "extras" jsonb DEFAULT '{"tuberias":0,"boletin":0,"distribucion":0}'::jsonb NOT NULL, + "baremo_minimo" integer, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "pricing_config_tenant_id_unique" UNIQUE("tenant_id") ); diff --git a/mvp/b2c/drizzle/0012_lame_sentinel.sql b/mvp/b2c/drizzle/0012_lame_sentinel.sql new file mode 100644 index 0000000..9fbe195 --- /dev/null +++ b/mvp/b2c/drizzle/0012_lame_sentinel.sql @@ -0,0 +1 @@ +ALTER TABLE "pricing_config" ADD COLUMN "baremo_minimo" integer; \ No newline at end of file diff --git a/mvp/b2c/drizzle/meta/0012_snapshot.json b/mvp/b2c/drizzle/meta/0012_snapshot.json new file mode 100644 index 0000000..d405c52 --- /dev/null +++ b/mvp/b2c/drizzle/meta/0012_snapshot.json @@ -0,0 +1,2458 @@ +{ + "id": "46d15362-42e0-4ec1-834d-d28813795bc9", + "prevId": "1f180cf2-9424-4d35-a856-6b459ab3a67b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.catalog_items": { + "name": "catalog_items", + "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 + }, + "categoria": { + "name": "categoria", + "type": "categoria_material", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "nombre": { + "name": "nombre", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "calidad": { + "name": "calidad", + "type": "calidad", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "precio_unit": { + "name": "precio_unit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "unidad": { + "name": "unidad", + "type": "unidad_medida", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "descriptor_render": { + "name": "descriptor_render", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "es_default": { + "name": "es_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sku": { + "name": "sku", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "catalog_tenant_idx": { + "name": "catalog_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "catalog_tenant_sku_idx": { + "name": "catalog_tenant_sku_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sku", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "catalog_items_tenant_id_tenants_id_fk": { + "name": "catalog_items_tenant_id_tenants_id_fk", + "tableFrom": "catalog_items", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversacion_whatsapp": { + "name": "conversacion_whatsapp", + "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 + }, + "rol": { + "name": "rol", + "type": "rol_mensaje", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "mensaje": { + "name": "mensaje", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "media_url": { + "name": "media_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transcripcion_audio": { + "name": "transcripcion_audio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_conversacion_whatsapp_lead_id": { + "name": "idx_conversacion_whatsapp_lead_id", + "columns": [ + { + "expression": "lead_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conversacion_whatsapp_created_at": { + "name": "idx_conversacion_whatsapp_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversacion_whatsapp_lead_id_leads_id_fk": { + "name": "conversacion_whatsapp_lead_id_leads_id_fk", + "tableFrom": "conversacion_whatsapp", + "tableTo": "leads", + "columnsFrom": [ + "lead_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.galeria_fotos": { + "name": "galeria_fotos", + "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 + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "titulo": { + "name": "titulo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "galeria_tenant_idx": { + "name": "galeria_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "galeria_fotos_tenant_id_tenants_id_fk": { + "name": "galeria_fotos_tenant_id_tenants_id_fk", + "tableFrom": "galeria_fotos", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.intentos_contacto": { + "name": "intentos_contacto", + "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 + }, + "canal": { + "name": "canal", + "type": "canal_contacto", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resultado": { + "name": "resultado", + "type": "resultado_contacto", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "completado": { + "name": "completado", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "numero_intento": { + "name": "numero_intento", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duracion_seg": { + "name": "duracion_seg", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "intentado_at": { + "name": "intentado_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "notas": { + "name": "notas", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_intentos_contacto_lead_id": { + "name": "idx_intentos_contacto_lead_id", + "columns": [ + { + "expression": "lead_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "intentos_contacto_lead_id_leads_id_fk": { + "name": "intentos_contacto_lead_id_leads_id_fk", + "tableFrom": "intentos_contacto", + "tableTo": "leads", + "columnsFrom": [ + "lead_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lead_calificacion": { + "name": "lead_calificacion", + "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 + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "nivel": { + "name": "nivel", + "type": "nivel_calificacion", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "criterios": { + "name": "criterios", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notas_agente": { + "name": "notas_agente", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "calificado_por": { + "name": "calificado_por", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "calificado_at": { + "name": "calificado_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_lead_calificacion_lead_id": { + "name": "idx_lead_calificacion_lead_id", + "columns": [ + { + "expression": "lead_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "lead_calificacion_lead_id_leads_id_fk": { + "name": "lead_calificacion_lead_id_leads_id_fk", + "tableFrom": "lead_calificacion", + "tableTo": "leads", + "columnsFrom": [ + "lead_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lead_calificacion_calificado_por_users_id_fk": { + "name": "lead_calificacion_calificado_por_users_id_fk", + "tableFrom": "lead_calificacion", + "tableTo": "users", + "columnsFrom": [ + "calificado_por" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "lead_calificacion_lead_id_unique": { + "name": "lead_calificacion_lead_id_unique", + "nullsNotDistinct": false, + "columns": [ + "lead_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "lead_calificacion_score_check": { + "name": "lead_calificacion_score_check", + "value": "\"lead_calificacion\".\"score\" >= 0 AND \"lead_calificacion\".\"score\" <= 100" + } + }, + "isRLSEnabled": false + }, + "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 + }, + "momento": { + "name": "momento", + "type": "foto_momento", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'antes'" + }, + "zona": { + "name": "zona", + "type": "tipo_reforma", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "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_notas": { + "name": "lead_notas", + "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 + }, + "zona": { + "name": "zona", + "type": "tipo_reforma", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "texto": { + "name": "texto", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origen": { + "name": "origen", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ep'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "lead_notas_lead_id_leads_id_fk": { + "name": "lead_notas_lead_id_leads_id_fk", + "tableFrom": "lead_notas", + "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 + }, + "testimonio_solicitado_at": { + "name": "testimonio_solicitado_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "m2_suelo": { + "name": "m2_suelo", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "altura_techo": { + "name": "altura_techo", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "calidad_global": { + "name": "calidad_global", + "type": "calidad", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "estructural": { + "name": "estructural", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "anterior_a_2000": { + "name": "anterior_a_2000", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cambio_distribucion": { + "name": "cambio_distribucion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "material_selections": { + "name": "material_selections", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "desglose_snapshot": { + "name": "desglose_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "urgencia": { + "name": "urgencia", + "type": "urgencia", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "presupuesto_target": { + "name": "presupuesto_target", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "taste_text": { + "name": "taste_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferences_snapshot": { + "name": "preferences_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "estado_wa": { + "name": "estado_wa", + "type": "estado_wa", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "bot_step": { + "name": "bot_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canal_origen": { + "name": "canal_origen", + "type": "canal_origen", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "espacio": { + "name": "espacio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rango_m2": { + "name": "rango_m2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "estilo": { + "name": "estilo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "presupuesto_declarado": { + "name": "presupuesto_declarado", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "viable": { + "name": "viable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "fotos_solicitadas_at": { + "name": "fotos_solicitadas_at", + "type": "timestamp with time zone", + "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.plans": { + "name": "plans", + "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": { + "name": "nombre", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "precio_mensual": { + "name": "precio_mensual", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "leads_incluidos": { + "name": "leads_incluidos", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "features": { + "name": "features", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "activo": { + "name": "activo", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plans_slug_unique": { + "name": "plans_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "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.pricing_config": { + "name": "pricing_config", + "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 + }, + "altura_techo_default": { + "name": "altura_techo_default", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 2.5 + }, + "factor_zona": { + "name": "factor_zona", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "mano_obra": { + "name": "mano_obra", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "extras": { + "name": "extras", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"tuberias\":0,\"boletin\":0,\"distribucion\":0}'::jsonb" + }, + "baremo_minimo": { + "name": "baremo_minimo", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pricing_config_tenant_id_tenants_id_fk": { + "name": "pricing_config_tenant_id_tenants_id_fk", + "tableFrom": "pricing_config", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pricing_config_tenant_id_unique": { + "name": "pricing_config_tenant_id_unique", + "nullsNotDistinct": false, + "columns": [ + "tenant_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_hash_unique": { + "name": "sessions_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "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 + }, + "seo_title": { + "name": "seo_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "seo_description": { + "name": "seo_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "about_enabled": { + "name": "about_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "about_foto_url": { + "name": "about_foto_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "about_texto": { + "name": "about_texto", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "anios_experiencia": { + "name": "anios_experiencia", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "theme_preset": { + "name": "theme_preset", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pizarra'" + }, + "theme_color": { + "name": "theme_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cif": { + "name": "cif", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "direccion": { + "name": "direccion", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telefono": { + "name": "telefono", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web": { + "name": "web", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_id": { + "name": "plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subscription_status": { + "name": "subscription_status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'trial'" + }, + "envio_presupuesto": { + "name": "envio_presupuesto", + "type": "envio_presupuesto_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'automatico'" + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tenants_plan_id_plans_id_fk": { + "name": "tenants_plan_id_plans_id_fk", + "tableFrom": "tenants", + "tableTo": "plans", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tenants_slug_unique": { + "name": "tenants_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.testimonio_fotos": { + "name": "testimonio_fotos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "testimonio_id": { + "name": "testimonio_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": { + "testimonio_fotos_testimonio_id_testimonios_id_fk": { + "name": "testimonio_fotos_testimonio_id_testimonios_id_fk", + "tableFrom": "testimonio_fotos", + "tableTo": "testimonios", + "columnsFrom": [ + "testimonio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.testimonios": { + "name": "testimonios", + "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 + }, + "lead_id": { + "name": "lead_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "nombre": { + "name": "nombre", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contexto": { + "name": "contexto", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rating": { + "name": "rating", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "texto": { + "name": "texto", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estado": { + "name": "estado", + "type": "testimonio_estado", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pendiente'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "testimonios_tenant_estado_idx": { + "name": "testimonios_tenant_estado_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "estado", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "testimonios_lead_idx": { + "name": "testimonios_lead_idx", + "columns": [ + { + "expression": "lead_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "testimonios_tenant_id_tenants_id_fk": { + "name": "testimonios_tenant_id_tenants_id_fk", + "tableFrom": "testimonios", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "testimonios_lead_id_leads_id_fk": { + "name": "testimonios_lead_id_leads_id_fk", + "tableFrom": "testimonios", + "tableTo": "leads", + "columnsFrom": [ + "lead_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nombre": { + "name": "nombre", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'reformista'" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "user_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'activo'" + }, + "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()" + } + }, + "indexes": { + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.visitas": { + "name": "visitas", + "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 + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "fecha_propuesta": { + "name": "fecha_propuesta", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fecha_confirmada": { + "name": "fecha_confirmada", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "estado": { + "name": "estado", + "type": "visita_estado", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'propuesta'" + }, + "direccion": { + "name": "direccion", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notas": { + "name": "notas", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_visitas_lead_id": { + "name": "idx_visitas_lead_id", + "columns": [ + { + "expression": "lead_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_visitas_tenant_id": { + "name": "idx_visitas_tenant_id", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "visitas_lead_id_leads_id_fk": { + "name": "visitas_lead_id_leads_id_fk", + "tableFrom": "visitas", + "tableTo": "leads", + "columnsFrom": [ + "lead_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "visitas_tenant_id_tenants_id_fk": { + "name": "visitas_tenant_id_tenants_id_fk", + "tableFrom": "visitas", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.worker_jobs": { + "name": "worker_jobs", + "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 + }, + "tipo": { + "name": "tipo", + "type": "job_tipo", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "estado_job": { + "name": "estado_job", + "type": "job_estado", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pendiente'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "webhook_url": { + "name": "webhook_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resultado_url": { + "name": "resultado_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intentos": { + "name": "intentos", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_msg": { + "name": "error_msg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_worker_jobs_lead_id": { + "name": "idx_worker_jobs_lead_id", + "columns": [ + { + "expression": "lead_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_worker_jobs_estado": { + "name": "idx_worker_jobs_estado", + "columns": [ + { + "expression": "estado_job", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_worker_jobs_tipo": { + "name": "idx_worker_jobs_tipo", + "columns": [ + { + "expression": "tipo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "worker_jobs_lead_id_leads_id_fk": { + "name": "worker_jobs_lead_id_leads_id_fk", + "tableFrom": "worker_jobs", + "tableTo": "leads", + "columnsFrom": [ + "lead_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.calidad": { + "name": "calidad", + "schema": "public", + "values": [ + "basica", + "media", + "premium" + ] + }, + "public.canal_contacto": { + "name": "canal_contacto", + "schema": "public", + "values": [ + "formulario", + "whatsapp", + "llamada" + ] + }, + "public.canal_origen": { + "name": "canal_origen", + "schema": "public", + "values": [ + "formulario_web", + "whatsapp", + "llamada", + "referido", + "anuncio" + ] + }, + "public.categoria_material": { + "name": "categoria_material", + "schema": "public", + "values": [ + "suelo", + "pared", + "pintura", + "mobiliario" + ] + }, + "public.envio_presupuesto_mode": { + "name": "envio_presupuesto_mode", + "schema": "public", + "values": [ + "automatico", + "revision" + ] + }, + "public.estado_wa": { + "name": "estado_wa", + "schema": "public", + "values": [ + "sin_enviar", + "enviado", + "entregado", + "leido", + "fallido" + ] + }, + "public.foto_momento": { + "name": "foto_momento", + "schema": "public", + "values": [ + "antes", + "despues" + ] + }, + "public.job_estado": { + "name": "job_estado", + "schema": "public", + "values": [ + "pendiente", + "procesando", + "completado", + "error" + ] + }, + "public.job_tipo": { + "name": "job_tipo", + "schema": "public", + "values": [ + "analisis_fotos", + "render", + "presupuesto_ia" + ] + }, + "public.lead_estado": { + "name": "lead_estado", + "schema": "public", + "values": [ + "nuevo", + "contactado", + "visita_agendada", + "presupuesto_enviado", + "ganado", + "perdido" + ] + }, + "public.nivel_calificacion": { + "name": "nivel_calificacion", + "schema": "public", + "values": [ + "A", + "B", + "C", + "D" + ] + }, + "public.pipeline_stage": { + "name": "pipeline_stage", + "schema": "public", + "values": [ + "form_completado", + "fotos_subidas", + "prellamada_enviada", + "llamada_completada", + "render_generado", + "presupuesto_generado", + "whatsapp_entregado" + ] + }, + "public.resultado_contacto": { + "name": "resultado_contacto", + "schema": "public", + "values": [ + "exitoso", + "no_contesta", + "ocupado", + "rechaza", + "error_tecnico" + ] + }, + "public.rol_mensaje": { + "name": "rol_mensaje", + "schema": "public", + "values": [ + "user", + "assistant", + "system" + ] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "trial", + "activo", + "cancelado", + "vencido" + ] + }, + "public.testimonio_estado": { + "name": "testimonio_estado", + "schema": "public", + "values": [ + "pendiente", + "publicado", + "oculto" + ] + }, + "public.tipo_reforma": { + "name": "tipo_reforma", + "schema": "public", + "values": [ + "cocina", + "bano", + "salon", + "comedor", + "integral", + "otro" + ] + }, + "public.unidad_medida": { + "name": "unidad_medida", + "schema": "public", + "values": [ + "m2", + "ml", + "ud" + ] + }, + "public.urgencia": { + "name": "urgencia", + "schema": "public", + "values": [ + "alta", + "media", + "baja" + ] + }, + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": [ + "reformista", + "admin" + ] + }, + "public.user_status": { + "name": "user_status", + "schema": "public", + "values": [ + "activo", + "deshabilitado" + ] + }, + "public.visita_estado": { + "name": "visita_estado", + "schema": "public", + "values": [ + "propuesta", + "confirmada", + "realizada", + "cancelada", + "reprogramada" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/mvp/b2c/drizzle/meta/_journal.json b/mvp/b2c/drizzle/meta/_journal.json index 8a08d08..dc110a9 100644 --- a/mvp/b2c/drizzle/meta/_journal.json +++ b/mvp/b2c/drizzle/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1780593183911, "tag": "0011_warm_post", "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1781189893331, + "tag": "0012_lame_sentinel", + "breakpoints": true } ] } \ No newline at end of file diff --git a/mvp/b2c/src/app/panel/[id]/page.tsx b/mvp/b2c/src/app/panel/[id]/page.tsx index 16742c3..ada2a1c 100644 --- a/mvp/b2c/src/app/panel/[id]/page.tsx +++ b/mvp/b2c/src/app/panel/[id]/page.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; import { headers } from 'next/headers'; import { notFound } from 'next/navigation'; import { getLead } from '@/db/queries'; +import { getPricingConfigFor } from '@/db/pricing-queries'; import EstadoControl from '@/components/panel/EstadoControl'; import ConceptosEditor from '@/components/panel/ConceptosEditor'; import OpinionLinkBox from '@/components/panel/OpinionLinkBox'; @@ -41,6 +42,14 @@ export default async function LeadDetailPage({ params }: { params: Promise<{ id: const prefs = lead.preferencesSnapshot as import('@/lib/voice/preferences').AbstractedPreferences | null; const yaEnviado = lead.pipelineStage === 'whatsapp_entregado'; + // Baremo de rentabilidad del reformista (informativo): si el presupuesto estimado no lo alcanza, + // se marca en rojo. null = sin baremo o sin presupuesto aún (no se marca nada). + const baremoMinimo = (await getPricingConfigFor(lead.tenantId)).baremoMinimo ?? null; + const pasaBaremo = + baremoMinimo != null && lead.presupuestoEstimado != null + ? lead.presupuestoEstimado >= baremoMinimo + : null; + const h = await headers(); const host = h.get('x-forwarded-host') ?? h.get('host') ?? 'reformix.dv3.com.es'; const proto = h.get('x-forwarded-proto') ?? 'https'; @@ -64,7 +73,14 @@ export default async function LeadDetailPage({ params }: { params: Promise<{ id:
Presupuesto estimado
-
{formatEuros(lead.presupuestoEstimado)}
+
+ {formatEuros(lead.presupuestoEstimado)} +
+ {pasaBaremo === false && baremoMinimo != null && ( +
+ Por debajo de tu baremo ({formatEuros(baremoMinimo)}) +
+ )}
+ {/* Baremo de rentabilidad */} +
+

Baremo de rentabilidad

+

+ Importe mínimo de un trabajo para que te resulte rentable. Es solo orientativo para ti: en la + ficha de cada lead verás marcados en otro color los presupuestos que no lleguen a este valor. + No afecta a lo que ve el cliente ni a la conversación de los agentes. Déjalo vacío para no usar + baremo. +

+
+ + +
+
+ {/* Extras fijos */}

Extras fijos

diff --git a/mvp/b2c/src/budget/types.ts b/mvp/b2c/src/budget/types.ts index 0ddd0ae..8e45856 100644 --- a/mvp/b2c/src/budget/types.ts +++ b/mvp/b2c/src/budget/types.ts @@ -46,6 +46,7 @@ export interface PricingConfig { factorZona: Record; // provincia -> multiplicador manoObra: Record; // céntimos por m² de suelo extras?: ExtrasFijos; // importes fijos en céntimos + baremoMinimo?: number | null; // céntimos; trabajo mínimo rentable (informativo, no lo usan los agentes) } export interface BudgetInputs { diff --git a/mvp/b2c/src/db/pricing-queries.ts b/mvp/b2c/src/db/pricing-queries.ts index cbcc46a..1e5810a 100644 --- a/mvp/b2c/src/db/pricing-queries.ts +++ b/mvp/b2c/src/db/pricing-queries.ts @@ -42,6 +42,7 @@ export async function getPricingConfigFor(tenantId: string): Promise) }, extras: { ...EXTRAS_DEFAULT, ...(row.extras ?? {}) }, + baremoMinimo: row.baremoMinimo ?? null, }; } diff --git a/mvp/b2c/src/db/schema.ts b/mvp/b2c/src/db/schema.ts index 3810d73..f1ec578 100644 --- a/mvp/b2c/src/db/schema.ts +++ b/mvp/b2c/src/db/schema.ts @@ -396,6 +396,10 @@ export const pricingConfig = pgTable('pricing_config', { .$type<{ tuberias: number; boletin: number; distribucion: number }>() .notNull() .default({ tuberias: 0, boletin: 0, distribucion: 0 }), + // Baremo de rentabilidad (céntimos): importe mínimo que el reformista considera rentable. Solo + // informativo en el panel (marca en otro color los leads por debajo); los agentes NO lo usan para + // decidir nada. Null = sin baremo configurado. + baremoMinimo: integer('baremo_minimo'), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }); From d92d5e2f1250bb8b01bcb04e81bce5cb109c5c1d Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Thu, 11 Jun 2026 17:19:01 +0200 Subject: [PATCH 2/6] =?UTF-8?q?A=C3=B1ade=20onboarding=20guiado=20del=20pa?= =?UTF-8?q?nel=20(tour=20con=20driver.js)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tour por pestañas que explica los puntos clave: en Leads recorre la navegación (las pestañas secundarias de pasada) + filtros y tabla; en la ficha del lead el presupuesto/baremo, estado, render y desglose; en Precios el baremo, la mano de obra y el catálogo. Auto-arranca la primera vez por pestaña (flag en localStorage) y deja un botón flotante "❓ Tour" para repetir. Pasos sin elemento visible se descartan (degrada en móvil). - Dependencia: driver.js (librería estándar de tours, ~5kb, sin más deps; evita reinventar overlay/posicionamiento/foco/accesibilidad). - src/lib/onboarding/panel-tour.ts: pasos por ruta. PanelTour.tsx: cliente que lanza driver.js. data-tour en nav, leads, ficha y precios. - Copy en COPY-GUIDE.md (sección "Onboarding del panel"). Co-Authored-By: Claude Opus 4.8 --- copy/COPY-GUIDE.md | 34 +++++ mvp/b2c/package-lock.json | 7 + mvp/b2c/package.json | 1 + mvp/b2c/src/app/panel/[id]/page.tsx | 33 +++-- mvp/b2c/src/app/panel/layout.tsx | 2 + mvp/b2c/src/app/panel/page.tsx | 6 +- mvp/b2c/src/app/panel/precios/page.tsx | 6 +- mvp/b2c/src/components/AppNav.tsx | 1 + mvp/b2c/src/components/panel/PanelTour.tsx | 71 ++++++++++ mvp/b2c/src/lib/onboarding/panel-tour.ts | 147 +++++++++++++++++++++ 10 files changed, 293 insertions(+), 15 deletions(-) create mode 100644 mvp/b2c/src/components/panel/PanelTour.tsx create mode 100644 mvp/b2c/src/lib/onboarding/panel-tour.ts diff --git a/copy/COPY-GUIDE.md b/copy/COPY-GUIDE.md index 8463403..91d9769 100644 --- a/copy/COPY-GUIDE.md +++ b/copy/COPY-GUIDE.md @@ -714,6 +714,40 @@ del espacio. `[url]` apunta a su formulario personal del funnel. Tono: una sola --- +## Onboarding del panel (tour guiado) + +> Tooltips del tour del panel (driver.js). Tono cercano y útil, una idea por paso, frases cortas. Las pestañas secundarias se explican "de pasada" (una línea). Copy usado en `src/lib/onboarding/panel-tour.ts`. + +### Pestaña Leads (`/panel`) + +- **Intro** — *Tu panel de Reformix* · "Te enseño en 30 segundos qué hay en cada sitio. Puedes saltártelo cuando quieras con la X." +- **Leads** — "Aquí caen tus clientes con su presupuesto y su render ya preparados. Es tu día a día." +- **Precios y baremo** — "Tu tabla de precios, la mano de obra y el baremo de rentabilidad. De aquí salen los presupuestos." +- **Galería** — "Tus fotos de trabajos para enseñar en la web." +- **Opiniones** — "Reseñas de tus clientes; las apruebas tú antes de publicarlas." +- **Empresa** — "Tu marca, logo y datos de contacto." +- **Filtra por estado** — "Nuevos, contactados, presupuestados… para centrarte en lo que toca ahora." +- **Tus leads** — "Cada cliente con su estado y su presupuesto. Ábrelo para ver el detalle completo." + +### Ficha del lead (`/panel/{id}`) + +- **Presupuesto estimado** — "Lo que costaría la reforma según tu catálogo. Si no llega a tu baremo, lo verás en rojo." +- **Estado del lead** — "Avanza el lead por el funnel: contactado, presupuestado, ganado…" +- **Render de la reforma** — "La imagen del «después» que ve tu cliente, generada a partir de su foto y sus gustos." +- **Presupuesto desglosado** — "Edita las partidas, recalcula desde el catálogo y envíaselo al cliente por WhatsApp." + +### Precios y baremo (`/panel/precios`) + +- **Baremo de rentabilidad** — "El trabajo mínimo que te compensa. Solo te avisa a ti: marca en rojo los leads por debajo." +- **Mano de obra** — "Tus precios de mano de obra por m². El motor los usa para calcular cada presupuesto." +- **Tu catálogo** — "Materiales y precios por calidad. Puedes importarlos en bloque por CSV." + +### Botón para repetir + +- **Botón flotante** — "❓ Tour" (relanza el tour de la pestaña actual). + +--- + ## Principios aplicados en todo el documento 1. **Beneficios > Features** — "Tus clientes verán su reforma" > "Render IA con SDXL" diff --git a/mvp/b2c/package-lock.json b/mvp/b2c/package-lock.json index 4ca9c0f..6c0a51a 100644 --- a/mvp/b2c/package-lock.json +++ b/mvp/b2c/package-lock.json @@ -11,6 +11,7 @@ "@react-pdf/renderer": "^4.5.1", "@tailwindcss/postcss": "^4.3.0", "bcryptjs": "^3.0.3", + "driver.js": "^1.4.0", "drizzle-orm": "^0.45.2", "next": "16.2.6", "nodemailer": "^8.0.10", @@ -4546,6 +4547,12 @@ "url": "https://dotenvx.com" } }, + "node_modules/driver.js": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz", + "integrity": "sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==", + "license": "MIT" + }, "node_modules/drizzle-kit": { "version": "0.31.10", "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", diff --git a/mvp/b2c/package.json b/mvp/b2c/package.json index 8199518..962d3cb 100644 --- a/mvp/b2c/package.json +++ b/mvp/b2c/package.json @@ -21,6 +21,7 @@ "@react-pdf/renderer": "^4.5.1", "@tailwindcss/postcss": "^4.3.0", "bcryptjs": "^3.0.3", + "driver.js": "^1.4.0", "drizzle-orm": "^0.45.2", "next": "16.2.6", "nodemailer": "^8.0.10", diff --git a/mvp/b2c/src/app/panel/[id]/page.tsx b/mvp/b2c/src/app/panel/[id]/page.tsx index ada2a1c..d4e30ba 100644 --- a/mvp/b2c/src/app/panel/[id]/page.tsx +++ b/mvp/b2c/src/app/panel/[id]/page.tsx @@ -20,9 +20,20 @@ import type { BudgetResult } from '@/budget/types'; export const dynamic = 'force-dynamic'; -function Section({ title, children }: { title: string; children: React.ReactNode }) { +function Section({ + title, + children, + tour, +}: { + title: string; + children: React.ReactNode; + tour?: string; +}) { return ( -
+

{title}

{children}
@@ -71,7 +82,7 @@ export default async function LeadDetailPage({ params }: { params: Promise<{ id: {lead.provincia ?? '—'} · entró {formatFecha(lead.createdAt)}

-
+
Presupuesto estimado
{formatEuros(lead.presupuestoEstimado)} @@ -83,11 +94,13 @@ export default async function LeadDetailPage({ params }: { params: Promise<{ id: )}
- +
+ +
{/* Solicitar opinión al cliente */} @@ -180,7 +193,7 @@ export default async function LeadDetailPage({ params }: { params: Promise<{ id:
{/* 4. Render */} -
+
{lead.renderUrl ? ( // eslint-disable-next-line @next/next/no-img-element Render de la reforma @@ -327,7 +340,7 @@ export default async function LeadDetailPage({ params }: { params: Promise<{ id: )} {/* Presupuesto desglosado */} -
+
); } diff --git a/mvp/b2c/src/app/panel/page.tsx b/mvp/b2c/src/app/panel/page.tsx index 5147ad2..0f4deca 100644 --- a/mvp/b2c/src/app/panel/page.tsx +++ b/mvp/b2c/src/app/panel/page.tsx @@ -69,7 +69,7 @@ export default async function PanelPage({ {/* Filtros por estado */} -
+
{FILTROS.map((f) => { const active = f.value === filtro; const count = f.value === 'todos' ? resumen.total : resumen.porEstado[f.value] ?? 0; @@ -89,7 +89,9 @@ export default async function PanelPage({ })}
- +
+ +
); } diff --git a/mvp/b2c/src/app/panel/precios/page.tsx b/mvp/b2c/src/app/panel/precios/page.tsx index c45415b..d8bee7c 100644 --- a/mvp/b2c/src/app/panel/precios/page.tsx +++ b/mvp/b2c/src/app/panel/precios/page.tsx @@ -82,7 +82,7 @@ export default async function PreciosPage() {
{/* Config general */} -
+

Configuración general

{/* Baremo de rentabilidad */} -
+

Baremo de rentabilidad

Importe mínimo de un trabajo para que te resulte rentable. Es solo orientativo para ti: en la @@ -285,7 +285,7 @@ export default async function PreciosPage() { })} {/* Import CSV */} -

+

Importar catálogo (CSV)

Cabecera: categoria,nombre,calidad,precio,unidad,descriptor_render,sku. El diff --git a/mvp/b2c/src/components/AppNav.tsx b/mvp/b2c/src/components/AppNav.tsx index 2d47f81..953fbd8 100644 --- a/mvp/b2c/src/components/AppNav.tsx +++ b/mvp/b2c/src/components/AppNav.tsx @@ -116,6 +116,7 @@ export default function AppNav({ links }: { links: readonly AppNavLink[] }) { {l.label} diff --git a/mvp/b2c/src/components/panel/PanelTour.tsx b/mvp/b2c/src/components/panel/PanelTour.tsx new file mode 100644 index 0000000..9e62b38 --- /dev/null +++ b/mvp/b2c/src/components/panel/PanelTour.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { usePathname } from 'next/navigation'; +import { driver, type DriveStep } from 'driver.js'; +import 'driver.js/dist/driver.css'; +import { tourForPath } from '@/lib/onboarding/panel-tour'; + +const SEEN_PREFIX = 'reformix_tour_v1_'; + +// Onboarding del panel con driver.js. Lanza el tour de la pestaña actual la primera vez que se +// visita (flag por pestaña en localStorage) y deja un botón flotante para repetirlo. Los pasos +// cuyo elemento no exista o esté oculto (p. ej. la nav de escritorio en móvil) se descartan. +export default function PanelTour() { + const pathname = usePathname(); + const [hayTour, setHayTour] = useState(false); + + useEffect(() => { + const tour = tourForPath(pathname); + setHayTour(Boolean(tour)); + if (!tour) return; + if (localStorage.getItem(SEEN_PREFIX + tour.key) === '1') return; + + // Espera a que el contenido de la página esté montado antes de resaltar. + const t = setTimeout(() => { + localStorage.setItem(SEEN_PREFIX + tour.key, '1'); + lanzar(tour.steps); + }, 700); + return () => clearTimeout(t); + }, [pathname]); + + function visibles(steps: DriveStep[]): DriveStep[] { + return steps.filter((s) => { + const sel = s.element; + if (!sel || typeof sel !== 'string') return true; // paso centrado (intro) + const el = document.querySelector(sel) as HTMLElement | null; + return !!el && el.offsetParent !== null; + }); + } + + function lanzar(steps: DriveStep[]) { + const pasos = visibles(steps); + if (pasos.length === 0) return; + driver({ + showProgress: true, + overlayColor: '#0b1220', + nextBtnText: 'Siguiente', + prevBtnText: 'Atrás', + doneBtnText: 'Listo', + progressText: '{{current}} de {{total}}', + steps: pasos, + }).drive(); + } + + function repetir() { + const tour = tourForPath(pathname); + if (tour) lanzar(tour.steps); + } + + if (!hayTour) return null; + return ( + + ); +} diff --git a/mvp/b2c/src/lib/onboarding/panel-tour.ts b/mvp/b2c/src/lib/onboarding/panel-tour.ts new file mode 100644 index 0000000..8a634bd --- /dev/null +++ b/mvp/b2c/src/lib/onboarding/panel-tour.ts @@ -0,0 +1,147 @@ +import type { DriveStep } from 'driver.js'; + +// Pasos del onboarding del panel, por pestaña. El copy vive también en copy/COPY-GUIDE.md +// (sección "Onboarding del panel"). Los pasos cuyo elemento no exista o no esté visible se +// descartan en PanelTour (degrada con naturalidad en móvil o si una sección no aparece). + +const PASOS_PANEL: DriveStep[] = [ + { + popover: { + title: 'Tu panel de Reformix', + description: + 'Te enseño en 30 segundos qué hay en cada sitio. Puedes saltártelo cuando quieras con la X.', + }, + }, + { + element: '[data-tour="nav-leads"]', + popover: { + title: 'Leads', + description: + 'Aquí caen tus clientes con su presupuesto y su render ya preparados. Es tu día a día.', + side: 'bottom', + }, + }, + { + element: '[data-tour="nav-precios"]', + popover: { + title: 'Precios y baremo', + description: + 'Tu tabla de precios, la mano de obra y el baremo de rentabilidad. De aquí salen los presupuestos.', + side: 'bottom', + }, + }, + { + element: '[data-tour="nav-galeria"]', + popover: { title: 'Galería', description: 'Tus fotos de trabajos para enseñar en la web.', side: 'bottom' }, + }, + { + element: '[data-tour="nav-opiniones"]', + popover: { + title: 'Opiniones', + description: 'Reseñas de tus clientes; las apruebas tú antes de publicarlas.', + side: 'bottom', + }, + }, + { + element: '[data-tour="nav-empresa"]', + popover: { title: 'Empresa', description: 'Tu marca, logo y datos de contacto.', side: 'bottom' }, + }, + { + element: '[data-tour="leads-filtros"]', + popover: { + title: 'Filtra por estado', + description: 'Nuevos, contactados, presupuestados… para centrarte en lo que toca ahora.', + side: 'bottom', + }, + }, + { + element: '[data-tour="leads-tabla"]', + popover: { + title: 'Tus leads', + description: 'Cada cliente con su estado y su presupuesto. Ábrelo para ver el detalle completo.', + side: 'top', + }, + }, +]; + +const PASOS_FICHA: DriveStep[] = [ + { + element: '[data-tour="ficha-presupuesto"]', + popover: { + title: 'Presupuesto estimado', + description: + 'Lo que costaría la reforma según tu catálogo. Si no llega a tu baremo, lo verás en rojo.', + side: 'bottom', + }, + }, + { + element: '[data-tour="ficha-estado"]', + popover: { + title: 'Estado del lead', + description: 'Avanza el lead por el funnel: contactado, presupuestado, ganado…', + side: 'bottom', + }, + }, + { + element: '[data-tour="ficha-render"]', + popover: { + title: 'Render de la reforma', + description: + 'La imagen del “después” que ve tu cliente, generada a partir de su foto y sus gustos.', + side: 'top', + }, + }, + { + element: '[data-tour="ficha-desglose"]', + popover: { + title: 'Presupuesto desglosado', + description: + 'Edita las partidas, recalcula desde el catálogo y envíaselo al cliente por WhatsApp.', + side: 'top', + }, + }, +]; + +const PASOS_PRECIOS: DriveStep[] = [ + { + element: '[data-tour="precios-baremo"]', + popover: { + title: 'Baremo de rentabilidad', + description: + 'El trabajo mínimo que te compensa. Solo te avisa a ti: marca en rojo los leads por debajo.', + side: 'bottom', + }, + }, + { + element: '[data-tour="precios-config"]', + popover: { + title: 'Mano de obra', + description: 'Tus precios de mano de obra por m². El motor los usa para calcular cada presupuesto.', + side: 'top', + }, + }, + { + element: '[data-tour="precios-catalogo"]', + popover: { + title: 'Tu catálogo', + description: 'Materiales y precios por calidad. Puedes importarlos en bloque por CSV.', + side: 'top', + }, + }, +]; + +export interface PanelTour { + key: string; + steps: DriveStep[]; +} + +// Devuelve el tour que corresponde a la ruta actual del panel, o null si esa ruta no tiene tour. +export function tourForPath(pathname: string): PanelTour | null { + if (pathname === '/panel') return { key: 'panel', steps: PASOS_PANEL }; + if (pathname === '/panel/precios') return { key: 'precios', steps: PASOS_PRECIOS }; + const m = pathname.match(/^\/panel\/([^/]+)\/?$/); + if (m && !['precios', 'galeria', 'opiniones', 'empresa'].includes(m[1])) { + return { key: 'ficha', steps: PASOS_FICHA }; + } + return null; +} From df700bcbfb73b2849737bf21504e77a23eb77d6c Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Thu, 11 Jun 2026 17:25:15 +0200 Subject: [PATCH 3/6] =?UTF-8?q?Landing=20B2B:=20galer=C3=ADa=20de=20ejempl?= =?UTF-8?q?os=20apaisada=20con=20lightbox=20y=20quita=20"Entrar"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nueva sección "Ejemplos reales": 3 renders (cocina, baño, salón-comedor) en formato apaisado (16/10), clicables. - Lightbox reutilizable (data-zoom + data-gallery): amplía la imagen con navegación ‹ ›, Esc y clic fuera para cerrar; agrupa por galería. - El antes/después de "Cómo funciona" pasa a apaisado (4/3) y también abre el lightbox (grupo cocina). - Quita el botón "Entrar" del header (queda solo "Empezar gratis"). Co-Authored-By: Claude Opus 4.8 --- mvp/b2c/public/b2b.html | 126 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 5 deletions(-) diff --git a/mvp/b2c/public/b2b.html b/mvp/b2c/public/b2b.html index cd026cb..21a2773 100644 --- a/mvp/b2c/public/b2b.html +++ b/mvp/b2c/public/b2b.html @@ -1472,8 +1472,8 @@ h3, h4, h5, h6 { overflow: hidden; border: 1px solid var(--surface-border); } - .step .ba figure { position: relative; margin: 0; aspect-ratio: 1 / 1; } - .step .ba img { width: 100%; height: 100%; object-fit: cover; display: block; } + .step .ba figure { position: relative; margin: 0; aspect-ratio: 4 / 3; } + .step .ba img { width: 100%; height: 100%; object-fit: cover; display: block; cursor: zoom-in; } .step .ba figcaption { position: absolute; top: 6px; left: 6px; background: rgba(0, 0, 0, 0.6); color: #fff; @@ -1482,6 +1482,34 @@ h3, h4, h5, h6 { } .step .ba figure.after figcaption { background: var(--color-primary-600); } + /* Galería de ejemplos (apaisada, ampliable) */ + .gallery-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-4); margin-top: var(--space-8); } + @media (min-width: 640px) { .gallery-grid { grid-template-columns: repeat(2, 1fr); } } + @media (min-width: 1024px) { .gallery-grid { grid-template-columns: repeat(3, 1fr); gap: var(--space-6); } } + .gcard { + position: relative; margin: 0; border-radius: 14px; overflow: hidden; + border: 1px solid var(--surface-border); cursor: zoom-in; + background: var(--color-neutral-100, #f4f4f5); + } + .gcard img { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; display: block; transition: transform .45s ease; } + .gcard:hover img { transform: scale(1.04); } + .gcard figcaption { + position: absolute; left: 10px; bottom: 10px; + background: rgba(0, 0, 0, 0.62); color: #fff; + font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; + padding: 3px 8px; border-radius: 6px; + } + + /* Lightbox */ + #lightbox { position: fixed; inset: 0; z-index: 100; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(8, 12, 20, 0.92); } + #lightbox.open { display: flex; } + #lightbox .lb-img { max-width: min(1100px, 94vw); max-height: 86vh; width: auto; height: auto; border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); } + #lightbox .lb-cap { position: absolute; bottom: 18px; left: 0; right: 0; text-align: center; color: rgba(255, 255, 255, 0.85); font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.06em; padding: 0 16px; } + #lightbox .lb-close { position: absolute; top: 16px; right: 18px; width: 40px; height: 40px; border: none; border-radius: 50%; background: rgba(255, 255, 255, 0.12); color: #fff; font-size: 22px; line-height: 1; cursor: pointer; } + #lightbox .lb-btn { position: absolute; top: 50%; transform: translateY(-50%); width: 46px; height: 46px; border: none; border-radius: 50%; background: rgba(255, 255, 255, 0.12); color: #fff; font-size: 26px; line-height: 1; cursor: pointer; } + #lightbox .lb-prev { left: 18px; } #lightbox .lb-next { right: 18px; } + #lightbox .lb-close:hover, #lightbox .lb-btn:hover { background: rgba(255, 255, 255, 0.26); } + /* Bajo reduced-motion: mostrar todo sin animar */ @media (prefers-reduced-motion: reduce) { html.js .reveal, @@ -1508,7 +1536,6 @@ h3, h4, h5, h6 { Preguntas

@@ -1705,8 +1732,8 @@ h3, h4, h5, h6 {

Se genera un render orientativo

A partir de las fotos del cliente, Reformix devuelve 3 propuestas visuales con calidades básica, media y premium. Ya tienes de qué hablar en la visita.

-
Cocina antes de la reforma
ANTES
-
Render de la cocina reformada
DESPUÉS
+
Cocina antes de la reforma
ANTES
+
Render de la cocina reformada
DESPUÉS
⌁ Render IA
@@ -1726,6 +1753,33 @@ h3, h4, h5, h6 {
+ +
+
+
+

Ejemplos reales

+

El «después» que tu cliente ve antes de que vayas.

+

Una foto del espacio y Reformix devuelve el render orientativo. Toca cualquiera para ampliarla.

+
+ +
+
+ @@ -2160,4 +2214,66 @@ h3, h4, h5, h6 { })(); + + + + \ No newline at end of file From 5afda5af05821dc1236e277682a2a68b4462080f Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Thu, 11 Jun 2026 17:38:38 +0200 Subject: [PATCH 4/6] =?UTF-8?q?Arregla=20p=C3=A9rdida=20de=20datos=20al=20?= =?UTF-8?q?desplegar:=20el=20seed=20solo=20siembra=20si=20la=20DB=20est?= =?UTF-8?q?=C3=A1=20vac=C3=ADa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El docker-entrypoint corre db:seed en cada arranque. El guard comprobaba si existía un tenant CONCRETO ("reformas-ejemplo"); si ese no estaba pero había otros (una empresa creada por el reformista), el seed ejecutaba TRUNCATE de todas las tablas y resembraba el demo → borraba los datos reales en cada deploy. Ahora el guard salta si existe CUALQUIER tenant, así que con datos reales el seed nunca toca la DB. SEED_FORCE=1 sigue forzando el reseed (borra todo) a propósito. Co-Authored-By: Claude Opus 4.8 --- mvp/b2c/src/db/seed.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mvp/b2c/src/db/seed.ts b/mvp/b2c/src/db/seed.ts index a14b46b..7fe8c07 100644 --- a/mvp/b2c/src/db/seed.ts +++ b/mvp/b2c/src/db/seed.ts @@ -291,13 +291,13 @@ const SEED_LEADS: SeedLead[] = [ const STAGE_ORDER = schema.pipelineStage.enumValues; async function main() { - const [existing] = await db - .select() - .from(schema.tenants) - .where(eq(schema.tenants.slug, 'reformas-ejemplo')) - .limit(1); + // Guard de seguridad: solo sembramos si la base de datos está VACÍA (sin ningún tenant). Antes se + // comprobaba un slug concreto ("reformas-ejemplo"); si ese tenant no estaba pero había otros + // (p. ej. una empresa creada por el reformista), el seed los TRUNCABA en cada deploy → pérdida de + // datos. Ahora cualquier tenant existente protege toda la DB. SEED_FORCE=1 fuerza el reseed (BORRA TODO). + const [existing] = await db.select({ id: schema.tenants.id }).from(schema.tenants).limit(1); if (existing && !process.env.SEED_FORCE) { - console.log('Ya hay datos (tenant "reformas-ejemplo"). Saltando seed. Usa SEED_FORCE=1 para forzar.'); + console.log('La base de datos ya tiene datos (existe al menos un tenant). Saltando seed para no borrar nada. Usa SEED_FORCE=1 para forzar (¡BORRA TODO!).'); await client.end(); return; } From facf3cd79f100eaac408daad10b867fd2939d3b5 Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Thu, 11 Jun 2026 17:40:12 +0200 Subject: [PATCH 5/6] =?UTF-8?q?Landing=20del=20cliente:=20galer=C3=ADa=20a?= =?UTF-8?q?paisada=20con=20lightbox=20y=20quita=20"Entrar"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La galería de trabajos (GaleriaTrabajos) en la landing personalizada del cliente pasa a formato apaisado (3/2) y, al pulsar una foto, se amplía en un lightbox con navegación ‹ ›, Esc y clic fuera para cerrar. Se quita el botón "Entrar" del header de esa landing (TenantBrand sin showLogin): el cliente final no entra al panel. Revierte el cambio anterior en public/b2b.html (era la landing equivocada). Co-Authored-By: Claude Opus 4.8 --- mvp/b2c/public/b2b.html | 126 +----------------- mvp/b2c/src/app/[slug]/page.tsx | 2 +- .../src/components/funnel/GaleriaTrabajos.tsx | 123 +++++++++++++++-- 3 files changed, 116 insertions(+), 135 deletions(-) diff --git a/mvp/b2c/public/b2b.html b/mvp/b2c/public/b2b.html index 21a2773..cd026cb 100644 --- a/mvp/b2c/public/b2b.html +++ b/mvp/b2c/public/b2b.html @@ -1472,8 +1472,8 @@ h3, h4, h5, h6 { overflow: hidden; border: 1px solid var(--surface-border); } - .step .ba figure { position: relative; margin: 0; aspect-ratio: 4 / 3; } - .step .ba img { width: 100%; height: 100%; object-fit: cover; display: block; cursor: zoom-in; } + .step .ba figure { position: relative; margin: 0; aspect-ratio: 1 / 1; } + .step .ba img { width: 100%; height: 100%; object-fit: cover; display: block; } .step .ba figcaption { position: absolute; top: 6px; left: 6px; background: rgba(0, 0, 0, 0.6); color: #fff; @@ -1482,34 +1482,6 @@ h3, h4, h5, h6 { } .step .ba figure.after figcaption { background: var(--color-primary-600); } - /* Galería de ejemplos (apaisada, ampliable) */ - .gallery-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-4); margin-top: var(--space-8); } - @media (min-width: 640px) { .gallery-grid { grid-template-columns: repeat(2, 1fr); } } - @media (min-width: 1024px) { .gallery-grid { grid-template-columns: repeat(3, 1fr); gap: var(--space-6); } } - .gcard { - position: relative; margin: 0; border-radius: 14px; overflow: hidden; - border: 1px solid var(--surface-border); cursor: zoom-in; - background: var(--color-neutral-100, #f4f4f5); - } - .gcard img { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; display: block; transition: transform .45s ease; } - .gcard:hover img { transform: scale(1.04); } - .gcard figcaption { - position: absolute; left: 10px; bottom: 10px; - background: rgba(0, 0, 0, 0.62); color: #fff; - font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; - padding: 3px 8px; border-radius: 6px; - } - - /* Lightbox */ - #lightbox { position: fixed; inset: 0; z-index: 100; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(8, 12, 20, 0.92); } - #lightbox.open { display: flex; } - #lightbox .lb-img { max-width: min(1100px, 94vw); max-height: 86vh; width: auto; height: auto; border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); } - #lightbox .lb-cap { position: absolute; bottom: 18px; left: 0; right: 0; text-align: center; color: rgba(255, 255, 255, 0.85); font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.06em; padding: 0 16px; } - #lightbox .lb-close { position: absolute; top: 16px; right: 18px; width: 40px; height: 40px; border: none; border-radius: 50%; background: rgba(255, 255, 255, 0.12); color: #fff; font-size: 22px; line-height: 1; cursor: pointer; } - #lightbox .lb-btn { position: absolute; top: 50%; transform: translateY(-50%); width: 46px; height: 46px; border: none; border-radius: 50%; background: rgba(255, 255, 255, 0.12); color: #fff; font-size: 26px; line-height: 1; cursor: pointer; } - #lightbox .lb-prev { left: 18px; } #lightbox .lb-next { right: 18px; } - #lightbox .lb-close:hover, #lightbox .lb-btn:hover { background: rgba(255, 255, 255, 0.26); } - /* Bajo reduced-motion: mostrar todo sin animar */ @media (prefers-reduced-motion: reduce) { html.js .reveal, @@ -1536,6 +1508,7 @@ h3, h4, h5, h6 { Preguntas @@ -1732,8 +1705,8 @@ h3, h4, h5, h6 {

Se genera un render orientativo

A partir de las fotos del cliente, Reformix devuelve 3 propuestas visuales con calidades básica, media y premium. Ya tienes de qué hablar en la visita.

-
Cocina antes de la reforma
ANTES
-
Render de la cocina reformada
DESPUÉS
+
Cocina antes de la reforma
ANTES
+
Render de la cocina reformada
DESPUÉS
⌁ Render IA
@@ -1753,33 +1726,6 @@ h3, h4, h5, h6 {
- -
-
-
-

Ejemplos reales

-

El «después» que tu cliente ve antes de que vayas.

-

Una foto del espacio y Reformix devuelve el render orientativo. Toca cualquiera para ampliarla.

-
- -
-
- @@ -2214,66 +2160,4 @@ h3, h4, h5, h6 { })(); - - - - \ No newline at end of file diff --git a/mvp/b2c/src/app/[slug]/page.tsx b/mvp/b2c/src/app/[slug]/page.tsx index 2b9d7f2..c8b9806 100644 --- a/mvp/b2c/src/app/[slug]/page.tsx +++ b/mvp/b2c/src/app/[slug]/page.tsx @@ -46,7 +46,7 @@ export default async function FunnelPage({ params }: { params: Promise<{ slug: s className={theme.heading === 'serif' ? 'theme-serif' : undefined} style={themeStyle(tenant.themePreset, tenant.themeColor)} > - +
diff --git a/mvp/b2c/src/components/funnel/GaleriaTrabajos.tsx b/mvp/b2c/src/components/funnel/GaleriaTrabajos.tsx index a54b375..772439c 100644 --- a/mvp/b2c/src/components/funnel/GaleriaTrabajos.tsx +++ b/mvp/b2c/src/components/funnel/GaleriaTrabajos.tsx @@ -1,3 +1,6 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; import type { PublicGaleriaFoto } from '@/lib/funnel/public-queries'; type GaleriaTrabajosProps = { @@ -5,11 +8,37 @@ type GaleriaTrabajosProps = { nombreEmpresa: string; }; -// Galería de trabajos del reformista en su landing pública. Solo se muestra si -// el reformista ha subido fotos desde su panel. +// Galería de trabajos del reformista en su landing pública. Solo se muestra si el reformista ha +// subido fotos desde su panel. Formato apaisado y, al pulsar una foto, se amplía en un lightbox +// con navegación entre todas las imágenes. export default function GaleriaTrabajos({ fotos, nombreEmpresa }: GaleriaTrabajosProps) { + const [idx, setIdx] = useState(null); + + const cerrar = useCallback(() => setIdx(null), []); + const mover = useCallback( + (d: number) => setIdx((cur) => (cur === null ? cur : (cur + d + fotos.length) % fotos.length)), + [fotos.length], + ); + + useEffect(() => { + if (idx === null) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') cerrar(); + else if (e.key === 'ArrowRight') mover(1); + else if (e.key === 'ArrowLeft') mover(-1); + }; + document.addEventListener('keydown', onKey); + document.body.style.overflow = 'hidden'; + return () => { + document.removeEventListener('keydown', onKey); + document.body.style.overflow = ''; + }; + }, [idx, cerrar, mover]); + if (fotos.length === 0) return null; + const actual = idx !== null ? fotos[idx] : null; + return (
@@ -24,24 +53,31 @@ export default function GaleriaTrabajos({ fotos, nombreEmpresa }: GaleriaTrabajo Reformas que ya hemos hecho

- Una muestra real del trabajo de {nombreEmpresa}. Calidad de acabados, plazos cumplidos. + Una muestra real del trabajo de {nombreEmpresa}. Toca cualquier imagen para verla en grande.

-
- {fotos.map((f) => ( +
+ {fotos.map((f, i) => (
- {/* eslint-disable-next-line @next/next/no-img-element */} - {f.titulo + {f.titulo && ( -
+
{f.titulo}
)} @@ -49,6 +85,67 @@ export default function GaleriaTrabajos({ fotos, nombreEmpresa }: GaleriaTrabajo ))}
+ + {actual && ( +
+ + + {fotos.length > 1 && ( + + )} + + {/* eslint-disable-next-line @next/next/no-img-element */} + {actual.titulo e.stopPropagation()} + className="max-h-[86vh] max-w-[94vw] w-auto rounded-lg shadow-2xl" + /> + + {fotos.length > 1 && ( + + )} + + {actual.titulo && ( +
+ {actual.titulo} +
+ )} +
+ )}
); } From dbef9ef670418e5567116e816a66e60aba0230fe Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Thu, 11 Jun 2026 17:42:09 +0200 Subject: [PATCH 6/6] =?UTF-8?q?A=C3=B1ade=20stepper=20de=20progreso=20y=20?= =?UTF-8?q?bloque=20"Qu=C3=A9=20pasa=20despu=C3=A9s"=20al=20chooser=20de?= =?UTF-8?q?=20canal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El chooser (solicitud/[id]) ahora muestra un stepper de 3 pasos (Tus datos ✓, Tu reforma actual, Render + presupuesto pendiente) y, debajo de las tarjetas de canal, una sección "Elijas lo que elijas, esto es lo que pasa después" con los 3 pasos del flujo (nos cuentas tu reforma, render + presupuesto en minutos, visita gratuita para el presupuesto final). Las tarjetas de canal pasan a grid de 3 columnas en desktop, con iconos SV --- copy/COPY-GUIDE.md | 19 ++ mvp/b2c/src/app/globals.css | 14 ++ mvp/b2c/src/app/solicitud/[id]/page.tsx | 289 ++++++++++++++++++++---- 3 files changed, 284 insertions(+), 38 deletions(-) diff --git a/copy/COPY-GUIDE.md b/copy/COPY-GUIDE.md index 91d9769..4a2925e 100644 --- a/copy/COPY-GUIDE.md +++ b/copy/COPY-GUIDE.md @@ -327,9 +327,15 @@ Somos los únicos en España que combinamos **captura instantánea del lead + ll - **Título del paso:** ¿Cómo prefieres contarnos tu reforma, [Nombre]? - **Subtitle:** Tú eliges. Por cualquiera de las tres nos das lo que necesitamos para preparar tu render y tu presupuesto. +- **Stepper de progreso (encima del título):** + - Paso 1 (completado): *Tus datos* + - Paso 2 (actual): *Tu reforma* + - Paso 3 (pendiente): *Render + presupuesto* + - **Tarjeta Llamada — título:** Que te llamemos **Descripción:** Un asistente te llama y te hace unas preguntas rápidas. Lo más cómodo: no escribes nada. **CTA:** Quiero que me llamen + **Badge:** La más rápida - **Tarjeta WhatsApp — título:** Por WhatsApp **Descripción:** Seguimos por chat a tu ritmo. Puedes mandar fotos y notas cuando quieras. **CTA:** Seguir por WhatsApp @@ -337,6 +343,19 @@ Somos los únicos en España que combinamos **captura instantánea del lead + ll **Descripción:** Tú lo cuentas zona por zona y subes las fotos. Recibes el presupuesto al instante. **CTA:** Rellenar el formulario +#### Bloque "Qué pasa después" (debajo de las tarjetas del chooser) + +> Recuerda al lead lo que va a recibir elija el canal que elija: personalización, render con +> imágenes en minutos, y visita gratuita posterior para el presupuesto definitivo. + +- **Título:** Elijas lo que elijas, esto es lo que pasa después +- **Paso 1 — título:** Nos cuentas tu reforma a tu manera + **Body:** Fotos, medidas, gustos. Cuanto más nos cuentes, más se parecerá el resultado a lo que tienes en la cabeza. +- **Paso 2 — título:** Render + presupuesto en minutos + **Body:** Ves tu espacio ya reformado en imágenes, con un presupuesto orientativo desglosado partida a partida. +- **Paso 3 — título:** Visita gratuita para el presupuesto final + **Body:** Si te convence, acuerdas una visita con [Reformista]: ve la obra, la mide con detalle y te confirma el precio definitivo. Sin compromiso. + ### Paso 2 (canal llamada) - **Título del paso:** Te llamamos cuando quieras diff --git a/mvp/b2c/src/app/globals.css b/mvp/b2c/src/app/globals.css index 02cdd60..fda30e1 100644 --- a/mvp/b2c/src/app/globals.css +++ b/mvp/b2c/src/app/globals.css @@ -58,6 +58,20 @@ --transition-fast: 150ms ease; --transition-base: 250ms ease; --transition-slow: 400ms ease; + + /* Animations (usar con motion-safe: para respetar prefers-reduced-motion) */ + --animate-fade-up: fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) both; + + @keyframes fade-up { + from { + opacity: 0; + transform: translateY(14px); + } + to { + opacity: 1; + transform: none; + } + } } @layer base { diff --git a/mvp/b2c/src/app/solicitud/[id]/page.tsx b/mvp/b2c/src/app/solicitud/[id]/page.tsx index 2fd5062..9e50c4d 100644 --- a/mvp/b2c/src/app/solicitud/[id]/page.tsx +++ b/mvp/b2c/src/app/solicitud/[id]/page.tsx @@ -1,82 +1,295 @@ import Link from 'next/link'; import { notFound } from 'next/navigation'; import { getPublicLead } from '@/lib/funnel/public-queries'; +import { resolveTheme, themeStyle } from '@/lib/funnel/themes'; import TenantBrand from '@/components/funnel/TenantBrand'; export const dynamic = 'force-dynamic'; +const BRAND = 'var(--brand, #0a0a0a)'; +const BRAND_CONTRAST = 'var(--brand-contrast, #ffffff)'; + +function IconLlamada() { + return ( + + ); +} + +function IconWhatsapp() { + return ( + + ); +} + +function IconFormulario() { + return ( + + ); +} + const CANALES = [ { slug: 'llamada', - icon: '📞', + icon: , titulo: 'Que te llamemos', descripcion: 'Un asistente te llama y te hace unas preguntas rápidas. Lo más cómodo: no escribes nada.', cta: 'Quiero que me llamen', + badge: 'La más rápida', }, { slug: 'whatsapp', - icon: '💬', + icon: , titulo: 'Por WhatsApp', - descripcion: - 'Seguimos por chat a tu ritmo. Puedes mandar fotos y notas cuando quieras.', + descripcion: 'Seguimos por chat a tu ritmo. Puedes mandar fotos y notas cuando quieras.', cta: 'Seguir por WhatsApp', + badge: null, }, { slug: 'formulario', - icon: '📝', + icon: , titulo: 'Rellenar un formulario', descripcion: 'Tú lo cuentas zona por zona y subes las fotos. Recibes el presupuesto al instante.', cta: 'Rellenar el formulario', + badge: null, }, ] as const; +const PASOS_DESPUES = [ + { + titulo: 'Nos cuentas tu reforma a tu manera', + body: 'Fotos, medidas, gustos. Cuanto más nos cuentes, más se parecerá el resultado a lo que tienes en la cabeza.', + }, + { + titulo: 'Render + presupuesto en minutos', + body: 'Ves tu espacio ya reformado en imágenes, con un presupuesto orientativo desglosado partida a partida.', + }, + // El tercer paso interpola el nombre del reformista; se monta en el componente. +] as const; + +function Stepper() { + const conectores = 'h-px flex-1 min-w-4'; + return ( +
    +
  1. + + + + Tus datos +
  2. +
  3. + + 2 + + Tu reforma +
  4. +
  5. + + 3 + + + Render + presupuesto + +
  6. +
+ ); +} + export default async function ChooserPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const data = await getPublicLead(id); if (!data) notFound(); const { lead, tenant } = data; + const theme = resolveTheme(tenant?.themePreset, tenant?.themeColor); + const nombrePila = lead.nombre.split(' ')[0]; + const nombreReformista = tenant?.nombreEmpresa ?? 'el reformista'; + + const pasosDespues = [ + ...PASOS_DESPUES, + { + titulo: 'Visita gratuita para el presupuesto final', + body: `Si te convence, acuerdas una visita con ${nombreReformista}: ve la obra, la mide con detalle y te confirma el precio definitivo. Sin compromiso.`, + }, + ]; return ( - <> +
{tenant && } -
-
- - Elige cómo seguir - -

- ¿Cómo prefieres contarnos tu reforma, {lead.nombre.split(' ')[0]}? -

-

- Tú eliges. Por cualquiera de las tres nos das lo que necesitamos para preparar tu render - y tu presupuesto. -

-
-
- {CANALES.map((c) => ( - - -
- {c.titulo} - {c.descripcion} - - {c.cta} → +
+ {/* Halo sutil con el color de marca del reformista */} +
- +
); }