Inclusão de uma tabela de acompanhamento

This commit is contained in:
2026-07-13 18:12:04 -03:00
parent bd61b32edd
commit 75d82227c9
4 changed files with 171 additions and 0 deletions

View File

@@ -7,6 +7,9 @@
class OperacoesDashboardController < ApplicationController class OperacoesDashboardController < ApplicationController
MODOS = %w[operacao global comparar].freeze MODOS = %w[operacao global comparar].freeze
# Linhas por página da tabela espelho (planilha da operação)
ESPELHO_POR_PAGINA = 15
def index def index
authorize :dashboard, :operacoes? authorize :dashboard, :operacoes?
@@ -32,10 +35,25 @@ class OperacoesDashboardController < ApplicationController
@operacao = Operacao.sanitizar([params[:operacao]]).first || @tabelas_validas.first @operacao = Operacao.sanitizar([params[:operacao]]).first || @tabelas_validas.first
@metricas = montar([@operacao], filtros: @filtros, data: @data_filtro) if @operacao @metricas = montar([@operacao], filtros: @filtros, data: @data_filtro) if @operacao
end end
montar_tabela_espelho if @metricas && %w[operacao global].include?(@modo)
end end
private private
# Tabela espelho da planilha da operação: busca textual (q) + paginação (pg)
# sobre as linhas já cross-filtradas. Ordena pelas mais recentes (checkout;
# sem checkout vai para o fim).
def montar_tabela_espelho
@busca = params[:q].to_s.strip
linhas = @metricas.buscar(@busca).sort_by { |r| r['checkout'].to_s }.reverse
@espelho_total = linhas.size
@espelho_paginas = [(@espelho_total / ESPELHO_POR_PAGINA.to_f).ceil, 1].max
@espelho_pagina = params[:pg].to_i.clamp(1, @espelho_paginas)
@espelho_linhas = linhas[(@espelho_pagina - 1) * ESPELHO_POR_PAGINA, ESPELHO_POR_PAGINA] || []
end
def montar(tabelas, inicio: nil, fim: nil, filtros: {}, data: nil) def montar(tabelas, inicio: nil, fim: nil, filtros: {}, data: nil)
Analytics::OperacaoMetricas.new(tabelas: tabelas, inicio: inicio, fim: fim, filtros: filtros, data: data) Analytics::OperacaoMetricas.new(tabelas: tabelas, inicio: inicio, fim: fim, filtros: filtros, data: data)
end end

View File

@@ -45,6 +45,18 @@ module Analytics
end end
end end
# Campos pesquisáveis da tabela espelho (planilha da operação).
BUSCA_CAMPOS = %w[reference_id driver vehicle status observation contact_name
address nome_completo endereco_completo status_gade operacao].freeze
# Busca textual da tabela espelho: filtra as linhas (já pós cross-filter)
# por qualquer campo exibido na planilha.
def buscar(texto)
termo = texto.to_s.strip.downcase
return linhas if termo.empty?
linhas.select { |r| BUSCA_CAMPOS.any? { |c| r[c].to_s.downcase.include?(termo) } }
end
# Taxas (úteis no comparativo). # Taxas (úteis no comparativo).
def taxa_sucesso def taxa_sucesso
pct(sucesso, total) pct(sucesso, total)

View File

