VENDEDOR EN PEDIDO Y EN ADMINISTRADOR
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user