From d92d5e2f1250bb8b01bcb04e81bce5cb109c5c1d Mon Sep 17 00:00:00 2001
From: Carlos Narro {title}
{children}
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 */}
-
Cabecera: Importar catálogo (CSV)
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;
+}