Inclusão de uma tabela de acompanhamento
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
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 %>
|
<% 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 %>
|
||||||
|
|||||||
Reference in New Issue
Block a user