- 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
216 lines
11 KiB
HTML
216 lines
11 KiB
HTML
<!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, '"'); }
|
||
|
||
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>
|