teste #3

Merged
victor merged 20 commits from teste into main 2026-07-02 15:31:51 -03:00
3 changed files with 53 additions and 28 deletions
Showing only changes of commit a5742d178a - Show all commits

View File

@@ -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)

View File

@@ -16,22 +16,35 @@
<% end %>
<%# ── KPIs ─────────────────────────────────────────────── %>
<% pct_total = ->(parte) { metricas.total.zero? ? 0 : (parte * 100.0 / metricas.total).round(1) } %>
<div class="grid grid-cols-2 xl:grid-cols-4 gap-4">
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<div class="group bg-[#1a1a1a] rounded-2xl border border-white/5 p-6 transition-all duration-200 hover:border-white/20 hover:bg-[#202020] hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/40">
<p class="text-gray-400 text-sm font-medium mb-2">📦 Total de Entregas</p>
<p class="text-4xl font-black text-white"><%= metricas.total %></p>
<p class="text-4xl font-black text-white inline-block origin-left transition-transform duration-200 group-hover:scale-110"><%= metricas.total %></p>
<p class="text-xs text-gray-500 mt-2 h-0 overflow-hidden opacity-0 transition-all duration-200 group-hover:h-4 group-hover:opacity-100">
✅ <%= metricas.sucesso %> · <span class="text-blue-500">●</span> <%= metricas.recusas %> · ⏳ <%= metricas.pendentes %>
</p>
</div>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<div class="group bg-[#1a1a1a] rounded-2xl border border-white/5 p-6 transition-all duration-200 hover:border-green-500/40 hover:bg-[#202020] hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/40">
<p class="text-gray-400 text-sm font-medium mb-2">✅ Total de Sucesso</p>
<p class="text-4xl font-black text-green-400"><%= metricas.sucesso %></p>
<p class="text-4xl font-black text-green-400 inline-block origin-left transition-transform duration-200 group-hover:scale-110"><%= metricas.sucesso %></p>
<p class="text-xs text-gray-500 mt-2 h-0 overflow-hidden opacity-0 transition-all duration-200 group-hover:h-4 group-hover:opacity-100">
<%= pct_total.(metricas.sucesso) %>% do total
</p>
</div>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<div class="group bg-[#1a1a1a] rounded-2xl border border-white/5 p-6 transition-all duration-200 hover:border-blue-500/40 hover:bg-[#202020] hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/40">
<p class="text-gray-400 text-sm font-medium mb-2">❌ Total de Recusas</p>
<p class="text-4xl font-black text-red-400"><%= metricas.recusas %></p>
<p class="text-4xl font-black text-blue-500 inline-block origin-left transition-transform duration-200 group-hover:scale-110"><%= metricas.recusas %></p>
<p class="text-xs text-gray-500 mt-2 h-0 overflow-hidden opacity-0 transition-all duration-200 group-hover:h-4 group-hover:opacity-100">
<%= pct_total.(metricas.recusas) %>% do total
</p>
</div>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<div class="group bg-[#1a1a1a] rounded-2xl border border-white/5 p-6 transition-all duration-200 hover:border-yellow-500/40 hover:bg-[#202020] hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/40">
<p class="text-gray-400 text-sm font-medium mb-2">⏳ Entregas Pendentes</p>
<p class="text-4xl font-black text-yellow-400"><%= metricas.pendentes %></p>
<p class="text-4xl font-black text-yellow-400 inline-block origin-left transition-transform duration-200 group-hover:scale-110"><%= metricas.pendentes %></p>
<p class="text-xs text-gray-500 mt-2 h-0 overflow-hidden opacity-0 transition-all duration-200 group-hover:h-4 group-hover:opacity-100">
<%= pct_total.(metricas.pendentes) %>% do total
</p>
</div>
</div>
@@ -49,7 +62,7 @@
</div>
<div class="flex justify-center gap-4 mt-3 text-xs">
<span class="text-orange-400">● Completas <%= ins[:pct_completed] %>%</span>
<span class="text-red-400">● Falhas <%= ins[:pct_failed] %>%</span>
<span class="text-blue-500">● Falhas <%= ins[:pct_failed] %>%</span>
</div>
<% end %>
</div>

View File

@@ -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); }
</style>
<% 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"></a>' +
'<div style="display:none;margin-top:6px;color:#9ca3af;font-size:11px">📷 Foto indisponível (link expirado/sem acesso)</div>'
: '<div style="margin-top:6px;color:#6b7280;font-size:11px">📷 Sem foto da fachada para esta entrega</div>';
const statusLinha = p.sucesso
? '<div style="color:#16a34a;font-size:11px;margin-bottom:6px">✔ Entrega realizada</div>'
: '<div style="color:#3b82f6;font-size:11px;margin-bottom:6px">✖ Insucesso' +
(p.motivo ? ' — ' + escapeHtml(p.motivo) : '') + '</div>';
return '' +
'<div style="min-width:210px;max-width:240px">' +
'<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>' +
statusLinha +
linha('📍', p.endereco) +
linha('🏥', p.unidade) +
linha('🚚', p.motorista) +
@@ -364,13 +369,14 @@
'</div>';
}
// Í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: '<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"/>' +
'fill="' + cor + '" stroke="#0a0a0a" stroke-width="1.5"/>' +
'<circle cx="14" cy="13" r="5" fill="#0a0a0a"/></svg>',
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 });