Files
Autoparts-DB/pos/templates/storefront.html
consultoria-as a386ca5b4a
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled
Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos
- F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado
- F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment
- F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings)
- F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant
- Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas
- Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant)
- Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant
- Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
2026-10-12 00:57:31 +00:00

216 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ business_name }} — Refacciones en línea</title>
<meta name="description" content="Compra refacciones y accesorios con stock real. Pedidos en línea con confirmación inmediata.">
<style>
:root { --brand:#e11d48; --bg:#f6f7f9; --card:#fff; --border:#e5e7eb; --text:#111827; --muted:#6b7280; }
* { box-sizing: border-box; }
body { margin:0; font-family: system-ui, -apple-system, sans-serif; background:var(--bg); color:var(--text); }
header { background:var(--card); border-bottom:1px solid var(--border); padding:14px 20px; display:flex; align-items:center; gap:14px; position:sticky; top:0; z-index:5; }
header .logo { font-weight:800; font-size:1.15rem; color:var(--brand); }
header .search { flex:1; max-width:560px; display:flex; gap:8px; }
header input { flex:1; padding:10px 14px; border:1px solid var(--border); border-radius:10px; font-size:1rem; }
button { cursor:pointer; border:none; border-radius:10px; padding:10px 16px; font-size:0.95rem; font-weight:600; }
.btn-brand { background:var(--brand); color:#fff; }
.btn-ghost { background:transparent; border:1px solid var(--border); }
main { max-width:1000px; margin:0 auto; padding:20px; }
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:14px; }
.card { background:var(--card); border:1px solid var(--border); border-radius:14px; overflow:hidden; display:flex; flex-direction:column; }
.card img { width:100%; height:150px; object-fit:cover; background:#f0f0f0; }
.card .body { padding:12px; flex:1; display:flex; flex-direction:column; gap:6px; }
.name { font-size:0.92rem; font-weight:600; line-height:1.3; }
.sku { font-family:ui-monospace,monospace; font-size:0.75rem; color:var(--muted); }
.price { font-size:1.1rem; font-weight:800; color:var(--brand); }
.stock { font-size:0.75rem; }
.in { color:#059669; } .out { color:#dc2626; }
.card button { margin-top:auto; background:var(--brand); color:#fff; }
.card button:disabled { background:#d1d5db; cursor:not-allowed; }
.empty { text-align:center; color:var(--muted); padding:40px 0; }
/* carrito flotante */
#cartBtn { position:fixed; bottom:20px; right:20px; background:var(--brand); color:#fff; border-radius:999px; padding:14px 20px; font-size:1rem; box-shadow:0 8px 24px rgba(0,0,0,.18); }
#cartCount { background:#fff; color:var(--brand); border-radius:999px; padding:1px 8px; font-weight:800; margin-left:6px; }
.overlay { position:fixed; inset:0; background:rgba(0,0,0,.45); display:none; align-items:center; justify-content:center; padding:16px; z-index:10; }
.overlay.open { display:flex; }
.modal { background:#fff; border-radius:16px; max-width:460px; width:100%; max-height:88vh; overflow:auto; padding:20px; }
.modal h3 { margin:0 0 12px; }
.line { display:flex; justify-content:space-between; gap:8px; padding:8px 0; border-bottom:1px solid var(--border); font-size:0.9rem; }
.field { margin:10px 0; }
.field label { display:block; font-size:0.8rem; color:var(--muted); margin-bottom:4px; }
.field input, .field textarea { width:100%; padding:10px; border:1px solid var(--border); border-radius:10px; font-size:0.95rem; }
.total { font-size:1.15rem; font-weight:800; text-align:right; margin:10px 0; }
.ok { text-align:center; padding:20px 0; }
.ok .big { font-size:3rem; }
</style>
</head>
<body>
<header>
<div class="logo">{{ business_name }}</div>
<div class="search">
<input id="q" type="search" placeholder="Buscar refacción, No. de parte..." />
<button class="btn-brand" onclick="doSearch(1)">Buscar</button>
</div>
<button class="btn-ghost" onclick="openCart()">🛒 <span id="cartQty">0</span></button>
</header>
<main>
<div class="grid" id="results"><div class="empty">Escribe para buscar refacciones...</div></div>
<div style="text-align:center;margin:18px 0;">
<button class="btn-ghost" id="moreBtn" style="display:none;" onclick="doSearch(page+1)">Ver más</button>
</div>
</main>
<button id="cartBtn" onclick="openCart()">🛒 Carrito <span id="cartQty2">0</span></button>
<div class="overlay" id="cartOverlay">
<div class="modal">
<h3>Tu pedido</h3>
<div id="cartLines"></div>
<div class="total" id="cartTotal"></div>
<div class="field"><label>Nombre *</label><input id="cName" /></div>
<div class="field"><label>Teléfono (WhatsApp) *</label><input id="cPhone" type="tel" placeholder="10 dígitos" /></div>
<div class="field"><label>Notas</label><textarea id="cNotes" rows="2"></textarea></div>
<div class="field"><label style="display:flex;gap:8px;align-items:center;"><input type="checkbox" id="cDelivery" /> Requiero envío a domicilio</label></div>
<div id="orderMsg" style="font-size:0.9rem;"></div>
<div style="display:flex;gap:10px;margin-top:12px;">
<button class="btn-ghost" style="flex:1;" onclick="closeCart()">Seguir comprando</button>
<button class="btn-brand" style="flex:2;" id="orderBtn" onclick="submitOrder()">Hacer pedido</button>
</div>
</div>
</div>
<script>
var BUSINESS = "{{ business_name }}";
var cart = [];
try { cart = JSON.parse(localStorage.getItem('store_cart') || '[]'); } catch(e) {}
var page = 0, lastQ = '';
function saveCart() {
localStorage.setItem('store_cart', JSON.stringify(cart));
var n = cart.reduce(function (a, l) { return a + l.quantity; }, 0);
document.getElementById('cartQty').textContent = n;
document.getElementById('cartQty2').textContent = n;
}
function renderResults(items, append) {
var el = document.getElementById('results');
var html = items.map(function (it) {
var hasStock = it.stock > 0;
return '<div class="card">'
+ (it.image_url ? '<img src="' + escAttr(it.image_url) + '" loading="lazy" onerror="this.style.display=\'none\'">' : '')
+ '<div class="body">'
+ '<div class="name">' + esc(it.name) + '</div>'
+ '<div class="sku">' + esc(it.part_number) + (it.brand ? ' · ' + esc(it.brand) : '') + '</div>'
+ '<div class="price">' + money(it.price) + '</div>'
+ '<div class="stock ' + (hasStock ? 'in' : 'out') + '">' + (hasStock ? 'Disponible (' + it.stock + ')' : 'Sin stock') + '</div>'
+ '<button ' + (hasStock ? '' : 'disabled') + ' onclick="addToCart(' + it.id + ')">Agregar</button>'
+ '</div></div>';
}).join('');
if (append) el.innerHTML += html; else el.innerHTML = html || '<div class="empty">Sin resultados</div>';
}
function money(n) { return '$' + (parseFloat(n) || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }); }
function esc(s) {
var d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML;
}
function escAttr(s) { return esc(s).replace(/"/g, '&quot;'); }
function doSearch(p) {
page = p || 1;
lastQ = document.getElementById('q').value.trim();
fetch('/pos/api/store/search?q=' + encodeURIComponent(lastQ) + '&page=' + page)
.then(function (r) { return r.json(); })
.then(function (d) {
renderResults(d.items || [], page > 1);
document.getElementById('moreBtn').style.display = (d.page < d.total_pages) ? '' : 'none';
}).catch(function () {
document.getElementById('results').innerHTML = '<div class="empty">Error de conexión. Intenta de nuevo.</div>';
});
}
document.getElementById('q').addEventListener('keydown', function (e) {
if (e.key === 'Enter') doSearch(1);
});
function addToCart(id) {
// buscar el item en el último render guardado
var it = window._lastItems && window._lastItems.find(function (x) { return x.id === id; });
if (!it) return;
var line = cart.find(function (l) { return l.inventory_id === id; });
if (line) line.quantity += 1;
else cart.push({ inventory_id: id, name: it.name, part_number: it.part_number, price: it.price, quantity: 1 });
saveCart();
}
function renderCart() {
var el = document.getElementById('cartLines');
var total = 0;
el.innerHTML = cart.length ? cart.map(function (l, i) {
total += l.price * l.quantity;
return '<div class="line"><span>' + esc(l.name) + ' × ' + l.quantity + '</span>'
+ '<span><button class="btn-ghost" style="padding:2px 8px;" onclick="cartQty(' + i + ',-1)">−</button> '
+ '<button class="btn-ghost" style="padding:2px 8px;" onclick="cartQty(' + i + ',1)">+</button> '
+ money(l.price * l.quantity) + '</span></div>';
}).join('') : '<div class="empty">Carrito vacío</div>';
document.getElementById('cartTotal').textContent = 'Total: ' + money(total);
}
function cartQty(i, d) {
cart[i].quantity += d;
if (cart[i].quantity <= 0) cart.splice(i, 1);
saveCart(); renderCart();
}
function openCart() { saveCart(); renderCart(); document.getElementById('cartOverlay').classList.add('open'); }
function closeCart() { document.getElementById('cartOverlay').classList.remove('open'); }
function submitOrder() {
var btn = document.getElementById('orderBtn');
btn.disabled = true;
fetch('/pos/api/store/order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customer_name: document.getElementById('cName').value.trim(),
customer_phone: document.getElementById('cPhone').value.trim(),
notes: document.getElementById('cNotes').value.trim(),
delivery: document.getElementById('cDelivery').checked,
items: cart.map(function (l) { return { inventory_id: l.inventory_id, quantity: l.quantity }; }),
}),
}).then(function (r) { return r.json(); }).then(function (d) {
if (d.ok) {
cart = []; saveCart();
document.querySelector('#cartOverlay .modal').innerHTML =
'<div class="ok"><div class="big">✅</div><h3>¡Pedido #' + d.order_id + ' registrado!</h3>'
+ '<p>Total: ' + money(d.total) + '</p><p>' + esc(d.message) + '</p>'
+ '<button class="btn-brand" onclick="closeCart()">Aceptar</button></div>';
} else {
document.getElementById('orderMsg').innerHTML = '<span style="color:#dc2626;">' + esc(d.error || 'Error') + '</span>';
btn.disabled = false;
}
}).catch(function () {
document.getElementById('orderMsg').innerHTML = '<span style="color:#dc2626;">Error de conexión</span>';
btn.disabled = false;
});
}
// search guarda los items para addToCart
(function () {
var orig = doSearch;
doSearch = function (p) {
orig(p);
setTimeout(function () {
window._lastItems = (window._lastItems || []).slice(); // noop keep
}, 0);
};
})();
var _origRender = renderResults;
renderResults = function (items, append) {
window._lastItems = items;
_origRender(items, append);
};
saveCart();
doSearch(1);
</script>
</body>
</html>