# app/controllers/admin/romaneios_controller.rb # # Romaneio "CONTROLE DE ENTREGA": importa o plano do dia, deixa o operador revisar a # tabela com a prévia do PDF real ao lado, e imprime uma folha A4 paisagem por # veículo. # # As bases externas (espelho de rastreio e gade_entregas_*) continuam SOMENTE # LEITURA — o que gravamos são as tabelas `romaneio*`, nossas. class Admin::RomaneiosController < ApplicationController include Auditavel before_action :carregar_romaneio, except: %i[index create importar_planilha] def index authorize Romaneio # includes: a listagem mostra a contagem de veículos de cada romaneio. @romaneios = Romaneio.recentes.includes(:romaneio_veiculos).limit(50) @operacoes_agrupadas = Operacao.agrupadas_por_mes @data_padrao = Date.current end def show authorize @romaneio @veiculos = @romaneio.romaneio_veiculos.ordenados.to_a @veiculo = veiculo_selecionado @linhas = @romaneio.linhas_vigentes.do_veiculo(@veiculo).to_a if @veiculo @removidas = @romaneio.linhas_removidas.to_a @colunas_ausentes = colunas_ausentes_da_operacao end # Caminho A: busca o plano no SimpliRoute (API, com queda para o espelho). def create authorize Romaneio @romaneio = preparar_romaneio return if performed? plano = Romaneios::PlanoDoDia.new(data: @romaneio.planned_date) importar(plano.linhas, origem: plano.origem) rescue SimpliRoute::Error => e # NotFound herda de Error: a mensagem dele já diz para enviar o .xlsx. descartar_romaneio_vazio redirect_to admin_romaneios_path, alert: e.message end # Caminho B: upload do .xlsx do plano, para quando o plano do dia ainda não foi # publicado ou a API não responde. def importar_planilha authorize Romaneio @romaneio = preparar_romaneio return if performed? arquivo = params[:arquivo] if arquivo.blank? descartar_romaneio_vazio return redirect_to(admin_romaneios_path, alert: 'Selecione a planilha do plano (.xlsx).') end importar(Romaneios::PlanoPlanilha.new(arquivo).linhas, origem: 'planilha') rescue Romaneios::PlanoPlanilha::Erro => e descartar_romaneio_vazio redirect_to admin_romaneios_path, alert: e.message end # Rebusca o plano e reconcilia com o que já está editado (a regra está em # Romaneios::Importador). def reimportar authorize @romaneio # Reimportar com arquivo em mãos usa o arquivo; sem arquivo, volta ao # SimpliRoute — inclusive para um romaneio que nasceu de planilha (é justamente # assim que o operador troca o plano B pelo A quando a API volta). if params[:arquivo].present? plano = Romaneios::PlanoPlanilha.new(params[:arquivo]) linhas = plano.linhas origem = 'planilha' else plano = Romaneios::PlanoDoDia.new(data: @romaneio.planned_date) linhas = plano.linhas origem = plano.origem end resultado = aplicar(linhas, origem: origem) auditar!(:reimportar, @romaneio, dados_novos: resultado.to_h) redirect_to admin_romaneio_path(@romaneio, veiculo: params[:veiculo]), notice: "Plano reimportado — #{resultado.resumo}." rescue SimpliRoute::Error, Romaneios::PlanoPlanilha::Erro => e redirect_to admin_romaneio_path(@romaneio, veiculo: params[:veiculo]), alert: e.message end # Com ?veiculo= sai INLINE (é a prévia do editor, uma folha só, sub-100ms); # sem veiculo sai o romaneio inteiro como download. def pdf authorize @romaneio veiculo = params[:veiculo].to_s binario = Pdf::RomaneioPdf.new(romaneio: @romaneio, veiculos: veiculo.presence && [veiculo]).render # A prévia é regerada a cada edição: cache do navegador aqui devolveria o PDF # anterior e o operador acharia que a alteração não gravou. expires_now if veiculo.present? send_data binario, type: 'application/pdf', disposition: veiculo.present? ? 'inline' : 'attachment', filename: nome_do_arquivo(veiculo) end # JSON — grava UM campo de UMA parada. `valor: null` restaura o que o plano # trouxe (e destrava o campo para a próxima reimportação). def atualizar_linha authorize @romaneio linha = @romaneio.romaneio_linhas.find(params[:linha_id]) campo = params[:campo].to_s unless RomaneioLinha::CAMPOS_EDITAVEIS.include?(campo) return render json: { ok: false, erro: 'Campo não editável.' }, status: :unprocessable_entity end if params[:valor].nil? linha[campo] = linha.valor_do_plano(campo) linha.desmarcar_editado!(campo) else linha[campo] = params[:valor].to_s linha.marcar_editado!(campo) end linha.save! # Devolvemos o valor GRAVADO e o JS o reescreve no input: a gravação é # last-write-wins, e assim uma sobreposição de duas abas fica visível em vez # de silenciosa. render json: { ok: true, valor: linha[campo], editado: linha.editado?(campo) } end def atualizar_veiculo authorize @romaneio registro = @romaneio.romaneio_veiculos.find_by!(veiculo: params[:veiculo].to_s) registro.update!(motorista: params[:motorista].to_s) render json: { ok: true, valor: registro.motorista } end def atualizar_rotulo authorize @romaneio @romaneio.update!(rotulo_plano: params[:rotulo_plano].to_s) render json: { ok: true, valor: @romaneio.rotulo_plano } end def destroy authorize @romaneio # Auditar ANTES: depois do destroy não há mais atributos para registrar. auditar!(:excluir, @romaneio, dados_anteriores: @romaneio.attributes) @romaneio.destroy! redirect_to admin_romaneios_path, notice: 'Romaneio excluído.' end private def carregar_romaneio @romaneio = Romaneio.find(params[:id]) end # Quais colunas a operação NÃO tem. Perguntamos ao serviço em vez de deduzir de # "a coluna APARELHO saiu toda vazia": vazio também é o resultado legítimo de uma # operação só com pacientes recorrentes, e um aviso que erra o motivo é pior do # que nenhum. def colunas_ausentes_da_operacao return [] if @romaneio.operacao_tabela.blank? Romaneios::EnriquecimentoGade.new(@romaneio.operacao_tabela).colunas_ausentes rescue ArgumentError # Operação removida do banco depois que o romaneio foi criado. Romaneios::EnriquecimentoGade::COLUNAS.keys end # Acha (ou cria) o romaneio da data + operação. `find_or_create_by!` é o que faz # "importar de novo o mesmo dia" cair na reconciliação em vez de criar um segundo # romaneio — a chave única (planned_date, operacao_tabela) é a garantia disso. def preparar_romaneio data = params[:planned_date].presence if data.blank? redirect_to admin_romaneios_path, alert: 'Informe a data do plano.' return nil end tabela = params[:operacao_tabela].to_s if tabela.present? && !Operacao.valida?(tabela) redirect_to admin_romaneios_path, alert: 'Selecione uma operação válida.' return nil end data = Date.parse(data) Romaneio.find_or_create_by!(planned_date: data, operacao_tabela: tabela) do |r| r.rotulo_plano = params[:rotulo_plano].presence || Romaneio.rotulo_sugerido(tabela, data) r.importado_por = current_user end rescue Date::Error redirect_to admin_romaneios_path, alert: 'Data do plano inválida.' nil end def importar(linhas, origem:) resultado = aplicar(linhas, origem: origem) auditar!(:criar, @romaneio, dados_novos: resultado.to_h) redirect_to admin_romaneio_path(@romaneio), notice: "Plano importado — #{resultado.resumo}." end def aplicar(linhas, origem:) Romaneios::Importador.new(romaneio: @romaneio, linhas: linhas, usuario: current_user, origem: origem).aplicar! end # Um romaneio recém-criado que não chegou a receber nenhuma parada é lixo: some # da lista para o operador não achar que a importação funcionou. def descartar_romaneio_vazio @romaneio&.destroy! if @romaneio&.persisted? && @romaneio.romaneio_linhas.none? end def veiculo_selecionado pedido = params[:veiculo].to_s return pedido if @veiculos.any? { |v| v.veiculo == pedido } @veiculos.first&.veiculo end def nome_do_arquivo(veiculo) partes = ['romaneio', @romaneio.rotulo_plano.presence || @romaneio.planned_date.to_s, veiculo] "#{partes.compact_blank.join('_').parameterize(separator: '_')}.pdf" end end