Adição de Mata com os pontos da onde foi entreque e corrige o filtro ao vivo

This commit is contained in:
2026-06-29 21:06:08 -03:00
parent 6b899ebafa
commit 7d6c79f878
5 changed files with 144 additions and 27 deletions

View File

@@ -1,15 +1,31 @@
<%# locals: metricas (Analytics::OperacaoMetricas) %>
<% pontos = metricas.pontos_mapa %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<div class="flex items-center justify-between mb-4">
<p class="text-gray-400 text-sm font-medium">Mapa de Calor de Entregas</p>
<span class="text-xs text-gray-500"><%= pontos.size %> pontos</span>
<% heat = metricas.pontos_mapa %>
<% pontos = metricas.pontos_detalhados %>
<div class="mapa-box bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<p class="text-gray-400 text-sm font-medium">Mapa de Entregas</p>
<div class="flex items-center gap-2">
<div class="map-toggle inline-flex rounded-lg overflow-hidden border border-white/10 text-xs">
<button type="button" data-view="heat"
class="px-3 py-1.5 bg-orange-500 text-black font-semibold">🔥 Calor</button>
<button type="button" data-view="pontos"
class="px-3 py-1.5 text-gray-300 hover:text-white">📍 Pontos (<%= pontos.size %>)</button>
</div>
<button type="button" class="map-fullscreen px-3 py-1.5 rounded-lg border border-white/10 text-gray-300 hover:text-white text-xs" title="Ver em tela cheia">
⛶ Tela cheia
</button>
</div>
</div>
<% if pontos.empty? %>
<% if heat.empty? && pontos.empty? %>
<p class="text-center py-8 text-gray-500 text-sm">Sem coordenadas para exibir no período.</p>
<% else %>
<div class="mapa-calor h-96 rounded-xl overflow-hidden"
<div class="mapa-calor w-full h-[70vh] min-h-[480px] rounded-xl overflow-hidden bg-[#1a1a1a]"
data-heat="<%= heat.to_json %>"
data-pontos="<%= pontos.to_json %>"
data-centro="<%= metricas.centro_mapa.to_json %>"></div>
<p class="text-xs text-gray-600 mt-2">
🔥 Calor mostra a densidade · 📍 Pontos mostra cada entrega (clique no marcador para detalhes; coordenada de check-out).
</p>
<% end %>
</div>

View File

@@ -25,11 +25,13 @@
<% celulas.each_with_index do |cel, i| %>
<td class="py-2 align-top <%= i.zero? ? 'pr-3 text-gray-300' : 'pl-4 text-right whitespace-nowrap font-semibold text-white' %>">
<% if i.zero? && filtro %>
<%= link_to cel,
operacoes_dashboard_path(base.merge(filtro => cel.to_s)),
<% destino = ativo ? base.except(filtro) : base.merge(filtro => cel.to_s) %>
<%= link_to operacoes_dashboard_path(destino),
data: { turbo: false },
title: "Filtrar por #{cel}",
class: "inline-flex items-center gap-1 hover:text-orange-400 #{'text-orange-400 font-semibold' if ativo}" %>
title: (ativo ? 'Clique para remover o filtro' : "Filtrar por #{cel}"),
class: "inline-flex items-center gap-1 hover:text-orange-400 #{'text-orange-400 font-semibold' if ativo}" do %>
<%= cel %><% if ativo %> <span aria-hidden="true">✕</span><% end %>
<% end %>
<% else %>
<%= cel %>
<% end %>

View File

@@ -333,19 +333,88 @@
}
// ── Mapa de calor (Leaflet) ──
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function popupHtml(p) {
const linha = (icone, val) => val ? ('<div>' + icone + ' ' + escapeHtml(val) + '</div>') : '';
return '' +
'<div style="min-width:200px">' +
'<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="color:#16a34a;font-size:11px;margin-bottom:6px">✔ Entrega realizada</div>' +
linha('📍', p.endereco) +
linha('🏥', p.unidade) +
linha('🚚', p.motorista) +
linha('🕒', p.checkout) +
'</div>';
}
function initMapas() {
if (typeof L === 'undefined') return;
document.querySelectorAll('.mapa-calor').forEach(function (el) {
if (el._leaflet_id) return;
const pontos = JSON.parse(el.dataset.pontos || '[]');
const centro = JSON.parse(el.dataset.centro || '[-23.55,-46.63]');
document.querySelectorAll('.mapa-box').forEach(function (box) {
const el = box.querySelector('.mapa-calor');
if (!el || el._leaflet_id) return;
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', {
attribution: '© OpenStreetMap', maxZoom: 19
}).addTo(mapa);
if (pontos.length && L.heatLayer) {
L.heatLayer(pontos, { radius: 18, blur: 22, maxZoom: 14 }).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)
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);
});
// Começa no Calor (cai para Pontos se não houver calor)
let atual = heatLayer || marcadores;
atual.addTo(mapa);
function mostrar(view) {
mapa.removeLayer(heatLayer || marcadores);
if (marcadores) mapa.removeLayer(marcadores);
if (heatLayer) mapa.removeLayer(heatLayer);
if (view === 'pontos') { marcadores.addTo(mapa); }
else { (heatLayer || marcadores).addTo(mapa); }
box.querySelectorAll('.map-toggle button').forEach(function (b) {
const on = b.dataset.view === view;
b.classList.toggle('bg-orange-500', on);
b.classList.toggle('text-black', on);
b.classList.toggle('font-semibold', on);
b.classList.toggle('text-gray-300', !on);
});
}
box.querySelectorAll('.map-toggle button').forEach(function (b) {
b.addEventListener('click', function () { mostrar(b.dataset.view); });
});
// Tela cheia (Fullscreen API sobre o container do mapa)
const btnFs = box.querySelector('.map-fullscreen');
if (btnFs) {
btnFs.addEventListener('click', function () {
if (document.fullscreenElement) { document.exitFullscreen(); }
else if (el.requestFullscreen) { el.requestFullscreen(); }
});
}
document.addEventListener('fullscreenchange', function () {
setTimeout(function () { mapa.invalidateSize(); }, 150);
});
setTimeout(function () { mapa.invalidateSize(); }, 200);
});
}