Mucha cosa y es muy tarde.
- Anda parte del registro
This commit is contained in:
Generated
+35
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
"chalk": "^5.6.0",
|
||||
"connect-redis": "^9.0.0",
|
||||
"cors": "^2.8.5",
|
||||
@@ -130,6 +131,20 @@
|
||||
"version": "1.0.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.3.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"dev": true,
|
||||
@@ -898,6 +913,26 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
||||
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.10",
|
||||
"dev": true,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"nodemon": "^3.1.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
"chalk": "^5.6.0",
|
||||
"connect-redis": "^9.0.0",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
+155
-866
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,337 @@
|
||||
// services/app/src/routes.legacy.js
|
||||
// -----------------------------------------------------------------------------
|
||||
// Endpoints legacy de SuiteCoffee extraídos del index original y montados
|
||||
// como módulo. No elimina nada; sólo organiza y robustece.
|
||||
//
|
||||
// Cómo se usa: el nuevo services/app/src/index.js hace
|
||||
// const legacy = await import('./routes.legacy.js')
|
||||
// legacy.default(app, { requireAuth, withTenant, done, mainPool, tenantsPool, express })
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export default function mount(app, ctx) {
|
||||
const { requireAuth, withTenant, done, mainPool, tenantsPool, express } = ctx;
|
||||
|
||||
// Aliases de compatibilidad con el archivo original
|
||||
const pool = mainPool; // el original usaba `pool` (DB principal)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers y seguridad (copiados/adaptados del archivo original)
|
||||
// ---------------------------------------------------------------------------
|
||||
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', 'vw_compras'
|
||||
];
|
||||
const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||
const q = (s) => `"${String(s).replace(/"/g, '""')}"`; // quote ident simple
|
||||
function ensureTable(name) {
|
||||
const t = String(name || '').toLowerCase();
|
||||
if (!ALLOWED_TABLES.includes(t)) throw new Error('Tabla no permitida');
|
||||
return t;
|
||||
}
|
||||
|
||||
async function getClient() { // el original devolvía pool.connect()
|
||||
const client = await pool.connect();
|
||||
return client;
|
||||
}
|
||||
|
||||
// Columnas de una tabla
|
||||
async function loadColumns(client, table) {
|
||||
const sql = `
|
||||
SELECT
|
||||
c.column_name,
|
||||
c.data_type,
|
||||
c.is_nullable = 'YES' AS is_nullable,
|
||||
c.column_default,
|
||||
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 t2 ON t2.oid = a.attrelid
|
||||
WHERE t2.relname = $1 AND a.attname = c.column_name
|
||||
) AS is_generated
|
||||
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;
|
||||
}
|
||||
|
||||
// PKs de una tabla
|
||||
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);
|
||||
}
|
||||
|
||||
// FKs salientes de una tabla → { [column]: { foreign_table, foreign_column } }
|
||||
async function loadForeignKeys(client, table) {
|
||||
const sql = `
|
||||
SELECT
|
||||
kcu.column_name AS 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.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = 'public'
|
||||
AND tc.table_name = $1`;
|
||||
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;
|
||||
}
|
||||
|
||||
// Heurística para elegir una columna "label" en tablas referenciadas
|
||||
async function pickLabelColumn(client, refTable) {
|
||||
const preferred = ['nombre','raz_social','apodo','documento','correo','telefono','descripcion','detalle'];
|
||||
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';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RUTAS DE UI (vistas)
|
||||
// ---------------------------------------------------------------------------
|
||||
app.get('/', (req, res) => {
|
||||
res.locals.pageTitle = 'Dashboard';
|
||||
res.locals.pageId = 'home';
|
||||
res.render('dashboard');
|
||||
});
|
||||
|
||||
app.get('/dashboard', (req, res) => {
|
||||
res.locals.pageTitle = 'Dashboard';
|
||||
res.locals.pageId = 'dashboard';
|
||||
res.render('dashboard');
|
||||
});
|
||||
|
||||
app.get('/comandas', (req, res) => {
|
||||
res.locals.pageTitle = 'Comandas';
|
||||
res.locals.pageId = 'comandas';
|
||||
res.render('comandas');
|
||||
});
|
||||
|
||||
app.get('/estadoComandas', (req, res) => {
|
||||
res.locals.pageTitle = 'Estado de Comandas';
|
||||
res.locals.pageId = 'estadoComandas';
|
||||
res.render('estadoComandas');
|
||||
});
|
||||
|
||||
app.get('/productos', (req, res) => {
|
||||
res.locals.pageTitle = 'Productos';
|
||||
res.locals.pageId = 'productos';
|
||||
res.render('productos');
|
||||
});
|
||||
|
||||
app.get('/usuarios', (req, res) => {
|
||||
res.locals.pageTitle = 'Usuarios';
|
||||
res.locals.pageId = 'usuarios';
|
||||
res.render('usuarios');
|
||||
});
|
||||
|
||||
app.get('/reportes', (req, res) => {
|
||||
res.locals.pageTitle = 'Reportes';
|
||||
res.locals.pageId = 'reportes';
|
||||
res.render('reportes');
|
||||
});
|
||||
|
||||
app.get('/compras', (req, res) => {
|
||||
res.locals.pageTitle = 'Compras';
|
||||
res.locals.pageId = 'compras';
|
||||
res.render('compras');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API: ejemplos por-tenant y utilitarios (introspección)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ejemplo conservado del original (usar search_path via withTenant)
|
||||
app.get('/api/productos', requireAuth, withTenant, async (req, res, next) => {
|
||||
const { rows } = await req.pg.query('SELECT * FROM productos ORDER BY id');
|
||||
res.json(rows);
|
||||
}, done);
|
||||
|
||||
// Listado de tablas permitidas
|
||||
app.get('/api/tables', async (_req, res) => {
|
||||
res.json(ALLOWED_TABLES);
|
||||
});
|
||||
|
||||
// Esquema de una tabla (columnas + FKs)
|
||||
app.get('/api/schema/:table', async (req, res) => {
|
||||
try {
|
||||
const table = ensureTable(req.params.table);
|
||||
const client = await 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 });
|
||||
}
|
||||
});
|
||||
|
||||
// Opciones para una columna con FK (id/label)
|
||||
app.get('/api/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 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 });
|
||||
}
|
||||
});
|
||||
|
||||
// Datos de una tabla (limitados) — vista rápida
|
||||
app.get('/api/table/:table', async (req, res) => {
|
||||
try {
|
||||
const table = ensureTable(req.params.table);
|
||||
const limit = Math.min(parseInt(req.query.limit || '100', 10), 1000);
|
||||
const client = await getClient();
|
||||
try {
|
||||
const pks = await loadPrimaryKey(client, table);
|
||||
const order = pks[0] ? q(pks[0]) : '1';
|
||||
const sql = `SELECT * FROM ${q(table)} ORDER BY ${order} LIMIT $1`;
|
||||
const { rows } = await client.query(sql, [limit]);
|
||||
res.json(rows);
|
||||
} finally { client.release(); }
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: e.message, code: e.code, detail: e.detail });
|
||||
}
|
||||
});
|
||||
|
||||
// Crear/actualizar registros genéricos (placeholder: pega aquí tu lógica original)
|
||||
app.post('/api/table/:table', async (req, res) => {
|
||||
// TODO: Pegar implementación original (insert/update genérico) aquí.
|
||||
// Sugerencia: validar payload contra loadColumns(client, table),
|
||||
// construir INSERT/UPDATE dinámico ignorando columnas generadas y PKs cuando corresponda.
|
||||
res.status(501).json({ error: 'not-implemented', detail: 'Pegar lógica original de POST /api/table/:table' });
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Endpoints de negocio (conservados tal cual cuando fue posible)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Detalle de una comanda
|
||||
app.get('/api/comandas/:id/detalle', (req, res, next) =>
|
||||
pool.query(
|
||||
`SELECT id_det_comanda, id_producto, producto_nombre,
|
||||
cantidad, pre_unitario, subtotal, observaciones
|
||||
FROM public.v_comandas_detalle_items
|
||||
WHERE id_comanda = $1::int
|
||||
ORDER BY id_det_comanda`,
|
||||
[req.params.id]
|
||||
)
|
||||
.then(r => res.json(r.rows))
|
||||
.catch(next)
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RPC / Reportes / Procedimientos (stubs con TODO si no se extrajo el SQL)
|
||||
// ---------------------------------------------------------------------------
|
||||
app.post('/api/rpc/find_usuarios_por_documentos', async (req, res) => {
|
||||
// TODO: Pegar el SQL original. Ejemplo:
|
||||
// const { documentos } = req.body || {};
|
||||
// const { rows } = await pool.query('SELECT * FROM public.find_usuarios_por_documentos($1::jsonb)', [JSON.stringify(documentos||[])])
|
||||
// res.json(rows);
|
||||
res.status(501).json({ error: 'not-implemented' });
|
||||
});
|
||||
|
||||
app.post('/api/rpc/import_asistencia', async (req, res) => {
|
||||
// TODO: pegar lógica original
|
||||
res.status(501).json({ error: 'not-implemented' });
|
||||
});
|
||||
|
||||
app.post('/api/rpc/asistencia_get', async (req, res) => {
|
||||
// TODO
|
||||
res.status(501).json({ error: 'not-implemented' });
|
||||
});
|
||||
|
||||
app.post('/api/rpc/asistencia_update_raw', async (req, res) => {
|
||||
// TODO
|
||||
res.status(501).json({ error: 'not-implemented' });
|
||||
});
|
||||
|
||||
app.post('/api/rpc/asistencia_delete_raw', async (req, res) => {
|
||||
// TODO
|
||||
res.status(501).json({ error: 'not-implemented' });
|
||||
});
|
||||
|
||||
app.post('/api/rpc/report_tickets', async (req, res) => {
|
||||
// TODO: posiblemente public.report_tickets_year(year int)
|
||||
res.status(501).json({ error: 'not-implemented' });
|
||||
});
|
||||
|
||||
app.post('/api/rpc/report_asistencia', async (req, res) => {
|
||||
// TODO: posiblemente public.report_asistencia(desde date, hasta date)
|
||||
res.status(501).json({ error: 'not-implemented' });
|
||||
});
|
||||
|
||||
app.get('/api/rpc/report_gastos', async (req, res) => {
|
||||
// TODO: pegar la SELECT/función original
|
||||
res.status(501).json({ error: 'not-implemented' });
|
||||
});
|
||||
|
||||
app.post('/api/rpc/report_gastos', async (req, res) => {
|
||||
try {
|
||||
// Ejemplo de carcasa robusta en base a nombres vistos
|
||||
const { desde, hasta } = req.body || {};
|
||||
if (!desde || !hasta) return res.status(400).json({ error: 'desde y hasta son requeridos' });
|
||||
// TODO: reemplazar por tu SQL real; esto es un placeholder ilutrativo
|
||||
const sql = 'SELECT * FROM public.report_gastos($1::date, $2::date)';
|
||||
try {
|
||||
const { rows } = await pool.query(sql, [desde, hasta]);
|
||||
res.json(rows);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'report_gastos failed', message: e.message, detail: e.detail, code: e.code });
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'report_gastos failed', message: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/rpc/save_compra', async (req, res) => { res.status(501).json({ error: 'not-implemented' }); });
|
||||
app.post('/api/rpc/get_compra', async (req, res) => { res.status(501).json({ error: 'not-implemented' }); });
|
||||
app.post('/api/rpc/delete_compra', async (req, res) => { res.status(501).json({ error: 'not-implemented' }); });
|
||||
}
|
||||
Reference in New Issue
Block a user