Files
frontend/src/pages/Settings.jsx

1153 lines
52 KiB
JavaScript

import React, { useState, useEffect, useLayoutEffect, useRef } from 'react';
import { getCurrentUser } from '../api/users.ts';
import { useNotification } from '../context/NotificationContext';
import { fetchWithAuth, patchWithAuth, postWithAuth, postFormDataWithAuth } from '../fetchWithAuth';
// Animación fade-in/slide-up para el componente
const fadeInSlideUp = `@keyframes fadein-slideup { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; transform: translateY(0); } }`;
if (typeof document !== 'undefined' && !document.getElementById('fadein-slideup-settings')) {
const style = document.createElement('style');
style.id = 'fadein-slideup-settings';
style.innerHTML = fadeInSlideUp;
document.head.appendChild(style);
}
const Settings = () => {
const [activeTab, setActiveTab] = useState('profile');
const [currentUser, setCurrentUser] = useState(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const { showMessage } = useNotification();
// Estados para el formulario de perfil
const [formData, setFormData] = useState({
first_name: '',
last_name: '',
email: '',
username: '',
rfc: ''
});
// Estados para cambio de contraseña
const [passwordData, setPasswordData] = useState({
current_password: '',
new_password: '',
confirm_password: ''
});
const [passwordErrors, setPasswordErrors] = useState({});
const [changingPassword, setChangingPassword] = useState(false);
// Estado para controlar la animación de entrada
const [showAnimation, setShowAnimation] = useState(false);
const [hasAnimated, setHasAnimated] = useState(false);
// Ref para el input de archivo
const fileInputRef = useRef(null);
useLayoutEffect(() => {
// Forzar un render antes de activar la animación
setShowAnimation(true);
}, []);
useEffect(() => {
if (showAnimation && !hasAnimated) {
const timeout = setTimeout(() => {
setHasAnimated(true);
setShowAnimation(false);
}, 700); // Duración igual a la animación
return () => clearTimeout(timeout);
}
}, [showAnimation, hasAnimated]);
// Cargar información del usuario al montar el componente
useEffect(() => {
const loadUserData = async () => {
try {
const token = localStorage.getItem('access');
if (token) {
const userData = await getCurrentUser(token);
setCurrentUser(userData);
// Inicializar formData con los datos del usuario
setFormData({
first_name: userData.first_name || '',
last_name: userData.last_name || '',
email: userData.email || '',
username: userData.username || '',
rfc: userData.rfc || ''
});
}
} catch (error) {
console.error('Error al cargar datos del usuario:', error);
showMessage('Error al cargar los datos del usuario', 'error');
} finally {
setLoading(false);
}
};
loadUserData();
}, [showMessage]);
// Función para actualizar el usuario
const updateUser = async (userData) => {
const API_URL = import.meta.env.VITE_EFC_API_URL;
try {
const response = await patchWithAuth(`${API_URL}/user/users/${currentUser.id}/`, userData);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Error al actualizar el usuario');
}
return await response.json();
} catch (error) {
console.error('Error updating user:', error);
throw error;
}
};
// Manejar cambios en el formulario
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
// Manejar cambios en el formulario de contraseña
const handlePasswordChange = (e) => {
const { name, value } = e.target;
setPasswordData(prev => ({
...prev,
[name]: value
}));
// Limpiar errores cuando el usuario empiece a escribir
if (passwordErrors[name]) {
setPasswordErrors(prev => ({
...prev,
[name]: ''
}));
}
};
// Validar contraseña
const validatePassword = (password) => {
const errors = [];
if (password.length < 8) {
errors.push('Debe tener al menos 8 caracteres');
}
if (!/(?=.*[a-z])/.test(password)) {
errors.push('Debe contener al menos una letra minúscula');
}
if (!/(?=.*[A-Z])/.test(password)) {
errors.push('Debe contener al menos una letra mayúscula');
}
if (!/(?=.*\d)/.test(password)) {
errors.push('Debe contener al menos un número');
}
return errors;
};
// Cambiar contraseña
const handleChangePassword = async () => {
const errors = {};
// Validaciones
if (!passwordData.current_password.trim()) {
errors.current_password = 'La contraseña actual es requerida';
}
if (!passwordData.new_password.trim()) {
errors.new_password = 'La nueva contraseña es requerida';
} else {
const passwordValidationErrors = validatePassword(passwordData.new_password);
if (passwordValidationErrors.length > 0) {
errors.new_password = passwordValidationErrors[0]; // Mostrar el primer error
}
}
if (!passwordData.confirm_password.trim()) {
errors.confirm_password = 'Confirma tu nueva contraseña';
} else if (passwordData.new_password !== passwordData.confirm_password) {
errors.confirm_password = 'Las contraseñas no coinciden';
}
if (Object.keys(errors).length > 0) {
setPasswordErrors(errors);
return;
}
setChangingPassword(true);
try {
const API_URL = import.meta.env.VITE_EFC_API_URL;
const response = await postWithAuth(`${API_URL}/user/users/${currentUser.id}/change_password/`, {
old_password: passwordData.current_password,
new_password: passwordData.new_password
});
if (!response.ok) {
const errorData = await response.json();
if (errorData.old_password) {
setPasswordErrors({ current_password: 'La contraseña actual es incorrecta' });
} else {
throw new Error(errorData.detail || 'Error al cambiar la contraseña');
}
return;
}
// Limpiar formulario y mostrar mensaje de éxito
setPasswordData({
current_password: '',
new_password: '',
confirm_password: ''
});
setPasswordErrors({});
showMessage('Contraseña cambiada exitosamente', 'success');
} catch (error) {
console.error('Error changing password:', error);
showMessage(error.message || 'Error al cambiar la contraseña', 'error');
} finally {
setChangingPassword(false);
}
};
// Guardar cambios del perfil
const handleSaveProfile = async () => {
if (!currentUser) return;
// Validaciones básicas
if (!formData.first_name.trim()) {
showMessage('El nombre es requerido', 'error');
return;
}
if (!formData.last_name.trim()) {
showMessage('El apellido es requerido', 'error');
return;
}
if (!formData.email.trim()) {
showMessage('El email es requerido', 'error');
return;
}
// Username validation removed - field is now read-only
// Validar formato de email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
showMessage('Por favor ingresa un email válido', 'error');
return;
}
setSaving(true);
try {
// Preparar datos para enviar - NO incluir grupos ni RFC para preservarlos
const updateData = {
first_name: formData.first_name.trim(),
last_name: formData.last_name.trim(),
email: formData.email.trim(),
// Username NO se incluye - no se puede modificar
// RFC NO se incluye - no se puede modificar
is_importador: currentUser.is_importador, // Preservar estado actual
is_active: currentUser.is_active // Preservar estado actual
};
const updatedUser = await updateUser(updateData);
if (updatedUser) {
setCurrentUser(updatedUser);
showMessage('Perfil actualizado exitosamente', 'success');
}
} catch (error) {
showMessage(error.message || 'Error al actualizar el perfil', 'error');
} finally {
setSaving(false);
}
};
// Manejar cambio de foto
const handlePhotoChange = async (event) => {
const file = event.target.files[0];
if (!file) return;
// Validar tipo de archivo
if (!file.type.startsWith('image/')) {
showMessage('Por favor selecciona un archivo de imagen válido', 'error');
return;
}
// Validar tamaño (5MB máximo)
if (file.size > 5 * 1024 * 1024) {
showMessage('La imagen debe ser menor a 5MB', 'error');
return;
}
setSaving(true);
try {
const token = localStorage.getItem('access');
const API_URL = import.meta.env.VITE_EFC_API_URL;
const formDataPhoto = new FormData();
formDataPhoto.append('profile_picture', file);
const response = await fetch(`${API_URL}/user/users/${currentUser.id}/`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
},
body: formDataPhoto,
});
if (response.status === 401) {
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
localStorage.removeItem('access');
localStorage.removeItem('refresh');
setTimeout(() => {
window.location.href = '/login';
}, 2000);
return;
}
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Error al actualizar la foto');
}
const updatedUser = await response.json();
setCurrentUser(updatedUser);
showMessage('Foto de perfil actualizada exitosamente', 'success');
} catch (error) {
console.error('Error updating photo:', error);
showMessage(error.message || 'Error al actualizar la foto', 'error');
} finally {
setSaving(false);
}
};
// Función para abrir selector de archivos
const handlePhotoClick = () => {
fileInputRef.current?.click();
};
// Verificar si hay cambios en el formulario
const hasChanges = () => {
if (!currentUser) return false;
return (
formData.first_name !== (currentUser.first_name || '') ||
formData.last_name !== (currentUser.last_name || '') ||
formData.email !== (currentUser.email || '')
// RFC excluded from change detection - field is read-only
);
};
// Solo mostrar tabs permitidas si es importador
const isImportador = typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true';
const tabs = [
{ id: 'profile', name: 'Perfil', icon: 'user' },
{ id: 'organization', name: 'Organización', icon: 'building' },
{ id: 'security', name: 'Seguridad', icon: 'shield' },
{ id: 'notifications', name: 'Notificaciones', icon: 'bell' }
].filter(tab =>
isImportador
? tab.id === 'profile' || tab.id === 'security' || tab.id === 'notifications'
: true
);
const getTabIcon = (iconType) => {
const icons = {
user: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
),
building: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
),
shield: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
),
bell: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-5-5v5zM4 4h5l-5 5v-5z" />
</svg>
)
};
return icons[iconType];
};
const renderProfileTab = () => (
<div className="space-y-8">
<div>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-gray-900">Información Personal</h3>
<div className="bg-blue-50 border border-blue-200 rounded-lg px-3 py-1">
<span className="text-blue-700 text-sm font-medium">Perfil</span>
</div>
</div>
{loading ? (
<div className="animate-pulse space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center space-y-4 sm:space-y-0 sm:space-x-6">
<div className="w-24 h-24 bg-gray-200 rounded-full mx-auto sm:mx-0"></div>
<div className="space-y-3 text-center sm:text-left">
<div className="h-5 bg-gray-200 rounded w-40 mx-auto sm:mx-0"></div>
<div className="h-4 bg-gray-200 rounded w-32 mx-auto sm:mx-0"></div>
<div className="h-4 bg-gray-200 rounded w-24 mx-auto sm:mx-0"></div>
</div>
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div className="h-12 bg-gray-200 rounded"></div>
<div className="h-12 bg-gray-200 rounded"></div>
<div className="h-12 bg-gray-200 rounded"></div>
<div className="h-12 bg-gray-200 rounded"></div>
</div>
</div>
) : (
<>
{/* Avatar y información básica mejorada */}
<div className="bg-gradient-to-r from-blue-50 to-blue-100 rounded-xl p-6 mb-8">
<div className="flex flex-col sm:flex-row sm:items-center space-y-4 sm:space-y-0 sm:space-x-6">
<div className="flex-shrink-0 mx-auto sm:mx-0">
{currentUser?.profile_picture ? (
<img
className="w-24 h-24 rounded-full object-cover border-4 border-white shadow-lg"
src={currentUser.profile_picture}
alt="Avatar del usuario"
/>
) : (
<div className="w-24 h-24 bg-gradient-to-br from-blue-400 to-blue-600 rounded-full flex items-center justify-center border-4 border-white shadow-lg">
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
)}
</div>
<div className="text-center sm:text-left">
<h4 className="text-xl font-bold text-gray-900">
{currentUser ? `${currentUser.first_name} ${currentUser.last_name}` : 'Usuario'}
</h4>
<p className="text-blue-600 font-medium">
@{currentUser?.username || 'usuario'}
</p>
<p className="text-gray-600 text-sm">
ID: {currentUser?.id || 'Sin ID'}
</p>
<button
onClick={handlePhotoClick}
disabled={saving}
className="mt-3 inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-medium text-white text-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Actualizando...
</>
) : (
<>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Cambiar foto
</>
)}
</button>
{/* Input oculto para seleccionar archivo */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handlePhotoChange}
className="hidden"
/>
</div>
</div>
</div>
{/* Formulario de información mejorado */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div className="space-y-2">
<label className="block text-sm font-semibold text-gray-700">
Nombre
</label>
<input
type="text"
name="first_name"
value={formData.first_name}
onChange={handleInputChange}
className="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-all duration-200 hover:border-gray-400"
placeholder="Ingresa tu nombre"
/>
</div>
<div className="space-y-2">
<label className="block text-sm font-semibold text-gray-700">
Apellido
</label>
<input
type="text"
name="last_name"
value={formData.last_name}
onChange={handleInputChange}
className="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-all duration-200 hover:border-gray-400"
placeholder="Ingresa tu apellido"
/>
</div>
<div className="space-y-2">
<label className="block text-sm font-semibold text-gray-700">
Email
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-all duration-200 hover:border-gray-400"
placeholder="correo@ejemplo.com"
/>
</div>
<div className="space-y-2">
<label className="block text-sm font-semibold text-gray-700">
Username
</label>
<input
type="text"
name="username"
value={formData.username}
disabled
className="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm bg-gray-50 text-gray-500 cursor-not-allowed text-sm"
placeholder="nombre_usuario"
/>
<p className="text-xs text-gray-500">
El username no puede ser modificado por razones de seguridad
</p>
</div>
{currentUser?.rfc !== undefined && (
<div className="space-y-2">
<label className="block text-sm font-semibold text-gray-700">
RFC
</label>
<input
type="text"
name="rfc"
value={formData.rfc}
disabled
className="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm bg-gray-50 text-gray-500 cursor-not-allowed text-sm"
placeholder="XXXX000000XXX"
/>
<p className="text-xs text-gray-500">
El RFC no puede ser modificado por razones de seguridad
</p>
</div>
)}
<div className="space-y-2">
<label className="block text-sm font-semibold text-gray-700">
Organización ID
</label>
<input
type="text"
value={currentUser?.organizacion || ''}
disabled
className="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm bg-gray-50 text-gray-500 cursor-not-allowed text-sm"
placeholder="ID de organización"
/>
</div>
</div>
<div className="flex flex-col sm:flex-row sm:justify-end space-y-3 sm:space-y-0 sm:space-x-3 pt-8 border-t border-gray-200">
{hasChanges() && (
<div className="flex items-center text-sm text-amber-600 sm:mr-auto">
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
Tienes cambios sin guardar
</div>
)}
<button
type="button"
onClick={() => {
// Resetear formulario a los valores originales (username y RFC no se resetean porque no se pueden cambiar)
setFormData({
first_name: currentUser?.first_name || '',
last_name: currentUser?.last_name || '',
email: currentUser?.email || '',
username: currentUser?.username || '', // Mantener valor original
rfc: currentUser?.rfc || '' // Mantener valor original
});
}}
disabled={saving}
className="w-full sm:w-auto px-6 py-3 border border-gray-300 rounded-lg shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancelar
</button>
<button
type="button"
onClick={handleSaveProfile}
disabled={saving || !hasChanges()}
className="w-full sm:w-auto px-6 py-3 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Guardando...
</>
) : (
'Guardar cambios'
)}
</button>
</div>
</>
)}
</div>
</div>
);
const renderOrganizationTab = () => (
<div className="space-y-8">
<div>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-gray-900">Configuración de Organización</h3>
<div className="bg-blue-50 border border-blue-200 rounded-lg px-3 py-1">
<span className="text-blue-700 text-sm font-medium">Organización</span>
</div>
</div>
<p className="text-gray-600 mb-8">
Gestiona la configuración relacionada con tu organización y equipos de trabajo.
</p>
<div className="bg-gradient-to-r from-yellow-50 to-orange-50 border border-yellow-200 rounded-xl p-6">
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="w-12 h-12 bg-yellow-100 rounded-lg flex items-center justify-center">
<svg className="h-6 w-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
</div>
<div className="ml-4">
<h3 className="text-lg font-semibold text-yellow-800">Funcionalidad en desarrollo</h3>
<div className="mt-3 text-sm text-yellow-700">
<p className="mb-2">
Las configuraciones de organización estarán disponibles próximamente. Esta sección incluirá:
</p>
<ul className="list-disc list-inside space-y-1 ml-4">
<li>Gestión de equipos y departamentos</li>
<li>Configuración de permisos y roles</li>
<li>Preferencias de la organización</li>
<li>Integración con sistemas externos</li>
<li>Configuración de flujos de trabajo</li>
</ul>
</div>
<div className="mt-4">
<button className="inline-flex items-center px-4 py-2 bg-yellow-100 border border-yellow-300 rounded-lg text-sm font-medium text-yellow-800 hover:bg-yellow-200 transition-colors duration-200">
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-5 5v-5zM4 4h5l-5 5v-5z" />
</svg>
Solicitar acceso anticipado
</button>
</div>
</div>
</div>
</div>
{/* Información actual de la organización */}
{currentUser?.organizacion && (
<div className="bg-white border border-gray-200 rounded-xl p-6">
<h4 className="text-lg font-semibold text-gray-900 mb-4">Información actual</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<div>
<p className="text-sm font-medium text-gray-900">ID de organización</p>
<p className="text-sm text-gray-500">{currentUser.organizacion}</p>
</div>
</div>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div>
<p className="text-sm font-medium text-gray-900">Tu rol</p>
<p className="text-sm text-gray-500">
{typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true'
? 'Importador'
: 'Administrador'}
</p>
</div>
</div>
</div>
</div>
)}
</div>
</div>
);
const renderSecurityTab = () => (
<div className="space-y-8">
<div>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-gray-900">Seguridad de la cuenta</h3>
<div className="bg-blue-50 border border-blue-200 rounded-lg px-3 py-1">
<span className="text-blue-700 text-sm font-medium">Seguridad</span>
</div>
</div>
<div className="space-y-8">
{/* Cambiar contraseña */}
<div className="bg-gradient-to-r from-blue-50 to-blue-100 rounded-xl p-6">
<div className="flex items-center mb-4">
<div className="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center mr-3">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<h4 className="text-lg font-semibold text-gray-900">Cambiar contraseña</h4>
</div>
<p className="text-gray-600 text-sm mb-6">
Mantén tu cuenta segura con una contraseña fuerte y única.
</p>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div className="lg:col-span-2">
<label className="block text-sm font-semibold text-gray-700 mb-2">
Contraseña actual
</label>
<input
type="password"
name="current_password"
value={passwordData.current_password}
onChange={handlePasswordChange}
className={`w-full px-4 py-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2 text-sm transition-all duration-200 hover:border-gray-400 ${
passwordErrors.current_password
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
}`}
placeholder="Ingresa tu contraseña actual"
/>
{passwordErrors.current_password && (
<p className="mt-1 text-sm text-red-600">{passwordErrors.current_password}</p>
)}
</div>
<div className="space-y-2">
<label className="block text-sm font-semibold text-gray-700">
Nueva contraseña
</label>
<input
type="password"
name="new_password"
value={passwordData.new_password}
onChange={handlePasswordChange}
className={`w-full px-4 py-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2 text-sm transition-all duration-200 hover:border-gray-400 ${
passwordErrors.new_password
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
}`}
placeholder="Ingresa una nueva contraseña"
/>
{passwordErrors.new_password && (
<p className="mt-1 text-sm text-red-600">{passwordErrors.new_password}</p>
)}
</div>
<div className="space-y-2">
<label className="block text-sm font-semibold text-gray-700">
Confirmar nueva contraseña
</label>
<input
type="password"
name="confirm_password"
value={passwordData.confirm_password}
onChange={handlePasswordChange}
className={`w-full px-4 py-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2 text-sm transition-all duration-200 hover:border-gray-400 ${
passwordErrors.confirm_password
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
}`}
placeholder="Confirma tu nueva contraseña"
/>
{passwordErrors.confirm_password && (
<p className="mt-1 text-sm text-red-600">{passwordErrors.confirm_password}</p>
)}
</div>
</div>
<div className="mt-6">
<button
type="button"
onClick={handleChangePassword}
disabled={changingPassword}
className="inline-flex items-center px-6 py-3 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{changingPassword ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Cambiando contraseña...
</>
) : (
<>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Actualizar contraseña
</>
)}
</button>
</div>
</div>
{/* Información de seguridad adicional */}
<div className="bg-gray-50 rounded-xl p-6">
<h4 className="text-lg font-semibold text-gray-900 mb-4">Requisitos de contraseña</h4>
<div className="space-y-3">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-green-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<p className="ml-3 text-sm text-gray-600">
Mínimo 8 caracteres de longitud
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-green-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<p className="ml-3 text-sm text-gray-600">
Al menos una letra minúscula (a-z)
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-green-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<p className="ml-3 text-sm text-gray-600">
Al menos una letra mayúscula (A-Z)
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-green-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<p className="ml-3 text-sm text-gray-600">
Al menos un número (0-9)
</p>
</div>
</div>
<div className="mt-6 pt-4 border-t border-gray-200">
<h5 className="text-sm font-semibold text-gray-900 mb-2">Consejos adicionales</h5>
<div className="space-y-2">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-4 h-4 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="ml-2 text-xs text-gray-600">
Evita usar información personal como nombres o fechas de nacimiento
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-4 h-4 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="ml-2 text-xs text-gray-600">
No reutilices contraseñas de otras cuentas
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-4 h-4 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="ml-2 text-xs text-gray-600">
Considera usar símbolos especiales para mayor seguridad
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
const renderNotificationsTab = () => (
<div className="space-y-8">
<div>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-gray-900">Preferencias de notificaciones</h3>
<div className="bg-blue-50 border border-blue-200 rounded-lg px-3 py-1">
<span className="text-blue-700 text-sm font-medium">Notificaciones</span>
</div>
</div>
<div className="space-y-6">
{/* Notificaciones por email */}
<div className="bg-white border border-gray-200 rounded-xl p-6 hover:shadow-md transition-shadow duration-200">
<div className="flex items-center justify-between">
<div className="flex items-start space-x-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div>
<label className="text-sm font-semibold text-gray-900">
Notificaciones por email
</label>
<p className="text-sm text-gray-500 mt-1">
Recibir notificaciones importantes por correo electrónico
</p>
</div>
</div>
<button
type="button"
className="bg-blue-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<span className="translate-x-5 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"></span>
</button>
</div>
</div>
{/* Notificaciones de documentos */}
<div className="bg-white border border-gray-200 rounded-xl p-6 hover:shadow-md transition-shadow duration-200">
<div className="flex items-center justify-between">
<div className="flex items-start space-x-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div>
<label className="text-sm font-semibold text-gray-900">
Notificaciones de documentos
</label>
<p className="text-sm text-gray-500 mt-1">
Notificar cuando se suban o actualicen documentos
</p>
</div>
</div>
<button
type="button"
className="bg-blue-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<span className="translate-x-5 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"></span>
</button>
</div>
</div>
{/* Notificaciones del sistema */}
<div className="bg-white border border-gray-200 rounded-xl p-6 hover:shadow-md transition-shadow duration-200">
<div className="flex items-center justify-between">
<div className="flex items-start space-x-3">
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="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" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div>
<label className="text-sm font-semibold text-gray-900">
Notificaciones del sistema
</label>
<p className="text-sm text-gray-500 mt-1">
Recibir actualizaciones del sistema y mantenimiento
</p>
</div>
</div>
<button
type="button"
className="bg-gray-200 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<span className="translate-x-0 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"></span>
</button>
</div>
</div>
{/* Información adicional */}
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="ml-3">
<h4 className="text-sm font-semibold text-blue-900">Información importante</h4>
<div className="mt-2 text-sm text-blue-700">
<p>
Las notificaciones críticas de seguridad y las alertas del sistema siempre se enviarán,
independientemente de tus preferencias de notificación.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
const renderTabContent = () => {
switch (activeTab) {
case 'profile':
return renderProfileTab();
case 'organization':
return renderOrganizationTab();
case 'security':
return renderSecurityTab();
case 'notifications':
return renderNotificationsTab();
default:
return renderProfileTab();
}
};
return (
<div className={`min-h-screen bg-gray-50 ${showAnimation ? 'animate-[fadein-slideup_0.7s_ease-out]' : ''}`}>
{/* Header mejorado con gradiente azul */}
<div className="bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 text-white shadow-lg">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div className="flex-1 min-w-0">
<h1 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
Configuración
</h1>
<p className="mt-2 text-blue-100 text-sm sm:text-base">
Gestiona tu perfil, configuración de cuenta y preferencias del sistema
</p>
</div>
<div className="mt-4 sm:mt-0 flex-shrink-0">
<div className="flex items-center space-x-2">
<div className="bg-blue-500/20 backdrop-blur-sm border border-blue-400/30 rounded-lg px-3 py-1">
<span className="text-blue-100 text-sm font-medium">
{currentUser ? `${currentUser.first_name} ${currentUser.last_name}` : 'Usuario'}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Navegación responsive para móviles */}
<div className="lg:hidden mb-6">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="bg-gradient-to-r from-blue-50 to-blue-100 px-4 py-3 border-b border-gray-200">
<h3 className="text-sm font-semibold text-blue-900">Configuración</h3>
</div>
<div className="grid grid-cols-2 gap-1 p-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`${
activeTab === tab.id
? 'bg-blue-50 border-blue-500 text-blue-700 shadow-sm'
: 'border-transparent text-gray-700 hover:bg-gray-50 hover:text-gray-900'
} border-2 px-3 py-3 flex flex-col items-center text-xs font-medium rounded-lg transition-all duration-200`}
>
<span
className={`${
activeTab === tab.id
? 'text-blue-500'
: 'text-gray-400'
} w-5 h-5 mb-1 transition-colors duration-200`}
>
{getTabIcon(tab.icon)}
</span>
<span className="text-center">{tab.name}</span>
</button>
))}
</div>
</div>
</div>
<div className="lg:grid lg:grid-cols-12 lg:gap-8">
{/* Sidebar de navegación mejorado - solo visible en desktop */}
<aside className="hidden lg:block lg:col-span-3">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden sticky top-8">
<div className="bg-gradient-to-r from-blue-50 to-blue-100 px-4 py-3 border-b border-gray-200">
<h3 className="text-sm font-semibold text-blue-900">Navegación</h3>
</div>
<nav className="p-2 space-y-1">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`${
activeTab === tab.id
? 'bg-blue-50 border-blue-500 text-blue-700 shadow-sm'
: 'border-transparent text-gray-700 hover:bg-gray-50 hover:text-gray-900'
} group border-l-4 px-3 py-3 flex items-center text-sm font-medium w-full text-left rounded-r-md transition-all duration-200`}
>
<span
className={`${
activeTab === tab.id
? 'text-blue-500'
: 'text-gray-400 group-hover:text-gray-500'
} flex-shrink-0 -ml-1 mr-3 h-5 w-5 transition-colors duration-200`}
>
{getTabIcon(tab.icon)}
</span>
<span className="truncate">{tab.name}</span>
</button>
))}
</nav>
</div>
</aside>
{/* Contenido principal mejorado */}
<div className="lg:col-span-9">
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="px-4 sm:px-6 py-6">
{renderTabContent()}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Settings;