Inclusão de uma tabela de acompanhamento
This commit is contained in:
@@ -7,6 +7,9 @@
|
||||
class OperacoesDashboardController < ApplicationController
|
||||
MODOS = %w[operacao global comparar].freeze
|
||||
|
||||
# Linhas por página da tabela espelho (planilha da operação)
|
||||
ESPELHO_POR_PAGINA = 15
|
||||
|
||||
def index
|
||||
authorize :dashboard, :operacoes?
|
||||
|
||||
@@ -32,10 +35,25 @@ class OperacoesDashboardController < ApplicationController
|
||||
@operacao = Operacao.sanitizar([params[:operacao]]).first || @tabelas_validas.first
|
||||
@metricas = montar([@operacao], filtros: @filtros, data: @data_filtro) if @operacao
|
||||
end
|
||||
|
||||
montar_tabela_espelho if @metricas && %w[operacao global].include?(@modo)
|
||||
end
|
||||
|
||||
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)
|
||||
Analytics::OperacaoMetricas.new(tabelas: tabelas, inicio: inicio, fim: fim, filtros: filtros, data: data)
|
||||
end
|
||||
|
||||
@@ -45,6 +45,18 @@ module Analytics
|
||||
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).
|
||||
def taxa_sucesso
|
||||
pct(sucesso, total)
|
||||
|
||||
139
app/views/operacoes_dashboard/_espelho.html.erb
Normal file
139
app/views/operacoes_dashboard/_espelho.html.erb
Normal 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>
|
||||
@@ -144,6 +144,7 @@
|
||||
<% if @metricas %>
|
||||
<%= render 'painel', metricas: @metricas, filtravel: true %>
|
||||
<%= render 'mapa', metricas: @metricas %>
|
||||
<%= render 'espelho' %>
|
||||
<% end %>
|
||||
|
||||
<% elsif @modo == 'global' %>
|
||||
@@ -154,6 +155,7 @@
|
||||
<% if @metricas %>
|
||||
<%= render 'painel', metricas: @metricas, filtravel: true %>
|
||||
<%= render 'mapa', metricas: @metricas %>
|
||||
<%= render 'espelho' %>
|
||||
<% end %>
|
||||
|
||||
<% else # comparar %>
|
||||
|
||||
Reference in New Issue
Block a user