teste #3
@@ -181,7 +181,8 @@ module Analytics
|
|||||||
endereco: r['endereco_completo'].presence || r['address'].presence,
|
endereco: r['endereco_completo'].presence || r['address'].presence,
|
||||||
unidade: r['contact_name'],
|
unidade: r['contact_name'],
|
||||||
motorista: r['driver'],
|
motorista: r['driver'],
|
||||||
checkout: data_de(r['checkout'])&.strftime('%d/%m/%Y')
|
checkout: data_de(r['checkout'])&.strftime('%d/%m/%Y'),
|
||||||
|
foto: foto_url(r['foto_da_fachada'])
|
||||||
}
|
}
|
||||||
end.first(MAX_PONTOS)
|
end.first(MAX_PONTOS)
|
||||||
end
|
end
|
||||||
@@ -218,6 +219,12 @@ module Analytics
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Só aceita URL http(s) — evita injetar lixo no <img src> do popup.
|
||||||
|
def foto_url(valor)
|
||||||
|
url = valor.to_s.strip
|
||||||
|
url.match?(%r{\Ahttps?://}i) ? url : nil
|
||||||
|
end
|
||||||
|
|
||||||
def carregar
|
def carregar
|
||||||
return [] if @tabelas.empty?
|
return [] if @tabelas.empty?
|
||||||
|
|
||||||
@@ -229,7 +236,7 @@ module Analytics
|
|||||||
<<~SQL.strip
|
<<~SQL.strip
|
||||||
SELECT #{label} AS operacao,
|
SELECT #{label} AS operacao,
|
||||||
r.reference_id, r.driver, r.status, r.observation, r.contact_name, r.address,
|
r.reference_id, r.driver, r.status, r.observation, r.contact_name, r.address,
|
||||||
r.checkout, r.planned_date,
|
r.checkout, r.planned_date, r.foto_da_fachada,
|
||||||
r.latitude, r.longitude,
|
r.latitude, r.longitude,
|
||||||
r.checkout_latitude, r.checkout_longitude,
|
r.checkout_latitude, r.checkout_longitude,
|
||||||
#{selects_gade(conn, tabela)}
|
#{selects_gade(conn, tabela)}
|
||||||
|
|||||||
@@ -341,8 +341,12 @@
|
|||||||
|
|
||||||
function popupHtml(p) {
|
function popupHtml(p) {
|
||||||
const linha = (icone, val) => val ? ('<div>' + icone + ' ' + escapeHtml(val) + '</div>') : '';
|
const linha = (icone, val) => val ? ('<div>' + icone + ' ' + escapeHtml(val) + '</div>') : '';
|
||||||
|
const foto = p.foto
|
||||||
|
? '<img src="' + escapeHtml(p.foto) + '" alt="Fachada" loading="lazy" ' +
|
||||||
|
'style="margin-top:8px;width:100%;max-height:150px;object-fit:cover;border-radius:6px">'
|
||||||
|
: '';
|
||||||
return '' +
|
return '' +
|
||||||
'<div style="min-width:200px">' +
|
'<div style="min-width:210px;max-width:240px">' +
|
||||||
'<div style="font-weight:700;color:#f97316">NF ' + escapeHtml(p.nf) + '</div>' +
|
'<div style="font-weight:700;color:#f97316">NF ' + escapeHtml(p.nf) + '</div>' +
|
||||||
'<div style="font-weight:600;margin-bottom:4px">' + escapeHtml(p.nome) + '</div>' +
|
'<div style="font-weight:600;margin-bottom:4px">' + escapeHtml(p.nome) + '</div>' +
|
||||||
'<div style="color:#16a34a;font-size:11px;margin-bottom:6px">✔ Entrega realizada</div>' +
|
'<div style="color:#16a34a;font-size:11px;margin-bottom:6px">✔ Entrega realizada</div>' +
|
||||||
@@ -350,11 +354,27 @@
|
|||||||
linha('🏥', p.unidade) +
|
linha('🏥', p.unidade) +
|
||||||
linha('🚚', p.motorista) +
|
linha('🚚', p.motorista) +
|
||||||
linha('🕒', p.checkout) +
|
linha('🕒', p.checkout) +
|
||||||
|
foto +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ícone de localização (balão) — laranja da marca, sem depender de imagem externa.
|
||||||
|
function pinIcon() {
|
||||||
|
return L.divIcon({
|
||||||
|
className: 'pin-entrega',
|
||||||
|
html: '<svg width="28" height="40" viewBox="0 0 28 40" xmlns="http://www.w3.org/2000/svg">' +
|
||||||
|
'<path d="M14 0C6.8 0 1 5.8 1 13c0 9.7 13 27 13 27s13-17.3 13-27C27 5.8 21.2 0 14 0z" ' +
|
||||||
|
'fill="#f97316" stroke="#0a0a0a" stroke-width="1.5"/>' +
|
||||||
|
'<circle cx="14" cy="13" r="5" fill="#0a0a0a"/></svg>',
|
||||||
|
iconSize: [28, 40],
|
||||||
|
iconAnchor: [14, 40],
|
||||||
|
popupAnchor: [0, -36]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initMapas() {
|
function initMapas() {
|
||||||
if (typeof L === 'undefined') return;
|
if (typeof L === 'undefined') return;
|
||||||
|
const ICONE = pinIcon();
|
||||||
document.querySelectorAll('.mapa-box').forEach(function (box) {
|
document.querySelectorAll('.mapa-box').forEach(function (box) {
|
||||||
const el = box.querySelector('.mapa-calor');
|
const el = box.querySelector('.mapa-calor');
|
||||||
if (!el || el._leaflet_id) return;
|
if (!el || el._leaflet_id) return;
|
||||||
@@ -362,22 +382,35 @@
|
|||||||
const heat = JSON.parse(el.dataset.heat || '[]');
|
const heat = JSON.parse(el.dataset.heat || '[]');
|
||||||
const pontos = JSON.parse(el.dataset.pontos || '[]');
|
const pontos = JSON.parse(el.dataset.pontos || '[]');
|
||||||
const centro = JSON.parse(el.dataset.centro || '[-23.55,-46.63]');
|
const centro = JSON.parse(el.dataset.centro || '[-23.55,-46.63]');
|
||||||
const mapa = L.map(el).setView(centro, 11);
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
// Camadas base (todas grátis, sem chave): escuro (padrão, combina com o
|
||||||
|
// tema), claro e satélite.
|
||||||
|
const escuro = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||||
|
attribution: '© OpenStreetMap © CARTO', subdomains: 'abcd', maxZoom: 20
|
||||||
|
});
|
||||||
|
const claro = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
attribution: '© OpenStreetMap', maxZoom: 19
|
attribution: '© OpenStreetMap', maxZoom: 19
|
||||||
}).addTo(mapa);
|
});
|
||||||
|
const satelite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||||
|
attribution: 'Tiles © Esri', maxZoom: 19
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapa = L.map(el, { layers: [escuro] }).setView(centro, 11);
|
||||||
|
L.control.layers(
|
||||||
|
{ '🌙 Escuro': escuro, '🗺️ Claro': claro, '🛰️ Satélite': satelite },
|
||||||
|
{}, { position: 'topright', collapsed: true }
|
||||||
|
).addTo(mapa);
|
||||||
|
|
||||||
// Camada de calor
|
// Camada de calor
|
||||||
const heatLayer = (heat.length && L.heatLayer)
|
const heatLayer = (heat.length && L.heatLayer)
|
||||||
? L.heatLayer(heat, { radius: 18, blur: 22, maxZoom: 14 }) : null;
|
? L.heatLayer(heat, { radius: 18, blur: 22, maxZoom: 14 }) : null;
|
||||||
|
|
||||||
// Camada de pontos (marcadores com popup)
|
// Camada de pontos (marcadores em balão com popup)
|
||||||
const marcadores = L.layerGroup();
|
const marcadores = L.layerGroup();
|
||||||
pontos.forEach(function (p) {
|
pontos.forEach(function (p) {
|
||||||
L.circleMarker([p.lat, p.lng], {
|
L.marker([p.lat, p.lng], { icon: ICONE })
|
||||||
radius: 6, color: '#0a0a0a', weight: 1,
|
.bindPopup(popupHtml(p))
|
||||||
fillColor: '#f97316', fillOpacity: 0.85
|
.addTo(marcadores);
|
||||||
}).bindPopup(popupHtml(p)).addTo(marcadores);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Começa no Calor (cai para Pontos se não houver calor)
|
// Começa no Calor (cai para Pontos se não houver calor)
|
||||||
|
|||||||
Reference in New Issue
Block a user