Improve the frontend 3

This commit is contained in:
Urtzi Alfaro
2025-10-30 21:08:07 +01:00
parent 36217a2729
commit 63f5c6d512
184 changed files with 21512 additions and 7442 deletions

View File

@@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Input, Card } from '../../ui';
import { useAuthActions, useAuthLoading, useAuthError } from '../../../stores/auth.store';
import { useToast } from '../../../hooks/ui/useToast';
import { showToast } from '../../../utils/toast';
interface LoginFormProps {
onSuccess?: () => void;
@@ -38,7 +38,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
const { login } = useAuthActions();
const isLoading = useAuthLoading();
const error = useAuthError();
const { success, error: showError } = useToast();
// Auto-focus on email field when component mounts
useEffect(() => {
@@ -78,7 +78,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
try {
await login(credentials.email, credentials.password);
success('¡Bienvenido de vuelta a tu panadería!', {
showToast.success('¡Bienvenido de vuelta a tu panadería!', {
title: 'Sesión iniciada correctamente'
});
onSuccess?.();

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import { Button, Input, Card } from '../../ui';
import { PasswordCriteria, validatePassword, getPasswordErrors } from '../../ui/PasswordCriteria';
import { useAuthActions } from '../../../stores/auth.store';
import { useToast } from '../../../hooks/ui/useToast';
import { showToast } from '../../../utils/toast';
import { useResetPassword } from '../../../api/hooks/auth';
interface PasswordResetFormProps {
@@ -39,7 +39,7 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
const { mutateAsync: resetPasswordMutation, isPending: isResetting } = useResetPassword();
const isLoading = isResetting;
const error = null;
const { showToast } = useToast();
const isResetMode = Boolean(token) || mode === 'reset';
@@ -62,11 +62,9 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
setIsTokenValid(isValidFormat);
if (!isValidFormat) {
showToast({
type: 'error',
title: 'Token inválido',
message: 'El enlace de restablecimiento no es válido o ha expirado'
});
showToast.error('El enlace de restablecimiento no es válido o ha expirado', {
title: 'Token inválido'
});
}
}
}, [token, showToast]);
@@ -154,16 +152,12 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
// Note: Password reset request functionality needs to be implemented in backend
// For now, show a message that the feature is coming soon
setIsEmailSent(true);
showToast({
type: 'info',
title: 'Función en desarrollo',
message: 'La solicitud de restablecimiento de contraseña estará disponible próximamente. Por favor, contacta al administrador.'
showToast.info('La solicitud de restablecimiento de contraseña estará disponible próximamente. Por favor, contacta al administrador.', {
title: 'Función en desarrollo'
});
} catch (err) {
showToast({
type: 'error',
title: 'Error de conexión',
message: 'No se pudo conectar con el servidor. Verifica tu conexión a internet.'
showToast.error('No se pudo conectar con el servidor. Verifica tu conexión a internet.', {
title: 'Error de conexión'
});
}
};
@@ -180,10 +174,8 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
}
if (!token || isTokenValid === false) {
showToast({
type: 'error',
title: 'Token inválido',
message: 'El enlace de restablecimiento no es válido. Solicita uno nuevo.'
showToast.error('El enlace de restablecimiento no es válido. Solicita uno nuevo.', {
title: 'Token inválido'
});
return;
}
@@ -195,18 +187,14 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
new_password: password
});
showToast({
type: 'success',
title: 'Contraseña actualizada',
message: '¡Tu contraseña ha sido restablecida exitosamente! Ya puedes iniciar sesión.'
showToast.success('¡Tu contraseña ha sido restablecida exitosamente! Ya puedes iniciar sesión.', {
title: 'Contraseña actualizada'
});
onSuccess?.();
} catch (err: any) {
const errorMessage = err?.response?.data?.detail || err?.message || 'El enlace ha expirado o no es válido. Solicita un nuevo restablecimiento.';
showToast({
type: 'error',
title: 'Error al restablecer contraseña',
message: errorMessage
showToast.error(errorMessage, {
title: 'Error al restablecer contraseña'
});
}
};
@@ -599,4 +587,4 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
);
};
export default PasswordResetForm;
export default PasswordResetForm;

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { Button, Input, Card, Select, Avatar, Modal } from '../../ui';
import { useAuthUser } from '../../../stores/auth.store';
import { useToast } from '../../../hooks/ui/useToast';
import { showToast } from '../../../utils/toast';
import { useUpdateProfile, useChangePassword, useAuthProfile } from '../../../api/hooks/auth';
interface ProfileSettingsProps {
@@ -42,7 +42,7 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
initialTab = 'profile'
}) => {
const user = useAuthUser();
const { showToast } = useToast();
const fileInputRef = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -139,20 +139,16 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
// Validate file type
if (!file.type.startsWith('image/')) {
showToast({
type: 'error',
title: 'Archivo inválido',
message: 'Por favor, selecciona una imagen válida'
showToast.error('Solo se permiten archivos de imagen (JPEG, PNG, GIF, WEBP)', {
title: 'Error'
});
return;
}
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
showToast({
type: 'error',
title: 'Archivo muy grande',
message: 'La imagen debe ser menor a 5MB'
showToast.error('El archivo es demasiado grande. Máximo 5MB permitido', {
title: 'Error'
});
return;
}
@@ -174,16 +170,12 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
setProfileData(prev => ({ ...prev, avatar: newImageUrl }));
setHasChanges(prev => ({ ...prev, profile: true }));
showToast({
type: 'success',
title: 'Imagen subida',
message: 'Tu foto de perfil ha sido actualizada'
showToast.success('¡Éxito!', {
title: 'Foto de perfil actualizada correctamente'
});
} catch (error) {
showToast({
type: 'error',
title: 'Error al subir imagen',
message: 'No se pudo subir la imagen. Intenta de nuevo.'
showToast.error('No se pudo actualizar la foto de perfil', {
title: 'Error'
});
} finally {
setUploadingImage(false);
@@ -283,17 +275,13 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
});
setHasChanges(false);
showToast({
type: 'success',
title: 'Perfil actualizado',
message: 'Tu información ha sido guardada correctamente'
showToast.success('¡Éxito!', {
title: 'Perfil actualizado correctamente'
});
onSuccess?.();
} catch (err) {
showToast({
type: 'error',
title: 'Error al actualizar',
message: 'No se pudo actualizar tu perfil'
showToast.error('No se pudo actualizar el perfil', {
title: 'Error'
});
}
};
@@ -311,10 +299,8 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
new_password: passwordData.newPassword,
});
showToast({
type: 'success',
title: 'Contraseña actualizada',
message: 'Tu contraseña ha sido cambiada correctamente'
showToast.success('¡Éxito!', {
title: 'Contraseña cambiada correctamente'
});
setPasswordData({
@@ -323,10 +309,8 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
confirmNewPassword: ''
});
} catch (error) {
showToast({
type: 'error',
title: 'Error al cambiar contraseña',
message: 'No se pudo cambiar tu contraseña. Por favor, verifica tu contraseña actual.'
showToast.error('No se pudo cambiar la contraseña', {
title: 'Error'
});
}
};
@@ -725,4 +709,4 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
);
};
export default ProfileSettings;
export default ProfileSettings;

View File

@@ -4,7 +4,7 @@ import { useSearchParams } from 'react-router-dom';
import { Button, Input, Card } from '../../ui';
import { PasswordCriteria, validatePassword, getPasswordErrors } from '../../ui/PasswordCriteria';
import { useAuthActions, useAuthLoading, useAuthError } from '../../../stores/auth.store';
import { useToast } from '../../../hooks/ui/useToast';
import { showToast } from '../../../utils/toast';
import { SubscriptionPricingCards } from '../../subscription/SubscriptionPricingCards';
import PaymentForm from './PaymentForm';
import { loadStripe } from '@stripe/stripe-js';
@@ -68,7 +68,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
const { register } = useAuthActions();
const isLoading = useAuthLoading();
const error = useAuthError();
const { success: showSuccessToast, error: showErrorToast } = useToast();
// Detect pilot program participation
const { isPilot, couponCode, trialMonths } = usePilotDetection();
@@ -236,12 +236,12 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
? '¡Bienvenido al programa piloto! Tu cuenta ha sido creada con 3 meses gratis.'
: '¡Bienvenido! Tu cuenta ha sido creada correctamente.';
showSuccessToast(t('auth:register.registering', successMessage), {
showToast.success(t('auth:register.registering', successMessage), {
title: t('auth:alerts.success_create', 'Cuenta creada exitosamente')
});
onSuccess?.();
} catch (err) {
showErrorToast(error || t('auth:register.register_button', 'No se pudo crear la cuenta. Verifica que el email no esté en uso.'), {
showToast.error(error || t('auth:register.register_button', 'No se pudo crear la cuenta. Verifica que el email no esté en uso.'), {
title: t('auth:alerts.error_create', 'Error al crear la cuenta')
});
}
@@ -252,7 +252,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
};
const handlePaymentError = (errorMessage: string) => {
showErrorToast(errorMessage, {
showToast.error(errorMessage, {
title: 'Error en el pago'
});
};

View File

@@ -3,7 +3,7 @@ import { Zap, Key, Settings as SettingsIcon, RefreshCw } from 'lucide-react';
import { AddModal, AddModalSection, AddModalField } from '../../ui/AddModal/AddModal';
import { posService } from '../../../api/services/pos';
import { POSProviderConfig, POSSystem, POSEnvironment } from '../../../api/types/pos';
import { useToast } from '../../../hooks/ui/useToast';
import { showToast } from '../../../utils/toast';
import { statusColors } from '../../../styles/colors';
interface CreatePOSConfigModalProps {
@@ -29,7 +29,7 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
}) => {
const [loading, setLoading] = useState(false);
const [selectedProvider, setSelectedProvider] = useState<POSSystem | ''>('');
const { addToast } = useToast();
// Initialize selectedProvider in edit mode
React.useEffect(() => {
@@ -250,7 +250,7 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
// Find selected provider
const provider = supportedProviders.find(p => p.id === formData.provider);
if (!provider) {
addToast('Por favor selecciona un sistema POS', { type: 'error' });
showToast.error('Por favor selecciona un sistema POS');
return;
}
@@ -298,17 +298,17 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
...payload,
config_id: existingConfig.id
});
addToast('Configuración actualizada correctamente', { type: 'success' });
showToast.success('Configuración actualizada correctamente');
} else {
await posService.createPOSConfiguration(payload);
addToast('Configuración creada correctamente', { type: 'success' });
showToast.success('Configuración creada correctamente');
}
onSuccess?.();
onClose();
} catch (error: any) {
console.error('Error saving POS configuration:', error);
addToast(error?.message || 'Error al guardar la configuración', { type: 'error' });
showToast.error(error?.message || 'Error al guardar la configuración');
throw error; // Let AddModal handle error state
} finally {
setLoading(false);
@@ -345,7 +345,7 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
// Custom validation if needed
if (errors && Object.keys(errors).length > 0) {
const firstError = Object.values(errors)[0];
addToast(firstError, { type: 'error' });
showToast.error(firstError);
}
}}
onFieldChange={handleFieldChange}

View File

@@ -396,6 +396,14 @@ export const Footer = forwardRef<FooterRef, FooterProps>(({
)}
</div>
{/* Made with love in Madrid */}
{!compact && (
<div className="flex items-center gap-2 text-[var(--text-tertiary)]">
<Heart className="w-4 h-4 text-red-500 fill-red-500" />
<span>{t('common:footer.made_with_love', 'Hecho con amor en Madrid')}</span>
</div>
)}
{/* Essential utilities only */}
<div className="flex items-center gap-4">
{/* Privacy links - minimal set */}

View File

@@ -168,12 +168,6 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
const baseNavigationRoutes = useMemo(() => getNavigationRoutes(), []);
const { filteredRoutes: subscriptionFilteredRoutes } = useSubscriptionAwareRoutes(baseNavigationRoutes);
// Force re-render when subscription changes
useEffect(() => {
// The subscriptionVersion change will trigger a re-render
// This ensures the sidebar picks up new route filtering based on updated subscription
}, [subscriptionVersion]);
// Map route paths to translation keys
const getTranslationKey = (routePath: string): string => {
const pathMappings: Record<string, string> = {

View File

@@ -0,0 +1,46 @@
import React from 'react';
export interface SliderProps {
min: number;
max: number;
step?: number;
value: number[];
onValueChange: (value: number[]) => void;
disabled?: boolean;
className?: string;
}
const Slider: React.FC<SliderProps> = ({
min,
max,
step = 1,
value,
onValueChange,
disabled = false,
className = '',
}) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = parseFloat(e.target.value);
onValueChange([newValue]);
};
return (
<div className={`flex items-center space-x-4 ${className}`}>
<input
type="range"
min={min}
max={max}
step={step}
value={value[0]}
onChange={handleChange}
disabled={disabled}
className="w-full h-2 bg-[var(--bg-secondary)] rounded-lg appearance-none cursor-pointer accent-[var(--color-primary)] disabled:opacity-50 disabled:cursor-not-allowed"
/>
<span className="text-sm text-[var(--text-secondary)] min-w-12">
{(value[0] * 100).toFixed(0)}%
</span>
</div>
);
};
export default Slider;

View File

@@ -0,0 +1,3 @@
export { default } from './Slider';
export { default as Slider } from './Slider';
export type { SliderProps } from './Slider';

View File

@@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { useTenant } from '../../stores/tenant.store';
import { useToast } from '../../hooks/ui/useToast';
import { showToast } from '../../utils/toast';
import { ChevronDown, Building2, Check, AlertCircle, Plus, X } from 'lucide-react';
interface TenantSwitcherProps {
@@ -36,7 +36,7 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
clearError,
} = useTenant();
const { success: showSuccessToast, error: showErrorToast } = useToast();
// Load tenants on mount
useEffect(() => {
@@ -150,11 +150,11 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
if (success) {
const newTenant = availableTenants?.find(t => t.id === tenantId);
showSuccessToast(`Switched to ${newTenant?.name}`, {
showToast.success(`Switched to ${newTenant?.name}`, {
title: 'Tenant Switched'
});
} else {
showErrorToast(error || 'Failed to switch tenant', {
showToast.error(error || 'Failed to switch tenant', {
title: 'Switch Failed'
});
}