VENDEDOR EN PEDIDO Y EN ADMINISTRADOR

This commit is contained in:
2026-06-11 14:08:03 -05:00
parent ad3911d3c3
commit e4588b75db
14 changed files with 758 additions and 187 deletions
+177
View File
@@ -2522,3 +2522,180 @@ select {
min-width: 0;
}
}
/* ════════════════════════════════
PEDIDOS VIEW
════════════════════════════════ */
.view-section-group {
display: flex;
flex-direction: column;
gap: 20px;
}
.view-section-title {
font-size: 1rem;
font-weight: 700;
color: #1a1a2e;
margin: 0 0 4px;
}
.view-section-desc {
font-size: 0.82rem;
color: #8b8ba7;
margin: 0 0 16px;
}
/* Vendedores */
.vendedores-list {
margin-bottom: 12px;
}
.vendedor-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #f8f7fc;
border-radius: 8px;
margin-bottom: 6px;
}
.vendedor-nombre {
font-size: 0.9rem;
font-weight: 600;
color: #1a1a2e;
}
.vendedor-remove {
background: none;
border: none;
color: #ef4444;
font-size: 0.9rem;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.15s;
}
.vendedor-remove:hover {
background: #fef2f2;
}
.vendedores-add {
display: flex;
gap: 8px;
}
/* Filtro */
.pedidos-filter-bar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.pedidos-filter-bar label {
font-size: 0.85rem;
font-weight: 600;
color: #4a4a68;
white-space: nowrap;
}
/* Tabla de pedidos */
.pedidos-table-wrap {
overflow-x: auto;
}
.pedidos-table {
width: 100%;
border-collapse: collapse;
font-size: 0.82rem;
}
.pedidos-table th {
text-align: left;
padding: 10px 12px;
font-weight: 700;
color: #4a4a68;
border-bottom: 2px solid #e8e6f0;
white-space: nowrap;
}
.pedidos-table td {
padding: 10px 12px;
border-bottom: 1px solid #e8e6f0;
color: #1a1a2e;
}
.pedidos-table tbody tr:hover {
background: #f8f7fc;
}
.pedidos-total {
font-weight: 700;
color: #8b5cf6;
white-space: nowrap;
}
.pedidos-actions-col {
text-align: left;
width: 200px;
}
.pedidos-table td:last-child,
.pedidos-table th:last-child {
white-space: nowrap;
}
.pedidos-actions-group {
display: inline-flex;
align-items: center;
gap: 8px;
}
.pedido-delete {
background: none;
border: 1.5px solid #e0dce8;
color: #b91c1c;
padding: 6px 10px;
border-radius: 8px;
font-size: 0.75rem;
font-family: inherit;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
line-height: 1;
}
.pedido-delete:hover {
background: #fef2f2;
border-color: #b91c1c;
}
.pedidos-empty {
text-align: center;
color: #8b8ba7;
padding: 24px 0;
font-size: 0.85rem;
}
.btn-sm {
padding: 6px 10px;
font-size: 0.75rem;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
}
@media (max-width: 600px) {
.vendedores-add {
flex-direction: column;
}
.pedidos-filter-bar {
flex-direction: column;
align-items: stretch;
}
.pedidos-filter-bar .form-group {
margin-bottom: 0;
}
}
+203 -1
View File
@@ -12,7 +12,8 @@ const TABS = [
{ id: 'agotados', label: 'Agotados', icon: '🚫' },
{ id: 'descargas', label: 'Descargas', icon: '⬇️' },
{ id: 'categorias', label: 'Categorías', icon: '🗂️' },
{ id: 'usuarios', label: 'Usuarios', icon: '👥' }
{ id: 'usuarios', label: 'Usuarios', icon: '👥' },
{ id: 'pedidos', label: 'Pedidos', icon: '📋' }
];
const ROLES = {
@@ -166,6 +167,10 @@ function cambiarVista(view) {
if (view === 'descargas') {
cargarDescargas();
}
if (view === 'pedidos') {
cargarPedidos();
}
}
async function cargarEstadisticas() {
@@ -2163,6 +2168,203 @@ document.addEventListener('DOMContentLoaded', () => {
if (btn) btn.addEventListener('click', confirmarEliminarProductoDef);
});
// ════════════════════════════════
// PEDIDOS — vendedores + listado de pedidos
// ════════════════════════════════
async function cargarPedidos() {
console.log('[pedidos] cargando…');
setupVendedores();
setupPedidosFiltro();
try { await actualizarSelectVendedores(); } catch (e) { console.error('[pedidos] select error:', e); }
await Promise.all([
cargarVendedores().catch(e => console.error('[pedidos] vendedores error:', e)),
cargarListadoPedidos().catch(e => console.error('[pedidos] listado error:', e))
]);
}
// ─── VENDEDORES ───
async function cargarVendedores() {
const listEl = document.getElementById('vendedoresList');
const errEl = document.getElementById('vendedorError');
errEl.classList.remove('show');
try {
const res = await fetch('/api/vendedores', { credentials: 'include' });
if (!res.ok) throw new Error('Error al cargar vendedores');
const vendedores = await res.json();
if (vendedores.length === 0) {
listEl.innerHTML = '<p class="pedidos-empty" style="text-align:left;padding:8px 0">No hay vendedores. Agrega el primero.</p>';
} else {
listEl.innerHTML = vendedores.map(v => `
<div class="vendedor-item" data-id="${v.id}">
<span class="vendedor-nombre">${escapeHtml(v.nombre)}</span>
<button class="vendedor-remove" data-id="${v.id}" title="Eliminar vendedor">✕</button>
</div>
`).join('');
}
// Wire delete
listEl.querySelectorAll('.vendedor-remove').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.dataset.id;
try {
const delRes = await fetch(`/api/vendedores/${id}`, {
method: 'DELETE',
credentials: 'include'
});
if (!delRes.ok) throw new Error('Error al eliminar');
await cargarVendedores();
actualizarSelectVendedores();
} catch (err) {
errEl.textContent = err.message;
errEl.classList.add('show');
}
});
});
} catch (err) {
listEl.innerHTML = `<p class="pedidos-empty" style="text-align:left;padding:8px 0">Error: ${err.message}</p>`;
}
}
async function actualizarSelectVendedores() {
// Actualiza el select del modal público y el filtro de pedidos
try {
const res = await fetch('/api/vendedores');
const vendedores = await res.json();
const selects = ['pedidosFilterVendedor'];
selects.forEach(id => {
const sel = document.getElementById(id);
if (!sel) return;
const actual = sel.value;
sel.innerHTML = '<option value="">Todos los vendedores</option>' +
vendedores.map(v => `<option value="${escapeHtml(v.nombre)}">${escapeHtml(v.nombre)}</option>`).join('');
if (actual) sel.value = actual;
});
} catch {}
}
function setupVendedores() {
const btn = document.getElementById('btnAgregarVendedor');
const input = document.getElementById('vendedorInput');
const errEl = document.getElementById('vendedorError');
if (!btn || btn.dataset.vendWired) return;
btn.dataset.vendWired = '1';
async function agregar() {
const nombre = input.value.trim();
if (!nombre) {
errEl.textContent = 'Escribe un nombre de vendedor';
errEl.classList.add('show');
return;
}
errEl.classList.remove('show');
try {
const res = await fetch('/api/vendedores', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ nombre })
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || 'Error al agregar');
}
input.value = '';
await cargarVendedores();
await actualizarSelectVendedores();
} catch (err) {
errEl.textContent = err.message;
errEl.classList.add('show');
}
}
btn.addEventListener('click', agregar);
input.addEventListener('keydown', e => { if (e.key === 'Enter') agregar(); });
}
// ─── LISTADO DE PEDIDOS ───
async function cargarListadoPedidos() {
const tbody = document.getElementById('pedidosListBody');
const filter = document.getElementById('pedidosFilterVendedor');
const vendedor = filter ? filter.value : '';
try {
let url = '/api/pedidos';
if (vendedor) url += `?vendedor=${encodeURIComponent(vendedor)}`;
const res = await fetch(url, { credentials: 'include' });
if (!res.ok) {
tbody.innerHTML = '<tr><td colspan="6" class="pedidos-empty">No autorizado</td></tr>';
return;
}
const pedidos = await res.json();
// Ordenar por fecha descendente
pedidos.sort((a, b) => new Date(b.fecha) - new Date(a.fecha));
if (pedidos.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="pedidos-empty">No hay pedidos aún.</td></tr>';
return;
}
const renderedRows = pedidos.map(p => {
const fecha = new Date(p.fecha).toLocaleDateString('es-CO', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
});
const itemsCount = Array.isArray(p.items) ? p.items.reduce((s, i) => s + (i.cantidad || 1), 0) : 0;
const total = p.total || 0;
const clienteNombre = p.cliente ? p.cliente.nombre : '—';
const vendedorNombre = p.vendedor || '—';
return `<tr>
<td>${escapeHtml(fecha)}</td>
<td>${escapeHtml(clienteNombre)}</td>
<td>${escapeHtml(vendedorNombre)}</td>
<td>${itemsCount}</td>
<td class="pedidos-total">$${total.toLocaleString('es-CO')}</td>
<td>
<span class="pedidos-actions-group">
<a href="/pedidos/${p.filename}" class="btn btn-ghost btn-sm" target="_blank" download>📄 Ver PDF</a>
<button type="button" class="btn btn-danger btn-sm pedido-delete" data-filename="${p.filename}" title="Eliminar pedido">Eliminar</button>
</span>
</td>
</tr>`;
});
tbody.innerHTML = renderedRows.join('');
// Wire delete buttons
tbody.querySelectorAll('.pedido-delete').forEach(btn => {
btn.addEventListener('click', async () => {
const filename = btn.dataset.filename;
if (!confirm(`¿Eliminar el pedido "${filename}"? Esta acción no se puede deshacer.`)) return;
try {
const res = await fetch(`/api/pedidos/${encodeURIComponent(filename)}`, {
method: 'DELETE',
credentials: 'include'
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || 'Error al eliminar');
}
await cargarListadoPedidos();
} catch (err) {
alert('Error: ' + err.message);
}
});
});
} catch (err) {
tbody.innerHTML = `<tr><td colspan="6" class="pedidos-empty">Error: ${err.message}</td></tr>`;
}
}
function setupPedidosFiltro() {
const filter = document.getElementById('pedidosFilterVendedor');
if (!filter || filter.dataset.pedWired) return;
filter.dataset.pedWired = '1';
filter.addEventListener('change', cargarListadoPedidos);
}
// ════════════════════════════════
// DESCARGAS — gestor genérico de árbol de selección
// Usado por los subtabs PDF y Fotos.
+66 -2
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<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 rel="stylesheet" href="admin.css?v=17">
<link rel="stylesheet" href="admin.css?v=22">
</head>
<body>
@@ -85,6 +85,10 @@
<span class="nav-icon">👥</span>
<span class="nav-label">Usuarios</span>
</button>
<button type="button" class="nav-item" data-view="pedidos">
<span class="nav-icon">📋</span>
<span class="nav-label">Pedidos</span>
</button>
</div>
</nav>
@@ -425,6 +429,66 @@
</div>
</section>
<!-- PEDIDOS VIEW -->
<section class="view" data-view="pedidos">
<header class="view-header view-header-with-action">
<div>
<h1 class="view-title">📋 Pedidos</h1>
<p class="view-subtitle">Gestiona los vendedores y revisa los pedidos generados.</p>
</div>
</header>
<div class="view-section-group">
<!-- Gestión de vendedores -->
<div class="view-content-card">
<h3 class="view-section-title">Vendedores</h3>
<p class="view-section-desc">Agrega o quita los vendedores que aparecerán en el formulario de pedido público.</p>
<div class="vendedores-list" id="vendedoresList">
<p class="pdf-selector-loading">Cargando vendedores…</p>
</div>
<div class="vendedores-add">
<div class="form-group" style="flex:1;margin-bottom:0">
<input type="text" id="vendedorInput" placeholder="Nombre del vendedor" maxlength="60">
</div>
<button type="button" class="btn btn-primary" id="btnAgregarVendedor">+ Agregar</button>
</div>
<div class="form-error" id="vendedorError" role="alert"></div>
</div>
<!-- Listado de pedidos -->
<div class="view-content-card">
<div class="pedidos-filter-bar">
<label for="pedidosFilterVendedor">Filtrar por vendedor:</label>
<div class="form-group" style="flex:1;margin-bottom:0">
<select id="pedidosFilterVendedor">
<option value="">Todos los vendedores</option>
</select>
</div>
</div>
<div class="pedidos-table-wrap">
<table class="pedidos-table">
<thead>
<tr>
<th>Fecha</th>
<th>Cliente</th>
<th>Vendedor</th>
<th>Productos</th>
<th>Total</th>
<th class="pedidos-actions-col">Acción</th>
</tr>
</thead>
<tbody id="pedidosListBody">
<tr><td colspan="6" class="pedidos-empty">No hay pedidos aún.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
</main>
<!-- BOTÓN FIJO: Cerrar sesión (esquina superior derecha) -->
@@ -677,6 +741,6 @@
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="admin.js?v=19"></script>
<script src="admin.js?v=23"></script>
</body>
</html>