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) {