Files
bakery-ia/frontend/src/pages/app/settings/profile/ProfilePage.tsx

383 lines
12 KiB
TypeScript
Raw Normal View History

2025-08-28 23:40:44 +02:00
import React, { useState } from 'react';
2025-08-31 22:14:05 +02:00
import { User, Mail, Phone, Lock, Globe, Clock, Camera, Save, X } from 'lucide-react';
import { Button, Card, Avatar, Input, Select } from '../../../../components/ui';
2025-08-28 23:40:44 +02:00
import { PageHeader } from '../../../../components/layout';
2025-08-31 22:14:05 +02:00
import { useAuthUser } from '../../../../stores/auth.store';
import { useToast } from '../../../../hooks/ui/useToast';
2025-09-11 18:21:32 +02:00
import { useAuthProfile, useUpdateProfile, useChangePassword } from '../../../../api/hooks/auth';
2025-08-31 22:14:05 +02:00
interface ProfileFormData {
first_name: string;
last_name: string;
email: string;
phone: string;
language: string;
timezone: string;
}
interface PasswordData {
currentPassword: string;
newPassword: string;
confirmPassword: string;
}
2025-08-28 23:40:44 +02:00
const ProfilePage: React.FC = () => {
2025-08-31 22:14:05 +02:00
const user = useAuthUser();
2025-09-11 18:21:32 +02:00
const { addToast } = useToast();
const { data: profile, isLoading: profileLoading, error: profileError } = useAuthProfile();
const updateProfileMutation = useUpdateProfile();
const changePasswordMutation = useChangePassword();
2025-08-31 22:14:05 +02:00
2025-08-28 23:40:44 +02:00
const [isEditing, setIsEditing] = useState(false);
2025-08-31 22:14:05 +02:00
const [isLoading, setIsLoading] = useState(false);
const [showPasswordForm, setShowPasswordForm] = useState(false);
const [profileData, setProfileData] = useState<ProfileFormData>({
2025-09-11 18:21:32 +02:00
first_name: '',
last_name: '',
email: '',
phone: '',
2025-08-31 22:14:05 +02:00
language: 'es',
timezone: 'Europe/Madrid'
});
2025-09-11 18:21:32 +02:00
// Update profile data when profile is loaded
React.useEffect(() => {
if (profile) {
setProfileData({
first_name: profile.first_name || '',
last_name: profile.last_name || '',
email: profile.email || '',
phone: profile.phone || '',
language: profile.language || 'es',
timezone: profile.timezone || 'Europe/Madrid'
});
}
}, [profile]);
2025-08-31 22:14:05 +02:00
const [passwordData, setPasswordData] = useState<PasswordData>({
currentPassword: '',
newPassword: '',
confirmPassword: ''
2025-08-28 23:40:44 +02:00
});
2025-08-31 22:14:05 +02:00
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 validatePassword = (): boolean => {
const newErrors: Record<string, string> = {};
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;
2025-08-28 23:40:44 +02:00
};
2025-08-31 22:14:05 +02:00
const handleSaveProfile = async () => {
if (!validateProfile()) return;
setIsLoading(true);
try {
2025-09-11 18:21:32 +02:00
await updateProfileMutation.mutateAsync(profileData);
2025-08-31 22:14:05 +02:00
setIsEditing(false);
2025-09-11 18:21:32 +02:00
addToast('Perfil actualizado correctamente', 'success');
2025-08-31 22:14:05 +02:00
} catch (error) {
2025-09-11 18:21:32 +02:00
addToast('No se pudo actualizar tu perfil', 'error');
2025-08-31 22:14:05 +02:00
} finally {
setIsLoading(false);
}
2025-08-28 23:40:44 +02:00
};
2025-09-11 18:21:32 +02:00
const handleChangePasswordSubmit = async () => {
2025-08-31 22:14:05 +02:00
if (!validatePassword()) return;
setIsLoading(true);
try {
2025-09-11 18:21:32 +02:00
await changePasswordMutation.mutateAsync({
current_password: passwordData.currentPassword,
new_password: passwordData.newPassword,
confirm_password: passwordData.confirmPassword
});
2025-08-31 22:14:05 +02:00
setShowPasswordForm(false);
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
2025-09-11 18:21:32 +02:00
addToast('Contraseña actualizada correctamente', 'success');
2025-08-31 22:14:05 +02:00
} catch (error) {
2025-09-11 18:21:32 +02:00
addToast('No se pudo cambiar tu contraseña', 'error');
2025-08-31 22:14:05 +02:00
} finally {
setIsLoading(false);
}
2025-08-28 23:40:44 +02:00
};
2025-08-31 22:14:05 +02:00
const handleInputChange = (field: keyof ProfileFormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
setProfileData(prev => ({ ...prev, [field]: e.target.value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
2025-08-28 23:40:44 +02:00
};
2025-08-31 22:14:05 +02:00
const handleSelectChange = (field: keyof ProfileFormData) => (value: string) => {
setProfileData(prev => ({ ...prev, [field]: value }));
2025-08-28 23:40:44 +02:00
};
2025-08-31 22:14:05 +02:00
const handlePasswordChange = (field: keyof PasswordData) => (e: React.ChangeEvent<HTMLInputElement>) => {
setPasswordData(prev => ({ ...prev, [field]: e.target.value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
2025-08-28 23:40:44 +02:00
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Mi Perfil"
description="Gestiona tu información personal y configuración de cuenta"
/>
2025-08-31 22:14:05 +02:00
{/* Profile Header */}
<Card className="p-6">
<div className="flex items-center gap-6">
<div className="relative">
<Avatar
2025-09-11 18:21:32 +02:00
src={profile?.avatar_url}
2025-08-31 22:14:05 +02:00
name={`${profileData.first_name} ${profileData.last_name}`}
size="xl"
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>
2025-08-28 23:40:44 +02:00
</div>
2025-08-31 22:14:05 +02:00
<div className="flex-1">
<h1 className="text-2xl font-bold text-text-primary mb-1">
{profileData.first_name} {profileData.last_name}
</h1>
<p className="text-text-secondary">{profileData.email}</p>
<div className="flex items-center gap-2 mt-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-sm text-text-tertiary">En línea</span>
2025-08-28 23:40:44 +02:00
</div>
</div>
2025-08-31 22:14:05 +02:00
<div className="flex gap-2">
{!isEditing && (
<Button
variant="outline"
onClick={() => setIsEditing(true)}
className="flex items-center gap-2"
>
<User className="w-4 h-4" />
Editar Perfil
</Button>
)}
<Button
variant="outline"
onClick={() => setShowPasswordForm(!showPasswordForm)}
className="flex items-center gap-2"
>
<Lock className="w-4 h-4" />
Cambiar Contraseña
</Button>
2025-08-28 23:40:44 +02:00
</div>
2025-08-31 22:14:05 +02:00
</div>
</Card>
2025-08-28 23:40:44 +02:00
2025-08-31 22:14:05 +02:00
{/* Profile Form */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Información Personal</h2>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
<Input
label="Nombre"
value={profileData.first_name}
onChange={handleInputChange('first_name')}
error={errors.first_name}
disabled={!isEditing || isLoading}
leftIcon={<User className="w-4 h-4" />}
/>
<Input
label="Apellidos"
value={profileData.last_name}
onChange={handleInputChange('last_name')}
error={errors.last_name}
disabled={!isEditing || isLoading}
/>
<Input
type="email"
label="Correo Electrónico"
value={profileData.email}
onChange={handleInputChange('email')}
error={errors.email}
disabled={!isEditing || isLoading}
leftIcon={<Mail className="w-4 h-4" />}
/>
<Input
type="tel"
label="Teléfono"
value={profileData.phone}
onChange={handleInputChange('phone')}
error={errors.phone}
disabled={!isEditing || isLoading}
placeholder="+34 600 000 000"
leftIcon={<Phone className="w-4 h-4" />}
/>
<Select
label="Idioma"
options={languageOptions}
value={profileData.language}
onChange={handleSelectChange('language')}
disabled={!isEditing || isLoading}
leftIcon={<Globe className="w-4 h-4" />}
/>
<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>
2025-08-28 23:40:44 +02:00
</div>
2025-08-31 22:14:05 +02:00
)}
</Card>
2025-08-28 23:40:44 +02:00
2025-08-31 22:14:05 +02:00
{/* Password Change Form */}
{showPasswordForm && (
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Cambiar Contraseña</h2>
<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" />}
/>
2025-08-28 23:40:44 +02:00
2025-08-31 22:14:05 +02:00
<Input
type="password"
label="Nueva Contraseña"
value={passwordData.newPassword}
onChange={handlePasswordChange('newPassword')}
error={errors.newPassword}
disabled={isLoading}
leftIcon={<Lock className="w-4 h-4" />}
/>
2025-08-28 23:40:44 +02:00
2025-08-31 22:14:05 +02:00
<Input
type="password"
label="Confirmar Nueva Contraseña"
value={passwordData.confirmPassword}
onChange={handlePasswordChange('confirmPassword')}
error={errors.confirmPassword}
disabled={isLoading}
leftIcon={<Lock className="w-4 h-4" />}
/>
2025-08-28 23:40:44 +02:00
</div>
2025-08-31 22:14:05 +02:00
<div className="flex gap-3 pt-6 mt-6 border-t">
<Button
variant="outline"
onClick={() => {
setShowPasswordForm(false);
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
setErrors({});
}}
disabled={isLoading}
>
Cancelar
</Button>
<Button
variant="primary"
2025-09-11 18:21:32 +02:00
onClick={handleChangePasswordSubmit}
2025-08-31 22:14:05 +02:00
isLoading={isLoading}
loadingText="Cambiando..."
>
Cambiar Contraseña
</Button>
2025-08-28 23:40:44 +02:00
</div>
</Card>
)}
</div>
);
};
export default ProfilePage;