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; +}