diff --git a/Imagenes/bolsos/dama/aaa/TEST-A.webp b/Imagenes/agotados/TEST-A.webp similarity index 100% rename from Imagenes/bolsos/dama/aaa/TEST-A.webp rename to Imagenes/agotados/TEST-A.webp diff --git a/Imagenes/bolsos/dama/aaa/TEST-B.webp b/Imagenes/agotados/TEST-B.webp similarity index 100% rename from Imagenes/bolsos/dama/aaa/TEST-B.webp rename to Imagenes/agotados/TEST-B.webp diff --git a/Imagenes/bolsos/dama/aaa/TEST-D.webp b/Imagenes/agotados/TEST-D.webp similarity index 100% rename from Imagenes/bolsos/dama/aaa/TEST-D.webp rename to Imagenes/agotados/TEST-D.webp diff --git a/backend/server.js b/backend/server.js index b622eee..c34d123 100644 --- a/backend/server.js +++ b/backend/server.js @@ -23,6 +23,7 @@ const DATA_DIR = path.join(__dirname, 'data'); const FRONTEND_DIR = path.join(__dirname, 'frontend'); const IMG_DIR = path.join(__dirname, 'Imagenes'); const PEDIDOS_DIR = path.join(__dirname, 'pedidos'); +const VIDEOS_DIR = path.join(__dirname, 'videos'); // ════════════════════════════════ // MIDDLEWARES @@ -32,6 +33,7 @@ app.use(cookieParser(SECRET)); app.use(express.static(FRONTEND_DIR)); app.use('/Imagenes', express.static(IMG_DIR, { maxAge: '7d' })); app.use('/pedidos', express.static(PEDIDOS_DIR)); +app.use('/videos', express.static(VIDEOS_DIR)); // Evita el 404 de favicon.ico app.get('/favicon.ico', (req, res) => res.status(204).end()); @@ -2162,6 +2164,169 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => { } }); +// ════════════════════════════════ +// REDES SOCIALES — gestión de publicaciones embebidas +// ════════════════════════════════ +const REDES_FILE = 'redes.json'; + +async function getRedes() { + try { return await readJson(REDES_FILE); } + catch { return []; } +} + +function extraerPlataforma(url) { + if (/instagram\.com/i.test(url)) return 'instagram'; + if (/facebook\.com/i.test(url)) return 'facebook'; + return null; +} + +// Público: solo activas +app.get('/api/redes', async (req, res) => { + const todas = await getRedes(); + res.json(todas.filter(r => r.activo).sort((a, b) => a.orden - b.orden)); +}); + +// Admin: todas +app.get('/api/redes/admin', requireAdmin, async (req, res) => { + const todas = await getRedes(); + res.json(todas.sort((a, b) => a.orden - b.orden)); +}); + +app.post('/api/redes', requireAdmin, async (req, res) => { + const { url } = req.body || {}; + if (!url || !url.trim()) { + return res.status(400).json({ error: 'La URL es obligatoria' }); + } + const plataforma = extraerPlataforma(url); + if (!plataforma) { + return res.status(400).json({ error: 'La URL debe ser de Instagram o Facebook' }); + } + const todas = await getRedes(); + const nueva = { + id: String(Date.now()), + url: url.trim(), + plataforma, + activo: true, + orden: todas.length + }; + todas.push(nueva); + await writeJson(REDES_FILE, todas); + res.json(todas); +}); + +app.delete('/api/redes/:id', requireAdmin, async (req, res) => { + let todas = await getRedes(); + todas = todas.filter(r => r.id !== req.params.id); + await writeJson(REDES_FILE, todas); + res.json(todas); +}); + +app.put('/api/redes/:id', requireAdmin, async (req, res) => { + const todas = await getRedes(); + const idx = todas.findIndex(r => r.id === req.params.id); + if (idx === -1) return res.status(404).json({ error: 'No encontrada' }); + const { activo } = req.body || {}; + if (typeof activo === 'boolean') todas[idx].activo = activo; + await writeJson(REDES_FILE, todas); + res.json(todas); +}); + +// ════════════════════════════════ +// VIDEOS — gestión de videos subidos +// ════════════════════════════════ +const MAX_VIDEOS = 10; + +const videoUpload = multer({ + storage: multer.diskStorage({ + destination: VIDEOS_DIR, + filename: (req, file, cb) => { + const ext = path.extname(file.originalname) || '.mp4'; + cb(null, `video-${Date.now()}${ext}`); + } + }), + limits: { fileSize: 200 * 1024 * 1024 } // 200MB +}); + +app.get('/api/videos', async (req, res) => { + try { + await fs.mkdir(VIDEOS_DIR, { recursive: true }); + const files = await fs.readdir(VIDEOS_DIR); + const videos = files + .filter(f => f.startsWith('video-')) + .sort() + .reverse() + .map(f => ({ filename: f, url: `/videos/${f}` })); + res.json(videos); + } catch (err) { + console.error('Error listing videos:', err); + res.status(500).json({ error: 'Error al listar videos' }); + } +}); + +app.post('/api/videos', requireAdmin, videoUpload.single('video'), async (req, res) => { + try { + await fs.mkdir(VIDEOS_DIR, { recursive: true }); + const files = await fs.readdir(VIDEOS_DIR); + const videoFiles = files.filter(f => f.startsWith('video-')); + if (videoFiles.length > MAX_VIDEOS) { + // Eliminar el que se subió justo ahora si excede + if (req.file) await fs.unlink(req.file.path).catch(() => {}); + return res.status(400).json({ error: `Máximo ${MAX_VIDEOS} videos. Elimina uno antes de subir otro.` }); + } + const videos = videoFiles.sort().reverse().map(f => ({ filename: f, url: `/videos/${f}` })); + res.json(videos); + } catch (err) { + console.error('Error uploading video:', err); + if (req.file) await fs.unlink(req.file.path).catch(() => {}); + res.status(500).json({ error: 'Error al subir video' }); + } +}); + +app.delete('/api/videos/:filename', requireAdmin, async (req, res) => { + const { filename } = req.params; + if (!filename.startsWith('video-')) { + return res.status(400).json({ error: 'Nombre de archivo inválido' }); + } + try { + await fs.unlink(path.join(VIDEOS_DIR, filename)); + res.json({ ok: true }); + } catch (err) { + if (err.code === 'ENOENT') return res.status(404).json({ error: 'Video no encontrado' }); + console.error('Error deleting video:', err); + res.status(500).json({ error: 'Error al eliminar video' }); + } +}); + +// Proxy para embeds de redes sociales (evita bloqueo de terceros en móviles) +app.get('/api/redes/proxy', async (req, res) => { + const { url } = req.query; + if (!url) return res.status(400).json({ error: 'Falta url' }); + + let embedUrl; + if (url.includes('instagram.com/p/')) { + const match = url.match(/instagram\.com\/p\/([^/?]+)/); + if (!match) return res.status(400).json({ error: 'URL de Instagram inválida' }); + embedUrl = `https://www.instagram.com/p/${match[1]}/embed`; + } else if (url.includes('facebook.com')) { + embedUrl = `https://www.facebook.com/plugins/post.php?href=${encodeURIComponent(url)}&show_text=true`; + } else { + return res.status(400).json({ error: 'Solo se admiten URLs de Instagram y Facebook' }); + } + + try { + const response = await fetch(embedUrl, { + headers: { 'User-Agent': 'Mozilla/5.0 (compatible; InfinityBot)' } + }); + const html = await response.text(); + res.set('Content-Type', 'text/html; charset=utf-8'); + res.set('X-Frame-Options', ''); + res.send(html); + } catch (err) { + console.error('Redes proxy error:', err); + res.status(502).send('Error al cargar el contenido'); + } +}); + // ════════════════════════════════ // START // ════════════════════════════════ diff --git a/data/pedidos-meta.json b/data/pedidos-meta.json index 6a78426..fe51488 100644 --- a/data/pedidos-meta.json +++ b/data/pedidos-meta.json @@ -1,28 +1 @@ -[ - { - "filename": "pedido-1781199786075.pdf", - "fecha": "2026-06-11T17:43:06.095Z", - "cliente": { - "nombre": "juan", - "ciudad": "medellin", - "telefono": "4444", - "direccion": "44444" - }, - "items": [ - { - "ref": "AF100-198", - "nombre": "BOLSO", - "precio": 10000, - "cantidad": 1 - }, - { - "ref": "AF100-197", - "nombre": "BOLSO", - "precio": 10000, - "cantidad": 1 - } - ], - "vendedor": "JUAN PABLO", - "total": 20000 - } -] +[] diff --git a/data/productos.json b/data/productos.json index 1bd4120..06dec70 100644 --- a/data/productos.json +++ b/data/productos.json @@ -205,7 +205,7 @@ "dama", "aaa" ], - "img": "bolsos/dama/aaa/TEST-A.webp" + "img": "agotados/TEST-A.webp" }, { "id": "TEST-B", @@ -215,7 +215,7 @@ "dama", "aaa" ], - "img": "bolsos/dama/aaa/TEST-B.webp" + "img": "agotados/TEST-B.webp" }, { "id": "TEST-C", @@ -235,7 +235,7 @@ "dama", "aaa" ], - "img": "bolsos/dama/aaa/TEST-D.webp" + "img": "agotados/TEST-D.webp" }, { "id": "AF2296", diff --git a/data/redes.json b/data/redes.json new file mode 100644 index 0000000..f013b98 --- /dev/null +++ b/data/redes.json @@ -0,0 +1,9 @@ +[ + { + "id": "1781231563437", + "url": "https://www.instagram.com/reel/DZdHX9QJoNS/?igsh=NndzcGRvcjRzbzZ4", + "plataforma": "instagram", + "activo": true, + "orden": 0 + } +] diff --git a/data/sesiones.json b/data/sesiones.json index 9ba12ee..72190a5 100644 --- a/data/sesiones.json +++ b/data/sesiones.json @@ -1,110 +1,20 @@ [ { - "token": "079fdd39b3668e3b9e2fddd3226282aa3a5f45f0162db16b95746203dbe8af74", + "token": "dc141a69e7bb5c9f66d1f1131c0cb6902e6a7082289febb54077b685e2f1449e", "usuario": "admin", "rol": "admin", - "createdAt": 1781197392273 + "createdAt": 1781357350132 }, { - "token": "b34f81ffe57e3870875439696e6150eb5a1ea900ad610a25da409dc6b576e9ed", + "token": "13beb84b5e8f84e499bd00950852ac0a247eb4627e277296ba9e1e65a30d02e8", "usuario": "admin", "rol": "admin", - "createdAt": 1781197414777 + "createdAt": 1781357357508 }, { - "token": "db6d37e43fe99bb6adb3f824425df3c3c5ef1cb638695578e1807659ad35e102", + "token": "268cfe4078b47c62418fbe26aef0472d0029680b86f6c44252a6cc8faba3f7b6", "usuario": "admin", "rol": "admin", - "createdAt": 1781197734902 - }, - { - "token": "33bd4d4276521f5f800455a1a29ca5b4b9108a746ac602399aba623dc2c55d9d", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781197806825 - }, - { - "token": "63d52b8966cbf5df5449d8b0ae33efbbb4c75f8e1542e1e1ddde46d5c3e9cae1", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781198005875 - }, - { - "token": "2784a09fe817c7654283e0ad7dfb68f6d80737b95f38fa863d76dcc7c8b7eb05", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781198034642 - }, - { - "token": "c3c5b6b92ad256ea628da9c18ba298b2f95c637443dab0c64f3586eba400fb62", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781198193732 - }, - { - "token": "fc8fcf45847ebdc70f89667a2cf462a2c8ed5a456e80afba7dacf9dfc6b8e151", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781198211090 - }, - { - "token": "58217cadc5e85afb045d9c0d3dbfa40504916c2a4029b8598b0f1596036e3662", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781198211127 - }, - { - "token": "012f430c03a3e605ef51f4c58dde55bf0b914c37c53e2f5bf123a79c0cd68eb7", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781198383754 - }, - { - "token": "85e084066b269d9b5601f9ee721b45decbafebfc13ea083f27e3161d72777700", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781198491124 - }, - { - "token": "25a5413011b12ba2508f960f6f743ff7c18868544a83479d6b1ecccf20d838a9", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781198723225 - }, - { - "token": "1d0ebe5a9496bd1df9cfc8e7d49f10048b317a4833441bd0effc57c3225efa69", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781198967207 - }, - { - "token": "269e3c40159d2faeb0767216feae6e3e0d88c9d1c8ca9ab9d2497986ff1621b7", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781199074685 - }, - { - "token": "1bc1e7c6c2a505208e3165753de8abd2609c34590375e48ef1abfa587b58e64a", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781199372671 - }, - { - "token": "4dfe860e82bf21f1216ac252fa4ae378dee65914827df3c326b5812c4f69ff93", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781199472733 - }, - { - "token": "f8255e39ecef32f69c94e723ff4c7a06976ebfa4cbe92c8600f9644de97c4d0a", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781199500791 - }, - { - "token": "88000048fc7e0116dcf7101416ff75e119513a2e18ab9e7f87273b070ebbe0e2", - "usuario": "admin", - "rol": "admin", - "createdAt": 1781199793144 + "createdAt": 1781358702492 } ] diff --git a/docker-compose.yml b/docker-compose.yml index 98a78aa..c8768df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: - ./Imagenes:/app/Imagenes - ./pedidos:/app/pedidos - ./frontend:/app/frontend + - ./videos:/app/videos environment: - NODE_ENV=production - PORT=3000 diff --git a/frontend/administrativo/admin.css b/frontend/administrativo/admin.css index 45f561b..4ae1476 100644 --- a/frontend/administrativo/admin.css +++ b/frontend/administrativo/admin.css @@ -2687,6 +2687,42 @@ select { gap: 4px; } +.video-item { + display: flex; + align-items: center; + gap: 14px; + padding: 10px 14px; + background: #fafaff; + border: 1.5px solid #e8e6f0; + border-radius: 10px; + font-size: 0.85rem; +} + +.video-filename { + flex: 1; + color: #4a4a68; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.video-delete { + background: none; + border: none; + color: #b91c1c; + font-size: 1rem; + cursor: pointer; + padding: 4px 6px; + border-radius: 6px; + transition: background 0.2s; + line-height: 1; +} + +.video-delete:hover { + background: #fef2f2; +} + @media (max-width: 600px) { .vendedores-add { flex-direction: column; diff --git a/frontend/administrativo/admin.js b/frontend/administrativo/admin.js index 6330d7e..515f8fb 100644 --- a/frontend/administrativo/admin.js +++ b/frontend/administrativo/admin.js @@ -186,6 +186,7 @@ async function cargarEstadisticas() { document.getElementById('statProductos').textContent = productos.length; document.getElementById('statCategorias').textContent = categorias.length; + cargarRedes(); // carga la sección de videos } catch (err) { console.error('Error cargando estadísticas:', err); } @@ -1858,12 +1859,15 @@ function setupProductosSearch() { // EXCEL/CSV — asignar nombre + precio por referencia // ════════════════════════════════ function setupExcelUpload() { - const input = document.getElementById('excelArchivo'); - const hint = document.getElementById('excelArchivoHint'); - const form = document.getElementById('formExcelUpload'); - const btn = document.getElementById('btnProcesarExcel'); + const input = document.getElementById('excelArchivo'); + const hint = document.getElementById('excelArchivoHint'); + const form = document.getElementById('formExcelUpload'); + const btn = document.getElementById('btnProcesarExcel'); + const selBtn = document.getElementById('btnSeleccionarExcel'); if (!input || !form) return; + selBtn.addEventListener('click', () => input.click()); + input.addEventListener('change', () => { const f = input.files && input.files[0]; if (f) { @@ -2744,3 +2748,106 @@ function initGestores() { // cargarDescargas() ahora usa los gestores // (definido arriba en la sección DESCARGAS) + +// ════════════════════════════════ +// VIDEOS — gestión desde Dashboard +// ════════════════════════════════ +async function cargarRedes() { + await cargarListadoVideos(); + setupVideosForm(); +} + +async function cargarListadoVideos() { + const listEl = document.getElementById('videosList'); + const errEl = document.getElementById('videosError'); + errEl.classList.remove('show'); + try { + const res = await fetch('/api/videos'); + const videos = await res.json(); + if (videos.length === 0) { + listEl.innerHTML = '

