Creación de sección Usuarios para administrar las entradas y salidas del personal
This commit is contained in:
parent
9c5219863b
commit
c9b4b4871d
@ -83,7 +83,8 @@ const ALLOWED_TABLES = [
|
||||
'proveedores','compras','deta_comp_producto',
|
||||
'mate_primas','deta_comp_materias',
|
||||
'prov_producto','prov_mate_prima',
|
||||
'receta_producto'
|
||||
'receta_producto', 'asistencia_resumen_diario',
|
||||
'asistencia_intervalo'
|
||||
];
|
||||
|
||||
const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||
@ -235,6 +236,13 @@ app.get("/productos", (req, res) => {
|
||||
res.locals.pageId = "productos";
|
||||
res.render("productos");
|
||||
});
|
||||
|
||||
app.get('/usuarios', (req, res) => {
|
||||
res.locals.pageTitle = 'Usuarios';
|
||||
res.locals.pageId = 'usuarios';
|
||||
res.render('usuarios');
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// API
|
||||
// ----------------------------------------------------------
|
||||
@ -578,6 +586,71 @@ app.post('/api/rpc/save_materia', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/rpc/find_usuarios_por_documentos { docs: ["12345678","09123456", ...] }
|
||||
app.post('/api/rpc/find_usuarios_por_documentos', async (req, res) => {
|
||||
try {
|
||||
const docs = Array.isArray(req.body?.docs) ? req.body.docs : [];
|
||||
const sql = 'SELECT public.find_usuarios_por_documentos($1::jsonb) AS data';
|
||||
const { rows } = await pool.query(sql, [JSON.stringify(docs)]);
|
||||
res.json(rows[0]?.data || {});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: 'find_usuarios_por_documentos failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/rpc/import_asistencia { registros: [...], origen?: "AGL_001.txt" }
|
||||
app.post('/api/rpc/import_asistencia', async (req, res) => {
|
||||
try {
|
||||
const registros = Array.isArray(req.body?.registros) ? req.body.registros : [];
|
||||
const origen = req.body?.origen || null;
|
||||
const sql = 'SELECT public.import_asistencia($1::jsonb,$2) AS data';
|
||||
const { rows } = await pool.query(sql, [JSON.stringify(registros), origen]);
|
||||
res.json(rows[0]?.data || {});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: 'import_asistencia failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Consultar datos de asistencia (raw + pares) para un usuario y rango
|
||||
app.post('/api/rpc/asistencia_get', async (req, res) => {
|
||||
try {
|
||||
const { doc, desde, hasta } = req.body || {};
|
||||
const sql = 'SELECT public.asistencia_get($1::text,$2::date,$3::date) AS data';
|
||||
const { rows } = await pool.query(sql, [doc, desde, hasta]);
|
||||
res.json(rows[0]?.data || {});
|
||||
} catch (e) {
|
||||
console.error(e); res.status(500).json({ error: 'asistencia_get failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Editar un registro crudo y recalcular pares
|
||||
app.post('/api/rpc/asistencia_update_raw', async (req, res) => {
|
||||
try {
|
||||
const { id_raw, fecha, hora, modo } = req.body || {};
|
||||
const sql = 'SELECT public.asistencia_update_raw($1::bigint,$2::date,$3::text,$4::text) AS data';
|
||||
const { rows } = await pool.query(sql, [id_raw, fecha, hora, modo ?? null]);
|
||||
res.json(rows[0]?.data || {});
|
||||
} catch (e) {
|
||||
console.error(e); res.status(500).json({ error: 'asistencia_update_raw failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Eliminar un registro crudo y recalcular pares
|
||||
app.post('/api/rpc/asistencia_delete_raw', async (req, res) => {
|
||||
try {
|
||||
const { id_raw } = req.body || {};
|
||||
const sql = 'SELECT public.asistencia_delete_raw($1::bigint) AS data';
|
||||
const { rows } = await pool.query(sql, [id_raw]);
|
||||
res.json(rows[0]?.data || {});
|
||||
} catch (e) {
|
||||
console.error(e); res.status(500).json({ error: 'asistencia_delete_raw failed' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Verificación de conexión
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@ -86,21 +86,58 @@
|
||||
productos: [],
|
||||
mesas: [],
|
||||
usuarios: [],
|
||||
carrito: [], // [{id_producto, nombre, pre_unitario, cantidad}]
|
||||
categorias: [], // <--- NUEVO
|
||||
carrito: [],
|
||||
filtro: ''
|
||||
};
|
||||
|
||||
function norm(s='') {
|
||||
return s.toString().toLowerCase()
|
||||
.normalize('NFD').replace(/\p{Diacritic}/gu,''); // "café" -> "cafe"
|
||||
}
|
||||
|
||||
function isTakeaway(apodo) {
|
||||
return /^takeaway$/i.test(String(apodo || '').trim());
|
||||
}
|
||||
|
||||
function groupOrderByCatName(catName='') {
|
||||
const n = norm(catName);
|
||||
if (n.includes('bar')) return 1;
|
||||
if (n.includes('cafe')) return 2;
|
||||
if (n.includes('cafeter')) return 3;
|
||||
if (n.includes('trago') || n.includes('refresc')) return 4;
|
||||
return 99; // otros
|
||||
}
|
||||
|
||||
// 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 apodo = String(data.mesa_apodo ?? '').trim();
|
||||
const numero = data.mesa_numero ?? '';
|
||||
const take = isTakeaway(apodo);
|
||||
|
||||
const mesaTxt = take ? apodo.toUpperCase() : `Mesa #${numero}${apodo ? ' · ' + apodo : ''}`;
|
||||
// const isTakeaway = /Takeaway/i.test(String(data.mesa_apodo ?? '')) || /Takeaway/i.test(String(data.mesa_numero ?? ''));
|
||||
const mesaClass = take ? 'bigline' : 'mesa-medium';
|
||||
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('');
|
||||
|
||||
|
||||
// Productos ya vienen con su "g" (grupo numérico 1..4/99) y cantidad formateada
|
||||
const items = data.productos.slice().sort((a,b)=> (a.g||99) - (b.g||99));
|
||||
|
||||
let productosHtml = '';
|
||||
let prevG = null;
|
||||
for (const p of items) {
|
||||
if (prevG !== null && p.g !== prevG) {
|
||||
productosHtml += `<div class="hr dotted"></div>`; // separación punteada entre grupos
|
||||
}
|
||||
productosHtml += `
|
||||
<div class="row">
|
||||
<div class="qty">x${p.cantidad}</div>
|
||||
<div class="name">${p.nombre}</div>
|
||||
</div>`;
|
||||
prevG = p.g;
|
||||
}
|
||||
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
@ -109,66 +146,54 @@
|
||||
<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 */
|
||||
--w: 80mm;
|
||||
--fz-base: 16px;
|
||||
--fz-md: 16px; /* observaciones */
|
||||
--fz-item: 18px; /* filas */
|
||||
--fz-xl: 26px; /* <--- NUEVO: tamaño “grande” (mesa) */
|
||||
--fz-xxl: 34px; /* título (#comanda) */
|
||||
--fz-sm: 12px;
|
||||
}
|
||||
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 */
|
||||
font-size: var(--fz-base);
|
||||
line-height: 1.35;
|
||||
color:#000;
|
||||
font-weight: 700;
|
||||
}
|
||||
#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 */
|
||||
.row .qty { min-width: 22mm; font-size: var(--fz-item); letter-spacing:.2px; }
|
||||
.row .name { flex:1; font-size: var(--fz-item); text-transform: uppercase; word-break: break-word; }
|
||||
.hr { border-top: 2px dashed #000; margin: 8px 0; }
|
||||
.hr.dotted { border-top: 2px dotted #000; }
|
||||
.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; }
|
||||
.bigline { font-size: var(--fz-xxl); text-transform: uppercase; }
|
||||
.mesa-medium { font-size: var(--fz-xl); text-transform: uppercase; }
|
||||
.obs { font-size: var(--fz-md); }
|
||||
.mt4{margin-top:4px}.mt8{margin-top:8px}.mb4{margin-bottom:4px}.mb8{margin-bottom:8px}
|
||||
@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>
|
||||
<!-- SIN TÍTULO -->
|
||||
<div class="center bigline">#${data.id_comanda}</div>
|
||||
<div class="center ${mesaClass}">${mesaTxt}</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="small mt4">Fecha: ${data.fecha} ${data.hora}</div>
|
||||
<div class="small mt4">Mozo: ${data.usuario || '—'}</div>
|
||||
${obs ? `<div class="obs mt8">Obs: ${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="small">Ítems: ${data.items} · Unidades: ${data.units}</div>
|
||||
<div class="center mt8 small">— fin —</div>
|
||||
</div>
|
||||
<script>window.onload = () => { window.focus(); window.print(); }<\/script>
|
||||
@ -231,15 +256,21 @@
|
||||
|
||||
// Carga inicial
|
||||
async function init() {
|
||||
const [prods, mesas, usuarios] = await Promise.all([
|
||||
const [prods, mesas, usuarios, categorias] = await Promise.all([
|
||||
jget('/api/table/productos?limit=1000'),
|
||||
jget('/api/table/mesas?limit=1000'),
|
||||
jget('/api/table/usuarios?limit=1000')
|
||||
jget('/api/table/usuarios?limit=1000'),
|
||||
jget('/api/table/categorias?limit=1000') // <--- NUEVO
|
||||
]);
|
||||
|
||||
state.productos = prods.filter(p => p.activo !== false);
|
||||
state.mesas = mesas;
|
||||
state.usuarios = usuarios.filter(u => u.activo !== false);
|
||||
state.categorias = Array.isArray(categorias) ? categorias : [];
|
||||
|
||||
// Mapas para buscar categoría por id de producto
|
||||
state.catById = new Map(state.categorias.map(c => [c.id_categoria, (c.nombre||'').toString()]));
|
||||
state.prodCatNameById = new Map(state.productos.map(p => [p.id_producto, state.catById.get(p.id_categoria)||'']));
|
||||
|
||||
hydrateMesas();
|
||||
hydrateUsuarios();
|
||||
@ -450,6 +481,15 @@
|
||||
const units = cartSnapshot.reduce((acc, it) => acc + Number(it.cantidad || 0), 0);
|
||||
const items = cartSnapshot.length;
|
||||
|
||||
// map producto -> nombre de categoría
|
||||
const prodCat = state.prodCatNameById || new Map();
|
||||
|
||||
const productosParaTicket = cartSnapshot.map(it => ({
|
||||
nombre: it.nombre,
|
||||
cantidad: fmtQty(it.cantidad),
|
||||
g: groupOrderByCatName(prodCat.get(it.id_producto) || '') // 1..4/99
|
||||
}));
|
||||
|
||||
const ticketHtml = buildKitchenTicketHTML({
|
||||
id_comanda,
|
||||
fecha, hora,
|
||||
@ -459,10 +499,7 @@
|
||||
observaciones,
|
||||
items,
|
||||
units,
|
||||
productos: cartSnapshot.map(it => ({
|
||||
nombre: it.nombre,
|
||||
cantidad: fmtQty(it.cantidad)
|
||||
}))
|
||||
productos: productosParaTicket // <--- con grupos
|
||||
});
|
||||
|
||||
await printHtmlViaIframe(ticketHtml);
|
||||
|
||||
@ -13,8 +13,8 @@
|
||||
<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>
|
||||
<li class="nav-item"><a class="nav-link" href="/usuarios">Usuarios</a></li>
|
||||
<!-- <li class="nav-item"><a class="nav-link" href="/reportes">Reportes</a></li> -->
|
||||
<!-- agrega las que necesites -->
|
||||
</ul>
|
||||
|
||||
|
||||
@ -36,6 +36,9 @@
|
||||
{ text: "Nuevo producto", href: "/productos/nuevo" },
|
||||
{ text: "Importar catálogo", href: "/productos/importar" },
|
||||
{ text: "Reportes", href: "/reportes" },
|
||||
],
|
||||
"usuarios": [
|
||||
{ text: "Exportar (CSV)", href: "#", attr: { "data-action": "export-csv" } },
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
1030
services/manso/src/views/usuarios.ejs
Normal file
1030
services/manso/src/views/usuarios.ejs
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user