Improve frontend 5
This commit is contained in:
@@ -8,6 +8,7 @@ import { LoadingSpinner } from './components/shared/LoadingSpinner';
|
|||||||
import { AppRouter } from './router/AppRouter';
|
import { AppRouter } from './router/AppRouter';
|
||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
import { AuthProvider } from './contexts/AuthContext';
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
|
import { BakeryProvider } from './contexts/BakeryContext';
|
||||||
import { SSEProvider } from './contexts/SSEContext';
|
import { SSEProvider } from './contexts/SSEContext';
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@@ -28,21 +29,23 @@ function App() {
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<SSEProvider>
|
<BakeryProvider>
|
||||||
<Suspense fallback={<LoadingSpinner overlay />}>
|
<SSEProvider>
|
||||||
<AppRouter />
|
<Suspense fallback={<LoadingSpinner overlay />}>
|
||||||
<Toaster
|
<AppRouter />
|
||||||
position="top-right"
|
<Toaster
|
||||||
toastOptions={{
|
position="top-right"
|
||||||
duration: 4000,
|
toastOptions={{
|
||||||
style: {
|
duration: 4000,
|
||||||
background: '#363636',
|
style: {
|
||||||
color: '#fff',
|
background: '#363636',
|
||||||
},
|
color: '#fff',
|
||||||
}}
|
},
|
||||||
/>
|
}}
|
||||||
</Suspense>
|
/>
|
||||||
</SSEProvider>
|
</Suspense>
|
||||||
|
</SSEProvider>
|
||||||
|
</BakeryProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { Button, Input, Card } from '../../ui';
|
import { Button, Input, Card } from '../../ui';
|
||||||
import { useAuth } from '../../../hooks/api/useAuth';
|
import { useAuthActions, useAuthLoading, useAuthError } from '../../../stores/auth.store';
|
||||||
import { UserLogin } from '../../../types/auth.types';
|
|
||||||
import { useToast } from '../../../hooks/ui/useToast';
|
import { useToast } from '../../../hooks/ui/useToast';
|
||||||
|
|
||||||
interface LoginFormProps {
|
interface LoginFormProps {
|
||||||
@@ -12,7 +11,9 @@ interface LoginFormProps {
|
|||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExtendedUserLogin extends UserLogin {
|
interface LoginCredentials {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
remember_me: boolean;
|
remember_me: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,16 +24,18 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
|||||||
className,
|
className,
|
||||||
autoFocus = true
|
autoFocus = true
|
||||||
}) => {
|
}) => {
|
||||||
const [credentials, setCredentials] = useState<ExtendedUserLogin>({
|
const [credentials, setCredentials] = useState<LoginCredentials>({
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
remember_me: false
|
remember_me: false
|
||||||
});
|
});
|
||||||
const [errors, setErrors] = useState<Partial<ExtendedUserLogin>>({});
|
const [errors, setErrors] = useState<Partial<LoginCredentials>>({});
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const emailInputRef = useRef<HTMLInputElement>(null);
|
const emailInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const { login, isLoading, error } = useAuth();
|
const { login } = useAuthActions();
|
||||||
|
const isLoading = useAuthLoading();
|
||||||
|
const error = useAuthError();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
|
||||||
// Auto-focus on email field when component mounts
|
// Auto-focus on email field when component mounts
|
||||||
@@ -43,7 +46,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
|||||||
}, [autoFocus]);
|
}, [autoFocus]);
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: Partial<ExtendedUserLogin> = {};
|
const newErrors: Partial<LoginCredentials> = {};
|
||||||
|
|
||||||
if (!credentials.email.trim()) {
|
if (!credentials.email.trim()) {
|
||||||
newErrors.email = 'El email es requerido';
|
newErrors.email = 'El email es requerido';
|
||||||
@@ -74,43 +77,39 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loginData: UserLogin = {
|
await login(credentials.email, credentials.password);
|
||||||
email: credentials.email,
|
showToast({
|
||||||
password: credentials.password,
|
type: 'success',
|
||||||
remember_me: credentials.remember_me
|
title: 'Sesión iniciada correctamente',
|
||||||
};
|
message: '¡Bienvenido de vuelta a tu panadería!'
|
||||||
|
});
|
||||||
const success = await login(loginData);
|
onSuccess?.();
|
||||||
if (success) {
|
|
||||||
showToast({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Sesión iniciada correctamente',
|
|
||||||
message: '¡Bienvenido de vuelta a tu panadería!'
|
|
||||||
});
|
|
||||||
onSuccess?.();
|
|
||||||
} else {
|
|
||||||
showToast({
|
|
||||||
type: 'error',
|
|
||||||
title: 'Error al iniciar sesión',
|
|
||||||
message: error || 'Email o contraseña incorrectos. Verifica tus credenciales.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast({
|
showToast({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Error de conexión',
|
title: 'Error al iniciar sesión',
|
||||||
message: 'No se pudo conectar con el servidor. Verifica tu conexión a internet.'
|
message: error || 'Email o contraseña incorrectos. Verifica tus credenciales.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputChange = (field: keyof ExtendedUserLogin) => (value: string | boolean) => {
|
const handleInputChange = (field: keyof LoginCredentials) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
||||||
setCredentials(prev => ({ ...prev, [field]: value }));
|
setCredentials(prev => ({ ...prev, [field]: value }));
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors(prev => ({ ...prev, [field]: undefined }));
|
setErrors(prev => ({ ...prev, [field]: undefined }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDemoLogin = () => {
|
||||||
|
setCredentials({
|
||||||
|
email: 'admin@bakery.com',
|
||||||
|
password: 'admin12345',
|
||||||
|
remember_me: false
|
||||||
|
});
|
||||||
|
setErrors({});
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !isLoading) {
|
if (e.key === 'Enter' && !isLoading) {
|
||||||
handleSubmit(e as any);
|
handleSubmit(e as any);
|
||||||
@@ -235,7 +234,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={credentials.remember_me}
|
checked={credentials.remember_me}
|
||||||
onChange={(e) => handleInputChange('remember_me')(e.target.checked)}
|
onChange={handleInputChange('remember_me')}
|
||||||
className="rounded border-border-primary text-color-primary focus:ring-color-primary focus:ring-offset-0 h-4 w-4"
|
className="rounded border-border-primary text-color-primary focus:ring-color-primary focus:ring-offset-0 h-4 w-4"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
aria-describedby="remember-me-description"
|
aria-describedby="remember-me-description"
|
||||||
@@ -295,6 +294,30 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
|||||||
<div id="login-button-description" className="sr-only">
|
<div id="login-button-description" className="sr-only">
|
||||||
Presiona Enter o haz clic para iniciar sesión con tus credenciales
|
Presiona Enter o haz clic para iniciar sesión con tus credenciales
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Demo Login Section */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-border-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="px-2 bg-background-primary text-text-tertiary">Demo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleDemoLogin}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
Usar credenciales de demostración
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{onRegisterClick && (
|
{onRegisterClick && (
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle password change to update strength
|
// Handle password change to update strength
|
||||||
const handlePasswordChange = (value: string) => {
|
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
setPassword(value);
|
setPassword(value);
|
||||||
setPasswordStrength(calculatePasswordStrength(value));
|
setPasswordStrength(calculatePasswordStrength(value));
|
||||||
clearError('password');
|
clearError('password');
|
||||||
@@ -361,8 +362,8 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
|
|||||||
label="Correo Electrónico"
|
label="Correo Electrónico"
|
||||||
placeholder="tu.email@panaderia.com"
|
placeholder="tu.email@panaderia.com"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(value) => {
|
onChange={(e) => {
|
||||||
setEmail(value);
|
setEmail(e.target.value);
|
||||||
clearError('email');
|
clearError('email');
|
||||||
}}
|
}}
|
||||||
error={errors.email}
|
error={errors.email}
|
||||||
@@ -486,8 +487,8 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
|
|||||||
label="Confirmar Nueva Contraseña"
|
label="Confirmar Nueva Contraseña"
|
||||||
placeholder="Repite tu nueva contraseña"
|
placeholder="Repite tu nueva contraseña"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(value) => {
|
onChange={(e) => {
|
||||||
setConfirmPassword(value);
|
setConfirmPassword(e.target.value);
|
||||||
clearError('confirmPassword');
|
clearError('confirmPassword');
|
||||||
}}
|
}}
|
||||||
error={errors.confirmPassword}
|
error={errors.confirmPassword}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Button, Input, Card, Select, Avatar, Modal } from '../../ui';
|
import { Button, Input, Card, Select, Avatar, Modal } from '../../ui';
|
||||||
import { useAuth } from '../../../hooks/api/useAuth';
|
import { useAuthUser } from '../../../stores/auth.store';
|
||||||
import { User } from '../../../types/auth.types';
|
|
||||||
import { useToast } from '../../../hooks/ui/useToast';
|
import { useToast } from '../../../hooks/ui/useToast';
|
||||||
|
|
||||||
interface ProfileSettingsProps {
|
interface ProfileSettingsProps {
|
||||||
@@ -11,7 +10,8 @@ interface ProfileSettingsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ProfileFormData {
|
interface ProfileFormData {
|
||||||
full_name: string;
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
language: string;
|
language: string;
|
||||||
@@ -19,16 +19,6 @@ interface ProfileFormData {
|
|||||||
avatar_url?: string;
|
avatar_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BakeryFormData {
|
|
||||||
bakery_name: string;
|
|
||||||
bakery_type: string;
|
|
||||||
address: string;
|
|
||||||
city: string;
|
|
||||||
postal_code: string;
|
|
||||||
country: string;
|
|
||||||
website?: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NotificationSettings {
|
interface NotificationSettings {
|
||||||
email_notifications: boolean;
|
email_notifications: boolean;
|
||||||
@@ -50,29 +40,23 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
|||||||
className,
|
className,
|
||||||
initialTab = 'profile'
|
initialTab = 'profile'
|
||||||
}) => {
|
}) => {
|
||||||
const { user, updateProfile, isLoading, error } = useAuth();
|
const user = useAuthUser();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'profile' | 'security' | 'preferences' | 'notifications'>(initialTab);
|
const [activeTab, setActiveTab] = useState<'profile' | 'security' | 'preferences' | 'notifications'>(initialTab);
|
||||||
|
|
||||||
|
// Mock data for profile
|
||||||
const [profileData, setProfileData] = useState<ProfileFormData>({
|
const [profileData, setProfileData] = useState<ProfileFormData>({
|
||||||
full_name: '',
|
first_name: 'María',
|
||||||
email: '',
|
last_name: 'González Pérez',
|
||||||
phone: '',
|
email: 'admin@bakery.com',
|
||||||
|
phone: '+34 612 345 678',
|
||||||
language: 'es',
|
language: 'es',
|
||||||
timezone: 'Europe/Madrid',
|
timezone: 'Europe/Madrid',
|
||||||
avatar_url: ''
|
avatar_url: 'https://images.unsplash.com/photo-1494790108755-2616b612b372?w=150&h=150&fit=crop&crop=face'
|
||||||
});
|
|
||||||
|
|
||||||
const [bakeryData, setBakeryData] = useState<BakeryFormData>({
|
|
||||||
bakery_name: '',
|
|
||||||
bakery_type: 'traditional',
|
|
||||||
address: '',
|
|
||||||
city: '',
|
|
||||||
postal_code: '',
|
|
||||||
country: 'España',
|
|
||||||
website: '',
|
|
||||||
description: ''
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [notificationSettings, setNotificationSettings] = useState<NotificationSettings>({
|
const [notificationSettings, setNotificationSettings] = useState<NotificationSettings>({
|
||||||
@@ -105,17 +89,6 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
|||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
|
||||||
const bakeryTypeOptions = [
|
|
||||||
{ value: 'traditional', label: 'Panadería Tradicional' },
|
|
||||||
{ value: 'artisan', label: 'Panadería Artesanal' },
|
|
||||||
{ value: 'industrial', label: 'Panadería Industrial' },
|
|
||||||
{ value: 'confectionery', label: 'Pastelería' },
|
|
||||||
{ value: 'bakery_cafe', label: 'Panadería Cafetería' },
|
|
||||||
{ value: 'specialty', label: 'Panadería Especializada' },
|
|
||||||
{ value: 'patisserie', label: 'Pastelería Francesa' },
|
|
||||||
{ value: 'organic', label: 'Panadería Orgánica' },
|
|
||||||
{ value: 'gluten_free', label: 'Panadería Sin Gluten' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const languageOptions = [
|
const languageOptions = [
|
||||||
{ value: 'es', label: 'Español' },
|
{ value: 'es', label: 'Español' },
|
||||||
@@ -133,31 +106,25 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
|||||||
{ value: 'Europe/Rome', label: 'Roma (CET/CEST)' }
|
{ value: 'Europe/Rome', label: 'Roma (CET/CEST)' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Initialize form data with user data
|
// Mock update profile function
|
||||||
useEffect(() => {
|
const updateProfile = async (data: any): Promise<boolean> => {
|
||||||
if (user) {
|
setIsLoading(true);
|
||||||
setProfileData({
|
setError(null);
|
||||||
full_name: user.full_name || '',
|
|
||||||
email: user.email || '',
|
try {
|
||||||
phone: user.phone || '',
|
// Simulate API delay
|
||||||
language: user.language || 'es',
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
timezone: user.timezone || 'Europe/Madrid',
|
|
||||||
avatar_url: user.avatar_url || ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize bakery data (would come from a separate bakery API)
|
// Simulate successful update
|
||||||
setBakeryData({
|
console.log('Profile updated:', data);
|
||||||
bakery_name: (user as any).bakery_name || '',
|
setIsLoading(false);
|
||||||
bakery_type: (user as any).bakery_type || 'traditional',
|
return true;
|
||||||
address: (user as any).address || '',
|
} catch (err) {
|
||||||
city: (user as any).city || '',
|
setError('Error updating profile');
|
||||||
postal_code: (user as any).postal_code || '',
|
setIsLoading(false);
|
||||||
country: (user as any).country || 'España',
|
return false;
|
||||||
website: (user as any).website || '',
|
|
||||||
description: (user as any).description || ''
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [user]);
|
};
|
||||||
|
|
||||||
// Profile picture upload handler
|
// Profile picture upload handler
|
||||||
const handleImageUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImageUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -227,10 +194,16 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
|||||||
const validateProfileForm = (): boolean => {
|
const validateProfileForm = (): boolean => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
if (!profileData.full_name.trim()) {
|
if (!profileData.first_name.trim()) {
|
||||||
newErrors.full_name = 'El nombre completo es requerido';
|
newErrors.first_name = 'El nombre es requerido';
|
||||||
} else if (profileData.full_name.trim().length < 2) {
|
} else if (profileData.first_name.trim().length < 2) {
|
||||||
newErrors.full_name = 'El nombre debe tener al menos 2 caracteres';
|
newErrors.first_name = 'El nombre debe tener al menos 2 caracteres';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profileData.last_name.trim()) {
|
||||||
|
newErrors.last_name = 'Los apellidos son requeridos';
|
||||||
|
} else if (profileData.last_name.trim().length < 2) {
|
||||||
|
newErrors.last_name = 'Los apellidos deben tener al menos 2 caracteres';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!profileData.email.trim()) {
|
if (!profileData.email.trim()) {
|
||||||
@@ -242,37 +215,12 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
|||||||
if (profileData.phone && !/^(\+34|0034|34)?[6-9][0-9]{8}$/.test(profileData.phone.replace(/\s/g, ''))) {
|
if (profileData.phone && !/^(\+34|0034|34)?[6-9][0-9]{8}$/.test(profileData.phone.replace(/\s/g, ''))) {
|
||||||
newErrors.phone = 'Por favor, ingrese un teléfono español válido (ej: +34 600 000 000)';
|
newErrors.phone = 'Por favor, ingrese un teléfono español válido (ej: +34 600 000 000)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateBakeryForm = (): boolean => {
|
|
||||||
const newErrors: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (!bakeryData.bakery_name.trim()) {
|
|
||||||
newErrors.bakery_name = 'El nombre de la panadería es requerido';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bakeryData.address.trim()) {
|
|
||||||
newErrors.address = 'La dirección es requerida';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bakeryData.city.trim()) {
|
|
||||||
newErrors.city = 'La ciudad es requerida';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bakeryData.postal_code && !/^[0-5][0-9]{4}$/.test(bakeryData.postal_code)) {
|
|
||||||
newErrors.postal_code = 'Por favor, ingrese un código postal español válido';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bakeryData.website && !/^https?:\/\/.+\..+/.test(bakeryData.website)) {
|
|
||||||
newErrors.website = 'Por favor, ingrese una URL válida (ej: https://mipanaderia.com)';
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors(newErrors);
|
|
||||||
return Object.keys(newErrors).length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validatePasswordForm = (): boolean => {
|
const validatePasswordForm = (): boolean => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
@@ -358,15 +306,25 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProfileInputChange = (field: keyof ProfileFormData) => (value: string) => {
|
const handleProfileInputChange = (field: keyof ProfileFormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
setProfileData(prev => ({ ...prev, [field]: value }));
|
setProfileData(prev => ({ ...prev, [field]: value }));
|
||||||
setHasChanges(true);
|
setHasChanges(prev => ({ ...prev, profile: true }));
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors(prev => ({ ...prev, [field]: undefined }));
|
setErrors(prev => ({ ...prev, [field]: undefined }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePasswordInputChange = (field: keyof PasswordChangeData) => (value: string) => {
|
const handleSelectChange = (field: keyof ProfileFormData) => (value: string) => {
|
||||||
|
setProfileData(prev => ({ ...prev, [field]: value }));
|
||||||
|
setHasChanges(prev => ({ ...prev, profile: true }));
|
||||||
|
if (errors[field]) {
|
||||||
|
setErrors(prev => ({ ...prev, [field]: undefined }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordInputChange = (field: keyof PasswordChangeData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
setPasswordData(prev => ({ ...prev, [field]: value }));
|
setPasswordData(prev => ({ ...prev, [field]: value }));
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors(prev => ({ ...prev, [field]: undefined }));
|
setErrors(prev => ({ ...prev, [field]: undefined }));
|
||||||
@@ -386,32 +344,47 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
|||||||
{ id: 'preferences' as const, label: 'Preferencias', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z' }
|
{ id: 'preferences' as const, label: 'Preferencias', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z' }
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!user) {
|
// Mock user data for display
|
||||||
return (
|
const mockUser = {
|
||||||
<Card className={`p-8 ${className || ''}`}>
|
first_name: 'María',
|
||||||
<div className="text-center text-text-secondary">
|
last_name: 'González',
|
||||||
Cargando información del usuario...
|
email: 'admin@bakery.com',
|
||||||
</div>
|
bakery_name: 'Panadería San Miguel',
|
||||||
</Card>
|
avatar_url: 'https://images.unsplash.com/photo-1494790108755-2616b612b372?w=150&h=150&fit=crop&crop=face'
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-6 ${className || ''}`}>
|
<div className={`space-y-6 ${className || ''}`}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Card className="p-6">
|
<Card className="p-8">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-6">
|
||||||
<Avatar
|
<div className="relative">
|
||||||
src={user.avatar_url}
|
<Avatar
|
||||||
name={`${user.first_name} ${user.last_name}`}
|
src={mockUser.avatar_url}
|
||||||
size="lg"
|
name={`${mockUser.first_name} ${mockUser.last_name}`}
|
||||||
/>
|
size="xl"
|
||||||
<div>
|
className="w-20 h-20 border-4 border-background-primary shadow-lg"
|
||||||
<h1 className="text-2xl font-bold text-text-primary">
|
/>
|
||||||
{user.first_name} {user.last_name}
|
<div className="absolute -bottom-1 -right-1 w-6 h-6 bg-color-success rounded-full border-2 border-background-primary"></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h1 className="text-3xl font-bold text-text-primary mb-2">
|
||||||
|
{mockUser.first_name} {mockUser.last_name}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-text-secondary">{user.email}</p>
|
<p className="text-text-secondary text-lg mb-1">{mockUser.email}</p>
|
||||||
<p className="text-sm text-text-secondary">{user.bakery_name}</p>
|
<p className="text-sm text-text-tertiary flex items-center">
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||||
|
</svg>
|
||||||
|
Trabajando en {mockUser.bakery_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="inline-flex items-center px-3 py-1 rounded-full text-sm bg-color-success/10 text-color-success mb-2">
|
||||||
|
<div className="w-2 h-2 bg-color-success rounded-full mr-2"></div>
|
||||||
|
En línea
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-text-tertiary">Última vez activo: ahora</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -439,141 +412,170 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6">
|
<div className="p-8">
|
||||||
{activeTab === 'profile' && (
|
{activeTab === 'profile' && (
|
||||||
<form onSubmit={handleProfileSubmit} className="space-y-6">
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
{/* Profile Photo Section */}
|
||||||
<div className="space-y-4">
|
<div className="flex items-center space-x-8 mb-8 p-6 bg-background-secondary rounded-lg">
|
||||||
<h3 className="text-lg font-semibold text-text-primary border-b border-border-primary pb-2">
|
<div className="relative">
|
||||||
Información Personal
|
<Avatar
|
||||||
</h3>
|
src={profileData.avatar_url}
|
||||||
|
name={`${profileData.first_name} ${profileData.last_name}`}
|
||||||
<Input
|
size="xl"
|
||||||
label="Nombre"
|
className="w-24 h-24"
|
||||||
value={profileData.first_name}
|
|
||||||
onChange={handleProfileInputChange('first_name')}
|
|
||||||
error={errors.first_name}
|
|
||||||
disabled={isLoading}
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
<Input
|
type="button"
|
||||||
label="Apellidos"
|
onClick={() => fileInputRef.current?.click()}
|
||||||
value={profileData.last_name}
|
className="absolute -bottom-2 -right-2 bg-color-primary text-white rounded-full p-2 shadow-lg hover:bg-color-primary-dark transition-colors"
|
||||||
onChange={handleProfileInputChange('last_name')}
|
|
||||||
error={errors.last_name}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
required
|
>
|
||||||
/>
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||||||
<Input
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
type="email"
|
</svg>
|
||||||
label="Email"
|
</button>
|
||||||
value={profileData.email}
|
<input
|
||||||
onChange={handleProfileInputChange('email')}
|
ref={fileInputRef}
|
||||||
error={errors.email}
|
type="file"
|
||||||
disabled={isLoading}
|
accept="image/*"
|
||||||
required
|
onChange={handleImageUpload}
|
||||||
/>
|
className="hidden"
|
||||||
|
|
||||||
<Input
|
|
||||||
type="tel"
|
|
||||||
label="Teléfono"
|
|
||||||
value={profileData.phone}
|
|
||||||
onChange={handleProfileInputChange('phone')}
|
|
||||||
error={errors.phone}
|
|
||||||
disabled={isLoading}
|
|
||||||
placeholder="+34 600 000 000"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
<div className="space-y-4">
|
<h2 className="text-2xl font-bold text-text-primary mb-2">
|
||||||
<h3 className="text-lg font-semibold text-text-primary border-b border-border-primary pb-2">
|
{profileData.first_name} {profileData.last_name}
|
||||||
Información del Negocio
|
</h2>
|
||||||
</h3>
|
<p className="text-text-secondary text-lg mb-2">{profileData.email}</p>
|
||||||
|
<p className="text-text-tertiary text-sm">Última actualización: Hace 2 días</p>
|
||||||
<Input
|
</div>
|
||||||
label="Nombre de la Panadería"
|
{uploadingImage && (
|
||||||
value={profileData.bakery_name}
|
<div className="text-color-primary">
|
||||||
onChange={handleProfileInputChange('bakery_name')}
|
<svg className="animate-spin h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||||
error={errors.bakery_name}
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
disabled={isLoading}
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
required
|
</svg>
|
||||||
/>
|
</div>
|
||||||
|
)}
|
||||||
<Select
|
</div>
|
||||||
label="Tipo de Panadería"
|
|
||||||
options={bakeryTypeOptions}
|
<form onSubmit={handleProfileSubmit} className="space-y-8">
|
||||||
value={profileData.bakery_type}
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
onChange={handleProfileInputChange('bakery_type')}
|
{/* Personal Information */}
|
||||||
disabled={isLoading}
|
<div className="space-y-6">
|
||||||
required
|
<h3 className="text-xl font-semibold text-text-primary border-b border-border-primary pb-3">
|
||||||
/>
|
Información Personal
|
||||||
|
</h3>
|
||||||
<Input
|
|
||||||
label="Dirección"
|
|
||||||
value={profileData.address}
|
|
||||||
onChange={handleProfileInputChange('address')}
|
|
||||||
error={errors.address}
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<Input
|
|
||||||
label="Ciudad"
|
|
||||||
value={profileData.city}
|
|
||||||
onChange={handleProfileInputChange('city')}
|
|
||||||
error={errors.city}
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
<div className="space-y-4">
|
||||||
label="País"
|
<Input
|
||||||
value={profileData.country}
|
label="Nombre"
|
||||||
onChange={handleProfileInputChange('country')}
|
value={profileData.first_name}
|
||||||
disabled={isLoading}
|
onChange={handleProfileInputChange('first_name')}
|
||||||
/>
|
error={errors.first_name}
|
||||||
|
disabled={isLoading}
|
||||||
|
required
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Apellidos"
|
||||||
|
value={profileData.last_name}
|
||||||
|
onChange={handleProfileInputChange('last_name')}
|
||||||
|
error={errors.last_name}
|
||||||
|
disabled={isLoading}
|
||||||
|
required
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
label="Correo Electrónico"
|
||||||
|
value={profileData.email}
|
||||||
|
onChange={handleProfileInputChange('email')}
|
||||||
|
error={errors.email}
|
||||||
|
disabled={isLoading}
|
||||||
|
required
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="tel"
|
||||||
|
label="Teléfono"
|
||||||
|
value={profileData.phone}
|
||||||
|
onChange={handleProfileInputChange('phone')}
|
||||||
|
error={errors.phone}
|
||||||
|
disabled={isLoading}
|
||||||
|
placeholder="+34 600 000 000"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preferences */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-xl font-semibold text-text-primary border-b border-border-primary pb-3">
|
||||||
|
Preferencias
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Select
|
||||||
|
label="Idioma"
|
||||||
|
options={languageOptions}
|
||||||
|
value={profileData.language}
|
||||||
|
onChange={handleSelectChange('language')}
|
||||||
|
disabled={isLoading}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Zona Horaria"
|
||||||
|
options={timezoneOptions}
|
||||||
|
value={profileData.timezone}
|
||||||
|
onChange={handleSelectChange('timezone')}
|
||||||
|
disabled={isLoading}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className="flex justify-end space-x-4 pt-8 border-t border-border-primary">
|
||||||
<div className="flex justify-end space-x-4 pt-6 border-t border-border-primary">
|
<Button
|
||||||
<Button
|
type="button"
|
||||||
type="button"
|
variant="outline"
|
||||||
variant="outline"
|
size="lg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (user) {
|
|
||||||
setProfileData({
|
setProfileData({
|
||||||
first_name: user.first_name || '',
|
first_name: 'María',
|
||||||
last_name: user.last_name || '',
|
last_name: 'González Pérez',
|
||||||
email: user.email || '',
|
email: 'admin@bakery.com',
|
||||||
phone: user.phone || '',
|
phone: '+34 612 345 678',
|
||||||
bakery_name: user.bakery_name || '',
|
language: 'es',
|
||||||
bakery_type: user.bakery_type || 'traditional',
|
timezone: 'Europe/Madrid',
|
||||||
address: user.address || '',
|
avatar_url: 'https://images.unsplash.com/photo-1494790108755-2616b612b372?w=150&h=150&fit=crop&crop=face'
|
||||||
city: user.city || '',
|
|
||||||
country: user.country || 'España',
|
|
||||||
avatar_url: user.avatar_url || ''
|
|
||||||
});
|
});
|
||||||
setHasChanges(false);
|
setHasChanges(prev => ({ ...prev, profile: false }));
|
||||||
setErrors({});
|
setErrors({});
|
||||||
}
|
}}
|
||||||
}}
|
disabled={!hasChanges.profile || isLoading}
|
||||||
disabled={!hasChanges || isLoading}
|
>
|
||||||
>
|
Cancelar
|
||||||
Cancelar
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
type="submit"
|
||||||
type="submit"
|
variant="primary"
|
||||||
variant="primary"
|
size="lg"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
loadingText="Guardando..."
|
loadingText="Guardando..."
|
||||||
disabled={!hasChanges}
|
disabled={!hasChanges.profile}
|
||||||
>
|
>
|
||||||
Guardar Cambios
|
Guardar Cambios
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'security' && (
|
{activeTab === 'security' && (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Button, Input, Card, Select } from '../../ui';
|
import { Button, Input, Card } from '../../ui';
|
||||||
import { useAuth } from '../../../hooks/api/useAuth';
|
import { useAuth } from '../../../hooks/api/useAuth';
|
||||||
import { UserRegistration } from '../../../types/auth.types';
|
import { UserRegistration } from '../../../types/auth.types';
|
||||||
import { useToast } from '../../../hooks/ui/useToast';
|
import { useToast } from '../../../hooks/ui/useToast';
|
||||||
@@ -8,89 +8,38 @@ interface RegisterFormProps {
|
|||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
onLoginClick?: () => void;
|
onLoginClick?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
showProgressSteps?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegistrationStep = 'personal' | 'bakery' | 'security' | 'verification';
|
interface SimpleUserRegistration {
|
||||||
|
|
||||||
interface ExtendedUserRegistration {
|
|
||||||
// Personal Information
|
|
||||||
full_name: string;
|
full_name: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
|
||||||
|
|
||||||
// Bakery Information
|
|
||||||
tenant_name: string;
|
|
||||||
bakery_type: string;
|
|
||||||
address: string;
|
|
||||||
city: string;
|
|
||||||
postal_code: string;
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
// Security
|
|
||||||
password: string;
|
password: string;
|
||||||
confirmPassword: string;
|
confirmPassword: string;
|
||||||
|
|
||||||
// Terms
|
|
||||||
acceptTerms: boolean;
|
acceptTerms: boolean;
|
||||||
acceptPrivacy: boolean;
|
|
||||||
acceptMarketing: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RegisterForm: React.FC<RegisterFormProps> = ({
|
export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||||
onSuccess,
|
onSuccess,
|
||||||
onLoginClick,
|
onLoginClick,
|
||||||
className,
|
className
|
||||||
showProgressSteps = true
|
|
||||||
}) => {
|
}) => {
|
||||||
const [currentStep, setCurrentStep] = useState<RegistrationStep>('personal');
|
const [formData, setFormData] = useState<SimpleUserRegistration>({
|
||||||
const [completedSteps, setCompletedSteps] = useState<Set<RegistrationStep>>(new Set());
|
|
||||||
const [isEmailVerificationSent, setIsEmailVerificationSent] = useState(false);
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState<ExtendedUserRegistration>({
|
|
||||||
full_name: '',
|
full_name: '',
|
||||||
email: '',
|
email: '',
|
||||||
phone: '',
|
|
||||||
tenant_name: '',
|
|
||||||
bakery_type: 'traditional',
|
|
||||||
address: '',
|
|
||||||
city: '',
|
|
||||||
postal_code: '',
|
|
||||||
country: 'España',
|
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
acceptTerms: false,
|
acceptTerms: false
|
||||||
acceptPrivacy: false,
|
|
||||||
acceptMarketing: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [errors, setErrors] = useState<Partial<ExtendedUserRegistration>>({});
|
const [errors, setErrors] = useState<Partial<SimpleUserRegistration>>({});
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
|
||||||
const { register, verifyEmail, isLoading, error } = useAuth();
|
const { register, isLoading, error } = useAuth();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
|
||||||
const steps: { id: RegistrationStep; title: string; description: string }[] = [
|
const validateForm = (): boolean => {
|
||||||
{ id: 'personal', title: 'Información Personal', description: 'Tus datos básicos' },
|
const newErrors: Partial<SimpleUserRegistration> = {};
|
||||||
{ id: 'bakery', title: 'Tu Panadería', description: 'Detalles del negocio' },
|
|
||||||
{ id: 'security', title: 'Seguridad', description: 'Contraseña y términos' },
|
|
||||||
{ id: 'verification', title: 'Verificación', description: 'Confirmar email' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const bakeryTypeOptions = [
|
|
||||||
{ value: 'traditional', label: 'Panadería Tradicional' },
|
|
||||||
{ value: 'artisan', label: 'Panadería Artesanal' },
|
|
||||||
{ value: 'industrial', label: 'Panadería Industrial' },
|
|
||||||
{ value: 'confectionery', label: 'Pastelería' },
|
|
||||||
{ value: 'bakery_cafe', label: 'Panadería Cafetería' },
|
|
||||||
{ value: 'specialty', label: 'Panadería Especializada' },
|
|
||||||
{ value: 'patisserie', label: 'Pastelería Francesa' },
|
|
||||||
{ value: 'organic', label: 'Panadería Orgánica' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const validatePersonalStep = (): boolean => {
|
|
||||||
const newErrors: Partial<ExtendedUserRegistration> = {};
|
|
||||||
|
|
||||||
if (!formData.full_name.trim()) {
|
if (!formData.full_name.trim()) {
|
||||||
newErrors.full_name = 'El nombre completo es requerido';
|
newErrors.full_name = 'El nombre completo es requerido';
|
||||||
@@ -104,52 +53,10 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
newErrors.email = 'Por favor, ingrese un email válido';
|
newErrors.email = 'Por favor, ingrese un email válido';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.phone.trim()) {
|
|
||||||
newErrors.phone = 'El teléfono es requerido';
|
|
||||||
} else if (!/^(\+34|0034|34)?[6-9][0-9]{8}$/.test(formData.phone.replace(/\s/g, ''))) {
|
|
||||||
newErrors.phone = 'Por favor, ingrese un teléfono español válido (ej: +34 600 000 000)';
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors(prev => ({ ...prev, ...newErrors }));
|
|
||||||
return Object.keys(newErrors).length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateBakeryStep = (): boolean => {
|
|
||||||
const newErrors: Partial<ExtendedUserRegistration> = {};
|
|
||||||
|
|
||||||
if (!formData.tenant_name.trim()) {
|
|
||||||
newErrors.tenant_name = 'El nombre de la panadería es requerido';
|
|
||||||
} else if (formData.tenant_name.trim().length < 2) {
|
|
||||||
newErrors.tenant_name = 'El nombre debe tener al menos 2 caracteres';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.address.trim()) {
|
|
||||||
newErrors.address = 'La dirección es requerida';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.city.trim()) {
|
|
||||||
newErrors.city = 'La ciudad es requerida';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.postal_code.trim()) {
|
|
||||||
newErrors.postal_code = 'El código postal es requerido';
|
|
||||||
} else if (!/^[0-5][0-9]{4}$/.test(formData.postal_code)) {
|
|
||||||
newErrors.postal_code = 'Por favor, ingrese un código postal español válido (ej: 28001)';
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors(prev => ({ ...prev, ...newErrors }));
|
|
||||||
return Object.keys(newErrors).length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateSecurityStep = (): boolean => {
|
|
||||||
const newErrors: Partial<ExtendedUserRegistration> = {};
|
|
||||||
|
|
||||||
if (!formData.password) {
|
if (!formData.password) {
|
||||||
newErrors.password = 'La contraseña es requerida';
|
newErrors.password = 'La contraseña es requerida';
|
||||||
} else if (formData.password.length < 8) {
|
} else if (formData.password.length < 8) {
|
||||||
newErrors.password = 'La contraseña debe tener al menos 8 caracteres';
|
newErrors.password = 'La contraseña debe tener al menos 8 caracteres';
|
||||||
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])?/.test(formData.password)) {
|
|
||||||
newErrors.password = 'La contraseña debe contener mayúsculas, minúsculas y números';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.confirmPassword) {
|
if (!formData.confirmPassword) {
|
||||||
@@ -162,87 +69,41 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
newErrors.acceptTerms = 'Debes aceptar los términos y condiciones';
|
newErrors.acceptTerms = 'Debes aceptar los términos y condiciones';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.acceptPrivacy) {
|
setErrors(newErrors);
|
||||||
newErrors.acceptPrivacy = 'Debes aceptar la política de privacidad';
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors(prev => ({ ...prev, ...newErrors }));
|
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNextStep = (e?: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
if (e) e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
let isValid = false;
|
if (!validateForm()) {
|
||||||
switch (currentStep) {
|
return;
|
||||||
case 'personal':
|
|
||||||
isValid = validatePersonalStep();
|
|
||||||
break;
|
|
||||||
case 'bakery':
|
|
||||||
isValid = validateBakeryStep();
|
|
||||||
break;
|
|
||||||
case 'security':
|
|
||||||
isValid = validateSecurityStep();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
isValid = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isValid) {
|
|
||||||
setCompletedSteps(prev => new Set([...prev, currentStep]));
|
|
||||||
|
|
||||||
const stepOrder: RegistrationStep[] = ['personal', 'bakery', 'security', 'verification'];
|
|
||||||
const currentIndex = stepOrder.indexOf(currentStep);
|
|
||||||
|
|
||||||
if (currentIndex < stepOrder.length - 1) {
|
|
||||||
setCurrentStep(stepOrder[currentIndex + 1]);
|
|
||||||
setErrors({}); // Clear errors when moving to next step
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is the security step, submit the registration
|
|
||||||
if (currentStep === 'security') {
|
|
||||||
handleSubmitRegistration();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePreviousStep = () => {
|
|
||||||
const stepOrder: RegistrationStep[] = ['personal', 'bakery', 'security', 'verification'];
|
|
||||||
const currentIndex = stepOrder.indexOf(currentStep);
|
|
||||||
|
|
||||||
if (currentIndex > 0) {
|
|
||||||
setCurrentStep(stepOrder[currentIndex - 1]);
|
|
||||||
setErrors({}); // Clear errors when going back
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmitRegistration = async () => {
|
|
||||||
try {
|
try {
|
||||||
const registrationData: UserRegistration = {
|
const registrationData: UserRegistration = {
|
||||||
full_name: formData.full_name,
|
full_name: formData.full_name,
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
tenant_name: formData.tenant_name,
|
tenant_name: 'Default Bakery', // Default value since we're not collecting it
|
||||||
phone: formData.phone
|
phone: '' // Optional field
|
||||||
};
|
};
|
||||||
|
|
||||||
const success = await register(registrationData);
|
const success = await register(registrationData);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
setIsEmailVerificationSent(true);
|
|
||||||
setCurrentStep('verification');
|
|
||||||
showToast({
|
showToast({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Cuenta creada exitosamente',
|
title: 'Cuenta creada exitosamente',
|
||||||
message: 'Revisa tu email para verificar tu cuenta y completar el registro'
|
message: '¡Bienvenido! Tu cuenta ha sido creada correctamente.'
|
||||||
});
|
});
|
||||||
|
onSuccess?.();
|
||||||
} else {
|
} else {
|
||||||
showToast({
|
showToast({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Error al crear la cuenta',
|
title: 'Error al crear la cuenta',
|
||||||
message: error || 'No se pudo crear la cuenta. Verifica que el email no esté en uso.'
|
message: error || 'No se pudo crear la cuenta. Verifica que el email no esté en uso.'
|
||||||
});
|
});
|
||||||
setCurrentStep('personal'); // Go back to first step to fix issues
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast({
|
showToast({
|
||||||
@@ -250,540 +111,181 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|||||||
title: 'Error de conexión',
|
title: 'Error de conexión',
|
||||||
message: 'No se pudo conectar con el servidor. Verifica tu conexión a internet.'
|
message: 'No se pudo conectar con el servidor. Verifica tu conexión a internet.'
|
||||||
});
|
});
|
||||||
setCurrentStep('personal');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputChange = (field: keyof ExtendedUserRegistration) => (value: string | boolean) => {
|
const handleInputChange = (field: keyof SimpleUserRegistration) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors(prev => ({ ...prev, [field]: undefined }));
|
setErrors(prev => ({ ...prev, [field]: undefined }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentStepIndex = () => {
|
|
||||||
const stepOrder: RegistrationStep[] = ['personal', 'bakery', 'security', 'verification'];
|
|
||||||
return stepOrder.indexOf(currentStep);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProgressPercentage = () => {
|
|
||||||
return ((getCurrentStepIndex()) / (steps.length - 1)) * 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Email verification success handler
|
|
||||||
const handleEmailVerification = async (token: string) => {
|
|
||||||
try {
|
|
||||||
const success = await verifyEmail(token);
|
|
||||||
if (success) {
|
|
||||||
showToast({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Email verificado exitosamente',
|
|
||||||
message: '¡Tu cuenta ha sido activada! Ya puedes iniciar sesión.'
|
|
||||||
});
|
|
||||||
onSuccess?.();
|
|
||||||
} else {
|
|
||||||
showToast({
|
|
||||||
type: 'error',
|
|
||||||
title: 'Error de verificación',
|
|
||||||
message: 'El enlace de verificación no es válido o ha expirado'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showToast({
|
|
||||||
type: 'error',
|
|
||||||
title: 'Error de conexión',
|
|
||||||
message: 'No se pudo verificar el email. Intenta más tarde.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={`p-8 w-full max-w-4xl ${className || ''}`} role="main">
|
<Card className={`p-8 w-full max-w-md ${className || ''}`} role="main">
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-3xl font-bold text-text-primary mb-2">
|
<h1 className="text-3xl font-bold text-text-primary mb-2">
|
||||||
Registra tu Panadería
|
Crear Cuenta
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-text-secondary text-lg">
|
<p className="text-text-secondary text-lg">
|
||||||
Crea tu cuenta y comienza a digitalizar tu negocio
|
Únete y comienza hoy mismo
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Indicator */}
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{showProgressSteps && (
|
<Input
|
||||||
<div className="mb-8">
|
label="Nombre Completo"
|
||||||
<div className="flex items-center justify-between mb-4">
|
placeholder="Juan Pérez García"
|
||||||
{steps.map((step, index) => (
|
value={formData.full_name}
|
||||||
<div
|
onChange={handleInputChange('full_name')}
|
||||||
key={step.id}
|
error={errors.full_name}
|
||||||
className="flex flex-col items-center flex-1"
|
disabled={isLoading}
|
||||||
role="progressbar"
|
required
|
||||||
aria-valuenow={getCurrentStepIndex() + 1}
|
autoComplete="name"
|
||||||
aria-valuemax={steps.length}
|
leftIcon={
|
||||||
aria-label={`Paso ${index + 1} de ${steps.length}: ${step.title}`}
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold mb-2 transition-all duration-300 ${
|
|
||||||
completedSteps.has(step.id)
|
|
||||||
? 'bg-color-success text-white'
|
|
||||||
: currentStep === step.id
|
|
||||||
? 'bg-color-primary text-white ring-4 ring-color-primary/20'
|
|
||||||
: 'bg-background-secondary text-text-secondary'
|
|
||||||
}`}>
|
|
||||||
{completedSteps.has(step.id) ? (
|
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
index + 1
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className={`text-xs font-medium ${
|
|
||||||
currentStep === step.id ? 'text-color-primary' : 'text-text-secondary'
|
|
||||||
}`}>
|
|
||||||
{step.title}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-text-secondary mt-1">
|
|
||||||
{step.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{index < steps.length - 1 && (
|
|
||||||
<div className={`absolute top-5 left-1/2 w-full h-0.5 -z-10 ${
|
|
||||||
completedSteps.has(step.id) ? 'bg-color-success' : 'bg-background-secondary'
|
|
||||||
}`} style={{ marginLeft: '2.5rem' }} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress Bar */}
|
|
||||||
<div className="w-full bg-background-secondary rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="bg-color-primary h-2 rounded-full transition-all duration-500 ease-in-out"
|
|
||||||
style={{ width: `${getProgressPercentage()}%` }}
|
|
||||||
role="progressbar"
|
|
||||||
aria-label="Progreso del registro"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Step Content */}
|
|
||||||
{currentStep !== 'verification' ? (
|
|
||||||
<form onSubmit={handleNextStep} className="space-y-8">
|
|
||||||
{/* Personal Information Step */}
|
|
||||||
{currentStep === 'personal' && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<h2 className="text-xl font-semibold text-text-primary mb-2">
|
|
||||||
Información Personal
|
|
||||||
</h2>
|
|
||||||
<p className="text-text-secondary">
|
|
||||||
Cuéntanos sobre ti para personalizar tu experiencia
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6">
|
|
||||||
<Input
|
|
||||||
label="Nombre Completo"
|
|
||||||
placeholder="Juan Pérez García"
|
|
||||||
value={formData.full_name}
|
|
||||||
onChange={handleInputChange('full_name')}
|
|
||||||
error={errors.full_name}
|
|
||||||
disabled={isLoading}
|
|
||||||
required
|
|
||||||
autoComplete="name"
|
|
||||||
leftIcon={
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
label="Correo Electrónico"
|
|
||||||
placeholder="tu.email@panaderia.com"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleInputChange('email')}
|
|
||||||
error={errors.email}
|
|
||||||
disabled={isLoading}
|
|
||||||
required
|
|
||||||
autoComplete="email"
|
|
||||||
leftIcon={
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
type="tel"
|
|
||||||
label="Teléfono"
|
|
||||||
placeholder="+34 600 000 000"
|
|
||||||
value={formData.phone}
|
|
||||||
onChange={handleInputChange('phone')}
|
|
||||||
error={errors.phone}
|
|
||||||
disabled={isLoading}
|
|
||||||
required
|
|
||||||
autoComplete="tel"
|
|
||||||
leftIcon={
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* Bakery Information Step */}
|
|
||||||
{currentStep === 'bakery' && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<h2 className="text-xl font-semibold text-text-primary mb-2">
|
|
||||||
Información de tu Panadería
|
|
||||||
</h2>
|
|
||||||
<p className="text-text-secondary">
|
|
||||||
Déjanos conocer los detalles de tu negocio
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<Input
|
|
||||||
label="Nombre de la Panadería"
|
|
||||||
placeholder="Panadería San José"
|
|
||||||
value={formData.tenant_name}
|
|
||||||
onChange={handleInputChange('tenant_name')}
|
|
||||||
error={errors.tenant_name}
|
|
||||||
disabled={isLoading}
|
|
||||||
required
|
|
||||||
leftIcon={
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
label="Tipo de Panadería"
|
|
||||||
options={bakeryTypeOptions}
|
|
||||||
value={formData.bakery_type}
|
|
||||||
onChange={handleInputChange('bakery_type')}
|
|
||||||
error={errors.bakery_type}
|
|
||||||
disabled={isLoading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Dirección"
|
|
||||||
placeholder="Calle Principal 123"
|
|
||||||
value={formData.address}
|
|
||||||
onChange={handleInputChange('address')}
|
|
||||||
error={errors.address}
|
|
||||||
disabled={isLoading}
|
|
||||||
required
|
|
||||||
leftIcon={
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<Input
|
|
||||||
label="Ciudad"
|
|
||||||
placeholder="Madrid"
|
|
||||||
value={formData.city}
|
|
||||||
onChange={handleInputChange('city')}
|
|
||||||
error={errors.city}
|
|
||||||
disabled={isLoading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Código Postal"
|
|
||||||
placeholder="28001"
|
|
||||||
value={formData.postal_code}
|
|
||||||
onChange={handleInputChange('postal_code')}
|
|
||||||
error={errors.postal_code}
|
|
||||||
disabled={isLoading}
|
|
||||||
required
|
|
||||||
maxLength={5}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="País"
|
|
||||||
value={formData.country}
|
|
||||||
onChange={handleInputChange('country')}
|
|
||||||
disabled={true}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Security Step */}
|
|
||||||
{currentStep === 'security' && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<h2 className="text-xl font-semibold text-text-primary mb-2">
|
|
||||||
Seguridad y Términos
|
|
||||||
</h2>
|
|
||||||
<p className="text-text-secondary">
|
|
||||||
Crea una contraseña segura y acepta nuestros términos
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<Input
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
label="Contraseña"
|
|
||||||
placeholder="Contraseña segura"
|
|
||||||
value={formData.password}
|
|
||||||
onChange={handleInputChange('password')}
|
|
||||||
error={errors.password}
|
|
||||||
disabled={isLoading}
|
|
||||||
autoComplete="new-password"
|
|
||||||
required
|
|
||||||
leftIcon={
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
rightIcon={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
className="text-text-secondary hover:text-text-primary"
|
|
||||||
aria-label={showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
type={showConfirmPassword ? 'text' : 'password'}
|
|
||||||
label="Confirmar Contraseña"
|
|
||||||
placeholder="Repite tu contraseña"
|
|
||||||
value={formData.confirmPassword}
|
|
||||||
onChange={handleInputChange('confirmPassword')}
|
|
||||||
error={errors.confirmPassword}
|
|
||||||
disabled={isLoading}
|
|
||||||
autoComplete="new-password"
|
|
||||||
required
|
|
||||||
leftIcon={
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
rightIcon={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
||||||
className="text-text-secondary hover:text-text-primary"
|
|
||||||
aria-label={showConfirmPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
|
|
||||||
>
|
|
||||||
{showConfirmPassword ? (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4 pt-4 border-t border-border-primary">
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="acceptTerms"
|
|
||||||
checked={formData.acceptTerms}
|
|
||||||
onChange={(e) => handleInputChange('acceptTerms')(e.target.checked)}
|
|
||||||
className="mt-1 h-4 w-4 rounded border-border-primary text-color-primary focus:ring-color-primary focus:ring-offset-0"
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
<label htmlFor="acceptTerms" className="text-sm text-text-secondary cursor-pointer">
|
|
||||||
Acepto los{' '}
|
|
||||||
<a href="#" className="text-color-primary hover:text-color-primary-dark underline">
|
|
||||||
términos y condiciones
|
|
||||||
</a>{' '}
|
|
||||||
de uso
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{errors.acceptTerms && (
|
|
||||||
<p className="text-color-error text-sm ml-7">{errors.acceptTerms}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="acceptPrivacy"
|
|
||||||
checked={formData.acceptPrivacy}
|
|
||||||
onChange={(e) => handleInputChange('acceptPrivacy')(e.target.checked)}
|
|
||||||
className="mt-1 h-4 w-4 rounded border-border-primary text-color-primary focus:ring-color-primary focus:ring-offset-0"
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
<label htmlFor="acceptPrivacy" className="text-sm text-text-secondary cursor-pointer">
|
|
||||||
Acepto la{' '}
|
|
||||||
<a href="#" className="text-color-primary hover:text-color-primary-dark underline">
|
|
||||||
política de privacidad
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{errors.acceptPrivacy && (
|
|
||||||
<p className="text-color-error text-sm ml-7">{errors.acceptPrivacy}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="acceptMarketing"
|
|
||||||
checked={formData.acceptMarketing}
|
|
||||||
onChange={(e) => handleInputChange('acceptMarketing')(e.target.checked)}
|
|
||||||
className="mt-1 h-4 w-4 rounded border-border-primary text-color-primary focus:ring-color-primary focus:ring-offset-0"
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
<label htmlFor="acceptMarketing" className="text-sm text-text-secondary cursor-pointer">
|
|
||||||
Deseo recibir información sobre nuevas funcionalidades y ofertas especiales
|
|
||||||
<span className="text-text-tertiary"> (opcional)</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Navigation Buttons */}
|
|
||||||
<div className="flex justify-between pt-6 border-t border-border-primary">
|
|
||||||
{currentStep !== 'personal' && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handlePreviousStep}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="flex items-center space-x-2"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
<span>Anterior</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex-1" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
isLoading={isLoading}
|
|
||||||
loadingText={currentStep === 'security' ? 'Creando cuenta...' : 'Procesando...'}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="flex items-center space-x-2 min-w-[140px]"
|
|
||||||
>
|
|
||||||
{currentStep === 'security' ? (
|
|
||||||
<>
|
|
||||||
<span>Crear Cuenta</span>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span>Continuar</span>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="bg-color-error/10 border border-color-error/20 text-color-error px-4 py-3 rounded-lg text-sm flex items-start space-x-3" role="alert">
|
|
||||||
<svg className="w-5 h-5 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
<span>{error}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
/* Verification Step */
|
|
||||||
<div className="text-center space-y-6">
|
|
||||||
<div className="w-20 h-20 bg-color-success/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
||||||
<svg className="w-10 h-10 text-color-success" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
<h2 className="text-2xl font-bold text-text-primary mb-4">
|
|
||||||
¡Cuenta creada exitosamente!
|
<Input
|
||||||
</h2>
|
type="email"
|
||||||
|
label="Correo Electrónico"
|
||||||
<div className="bg-background-secondary rounded-lg p-6 max-w-md mx-auto">
|
placeholder="tu.email@ejemplo.com"
|
||||||
<p className="text-text-secondary mb-4">
|
value={formData.email}
|
||||||
Hemos enviado un enlace de verificación a:
|
onChange={handleInputChange('email')}
|
||||||
</p>
|
error={errors.email}
|
||||||
<p className="text-text-primary font-semibold text-lg mb-4">
|
disabled={isLoading}
|
||||||
{formData.email}
|
required
|
||||||
</p>
|
autoComplete="email"
|
||||||
<p className="text-text-secondary text-sm">
|
leftIcon={
|
||||||
Revisa tu bandeja de entrada (y la carpeta de spam) y haz clic en el enlace para activar tu cuenta.
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</p>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
|
||||||
</div>
|
</svg>
|
||||||
|
}
|
||||||
<div className="space-y-4 max-w-md mx-auto">
|
/>
|
||||||
<Button
|
|
||||||
variant="outline"
|
<Input
|
||||||
onClick={() => {
|
type={showPassword ? 'text' : 'password'}
|
||||||
setCurrentStep('personal');
|
label="Contraseña"
|
||||||
setIsEmailVerificationSent(false);
|
placeholder="Contraseña segura"
|
||||||
setFormData({
|
value={formData.password}
|
||||||
full_name: '',
|
onChange={handleInputChange('password')}
|
||||||
email: '',
|
error={errors.password}
|
||||||
phone: '',
|
disabled={isLoading}
|
||||||
tenant_name: '',
|
autoComplete="new-password"
|
||||||
bakery_type: 'traditional',
|
required
|
||||||
address: '',
|
leftIcon={
|
||||||
city: '',
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
postal_code: '',
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||||
country: 'España',
|
</svg>
|
||||||
password: '',
|
}
|
||||||
confirmPassword: '',
|
rightIcon={
|
||||||
acceptTerms: false,
|
<button
|
||||||
acceptPrivacy: false,
|
type="button"
|
||||||
acceptMarketing: false
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
});
|
className="text-text-secondary hover:text-text-primary"
|
||||||
setErrors({});
|
aria-label={showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
|
||||||
setCompletedSteps(new Set());
|
|
||||||
}}
|
|
||||||
className="w-full"
|
|
||||||
>
|
>
|
||||||
Registrar otra cuenta
|
{showPassword ? (
|
||||||
</Button>
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
|
label="Confirmar Contraseña"
|
||||||
|
placeholder="Repite tu contraseña"
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
onChange={handleInputChange('confirmPassword')}
|
||||||
|
error={errors.confirmPassword}
|
||||||
|
disabled={isLoading}
|
||||||
|
autoComplete="new-password"
|
||||||
|
required
|
||||||
|
leftIcon={
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
rightIcon={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
className="text-text-secondary hover:text-text-primary"
|
||||||
|
aria-label={showConfirmPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-4 pt-4 border-t border-border-primary">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="acceptTerms"
|
||||||
|
checked={formData.acceptTerms}
|
||||||
|
onChange={handleInputChange('acceptTerms')}
|
||||||
|
className="mt-1 h-4 w-4 rounded border-border-primary text-color-primary focus:ring-color-primary focus:ring-offset-0"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<label htmlFor="acceptTerms" className="text-sm text-text-secondary cursor-pointer">
|
||||||
|
Acepto los{' '}
|
||||||
|
<a href="#" className="text-color-primary hover:text-color-primary-dark underline">
|
||||||
|
términos y condiciones
|
||||||
|
</a>{' '}
|
||||||
|
de uso
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{errors.acceptTerms && (
|
||||||
|
<p className="text-color-error text-sm ml-7">{errors.acceptTerms}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
isLoading={isLoading}
|
||||||
|
loadingText="Creando cuenta..."
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Crear Cuenta
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-color-error/10 border border-color-error/20 text-color-error px-4 py-3 rounded-lg text-sm flex items-start space-x-3" role="alert">
|
||||||
|
<svg className="w-5 h-5 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
|
||||||
{/* Login Link */}
|
{/* Login Link */}
|
||||||
{onLoginClick && currentStep !== 'verification' && (
|
{onLoginClick && (
|
||||||
<div className="mt-8 text-center border-t border-border-primary pt-6">
|
<div className="mt-8 text-center border-t border-border-primary pt-6">
|
||||||
<p className="text-text-secondary mb-4">
|
<p className="text-text-secondary mb-4">
|
||||||
¿Ya tienes una cuenta?
|
¿Ya tienes una cuenta?
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import { clsx } from 'clsx';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuthUser, useIsAuthenticated, useAuthActions } from '../../../stores';
|
import { useAuthUser, useIsAuthenticated, useAuthActions } from '../../../stores';
|
||||||
import { useTheme } from '../../../contexts/ThemeContext';
|
import { useTheme } from '../../../contexts/ThemeContext';
|
||||||
|
import { useBakery } from '../../../contexts/BakeryContext';
|
||||||
import { Button } from '../../ui';
|
import { Button } from '../../ui';
|
||||||
import { Avatar } from '../../ui';
|
import { Avatar } from '../../ui';
|
||||||
import { Badge } from '../../ui';
|
import { Badge } from '../../ui';
|
||||||
import { Modal } from '../../ui';
|
import { Modal } from '../../ui';
|
||||||
|
import { BakerySelector } from '../../ui/BakerySelector/BakerySelector';
|
||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
Search,
|
Search,
|
||||||
@@ -100,6 +102,7 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
|||||||
const isAuthenticated = useIsAuthenticated();
|
const isAuthenticated = useIsAuthenticated();
|
||||||
const { logout } = useAuthActions();
|
const { logout } = useAuthActions();
|
||||||
const { theme, resolvedTheme, setTheme } = useTheme();
|
const { theme, resolvedTheme, setTheme } = useTheme();
|
||||||
|
const { bakeries, currentBakery, selectBakery } = useBakery();
|
||||||
|
|
||||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
||||||
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
||||||
@@ -216,7 +219,7 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
|||||||
aria-label="Navegación principal"
|
aria-label="Navegación principal"
|
||||||
>
|
>
|
||||||
{/* Left section */}
|
{/* Left section */}
|
||||||
<div className="flex items-center gap-4 flex-1 min-w-0 h-full">
|
<div className="flex items-center gap-2 sm:gap-4 flex-1 min-w-0 h-full">
|
||||||
{/* Mobile menu button */}
|
{/* Mobile menu button */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -229,7 +232,7 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-shrink-0">
|
||||||
{logo || (
|
{logo || (
|
||||||
<>
|
<>
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-lg flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
|
<div className="w-8 h-8 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-lg flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
|
||||||
@@ -237,7 +240,7 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
|||||||
</div>
|
</div>
|
||||||
<h1 className={clsx(
|
<h1 className={clsx(
|
||||||
'font-semibold text-[var(--text-primary)] transition-opacity duration-300',
|
'font-semibold text-[var(--text-primary)] transition-opacity duration-300',
|
||||||
'hidden sm:block text-lg leading-tight',
|
'hidden md:block text-lg leading-tight whitespace-nowrap',
|
||||||
'self-center',
|
'self-center',
|
||||||
sidebarCollapsed ? 'lg:block' : 'lg:hidden xl:block'
|
sidebarCollapsed ? 'lg:block' : 'lg:hidden xl:block'
|
||||||
)}>
|
)}>
|
||||||
@@ -247,6 +250,40 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Bakery Selector - Desktop */}
|
||||||
|
{isAuthenticated && currentBakery && bakeries.length > 0 && (
|
||||||
|
<div className="hidden md:block mx-2 lg:mx-4 flex-shrink-0">
|
||||||
|
<BakerySelector
|
||||||
|
bakeries={bakeries}
|
||||||
|
selectedBakery={currentBakery}
|
||||||
|
onSelectBakery={selectBakery}
|
||||||
|
onAddBakery={() => {
|
||||||
|
// TODO: Navigate to add bakery page or open modal
|
||||||
|
console.log('Add new bakery');
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
className="min-w-[160px] max-w-[220px] lg:min-w-[200px] lg:max-w-[280px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bakery Selector - Mobile (in title area) */}
|
||||||
|
{isAuthenticated && currentBakery && bakeries.length > 0 && (
|
||||||
|
<div className="md:hidden flex-1 min-w-0 ml-3">
|
||||||
|
<BakerySelector
|
||||||
|
bakeries={bakeries}
|
||||||
|
selectedBakery={currentBakery}
|
||||||
|
onSelectBakery={selectBakery}
|
||||||
|
onAddBakery={() => {
|
||||||
|
// TODO: Navigate to add bakery page or open modal
|
||||||
|
console.log('Add new bakery');
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
className="w-full max-w-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
{showSearch && isAuthenticated && (
|
{showSearch && isAuthenticated && (
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
GraduationCap,
|
GraduationCap,
|
||||||
Bell,
|
Bell,
|
||||||
Settings,
|
Settings,
|
||||||
|
User,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -93,6 +94,7 @@ const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
|||||||
training: GraduationCap,
|
training: GraduationCap,
|
||||||
notifications: Bell,
|
notifications: Bell,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
|
user: User,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
312
frontend/src/components/ui/BakerySelector/BakerySelector.tsx
Normal file
312
frontend/src/components/ui/BakerySelector/BakerySelector.tsx
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
import { ChevronDown, Building, Check, Plus } from 'lucide-react';
|
||||||
|
import { Button } from '../Button';
|
||||||
|
import { Avatar } from '../Avatar';
|
||||||
|
|
||||||
|
interface Bakery {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
logo?: string;
|
||||||
|
role: 'owner' | 'manager' | 'baker' | 'staff';
|
||||||
|
status: 'active' | 'inactive';
|
||||||
|
address?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BakerySelectorProps {
|
||||||
|
bakeries: Bakery[];
|
||||||
|
selectedBakery: Bakery;
|
||||||
|
onSelectBakery: (bakery: Bakery) => void;
|
||||||
|
onAddBakery?: () => void;
|
||||||
|
className?: string;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleLabels = {
|
||||||
|
owner: 'Propietario',
|
||||||
|
manager: 'Gerente',
|
||||||
|
baker: 'Panadero',
|
||||||
|
staff: 'Personal'
|
||||||
|
};
|
||||||
|
|
||||||
|
const roleColors = {
|
||||||
|
owner: 'text-color-success',
|
||||||
|
manager: 'text-color-info',
|
||||||
|
baker: 'text-color-warning',
|
||||||
|
staff: 'text-text-secondary'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BakerySelector: React.FC<BakerySelectorProps> = ({
|
||||||
|
bakeries,
|
||||||
|
selectedBakery,
|
||||||
|
onSelectBakery,
|
||||||
|
onAddBakery,
|
||||||
|
className,
|
||||||
|
size = 'md'
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
// Calculate dropdown position when opening
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && buttonRef.current) {
|
||||||
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const isMobile = viewportWidth < 640; // sm breakpoint
|
||||||
|
|
||||||
|
let top = rect.bottom + window.scrollY + 8;
|
||||||
|
let left = rect.left + window.scrollX;
|
||||||
|
let width = Math.max(rect.width, isMobile ? viewportWidth - 32 : 320); // 16px margin on each side for mobile
|
||||||
|
|
||||||
|
// Adjust for mobile - center dropdown with margins
|
||||||
|
if (isMobile) {
|
||||||
|
left = 16; // 16px margin from left
|
||||||
|
width = viewportWidth - 32; // 16px margins on both sides
|
||||||
|
} else {
|
||||||
|
// Adjust horizontal position to prevent overflow
|
||||||
|
const dropdownWidth = Math.max(width, 320);
|
||||||
|
if (left + dropdownWidth > viewportWidth - 16) {
|
||||||
|
left = viewportWidth - dropdownWidth - 16;
|
||||||
|
}
|
||||||
|
if (left < 16) {
|
||||||
|
left = 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust vertical position if dropdown would overflow bottom
|
||||||
|
const dropdownMaxHeight = 320; // Approximate max height
|
||||||
|
const headerHeight = 64; // Approximate header height
|
||||||
|
|
||||||
|
if (top + dropdownMaxHeight > viewportHeight + window.scrollY - 16) {
|
||||||
|
// Try to position above the button
|
||||||
|
const topPosition = rect.top + window.scrollY - dropdownMaxHeight - 8;
|
||||||
|
|
||||||
|
// Ensure it doesn't go above the header
|
||||||
|
if (topPosition < window.scrollY + headerHeight) {
|
||||||
|
// If it can't fit above, position it at the top of the visible area
|
||||||
|
top = window.scrollY + headerHeight + 8;
|
||||||
|
} else {
|
||||||
|
top = topPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDropdownPosition({ top, left, width });
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (buttonRef.current && !buttonRef.current.contains(event.target as Node) &&
|
||||||
|
dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Close on escape key and handle body scroll lock
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setIsOpen(false);
|
||||||
|
buttonRef.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
// Prevent body scroll on mobile when dropdown is open
|
||||||
|
const isMobile = window.innerWidth < 640;
|
||||||
|
if (isMobile) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
// Restore body scroll
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'h-10 px-3 text-sm sm:h-8', // Always at least 40px (10) for better touch targets on mobile
|
||||||
|
md: 'h-12 px-4 text-base sm:h-10', // 48px (12) on mobile, 40px on desktop
|
||||||
|
lg: 'h-14 px-5 text-lg sm:h-12' // 56px (14) on mobile, 48px on desktop
|
||||||
|
};
|
||||||
|
|
||||||
|
const avatarSizes = {
|
||||||
|
sm: 'sm' as const, // Changed from xs to sm for better mobile visibility
|
||||||
|
md: 'sm' as const,
|
||||||
|
lg: 'md' as const
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBakeryInitials = (name: string) => {
|
||||||
|
return name
|
||||||
|
.split(' ')
|
||||||
|
.map(word => word[0])
|
||||||
|
.join('')
|
||||||
|
.toUpperCase()
|
||||||
|
.slice(0, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx('relative', className)} ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className={clsx(
|
||||||
|
'flex items-center gap-2 sm:gap-3 bg-[var(--bg-primary)] border border-[var(--border-primary)]',
|
||||||
|
'rounded-lg transition-all duration-200 hover:bg-[var(--bg-secondary)]',
|
||||||
|
'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]',
|
||||||
|
'active:scale-[0.98] w-full',
|
||||||
|
sizeClasses[size],
|
||||||
|
isOpen && 'ring-2 ring-[var(--color-primary)]/20 border-[var(--color-primary)]'
|
||||||
|
)}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-label={`Panadería seleccionada: ${selectedBakery.name}`}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
src={selectedBakery.logo}
|
||||||
|
name={selectedBakery.name}
|
||||||
|
size={avatarSizes[size]}
|
||||||
|
className="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 text-left min-w-0">
|
||||||
|
<div className="text-[var(--text-primary)] font-medium truncate text-sm sm:text-base">
|
||||||
|
{selectedBakery.name}
|
||||||
|
</div>
|
||||||
|
{size !== 'sm' && (
|
||||||
|
<div className={clsx('text-xs truncate hidden sm:block', roleColors[selectedBakery.role])}>
|
||||||
|
{roleLabels[selectedBakery.role]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChevronDown
|
||||||
|
className={clsx(
|
||||||
|
'flex-shrink-0 transition-transform duration-200 text-[var(--text-secondary)]',
|
||||||
|
size === 'sm' ? 'w-4 h-4' : 'w-4 h-4', // Consistent sizing
|
||||||
|
isOpen && 'rotate-180'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && createPortal(
|
||||||
|
<>
|
||||||
|
{/* Mobile backdrop */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/20 z-[9998] sm:hidden"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={dropdownRef}
|
||||||
|
className="fixed bg-[var(--bg-primary)] border border-[var(--border-primary)] rounded-lg shadow-xl py-2 z-[9999] sm:min-w-80 sm:max-w-96"
|
||||||
|
style={{
|
||||||
|
top: `${dropdownPosition.top}px`,
|
||||||
|
left: `${dropdownPosition.left}px`,
|
||||||
|
width: `${dropdownPosition.width}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="px-3 py-2 text-xs font-medium text-[var(--text-tertiary)] border-b border-[var(--border-primary)]">
|
||||||
|
Mis Panaderías ({bakeries.length})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-64 sm:max-h-64 max-h-[60vh] overflow-y-auto scrollbar-thin scrollbar-thumb-[var(--border-secondary)] scrollbar-track-transparent">
|
||||||
|
{bakeries.map((bakery) => (
|
||||||
|
<button
|
||||||
|
key={bakery.id}
|
||||||
|
onClick={() => {
|
||||||
|
onSelectBakery(bakery);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
className={clsx(
|
||||||
|
'w-full flex items-center gap-3 px-3 py-4 text-left min-h-[60px] sm:py-3 sm:min-h-[48px]',
|
||||||
|
'hover:bg-[var(--bg-secondary)] active:bg-[var(--bg-tertiary)] transition-colors',
|
||||||
|
'focus:outline-none focus:bg-[var(--bg-secondary)]',
|
||||||
|
'touch-manipulation', // Improves touch responsiveness
|
||||||
|
selectedBakery.id === bakery.id && 'bg-[var(--bg-secondary)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
src={bakery.logo}
|
||||||
|
name={bakery.name}
|
||||||
|
size="sm"
|
||||||
|
className="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[var(--text-primary)] font-medium truncate">
|
||||||
|
{bakery.name}
|
||||||
|
</span>
|
||||||
|
{selectedBakery.id === bakery.id && (
|
||||||
|
<Check className="w-4 h-4 text-[var(--color-primary)] flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<span className={clsx('text-xs', roleColors[bakery.role])}>
|
||||||
|
{roleLabels[bakery.role]}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-[var(--text-tertiary)]">•</span>
|
||||||
|
<span className={clsx(
|
||||||
|
'text-xs',
|
||||||
|
bakery.status === 'active' ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'
|
||||||
|
)}>
|
||||||
|
{bakery.status === 'active' ? 'Activa' : 'Inactiva'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{bakery.address && (
|
||||||
|
<div className="text-xs text-[var(--text-tertiary)] truncate mt-1">
|
||||||
|
{bakery.address}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{onAddBakery && (
|
||||||
|
<>
|
||||||
|
<div className="border-t border-[var(--border-primary)] my-2"></div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onAddBakery();
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center gap-3 px-3 py-4 text-left min-h-[60px] sm:py-3 sm:min-h-[48px] hover:bg-[var(--bg-secondary)] active:bg-[var(--bg-tertiary)] transition-colors text-[var(--color-primary)] touch-manipulation"
|
||||||
|
>
|
||||||
|
<div className="w-8 h-8 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium">Agregar Panadería</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BakerySelector;
|
||||||
2
frontend/src/components/ui/BakerySelector/index.ts
Normal file
2
frontend/src/components/ui/BakerySelector/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { BakerySelector } from './BakerySelector';
|
||||||
|
export type { default as BakerySelector } from './BakerySelector';
|
||||||
@@ -16,6 +16,7 @@ export { ListItem } from './ListItem';
|
|||||||
export { StatsCard, StatsGrid } from './Stats';
|
export { StatsCard, StatsGrid } from './Stats';
|
||||||
export { StatusCard, getStatusColor } from './StatusCard';
|
export { StatusCard, getStatusColor } from './StatusCard';
|
||||||
export { StatusModal } from './StatusModal';
|
export { StatusModal } from './StatusModal';
|
||||||
|
export { BakerySelector } from './BakerySelector';
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export type { ButtonProps } from './Button';
|
export type { ButtonProps } from './Button';
|
||||||
|
|||||||
359
frontend/src/contexts/BakeryContext.tsx
Normal file
359
frontend/src/contexts/BakeryContext.tsx
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import React, { createContext, useContext, useReducer, useEffect, ReactNode } from 'react';
|
||||||
|
|
||||||
|
export interface Bakery {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
logo?: string;
|
||||||
|
role: 'owner' | 'manager' | 'baker' | 'staff';
|
||||||
|
status: 'active' | 'inactive';
|
||||||
|
address?: string;
|
||||||
|
description?: string;
|
||||||
|
settings?: {
|
||||||
|
timezone: string;
|
||||||
|
currency: string;
|
||||||
|
language: string;
|
||||||
|
};
|
||||||
|
permissions?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BakeryState {
|
||||||
|
bakeries: Bakery[];
|
||||||
|
currentBakery: Bakery | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type BakeryAction =
|
||||||
|
| { type: 'SET_LOADING'; payload: boolean }
|
||||||
|
| { type: 'SET_ERROR'; payload: string | null }
|
||||||
|
| { type: 'SET_BAKERIES'; payload: Bakery[] }
|
||||||
|
| { type: 'SET_CURRENT_BAKERY'; payload: Bakery }
|
||||||
|
| { type: 'ADD_BAKERY'; payload: Bakery }
|
||||||
|
| { type: 'UPDATE_BAKERY'; payload: { id: string; updates: Partial<Bakery> } }
|
||||||
|
| { type: 'REMOVE_BAKERY'; payload: string };
|
||||||
|
|
||||||
|
const initialState: BakeryState = {
|
||||||
|
bakeries: [],
|
||||||
|
currentBakery: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock bakeries data
|
||||||
|
const mockBakeries: Bakery[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Panadería San Miguel',
|
||||||
|
logo: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=100&h=100&fit=crop&crop=center',
|
||||||
|
role: 'owner',
|
||||||
|
status: 'active',
|
||||||
|
address: 'Calle Mayor 123, Madrid, 28013',
|
||||||
|
description: 'Panadería tradicional familiar con más de 30 años de experiencia.',
|
||||||
|
settings: {
|
||||||
|
timezone: 'Europe/Madrid',
|
||||||
|
currency: 'EUR',
|
||||||
|
language: 'es',
|
||||||
|
},
|
||||||
|
permissions: ['*'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Panadería La Artesana',
|
||||||
|
logo: 'https://images.unsplash.com/photo-1509440159596-0249088772ff?w=100&h=100&fit=crop&crop=center',
|
||||||
|
role: 'manager',
|
||||||
|
status: 'active',
|
||||||
|
address: 'Avenida de la Paz 45, Barcelona, 08001',
|
||||||
|
description: 'Panadería artesanal especializada en panes tradicionales catalanes.',
|
||||||
|
settings: {
|
||||||
|
timezone: 'Europe/Madrid',
|
||||||
|
currency: 'EUR',
|
||||||
|
language: 'ca',
|
||||||
|
},
|
||||||
|
permissions: ['inventory:*', 'production:*', 'sales:*', 'reports:read'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Pan y Masa',
|
||||||
|
logo: 'https://images.unsplash.com/photo-1524704654690-b56c05c78a00?w=100&h=100&fit=crop&crop=center',
|
||||||
|
role: 'baker',
|
||||||
|
status: 'active',
|
||||||
|
address: 'Plaza Central 8, Valencia, 46001',
|
||||||
|
description: 'Panadería moderna con enfoque en productos orgánicos.',
|
||||||
|
settings: {
|
||||||
|
timezone: 'Europe/Madrid',
|
||||||
|
currency: 'EUR',
|
||||||
|
language: 'es',
|
||||||
|
},
|
||||||
|
permissions: ['production:*', 'inventory:read', 'inventory:update'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'Horno Dorado',
|
||||||
|
role: 'staff',
|
||||||
|
status: 'inactive',
|
||||||
|
address: 'Calle del Sol 67, Sevilla, 41001',
|
||||||
|
description: 'Panadería tradicional andaluza.',
|
||||||
|
settings: {
|
||||||
|
timezone: 'Europe/Madrid',
|
||||||
|
currency: 'EUR',
|
||||||
|
language: 'es',
|
||||||
|
},
|
||||||
|
permissions: ['inventory:read', 'sales:read'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function bakeryReducer(state: BakeryState, action: BakeryAction): BakeryState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_LOADING':
|
||||||
|
return { ...state, isLoading: action.payload };
|
||||||
|
|
||||||
|
case 'SET_ERROR':
|
||||||
|
return { ...state, error: action.payload, isLoading: false };
|
||||||
|
|
||||||
|
case 'SET_BAKERIES':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
bakeries: action.payload,
|
||||||
|
currentBakery: state.currentBakery || action.payload[0] || null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_CURRENT_BAKERY':
|
||||||
|
// Save to localStorage for persistence
|
||||||
|
localStorage.setItem('selectedBakery', action.payload.id);
|
||||||
|
return { ...state, currentBakery: action.payload };
|
||||||
|
|
||||||
|
case 'ADD_BAKERY':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
bakeries: [...state.bakeries, action.payload]
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_BAKERY':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
bakeries: state.bakeries.map(bakery =>
|
||||||
|
bakery.id === action.payload.id
|
||||||
|
? { ...bakery, ...action.payload.updates }
|
||||||
|
: bakery
|
||||||
|
),
|
||||||
|
currentBakery: state.currentBakery?.id === action.payload.id
|
||||||
|
? { ...state.currentBakery, ...action.payload.updates }
|
||||||
|
: state.currentBakery
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'REMOVE_BAKERY':
|
||||||
|
const remainingBakeries = state.bakeries.filter(b => b.id !== action.payload);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
bakeries: remainingBakeries,
|
||||||
|
currentBakery: state.currentBakery?.id === action.payload
|
||||||
|
? remainingBakeries[0] || null
|
||||||
|
: state.currentBakery
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BakeryContextType extends BakeryState {
|
||||||
|
selectBakery: (bakery: Bakery) => void;
|
||||||
|
addBakery: (bakery: Omit<Bakery, 'id'>) => Promise<void>;
|
||||||
|
updateBakery: (id: string, updates: Partial<Bakery>) => Promise<void>;
|
||||||
|
removeBakery: (id: string) => Promise<void>;
|
||||||
|
refreshBakeries: () => Promise<void>;
|
||||||
|
hasPermission: (permission: string) => boolean;
|
||||||
|
canAccess: (resource: string, action: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BakeryContext = createContext<BakeryContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
interface BakeryProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BakeryProvider({ children }: BakeryProviderProps) {
|
||||||
|
const [state, dispatch] = useReducer(bakeryReducer, initialState);
|
||||||
|
|
||||||
|
// Load bakeries on mount
|
||||||
|
useEffect(() => {
|
||||||
|
loadBakeries();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load saved bakery selection
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.bakeries.length > 0 && !state.currentBakery) {
|
||||||
|
const savedBakeryId = localStorage.getItem('selectedBakery');
|
||||||
|
const savedBakery = savedBakeryId
|
||||||
|
? state.bakeries.find(b => b.id === savedBakeryId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (savedBakery) {
|
||||||
|
dispatch({ type: 'SET_CURRENT_BAKERY', payload: savedBakery });
|
||||||
|
} else if (state.bakeries[0]) {
|
||||||
|
dispatch({ type: 'SET_CURRENT_BAKERY', payload: state.bakeries[0] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [state.bakeries, state.currentBakery]);
|
||||||
|
|
||||||
|
const loadBakeries = async () => {
|
||||||
|
try {
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: true });
|
||||||
|
|
||||||
|
// Simulate API call delay
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// In a real app, this would be an API call
|
||||||
|
dispatch({ type: 'SET_BAKERIES', payload: mockBakeries });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_ERROR',
|
||||||
|
payload: error instanceof Error ? error.message : 'Error loading bakeries'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectBakery = (bakery: Bakery) => {
|
||||||
|
dispatch({ type: 'SET_CURRENT_BAKERY', payload: bakery });
|
||||||
|
};
|
||||||
|
|
||||||
|
const addBakery = async (bakeryData: Omit<Bakery, 'id'>) => {
|
||||||
|
try {
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: true });
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
const newBakery: Bakery = {
|
||||||
|
...bakeryData,
|
||||||
|
id: Date.now().toString(), // In real app, this would come from the API
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch({ type: 'ADD_BAKERY', payload: newBakery });
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: false });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_ERROR',
|
||||||
|
payload: error instanceof Error ? error.message : 'Error adding bakery'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBakery = async (id: string, updates: Partial<Bakery>) => {
|
||||||
|
try {
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: true });
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
dispatch({ type: 'UPDATE_BAKERY', payload: { id, updates } });
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: false });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_ERROR',
|
||||||
|
payload: error instanceof Error ? error.message : 'Error updating bakery'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeBakery = async (id: string) => {
|
||||||
|
try {
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: true });
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
dispatch({ type: 'REMOVE_BAKERY', payload: id });
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: false });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_ERROR',
|
||||||
|
payload: error instanceof Error ? error.message : 'Error removing bakery'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshBakeries = async () => {
|
||||||
|
await loadBakeries();
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasPermission = (permission: string): boolean => {
|
||||||
|
if (!state.currentBakery) return false;
|
||||||
|
|
||||||
|
const permissions = state.currentBakery.permissions || [];
|
||||||
|
|
||||||
|
// Admin/owner has all permissions
|
||||||
|
if (permissions.includes('*')) return true;
|
||||||
|
|
||||||
|
return permissions.includes(permission);
|
||||||
|
};
|
||||||
|
|
||||||
|
const canAccess = (resource: string, action: string): boolean => {
|
||||||
|
if (!state.currentBakery) return false;
|
||||||
|
|
||||||
|
// Check specific permission
|
||||||
|
if (hasPermission(`${resource}:${action}`)) return true;
|
||||||
|
|
||||||
|
// Check wildcard permissions
|
||||||
|
if (hasPermission(`${resource}:*`)) return true;
|
||||||
|
|
||||||
|
// Role-based access fallback
|
||||||
|
switch (state.currentBakery.role) {
|
||||||
|
case 'owner':
|
||||||
|
return true;
|
||||||
|
case 'manager':
|
||||||
|
return ['inventory', 'production', 'sales', 'reports'].includes(resource);
|
||||||
|
case 'baker':
|
||||||
|
return ['production', 'inventory'].includes(resource) &&
|
||||||
|
['read', 'update'].includes(action);
|
||||||
|
case 'staff':
|
||||||
|
return ['inventory', 'sales'].includes(resource) &&
|
||||||
|
action === 'read';
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const value: BakeryContextType = {
|
||||||
|
...state,
|
||||||
|
selectBakery,
|
||||||
|
addBakery,
|
||||||
|
updateBakery,
|
||||||
|
removeBakery,
|
||||||
|
refreshBakeries,
|
||||||
|
hasPermission,
|
||||||
|
canAccess,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BakeryContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</BakeryContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBakery(): BakeryContextType {
|
||||||
|
const context = useContext(BakeryContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useBakery must be used within a BakeryProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience hooks
|
||||||
|
export const useCurrentBakery = () => {
|
||||||
|
const { currentBakery } = useBakery();
|
||||||
|
return currentBakery;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBakeries = () => {
|
||||||
|
const { bakeries } = useBakery();
|
||||||
|
return bakeries;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBakeryPermissions = () => {
|
||||||
|
const { hasPermission, canAccess } = useBakery();
|
||||||
|
return { hasPermission, canAccess };
|
||||||
|
};
|
||||||
@@ -1,66 +1,76 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, RotateCcw } from 'lucide-react';
|
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, X, Edit3 } from 'lucide-react';
|
||||||
import { Button, Card, Input } from '../../../../components/ui';
|
import { Button, Card, Input, Select } from '../../../../components/ui';
|
||||||
import { PageHeader } from '../../../../components/layout';
|
import { PageHeader } from '../../../../components/layout';
|
||||||
|
import { useToast } from '../../../../hooks/ui/useToast';
|
||||||
|
|
||||||
|
interface BakeryConfig {
|
||||||
|
// General Info
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
website: string;
|
||||||
|
// Location
|
||||||
|
address: string;
|
||||||
|
city: string;
|
||||||
|
postalCode: string;
|
||||||
|
country: string;
|
||||||
|
// Business
|
||||||
|
taxId: string;
|
||||||
|
currency: string;
|
||||||
|
timezone: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BusinessHours {
|
||||||
|
[key: string]: {
|
||||||
|
open: string;
|
||||||
|
close: string;
|
||||||
|
closed: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const BakeryConfigPage: React.FC = () => {
|
const BakeryConfigPage: React.FC = () => {
|
||||||
const [config, setConfig] = useState({
|
const { showToast } = useToast();
|
||||||
general: {
|
|
||||||
name: 'Panadería Artesanal San Miguel',
|
const [activeTab, setActiveTab] = useState<'general' | 'location' | 'business' | 'hours'>('general');
|
||||||
description: 'Panadería tradicional con más de 30 años de experiencia',
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
logo: '',
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
website: 'https://panaderiasanmiguel.com',
|
|
||||||
email: 'info@panaderiasanmiguel.com',
|
const [config, setConfig] = useState<BakeryConfig>({
|
||||||
phone: '+34 912 345 678'
|
name: 'Panadería Artesanal San Miguel',
|
||||||
},
|
description: 'Panadería tradicional con más de 30 años de experiencia',
|
||||||
location: {
|
email: 'info@panaderiasanmiguel.com',
|
||||||
address: 'Calle Mayor 123',
|
phone: '+34 912 345 678',
|
||||||
city: 'Madrid',
|
website: 'https://panaderiasanmiguel.com',
|
||||||
postalCode: '28001',
|
address: 'Calle Mayor 123',
|
||||||
country: 'España',
|
city: 'Madrid',
|
||||||
coordinates: {
|
postalCode: '28001',
|
||||||
lat: 40.4168,
|
country: 'España',
|
||||||
lng: -3.7038
|
taxId: 'B12345678',
|
||||||
}
|
currency: 'EUR',
|
||||||
},
|
timezone: 'Europe/Madrid',
|
||||||
schedule: {
|
language: 'es'
|
||||||
monday: { open: '07:00', close: '20:00', closed: false },
|
|
||||||
tuesday: { open: '07:00', close: '20:00', closed: false },
|
|
||||||
wednesday: { open: '07:00', close: '20:00', closed: false },
|
|
||||||
thursday: { open: '07:00', close: '20:00', closed: false },
|
|
||||||
friday: { open: '07:00', close: '20:00', closed: false },
|
|
||||||
saturday: { open: '08:00', close: '14:00', closed: false },
|
|
||||||
sunday: { open: '09:00', close: '13:00', closed: false }
|
|
||||||
},
|
|
||||||
business: {
|
|
||||||
taxId: 'B12345678',
|
|
||||||
registrationNumber: 'REG-2024-001',
|
|
||||||
licenseNumber: 'LIC-FOOD-2024',
|
|
||||||
currency: 'EUR',
|
|
||||||
timezone: 'Europe/Madrid',
|
|
||||||
language: 'es'
|
|
||||||
},
|
|
||||||
preferences: {
|
|
||||||
enableOnlineOrders: true,
|
|
||||||
enableReservations: false,
|
|
||||||
enableDelivery: true,
|
|
||||||
deliveryRadius: 5,
|
|
||||||
minimumOrderAmount: 15.00,
|
|
||||||
enableLoyaltyProgram: true,
|
|
||||||
autoBackup: true,
|
|
||||||
emailNotifications: true,
|
|
||||||
smsNotifications: false
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [businessHours, setBusinessHours] = useState<BusinessHours>({
|
||||||
const [activeTab, setActiveTab] = useState('general');
|
monday: { open: '07:00', close: '20:00', closed: false },
|
||||||
|
tuesday: { open: '07:00', close: '20:00', closed: false },
|
||||||
|
wednesday: { open: '07:00', close: '20:00', closed: false },
|
||||||
|
thursday: { open: '07:00', close: '20:00', closed: false },
|
||||||
|
friday: { open: '07:00', close: '20:00', closed: false },
|
||||||
|
saturday: { open: '08:00', close: '14:00', closed: false },
|
||||||
|
sunday: { open: '09:00', close: '13:00', closed: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'general', label: 'General', icon: Store },
|
{ id: 'general' as const, label: 'General', icon: Store },
|
||||||
{ id: 'location', label: 'Ubicación', icon: MapPin },
|
{ id: 'location' as const, label: 'Ubicación', icon: MapPin },
|
||||||
{ id: 'schedule', label: 'Horarios', icon: Clock },
|
{ id: 'business' as const, label: 'Empresa', icon: Globe },
|
||||||
{ id: 'business', label: 'Empresa', icon: Globe }
|
{ id: 'hours' as const, label: 'Horarios', icon: Clock }
|
||||||
];
|
];
|
||||||
|
|
||||||
const daysOfWeek = [
|
const daysOfWeek = [
|
||||||
@@ -73,40 +83,94 @@ const BakeryConfigPage: React.FC = () => {
|
|||||||
{ key: 'sunday', label: 'Domingo' }
|
{ key: 'sunday', label: 'Domingo' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleInputChange = (section: string, field: string, value: any) => {
|
const currencyOptions = [
|
||||||
setConfig(prev => ({
|
{ value: 'EUR', label: 'EUR (€)' },
|
||||||
|
{ value: 'USD', label: 'USD ($)' },
|
||||||
|
{ value: 'GBP', label: 'GBP (£)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const timezoneOptions = [
|
||||||
|
{ value: 'Europe/Madrid', label: 'Madrid (CET/CEST)' },
|
||||||
|
{ value: 'Atlantic/Canary', label: 'Canarias (WET/WEST)' },
|
||||||
|
{ value: 'Europe/London', label: 'Londres (GMT/BST)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const languageOptions = [
|
||||||
|
{ value: 'es', label: 'Español' },
|
||||||
|
{ value: 'ca', label: 'Català' },
|
||||||
|
{ value: 'en', label: 'English' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const validateConfig = (): boolean => {
|
||||||
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (!config.name.trim()) {
|
||||||
|
newErrors.name = 'El nombre es requerido';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.email.trim()) {
|
||||||
|
newErrors.email = 'El email es requerido';
|
||||||
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(config.email)) {
|
||||||
|
newErrors.email = 'Email inválido';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.address.trim()) {
|
||||||
|
newErrors.address = 'La dirección es requerida';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.city.trim()) {
|
||||||
|
newErrors.city = 'La ciudad es requerida';
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveConfig = async () => {
|
||||||
|
if (!validateConfig()) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
setIsEditing(false);
|
||||||
|
showToast({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Configuración actualizada',
|
||||||
|
message: 'Los datos de la panadería han sido guardados correctamente'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showToast({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: 'No se pudo actualizar la configuración'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (field: keyof BakeryConfig) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
setConfig(prev => ({ ...prev, [field]: e.target.value }));
|
||||||
|
if (errors[field]) {
|
||||||
|
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (field: keyof BakeryConfig) => (value: string) => {
|
||||||
|
setConfig(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHoursChange = (day: string, field: 'open' | 'close' | 'closed', value: string | boolean) => {
|
||||||
|
setBusinessHours(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[section]: {
|
[day]: {
|
||||||
...prev[section as keyof typeof prev],
|
...prev[day],
|
||||||
[field]: value
|
[field]: value
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
setHasChanges(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScheduleChange = (day: string, field: string, value: any) => {
|
|
||||||
setConfig(prev => ({
|
|
||||||
...prev,
|
|
||||||
schedule: {
|
|
||||||
...prev.schedule,
|
|
||||||
[day]: {
|
|
||||||
...prev.schedule[day as keyof typeof prev.schedule],
|
|
||||||
[field]: value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
setHasChanges(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
// Handle save logic
|
|
||||||
console.log('Saving bakery config:', config);
|
|
||||||
setHasChanges(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
// Reset to defaults
|
|
||||||
setHasChanges(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -114,366 +178,302 @@ const BakeryConfigPage: React.FC = () => {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Configuración de Panadería"
|
title="Configuración de Panadería"
|
||||||
description="Configura los datos básicos y preferencias de tu panadería"
|
description="Configura los datos básicos y preferencias de tu panadería"
|
||||||
action={
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button variant="outline" onClick={handleReset}>
|
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
|
||||||
Restaurar
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSave} disabled={!hasChanges}>
|
|
||||||
<Save className="w-4 h-4 mr-2" />
|
|
||||||
Guardar Cambios
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row gap-6">
|
{/* Bakery Header */}
|
||||||
{/* Sidebar */}
|
<Card className="p-6">
|
||||||
<div className="w-full lg:w-64">
|
<div className="flex items-center gap-6">
|
||||||
<Card className="p-4">
|
<div className="w-16 h-16 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-lg flex items-center justify-center text-white font-bold text-xl">
|
||||||
<nav className="space-y-2">
|
{config.name.charAt(0)}
|
||||||
{tabs.map((tab) => (
|
</div>
|
||||||
<button
|
<div className="flex-1">
|
||||||
key={tab.id}
|
<h1 className="text-2xl font-bold text-text-primary mb-1">
|
||||||
onClick={() => setActiveTab(tab.id)}
|
{config.name}
|
||||||
className={`w-full flex items-center space-x-3 px-3 py-2 text-left rounded-lg transition-colors ${
|
</h1>
|
||||||
activeTab === tab.id
|
<p className="text-text-secondary">{config.email}</p>
|
||||||
? 'bg-[var(--color-info)]/10 text-[var(--color-info)]'
|
<p className="text-text-tertiary text-sm">{config.address}, {config.city}</p>
|
||||||
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
|
</div>
|
||||||
}`}
|
<div className="flex gap-2">
|
||||||
>
|
{!isEditing && (
|
||||||
<tab.icon className="w-4 h-4" />
|
<Button
|
||||||
<span className="text-sm font-medium">{tab.label}</span>
|
variant="outline"
|
||||||
</button>
|
onClick={() => setIsEditing(true)}
|
||||||
))}
|
className="flex items-center gap-2"
|
||||||
</nav>
|
>
|
||||||
</Card>
|
<Edit3 className="w-4 h-4" />
|
||||||
|
Editar Configuración
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Configuration Tabs */}
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className="border-b border-border-primary">
|
||||||
|
<nav className="flex">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`flex items-center gap-2 px-6 py-4 text-sm font-medium transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'text-color-primary border-b-2 border-color-primary bg-color-primary/5'
|
||||||
|
: 'text-text-secondary hover:text-text-primary hover:bg-bg-secondary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<tab.icon className="w-4 h-4" />
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Tab Content */}
|
||||||
<div className="flex-1">
|
<div className="p-6">
|
||||||
{activeTab === 'general' && (
|
{activeTab === 'general' && (
|
||||||
<Card className="p-6">
|
<div className="space-y-6">
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Información General</h3>
|
<h3 className="text-lg font-semibold text-text-primary">Información General</h3>
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
label="Nombre de la Panadería"
|
||||||
Nombre de la Panadería
|
value={config.name}
|
||||||
</label>
|
onChange={handleInputChange('name')}
|
||||||
<Input
|
error={errors.name}
|
||||||
value={config.general.name}
|
disabled={!isEditing || isLoading}
|
||||||
onChange={(e) => handleInputChange('general', 'name', e.target.value)}
|
placeholder="Nombre de tu panadería"
|
||||||
placeholder="Nombre de tu panadería"
|
leftIcon={<Store className="w-4 h-4" />}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
<Input
|
||||||
<div>
|
type="email"
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
label="Email de Contacto"
|
||||||
Sitio Web
|
value={config.email}
|
||||||
</label>
|
onChange={handleInputChange('email')}
|
||||||
<Input
|
error={errors.email}
|
||||||
value={config.general.website}
|
disabled={!isEditing || isLoading}
|
||||||
onChange={(e) => handleInputChange('general', 'website', e.target.value)}
|
placeholder="contacto@panaderia.com"
|
||||||
placeholder="https://tu-panaderia.com"
|
leftIcon={<Mail className="w-4 h-4" />}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
<Input
|
||||||
|
type="tel"
|
||||||
<div>
|
label="Teléfono"
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
value={config.phone}
|
||||||
Descripción
|
onChange={handleInputChange('phone')}
|
||||||
</label>
|
error={errors.phone}
|
||||||
<textarea
|
disabled={!isEditing || isLoading}
|
||||||
value={config.general.description}
|
placeholder="+34 912 345 678"
|
||||||
onChange={(e) => handleInputChange('general', 'description', e.target.value)}
|
leftIcon={<Phone className="w-4 h-4" />}
|
||||||
rows={3}
|
/>
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
|
||||||
placeholder="Describe tu panadería..."
|
<Input
|
||||||
/>
|
label="Sitio Web"
|
||||||
</div>
|
value={config.website}
|
||||||
|
onChange={handleInputChange('website')}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
disabled={!isEditing || isLoading}
|
||||||
<div>
|
placeholder="https://tu-panaderia.com"
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
leftIcon={<Globe className="w-4 h-4" />}
|
||||||
Email de Contacto
|
className="md:col-span-2 xl:col-span-3"
|
||||||
</label>
|
/>
|
||||||
<div className="relative">
|
|
||||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
|
||||||
<Input
|
|
||||||
value={config.general.email}
|
|
||||||
onChange={(e) => handleInputChange('general', 'email', e.target.value)}
|
|
||||||
className="pl-10"
|
|
||||||
type="email"
|
|
||||||
placeholder="contacto@panaderia.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Teléfono
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
|
||||||
<Input
|
|
||||||
value={config.general.phone}
|
|
||||||
onChange={(e) => handleInputChange('general', 'phone', e.target.value)}
|
|
||||||
className="pl-10"
|
|
||||||
type="tel"
|
|
||||||
placeholder="+34 912 345 678"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-text-secondary mb-2">
|
||||||
|
Descripción
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={config.description}
|
||||||
|
onChange={handleInputChange('description')}
|
||||||
|
disabled={!isEditing || isLoading}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg resize-none bg-[var(--bg-primary)] text-[var(--text-primary)] disabled:bg-[var(--bg-secondary)] disabled:text-[var(--text-secondary)]"
|
||||||
|
placeholder="Describe tu panadería..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'location' && (
|
{activeTab === 'location' && (
|
||||||
<Card className="p-6">
|
<div className="space-y-6">
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Ubicación</h3>
|
<h3 className="text-lg font-semibold text-text-primary">Ubicación</h3>
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<Input
|
||||||
Dirección
|
label="Dirección"
|
||||||
</label>
|
value={config.address}
|
||||||
<Input
|
onChange={handleInputChange('address')}
|
||||||
value={config.location.address}
|
error={errors.address}
|
||||||
onChange={(e) => handleInputChange('location', 'address', e.target.value)}
|
disabled={!isEditing || isLoading}
|
||||||
placeholder="Calle, número, etc."
|
placeholder="Calle, número, etc."
|
||||||
/>
|
leftIcon={<MapPin className="w-4 h-4" />}
|
||||||
</div>
|
className="md:col-span-2"
|
||||||
|
/>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<div>
|
<Input
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
label="Ciudad"
|
||||||
Ciudad
|
value={config.city}
|
||||||
</label>
|
onChange={handleInputChange('city')}
|
||||||
<Input
|
error={errors.city}
|
||||||
value={config.location.city}
|
disabled={!isEditing || isLoading}
|
||||||
onChange={(e) => handleInputChange('location', 'city', e.target.value)}
|
placeholder="Ciudad"
|
||||||
placeholder="Ciudad"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
<Input
|
||||||
|
label="Código Postal"
|
||||||
<div>
|
value={config.postalCode}
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
onChange={handleInputChange('postalCode')}
|
||||||
Código Postal
|
disabled={!isEditing || isLoading}
|
||||||
</label>
|
placeholder="28001"
|
||||||
<Input
|
/>
|
||||||
value={config.location.postalCode}
|
|
||||||
onChange={(e) => handleInputChange('location', 'postalCode', e.target.value)}
|
<Input
|
||||||
placeholder="28001"
|
label="País"
|
||||||
/>
|
value={config.country}
|
||||||
</div>
|
onChange={handleInputChange('country')}
|
||||||
|
disabled={!isEditing || isLoading}
|
||||||
<div>
|
placeholder="España"
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
/>
|
||||||
País
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={config.location.country}
|
|
||||||
onChange={(e) => handleInputChange('location', 'country', e.target.value)}
|
|
||||||
placeholder="España"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Latitud
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={config.location.coordinates.lat}
|
|
||||||
onChange={(e) => handleInputChange('location', 'coordinates', {
|
|
||||||
...config.location.coordinates,
|
|
||||||
lat: parseFloat(e.target.value) || 0
|
|
||||||
})}
|
|
||||||
type="number"
|
|
||||||
step="0.000001"
|
|
||||||
placeholder="40.4168"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Longitud
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={config.location.coordinates.lng}
|
|
||||||
onChange={(e) => handleInputChange('location', 'coordinates', {
|
|
||||||
...config.location.coordinates,
|
|
||||||
lng: parseFloat(e.target.value) || 0
|
|
||||||
})}
|
|
||||||
type="number"
|
|
||||||
step="0.000001"
|
|
||||||
placeholder="-3.7038"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'schedule' && (
|
{activeTab === 'business' && (
|
||||||
<Card className="p-6">
|
<div className="space-y-6">
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Horarios de Apertura</h3>
|
<h3 className="text-lg font-semibold text-text-primary">Datos de Empresa</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
|
<Input
|
||||||
|
label="NIF/CIF"
|
||||||
|
value={config.taxId}
|
||||||
|
onChange={handleInputChange('taxId')}
|
||||||
|
disabled={!isEditing || isLoading}
|
||||||
|
placeholder="B12345678"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Moneda"
|
||||||
|
options={currencyOptions}
|
||||||
|
value={config.currency}
|
||||||
|
onChange={handleSelectChange('currency')}
|
||||||
|
disabled={!isEditing || isLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Zona Horaria"
|
||||||
|
options={timezoneOptions}
|
||||||
|
value={config.timezone}
|
||||||
|
onChange={handleSelectChange('timezone')}
|
||||||
|
disabled={!isEditing || isLoading}
|
||||||
|
leftIcon={<Clock className="w-4 h-4" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Idioma"
|
||||||
|
options={languageOptions}
|
||||||
|
value={config.language}
|
||||||
|
onChange={handleSelectChange('language')}
|
||||||
|
disabled={!isEditing || isLoading}
|
||||||
|
leftIcon={<Globe className="w-4 h-4" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'hours' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-semibold text-text-primary">Horarios de Apertura</h3>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{daysOfWeek.map((day) => {
|
{daysOfWeek.map((day) => {
|
||||||
const schedule = config.schedule[day.key as keyof typeof config.schedule];
|
const hours = businessHours[day.key];
|
||||||
return (
|
return (
|
||||||
<div key={day.key} className="flex items-center space-x-4 p-4 border rounded-lg">
|
<div key={day.key} className="grid grid-cols-12 items-center gap-4 p-4 border border-border-primary rounded-lg">
|
||||||
<div className="w-20">
|
{/* Day Name */}
|
||||||
<span className="text-sm font-medium text-[var(--text-secondary)]">{day.label}</span>
|
<div className="col-span-2">
|
||||||
|
<span className="text-sm font-medium text-text-secondary">{day.label}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="flex items-center space-x-2">
|
{/* Closed Checkbox */}
|
||||||
<input
|
<div className="col-span-2">
|
||||||
type="checkbox"
|
<label className="flex items-center gap-2">
|
||||||
checked={schedule.closed}
|
<input
|
||||||
onChange={(e) => handleScheduleChange(day.key, 'closed', e.target.checked)}
|
type="checkbox"
|
||||||
className="rounded border-[var(--border-secondary)]"
|
checked={hours.closed}
|
||||||
/>
|
onChange={(e) => handleHoursChange(day.key, 'closed', e.target.checked)}
|
||||||
<span className="text-sm text-[var(--text-secondary)]">Cerrado</span>
|
disabled={!isEditing || isLoading}
|
||||||
</label>
|
className="rounded border-border-primary"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-text-secondary">Cerrado</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
{!schedule.closed && (
|
{/* Time Inputs */}
|
||||||
<>
|
<div className="col-span-8 flex items-center gap-6">
|
||||||
<div>
|
{!hours.closed ? (
|
||||||
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Apertura</label>
|
<>
|
||||||
<input
|
<div className="flex-1">
|
||||||
type="time"
|
<label className="block text-xs text-text-tertiary mb-1">Apertura</label>
|
||||||
value={schedule.open}
|
<input
|
||||||
onChange={(e) => handleScheduleChange(day.key, 'open', e.target.value)}
|
type="time"
|
||||||
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
|
value={hours.open}
|
||||||
/>
|
onChange={(e) => handleHoursChange(day.key, 'open', e.target.value)}
|
||||||
|
disabled={!isEditing || isLoading}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg text-sm bg-[var(--bg-primary)] text-[var(--text-primary)] disabled:bg-[var(--bg-secondary)] disabled:text-[var(--text-secondary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-xs text-text-tertiary mb-1">Cierre</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={hours.close}
|
||||||
|
onChange={(e) => handleHoursChange(day.key, 'close', e.target.value)}
|
||||||
|
disabled={!isEditing || isLoading}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg text-sm bg-[var(--bg-primary)] text-[var(--text-primary)] disabled:bg-[var(--bg-secondary)] disabled:text-[var(--text-secondary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-text-tertiary italic">
|
||||||
|
Cerrado todo el día
|
||||||
</div>
|
</div>
|
||||||
<div>
|
)}
|
||||||
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Cierre</label>
|
</div>
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={schedule.close}
|
|
||||||
onChange={(e) => handleScheduleChange(day.key, 'close', e.target.value)}
|
|
||||||
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'business' && (
|
|
||||||
<Card className="p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Datos de Empresa</h3>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
NIF/CIF
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={config.business.taxId}
|
|
||||||
onChange={(e) => handleInputChange('business', 'taxId', e.target.value)}
|
|
||||||
placeholder="B12345678"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Número de Registro
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={config.business.registrationNumber}
|
|
||||||
onChange={(e) => handleInputChange('business', 'registrationNumber', e.target.value)}
|
|
||||||
placeholder="REG-2024-001"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Licencia Sanitaria
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={config.business.licenseNumber}
|
|
||||||
onChange={(e) => handleInputChange('business', 'licenseNumber', e.target.value)}
|
|
||||||
placeholder="LIC-FOOD-2024"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Moneda
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={config.business.currency}
|
|
||||||
onChange={(e) => handleInputChange('business', 'currency', e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
|
||||||
>
|
|
||||||
<option value="EUR">EUR (€)</option>
|
|
||||||
<option value="USD">USD ($)</option>
|
|
||||||
<option value="GBP">GBP (£)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Zona Horaria
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={config.business.timezone}
|
|
||||||
onChange={(e) => handleInputChange('business', 'timezone', e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
|
||||||
>
|
|
||||||
<option value="Europe/Madrid">Madrid (GMT+1)</option>
|
|
||||||
<option value="Europe/London">Londres (GMT)</option>
|
|
||||||
<option value="America/New_York">Nueva York (GMT-5)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Idioma
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={config.business.language}
|
|
||||||
onChange={(e) => handleInputChange('business', 'language', e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
|
||||||
>
|
|
||||||
<option value="es">Español</option>
|
|
||||||
<option value="en">English</option>
|
|
||||||
<option value="fr">Français</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Save Actions */}
|
||||||
{/* Save Changes Banner */}
|
{isEditing && (
|
||||||
{hasChanges && (
|
<div className="flex gap-3 px-6 py-4 bg-bg-secondary border-t border-border-primary">
|
||||||
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
|
<Button
|
||||||
<span className="text-sm">Tienes cambios sin guardar</span>
|
variant="outline"
|
||||||
<div className="flex space-x-2">
|
onClick={() => setIsEditing(false)}
|
||||||
<Button size="sm" variant="outline" className="text-[var(--color-info)] bg-white" onClick={handleReset}>
|
disabled={isLoading}
|
||||||
Descartar
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
|
<Button
|
||||||
Guardar
|
variant="primary"
|
||||||
|
onClick={handleSaveConfig}
|
||||||
|
isLoading={isLoading}
|
||||||
|
loadingText="Guardando..."
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Save className="w-4 h-4" />
|
||||||
|
Guardar Configuración
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,49 +1,173 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { User, Mail, Phone, MapPin, Building, Shield, Activity, Settings, Edit3, Lock, Bell, Download } from 'lucide-react';
|
import { User, Mail, Phone, Lock, Globe, Clock, Camera, Save, X } from 'lucide-react';
|
||||||
import { Button, Card, Badge, Avatar, Input, ProgressBar } from '../../../../components/ui';
|
import { Button, Card, Avatar, Input, Select } from '../../../../components/ui';
|
||||||
import { PageHeader } from '../../../../components/layout';
|
import { PageHeader } from '../../../../components/layout';
|
||||||
import { ProfileSettings } from '../../../../components/domain/auth';
|
import { useAuthUser } from '../../../../stores/auth.store';
|
||||||
|
import { useToast } from '../../../../hooks/ui/useToast';
|
||||||
|
|
||||||
|
interface ProfileFormData {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
language: string;
|
||||||
|
timezone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PasswordData {
|
||||||
|
currentPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
const ProfilePage: React.FC = () => {
|
const ProfilePage: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState('profile');
|
const user = useAuthUser();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [userInfo, setUserInfo] = useState({
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
name: 'María González',
|
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
||||||
email: 'maria.gonzalez@panaderia.com',
|
|
||||||
phone: '+34 123 456 789',
|
const [profileData, setProfileData] = useState<ProfileFormData>({
|
||||||
address: 'Calle Mayor 123, Madrid',
|
first_name: 'María',
|
||||||
bakery: 'Panadería La Tradicional',
|
last_name: 'González Pérez',
|
||||||
role: 'Propietario'
|
email: 'admin@bakery.com',
|
||||||
|
phone: '+34 612 345 678',
|
||||||
|
language: 'es',
|
||||||
|
timezone: 'Europe/Madrid'
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockProfileStats = {
|
const [passwordData, setPasswordData] = useState<PasswordData>({
|
||||||
profileCompletion: 85,
|
currentPassword: '',
|
||||||
securityScore: 94,
|
newPassword: '',
|
||||||
lastLogin: '2 horas',
|
confirmPassword: ''
|
||||||
activeSessions: 2,
|
});
|
||||||
twoFactorEnabled: false,
|
|
||||||
passwordLastChanged: '2 meses'
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
const languageOptions = [
|
||||||
|
{ value: 'es', label: 'Español' },
|
||||||
|
{ value: 'ca', label: 'Català' },
|
||||||
|
{ value: 'en', label: 'English' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const timezoneOptions = [
|
||||||
|
{ value: 'Europe/Madrid', label: 'Madrid (CET/CEST)' },
|
||||||
|
{ value: 'Atlantic/Canary', label: 'Canarias (WET/WEST)' },
|
||||||
|
{ value: 'Europe/London', label: 'Londres (GMT/BST)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const validateProfile = (): boolean => {
|
||||||
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (!profileData.first_name.trim()) {
|
||||||
|
newErrors.first_name = 'El nombre es requerido';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profileData.last_name.trim()) {
|
||||||
|
newErrors.last_name = 'Los apellidos son requeridos';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profileData.email.trim()) {
|
||||||
|
newErrors.email = 'El email es requerido';
|
||||||
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(profileData.email)) {
|
||||||
|
newErrors.email = 'Email inválido';
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const validatePassword = (): boolean => {
|
||||||
setIsEditing(false);
|
const newErrors: Record<string, string> = {};
|
||||||
console.log('Profile updated:', userInfo);
|
|
||||||
|
if (!passwordData.currentPassword) {
|
||||||
|
newErrors.currentPassword = 'Contraseña actual requerida';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passwordData.newPassword) {
|
||||||
|
newErrors.newPassword = 'Nueva contraseña requerida';
|
||||||
|
} else if (passwordData.newPassword.length < 8) {
|
||||||
|
newErrors.newPassword = 'Mínimo 8 caracteres';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordData.newPassword !== passwordData.confirmPassword) {
|
||||||
|
newErrors.confirmPassword = 'Las contraseñas no coinciden';
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleSaveProfile = async () => {
|
||||||
setIsEditing(false);
|
if (!validateProfile()) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
setIsEditing(false);
|
||||||
|
showToast({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Perfil actualizado',
|
||||||
|
message: 'Tu información ha sido guardada correctamente'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showToast({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: 'No se pudo actualizar tu perfil'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEnable2FA = () => {
|
const handleChangePassword = async () => {
|
||||||
console.log('Enabling 2FA');
|
if (!validatePassword()) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
setShowPasswordForm(false);
|
||||||
|
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||||
|
showToast({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Contraseña actualizada',
|
||||||
|
message: 'Tu contraseña ha sido cambiada correctamente'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showToast({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: 'No se pudo cambiar tu contraseña'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangePassword = () => {
|
const handleInputChange = (field: keyof ProfileFormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
console.log('Change password');
|
setProfileData(prev => ({ ...prev, [field]: e.target.value }));
|
||||||
|
if (errors[field]) {
|
||||||
|
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleManageSessions = () => {
|
const handleSelectChange = (field: keyof ProfileFormData) => (value: string) => {
|
||||||
console.log('Manage sessions');
|
setProfileData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = (field: keyof PasswordData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setPasswordData(prev => ({ ...prev, [field]: e.target.value }));
|
||||||
|
if (errors[field]) {
|
||||||
|
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,329 +175,199 @@ const ProfilePage: React.FC = () => {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Mi Perfil"
|
title="Mi Perfil"
|
||||||
description="Gestiona tu información personal y configuración de cuenta"
|
description="Gestiona tu información personal y configuración de cuenta"
|
||||||
action={
|
|
||||||
<Button onClick={() => setIsEditing(!isEditing)}>
|
|
||||||
<Edit3 className="w-4 h-4 mr-2" />
|
|
||||||
{isEditing ? 'Guardar Cambios' : 'Editar Perfil'}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Profile Stats */}
|
{/* Profile Header */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
|
<Card className="p-6">
|
||||||
<Card className="p-4">
|
<div className="flex items-center gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="relative">
|
||||||
<div>
|
<Avatar
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Perfil Completado</p>
|
src="https://images.unsplash.com/photo-1494790108755-2616b612b372?w=150&h=150&fit=crop&crop=face"
|
||||||
<p className="text-2xl font-bold text-[var(--color-success)]">{mockProfileStats.profileCompletion}%</p>
|
name={`${profileData.first_name} ${profileData.last_name}`}
|
||||||
</div>
|
size="xl"
|
||||||
<User className="h-8 w-8 text-[var(--color-success)]" />
|
className="w-20 h-20"
|
||||||
|
/>
|
||||||
|
<button className="absolute -bottom-1 -right-1 bg-color-primary text-white rounded-full p-2 shadow-lg hover:bg-color-primary-dark transition-colors">
|
||||||
|
<Camera className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<div className="flex-1">
|
||||||
|
<h1 className="text-2xl font-bold text-text-primary mb-1">
|
||||||
<Card className="p-4">
|
{profileData.first_name} {profileData.last_name}
|
||||||
<div className="flex items-center justify-between">
|
</h1>
|
||||||
<div>
|
<p className="text-text-secondary">{profileData.email}</p>
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Seguridad</p>
|
<div className="flex items-center gap-2 mt-2">
|
||||||
<p className="text-2xl font-bold text-[var(--color-info)]">{mockProfileStats.securityScore}%</p>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
</div>
|
<span className="text-sm text-text-tertiary">En línea</span>
|
||||||
<Shield className="h-8 w-8 text-[var(--color-info)]" />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Último Acceso</p>
|
|
||||||
<p className="text-2xl font-bold text-[var(--text-primary)]">{mockProfileStats.lastLogin}</p>
|
|
||||||
</div>
|
|
||||||
<Activity className="h-8 w-8 text-[var(--color-primary)]" />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Sesiones</p>
|
|
||||||
<p className="text-2xl font-bold text-purple-600">{mockProfileStats.activeSessions}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
|
|
||||||
<Settings className="h-5 w-5 text-purple-600" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<div className="flex gap-2">
|
||||||
|
{!isEditing && (
|
||||||
<Card className="p-4">
|
<Button
|
||||||
<div className="flex items-center justify-between">
|
variant="outline"
|
||||||
<div>
|
onClick={() => setIsEditing(true)}
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">2FA</p>
|
className="flex items-center gap-2"
|
||||||
<p className="text-lg font-bold text-[var(--color-warning)]">{mockProfileStats.twoFactorEnabled ? 'Activo' : 'Pendiente'}</p>
|
>
|
||||||
</div>
|
<User className="w-4 h-4" />
|
||||||
<Lock className="h-8 w-8 text-[var(--color-warning)]" />
|
Editar Perfil
|
||||||
</div>
|
</Button>
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Contraseña</p>
|
|
||||||
<p className="text-lg font-bold text-indigo-600">{mockProfileStats.passwordLastChanged}</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-8 w-8 bg-indigo-100 rounded-full flex items-center justify-center">
|
|
||||||
<Shield className="h-5 w-5 text-indigo-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tabs Navigation */}
|
|
||||||
<div className="border-b border-[var(--border-primary)]">
|
|
||||||
<nav className="-mb-px flex space-x-8">
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('profile')}
|
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
|
||||||
activeTab === 'profile'
|
|
||||||
? 'border-orange-500 text-[var(--color-primary)]'
|
|
||||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Información Personal
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('security')}
|
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
|
||||||
activeTab === 'security'
|
|
||||||
? 'border-orange-500 text-[var(--color-primary)]'
|
|
||||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Seguridad
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('activity')}
|
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
|
||||||
activeTab === 'activity'
|
|
||||||
? 'border-orange-500 text-[var(--color-primary)]'
|
|
||||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Actividad
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab Content */}
|
|
||||||
{activeTab === 'profile' && (
|
|
||||||
<Card>
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h3 className="text-lg font-medium text-[var(--text-primary)]">Información Personal</h3>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<Download className="w-4 h-4 mr-2" />
|
|
||||||
Exportar Datos
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Avatar and Basic Info */}
|
|
||||||
<div className="flex items-center gap-6 mb-8">
|
|
||||||
<Avatar
|
|
||||||
src="/api/placeholder/120/120"
|
|
||||||
alt={userInfo.name}
|
|
||||||
size="lg"
|
|
||||||
className="w-20 h-20"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h2 className="text-xl font-semibold text-[var(--text-primary)]">{userInfo.name}</h2>
|
|
||||||
<p className="text-[var(--text-secondary)]">{userInfo.role}</p>
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
|
||||||
<Badge variant="success">Verificado</Badge>
|
|
||||||
<Badge variant="info">Premium</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Form Fields */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
|
||||||
<User className="w-4 h-4 inline mr-2" />
|
|
||||||
Nombre Completo
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={userInfo.name}
|
|
||||||
onChange={(e) => setUserInfo({...userInfo, name: e.target.value})}
|
|
||||||
disabled={!isEditing}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
|
||||||
<Mail className="w-4 h-4 inline mr-2" />
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={userInfo.email}
|
|
||||||
onChange={(e) => setUserInfo({...userInfo, email: e.target.value})}
|
|
||||||
disabled={!isEditing}
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
|
||||||
<Phone className="w-4 h-4 inline mr-2" />
|
|
||||||
Teléfono
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={userInfo.phone}
|
|
||||||
onChange={(e) => setUserInfo({...userInfo, phone: e.target.value})}
|
|
||||||
disabled={!isEditing}
|
|
||||||
type="tel"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
|
||||||
<Building className="w-4 h-4 inline mr-2" />
|
|
||||||
Panadería
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={userInfo.bakery}
|
|
||||||
onChange={(e) => setUserInfo({...userInfo, bakery: e.target.value})}
|
|
||||||
disabled={!isEditing}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
|
||||||
<MapPin className="w-4 h-4 inline mr-2" />
|
|
||||||
Dirección
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={userInfo.address}
|
|
||||||
onChange={(e) => setUserInfo({...userInfo, address: e.target.value})}
|
|
||||||
disabled={!isEditing}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
{isEditing && (
|
|
||||||
<div className="flex gap-3 pt-6 mt-6 border-t border-[var(--border-primary)]">
|
|
||||||
<Button onClick={handleSave}>Guardar Cambios</Button>
|
|
||||||
<Button variant="outline" onClick={handleCancel}>Cancelar</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowPasswordForm(!showPasswordForm)}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Lock className="w-4 h-4" />
|
||||||
|
Cambiar Contraseña
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
)}
|
</Card>
|
||||||
|
|
||||||
{activeTab === 'security' && (
|
{/* Profile Form */}
|
||||||
<Card>
|
<Card className="p-6">
|
||||||
<div className="p-6">
|
<h2 className="text-lg font-semibold mb-4">Información Personal</h2>
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h3 className="text-lg font-medium text-[var(--text-primary)]">Configuración de Seguridad</h3>
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
</div>
|
<Input
|
||||||
|
label="Nombre"
|
||||||
<div className="space-y-6">
|
value={profileData.first_name}
|
||||||
<div className="flex items-center justify-between p-4 border border-[var(--border-primary)] rounded-lg">
|
onChange={handleInputChange('first_name')}
|
||||||
<div className="flex items-center gap-3">
|
error={errors.first_name}
|
||||||
<Shield className="w-5 h-5 text-[var(--color-info)]" />
|
disabled={!isEditing || isLoading}
|
||||||
<div>
|
leftIcon={<User className="w-4 h-4" />}
|
||||||
<p className="font-medium text-[var(--text-primary)]">Autenticación de Dos Factores</p>
|
/>
|
||||||
<p className="text-sm text-[var(--text-secondary)]">Protege tu cuenta con 2FA</p>
|
|
||||||
</div>
|
<Input
|
||||||
</div>
|
label="Apellidos"
|
||||||
<div className="flex items-center gap-3">
|
value={profileData.last_name}
|
||||||
<Badge variant={mockProfileStats.twoFactorEnabled ? "success" : "warning"}>
|
onChange={handleInputChange('last_name')}
|
||||||
{mockProfileStats.twoFactorEnabled ? "Activo" : "Pendiente"}
|
error={errors.last_name}
|
||||||
</Badge>
|
disabled={!isEditing || isLoading}
|
||||||
<Button variant="outline" size="sm" onClick={handleEnable2FA}>
|
/>
|
||||||
{mockProfileStats.twoFactorEnabled ? "Desactivar" : "Activar"}
|
|
||||||
</Button>
|
<Input
|
||||||
</div>
|
type="email"
|
||||||
</div>
|
label="Correo Electrónico"
|
||||||
|
value={profileData.email}
|
||||||
<div className="flex items-center justify-between p-4 border border-[var(--border-primary)] rounded-lg">
|
onChange={handleInputChange('email')}
|
||||||
<div className="flex items-center gap-3">
|
error={errors.email}
|
||||||
<Lock className="w-5 h-5 text-[var(--color-primary)]" />
|
disabled={!isEditing || isLoading}
|
||||||
<div>
|
leftIcon={<Mail className="w-4 h-4" />}
|
||||||
<p className="font-medium text-[var(--text-primary)]">Contraseña</p>
|
/>
|
||||||
<p className="text-sm text-[var(--text-secondary)]">Actualizada hace {mockProfileStats.passwordLastChanged}</p>
|
|
||||||
</div>
|
<Input
|
||||||
</div>
|
type="tel"
|
||||||
<Button variant="outline" size="sm" onClick={handleChangePassword}>
|
label="Teléfono"
|
||||||
Cambiar
|
value={profileData.phone}
|
||||||
</Button>
|
onChange={handleInputChange('phone')}
|
||||||
</div>
|
error={errors.phone}
|
||||||
|
disabled={!isEditing || isLoading}
|
||||||
<div className="flex items-center justify-between p-4 border border-[var(--border-primary)] rounded-lg">
|
placeholder="+34 600 000 000"
|
||||||
<div className="flex items-center gap-3">
|
leftIcon={<Phone className="w-4 h-4" />}
|
||||||
<Settings className="w-5 h-5 text-purple-600" />
|
/>
|
||||||
<div>
|
|
||||||
<p className="font-medium text-[var(--text-primary)]">Sesiones Activas</p>
|
<Select
|
||||||
<p className="text-sm text-[var(--text-secondary)]">{mockProfileStats.activeSessions} dispositivos conectados</p>
|
label="Idioma"
|
||||||
</div>
|
options={languageOptions}
|
||||||
</div>
|
value={profileData.language}
|
||||||
<Button variant="outline" size="sm" onClick={handleManageSessions}>
|
onChange={handleSelectChange('language')}
|
||||||
Gestionar
|
disabled={!isEditing || isLoading}
|
||||||
</Button>
|
leftIcon={<Globe className="w-4 h-4" />}
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
|
<Select
|
||||||
|
label="Zona Horaria"
|
||||||
|
options={timezoneOptions}
|
||||||
|
value={profileData.timezone}
|
||||||
|
onChange={handleSelectChange('timezone')}
|
||||||
|
disabled={!isEditing || isLoading}
|
||||||
|
leftIcon={<Clock className="w-4 h-4" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<div className="flex gap-3 mt-6 pt-4 border-t">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsEditing(false)}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleSaveProfile}
|
||||||
|
isLoading={isLoading}
|
||||||
|
loadingText="Guardando..."
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Save className="w-4 h-4" />
|
||||||
|
Guardar Cambios
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
)}
|
||||||
)}
|
</Card>
|
||||||
|
|
||||||
{activeTab === 'activity' && (
|
{/* Password Change Form */}
|
||||||
<Card>
|
{showPasswordForm && (
|
||||||
<div className="p-6">
|
<Card className="p-6">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<h2 className="text-lg font-semibold mb-4">Cambiar Contraseña</h2>
|
||||||
<h3 className="text-lg font-medium text-[var(--text-primary)]">Actividad Reciente</h3>
|
|
||||||
</div>
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 max-w-4xl">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
label="Contraseña Actual"
|
||||||
|
value={passwordData.currentPassword}
|
||||||
|
onChange={handlePasswordChange('currentPassword')}
|
||||||
|
error={errors.currentPassword}
|
||||||
|
disabled={isLoading}
|
||||||
|
leftIcon={<Lock className="w-4 h-4" />}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<Input
|
||||||
<div className="flex items-center gap-4 p-4 border border-[var(--border-primary)] rounded-lg">
|
type="password"
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
label="Nueva Contraseña"
|
||||||
<Activity className="w-5 h-5 text-green-500" />
|
value={passwordData.newPassword}
|
||||||
<div className="flex-1">
|
onChange={handlePasswordChange('newPassword')}
|
||||||
<p className="font-medium text-[var(--text-primary)]">Inicio de sesión</p>
|
error={errors.newPassword}
|
||||||
<p className="text-sm text-[var(--text-secondary)]">Hace 2 horas desde Chrome en Madrid, España</p>
|
disabled={isLoading}
|
||||||
</div>
|
leftIcon={<Lock className="w-4 h-4" />}
|
||||||
<span className="text-xs text-[var(--text-tertiary)]">Hoy 14:30</span>
|
/>
|
||||||
</div>
|
|
||||||
|
<Input
|
||||||
<div className="flex items-center gap-4 p-4 border border-[var(--border-primary)] rounded-lg">
|
type="password"
|
||||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
label="Confirmar Nueva Contraseña"
|
||||||
<User className="w-5 h-5 text-blue-500" />
|
value={passwordData.confirmPassword}
|
||||||
<div className="flex-1">
|
onChange={handlePasswordChange('confirmPassword')}
|
||||||
<p className="font-medium text-[var(--text-primary)]">Perfil actualizado</p>
|
error={errors.confirmPassword}
|
||||||
<p className="text-sm text-[var(--text-secondary)]">Se modificó la información de contacto</p>
|
disabled={isLoading}
|
||||||
</div>
|
leftIcon={<Lock className="w-4 h-4" />}
|
||||||
<span className="text-xs text-[var(--text-tertiary)]">Ayer 09:15</span>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 p-4 border border-[var(--border-primary)] rounded-lg">
|
<div className="flex gap-3 pt-6 mt-6 border-t">
|
||||||
<div className="w-2 h-2 bg-orange-500 rounded-full"></div>
|
<Button
|
||||||
<Shield className="w-5 h-5 text-orange-500" />
|
variant="outline"
|
||||||
<div className="flex-1">
|
onClick={() => {
|
||||||
<p className="font-medium text-[var(--text-primary)]">Contraseña cambiada</p>
|
setShowPasswordForm(false);
|
||||||
<p className="text-sm text-[var(--text-secondary)]">Contraseña actualizada exitosamente</p>
|
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||||
</div>
|
setErrors({});
|
||||||
<span className="text-xs text-[var(--text-tertiary)]">Hace 2 meses</span>
|
}}
|
||||||
</div>
|
disabled={isLoading}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-4 p-4 border border-[var(--border-primary)] rounded-lg">
|
Cancelar
|
||||||
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
|
</Button>
|
||||||
<Bell className="w-5 h-5 text-purple-500" />
|
<Button
|
||||||
<div className="flex-1">
|
variant="primary"
|
||||||
<p className="font-medium text-[var(--text-primary)]">Configuración de notificaciones</p>
|
onClick={handleChangePassword}
|
||||||
<p className="text-sm text-[var(--text-secondary)]">Se habilitaron las notificaciones por email</p>
|
isLoading={isLoading}
|
||||||
</div>
|
loadingText="Cambiando..."
|
||||||
<span className="text-xs text-[var(--text-tertiary)]">Hace 1 semana</span>
|
>
|
||||||
</div>
|
Cambiar Contraseña
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useAuthActions, useAuthError, useAuthLoading, useIsAuthenticated } from '../../stores';
|
import { useIsAuthenticated, useAuthLoading } from '../../stores';
|
||||||
import { Button, Input, Card } from '../../components/ui';
|
import { LoginForm } from '../../components/domain/auth';
|
||||||
import { PublicLayout } from '../../components/layout';
|
import { PublicLayout } from '../../components/layout';
|
||||||
|
|
||||||
const LoginPage: React.FC = () => {
|
const LoginPage: React.FC = () => {
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [rememberMe, setRememberMe] = useState(false);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const { login } = useAuthActions();
|
|
||||||
const error = useAuthError();
|
|
||||||
const loading = useAuthLoading();
|
const loading = useAuthLoading();
|
||||||
const isAuthenticated = useIsAuthenticated();
|
const isAuthenticated = useIsAuthenticated();
|
||||||
|
|
||||||
@@ -28,15 +22,12 @@ const LoginPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [isAuthenticated, loading, navigate, from]);
|
}, [isAuthenticated, loading, navigate, from]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleLoginSuccess = () => {
|
||||||
e.preventDefault();
|
navigate(from, { replace: true });
|
||||||
if (!email || !password) return;
|
};
|
||||||
|
|
||||||
try {
|
const handleRegisterClick = () => {
|
||||||
await login(email, password);
|
navigate('/register');
|
||||||
} catch (err) {
|
|
||||||
// Error is handled by the store
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -49,151 +40,11 @@ const LoginPage: React.FC = () => {
|
|||||||
variant: "minimal"
|
variant: "minimal"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-md mx-auto space-y-8">
|
<LoginForm
|
||||||
<div>
|
onSuccess={handleLoginSuccess}
|
||||||
<div className="flex justify-center">
|
onRegisterClick={handleRegisterClick}
|
||||||
<div className="w-12 h-12 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-lg flex items-center justify-center text-white font-bold text-lg">
|
className="mx-auto"
|
||||||
PI
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-[var(--text-primary)]">
|
|
||||||
Inicia sesión en tu cuenta
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2 text-center text-sm text-[var(--text-secondary)]">
|
|
||||||
O{' '}
|
|
||||||
<Link
|
|
||||||
to="/register"
|
|
||||||
className="font-medium text-[var(--color-primary)] hover:text-[var(--color-primary-light)]"
|
|
||||||
>
|
|
||||||
regístrate para comenzar tu prueba gratuita
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className="p-8">
|
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
|
||||||
{error && (
|
|
||||||
<div className="bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-md p-4">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<svg className="h-5 w-5 text-[var(--color-error)]" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
<h3 className="text-sm font-medium text-[var(--color-error)]">
|
|
||||||
Error de autenticación
|
|
||||||
</h3>
|
|
||||||
<div className="mt-2 text-sm text-[var(--color-error)]">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="sr-only">
|
|
||||||
Correo electrónico
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
autoComplete="email"
|
|
||||||
required
|
|
||||||
placeholder="Correo electrónico"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="sr-only">
|
|
||||||
Contraseña
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
autoComplete="current-password"
|
|
||||||
required
|
|
||||||
placeholder="Contraseña"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
id="remember-me"
|
|
||||||
name="remember-me"
|
|
||||||
type="checkbox"
|
|
||||||
className="h-4 w-4 text-[var(--color-primary)] focus:ring-[var(--color-primary)] border-[var(--border-primary)] rounded"
|
|
||||||
checked={rememberMe}
|
|
||||||
onChange={(e) => setRememberMe(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-[var(--text-primary)]">
|
|
||||||
Recordarme
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm">
|
|
||||||
<a href="#" className="font-medium text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
|
|
||||||
¿Olvidaste tu contraseña?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full flex justify-center"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? 'Iniciando sesión...' : 'Iniciar sesión'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<div className="w-full border-t border-[var(--border-primary)]" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-sm">
|
|
||||||
<span className="px-2 bg-[var(--bg-primary)] text-[var(--text-tertiary)]">Demo</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
// TODO: Handle demo login
|
|
||||||
console.log('Demo login');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Usar cuenta de demo
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 text-center text-xs text-[var(--text-tertiary)]">
|
|
||||||
Al iniciar sesión, aceptas nuestros{' '}
|
|
||||||
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
|
|
||||||
Términos de Servicio
|
|
||||||
</a>
|
|
||||||
{' '}y{' '}
|
|
||||||
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
|
|
||||||
Política de Privacidad
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</PublicLayout>
|
</PublicLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,371 +1,34 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Button, Input, Card, Select } from '../../components/ui';
|
import { RegisterForm } from '../../components/domain/auth';
|
||||||
import { PublicLayout } from '../../components/layout';
|
import { PublicLayout } from '../../components/layout';
|
||||||
|
|
||||||
const RegisterPage: React.FC = () => {
|
const RegisterPage: React.FC = () => {
|
||||||
const [step, setStep] = useState(1);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
// Personal info
|
|
||||||
firstName: '',
|
|
||||||
lastName: '',
|
|
||||||
email: '',
|
|
||||||
phone: '',
|
|
||||||
|
|
||||||
// Company info
|
|
||||||
companyName: '',
|
|
||||||
companyType: '',
|
|
||||||
employeeCount: '',
|
|
||||||
|
|
||||||
// Account info
|
|
||||||
password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
acceptTerms: false,
|
|
||||||
acceptMarketing: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: string | boolean) => {
|
const handleRegistrationSuccess = () => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
navigate('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNextStep = () => {
|
const handleLoginClick = () => {
|
||||||
setStep(prev => prev + 1);
|
navigate('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrevStep = () => {
|
|
||||||
setStep(prev => prev - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Simulate API call
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
// Redirect to onboarding
|
|
||||||
navigate('/onboarding');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Registration failed:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isStep1Valid = formData.firstName && formData.lastName && formData.email && formData.phone;
|
|
||||||
const isStep2Valid = formData.companyName && formData.companyType && formData.employeeCount;
|
|
||||||
const isStep3Valid = formData.password && formData.confirmPassword &&
|
|
||||||
formData.password === formData.confirmPassword && formData.acceptTerms;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PublicLayout
|
<PublicLayout
|
||||||
variant="centered"
|
variant="centered"
|
||||||
maxWidth="md"
|
maxWidth="xl"
|
||||||
headerProps={{
|
headerProps={{
|
||||||
showThemeToggle: true,
|
showThemeToggle: true,
|
||||||
showAuthButtons: false,
|
showAuthButtons: false,
|
||||||
variant: "minimal"
|
variant: "minimal"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-md mx-auto space-y-8">
|
<RegisterForm
|
||||||
<div>
|
onSuccess={handleRegistrationSuccess}
|
||||||
<div className="flex justify-center">
|
onLoginClick={handleLoginClick}
|
||||||
<div className="w-12 h-12 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] rounded-lg flex items-center justify-center text-white font-bold text-lg">
|
className="mx-auto"
|
||||||
PI
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-[var(--text-primary)]">
|
|
||||||
Crea tu cuenta
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2 text-center text-sm text-[var(--text-secondary)]">
|
|
||||||
O{' '}
|
|
||||||
<Link
|
|
||||||
to="/login"
|
|
||||||
className="font-medium text-[var(--color-primary)] hover:text-[var(--color-primary-light)]"
|
|
||||||
>
|
|
||||||
inicia sesión si ya tienes una cuenta
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className="p-8">
|
|
||||||
{/* Progress indicator */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
{[
|
|
||||||
{ step: 1, label: 'Datos personales' },
|
|
||||||
{ step: 2, label: 'Información empresarial' },
|
|
||||||
{ step: 3, label: 'Crear cuenta' }
|
|
||||||
].map((stepInfo) => (
|
|
||||||
<div key={stepInfo.step} className="flex flex-col items-center">
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
|
||||||
step >= stepInfo.step
|
|
||||||
? 'bg-[var(--color-primary)] text-white'
|
|
||||||
: 'bg-[var(--bg-quaternary)] text-[var(--text-tertiary)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{stepInfo.step}
|
|
||||||
</div>
|
|
||||||
<span className="mt-2 text-xs text-[var(--text-secondary)] text-center max-w-[80px]">
|
|
||||||
{stepInfo.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
{step === 1 && (
|
|
||||||
<>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="firstName" className="block text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
Nombre *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="firstName"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.firstName}
|
|
||||||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
|
||||||
placeholder="Tu nombre"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="lastName" className="block text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
Apellido *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="lastName"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.lastName}
|
|
||||||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
|
||||||
placeholder="Tu apellido"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
Correo electrónico *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
value={formData.email}
|
|
||||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
|
||||||
placeholder="tu@email.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="phone" className="block text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
Teléfono *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="phone"
|
|
||||||
type="tel"
|
|
||||||
required
|
|
||||||
value={formData.phone}
|
|
||||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
|
||||||
placeholder="+34 600 000 000"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={handleNextStep}
|
|
||||||
disabled={!isStep1Valid}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
Continuar
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 2 && (
|
|
||||||
<>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="companyName" className="block text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
Nombre de la panadería *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="companyName"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.companyName}
|
|
||||||
onChange={(e) => handleInputChange('companyName', e.target.value)}
|
|
||||||
placeholder="Panadería San Miguel"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="companyType" className="block text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
Tipo de negocio *
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={formData.companyType}
|
|
||||||
onChange={(value) => handleInputChange('companyType', value as string)}
|
|
||||||
placeholder="Selecciona el tipo"
|
|
||||||
options={[
|
|
||||||
{ value: "traditional", label: "Panadería tradicional" },
|
|
||||||
{ value: "artisan", label: "Panadería artesanal" },
|
|
||||||
{ value: "industrial", label: "Panadería industrial" },
|
|
||||||
{ value: "bakery-cafe", label: "Panadería-cafetería" },
|
|
||||||
{ value: "specialty", label: "Panadería especializada" }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="employeeCount" className="block text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
Número de empleados *
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={formData.employeeCount}
|
|
||||||
onChange={(value) => handleInputChange('employeeCount', value as string)}
|
|
||||||
placeholder="Selecciona el rango"
|
|
||||||
options={[
|
|
||||||
{ value: "1", label: "Solo yo" },
|
|
||||||
{ value: "2-5", label: "2-5 empleados" },
|
|
||||||
{ value: "6-15", label: "6-15 empleados" },
|
|
||||||
{ value: "16-50", label: "16-50 empleados" },
|
|
||||||
{ value: "51+", label: "Más de 50 empleados" }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handlePrevStep}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
Atrás
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={handleNextStep}
|
|
||||||
disabled={!isStep2Valid}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
Continuar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 3 && (
|
|
||||||
<>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
Contraseña *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
value={formData.password}
|
|
||||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
|
||||||
placeholder="Mínimo 8 caracteres"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
Confirmar contraseña *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="confirmPassword"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
value={formData.confirmPassword}
|
|
||||||
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
|
|
||||||
placeholder="Repite la contraseña"
|
|
||||||
/>
|
|
||||||
{formData.confirmPassword && formData.password !== formData.confirmPassword && (
|
|
||||||
<p className="mt-1 text-sm text-[var(--color-error)]">Las contraseñas no coinciden</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-start">
|
|
||||||
<input
|
|
||||||
id="acceptTerms"
|
|
||||||
type="checkbox"
|
|
||||||
className="h-4 w-4 text-[var(--color-primary)] focus:ring-[var(--color-primary)] border-[var(--border-secondary)] rounded mt-0.5"
|
|
||||||
checked={formData.acceptTerms}
|
|
||||||
onChange={(e) => handleInputChange('acceptTerms', e.target.checked)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="acceptTerms" className="ml-2 block text-sm text-[var(--text-primary)]">
|
|
||||||
Acepto los{' '}
|
|
||||||
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
|
|
||||||
Términos de Servicio
|
|
||||||
</a>{' '}
|
|
||||||
y la{' '}
|
|
||||||
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
|
|
||||||
Política de Privacidad
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<input
|
|
||||||
id="acceptMarketing"
|
|
||||||
type="checkbox"
|
|
||||||
className="h-4 w-4 text-[var(--color-primary)] focus:ring-[var(--color-primary)] border-[var(--border-secondary)] rounded mt-0.5"
|
|
||||||
checked={formData.acceptMarketing}
|
|
||||||
onChange={(e) => handleInputChange('acceptMarketing', e.target.checked)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="acceptMarketing" className="ml-2 block text-sm text-[var(--text-primary)]">
|
|
||||||
Quiero recibir newsletters y novedades sobre el producto (opcional)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handlePrevStep}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
Atrás
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isStep3Valid || loading}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
{loading ? 'Creando cuenta...' : 'Crear cuenta'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 text-center text-xs text-[var(--text-secondary)]">
|
|
||||||
¿Necesitas ayuda?{' '}
|
|
||||||
<a href="#" className="text-[var(--color-primary)] hover:text-[var(--color-primary-light)]">
|
|
||||||
Contáctanos
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</PublicLayout>
|
</PublicLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const mockLogin = async (email: string, password: string): Promise<{ user: User;
|
|||||||
// Simulate API delay
|
// Simulate API delay
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
if (email === 'admin@bakery.com' && password === 'admin') {
|
if (email === 'admin@bakery.com' && password === 'admin12345') {
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
id: '1',
|
id: '1',
|
||||||
|
|||||||
Reference in New Issue
Block a user