No hay videos. Sube el primero.

'; + return; + } + listEl.innerHTML = videos.map(v => ` +
+ + ${escapeHtml(v.filename)} + +
+ `).join(''); + + listEl.querySelectorAll('.video-delete').forEach(btn => { + btn.addEventListener('click', async () => { + if (!confirm('¿Eliminar este video?')) return; + try { + const res = await fetch(`/api/videos/${btn.dataset.filename}`, { + method: 'DELETE', + credentials: 'include' + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || 'Error al eliminar'); + } + await cargarListadoVideos(); + } catch (err) { + errEl.textContent = err.message; + errEl.classList.add('show'); + } + }); + }); + } catch (err) { + listEl.innerHTML = `

Error: ${err.message}

`; + } +} + +function setupVideosForm() { + const btn = document.getElementById('btnSubirVideo'); + const input = document.getElementById('videoFileInput'); + const errEl = document.getElementById('videosError'); + const selBtn = document.getElementById('btnSeleccionarVideo'); + const nameEl = document.getElementById('videoFileName'); + if (!btn || btn.dataset.vidWired) return; + btn.dataset.vidWired = '1'; + + selBtn.addEventListener('click', () => input.click()); + + input.addEventListener('change', () => { + nameEl.textContent = input.files[0] ? input.files[0].name : 'Ningún archivo seleccionado'; + }); + + async function subir() { + const file = input.files[0]; + if (!file) { + errEl.textContent = 'Selecciona un archivo de video'; + errEl.classList.add('show'); + return; + } + errEl.classList.remove('show'); + btn.disabled = true; + btn.textContent = '⏳ Subiendo…'; + try { + const fd = new FormData(); + fd.append('video', file); + const res = await fetch('/api/videos', { + method: 'POST', + credentials: 'include', + body: fd + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || 'Error al subir'); + } + input.value = ''; + nameEl.textContent = 'Ningún archivo seleccionado'; + await cargarListadoVideos(); + } catch (err) { + errEl.textContent = err.message; + errEl.classList.add('show'); + } finally { + btn.disabled = false; + btn.textContent = '+ Subir video'; + } + } + + btn.addEventListener('click', subir); +} diff --git a/frontend/administrativo/index.html b/frontend/administrativo/index.html index d13f390..fb71258 100644 --- a/frontend/administrativo/index.html +++ b/frontend/administrativo/index.html @@ -7,7 +7,7 @@ - + @@ -113,6 +113,25 @@ + + +
+

🎬 Videos

+

Sube hasta 10 videos para que aparezcan en el catálogo público, debajo del botón "Ver catálogo".

+ +
+
+ +
+ + Ningún archivo seleccionado +
+
+ +
+ +
+
@@ -206,8 +225,11 @@
- - Ningún archivo seleccionado +
+ + + Ningún archivo seleccionado +
@@ -741,6 +763,6 @@ - + diff --git a/frontend/app.js b/frontend/app.js index bbfd19c..ee7c274 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -25,6 +25,23 @@ let cargando = true; // ════════════════════════════════ // CARGA DE DATOS // ════════════════════════════════ +async function cargarRedes() { + try { + const res = await fetch('/api/videos'); + const videos = await res.json(); + const grid = document.getElementById('redesGrid'); + if (videos.length === 0) { grid.innerHTML = ''; return; } + + grid.innerHTML = videos.map(v => ` +
+ +
+ `).join(''); + } catch (err) { + console.error('Error cargando videos:', err); + } +} + async function cargarDatos() { try { const [resCats, resProds] = await Promise.all([ @@ -608,6 +625,7 @@ document.addEventListener('DOMContentLoaded', () => { setupNav(); setupCartModals(); cargarDatos(); + cargarRedes(); }); diff --git a/frontend/estilos.css b/frontend/estilos.css index 8f5b452..cc53166 100644 --- a/frontend/estilos.css +++ b/frontend/estilos.css @@ -539,6 +539,64 @@ section { box-shadow: var(--shadow-sm); } +/* ════════════════════════════════ + REDES SOCIALES +════════════════════════════════ */ +.redes-section { + padding: 20px 20px 10px; + max-width: 1200px; + margin: 0 auto; +} + +.redes-grid { + display: flex; + gap: 24px; + overflow-x: auto; + scroll-snap-type: x mandatory; + -webkit-overflow-scrolling: touch; + padding-bottom: 12px; + scrollbar-width: thin; + scrollbar-color: #d4d0e0 transparent; +} + +.redes-grid::-webkit-scrollbar { + height: 6px; +} + +.redes-grid::-webkit-scrollbar-track { + background: transparent; +} + +.redes-grid::-webkit-scrollbar-thumb { + background: #d4d0e0; + border-radius: 99px; +} + +.redes-grid .video-card { + flex: 0 0 auto; + width: 360px; + max-width: 85vw; + scroll-snap-align: start; + border-radius: 12px; + overflow: hidden; + background: #000; + box-shadow: 0 4px 20px rgba(0,0,0,0.12); +} + +.redes-grid .video-card video { + width: 100%; + display: block; + max-height: 600px; + object-fit: contain; + background: #000; +} + +.redes-grid .red-embed .instagram-media { + min-width: 326px !important; + max-width: 100% !important; + width: 100% !important; +} + /* ════════════════════════════════ CONTADOR + PRODUCTOS ════════════════════════════════ */ diff --git a/frontend/index.html b/frontend/index.html index 1bd5724..7ffd982 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9,7 +9,7 @@ - + @@ -55,6 +55,11 @@ + +
+
+
+