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

598 lines
26 KiB
Ruby

# app/controllers/consolidacao_entregas_controller.rb
class ConsolidacaoEntregasController < ApplicationController
include Auditavel
before_action :set_consolidacao
before_action :bloquear_se_finalizada, except: %i[validar revisar]
# Rótulo/sentinela do grupo de entregas sem veículo no filtro "fechar por carro".
SEM_VEICULO = '(sem veículo)'.freeze
# GET /consolidacoes/:consolidacao_id/consolidacao_entregas/validar?motorista=X
# WIZARD PASSO 2 — tela de validação com toggles
def validar
authorize @consolidacao, :show?
@motorista = params[:motorista]
@veiculos_sel = veiculos_param(params[:vehicle]) # filtro por veículo(s) — multi-seleção
# Entregas atendidas do período no banco existente (read-only): sucesso +
# insucesso, pois o motorista foi ao local e a entrega precisa constar.
# Período pela DATA REAL (checkout), não pela planejada.
@entregas = Entrega.atendidas
.da_conta_gade
.no_periodo_checkout(@consolidacao.data_inicio, @consolidacao.data_fim)
.do_motorista(@motorista)
@entregas = @entregas.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
@entregas = @entregas.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present?
@entregas = filtrar_por_veiculo(@entregas, @veiculos_sel) if @veiculos_sel.any?
@entregas = @entregas.order(:checkout)
# Classificações já feitas — agrupadas por tracking_id (uma entrega pode ter
# vários pilares). { tracking_id => [ConsolidacaoEntrega, ...] }
@classificacoes = @consolidacao.consolidacao_entregas
.where(motorista_nome: @motorista)
.group_by(&:tracking_id)
# Status "fechado" por veículo (filtro + acompanhamento) e totais com escopo:
# quando um carro está selecionado, a barra reflete só aquele carro.
@veiculos_status = veiculos_status(@motorista)
if @veiculos_sel.any?
sel = @veiculos_status.select { |v| @veiculos_sel.include?(v[:vehicle]) }
@total_entregas = sel.sum { |v| v[:eligible] }
@classificadas = sel.sum { |v| v[:classificadas] }
else
@total_entregas = @consolidacao.entregas_elegiveis_count(@motorista)
@classificadas = @consolidacao.classificadas_count(@motorista)
end
# Nomes dos motoristas ATIVOS — alimenta o seletor do apontamento 100% manual.
@motoristas_consolidacao = @consolidacao.consolidacao_motoristas.ativos.pluck(:motorista_nome)
# Lançamentos manuais do motorista (apontamentos + termo) — feedback no resumo
# e na barra de progresso. Ficam fora do conjunto elegível do período.
@manuais = contar_manuais(@consolidacao.consolidacao_entregas
.where(motorista_nome: @motorista, manual: true))
# Filtro "apenas não classificadas"
if params[:apenas_pendentes] == '1'
@entregas = @entregas.reject { |e| @classificacoes.key?(e.tracking_id) }
end
@precos = {
entrega_normal: Configuracao.preco_entrega,
retirada: Configuracao.preco_retirada,
bonus: Configuracao.preco_bonus,
desconto: Configuracao.preco_desconto,
extraordinaria: Configuracao.preco_extraordinaria,
termo: Configuracao.preco_termo
}
end
# POST /consolidacoes/:consolidacao_id/consolidacao_entregas/classificar
# Classifica UMA entrega (chamado pelo toggle via Turbo/fetch)
def classificar
authorize @consolidacao, :update?
return render_nao_elegivel unless entrega_elegivel?(params[:motorista], params[:tracking_id])
# Toggle do pilar: se já existe, remove; senão, adiciona (permite combinar
# vários pilares na mesma entrega).
ce = @consolidacao.consolidacao_entregas
.find_by(tracking_id: params[:tracking_id], tipo: params[:tipo])
if ce
ce.destroy
ativo = false
else
@consolidacao.consolidacao_entregas.create!(
tracking_id: params[:tracking_id],
motorista_nome: params[:motorista],
tipo: params[:tipo],
valor_aplicado: valor_classificacao(params[:tipo]),
vehicle: veiculo_do_tracking(params[:tracking_id]), # snapshot p/ fechar por veículo
created_by: current_user.id
)
ativo = true
end
recalcular_motorista(params[:motorista])
respond_to do |format|
format.json do
render json: resumo_json(params[:motorista], vehicles: params[:vehicle]).merge(
ativo: ativo, tracking_id: params[:tracking_id], tipo: params[:tipo]
)
end
format.html { redirect_back fallback_location: wizard_consolidacao_path(@consolidacao) }
end
end
# POST /consolidacoes/:consolidacao_id/consolidacao_entregas/classificar_em_massa
# Ações em massa: marcar todos / marcar selecionados
def classificar_em_massa
authorize @consolidacao, :update?
tipo = params[:tipo]
motorista = params[:motorista]
tracking_ids = Array(params[:tracking_ids]).reject(&:blank?)
return render_nao_elegivel unless motorista_na_consolidacao?(motorista)
# Recorte por veículo(s) selecionado(s). Sem veículo = todos.
elegiveis = tracking_ids_elegiveis(motorista, params[:vehicle])
# Sem tracking_ids = todas as elegíveis do período/veículo (botão "todos").
# Com tracking_ids → mantém só as que de fato são elegíveis para o motorista.
tracking_ids = tracking_ids.empty? ? elegiveis : (tracking_ids & elegiveis)
remover = params[:acao].to_s == 'remover'
ActiveRecord::Base.transaction do
if remover
# Desmarca o pilar das entregas selecionadas (mantém os demais pilares).
@consolidacao.consolidacao_entregas
.where(motorista_nome: motorista, tipo: tipo, tracking_id: tracking_ids)
.destroy_all
else
# ADICIONA o pilar às entregas (não remove os demais já marcados).
valor = valor_classificacao(tipo)
veiculos = veiculos_dos_trackings(tracking_ids) # tracking_id => vehicle (snapshot)
tracking_ids.each do |tid|
ce = @consolidacao.consolidacao_entregas.find_or_initialize_by(tracking_id: tid, tipo: tipo)
ce.assign_attributes(motorista_nome: motorista,
valor_aplicado: valor, created_by: current_user.id)
ce.vehicle = veiculos[tid] if ce.vehicle.blank?
ce.save!
end
end
end
recalcular_motorista(motorista)
auditar!(:editar, @consolidacao,
dados_novos: { acao_massa: tipo, modo: (remover ? 'remover' : 'adicionar'),
qtd: tracking_ids.size, motorista: motorista })
respond_to do |format|
format.json { render json: resumo_json(motorista, vehicles: params[:vehicle]) }
format.html do
verbo = remover ? 'desmarcadas de' : 'marcadas como'
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista, vehicle: veiculos_param(params[:vehicle]).presence),
notice: "#{tracking_ids.size} entregas #{verbo} #{tipo.humanize}."
end
end
end
# GET /consolidacoes/:consolidacao_id/consolidacao_entregas/revisar?motorista=X
# WIZARD PASSO 3 — revisão do motorista
def revisar
authorize @consolidacao, :show?
@motorista = params[:motorista]
todas = @consolidacao.consolidacao_entregas.where(motorista_nome: @motorista)
# Agregados calculados sobre o conjunto COMPLETO (não só a página exibida).
# sum(:quantidade) (default 1) conta os termos lançados em lote como N entregas.
@resumo = todas.group(:tipo).sum(:quantidade)
@total = todas.sum do |c|
c.tipo == 'desconto' ? -c.valor_aplicado : c.valor_aplicado
end
@total_pilares = todas.count
@total_classificadas = todas.distinct.count(:tracking_id)
# Paginação só da tabela exibida — 20 padrão, com seletor 20/50/100
@por_pagina = [20, 50, 100].include?(params[:por_pagina].to_i) ? params[:por_pagina].to_i : 20
begin
@pagy, @classificacoes = pagy(todas.order(:created_at), limit: @por_pagina)
rescue Pagy::OverflowError
@pagy, @classificacoes = pagy(todas.order(:created_at), limit: @por_pagina, page: 1)
end
# Próximo motorista pendente (para botão "Próximo Motorista") — só ativos
nomes_completos = @consolidacao.consolidacao_motoristas.ativos.map(&:motorista_nome)
@proximo = nomes_completos.find do |nome|
next false if nome == @motorista
total = @consolidacao.entregas_elegiveis_count(nome)
@consolidacao.classificadas_count(nome) < total
end
end
# GET /consolidacoes/:consolidacao_id/consolidacao_entregas/buscar_nf?nf=123
# APONTAMENTO MANUAL — busca a NF na SimpleRoute (sem filtro de período).
def buscar_nf
authorize @consolidacao, :update?
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/:consolidacao_id/consolidacao_entregas/apontar
# Cria um apontamento manual a partir de um tracking_id encontrado pela NF.
# Aceita um ou vários pilares (tipos[]) na mesma entrega.
def apontar
authorize @consolidacao, :update?
entrega = Entrega.da_conta_gade.find_by(tracking_id: params[:tracking_id])
return render_apontamento_erro('Entrega não encontrada na base de rastreio.') unless entrega
motorista = @consolidacao.adicionar_apontamento!(entrega, tipos_param, current_user)
auditar!(:editar, @consolidacao,
dados_novos: { apontamento_manual: entrega.numero_nf, tipos: tipos_param, motorista: motorista })
respond_to do |format|
format.json { render json: resumo_json(motorista).merge(motorista: motorista) }
format.html do
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista),
notice: "Apontamento da nota #{entrega.numero_nf} adicionado para #{motorista}."
end
end
rescue ArgumentError => e
render_apontamento_erro(e.message)
end
# POST /consolidacoes/:consolidacao_id/consolidacao_entregas/apontar_manual
# Apontamento 100% MANUAL — não consulta a base de rastreio. O admin informa o
# motorista (pode ser outro que não o da tela), a NF (texto livre), uma
# observação/local e os pilares. Usado quando o motorista foi ao local mas a
# NF ficou registrada com outro motorista no rastreio.
def apontar_manual
authorize @consolidacao, :update?
motorista = @consolidacao.adicionar_apontamento_manual!(
motorista: params[:motorista].to_s.strip,
tipos: tipos_param,
nf: params[:nf],
obs: params[:obs],
valor_extra: valor_extraordinaria_param,
vehicle: params[:vehicle], # veículo escolhido no lançamento (Q3)
user: current_user
)
auditar!(:editar, @consolidacao,
dados_novos: { apontamento_100_manual: params[:nf], motorista: motorista, tipos: tipos_param })
respond_to do |format|
format.json { render json: resumo_json(motorista).merge(motorista: motorista) }
format.html do
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista),
notice: "Apontamento manual adicionado para #{motorista}."
end
end
rescue ArgumentError => e
render_apontamento_erro(e.message)
end
# POST /consolidacoes/:consolidacao_id/consolidacao_entregas/apontar_termo
# ENTREGA DE TERMO — lança um lote de N termos para o motorista. Não há NF nem
# código de rastreio: só a quantidade, a um preço fixo configurável.
def apontar_termo
authorize @consolidacao, :update?
motorista = @consolidacao.adicionar_termos!(
motorista: params[:motorista].to_s.strip,
quantidade: params[:quantidade],
vehicle: params[:vehicle], # veículo escolhido no lançamento (Q3)
user: current_user
)
auditar!(:editar, @consolidacao,
dados_novos: { entrega_termo: params[:quantidade].to_i, motorista: motorista })
respond_to do |format|
format.json { render json: resumo_json(motorista).merge(motorista: motorista) }
format.html do
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista),
notice: "#{params[:quantidade].to_i} entrega(s) de termo adicionada(s) para #{motorista}."
end
end
rescue ArgumentError => e
render_apontamento_erro(e.message)
end
# DELETE /consolidacoes/:consolidacao_id/consolidacao_entregas/remover_apontamento
# Remove um apontamento manual — por ?id= (um pilar) ou por ?tracking_id=&motorista=
# (todos os pilares daquela nota). Não passa pelo guard de elegibilidade, pois o
# tracking está fora do período (por isso não reusa o toggle classificar).
def remover_apontamento
authorize @consolidacao, :update?
scope = @consolidacao.consolidacao_entregas.apontamentos
registros = if params[:id].present?
Array(scope.find_by(id: params[:id]))
else
scope.where(tracking_id: params[:tracking_id], motorista_nome: params[:motorista]).to_a
end
return render_apontamento_erro('Apontamento não encontrado.') if registros.empty?
motorista = registros.first.motorista_nome
tracking = registros.first.tracking_id
registros.each(&:destroy)
@consolidacao.recalcular_motorista!(motorista)
auditar!(:editar, @consolidacao,
dados_novos: { remover_apontamento_manual: tracking, motorista: motorista })
respond_to do |format|
format.json { render json: resumo_json(motorista).merge(motorista: motorista) }
format.html { redirect_back fallback_location: consolidacao_path(@consolidacao), notice: 'Apontamento removido.' }
end
end
# POST /consolidacoes/:consolidacao_id/consolidacao_entregas/fechar_veiculo
# Consolida (fecha) UM veículo do motorista. Exige que o veículo esteja 100%
# classificado. O pagamento continua sendo por motorista (não é pago aqui).
def fechar_veiculo
authorize @consolidacao, :update?
motorista = params[:motorista]
veiculo = params[:vehicle].to_s
return render_veiculo_erro('Veículo/motorista inválido.') unless motorista_na_consolidacao?(motorista) && veiculo.present?
status = veiculos_status(motorista).find { |v| v[:vehicle] == veiculo }
return render_veiculo_erro('Classifique todas as entregas deste veículo antes de consolidar.') if status && !status[:completo]
cv = @consolidacao.consolidacao_veiculos.find_or_create_by!(motorista_nome: motorista, vehicle: veiculo)
cv.fechar!(current_user)
auditar!(:editar, @consolidacao, dados_novos: { fechar_veiculo: veiculo, motorista: motorista })
responder_veiculo(motorista, "Veículo #{veiculo} consolidado.")
end
# POST .../reabrir_veiculo — reabre um veículo já consolidado (reversível).
def reabrir_veiculo
authorize @consolidacao, :update?
motorista = params[:motorista]
cv = @consolidacao.consolidacao_veiculos.find_by(motorista_nome: motorista, vehicle: params[:vehicle].to_s)
return render_veiculo_erro('Veículo não encontrado.') unless cv
cv.reabrir!(current_user)
auditar!(:editar, @consolidacao, dados_novos: { reabrir_veiculo: cv.vehicle, motorista: motorista })
responder_veiculo(motorista, "Veículo #{cv.vehicle} reaberto.")
end
# POST .../arquivar_veiculo — tira um veículo do gate de finalização (reversível).
# As entregas já classificadas dele continuam somando no pagamento do motorista.
def arquivar_veiculo
authorize @consolidacao, :update?
motorista = params[:motorista]
veiculo = params[:vehicle].to_s
return render_veiculo_erro('Veículo/motorista inválido.') unless motorista_na_consolidacao?(motorista) && veiculo.present?
cv = @consolidacao.consolidacao_veiculos.find_or_create_by!(motorista_nome: motorista, vehicle: veiculo)
cv.arquivar!(current_user)
auditar!(:editar, @consolidacao, dados_novos: { arquivar_veiculo: veiculo, motorista: motorista })
responder_veiculo(motorista, "Veículo #{veiculo} arquivado — fora da finalização.")
end
# POST .../desarquivar_veiculo — restaura um veículo arquivado.
def desarquivar_veiculo
authorize @consolidacao, :update?
motorista = params[:motorista]
cv = @consolidacao.consolidacao_veiculos.find_by(motorista_nome: motorista, vehicle: params[:vehicle].to_s)
return render_veiculo_erro('Veículo não encontrado.') unless cv
cv.desarquivar!(current_user)
auditar!(:editar, @consolidacao, dados_novos: { desarquivar_veiculo: cv.vehicle, motorista: motorista })
responder_veiculo(motorista, "Veículo #{cv.vehicle} restaurado.")
end
private
def set_consolidacao
@consolidacao = Consolidacao.find(params[:consolidacao_id])
end
# Resposta padrão das ações de veículo: JSON com o resumo atualizado (barra +
# chips com o novo estado) ou redirect de volta pra tela de validação.
def responder_veiculo(motorista, aviso)
respond_to do |format|
format.json { render json: resumo_json(motorista, vehicles: params[:vehicle_sel]) }
format.html do
redirect_to validar_consolidacao_consolidacao_entregas_path(
@consolidacao, motorista: motorista, vehicle: veiculos_param(params[:vehicle_sel]).presence
), notice: aviso
end
end
end
def render_veiculo_erro(msg)
respond_to do |format|
format.json { render json: { ok: false, erro: msg }, status: :unprocessable_entity }
format.html do
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: params[:motorista]),
alert: msg
end
end
end
# Veículo (snapshot da rastreio) de UM tracking_id. nil para sintéticos.
def veiculo_do_tracking(tracking_id)
Entrega.da_conta_gade.find_by(tracking_id: tracking_id)&.vehicle
end
# Mapa tracking_id => vehicle para um lote (uma query só).
def veiculos_dos_trackings(ids)
return {} if ids.blank?
Entrega.da_conta_gade.where(tracking_id: ids).pluck(:tracking_id, :vehicle).to_h
end
def bloquear_se_finalizada
return unless @consolidacao.finalizada? && current_user.operador?
redirect_to @consolidacao, alert: 'Consolidação finalizada — apenas admin/gerente podem editar.'
end
# Motorista realmente faz parte desta consolidação? (ignora arquivados)
def motorista_na_consolidacao?(nome)
nome.present? && @consolidacao.consolidacao_motoristas.ativos.exists?(motorista_nome: nome)
end
# tracking_ids atendidos/elegíveis para o motorista no período (respeita veículos).
# Atendidas = sucesso + insucesso com checkout (o motorista foi ao local).
# `vehicle` (opcional) recorta para UM carro — é o que faz "Marcar todos" fechar
# só o veículo selecionado.
def tracking_ids_elegiveis(motorista, vehicle = nil)
return [] unless motorista.present?
rel = Entrega.atendidas.da_conta_gade
.no_periodo_checkout(@consolidacao.data_inicio, @consolidacao.data_fim)
.do_motorista(motorista)
rel = rel.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
rel = rel.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present?
rel = filtrar_por_veiculo(rel, vehicle) if vehicle.present?
rel.pluck(:tracking_id)
end
# Normaliza o parâmetro de veículo (string única OU array vindo de vehicle[])
# numa lista limpa de selecionados.
def veiculos_param(raw)
Array(raw).reject(&:blank?)
end
# Recorte por um OU MAIS veículos (trata o grupo "sem veículo"). Lista vazia
# = sem recorte (todos os veículos).
def filtrar_por_veiculo(rel, veiculos)
lista = veiculos_param(veiculos)
return rel if lista.empty?
valores = lista.reject { |v| v == SEM_VEICULO }
valores += [nil, ''] if lista.include?(SEM_VEICULO)
rel.where(vehicle: valores)
end
# Status "fechado" por veículo do motorista: lista de
# { vehicle:, eligible:, classificadas:, fechado: }. Fonte única do filtro de
# carros e do acompanhamento (chips). 2 queries: elegíveis (com a coluna do
# veículo) e as já classificadas (não-manuais).
# Status por veículo do motorista: { vehicle, eligible, classificadas, completo,
# fechado, arquivado }. `completo` = 100% classificado (verde, pode consolidar);
# `fechado` = veículo consolidado (cadeado, vem de consolidacao_veiculos);
# `arquivado` = fora do gate. Une os veículos com entregas elegíveis (rastreio)
# com os que só existem como linha de consolidacao_veiculos (arquivados/manuais).
def veiculos_status(motorista)
return [] unless motorista.present?
rel = Entrega.atendidas.da_conta_gade
.no_periodo_checkout(@consolidacao.data_inicio, @consolidacao.data_fim)
.do_motorista(motorista)
rel = rel.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
rel = rel.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present?
por_veiculo = rel.pluck(:tracking_id, :vehicle).group_by { |(_id, v)| v.presence || SEM_VEICULO }
classificadas = @consolidacao.consolidacao_entregas
.where(motorista_nome: motorista, manual: false)
.distinct.pluck(:tracking_id).to_set
estados = @consolidacao.consolidacao_veiculos.where(motorista_nome: motorista).index_by(&:vehicle)
veiculos = (por_veiculo.keys + estados.keys).uniq
veiculos.map do |veiculo|
ids = (por_veiculo[veiculo] || []).map(&:first)
feito = ids.count { |id| classificadas.include?(id) }
cv = estados[veiculo]
{ vehicle: veiculo, eligible: ids.size, classificadas: feito,
completo: ids.size.positive? && feito >= ids.size,
fechado: cv&.fechado? || false,
arquivado: cv&.arquivado? || false }
end.sort_by { |v| v[:vehicle] }
end
# Valida que a entrega pertence ao motorista informado e ao período/veículo da
# consolidação — impede classificar tracking_ids arbitrários via requisição.
def entrega_elegivel?(motorista, tracking_id)
motorista_na_consolidacao?(motorista) &&
tracking_id.present? &&
tracking_ids_elegiveis(motorista).include?(tracking_id)
end
# Pilares (tipos) selecionados no modal — aceita tipos[] (vários) ou tipo (um).
def tipos_param
tipos = Array(params[:tipos]).presence || Array(params[:tipo])
tipos.reject(&:blank?)
end
def render_apontamento_erro(msg)
respond_to do |format|
format.json { render json: { ok: false, erro: msg }, status: :unprocessable_entity }
format.html { redirect_to wizard_consolidacao_path(@consolidacao), alert: msg }
end
end
def render_nao_elegivel
respond_to do |format|
format.json { render json: { ok: false, erro: 'Entrega não elegível para este motorista.' }, status: :unprocessable_entity }
format.html do
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: params[:motorista]),
alert: 'Entrega não elegível para este motorista.'
end
end
end
def valor_para_tipo(tipo)
ConsolidacaoEntrega.valor_para(tipo)
end
# Valor a aplicar na classificação. Para o pilar "extraordinária" o admin pode
# informar um valor customizado (params[:valor]) por ser uma exceção; se vier
# ausente/inválido, cai no preço padrão de Configurações. Demais pilares usam
# sempre o preço configurado.
def valor_classificacao(tipo)
return valor_para_tipo(tipo) unless tipo.to_s == 'extraordinaria'
valor = params[:valor].to_s.strip.tr(',', '.')
valor = BigDecimal(valor) rescue nil
valor.present? && valor >= 0 ? valor : Configuracao.preco_extraordinaria
end
# Valor customizado da Extraordinária vindo do formulário (params[:valor]),
# aceitando vírgula decimal. Retorna nil se ausente/inválido — aí o model usa
# o preço padrão de Configurações.
def valor_extraordinaria_param
bruto = params[:valor].to_s.strip.tr(',', '.')
return nil if bruto.blank?
valor = BigDecimal(bruto) rescue nil
valor if valor && valor >= 0
end
def recalcular_motorista(nome)
@consolidacao.recalcular_motorista!(nome)
end
# Conta lançamentos manuais como ENTREGAS: cada termo vale `quantidade` (lote
# numa só linha); os demais apontamentos manuais valem 1 por nota (tracking).
def contar_manuais(rel_manual)
rel_manual.termo.sum(:quantidade) +
rel_manual.where.not(tipo: ConsolidacaoEntrega.tipos[:termo]).distinct.count(:tracking_id)
end
# `vehicles` (opcional, string ou array): quando presente, TODO o resumo
# (barra de progresso, contagem por pilar e valor total) reflete apenas o(s)
# veículo(s) selecionado(s). `veiculos_status` acompanha o payload para o front
# atualizar os chips sem recarregar.
def resumo_json(motorista, vehicles: nil)
lista = veiculos_param(vehicles)
classificacoes = @consolidacao.consolidacao_entregas.where(motorista_nome: motorista)
status = veiculos_status(motorista)
classificadas_motorista = classificacoes.distinct.count(:tracking_id)
if lista.any?
# Recorta as classificações às NFs dos veículos selecionados.
ids = tracking_ids_elegiveis(motorista, lista)
classificacoes = classificacoes.where(tracking_id: ids)
escopo = status.select { |v| lista.include?(v[:vehicle]) }.sum { |v| v[:classificadas] }
else
escopo = classificadas_motorista
end
{
ok: true,
classificadas: classificadas_motorista,
classificadas_escopo: escopo,
manuais: contar_manuais(classificacoes.where(manual: true)),
por_tipo: classificacoes.group(:tipo).sum(:quantidade),
valor_total: classificacoes.sum { |e| e.tipo == 'desconto' ? -e.valor_aplicado : e.valor_aplicado }.to_f,
veiculos_status: status
}
end
end