356 lines
14 KiB
HTML
356 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
<title>Comandas</title>
|
|
<style>
|
|
:root { --gap: 12px; --radius: 10px; }
|
|
* { box-sizing: border-box; }
|
|
body { margin:0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial; background:#f6f7fb; color:#111; }
|
|
header { position: sticky; top: 0; background:#fff; border-bottom:1px solid #e7e7ef; padding:14px 18px; display:flex; gap:10px; align-items:center; z-index:2; }
|
|
header h1 { margin:0; font-size:16px; font-weight:600; }
|
|
main { padding: 18px; max-width: 1200px; margin: 0 auto; display:grid; grid-template-columns: 1.15fr 0.85fr; gap: var(--gap); }
|
|
.card { background:#fff; border:1px solid #e7e7ef; border-radius: var(--radius); }
|
|
.card .hd { padding:12px 14px; border-bottom:1px solid #eee; display:flex; align-items:center; gap:10px; }
|
|
.card .bd { padding:14px; }
|
|
.row { display:flex; gap:10px; align-items:center; flex-wrap: wrap; }
|
|
.grid { display:grid; gap:10px; }
|
|
.grid.cols-2 { grid-template-columns: 1fr 1fr; }
|
|
.muted { color:#666; }
|
|
select, input, textarea, button { font: inherit; padding:10px; border:1px solid #dadbe4; border-radius:8px; background:#fff; }
|
|
select:focus, input:focus, textarea:focus { outline:none; border-color:#999; }
|
|
input[type="number"] { width: 100%; }
|
|
textarea { width:100%; min-height: 68px; resize: vertical; }
|
|
button { cursor: pointer; }
|
|
.btn { padding:10px 12px; border-radius:8px; border:1px solid #dadbe4; background:#fafafa; }
|
|
.btn.primary { background:#111; border-color:#111; color:#fff; }
|
|
.btn.ghost { background:#fff; }
|
|
.btn.small { padding:6px 8px; font-size: 13px; }
|
|
.pill { font-size:12px; padding:2px 8px; border-radius:99px; border:1px solid #e0e0ea; background:#fafafa; display:inline-block; }
|
|
.toolbar { display:flex; gap:10px; align-items:center; }
|
|
.spacer { flex:1 }
|
|
.search { display:flex; gap:8px; }
|
|
.search input { flex:1; }
|
|
table { width:100%; border-collapse: collapse; }
|
|
thead th { text-align:left; font-size:12px; text-transform: uppercase; letter-spacing:.04em; color:#555; background:#fafafa; }
|
|
th, td { padding:8px 10px; border-bottom:1px solid #eee; vertical-align: middle; }
|
|
.qty { display:flex; align-items:center; gap:6px; }
|
|
.qty input { width: 90px; }
|
|
.right { text-align:right; }
|
|
.total { font-size: 22px; font-weight: 700; }
|
|
.notice { padding:10px; border-radius:8px; border:1px solid #e7e7ef; background:#fafafa; }
|
|
.ok { color:#0a7d28; }
|
|
.err { color:#b00020; }
|
|
.sticky-footer { position: sticky; bottom: 0; background:#fff; padding:12px 14px; border-top:1px solid #eee; display:flex; gap:10px; align-items:center; }
|
|
.kpi { display:flex; gap:6px; align-items: baseline; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>📋 Nueva Comanda</h1>
|
|
<div class="spacer"></div>
|
|
<span class="pill muted">/api/*</span>
|
|
</header>
|
|
|
|
<main>
|
|
<!-- Panel izquierdo: productos -->
|
|
<section class="card" id="panelProductos">
|
|
<div class="hd">
|
|
<strong>Productos</strong>
|
|
<div class="spacer"></div>
|
|
<div class="toolbar">
|
|
<span class="muted" id="prodCount">0 ítems</span>
|
|
</div>
|
|
</div>
|
|
<div class="bd">
|
|
<div class="row search" style="margin-bottom:10px;">
|
|
<input id="busqueda" type="search" placeholder="Buscar por nombre o categoría…"/>
|
|
<button class="btn" id="limpiarBusqueda">Limpiar</button>
|
|
</div>
|
|
<div id="listadoProductos" style="max-height: 58vh; overflow:auto;">
|
|
<!-- tabla productos -->
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Panel derecho: datos + carrito + crear -->
|
|
<section class="card" id="panelComanda">
|
|
<div class="hd"><strong>Detalles</strong></div>
|
|
<div class="bd grid" style="gap:14px;">
|
|
<div class="grid cols-2">
|
|
<div>
|
|
<label class="muted">Mesa</label>
|
|
<select id="selMesa"></select>
|
|
</div>
|
|
<div>
|
|
<label class="muted">Usuario</label>
|
|
<select id="selUsuario"></select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="muted">Observaciones</label>
|
|
<textarea id="obs"></textarea>
|
|
</div>
|
|
|
|
<div class="notice muted">La fecha se completa automáticamente y los estados/activos usan sus valores por defecto.</div>
|
|
|
|
<div class="card">
|
|
<div class="hd"><strong>Carrito</strong></div>
|
|
<div class="bd" id="carritoWrap">
|
|
<div class="muted">Aún no agregaste productos.</div>
|
|
</div>
|
|
<div class="sticky-footer">
|
|
<div class="kpi"><span class="muted">Ítems:</span><strong id="kpiItems">0</strong></div>
|
|
<div class="kpi" style="margin-left:8px;"><span class="muted">Total:</span><strong id="kpiTotal">$ 0.00</strong></div>
|
|
<div class="spacer"></div>
|
|
<button class="btn ghost" id="vaciar">Vaciar</button>
|
|
<button class="btn primary" id="crear">Crear Comanda</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="msg" class="muted"></div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
const $ = (s, r=document) => r.querySelector(s);
|
|
const $$ = (s, r=document) => Array.from(r.querySelectorAll(s));
|
|
|
|
const state = {
|
|
productos: [],
|
|
mesas: [],
|
|
usuarios: [],
|
|
carrito: [], // [{id_producto, nombre, pre_unitario, cantidad}]
|
|
filtro: ''
|
|
};
|
|
|
|
// ---------- Utils ----------
|
|
const money = (n) => (isNaN(n) ? '—' : new Intl.NumberFormat('es-UY', { style:'currency', currency:'UYU' }).format(Number(n)));
|
|
const toast = (msg, ok=false) => { const el = $('#msg'); el.className = ok ? 'ok' : 'err'; el.textContent = msg; setTimeout(()=>{ el.textContent=''; el.className='muted'; }, 3500); };
|
|
async function jget(url) {
|
|
const res = await fetch(url);
|
|
let data; try { data = await res.json(); } catch { data = null; }
|
|
if (!res.ok) throw new Error(data?.error || `${res.status} ${res.statusText}`);
|
|
return data;
|
|
}
|
|
async function jpost(url, body) {
|
|
const res = await fetch(url, { method:'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
|
const data = await res.json().catch(()=>null);
|
|
if (!res.ok) throw new Error(data?.error || `${res.status} ${res.statusText}`);
|
|
return data;
|
|
}
|
|
|
|
// ---------- Load data ----------
|
|
async function init() {
|
|
// productos, mesas, usuarios
|
|
const [prods, mesas, usuarios] = await Promise.all([
|
|
jget('/api/table/productos?limit=1000'),
|
|
jget('/api/table/mesas?limit=1000'),
|
|
jget('/api/table/usuarios?limit=1000')
|
|
]);
|
|
|
|
state.productos = prods.filter(p => p.activo !== false); // si existe activo=false, filtrarlo
|
|
state.mesas = mesas;
|
|
state.usuarios = usuarios.filter(u => u.activo !== false);
|
|
|
|
hydrateMesas();
|
|
hydrateUsuarios();
|
|
renderProductos();
|
|
renderCarrito();
|
|
|
|
$('#busqueda').addEventListener('input', () => { state.filtro = $('#busqueda').value.trim().toLowerCase(); renderProductos(); });
|
|
$('#limpiarBusqueda').addEventListener('click', () => { $('#busqueda').value=''; state.filtro=''; renderProductos(); });
|
|
$('#vaciar').addEventListener('click', () => { state.carrito=[]; renderCarrito(); });
|
|
$('#crear').addEventListener('click', crearComanda);
|
|
}
|
|
|
|
function hydrateMesas() {
|
|
const sel = $('#selMesa'); sel.innerHTML = '';
|
|
for (const m of state.mesas) {
|
|
const o = document.createElement('option');
|
|
o.value = m.id_mesa;
|
|
o.textContent = `#${m.numero} · ${m.apodo} (${m.estado})`;
|
|
sel.appendChild(o);
|
|
}
|
|
}
|
|
|
|
function hydrateUsuarios() {
|
|
const sel = $('#selUsuario'); sel.innerHTML = '';
|
|
for (const u of state.usuarios) {
|
|
const o = document.createElement('option');
|
|
o.value = u.id_usuario;
|
|
o.textContent = `${u.nombre} ${u.apellido}`.trim();
|
|
sel.appendChild(o);
|
|
}
|
|
}
|
|
|
|
// ---------- Productos ----------
|
|
function renderProductos() {
|
|
let rows = state.productos.slice();
|
|
if (state.filtro) {
|
|
rows = rows.filter(p =>
|
|
(p.nombre || '').toLowerCase().includes(state.filtro) ||
|
|
String(p.id_categoria ?? '').includes(state.filtro)
|
|
);
|
|
}
|
|
$('#prodCount').textContent = `${rows.length} ítems`;
|
|
|
|
if (!rows.length) {
|
|
$('#listadoProductos').innerHTML = '<div class="muted">Sin resultados.</div>';
|
|
return;
|
|
}
|
|
|
|
const tbl = document.createElement('table');
|
|
tbl.innerHTML = `
|
|
<thead>
|
|
<tr>
|
|
<th>Producto</th>
|
|
<th class="right">Precio</th>
|
|
<th style="width:180px;">Cantidad</th>
|
|
<th style="width:90px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
`;
|
|
const tb = tbl.querySelector('tbody');
|
|
|
|
for (const p of rows) {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${p.nombre}</td>
|
|
<td class="right">${money(p.precio)}</td>
|
|
<td>
|
|
<div class="qty">
|
|
<input type="number" min="0.001" step="0.001" value="1.000" data-qty />
|
|
<button class="btn small" data-dec>-</button>
|
|
<button class="btn small" data-inc>+</button>
|
|
</div>
|
|
</td>
|
|
<td><button class="btn primary small" data-add>Agregar</button></td>
|
|
`;
|
|
const qty = tr.querySelector('[data-qty]');
|
|
tr.querySelector('[data-dec]').addEventListener('click', () => { qty.value = Math.max(0.001, (parseFloat(qty.value||'0') - 1)).toFixed(3); });
|
|
tr.querySelector('[data-inc]').addEventListener('click', () => { qty.value = (parseFloat(qty.value||'0') + 1).toFixed(3); });
|
|
tr.querySelector('[data-add]').addEventListener('click', () => addToCart(p, parseFloat(qty.value||'1')) );
|
|
tb.appendChild(tr);
|
|
}
|
|
|
|
$('#listadoProductos').innerHTML = '';
|
|
$('#listadoProductos').appendChild(tbl);
|
|
}
|
|
|
|
function addToCart(prod, cantidad) {
|
|
if (!(cantidad > 0)) { toast('Cantidad inválida'); return; }
|
|
const precio = parseFloat(prod.precio);
|
|
const it = state.carrito.find(i => i.id_producto === prod.id_producto && i.pre_unitario === precio);
|
|
if (it) it.cantidad = Number((it.cantidad + cantidad).toFixed(3));
|
|
else state.carrito.push({ id_producto: prod.id_producto, nombre: prod.nombre, pre_unitario: precio, cantidad: Number(cantidad.toFixed(3)) });
|
|
renderCarrito();
|
|
}
|
|
|
|
// ---------- Carrito ----------
|
|
function renderCarrito() {
|
|
const wrap = $('#carritoWrap');
|
|
if (!state.carrito.length) { wrap.innerHTML = '<div class="muted">Aún no agregaste productos.</div>'; $('#kpiItems').textContent='0'; $('#kpiTotal').textContent=money(0); return; }
|
|
|
|
const tbl = document.createElement('table');
|
|
tbl.innerHTML = `
|
|
<thead>
|
|
<tr>
|
|
<th>Producto</th>
|
|
<th class="right">Unitario</th>
|
|
<th class="right">Cantidad</th>
|
|
<th class="right">Subtotal</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
`;
|
|
const tb = tbl.querySelector('tbody');
|
|
|
|
let items = 0, total = 0;
|
|
state.carrito.forEach((it, idx) => {
|
|
items += 1;
|
|
const sub = Number(it.pre_unitario) * Number(it.cantidad);
|
|
total += sub;
|
|
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${it.nombre}</td>
|
|
<td class="right">${money(it.pre_unitario)}</td>
|
|
<td class="right">
|
|
<input type="number" min="0.001" step="0.001" value="${it.cantidad.toFixed(3)}" style="width:120px"/>
|
|
</td>
|
|
<td class="right">${money(sub)}</td>
|
|
<td class="right">
|
|
<button class="btn small" data-del>Quitar</button>
|
|
</td>
|
|
`;
|
|
const qty = tr.querySelector('input[type="number"]');
|
|
qty.addEventListener('change', () => {
|
|
const v = parseFloat(qty.value||'0');
|
|
if (!(v>0)) { toast('Cantidad inválida'); qty.value = it.cantidad.toFixed(3); return; }
|
|
it.cantidad = Number(v.toFixed(3));
|
|
renderCarrito();
|
|
});
|
|
tr.querySelector('[data-del]').addEventListener('click', () => {
|
|
state.carrito.splice(idx,1);
|
|
renderCarrito();
|
|
});
|
|
|
|
tb.appendChild(tr);
|
|
});
|
|
|
|
wrap.innerHTML = '';
|
|
wrap.appendChild(tbl);
|
|
$('#kpiItems').textContent = String(items);
|
|
$('#kpiTotal').textContent = money(total);
|
|
}
|
|
|
|
// ---------- Crear comanda ----------
|
|
async function crearComanda() {
|
|
if (!state.carrito.length) { toast('Agrega al menos un producto'); return; }
|
|
const id_mesa = parseInt($('#selMesa').value, 10);
|
|
const id_usuario = parseInt($('#selUsuario').value, 10);
|
|
if (!id_mesa || !id_usuario) { toast('Selecciona mesa y usuario'); return; }
|
|
|
|
const observaciones = $('#obs').value.trim() || null;
|
|
|
|
try {
|
|
// 1) encabezado comanda (estado por defecto: 'abierta'; fecha la pone la DB)
|
|
const { inserted: com } = await jpost('/api/table/comandas', {
|
|
id_usuario,
|
|
id_mesa,
|
|
estado: 'abierta',
|
|
observaciones
|
|
});
|
|
|
|
// 2) detalle (una inserción por renglón)
|
|
const id_comanda = com.id_comanda;
|
|
const payloads = state.carrito.map(it => ({
|
|
id_comanda,
|
|
id_producto: it.id_producto,
|
|
cantidad: it.cantidad,
|
|
pre_unitario: it.pre_unitario
|
|
}));
|
|
|
|
await Promise.all(payloads.map(p => jpost('/api/table/deta_comandas', p)));
|
|
|
|
state.carrito = [];
|
|
renderCarrito();
|
|
$('#obs').value = '';
|
|
toast(`Comanda #${id_comanda} creada`, true);
|
|
} catch (e) {
|
|
toast(e.message || 'No se pudo crear la comanda');
|
|
}
|
|
}
|
|
|
|
// GO
|
|
init().catch(err => toast(err.message || 'Error cargando datos'));
|
|
</script>
|
|
</body>
|
|
</html>
|