1153 lines
52 KiB
JavaScript
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;
|