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',
|
'proveedores','compras','deta_comp_producto',
|
||||||
'mate_primas','deta_comp_materias',
|
'mate_primas','deta_comp_materias',
|
||||||
'prov_producto','prov_mate_prima',
|
'prov_producto','prov_mate_prima',
|
||||||
'receta_producto'
|
'receta_producto', 'asistencia_resumen_diario',
|
||||||
|
'asistencia_intervalo'
|
||||||
];
|
];
|
||||||
|
|
||||||
const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||||
@ -235,6 +236,13 @@ app.get("/productos", (req, res) => {
|
|||||||
res.locals.pageId = "productos";
|
res.locals.pageId = "productos";
|
||||||
res.render("productos");
|
res.render("productos");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/usuarios', (req, res) => {
|
||||||
|
res.locals.pageTitle = 'Usuarios';
|
||||||
|
res.locals.pageId = 'usuarios';
|
||||||
|
res.render('usuarios');
|
||||||
|
});
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// API
|
// 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
|
// Verificación de conexión
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|||||||
@ -86,21 +86,58 @@
|
|||||||
productos: [],
|
productos: [],
|
||||||
mesas: [],
|
mesas: [],
|
||||||
usuarios: [],
|
usuarios: [],
|
||||||
carrito: [], // [{id_producto, nombre, pre_unitario, cantidad}]
|
categorias: [], // <--- NUEVO
|
||||||
|
carrito: [],
|
||||||
filtro: ''
|
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)
|
// Genera el HTML del ticket de cocina (80mm aprox)
|
||||||
function buildKitchenTicketHTML(data) {
|
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 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>
|
// Productos ya vienen con su "g" (grupo numérico 1..4/99) y cantidad formateada
|
||||||
<div class="name">${p.nombre}</div>
|
const items = data.productos.slice().sort((a,b)=> (a.g||99) - (b.g||99));
|
||||||
</div>
|
|
||||||
`).join('');
|
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>
|
return `<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
@ -109,66 +146,54 @@
|
|||||||
<title>Ticket Cocina</title>
|
<title>Ticket Cocina</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--w: 80mm; /* Cambia a 58mm si tu rollo es de 58 */
|
--w: 80mm;
|
||||||
--fz: 30px; /* Base más grande */
|
--fz-base: 16px;
|
||||||
--fz-sm: 13px;
|
--fz-md: 16px; /* observaciones */
|
||||||
--fz-lg: 20px; /* Filas de productos */
|
--fz-item: 18px; /* filas */
|
||||||
--fz-xl: 35px; /* Título */
|
--fz-xl: 26px; /* <--- NUEVO: tamaño “grande” (mesa) */
|
||||||
|
--fz-xxl: 34px; /* título (#comanda) */
|
||||||
|
--fz-sm: 12px;
|
||||||
}
|
}
|
||||||
html, body { margin:0; padding:0; }
|
html, body { margin:0; padding:0; }
|
||||||
body {
|
body {
|
||||||
width: var(--w);
|
width: var(--w);
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
font-size: var(--fz);
|
font-size: var(--fz-base);
|
||||||
font-weight: 700; /* TODO en negrita */
|
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
color:#000;
|
color:#000;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
#ticket { padding: 10px 8px; }
|
#ticket { padding: 10px 8px; }
|
||||||
.center { text-align:center; }
|
.center { text-align:center; }
|
||||||
.row { display:flex; gap:8px; margin: 4px 0; }
|
.row { display:flex; gap:8px; margin: 4px 0; }
|
||||||
.row .qty {
|
.row .qty { min-width: 22mm; font-size: var(--fz-item); letter-spacing:.2px; }
|
||||||
min-width: 24mm;
|
.row .name { flex:1; font-size: var(--fz-item); text-transform: uppercase; word-break: break-word; }
|
||||||
font-size: var(--fz-lg);
|
.hr { border-top: 2px dashed #000; margin: 8px 0; }
|
||||||
letter-spacing: 0.2px;
|
.hr.dotted { border-top: 2px dotted #000; }
|
||||||
}
|
|
||||||
.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); }
|
.small { font-size: var(--fz-sm); }
|
||||||
.bold { font-weight: 700; }
|
.bigline { font-size: var(--fz-xxl); text-transform: uppercase; }
|
||||||
.mt4 { margin-top: 4px; }
|
.mesa-medium { font-size: var(--fz-xl); text-transform: uppercase; }
|
||||||
.mt8 { margin-top: 8px; }
|
.obs { font-size: var(--fz-md); }
|
||||||
.mb4 { margin-bottom: 4px; }
|
.mt4{margin-top:4px}.mt8{margin-top:8px}.mb4{margin-bottom:4px}.mb8{margin-bottom:8px}
|
||||||
.mb8 { margin-bottom: 8px; }
|
|
||||||
.title { font-size: var(--fz-xl); letter-spacing: 0.3px; }
|
|
||||||
@page { size: var(--w) auto; margin: 0; }
|
@page { size: var(--w) auto; margin: 0; }
|
||||||
@media print { body { width: var(--w); } }
|
@media print { body { width: var(--w); } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="ticket">
|
<div id="ticket">
|
||||||
<div class="center bold title">COMANDA COCINA</div>
|
<!-- SIN TÍTULO -->
|
||||||
<div class="center small">#${data.id_comanda}</div>
|
<div class="center bigline">#${data.id_comanda}</div>
|
||||||
<div class="hr"></div>
|
<div class="center ${mesaClass}">${mesaTxt}</div>
|
||||||
|
|
||||||
<div class="small">Fecha: ${data.fecha} ${data.hora}</div>
|
<div class="small mt4">Fecha: ${data.fecha} ${data.hora}</div>
|
||||||
<div class="small">${mesaTxt}</div>
|
<div class="small mt4">Mozo: ${data.usuario || '—'}</div>
|
||||||
<div class="small">Mozo: ${data.usuario || '—'}</div>
|
${obs ? `<div class="obs mt8">Obs: ${obs}</div>` : ''}
|
||||||
|
|
||||||
${obs ? `<div class="mt8"><span class="bold">OBSERVACIONES:</span><br>${obs}</div>` : ''}
|
|
||||||
|
|
||||||
<div class="hr"></div>
|
<div class="hr"></div>
|
||||||
<div class="bold mb4">PRODUCTOS</div>
|
|
||||||
${productosHtml}
|
${productosHtml}
|
||||||
|
|
||||||
<div class="hr"></div>
|
<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 class="center mt8 small">— fin —</div>
|
||||||
</div>
|
</div>
|
||||||
<script>window.onload = () => { window.focus(); window.print(); }<\/script>
|
<script>window.onload = () => { window.focus(); window.print(); }<\/script>
|
||||||
@ -231,15 +256,21 @@
|
|||||||
|
|
||||||
// Carga inicial
|
// Carga inicial
|
||||||
async function init() {
|
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/productos?limit=1000'),
|
||||||
jget('/api/table/mesas?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.productos = prods.filter(p => p.activo !== false);
|
||||||
state.mesas = mesas;
|
state.mesas = mesas;
|
||||||
state.usuarios = usuarios.filter(u => u.activo !== false);
|
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();
|
hydrateMesas();
|
||||||
hydrateUsuarios();
|
hydrateUsuarios();
|
||||||
@ -450,6 +481,15 @@
|
|||||||
const units = cartSnapshot.reduce((acc, it) => acc + Number(it.cantidad || 0), 0);
|
const units = cartSnapshot.reduce((acc, it) => acc + Number(it.cantidad || 0), 0);
|
||||||
const items = cartSnapshot.length;
|
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({
|
const ticketHtml = buildKitchenTicketHTML({
|
||||||
id_comanda,
|
id_comanda,
|
||||||
fecha, hora,
|
fecha, hora,
|
||||||
@ -459,10 +499,7 @@
|
|||||||
observaciones,
|
observaciones,
|
||||||
items,
|
items,
|
||||||
units,
|
units,
|
||||||
productos: cartSnapshot.map(it => ({
|
productos: productosParaTicket // <--- con grupos
|
||||||
nombre: it.nombre,
|
|
||||||
cantidad: fmtQty(it.cantidad)
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await printHtmlViaIframe(ticketHtml);
|
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="/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="/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="/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="/usuarios">Usuarios</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/reportes">Reportes</a></li>
|
<!-- <li class="nav-item"><a class="nav-link" href="/reportes">Reportes</a></li> -->
|
||||||
<!-- agrega las que necesites -->
|
<!-- agrega las que necesites -->
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,9 @@
|
|||||||
{ text: "Nuevo producto", href: "/productos/nuevo" },
|
{ text: "Nuevo producto", href: "/productos/nuevo" },
|
||||||
{ text: "Importar catálogo", href: "/productos/importar" },
|
{ text: "Importar catálogo", href: "/productos/importar" },
|
||||||
{ text: "Reportes", href: "/reportes" },
|
{ 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