@@ -0,0 +1,139 @@
<%# app/views/operacoes_dashboard/_espelho.html.erb %>
<%# Tabela espelho da planilha da operação: uma linha por NF com resultado,
ocorrência, motorista etc. Busca textual (param q) e paginação (param pg)
são server-side; o restante da URL (modo/operação/período/cross-filters)
é preservado nos links e no form. %>
<% pg_path = ->(n) { operacoes_dashboard_path(request.query_parameters.merge('pg' => n)) } %>
<% resultado_badge = ->(status) {
if status == 'completed'
['Entregue', 'bg-green-500/10 text-green-400 border-green-500/30']
elsif Entrega::STATUS_FALHA.include?(status)
['Falha', 'bg-red-500/10 text-red-400 border-red-500/30']
else
['Pendente', 'bg-yellow-500/10 text-yellow-400 border-yellow-500/30']
end
} %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<div>
<h2 class="text-lg font-semibold text-white">📋 Planilha da operação</h2>
<p class="text-gray-400 text-sm">Espelho por NF — pesquise por nota, motorista, unidade, ocorrência, veículo...</p>
</div>
<form method="get" action="<%= operacoes_dashboard_path %>" data-turbo="false" class="flex flex-wrap items-center gap-2">
<% request.query_parameters.except('q', 'pg').each do |k, v| %>
<input type="hidden" name="<%= k %>" value="<%= v %>">
<% end %>
<div class="relative">
<span class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">🔎</span>
<input type="search" name="q" value="<%= @busca %>" placeholder="Pesquisar..."
class="pl-9 pr-3 py-2 bg-[#0a0a0a] border border-white/10 rounded-xl text-white text-sm
w-52 sm:w-72 focus:outline-none focus:border-orange-500 placeholder-gray-500">
</div>
<button type="submit"
class="px-3 py-2 rounded-xl text-sm bg-[#0a0a0a] border border-white/10 text-gray-300 hover:border-orange-500">
Buscar
</button>
<% if @busca.present? %>
<%= link_to '✕ Limpar', operacoes_dashboard_path(request.query_parameters.except('q', 'pg')),
data: { turbo: false }, class: 'text-xs text-orange-400 hover:text-orange-300 whitespace-nowrap' %>
<% end %>
</form>
</div>
<% if @espelho_linhas.blank? %>
<p class="text-gray-400 text-sm py-8 text-center">
<%= @busca.present? ? %(Nenhuma entrega encontrada para "#{@busca}".) : 'Nenhuma entrega para exibir.' %>
</p>
<% else %>
<div class="overflow-x-auto">
<table class="w-full text-sm whitespace-nowrap">
<thead>
<tr class="text-gray-400 text-left border-b border-white/5">
<th class="py-2 pr-3 font-medium">Data</th>
<th class="py-2 px-3 font-medium">NF</th>
<% if @modo == 'global' %>
<th class="py-2 px-3 font-medium">Operação</th>
<% end %>
<th class="py-2 px-3 font-medium">Destinatário</th>
<th class="py-2 px-3 font-medium">Unidade</th>
<th class="py-2 px-3 font-medium">Motorista</th>
<th class="py-2 px-3 font-medium">Veículo</th>
<th class="py-2 px-3 font-medium">Resultado</th>
<th class="py-2 px-3 font-medium">Ocorrência</th>
<th class="py-2 pl-3 font-medium">Status</th>
</tr>
</thead>
<tbody>
<% @espelho_linhas.each do |r| %>
<% rotulo, badge_css = resultado_badge.(r['status']) %>
<% data = r['checkout'] || r['planned_date'] %>
<% endereco = r['endereco_completo'].presence || r['address'].presence %>
<tr class="border-b border-white/5 hover:bg-white/[0.02]">
<td class="py-2.5 pr-3 text-gray-300">
<%= data.respond_to?(:strftime) ? data.strftime('%d/%m/%y %H:%M') : data.to_s.first(16) %>
</td>
<td class="py-2.5 px-3 text-white font-medium"><%= r['reference_id'] %></td>
<% if @modo == 'global' %>
<td class="py-2.5 px-3 text-gray-400"><%= r['operacao'] %></td>
<% end %>
<td class="py-2.5 px-3 text-gray-300" title="<%= endereco %>">
<%= (r['nome_completo'].presence || '—').truncate(28) %>
</td>
<td class="py-2.5 px-3 text-gray-400"><%= r['contact_name'].to_s.truncate(24).presence || '—' %></td>
<td class="py-2.5 px-3 text-gray-300"><%= r['driver'].presence || '—' %></td>
<td class="py-2.5 px-3 text-gray-400"><%= r['vehicle'].presence || '—' %></td>
<td class="py-2.5 px-3">
<span class="inline-block px-2 py-0.5 rounded-full text-xs font-semibold border <%= badge_css %>">
<%= rotulo %>
</span>
</td>
<td class="py-2.5 px-3 text-gray-400" title="<%= r['observation'] %>">
<%= r['observation'].to_s.truncate(32).presence || '—' %>
</td>
<td class="py-2.5 pl-3 text-gray-400"><%= r['status_gade'].presence || '—' %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<%# Paginação: « 1 … janela em torno da atual … última » %>
<div class="flex flex-wrap items-center justify-between gap-2 mt-3">
<p class="text-gray-400 text-xs">
<%= (@espelho_pagina - 1) * OperacoesDashboardController::ESPELHO_POR_PAGINA + 1 %><%=
[(@espelho_pagina) * OperacoesDashboardController::ESPELHO_POR_PAGINA, @espelho_total].min %>
de <%= @espelho_total %> entregas
</p>
<% if @espelho_paginas > 1 %>
<div class="flex items-center gap-1">
<% if @espelho_pagina > 1 %>
<%= link_to '', pg_path.(@espelho_pagina - 1), data: { turbo: false },
class: 'px-3 py-1.5 rounded-lg text-xs font-semibold bg-[#0a0a0a] text-gray-400 border border-white/5 hover:text-white' %>
<% end %>
<% janela = ([1, @espelho_pagina - 2].max..[@espelho_paginas, @espelho_pagina + 2].min).to_a %>
<% paginas = ([1] + janela + [@espelho_paginas]).uniq %>
<% anterior = 0 %>
<% paginas.each do |p| %>
<% if p > anterior + 1 %>
<span class="px-1 text-gray-500 text-xs">…</span>
<% end %>
<% anterior = p %>
<% if p == @espelho_pagina %>
<span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-[#f97316] text-white"><%= p %></span>
<% else %>
<%= link_to p, pg_path.(p), data: { turbo: false },
class: 'px-3 py-1.5 rounded-lg text-xs font-semibold bg-[#0a0a0a] text-gray-400 border border-white/5 hover:text-white' %>
<% end %>
<% end %>
<% if @espelho_pagina < @espelho_paginas %>
<%= link_to '', pg_path.(@espelho_pagina + 1), data: { turbo: false },
class: 'px-3 py-1.5 rounded-lg text-xs font-semibold bg-[#0a0a0a] text-gray-400 border border-white/5 hover:text-white' %>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>

View File

@@ -144,6 +144,7 @@
<% if @metricas %> <% if @metricas %>
<%= render 'painel', metricas: @metricas, filtravel: true %> <%= render 'painel', metricas: @metricas, filtravel: true %>
<%= render 'mapa', metricas: @metricas %> <%= render 'mapa', metricas: @metricas %>
<%= render 'espelho' %>
<% end %> <% end %>
<% elsif @modo == 'global' %> <% elsif @modo == 'global' %>
@@ -154,6 +155,7 @@
<% if @metricas %> <% if @metricas %>
<%= render 'painel', metricas: @metricas, filtravel: true %> <%= render 'painel', metricas: @metricas, filtravel: true %>
<%= render 'mapa', metricas: @metricas %> <%= render 'mapa', metricas: @metricas %>
<%= render 'espelho' %>
<% end %> <% end %>
<% else # comparar %> <% else # comparar %>