diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 29b89425..56a8b3b0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,6 +8,7 @@ import { LoadingSpinner } from './components/shared/LoadingSpinner'; import { AppRouter } from './router/AppRouter'; import { ThemeProvider } from './contexts/ThemeContext'; import { AuthProvider } from './contexts/AuthContext'; +import { BakeryProvider } from './contexts/BakeryContext'; import { SSEProvider } from './contexts/SSEContext'; const queryClient = new QueryClient({ @@ -28,21 +29,23 @@ function App() { - - }> - - - - + + + }> + + + + + diff --git a/frontend/src/components/domain/auth/LoginForm.tsx b/frontend/src/components/domain/auth/LoginForm.tsx index b84e8c3a..3fa0c291 100644 --- a/frontend/src/components/domain/auth/LoginForm.tsx +++ b/frontend/src/components/domain/auth/LoginForm.tsx @@ -1,7 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; import { Button, Input, Card } from '../../ui'; -import { useAuth } from '../../../hooks/api/useAuth'; -import { UserLogin } from '../../../types/auth.types'; +import { useAuthActions, useAuthLoading, useAuthError } from '../../../stores/auth.store'; import { useToast } from '../../../hooks/ui/useToast'; interface LoginFormProps { @@ -12,7 +11,9 @@ interface LoginFormProps { autoFocus?: boolean; } -interface ExtendedUserLogin extends UserLogin { +interface LoginCredentials { + email: string; + password: string; remember_me: boolean; } @@ -23,16 +24,18 @@ export const LoginForm: React.FC = ({ className, autoFocus = true }) => { - const [credentials, setCredentials] = useState({ + const [credentials, setCredentials] = useState({ email: '', password: '', remember_me: false }); - const [errors, setErrors] = useState>({}); + const [errors, setErrors] = useState>({}); const [showPassword, setShowPassword] = useState(false); const emailInputRef = useRef(null); - const { login, isLoading, error } = useAuth(); + const { login } = useAuthActions(); + const isLoading = useAuthLoading(); + const error = useAuthError(); const { showToast } = useToast(); // Auto-focus on email field when component mounts @@ -43,7 +46,7 @@ export const LoginForm: React.FC = ({ }, [autoFocus]); const validateForm = (): boolean => { - const newErrors: Partial = {}; + const newErrors: Partial = {}; if (!credentials.email.trim()) { newErrors.email = 'El email es requerido'; @@ -74,43 +77,39 @@ export const LoginForm: React.FC = ({ } try { - const loginData: UserLogin = { - email: credentials.email, - password: credentials.password, - remember_me: credentials.remember_me - }; - - const success = await login(loginData); - 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.' - }); - } + await login(credentials.email, credentials.password); + showToast({ + type: 'success', + title: 'Sesión iniciada correctamente', + message: '¡Bienvenido de vuelta a tu panadería!' + }); + onSuccess?.(); } catch (err) { showToast({ type: 'error', - title: 'Error de conexión', - message: 'No se pudo conectar con el servidor. Verifica tu conexión a internet.' + title: 'Error al iniciar sesión', + 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) => { + const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; setCredentials(prev => ({ ...prev, [field]: value })); if (errors[field]) { setErrors(prev => ({ ...prev, [field]: undefined })); } }; + const handleDemoLogin = () => { + setCredentials({ + email: 'admin@bakery.com', + password: 'admin12345', + remember_me: false + }); + setErrors({}); + }; + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !isLoading) { handleSubmit(e as any); @@ -235,7 +234,7 @@ export const LoginForm: React.FC = ({ 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" disabled={isLoading} aria-describedby="remember-me-description" @@ -295,6 +294,30 @@ export const LoginForm: React.FC = ({
Presiona Enter o haz clic para iniciar sesión con tus credenciales
+ + {/* Demo Login Section */} +
+
+
+
+
+
+ Demo +
+
+ +
+ +
+
{onRegisterClick && ( diff --git a/frontend/src/components/domain/auth/PasswordResetForm.tsx b/frontend/src/components/domain/auth/PasswordResetForm.tsx index 902c2111..ab742c98 100644 --- a/frontend/src/components/domain/auth/PasswordResetForm.tsx +++ b/frontend/src/components/domain/auth/PasswordResetForm.tsx @@ -128,7 +128,8 @@ export const PasswordResetForm: React.FC = ({ }; // Handle password change to update strength - const handlePasswordChange = (value: string) => { + const handlePasswordChange = (e: React.ChangeEvent) => { + const value = e.target.value; setPassword(value); setPasswordStrength(calculatePasswordStrength(value)); clearError('password'); @@ -361,8 +362,8 @@ export const PasswordResetForm: React.FC = ({ label="Correo Electrónico" placeholder="tu.email@panaderia.com" value={email} - onChange={(value) => { - setEmail(value); + onChange={(e) => { + setEmail(e.target.value); clearError('email'); }} error={errors.email} @@ -486,8 +487,8 @@ export const PasswordResetForm: React.FC = ({ label="Confirmar Nueva Contraseña" placeholder="Repite tu nueva contraseña" value={confirmPassword} - onChange={(value) => { - setConfirmPassword(value); + onChange={(e) => { + setConfirmPassword(e.target.value); clearError('confirmPassword'); }} error={errors.confirmPassword} diff --git a/frontend/src/components/domain/auth/ProfileSettings.tsx b/frontend/src/components/domain/auth/ProfileSettings.tsx index 04087487..a0023a5d 100644 --- a/frontend/src/components/domain/auth/ProfileSettings.tsx +++ b/frontend/src/components/domain/auth/ProfileSettings.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { Button, Input, Card, Select, Avatar, Modal } from '../../ui'; -import { useAuth } from '../../../hooks/api/useAuth'; -import { User } from '../../../types/auth.types'; +import { useAuthUser } from '../../../stores/auth.store'; import { useToast } from '../../../hooks/ui/useToast'; interface ProfileSettingsProps { @@ -11,7 +10,8 @@ interface ProfileSettingsProps { } interface ProfileFormData { - full_name: string; + first_name: string; + last_name: string; email: string; phone: string; language: string; @@ -19,16 +19,6 @@ interface ProfileFormData { 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 { email_notifications: boolean; @@ -50,29 +40,23 @@ export const ProfileSettings: React.FC = ({ className, initialTab = 'profile' }) => { - const { user, updateProfile, isLoading, error } = useAuth(); + const user = useAuthUser(); const { showToast } = useToast(); const fileInputRef = useRef(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState<'profile' | 'security' | 'preferences' | 'notifications'>(initialTab); + + // Mock data for profile const [profileData, setProfileData] = useState({ - full_name: '', - email: '', - phone: '', + first_name: 'María', + last_name: 'González Pérez', + email: 'admin@bakery.com', + phone: '+34 612 345 678', language: 'es', timezone: 'Europe/Madrid', - avatar_url: '' - }); - - const [bakeryData, setBakeryData] = useState({ - bakery_name: '', - bakery_type: 'traditional', - address: '', - city: '', - postal_code: '', - country: 'España', - website: '', - description: '' + avatar_url: 'https://images.unsplash.com/photo-1494790108755-2616b612b372?w=150&h=150&fit=crop&crop=face' }); const [notificationSettings, setNotificationSettings] = useState({ @@ -105,17 +89,6 @@ export const ProfileSettings: React.FC = ({ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [previewImage, setPreviewImage] = useState(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 = [ { value: 'es', label: 'Español' }, @@ -133,31 +106,25 @@ export const ProfileSettings: React.FC = ({ { value: 'Europe/Rome', label: 'Roma (CET/CEST)' } ]; - // Initialize form data with user data - useEffect(() => { - if (user) { - setProfileData({ - full_name: user.full_name || '', - email: user.email || '', - phone: user.phone || '', - language: user.language || 'es', - timezone: user.timezone || 'Europe/Madrid', - avatar_url: user.avatar_url || '' - }); + // Mock update profile function + const updateProfile = async (data: any): Promise => { + setIsLoading(true); + setError(null); + + try { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 1500)); - // Initialize bakery data (would come from a separate bakery API) - setBakeryData({ - bakery_name: (user as any).bakery_name || '', - bakery_type: (user as any).bakery_type || 'traditional', - address: (user as any).address || '', - city: (user as any).city || '', - postal_code: (user as any).postal_code || '', - country: (user as any).country || 'España', - website: (user as any).website || '', - description: (user as any).description || '' - }); + // Simulate successful update + console.log('Profile updated:', data); + setIsLoading(false); + return true; + } catch (err) { + setError('Error updating profile'); + setIsLoading(false); + return false; } - }, [user]); + }; // Profile picture upload handler const handleImageUpload = async (event: React.ChangeEvent) => { @@ -227,10 +194,16 @@ export const ProfileSettings: React.FC = ({ const validateProfileForm = (): boolean => { const newErrors: Record = {}; - if (!profileData.full_name.trim()) { - newErrors.full_name = 'El nombre completo es requerido'; - } else if (profileData.full_name.trim().length < 2) { - newErrors.full_name = 'El nombre debe tener al menos 2 caracteres'; + if (!profileData.first_name.trim()) { + newErrors.first_name = 'El nombre es requerido'; + } else if (profileData.first_name.trim().length < 2) { + 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()) { @@ -242,37 +215,12 @@ export const ProfileSettings: React.FC = ({ 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)'; } + setErrors(newErrors); return Object.keys(newErrors).length === 0; }; - const validateBakeryForm = (): boolean => { - const newErrors: Record = {}; - - 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 newErrors: Record = {}; @@ -358,15 +306,25 @@ export const ProfileSettings: React.FC = ({ }); }; - const handleProfileInputChange = (field: keyof ProfileFormData) => (value: string) => { + const handleProfileInputChange = (field: keyof ProfileFormData) => (e: React.ChangeEvent) => { + const value = e.target.value; setProfileData(prev => ({ ...prev, [field]: value })); - setHasChanges(true); + setHasChanges(prev => ({ ...prev, profile: true })); if (errors[field]) { 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) => { + const value = e.target.value; setPasswordData(prev => ({ ...prev, [field]: value })); if (errors[field]) { setErrors(prev => ({ ...prev, [field]: undefined })); @@ -386,32 +344,47 @@ export const ProfileSettings: React.FC = ({ { 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) { - return ( - -
- Cargando información del usuario... -
-
- ); - } + // Mock user data for display + const mockUser = { + first_name: 'María', + last_name: 'González', + email: 'admin@bakery.com', + bakery_name: 'Panadería San Miguel', + avatar_url: 'https://images.unsplash.com/photo-1494790108755-2616b612b372?w=150&h=150&fit=crop&crop=face' + }; return (
{/* Header */} - -
- -
-

- {user.first_name} {user.last_name} + +
+
+ +
+
+
+

+ {mockUser.first_name} {mockUser.last_name}

-

{user.email}

-

{user.bakery_name}

+

{mockUser.email}

+

+ + + + Trabajando en {mockUser.bakery_name} +

+
+
+
+
+ En línea +
+

Última vez activo: ahora

@@ -439,141 +412,170 @@ export const ProfileSettings: React.FC = ({

-
+
{activeTab === 'profile' && ( -
-
-
-

- Información Personal -

- - + {/* Profile Photo Section */} +
+
+ - - fileInputRef.current?.click()} + className="absolute -bottom-2 -right-2 bg-color-primary text-white rounded-full p-2 shadow-lg hover:bg-color-primary-dark transition-colors" disabled={isLoading} - required - /> - - - - + + + + + +
- -
-

- Información del Negocio -

- - - - - -
- +
+

+ {profileData.first_name} {profileData.last_name} +

+

{profileData.email}

+

Última actualización: Hace 2 días

+
+ {uploadingImage && ( +
+ + + + +
+ )} +
+ + +
+ {/* Personal Information */} +
+

+ Información Personal +

- +
+ + + + + + + +
+
+ + {/* Preferences */} +
+

+ Preferencias +

+ +
+ +
-
- -
- - -
- + }} + disabled={!hasChanges.profile || isLoading} + > + Cancelar + + +
+ +
)} {activeTab === 'security' && ( diff --git a/frontend/src/components/domain/auth/RegisterForm.tsx b/frontend/src/components/domain/auth/RegisterForm.tsx index 42573556..679ea4b3 100644 --- a/frontend/src/components/domain/auth/RegisterForm.tsx +++ b/frontend/src/components/domain/auth/RegisterForm.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect } from 'react'; -import { Button, Input, Card, Select } from '../../ui'; +import React, { useState } from 'react'; +import { Button, Input, Card } from '../../ui'; import { useAuth } from '../../../hooks/api/useAuth'; import { UserRegistration } from '../../../types/auth.types'; import { useToast } from '../../../hooks/ui/useToast'; @@ -8,89 +8,38 @@ interface RegisterFormProps { onSuccess?: () => void; onLoginClick?: () => void; className?: string; - showProgressSteps?: boolean; } -type RegistrationStep = 'personal' | 'bakery' | 'security' | 'verification'; - -interface ExtendedUserRegistration { - // Personal Information +interface SimpleUserRegistration { full_name: 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; confirmPassword: string; - - // Terms acceptTerms: boolean; - acceptPrivacy: boolean; - acceptMarketing: boolean; } export const RegisterForm: React.FC = ({ onSuccess, onLoginClick, - className, - showProgressSteps = true + className }) => { - const [currentStep, setCurrentStep] = useState('personal'); - const [completedSteps, setCompletedSteps] = useState>(new Set()); - const [isEmailVerificationSent, setIsEmailVerificationSent] = useState(false); - - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ full_name: '', email: '', - phone: '', - tenant_name: '', - bakery_type: 'traditional', - address: '', - city: '', - postal_code: '', - country: 'España', password: '', confirmPassword: '', - acceptTerms: false, - acceptPrivacy: false, - acceptMarketing: false + acceptTerms: false }); - const [errors, setErrors] = useState>({}); + const [errors, setErrors] = useState>({}); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); - const { register, verifyEmail, isLoading, error } = useAuth(); + const { register, isLoading, error } = useAuth(); const { showToast } = useToast(); - const steps: { id: RegistrationStep; title: string; description: string }[] = [ - { id: 'personal', title: 'Información Personal', description: 'Tus datos básicos' }, - { 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 = {}; + const validateForm = (): boolean => { + const newErrors: Partial = {}; if (!formData.full_name.trim()) { newErrors.full_name = 'El nombre completo es requerido'; @@ -104,52 +53,10 @@ export const RegisterForm: React.FC = ({ 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 = {}; - - 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 = {}; - if (!formData.password) { newErrors.password = 'La contraseña es requerida'; } else if (formData.password.length < 8) { 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) { @@ -162,87 +69,41 @@ export const RegisterForm: React.FC = ({ newErrors.acceptTerms = 'Debes aceptar los términos y condiciones'; } - if (!formData.acceptPrivacy) { - newErrors.acceptPrivacy = 'Debes aceptar la política de privacidad'; - } - - setErrors(prev => ({ ...prev, ...newErrors })); + setErrors(newErrors); return Object.keys(newErrors).length === 0; }; - const handleNextStep = (e?: React.FormEvent) => { - if (e) e.preventDefault(); + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); - let isValid = false; - switch (currentStep) { - case 'personal': - isValid = validatePersonalStep(); - break; - case 'bakery': - isValid = validateBakeryStep(); - break; - case 'security': - isValid = validateSecurityStep(); - break; - default: - isValid = true; + if (!validateForm()) { + return; } - 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 { const registrationData: UserRegistration = { full_name: formData.full_name, email: formData.email, password: formData.password, - tenant_name: formData.tenant_name, - phone: formData.phone + tenant_name: 'Default Bakery', // Default value since we're not collecting it + phone: '' // Optional field }; const success = await register(registrationData); if (success) { - setIsEmailVerificationSent(true); - setCurrentStep('verification'); showToast({ type: 'success', 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 { showToast({ type: 'error', title: 'Error al crear la cuenta', 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) { showToast({ @@ -250,540 +111,181 @@ export const RegisterForm: React.FC = ({ title: 'Error de conexión', 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) => { + const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; setFormData(prev => ({ ...prev, [field]: value })); if (errors[field]) { 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 ( - +

- Registra tu Panadería + Crear Cuenta

- Crea tu cuenta y comienza a digitalizar tu negocio + Únete y comienza hoy mismo

- {/* Progress Indicator */} - {showProgressSteps && ( -
-
- {steps.map((step, index) => ( -
-
- {completedSteps.has(step.id) ? ( - - - - ) : ( - index + 1 - )} -
-
-
- {step.title} -
-
- {step.description} -
-
- {index < steps.length - 1 && ( -
- )} -
- ))} -
- - {/* Progress Bar */} -
-
-
-
- )} - - {/* Step Content */} - {currentStep !== 'verification' ? ( -
- {/* Personal Information Step */} - {currentStep === 'personal' && ( -
-
-

- Información Personal -

-

- Cuéntanos sobre ti para personalizar tu experiencia -

-
- -
- - - - } - /> - - - - - } - /> - - - - - } - /> -
-
- )} - {/* Bakery Information Step */} - {currentStep === 'bakery' && ( -
-
-

- Información de tu Panadería -

-

- Déjanos conocer los detalles de tu negocio -

-
- -
- - - - } - /> - - - - - - } - /> - -
- - - - - -
-
- )} - - {/* Security Step */} - {currentStep === 'security' && ( -
-
-

- Seguridad y Términos -

-

- Crea una contraseña segura y acepta nuestros términos -

-
- -
- - - - } - rightIcon={ - - } - /> - - - - - } - rightIcon={ - - } - /> -
- -
-
- 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} - /> - -
- {errors.acceptTerms && ( -

{errors.acceptTerms}

- )} - -
- 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} - /> - -
- {errors.acceptPrivacy && ( -

{errors.acceptPrivacy}

- )} - -
- 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} - /> - -
-
-
- )} - - {/* Navigation Buttons */} -
- {currentStep !== 'personal' && ( - - )} - -
- - -
- - {error && ( -
- - - - {error} -
- )} - - ) : ( - /* Verification Step */ -
-
- - +
+
-
- -

- ¡Cuenta creada exitosamente! -

- -
-

- Hemos enviado un enlace de verificación a: -

-

- {formData.email} -

-

- Revisa tu bandeja de entrada (y la carpeta de spam) y haz clic en el enlace para activar tu cuenta. -

-
- -
- + {showPassword ? ( + + + + ) : ( + + + + + )} + + } + /> + + + + + } + rightIcon={ + + } + /> + +
+
+ +
+ {errors.acceptTerms && ( +

{errors.acceptTerms}

+ )}
- )} + + + + {error && ( +
+ + + + {error} +
+ )} + {/* Login Link */} - {onLoginClick && currentStep !== 'verification' && ( + {onLoginClick && (

¿Ya tienes una cuenta? diff --git a/frontend/src/components/layout/Header/Header.tsx b/frontend/src/components/layout/Header/Header.tsx index e811876f..114ff102 100644 --- a/frontend/src/components/layout/Header/Header.tsx +++ b/frontend/src/components/layout/Header/Header.tsx @@ -3,10 +3,12 @@ import { clsx } from 'clsx'; import { useNavigate } from 'react-router-dom'; import { useAuthUser, useIsAuthenticated, useAuthActions } from '../../../stores'; import { useTheme } from '../../../contexts/ThemeContext'; +import { useBakery } from '../../../contexts/BakeryContext'; import { Button } from '../../ui'; import { Avatar } from '../../ui'; import { Badge } from '../../ui'; import { Modal } from '../../ui'; +import { BakerySelector } from '../../ui/BakerySelector/BakerySelector'; import { Menu, Search, @@ -100,6 +102,7 @@ export const Header = forwardRef(({ const isAuthenticated = useIsAuthenticated(); const { logout } = useAuthActions(); const { theme, resolvedTheme, setTheme } = useTheme(); + const { bakeries, currentBakery, selectBakery } = useBakery(); const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); const [isSearchFocused, setIsSearchFocused] = useState(false); @@ -216,7 +219,7 @@ export const Header = forwardRef(({ aria-label="Navegación principal" > {/* Left section */} -

+
{/* Mobile menu button */} {/* Logo */} -
+
{logo || ( <>
@@ -237,7 +240,7 @@ export const Header = forwardRef(({

@@ -247,6 +250,40 @@ export const Header = forwardRef(({ )}

+ {/* Bakery Selector - Desktop */} + {isAuthenticated && currentBakery && bakeries.length > 0 && ( +
+ { + // 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]" + /> +
+ )} + + {/* Bakery Selector - Mobile (in title area) */} + {isAuthenticated && currentBakery && bakeries.length > 0 && ( +
+ { + // TODO: Navigate to add bakery page or open modal + console.log('Add new bakery'); + }} + size="sm" + className="w-full max-w-none" + /> +
+ )} + {/* Search */} {showSearch && isAuthenticated && (
> = { training: GraduationCap, notifications: Bell, settings: Settings, + user: User, }; /** diff --git a/frontend/src/components/ui/BakerySelector/BakerySelector.tsx b/frontend/src/components/ui/BakerySelector/BakerySelector.tsx new file mode 100644 index 00000000..70122a93 --- /dev/null +++ b/frontend/src/components/ui/BakerySelector/BakerySelector.tsx @@ -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 = ({ + 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(null); + const buttonRef = useRef(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 ( +
+ + + {isOpen && createPortal( + <> + {/* Mobile backdrop */} +
setIsOpen(false)} + /> + +
+
+ Mis Panaderías ({bakeries.length}) +
+ +
+ {bakeries.map((bakery) => ( + + ))} +
+ + {onAddBakery && ( + <> +
+ + + )} +
+ , + document.body + )} +
+ ); +}; + +export default BakerySelector; \ No newline at end of file diff --git a/frontend/src/components/ui/BakerySelector/index.ts b/frontend/src/components/ui/BakerySelector/index.ts new file mode 100644 index 00000000..2f8daa84 --- /dev/null +++ b/frontend/src/components/ui/BakerySelector/index.ts @@ -0,0 +1,2 @@ +export { BakerySelector } from './BakerySelector'; +export type { default as BakerySelector } from './BakerySelector'; \ No newline at end of file diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts index a04154d7..a4b6f0ae 100644 --- a/frontend/src/components/ui/index.ts +++ b/frontend/src/components/ui/index.ts @@ -16,6 +16,7 @@ export { ListItem } from './ListItem'; export { StatsCard, StatsGrid } from './Stats'; export { StatusCard, getStatusColor } from './StatusCard'; export { StatusModal } from './StatusModal'; +export { BakerySelector } from './BakerySelector'; // Export types export type { ButtonProps } from './Button'; diff --git a/frontend/src/contexts/BakeryContext.tsx b/frontend/src/contexts/BakeryContext.tsx new file mode 100644 index 00000000..84a42b39 --- /dev/null +++ b/frontend/src/contexts/BakeryContext.tsx @@ -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 } } + | { 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) => Promise; + updateBakery: (id: string, updates: Partial) => Promise; + removeBakery: (id: string) => Promise; + refreshBakeries: () => Promise; + hasPermission: (permission: string) => boolean; + canAccess: (resource: string, action: string) => boolean; +} + +const BakeryContext = createContext(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) => { + 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) => { + 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 ( + + {children} + + ); +} + +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 }; +}; \ No newline at end of file diff --git a/frontend/src/pages/app/settings/bakery-config/BakeryConfigPage.tsx b/frontend/src/pages/app/settings/bakery-config/BakeryConfigPage.tsx index ca477e7f..16a639c0 100644 --- a/frontend/src/pages/app/settings/bakery-config/BakeryConfigPage.tsx +++ b/frontend/src/pages/app/settings/bakery-config/BakeryConfigPage.tsx @@ -1,66 +1,76 @@ import React, { useState } from 'react'; -import { Store, MapPin, Clock, Phone, Mail, Globe, Save, RotateCcw } from 'lucide-react'; -import { Button, Card, Input } from '../../../../components/ui'; +import { Store, MapPin, Clock, Phone, Mail, Globe, Save, X, Edit3 } from 'lucide-react'; +import { Button, Card, Input, Select } from '../../../../components/ui'; 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 [config, setConfig] = useState({ - general: { - name: 'Panadería Artesanal San Miguel', - description: 'Panadería tradicional con más de 30 años de experiencia', - logo: '', - website: 'https://panaderiasanmiguel.com', - email: 'info@panaderiasanmiguel.com', - phone: '+34 912 345 678' - }, - location: { - address: 'Calle Mayor 123', - city: 'Madrid', - postalCode: '28001', - country: 'España', - coordinates: { - lat: 40.4168, - lng: -3.7038 - } - }, - schedule: { - 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 { showToast } = useToast(); + + const [activeTab, setActiveTab] = useState<'general' | 'location' | 'business' | 'hours'>('general'); + const [isEditing, setIsEditing] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const [config, setConfig] = useState({ + name: 'Panadería Artesanal San Miguel', + description: 'Panadería tradicional con más de 30 años de experiencia', + email: 'info@panaderiasanmiguel.com', + phone: '+34 912 345 678', + website: 'https://panaderiasanmiguel.com', + address: 'Calle Mayor 123', + city: 'Madrid', + postalCode: '28001', + country: 'España', + taxId: 'B12345678', + currency: 'EUR', + timezone: 'Europe/Madrid', + language: 'es' }); - const [hasChanges, setHasChanges] = useState(false); - const [activeTab, setActiveTab] = useState('general'); + const [businessHours, setBusinessHours] = useState({ + 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>({}); const tabs = [ - { id: 'general', label: 'General', icon: Store }, - { id: 'location', label: 'Ubicación', icon: MapPin }, - { id: 'schedule', label: 'Horarios', icon: Clock }, - { id: 'business', label: 'Empresa', icon: Globe } + { id: 'general' as const, label: 'General', icon: Store }, + { id: 'location' as const, label: 'Ubicación', icon: MapPin }, + { id: 'business' as const, label: 'Empresa', icon: Globe }, + { id: 'hours' as const, label: 'Horarios', icon: Clock } ]; const daysOfWeek = [ @@ -73,40 +83,94 @@ const BakeryConfigPage: React.FC = () => { { key: 'sunday', label: 'Domingo' } ]; - const handleInputChange = (section: string, field: string, value: any) => { - setConfig(prev => ({ + const currencyOptions = [ + { 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 = {}; + + 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) => { + 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, - [section]: { - ...prev[section as keyof typeof prev], + [day]: { + ...prev[day], [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 ( @@ -114,366 +178,302 @@ const BakeryConfigPage: React.FC = () => { - - -
- } /> -
- {/* Sidebar */} -
- - - + {/* Bakery Header */} + +
+
+ {config.name.charAt(0)} +
+
+

+ {config.name} +

+

{config.email}

+

{config.address}, {config.city}

+
+
+ {!isEditing && ( + + )} +
+
+
+ + {/* Configuration Tabs */} + + {/* Tab Navigation */} +
+
- {/* Content */} -
+ {/* Tab Content */} +
{activeTab === 'general' && ( - -

Información General

-
-
-
- - handleInputChange('general', 'name', e.target.value)} - placeholder="Nombre de tu panadería" - /> -
- -
- - handleInputChange('general', 'website', e.target.value)} - placeholder="https://tu-panaderia.com" - /> -
-
- -
- -