diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a4829bb3..bb5b2eb8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { Suspense } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { BrowserRouter } from 'react-router-dom'; +import { I18nextProvider } from 'react-i18next'; import { Toaster } from 'react-hot-toast'; import { ErrorBoundary } from './components/shared/ErrorBoundary'; import { LoadingSpinner } from './components/shared/LoadingSpinner'; @@ -10,7 +11,7 @@ import { ThemeProvider } from './contexts/ThemeContext'; import { AuthProvider } from './contexts/AuthContext'; import { SSEProvider } from './contexts/SSEContext'; import GlobalSubscriptionHandler from './components/auth/GlobalSubscriptionHandler'; -import './i18n'; +import i18n from './i18n'; const queryClient = new QueryClient({ defaultOptions: { @@ -27,34 +28,36 @@ function App() { return ( - - - - - }> - - - - - - - - - + + + + + + }> + + + + + + + + + + ); diff --git a/frontend/src/api/services/tenant.ts b/frontend/src/api/services/tenant.ts index 0ef1c56f..ef136794 100644 --- a/frontend/src/api/services/tenant.ts +++ b/frontend/src/api/services/tenant.ts @@ -140,7 +140,9 @@ export class TenantService { // Context Management (Frontend-only operations) setCurrentTenant(tenant: TenantResponse): void { // Set tenant context in API client - apiClient.setTenantId(tenant.id); + if (tenant && tenant.id) { + apiClient.setTenantId(tenant.id); + } } clearCurrentTenant(): void { diff --git a/frontend/src/components/domain/onboarding/OnboardingWizard.tsx b/frontend/src/components/domain/onboarding/OnboardingWizard.tsx index 656f6652..0c3efd67 100644 --- a/frontend/src/components/domain/onboarding/OnboardingWizard.tsx +++ b/frontend/src/components/domain/onboarding/OnboardingWizard.tsx @@ -1,17 +1,18 @@ import React, { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { Button } from '../../ui/Button'; import { Card, CardHeader, CardBody } from '../../ui/Card'; import { useAuth } from '../../../contexts/AuthContext'; import { useUserProgress, useMarkStepCompleted } from '../../../api/hooks/onboarding'; import { useTenantActions } from '../../../stores/tenant.store'; import { useTenantInitializer } from '../../../stores/useTenantInitializer'; -import { +import { RegisterTenantStep, UploadSalesDataStep, MLTrainingStep, CompletionStep } from './steps'; +import { Building2 } from 'lucide-react'; interface StepConfig { id: string; @@ -58,11 +59,26 @@ const STEPS: StepConfig[] = [ ]; export const OnboardingWizard: React.FC = () => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isInitialized, setIsInitialized] = useState(false); + const [searchParams] = useSearchParams(); const navigate = useNavigate(); const { user } = useAuth(); - + + // Check if this is a fresh onboarding (new tenant creation) + const isNewTenant = searchParams.get('new') === 'true'; + + // Initialize state based on whether this is a new tenant or not + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isInitialized, setIsInitialized] = useState(isNewTenant); // If new tenant, consider initialized immediately + + // Debug log for new tenant creation + useEffect(() => { + if (isNewTenant) { + console.log('🆕 New tenant creation detected - UI will reset to step 0'); + console.log('📊 Current step index:', currentStepIndex); + console.log('🎯 Is initialized:', isInitialized); + } + }, [isNewTenant, currentStepIndex, isInitialized]); + // Initialize tenant data for authenticated users useTenantInitializer(); @@ -106,64 +122,77 @@ export const OnboardingWizard: React.FC = () => { // Initialize step index based on backend progress with validation useEffect(() => { + // Skip backend progress loading for new tenant creation + if (isNewTenant) { + return; // Already initialized to step 0 + } + if (userProgress && !isInitialized) { console.log('🔄 Initializing onboarding progress:', userProgress); - + // Check if user_registered step is completed const userRegisteredStep = userProgress.steps.find(s => s.step_name === 'user_registered'); if (!userRegisteredStep?.completed) { console.log('⏳ Waiting for user_registered step to be auto-completed...'); return; // Wait for auto-completion to finish } - - // Find the current step index based on backend progress - const currentStepFromBackend = userProgress.current_step; - let stepIndex = STEPS.findIndex(step => step.id === currentStepFromBackend); - - console.log(`🎯 Backend current step: "${currentStepFromBackend}", found at index: ${stepIndex}`); - - // If current step is not found (e.g., suppliers step), find the next incomplete step - if (stepIndex === -1) { - console.log('🔍 Current step not found in UI steps, finding first incomplete step...'); - - // Find the first incomplete step that user can access - for (let i = 0; i < STEPS.length; i++) { - const step = STEPS[i]; - const stepProgress = userProgress.steps.find(s => s.step_name === step.id); - - if (!stepProgress?.completed) { - stepIndex = i; - console.log(`📍 Found first incomplete step: "${step.id}" at index ${i}`); - break; + + let stepIndex = 0; // Default to first step + + // If this is a new tenant creation, always start from the beginning + if (isNewTenant) { + console.log('🆕 New tenant creation - starting from first step'); + stepIndex = 0; + } else { + // Find the current step index based on backend progress + const currentStepFromBackend = userProgress.current_step; + stepIndex = STEPS.findIndex(step => step.id === currentStepFromBackend); + + console.log(`🎯 Backend current step: "${currentStepFromBackend}", found at index: ${stepIndex}`); + + // If current step is not found (e.g., suppliers step), find the next incomplete step + if (stepIndex === -1) { + console.log('🔍 Current step not found in UI steps, finding first incomplete step...'); + + // Find the first incomplete step that user can access + for (let i = 0; i < STEPS.length; i++) { + const step = STEPS[i]; + const stepProgress = userProgress.steps.find(s => s.step_name === step.id); + + if (!stepProgress?.completed) { + stepIndex = i; + console.log(`📍 Found first incomplete step: "${step.id}" at index ${i}`); + break; + } + } + + // If all visible steps are completed, go to last step + if (stepIndex === -1) { + stepIndex = STEPS.length - 1; + console.log('✅ All steps completed, going to last step'); } } - - // If all visible steps are completed, go to last step - if (stepIndex === -1) { - stepIndex = STEPS.length - 1; - console.log('✅ All steps completed, going to last step'); + + // Ensure user can't skip ahead - find the first incomplete step + const firstIncompleteStepIndex = STEPS.findIndex(step => { + const stepProgress = userProgress.steps.find(s => s.step_name === step.id); + return !stepProgress?.completed; + }); + + if (firstIncompleteStepIndex !== -1 && stepIndex > firstIncompleteStepIndex) { + console.log(`🚫 User trying to skip ahead. Redirecting to first incomplete step at index ${firstIncompleteStepIndex}`); + stepIndex = firstIncompleteStepIndex; } } - - // Ensure user can't skip ahead - find the first incomplete step - const firstIncompleteStepIndex = STEPS.findIndex(step => { - const stepProgress = userProgress.steps.find(s => s.step_name === step.id); - return !stepProgress?.completed; - }); - - if (firstIncompleteStepIndex !== -1 && stepIndex > firstIncompleteStepIndex) { - console.log(`🚫 User trying to skip ahead. Redirecting to first incomplete step at index ${firstIncompleteStepIndex}`); - stepIndex = firstIncompleteStepIndex; - } - + console.log(`🎯 Final step index: ${stepIndex} ("${STEPS[stepIndex]?.id}")`); - + if (stepIndex !== currentStepIndex) { setCurrentStepIndex(stepIndex); } setIsInitialized(true); } - }, [userProgress, isInitialized, currentStepIndex]); + }, [userProgress, isInitialized, currentStepIndex, isNewTenant]); const currentStep = STEPS[currentStepIndex]; @@ -214,7 +243,13 @@ export const OnboardingWizard: React.FC = () => { } if (currentStep.id === 'completion') { - navigate('/app'); + // Navigate to dashboard after completion + if (isNewTenant) { + // For new tenant creation, navigate to dashboard and remove the new param + navigate('/app/dashboard'); + } else { + navigate('/app'); + } } else { // Auto-advance to next step after successful completion if (currentStepIndex < STEPS.length - 1) { @@ -246,8 +281,8 @@ export const OnboardingWizard: React.FC = () => { } }; - // Show loading state while initializing progress - if (isLoadingProgress || !isInitialized) { + // Show loading state while initializing progress (skip for new tenant) + if (!isNewTenant && (isLoadingProgress || !isInitialized)) { return (
@@ -262,8 +297,8 @@ export const OnboardingWizard: React.FC = () => { ); } - // Show error state if progress fails to load - if (progressError) { + // Show error state if progress fails to load (skip for new tenant) + if (!isNewTenant && progressError) { return (
@@ -297,19 +332,47 @@ export const OnboardingWizard: React.FC = () => { } const StepComponent = currentStep.component; - const progressPercentage = userProgress?.completion_percentage || ((currentStepIndex + 1) / STEPS.length) * 100; + + // Calculate progress percentage - reset for new tenant creation + const progressPercentage = isNewTenant + ? ((currentStepIndex + 1) / STEPS.length) * 100 // For new tenant, base progress only on current step + : userProgress?.completion_percentage || ((currentStepIndex + 1) / STEPS.length) * 100; return (
+ {/* New Tenant Info Banner */} + {isNewTenant && ( + + +
+
+ +
+
+

+ Creando Nueva Organización +

+

+ Configurarás una nueva panadería desde cero. Este proceso es independiente de tus organizaciones existentes. +

+
+
+
+
+ )} + {/* Enhanced Progress Header */}

- Bienvenido a Bakery IA + {isNewTenant ? 'Crear Nueva Organización' : 'Bienvenido a Bakery IA'}

- Configura tu sistema de gestión inteligente paso a paso + {isNewTenant + ? 'Configura tu nueva panadería desde cero' + : 'Configura tu sistema de gestión inteligente paso a paso' + }

@@ -318,6 +381,7 @@ export const OnboardingWizard: React.FC = () => {
{Math.round(progressPercentage)}% completado + {isNewTenant && (nuevo)}
@@ -334,7 +398,10 @@ export const OnboardingWizard: React.FC = () => {
{STEPS.map((step, index) => { - const isCompleted = userProgress?.steps.find(s => s.step_name === step.id)?.completed || index < currentStepIndex; + // For new tenant creation, only show completed if index is less than current step + const isCompleted = isNewTenant + ? index < currentStepIndex + : userProgress?.steps.find(s => s.step_name === step.id)?.completed || index < currentStepIndex; const isCurrent = index === currentStepIndex; return ( @@ -377,7 +444,10 @@ export const OnboardingWizard: React.FC = () => { {/* Desktop Step Indicators */}
{STEPS.map((step, index) => { - const isCompleted = userProgress?.steps.find(s => s.step_name === step.id)?.completed || index < currentStepIndex; + // For new tenant creation, only show completed if index is less than current step + const isCompleted = isNewTenant + ? index < currentStepIndex + : userProgress?.steps.find(s => s.step_name === step.id)?.completed || index < currentStepIndex; const isCurrent = index === currentStepIndex; return ( diff --git a/frontend/src/components/layout/Header/Header.tsx b/frontend/src/components/layout/Header/Header.tsx index 15cea68e..ad603509 100644 --- a/frontend/src/components/layout/Header/Header.tsx +++ b/frontend/src/components/layout/Header/Header.tsx @@ -1,23 +1,20 @@ import React, { useState, useCallback, forwardRef } from 'react'; import { clsx } from 'clsx'; import { useNavigate } from 'react-router-dom'; -import { useAuthUser, useIsAuthenticated, useAuthActions } from '../../../stores'; +import { useTranslation } from 'react-i18next'; +import { useAuthUser, useIsAuthenticated } from '../../../stores'; import { useTheme } from '../../../contexts/ThemeContext'; import { useNotifications } from '../../../hooks/useNotifications'; import { Button } from '../../ui'; -import { Avatar } from '../../ui'; import { Badge } from '../../ui'; -import { Modal } from '../../ui'; import { TenantSwitcher } from '../../ui/TenantSwitcher'; import { ThemeToggle } from '../../ui/ThemeToggle'; import { NotificationPanel } from '../../ui/NotificationPanel/NotificationPanel'; +import { CompactLanguageSelector } from '../../ui/LanguageSelector'; import { Menu, Search, Bell, - Settings, - User, - LogOut, X } from 'lucide-react'; @@ -43,10 +40,6 @@ export interface HeaderProps { * Show/hide theme toggle */ showThemeToggle?: boolean; - /** - * Show/hide user menu - */ - showUserMenu?: boolean; /** * Custom logo component */ @@ -67,17 +60,14 @@ export interface HeaderProps { export interface HeaderRef { focusSearch: () => void; - toggleUserMenu: () => void; - closeUserMenu: () => void; } /** - * Header - Top navigation header with logo, user menu, notifications, theme toggle - * + * Header - Top navigation header with logo, notifications, theme toggle + * * Features: * - Logo/brand area with responsive sizing * - Global search functionality with keyboard shortcuts - * - User avatar with dropdown menu * - Notifications bell with badge count * - Theme toggle button (light/dark/system) * - Mobile hamburger menu integration @@ -90,16 +80,15 @@ export const Header = forwardRef(({ showSearch = true, showNotifications = true, showThemeToggle = true, - showUserMenu = true, logo, - searchPlaceholder = 'Buscar...', + searchPlaceholder, notificationCount = 0, onNotificationClick, }, ref) => { + const { t } = useTranslation(); const navigate = useNavigate(); const user = useAuthUser(); const isAuthenticated = useIsAuthenticated(); - const { logout } = useAuthActions(); const { theme, resolvedTheme, setTheme } = useTheme(); const { notifications, @@ -111,34 +100,22 @@ export const Header = forwardRef(({ clearAll } = useNotifications(); - const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); const [isSearchFocused, setIsSearchFocused] = useState(false); const [searchValue, setSearchValue] = useState(''); const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false); const searchInputRef = React.useRef(null); + const defaultSearchPlaceholder = searchPlaceholder || t('common:forms.search_placeholder', 'Search...'); // Focus search input const focusSearch = useCallback(() => { searchInputRef.current?.focus(); }, []); - // Toggle user menu - const toggleUserMenu = useCallback(() => { - setIsUserMenuOpen(prev => !prev); - }, []); - - // Close user menu - const closeUserMenu = useCallback(() => { - setIsUserMenuOpen(false); - }, []); - // Expose ref methods React.useImperativeHandle(ref, () => ({ focusSearch, - toggleUserMenu, - closeUserMenu, - }), [focusSearch, toggleUserMenu, closeUserMenu]); + }), [focusSearch]); // Handle search const handleSearchChange = useCallback((e: React.ChangeEvent) => { @@ -158,11 +135,6 @@ export const Header = forwardRef(({ searchInputRef.current?.focus(); }, []); - // Handle logout - const handleLogout = useCallback(async () => { - await logout(); - setIsUserMenuOpen(false); - }, [logout]); // Keyboard shortcuts React.useEffect(() => { @@ -175,7 +147,6 @@ export const Header = forwardRef(({ // Escape to close menus if (e.key === 'Escape') { - setIsUserMenuOpen(false); setIsNotificationPanelOpen(false); if (isSearchFocused) { searchInputRef.current?.blur(); @@ -191,9 +162,6 @@ export const Header = forwardRef(({ React.useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Element; - if (!target.closest('[data-user-menu]')) { - setIsUserMenuOpen(false); - } if (!target.closest('[data-notification-panel]')) { setIsNotificationPanelOpen(false); } @@ -287,7 +255,7 @@ export const Header = forwardRef(({ onChange={handleSearchChange} onFocus={() => setIsSearchFocused(true)} onBlur={() => setIsSearchFocused(false)} - placeholder={searchPlaceholder} + placeholder={defaultSearchPlaceholder} className={clsx( 'w-full pl-10 pr-12 py-2.5 text-sm', 'bg-[var(--bg-secondary)] border border-[var(--border-primary)]', @@ -297,14 +265,14 @@ export const Header = forwardRef(({ 'placeholder:text-[var(--text-tertiary)]', 'h-9' )} - aria-label="Buscar en la aplicación" + aria-label={t('common:accessibility.search', 'Search in the application')} /> {searchValue ? ( @@ -330,12 +298,15 @@ export const Header = forwardRef(({ size="sm" onClick={focusSearch} className="md:hidden w-10 h-10 p-0 flex items-center justify-center" - aria-label="Buscar" + aria-label={t('common:actions.search', 'Search')} > )} + {/* Language selector */} + + {/* Theme toggle */} {showThemeToggle && ( @@ -353,8 +324,8 @@ export const Header = forwardRef(({ !isConnected && "opacity-50", isNotificationPanelOpen && "bg-[var(--bg-secondary)]" )} - aria-label={`Notificaciones${unreadCount > 0 ? ` (${unreadCount})` : ''}${!isConnected ? ' - Desconectado' : ''}`} - title={!isConnected ? 'Sin conexión en tiempo real' : undefined} + aria-label={`${t('common:navigation.notifications', 'Notifications')}${unreadCount > 0 ? ` (${unreadCount})` : ''}${!isConnected ? ` - ${t('common:status.disconnected', 'Disconnected')}` : ''}`} + title={!isConnected ? t('common:status.no_realtime_connection', 'No real-time connection') : undefined} aria-expanded={isNotificationPanelOpen} aria-haspopup="true" > @@ -385,93 +356,6 @@ export const Header = forwardRef(({
)} - {/* User menu */} - {showUserMenu && user && ( -
- - - {isUserMenuOpen && ( -
- {/* User info */} -
- -
-
- {user.email} -
-
-
- - {/* Menu items */} -
- - -
- - {/* Logout */} -
- -
-
- )} -
- )}
)} diff --git a/frontend/src/components/layout/Sidebar/Sidebar.tsx b/frontend/src/components/layout/Sidebar/Sidebar.tsx index 71755622..a0cc35f5 100644 --- a/frontend/src/components/layout/Sidebar/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar/Sidebar.tsx @@ -1,13 +1,15 @@ import React, { useState, useCallback, forwardRef, useMemo } from 'react'; import { clsx } from 'clsx'; import { useLocation, useNavigate } from 'react-router-dom'; -import { useAuthUser, useIsAuthenticated } from '../../../stores'; +import { useTranslation } from 'react-i18next'; +import { useAuthUser, useIsAuthenticated, useAuthActions } from '../../../stores'; import { useCurrentTenantAccess } from '../../../stores/tenant.store'; import { getNavigationRoutes, canAccessRoute, ROUTES } from '../../../router/routes.config'; import { useSubscriptionAwareRoutes } from '../../../hooks/useSubscriptionAwareRoutes'; import { Button } from '../../ui'; import { Badge } from '../../ui'; import { Tooltip } from '../../ui'; +import { Avatar } from '../../ui'; import { LayoutDashboard, Package, @@ -28,7 +30,10 @@ import { ChevronRight, ChevronDown, Dot, - Menu + Menu, + LogOut, + MoreHorizontal, + X } from 'lucide-react'; export interface SidebarProps { @@ -127,36 +132,71 @@ export const Sidebar = forwardRef(({ showCollapseButton = true, showFooter = true, }, ref) => { + const { t } = useTranslation(); const location = useLocation(); const navigate = useNavigate(); const user = useAuthUser(); const isAuthenticated = useIsAuthenticated(); const currentTenantAccess = useCurrentTenantAccess(); + const { logout } = useAuthActions(); const [expandedItems, setExpandedItems] = useState>(new Set()); const [hoveredItem, setHoveredItem] = useState(null); + const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); const sidebarRef = React.useRef(null); // Get subscription-aware navigation routes const baseNavigationRoutes = useMemo(() => getNavigationRoutes(), []); const { filteredRoutes: subscriptionFilteredRoutes } = useSubscriptionAwareRoutes(baseNavigationRoutes); + // Map route paths to translation keys + const getTranslationKey = (routePath: string): string => { + const pathMappings: Record = { + '/app/dashboard': 'navigation.dashboard', + '/app/operations': 'navigation.operations', + '/app/operations/procurement': 'navigation.procurement', + '/app/operations/production': 'navigation.production', + '/app/operations/pos': 'navigation.pos', + '/app/bakery': 'navigation.bakery', + '/app/bakery/recipes': 'navigation.recipes', + '/app/database': 'navigation.data', + '/app/database/inventory': 'navigation.inventory', + '/app/analytics': 'navigation.analytics', + '/app/analytics/forecasting': 'navigation.forecasting', + '/app/analytics/sales': 'navigation.sales', + '/app/analytics/performance': 'navigation.performance', + '/app/ai': 'navigation.insights', + '/app/communications': 'navigation.communications', + '/app/communications/notifications': 'navigation.notifications', + '/app/communications/alerts': 'navigation.alerts', + }; + + return pathMappings[routePath] || routePath; + }; + // Convert routes to navigation items - memoized const navigationItems = useMemo(() => { const convertRoutesToItems = (routes: typeof subscriptionFilteredRoutes): NavigationItem[] => { - return routes.map(route => ({ - id: route.path, - label: route.title, - path: route.path, - icon: route.icon ? iconMap[route.icon] : undefined, - requiredPermissions: route.requiredPermissions, - requiredRoles: route.requiredRoles, - children: route.children ? convertRoutesToItems(route.children) : undefined, - })); + return routes.map(route => { + const translationKey = getTranslationKey(route.path); + const label = translationKey.startsWith('navigation.') + ? t(`common:${translationKey}`, route.title) + : route.title; + + return { + id: route.path, + label, + path: route.path, + icon: route.icon ? iconMap[route.icon] : undefined, + requiredPermissions: route.requiredPermissions, + requiredRoles: route.requiredRoles, + children: route.children ? convertRoutesToItems(route.children) : undefined, + }; + }); }; return customItems || convertRoutesToItems(subscriptionFilteredRoutes); - }, [customItems, subscriptionFilteredRoutes]); + }, [customItems, subscriptionFilteredRoutes, t]); // Filter items based on user permissions - memoized to prevent infinite re-renders const visibleItems = useMemo(() => { @@ -219,6 +259,17 @@ export const Sidebar = forwardRef(({ } }, [navigate, onClose]); + // Handle logout + const handleLogout = useCallback(async () => { + await logout(); + setIsProfileMenuOpen(false); + }, [logout]); + + // Handle profile menu toggle + const handleProfileMenuToggle = useCallback(() => { + setIsProfileMenuOpen(prev => !prev); + }, []); + // Scroll to item const scrollToItem = useCallback((path: string) => { const element = sidebarRef.current?.querySelector(`[data-path="${path}"]`); @@ -277,8 +328,12 @@ export const Sidebar = forwardRef(({ // Handle keyboard navigation and touch gestures React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape' && isOpen && onClose) { - onClose(); + if (e.key === 'Escape') { + if (isProfileMenuOpen) { + setIsProfileMenuOpen(false); + } else if (isOpen && onClose) { + onClose(); + } } }; @@ -310,13 +365,28 @@ export const Sidebar = forwardRef(({ document.addEventListener('keydown', handleKeyDown); document.addEventListener('touchstart', handleTouchStart, { passive: true }); document.addEventListener('touchmove', handleTouchMove, { passive: true }); - + return () => { document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('touchstart', handleTouchStart); document.removeEventListener('touchmove', handleTouchMove); }; - }, [isOpen, onClose]); + }, [isOpen, onClose, isProfileMenuOpen]); + + // Handle click outside profile menu + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + if (!target.closest('[data-profile-menu]')) { + setIsProfileMenuOpen(false); + } + }; + + if (isProfileMenuOpen) { + document.addEventListener('click', handleClickOutside); + return () => document.removeEventListener('click', handleClickOutside); + } + }, [isProfileMenuOpen]); // Render submenu overlay for collapsed sidebar const renderSubmenuOverlay = (item: NavigationItem) => { @@ -383,14 +453,14 @@ export const Sidebar = forwardRef(({ >
{ItemIcon && ( - )} @@ -466,7 +536,7 @@ export const Sidebar = forwardRef(({ isActive && 'bg-[var(--color-primary)]/10 border-l-2 border-[var(--color-primary)]', !isActive && 'hover:bg-[var(--bg-secondary)]', item.disabled && 'opacity-50 cursor-not-allowed', - isCollapsed && !hasChildren ? 'flex justify-center items-center p-2 mx-1' : 'p-3' + isCollapsed && level === 0 ? 'flex justify-center items-center p-3 aspect-square' : 'p-3' )} aria-expanded={hasChildren ? isExpanded : undefined} aria-current={isActive ? 'page' : undefined} @@ -524,15 +594,99 @@ export const Sidebar = forwardRef(({ isCollapsed ? 'w-[var(--sidebar-collapsed-width)]' : 'w-[var(--sidebar-width)]', className )} - aria-label="Navegación principal" + aria-label={t('common:accessibility.menu', 'Main navigation')} > {/* Navigation */} - - {/* Footer */} - {showFooter && ( -
-
- Panadería IA v2.0.0 + {/* Profile section - Mobile */} + {user && ( +
+
+ + + {/* Profile menu dropdown - Mobile */} + {isProfileMenuOpen && ( +
+
+ + +
+ +
+ +
+
+ )}
)} + ); diff --git a/frontend/src/components/ui/LanguageSelector.tsx b/frontend/src/components/ui/LanguageSelector.tsx new file mode 100644 index 00000000..6c5d9a0c --- /dev/null +++ b/frontend/src/components/ui/LanguageSelector.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { + languageConfig, + supportedLanguages, + type SupportedLanguage +} from '../../locales'; +import { useLanguageSwitcher } from '../../hooks/useLanguageSwitcher'; +import { Select } from './Select'; + +interface LanguageSelectorProps { + className?: string; + variant?: 'compact' | 'full'; +} + +export function LanguageSelector({ + className, + variant = 'full' +}: LanguageSelectorProps) { + const { currentLanguage, changeLanguage, isChanging } = useLanguageSwitcher(); + + const languageOptions = supportedLanguages.map(lang => ({ + value: lang, + label: variant === 'compact' + ? `${languageConfig[lang].flag} ${languageConfig[lang].code.toUpperCase()}` + : `${languageConfig[lang].flag} ${languageConfig[lang].nativeName}`, + })); + + const handleLanguageChange = (value: string | number | Array) => { + if (typeof value === 'string') { + const newLanguage = value as SupportedLanguage; + changeLanguage(newLanguage); + } + }; + + return ( +