, Correçoes na busca da API para ver os ultimos planos
This commit is contained in:
@@ -18,10 +18,22 @@ class Admin::RomaneiosController < ApplicationController
|
||||
@operacoes_agrupadas = Operacao.agrupadas_por_mes
|
||||
@data_padrao = Date.current
|
||||
|
||||
# Os 5 planos mais recentes, para o operador ESCOLHER em vez de adivinhar a
|
||||
# data. Best-effort de propósito: API fora do ar devolve [] e a tela cai no
|
||||
# campo de data de sempre — a importação nunca fica bloqueada por isso.
|
||||
@planos = SimpliRoute::Client.new.planos(limite: 5)
|
||||
# Os planos para o operador ESCOLHER em vez de adivinhar a data.
|
||||
#
|
||||
# Sem período: os 5 mais recentes — a ferramenta é usada logo depois de
|
||||
# lançar uma operação, então o que ele quer está sempre entre os últimos, e
|
||||
# o caso comum fica em zero clique.
|
||||
# Com período: TODOS os planos da faixa. É assim que se alcança um plano
|
||||
# antigo sem saber o dia dele — e é o mesmo recorte da tela do SimpliRoute,
|
||||
# para o operador não ter que traduzir nada entre as duas.
|
||||
@plano_inicio = params[:plano_inicio].presence
|
||||
@plano_fim = params[:plano_fim].presence
|
||||
@planos = SimpliRoute::Client.new.planos(limite: 5, inicio: @plano_inicio, fim: @plano_fim)
|
||||
@planos_filtrados = @plano_inicio.present? && @plano_fim.present?
|
||||
|
||||
# O logo escolhido sumiu do disco? A tela precisa dizer — foi exatamente
|
||||
# assim que o PDF saiu com o logo antigo sem ninguém entender (28/08/2026).
|
||||
_, @logo_problema = Pdf::RomaneioPdf.logo_configurado
|
||||
# Nome ORIGINAL do logo escolhido (a descricao), para a tela mostrar o mesmo
|
||||
# que o operador enviou; nil = ainda usando o logo da GADE do repositório.
|
||||
@logo_atual = Configuracao.find_by(chave: 'romaneio_logo')&.descricao
|
||||
@@ -87,7 +99,28 @@ class Admin::RomaneiosController < ApplicationController
|
||||
#
|
||||
# PNG/JPG apenas porque é o que o Prawn embute — um SVG aceito aqui sairia como
|
||||
# folha SEM logo, sem erro nenhum, e ninguém descobriria antes de imprimir.
|
||||
LOGO_TIPOS = { 'image/png' => '.png', 'image/jpeg' => '.jpg' }.freeze
|
||||
# ⚠️ O FORMATO É DECIDIDO PELO CONTEÚDO DO ARQUIVO, não pelo `content_type`
|
||||
# que o navegador manda. O content_type é entrada do usuário: Windows e alguns
|
||||
# navegadores mandam `application/octet-stream` para um PNG perfeitamente
|
||||
# válido, e a versão antiga (`LOGO_TIPOS[arquivo.content_type]`) RECUSAVA esse
|
||||
# arquivo dizendo "precisa ser PNG ou JPG" — para um PNG. Assinatura de arquivo
|
||||
# não mente.
|
||||
#
|
||||
# A lista é curta porque quem manda aqui é o Prawn: ele embute **PNG e JPEG**,
|
||||
# e só. Formato que ele não embute não pode ser aceito "para ver no que dá" —
|
||||
# viraria um PDF quebrado na hora de imprimir, ou um logo que some sem aviso.
|
||||
# Os outros formatos são RECONHECIDOS mesmo assim, para a mensagem poder dizer
|
||||
# o que a pessoa mandou e o que fazer, em vez de repetir "PNG ou JPG".
|
||||
ASSINATURAS = [
|
||||
["\x89PNG\r\n\x1a\n".b, '.png', 'PNG', true],
|
||||
["\xFF\xD8\xFF".b, '.jpg', 'JPEG', true],
|
||||
['RIFF'.b, nil, 'WebP', false], # confirmado abaixo pelo 'WEBP'
|
||||
['GIF8'.b, nil, 'GIF', false],
|
||||
['BM'.b, nil, 'BMP', false],
|
||||
['%PDF'.b, nil, 'PDF', false],
|
||||
['<?xml'.b, nil, 'SVG', false],
|
||||
['<svg'.b, nil, 'SVG', false]
|
||||
].freeze
|
||||
LOGO_TAMANHO_MAX = 2.megabytes
|
||||
|
||||
def atualizar_logo
|
||||
@@ -96,23 +129,36 @@ class Admin::RomaneiosController < ApplicationController
|
||||
arquivo = params[:logo]
|
||||
return redirect_to(admin_romaneios_path, alert: 'Escolha o arquivo do logo (PNG ou JPG).') if arquivo.blank?
|
||||
|
||||
extensao = LOGO_TIPOS[arquivo.content_type.to_s]
|
||||
if extensao.nil?
|
||||
return redirect_to(admin_romaneios_path,
|
||||
alert: 'O logo precisa ser PNG ou JPG — é o que o PDF consegue embutir.')
|
||||
end
|
||||
if arquivo.size > LOGO_TAMANHO_MAX
|
||||
return redirect_to(admin_romaneios_path,
|
||||
alert: 'Logo acima de 2 MB. Reduza a imagem antes de enviar.')
|
||||
end
|
||||
|
||||
# Lê tudo UMA vez: o mesmo conteúdo serve para identificar o formato e para
|
||||
# gravar. Reler o upload depois de `read` devolveria vazio.
|
||||
conteudo = arquivo.read.to_s.b
|
||||
extensao, formato, aceito = identificar_imagem(conteudo)
|
||||
|
||||
if extensao.nil?
|
||||
recado = if formato
|
||||
"O arquivo é #{formato}, e o PDF só embute PNG ou JPG. " \
|
||||
'Salve o logo como PNG (qualquer editor exporta) e envie de novo.'
|
||||
else
|
||||
'Não reconheci esse arquivo como imagem. O logo precisa ser PNG ou JPG.'
|
||||
end
|
||||
return redirect_to(admin_romaneios_path, alert: recado)
|
||||
end
|
||||
# `aceito` existe para o dia em que alguém adicionar conversão: hoje só
|
||||
# chega aqui o que o Prawn embute.
|
||||
return redirect_to(admin_romaneios_path, alert: 'Formato não suportado.') unless aceito
|
||||
|
||||
# Nome GERADO por nós: o nome que vem do navegador é entrada do usuário e não
|
||||
# entra em caminho de arquivo. O original fica só na descrição, para a tela
|
||||
# mostrar "Logo: LOGOGADE.png" como o operador reconhece.
|
||||
nome = "logo-#{Time.current.strftime('%Y%m%d%H%M%S')}#{extensao}"
|
||||
destino = Pdf::RomaneioPdf.pasta_logos
|
||||
FileUtils.mkdir_p(destino)
|
||||
File.binwrite(destino.join(nome), arquivo.read)
|
||||
File.binwrite(destino.join(nome), conteudo)
|
||||
|
||||
config = Configuracao.find_or_initialize_by(chave: 'romaneio_logo')
|
||||
config.update!(valor: nome, descricao: arquivo.original_filename.to_s.first(120),
|
||||
@@ -281,8 +327,7 @@ class Admin::RomaneiosController < ApplicationController
|
||||
data = params[:planned_date].presence
|
||||
|
||||
if params[:plano_id].present?
|
||||
@plano_escolhido = SimpliRoute::Client.new.planos(limite: 20)
|
||||
.find { |pl| pl['id'].to_s == params[:plano_id].to_s }
|
||||
@plano_escolhido = SimpliRoute::Client.new.plano(params[:plano_id])
|
||||
if @plano_escolhido.nil?
|
||||
redirect_to admin_romaneios_path,
|
||||
alert: 'Plano não encontrado no SimpliRoute — recarregue a página e escolha de novo.'
|
||||
@@ -342,6 +387,20 @@ class Admin::RomaneiosController < ApplicationController
|
||||
nil
|
||||
end
|
||||
|
||||
# Formato pela ASSINATURA do arquivo. Devolve [extensao, nome_do_formato,
|
||||
# aceito?] — com nome preenchido mesmo para o que não serve, para a mensagem
|
||||
# poder dizer "é WebP" em vez de "não é PNG ou JPG".
|
||||
def identificar_imagem(conteudo)
|
||||
achado = ASSINATURAS.find { |assinatura, _, _, _| conteudo.start_with?(assinatura) }
|
||||
return [nil, nil, false] if achado.nil?
|
||||
|
||||
_, extensao, formato, aceito = achado
|
||||
# RIFF é contêiner (WebP, WAV, AVI): o que diz que é WebP são os bytes 8..11.
|
||||
return [nil, nil, false] if formato == 'WebP' && conteudo[8, 4] != 'WEBP'
|
||||
|
||||
[extensao, formato, aceito]
|
||||
end
|
||||
|
||||
def importar(linhas, origem:)
|
||||
resultado = aplicar(linhas, origem: origem)
|
||||
auditar!(:criar, @romaneio, dados_novos: resultado.to_h)
|
||||
|
||||
@@ -126,15 +126,47 @@ module Pdf
|
||||
end
|
||||
|
||||
def self.logo_escolhido
|
||||
caminho, = logo_configurado
|
||||
caminho
|
||||
end
|
||||
|
||||
# O logo escolhido E o motivo de ele não estar valendo — os dois juntos,
|
||||
# porque quem chama precisa saber a diferença entre "ninguém escolheu logo"
|
||||
# e "o logo que o operador subiu SUMIU do disco".
|
||||
#
|
||||
# ⚠️ ISTO ERA UM `File.exist?(x) ? x : nil` MUDO, e foi um bug real
|
||||
# (relatado em 28/08/2026): o operador subiu outro logo, a tela disse "Logo
|
||||
# atualizado", e o PDF continuou saindo com o logo antigo — porque o arquivo
|
||||
# não estava mais na pasta e o método caía no padrão sem dizer nada a
|
||||
# ninguém. Falha silenciosa em cima de documento que vai IMPRESSO para o
|
||||
# cliente é o pior tipo: ninguém procura o que não reclama.
|
||||
#
|
||||
# A causa mais provável do arquivo sumir: `storage/` é gitignorado
|
||||
# (`/storage/*`) e o deploy é cópia de pasta pelo share SMB do NAS — a cópia
|
||||
# leva junto a pasta vazia da máquina de quem publicou. Se acontecer de
|
||||
# novo, o log e a tela agora dizem.
|
||||
#
|
||||
# Devolve [caminho, problema]:
|
||||
# [nil, nil] -> ninguém escolheu logo (usa o da GADE, normal)
|
||||
# [caminho, nil] -> logo escolhido e presente
|
||||
# [nil, :sumiu] -> escolhido MAS o arquivo não está lá
|
||||
# [nil, :sem_banco] -> sem Rails/tabela (o container pelado do layout)
|
||||
def self.logo_configurado
|
||||
nome = Configuracao.valor('romaneio_logo').to_s
|
||||
return nil if nome.strip.empty?
|
||||
return [nil, nil] if nome.strip.empty?
|
||||
|
||||
caminho = pasta_logos.join(nome).to_s
|
||||
File.exist?(caminho) ? caminho : nil
|
||||
return [caminho, nil] if File.exist?(caminho)
|
||||
|
||||
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
||||
Rails.logger.error("[romaneio] logo '#{nome}' configurado mas AUSENTE em #{pasta_logos} — " \
|
||||
'o PDF saiu com o logo padrão')
|
||||
end
|
||||
[nil, :sumiu]
|
||||
rescue StandardError
|
||||
# Sem banco (a conferência de layout roda num container Ruby pelado) ou
|
||||
# tabela ainda não migrada: o PDF continua saindo, com o logo padrão.
|
||||
nil
|
||||
[nil, :sem_banco]
|
||||
end
|
||||
|
||||
def render
|
||||
|
||||
@@ -64,6 +64,16 @@ module SimpliRoute
|
||||
Array(get(caminho))
|
||||
end
|
||||
|
||||
# UM plano pelo uuid. Existe separado de `planos` porque a busca do plano
|
||||
# ESCOLHIDO não pode depender de recorte nenhum: o operador pode ter filtrado
|
||||
# por período e escolhido um plano fora dos mais recentes, e procurá-lo numa
|
||||
# lista cortada devolveria "plano não encontrado" para um plano que existe.
|
||||
def plano(id)
|
||||
Array(get('/v1/routes/plans/')).find { |pl| pl['id'].to_s == id.to_s }
|
||||
rescue Error
|
||||
nil
|
||||
end
|
||||
|
||||
# UMA rota pelo uuid. Serve para descobrir o DIA REAL de um plano: o registro
|
||||
# do plano só traz a JANELA (`start_date`/`end_date`), e o dia das rotas nem
|
||||
# sempre é o começo dela — medido em 28/08/2026: EMAD SETEMBRO 2026 (31/08 a
|
||||
@@ -113,12 +123,22 @@ module SimpliRoute
|
||||
# (o EMAD SETEMBRO 2026 vai de 31/08 a 08/09), e é justamente essa janela que
|
||||
# tornava impossível adivinhar "a data do plano". Quem publicou por último é
|
||||
# quem o operador quer — ele abre a ferramenta logo depois de lançar.
|
||||
def planos(limite: 5)
|
||||
Array(get('/v1/routes/plans/'))
|
||||
.sort_by { |p| p['created'].to_s }
|
||||
.reverse
|
||||
.first(limite)
|
||||
rescue Error
|
||||
#
|
||||
# COM PERÍODO (`inicio`/`fim`) o corte muda de propósito: em vez dos N mais
|
||||
# recentes, devolve TODOS os planos da faixa — é assim que se alcança um
|
||||
# plano antigo sem saber o dia dele, e é o mesmo recorte que a tela do
|
||||
# SimpliRoute usa. O critério é a JANELA DO PLANO CRUZAR o período
|
||||
# (`start_date <= fim AND end_date >= inicio`), a mesma semântica de
|
||||
# `Consolidacao.cruzando_periodo`: um plano de 31/08 a 08/09 pertence a
|
||||
# agosto E a setembro, e some da lista se a regra for "está dentro".
|
||||
def planos(limite: 5, inicio: nil, fim: nil)
|
||||
lista = Array(get('/v1/routes/plans/')).sort_by { |p| p['created'].to_s }.reverse
|
||||
return lista.first(limite) if inicio.blank? || fim.blank?
|
||||
|
||||
i = inicio.to_date
|
||||
f = fim.to_date
|
||||
lista.select { |p| cruza_periodo?(p, i, f) }
|
||||
rescue Error, Date::Error
|
||||
[]
|
||||
end
|
||||
|
||||
@@ -192,6 +212,18 @@ module SimpliRoute
|
||||
|
||||
private
|
||||
|
||||
# Plano sem data em um dos lados não some da lista por isso: usa a que tem.
|
||||
# Sumir em silêncio é o pior resultado possível numa lista de escolha.
|
||||
def cruza_periodo?(plano, inicio, fim)
|
||||
comeco = Date.parse(plano['start_date'].to_s) rescue nil
|
||||
termino = Date.parse(plano['end_date'].to_s) rescue nil
|
||||
comeco ||= termino
|
||||
termino ||= comeco
|
||||
return false if comeco.nil?
|
||||
|
||||
comeco <= fim && termino >= inicio
|
||||
end
|
||||
|
||||
def get(path)
|
||||
requisicao(Net::HTTP::Get.new(caminho(path)))
|
||||
end
|
||||
|
||||
@@ -36,6 +36,16 @@
|
||||
Em uso:
|
||||
<strong class="text-white"><%= @logo_atual.presence || 'logo-gade.png (padrão)' %></strong>
|
||||
</p>
|
||||
|
||||
<%# O logo está configurado mas o ARQUIVO sumiu do disco. Antes isto era
|
||||
mudo: a tela dizia o nome do logo novo e o PDF saía com o antigo.
|
||||
Agora aparece aqui, que é onde a pessoa está olhando quando troca. %>
|
||||
<% if @logo_problema == :sumiu %>
|
||||
<p class="text-sm text-red-300 bg-red-900/30 border border-red-500/40 rounded-lg px-3 py-2 mb-2">
|
||||
O arquivo deste logo <strong>não está mais no servidor</strong> — os PDFs estão
|
||||
saindo com o logo padrão. Envie o arquivo de novo.
|
||||
</p>
|
||||
<% end %>
|
||||
<input type="file" name="logo" accept="image/png,image/jpeg"
|
||||
class="w-full text-sm text-gray-300 file:mr-3 file:py-2.5 file:px-4 file:rounded-lg
|
||||
file:border-0 file:bg-[#1a1a1a] file:text-gray-200 file:cursor-pointer">
|
||||
@@ -66,6 +76,42 @@
|
||||
tela "A ação foi recusada por uma verificação de segurança" e volta ao
|
||||
início — enquanto o botão de buscar, na action original, passava. O helper
|
||||
sem argumentos devolve o token GLOBAL, válido para as duas actions. %>
|
||||
<%# ── Período dos planos ────────────────────────────────────────────────
|
||||
Form SEPARADO e em GET: filtra a LISTA de planos, não importa nada.
|
||||
Fica antes do formulário de importação porque é o que se mexe primeiro
|
||||
quando o plano procurado não está entre os 5.
|
||||
|
||||
Por que período e não data: período é aproximado por natureza — "agosto"
|
||||
a pessoa sabe; "20/08" ela não sabia, e era esse chute que a tela veio
|
||||
eliminar. É também o recorte que a própria tela do SimpliRoute usa, então
|
||||
não há tradução a fazer entre as duas. %>
|
||||
<%= form_with url: admin_romaneios_path, method: :get,
|
||||
class: 'flex flex-wrap items-end gap-3 mb-4 pb-4 border-b border-white/5' do %>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-400 mb-1.5">Planos de</label>
|
||||
<input type="date" name="plano_inicio" value="<%= @plano_inicio %>"
|
||||
class="bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-2.5 text-white
|
||||
focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-400 mb-1.5">até</label>
|
||||
<input type="date" name="plano_fim" value="<%= @plano_fim %>"
|
||||
class="bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-2.5 text-white
|
||||
focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="px-4 py-2.5 bg-[#1a1a1a] hover:bg-[#242424] text-gray-200 border border-white/10
|
||||
rounded-xl min-h-[48px] whitespace-nowrap">
|
||||
<%= icone :buscar, cor: nil %> Filtrar planos
|
||||
</button>
|
||||
<%# Rótulo que diz o que o controle FAZ (diretriz 2): limpa o período e
|
||||
devolve os 5 mais recentes — não "limpa a tela". %>
|
||||
<% if @planos_filtrados %>
|
||||
<%= link_to 'Voltar aos 5 mais recentes', admin_romaneios_path,
|
||||
class: 'px-4 py-2.5 text-gray-400 hover:text-white border border-[#2a2a2a] rounded-xl min-h-[48px] inline-flex items-center whitespace-nowrap' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= form_with url: admin_romaneios_path, method: :post, multipart: true,
|
||||
authenticity_token: form_authenticity_token,
|
||||
data: { turbo: false }, class: 'space-y-4' do %>
|
||||
@@ -96,26 +142,33 @@
|
||||
<%= pl['name'] %> · <%= Array(pl['routes']).size %> veículos
|
||||
</option>
|
||||
<% end %>
|
||||
<%# Escape para plano mais antigo que os 5, ou para refazer um
|
||||
romaneio de um dia específico: valor vazio faz o controller
|
||||
usar o campo de data abaixo, como sempre fez. %>
|
||||
<option value="">— usar uma data específica —</option>
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1.5">
|
||||
Os 5 planos mais recentes do SimpliRoute. O dia é descoberto pelo plano.
|
||||
<% if @planos_filtrados %>
|
||||
<%= pluralize(@planos.size, 'plano', plural: 'planos') %> no período.
|
||||
<% else %>
|
||||
Os 5 mais recentes. O dia é descoberto pelo plano.
|
||||
<% end %>
|
||||
</p>
|
||||
<% else %>
|
||||
<p class="text-xs text-orange-400/80 mb-1.5">
|
||||
Não consegui listar os planos do SimpliRoute agora — informe a data.
|
||||
<% if @planos_filtrados %>
|
||||
Nenhum plano nesse período — amplie a faixa ou limpe o filtro.
|
||||
<% else %>
|
||||
Não consegui listar os planos do SimpliRoute agora — informe a data.
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<%# Continua existindo sempre: é o caminho do "— usar uma data
|
||||
específica —" e a rede quando a API não responde. %>
|
||||
<input type="date" name="planned_date" value="<%= @data_padrao %>"
|
||||
<%= 'required' unless @planos.present? %>
|
||||
class="w-full mt-2 bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-3 text-white
|
||||
focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||||
<%# A data solta só aparece quando NÃO há lista de planos: ela é a rede
|
||||
para a API fora do ar, e voltar a oferecê-la de graça reintroduziria
|
||||
justamente o chute que esta tela existe para eliminar. Para alcançar
|
||||
um plano antigo o caminho é o filtro de período, logo abaixo. %>
|
||||
<% if @planos.blank? %>
|
||||
<input type="date" name="planned_date" required value="<%= @data_padrao %>"
|
||||
class="w-full mt-2 bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-3 text-white
|
||||
focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user