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') {
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user