From a5742d178a0099e4a068cb5649f3fd6691858b5ac33210f8186d9b86b0dbcba1 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 30 Jun 2026 11:25:31 -0300 Subject: [PATCH] =?UTF-8?q?Corre=C3=A7oes=20do=20campo=20de=20busca=20no?= =?UTF-8?q?=20mapa=20e=20ajustes=20de=20interface=20no=20dash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/analytics/operacao_metricas.rb | 15 ++++---- .../operacoes_dashboard/_painel.html.erb | 31 +++++++++++----- app/views/operacoes_dashboard/index.html.erb | 35 ++++++++++++------- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/app/services/analytics/operacao_metricas.rb b/app/services/analytics/operacao_metricas.rb index aad45f3..97080aa 100644 --- a/app/services/analytics/operacao_metricas.rb +++ b/app/services/analytics/operacao_metricas.rb @@ -146,11 +146,11 @@ module Analytics } end - # Pontos [lat, lng] das entregas concluídas para o heatmap. + # Pontos [lat, lng] das entregas ATENDIDAS (sucesso + falhas) para o heatmap. # Prefere as coordenadas de CHECKOUT (saída); cai para as de CHECKIN. def pontos_mapa @pontos_mapa ||= linhas.filter_map do |r| - next unless r['status'] == 'completed' + next unless Entrega::STATUS_ATENDIDO.include?(r['status']) lat = num(r['checkout_latitude']) || num(r['latitude']) lng = num(r['checkout_longitude']) || num(r['longitude']) next if lat.nil? || lng.nil? || (lat.zero? && lng.zero?) @@ -165,12 +165,13 @@ module Analytics # Limite de marcadores no modo "Pontos" (Leaflet fica pesado com milhares). MAX_PONTOS = 2000 - # Pontos detalhados (modo "Pontos" do mapa) — um por entrega CONCLUÍDA com - # coordenada de CHECKOUT (onde a entrega foi registrada). Inclui dados para o - # popup (NF, destinatário, endereço, unidade, motorista, data). + # Pontos detalhados (modo "Pontos" do mapa) — um por entrega ATENDIDA + # (sucesso OU insucesso: óbito e demais pilares de falha), com coordenada de + # CHECKOUT (ou planejada como fallback). Inclui dados para o popup (NF, + # destinatário, endereço, unidade, motorista, veículo, data, status, motivo). def pontos_detalhados @pontos_detalhados ||= linhas.filter_map do |r| - next unless r['status'] == 'completed' + next unless Entrega::STATUS_ATENDIDO.include?(r['status']) lat = num(r['checkout_latitude']) || num(r['latitude']) lng = num(r['checkout_longitude']) || num(r['longitude']) next if lat.nil? || lng.nil? || (lat.zero? && lng.zero?) @@ -183,6 +184,8 @@ module Analytics motorista: r['driver'], veiculo: r['vehicle'], checkout: data_de(r['checkout'])&.strftime('%d/%m/%Y'), + sucesso: r['status'] == 'completed', + motivo: r['observation'].presence, foto: foto_url(r['foto_da_fachada']) } end.first(MAX_PONTOS) diff --git a/app/views/operacoes_dashboard/_painel.html.erb b/app/views/operacoes_dashboard/_painel.html.erb index 8b966c8..273c41f 100644 --- a/app/views/operacoes_dashboard/_painel.html.erb +++ b/app/views/operacoes_dashboard/_painel.html.erb @@ -16,22 +16,35 @@ <% end %> <%# ── KPIs ─────────────────────────────────────────────── %> + <% pct_total = ->(parte) { metricas.total.zero? ? 0 : (parte * 100.0 / metricas.total).round(1) } %>
-
+

📦 Total de Entregas

-

<%= metricas.total %>

+

<%= metricas.total %>

+
-
+

✅ Total de Sucesso

-

<%= metricas.sucesso %>

+

<%= metricas.sucesso %>

+
-
+

❌ Total de Recusas

-

<%= metricas.recusas %>

+

<%= metricas.recusas %>

+
-
+

⏳ Entregas Pendentes

-

<%= metricas.pendentes %>

+

<%= metricas.pendentes %>

