281 lines
12 KiB
HTML
281 lines
12 KiB
HTML
<!-- pages/estadoComandas.html -->
|
||
<!DOCTYPE html>
|
||
<html lang="es">
|
||
<head>
|
||
<meta charset="utf-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||
<title>Estado de 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: 0.9fr 1.1fr; 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; }
|
||
.muted { color:#666; }
|
||
.pill { font-size:12px; padding:2px 8px; border-radius:99px; border:1px solid #e0e0ea; background:#fafafa; display:inline-block; }
|
||
.list { max-height: 70vh; overflow:auto; }
|
||
.list table { width:100%; border-collapse: collapse; }
|
||
.list th, .list td { padding:8px 10px; border-bottom:1px solid #eee; }
|
||
.list thead th { text-align:left; font-size:12px; text-transform: uppercase; letter-spacing:.04em; color:#555; background:#fafafa; }
|
||
.right { text-align:right; }
|
||
.btn { padding:10px 12px; border-radius:8px; border:1px solid #dadbe4; background:#fafafa; cursor:pointer; }
|
||
.btn.primary { background:#111; color:#fff; border-color:#111; }
|
||
.btn.danger { background:#b00020; color:#fff; border-color:#b00020; }
|
||
.btn.small { font-size: 13px; padding:6px 8px; }
|
||
select, input, textarea { font: inherit; padding:10px; border:1px solid #dadbe4; border-radius:8px; background:#fff; }
|
||
.kpi { display:flex; gap:6px; align-items: baseline; }
|
||
.sticky-footer { position: sticky; bottom: 0; background:#fff; padding:12px 14px; border-top:1px solid #eee; display:flex; gap:10px; align-items:center; }
|
||
.ok { color:#0a7d28; }
|
||
.err { color:#b00020; }
|
||
.tag { font-size:12px; padding:2px 8px; border-radius:6px; border:1px solid #e7e7ef; background:#fafafa; }
|
||
.tag.abierta { border-color:#0a7d28; color:#0a7d28; }
|
||
.tag.cerrada { border-color:#555; color:#555; }
|
||
.tag.pagada { border-color:#1b5e20; color:#1b5e20; }
|
||
.tag.anulada { border-color:#b00020; color:#b00020; }
|
||
table { width:100%; border-collapse: collapse; }
|
||
th, td { padding:8px 10px; border-bottom:1px solid #eee; }
|
||
thead th { text-align:left; font-size:12px; text-transform: uppercase; letter-spacing:.04em; color:#555; background:#fafafa; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>🧾 Estado de Comandas</h1>
|
||
<div style="flex:1"></div>
|
||
<a class="pill" href="/comandas">➕ Nueva comanda</a>
|
||
</header>
|
||
|
||
<main>
|
||
<!-- Izquierda: listado -->
|
||
<section class="card">
|
||
<div class="hd">
|
||
<strong>Listado</strong>
|
||
<div style="flex:1"></div>
|
||
<label class="muted" style="display:flex; gap:6px; align-items:center;">
|
||
<input id="soloAbiertas" type="checkbox" checked />
|
||
Solo abiertas
|
||
</label>
|
||
</div>
|
||
<div class="bd">
|
||
<div class="row" style="margin-bottom:10px;">
|
||
<input id="buscar" type="search" placeholder="Buscar por #, mesa o usuario…" style="flex:1"/>
|
||
<button class="btn" id="limpiar">Limpiar</button>
|
||
</div>
|
||
<div class="list" id="lista"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Derecha: detalle -->
|
||
<section class="card">
|
||
<div class="hd">
|
||
<strong>Detalle</strong>
|
||
<div style="flex:1"></div>
|
||
<span id="detalleEstado" class="tag">—</span>
|
||
</div>
|
||
<div class="bd" id="detalle">
|
||
<div class="muted">Selecciona una comanda para ver el detalle.</div>
|
||
</div>
|
||
<div class="sticky-footer">
|
||
<div class="kpi"><span class="muted">ID:</span><strong id="kpiId">—</strong></div>
|
||
<div class="kpi" style="margin-left:8px;"><span class="muted">Mesa:</span><strong id="kpiMesa">—</strong></div>
|
||
<div class="kpi" style="margin-left:8px;"><span class="muted">Total:</span><strong id="kpiTotal">$ 0.00</strong></div>
|
||
<div style="flex:1"></div>
|
||
<button class="btn" id="reabrir">Reabrir</button>
|
||
<button class="btn primary" id="cerrar">Cerrar</button>
|
||
</div>
|
||
<div class="bd">
|
||
<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 = {
|
||
filtro: '',
|
||
soloAbiertas: true,
|
||
lista: [], // [{ id_comanda, mesa_numero, mesa_apodo, usuario_nombre, usuario_apellido, fec_creacion, estado, observaciones }]
|
||
sel: null, // id seleccionado
|
||
detalle: [] // [{ id_det_comanda, producto_nombre, cantidad, pre_unitario, subtotal, observaciones }]
|
||
};
|
||
|
||
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);
|
||
const data = await res.json().catch(()=>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;
|
||
}
|
||
|
||
// ----------- Data -----------
|
||
async function loadLista() {
|
||
const estado = state.soloAbiertas ? 'abierta' : '';
|
||
const url = estado ? `/api/comandas?estado=${encodeURIComponent(estado)}&limit=300` : '/api/comandas?limit=300';
|
||
const rows = await jget(url);
|
||
state.lista = rows;
|
||
renderLista();
|
||
}
|
||
|
||
async function loadDetalle(id) {
|
||
const det = await jget(`/api/comandas/${id}/detalle`);
|
||
state.detalle = det;
|
||
renderDetalle();
|
||
}
|
||
|
||
// ----------- UI: Lista -----------
|
||
function renderLista() {
|
||
let rows = state.lista.slice();
|
||
const f = state.filtro;
|
||
if (f) {
|
||
const k = f.toLowerCase();
|
||
rows = rows.filter(r =>
|
||
String(r.id_comanda).includes(k) ||
|
||
(String(r.mesa_numero ?? '').includes(k)) ||
|
||
((`${r.usuario_nombre||''} ${r.usuario_apellido||''}`).toLowerCase().includes(k))
|
||
);
|
||
}
|
||
const box = $('#lista');
|
||
if (!rows.length) { box.innerHTML = '<div class="muted">Sin resultados.</div>'; return; }
|
||
|
||
const tbl = document.createElement('table');
|
||
tbl.innerHTML = `
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Mesa</th>
|
||
<th>Usuario</th>
|
||
<th>Fecha</th>
|
||
<th>Estado</th>
|
||
<th class="right">Items</th>
|
||
<th class="right">Total</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
`;
|
||
const tb = tbl.querySelector('tbody');
|
||
|
||
rows.forEach(r => {
|
||
const tr = document.createElement('tr');
|
||
tr.style.cursor = 'pointer';
|
||
tr.innerHTML = `
|
||
<td>${r.id_comanda}</td>
|
||
<td>#${r.mesa_numero} · ${r.mesa_apodo || ''}</td>
|
||
<td>${(r.usuario_nombre||'') + ' ' + (r.usuario_apellido||'')}</td>
|
||
<td>${new Date(r.fec_creacion).toLocaleString()}</td>
|
||
<td><span class="tag ${r.estado}">${r.estado}</span></td>
|
||
<td class="right">${r.items ?? '—'}</td>
|
||
<td class="right">${money(r.total ?? 0)}</td>
|
||
`;
|
||
tr.addEventListener('click', () => { state.sel = r.id_comanda; loadDetalle(r.id_comanda); applyHeader(r); });
|
||
tb.appendChild(tr);
|
||
});
|
||
|
||
box.innerHTML = '';
|
||
box.appendChild(tbl);
|
||
}
|
||
|
||
// ----------- UI: Detalle -----------
|
||
function applyHeader(r) {
|
||
$('#kpiId').textContent = r.id_comanda ?? '—';
|
||
$('#kpiMesa').textContent = r.mesa_numero ? `#${r.mesa_numero}` : '—';
|
||
$('#detalleEstado').className = `tag ${r.estado}`;
|
||
$('#detalleEstado').textContent = r.estado;
|
||
$('#kpiTotal').textContent = money(r.total ?? 0);
|
||
|
||
// Botones según estado
|
||
const cerr = $('#cerrar'), reab = $('#reabrir');
|
||
if (r.estado === 'abierta') {
|
||
cerr.disabled = false; cerr.title = '';
|
||
reab.disabled = true; reab.title = 'Ya está abierta';
|
||
} else {
|
||
cerr.disabled = false; // permitir cerrar (idempotente/override)
|
||
reab.disabled = false;
|
||
}
|
||
}
|
||
|
||
function renderDetalle() {
|
||
const box = $('#detalle');
|
||
if (!state.detalle.length) { box.innerHTML = '<div class="muted">Sin detalle.</div>'; 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>Observaciones</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
`;
|
||
const tb = tbl.querySelector('tbody');
|
||
|
||
let total = 0;
|
||
state.detalle.forEach(r => {
|
||
total += Number(r.subtotal||0);
|
||
const tr = document.createElement('tr');
|
||
tr.innerHTML = `
|
||
<td>${r.producto_nombre}</td>
|
||
<td class="right">${money(r.pre_unitario)}</td>
|
||
<td class="right">${Number(r.cantidad).toFixed(3)}</td>
|
||
<td class="right">${money(r.subtotal)}</td>
|
||
<td>${r.observaciones||''}</td>
|
||
`;
|
||
tb.appendChild(tr);
|
||
});
|
||
|
||
box.innerHTML = '';
|
||
box.appendChild(tbl);
|
||
$('#kpiTotal').textContent = money(total);
|
||
}
|
||
|
||
// ----------- Actions -----------
|
||
async function setEstado(estado) {
|
||
if (!state.sel) return;
|
||
try {
|
||
const { updated } = await jpost(`/api/comandas/${state.sel}/estado`, { estado });
|
||
toast(`Comanda #${updated.id_comanda} → ${updated.estado}`, true);
|
||
await loadLista();
|
||
// mantener seleccionada si sigue existiendo en filtro
|
||
const found = state.lista.find(x => x.id_comanda === updated.id_comanda);
|
||
if (found) { applyHeader(found); await loadDetalle(found.id_comanda); }
|
||
else {
|
||
state.sel = null;
|
||
$('#detalle').innerHTML = '<div class="muted">Selecciona una comanda para ver el detalle.</div>';
|
||
$('#detalleEstado').textContent = '—'; $('#detalleEstado').className = 'tag';
|
||
$('#kpiId').textContent = '—'; $('#kpiMesa').textContent='—'; $('#kpiTotal').textContent = money(0);
|
||
}
|
||
} catch (e) {
|
||
toast(e.message || 'No se pudo cambiar el estado');
|
||
}
|
||
}
|
||
|
||
// ----------- Init -----------
|
||
$('#soloAbiertas').addEventListener('change', async (e) => { state.soloAbiertas = e.target.checked; await loadLista(); });
|
||
$('#buscar').addEventListener('input', () => { state.filtro = $('#buscar').value.trim(); renderLista(); });
|
||
$('#limpiar').addEventListener('click', () => { $('#buscar').value=''; state.filtro=''; renderLista(); });
|
||
$('#cerrar').addEventListener('click', () => setEstado('cerrada'));
|
||
$('#reabrir').addEventListener('click', () => setEstado('abierta'));
|
||
|
||
(async function main(){ try { await loadLista(); } catch(e){ toast(e.message||'Error cargando comandas'); }})();
|
||
</script>
|
||
</body>
|
||
</html>
|