.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
// @suitecoffee/api/api.mjs
|
||||
// packages/api/api.mjs
|
||||
// Punto de entrada general del paquete de api.
|
||||
|
||||
export { default as apiv1 } from './v1/apiv1.mjs';
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@suitecoffee/api",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./api.mjs",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./api.mjs",
|
||||
"default": "./api.mjs"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
".api.mjs"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// packages/api/v1/apiv1.mjs
|
||||
import { Router } from 'express';
|
||||
|
||||
// Sub-routers (cada uno define sus propios paths absolutos)
|
||||
import comandasApiRouter from './routes/comandas.mjs';
|
||||
// import productosApiRouter from './routes/productos.mjs'; // cuando exista
|
||||
// import clientesApiRouter from './routes/clientes.mjs'; // etc.
|
||||
|
||||
const apiv1 = Router();
|
||||
|
||||
// Monta routers (no pongas prefijo aquí porque ya lo tienen adentro)
|
||||
apiv1.use(comandasApiRouter);
|
||||
// apiv1.use(productosApiRouter);
|
||||
// apiv1.use(clientesApiRouter);
|
||||
|
||||
|
||||
export default apiv1;
|
||||
|
||||
// (Opcional) re-export para tests puntuales
|
||||
// export { comandasApiRouter };
|
||||
// export { productosApiRouter };
|
||||
@@ -0,0 +1,111 @@
|
||||
// packages/api/v1/repositories/comandasRepo.mjs
|
||||
|
||||
import { withTenantClient } from './db.mjs';
|
||||
import { loadColumns, loadPrimaryKey } from '../routes/utils/schemaInspector.mjs';
|
||||
|
||||
const TABLE = 'comandas';
|
||||
const VALID_IDENT = /^[a-z_][a-z0-9_]*$/i;
|
||||
|
||||
export async function listComandas({ schema, abierta, limit }) {
|
||||
return withTenantClient(schema, async (db) => {
|
||||
const max = Math.min(parseInt(limit || 200, 10), 1000);
|
||||
const { rows } = await db.query(
|
||||
`SELECT * FROM public.f_comandas_resumen($1, $2)`,
|
||||
[abierta, max]
|
||||
);
|
||||
return rows;
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDetalleItems({ schema, id }) {
|
||||
return withTenantClient(schema, async (db) => {
|
||||
const { rows } = await db.query(
|
||||
`SELECT id_det_comanda, id_producto, producto_nombre,
|
||||
cantidad, pre_unitario, subtotal, observaciones
|
||||
FROM public.v_comandas_detalle_items
|
||||
WHERE id_comanda = $1::int
|
||||
ORDER BY id_det_comanda`,
|
||||
[id]
|
||||
);
|
||||
return rows;
|
||||
});
|
||||
}
|
||||
|
||||
export async function abrirComanda({ schema, id }) {
|
||||
return withTenantClient(schema, async (db) => {
|
||||
const st = await db.query(`SELECT eliminada FROM public.${q(TABLE)} WHERE id_comanda = $1`, [id]);
|
||||
if (!st.rowCount) return null;
|
||||
if (st.rows[0].eliminada === true) {
|
||||
const err = new Error('Comanda eliminada. Debe restaurarse antes de abrir.');
|
||||
err.http = { status: 409 };
|
||||
throw err;
|
||||
}
|
||||
const { rows } = await db.query(`SELECT public.f_abrir_comanda($1) AS data`, [id]);
|
||||
return rows[0]?.data || null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function cerrarComanda({ schema, id }) {
|
||||
return withTenantClient(schema, async (db) => {
|
||||
const { rows } = await db.query(`SELECT public.f_cerrar_comanda($1) AS data`, [id]);
|
||||
return rows[0]?.data || null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function restaurarComanda({ schema, id }) {
|
||||
return withTenantClient(schema, async (db) => {
|
||||
const { rows } = await db.query(`SELECT public.f_restaurar_comanda($1) AS data`, [id]);
|
||||
return rows[0]?.data || null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function eliminarComanda({ schema, id }) {
|
||||
return withTenantClient(schema, async (db) => {
|
||||
const { rows } = await db.query(`SELECT public.f_eliminar_comanda($1) AS data`, [id]);
|
||||
return rows[0]?.data || null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function patchComanda({ schema, id, payload }) {
|
||||
return withTenantClient(schema, async (db) => {
|
||||
const columns = await loadColumns(db, TABLE);
|
||||
const updatable = new Set(
|
||||
columns
|
||||
.filter(c =>
|
||||
!c.is_primary &&
|
||||
!c.is_identity &&
|
||||
!(String(c.column_default || '').startsWith('nextval('))
|
||||
)
|
||||
.map(c => c.column_name)
|
||||
);
|
||||
|
||||
const sets = [];
|
||||
const params = [];
|
||||
let idx = 1;
|
||||
for (const [k, v] of Object.entries(payload || {})) {
|
||||
if (!VALID_IDENT.test(k)) continue;
|
||||
if (!updatable.has(k)) continue;
|
||||
sets.push(`${q(k)} = $${idx++}`);
|
||||
params.push(v);
|
||||
}
|
||||
if (!sets.length) return { error: 'Nada para actualizar' };
|
||||
|
||||
const pks = await loadPrimaryKey(db, TABLE);
|
||||
if (pks.length !== 1) {
|
||||
const err = new Error('PK compuesta no soportada');
|
||||
err.http = { status: 400 };
|
||||
throw err;
|
||||
}
|
||||
params.push(id);
|
||||
|
||||
const { rows } = await db.query(
|
||||
`UPDATE ${q(TABLE)} SET ${sets.join(', ')} WHERE ${q(pks[0])} = $${idx} RETURNING *`,
|
||||
params
|
||||
);
|
||||
return rows[0] || null;
|
||||
});
|
||||
}
|
||||
|
||||
function q(ident) {
|
||||
return `"${String(ident).replace(/"/g, '""')}"`;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// packages/api/v1/repositories/db.mjs
|
||||
|
||||
import { poolTenants } from '@suitecoffee/db';
|
||||
|
||||
const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||
|
||||
export async function withTenantClient(req, fn, { trx = false } = {}) {
|
||||
const schema = req?.tenant?.schema;
|
||||
if (!schema || !VALID_IDENT.test(schema)) {
|
||||
throw new Error('Schema de tenant no resuelto/ inválido');
|
||||
}
|
||||
const client = await poolTenants.connect();
|
||||
try {
|
||||
if (trx) await client.query('BEGIN');
|
||||
await client.query(`SET LOCAL search_path = "${schema}", public`);
|
||||
const result = await fn(client);
|
||||
if (trx) await client.query('COMMIT');
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (trx) await client.query('ROLLBACK');
|
||||
throw e;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function tquery(req, sql, params = [], opts = {}) {
|
||||
return withTenantClient(req, (c) => c.query(sql, params), opts);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// packages/api/v1/routes/comandas.mjs
|
||||
|
||||
import { Router } from 'express';
|
||||
|
||||
import { tenantContext } from '@suitecoffee/middlewares';
|
||||
import { listarComandas, detalleComanda, actualizarComanda, eliminarComanda } from './handlers/comandasHand.mjs';
|
||||
|
||||
const comandasApiRouter = Router();
|
||||
|
||||
comandasApiRouter.use(tenantContext);
|
||||
|
||||
// Colección
|
||||
comandasApiRouter.route('/comandas').get(listarComandas);
|
||||
|
||||
// Recurso
|
||||
comandasApiRouter.route('/comandas/:id').get(detalleComanda)
|
||||
.put(actualizarComanda)
|
||||
.delete(eliminarComanda);
|
||||
|
||||
export default comandasApiRouter;
|
||||
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// API Comandas
|
||||
/*
|
||||
Escalabilidad: si luego agregás PUT /comandas/:id o DELETE /comandas/:id,
|
||||
lo hacés en la misma ruta encadenando métodos:
|
||||
|
||||
router
|
||||
.route('/comandas/:id')
|
||||
.get(detalleComanda)
|
||||
.put(actualizarComanda)
|
||||
.delete(eliminarComanda);
|
||||
|
||||
Middleware común: podrías usar .all(requireAuth) o .all(validarTenant) si necesitás autenticación o contexto del tenant.
|
||||
*/
|
||||
// ----------------------------------------------------------
|
||||
/*
|
||||
router.route('/comandas').get(listarComandas); // GET /comandas
|
||||
|
||||
// router.route('/comandas/:id').get(detalleComanda); // GET /comandas/:id
|
||||
// router.route('/comandas/:id/abrir').post(abrirComanda); // POST /comandas/:id/abrir
|
||||
// router.route('/comandas/:id/cerrar').post(cerrarComanda); // POST /comandas/:id/cerrar
|
||||
|
||||
// Recurso
|
||||
router.route('/comandas/:id')
|
||||
.get(detalleComanda) // GET /comandas/:id
|
||||
.put(actualizarComanda) // PUT /comandas/:id (accion: abrir|cerrar|restaurar) o patch genérico
|
||||
.delete(eliminarComanda); // DELETE /comandas/:id -> borrado lógico (eliminada=true)
|
||||
*/
|
||||
@@ -0,0 +1,91 @@
|
||||
// packages/api/v1/routes/handlers/comandas.js
|
||||
|
||||
import {
|
||||
listComandas,
|
||||
getDetalleItems,
|
||||
abrirComanda,
|
||||
cerrarComanda,
|
||||
restaurarComanda,
|
||||
eliminarComanda as eliminarComandaRepo,
|
||||
patchComanda
|
||||
} from '../../repositories/comandasRepo.mjs';
|
||||
|
||||
const asBoolean = (v) => {
|
||||
const s = (v ?? '').toString().trim().toLowerCase();
|
||||
return s === 'true' ? true : s === 'false' ? false : null;
|
||||
};
|
||||
|
||||
export async function listarComandas(req, res, next) {
|
||||
try {
|
||||
const abierta = asBoolean(req.query.abierta);
|
||||
const limit = req.query.limit;
|
||||
const rows = await listComandas({ schema: req.tenant.schema, abierta, limit });
|
||||
res.json(rows);
|
||||
} catch (e) { next(e); }
|
||||
}
|
||||
|
||||
export async function detalleComanda(req, res, next) {
|
||||
try {
|
||||
const id = parseId(req.params.id);
|
||||
const rows = await getDetalleItems({ schema: req.tenant.schema, id });
|
||||
res.json(rows);
|
||||
} catch (e) { next(e); }
|
||||
}
|
||||
|
||||
export async function actualizarComanda(req, res, next) {
|
||||
try {
|
||||
const id = parseId(req.params.id);
|
||||
const { accion, ...patch } = req.body || {};
|
||||
|
||||
if (accion === 'abrir') {
|
||||
const data = await abrirComanda({ schema: req.tenant.schema, id });
|
||||
return data ? res.json(data) : res.status(404).json({ error: 'Comanda no encontrada' });
|
||||
}
|
||||
if (accion === 'cerrar') {
|
||||
const data = await cerrarComanda({ schema: req.tenant.schema, id });
|
||||
return data ? res.json(data) : res.status(404).json({ error: 'Comanda no encontrada' });
|
||||
}
|
||||
if (accion === 'restaurar') {
|
||||
const data = await restaurarComanda({ schema: req.tenant.schema, id });
|
||||
return data ? res.json(data) : res.status(404).json({ error: 'Comanda no encontrada' });
|
||||
}
|
||||
|
||||
const result = await patchComanda({ schema: req.tenant.schema, id, payload: patch });
|
||||
if (!result) return res.status(404).json({ error: 'Comanda no encontrada' });
|
||||
if (result?.error) return res.status(400).json({ error: result.error });
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
if (e?.http?.status) return res.status(e.http.status).json({ error: e.message });
|
||||
// PG codes comunes
|
||||
if (e?.code === '23503') return res.status(409).json({ error: 'Violación de clave foránea', detail: e.detail });
|
||||
if (e?.code === '23505') return res.status(400).json({ error: 'Violación de unicidad', detail: e.detail });
|
||||
if (e?.code === '23514') return res.status(400).json({ error: 'Violación de CHECK', detail: e.detail });
|
||||
if (e?.code === '23502') return res.status(400).json({ error: 'Campo NOT NULL faltante', detail: e.detail });
|
||||
next(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function eliminarComanda(req, res, next) {
|
||||
try {
|
||||
const id = parseId(req.params.id);
|
||||
const data = await eliminarComandaRepo({ schema: req.tenant.schema, id });
|
||||
return data ? res.json(data) : res.status(404).json({ error: 'Comanda no encontrada' });
|
||||
} catch (e) {
|
||||
if (e?.http?.status) return res.status(e.http.status).json({ error: e.message });
|
||||
if (e?.code === '23503') return res.status(409).json({ error: 'Violación de clave foránea', detail: e.detail });
|
||||
if (e?.code === '23505') return res.status(400).json({ error: 'Violación de unicidad', detail: e.detail });
|
||||
if (e?.code === '23514') return res.status(400).json({ error: 'Violación de CHECK', detail: e.detail });
|
||||
if (e?.code === '23502') return res.status(400).json({ error: 'Campo NOT NULL faltante', detail: e.detail });
|
||||
next(e);
|
||||
}
|
||||
}
|
||||
|
||||
function parseId(value) {
|
||||
const id = Number(value);
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
const err = new Error('id inválido');
|
||||
err.http = { status: 400 };
|
||||
throw err;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
// packages/api/v1/routes/routes.js
|
||||
|
||||
import { Router } from 'express';
|
||||
import { withTenantClient, tquery } from '../repositories/db.mjs'
|
||||
import { listarComandas, detalleComanda, actualizarComanda, eliminarComanda } from './handlers/comandasHand.mjs';
|
||||
import { loadColumns, loadForeignKeys, loadPrimaryKey, pickLabelColumn } from './utils/schemaInspector.mjs';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const ALLOWED_TABLES = [
|
||||
'roles', 'usuarios', 'usua_roles',
|
||||
'categorias', 'productos',
|
||||
'clientes', 'mesas',
|
||||
'comandas', 'deta_comandas',
|
||||
'proveedores', 'compras', 'deta_comp_producto',
|
||||
'mate_primas', 'deta_comp_materias',
|
||||
'prov_producto', 'prov_mate_prima',
|
||||
'receta_producto', 'asistencia_resumen_diario',
|
||||
'asistencia_intervalo', 'asistencia_detalle',
|
||||
'vw_compras'
|
||||
];
|
||||
|
||||
const VALID_IDENT = /^[a-z_][a-z0-9_]*$/i;
|
||||
const q = (ident) => `"${String(ident).replace(/"/g, '""')}"`;
|
||||
|
||||
function ensureTable(name) {
|
||||
if (!VALID_IDENT.test(name)) throw new Error('Identificador inválido');
|
||||
if (!ALLOWED_TABLES.includes(name)) throw new Error('Tabla no permitida');
|
||||
return name;
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// Rutas de API v1
|
||||
// ==========================================================
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// API Tablas
|
||||
/*router.route('/tables').get( async (_req, res) => {
|
||||
res.json(ALLOWED_TABLES);
|
||||
});*/
|
||||
|
||||
// GET /api/schema/:table → columnas + foreign keys
|
||||
/*router.get('/schema/:table', async (req, res) => {
|
||||
try {
|
||||
const table = ensureTable(req.params.table);
|
||||
const client = await poolTenants.getClient();
|
||||
try {
|
||||
const columns = await loadColumns(client, table);
|
||||
const fks = await loadForeignKeys(client, table);
|
||||
const enriched = columns.map(c => ({ ...c, foreign: fks[c.column_name] || null }));
|
||||
res.json({ table, columns: enriched });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: e.message });
|
||||
}
|
||||
});*/
|
||||
|
||||
// GET /api/options/:table/:column → opciones FK
|
||||
/*router.get('/options/:table/:column', async (req, res) => {
|
||||
try {
|
||||
const table = ensureTable(req.params.table);
|
||||
const column = req.params.column;
|
||||
if (!VALID_IDENT.test(column)) throw new Error('Columna inválida');
|
||||
|
||||
const client = await poolTenants.getClient();
|
||||
try {
|
||||
const fks = await loadForeignKeys(client, table);
|
||||
const fk = fks[column];
|
||||
if (!fk) return res.json([]);
|
||||
|
||||
const refTable = fk.foreign_table;
|
||||
const refId = fk.foreign_column;
|
||||
const labelCol = await pickLabelColumn(client, refTable);
|
||||
|
||||
const sql = `SELECT ${q(refId)} AS id, ${q(labelCol)} AS label
|
||||
FROM ${q(refTable)} ORDER BY ${q(labelCol)} LIMIT 1000`;
|
||||
const result = await client.query(sql);
|
||||
res.json(result.rows);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: e.message });
|
||||
}
|
||||
});*/
|
||||
|
||||
// GET /api/table/:table → preview de datos
|
||||
/*router.get('/table/:table', async (req, res) => {
|
||||
try {
|
||||
const table = ensureTable(req.params.table);
|
||||
const limit = Math.min(parseInt(req.query.limit || '100', 10), 1000);
|
||||
await withTenantClient(req, async (client) => {
|
||||
const pks = await loadPrimaryKey(client, table);
|
||||
const orderBy = pks.length ? `ORDER BY ${pks.map(q).join(', ')} DESC` : '';
|
||||
const sql = `SELECT * FROM ${q(table)} ${orderBy} LIMIT ${limit}`;
|
||||
const { rows } = await client.query(sql);
|
||||
res.json(rows);
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: e.message, code: e.code, detail: e.detail });
|
||||
}
|
||||
});*/
|
||||
|
||||
// POST /api/table/:table → insertar fila
|
||||
/*router.post('/table/:table', async (req, res) => {
|
||||
const table = ensureTable(req.params.table);
|
||||
const payload = req.body || {};
|
||||
try {
|
||||
const client = await poolTenants.getClient();
|
||||
try {
|
||||
const columns = await loadColumns(client, table);
|
||||
const insertable = columns.filter(c =>
|
||||
!c.is_primary &&
|
||||
!c.is_identity &&
|
||||
!(c.column_default || '').startsWith('nextval(')
|
||||
);
|
||||
const allowedCols = new Set(insertable.map(c => c.column_name));
|
||||
|
||||
const cols = [];
|
||||
const vals = [];
|
||||
const params = [];
|
||||
let idx = 1;
|
||||
for (const [k, v] of Object.entries(payload)) {
|
||||
if (!allowedCols.has(k)) continue;
|
||||
if (!VALID_IDENT.test(k)) continue;
|
||||
cols.push(q(k));
|
||||
vals.push(`$${idx++}`);
|
||||
params.push(v);
|
||||
}
|
||||
|
||||
let rows;
|
||||
if (!cols.length) {
|
||||
({ rows } = await client.query(
|
||||
`INSERT INTO ${q(table)} DEFAULT VALUES RETURNING *`
|
||||
));
|
||||
} else {
|
||||
({ rows } = await client.query(
|
||||
`INSERT INTO ${q(table)} (${cols.join(', ')})
|
||||
VALUES (${vals.join(', ')}) RETURNING *`,
|
||||
params
|
||||
));
|
||||
}
|
||||
res.status(201).json({ inserted: rows[0] });
|
||||
} catch (e) {
|
||||
if (e.code === '23503') return res.status(400).json({ error: 'Violación de clave foránea', detail: e.detail });
|
||||
if (e.code === '23505') return res.status(400).json({ error: 'Violación de unicidad', detail: e.detail });
|
||||
if (e.code === '23514') return res.status(400).json({ error: 'Violación de CHECK', detail: e.detail });
|
||||
if (e.code === '23502') return res.status(400).json({ error: 'Campo NOT NULL faltante', detail: e.detail });
|
||||
throw e;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// API Productos
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// GET producto + receta
|
||||
/*router.route('/rpc/get_producto/:id').get( async (req, res) => {
|
||||
const client = await poolTenants.getClient()
|
||||
const id = Number(req.params.id);
|
||||
const { rows } = await client.query('SELECT public.get_producto($1) AS data', [id]);
|
||||
res.json(rows[0]?.data || {});
|
||||
client.release();
|
||||
});*/
|
||||
|
||||
// POST guardar producto + receta
|
||||
/*router.route('/rpc/save_producto').post(async (req, res) => {
|
||||
try {
|
||||
// console.debug('receta payload:', req.body?.receta); // habilitalo si lo necesitás
|
||||
const client = await poolTenants.getClient()
|
||||
const q = 'SELECT public.save_producto($1,$2,$3,$4,$5,$6,$7::jsonb) AS id_producto';
|
||||
const { id_producto=null, nombre, img_producto=null, precio=0, activo=true, id_categoria=null, receta=[] } = req.body || {};
|
||||
const params = [id_producto, nombre, img_producto, precio, activo, id_categoria, JSON.stringify(receta||[])];
|
||||
const { rows } = await client.query(q, params);
|
||||
res.json(rows[0] || {});
|
||||
client.release();
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: 'save_producto failed' });
|
||||
}
|
||||
});*/
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// API Materias Primas
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// GET MP + proveedores
|
||||
/*router.route('/rpc/get_materia/:id').get(async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
try {
|
||||
const client = await poolTenants.getClient()
|
||||
const { rows } = await client.query('SELECT public.get_materia_prima($1) AS data', [id]);
|
||||
res.json(rows[0]?.data || {});
|
||||
client.release();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: 'get_materia failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// SAVE MP + proveedores (array)
|
||||
router.route('/rpc/save_materia').post( async (req, res) => {
|
||||
const { id_mat_prima=null, nombre, unidad, activo=true, proveedores=[] } = req.body || {};
|
||||
try {
|
||||
const q = 'SELECT public.save_materia_prima($1,$2,$3,$4,$5::jsonb) AS id_mat_prima';
|
||||
const params = [id_mat_prima, nombre, unidad, activo, JSON.stringify(proveedores||[])];
|
||||
const { rows } = await poolTenants.query(q, params);
|
||||
res.json(rows[0] || {});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: 'save_materia failed' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// API Usuarios y Asistencias
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// POST /api/rpc/find_usuarios_por_documentos { docs: ["12345678","09123456", ...] }
|
||||
router.route('/rpc/find_usuarios_por_documentos').post( 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 poolTenants.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" }
|
||||
router.route('/rpc/import_asistencia').post( 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 poolTenants.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
|
||||
router.route('/rpc/asistencia_get').post( 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 poolTenants.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
|
||||
router.route('/rpc/asistencia_update_raw').post( 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 poolTenants.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
|
||||
router.route('/rpc/asistencia_delete_raw').post( async (req, res) => {
|
||||
try {
|
||||
const { id_raw } = req.body || {};
|
||||
const sql = 'SELECT public.asistencia_delete_raw($1::bigint) AS data';
|
||||
const { rows } = await poolTenants.query(sql, [id_raw]);
|
||||
res.json(rows[0]?.data || {});
|
||||
} catch (e) {
|
||||
console.error(e); res.status(500).json({ error: 'asistencia_delete_raw failed' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// API Reportes
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// POST /api/rpc/report_tickets { year }
|
||||
router.route('/rpc/report_tickets').post( async (req, res) => {
|
||||
try {
|
||||
const y = parseInt(req.body?.year ?? req.query?.year, 10);
|
||||
const year = (Number.isFinite(y) && y >= 2000 && y <= 2100)
|
||||
? y
|
||||
: (new Date()).getFullYear();
|
||||
|
||||
const { rows } = await poolTenants.query(
|
||||
'SELECT public.report_tickets_year($1::int) AS j', [year]
|
||||
);
|
||||
res.json(rows[0].j);
|
||||
} catch (e) {
|
||||
console.error('report_tickets error:', e);
|
||||
res.status(500).json({
|
||||
error: 'report_tickets failed',
|
||||
message: e.message, detail: e.detail, where: e.where, code: e.code
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/rpc/report_asistencia { desde: 'YYYY-MM-DD', hasta: 'YYYY-MM-DD' }
|
||||
router.route('/rpc/report_asistencia').post( async (req, res) => {
|
||||
try {
|
||||
let { desde, hasta } = req.body || {};
|
||||
// defaults si vienen vacíos/invalidos
|
||||
const re = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!re.test(desde) || !re.test(hasta)) {
|
||||
const end = new Date();
|
||||
const start = new Date(end); start.setDate(end.getDate()-30);
|
||||
desde = start.toISOString().slice(0,10);
|
||||
hasta = end.toISOString().slice(0,10);
|
||||
}
|
||||
|
||||
const { rows } = await poolTenants.query(
|
||||
'SELECT public.report_asistencia($1::date,$2::date) AS j', [desde, hasta]
|
||||
);
|
||||
res.json(rows[0].j);
|
||||
} catch (e) {
|
||||
console.error('report_asistencia error:', e);
|
||||
res.status(500).json({
|
||||
error: 'report_asistencia failed',
|
||||
message: e.message, detail: e.detail, where: e.where, code: e.code
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// API Compras y Gastos
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// Guardar (insert/update)
|
||||
router.route('/rpc/save_compra').post( async (req, res) => {
|
||||
try {
|
||||
const { id_compra, id_proveedor, fec_compra, detalles } = req.body || {};
|
||||
const sql = 'SELECT * FROM public.save_compra($1::int,$2::int,$3::timestamptz,$4::jsonb)';
|
||||
const args = [id_compra ?? null, id_proveedor, fec_compra ? new Date(fec_compra) : null, JSON.stringify(detalles)];
|
||||
const { rows } = await poolTenants.query(sql, args);
|
||||
res.json(rows[0]); // { id_compra, total }
|
||||
} catch (e) {
|
||||
console.error('save_compra error:', e);
|
||||
res.status(500).json({ error: 'save_compra failed', message: e.message, detail: e.detail, where: e.where, code: e.code });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Obtener para editar
|
||||
router.route('/rpc/get_compra').post( async (req, res) => {
|
||||
try {
|
||||
const { id_compra } = req.body || {};
|
||||
const sql = `SELECT public.get_compra($1::int) AS data`;
|
||||
const { rows } = await poolTenants.query(sql, [id_compra]);
|
||||
res.json(rows[0]?.data || {});
|
||||
} catch (e) {
|
||||
console.error(e); res.status(500).json({ error: 'get_compra failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Eliminar
|
||||
router.route('/rpc/delete_compra').post( async (req, res) => {
|
||||
try {
|
||||
const { id_compra } = req.body || {};
|
||||
await poolTenants.query(`SELECT public.delete_compra($1::int)`, [id_compra]);
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
console.error(e); res.status(500).json({ error: 'delete_compra failed' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// POST /api/rpc/report_gastos { year: 2025 }
|
||||
router.route('/rpc/report_gastos').post( async (req, res) => {
|
||||
try {
|
||||
const year = parseInt(req.body?.year ?? new Date().getFullYear(), 10);
|
||||
const { rows } = await poolTenants.query(
|
||||
'SELECT public.report_gastos($1::int) AS j', [year]
|
||||
);
|
||||
res.json(rows[0].j);
|
||||
} catch (e) {
|
||||
console.error('report_gastos error:', e);
|
||||
res.status(500).json({
|
||||
error: 'report_gastos failed',
|
||||
message: e.message, detail: e.detail, code: e.code
|
||||
});
|
||||
}
|
||||
});*/
|
||||
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,76 @@
|
||||
// services/app/src/api/v1/routes/utils/schemaInspector.mjs
|
||||
// Utilidades para inspeccionar columnas, claves y relaciones en PostgreSQL.
|
||||
|
||||
export async function loadColumns(client, table) {
|
||||
const sql = `
|
||||
SELECT
|
||||
c.column_name,
|
||||
c.data_type,
|
||||
c.is_nullable = 'YES' AS is_nullable,
|
||||
c.column_default,
|
||||
(SELECT EXISTS (
|
||||
SELECT 1 FROM pg_attribute a
|
||||
JOIN pg_class t ON t.oid = a.attrelid
|
||||
JOIN pg_index i ON i.indrelid = t.oid AND a.attnum = ANY(i.indkey)
|
||||
WHERE t.relname = $1 AND i.indisprimary AND a.attname = c.column_name
|
||||
)) AS is_primary,
|
||||
(SELECT a.attgenerated = 's' OR a.attidentity IN ('a','d')
|
||||
FROM pg_attribute a
|
||||
JOIN pg_class t ON t.oid = a.attrelid
|
||||
WHERE t.relname = $1 AND a.attname = c.column_name
|
||||
) AS is_identity
|
||||
FROM information_schema.columns c
|
||||
WHERE c.table_schema='public' AND c.table_name=$1
|
||||
ORDER BY c.ordinal_position
|
||||
`;
|
||||
const { rows } = await client.query(sql, [table]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function loadForeignKeys(client, table) {
|
||||
const sql = `
|
||||
SELECT
|
||||
kcu.column_name,
|
||||
ccu.table_name AS foreign_table,
|
||||
ccu.column_name AS foreign_column
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage ccu
|
||||
ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
|
||||
WHERE tc.table_schema='public' AND tc.table_name=$1 AND tc.constraint_type='FOREIGN KEY'
|
||||
`;
|
||||
const { rows } = await client.query(sql, [table]);
|
||||
const map = {};
|
||||
for (const r of rows)
|
||||
map[r.column_name] = { foreign_table: r.foreign_table, foreign_column: r.foreign_column };
|
||||
return map;
|
||||
}
|
||||
|
||||
export async function loadPrimaryKey(client, table) {
|
||||
const sql = `
|
||||
SELECT a.attname AS column_name
|
||||
FROM pg_index i
|
||||
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
||||
JOIN pg_class t ON t.oid = i.indrelid
|
||||
WHERE t.relname = $1 AND i.indisprimary
|
||||
`;
|
||||
const { rows } = await client.query(sql, [table]);
|
||||
return rows.map(r => r.column_name);
|
||||
}
|
||||
|
||||
export async function pickLabelColumn(client, refTable) {
|
||||
const preferred = ['nombre','raz_social','apodo','documento','correo','telefono'];
|
||||
const { rows } = await client.query(
|
||||
`SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name=$1
|
||||
ORDER BY ordinal_position`, [refTable]
|
||||
);
|
||||
for (const cand of preferred)
|
||||
if (rows.find(r => r.column_name === cand)) return cand;
|
||||
|
||||
const textish = rows.find(r => /text|character varying|varchar/i.test(r.data_type));
|
||||
if (textish) return textish.column_name;
|
||||
return rows[0]?.column_name || 'id';
|
||||
}
|
||||
Reference in New Issue
Block a user