+
+
+ {isOpen && (
+ <>
+ {/* Backdrop */}
+
setIsOpen(false)}
+ />
+
+ {/* Dropdown */}
+
+
+
+
+ {tenants.map((tenant) => (
+
+ ))}
+
+
+
+
+
+
+ >
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts
new file mode 100644
index 00000000..c545d577
--- /dev/null
+++ b/frontend/src/hooks/useAuth.ts
@@ -0,0 +1,64 @@
+import { useCallback } from 'react';
+import { useDispatch } from 'react-redux';
+import { loginSuccess, logout } from '../store/slices/authSlice';
+import { setCurrentTenant } from '../store/slices/tenantSlice';
+
+interface User {
+ id: string;
+ email: string;
+ fullName: string;
+ role: string;
+ isOnboardingComplete: boolean;
+ tenant_id?: string;
+}
+
+export const useAuth = () => {
+ const dispatch = useDispatch();
+
+ const initializeAuth = useCallback(async () => {
+ try {
+ // Check for stored auth token
+ const token = localStorage.getItem('auth_token');
+ const userData = localStorage.getItem('user_data');
+ const selectedTenantId = localStorage.getItem('selectedTenantId');
+
+ if (token && userData) {
+ const user: User = JSON.parse(userData);
+
+ // Set user in auth state
+ dispatch(loginSuccess({ user, token }));
+
+ // If there's a selected tenant, try to load it
+ if (selectedTenantId) {
+ // This would normally fetch tenant data from API
+ // For now, we'll just set a placeholder
+ const tenantData = {
+ id: selectedTenantId,
+ name: 'Mi Panadería',
+ business_type: 'individual',
+ address: 'Dirección de ejemplo'
+ };
+ dispatch(setCurrentTenant(tenantData));
+ }
+ }
+ } catch (error) {
+ console.error('Failed to initialize auth:', error);
+ // Clear invalid tokens
+ localStorage.removeItem('auth_token');
+ localStorage.removeItem('user_data');
+ localStorage.removeItem('selectedTenantId');
+ }
+ }, [dispatch]);
+
+ const handleLogout = useCallback(() => {
+ dispatch(logout());
+ localStorage.removeItem('auth_token');
+ localStorage.removeItem('user_data');
+ localStorage.removeItem('selectedTenantId');
+ }, [dispatch]);
+
+ return {
+ initializeAuth,
+ handleLogout
+ };
+};
\ No newline at end of file
diff --git a/frontend/src/hooks/useBakeryType.ts b/frontend/src/hooks/useBakeryType.ts
new file mode 100644
index 00000000..522d7f5d
--- /dev/null
+++ b/frontend/src/hooks/useBakeryType.ts
@@ -0,0 +1,56 @@
+import { useSelector } from 'react-redux';
+import { RootState } from '../store';
+
+export type BakeryType = 'individual' | 'central_workshop';
+
+interface BakeryTypeConfig {
+ bakeryType: BakeryType;
+ isIndividual: boolean;
+ isCentral: boolean;
+ getLabel: () => string;
+ getDescription: () => string;
+ getInventoryLabel: () => string;
+ getProductionLabel: () => string;
+ getSupplierLabel: () => string;
+}
+
+export const useBakeryType = (): BakeryTypeConfig => {
+ const { currentTenant } = useSelector((state: RootState) => state.tenant);
+
+ const bakeryType: BakeryType = currentTenant?.business_type || 'individual';
+ const isIndividual = bakeryType === 'individual';
+ const isCentral = bakeryType === 'central_workshop';
+
+ const getLabel = (): string => {
+ return isIndividual ? 'Panadería Individual' : 'Obrador Central';
+ };
+
+ const getDescription = (): string => {
+ return isIndividual
+ ? 'Panadería con producción in-situ usando ingredientes frescos'
+ : 'Obrador central que distribuye productos semi-terminados o terminados';
+ };
+
+ const getInventoryLabel = (): string => {
+ return isIndividual ? 'Ingredientes' : 'Productos';
+ };
+
+ const getProductionLabel = (): string => {
+ return isIndividual ? 'Producción' : 'Distribución';
+ };
+
+ const getSupplierLabel = (): string => {
+ return isIndividual ? 'Proveedores de Ingredientes' : 'Proveedores de Productos';
+ };
+
+ return {
+ bakeryType,
+ isIndividual,
+ isCentral,
+ getLabel,
+ getDescription,
+ getInventoryLabel,
+ getProductionLabel,
+ getSupplierLabel
+ };
+};
\ No newline at end of file
diff --git a/frontend/src/hooks/usePermissions.ts b/frontend/src/hooks/usePermissions.ts
new file mode 100644
index 00000000..d35ede7e
--- /dev/null
+++ b/frontend/src/hooks/usePermissions.ts
@@ -0,0 +1,112 @@
+import { useSelector } from 'react-redux';
+import { RootState } from '../store';
+
+export type UserRole = 'owner' | 'admin' | 'manager' | 'worker';
+
+interface Permission {
+ action: string;
+ resource: string;
+}
+
+interface PermissionsConfig {
+ userRole: UserRole;
+ hasRole: (role: UserRole | UserRole[]) => boolean;
+ hasPermission: (permission: string) => boolean;
+ canManageUsers: boolean;
+ canManageTenants: boolean;
+ canViewAnalytics: boolean;
+ canEditRecipes: boolean;
+ canViewFinancials: boolean;
+ canManageSettings: boolean;
+}
+
+// Define role hierarchy (higher index = more permissions)
+const ROLE_HIERARCHY: UserRole[] = ['worker', 'manager', 'admin', 'owner'];
+
+// Define permissions for each role
+const ROLE_PERMISSIONS: Record
= {
+ worker: [
+ 'view:inventory',
+ 'view:production',
+ 'view:orders',
+ 'update:production_status',
+ 'view:recipes_basic'
+ ],
+ manager: [
+ 'view:inventory',
+ 'view:production',
+ 'view:orders',
+ 'view:sales',
+ 'update:production_status',
+ 'update:inventory',
+ 'create:orders',
+ 'view:recipes_basic',
+ 'view:analytics_basic',
+ 'view:reports_operational'
+ ],
+ admin: [
+ 'view:inventory',
+ 'view:production',
+ 'view:orders',
+ 'view:sales',
+ 'view:analytics',
+ 'view:financials',
+ 'update:production_status',
+ 'update:inventory',
+ 'create:orders',
+ 'manage:recipes',
+ 'manage:users',
+ 'view:reports_all',
+ 'manage:settings_tenant'
+ ],
+ owner: [
+ '*' // All permissions
+ ]
+};
+
+export const usePermissions = (): PermissionsConfig => {
+ const { user } = useSelector((state: RootState) => state.auth);
+
+ const userRole: UserRole = (user?.role as UserRole) || 'worker';
+
+ const hasRole = (role: UserRole | UserRole[]): boolean => {
+ if (Array.isArray(role)) {
+ return role.includes(userRole);
+ }
+
+ const userRoleIndex = ROLE_HIERARCHY.indexOf(userRole);
+ const requiredRoleIndex = ROLE_HIERARCHY.indexOf(role);
+
+ return userRoleIndex >= requiredRoleIndex;
+ };
+
+ const hasPermission = (permission: string): boolean => {
+ const userPermissions = ROLE_PERMISSIONS[userRole] || [];
+
+ // Owner has all permissions
+ if (userPermissions.includes('*')) {
+ return true;
+ }
+
+ return userPermissions.includes(permission);
+ };
+
+ const canManageUsers = hasPermission('manage:users');
+ const canManageTenants = hasRole(['admin', 'owner']);
+ const canViewAnalytics = hasPermission('view:analytics') || hasPermission('view:analytics_basic');
+ const canEditRecipes = hasPermission('manage:recipes');
+ const canViewFinancials = hasPermission('view:financials');
+ const canManageSettings = hasRole(['admin', 'owner']);
+
+ return {
+ userRole,
+ hasRole,
+ hasPermission,
+ canManageUsers,
+ canManageTenants,
+ canViewAnalytics,
+ canEditRecipes,
+ canViewFinancials,
+ canManageSettings
+ };
+};
\ No newline at end of file
diff --git a/frontend/src/pages/analytics/AIInsightsPage.tsx b/frontend/src/pages/analytics/AIInsightsPage.tsx
new file mode 100644
index 00000000..0f77a732
--- /dev/null
+++ b/frontend/src/pages/analytics/AIInsightsPage.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+const AIInsightsPage: React.FC = () => {
+ return (
+
+
+
Insights de IA
+
+
+
+
Insights de IA en desarrollo
+
+
+ );
+};
+
+export default AIInsightsPage;
\ No newline at end of file
diff --git a/frontend/src/pages/analytics/FinancialReportsPage.tsx b/frontend/src/pages/analytics/FinancialReportsPage.tsx
new file mode 100644
index 00000000..345eeefb
--- /dev/null
+++ b/frontend/src/pages/analytics/FinancialReportsPage.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+const FinancialReportsPage: React.FC = () => {
+ return (
+
+
+
Reportes Financieros
+
+
+
+
Reportes financieros en desarrollo
+
+
+ );
+};
+
+export default FinancialReportsPage;
\ No newline at end of file
diff --git a/frontend/src/pages/analytics/PerformanceKPIsPage.tsx b/frontend/src/pages/analytics/PerformanceKPIsPage.tsx
new file mode 100644
index 00000000..5cc85456
--- /dev/null
+++ b/frontend/src/pages/analytics/PerformanceKPIsPage.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+const PerformanceKPIsPage: React.FC = () => {
+ return (
+
+
+
KPIs de Rendimiento
+
+
+
+
KPIs de rendimiento en desarrollo
+
+
+ );
+};
+
+export default PerformanceKPIsPage;
\ No newline at end of file
diff --git a/frontend/src/pages/analytics/ProductionReportsPage.tsx b/frontend/src/pages/analytics/ProductionReportsPage.tsx
new file mode 100644
index 00000000..374c8686
--- /dev/null
+++ b/frontend/src/pages/analytics/ProductionReportsPage.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { useBakeryType } from '../../hooks/useBakeryType';
+
+const ProductionReportsPage: React.FC = () => {
+ const { getProductionLabel } = useBakeryType();
+
+ return (
+
+
+
Reportes de {getProductionLabel()}
+
+
+
+
Reportes de {getProductionLabel().toLowerCase()} en desarrollo
+
+
+ );
+};
+
+export default ProductionReportsPage;
\ No newline at end of file
diff --git a/frontend/src/pages/analytics/SalesAnalyticsPage.tsx b/frontend/src/pages/analytics/SalesAnalyticsPage.tsx
new file mode 100644
index 00000000..b292fd58
--- /dev/null
+++ b/frontend/src/pages/analytics/SalesAnalyticsPage.tsx
@@ -0,0 +1,120 @@
+import React, { useState } from 'react';
+import { TrendingUp, DollarSign, ShoppingCart, Calendar } from 'lucide-react';
+import { useBakeryType } from '../../hooks/useBakeryType';
+
+const SalesAnalyticsPage: React.FC = () => {
+ const { isIndividual, isCentral } = useBakeryType();
+ const [timeRange, setTimeRange] = useState('week');
+
+ return (
+
+
+
Análisis de Ventas
+
+ {isIndividual
+ ? 'Analiza el rendimiento de ventas de tu panadería'
+ : 'Analiza el rendimiento de ventas de todos tus puntos de venta'
+ }
+
+
+
+ {/* Time Range Selector */}
+
+
+ {['day', 'week', 'month', 'quarter'].map((range) => (
+
+ ))}
+
+
+
+ {/* KPI Cards */}
+
+
+
+
+
+
+
+
Ingresos Totales
+
€2,847
+
+
+
+
+
+
+
+
+
+
+
+ {isIndividual ? 'Productos Vendidos' : 'Productos Distribuidos'}
+
+
1,429
+
+
+
+
+
+
+
+
+
+ {/* Charts placeholder */}
+
+
+
+ Tendencia de Ventas
+
+
+
Gráfico de tendencias aquí
+
+
+
+
+
+ {isIndividual ? 'Productos Más Vendidos' : 'Productos Más Distribuidos'}
+
+
+
Gráfico de productos aquí
+
+
+
+
+ );
+};
+
+export default SalesAnalyticsPage;
\ No newline at end of file
diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx
index 47c68265..0982e802 100644
--- a/frontend/src/pages/auth/LoginPage.tsx
+++ b/frontend/src/pages/auth/LoginPage.tsx
@@ -1,6 +1,9 @@
import React, { useState } from 'react';
+import { Link, useNavigate, useLocation } from 'react-router-dom';
+import { useDispatch } from 'react-redux';
import { Eye, EyeOff, Loader2 } from 'lucide-react';
import toast from 'react-hot-toast';
+import { loginSuccess } from '../../store/slices/authSlice';
import {
useAuth,
@@ -8,8 +11,7 @@ import {
} from '../../api';
interface LoginPageProps {
- onLogin: (user: any, token: string) => void;
- onNavigateToRegister: () => void;
+ // No props needed with React Router
}
interface LoginForm {
@@ -17,11 +19,15 @@ interface LoginForm {
password: string;
}
-const LoginPage: React.FC = ({ onLogin, onNavigateToRegister }) => {
-
-
+const LoginPage: React.FC = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const dispatch = useDispatch();
const { login, isLoading, isAuthenticated } = useAuth();
+ // Get the intended destination from state, default to app
+ const from = (location.state as any)?.from?.pathname || '/app';
+
const [formData, setFormData] = useState({
email: '',
password: ''
@@ -70,7 +76,13 @@ const LoginPage: React.FC = ({ onLogin, onNavigateToRegister })
const token = localStorage.getItem('auth_token');
if (userData && token) {
- onLogin(JSON.parse(userData), token);
+ const user = JSON.parse(userData);
+
+ // Set auth state
+ dispatch(loginSuccess({ user, token }));
+
+ // Navigate to intended destination
+ navigate(from, { replace: true });
}
} catch (error: any) {
@@ -245,12 +257,12 @@ const LoginPage: React.FC = ({ onLogin, onNavigateToRegister })
¿No tienes una cuenta?{' '}
-
+
diff --git a/frontend/src/pages/auth/SimpleRegisterPage.tsx b/frontend/src/pages/auth/SimpleRegisterPage.tsx
new file mode 100644
index 00000000..802ee9ce
--- /dev/null
+++ b/frontend/src/pages/auth/SimpleRegisterPage.tsx
@@ -0,0 +1,421 @@
+import React, { useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import { useDispatch } from 'react-redux';
+import { Eye, EyeOff, Loader2, User, Mail, Lock } from 'lucide-react';
+import toast from 'react-hot-toast';
+import { loginSuccess } from '../../store/slices/authSlice';
+import { authService } from '../../api/services/auth.service';
+import { onboardingService } from '../../api/services/onboarding.service';
+import type { RegisterRequest } from '../../api/types/auth';
+
+interface RegisterForm {
+ fullName: string;
+ email: string;
+ password: string;
+ confirmPassword: string;
+ acceptTerms: boolean;
+}
+
+const RegisterPage: React.FC = () => {
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+
+ const [formData, setFormData] = useState