From 21889a37616140afceba579692e1169cb684f987 Mon Sep 17 00:00:00 2001 From: Juan Pablo Restrepo Date: Fri, 5 Jun 2026 23:14:20 -0500 Subject: [PATCH] =?UTF-8?q?Usuario=20vendedor=20y=20pesta=C3=B1a=20descarg?= =?UTF-8?q?as?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 6 +- data/contraseñas.json | 4 +- data/productos.json | 2 +- data/sesiones.json | 24 ++++++++ frontend/administrativo/admin.css | 94 ++++++++++++++++++++++++++++++ frontend/administrativo/admin.js | 88 +++++++++++++++++++++------- frontend/administrativo/index.html | 49 +++++++++++++++- 7 files changed, 238 insertions(+), 29 deletions(-) diff --git a/backend/server.js b/backend/server.js index 3b37056..224d918 100644 --- a/backend/server.js +++ b/backend/server.js @@ -576,7 +576,7 @@ app.post('/api/usuarios', requireUserManagement, async (req, res) => { if (contraseña.length < 6) { 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' }); } @@ -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' }); } - // 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' ? [] : (Array.isArray(permisos) ? permisos : []); @@ -606,7 +606,7 @@ app.put('/api/usuarios/:usuario', requireUserManagement, async (req, res) => { const targetUsuario = req.params.usuario; 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' }); } if (contraseña && contraseña.length < 6) { diff --git a/data/contraseñas.json b/data/contraseñas.json index 87e35d2..99e1849 100644 --- a/data/contraseñas.json +++ b/data/contraseñas.json @@ -8,9 +8,9 @@ { "usuario": "juan", "contraseña": "123456", - "rol": "editor", + "rol": "vendedor", "permisos": [ - "productos" + "descargas" ] } ] diff --git a/data/productos.json b/data/productos.json index d46808f..ea2812c 100644 --- a/data/productos.json +++ b/data/productos.json @@ -3,7 +3,7 @@ "id": "AF100-124", "ref": "AF100-124", "cat": "bolsos", - "img": "bolsos/AF100-124.webp", + "img": "agotados/AF100-124.webp", "nombre": "Bolso de cuero negro estilo clásico", "precio": 50000 }, diff --git a/data/sesiones.json b/data/sesiones.json index a2471c6..9884b65 100644 --- a/data/sesiones.json +++ b/data/sesiones.json @@ -4,5 +4,29 @@ "usuario": "admin", "rol": "admin", "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 } ] diff --git a/frontend/administrativo/admin.css b/frontend/administrativo/admin.css index e5dd21c..0efd232 100644 --- a/frontend/administrativo/admin.css +++ b/frontend/administrativo/admin.css @@ -1025,6 +1025,11 @@ select { 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 { display: inline-block; margin-left: 6px; @@ -1194,6 +1199,89 @@ select { 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) { *, *::before, *::after { animation-duration: 0.01ms !important; @@ -1202,6 +1290,12 @@ select { .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 ════════════════════════════════ */ diff --git a/frontend/administrativo/admin.js b/frontend/administrativo/admin.js index b310cb9..a2e81d3 100644 --- a/frontend/administrativo/admin.js +++ b/frontend/administrativo/admin.js @@ -10,13 +10,15 @@ const TABS = [ { id: 'productos', label: 'Productos', icon: '📦' }, { id: 'agotados', label: 'Agotados', icon: '🚫' }, + { id: 'descargas', label: 'Descargas', icon: '⬇️' }, { id: 'categorias', label: 'Categorías', icon: '🗂️' }, { id: 'usuarios', label: 'Usuarios', icon: '👥' } ]; const ROLES = { - admin: 'Administrador', - editor: 'Editor' + admin: 'Administrador', + editor: 'Editor', + vendedor: 'Vendedor' }; // Iconos disponibles para categorías @@ -41,6 +43,7 @@ document.addEventListener('DOMContentLoaded', async () => { setupLoginForm(); setupLogout(); setupNavigation(); + setupSubtabs(); setupModals(); setupPermisosToggles(); setupCategoriasModals(); @@ -94,19 +97,28 @@ function mostrarDashboard(user) { // Aplicar permisos (visibilidad de nav, subsección admin-only, etc.) aplicarPermisos(); - // Reset a la vista dashboard - cambiarVista('dashboard'); + // Determinar vista inicial + 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(); } function cambiarVista(view) { - // Check defensivo: si el usuario no tiene permiso para esta vista, redirige a dashboard - if (currentUser && view !== 'dashboard') { + // Check defensivo: si el usuario no tiene permiso para esta vista, redirige a la primera vista visible + if (currentUser) { const esAdmin = currentUser.rol === 'admin'; - const permisos = currentUser.permisos || []; - if (!esAdmin && !permisos.includes(view)) { - view = 'dashboard'; + const perms = getPermisosEfectivos(); + if (!esAdmin && !perms.includes(view)) { + view = perms[0] || 'descargas'; } } @@ -150,6 +162,10 @@ function cambiarVista(view) { if (view === 'agotados') { cargarAgotados(); } + + if (view === 'descargas') { + cargarDescargas(); + } } async function cargarEstadisticas() { @@ -172,27 +188,28 @@ async function cargarEstadisticas() { // ════════════════════════════════ // 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() { if (!currentUser) return; const esAdmin = currentUser.rol === 'admin'; - const permisos = currentUser.permisos || []; - const puedeGestionarUsuarios = esAdmin || permisos.includes('usuarios'); + const perms = getPermisosEfectivos(); + const puedeGestionarUsuarios = esAdmin || perms.includes('usuarios'); // Mostrar/ocultar cada nav-item según permisos document.querySelectorAll('.nav-item[data-view]').forEach(item => { const view = item.dataset.view; - let visible = false; - - 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 - } - + const visible = esAdmin || perms.includes(view); 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 // ════════════════════════════════ diff --git a/frontend/administrativo/index.html b/frontend/administrativo/index.html index 2bbe1e0..debacff 100644 --- a/frontend/administrativo/index.html +++ b/frontend/administrativo/index.html @@ -7,7 +7,7 @@ - + @@ -73,6 +73,10 @@ 🚫 Agotados + + + + +
+
+
+ 📄 +

Descargas en PDF

+

Próximamente: herramientas para generar y descargar PDFs del catálogo.

+
+
+
+ +
+
+
+ 🖼️ +

Descargas de fotos

+

Próximamente: herramientas para descargar lotes de fotos del catálogo.

+
+
+
+ +
@@ -337,6 +380,7 @@ @@ -382,6 +426,7 @@ @@ -500,6 +545,6 @@ - +