-
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
-
+
+
+
+
+ Para link não dedicado (Banda Larga):
+
+
+
+
+
+
+
+
+
+
+
+
+ Para link dedicado:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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!
+
+
+
+
+
+
+
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' });