# app/controllers/operacoes_dashboard_controller.rb # # Dashboard de análise de ENTREGAS por operação (separado do dashboard # financeiro). O operador escolhe uma operação (UBS Norte, EMAD, ...) e vê os # KPIs/insucessos/STS/mapa; também há os modos "global" (todas no período) e # "comparar" (uma operação vs outra). Os números vêm de Analytics::OperacaoMetricas. class OperacoesDashboardController < ApplicationController MODOS = %w[operacao global comparar].freeze # Linhas por página da tabela espelho (planilha da operação) ESPELHO_POR_PAGINA = 15 XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'.freeze def index authorize :dashboard, :operacoes? @periodo_inicio, @periodo_fim = periodo_selecionado @modo = MODOS.include?(params[:modo]) ? params[:modo] : 'operacao' @operacoes_agrupadas = Operacao.agrupadas_por_mes @tabelas_validas = Operacao.nomes_validos @filtros, @chips = cross_filtros @data_filtro = parse_data(params[:f_data]) # clique no gráfico "Entregas por Dia" case @modo when 'global' # Global: agrega TODAS as operações dentro da faixa de datas escolhida. @metricas = montar(@tabelas_validas, inicio: @periodo_inicio, fim: @periodo_fim, filtros: @filtros, data: @data_filtro) when 'comparar' # Comparar: cada operação inteira (sem filtro de data — só vale no Global). @op_a = Operacao.sanitizar([params[:op_a]]).first || @tabelas_validas[0] @op_b = Operacao.sanitizar([params[:op_b]]).first || @tabelas_validas[1] @metricas_a = montar([@op_a]) if @op_a @metricas_b = montar([@op_b]) if @op_b else # Operação única: a operação inteira (a data vem dos próprios dados). @operacao = Operacao.sanitizar([params[:operacao]]).first || @tabelas_validas.first @metricas = montar([@operacao], filtros: @filtros, data: @data_filtro) if @operacao end end # Planilha da operação (tabela espelho): página própria com busca e paginação. # Aceita os mesmos parâmetros do index (operacao/modo global/período/cross- # filters), então o botão no dashboard preserva o contexto atual. def planilha authorize :dashboard, :operacoes? @periodo_inicio, @periodo_fim = periodo_selecionado @modo = params[:modo] == 'global' ? 'global' : 'operacao' @operacoes_agrupadas = Operacao.agrupadas_por_mes @tabelas_validas = Operacao.nomes_validos @filtros, @chips = cross_filtros @data_filtro = parse_data(params[:f_data]) if @modo == 'global' @metricas = montar(@tabelas_validas, inicio: @periodo_inicio, fim: @periodo_fim, filtros: @filtros, data: @data_filtro) else @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 end # GET /dashboard/operacoes/planilha/baixar?operacao=gade_entregas_... # Baixa a planilha Entregas (.xlsx) da operação já preenchida com o resultado # do rastreio — o modelo que é entregue ao cliente. def baixar_planilha authorize :dashboard, :operacoes? tabela = params[:operacao].to_s unless Operacao.valida?(tabela) return redirect_to(operacoes_planilha_path, alert: 'Selecione uma operação válida.') end planilha = Analytics::PlanilhaEntregas.new(tabela) linhas = planilha.linhas if linhas.empty? return redirect_to(operacoes_planilha_path(operacao: tabela), alert: "A operação #{planilha.label} não tem registros.") end send_data Analytics::PlanilhaEntregasXlsx.new(linhas).gerar, filename: "Entregas #{planilha.label} #{Date.current.strftime('%m.%Y')}.xlsx", type: XLSX_MIME, disposition: 'attachment' 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 # Cross-filter por clique nas tabelas. Cada param vira um filtro de coluna e um # "chip" removível na tela. Retorna [filtros (coluna=>valor), chips (p/ a view)]. CROSS = { 'f_driver' => { coluna: 'driver', rotulo: 'Motorista' }, 'f_sts' => { coluna: 'contact_name', rotulo: 'Unidade' }, 'f_status' => { coluna: 'status_gade', rotulo: 'Status' }, 'f_obs' => { coluna: 'observation', rotulo: 'Observação' }, 'f_resultado' => { coluna: 'status', rotulo: 'Resultado' } }.freeze # Rótulos amigáveis para o filtro de Resultado (clique no donut "Insucessos %"). RESULTADO_LABEL = { 'completed' => 'Completas', 'failed' => 'Falhas' }.freeze def cross_filtros filtros = {} chips = [] CROSS.each do |param, cfg| valor = params[param].to_s.strip next if valor.empty? filtros[cfg[:coluna]] = valor display = param == 'f_resultado' ? (RESULTADO_LABEL[valor] || valor) : valor chips << { param: param, valor: valor, display: display, rotulo: cfg[:rotulo] } end # Filtro por DIA (clique no gráfico "Entregas por Dia") — tratado à parte (data). if (dia = parse_data(params[:f_data])) chips << { param: 'f_data', valor: params[:f_data].to_s, display: dia.strftime('%d/%m/%Y'), rotulo: 'Dia' } end [filtros, chips] end # Faixa de datas do filtro (params inicio/fim). Default: início do mês → hoje. # Espelha DashboardController#periodo_selecionado. def periodo_selecionado inicio = parse_data(params[:inicio]) || Date.current.beginning_of_month fim = parse_data(params[:fim]) || Date.current inicio, fim = fim, inicio if fim < inicio [inicio, fim] end def parse_data(str) return nil if str.blank? Date.parse(str) rescue ArgumentError, TypeError nil end end