diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6f3a291 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5501 +} \ No newline at end of file diff --git a/public/assets/Wpp.png b/public/assets/Wpp.png new file mode 100644 index 0000000..12109d6 Binary files /dev/null and b/public/assets/Wpp.png differ diff --git a/public/index.html b/public/index.html index 37fcf50..e1f341d 100644 --- a/public/index.html +++ b/public/index.html @@ -6,7 +6,7 @@ Validação Viabilidade - +
@@ -19,36 +19,85 @@
- -
-
-
Upload de CSV
-
-
- -
- -
-
- + +
+
+

Consulta por CEP

+
+
+

+ Lorem ipsum dolor sit, amet consectetur adipisicing elit. Harum reprehenderit modi, rerum minima vitae earum delectus repudiandae expedita, architecto voluptatum atque cumque autem repellendus. Maiores aut adipisci repellendus facilis repellat! +

-
Consulta Manual por CEP
-
-
-
+ +
+
+ + +
+
+ + +
-
+
+
+
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+ +
+
+

+ Lorem ipsum dolor sit, amet consectetur adipisicing elit. Similique, odio iste. Beatae soluta a quas unde iste quia voluptatum, recusandae illo aperiam, est quibusdam? Officia deserunt temporibus at impedit repellat! +

+
+ +
+
+ Ícone Whatsapp +
+
+ + 0800 020 1337 + +
+
+
+
+
diff --git a/public/main.js b/public/main.js index 18e09b7..b258c00 100644 --- a/public/main.js +++ b/public/main.js @@ -1,69 +1,37 @@ -document.getElementById('uploadForm').addEventListener('submit', async (e) => { - e.preventDefault(); - const fileEl = document.getElementById('csvfile'); - if (!fileEl.files.length) return; - const fd = new FormData(); - fd.append('csvfile', fileEl.files[0]); - const resEl = document.getElementById('uploadResult'); - resEl.innerText = 'Enviando...'; - try { - const resp = await fetch('/upload', { method: 'POST', body: fd }); - const data = await resp.json(); - if (data.jobId) { - // show progress bar - document.getElementById('progressWrap').style.display = 'block'; - pollJob(data.jobId); - resEl.innerText = `Consultando viabilidade...`; - } else if (data.error) { - resEl.innerText = 'Erro: ' + data.error; - } - } catch (e) { - resEl.innerText = 'Erro no upload'; - } -}); - -async function pollJob(jobId) { - const resEl = document.getElementById('uploadResult'); - const bar = document.getElementById('progressBar'); - try { - const resp = await fetch(`/status/${jobId}`); - const j = await resp.json(); - if (j.total && j.total > 0) { - const pct = Math.round((j.processed / j.total) * 100); - bar.style.width = pct + '%'; - bar.innerText = `${pct}%`; - } - if (j.status === 'done') { - bar.style.width = '100%'; - bar.innerText = '100%'; - resEl.innerHTML = `Concluído. Baixar CSV processado`; - return; - } - if (j.status === 'error') { - resEl.innerText = 'Erro no processamento: ' + j.error; - return; - } - // ainda processando - setTimeout(() => pollJob(jobId), 1000); - } catch (e) { - resEl.innerText = 'Erro ao consultar status do job'; - } -} - document.getElementById('btnConsultaCep').addEventListener('click', async () => { const cep = document.getElementById('cep').value; const numero = document.getElementById('numero').value; - const el = document.getElementById('consultaResult'); - el.innerText = 'Consultando...'; + const endereco = document.getElementById('consultaResultAddress'); + const resultados = document.getElementById('consultaResultViabilidade'); + const dedicado = document.getElementById('link-dedicado'); + const naoDedicado = document.getElementById('link-nao-dedicado'); + endereco.innerText = 'Consultando...'; try { const resp = await fetch(`/consulta-cep?cep=${encodeURIComponent(cep)}&numero=${encodeURIComponent(numero)}`); const data = await resp.json(); if (data.distancia) { - el.innerText = `Endereço: ${data.endereco}\nLat: ${data.latitude} Lon: ${data.longitude}\nDistância: ${data.distancia}`; + // colocar o card-results__container (resultados) com display block + endereco.innerText = `Endereço: ${data.endereco}.`; + resultados.style.display = 'block'; + + // insere nos spans link-dedicado e link-nao-dedicado os textos de viabilidade e se for viavel adicionar classe "viavel" e se for inviavel adicionar classe "inviavel" + dedicado.innerText = data.dedicado; + naoDedicado.innerText = data.naoDedicado; + if (data.dedicado === 'Viável') { + dedicado.classList.add('viavel'); + } else if (data.dedicado === 'Não viável') { + dedicado.classList.add('inviavel'); + } + + if (data.naoDedicado === 'Viável') { + naoDedicado.classList.add('viavel'); + } else if (data.naoDedicado === 'Não viável') { + naoDedicado.classList.add('inviavel'); + } } else if (data.error) { - el.innerText = 'Erro: ' + data.error; + endereco.innerText = 'Erro: ' + data.error; } } catch (e) { - el.innerText = 'Erro na consulta'; + endereco.innerText = 'Erro na consulta'; } }); diff --git a/public/style.css b/public/style.css index 6b720fe..fa82dab 100644 --- a/public/style.css +++ b/public/style.css @@ -21,6 +21,71 @@ padding: 0px !important; } +.description__container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 1rem; +} + +.description-title { + margin-bottom: 15px; +} + +.description-content > p { + text-align: center; +} + +.card-results__container{ + margin-top: 50px; + display: none; +} + +.card-results__container .card{ + margin-top: 5px; + min-height: 80px; + justify-content: center; +} + +.card-content { + display: flex; + align-items: center; + height: 80px; +} + +.card-content-label { + display: flex; + align-items: center; + border-right: 1px solid gray; + padding: 1rem; + width: 60%; + height: 100%; +} + +.card-content-result { + padding: 1rem; + display: flex; + justify-content: center; + width: 40%; + height: 100%; + +} + +.viavel { + font-weight: bold; + color: #234164; +} + +.inviavel { + font-weight: bold; + color: #d40000; +} + +a { + text-decoration: none; +} + button { background-color: #31a3b5 !important; border: none !important; @@ -38,6 +103,43 @@ button:hover { margin-top: 40px; } +.call__container { + margin-top: 50px; + padding: 1rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.call-text > p { + text-align: center; +} + +.call-button__container { + display: flex; + justify-content: center; + align-items: center; + padding: 0.5rem 1rem; + background-color: #25D366; + border-radius: 8px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin-top: 15px; +} + +.call-button-phone { + margin-left: 10px; +} + +.call-button-phone > span { + color: white; + font-weight: bold; +} + +.call__container { + margin-bottom: 50px; +} + @media screen and (max-width: 768px) { .header__container { @@ -52,4 +154,37 @@ button:hover { .header-logo__container > img { width: 80px; } +} + +@media screen and (min-width: 768px) { + body > div.container > div.card > div > div.row.g-2.card-body-input__container > div:nth-child(3){ + display: flex; + align-items: flex-end ; + margin-top: 0px; + } + + .description__container { + align-items: flex-start; + padding-left: 0px; + } + + .description-content { + width: 50%; + } + + .description-content > p { + text-align: left; + } + + .call-text { + width: 50%; + } + + .card-results__container .card { + min-height: fit-content; + } + + .card-content { + height: fit-content; + } } \ No newline at end of file diff --git a/server.js b/server.js index 6eb071c..cac4d0b 100644 --- a/server.js +++ b/server.js @@ -153,252 +153,6 @@ async function getMinDistance(lat, lon) { return null; } -// upload CSV endpoint -const jobs = {}; // jobId -> { status, total, processed, download, error } - -app.post('/upload', upload.single('csvfile'), (req, res) => { - if (!req.file) return res.status(400).json({ error: 'Nenhum arquivo enviado' }); - const filePath = req.file.path; - const jobId = `${Date.now()}-${Math.random().toString(36).slice(2,8)}`; - jobs[jobId] = { status: 'queued', total: 0, processed: 0, download: null, error: null }; - - (async () => { - jobs[jobId].status = 'processing'; - try { - const rows = []; - await new Promise((resolve, reject) => { - fs.createReadStream(filePath) - .pipe(csv({ separator: ';' })) - .on('data', (data) => rows.push(data)) - .on('end', resolve) - .on('error', reject); - }); - - jobs[jobId].total = rows.length; - - const coordCache = new Map(); - - const outRows = []; - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - // normalize keys to avoid duplicates caused by different headers - const norm = {}; - Object.keys(row).forEach(k => { - // normalize header: lowercase, remove diacritics and non-alphanumeric - const kn = k.trim().toLowerCase().normalize('NFKD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]/g, ''); - norm[kn] = row[k]; - }); - - // Input columns (from normalized map) - const rawCep = norm['cep'] ? String(norm['cep']).replace(/\D/g, '') : ''; - const rawNumero = norm['numero'] ? String(norm['numero']).trim() : ''; - // prefer lat/lon from normalized input if available - const rawLat = norm['latitude'] || norm['lat'] || null; - const rawLon = norm['longitude'] || norm['lon'] || norm['long'] || null; - - // Prefer existing lat/lon if provided from normalized fields - let lat = null, lon = null; - if (rawLat && rawLon) { - lat = Number(String(rawLat).replace(',', '.')); - lon = Number(String(rawLon).replace(',', '.')); - } - - let builtAddress = ''; - - // If no coords, try ViaCEP -> Google - if (!Number.isFinite(lat) || !Number.isFinite(lon)) { - if (rawCep) { - const cep8 = rawCep.padStart(8, '0'); - const viaCepData = await fetchJson(`https://viacep.com.br/ws/${cep8}/json/`); - if (viaCepData && !viaCepData.erro) { - const logradouro = viaCepData.logradouro || ''; - const bairro = viaCepData.bairro || ''; - const cidade = viaCepData.localidade || ''; - const uf = viaCepData.uf || ''; - if (logradouro) { - builtAddress = `${logradouro}, ${rawNumero}, ${bairro}, ${cidade} - ${uf}`.replace(/, ,/g, ',').replace(/^,\s*/, ''); - } else { - // fallback: use neighborhood/city - builtAddress = `${bairro || ''} ${cidade ? ', ' + cidade : ''} ${uf ? '- ' + uf : ''}`.trim(); - } - - // build addressToUse (builtAddress already assembled above) - if (!process.env.GOOGLE_API_KEY) { - console.error('[ERROR] GOOGLE_API_KEY não definida. Não será possível geocodificar. Defina a chave no .env ou em process.env'); - } else { - const addressToUse = builtAddress || `${cidade} ${uf} ${cep8}`; - const geo = await geocodeWithGoogle(addressToUse); - if (geo) { lat = geo.lat; lon = geo.lon; } - else console.warn(`Google Geocoding não retornou resultado para '${addressToUse}' (CEP ${cep8}, row ${i+1})`); - } - } else { - console.warn(`ViaCEP erro for CEP ${rawCep} (row ${i+1})`); - } - } else { - console.log(`Row ${i+1}: missing/invalid CEP -> '${rawCep}'`); - } - } - - // Prepare explicit output row to avoid extra columns - const out = { - 'CEP': rawCep || '', - 'Número': rawNumero || '', - 'Endereço': builtAddress || '', - // write lat/lon as strings with dot decimal and fixed precision to avoid locale swaps - 'Latitude': Number.isFinite(lat) ? Number(lat).toFixed(6) : '', - 'Longitude': Number.isFinite(lon) ? Number(lon).toFixed(6) : '', - 'Não dedicado': '', - 'Dedicado': '', - 'Distancia': '', - 'Parceiro/Sothis': '' - }; - - if (Number.isFinite(lat) && Number.isFinite(lon)) { - - const coordKey = `${lat.toFixed(6)},${lon.toFixed(6)}`; - if (coordCache.has(coordKey)) { - const cached = coordCache.get(coordKey); // cached is either null or { dist, pastaSigla } - if (cached !== null) { - const d = cached.dist; - const di = Math.round(Number(d)); - out['Não dedicado'] = di <= 500 ? 'viável' : 'Não viável'; - out['Dedicado'] = di <= 1000 ? 'viável' : 'Não viável'; - out['Distancia'] = `${di}M`; - out['Parceiro/Sothis'] = normalizePartnerSigla(cached.pastaSigla) || ''; - } else { - out['Não dedicado'] = 'Não viável'; - out['Dedicado'] = 'Não viável'; - out['Distancia'] = '5km +'; - out['Parceiro/Sothis'] = ''; - } - } else { - const minResult = await getMinDistance(lat, lon); // { dist, pastaSigla } or null - coordCache.set(coordKey, minResult); - if (minResult !== null) { - const di = Math.round(Number(minResult.dist)); - out['Não dedicado'] = di <= 500 ? 'viável' : 'Não viável'; - out['Dedicado'] = di <= 1000 ? 'viável' : 'Não viável'; - out['Distancia'] = `${di}M`; - out['Parceiro/Sothis'] = normalizePartnerSigla(minResult.pastaSigla) || ''; - } else { - out['Não dedicado'] = 'Não viável'; - out['Dedicado'] = 'Não viável'; - out['Distancia'] = '5km +'; - out['Parceiro/Sothis'] = ''; - } - await sleep(REQUEST_DELAY_MS); - } - } else { - // no coords available -> keep defaults - } - - outRows.push(out); - jobs[jobId].processed = i + 1; - } - - // write output csv - use explicit outRows and fixed header order - const outPath = path.join(__dirname, 'outputs'); - if (!fs.existsSync(outPath)) fs.mkdirSync(outPath); - const originalName = (req.file && req.file.originalname) ? req.file.originalname : `upload_${Date.now()}.csv`; - const parsed = path.parse(originalName); - let outBase = `${parsed.name}_output`; - let outFile = path.join(outPath, `${outBase}.csv`); - if (fs.existsSync(outFile)) { - outFile = path.join(outPath, `${outBase}_${Date.now()}.csv`); - } - const headers = ['CEP','Número','Endereço','Latitude','Longitude','Não dedicado','Dedicado','Distancia','Parceiro/Sothis']; - await new Promise((resolve, reject) => { - const ws = fs.createWriteStream(outFile); - ws.write('\uFEFF'); - fastCsv.write(outRows, { headers: headers, delimiter: ';' }).pipe(ws).on('finish', resolve).on('error', reject); - }); - - try { fs.unlinkSync(filePath); } catch (e) {} - - jobs[jobId].status = 'done'; - jobs[jobId].download = `/download/${path.basename(outFile)}`; - } catch (err) { - console.error(err); - jobs[jobId].status = 'error'; - jobs[jobId].error = String(err.message || err); - } - })(); - - return res.json({ jobId }); -}); - -// download endpoint -app.get('/download/:name', (req, res) => { - const name = req.params.name; - const p = path.join(__dirname, 'outputs', name); - if (!fs.existsSync(p)) return res.status(404).send('Arquivo não encontrado'); - res.download(p); -}); - -// job status endpoint -app.get('/status/:jobId', (req, res) => { - const job = jobs[req.params.jobId]; - if (!job) return res.status(404).json({ error: 'job não encontrado' }); - return res.json(job); -}); - -// manual query endpoint -// /consulta now accepts either latitude+longitude OR cep+numero. If cep is provided we resolve ViaCEP -> Google -> Geogrid -app.get('/consulta', async (req, res) => { - const { latitude: rawLat, longitude: rawLon, cep: rawCep, numero: rawNumero } = req.query; - - // If cep provided, use ViaCEP -> Google geocoding -> Geogrid - if (rawCep) { - const cep = String(rawCep).replace(/\D/g, ''); - const numero = rawNumero ? String(rawNumero).trim() : ''; - try { - const viaCepData = await fetchJson(`https://viacep.com.br/ws/${cep}/json/`); - if (!viaCepData || viaCepData.erro) return res.status(404).json({ error: 'CEP não encontrado' }); - const logradouro = viaCepData.logradouro || ''; - const bairro = viaCepData.bairro || ''; - const cidade = viaCepData.localidade || ''; - const uf = viaCepData.uf || ''; - const endereco = `${logradouro}, ${numero}, ${bairro}, ${cidade} - ${uf}`.replace(/, ,/g, ',').replace(/^,\s*/, ''); - - if (!process.env.GOOGLE_API_KEY) return res.status(500).json({ error: 'GOOGLE_API_KEY não definida no servidor' }); - const geo = await geocodeWithGoogle(endereco || `${cidade} ${uf} ${cep}`); - if (!geo) return res.status(404).json({ error: 'geocode não encontrado (Google)' }); - const lat = Number(geo.lat); - const lon = Number(geo.lon); - const result = await getMinDistance(lat, lon); - if (result && result.dist !== undefined) { - return res.json({ endereco, latitude: lat, longitude: lon, distancia: result.dist, parceiro: result.pastaSigla || '' }); - } - return res.json({ endereco, latitude: lat, longitude: lon, distancia: '5km +' }); - } catch (err) { - console.error(err); - return res.status(500).json({ error: 'Erro na consulta' }); - } - } - - // Otherwise require latitude+longitude - if (!rawLat || !rawLon) return res.status(400).json({ error: 'latitude e longitude são obrigatórios (ou forneça cep)' }); - const latitude = Number(String(rawLat).replace(',', '.')); - const longitude = Number(String(rawLon).replace(',', '.')); - if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) { - console.warn(`Consulta manual com parâmetros inválidos: lat='${rawLat}' lon='${rawLon}'`); - return res.status(400).json({ error: 'latitude ou longitude inválidos' }); - } - - try { - console.log(`Consulta manual: lat=${latitude} lon=${longitude}`); - const result = await getMinDistance(latitude, longitude); - console.log(`Resultado consulta manual: ${JSON.stringify(result)}`); - if (result && result.dist !== undefined) { - return res.json({ distancia: result.dist, parceiro: result.pastaSigla || '' }); - } - return res.json({ distancia: '5km +' }); - } catch (err) { - console.error(err); - return res.status(500).json({ error: 'Erro na consulta' }); - } -}); - // manual CEP+Numero query: resolves ViaCEP -> Nominatim -> Geogrid app.get('/consulta-cep', async (req, res) => { const { cep: rawCep, numero: rawNumero } = req.query; @@ -422,9 +176,15 @@ app.get('/consulta-cep', async (req, res) => { const lon = Number(geo.lon); const result = await getMinDistance(lat, lon); if (result && result.dist !== undefined) { - return res.json({ endereco, latitude: lat, longitude: lon, distancia: result.dist, parceiro: result.pastaSigla || '' }); + // preciso criar 2 campos: Link Dedicado e Link Não Dedicado em que o dedicado é viável até 1000m e o não dedicado até 500m + if (result.dist <= 500) { + return res.json({ endereco, latitude: lat, longitude: lon, distancia: result.dist, dedicado: 'Viável', naoDedicado: 'Viável' }); + } else if (result.dist <= 1000) { + return res.json({ endereco, latitude: lat, longitude: lon, distancia: result.dist, dedicado: 'Viável', naoDedicado: 'Não viável' }); + } else { + return res.json({ endereco, latitude: lat, longitude: lon, distancia: result.dist, dedicado: 'Não viável', naoDedicado: 'Não viável' }); + } } - return res.json({ endereco, latitude: lat, longitude: lon, distancia: '5km +' }); } catch (err) { console.error(err); return res.status(500).json({ error: 'erro na consulta' });