Files
Reem-Notas/app/controllers/consolidacoes_controller.rb

438 lines
18 KiB
Ruby

# app/controllers/consolidacoes_controller.rb
class ConsolidacoesController < ApplicationController
include Auditavel
before_action :set_consolidacao,
only: %i[show edit update destroy finalizar arquivar reativar wizard
registrar_pagamento cancelar_pagamento
arquivar_motorista desarquivar_motorista
preview_extrato gerar_pdf_relatorio gerar_pdf_extrato
gerar_pdf_financeiro]
# Formas de pagamento aceitas (o select da UI usa esta mesma lista).
FORMAS_PAGAMENTO = %w[pix transferencia dinheiro].freeze
# GET /consolidacoes — lista com filtros
def index
authorize Consolidacao
@consolidacoes = Consolidacao.ativas.recentes.includes(:criador)
# Filtros
@consolidacoes = @consolidacoes.where(status: params[:status]) if params[:status].present?
@consolidacoes = @consolidacoes.where('nome ILIKE ?', "%#{params[:nome]}%") if params[:nome].present?
@consolidacoes = @consolidacoes.where('data_inicio >= ?', params[:inicio]) if params[:inicio].present?
@consolidacoes = @consolidacoes.where('data_fim <= ?', params[:fim]) if params[:fim].present?
if params[:motorista].present?
ids = ConsolidacaoMotorista.where('motorista_nome ILIKE ?', "%#{params[:motorista]}%")
.pluck(:consolidacao_id)
@consolidacoes = @consolidacoes.where(id: ids)
end
if params[:vehicle].present?
@consolidacoes = @consolidacoes.where('vehicle_ids @> ?', [params[:vehicle]].to_json)
end
if %w[pago pendente parcial].include?(params[:pagamento])
@consolidacoes = @consolidacoes.public_send("pagamento_#{params[:pagamento]}")
end
end
# GET /consolidacoes/new
def new
authorize Consolidacao
@consolidacao = Consolidacao.new(
data_inicio: Date.current.beginning_of_month,
data_fim: Date.current
)
@motoristas = Entrega.motoristas_ativos
@veiculos = Entrega.veiculos_unicos
@operacoes = Operacao.todas
end
# POST /consolidacoes
def create
authorize Consolidacao
@consolidacao = Consolidacao.new(consolidacao_params.merge(created_by: current_user.id))
@consolidacao.operacoes = Operacao.sanitizar(@consolidacao.operacoes)
motoristas = Array(params[:motoristas]).reject(&:blank?)
if motoristas.empty?
@consolidacao.errors.add(:base, 'Selecione ao menos um motorista')
preparar_form_new
return render :new, status: :unprocessable_entity
end
ActiveRecord::Base.transaction do
@consolidacao.save!
motoristas.each do |nome|
@consolidacao.consolidacao_motoristas.create!(motorista_nome: nome)
end
end
auditar!(:criar, @consolidacao, dados_novos: @consolidacao.attributes)
redirect_to wizard_consolidacao_path(@consolidacao), notice: 'Consolidação criada! Selecione o motorista para validar.'
rescue ActiveRecord::RecordInvalid
preparar_form_new
render :new, status: :unprocessable_entity
end
# GET /consolidacoes/veiculos_disponiveis?motoristas[]=X&inicio=&fim=
# Lista (JSON) os veículos usados pelos motoristas selecionados no período —
# alimenta o filtro dinâmico de veículos no formulário de nova consolidação.
def veiculos_disponiveis
authorize Consolidacao, :new?
veiculos = Entrega.veiculos_de_motoristas(
params[:motoristas], inicio: params[:inicio], fim: params[:fim]
)
render json: { veiculos: veiculos }
end
# GET /consolidacoes/operacoes_disponiveis — lista (JSON) as tabelas de operação.
def operacoes_disponiveis
authorize Consolidacao, :new?
render json: { operacoes: Operacao.todas }
end
# GET /consolidacoes/dados_da_operacao?operacoes[]=X — motoristas/veículos/período
# derivados das operações selecionadas, para pré-preencher o formulário.
def dados_da_operacao
authorize Consolidacao, :new?
render json: Operacao.dados(params[:operacoes])
end
# GET /consolidacoes/buscar_nf?nf=123 — busca a NF na SimpleRoute (nota avulsa).
def buscar_nf
authorize Consolidacao, :create?
nf = params[:nf].to_s.strip
return render(json: { ok: false, erro: 'Informe o número da nota.' }, status: :unprocessable_entity) if nf.blank?
entregas = Entrega.da_conta_gade.por_nf(nf).order(:checkout)
if entregas.empty?
render json: { ok: false, erro: "Nota #{nf} não encontrada na base de rastreio." }
else
render json: { ok: true, nf: nf, entregas: entregas.map(&:resumo_apontamento) }
end
end
# POST /consolidacoes/avulsa — cria uma consolidação JÁ a partir de uma NF
# avulsa (entrega que chegou por fora) e adiciona o apontamento ao motorista.
def avulsa
authorize Consolidacao, :create?
entrega = Entrega.da_conta_gade.find_by(tracking_id: params[:tracking_id])
return redirect_to(consolidacoes_path, alert: 'Entrega não encontrada na base de rastreio.') unless entrega
data_ref = (entrega.checkout || entrega.planned_date)&.to_date || Date.current
consolidacao = nil
ActiveRecord::Base.transaction do
consolidacao = Consolidacao.create!(
nome: "Avulsa nf#{entrega.numero_nf}",
data_inicio: data_ref,
data_fim: data_ref.end_of_month,
created_by: current_user.id
)
consolidacao.adicionar_apontamento!(entrega, Array(params[:tipos]).reject(&:blank?), current_user)
end
auditar!(:criar, consolidacao,
dados_novos: { avulsa_nf: entrega.numero_nf, motorista: entrega.driver, tipos: params[:tipos] })
redirect_to consolidacao_path(consolidacao),
notice: "Consolidação avulsa criada para #{entrega.driver} (NF #{entrega.numero_nf})."
rescue ArgumentError => e
redirect_to consolidacoes_path, alert: e.message
rescue ActiveRecord::RecordInvalid => e
redirect_to consolidacoes_path, alert: "Não foi possível criar a consolidação avulsa: #{e.message}"
end
# GET /consolidacoes/:id — visão geral
def show
authorize @consolidacao
# A gestão de arquivamento de motorista fica no wizard; aqui listamos só os ativos.
@motoristas = @consolidacao.consolidacao_motoristas.ativos.order(:motorista_nome)
# Apontamentos manuais agrupados por nota (tracking) p/ exibição e remoção.
@apontamentos = @consolidacao.consolidacao_entregas.apontamentos
.order(:motorista_nome, :tracking_id)
.group_by { |ce| [ce.motorista_nome, ce.tracking_id] }
end
# GET /consolidacoes/:id/wizard — PASSO 1: selecionar motorista
def wizard
authorize @consolidacao, :update?
@arquivados = @consolidacao.consolidacao_motoristas.arquivados.order(:motorista_nome)
@motoristas = @consolidacao.consolidacao_motoristas.ativos.order(:motorista_nome).map do |cm|
total = @consolidacao.entregas_elegiveis_count(cm.motorista_nome)
classificadas = @consolidacao.classificadas_count(cm.motorista_nome)
apontamentos = @consolidacao.consolidacao_entregas
.apontamentos
.where(motorista_nome: cm.motorista_nome)
.distinct.count(:tracking_id)
{
registro: cm,
total: total,
classificadas: classificadas,
apontamentos: apontamentos,
# Espelha o gate do servidor (todas_entregas_classificadas?): um motorista
# só com apontamento manual tem 0 entregas elegíveis e já está "completo".
completo: total.zero? || classificadas >= total
}
end
end
# POST /consolidacoes/:id/finalizar
def finalizar
authorize @consolidacao, :update?
if @consolidacao.finalizar!(current_user)
auditar!(:finalizar, @consolidacao)
NotificacaoService.notificar_finalizacao(@consolidacao) # WhatsApp/email aos motoristas
redirect_to @consolidacao, notice: 'Consolidação finalizada! Motoristas notificados.'
else
redirect_to wizard_consolidacao_path(@consolidacao),
alert: 'Não é possível finalizar: existem entregas não classificadas.'
end
end
# POST /consolidacoes/:id/arquivar
def arquivar
authorize @consolidacao, :destroy?
@consolidacao.arquivar!(current_user)
auditar!(:arquivar, @consolidacao)
redirect_to consolidacoes_path, notice: 'Consolidação arquivada.'
end
# GET /consolidacoes/arquivadas — histórico de consolidações arquivadas
def arquivadas
authorize Consolidacao
@consolidacoes = Consolidacao.arquivadas.recentes.includes(:criador, :arquivador)
end
# POST /consolidacoes/:id/reativar — restaura uma consolidação arquivada
def reativar
authorize @consolidacao, :destroy?
@consolidacao.reativar!(current_user)
auditar!(:reativar, @consolidacao)
redirect_to consolidacoes_path, notice: 'Consolidação reativada.'
end
# DELETE /consolidacoes/:id — exclui PERMANENTEMENTE (apenas arquivadas)
def destroy
authorize @consolidacao, :destroy?
@consolidacao.destroy!
auditar!(:excluir, @consolidacao)
redirect_to arquivadas_consolidacoes_path, notice: 'Consolidação excluída permanentemente.'
end
# POST /consolidacoes/:id/registrar_pagamento
# Marca 1 motorista (param consolidacao_motorista_id) ou TODOS os pendentes.
def registrar_pagamento
authorize @consolidacao, :registrar_pagamento?
return pagamento_indisponivel unless @consolidacao.finalizada?
forma = params[:forma_pagamento].presence_in(FORMAS_PAGAMENTO) || 'pix'
alvos = motoristas_alvo.select { |cm| !cm.pago? }
# NF obrigatória por motorista. Se faltar para qualquer alvo, aborta sem
# marcar nenhum (a NF vai pro painel do motorista e pro Extrato).
notas = nf_por_motorista(alvos)
if notas.nil?
return redirect_to @consolidacao,
alert: 'Informe o número da Nota Fiscal de cada motorista antes de marcar como pago.'
end
alvos.each do |cm|
cm.marcar_pago!(current_user, forma: forma, nota_fiscal: notas[cm.id])
NotificacaoService.notificar_pagamento(@consolidacao, cm)
end
auditar!(:registrar_pagamento, @consolidacao,
dados_novos: { motoristas: alvos.map(&:motorista_nome), forma: forma,
notas_fiscais: alvos.map { |cm| [cm.motorista_nome, notas[cm.id]] }.to_h })
redirect_to @consolidacao, notice: pagamento_notice(alvos.size, :pago)
end
# POST /consolidacoes/:id/cancelar_pagamento — estorna 1 motorista ou TODOS.
def cancelar_pagamento
authorize @consolidacao, :cancelar_pagamento?
alvos = motoristas_alvo.select(&:pago?)
alvos.each { |cm| cm.cancelar_pagamento!(current_user) }
auditar!(:cancelar_pagamento, @consolidacao,
dados_novos: { motoristas: alvos.map(&:motorista_nome) })
redirect_to @consolidacao, notice: pagamento_notice(alvos.size, :estornado)
end
# POST /consolidacoes/:id/arquivar_motorista — arquiva 1 motorista da
# consolidação (reversível). Ele sai dos totais/pagamento/gate, mas os dados
# ficam guardados e podem ser restaurados. Não arquiva motorista já pago.
def arquivar_motorista
authorize @consolidacao, :update?
cm = @consolidacao.consolidacao_motoristas.find(params[:consolidacao_motorista_id])
if cm.pago?
return redirect_to wizard_consolidacao_path(@consolidacao),
alert: "#{cm.motorista_nome} já está pago — estorne o pagamento antes de arquivar."
end
cm.arquivar!(current_user)
@consolidacao.recalcular_motorista!(cm.motorista_nome) # tira o valor do total
auditar!(:editar, @consolidacao, dados_novos: { arquivar_motorista: cm.motorista_nome })
redirect_to wizard_consolidacao_path(@consolidacao),
notice: "#{cm.motorista_nome} arquivado. Você pode restaurar quando quiser."
end
# POST /consolidacoes/:id/desarquivar_motorista — restaura um motorista arquivado.
def desarquivar_motorista
authorize @consolidacao, :update?
cm = @consolidacao.consolidacao_motoristas.find(params[:consolidacao_motorista_id])
cm.desarquivar!(current_user)
@consolidacao.recalcular_motorista!(cm.motorista_nome) # devolve o valor ao total
auditar!(:editar, @consolidacao, dados_novos: { desarquivar_motorista: cm.motorista_nome })
redirect_to wizard_consolidacao_path(@consolidacao),
notice: "#{cm.motorista_nome} restaurado para a consolidação."
end
# ── FASE 7 — PDFs ────────────────────────────────────────────
# GET /consolidacoes/:id/gerar_pdf_relatorio?motorista=X
def gerar_pdf_relatorio
authorize @consolidacao, :show?
motorista = params[:motorista]
pdf = Pdf::RelatorioMotoristaPdf.new(consolidacao: @consolidacao, motorista: motorista)
auditar!(:editar, @consolidacao, dados_novos: { pdf: 'relatorio', motorista: motorista })
send_data pdf.render,
filename: "relatorio_#{motorista.parameterize}_#{@consolidacao.id}.pdf",
type: 'application/pdf',
disposition: 'attachment'
end
# GET /consolidacoes/:id/gerar_pdf_financeiro — relatório financeiro da consolidação
def gerar_pdf_financeiro
authorize @consolidacao, :show?
pdf = Pdf::RelatorioFinanceiroConsolidacaoPdf.new(consolidacao: @consolidacao)
auditar!(:editar, @consolidacao, dados_novos: { pdf: 'financeiro' })
send_data pdf.render,
filename: "financeiro_#{@consolidacao.nome.parameterize}_#{@consolidacao.id}.pdf",
type: 'application/pdf',
disposition: 'attachment'
end
# GET /consolidacoes/:id/gerar_pdf_extrato?motorista=X
def gerar_pdf_extrato
autorizar_extrato!(params[:motorista])
motorista = params[:motorista]
user_mot = buscar_user_motorista(motorista)
user_mot&.regenerar_login_token! if user_mot&.login_token.blank?
cm = @consolidacao.consolidacao_motoristas.find_by(motorista_nome: motorista)
pdf = Pdf::ExtratoPdf.new(consolidacao: @consolidacao, motorista: motorista,
user_motorista: user_mot, app_host: ENV['APP_HOST'],
pago_em: cm&.pago_em, forma_pagamento: cm&.forma_pagamento,
nota_fiscal: cm&.nota_fiscal)
auditar!(:editar, @consolidacao, dados_novos: { pdf: 'extrato', motorista: motorista })
send_data pdf.render,
filename: "extrato_#{motorista.parameterize}_#{@consolidacao.id}.pdf",
type: 'application/pdf',
disposition: 'attachment'
end
# GET /consolidacoes/:id/preview_extrato?motorista=X — preview HTML antes do PDF
def preview_extrato
authorize @consolidacao, :show?
@motorista = params[:motorista]
@entregas = @consolidacao.consolidacao_entregas.where(motorista_nome: @motorista)
# sum(:quantidade) (default 1) conta os termos lançados em lote como N entregas.
@resumo = @entregas.group(:tipo).sum(:quantidade)
@total = @entregas.sum { |e| e.desconto? ? -e.valor_aplicado : e.valor_aplicado }
render layout: false # renderiza dentro do modal (Turbo Frame)
end
private
# Acha o User motorista pelo nome para gerar o QR Code de acesso. O
# motorista_nome vem dos dados externos de entrega e o users.nome é digitado
# no admin — então toleramos diferenças de caixa e de espaços (extras/duplos).
# Sem isso, qualquer divergência deixava user_mot nil e o QR sumia do extrato.
def buscar_user_motorista(nome)
exato = User.motorista.find_by('LOWER(nome) = ?', nome.to_s.downcase.strip)
return exato if exato
alvo = normalizar_nome(nome)
User.motorista.find { |u| normalizar_nome(u.nome) == alvo }
end
def normalizar_nome(nome)
nome.to_s.strip.downcase.gsub(/\s+/, ' ')
end
# Staff (show?) baixa qualquer extrato. O motorista baixa só o PRÓPRIO — o PDF
# já é filtrado pelo nome dele, então não há vazamento de dados de terceiros.
def autorizar_extrato!(motorista_nome)
if current_user.motorista?
raise Pundit::NotAuthorizedError unless motorista_nome.to_s.casecmp?(current_user.nome.to_s)
skip_authorization
else
authorize @consolidacao, :show?
end
end
def set_consolidacao
@consolidacao = Consolidacao.find(params[:id])
end
# Um motorista específico (consolidacao_motorista_id) ou todos da consolidação.
# "Todos" cobre apenas os ATIVOS — motorista arquivado não recebe pagamento.
def motoristas_alvo
if params[:consolidacao_motorista_id].present?
[@consolidacao.consolidacao_motoristas.find(params[:consolidacao_motorista_id])]
else
@consolidacao.consolidacao_motoristas.ativos.to_a
end
end
# Mapeia { cm.id => "numero da NF" } a partir de params[:notas_fiscais].
# Retorna nil se a NF de QUALQUER alvo estiver em branco (pagamento bloqueado).
def nf_por_motorista(alvos)
informadas = params[:notas_fiscais] || {}
alvos.each_with_object({}) do |cm, mapa|
nf = informadas[cm.id.to_s].to_s.strip
return nil if nf.blank?
mapa[cm.id] = nf
end
end
def pagamento_indisponivel
redirect_to @consolidacao, alert: 'Só é possível registrar pagamento de uma consolidação finalizada.'
end
def pagamento_notice(qtd, acao)
return 'Nenhuma alteração: motoristas já estavam no estado solicitado.' if qtd.zero?
verbo = acao == :pago ? 'marcado(s) como pago(s)' : 'com pagamento estornado'
"#{qtd} motorista(s) #{verbo}."
end
def preparar_form_new
@motoristas = Entrega.motoristas_ativos
@veiculos = Entrega.veiculos_unicos
@operacoes = Operacao.todas
end
def consolidacao_params
params.require(:consolidacao).permit(:nome, :data_inicio, :data_fim, vehicle_ids: [], operacoes: [])
end
end