Desarrollo de views + frontend
This commit is contained in:
parent
09610df995
commit
44d1adecdc
80
services/manso/package-lock.json
generated
80
services/manso/package-lock.json
generated
@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "aplication",
|
"name": "workarround",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "aplication",
|
"name": "workarround",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.0",
|
"chalk": "^5.6.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-ejs-layouts": "^2.5.1",
|
"express-ejs-layouts": "^2.5.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
@ -56,11 +57,16 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/async": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
@ -345,6 +351,21 @@
|
|||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ejs": {
|
||||||
|
"version": "3.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||||
|
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"jake": "^10.8.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ejs": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
@ -446,6 +467,36 @@
|
|||||||
"resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz",
|
||||||
"integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA=="
|
"integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/filelist": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"minimatch": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/filelist/node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/filelist/node_modules/minimatch": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@ -732,6 +783,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/jake": {
|
||||||
|
"version": "10.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
|
||||||
|
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"async": "^3.2.6",
|
||||||
|
"filelist": "^1.0.4",
|
||||||
|
"picocolors": "^1.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"jake": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@ -1018,6 +1086,12 @@
|
|||||||
"split2": "^4.1.0"
|
"split2": "^4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
"chalk": "^5.6.0",
|
"chalk": "^5.6.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-ejs-layouts": "^2.5.1",
|
"express-ejs-layouts": "^2.5.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
|||||||
@ -35,6 +35,18 @@ app.use(express.json());
|
|||||||
app.use(express.json({ limit: '1mb' }));
|
app.use(express.json({ limit: '1mb' }));
|
||||||
app.use(express.static(path.join(__dirname, 'pages')));
|
app.use(express.static(path.join(__dirname, 'pages')));
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Motor de vistas EJS
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
app.set("views", path.join(__dirname, "views"));
|
||||||
|
app.set("view engine", "ejs");
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set("layout", "layouts/main");
|
||||||
|
|
||||||
|
// Archivos estáticos
|
||||||
|
app.use(express.static(path.join(__dirname, "public")));
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// Configuración de conexión PostgreSQL
|
// Configuración de conexión PostgreSQL
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
@ -50,16 +62,6 @@ const dbConfig = {
|
|||||||
|
|
||||||
const pool = new Pool(dbConfig);
|
const pool = new Pool(dbConfig);
|
||||||
|
|
||||||
// Helper de consulta con acquire/release explícito (del código original, referencial)
|
|
||||||
// async function q(text, params) {
|
|
||||||
// const client = await pool.connect();
|
|
||||||
// try {
|
|
||||||
// return await client.query(text, params);
|
|
||||||
// } finally {
|
|
||||||
// client.release();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// Seguridad: Tablas permitidas
|
// Seguridad: Tablas permitidas
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
@ -167,13 +169,45 @@ async function pickLabelColumn(client, refTable) {
|
|||||||
return rows[0]?.column_name || 'id';
|
return rows[0]?.column_name || 'id';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Middleware para datos globales
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.locals.currentPath = req.path;
|
||||||
|
res.locals.pageTitle = "SuiteCoffee";
|
||||||
|
res.locals.pageId = "";
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// Rutas de UI
|
// Rutas de UI
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.sendFile(path.join(__dirname, 'pages', 'dashboard.html'));
|
app.get("/", (req, res) => {
|
||||||
|
res.locals.pageTitle = "Inicio";
|
||||||
|
res.locals.pageId = "home";
|
||||||
|
res.render("estadoComandas");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// app.get('/', (req, res) => {
|
||||||
|
// res.sendFile(path.join(__dirname, 'pages', 'dashboard.html'));
|
||||||
|
// });
|
||||||
|
|
||||||
|
app.get('/comandas', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'pages', 'comandas.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/estadoComandas", (req, res) => {
|
||||||
|
res.locals.pageTitle = "Estado de Comandas";
|
||||||
|
res.locals.pageId = "estadoComandas";
|
||||||
|
res.render("estadoComandas");
|
||||||
|
});
|
||||||
|
|
||||||
|
// app.get('/estadoComandas', (req, res) => {
|
||||||
|
// res.sendFile(path.join(__dirname, 'pages', 'estadoComandas.html'));
|
||||||
|
// });
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// API
|
// API
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
@ -291,6 +325,85 @@ app.post('/api/table/:table', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listado (con join) y totales por comanda
|
||||||
|
app.get('/api/comandas', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const estado = (req.query.estado || '').trim();
|
||||||
|
const limit = Math.min(parseInt(req.query.limit || '200', 10), 1000);
|
||||||
|
const params = [];
|
||||||
|
let where = '';
|
||||||
|
if (estado) { params.push(estado); where = `WHERE c.estado = $${params.length}`; }
|
||||||
|
params.push(limit);
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
WITH items AS (
|
||||||
|
SELECT d.id_comanda,
|
||||||
|
COUNT(*) AS items,
|
||||||
|
SUM(d.cantidad * d.pre_unitario) AS total
|
||||||
|
FROM deta_comandas d
|
||||||
|
GROUP BY d.id_comanda
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
c.id_comanda, c.fec_creacion, c.estado, c.observaciones,
|
||||||
|
u.id_usuario, u.nombre AS usuario_nombre, u.apellido AS usuario_apellido,
|
||||||
|
m.id_mesa, m.numero AS mesa_numero, m.apodo AS mesa_apodo,
|
||||||
|
COALESCE(i.items, 0) AS items,
|
||||||
|
COALESCE(i.total, 0) AS total
|
||||||
|
FROM comandas c
|
||||||
|
JOIN usuarios u ON u.id_usuario = c.id_usuario
|
||||||
|
JOIN mesas m ON m.id_mesa = c.id_mesa
|
||||||
|
LEFT JOIN items i ON i.id_comanda = c.id_comanda
|
||||||
|
${where}
|
||||||
|
ORDER BY c.id_comanda DESC
|
||||||
|
LIMIT $${params.length}
|
||||||
|
`;
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
const { rows } = await client.query(sql, params);
|
||||||
|
res.json(rows);
|
||||||
|
} finally { client.release(); }
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 (!id) return res.status(400).json({ error: 'id inválido' });
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT d.id_det_comanda, d.id_producto, p.nombre AS producto_nombre,
|
||||||
|
d.cantidad, d.pre_unitario, (d.cantidad * d.pre_unitario) AS subtotal,
|
||||||
|
d.observaciones
|
||||||
|
FROM deta_comandas d
|
||||||
|
JOIN productos p ON p.id_producto = d.id_producto
|
||||||
|
WHERE d.id_comanda = $1
|
||||||
|
ORDER BY d.id_det_comanda
|
||||||
|
`;
|
||||||
|
const { rows } = await pool.query(sql, [id]);
|
||||||
|
res.json(rows);
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cambiar estado (abrir/cerrar)
|
||||||
|
app.post('/api/comandas/:id/estado', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id, 10);
|
||||||
|
let { estado } = req.body || {};
|
||||||
|
if (!id) return res.status(400).json({ error: 'id inválido' });
|
||||||
|
|
||||||
|
const allowed = new Set(['abierta','cerrada','pagada','anulada']);
|
||||||
|
if (!allowed.has(estado)) return res.status(400).json({ error: 'estado inválido' });
|
||||||
|
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`UPDATE comandas SET estado = $2 WHERE id_comanda = $1 RETURNING *`,
|
||||||
|
[id, estado]
|
||||||
|
);
|
||||||
|
if (!rows.length) return res.status(404).json({ error: 'comanda no encontrada' });
|
||||||
|
res.json({ updated: rows[0] });
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// Verificación de conexión
|
// Verificación de conexión
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
@ -310,7 +423,6 @@ async function verificarConexion() {
|
|||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// Inicio del servidor
|
// Inicio del servidor
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
app.use(expressLayouts);
|
|
||||||
|
|
||||||
const PORT = process.env.PORT ? Number(process.env.PORT) : 3000;
|
const PORT = process.env.PORT ? Number(process.env.PORT) : 3000;
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
|
|||||||
@ -1,70 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="es">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Categorías</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Categorías</h1>
|
|
||||||
|
|
||||||
<h2>Crear categoría</h2>
|
|
||||||
<form id="form-categoria">
|
|
||||||
<label>Nombre:
|
|
||||||
<input type="text" name="nombre" required />
|
|
||||||
</label>
|
|
||||||
<label>Visible:
|
|
||||||
<select name="visible">
|
|
||||||
<option value="true" selected>Sí</option>
|
|
||||||
<option value="false">No</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<button type="submit">Guardar</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<h2>Listado</h2>
|
|
||||||
<button id="btn-recargar">Recargar</button>
|
|
||||||
<table border="1" cellpadding="6">
|
|
||||||
<thead><tr><th>ID</th><th>Nombre</th><th>Visible</th></tr></thead>
|
|
||||||
<tbody id="tbody"></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const API = '/api/categorias';
|
|
||||||
|
|
||||||
async function listar() {
|
|
||||||
const res = await fetch(API);
|
|
||||||
const data = await res.json();
|
|
||||||
const tbody = document.getElementById('tbody');
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
data.forEach(c => {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.innerHTML = `<td>${c.id_categoria}</td><td>${c.nombre}</td><td>${c.visible ? 'Sí' : 'No'}</td>`;
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('form-categoria').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const fd = new FormData(e.target);
|
|
||||||
const nombre = fd.get('nombre').trim();
|
|
||||||
const visible = fd.get('visible') === 'true';
|
|
||||||
if (!nombre) return;
|
|
||||||
const res = await fetch(API, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type':'application/json'},
|
|
||||||
body: JSON.stringify({ nombre, visible })
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const err = await res.json().catch(()=>({error:'Error'}));
|
|
||||||
alert('Error: ' + (err.error || res.statusText));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.target.reset();
|
|
||||||
await listar();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btn-recargar').addEventListener('click', listar);
|
|
||||||
listar();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
355
services/manso/src/pages/comandas.html
Normal file
355
services/manso/src/pages/comandas.html
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
<!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>
|
||||||
280
services/manso/src/pages/estadoComandas.html.bak
Normal file
280
services/manso/src/pages/estadoComandas.html.bak
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
<!-- 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>
|
||||||
@ -1,106 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="es">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Productos</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Productos</h1>
|
|
||||||
|
|
||||||
<h2>Crear producto</h2>
|
|
||||||
<form id="form-producto">
|
|
||||||
<div>
|
|
||||||
<label>Nombre:
|
|
||||||
<input name="nombre" type="text" required />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Precio:
|
|
||||||
<input name="precio" type="number" step="0.01" min="0" required />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Categoría:
|
|
||||||
<select name="id_categoria" id="sel-categoria" required></select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button type="submit">Guardar</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<h2>Listado</h2>
|
|
||||||
<button id="btn-recargar">Recargar</button>
|
|
||||||
<table border="1" cellpadding="6">
|
|
||||||
<thead>
|
|
||||||
<tr><th>ID</th><th>Nombre</th><th>Precio</th><th>Activo</th><th>ID Categoría</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="tbody"></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const API = '/api/productos';
|
|
||||||
const API_CAT = '/api/categorias';
|
|
||||||
|
|
||||||
async function cargarCategorias() {
|
|
||||||
const res = await fetch(API_CAT);
|
|
||||||
const data = await res.json();
|
|
||||||
const sel = document.getElementById('sel-categoria');
|
|
||||||
sel.innerHTML = '<option value="" disabled selected>Seleccione...</option>';
|
|
||||||
data.forEach(c => {
|
|
||||||
const opt = document.createElement('option');
|
|
||||||
opt.value = c.id_categoria;
|
|
||||||
opt.textContent = `${c.id_categoria} - ${c.nombre}`;
|
|
||||||
sel.appendChild(opt);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listar() {
|
|
||||||
const res = await fetch(API);
|
|
||||||
const data = await res.json();
|
|
||||||
const tbody = document.getElementById('tbody');
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
data.forEach(p => {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.innerHTML = `
|
|
||||||
<td>${p.id_producto}</td>
|
|
||||||
<td>${p.nombre}</td>
|
|
||||||
<td>${Number(p.precio).toFixed(2)}</td>
|
|
||||||
<td>${p.activo ? 'Sí' : 'No'}</td>
|
|
||||||
<td>${p.id_categoria}</td>
|
|
||||||
`;
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('form-producto').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const fd = new FormData(e.target);
|
|
||||||
const payload = {
|
|
||||||
nombre: fd.get('nombre').trim(),
|
|
||||||
precio: parseFloat(fd.get('precio')),
|
|
||||||
id_categoria: parseInt(fd.get('id_categoria'), 10)
|
|
||||||
};
|
|
||||||
if (!payload.nombre || isNaN(payload.precio) || isNaN(payload.id_categoria)) return;
|
|
||||||
|
|
||||||
const res = await fetch(API, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type':'application/json'},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const err = await res.json().catch(()=>({error:'Error'}));
|
|
||||||
alert('Error: ' + (err.error || res.statusText));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.target.reset();
|
|
||||||
await listar();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btn-recargar').addEventListener('click', listar);
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
await cargarCategorias();
|
|
||||||
await listar();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="es">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Roles</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Roles</h1>
|
|
||||||
|
|
||||||
<h2>Crear rol</h2>
|
|
||||||
<form id="form-rol">
|
|
||||||
<label>Nombre del rol:
|
|
||||||
<input type="text" name="nombre" required />
|
|
||||||
</label>
|
|
||||||
<button type="submit">Guardar</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<h2>Listado</h2>
|
|
||||||
<button id="btn-recargar">Recargar</button>
|
|
||||||
<table border="1" cellpadding="6">
|
|
||||||
<thead><tr><th>ID</th><th>Nombre</th></tr></thead>
|
|
||||||
<tbody id="tbody"></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const API = '/api/roles';
|
|
||||||
|
|
||||||
async function listar() {
|
|
||||||
const res = await fetch(API);
|
|
||||||
const data = await res.json();
|
|
||||||
const tbody = document.getElementById('tbody');
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
data.forEach(r => {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.innerHTML = `<td>${r.id_rol}</td><td>${r.nombre}</td>`;
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('form-rol').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const nombre = e.target.nombre.value.trim();
|
|
||||||
if (!nombre) return;
|
|
||||||
const res = await fetch(API, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type':'application/json'},
|
|
||||||
body: JSON.stringify({ nombre })
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const err = await res.json().catch(()=>({error:'Error'}));
|
|
||||||
alert('Error: ' + (err.error || res.statusText));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.target.reset();
|
|
||||||
await listar();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btn-recargar').addEventListener('click', listar);
|
|
||||||
listar();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="es">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Usuarios</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Usuarios</h1>
|
|
||||||
|
|
||||||
<h2>Crear usuario</h2>
|
|
||||||
<form id="form-usuario">
|
|
||||||
<div>
|
|
||||||
<label>Documento:
|
|
||||||
<input name="documento" type="text" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Nombre:
|
|
||||||
<input name="nombre" type="text" required />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Apellido:
|
|
||||||
<input name="apellido" type="text" required />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Correo:
|
|
||||||
<input name="correo" type="email" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Teléfono:
|
|
||||||
<input name="telefono" type="text" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Fecha de nacimiento:
|
|
||||||
<input name="fec_nacimiento" type="date" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button type="submit">Guardar</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<h2>Listado</h2>
|
|
||||||
<button id="btn-recargar">Recargar</button>
|
|
||||||
<table border="1" cellpadding="6">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th><th>Documento</th><th>Nombre</th><th>Apellido</th>
|
|
||||||
<th>Correo</th><th>Teléfono</th><th>Nacimiento</th><th>Activo</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="tbody"></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const API = '/api/usuarios';
|
|
||||||
|
|
||||||
async function listar() {
|
|
||||||
const res = await fetch(API);
|
|
||||||
const data = await res.json();
|
|
||||||
const tbody = document.getElementById('tbody');
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
data.forEach(u => {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.innerHTML = `
|
|
||||||
<td>${u.id_usuario}</td>
|
|
||||||
<td>${u.documento ?? ''}</td>
|
|
||||||
<td>${u.nombre}</td>
|
|
||||||
<td>${u.apellido}</td>
|
|
||||||
<td>${u.correo ?? ''}</td>
|
|
||||||
<td>${u.telefono ?? ''}</td>
|
|
||||||
<td>${u.fec_nacimiento ? u.fec_nacimiento.substring(0,10) : ''}</td>
|
|
||||||
<td>${u.activo ? 'Sí' : 'No'}</td>
|
|
||||||
`;
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('form-usuario').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const fd = new FormData(e.target);
|
|
||||||
const payload = Object.fromEntries(fd.entries());
|
|
||||||
if (payload.fec_nacimiento === '') delete payload.fec_nacimiento;
|
|
||||||
const res = await fetch(API, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type':'application/json'},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const err = await res.json().catch(()=>({error:'Error'}));
|
|
||||||
alert('Error: ' + (err.error || res.statusText));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.target.reset();
|
|
||||||
await listar();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btn-recargar').addEventListener('click', listar);
|
|
||||||
listar();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
18
services/manso/src/views/estadoComanas.ejs
Normal file
18
services/manso/src/views/estadoComanas.ejs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
|
<h1 class="h4 m-0">Estado de Comandas</h1>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="soloAbiertas">
|
||||||
|
<label class="form-check-label" for="soloAbiertas">Solo abiertas</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- tu tabla/listado acá -->
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>ID</th><th>Mesa</th><th>Estado</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>1</td><td>5</td>
|
||||||
|
<td><span class="badge badge-outline badge-estado-abierta">Abierta</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
16
services/manso/src/views/layouts/main.ejs
Normal file
16
services/manso/src/views/layouts/main.ejs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<% include ../partials/_head %>
|
||||||
|
</head>
|
||||||
|
<body data-page="<%= pageId %>">
|
||||||
|
<% include ../partials/_navbar %>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<%- body %>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<% include ../partials/_sidebar %>
|
||||||
|
<% include ../partials/_footer %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
42
services/manso/src/views/partials/_footer.ejs
Normal file
42
services/manso/src/views/partials/_footer.ejs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<!-- /partials/_footer.html -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* (Solo si usas HTML estático)
|
||||||
|
* Carga “partials” desde elementos con [data-include="/partials/..."].
|
||||||
|
* Si usas EJS/templating, podés quitar esto.
|
||||||
|
*/
|
||||||
|
async function scLoadPartials(){
|
||||||
|
const includes = document.querySelectorAll("[data-include]");
|
||||||
|
for (const el of includes) {
|
||||||
|
const url = el.getAttribute("data-include");
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {cache:"no-store"});
|
||||||
|
el.innerHTML = await res.text();
|
||||||
|
} catch (e) {
|
||||||
|
el.innerHTML = `<div class="text-danger small">No se pudo cargar ${url}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Export util por si querés llamarlo manualmente
|
||||||
|
window.scLoadPartials = scLoadPartials;
|
||||||
|
|
||||||
|
// Eventos genéricos que el sidebar dispara (ajustá a tu lógica real)
|
||||||
|
window.addEventListener("sc:toggle-abiertas", () => {
|
||||||
|
// Ej.: togglear checkbox/estado en páginas que lo usen
|
||||||
|
const chk = document.getElementById("soloAbiertas");
|
||||||
|
if (chk) { chk.checked = !chk.checked; chk.dispatchEvent(new Event("change")); }
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("sc:export-csv", () => {
|
||||||
|
// Implementá tu export acá
|
||||||
|
if (window.scExportCsv) return window.scExportCsv();
|
||||||
|
alert("Exportar CSV: implementame 😄");
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("sc:refresh-list", () => {
|
||||||
|
if (window.scRefreshList) return window.scRefreshList();
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
22
services/manso/src/views/partials/_head.ejs
Normal file
22
services/manso/src/views/partials/_head.ejs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!-- /partials/_head.html -->
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title><%= typeof pageTitle !== "undefined" ? pageTitle : "SuiteCoffee" %></title>
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root { --navbar-h: 56px; }
|
||||||
|
body { padding-top: var(--navbar-h); background: #f7f8fb; }
|
||||||
|
.brand-mini { font-weight: 700; letter-spacing: .2px; }
|
||||||
|
/* Layout contenedor principal */
|
||||||
|
main { padding-block: 1rem 2rem; }
|
||||||
|
/* Tabla compacta */
|
||||||
|
.table-sm th, .table-sm td { padding: .5rem .6rem; }
|
||||||
|
/* Chips/etiquetas de estado */
|
||||||
|
.badge-outline { border: 1px solid #dee2e6; background: #fff; color: #495057; }
|
||||||
|
.badge-estado-abierta { border-color:#198754; color:#198754; }
|
||||||
|
.badge-estado-cerrada { border-color:#6c757d; color:#6c757d; }
|
||||||
|
.badge-estado-anulada { border-color:#dc3545; color:#dc3545; }
|
||||||
|
.badge-estado-pagada { border-color:#146c43; color:#146c43; }
|
||||||
|
</style>
|
||||||
29
services/manso/src/views/partials/_navbar.ejs
Normal file
29
services/manso/src/views/partials/_navbar.ejs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<!-- /partials/_navbar.html -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom fixed-top">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand brand-mini" href="/">SuiteCoffee</a>
|
||||||
|
|
||||||
|
<!-- Links principales (colapsables en mobile) -->
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#scNav" aria-controls="scNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span> <!-- hamburguesa principal -->
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="scNav">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0 small">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/comandas">Comandas</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/estadoComandas">Estado</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/productos">Productos</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/mesas">Mesas</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/reportes">Reportes</a></li>
|
||||||
|
<!-- agrega las que necesites -->
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Botón “hamburguesa” para abrir el menú contextual (sidebar derecha) -->
|
||||||
|
<button class="btn btn-outline-secondary btn-sm d-flex align-items-center" type="button"
|
||||||
|
data-bs-toggle="offcanvas" data-bs-target="#scSidebar" aria-controls="scSidebar">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" class="me-1" viewBox="0 0 24 24" fill="none" stroke="currentColor"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||||
|
Opciones
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
62
services/manso/src/views/partials/_sidebar.ejs
Normal file
62
services/manso/src/views/partials/_sidebar.ejs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<!-- /partials/_sidebar.html -->
|
||||||
|
<div class="offcanvas offcanvas-end" tabindex="-1" id="scSidebar" aria-labelledby="scSidebarLabel">
|
||||||
|
<div class="offcanvas-header">
|
||||||
|
<h5 class="offcanvas-title" id="scSidebarLabel">Opciones</h5>
|
||||||
|
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Cerrar"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body">
|
||||||
|
<!-- Contenido se inyecta según la página actual -->
|
||||||
|
<div id="scSidebarContent" class="list-group list-group-flush small"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Map de opciones por página. Usa body[data-page] o window.scPageId.
|
||||||
|
const SC_SIDEBAR_ITEMS = {
|
||||||
|
// === ejemplos ===
|
||||||
|
"estadoComandas": [
|
||||||
|
{ text: "➕ Nueva comanda", href: "/comandas" },
|
||||||
|
{ text: "Solo abiertas", href: "#", attr: { "data-action": "toggle-abiertas" } },
|
||||||
|
{ text: "Exportar (CSV)", href: "#", attr: { "data-action": "export-csv" } },
|
||||||
|
{ text: "Actualizar listado", href: "#", attr: { "data-action": "refresh-list" } },
|
||||||
|
],
|
||||||
|
"comandas": [
|
||||||
|
{ text: "Volver a Estado", href: "/estadoComandas" },
|
||||||
|
{ text: "Cargar productos", href: "/productos" },
|
||||||
|
{ text: "Mesas", href: "/mesas" },
|
||||||
|
],
|
||||||
|
"productos": [
|
||||||
|
{ text: "Nuevo producto", href: "/productos/nuevo" },
|
||||||
|
{ text: "Importar catálogo", href: "/productos/importar" },
|
||||||
|
{ text: "Reportes", href: "/reportes" },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
(function initSidebar(){
|
||||||
|
const page = (document.body.dataset.page || window.scPageId || "").trim();
|
||||||
|
const items = SC_SIDEBAR_ITEMS[page] || [
|
||||||
|
{ text: "Inicio", href: "/" }
|
||||||
|
];
|
||||||
|
const box = document.getElementById("scSidebarContent");
|
||||||
|
box.innerHTML = "";
|
||||||
|
for (const it of items) {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.className = "list-group-item list-group-item-action";
|
||||||
|
a.textContent = it.text;
|
||||||
|
a.href = it.href || "#";
|
||||||
|
if (it.attr) for (const [k,v] of Object.entries(it.attr)) a.setAttribute(k,v);
|
||||||
|
box.appendChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acciones ejemplo (opcionales). Adaptá a tus funciones reales.
|
||||||
|
box.addEventListener("click", (ev) => {
|
||||||
|
const a = ev.target.closest("a[data-action]");
|
||||||
|
if (!a) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
const action = a.getAttribute("data-action");
|
||||||
|
if (action === "toggle-abiertas") window.dispatchEvent(new CustomEvent("sc:toggle-abiertas"));
|
||||||
|
if (action === "export-csv") window.dispatchEvent(new CustomEvent("sc:export-csv"));
|
||||||
|
if (action === "refresh-list") window.dispatchEvent(new CustomEvent("sc:refresh-list"));
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user