diff --git a/controller/viabilidadeController.js b/controller/viabilidadeController.js index f20b125..f60e3ad 100644 --- a/controller/viabilidadeController.js +++ b/controller/viabilidadeController.js @@ -1,6 +1,8 @@ const { consultarViabilidade } = require('../service/viabilidadeService'); -const { processCsvFile } = require('../service/csvService'); -const { getJob } = require('../service/jobStore.service'); +const { processCsvFile, countValidLines } = require('../service/csvService'); +const { getJob, createJob } = require('../service/jobStore.service'); +const fs = require('fs'); +const path = require('path'); // Controlador para consultar viabilidade @@ -25,15 +27,27 @@ async function uploadCsvFile(req, res) { const filePath = req.file.path; const originalName = req.file.originalname || req.file.filename || 'input.csv'; - const out = await processCsvFile(filePath, originalName); + // Conta as linhas válidas primeiro + const total = await countValidLines(filePath); + if (total === 0) { + return res.status(400).json({ error: 'Nenhuma linha válida encontrada no CSV. Verifique se há colunas CEP e Número.' }); + } - // normaliza retorno (processCsvFile pode retornar string ou objeto) - const outputPath = (typeof out === 'string') ? out : (out && out.outputPath) || null; + // Cria o job + const jobId = createJob(total); - return res.json({ outputPath }); + // Inicia o processamento em background (não aguarda) + processCsvFile(jobId, filePath, originalName).catch(err => { + console.error('Erro no processamento em background:', err); + // Em caso de erro, marca o job como falhado + require('../service/jobStore.service').failJob(jobId, err.message); + }); + + // Retorna o jobId imediatamente para acompanhamento em tempo real + return res.json({ jobId }); } catch (error) { - console.error("Erro ao processar CSV:", error && (error.message || error)); - return res.status(500).json({ error: 'Erro ao processar CSV' }); + console.error("Erro ao iniciar processamento do CSV:", error && (error.message || error)); + return res.status(500).json({ error: 'Erro ao iniciar processamento do CSV' }); } } @@ -51,4 +65,89 @@ async function getJobController(req, res) { } } -module.exports = { consultarViabilidadeController, uploadCsvFile, getJobController }; \ No newline at end of file +// Controlador para download do CSV processado +// Verifica se o job está concluído e serve o arquivo para download +async function downloadCsvController(req, res) { + try { + const jobId = req.params.jobId; + const job = getJob(jobId); + + // Verifica se o job existe, está concluído e tem um link de download + if (!job) { + return res.status(404).json({ error: 'Job não encontrado' }); + } + if (job.status !== 'done') { + return res.status(400).json({ error: 'Processamento ainda não concluído' }); + } + if (!job.download) { + return res.status(404).json({ error: 'Arquivo de download não disponível' }); + } + + // Extrai o nome do arquivo do campo download (ex: '/download/processed_123.csv' -> 'processed_123.csv') + const filename = path.basename(job.download); + const filePath = path.join(__dirname, '..', 'outputs', filename); + + // Verifica se o arquivo existe no sistema de arquivos + if (!fs.existsSync(filePath)) { + return res.status(404).json({ error: 'Arquivo não encontrado no servidor' }); + } + + // Inicia o download do arquivo + res.download(filePath, filename, (err) => { + if (err) { + console.error("Erro ao fazer download do arquivo:", err); + // Se o download falhar, envia erro (mas headers já podem ter sido enviados) + if (!res.headersSent) { + res.status(500).json({ error: 'Erro ao fazer download do arquivo' }); + } + } + }); + } catch (error) { + console.error("Erro no controlador de download:", error && (error.message || error)); + if (!res.headersSent) { + res.status(500).json({ error: 'Erro interno no servidor' }); + } + } +} + +// Controlador para download dos modelos de CSV +// Aceita parâmetro :type ('cep' ou 'geo') para escolher qual modelo baixar +async function downloadModelController(req, res) { + try { + const type = req.params.type; + let filename; + + // Define o nome do arquivo baseado no tipo + if (type === 'cep') { + filename = 'modelo.viabilidade-cep.csv'; + } else if (type === 'geo') { + filename = 'modelo.viabilidade-geolocalizacao.csv'; + } else { + return res.status(400).json({ error: 'Tipo de modelo inválido. Use "cep" ou "geo".' }); + } + + const filePath = path.join(__dirname, '..', 'models', filename); + + // Verifica se o arquivo existe + if (!fs.existsSync(filePath)) { + return res.status(404).json({ error: 'Arquivo de modelo não encontrado' }); + } + + // Inicia o download do arquivo + res.download(filePath, filename, (err) => { + if (err) { + console.error("Erro ao fazer download do modelo:", err); + if (!res.headersSent) { + res.status(500).json({ error: 'Erro ao fazer download do modelo' }); + } + } + }); + } catch (error) { + console.error("Erro no controlador de download de modelo:", error && (error.message || error)); + if (!res.headersSent) { + res.status(500).json({ error: 'Erro interno no servidor' }); + } + } +} + +module.exports = { consultarViabilidadeController, uploadCsvFile, getJobController, downloadCsvController, downloadModelController }; \ No newline at end of file diff --git a/public/main.js b/public/main.js index c3c43c2..e7d3436 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 CSV processado`; return; } if (j.status === 'error') { @@ -84,39 +84,29 @@ document.getElementById('btnConsultaCep').addEventListener('click', async () => } }); -// baixar modelo -// botão que inicia download de todos os modelos individualmente +// baixar modelos +// botão que inicia download dos dois modelos simultaneamente via endpoints document.addEventListener("DOMContentLoaded", () => { const btn = document.getElementById("card__button-download"); if (!btn) return; - btn.addEventListener("click", async (e) => { + btn.addEventListener("click", (e) => { e.preventDefault(); - try { - const resp = await fetch("/download-models/list"); - if (!resp.ok) throw new Error("Não foi possível obter lista de modelos"); - const data = await resp.json(); - const files = data.files || []; - if (!files.length) { - alert("Nenhum arquivo de modelo disponível"); - return; - } + // Baixar modelo de CEP + const link1 = document.createElement('a'); + link1.href = '/download-model/cep'; + link1.download = 'modelo.viabilidade-cep.csv'; + document.body.appendChild(link1); + link1.click(); + link1.remove(); - // Para cada arquivo, cria um e clica nele para iniciar download individual - files.forEach((fname) => { - const url = "/download-model/" + encodeURIComponent(fname); - const a = document.createElement("a"); - a.href = url; - a.download = fname; - // necessário anexar no DOM para funcionar em alguns navegadores - document.body.appendChild(a); - a.click(); - a.remove(); - }); - } catch (err) { - console.error("Erro ao baixar modelos:", err); - alert("Erro ao iniciar downloads: " + (err.message || err)); - } + // Baixar modelo de geolocalização + const link2 = document.createElement('a'); + link2.href = '/download-model/geo'; + link2.download = 'modelo.viabilidade-geolocalizacao.csv'; + document.body.appendChild(link2); + link2.click(); + link2.remove(); }); }); \ No newline at end of file diff --git a/routes/viabilidadeRoutes.js b/routes/viabilidadeRoutes.js index f50ccd0..32b7218 100644 --- a/routes/viabilidadeRoutes.js +++ b/routes/viabilidadeRoutes.js @@ -13,12 +13,13 @@ router.post('/viabilidade', viabilidadeController.consultarViabilidadeController // rota de upload agora usa multer.single('csvfile') router.post('/upload', upload.single('csvfile'), viabilidadeController.uploadCsvFile); -router.get('/status/:jobId', (req, res) => { - const job = viabilidadeController.getJobController(req.params.jobId); - if (!job) { - return res.status(404).json({ error: 'Job não encontrado' }); - } - res.json(job); -}); +// Rota para verificar status do job +router.get('/status/:jobId', viabilidadeController.getJobController); + +// Rota para download do CSV processado +router.get('/download/:jobId', viabilidadeController.downloadCsvController); + +// Rota para download dos modelos +router.get('/download-model/:type', viabilidadeController.downloadModelController); module.exports = router; \ No newline at end of file diff --git a/service/csvService.js b/service/csvService.js index 64c753f..9360951 100644 --- a/service/csvService.js +++ b/service/csvService.js @@ -46,9 +46,11 @@ async function countValidLines(inputPath) { } // nova função: processa CSV linha a linha, chama consultarViabilidade e gera CSV de saída -async function processCsvFile(inputPath) { +// Recebe jobId já criado no controller +async function processCsvFile(jobId, inputPath, originalName) { const total = await countValidLines(inputPath); - const jobId = createJob(total); + // Job já criado no controller + // const jobId = createJob(total); const baseName = path.parse(inputPath).name; const outputFilename = `processed_${Date.now()}_${baseName}.csv`; const outputPath = path.join(__dirname, '..', 'outputs', outputFilename); @@ -137,9 +139,9 @@ async function processCsvFile(inputPath) { outStream.end(); await once(outStream, 'finish'); - finishJob(jobId, `/download/${path.basename(outputPath)}`); + finishJob(jobId, path.basename(outputPath)); - return { jobId, outputPath }; + return outputPath; } -module.exports = { processCsvFile }; \ No newline at end of file +module.exports = { processCsvFile, countValidLines }; \ No newline at end of file