576 lines
27 KiB
HTML
576 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="es">
|
||
<head>
|
||
<script>/*pos_theme_early*/(function(){var t=localStorage.getItem("pos_theme")||"industrial";document.documentElement.setAttribute("data-theme",t);})()</script>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Marketplace B2B — Nexus Autoparts POS</title>
|
||
<link rel="stylesheet" href="/pos/static/css/chat.css" />
|
||
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
|
||
<link rel="stylesheet" href="/pos/static/css/common.css" />
|
||
<link rel="stylesheet" href="/pos/static/css/pos-ui.css?v=2" />
|
||
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
|
||
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
|
||
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
|
||
<meta name="theme-color" content="#F5A623" />
|
||
|
||
<link rel="stylesheet" href="/pos/static/css/marketplace.css">
|
||
</head>
|
||
<body>
|
||
|
||
<header class="page-header">
|
||
<div class="page-title">Marketplace B2B</div>
|
||
<div style="display:flex;gap:var(--space-3);align-items:center;">
|
||
<span class="user-role" id="userRoleTag">cargando...</span>
|
||
<a href="/pos/catalog" style="color:var(--color-text-secondary);">← POS</a>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Tab bar — rendered dynamically based on role -->
|
||
<nav class="tab-bar" id="tabBar">
|
||
<!-- buttons injected by JS -->
|
||
</nav>
|
||
|
||
<!-- ══════════ TAB: Browse (buyer) ══════════ -->
|
||
<section class="tab-panel" id="panel-browse">
|
||
<div class="search-row">
|
||
<input type="text" id="browseQuery" placeholder="Buscar parte, marca, numero OEM..." autocomplete="off">
|
||
<input type="text" id="browseCity" placeholder="Ciudad (opcional)" style="max-width:200px;">
|
||
<button onclick="runBrowseSearch()">Buscar</button>
|
||
</div>
|
||
<div id="browseResults" class="results-grid"></div>
|
||
</section>
|
||
|
||
<!-- ══════════ TAB: Mis POs (buyer) ══════════ -->
|
||
<section class="tab-panel" id="panel-mine">
|
||
<h2 style="margin-bottom:var(--space-4);font-family:var(--font-heading);">Mis Pedidos</h2>
|
||
<div id="minePOList"></div>
|
||
</section>
|
||
|
||
<!-- ══════════ TAB: Inbox (seller) ══════════ -->
|
||
<section class="tab-panel" id="panel-inbox">
|
||
<h2 style="margin-bottom:var(--space-4);font-family:var(--font-heading);">Pedidos Recibidos</h2>
|
||
<div id="inboxPOList"></div>
|
||
</section>
|
||
|
||
<!-- ══════════ TAB: Mi Inventario (seller) ══════════ -->
|
||
<section class="tab-panel" id="panel-inventory">
|
||
<h2 style="margin-bottom:var(--space-4);font-family:var(--font-heading);">Mi Inventario</h2>
|
||
<div class="upload-box">
|
||
<label>Cargar inventario via CSV</label>
|
||
<textarea id="csvText" placeholder="part_number,stock,price,name AB-123,5,150.50,Filtro de aceite CD-456,12,89.00,Balata delantera"></textarea>
|
||
<div class="hint">
|
||
Columnas requeridas: <code>part_number, stock, price</code>.
|
||
Opcionales: <code>name, min_order, warehouse_location, currency</code>.
|
||
<br>Si la parte no existe en el catálogo, se crea automáticamente como <em>seller listing</em>.
|
||
</div>
|
||
<button style="margin-top:var(--space-3);padding:var(--space-3) var(--space-6);background:var(--gradient-accent);color:var(--btn-primary-text);border:none;border-radius:var(--radius-md);font-weight:bold;cursor:pointer;" onclick="uploadCSV()">Subir CSV</button>
|
||
<div id="uploadResult" style="margin-top:var(--space-3);font-size:var(--text-body-sm);"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══════════ PO Detail Modal ══════════ -->
|
||
<div class="modal-overlay" id="poModal">
|
||
<div class="modal-content">
|
||
<button class="modal-close" onclick="closeModal('poModal')">✕</button>
|
||
<div class="modal-title" id="poModalTitle">Pedido</div>
|
||
<div id="poModalBody">cargando...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══════════ Part Detail / Create PO Modal ══════════ -->
|
||
<div class="modal-overlay" id="partModal">
|
||
<div class="modal-content">
|
||
<button class="modal-close" onclick="closeModal('partModal')">✕</button>
|
||
<div class="modal-title" id="partModalTitle">Detalle de parte</div>
|
||
<div id="partModalBody">cargando...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
var API = '/pos/api/marketplace';
|
||
var TOKEN = localStorage.getItem('pos_token') || '';
|
||
|
||
// ── Auth headers helper ──
|
||
function headers() {
|
||
return {
|
||
'Authorization': 'Bearer ' + TOKEN,
|
||
'Content-Type': 'application/json',
|
||
'X-Device-Id': localStorage.getItem('pos_device_id') || 'web',
|
||
};
|
||
}
|
||
|
||
function apiFetch(path, opts) {
|
||
opts = opts || {};
|
||
opts.headers = headers();
|
||
return fetch(API + path, opts).then(function (r) {
|
||
if (r.status === 401) {
|
||
window.location.href = '/pos/login';
|
||
return null;
|
||
}
|
||
return r.json();
|
||
});
|
||
}
|
||
|
||
// ── HTML escape ──
|
||
function esc(s) {
|
||
var d = document.createElement('div');
|
||
d.textContent = s || '';
|
||
return d.innerHTML;
|
||
}
|
||
|
||
function fmt(n) {
|
||
return (n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
}
|
||
|
||
// ── Role detection (reads from JWT payload) ──
|
||
// Since we don't expose a /whoami endpoint, try to derive role from
|
||
// the orders/inbox endpoint — if it returns 403, the user is a buyer.
|
||
// Admin users can see everything so default to 'admin' view.
|
||
var userRole = 'buyer'; // default
|
||
function detectRole() {
|
||
// Use the /whoami endpoint to reliably detect the marketplace role.
|
||
return apiFetch('/whoami').then(function (data) {
|
||
if (data && data.marketplace_role) {
|
||
userRole = data.marketplace_role;
|
||
} else {
|
||
userRole = 'buyer';
|
||
}
|
||
document.getElementById('userRoleTag').textContent =
|
||
userRole + (data && data.bodega_id ? ' · bodega #' + data.bodega_id : '');
|
||
renderTabs();
|
||
});
|
||
}
|
||
|
||
// ── Render tab bar based on role ──
|
||
function renderTabs() {
|
||
var tabs;
|
||
if (userRole === 'seller' || userRole === 'admin') {
|
||
tabs = [
|
||
{ id: 'inbox', label: 'Pedidos Recibidos' },
|
||
{ id: 'inventory', label: 'Mi Inventario' },
|
||
{ id: 'browse', label: 'Explorar' },
|
||
];
|
||
} else {
|
||
tabs = [
|
||
{ id: 'browse', label: 'Explorar' },
|
||
{ id: 'mine', label: 'Mis Pedidos' },
|
||
];
|
||
}
|
||
|
||
var bar = document.getElementById('tabBar');
|
||
bar.innerHTML = tabs.map(function (t) {
|
||
return '<button class="tab-btn" data-tab="' + t.id + '" onclick="switchTab(\'' + t.id + '\')">' + t.label + '</button>';
|
||
}).join('');
|
||
|
||
// Open the first tab by default
|
||
switchTab(tabs[0].id);
|
||
}
|
||
|
||
window.switchTab = function (id) {
|
||
document.querySelectorAll('.tab-btn').forEach(function (b) {
|
||
b.classList.toggle('active', b.dataset.tab === id);
|
||
});
|
||
document.querySelectorAll('.tab-panel').forEach(function (p) {
|
||
p.classList.toggle('active', p.id === 'panel-' + id);
|
||
});
|
||
// Lazy-load content per tab
|
||
if (id === 'browse') runBrowseSearch();
|
||
if (id === 'mine') loadMyPOs();
|
||
if (id === 'inbox') loadInbox();
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// BROWSE
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
window.runBrowseSearch = function () {
|
||
var q = document.getElementById('browseQuery').value.trim();
|
||
var city = document.getElementById('browseCity').value.trim();
|
||
var params = [];
|
||
if (q) params.push('q=' + encodeURIComponent(q));
|
||
if (city) params.push('city=' + encodeURIComponent(city));
|
||
var url = '/inventory/search' + (params.length ? '?' + params.join('&') : '');
|
||
|
||
var grid = document.getElementById('browseResults');
|
||
grid.innerHTML = '<div class="empty-state">Buscando...</div>';
|
||
|
||
apiFetch(url).then(function (resp) {
|
||
if (!resp || !resp.data) { grid.innerHTML = '<div class="empty-state"><h3>Error</h3></div>'; return; }
|
||
if (resp.data.length === 0) {
|
||
grid.innerHTML = '<div class="empty-state"><h3>Sin resultados</h3><p>Ninguna bodega tiene esta parte en stock.</p></div>';
|
||
return;
|
||
}
|
||
grid.innerHTML = resp.data.map(function (p) {
|
||
var priceStr = p.min_price === p.max_price
|
||
? '$' + fmt(p.min_price)
|
||
: '$' + fmt(p.min_price) + ' – $' + fmt(p.max_price);
|
||
var isOem = p.listing_type === 'oem';
|
||
var badge = isOem
|
||
? '<span style="background:#3FB95020;color:#3FB950;padding:2px 6px;border-radius:4px;font-size:var(--text-caption);">Catálogo</span>'
|
||
: '<span style="background:#F5A62320;color:#F5A623;padding:2px 6px;border-radius:4px;font-size:var(--text-caption);">Listing</span>';
|
||
var onclick = isOem
|
||
? 'openPartDetail(' + p.id + ')'
|
||
: 'openListingDetail(' + p.id + ')';
|
||
return '<div class="part-card" onclick="' + onclick + '">' +
|
||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-1);">' +
|
||
'<div class="part-card__oem">' + esc(p.part_number) + '</div>' +
|
||
badge +
|
||
'</div>' +
|
||
'<div class="part-card__name">' + esc(p.name) + '</div>' +
|
||
'<div class="part-card__meta">' +
|
||
'<span class="price-range">' + priceStr + '</span>' +
|
||
'<span class="stock-pill">' + esc(p.total_stock_hint) + '</span>' +
|
||
'</div>' +
|
||
'<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-top:var(--space-2);">' +
|
||
p.bodega_count + ' bodega' + (p.bodega_count !== 1 ? 's' : '') +
|
||
'</div>' +
|
||
'</div>';
|
||
}).join('');
|
||
});
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// PART DETAIL + CREATE PO
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
var cart = []; // {part_id, bodega_id, part_name, oem, quantity, unit_price}
|
||
|
||
window.openPartDetail = function (partId) {
|
||
var modal = document.getElementById('partModal');
|
||
modal.classList.add('open');
|
||
document.getElementById('partModalBody').innerHTML = 'Cargando bodegas...';
|
||
|
||
apiFetch('/inventory/part/' + partId).then(function (resp) {
|
||
if (!resp || !resp.data) return;
|
||
var bodegas = resp.data;
|
||
if (bodegas.length === 0) {
|
||
document.getElementById('partModalBody').innerHTML = '<div class="empty-state">Ninguna bodega tiene esta parte en stock.</div>';
|
||
return;
|
||
}
|
||
var html = '<p style="color:var(--color-text-muted);margin-bottom:var(--space-3);">Elige una bodega para ordenar:</p>';
|
||
html += '<div style="display:flex;flex-direction:column;gap:var(--space-2);">';
|
||
bodegas.forEach(function (b) {
|
||
html += '<div style="padding:var(--space-3);border:1px solid var(--glass-border);border-radius:var(--radius-md);display:flex;justify-content:space-between;align-items:center;">' +
|
||
'<div>' +
|
||
'<strong>' + esc(b.name) + '</strong>' +
|
||
'<div style="color:var(--color-text-muted);font-size:var(--text-caption);">' + esc(b.city || '') + ' · ' + esc(b.stock_hint) + '</div>' +
|
||
'</div>' +
|
||
'<div style="text-align:right;">' +
|
||
'<div class="price-range">$' + fmt(b.price) + '</div>' +
|
||
'<button style="margin-top:var(--space-1);padding:var(--space-1) var(--space-3);background:var(--gradient-accent);color:var(--btn-primary-text);border:none;border-radius:var(--radius-sm);font-size:var(--text-caption);cursor:pointer;" onclick="createOrderFor(' + partId + ', null, ' + b.id_bodega + ', \'' + esc(b.name).replace(/\'/g, '\\\'') + '\', ' + (b.price || 0) + ')">Ordenar</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
});
|
||
html += '</div>';
|
||
document.getElementById('partModalBody').innerHTML = html;
|
||
});
|
||
};
|
||
|
||
window.openListingDetail = function (wiId) {
|
||
var modal = document.getElementById('partModal');
|
||
modal.classList.add('open');
|
||
document.getElementById('partModalBody').innerHTML = 'Cargando bodegas...';
|
||
|
||
apiFetch('/inventory/listing/' + wiId).then(function (resp) {
|
||
if (!resp || !resp.data) return;
|
||
var bodegas = resp.data;
|
||
if (bodegas.length === 0) {
|
||
document.getElementById('partModalBody').innerHTML = '<div class="empty-state">Ninguna bodega tiene esta parte en stock.</div>';
|
||
return;
|
||
}
|
||
var html = '<p style="color:var(--color-text-muted);margin-bottom:var(--space-3);">Elige una bodega para ordenar:</p>';
|
||
html += '<div style="display:flex;flex-direction:column;gap:var(--space-2);">';
|
||
bodegas.forEach(function (b) {
|
||
html += '<div style="padding:var(--space-3);border:1px solid var(--glass-border);border-radius:var(--radius-md);display:flex;justify-content:space-between;align-items:center;">' +
|
||
'<div>' +
|
||
'<strong>' + esc(b.name) + '</strong>' +
|
||
'<div style="color:var(--color-text-muted);font-size:var(--text-caption);">' + esc(b.city || '') + ' · ' + esc(b.stock_hint) + '</div>' +
|
||
'</div>' +
|
||
'<div style="text-align:right;">' +
|
||
'<div class="price-range">$' + fmt(b.price) + '</div>' +
|
||
'<button style="margin-top:var(--space-1);padding:var(--space-1) var(--space-3);background:var(--gradient-accent);color:var(--btn-primary-text);border:none;border-radius:var(--radius-sm);font-size:var(--text-caption);cursor:pointer;" onclick="createOrderFor(null, ' + wiId + ', ' + b.id_bodega + ', \'' + esc(b.name).replace(/\'/g, '\\\'') + '\', ' + (b.price || 0) + ')">Ordenar</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
});
|
||
html += '</div>';
|
||
document.getElementById('partModalBody').innerHTML = html;
|
||
});
|
||
};
|
||
|
||
window.createOrderFor = function (partId, wiId, bodegaId, bodegaName, price) {
|
||
var qty = prompt('Cantidad a ordenar para "' + bodegaName + '":', '1');
|
||
if (!qty) return;
|
||
qty = parseInt(qty);
|
||
if (qty < 1) return;
|
||
|
||
var notes = prompt('Notas para la bodega (opcional):', '') || '';
|
||
|
||
var item = { quantity: qty, unit_price: price };
|
||
if (partId) {
|
||
item.part_id = partId;
|
||
} else if (wiId) {
|
||
item.wi_id = wiId;
|
||
} else {
|
||
alert('Error: se requiere part_id o wi_id');
|
||
return;
|
||
}
|
||
|
||
// Create PO draft
|
||
apiFetch('/orders', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
bodega_id: bodegaId,
|
||
items: [item],
|
||
delivery_method: 'pickup',
|
||
buyer_notes: notes,
|
||
}),
|
||
}).then(function (resp) {
|
||
if (!resp || resp.error) {
|
||
alert('Error creando pedido: ' + (resp && resp.error || 'desconocido'));
|
||
return;
|
||
}
|
||
// Auto-submit immediately for now
|
||
apiFetch('/orders/' + resp.po_id + '/transition', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ new_status: 'submitted' }),
|
||
}).then(function (tr) {
|
||
if (tr && tr.ok) {
|
||
alert('✓ Pedido #' + resp.po_id + ' enviado a ' + bodegaName);
|
||
closeModal('partModal');
|
||
if (userRole === 'buyer') switchTab('mine');
|
||
} else {
|
||
alert('Pedido creado #' + resp.po_id + ' pero no se pudo enviar: ' + (tr && tr.error || ''));
|
||
}
|
||
});
|
||
});
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// MIS POS (buyer)
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
window.loadMyPOs = function () {
|
||
var container = document.getElementById('minePOList');
|
||
container.innerHTML = 'Cargando...';
|
||
apiFetch('/orders/mine?only_mine=false').then(function (resp) {
|
||
renderPOList(container, resp, 'Aun no has creado pedidos.');
|
||
});
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// INBOX (seller)
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
window.loadInbox = function () {
|
||
var container = document.getElementById('inboxPOList');
|
||
container.innerHTML = 'Cargando...';
|
||
apiFetch('/orders/inbox').then(function (resp) {
|
||
renderPOList(container, resp, 'Aun no hay pedidos recibidos.');
|
||
});
|
||
};
|
||
|
||
function renderPOList(container, resp, emptyMsg) {
|
||
if (!resp || resp.error || !resp.data) {
|
||
container.innerHTML = '<div class="empty-state"><h3>Error</h3><p>' + esc(resp && resp.error || 'unknown') + '</p></div>';
|
||
return;
|
||
}
|
||
if (resp.data.length === 0) {
|
||
container.innerHTML = '<div class="empty-state"><h3>Sin pedidos</h3><p>' + emptyMsg + '</p></div>';
|
||
return;
|
||
}
|
||
var html = '<table class="po-table"><thead><tr>' +
|
||
'<th>#</th>' +
|
||
'<th>Estado</th>' +
|
||
(userRole === 'seller' ? '<th>Comprador</th>' : '<th>Bodega</th>') +
|
||
'<th>Items</th>' +
|
||
'<th>Total</th>' +
|
||
'<th>Creado</th>' +
|
||
'</tr></thead><tbody>';
|
||
resp.data.forEach(function (po) {
|
||
var when = po.submitted_at ? new Date(po.submitted_at).toLocaleDateString('es-MX') : '—';
|
||
html += '<tr onclick="openPODetail(' + po.id_po + ')">' +
|
||
'<td><strong>#' + po.id_po + '</strong></td>' +
|
||
'<td><span class="status-badge status-' + po.status + '">' + po.status + '</span></td>' +
|
||
'<td>' + esc(po.bodega_name || po.buyer_display_name || '—') + '</td>' +
|
||
'<td>' + po.item_count + '</td>' +
|
||
'<td>$' + fmt(po.total_amount) + '</td>' +
|
||
'<td>' + when + '</td>' +
|
||
'</tr>';
|
||
});
|
||
html += '</tbody></table>';
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// PO DETAIL MODAL
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
window.openPODetail = function (poId) {
|
||
var modal = document.getElementById('poModal');
|
||
modal.classList.add('open');
|
||
document.getElementById('poModalTitle').textContent = 'Pedido #' + poId;
|
||
document.getElementById('poModalBody').innerHTML = 'Cargando...';
|
||
|
||
apiFetch('/orders/' + poId).then(function (po) {
|
||
if (!po || po.error) {
|
||
document.getElementById('poModalBody').innerHTML = '<div>Error: ' + esc(po && po.error || '') + '</div>';
|
||
return;
|
||
}
|
||
var html = '<div style="display:flex;justify-content:space-between;margin-bottom:var(--space-3);">' +
|
||
'<div>' +
|
||
'<div style="font-size:var(--text-caption);color:var(--color-text-muted);">COMPRADOR</div>' +
|
||
'<div>' + esc(po.buyer_display_name) + '</div>' +
|
||
'</div>' +
|
||
'<div>' +
|
||
'<div style="font-size:var(--text-caption);color:var(--color-text-muted);">BODEGA</div>' +
|
||
'<div>' + esc(po.bodega_name) + '</div>' +
|
||
'</div>' +
|
||
'<div>' +
|
||
'<div style="font-size:var(--text-caption);color:var(--color-text-muted);">ESTADO</div>' +
|
||
'<span class="status-badge status-' + po.status + '">' + po.status + '</span>' +
|
||
'</div>' +
|
||
'</div>';
|
||
|
||
html += '<h4 style="margin:var(--space-4) 0 var(--space-2);">Items</h4>';
|
||
html += '<table class="po-table"><thead><tr><th>OEM</th><th>Nombre</th><th>Cant</th><th>Precio</th><th>Subtotal</th></tr></thead><tbody>';
|
||
po.items.forEach(function (it) {
|
||
html += '<tr>' +
|
||
'<td style="font-family:var(--font-mono);">' + esc(it.oem_part_number) + '</td>' +
|
||
'<td>' + esc(it.part_name) + '</td>' +
|
||
'<td>' + it.quantity + '</td>' +
|
||
'<td>$' + fmt(it.unit_price) + '</td>' +
|
||
'<td>$' + fmt(it.subtotal) + '</td>' +
|
||
'</tr>';
|
||
});
|
||
html += '</tbody></table>';
|
||
|
||
html += '<div style="text-align:right;font-size:var(--text-h5);margin-top:var(--space-3);"><strong>Total: $' + fmt(po.total_amount) + ' ' + po.currency + '</strong></div>';
|
||
|
||
// Transition buttons based on current status + role
|
||
var actions = getActionsForStatus(po.status, userRole);
|
||
if (actions.length) {
|
||
html += '<div style="margin-top:var(--space-4);padding-top:var(--space-4);border-top:1px solid var(--glass-border);display:flex;gap:var(--space-2);">';
|
||
actions.forEach(function (a) {
|
||
html += '<button onclick="transitionPO(' + po.id_po + ', \'' + a.status + '\')" ' +
|
||
'style="padding:var(--space-2) var(--space-4);background:' + a.color + ';color:#fff;border:none;border-radius:var(--radius-md);font-weight:bold;cursor:pointer;">' +
|
||
a.label + '</button>';
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
// Status history
|
||
if (po.history && po.history.length) {
|
||
html += '<h4 style="margin:var(--space-4) 0 var(--space-2);">Historial</h4>';
|
||
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);">';
|
||
po.history.forEach(function (h) {
|
||
html += '<div>' + new Date(h.at).toLocaleString('es-MX') + ' — ' +
|
||
(h.from_status ? h.from_status + ' → ' : '') + h.to_status +
|
||
' (' + h.actor_kind + ')' +
|
||
(h.note ? ' · ' + esc(h.note) : '') +
|
||
'</div>';
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
document.getElementById('poModalBody').innerHTML = html;
|
||
});
|
||
};
|
||
|
||
function getActionsForStatus(status, role) {
|
||
// Map: {role: {status: [{status, label, color}]}}
|
||
var actionMap = {
|
||
buyer: {
|
||
ready: [{ status: 'delivered', label: 'Marcar recibido', color: '#3FB950' }],
|
||
delivered: [{ status: 'closed', label: 'Cerrar pedido', color: '#888' }],
|
||
},
|
||
seller: {
|
||
submitted: [
|
||
{ status: 'confirmed', label: 'Confirmar', color: '#00D4FF' },
|
||
{ status: 'rejected', label: 'Rechazar', color: '#F85149' },
|
||
],
|
||
confirmed: [{ status: 'ready', label: 'Marcar listo', color: '#A78BFA' }],
|
||
ready: [{ status: 'delivered', label: 'Marcar entregado', color: '#3FB950' }],
|
||
},
|
||
};
|
||
return (actionMap[role] || {})[status] || [];
|
||
}
|
||
|
||
window.transitionPO = function (poId, newStatus) {
|
||
var note = prompt('Nota opcional:', '') || '';
|
||
apiFetch('/orders/' + poId + '/transition', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ new_status: newStatus, note: note }),
|
||
}).then(function (r) {
|
||
if (r && r.ok) {
|
||
openPODetail(poId);
|
||
} else {
|
||
alert('Error: ' + (r && r.error || 'unknown'));
|
||
}
|
||
});
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
// CSV UPLOAD (seller)
|
||
// ═══════════════════════════════════════════════════════════════════
|
||
|
||
window.uploadCSV = function () {
|
||
var csv = document.getElementById('csvText').value;
|
||
if (!csv.trim()) { alert('Pega un CSV primero'); return; }
|
||
|
||
var resultDiv = document.getElementById('uploadResult');
|
||
resultDiv.innerHTML = 'Subiendo...';
|
||
|
||
apiFetch('/inventory/upload', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ csv: csv }),
|
||
}).then(function (r) {
|
||
if (!r || r.error) {
|
||
resultDiv.innerHTML = '<span style="color:#F85149;">Error: ' + esc(r && r.error || 'unknown') + '</span>';
|
||
return;
|
||
}
|
||
var msg = '<span style="color:#3FB950;">✓ ' + r.inserted + ' nuevos, ' +
|
||
r.updated + ' actualizados';
|
||
if (r.skipped > 0) msg += ', ' + r.skipped + ' omitidos';
|
||
msg += '</span>';
|
||
if (r.oem_count !== undefined || r.seller_count !== undefined) {
|
||
msg += '<div style="margin-top:var(--space-2);font-size:var(--text-caption);color:var(--color-text-muted);">' +
|
||
(r.oem_count || 0) + ' catálogo OEM · ' + (r.seller_count || 0) + ' seller listings</div>';
|
||
}
|
||
if (r.errors && r.errors.length) {
|
||
msg += '<div style="margin-top:var(--space-2);color:var(--color-text-muted);font-size:var(--text-caption);">';
|
||
r.errors.slice(0, 5).forEach(function (e) {
|
||
msg += '<div>• ' + esc(e) + '</div>';
|
||
});
|
||
if (r.total_errors > 5) msg += '<div>(' + (r.total_errors - 5) + ' errores mas)</div>';
|
||
msg += '</div>';
|
||
}
|
||
resultDiv.innerHTML = msg;
|
||
});
|
||
};
|
||
|
||
// ── Generic modal close ──
|
||
window.closeModal = function (id) {
|
||
document.getElementById(id).classList.remove('open');
|
||
};
|
||
// Close modal on outside click
|
||
document.querySelectorAll('.modal-overlay').forEach(function (m) {
|
||
m.addEventListener('click', function (e) {
|
||
if (e.target === m) m.classList.remove('open');
|
||
});
|
||
});
|
||
|
||
// ── Boot ──
|
||
if (!TOKEN) {
|
||
window.location.href = '/pos/login';
|
||
} else {
|
||
detectRole();
|
||
}
|
||
})();
|
||
</script>
|
||
<script src="/pos/static/js/chat.js" defer></script>
|
||
</body>
|
||
</html>
|