FEAT: Adicionado suporte para cabeçalhos em arquivos Excel e ajuste na leitura de linhas em CSV

This commit is contained in:
Rafael Alves Lopes 2026-05-12 17:57:30 -03:00
parent ae78907246
commit a3245592c8
3 changed files with 130 additions and 22 deletions

View File

@ -36,7 +36,7 @@ async function pollJob(jobId) {
if (j.status === 'done') { if (j.status === 'done') {
bar.style.width = '100%'; bar.style.width = '100%';
bar.innerText = '100%'; bar.innerText = '100%';
resEl.innerHTML = `Concluído. <a href="/download/${jobId}">Baixar CSV processado</a>`; resEl.innerHTML = `Concluído. <a href="/download/${jobId}">Baixar arquivo processado</a>`;
return; return;
} }
if (j.status === 'error') { if (j.status === 'error') {
@ -110,4 +110,4 @@ document.addEventListener("DOMContentLoaded", () => {
link2.click(); link2.click();
link2.remove(); link2.remove();
}); });
}); });

View File

@ -9,6 +9,8 @@ const {
finishJob finishJob
} = require('./jobStore.service'); } = require('./jobStore.service');
const RESULT_HEADERS = ['Provedor', 'Distancia', 'Dedicado', 'Nao Dedicado', 'Erro'];
function normalizeHeader(value) { function normalizeHeader(value) {
return String(value || '') return String(value || '')
.trim() .trim()
@ -92,7 +94,7 @@ function readExcelRows(filePath) {
return XLSX.utils.sheet_to_json(workbook.Sheets[firstSheetName], { return XLSX.utils.sheet_to_json(workbook.Sheets[firstSheetName], {
header: 1, header: 1,
blankrows: false, blankrows: true,
defval: '' defval: ''
}).map(row => row.map(cell => String(cell ?? '').trim())); }).map(row => row.map(cell => String(cell ?? '').trim()));
} }
@ -105,6 +107,11 @@ function findFirstHeaderIndex(headers, predicate) {
return headers.map(normalizeHeader).findIndex(predicate); return headers.map(normalizeHeader).findIndex(predicate);
} }
function hasHeaderAlias(headers, aliases) {
const normalizedAliases = aliases.map(normalizeHeader);
return headers.map(normalizeHeader).some(header => normalizedAliases.includes(header));
}
function hasCepHeader(headers) { function hasCepHeader(headers) {
return headers.map(normalizeHeader).some(header => /\bcep\b/.test(header) || header === 'codigo postal'); return headers.map(normalizeHeader).some(header => /\bcep\b/.test(header) || header === 'codigo postal');
} }
@ -116,9 +123,8 @@ function hasAddressOrNumberHeader(headers) {
} }
function hasGeoHeaders(headers) { function hasGeoHeaders(headers) {
const normalizedHeaders = headers.map(normalizeHeader); return hasHeaderAlias(headers, ['latitude', 'lat'])
return normalizedHeaders.some(header => header.includes('latitude')) && hasHeaderAlias(headers, ['longitude', 'long', 'lng', 'lon']);
&& normalizedHeaders.some(header => header.includes('longitude'));
} }
function findHeaderRowIndex(rows) { function findHeaderRowIndex(rows) {
@ -137,8 +143,8 @@ function resolveColumnIndexes(headers) {
idxCep: findFirstHeaderIndex(headers, header => /\bcep\b/.test(header) || header === 'codigo postal'), idxCep: findFirstHeaderIndex(headers, header => /\bcep\b/.test(header) || header === 'codigo postal'),
idxNumero: exactIndex(['numero', 'número', 'num', 'nº', 'n°']), idxNumero: exactIndex(['numero', 'número', 'num', 'nº', 'n°']),
idxEndereco: findFirstHeaderIndex(headers, header => header.includes('endereco') || header.includes('logradouro')), idxEndereco: findFirstHeaderIndex(headers, header => header.includes('endereco') || header.includes('logradouro')),
idxLatitude: findFirstHeaderIndex(headers, header => header.includes('latitude')), idxLatitude: exactIndex(['latitude', 'lat']),
idxLongitude: findFirstHeaderIndex(headers, header => header.includes('longitude')) idxLongitude: exactIndex(['longitude', 'long', 'lng', 'lon'])
}; };
} }
@ -227,6 +233,82 @@ function formatApiErrorResponse(error) {
return error && (error.message || String(error)); return error && (error.message || String(error));
} }
function buildSuccessResultColumns(viab) {
const provedor = viab.provedor ?? '';
const distancia = viab.distancia ?? (viab.raw && (viab.raw.distancia || viab.raw.distance)) ?? '';
const dedicado = viab.dedicado ? 'Viavel' : 'Nao Viavel';
const naoDedicado = viab.naoDedicado ? 'Viavel' : 'Nao Viavel';
const error = viab.error ? cleanCsvValue(viab.error) : '';
return [provedor, distancia, dedicado, naoDedicado, error];
}
function buildErrorResultColumns(err) {
return ['', '', '', '', cleanCsvValue(formatApiErrorResponse(err))];
}
function shiftCellAddress(address, colOffset) {
const decoded = XLSX.utils.decode_cell(address);
decoded.c += colOffset;
return XLSX.utils.encode_cell(decoded);
}
function shiftRange(range, colOffset) {
const decoded = typeof range === 'string' ? XLSX.utils.decode_range(range) : range;
return {
s: { r: decoded.s.r, c: decoded.s.c + colOffset },
e: { r: decoded.e.r, c: decoded.e.c + colOffset }
};
}
function prependResultColumnsToWorksheet(worksheet, headerRowIndex, rowResults) {
const colOffset = RESULT_HEADERS.length;
const shiftedWorksheet = {};
Object.keys(worksheet).forEach(key => {
if (key[0] === '!') return;
shiftedWorksheet[shiftCellAddress(key, colOffset)] = worksheet[key];
});
const originalRange = worksheet['!ref']
? XLSX.utils.decode_range(worksheet['!ref'])
: { s: { r: 0, c: 0 }, e: { r: headerRowIndex, c: 0 } };
shiftedWorksheet['!ref'] = XLSX.utils.encode_range({
s: { r: Math.min(originalRange.s.r, headerRowIndex), c: 0 },
e: { r: originalRange.e.r, c: originalRange.e.c + colOffset }
});
if (worksheet['!cols']) {
shiftedWorksheet['!cols'] = Array(colOffset).fill({ wch: 16 }).concat(worksheet['!cols']);
}
if (worksheet['!merges']) {
shiftedWorksheet['!merges'] = worksheet['!merges'].map(merge => shiftRange(merge, colOffset));
}
if (worksheet['!autofilter'] && worksheet['!autofilter'].ref) {
shiftedWorksheet['!autofilter'] = {
...worksheet['!autofilter'],
ref: XLSX.utils.encode_range(shiftRange(worksheet['!autofilter'].ref, colOffset))
};
}
RESULT_HEADERS.forEach((value, index) => {
const address = XLSX.utils.encode_cell({ r: headerRowIndex, c: index });
shiftedWorksheet[address] = { t: 's', v: value };
});
rowResults.forEach(({ rowIndex, values }) => {
values.forEach((value, index) => {
const address = XLSX.utils.encode_cell({ r: rowIndex, c: index });
shiftedWorksheet[address] = { t: 's', v: String(value ?? '') };
});
});
return shiftedWorksheet;
}
async function countValidLines(inputPath) { async function countValidLines(inputPath) {
await discoverDataType(inputPath); await discoverDataType(inputPath);
const rows = readRows(inputPath); const rows = readRows(inputPath);
@ -251,11 +333,44 @@ async function processCsvFile(jobId, inputPath, originalName) {
const headers = rows[headerRowIndex] || []; const headers = rows[headerRowIndex] || [];
const indexes = resolveColumnIndexes(headers); const indexes = resolveColumnIndexes(headers);
const baseName = path.parse(originalName || inputPath).name; const baseName = path.parse(originalName || inputPath).name;
const outputFilename = `processed_${Date.now()}_${baseName}.csv`; const isExcel = isExcelFile(inputPath);
const outputFilename = `processed_${Date.now()}_${baseName}${isExcel ? '.xlsx' : '.csv'}`;
const outputPath = path.join(__dirname, '..', 'outputs', outputFilename); const outputPath = path.join(__dirname, '..', 'outputs', outputFilename);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
if (isExcel) {
const workbook = XLSX.readFile(inputPath, { cellDates: false, raw: false, cellStyles: true });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const rowResults = [];
for (let rowIndex = headerRowIndex + 1; rowIndex < rows.length; rowIndex++) {
const cols = rows[rowIndex];
const geoPayload = buildGeoPayload(cols, indexes);
const cepPayload = buildCepPayload(cols, indexes);
if (!geoPayload && !cepPayload) continue;
try {
const viab = await consultarComFallback(geoPayload, cepPayload);
rowResults.push({ rowIndex, values: buildSuccessResultColumns(viab) });
incrementProcessed(jobId);
} catch (err) {
rowResults.push({ rowIndex, values: buildErrorResultColumns(err) });
incrementErrors(jobId);
incrementProcessed(jobId);
}
}
workbook.Sheets[firstSheetName] = prependResultColumnsToWorksheet(worksheet, headerRowIndex, rowResults);
XLSX.writeFile(workbook, outputPath, { bookType: 'xlsx' });
finishJob(jobId, path.basename(outputPath));
return outputPath;
}
const outStream = fs.createWriteStream(outputPath, { encoding: 'utf8' }); const outStream = fs.createWriteStream(outputPath, { encoding: 'utf8' });
outStream.write('\uFEFF'); outStream.write('\uFEFF');
outStream.write(['Provedor', 'Distancia', 'Dedicado', 'Nao Dedicado', 'Erro', ...headers].join(';') + '\n'); outStream.write([...RESULT_HEADERS, ...headers].join(';') + '\n');
for (const cols of rows.slice(headerRowIndex + 1)) { for (const cols of rows.slice(headerRowIndex + 1)) {
const geoPayload = buildGeoPayload(cols, indexes); const geoPayload = buildGeoPayload(cols, indexes);
@ -264,18 +379,11 @@ async function processCsvFile(jobId, inputPath, originalName) {
try { try {
const viab = await consultarComFallback(geoPayload, cepPayload); const viab = await consultarComFallback(geoPayload, cepPayload);
const provedor = viab.provedor ?? ''; const outCols = [...buildSuccessResultColumns(viab), ...cols].map(cleanCsvValue);
const distancia = viab.distancia ?? (viab.raw && (viab.raw.distancia || viab.raw.distance)) ?? '';
const dedicado = viab.dedicado ? 'Viavel' : 'Nao Viavel';
const naoDedicado = viab.naoDedicado ? 'Viavel' : 'Nao Viavel';
const error = viab.error ? cleanCsvValue(viab.error) : '';
const outCols = [provedor, distancia, dedicado, naoDedicado, error, ...cols].map(cleanCsvValue);
outStream.write(outCols.join(';') + '\n'); outStream.write(outCols.join(';') + '\n');
incrementProcessed(jobId); incrementProcessed(jobId);
} catch (err) { } catch (err) {
const errMsg = cleanCsvValue(formatApiErrorResponse(err)); const outCols = [...buildErrorResultColumns(err), ...cols].map(cleanCsvValue);
const outCols = ['', '', '', '', errMsg, ...cols].map(cleanCsvValue);
outStream.write(outCols.join(';') + '\n'); outStream.write(outCols.join(';') + '\n');
incrementErrors(jobId); incrementErrors(jobId);
incrementProcessed(jobId); incrementProcessed(jobId);

View File

@ -67,7 +67,7 @@ function readExcelHeaders(filePath) {
const rows = XLSX.utils.sheet_to_json(workbook.Sheets[firstSheetName], { const rows = XLSX.utils.sheet_to_json(workbook.Sheets[firstSheetName], {
header: 1, header: 1,
blankrows: false, blankrows: true,
defval: '' defval: ''
}); });
@ -105,8 +105,8 @@ function hasAddressOrNumberHeader(headers) {
} }
function hasGeoHeaders(headers) { function hasGeoHeaders(headers) {
return headers.some(header => header.includes('latitude')) return hasHeader(headers, ['latitude', 'lat'])
&& headers.some(header => header.includes('longitude')); && hasHeader(headers, ['longitude', 'long', 'lng', 'lon']);
} }
async function consultarViabilidade(data) { async function consultarViabilidade(data) {