Adição da opção de baixar a planilha preenchida para o cliente
This commit is contained in:
BIN
Imagens para implantação /Entregas SUDESTE 07.2026_FINAL.xlsx
Normal file
BIN
Imagens para implantação /Entregas SUDESTE 07.2026_FINAL.xlsx
Normal file
Binary file not shown.
@@ -10,6 +10,8 @@ class OperacoesDashboardController < ApplicationController
|
||||
# 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?
|
||||
|
||||
@@ -35,8 +37,53 @@ class OperacoesDashboardController < ApplicationController
|
||||
@operacao = Operacao.sanitizar([params[:operacao]]).first || @tabelas_validas.first
|
||||
@metricas = montar([@operacao], filtros: @filtros, data: @data_filtro) if @operacao
|
||||
end
|
||||
end
|
||||
|
||||
montar_tabela_espelho if @metricas && %w[operacao global].include?(@modo)
|
||||
# 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
|
||||
|
||||
93
app/services/analytics/planilha_entregas.rb
Normal file
93
app/services/analytics/planilha_entregas.rb
Normal file
@@ -0,0 +1,93 @@
|
||||
# app/services/analytics/planilha_entregas.rb
|
||||
#
|
||||
# Monta as linhas da "planilha Entregas" entregue ao cliente (modelo
|
||||
# "Entregas SUDESTE MM.AAAA_FINAL.xlsx", aba ENTREGAS): as colunas A..V vêm da
|
||||
# própria tabela da operação (gade_entregas_* — espelho da planilha original) e
|
||||
# as colunas de resultado W..Z (STATUS/ENTREGA/DATA OCORRÊNCIA/OCORRÊNCIA) são
|
||||
# preenchidas com o ÚLTIMO status de cada NF no rastreio — o preenchimento que
|
||||
# hoje é feito manualmente.
|
||||
#
|
||||
# SEGURANÇA: tabela passa pela whitelist (Operacao.sanitizar) + quote_table_name;
|
||||
# colunas são whitelist fixa e as ausentes viram NULL (mesmo padrão do
|
||||
# OperacaoMetricas). Bases SOMENTE LEITURA.
|
||||
module Analytics
|
||||
class PlanilhaEntregas
|
||||
# Colunas da tabela gade (snake_case) -> cabeçalho exato do modelo (A..V).
|
||||
# Coluna que não existir em alguma operação sai vazia, preservando o layout.
|
||||
COLUNAS_GADE = {
|
||||
'ordem' => 'Ordem',
|
||||
'operacao' => 'Operação',
|
||||
'coordenadoria' => 'Coordenadoria',
|
||||
'supervisao' => 'Supervisão',
|
||||
'unidade' => 'Unidade',
|
||||
'data_cadastro' => 'Data Cadastro',
|
||||
'cartao_sus' => 'Cartão SUS',
|
||||
'nome_completo' => 'Nome Completo',
|
||||
'endereco_completo' => 'Endereço Completo',
|
||||
'nome_responsavel' => 'Nome Responsável',
|
||||
'telefones' => 'Telefones',
|
||||
'area_de_risco' => 'Área de Risco',
|
||||
'numero_ativo' => 'Numero Ativo',
|
||||
'num_serie_base' => 'Num Serie Base',
|
||||
'num_serie_pulverizador' => 'Num Serie Pulverizador',
|
||||
'observacoes' => 'Observações',
|
||||
'endereco_sem_complemento' => 'Endereço Sem Complemento',
|
||||
'complemento' => 'Complemento',
|
||||
'latitude' => 'Latitude',
|
||||
'longitude' => 'Longitude',
|
||||
'status' => 'STATUS',
|
||||
'nota_fiscal' => 'NOTA FISCAL'
|
||||
}.freeze
|
||||
|
||||
# Cabeçalho completo A..Z (gade + resultado do rastreio).
|
||||
CABECALHO = (COLUNAS_GADE.values + ['STATUS', 'ENTREGA', 'DATA OCORRÊNCIA', 'OCORRÊNCIA']).freeze
|
||||
|
||||
attr_reader :tabela
|
||||
|
||||
def initialize(tabela)
|
||||
@tabela = Operacao.sanitizar([tabela]).first
|
||||
raise ArgumentError, "Operação inválida: #{tabela}" unless @tabela
|
||||
end
|
||||
|
||||
def label
|
||||
Operacao.label(@tabela)
|
||||
end
|
||||
|
||||
# Array de hashes: chaves de COLUNAS_GADE + rastreio_status/checkout/observation.
|
||||
# ORDER BY g.ctid preserva a ordem física de importação — a mesma ordem da
|
||||
# planilha original do cliente (tabelas gade são read-only após o import).
|
||||
def linhas
|
||||
gade = conn.quote_table_name(@tabela)
|
||||
sql = <<~SQL
|
||||
WITH ultimo AS (
|
||||
SELECT *,
|
||||
ROW_NUMBER() OVER (PARTITION BY reference_id ORDER BY checkout DESC NULLS LAST) AS rn
|
||||
FROM #{conn.quote_table_name(Entrega.table_name)}
|
||||
WHERE reference_id IS NOT NULL
|
||||
)
|
||||
SELECT #{selects_gade},
|
||||
r.status AS rastreio_status,
|
||||
r.checkout AS rastreio_checkout,
|
||||
r.observation AS rastreio_observation
|
||||
FROM #{gade} g
|
||||
LEFT JOIN ultimo r ON r.rn = 1 AND r.reference_id::text = g.nota_fiscal
|
||||
ORDER BY g.ctid
|
||||
SQL
|
||||
conn.select_all(sql).to_a
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def conn
|
||||
ActiveRecord::Base.connection
|
||||
end
|
||||
|
||||
# SELECT das colunas whitelisted; ausentes viram NULL com o mesmo alias.
|
||||
def selects_gade
|
||||
existentes = conn.columns(@tabela).map(&:name)
|
||||
COLUNAS_GADE.keys.map do |c|
|
||||
existentes.include?(c) ? "g.#{c} AS #{c}" : "CAST(NULL AS text) AS #{c}"
|
||||
end.join(', ')
|
||||
end
|
||||
end
|
||||
end
|
||||
60
app/services/analytics/planilha_entregas_xlsx.rb
Normal file
60
app/services/analytics/planilha_entregas_xlsx.rb
Normal file
@@ -0,0 +1,60 @@
|
||||
# app/services/analytics/planilha_entregas_xlsx.rb
|
||||
#
|
||||
# Gera o binário .xlsx da planilha Entregas do cliente (aba ENTREGAS) a partir
|
||||
# das linhas montadas por Analytics::PlanilhaEntregas — layout A..Z idêntico ao
|
||||
# modelo "Entregas SUDESTE MM.AAAA_FINAL.xlsx".
|
||||
require 'caxlsx'
|
||||
|
||||
module Analytics
|
||||
class PlanilhaEntregasXlsx
|
||||
def initialize(linhas)
|
||||
@linhas = linhas
|
||||
end
|
||||
|
||||
# String binária do .xlsx.
|
||||
def gerar
|
||||
pkg = Axlsx::Package.new
|
||||
pkg.workbook.add_worksheet(name: 'ENTREGAS') do |sheet|
|
||||
negrito = sheet.workbook.styles.add_style(b: true)
|
||||
sheet.add_row(PlanilhaEntregas::CABECALHO, style: negrito)
|
||||
@linhas.each do |l|
|
||||
valores, tipos = linha(l)
|
||||
sheet.add_row(valores, types: tipos)
|
||||
end
|
||||
sheet.auto_filter = "A1:Z#{@linhas.size + 1}"
|
||||
end
|
||||
pkg.to_stream.read
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# A..V direto da tabela gade (texto); W..Z do rastreio:
|
||||
# W STATUS (completed/failed...), X ENTREGA (Sim/Não), Y DATA OCORRÊNCIA
|
||||
# (data real do checkout), Z OCORRÊNCIA (motivo do insucesso).
|
||||
def linha(l)
|
||||
status_r = l['rastreio_status'].to_s
|
||||
entregue = if status_r == 'completed'
|
||||
'Sim'
|
||||
elsif Entrega::STATUS_FALHA.include?(status_r)
|
||||
'Não'
|
||||
else
|
||||
''
|
||||
end
|
||||
data_ocorrencia = data_de(l['rastreio_checkout'])
|
||||
|
||||
valores = PlanilhaEntregas::COLUNAS_GADE.keys.map { |c| l[c].to_s } +
|
||||
[status_r, entregue, data_ocorrencia || '', l['rastreio_observation'].to_s]
|
||||
tipos = Array.new(PlanilhaEntregas::COLUNAS_GADE.size + 2, :string) +
|
||||
[(data_ocorrencia ? :date : :string), :string]
|
||||
[valores, tipos]
|
||||
end
|
||||
|
||||
def data_de(valor)
|
||||
return nil if valor.nil?
|
||||
return valor.to_date if valor.respond_to?(:to_date)
|
||||
Date.parse(valor.to_s)
|
||||
rescue ArgumentError, TypeError
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,7 @@
|
||||
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)) } %>
|
||||
<% pg_path = ->(n) { operacoes_planilha_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']
|
||||
@@ -22,7 +22,7 @@
|
||||
<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">
|
||||
<form method="get" action="<%= operacoes_planilha_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 %>
|
||||
@@ -37,7 +37,7 @@
|
||||
Buscar
|
||||
</button>
|
||||
<% if @busca.present? %>
|
||||
<%= link_to '✕ Limpar', operacoes_dashboard_path(request.query_parameters.except('q', 'pg')),
|
||||
<%= link_to '✕ Limpar', operacoes_planilha_path(request.query_parameters.except('q', 'pg')),
|
||||
data: { turbo: false }, class: 'text-xs text-orange-400 hover:text-orange-300 whitespace-nowrap' %>
|
||||
<% end %>
|
||||
</form>
|
||||
|
||||
@@ -88,16 +88,28 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# ── Abas de modo ──────────────────────────────────────── %>
|
||||
<%# ── Abas de modo + atalho para a planilha ─────────────── %>
|
||||
<% modos = { 'operacao' => '🏥 Operação', 'global' => '🌐 Global', 'comparar' => '⚖️ Comparar' } %>
|
||||
<div class="flex flex-wrap items-center gap-2 border-b border-white/5 pb-3">
|
||||
<% modos.each do |m, label| %>
|
||||
<% ativo = @modo == m %>
|
||||
<%= link_to label,
|
||||
operacoes_dashboard_path(ctx_ops.merge(modo: m, inicio: ini_iso, fim: fim_iso)),
|
||||
data: { turbo: false },
|
||||
class: "px-4 py-2 rounded-xl text-sm font-semibold whitespace-nowrap #{ativo ? 'bg-orange-500 text-black' : 'bg-[#1a1a1a] text-gray-300 border border-white/10 hover:border-orange-500'}" %>
|
||||
<% end %>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 border-b border-white/5 pb-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<% modos.each do |m, label| %>
|
||||
<% ativo = @modo == m %>
|
||||
<%= link_to label,
|
||||
operacoes_dashboard_path(ctx_ops.merge(modo: m, inicio: ini_iso, fim: fim_iso)),
|
||||
data: { turbo: false },
|
||||
class: "px-4 py-2 rounded-xl text-sm font-semibold whitespace-nowrap #{ativo ? 'bg-orange-500 text-black' : 'bg-[#1a1a1a] text-gray-300 border border-white/10 hover:border-orange-500'}" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# Planilha da operação (tabela espelho) — leva o contexto atual junto %>
|
||||
<% planilha_ctx = request.query_parameters
|
||||
.slice('f_driver', 'f_sts', 'f_status', 'f_obs', 'f_resultado', 'f_data')
|
||||
.merge(modo: (@modo == 'global' ? 'global' : 'operacao'), inicio: ini_iso, fim: fim_iso) %>
|
||||
<% planilha_ctx[:operacao] = @operacao if @operacao %>
|
||||
<%= link_to '📋 Planilha da operação', operacoes_planilha_path(planilha_ctx),
|
||||
data: { turbo: false },
|
||||
class: 'px-4 py-2 rounded-xl text-sm font-semibold whitespace-nowrap bg-[#1a1a1a]
|
||||
text-orange-400 border border-orange-500/40 hover:bg-orange-500 hover:text-black' %>
|
||||
</div>
|
||||
|
||||
<% if @tabelas_validas.blank? %>
|
||||
@@ -144,7 +156,6 @@
|
||||
<% if @metricas %>
|
||||
<%= render 'painel', metricas: @metricas, filtravel: true %>
|
||||
<%= render 'mapa', metricas: @metricas %>
|
||||
<%= render 'espelho' %>
|
||||
<% end %>
|
||||
|
||||
<% elsif @modo == 'global' %>
|
||||
@@ -155,7 +166,6 @@
|
||||
<% if @metricas %>
|
||||
<%= render 'painel', metricas: @metricas, filtravel: true %>
|
||||
<%= render 'mapa', metricas: @metricas %>
|
||||
<%= render 'espelho' %>
|
||||
<% end %>
|
||||
|
||||
<% else # comparar %>
|
||||
|
||||
108
app/views/operacoes_dashboard/planilha.html.erb
Normal file
108
app/views/operacoes_dashboard/planilha.html.erb
Normal file
@@ -0,0 +1,108 @@
|
||||
<%# app/views/operacoes_dashboard/planilha.html.erb %>
|
||||
<%# Página da Planilha da Operação: tabela espelho (uma linha por NF) com busca
|
||||
e paginação. Chega-se aqui pelo botão no Dashboard de Operações, que passa
|
||||
o contexto (operação/modo global/período/cross-filters) na URL. %>
|
||||
|
||||
<% content_for :head do %>
|
||||
<meta name="turbo-cache-control" content="no-cache">
|
||||
<% end %>
|
||||
|
||||
<% ini_iso = @periodo_inicio.strftime('%Y-%m-%d') %>
|
||||
<% fim_iso = @periodo_fim.strftime('%Y-%m-%d') %>
|
||||
|
||||
<div class="space-y-6">
|
||||
|
||||
<%# ── Cabeçalho ─────────────────────────────────────────── %>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">📋 Planilha da Operação</h1>
|
||||
<p class="text-gray-400 text-sm mt-0.5">
|
||||
<% if @modo == 'global' %>
|
||||
🌐 Global · <%= @periodo_inicio.strftime('%d/%m/%Y') %> – <%= @periodo_fim.strftime('%d/%m/%Y') %>
|
||||
<% elsif @metricas&.data_inicio %>
|
||||
<%= @metricas.operacoes_label %> · <%= @metricas.data_inicio.strftime('%d/%m/%Y') %> – <%= @metricas.data_fim.strftime('%d/%m/%Y') %>
|
||||
<% elsif @operacao %>
|
||||
<%= Operacao.label(@operacao) %>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= link_to '← Dashboard de Operações',
|
||||
operacoes_dashboard_path({ modo: @modo, operacao: @operacao, inicio: ini_iso, fim: fim_iso }.compact),
|
||||
data: { turbo: false },
|
||||
class: 'px-4 py-2.5 rounded-xl text-sm whitespace-nowrap bg-[#1a1a1a] text-gray-300 border border-white/10 hover:border-orange-500' %>
|
||||
</div>
|
||||
|
||||
<%# ── Seleção de escopo: operação única ou global ───────── %>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<% pills = { 'operacao' => '🏥 Operação', 'global' => '🌐 Global' } %>
|
||||
<% pills.each do |m, label| %>
|
||||
<% ativo = @modo == m %>
|
||||
<%= link_to label,
|
||||
operacoes_planilha_path({ modo: m, operacao: @operacao, inicio: ini_iso, fim: fim_iso, q: @busca.presence }.compact),
|
||||
data: { turbo: false },
|
||||
class: "px-4 py-2 rounded-xl text-sm font-semibold whitespace-nowrap #{ativo ? 'bg-orange-500 text-black' : 'bg-[#1a1a1a] text-gray-300 border border-white/10 hover:border-orange-500'}" %>
|
||||
<% end %>
|
||||
|
||||
<% if @modo == 'operacao' %>
|
||||
<form method="get" action="<%= operacoes_planilha_path %>" data-turbo="false" class="flex items-center gap-2">
|
||||
<input type="hidden" name="modo" value="operacao">
|
||||
<input type="hidden" name="inicio" value="<%= ini_iso %>">
|
||||
<input type="hidden" name="fim" value="<%= fim_iso %>">
|
||||
<% if @busca.present? %><input type="hidden" name="q" value="<%= @busca %>"><% end %>
|
||||
<select name="operacao" id="planilha-operacao"
|
||||
class="bg-[#1a1a1a] border border-white/10 rounded-xl text-white text-sm px-3 py-2.5 focus:outline-none focus:border-orange-500 min-w-[240px]">
|
||||
<% @operacoes_agrupadas.each do |titulo, ops| %>
|
||||
<optgroup label="<%= titulo %>">
|
||||
<% ops.each do |op| %>
|
||||
<option value="<%= op[:tabela] %>" <%= 'selected' if op[:tabela] == @operacao %>><%= op[:label] %></option>
|
||||
<% end %>
|
||||
</optgroup>
|
||||
<% end %>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<%# Planilha Entregas preenchida (modelo entregue ao cliente) %>
|
||||
<% if @operacao %>
|
||||
<%= link_to '⬇️ Baixar Excel preenchido', operacoes_planilha_baixar_path(operacao: @operacao),
|
||||
data: { turbo: false },
|
||||
title: 'Planilha Entregas da operação com status/ocorrências preenchidos pelo rastreio',
|
||||
class: 'px-4 py-2.5 bg-orange-500 hover:bg-orange-600 text-black font-bold rounded-xl text-sm whitespace-nowrap' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# ── Filtros ativos herdados do dashboard (chips removíveis) ── %>
|
||||
<% if @chips.any? %>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs text-gray-500 uppercase tracking-wider">Filtros ativos:</span>
|
||||
<% @chips.each do |chip| %>
|
||||
<span class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-orange-500/15 border border-orange-500/30 text-orange-300 text-xs">
|
||||
<%= chip[:rotulo] %>: <strong class="text-white"><%= chip[:display] || chip[:valor] %></strong>
|
||||
<%= link_to '✕', operacoes_planilha_path(request.query_parameters.except(chip[:param], 'pg')),
|
||||
data: { turbo: false }, class: 'hover:text-white font-bold', title: 'Remover filtro' %>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= link_to 'Limpar tudo',
|
||||
operacoes_planilha_path(request.query_parameters.except('pg', *@chips.map { |c| c[:param] })),
|
||||
data: { turbo: false }, class: 'text-xs text-gray-400 hover:text-orange-400 underline' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# ── Tabela espelho ─────────────────────────────────────── %>
|
||||
<% if @metricas %>
|
||||
<%= render 'espelho' %>
|
||||
<% else %>
|
||||
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-10 text-center text-gray-400">
|
||||
Nenhuma operação encontrada no banco (tabelas <code class="text-orange-400">gade_entregas_*</code>).
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# Troca de operação recarrega a planilha %>
|
||||
<script>
|
||||
(function () {
|
||||
const sel = document.getElementById('planilha-operacao');
|
||||
if (sel) sel.addEventListener('change', function () { sel.form.requestSubmit(); });
|
||||
})();
|
||||
</script>
|
||||
@@ -18,6 +18,10 @@ Rails.application.routes.draw do
|
||||
|
||||
# Dashboard de análise de entregas por operação (UBS Norte, EMAD, ...)
|
||||
get '/dashboard/operacoes', to: 'operacoes_dashboard#index', as: :operacoes_dashboard
|
||||
# Planilha da operação (tabela espelho pesquisável, página própria)
|
||||
get '/dashboard/operacoes/planilha', to: 'operacoes_dashboard#planilha', as: :operacoes_planilha
|
||||
# Download .xlsx da planilha Entregas preenchida (modelo entregue ao cliente)
|
||||
get '/dashboard/operacoes/planilha/baixar', to: 'operacoes_dashboard#baixar_planilha', as: :operacoes_planilha_baixar
|
||||
|
||||
# Painel motorista
|
||||
get '/motorista', to: 'motorista/dashboard#index', as: :motorista_dashboard
|
||||
|
||||
Reference in New Issue
Block a user