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;
|