FEAT: Novos tipos de arquivos são permitidos
- Excel, CSV(; , ou tabulação) e TXT são agora aceitos para upload. - Formato de dados atualizado para arquivos que fogem do padrão pré definido.
This commit is contained in:
parent
3e39b9c36a
commit
827855295e
@ -46,13 +46,13 @@ async function uploadCsvFile(req, res) {
|
|||||||
// Verifica o tipo de dados do CSV
|
// Verifica o tipo de dados do CSV
|
||||||
const dataType = await discoverDataType(filePath);
|
const dataType = await discoverDataType(filePath);
|
||||||
if (dataType === 'unknown') {
|
if (dataType === 'unknown') {
|
||||||
return res.status(400).json({ error: 'Formato de CSV inválido. Deve conter colunas CEP e Número ou Latitude e Longitude.' });
|
return res.status(400).json({ error: 'Formato invalido. Envie CSV, XLS ou XLSX com CEP+Numero, CEP+Endereco, ou Latitude+Longitude.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conta as linhas válidas primeiro
|
// Conta as linhas válidas primeiro
|
||||||
const total = await countValidLines(filePath);
|
const total = await countValidLines(filePath);
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
return res.status(400).json({ error: 'Nenhuma linha válida encontrada no CSV. Verifique se há colunas CEP e Número.' });
|
return res.status(400).json({ error: 'Nenhuma linha valida encontrada. Verifique se ha CEP+Numero, CEP+Endereco, ou Latitude+Longitude.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cria o job
|
// Cria o job
|
||||||
@ -172,4 +172,4 @@ async function downloadModelController(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { consultarViabilidadeController, uploadCsvFile, getJobController, downloadCsvController, downloadModelController, consultarViaGeolocalizacaoController };
|
module.exports = { consultarViabilidadeController, uploadCsvFile, getJobController, downloadCsvController, downloadModelController, consultarViaGeolocalizacaoController };
|
||||||
|
|||||||
106
package-lock.json
generated
106
package-lock.json
generated
@ -18,7 +18,8 @@
|
|||||||
"fast-csv": "^4.3.6",
|
"fast-csv": "^4.3.6",
|
||||||
"ipaddr.js": "^2.2.0",
|
"ipaddr.js": "^2.2.0",
|
||||||
"multer": "*",
|
"multer": "*",
|
||||||
"querystring": "^0.2.1"
|
"querystring": "^0.2.1",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fast-csv/format": {
|
"node_modules/@fast-csv/format": {
|
||||||
@ -69,6 +70,15 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/append-field": {
|
"node_modules/append-field": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||||
@ -206,6 +216,28 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@ -282,6 +314,18 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csv-parser": {
|
"node_modules/csv-parser": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz",
|
||||||
@ -588,6 +632,15 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fresh": {
|
"node_modules/fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
@ -1253,6 +1306,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
@ -1352,6 +1417,45 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"fast-csv": "^4.3.6",
|
"fast-csv": "^4.3.6",
|
||||||
"ipaddr.js": "^2.2.0",
|
"ipaddr.js": "^2.2.0",
|
||||||
"multer": "*",
|
"multer": "*",
|
||||||
"querystring": "^0.2.1"
|
"querystring": "^0.2.1",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<form id="uploadForm">
|
<form id="uploadForm">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<input class="form-control" type="file" id="csvfile" accept=".csv" required />
|
<input class="form-control" type="file" id="csvfile" accept=".csv,.xls,.xlsx" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="card-buttons__container">
|
<div class="card-buttons__container">
|
||||||
<button class="btn btn-primary button-mobile" type="submit">Enviar CSV</button>
|
<button class="btn btn-primary button-mobile" type="submit">Enviar CSV</button>
|
||||||
|
|||||||
@ -1,53 +1,153 @@
|
|||||||
const { consultarViabilidade, discoverDataType } = require('./viabilidadeService');
|
const { consultarViabilidade, discoverDataType } = require('./viabilidadeService');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const readline = require('readline');
|
const XLSX = require('xlsx');
|
||||||
const { once } = require('events');
|
const { once } = require('events');
|
||||||
const {
|
const {
|
||||||
createJob,
|
|
||||||
incrementProcessed,
|
incrementProcessed,
|
||||||
incrementErrors,
|
incrementErrors,
|
||||||
finishJob,
|
finishJob
|
||||||
failJob
|
|
||||||
} = require('./jobStore.service');
|
} = require('./jobStore.service');
|
||||||
|
|
||||||
// conta linhas válidas no CSV (com CEP e Número ou Latitude e Longitude)
|
function normalizeHeader(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/[_-]+/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExcelFile(filePath) {
|
||||||
|
return ['.xls', '.xlsx'].includes(path.extname(filePath).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectDelimiter(line) {
|
||||||
|
const delimiters = [';', '\t', ','];
|
||||||
|
return delimiters
|
||||||
|
.map(delimiter => ({ delimiter, count: line.split(delimiter).length }))
|
||||||
|
.sort((a, b) => b.count - a.count)[0].delimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitDelimitedLine(line, delimiter) {
|
||||||
|
const cols = [];
|
||||||
|
let current = '';
|
||||||
|
let inQuotes = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const char = line[i];
|
||||||
|
const next = line[i + 1];
|
||||||
|
|
||||||
|
if (char === '"' && next === '"') {
|
||||||
|
current += '"';
|
||||||
|
i++;
|
||||||
|
} else if (char === '"') {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
} else if (char === delimiter && !inQuotes) {
|
||||||
|
cols.push(current.trim());
|
||||||
|
current = '';
|
||||||
|
} else {
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cols.push(current.trim());
|
||||||
|
return cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDelimitedRows(filePath) {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
|
||||||
|
const lines = content.split(/\r?\n/).filter(line => line.trim());
|
||||||
|
if (!lines.length) return [];
|
||||||
|
|
||||||
|
const delimiter = detectDelimiter(lines[0]);
|
||||||
|
return lines.map(line => splitDelimitedLine(line.replace(/\r$/, ''), delimiter));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readExcelRows(filePath) {
|
||||||
|
const workbook = XLSX.readFile(filePath, { cellDates: false, raw: false });
|
||||||
|
const firstSheetName = workbook.SheetNames[0];
|
||||||
|
if (!firstSheetName) return [];
|
||||||
|
|
||||||
|
return XLSX.utils.sheet_to_json(workbook.Sheets[firstSheetName], {
|
||||||
|
header: 1,
|
||||||
|
blankrows: false,
|
||||||
|
defval: ''
|
||||||
|
}).map(row => row.map(cell => String(cell ?? '').trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRows(filePath) {
|
||||||
|
return isExcelFile(filePath) ? readExcelRows(filePath) : readDelimitedRows(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFirstHeaderIndex(headers, predicate) {
|
||||||
|
return headers.map(normalizeHeader).findIndex(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveColumnIndexes(headers) {
|
||||||
|
const normalizedHeaders = headers.map(normalizeHeader);
|
||||||
|
const exactIndex = aliases => {
|
||||||
|
const normalizedAliases = aliases.map(normalizeHeader);
|
||||||
|
return normalizedHeaders.findIndex(header => normalizedAliases.includes(header));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
idxCep: findFirstHeaderIndex(headers, header => /\bcep\b/.test(header) || header === 'codigo postal'),
|
||||||
|
idxNumero: exactIndex(['numero', 'número', 'num', 'nº', 'n°']),
|
||||||
|
idxEndereco: findFirstHeaderIndex(headers, header => header.includes('endereco') || header.includes('logradouro')),
|
||||||
|
idxLatitude: exactIndex(['latitude']),
|
||||||
|
idxLongitude: exactIndex(['longitude'])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAddressNumber(address) {
|
||||||
|
const value = String(address || '').trim();
|
||||||
|
if (!value) return '1';
|
||||||
|
|
||||||
|
const withoutRoadKm = value
|
||||||
|
.replace(/\b(BR|SP|GO|MT|KM)\s*[-]?\s*\d+[A-Z]?\b/gi, ' ')
|
||||||
|
.replace(/\b\d+\s*[A-Z]?\b\s*(?=\))/gi, ' ');
|
||||||
|
|
||||||
|
const labeledNumber = withoutRoadKm.match(/\b(?:n|no|num|numero|número|nº|n°)\.?\s*[:,-]?\s*(\d+[A-Z]?)\b/i);
|
||||||
|
if (labeledNumber) return labeledNumber[1];
|
||||||
|
|
||||||
|
const commaNumber = withoutRoadKm.match(/,\s*(\d+[A-Z]?)\b/i);
|
||||||
|
if (commaNumber) return commaNumber[1];
|
||||||
|
|
||||||
|
const standaloneNumbers = withoutRoadKm.match(/\b\d+[A-Z]?\b/gi) || [];
|
||||||
|
return standaloneNumbers.length ? standaloneNumbers[standaloneNumbers.length - 1] : '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCepPayload(cols, indexes) {
|
||||||
|
const cepRaw = indexes.idxCep >= 0 ? cols[indexes.idxCep] : '';
|
||||||
|
const cep = String(cepRaw || '').replace(/\D/g, '');
|
||||||
|
const numeroRaw = indexes.idxNumero >= 0 ? cols[indexes.idxNumero] : '';
|
||||||
|
const enderecoRaw = indexes.idxEndereco >= 0 ? cols[indexes.idxEndereco] : '';
|
||||||
|
const numero = String(numeroRaw || '').trim() || extractAddressNumber(enderecoRaw);
|
||||||
|
|
||||||
|
if (!cep) return null;
|
||||||
|
return { cep, numero };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanCsvValue(value) {
|
||||||
|
const text = String(value ?? '').replace(/[\r\n;]/g, ' ');
|
||||||
|
return text.includes('"') ? text.replace(/"/g, "'") : text;
|
||||||
|
}
|
||||||
|
|
||||||
async function countValidLines(inputPath) {
|
async function countValidLines(inputPath) {
|
||||||
const dataType = await discoverDataType(inputPath);
|
const dataType = await discoverDataType(inputPath);
|
||||||
const instream = fs.createReadStream(inputPath, { encoding: 'utf8' });
|
const rows = readRows(inputPath);
|
||||||
const rl = readline.createInterface({ input: instream, crlfDelay: Infinity });
|
const headers = rows[0] || [];
|
||||||
|
const indexes = resolveColumnIndexes(headers);
|
||||||
let isHeader = true;
|
|
||||||
let headers = [];
|
|
||||||
let idxCep = -1;
|
|
||||||
let idxNumero = -1;
|
|
||||||
let idxLatitude = -1;
|
|
||||||
let idxLongitude = -1;
|
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
|
||||||
for await (const rawLine of rl) {
|
for (const cols of rows.slice(1)) {
|
||||||
const line = rawLine.replace(/\r$/, '');
|
|
||||||
if (!line.trim()) continue;
|
|
||||||
|
|
||||||
if (isHeader) {
|
|
||||||
headers = line.split(';').map(h => h.trim());
|
|
||||||
const lower = headers.map(h => h.toLowerCase());
|
|
||||||
idxCep = lower.indexOf('cep');
|
|
||||||
idxNumero = lower.indexOf('numero');
|
|
||||||
idxLatitude = lower.indexOf('latitude');
|
|
||||||
idxLongitude = lower.indexOf('longitude');
|
|
||||||
isHeader = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cols = line.split(';').map(c => c.trim());
|
|
||||||
if (dataType === 'cep') {
|
if (dataType === 'cep') {
|
||||||
const cep = idxCep >= 0 ? String(cols[idxCep] || '').replace(/\D/g, '') : '';
|
if (buildCepPayload(cols, indexes)) total++;
|
||||||
const numero = idxNumero >= 0 ? cols[idxNumero] : '';
|
|
||||||
if (cep && numero) total++;
|
|
||||||
} else if (dataType === 'geolocalizacao') {
|
} else if (dataType === 'geolocalizacao') {
|
||||||
const latitude = idxLatitude >= 0 ? parseFloat(cols[idxLatitude]) : NaN;
|
const latitude = indexes.idxLatitude >= 0 ? parseFloat(cols[indexes.idxLatitude]) : NaN;
|
||||||
const longitude = idxLongitude >= 0 ? parseFloat(cols[idxLongitude]) : NaN;
|
const longitude = indexes.idxLongitude >= 0 ? parseFloat(cols[indexes.idxLongitude]) : NaN;
|
||||||
if (!isNaN(latitude) && !isNaN(longitude)) total++;
|
if (!isNaN(latitude) && !isNaN(longitude)) total++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,113 +155,47 @@ async function countValidLines(inputPath) {
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
// nova função: processa CSV linha a linha, chama consultarViabilidade e gera CSV de saída
|
|
||||||
// Recebe jobId já criado no controller
|
|
||||||
async function processCsvFile(jobId, inputPath, originalName) {
|
async function processCsvFile(jobId, inputPath, originalName) {
|
||||||
const dataType = await discoverDataType(inputPath);
|
const dataType = await discoverDataType(inputPath);
|
||||||
const total = await countValidLines(inputPath);
|
const rows = readRows(inputPath);
|
||||||
// Job já criado no controller
|
const headers = rows[0] || [];
|
||||||
// const jobId = createJob(total);
|
const indexes = resolveColumnIndexes(headers);
|
||||||
const baseName = path.parse(inputPath).name;
|
const baseName = path.parse(originalName || inputPath).name;
|
||||||
const outputFilename = `processed_${Date.now()}_${baseName}.csv`;
|
const outputFilename = `processed_${Date.now()}_${baseName}.csv`;
|
||||||
const outputPath = path.join(__dirname, '..', 'outputs', outputFilename);
|
const outputPath = path.join(__dirname, '..', 'outputs', outputFilename);
|
||||||
|
|
||||||
const instream = fs.createReadStream(inputPath, { encoding: 'utf8' });
|
|
||||||
const rl = readline.createInterface({ input: instream, crlfDelay: Infinity });
|
|
||||||
const outStream = fs.createWriteStream(outputPath, { encoding: 'utf8' });
|
const outStream = fs.createWriteStream(outputPath, { encoding: 'utf8' });
|
||||||
outStream.write('\uFEFF');
|
outStream.write('\uFEFF');
|
||||||
|
outStream.write(['Distancia', 'Dedicado', 'Nao Dedicado', 'Erro', ...headers].join(';') + '\n');
|
||||||
|
|
||||||
let isHeader = true;
|
for (const cols of rows.slice(1)) {
|
||||||
let headers = [];
|
|
||||||
let idxCep = -1;
|
|
||||||
let idxNumero = -1;
|
|
||||||
let idxLatitude = -1;
|
|
||||||
let idxLongitude = -1;
|
|
||||||
|
|
||||||
for await (const rawLine of rl) {
|
|
||||||
const line = rawLine.replace(/\r$/, ''); // normalize CRLF
|
|
||||||
if (!line.trim()) continue;
|
|
||||||
|
|
||||||
if (isHeader) {
|
|
||||||
headers = line
|
|
||||||
.split(';')
|
|
||||||
.map(h => h.trim())
|
|
||||||
.filter(h => h !== '');
|
|
||||||
|
|
||||||
const lower = headers.map(h => h.toLowerCase());
|
|
||||||
idxCep = lower.indexOf('cep');
|
|
||||||
idxNumero = lower.indexOf('numero');
|
|
||||||
idxLatitude = lower.indexOf('latitude');
|
|
||||||
idxLongitude = lower.indexOf('longitude');
|
|
||||||
|
|
||||||
// se não encontrar, tenta variações comuns
|
|
||||||
const idx = lower.indexOf('codigo postal');
|
|
||||||
if (idx !== -1) idxCep = idx;
|
|
||||||
|
|
||||||
|
|
||||||
const outHeaders = [...headers, 'Distancia', 'Endereco', 'Não Dedicado', 'Dedicado', 'Erro'];
|
|
||||||
outStream.write(outHeaders.join(';') + '\n');
|
|
||||||
isHeader = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cols = line
|
|
||||||
.split(';')
|
|
||||||
.map(c => c.trim())
|
|
||||||
.filter(c => c !== '');
|
|
||||||
|
|
||||||
let dataToSend = {};
|
let dataToSend = {};
|
||||||
|
|
||||||
if (dataType === 'cep') {
|
if (dataType === 'cep') {
|
||||||
const cepRaw = (idxCep >= 0 && cols[idxCep]) ? cols[idxCep] : '';
|
dataToSend = buildCepPayload(cols, indexes);
|
||||||
const cep = String(cepRaw).replace(/\D/g, ''); // keep digits only
|
if (!dataToSend) continue;
|
||||||
const numero = (idxNumero >= 0 && cols[idxNumero]) ? cols[idxNumero] : '';
|
|
||||||
|
|
||||||
if (!cep || !numero) {
|
|
||||||
continue; // pula linha inválida
|
|
||||||
}
|
|
||||||
dataToSend = { cep, numero };
|
|
||||||
} else if (dataType === 'geolocalizacao') {
|
} else if (dataType === 'geolocalizacao') {
|
||||||
const latitude = (idxLatitude >= 0 && cols[idxLatitude]) ? parseFloat(cols[idxLatitude]) : NaN;
|
const latitude = indexes.idxLatitude >= 0 ? parseFloat(cols[indexes.idxLatitude]) : NaN;
|
||||||
const longitude = (idxLongitude >= 0 && cols[idxLongitude]) ? parseFloat(cols[idxLongitude]) : NaN;
|
const longitude = indexes.idxLongitude >= 0 ? parseFloat(cols[indexes.idxLongitude]) : NaN;
|
||||||
|
|
||||||
if (isNaN(latitude) || isNaN(longitude)) {
|
if (isNaN(latitude) || isNaN(longitude)) continue;
|
||||||
continue; // pula linha inválida
|
|
||||||
}
|
|
||||||
dataToSend = { latitude, longitude };
|
dataToSend = { latitude, longitude };
|
||||||
} else {
|
} else {
|
||||||
continue; // tipo desconhecido, pula
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const viab = await consultarViabilidade(dataToSend);
|
const viab = await consultarViabilidade(dataToSend);
|
||||||
|
|
||||||
const distancia = viab.distancia ?? (viab.raw && (viab.raw.distancia || viab.raw.distance)) ?? '';
|
const distancia = viab.distancia ?? (viab.raw && (viab.raw.distancia || viab.raw.distance)) ?? '';
|
||||||
if (dataType === 'cep' && viab.cep) {
|
const dedicado = viab.dedicado ? 'Viavel' : 'Nao Viavel';
|
||||||
var endereco = `${viab.logradouro || ''}, ${viab.bairro || ''}, ${viab.cidade || ''}/${viab.estado || ''}, ${viab.cep || ''}`;
|
const naoDedicado = viab.naoDedicado ? 'Viavel' : 'Nao Viavel';
|
||||||
} else {
|
const error = viab.error ? cleanCsvValue(viab.error) : '';
|
||||||
var endereco = viab.endereco;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viab.naoDedicado) {
|
const outCols = [distancia, dedicado, naoDedicado, error, ...cols].map(cleanCsvValue);
|
||||||
var naoDedicado = "Viavel";
|
|
||||||
} else {
|
|
||||||
var naoDedicado = "Não Viavel";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viab.dedicado) {
|
|
||||||
var dedicado = "Viavel";
|
|
||||||
} else {
|
|
||||||
var dedicado = "Não Viavel";
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = viab.error ? String(viab.error).replace(/[\r\n;]/g, ' ') : '';
|
|
||||||
|
|
||||||
const outCols = [...cols, distancia, endereco, naoDedicado, dedicado, error];
|
|
||||||
outStream.write(outCols.join(';') + '\n');
|
outStream.write(outCols.join(';') + '\n');
|
||||||
incrementProcessed(jobId);
|
incrementProcessed(jobId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errMsg = (err && (err.message || String(err))).replace(/[\r\n;]/g, ' ');
|
const errMsg = cleanCsvValue(err && (err.message || String(err)));
|
||||||
const outCols = [...cols, '', '', '', '', '', '', errMsg];
|
const outCols = ['', '', '', errMsg, ...cols].map(cleanCsvValue);
|
||||||
outStream.write(outCols.join(';') + '\n');
|
outStream.write(outCols.join(';') + '\n');
|
||||||
incrementErrors(jobId);
|
incrementErrors(jobId);
|
||||||
incrementProcessed(jobId);
|
incrementProcessed(jobId);
|
||||||
@ -176,4 +210,4 @@ async function processCsvFile(jobId, inputPath, originalName) {
|
|||||||
return outputPath;
|
return outputPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { processCsvFile, countValidLines };
|
module.exports = { processCsvFile, countValidLines };
|
||||||
|
|||||||
@ -1,8 +1,74 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const readline = require('readline');
|
const readline = require('readline');
|
||||||
|
const path = require('path');
|
||||||
|
const XLSX = require('xlsx');
|
||||||
const { apiConfig, apiViabilidadeUrl, apiUrlBase } = require('../config/apiConfig');
|
const { apiConfig, apiViabilidadeUrl, apiUrlBase } = require('../config/apiConfig');
|
||||||
|
|
||||||
|
function normalizeHeader(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/[_-]+/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasHeader(headers, aliases) {
|
||||||
|
const normalizedAliases = aliases.map(normalizeHeader);
|
||||||
|
return headers.some(header => normalizedAliases.includes(header));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExcelFile(filePath) {
|
||||||
|
return ['.xls', '.xlsx'].includes(path.extname(filePath).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectDelimiter(line) {
|
||||||
|
const delimiters = [';', '\t', ','];
|
||||||
|
return delimiters
|
||||||
|
.map(delimiter => ({ delimiter, count: line.split(delimiter).length }))
|
||||||
|
.sort((a, b) => b.count - a.count)[0].delimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readExcelHeaders(filePath) {
|
||||||
|
const workbook = XLSX.readFile(filePath, { cellDates: false, raw: false });
|
||||||
|
const firstSheetName = workbook.SheetNames[0];
|
||||||
|
if (!firstSheetName) return [];
|
||||||
|
|
||||||
|
const rows = XLSX.utils.sheet_to_json(workbook.Sheets[firstSheetName], {
|
||||||
|
header: 1,
|
||||||
|
blankrows: false,
|
||||||
|
defval: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
return (rows[0] || []).map(normalizeHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readDelimitedHeaders(filePath) {
|
||||||
|
const instream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
||||||
|
const rl = readline.createInterface({ input: instream, crlfDelay: Infinity });
|
||||||
|
|
||||||
|
for await (const rawLine of rl) {
|
||||||
|
const line = rawLine.replace(/^\uFEFF/, '').replace(/\r$/, '');
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
rl.close();
|
||||||
|
return line.split(detectDelimiter(line)).map(normalizeHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.close();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCepHeader(headers) {
|
||||||
|
return headers.some(header => /\bcep\b/.test(header) || header === 'codigo postal');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAddressOrNumberHeader(headers) {
|
||||||
|
return headers.some(header => ['numero', 'num', 'nº', 'n°'].includes(header)
|
||||||
|
|| header.includes('endereco')
|
||||||
|
|| header.includes('logradouro'));
|
||||||
|
}
|
||||||
|
|
||||||
async function consultarViabilidade(data) {
|
async function consultarViabilidade(data) {
|
||||||
try {
|
try {
|
||||||
@ -28,20 +94,12 @@ async function consultarViabilidade(data) {
|
|||||||
// Preciso de uma função para verificar se os dados vindos são de CEP ou de geolocalização
|
// Preciso de uma função para verificar se os dados vindos são de CEP ou de geolocalização
|
||||||
async function discoverDataType(input) {
|
async function discoverDataType(input) {
|
||||||
if (typeof input === 'string') {
|
if (typeof input === 'string') {
|
||||||
// Trata como filePath
|
const headers = isExcelFile(input)
|
||||||
const instream = fs.createReadStream(input, { encoding: 'utf8' });
|
? readExcelHeaders(input)
|
||||||
const rl = readline.createInterface({ input: instream, crlfDelay: Infinity });
|
: await readDelimitedHeaders(input);
|
||||||
|
|
||||||
let headers = [];
|
const hasCepNumero = hasCepHeader(headers) && hasAddressOrNumberHeader(headers);
|
||||||
for await (const rawLine of rl) {
|
if (hasCepNumero) {
|
||||||
const line = rawLine.replace(/\r$/, '');
|
|
||||||
if (!line.trim()) continue;
|
|
||||||
headers = line.split(';').map(h => h.trim().toLowerCase());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
rl.close();
|
|
||||||
|
|
||||||
if (headers.includes('cep') && headers.includes('numero')) {
|
|
||||||
return 'cep';
|
return 'cep';
|
||||||
} else if (headers.includes('latitude') && headers.includes('longitude')) {
|
} else if (headers.includes('latitude') && headers.includes('longitude')) {
|
||||||
return 'geolocalizacao';
|
return 'geolocalizacao';
|
||||||
@ -62,4 +120,4 @@ async function discoverDataType(input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { consultarViabilidade, discoverDataType };
|
module.exports = { consultarViabilidade, discoverDataType };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user