chore: import gti-maquina-comercial history via subtree

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

git-subtree-dir: gti-maquina-comercial
git-subtree-mainline: 93ca82979e
git-subtree-split: b77484946f
This commit is contained in:
Gustavo
2026-09-28 15:50:08 -03:00
co-authored by Claude Sonnet 5
110 changed files with 27501 additions and 0 deletions
@@ -0,0 +1,96 @@
import fs from 'node:fs';
import path from 'node:path';
import { paths } from '../../env.mjs';
import { countLeads } from '../../treatment/queries.mjs';
const TIPOS = new Set(['excel', 'zpro-oportunidades', 'zpro-bulk']);
function parseExportRow(row) {
return {
...row,
filtro: JSON.parse(row.filtro_json || '{}'),
params: JSON.parse(row.params_json || '{}'),
arquivos: JSON.parse(row.arquivos_json || '[]'),
filtro_json: undefined,
params_json: undefined,
arquivos_json: undefined
};
}
export function registerExportRoutes(router, { db, jobs }) {
router.add('POST', '/api/exports', async (req, res, { json, body }) => {
const tipo = String(body?.tipo || '');
if (!TIPOS.has(tipo)) {
json(400, { erro: `tipo invalido. Use: ${[...TIPOS].join(', ')}` });
return;
}
const filtro = body?.filtro && typeof body.filtro === 'object' ? body.filtro : {};
const params = body?.params && typeof body.params === 'object' ? body.params : {};
const total = countLeads(db, filtro);
if (!total) {
json(400, { erro: 'nenhum lead corresponde ao filtro selecionado' });
return;
}
const inserted = db.prepare(
"INSERT INTO exports (tipo, filtro_json, params_json, status) VALUES (?, ?, ?, 'pending')"
).run(tipo, JSON.stringify(filtro), JSON.stringify(params));
const exportId = Number(inserted.lastInsertRowid);
const jobId = jobs.startJob({
type: 'exportacao',
script: 'src/export/export-job.mjs',
args: [`--export-id=${exportId}`],
meta: { exportId, tipo, total }
});
db.prepare('UPDATE exports SET job_id = ? WHERE id = ?').run(jobId, exportId);
json(202, { exportId, jobId, totalEstimado: total });
});
router.add('GET', '/api/exports', async (req, res, { json, query }) => {
const limit = Math.min(100, Math.max(1, Number(query.get('limit')) || 20));
const rows = db.prepare('SELECT * FROM exports ORDER BY id DESC LIMIT ?').all(limit);
json(200, { exports: rows.map(parseExportRow) });
});
router.add('GET', '/api/exports/:id', async (req, res, { json, params }) => {
const row = db.prepare('SELECT * FROM exports WHERE id = ?').get(Number(params.id));
if (!row) {
json(404, { erro: 'exportacao nao encontrada' });
return;
}
json(200, parseExportRow(row));
});
router.add('GET', '/api/exports/:id/download', async (req, res, { json, params, query }) => {
const row = db.prepare('SELECT * FROM exports WHERE id = ?').get(Number(params.id));
if (!row) {
json(404, { erro: 'exportacao nao encontrada' });
return;
}
const arquivos = JSON.parse(row.arquivos_json || '[]');
const nome = String(query.get('arquivo') || arquivos[0] || '');
if (!arquivos.includes(nome)) {
json(400, { erro: 'arquivo nao pertence a esta exportacao' });
return;
}
const dir = path.resolve(paths.exports, `export-${row.id}`);
const resolved = path.resolve(dir, nome);
if (!resolved.startsWith(dir + path.sep) || !fs.existsSync(resolved)) {
json(404, { erro: 'arquivo nao encontrado' });
return;
}
res.writeHead(200, {
'content-type': 'application/octet-stream',
'content-disposition': `attachment; filename="${encodeURIComponent(nome)}"`,
'content-length': fs.statSync(resolved).size
});
fs.createReadStream(resolved).pipe(res);
});
}