-
-
- {showMenu && (
- <>
-
setShowMenu(false)}
- />
-
-
-
- {t('tour:trigger.button', 'Tours Disponibles')}
-
-
- {t('tour:trigger.tooltip', '¿Necesitas ayuda? Inicia un tour guiado')}
-
-
-
- {Object.entries(allTours).map(([key, tour]) => (
-
- ))}
-
-
- >
- )}
-
- );
- }
-
- // Single tour button variant
- if (tourId) {
- const tour = allTours[tourId];
- if (!tour) return null;
-
- const isCompleted = isTourCompleted(tour.id);
-
- return (
-
- );
- }
-
- // Default button that shows menu
- return (
-
-
-
- {showMenu && (
- <>
-
setShowMenu(false)}
- />
-
-
-
- {t('tour:trigger.button', 'Tours Disponibles')}
-
-
- {t('tour:trigger.tooltip', '¿Necesitas ayuda? Inicia un tour guiado')}
-
-
-
- {Object.entries(allTours).map(([key, tour]) => (
-
- ))}
-
-
- >
- )}
-
- );
-};
-
-export default TourButton;
diff --git a/frontend/src/components/ui/Tour/TourSpotlight.tsx b/frontend/src/components/ui/Tour/TourSpotlight.tsx
deleted file mode 100644
index d0add4df..00000000
--- a/frontend/src/components/ui/Tour/TourSpotlight.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import React, { useEffect, useState } from 'react';
-
-export interface TourSpotlightProps {
- target: string; // CSS selector
- padding?: number;
-}
-
-export const TourSpotlight: React.FC
= ({ target, padding = 8 }) => {
- const [rect, setRect] = useState(null);
-
- useEffect(() => {
- const updateRect = () => {
- const element = document.querySelector(target);
- if (element) {
- const elementRect = element.getBoundingClientRect();
- setRect(elementRect);
-
- // Scroll element into view if not fully visible
- element.scrollIntoView({
- behavior: 'smooth',
- block: 'center',
- inline: 'center',
- });
- }
- };
-
- updateRect();
-
- // Update on resize or scroll
- window.addEventListener('resize', updateRect);
- window.addEventListener('scroll', updateRect, true);
-
- return () => {
- window.removeEventListener('resize', updateRect);
- window.removeEventListener('scroll', updateRect, true);
- };
- }, [target]);
-
- if (!rect) return null;
-
- return (
- <>
- {/* Overlay with cutout */}
-
-
- {/* Highlighted border around target */}
-
- >
- );
-};
-
-export default TourSpotlight;
diff --git a/frontend/src/components/ui/Tour/TourTooltip.tsx b/frontend/src/components/ui/Tour/TourTooltip.tsx
deleted file mode 100644
index 91e358a5..00000000
--- a/frontend/src/components/ui/Tour/TourTooltip.tsx
+++ /dev/null
@@ -1,206 +0,0 @@
-import React, { useEffect, useState, useRef } from 'react';
-import { useTranslation } from 'react-i18next';
-import { X, ArrowLeft, ArrowRight, Check } from 'lucide-react';
-import Button from '../Button/Button';
-import { TourStep } from '../../../contexts/TourContext';
-
-export interface TourTooltipProps {
- step: TourStep;
- currentStep: number;
- totalSteps: number;
- onNext: () => void;
- onPrevious: () => void;
- onSkip: () => void;
- onComplete: () => void;
-}
-
-export const TourTooltip: React.FC = ({
- step,
- currentStep,
- totalSteps,
- onNext,
- onPrevious,
- onSkip,
- onComplete,
-}) => {
- const { t } = useTranslation();
- const [position, setPosition] = useState({ top: 0, left: 0 });
- const [placement, setPlacement] = useState(step.placement || 'bottom');
- const tooltipRef = useRef(null);
-
- useEffect(() => {
- const calculatePosition = () => {
- const target = document.querySelector(step.target);
- if (!target || !tooltipRef.current) return;
-
- const targetRect = target.getBoundingClientRect();
- const tooltipRect = tooltipRef.current.getBoundingClientRect();
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- let top = 0;
- let left = 0;
- let finalPlacement = step.placement || 'bottom';
-
- // Calculate initial position based on placement
- switch (step.placement || 'bottom') {
- case 'top':
- top = targetRect.top - tooltipRect.height - 16;
- left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
- break;
- case 'bottom':
- top = targetRect.bottom + 16;
- left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
- break;
- case 'left':
- top = targetRect.top + (targetRect.height - tooltipRect.height) / 2;
- left = targetRect.left - tooltipRect.width - 16;
- break;
- case 'right':
- top = targetRect.top + (targetRect.height - tooltipRect.height) / 2;
- left = targetRect.right + 16;
- break;
- }
-
- // Adjust if tooltip goes off screen
- if (left < 16) {
- left = 16;
- } else if (left + tooltipRect.width > viewportWidth - 16) {
- left = viewportWidth - tooltipRect.width - 16;
- }
-
- if (top < 16) {
- top = 16;
- finalPlacement = 'bottom';
- } else if (top + tooltipRect.height > viewportHeight - 16) {
- top = viewportHeight - tooltipRect.height - 16;
- finalPlacement = 'top';
- }
-
- setPosition({ top, left });
- setPlacement(finalPlacement);
- };
-
- // Calculate position on mount and when step changes
- calculatePosition();
-
- // Recalculate on window resize or scroll
- window.addEventListener('resize', calculatePosition);
- window.addEventListener('scroll', calculatePosition, true);
-
- return () => {
- window.removeEventListener('resize', calculatePosition);
- window.removeEventListener('scroll', calculatePosition, true);
- };
- }, [step]);
-
- const isFirstStep = currentStep === 0;
- const isLastStep = currentStep === totalSteps - 1;
-
- return (
-
- {/* Header */}
-
-
-
{step.title}
-
- {t('tour:step_progress', 'Paso {{current}} de {{total}}', {
- current: currentStep + 1,
- total: totalSteps,
- })}
-
-
-
-
-
- {/* Content */}
-
-
{step.content}
-
- {/* Action button if provided */}
- {step.action && (
-
-
-
- )}
-
-
- {/* Footer */}
-
- {/* Progress dots */}
-
- {Array.from({ length: totalSteps }).map((_, index) => (
-
- ))}
-
-
- {/* Navigation buttons */}
-
- {!isFirstStep && (
- }>
- {t('tour:previous', 'Anterior')}
-
- )}
-
- {isLastStep ? (
- }>
- {t('tour:finish', 'Finalizar')}
-
- ) : (
- }>
- {t('tour:next', 'Siguiente')}
-
- )}
-
-
-
- {/* Arrow pointing to target */}
-
-
- );
-};
-
-export default TourTooltip;
diff --git a/frontend/src/components/ui/Tour/index.ts b/frontend/src/components/ui/Tour/index.ts
deleted file mode 100644
index 195c1cbb..00000000
--- a/frontend/src/components/ui/Tour/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { Tour, default } from './Tour';
-export { TourTooltip } from './TourTooltip';
-export { TourSpotlight } from './TourSpotlight';
diff --git a/frontend/src/contexts/TourContext.tsx b/frontend/src/contexts/TourContext.tsx
deleted file mode 100644
index 2c951609..00000000
--- a/frontend/src/contexts/TourContext.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
-
-export interface TourStep {
- id: string;
- target: string; // CSS selector
- title: string;
- content: string;
- placement?: 'top' | 'bottom' | 'left' | 'right';
- action?: {
- label: string;
- onClick: () => void;
- };
- beforeShow?: () => void | Promise;
- afterShow?: () => void | Promise;
-}
-
-export interface Tour {
- id: string;
- name: string;
- steps: TourStep[];
- onComplete?: () => void;
- onSkip?: () => void;
-}
-
-export interface TourState {
- currentTour: Tour | null;
- currentStepIndex: number;
- isActive: boolean;
- completedTours: string[];
- skippedTours: string[];
-}
-
-export interface TourContextValue {
- state: TourState;
- startTour: (tour: Tour) => void;
- nextStep: () => void;
- previousStep: () => void;
- skipTour: () => void;
- completeTour: () => void;
- resetTour: () => void;
- isTourCompleted: (tourId: string) => boolean;
- isTourSkipped: (tourId: string) => boolean;
- markTourCompleted: (tourId: string) => void;
-}
-
-const initialState: TourState = {
- currentTour: null,
- currentStepIndex: 0,
- isActive: false,
- completedTours: [],
- skippedTours: [],
-};
-
-const TourContext = createContext(undefined);
-
-const STORAGE_KEY = 'bakery_ia_tours';
-
-export interface TourProviderProps {
- children: ReactNode;
-}
-
-export const TourProvider: React.FC = ({ children }) => {
- const [state, setState] = useState(() => {
- // Load persisted state from localStorage
- try {
- const stored = localStorage.getItem(STORAGE_KEY);
- if (stored) {
- const parsed = JSON.parse(stored);
- return {
- ...initialState,
- completedTours: parsed.completedTours || [],
- skippedTours: parsed.skippedTours || [],
- };
- }
- } catch (error) {
- console.error('Failed to load tour state:', error);
- }
- return initialState;
- });
-
- // Persist completed and skipped tours to localStorage
- useEffect(() => {
- try {
- const toStore = {
- completedTours: state.completedTours,
- skippedTours: state.skippedTours,
- };
- localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
- } catch (error) {
- console.error('Failed to save tour state:', error);
- }
- }, [state.completedTours, state.skippedTours]);
-
- const startTour = async (tour: Tour) => {
- // Don't start if already completed or skipped
- if (state.completedTours.includes(tour.id) || state.skippedTours.includes(tour.id)) {
- console.log(`Tour ${tour.id} already completed or skipped`);
- return;
- }
-
- // Execute beforeShow for first step
- if (tour.steps[0]?.beforeShow) {
- await tour.steps[0].beforeShow();
- }
-
- setState(prev => ({
- ...prev,
- currentTour: tour,
- currentStepIndex: 0,
- isActive: true,
- }));
-
- // Execute afterShow for first step
- if (tour.steps[0]?.afterShow) {
- await tour.steps[0].afterShow();
- }
- };
-
- const nextStep = async () => {
- if (!state.currentTour) return;
-
- const nextIndex = state.currentStepIndex + 1;
-
- if (nextIndex >= state.currentTour.steps.length) {
- // Tour completed
- completeTour();
- return;
- }
-
- const nextStep = state.currentTour.steps[nextIndex];
-
- // Execute beforeShow
- if (nextStep?.beforeShow) {
- await nextStep.beforeShow();
- }
-
- setState(prev => ({
- ...prev,
- currentStepIndex: nextIndex,
- }));
-
- // Execute afterShow
- if (nextStep?.afterShow) {
- await nextStep.afterShow();
- }
- };
-
- const previousStep = () => {
- if (!state.currentTour || state.currentStepIndex === 0) return;
-
- setState(prev => ({
- ...prev,
- currentStepIndex: prev.currentStepIndex - 1,
- }));
- };
-
- const skipTour = () => {
- if (!state.currentTour) return;
-
- const tourId = state.currentTour.id;
- state.currentTour.onSkip?.();
-
- setState(prev => ({
- ...prev,
- currentTour: null,
- currentStepIndex: 0,
- isActive: false,
- skippedTours: [...prev.skippedTours, tourId],
- }));
- };
-
- const completeTour = () => {
- if (!state.currentTour) return;
-
- const tourId = state.currentTour.id;
- state.currentTour.onComplete?.();
-
- setState(prev => ({
- ...prev,
- currentTour: null,
- currentStepIndex: 0,
- isActive: false,
- completedTours: [...prev.completedTours, tourId],
- }));
- };
-
- const resetTour = () => {
- setState(prev => ({
- ...prev,
- currentTour: null,
- currentStepIndex: 0,
- isActive: false,
- }));
- };
-
- const isTourCompleted = (tourId: string): boolean => {
- return state.completedTours.includes(tourId);
- };
-
- const isTourSkipped = (tourId: string): boolean => {
- return state.skippedTours.includes(tourId);
- };
-
- const markTourCompleted = (tourId: string) => {
- setState(prev => ({
- ...prev,
- completedTours: [...prev.completedTours, tourId],
- }));
- };
-
- const value: TourContextValue = {
- state,
- startTour,
- nextStep,
- previousStep,
- skipTour,
- completeTour,
- resetTour,
- isTourCompleted,
- isTourSkipped,
- markTourCompleted,
- };
-
- return {children};
-};
-
-/**
- * Hook to access tour context
- */
-export const useTour = (): TourContextValue => {
- const context = useContext(TourContext);
- if (!context) {
- throw new Error('useTour must be used within a TourProvider');
- }
- return context;
-};
-
-export default TourContext;
diff --git a/frontend/src/locales/es/tour.json b/frontend/src/locales/es/tour.json
deleted file mode 100644
index 25e5fbba..00000000
--- a/frontend/src/locales/es/tour.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
- "step_progress": "Paso {{current}} de {{total}}",
- "close": "Cerrar",
- "previous": "Anterior",
- "next": "Siguiente",
- "finish": "Finalizar",
- "skip": "Omitir tour",
- "start_tour": "Iniciar tour",
- "restart_tour": "Reiniciar tour",
- "tours": {
- "dashboard": {
- "name": "Tour del Dashboard",
- "description": "Conoce las funciones principales de tu dashboard"
- },
- "inventory": {
- "name": "Tour de Inventario",
- "description": "Aprende a gestionar tu inventario eficientemente"
- },
- "recipes": {
- "name": "Tour de Recetas",
- "description": "Descubre cómo crear y gestionar recetas"
- },
- "production": {
- "name": "Tour de Producción",
- "description": "Planifica tu producción con confianza"
- },
- "post_onboarding": {
- "name": "Primeros Pasos",
- "description": "Comienza a usar tu sistema de gestión"
- }
- },
- "completed": {
- "title": "¡Tour Completado!",
- "message": "Has completado el tour de {{tourName}}",
- "cta": "Entendido"
- },
- "trigger": {
- "tooltip": "¿Necesitas ayuda? Inicia un tour guiado",
- "button": "Tours Disponibles"
- }
-}
diff --git a/frontend/src/tours/tours.ts b/frontend/src/tours/tours.ts
deleted file mode 100644
index 21c08e4d..00000000
--- a/frontend/src/tours/tours.ts
+++ /dev/null
@@ -1,276 +0,0 @@
-import { Tour } from '../contexts/TourContext';
-
-/**
- * Dashboard Tour
- * Guides users through the main dashboard features
- */
-export const dashboardTour: Tour = {
- id: 'dashboard-tour',
- name: 'Tour del Dashboard',
- steps: [
- {
- id: 'dashboard-welcome',
- target: '[data-tour="dashboard-header"]',
- title: '¡Bienvenido a tu Dashboard!',
- content:
- 'Este es tu centro de control. Aquí verás un resumen de toda tu operación de panadería en tiempo real.',
- placement: 'bottom',
- },
- {
- id: 'dashboard-stats',
- target: '[data-tour="stats-cards"]',
- title: 'Estadísticas Clave',
- content:
- 'Estas tarjetas muestran las métricas más importantes: ventas del día, inventario crítico, producción pendiente y alertas de calidad.',
- placement: 'bottom',
- },
- {
- id: 'dashboard-forecast',
- target: '[data-tour="forecast-chart"]',
- title: 'Pronóstico de Demanda con IA',
- content:
- 'Nuestra IA analiza tus datos históricos para predecir la demanda futura. Úsalo para planificar tu producción.',
- placement: 'top',
- },
- {
- id: 'dashboard-inventory',
- target: '[data-tour="inventory-alerts"]',
- title: 'Alertas de Inventario',
- content:
- 'Aquí verás alertas sobre ingredientes que están por agotarse o que han caducado. ¡Nunca te quedarás sin stock!',
- placement: 'left',
- },
- {
- id: 'dashboard-navigation',
- target: '[data-tour="main-navigation"]',
- title: 'Navegación Principal',
- content:
- 'Usa este menú para acceder a todas las funciones: Inventario, Recetas, Producción, Ventas, Proveedores y más.',
- placement: 'right',
- },
- ],
-};
-
-/**
- * Inventory Tour
- * Guides users through inventory management features
- */
-export const inventoryTour: Tour = {
- id: 'inventory-tour',
- name: 'Tour de Inventario',
- steps: [
- {
- id: 'inventory-welcome',
- target: '[data-tour="inventory-header"]',
- title: 'Gestión de Inventario',
- content:
- 'Aquí administras todos tus ingredientes y productos terminados. Mantén el control total de tu stock.',
- placement: 'bottom',
- },
- {
- id: 'inventory-add',
- target: '[data-tour="add-item-button"]',
- title: 'Agregar Ingredientes',
- content:
- 'Haz clic aquí para agregar nuevos ingredientes. Puedes ingresar nombre, categoría, unidad de medida, precio y más.',
- placement: 'left',
- action: {
- label: 'Ver formulario',
- onClick: () => {
- const button = document.querySelector('[data-tour="add-item-button"]') as HTMLButtonElement;
- button?.click();
- },
- },
- },
- {
- id: 'inventory-search',
- target: '[data-tour="search-filter"]',
- title: 'Buscar y Filtrar',
- content:
- 'Usa la barra de búsqueda y filtros para encontrar rápidamente lo que necesitas. Filtra por categoría, estado de stock o proveedor.',
- placement: 'bottom',
- },
- {
- id: 'inventory-table',
- target: '[data-tour="inventory-table"]',
- title: 'Lista de Inventario',
- content:
- 'Aquí ves todos tus ingredientes con stock actual, punto de reorden, costo y acciones rápidas para editar o eliminar.',
- placement: 'top',
- },
- {
- id: 'inventory-alerts',
- target: '[data-tour="stock-alerts"]',
- title: 'Alertas de Stock',
- content:
- 'Los ingredientes con stock bajo aparecen resaltados. Configura puntos de reorden para recibir alertas automáticas.',
- placement: 'left',
- },
- ],
-};
-
-/**
- * Recipes Tour
- * Guides users through recipe management
- */
-export const recipesTour: Tour = {
- id: 'recipes-tour',
- name: 'Tour de Recetas',
- steps: [
- {
- id: 'recipes-welcome',
- target: '[data-tour="recipes-header"]',
- title: 'Gestión de Recetas',
- content:
- 'Define tus recetas con ingredientes, cantidades y procesos. El sistema calculará costos automáticamente.',
- placement: 'bottom',
- },
- {
- id: 'recipes-add',
- target: '[data-tour="add-recipe-button"]',
- title: 'Crear Receta',
- content:
- 'Haz clic para crear una nueva receta. Agrega ingredientes, define cantidades y establece el rendimiento esperado.',
- placement: 'left',
- },
- {
- id: 'recipes-cost',
- target: '[data-tour="recipe-cost-column"]',
- title: 'Cálculo de Costos',
- content:
- 'El sistema calcula automáticamente el costo de cada receta basándose en los precios de los ingredientes.',
- placement: 'top',
- },
- {
- id: 'recipes-yield',
- target: '[data-tour="recipe-yield"]',
- title: 'Rendimiento de Receta',
- content:
- 'Define cuántas unidades produce cada receta. Esto es crucial para planificar la producción correctamente.',
- placement: 'bottom',
- },
- {
- id: 'recipes-batch',
- target: '[data-tour="batch-multiplier"]',
- title: 'Multiplicador de Lote',
- content:
- '¿Necesitas hacer múltiples lotes? Usa el multiplicador para escalar automáticamente las cantidades de ingredientes.',
- placement: 'left',
- },
- ],
-};
-
-/**
- * Production Tour
- * Guides users through production planning
- */
-export const productionTour: Tour = {
- id: 'production-tour',
- name: 'Tour de Producción',
- steps: [
- {
- id: 'production-welcome',
- target: '[data-tour="production-header"]',
- title: 'Planificación de Producción',
- content:
- 'Planifica qué y cuánto producir cada día. El sistema te ayudará basándose en el pronóstico de demanda.',
- placement: 'bottom',
- },
- {
- id: 'production-schedule',
- target: '[data-tour="production-schedule"]',
- title: 'Calendario de Producción',
- content:
- 'Visualiza tu plan de producción por día, semana o mes. Arrastra y suelta para reorganizar fácilmente.',
- placement: 'top',
- },
- {
- id: 'production-forecast',
- target: '[data-tour="production-forecast"]',
- title: 'Recomendaciones de IA',
- content:
- 'La IA sugiere cantidades óptimas basadas en el pronóstico de demanda, inventario actual y ventas históricas.',
- placement: 'bottom',
- },
- {
- id: 'production-batch',
- target: '[data-tour="create-batch-button"]',
- title: 'Crear Lote de Producción',
- content:
- 'Crea un nuevo lote seleccionando la receta y cantidad. El sistema verificará que tengas suficiente inventario.',
- placement: 'left',
- },
- {
- id: 'production-status',
- target: '[data-tour="production-status"]',
- title: 'Estado de Lotes',
- content:
- 'Rastrea el estado de cada lote: Planificado, En Proceso, Completado o Cancelado. Actualiza en tiempo real.',
- placement: 'top',
- },
- ],
-};
-
-/**
- * Post-Onboarding Tour
- * First tour shown after completing onboarding
- */
-export const postOnboardingTour: Tour = {
- id: 'post-onboarding-tour',
- name: 'Primeros Pasos',
- steps: [
- {
- id: 'welcome',
- target: 'body',
- title: '¡Configuración Completa! 🎉',
- content:
- 'Tu panadería está lista. Ahora te mostraremos las funciones principales para que puedas empezar a trabajar.',
- placement: 'bottom',
- },
- {
- id: 'dashboard-overview',
- target: '[data-tour="main-navigation"]',
- title: 'Navegación Principal',
- content:
- 'Desde aquí puedes acceder a todas las secciones: Dashboard, Inventario, Recetas, Producción, Ventas y más.',
- placement: 'right',
- },
- {
- id: 'quick-actions',
- target: '[data-tour="quick-actions"]',
- title: 'Acciones Rápidas',
- content:
- 'Usa estos botones para acciones frecuentes: agregar ingrediente, crear receta, planificar producción.',
- placement: 'bottom',
- },
- {
- id: 'notifications',
- target: '[data-tour="notifications-button"]',
- title: 'Notificaciones',
- content:
- 'Aquí verás alertas importantes: stock bajo, lotes pendientes, vencimientos próximos.',
- placement: 'left',
- },
- {
- id: 'help',
- target: '[data-tour="help-button"]',
- title: 'Ayuda Siempre Disponible',
- content:
- 'Si necesitas ayuda, haz clic aquí para acceder a tutoriales, documentación o contactar soporte.',
- placement: 'left',
- },
- ],
-};
-
-/**
- * All available tours
- */
-export const allTours = {
- dashboard: dashboardTour,
- inventory: inventoryTour,
- recipes: recipesTour,
- production: productionTour,
- postOnboarding: postOnboardingTour,
-};
-
-export default allTours;