Baseline commit of the existing Maquina Comercial Pessoal project
Tracks the app that was already in place (capture, treatment, outreach, WhatsApp integration, server, frontend, tests, docs) so future feature work can branch and use isolated worktrees.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { csvEscape, excelTextCell, neutralizeSpreadsheetFormula, BOM } from '../util/csv.mjs';
|
||||
import { iterateLeads } from '../treatment/queries.mjs';
|
||||
|
||||
// Exportacao "planilha": CSV + .xls gemeo, ambos a prova de Excel — os campos
|
||||
// numericos longos (whatsapp, cnpj, cnae) sao gravados como TEXTO para nunca
|
||||
// virarem notacao cientifica. Mesmo formato dos arquivos TRATADOS legados:
|
||||
// CSV -> formula ="valor"
|
||||
// XLS -> tabela HTML com mso-number-format:"\@"
|
||||
|
||||
const HEADERS = [
|
||||
'empresa', 'whatsapp', 'ddd', 'cidade', 'uf', 'segmento', 'segmento_legivel', 'interesse',
|
||||
'cnpj', 'cnae', 'telefone_valido', 'motivo_telefone', 'cnpj_valido', 'empresa_valida',
|
||||
'motivo_empresa', 'score', 'classificacao', 'motivo_classificacao', 'chave_duplicidade', 'linha_origem',
|
||||
'score_qualidade', 'score_comercial', 'prioridade', 'produto_recomendado'
|
||||
];
|
||||
|
||||
const TEXT_COLUMNS = new Set(['whatsapp', 'ddd', 'cnpj', 'cnae']);
|
||||
|
||||
function leadToExportRow(lead) {
|
||||
return {
|
||||
empresa: lead.empresa,
|
||||
whatsapp: lead.whatsapp,
|
||||
ddd: lead.ddd,
|
||||
cidade: lead.cidade,
|
||||
uf: lead.uf,
|
||||
segmento: lead.segmento,
|
||||
segmento_legivel: lead.segmento_label,
|
||||
interesse: lead.interesse,
|
||||
cnpj: lead.cnpj,
|
||||
cnae: lead.cnae,
|
||||
telefone_valido: lead.telefone_valido ? 'sim' : 'nao',
|
||||
motivo_telefone: lead.telefone_valido ? 'telefone valido' : 'telefone invalido ou vazio',
|
||||
cnpj_valido: lead.cnpj_valido ? 'sim' : 'nao',
|
||||
empresa_valida: lead.empresa_valida ? 'sim' : 'nao',
|
||||
motivo_empresa: lead.empresa_valida ? 'empresa valida' : (lead.motivo_classificacao || 'empresa invalida'),
|
||||
score: lead.score,
|
||||
classificacao: lead.classificacao,
|
||||
motivo_classificacao: lead.motivo_classificacao,
|
||||
chave_duplicidade: lead.dedup_key,
|
||||
linha_origem: lead.id,
|
||||
score_qualidade: lead.score_qualidade,
|
||||
score_comercial: lead.score_comercial,
|
||||
prioridade: lead.prioridade,
|
||||
produto_recomendado: lead.produto_principal
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
async function writeStream(stream, text) {
|
||||
if (!stream.write(text)) {
|
||||
await new Promise((resolve) => stream.once('drain', resolve));
|
||||
}
|
||||
}
|
||||
|
||||
export async function exportExcel({ db, filtro, dir, onProgress = () => {}, collectIds }) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const baseName = 'leads-excel';
|
||||
const csvPath = path.join(dir, `${baseName}.csv`);
|
||||
const xlsPath = path.join(dir, `${baseName}.xls`);
|
||||
|
||||
const csv = fs.createWriteStream(csvPath, { encoding: 'utf8' });
|
||||
const xls = fs.createWriteStream(xlsPath, { encoding: 'utf8' });
|
||||
|
||||
await writeStream(csv, BOM + HEADERS.join(';') + '\n');
|
||||
await writeStream(xls, `${BOM}<html><head><meta charset="utf-8"><style>td.text{mso-number-format:"\\@";}</style></head><body><table border="1"><tr>${
|
||||
HEADERS.map((h) => `<th>${escapeHtml(h)}</th>`).join('')
|
||||
}</tr>\n`);
|
||||
|
||||
let total = 0;
|
||||
|
||||
for (const lead of iterateLeads(db, filtro)) {
|
||||
const row = leadToExportRow(lead);
|
||||
|
||||
const csvCells = HEADERS.map((h) => (TEXT_COLUMNS.has(h) && row[h] !== ''
|
||||
? excelTextCell(row[h])
|
||||
: csvEscape(neutralizeSpreadsheetFormula(row[h]))));
|
||||
await writeStream(csv, csvCells.join(';') + '\n');
|
||||
|
||||
const xlsCells = HEADERS.map((h) =>
|
||||
`<td${TEXT_COLUMNS.has(h) ? ' class="text"' : ''}>${escapeHtml(TEXT_COLUMNS.has(h) ? row[h] : neutralizeSpreadsheetFormula(row[h]))}</td>`
|
||||
);
|
||||
await writeStream(xls, `<tr>${xlsCells.join('')}</tr>\n`);
|
||||
|
||||
if (collectIds) collectIds.push(lead.id);
|
||||
total += 1;
|
||||
if (total % 10000 === 0) onProgress({ message: `${total} leads gravados...` });
|
||||
}
|
||||
|
||||
await writeStream(xls, '</table></body></html>\n');
|
||||
await Promise.all([
|
||||
new Promise((resolve, reject) => csv.end((e) => (e ? reject(e) : resolve()))),
|
||||
new Promise((resolve, reject) => xls.end((e) => (e ? reject(e) : resolve())))
|
||||
]);
|
||||
|
||||
return {
|
||||
total,
|
||||
files: [path.basename(csvPath), path.basename(xlsPath)]
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user