Primera version de frontend
This commit is contained in:
17
src/components/Layout.jsx
Normal file
17
src/components/Layout.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import Sidebar from './Sidebar';
|
||||
import NotificationBell from './NotificationBell';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<div className="flex h-screen bg-light-gray overflow-hidden">
|
||||
<Sidebar />
|
||||
<main className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<NotificationBell />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
src/components/LogoutButton.jsx
Normal file
28
src/components/LogoutButton.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function LogoutButton() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
|
||||
// Disparar evento para actualizar el navbar
|
||||
window.dispatchEvent(new CustomEvent('authStateChanged'));
|
||||
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition 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="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
Cerrar sesión
|
||||
</button>
|
||||
);
|
||||
}
|
||||
186
src/components/Navbar.jsx
Normal file
186
src/components/Navbar.jsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { colors } from '../theme';
|
||||
|
||||
export default function Navbar() {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = () => {
|
||||
const token = localStorage.getItem('access');
|
||||
setIsLoggedIn(!!token);
|
||||
};
|
||||
|
||||
checkAuthStatus();
|
||||
window.addEventListener('authStateChanged', checkAuthStatus);
|
||||
return () => window.removeEventListener('authStateChanged', checkAuthStatus);
|
||||
}, []);
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
window.dispatchEvent(new CustomEvent('authStateChanged'));
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
const navLinks = [
|
||||
{ path: '/', name: 'Inicio', public: true },
|
||||
{ path: '/admin', name: 'Dashboard', public: false },
|
||||
{ path: '/documents', name: 'Documentos', public: false },
|
||||
{ path: '/mi-organizacion', name: 'Organización', public: false },
|
||||
{ path: '/usuarios', name: 'Usuarios', public: false },
|
||||
{ path: '/reportes', name: 'Reportes', public: false },
|
||||
];
|
||||
|
||||
const isActiveLink = (path) => {
|
||||
return location.pathname === path;
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="bg-white shadow-xl border-b border-gray-200 sticky top-0 z-50 backdrop-blur-lg bg-white/95">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Logo */}
|
||||
<div className="flex-shrink-0">
|
||||
<Link to="/" className="flex items-center group">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-navy-500 to-navy-600 rounded-xl flex items-center justify-center shadow-lg mr-3 transition-all duration-200 group-hover:shadow-xl group-hover:scale-105">
|
||||
<span className="text-lg font-bold text-white">E</span>
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<span className="text-2xl font-bold bg-gradient-to-r from-navy-900 to-navy-700 bg-clip-text text-transparent">EFC</span>
|
||||
<span className="ml-2 text-sm text-text-secondary">
|
||||
Export & Finance Control
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:block">
|
||||
<div className="ml-10 flex items-baseline space-x-2">
|
||||
{navLinks
|
||||
.filter(link => link.public || isLoggedIn)
|
||||
.map((link) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
isActiveLink(link.path)
|
||||
? 'bg-gradient-to-r from-navy-600 to-navy-700 text-white shadow-lg transform scale-105'
|
||||
: 'text-text-primary hover:bg-gradient-to-r hover:from-light-gray-100 hover:to-light-gray-200 hover:text-navy-700 hover:shadow-md hover:transform hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auth buttons */}
|
||||
<div className="hidden md:block">
|
||||
<div className="ml-4 flex items-center md:ml-6">
|
||||
{isLoggedIn ? (
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-text-secondary text-sm font-medium">
|
||||
Bienvenido
|
||||
</span>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="bg-gradient-to-r from-danger-600 to-danger-700 hover:from-danger-700 hover:to-danger-800 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 flex items-center space-x-2 shadow-lg hover:shadow-xl transform hover:scale-105"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
<span>Salir</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
to="/login"
|
||||
className="bg-gradient-to-r from-navy-600 to-navy-700 hover:from-navy-700 hover:to-navy-800 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 flex items-center space-x-2 shadow-lg hover:shadow-xl transform hover:scale-105"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
<span>Ingresar</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className="inline-flex items-center justify-center p-2 rounded-md text-text-primary hover:text-navy hover:bg-light-gray focus:outline-none focus:ring-2 focus:ring-inset focus:ring-navy"
|
||||
>
|
||||
<svg
|
||||
className={`${isMenuOpen ? 'hidden' : 'block'} h-6 w-6`}
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg
|
||||
className={`${isMenuOpen ? 'block' : 'hidden'} h-6 w-6`}
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
<div className={`md:hidden ${isMenuOpen ? 'block' : 'hidden'}`}>
|
||||
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-light-gray border-t border-gray-200">
|
||||
{navLinks
|
||||
.filter(link => link.public || isLoggedIn)
|
||||
.map((link) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors duration-200 ${
|
||||
isActiveLink(link.path)
|
||||
? 'bg-navy text-white'
|
||||
: 'text-text-primary hover:bg-white hover:text-navy'
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* Mobile auth section */}
|
||||
<div className="border-t border-gray-300 pt-4 pb-3">
|
||||
{isLoggedIn ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
logout();
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
className="block w-full text-left px-3 py-2 rounded-md text-base font-medium text-error hover:bg-white transition-colors duration-200"
|
||||
>
|
||||
Salir
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
to="/login"
|
||||
className="block px-3 py-2 rounded-md text-base font-medium text-navy hover:bg-white transition-colors duration-200"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Ingresar
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
222
src/components/NotificationBell.jsx
Normal file
222
src/components/NotificationBell.jsx
Normal file
@@ -0,0 +1,222 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { fetchNotificaciones, marcarNotificacionComoVista } from '../api/notificaciones';
|
||||
|
||||
export default function NotificationBell() {
|
||||
const navigate = useNavigate();
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [showPanel, setShowPanel] = useState(false);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
fetchNotificaciones({ page: 1, pageSize: 50 })
|
||||
.then(data => {
|
||||
setNotifications(data.results);
|
||||
setUnreadCount(data.results.filter(n => !n.visto).length);
|
||||
})
|
||||
.catch(err => {
|
||||
setError('Error al cargar notificaciones');
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
// Opcional: aquí podrías hacer una petición PATCH/POST si es necesario
|
||||
const markAsRead = async (notificationId) => {
|
||||
try {
|
||||
await marcarNotificacionComoVista(notificationId);
|
||||
setNotifications(prev =>
|
||||
prev.map(notification =>
|
||||
notification.id === notificationId
|
||||
? { ...notification, visto: true }
|
||||
: notification
|
||||
)
|
||||
);
|
||||
setUnreadCount(prev => Math.max(0, prev - 1));
|
||||
} catch (e) {
|
||||
// Opcional: mostrar error
|
||||
}
|
||||
};
|
||||
|
||||
const markAllAsRead = async () => {
|
||||
const notVistas = notifications.filter(n => !n.visto);
|
||||
try {
|
||||
await Promise.all(notVistas.map(n => marcarNotificacionComoVista(n.id)));
|
||||
setNotifications(prev => prev.map(notification => ({ ...notification, visto: true })));
|
||||
setUnreadCount(0);
|
||||
} catch (e) {
|
||||
// Opcional: mostrar error
|
||||
}
|
||||
};
|
||||
|
||||
const getNotificationIcon = (tipo) => {
|
||||
switch (tipo) {
|
||||
case 'success':
|
||||
return (
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full 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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
case 'warning':
|
||||
return (
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-4 h-4 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>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-blue-100 rounded-full 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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTimestamp = (isoString) => {
|
||||
const timestamp = new Date(isoString);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - timestamp.getTime();
|
||||
const minutes = Math.floor(diff / (1000 * 60));
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
if (minutes < 1) return 'Ahora';
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
if (hours < 24) return `${hours}h`;
|
||||
return `${days}d`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Campanita flotante */}
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
<button
|
||||
onClick={() => setShowPanel(!showPanel)}
|
||||
className="relative bg-indigo-600 hover:bg-indigo-700 text-white p-3 rounded-full shadow-lg transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<svg className="w-6 h-6" 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>
|
||||
|
||||
{/* Contador de notificaciones no leídas */}
|
||||
{unreadCount > 0 && (
|
||||
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium">
|
||||
{unreadCount > 9 ? '9+' : unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Panel de notificaciones */}
|
||||
{showPanel && (
|
||||
<div className="fixed bottom-20 right-6 z-50 w-80 bg-white rounded-lg shadow-xl border border-gray-200">
|
||||
{/* Header del panel */}
|
||||
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Notificaciones</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
{unreadCount > 0 && (
|
||||
<button
|
||||
onClick={markAllAsRead}
|
||||
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
|
||||
>
|
||||
Marcar todas como leídas
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowPanel(false)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lista de notificaciones */}
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="p-6 text-center text-blue-500">Cargando notificaciones...</div>
|
||||
) : error ? (
|
||||
<div className="p-6 text-center text-red-500">{error}</div>
|
||||
) : notifications.length === 0 ? (
|
||||
<div className="p-6 text-center">
|
||||
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-5 5v-5zM13 3l-4 9h6l-4 9" />
|
||||
</svg>
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay notificaciones</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Cuando tengas nuevas notificaciones aparecerán aquí.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`p-4 hover:bg-gray-50 cursor-pointer transition-colors ${
|
||||
!notification.visto ? 'bg-blue-50' : ''
|
||||
}`}
|
||||
onClick={() => markAsRead(notification.id)}
|
||||
>
|
||||
<div className="flex space-x-3">
|
||||
{getNotificationIcon(notification.tipo?.tipo)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className={`text-sm font-medium ${
|
||||
!notification.visto ? 'text-gray-900' : 'text-gray-700'
|
||||
}`}>
|
||||
{notification.tipo?.descripcion || notification.tipo?.tipo || 'Notificación'}
|
||||
</p>
|
||||
<span className="text-xs text-gray-500">
|
||||
{formatTimestamp(notification.fecha_envio || notification.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-1">{notification.mensaje}</p>
|
||||
{!notification.visto && (
|
||||
<div className="mt-2">
|
||||
<span className="inline-block w-2 h-2 bg-blue-600 rounded-full"></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer del panel */}
|
||||
{notifications.length > 0 && (
|
||||
<div className="p-3 border-t border-gray-200 bg-gray-50 rounded-b-lg">
|
||||
<button
|
||||
className="w-full text-center text-sm text-indigo-600 hover:text-indigo-800 font-medium"
|
||||
onClick={() => {
|
||||
setShowPanel(false);
|
||||
navigate('/notificaciones');
|
||||
}}
|
||||
>
|
||||
Ver todas las notificaciones
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overlay para cerrar el panel al hacer clic fuera */}
|
||||
{showPanel && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setShowPanel(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
22
src/components/RequireAuth.jsx
Normal file
22
src/components/RequireAuth.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
// Esta función verifica si el usuario está autenticado (por ejemplo, si hay un token en localStorage)
|
||||
function isAuthenticated() {
|
||||
const token = localStorage.getItem('access');
|
||||
console.log('🔐 Verificando autenticación, token:', token ? 'presente' : 'ausente');
|
||||
return !!token;
|
||||
}
|
||||
|
||||
export default function RequireAuth({ children }) {
|
||||
const authenticated = isAuthenticated();
|
||||
console.log('🛡️ RequireAuth - usuario autenticado:', authenticated);
|
||||
|
||||
if (!authenticated) {
|
||||
console.log('❌ No autenticado, redirigiendo a /login');
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
console.log('✅ Usuario autenticado, mostrando contenido');
|
||||
return children;
|
||||
}
|
||||
370
src/components/Sidebar.jsx
Normal file
370
src/components/Sidebar.jsx
Normal file
@@ -0,0 +1,370 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useUser } from '../context/UserContext';
|
||||
|
||||
export default function Sidebar() {
|
||||
// Leer si el usuario es importador desde localStorage
|
||||
const isImportador = typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true';
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { user: currentUser, loading } = useUser();
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
window.dispatchEvent(new CustomEvent('authStateChanged'));
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
// El usuario y loading ahora vienen del contexto global
|
||||
|
||||
// Definir todas las secciones
|
||||
const allMenuSections = [
|
||||
{
|
||||
title: 'Organización',
|
||||
items: [
|
||||
{
|
||||
name: 'Home',
|
||||
path: '/admin',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Mi Organización',
|
||||
path: '/organization',
|
||||
icon: (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Servicios',
|
||||
items: [
|
||||
{
|
||||
name: 'Procesos',
|
||||
path: '/procesos',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Documentación',
|
||||
items: [
|
||||
{
|
||||
name: 'Reportes',
|
||||
path: '/reports',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Expedientes',
|
||||
path: '/expedientes',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<rect x="3" y="7" width="18" height="13" rx="2" ry="2" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M16 3H8a2 2 0 00-2 2v2h12V5a2 2 0 00-2-2z" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Documentos',
|
||||
path: '/documents',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" 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>
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
// Nueva sección Tableros
|
||||
{
|
||||
title: 'Tableros',
|
||||
items: [
|
||||
{
|
||||
name: 'Uso de Almacenamiento',
|
||||
path: '/tablero/almacenamiento',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Acceso a Usuarios',
|
||||
items: [
|
||||
...(
|
||||
isImportador
|
||||
? []
|
||||
: [
|
||||
{
|
||||
name: 'Usuarios',
|
||||
path: '/users',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
),
|
||||
{
|
||||
name: 'Vucem',
|
||||
path: '/vucem',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 12h8M12 8v8" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Filtrar secciones según si es importador
|
||||
// Modificar items según si es importador
|
||||
const menuSections = allMenuSections
|
||||
.map(section => {
|
||||
if (section.title === 'Organización') {
|
||||
return {
|
||||
...section,
|
||||
items: section.items.filter(item => !(isImportador && item.name === 'Mi Organización'))
|
||||
};
|
||||
}
|
||||
// Para Acceso a Usuarios, no filtrar la sección, solo los items ya están condicionados arriba
|
||||
if (section.title === 'Tableros' && isImportador) {
|
||||
return null;
|
||||
}
|
||||
return section;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<div className={`bg-slate-900 text-white transition-all duration-300 ${isCollapsed ? 'w-16' : 'w-64'} h-screen flex flex-col shadow-xl`}>
|
||||
{/* Header - Logo y colapsar */}
|
||||
<div className="p-4 border-b border-slate-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
{!isCollapsed && (
|
||||
<div className="flex items-center">
|
||||
{/* Logo de la organización */}
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3 shadow-lg">
|
||||
<svg className="w-5 h-5 text-white" 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>
|
||||
<h1 className="text-lg font-bold text-white">EFC Dashboard</h1>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="p-1.5 rounded-lg hover:bg-slate-700 transition-all duration-200 hover:shadow-md"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{isCollapsed ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-3 space-y-4 overflow-y-auto overflow-x-hidden">
|
||||
{menuSections.map((section, sectionIndex) => (
|
||||
<div key={sectionIndex} className="space-y-1">
|
||||
{/* Título de la sección */}
|
||||
{!isCollapsed && (
|
||||
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider px-3 py-1">
|
||||
{section.title}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
{/* Items de la sección */}
|
||||
<div className="space-y-1">
|
||||
{section.items.map((item) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
const isDisabled = item.disabled;
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={isDisabled ? '#' : item.path}
|
||||
className={`flex items-center px-3 py-2.5 text-sm rounded-lg transition-all duration-200 group relative ${
|
||||
isDisabled
|
||||
? 'opacity-50 cursor-not-allowed pointer-events-none bg-slate-800 text-gray-500'
|
||||
: isActive
|
||||
? 'bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg transform scale-[1.02]'
|
||||
: 'text-gray-300 hover:bg-slate-700 hover:text-white hover:transform hover:scale-[1.01]'
|
||||
}`}
|
||||
title={isCollapsed ? item.name : ''}
|
||||
tabIndex={isDisabled ? -1 : 0}
|
||||
aria-disabled={isDisabled ? 'true' : 'false'}
|
||||
>
|
||||
{/* Indicador activo lateral */}
|
||||
{isActive && !isDisabled && (
|
||||
<div className="absolute left-0 top-0 bottom-0 w-1 bg-cyan-400 rounded-r-full"></div>
|
||||
)}
|
||||
<div className="flex-shrink-0">
|
||||
{item.icon}
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<span className="ml-3 font-medium">{item.name}</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Línea separadora entre secciones (excepto la última) */}
|
||||
{!isCollapsed && sectionIndex < menuSections.length - 1 && (
|
||||
<div className="border-b border-slate-600 mx-3 my-3"></div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Configuración */}
|
||||
<div className="p-3 border-t border-slate-700 flex-shrink-0">
|
||||
<Link
|
||||
to="/settings"
|
||||
className={`flex items-center px-3 py-2.5 text-sm rounded-lg transition-all duration-200 group ${
|
||||
location.pathname === '/settings'
|
||||
? 'bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg'
|
||||
: 'text-gray-300 hover:bg-slate-700 hover:text-white hover:shadow-md'
|
||||
}`}
|
||||
title={isCollapsed ? 'Configuración' : ''}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="w-5 h-5" 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>
|
||||
{!isCollapsed && (
|
||||
<span className="ml-3 font-medium">Configuración</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Footer - Perfil del Usuario */}
|
||||
<div className="p-3 border-t border-slate-700 flex-shrink-0">
|
||||
{!isCollapsed ? (
|
||||
<div className="space-y-3">
|
||||
{/* Información del usuario */}
|
||||
<div className="flex items-center space-x-3 p-2 bg-slate-800 rounded-lg">
|
||||
<div className="flex-shrink-0">
|
||||
{currentUser?.profile_picture ? (
|
||||
<img
|
||||
className="w-10 h-10 rounded-full ring-2 ring-blue-500 shadow-lg"
|
||||
src={currentUser.profile_picture}
|
||||
alt="Avatar del usuario"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-gray-500 to-gray-600 rounded-full ring-2 ring-blue-500 flex items-center justify-center shadow-lg">
|
||||
<svg className="w-5 h-5 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="flex-1 min-w-0">
|
||||
{loading ? (
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-slate-600 rounded w-24 mb-1"></div>
|
||||
<div className="h-3 bg-slate-700 rounded w-16"></div>
|
||||
</div>
|
||||
) : currentUser ? (
|
||||
<>
|
||||
<p className="text-sm font-medium text-white truncate">
|
||||
{currentUser.first_name} {currentUser.last_name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 truncate">
|
||||
{currentUser.username}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm font-medium text-white truncate">
|
||||
Usuario
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 truncate">
|
||||
Sin datos
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{/* Debug temporal */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<div className="text-xs text-orange-400 mt-1">
|
||||
Debug: {loading ? 'Cargando...' : currentUser ? 'Datos OK' : 'Sin datos'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botón de logout más pequeño */}
|
||||
<div className="pt-1">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full inline-flex items-center justify-center px-3 py-2.5 border border-transparent text-xs font-medium rounded-lg text-white bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 focus:ring-offset-slate-900 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-[1.02]"
|
||||
>
|
||||
<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="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
Cerrar sesión
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center space-y-3">
|
||||
{/* Avatar pequeño */}
|
||||
{currentUser?.profile_picture ? (
|
||||
<img
|
||||
className="w-10 h-10 rounded-full ring-2 ring-blue-500 shadow-lg hover:ring-blue-400 transition-all duration-200"
|
||||
src={currentUser.profile_picture}
|
||||
alt="Avatar del usuario"
|
||||
title={currentUser ? `${currentUser.first_name} ${currentUser.last_name} - ${currentUser.username}` : 'Usuario'}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="w-10 h-10 bg-gradient-to-br from-gray-500 to-gray-600 rounded-full ring-2 ring-blue-500 flex items-center justify-center shadow-lg hover:ring-blue-400 transition-all duration-200"
|
||||
title={currentUser ? `${currentUser.first_name} ${currentUser.last_name} - ${currentUser.username}` : 'Usuario'}
|
||||
>
|
||||
<svg className="w-5 h-5 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>
|
||||
)}
|
||||
{/* Botón de logout compacto */}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-2 text-gray-300 hover:text-white bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105"
|
||||
title="Cerrar sesión"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
211
src/components/Sidebar.jsx.bak
Normal file
211
src/components/Sidebar.jsx.bak
Normal file
@@ -0,0 +1,211 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function Sidebar() {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
|
||||
// Disparar evento para actualizar el navbar
|
||||
window.dispatchEvent(new CustomEvent('authStateChanged'));
|
||||
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
export default function Sidebar() {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
name: 'Mi Organización',
|
||||
path: '/organization',
|
||||
icon: (
|
||||
<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>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Importadores',
|
||||
path: '/importers',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Usuarios',
|
||||
path: '/users',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Reportes',
|
||||
path: '/reports',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Documentos',
|
||||
path: '/documents',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" 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>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={`bg-gray-900 text-white transition-all duration-300 ${isCollapsed ? 'w-16' : 'w-64'} min-h-screen flex flex-col`}>
|
||||
{/* Header - Logo y colapsar */}
|
||||
<div className="p-4 border-b border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
{!isCollapsed && (
|
||||
<div className="flex items-center">
|
||||
{/* Logo de la organización */}
|
||||
<div className="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg className="w-5 h-5 text-white" 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>
|
||||
<h1 className="text-lg font-bold text-white">EFC Dashboard</h1>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="p-1.5 rounded-lg hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{isCollapsed ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-3 space-y-1">
|
||||
{menuItems.map((item) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={`flex items-center px-3 py-2.5 text-sm rounded-lg transition-colors group ${
|
||||
isActive
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
|
||||
}`}
|
||||
title={isCollapsed ? item.name : ''}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
{item.icon}
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<span className="ml-3 font-medium">{item.name}</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Configuración */}
|
||||
<div className="p-3 border-t border-gray-700">
|
||||
<Link
|
||||
to="/settings"
|
||||
className={`flex items-center px-3 py-2.5 text-sm rounded-lg transition-colors group ${
|
||||
location.pathname === '/settings'
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
|
||||
}`}
|
||||
title={isCollapsed ? 'Configuración' : ''}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="w-5 h-5" 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>
|
||||
{!isCollapsed && (
|
||||
<span className="ml-3 font-medium">Configuración</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Footer - Perfil del Usuario */}
|
||||
<div className="p-3 border-t border-gray-700">
|
||||
{!isCollapsed ? (
|
||||
<div className="space-y-3">
|
||||
{/* Información del usuario */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
<img
|
||||
className="w-8 h-8 rounded-full ring-2 ring-gray-700"
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Avatar del usuario"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">
|
||||
Juan Pérez
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 truncate">
|
||||
Administrador
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botón de logout más pequeño */}
|
||||
<div className="pt-2">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full inline-flex items-center justify-center px-3 py-2 border border-transparent text-xs font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition 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="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
Cerrar sesión
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
{/* Avatar pequeño */}
|
||||
<img
|
||||
className="w-8 h-8 rounded-full ring-2 ring-gray-700"
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Avatar del usuario"
|
||||
title="Juan Pérez - Administrador"
|
||||
/>
|
||||
{/* Botón de logout compacto */}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-1.5 text-gray-400 hover:text-white hover:bg-red-600 rounded transition-colors"
|
||||
title="Cerrar sesión"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
207
src/components/SidebarNew.jsx
Normal file
207
src/components/SidebarNew.jsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function Sidebar() {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
|
||||
// Disparar evento para actualizar el navbar
|
||||
window.dispatchEvent(new CustomEvent('authStateChanged'));
|
||||
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
name: 'Mi Organización',
|
||||
path: '/organization',
|
||||
icon: (
|
||||
<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>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Importadores',
|
||||
path: '/importers',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Usuarios',
|
||||
path: '/users',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Reportes',
|
||||
path: '/reports',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Documentos',
|
||||
path: '/documents',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" 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>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={`bg-gray-900 text-white transition-all duration-300 ${isCollapsed ? 'w-16' : 'w-64'} min-h-screen flex flex-col`}>
|
||||
{/* Header - Logo y colapsar */}
|
||||
<div className="p-4 border-b border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
{!isCollapsed && (
|
||||
<div className="flex items-center">
|
||||
{/* Logo de la organización */}
|
||||
<div className="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg className="w-5 h-5 text-white" 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>
|
||||
<h1 className="text-lg font-bold text-white">EFC Dashboard</h1>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="p-1.5 rounded-lg hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{isCollapsed ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-3 space-y-1">
|
||||
{menuItems.map((item) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={`flex items-center px-3 py-2.5 text-sm rounded-lg transition-colors group ${
|
||||
isActive
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
|
||||
}`}
|
||||
title={isCollapsed ? item.name : ''}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
{item.icon}
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<span className="ml-3 font-medium">{item.name}</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Configuración */}
|
||||
<div className="p-3 border-t border-gray-700">
|
||||
<Link
|
||||
to="/settings"
|
||||
className={`flex items-center px-3 py-2.5 text-sm rounded-lg transition-colors group ${
|
||||
location.pathname === '/settings'
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
|
||||
}`}
|
||||
title={isCollapsed ? 'Configuración' : ''}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="w-5 h-5" 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>
|
||||
{!isCollapsed && (
|
||||
<span className="ml-3 font-medium">Configuración</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Footer - Perfil del Usuario */}
|
||||
<div className="p-3 border-t border-gray-700">
|
||||
{!isCollapsed ? (
|
||||
<div className="space-y-3">
|
||||
{/* Información del usuario */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
<img
|
||||
className="w-8 h-8 rounded-full ring-2 ring-gray-700"
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Avatar del usuario"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">
|
||||
Juan Pérez
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 truncate">
|
||||
Administrador
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botón de logout más pequeño */}
|
||||
<div className="pt-2">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full inline-flex items-center justify-center px-3 py-2 border border-transparent text-xs font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition 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="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
Cerrar sesión
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
{/* Avatar pequeño */}
|
||||
<img
|
||||
className="w-8 h-8 rounded-full ring-2 ring-gray-700"
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Avatar del usuario"
|
||||
title="Juan Pérez - Administrador"
|
||||
/>
|
||||
{/* Botón de logout compacto */}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-1.5 text-gray-400 hover:text-white hover:bg-red-600 rounded transition-colors"
|
||||
title="Cerrar sesión"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
src/components/SuccessModal.jsx
Normal file
25
src/components/SuccessModal.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function SuccessModal({ open, onClose, message = 'Descarga exitosa' }) {
|
||||
if (!open) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40">
|
||||
<div className="bg-white rounded-xl shadow-2xl border border-green-200 p-8 max-w-sm w-full flex flex-col items-center animate-fade-in">
|
||||
<svg className="h-12 w-12 text-green-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<h2 className="text-2xl font-bold text-green-700 mb-2">{message}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="mt-4 px-6 py-2 bg-green-600 text-white rounded-lg font-semibold shadow hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-400"
|
||||
>
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
<style>{`
|
||||
@keyframes fade-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
||||
.animate-fade-in { animation: fade-in 0.3s ease; }
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/components/TestTailwind.jsx
Normal file
10
src/components/TestTailwind.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function TestTailwind() {
|
||||
return (
|
||||
<div className="bg-blue-500 text-white p-4 rounded-lg shadow-lg">
|
||||
<h1 className="text-2xl font-bold">¡Tailwind CSS funciona!</h1>
|
||||
<p className="mt-2">Si ves este texto en azul con padding y sombra, Tailwind está trabajando correctamente.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
src/components/card.jsx
Normal file
0
src/components/card.jsx
Normal file
Reference in New Issue
Block a user