Impresión de tickets correcta.
This commit is contained in:
parent
57dbd5b1fa
commit
ce3d01a180
28
services/manso/package-lock.json
generated
28
services/manso/package-lock.json
generated
@ -16,7 +16,8 @@
|
||||
"express": "^5.1.0",
|
||||
"express-ejs-layouts": "^2.5.1",
|
||||
"pg": "^8.16.3",
|
||||
"pg-format": "^1.0.4"
|
||||
"pg-format": "^1.0.4",
|
||||
"serve-favicon": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^10.0.0",
|
||||
@ -1293,6 +1294,31 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-favicon": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.1.tgz",
|
||||
"integrity": "sha512-JndLBslCLA/ebr7rS3d+/EKkzTsTi1jI2T9l+vHfAaGJ7A7NhtDpSZ0lx81HCNWnnE0yHncG+SSnVf9IMxOwXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "~0.5.2",
|
||||
"ms": "~2.1.3",
|
||||
"parseurl": "~1.3.2",
|
||||
"safe-buffer": "~5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-favicon/node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
||||
|
||||
@ -22,7 +22,8 @@
|
||||
"express": "^5.1.0",
|
||||
"express-ejs-layouts": "^2.5.1",
|
||||
"pg": "^8.16.3",
|
||||
"pg-format": "^1.0.4"
|
||||
"pg-format": "^1.0.4",
|
||||
"serve-favicon": "^2.5.1"
|
||||
},
|
||||
"keywords": [],
|
||||
"description": "Workarround para tener un MVP que llegue al verano para usarse"
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
// app/src/index.js
|
||||
import chalk from 'chalk'; // Colores!
|
||||
import favicon from 'serve-favicon'; // Favicon
|
||||
import express from 'express';
|
||||
import expressLayouts from 'express-ejs-layouts';
|
||||
import cors from 'cors';
|
||||
@ -47,6 +48,14 @@ app.set("layout", "layouts/main");
|
||||
// Archivos estáticos
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
app.use('/favicon', express.static(path.join(__dirname, 'public', 'favicon'), {
|
||||
maxAge: '1y'
|
||||
}));
|
||||
|
||||
app.use(favicon(path.join(__dirname, 'public', 'favicon', 'favicon.ico'), {
|
||||
maxAge: '1y'
|
||||
}));
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Configuración de conexión PostgreSQL
|
||||
// ----------------------------------------------------------
|
||||
@ -57,7 +66,6 @@ const dbConfig = {
|
||||
database: process.env.DB_NAME,
|
||||
port: process.env.DB_LOCAL_PORT ? Number(process.env.DB_LOCAL_PORT) : undefined,
|
||||
ssl: process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined,
|
||||
max: 10
|
||||
};
|
||||
|
||||
const pool = new Pool(dbConfig);
|
||||
@ -393,25 +401,40 @@ app.get('/api/comandas', async (req, res, next) => {
|
||||
|
||||
// Detalle de una comanda (con nombres de productos)
|
||||
|
||||
app.get('/api/comandas/:id/detalle', async (req, res, next) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
return res.status(400).json({ error: 'id inválido' });
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
id_det_comanda, id_producto, producto_nombre,
|
||||
cantidad, pre_unitario, subtotal, observaciones
|
||||
FROM public.v_comandas_detalle_items
|
||||
// GET /api/comandas/:id/detalle
|
||||
app.get('/api/comandas/:id/detalle', (req, res, next) =>
|
||||
pool.query(
|
||||
`SELECT id_det_comanda, id_producto, producto_nombre,
|
||||
cantidad, pre_unitario, subtotal, observaciones
|
||||
FROM public.v_comandas_detalle_items
|
||||
WHERE id_comanda = $1::int
|
||||
ORDER BY id_det_comanda
|
||||
`;
|
||||
const { rows } = await pool.query(sql, [id]);
|
||||
res.json(rows);
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
ORDER BY id_det_comanda`,
|
||||
[req.params.id]
|
||||
)
|
||||
.then(r => res.json(r.rows))
|
||||
.catch(next)
|
||||
);
|
||||
|
||||
|
||||
// app.get('/api/comandas/:id/detalle', async (req, res, next) => {
|
||||
// try {
|
||||
// const id = parseInt(req.params.id, 10);
|
||||
// if (!Number.isInteger(id) || id <= 0) {
|
||||
// return res.status(400).json({ error: 'id inválido' });
|
||||
// }
|
||||
|
||||
// const sql = `
|
||||
// SELECT
|
||||
// id_det_comanda, id_producto, producto_nombre,
|
||||
// cantidad, pre_unitario, subtotal, observaciones
|
||||
// FROM public.v_comandas_detalle_items
|
||||
// WHERE id_comanda = $1::int
|
||||
// ORDER BY id_det_comanda
|
||||
// `;
|
||||
// const { rows } = await pool.query(sql, [id]);
|
||||
// res.json(rows);
|
||||
// } catch (e) { next(e); }
|
||||
// });
|
||||
|
||||
|
||||
// app.get('/api/comandas/:id/detalle', async (req, res, next) => {
|
||||
|
||||
BIN
services/manso/src/public/favicon/android-chrome-192x192.png
Normal file
BIN
services/manso/src/public/favicon/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
BIN
services/manso/src/public/favicon/android-chrome-512x512.png
Normal file
BIN
services/manso/src/public/favicon/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 528 KiB |
BIN
services/manso/src/public/favicon/apple-touch-icon.png
Normal file
BIN
services/manso/src/public/favicon/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
BIN
services/manso/src/public/favicon/favicon-16x16.png
Normal file
BIN
services/manso/src/public/favicon/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 488 B |
BIN
services/manso/src/public/favicon/favicon-32x32.png
Normal file
BIN
services/manso/src/public/favicon/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
services/manso/src/public/favicon/favicon.ico
Normal file
BIN
services/manso/src/public/favicon/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
services/manso/src/public/favicon/site.webmanifest
Normal file
1
services/manso/src/public/favicon/site.webmanifest
Normal file
@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
@ -90,6 +90,123 @@
|
||||
filtro: ''
|
||||
};
|
||||
|
||||
// Genera el HTML del ticket de cocina (80mm aprox)
|
||||
function buildKitchenTicketHTML(data) {
|
||||
const mesaTxt = `Mesa #${data.mesa_numero ?? '—'}${data.mesa_apodo ? ' · ' + data.mesa_apodo : ''}`;
|
||||
const obs = (data.observaciones && data.observaciones.trim()) ? data.observaciones.trim() : '';
|
||||
|
||||
const productosHtml = data.productos.map(p => `
|
||||
<div class="row">
|
||||
<div class="qty">x${p.cantidad}</div>
|
||||
<div class="name">${p.nombre}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Ticket Cocina</title>
|
||||
<style>
|
||||
:root {
|
||||
--w: 80mm; /* Cambia a 58mm si tu rollo es de 58 */
|
||||
--fz: 30px; /* Base más grande */
|
||||
--fz-sm: 13px;
|
||||
--fz-lg: 20px; /* Filas de productos */
|
||||
--fz-xl: 35px; /* Título */
|
||||
}
|
||||
html, body { margin:0; padding:0; }
|
||||
body {
|
||||
width: var(--w);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: var(--fz);
|
||||
font-weight: 700; /* TODO en negrita */
|
||||
line-height: 1.35;
|
||||
color:#000;
|
||||
}
|
||||
#ticket { padding: 10px 8px; }
|
||||
.center { text-align:center; }
|
||||
.row { display:flex; gap:8px; margin: 4px 0; }
|
||||
.row .qty {
|
||||
min-width: 24mm;
|
||||
font-size: var(--fz-lg);
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
.row .name {
|
||||
flex:1;
|
||||
font-size: var(--fz-lg);
|
||||
text-transform: uppercase; /* Productos en MAYÚSCULAS */
|
||||
word-break: break-word;
|
||||
}
|
||||
.hr { border-top: 2px dashed #000; margin: 8px 0; } /* Separador más grueso */
|
||||
.small { font-size: var(--fz-sm); }
|
||||
.bold { font-weight: 700; }
|
||||
.mt4 { margin-top: 4px; }
|
||||
.mt8 { margin-top: 8px; }
|
||||
.mb4 { margin-bottom: 4px; }
|
||||
.mb8 { margin-bottom: 8px; }
|
||||
.title { font-size: var(--fz-xl); letter-spacing: 0.3px; }
|
||||
@page { size: var(--w) auto; margin: 0; }
|
||||
@media print { body { width: var(--w); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="ticket">
|
||||
<div class="center bold title">COMANDA COCINA</div>
|
||||
<div class="center small">#${data.id_comanda}</div>
|
||||
<div class="hr"></div>
|
||||
|
||||
<div class="small">Fecha: ${data.fecha} ${data.hora}</div>
|
||||
<div class="small">${mesaTxt}</div>
|
||||
<div class="small">Mozo: ${data.usuario || '—'}</div>
|
||||
|
||||
${obs ? `<div class="mt8"><span class="bold">OBSERVACIONES:</span><br>${obs}</div>` : ''}
|
||||
|
||||
<div class="hr"></div>
|
||||
<div class="bold mb4">PRODUCTOS</div>
|
||||
${productosHtml}
|
||||
|
||||
<div class="hr"></div>
|
||||
<div class="small">Ítems: ${data.items} · Unidades: ${data.units}</div>
|
||||
|
||||
<div class="center mt8 small">— fin —</div>
|
||||
</div>
|
||||
<script>window.onload = () => { window.focus(); window.print(); }<\/script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// Imprime HTML usando un iframe oculto (menos bloqueos de pop-up)
|
||||
function printHtmlViaIframe(html) {
|
||||
return new Promise((resolve) => {
|
||||
let iframe = document.getElementById('printFrame');
|
||||
if (!iframe) {
|
||||
iframe = document.createElement('iframe');
|
||||
iframe.id = 'printFrame';
|
||||
iframe.style.position = 'fixed';
|
||||
iframe.style.right = '-9999px';
|
||||
iframe.style.bottom = '0';
|
||||
iframe.style.width = '0';
|
||||
iframe.style.height = '0';
|
||||
iframe.style.border = '0';
|
||||
document.body.appendChild(iframe);
|
||||
}
|
||||
const doc = iframe.contentWindow.document;
|
||||
doc.open();
|
||||
doc.write(html);
|
||||
doc.close();
|
||||
|
||||
// Salida: remover iframe después de un rato para no acumular
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
// (si prefieres mantenerlo para reimpresiones, no lo quites)
|
||||
// document.body.removeChild(iframe);
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Utils
|
||||
const money = (n) => (isNaN(n) ? '—' : new Intl.NumberFormat('es-UY', { style:'currency', currency:'UYU' }).format(Number(n)));
|
||||
const toast = (msg, ok=false) => {
|
||||
@ -290,13 +407,17 @@
|
||||
$('#kpiTotal').textContent = money(total);
|
||||
}
|
||||
|
||||
// Crear comanda
|
||||
const fmtQty = (n) => Number(n).toFixed(3).replace(/\.?0+$/,'');
|
||||
|
||||
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; }
|
||||
|
||||
// Snapshot del carrito ANTES de limpiar (para imprimir)
|
||||
const cartSnapshot = state.carrito.map(it => ({ ...it }));
|
||||
|
||||
const observaciones = $('#obs').value.trim() || null;
|
||||
|
||||
try {
|
||||
@ -310,23 +431,90 @@
|
||||
|
||||
// 2) detalle
|
||||
const id_comanda = com.id_comanda;
|
||||
const payloads = state.carrito.map(it => ({
|
||||
const payloads = cartSnapshot.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)));
|
||||
|
||||
// 3) Datos auxiliares para ticket
|
||||
const mesa = state.mesas.find(m => m.id_mesa === id_mesa) || {};
|
||||
const usuario = state.usuarios.find(u => u.id_usuario === id_usuario) || {};
|
||||
const now = new Date();
|
||||
const fecha = now.toLocaleDateString();
|
||||
const hora = now.toLocaleTimeString();
|
||||
|
||||
// 4) Construir e imprimir Ticket de Cocina (sin precios)
|
||||
const units = cartSnapshot.reduce((acc, it) => acc + Number(it.cantidad || 0), 0);
|
||||
const items = cartSnapshot.length;
|
||||
|
||||
const ticketHtml = buildKitchenTicketHTML({
|
||||
id_comanda,
|
||||
fecha, hora,
|
||||
mesa_numero: mesa?.numero,
|
||||
mesa_apodo: mesa?.apodo,
|
||||
usuario: `${usuario?.nombre || ''} ${usuario?.apellido || ''}`.trim(),
|
||||
observaciones,
|
||||
items,
|
||||
units,
|
||||
productos: cartSnapshot.map(it => ({
|
||||
nombre: it.nombre,
|
||||
cantidad: fmtQty(it.cantidad)
|
||||
}))
|
||||
});
|
||||
|
||||
await printHtmlViaIframe(ticketHtml);
|
||||
|
||||
// 5) Reset UI
|
||||
state.carrito = [];
|
||||
renderCarrito();
|
||||
$('#obs').value = '';
|
||||
toast(`Comanda #${id_comanda} creada`, true);
|
||||
toast(`Comanda #${id_comanda} creada e impresa`, true);
|
||||
|
||||
} catch (e) {
|
||||
toast(e.message || 'No se pudo crear la comanda');
|
||||
}
|
||||
}
|
||||
|
||||
// // 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
|
||||
// const { inserted: com } = await jpost('/api/table/comandas', {
|
||||
// id_usuario,
|
||||
// id_mesa,
|
||||
// estado: 'abierta',
|
||||
// observaciones
|
||||
// });
|
||||
|
||||
// // 2) detalle
|
||||
// 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'));
|
||||
|
||||
@ -80,6 +80,247 @@
|
||||
return 'badge badge-outline';
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ===== Estado =====
|
||||
const state = {
|
||||
filtro: '',
|
||||
soloAbiertas: true,
|
||||
lista: [], // [{ id_comanda, mesa_numero, mesa_apodo, usuario_nombre, usuario_apellido, fec_creacion, estado, items, total }]
|
||||
sel: null, // id seleccionado
|
||||
detalle: [] // [{ id_det_comanda, producto_nombre, cantidad, pre_unitario, subtotal, observaciones }]
|
||||
};
|
||||
|
||||
// ===== 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 = Array.isArray(rows) ? rows : [];
|
||||
renderLista();
|
||||
}
|
||||
|
||||
async function loadDetalle(id) {
|
||||
const det = await jget(`/api/comandas/${id}/detalle`);
|
||||
state.detalle = Array.isArray(det) ? det : [];
|
||||
renderDetalle();
|
||||
}
|
||||
|
||||
// ===== UI: Lista =====
|
||||
function renderLista(){
|
||||
let rows = state.lista.slice();
|
||||
const f = state.filtro?.trim().toLowerCase();
|
||||
if (f) {
|
||||
rows = rows.filter(r =>
|
||||
String(r.id_comanda).includes(f) ||
|
||||
String(r.mesa_numero ?? '').includes(f) ||
|
||||
(`${r.usuario_nombre||''} ${r.usuario_apellido||''}`).toLowerCase().includes(f)
|
||||
);
|
||||
}
|
||||
|
||||
const box = $('#lista');
|
||||
if (!rows.length) { box.innerHTML = '<div class="p-3 text-muted">Sin resultados.</div>'; return; }
|
||||
|
||||
const tbl = document.createElement('table');
|
||||
tbl.className = 'table table-sm align-middle mb-0';
|
||||
tbl.innerHTML = `
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Mesa</th>
|
||||
<th>Usuario</th>
|
||||
<th>Fecha</th>
|
||||
<th>Estado</th>
|
||||
<th class="text-end">Ítems</th>
|
||||
<th class="text-end">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 ? '· '+r.mesa_apodo : ''}</td>
|
||||
<td>${(r.usuario_nombre||'') + ' ' + (r.usuario_apellido||'')}</td>
|
||||
<td>${r.fec_creacion ? new Date(r.fec_creacion).toLocaleString() : '—'}</td>
|
||||
<td><span class="${badgeClass(r.estado)}">${r.estado ?? '—'}</span></td>
|
||||
<td class="text-end">${r.items ?? '—'}</td>
|
||||
<td class="text-end">${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 + KPIs =====
|
||||
function applyHeader(r){
|
||||
$('#kpiId').textContent = r.id_comanda ?? '—';
|
||||
$('#kpiMesa').textContent = r.mesa_numero ? `#${r.mesa_numero}` : '—';
|
||||
$('#detalleEstado').className = badgeClass(r.estado);
|
||||
$('#detalleEstado').textContent = r.estado ?? '—';
|
||||
$('#kpiTotal').textContent = money(r.total ?? 0);
|
||||
|
||||
// Botones (más precisos según estado)
|
||||
const cerr = $('#cerrar'), reab = $('#reabrir');
|
||||
const s = String(r.estado||'').toLowerCase();
|
||||
if (s.includes('abier')) {
|
||||
cerr.disabled = false; cerr.title = '';
|
||||
reab.disabled = true; reab.title = 'Ya está abierta';
|
||||
} else if (s.includes('cerr')) {
|
||||
cerr.disabled = true; cerr.title = 'Ya está cerrada';
|
||||
reab.disabled = false; reab.title = '';
|
||||
} else {
|
||||
// Otros estados: permitir ambas acciones
|
||||
cerr.disabled = false; cerr.title = '';
|
||||
reab.disabled = false; reab.title = '';
|
||||
}
|
||||
}
|
||||
|
||||
function renderDetalle(){
|
||||
const box = $('#detalle');
|
||||
if (!state.detalle.length) {
|
||||
box.innerHTML = '<div class="text-muted">Sin detalle.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const tbl = document.createElement('table');
|
||||
tbl.className = 'table table-sm align-middle mb-0';
|
||||
tbl.innerHTML = `
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Producto</th>
|
||||
<th class="text-end">Unitario</th>
|
||||
<th class="text-end">Cantidad</th>
|
||||
<th class="text-end">Subtotal</th>
|
||||
<th>Observaciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
`;
|
||||
const tb = tbl.querySelector('tbody');
|
||||
|
||||
let total = 0;
|
||||
state.detalle.forEach(r => {
|
||||
const sub = Number(r.subtotal || (Number(r.pre_unitario||0) * Number(r.cantidad||0)));
|
||||
total += sub;
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${r.producto_nombre ?? '—'}</td>
|
||||
<td class="text-end">${money(r.pre_unitario)}</td>
|
||||
<td class="text-end">${Number(r.cantidad||0).toFixed(3)}</td>
|
||||
<td class="text-end">${money(sub)}</td>
|
||||
<td>${r.observaciones || ''}</td>
|
||||
`;
|
||||
tb.appendChild(tr);
|
||||
});
|
||||
|
||||
box.innerHTML = '';
|
||||
box.appendChild(tbl);
|
||||
$('#kpiTotal').textContent = money(total);
|
||||
}
|
||||
|
||||
// ===== Actions (usa /abrir y /cerrar) =====
|
||||
async function accionComanda(accion){ // 'abrir' | 'cerrar'
|
||||
if (!state.sel) return;
|
||||
try {
|
||||
await jpost(`/api/comandas/${state.sel}/${accion}`, {}); // el body no se usa en el backend
|
||||
toast(`Comanda #${state.sel} ${accion === 'abrir' ? 'reabierta' : 'cerrada'}`, true);
|
||||
|
||||
// Recargar lista y re-aplicar cabecera/detalle si sigue visible
|
||||
const id = state.sel;
|
||||
await loadLista();
|
||||
const found = state.lista.find(x => x.id_comanda === id);
|
||||
if (found) {
|
||||
applyHeader(found);
|
||||
await loadDetalle(found.id_comanda);
|
||||
} else {
|
||||
// Puede desaparecer del listado si está activado "Solo abiertas" y la cerramos
|
||||
state.sel = null;
|
||||
$('#detalle').innerHTML = '<div class="text-muted">Selecciona una comanda para ver el detalle.</div>';
|
||||
$('#detalleEstado').textContent = '—'; $('#detalleEstado').className = 'badge badge-outline';
|
||||
$('#kpiId').textContent = '—'; $('#kpiMesa').textContent='—'; $('#kpiTotal').textContent = money(0);
|
||||
}
|
||||
} catch (e) {
|
||||
toast(e.message || 'No se pudo actualizar la comanda');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Hooks con Sidebar (offcanvas) =====
|
||||
window.scRefreshList = loadLista;
|
||||
window.scExportCsv = function(){
|
||||
const rows = state.lista.slice();
|
||||
const header = ["id_comanda","mesa_numero","mesa_apodo","usuario","fec_creacion","estado","items","total"];
|
||||
const csv = [header.join(",")].concat(rows.map(r => {
|
||||
const usuario = `${r.usuario_nombre||''} ${r.usuario_apellido||''}`.trim();
|
||||
const vals = [
|
||||
r.id_comanda,
|
||||
r.mesa_numero ?? '',
|
||||
(r.mesa_apodo ?? '').replaceAll('"','""'),
|
||||
usuario.replaceAll('"','""'),
|
||||
r.fec_creacion ?? '',
|
||||
r.estado ?? '',
|
||||
r.items ?? '',
|
||||
r.total ?? ''
|
||||
];
|
||||
return vals.map(v => `"${String(v).replaceAll('"','""')}"`).join(",");
|
||||
})).join("\n");
|
||||
const blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = Object.assign(document.createElement("a"), {href:url, download:`estadoComandas_${new Date().toISOString().slice(0,10)}.csv`});
|
||||
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// ===== 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(); });
|
||||
|
||||
// Ahora los botones llaman a los nuevos endpoints
|
||||
$('#cerrar').addEventListener('click', () => accionComanda('cerrar'));
|
||||
$('#reabrir').addEventListener('click', () => accionComanda('abrir'));
|
||||
|
||||
(async function main(){ try { await loadLista(); } catch(e){ toast(e.message||'Error cargando comandas'); }})();
|
||||
</script>
|
||||
|
||||
|
||||
<!-- <script>
|
||||
// ===== Helpers =====
|
||||
const $ = (s, r=document) => r.querySelector(s);
|
||||
const $$ = (s, r=document) => Array.from(r.querySelectorAll(s));
|
||||
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 ? 'text-success small' : 'text-danger small';
|
||||
el.textContent = msg;
|
||||
setTimeout(()=>{ el.textContent=''; el.className='text-muted small'; }, 3000);
|
||||
};
|
||||
const badgeClass = (estadoRaw) => {
|
||||
const s = String(estadoRaw||'').toLowerCase();
|
||||
if (s.includes('abier')) return 'badge badge-outline badge-estado-abierta';
|
||||
if (s.includes('pagad') || s.includes('paga')) return 'badge badge-outline badge-estado-pagada';
|
||||
if (s.includes('cerr')) return 'badge badge-outline badge-estado-cerrada';
|
||||
if (s.includes('anul') || s.includes('cancel')) return 'badge badge-outline badge-estado-anulada';
|
||||
return 'badge badge-outline';
|
||||
};
|
||||
|
||||
async function jget(url){
|
||||
const res = await fetch(url);
|
||||
const data = await res.json().catch(()=>null);
|
||||
@ -288,4 +529,4 @@
|
||||
$('#reabrir').addEventListener('click', () => setEstado('abierta'));
|
||||
|
||||
(async function main(){ try { await loadLista(); } catch(e){ toast(e.message||'Error cargando comandas'); }})();
|
||||
</script>
|
||||
</script> -->
|
||||
|
||||
@ -5,6 +5,14 @@
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<link rel="icon" href="/favicon/favicon.ico" sizes="any">
|
||||
<link rel="icon" href="/favicon/favicon-16x16.png" sizes="16x16">
|
||||
<link rel="icon" href="/favicon/favicon-32x32.png" sizes="32x32">
|
||||
<link rel="icon" href="/favicon/apple-touch-icon.png" sizes="180x180">
|
||||
<link rel="icon" href="/favicon/android-chrome-512x512.png" sizes="512x512">
|
||||
<link rel="icon" href="/favicon/android-chrome-192x192.png" sizes="192x192">
|
||||
<link rel="manifest" href="/favicon/site.webmanifest">
|
||||
|
||||
<style>
|
||||
:root { --navbar-h: 56px; }
|
||||
body { padding-top: var(--navbar-h); background: #f7f8fb; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user