+
@@ -49,7 +62,7 @@
● Completas <%= ins[:pct_completed] %>% - ● Falhas <%= ins[:pct_failed] %>% + ● Falhas <%= ins[:pct_failed] %>%
<% end %>
diff --git a/app/views/operacoes_dashboard/index.html.erb b/app/views/operacoes_dashboard/index.html.erb index 3612f7c..37f324e 100644 --- a/app/views/operacoes_dashboard/index.html.erb +++ b/app/views/operacoes_dashboard/index.html.erb @@ -24,8 +24,9 @@ .flatpickr-months .flatpickr-prev-month svg, .flatpickr-months .flatpickr-next-month svg { fill: #f97316; } /* O mapa do Leaflet usa fundo claro — moldura escura ao redor */ .leaflet-container { background: #1a1a1a; } - /* Realça ruas e nomes no tile escuro (CARTO dark_all tem baixo contraste) */ - .tiles-escuro-contraste { filter: brightness(1.45) contrast(1.2) saturate(0.9); } + /* Modo escuro = OpenStreetMap completo (detalhe igual ao claro) invertido, + para ruas e nomes ficarem bem visíveis sobre fundo escuro. */ + .tiles-escuro-contraste { filter: invert(1) hue-rotate(180deg) brightness(0.95) contrast(1.05); } <% end %> @@ -262,9 +263,9 @@ }); } - // ── Cores da marca (laranja + vermelho p/ falha) ── + // ── Cores da marca (laranja + azul p/ falha) ── const COR_OK = '#f97316'; // laranja Reem (completas) - const COR_FAIL = '#ef4444'; // vermelho (falhas), igual ao card de Recusas + const COR_FAIL = '#3b82f6'; // azul (falhas), igual ao card de Recusas const COR_A = '#f97316'; // comparativo: A = laranja da marca const COR_B = '#3b82f6'; // comparativo: B = azul (contraste) @@ -350,11 +351,15 @@ 'style="margin-top:8px;width:100%;max-height:160px;object-fit:cover;border-radius:6px;display:block">' + '
📷 Foto indisponível (link expirado/sem acesso)
' : '
📷 Sem foto da fachada para esta entrega
'; + const statusLinha = p.sucesso + ? '
✔ Entrega realizada
' + : '
✖ Insucesso' + + (p.motivo ? ' — ' + escapeHtml(p.motivo) : '') + '
'; return '' + '
' + '
NF ' + escapeHtml(p.nf) + '
' + '
' + escapeHtml(p.nome) + '
' + - '
✔ Entrega realizada
' + + statusLinha + linha('📍', p.endereco) + linha('🏥', p.unidade) + linha('🚚', p.motorista) + @@ -364,13 +369,14 @@ '
'; } - // Ícone de localização (balão) — laranja da marca, sem depender de imagem externa. - function pinIcon() { + // Ícone de localização (balão) — sem depender de imagem externa. + // Cor por resultado: laranja = sucesso, azul = insucesso (falha/óbito). + function pinIcon(cor) { return L.divIcon({ className: 'pin-entrega', html: '' + '' + + 'fill="' + cor + '" stroke="#0a0a0a" stroke-width="1.5"/>' + '', iconSize: [28, 40], iconAnchor: [14, 40], @@ -380,7 +386,8 @@ function initMapas() { if (typeof L === 'undefined') return; - const ICONE = pinIcon(); + const ICONE_OK = pinIcon('#f97316'); // sucesso — laranja + const ICONE_FALHA = pinIcon('#3b82f6'); // insucesso — azul document.querySelectorAll('.mapa-box').forEach(function (box) { const el = box.querySelector('.mapa-calor'); if (!el || el._leaflet_id) return; @@ -390,9 +397,11 @@ const centro = JSON.parse(el.dataset.centro || '[-23.55,-46.63]'); // 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, + // tema), claro e satélite. O escuro usa o OpenStreetMap COMPLETO (mesmo + // detalhe do claro: ruas, nomes, POIs) e é escurecido por filtro CSS de + // inversão (.tiles-escuro-contraste). + const escuro = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap', maxZoom: 19, className: 'tiles-escuro-contraste' }); const claro = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { @@ -422,7 +431,7 @@ if (marcadores) return marcadores; marcadores = L.layerGroup(); pontos.forEach(function (p) { - const marker = L.marker([p.lat, p.lng], { icon: ICONE }) + const marker = L.marker([p.lat, p.lng], { icon: p.sucesso ? ICONE_OK : ICONE_FALHA }) .bindPopup(function () { return popupHtml(p); }, { maxWidth: 260 }); marker.addTo(marcadores); entradas.push({ marker: marker, p: p });