diff --git a/app/services/analytics/operacao_metricas.rb b/app/services/analytics/operacao_metricas.rb
index 744a6ed..7a19cc1 100644
--- a/app/services/analytics/operacao_metricas.rb
+++ b/app/services/analytics/operacao_metricas.rb
@@ -181,7 +181,8 @@ module Analytics
endereco: r['endereco_completo'].presence || r['address'].presence,
unidade: r['contact_name'],
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
@@ -218,6 +219,12 @@ module Analytics
nil
end
+ # Só aceita URL http(s) — evita injetar lixo no
do popup.
+ def foto_url(valor)
+ url = valor.to_s.strip
+ url.match?(%r{\Ahttps?://}i) ? url : nil
+ end
+
def carregar
return [] if @tabelas.empty?
@@ -229,7 +236,7 @@ module Analytics
<<~SQL.strip
SELECT #{label} AS operacao,
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.checkout_latitude, r.checkout_longitude,
#{selects_gade(conn, tabela)}
diff --git a/app/views/operacoes_dashboard/index.html.erb b/app/views/operacoes_dashboard/index.html.erb
index c6cc1d4..f9c05ed 100644
--- a/app/views/operacoes_dashboard/index.html.erb
+++ b/app/views/operacoes_dashboard/index.html.erb
@@ -341,8 +341,12 @@
function popupHtml(p) {
const linha = (icone, val) => val ? ('
' + icone + ' ' + escapeHtml(val) + '
') : '';
+ const foto = p.foto
+ ? '
'
+ : '';
return '' +
- '' +
+ '
' +
'
NF ' + escapeHtml(p.nf) + '
' +
'
' + escapeHtml(p.nome) + '
' +
'
✔ Entrega realizada
' +
@@ -350,11 +354,27 @@
linha('🏥', p.unidade) +
linha('🚚', p.motorista) +
linha('🕒', p.checkout) +
+ foto +
'
';
}
+ // Ícone de localização (balão) — laranja da marca, sem depender de imagem externa.
+ function pinIcon() {
+ return L.divIcon({
+ className: 'pin-entrega',
+ html: '
',
+ iconSize: [28, 40],
+ iconAnchor: [14, 40],
+ popupAnchor: [0, -36]
+ });
+ }
+
function initMapas() {
if (typeof L === 'undefined') return;
+ const ICONE = pinIcon();
document.querySelectorAll('.mapa-box').forEach(function (box) {
const el = box.querySelector('.mapa-calor');
if (!el || el._leaflet_id) return;
@@ -362,22 +382,35 @@
const heat = JSON.parse(el.dataset.heat || '[]');
const pontos = JSON.parse(el.dataset.pontos || '[]');
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
- }).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
const heatLayer = (heat.length && L.heatLayer)
? 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();
pontos.forEach(function (p) {
- L.circleMarker([p.lat, p.lng], {
- radius: 6, color: '#0a0a0a', weight: 1,
- fillColor: '#f97316', fillOpacity: 0.85
- }).bindPopup(popupHtml(p)).addTo(marcadores);
+ L.marker([p.lat, p.lng], { icon: ICONE })
+ .bindPopup(popupHtml(p))
+ .addTo(marcadores);
});
// Começa no Calor (cai para Pontos se não houver calor)