FEAT: Adicionado suporte para cabeçalhos em arquivos Excel e ajuste na leitura de linhas em CSV
This commit is contained in:
parent
ae78907246
commit
a3245592c8
@ -36,7 +36,7 @@ async function pollJob(jobId) {
|
||||
if (j.status === 'done') {
|
||||
bar.style.width = '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;
|
||||
}
|
||||
if (j.status === 'error') {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user