diff --git a/copy/COPY-GUIDE.md b/copy/COPY-GUIDE.md index 8463403..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 @@ -714,6 +733,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/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/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/[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/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/panel/[id]/page.tsx b/mvp/b2c/src/app/panel/[id]/page.tsx index 16742c3..d4e30ba 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'; @@ -19,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}
@@ -41,6 +53,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'; @@ -62,16 +82,25 @@ export default async function LeadDetailPage({ params }: { params: Promise<{ id: {lead.provincia ?? '—'} · entró {formatFecha(lead.createdAt)}

-
+
Presupuesto estimado
-
{formatEuros(lead.presupuestoEstimado)}
+
+ {formatEuros(lead.presupuestoEstimado)} +
+ {pasaBaremo === false && baremoMinimo != null && ( +
+ Por debajo de tu baremo ({formatEuros(baremoMinimo)}) +
+ )}
- +
+ +
{/* Solicitar opinión al cliente */} @@ -164,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 @@ -311,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/actions.ts b/mvp/b2c/src/app/panel/precios/actions.ts index 0ac8156..d149f40 100644 --- a/mvp/b2c/src/app/panel/precios/actions.ts +++ b/mvp/b2c/src/app/panel/precios/actions.ts @@ -93,6 +93,19 @@ export async function actualizarExtras(formData: FormData) { revalidatePath('/panel/precios'); } +export async function actualizarBaremo(formData: FormData) { + const tenantId = await getTenantId(); + const raw = formData.get('baremoMinimo'); + const txt = typeof raw === 'string' ? raw.trim() : ''; + // Vacío = sin baremo (null). Con valor = euros → céntimos. + const baremoMinimo = txt === '' ? null : eurosToCents(raw, 'baremo de rentabilidad'); + await db + .update(pricingConfig) + .set({ baremoMinimo, updatedAt: new Date() }) + .where(eq(pricingConfig.tenantId, tenantId)); + revalidatePath('/panel/precios'); +} + export async function actualizarEnvio(formData: FormData) { const tenantId = await getTenantId(); const modo = formData.get('modo'); diff --git a/mvp/b2c/src/app/panel/precios/page.tsx b/mvp/b2c/src/app/panel/precios/page.tsx index 75cca9d..d8bee7c 100644 --- a/mvp/b2c/src/app/panel/precios/page.tsx +++ b/mvp/b2c/src/app/panel/precios/page.tsx @@ -5,6 +5,7 @@ import { borrarMaterial, actualizarConfig, actualizarExtras, + actualizarBaremo, actualizarEnvio, importarCatalogoCsv, } from './actions'; @@ -81,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 + 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

@@ -256,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/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 */} +
- +
); } 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/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/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} +
+ )} +
+ )}
); } 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/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(), }); 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; } 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; +}