Usuario vendedor y pestaña descargas

This commit is contained in:
2026-06-05 23:14:20 -05:00
parent 984f2f103d
commit 21889a3761
7 changed files with 238 additions and 29 deletions
+3 -3
View File
@@ -576,7 +576,7 @@ app.post('/api/usuarios', requireUserManagement, async (req, res) => {
if (contraseña.length < 6) { if (contraseña.length < 6) {
return res.status(400).json({ error: 'La contraseña debe tener al menos 6 caracteres' }); return res.status(400).json({ error: 'La contraseña debe tener al menos 6 caracteres' });
} }
if (!['admin', 'editor'].includes(rol)) { if (!['admin', 'editor', 'vendedor'].includes(rol)) {
return res.status(400).json({ error: 'Rol inválido' }); return res.status(400).json({ error: 'Rol inválido' });
} }
@@ -588,7 +588,7 @@ app.post('/api/usuarios', requireUserManagement, async (req, res) => {
return res.status(409).json({ error: 'Ya existe un usuario con ese nombre' }); return res.status(409).json({ error: 'Ya existe un usuario con ese nombre' });
} }
// Admin siempre tiene todos los permisos; editor recibe la lista enviada // Admin siempre tiene todos los permisos; editor/vendedor reciben la lista enviada
const permisosFinal = rol === 'admin' const permisosFinal = rol === 'admin'
? [] ? []
: (Array.isArray(permisos) ? permisos : []); : (Array.isArray(permisos) ? permisos : []);
@@ -606,7 +606,7 @@ app.put('/api/usuarios/:usuario', requireUserManagement, async (req, res) => {
const targetUsuario = req.params.usuario; const targetUsuario = req.params.usuario;
const { contraseña, rol, permisos } = req.body || {}; const { contraseña, rol, permisos } = req.body || {};
if (rol && !['admin', 'editor'].includes(rol)) { if (rol && !['admin', 'editor', 'vendedor'].includes(rol)) {
return res.status(400).json({ error: 'Rol inválido' }); return res.status(400).json({ error: 'Rol inválido' });
} }
if (contraseña && contraseña.length < 6) { if (contraseña && contraseña.length < 6) {
+2 -2
View File
@@ -8,9 +8,9 @@
{ {
"usuario": "juan", "usuario": "juan",
"contraseña": "123456", "contraseña": "123456",
"rol": "editor", "rol": "vendedor",
"permisos": [ "permisos": [
"productos" "descargas"
] ]
} }
] ]
+1 -1
View File
@@ -3,7 +3,7 @@
"id": "AF100-124", "id": "AF100-124",
"ref": "AF100-124", "ref": "AF100-124",
"cat": "bolsos", "cat": "bolsos",
"img": "bolsos/AF100-124.webp", "img": "agotados/AF100-124.webp",
"nombre": "Bolso de cuero negro estilo clásico", "nombre": "Bolso de cuero negro estilo clásico",
"precio": 50000 "precio": 50000
}, },
+24
View File
@@ -4,5 +4,29 @@
"usuario": "admin", "usuario": "admin",
"rol": "admin", "rol": "admin",
"createdAt": 1780718070269 "createdAt": 1780718070269
},
{
"token": "18342f8390074dea906ffebf0f0a5e34feb6a27c9caf2c7c721542dd961d5d59",
"usuario": "admin",
"rol": "admin",
"createdAt": 1780719045054
},
{
"token": "7e95e1adfdc6f90a52122d72e6e05d24bcafd4c9727be52976ea312a636ab2f5",
"usuario": "vendetest",
"rol": "vendedor",
"createdAt": 1780719045099
},
{
"token": "a9b4be06e4e40fe83595d388a8c08b9270df7557e2a2b63b793552609b2d2d71",
"usuario": "vendetest",
"rol": "vendedor",
"createdAt": 1780719061095
},
{
"token": "4cb3d9aa79e1c83e87863a32d4cbbc9a2eafadaa08ba110964250121939b8d3a",
"usuario": "admin",
"rol": "admin",
"createdAt": 1780719205299
} }
] ]
+94
View File
@@ -1025,6 +1025,11 @@ select {
color: #0e7490; color: #0e7490;
} }
.rol-vendedor {
background: linear-gradient(135deg, rgba(252, 211, 77, 0.22) 0%, rgba(251, 146, 60, 0.22) 100%);
color: #b45309;
}
.you-badge { .you-badge {
display: inline-block; display: inline-block;
margin-left: 6px; margin-left: 6px;
@@ -1194,6 +1199,89 @@ select {
75% { transform: translateX(6px); } 75% { transform: translateX(6px); }
} }
/* ════════════════════════════════
SUBTABS (vista Descargas)
════════════════════════════════ */
.subtabs {
display: flex;
gap: 8px;
padding: 6px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-lg);
margin-bottom: 18px;
width: fit-content;
max-width: 100%;
overflow-x: auto;
}
.subtab {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 18px;
border: 1px solid transparent;
background: transparent;
color: var(--ink-mute);
font: inherit;
font-weight: 600;
font-size: 0.88rem;
border-radius: calc(var(--radius-lg) - 6px);
cursor: pointer;
transition: color .18s ease, background .18s ease, border-color .18s ease;
white-space: nowrap;
}
.subtab:hover:not(.active) {
color: var(--ink);
background: rgba(255,255,255,0.45);
}
.subtab.active {
background: linear-gradient(135deg, var(--violet), var(--pink));
color: #fff;
border-color: var(--line-strong);
box-shadow: 0 4px 14px -6px rgba(167, 139, 250, 0.55);
}
.subtab-icon { font-size: 1rem; line-height: 1; }
.subtab-label { line-height: 1; }
.subtab-panel { display: none; }
.subtab-panel.active { display: block; }
.subtab-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 48px 24px;
gap: 12px;
}
.subtab-placeholder-icon {
font-size: 3.2rem;
line-height: 1;
opacity: 0.85;
filter: drop-shadow(0 6px 18px rgba(167, 139, 250, 0.35));
}
.subtab-placeholder h3 {
margin: 0;
font-family: var(--font-serif);
font-size: 1.35rem;
font-weight: 400;
color: var(--ink);
}
.subtab-placeholder p {
margin: 0;
color: var(--ink-mute);
max-width: 460px;
line-height: 1.55;
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
*, *::before, *::after { *, *::before, *::after {
animation-duration: 0.01ms !important; animation-duration: 0.01ms !important;
@@ -1202,6 +1290,12 @@ select {
.mesh-blob { animation: none; } .mesh-blob { animation: none; }
} }
@media (max-width: 480px) {
.subtab { padding: 8px 12px; font-size: 0.82rem; }
.subtab-placeholder { padding: 32px 16px; }
.subtab-placeholder-icon { font-size: 2.6rem; }
}
/* ════════════════════════════════ /* ════════════════════════════════
RESPONSIVE — topbar wrap RESPONSIVE — topbar wrap
════════════════════════════════ */ ════════════════════════════════ */
+66 -20
View File
@@ -10,13 +10,15 @@
const TABS = [ const TABS = [
{ id: 'productos', label: 'Productos', icon: '📦' }, { id: 'productos', label: 'Productos', icon: '📦' },
{ id: 'agotados', label: 'Agotados', icon: '🚫' }, { id: 'agotados', label: 'Agotados', icon: '🚫' },
{ id: 'descargas', label: 'Descargas', icon: '⬇️' },
{ id: 'categorias', label: 'Categorías', icon: '🗂️' }, { id: 'categorias', label: 'Categorías', icon: '🗂️' },
{ id: 'usuarios', label: 'Usuarios', icon: '👥' } { id: 'usuarios', label: 'Usuarios', icon: '👥' }
]; ];
const ROLES = { const ROLES = {
admin: 'Administrador', admin: 'Administrador',
editor: 'Editor' editor: 'Editor',
vendedor: 'Vendedor'
}; };
// Iconos disponibles para categorías // Iconos disponibles para categorías
@@ -41,6 +43,7 @@ document.addEventListener('DOMContentLoaded', async () => {
setupLoginForm(); setupLoginForm();
setupLogout(); setupLogout();
setupNavigation(); setupNavigation();
setupSubtabs();
setupModals(); setupModals();
setupPermisosToggles(); setupPermisosToggles();
setupCategoriasModals(); setupCategoriasModals();
@@ -94,19 +97,28 @@ function mostrarDashboard(user) {
// Aplicar permisos (visibilidad de nav, subsección admin-only, etc.) // Aplicar permisos (visibilidad de nav, subsección admin-only, etc.)
aplicarPermisos(); aplicarPermisos();
// Reset a la vista dashboard // Determinar vista inicial
cambiarVista('dashboard'); const esAdmin = user.rol === 'admin';
const perms = getPermisosEfectivos();
let viewInicial;
if (esAdmin || perms.includes('dashboard')) {
viewInicial = 'dashboard';
} else {
// Primera vista visible según permisos
viewInicial = perms[0] || 'descargas';
}
cambiarVista(viewInicial);
cargarEstadisticas(); cargarEstadisticas();
} }
function cambiarVista(view) { function cambiarVista(view) {
// Check defensivo: si el usuario no tiene permiso para esta vista, redirige a dashboard // Check defensivo: si el usuario no tiene permiso para esta vista, redirige a la primera vista visible
if (currentUser && view !== 'dashboard') { if (currentUser) {
const esAdmin = currentUser.rol === 'admin'; const esAdmin = currentUser.rol === 'admin';
const permisos = currentUser.permisos || []; const perms = getPermisosEfectivos();
if (!esAdmin && !permisos.includes(view)) { if (!esAdmin && !perms.includes(view)) {
view = 'dashboard'; view = perms[0] || 'descargas';
} }
} }
@@ -150,6 +162,10 @@ function cambiarVista(view) {
if (view === 'agotados') { if (view === 'agotados') {
cargarAgotados(); cargarAgotados();
} }
if (view === 'descargas') {
cargarDescargas();
}
} }
async function cargarEstadisticas() { async function cargarEstadisticas() {
@@ -172,27 +188,28 @@ async function cargarEstadisticas() {
// ════════════════════════════════ // ════════════════════════════════
// PERMISOS — visibilidad según rol + permisos[] // PERMISOS — visibilidad según rol + permisos[]
// Vendedor: SIEMPRE ve 'descargas' (implícito) + lo que le hayan dado
// ════════════════════════════════ // ════════════════════════════════
function getPermisosEfectivos() {
if (!currentUser) return [];
const permisos = currentUser.permisos || [];
if (currentUser.rol === 'vendedor' && !permisos.includes('descargas')) {
return ['descargas', ...permisos];
}
return permisos;
}
function aplicarPermisos() { function aplicarPermisos() {
if (!currentUser) return; if (!currentUser) return;
const esAdmin = currentUser.rol === 'admin'; const esAdmin = currentUser.rol === 'admin';
const permisos = currentUser.permisos || []; const perms = getPermisosEfectivos();
const puedeGestionarUsuarios = esAdmin || permisos.includes('usuarios'); const puedeGestionarUsuarios = esAdmin || perms.includes('usuarios');
// Mostrar/ocultar cada nav-item según permisos // Mostrar/ocultar cada nav-item según permisos
document.querySelectorAll('.nav-item[data-view]').forEach(item => { document.querySelectorAll('.nav-item[data-view]').forEach(item => {
const view = item.dataset.view; const view = item.dataset.view;
let visible = false; const visible = esAdmin || perms.includes(view);
if (view === 'dashboard') {
visible = true; // Dashboard siempre visible
} else if (esAdmin) {
visible = true; // Admin ve todo
} else {
visible = permisos.includes(view); // Editor: solo sus permisos
}
item.style.display = visible ? '' : 'none'; item.style.display = visible ? '' : 'none';
}); });
@@ -220,6 +237,35 @@ function setupNavigation() {
}); });
} }
// ════════════════════════════════
// SUBTABS (dentro de la view 'descargas')
// ════════════════════════════════
function setupSubtabs() {
document.querySelectorAll('.subtab').forEach(btn => {
btn.addEventListener('click', () => {
const subtab = btn.dataset.subtab;
// Activar solo este botón
btn.parentElement.querySelectorAll('.subtab').forEach(b => {
b.classList.toggle('active', b === btn);
b.setAttribute('aria-selected', b === btn ? 'true' : 'false');
});
// Mostrar solo el panel correspondiente
document.querySelectorAll('.subtab-panel').forEach(p => {
p.classList.toggle('active', p.dataset.subtabPanel === subtab);
});
});
});
}
// ════════════════════════════════
// DESCARGAS (vista + subtabs pdf / fotos)
// Funcionalidad TBD — placeholders por ahora
// ════════════════════════════════
function cargarDescargas() {
// Placeholder. Cuando se implemente cada subpestaña, se cargan
// los datos del backend (PDFs disponibles, lotes de fotos, etc.).
}
// ════════════════════════════════ // ════════════════════════════════
// PERMISOS EN MODALES — render y lectura // PERMISOS EN MODALES — render y lectura
// ════════════════════════════════ // ════════════════════════════════
+47 -2
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="admin.css?v=5"> <link rel="stylesheet" href="admin.css?v=6">
</head> </head>
<body> <body>
@@ -73,6 +73,10 @@
<span class="nav-icon">🚫</span> <span class="nav-icon">🚫</span>
<span class="nav-label">Agotados</span> <span class="nav-label">Agotados</span>
</button> </button>
<button type="button" class="nav-item" data-view="descargas">
<span class="nav-icon">⬇️</span>
<span class="nav-label">Descargas</span>
</button>
<button type="button" class="nav-item" data-view="categorias"> <button type="button" class="nav-item" data-view="categorias">
<span class="nav-icon">🗂️</span> <span class="nav-icon">🗂️</span>
<span class="nav-label">Categorías</span> <span class="nav-label">Categorías</span>
@@ -233,6 +237,45 @@
</div> </div>
</section> </section>
<!-- DESCARGAS VIEW (vendedores y otros usuarios con permiso) -->
<section class="view" data-view="descargas">
<header class="view-header">
<h1 class="view-title">Descargas</h1>
<p class="view-subtitle">Recursos descargables del catálogo.</p>
</header>
<div class="subtabs" role="tablist" aria-label="Tipos de descarga">
<button type="button" class="subtab active" data-subtab="pdf" role="tab" aria-selected="true">
<span class="subtab-icon">📄</span>
<span class="subtab-label">PDF</span>
</button>
<button type="button" class="subtab" data-subtab="fotos" role="tab" aria-selected="false">
<span class="subtab-icon">🖼️</span>
<span class="subtab-label">Fotos</span>
</button>
</div>
<div class="subtab-panel active" data-subtab-panel="pdf">
<div class="view-content-card">
<div class="subtab-placeholder">
<span class="subtab-placeholder-icon">📄</span>
<h3>Descargas en PDF</h3>
<p>Próximamente: herramientas para generar y descargar PDFs del catálogo.</p>
</div>
</div>
</div>
<div class="subtab-panel" data-subtab-panel="fotos">
<div class="view-content-card">
<div class="subtab-placeholder">
<span class="subtab-placeholder-icon">🖼️</span>
<h3>Descargas de fotos</h3>
<p>Próximamente: herramientas para descargar lotes de fotos del catálogo.</p>
</div>
</div>
</div>
</section>
<!-- CATEGORÍAS VIEW --> <!-- CATEGORÍAS VIEW -->
<section class="view" data-view="categorias"> <section class="view" data-view="categorias">
<header class="view-header view-header-with-action"> <header class="view-header view-header-with-action">
@@ -337,6 +380,7 @@
<label for="nuevoRol">Rol</label> <label for="nuevoRol">Rol</label>
<select id="nuevoRol" required> <select id="nuevoRol" required>
<option value="editor">Editor</option> <option value="editor">Editor</option>
<option value="vendedor">Vendedor</option>
<option value="admin">Administrador</option> <option value="admin">Administrador</option>
</select> </select>
</div> </div>
@@ -382,6 +426,7 @@
<label for="editarRol">Rol</label> <label for="editarRol">Rol</label>
<select id="editarRol" required> <select id="editarRol" required>
<option value="editor">Editor</option> <option value="editor">Editor</option>
<option value="vendedor">Vendedor</option>
<option value="admin">Administrador</option> <option value="admin">Administrador</option>
</select> </select>
</div> </div>
@@ -500,6 +545,6 @@
</div> </div>
</div> </div>
<script src="admin.js?v=5"></script> <script src="admin.js?v=6"></script>
</body> </body>
</html> </html>