diff --git a/public/main.js b/public/main.js
index 9adf58f..264ab9b 100644
--- a/public/main.js
+++ b/public/main.js
@@ -36,7 +36,7 @@ async function pollJob(jobId) {
if (j.status === 'done') {
bar.style.width = '100%';
bar.innerText = '100%';
- resEl.innerHTML = `Concluído. Baixar CSV processado`;
+ resEl.innerHTML = `Concluído. Baixar arquivo processado`;
return;
}
if (j.status === 'error') {
@@ -110,4 +110,4 @@ document.addEventListener("DOMContentLoaded", () => {
link2.click();
link2.remove();
});
-});
\ No newline at end of file
+});
diff --git a/service/csvService.js b/service/csvService.js
index 237c09d..41aba2c 100644
--- a/service/csvService.js
+++ b/service/csvService.js
@@ -9,6 +9,8 @@ const {
finishJob
} = require('./jobStore.service');
+const RESULT_HEADERS = ['Provedor', 'Distancia', 'Dedicado', 'Nao Dedicado', 'Erro'];
+
function normalizeHeader(value) {
return String(value || '')
.trim()
@@ -92,7 +94,7 @@ function readExcelRows(filePath) {
return XLSX.utils.sheet_to_json(workbook.Sheets[firstSheetName], {
header: 1,
- blankrows: false,
+ blankrows: true,
defval: ''
}).map(row => row.map(cell => String(cell ?? '').trim()));
}
@@ -105,6 +107,11 @@ function findFirstHeaderIndex(headers, 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) {
return headers.map(normalizeHeader).some(header => /\bcep\b/.test(header) || header === 'codigo postal');
}
@@ -116,9 +123,8 @@ function hasAddressOrNumberHeader(headers) {
}
function hasGeoHeaders(headers) {
- const normalizedHeaders = headers.map(normalizeHeader);
- return normalizedHeaders.some(header => header.includes('latitude'))
- && normalizedHeaders.some(header => header.includes('longitude'));
+ return hasHeaderAlias(headers, ['latitude', 'lat'])
+ && hasHeaderAlias(headers, ['longitude', 'long', 'lng', 'lon']);
}
function findHeaderRowIndex(rows) {
@@ -137,8 +143,8 @@ function resolveColumnIndexes(headers) {
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: findFirstHeaderIndex(headers, header => header.includes('latitude')),
- idxLongitude: findFirstHeaderIndex(headers, header => header.includes('longitude'))
+ idxLatitude: exactIndex(['latitude', 'lat']),
+ idxLongitude: exactIndex(['longitude', 'long', 'lng', 'lon'])
};
}
@@ -227,6 +233,82 @@ function formatApiErrorResponse(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) {
await discoverDataType(inputPath);
const rows = readRows(inputPath);
@@ -251,11 +333,44 @@ async function processCsvFile(jobId, inputPath, originalName) {
const headers = rows[headerRowIndex] || [];
const indexes = resolveColumnIndexes(headers);
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);
+ 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' });
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)) {
const geoPayload = buildGeoPayload(cols, indexes);
@@ -264,18 +379,11 @@ async function processCsvFile(jobId, inputPath, originalName) {
try {
const viab = await consultarComFallback(geoPayload, cepPayload);
- 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) : '';
-
- const outCols = [provedor, distancia, dedicado, naoDedicado, error, ...cols].map(cleanCsvValue);
+ const outCols = [...buildSuccessResultColumns(viab), ...cols].map(cleanCsvValue);
outStream.write(outCols.join(';') + '\n');
incrementProcessed(jobId);
} catch (err) {
- const errMsg = cleanCsvValue(formatApiErrorResponse(err));
- const outCols = ['', '', '', '', errMsg, ...cols].map(cleanCsvValue);
+ const outCols = [...buildErrorResultColumns(err), ...cols].map(cleanCsvValue);
outStream.write(outCols.join(';') + '\n');
incrementErrors(jobId);
incrementProcessed(jobId);
diff --git a/service/viabilidadeService.js b/service/viabilidadeService.js
index 49e23fe..8a14fe8 100644
--- a/service/viabilidadeService.js
+++ b/service/viabilidadeService.js
@@ -67,7 +67,7 @@ function readExcelHeaders(filePath) {
const rows = XLSX.utils.sheet_to_json(workbook.Sheets[firstSheetName], {
header: 1,
- blankrows: false,
+ blankrows: true,
defval: ''
});
@@ -105,8 +105,8 @@ function hasAddressOrNumberHeader(headers) {
}
function hasGeoHeaders(headers) {
- return headers.some(header => header.includes('latitude'))
- && headers.some(header => header.includes('longitude'));
+ return hasHeader(headers, ['latitude', 'lat'])
+ && hasHeader(headers, ['longitude', 'long', 'lng', 'lon']);
}
async function consultarViabilidade(data) {