Grande correção do filtro de veiculos na area de consolidação
This commit is contained in:
BIN
Imagens para correção /Imagem colada (2).png
Normal file
BIN
Imagens para correção /Imagem colada (2).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
@@ -88,6 +88,7 @@ class ConsolidacaoEntregasController < ApplicationController
|
|||||||
motorista_nome: params[:motorista],
|
motorista_nome: params[:motorista],
|
||||||
tipo: params[:tipo],
|
tipo: params[:tipo],
|
||||||
valor_aplicado: valor_classificacao(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
|
created_by: current_user.id
|
||||||
)
|
)
|
||||||
ativo = true
|
ativo = true
|
||||||
@@ -132,11 +133,13 @@ class ConsolidacaoEntregasController < ApplicationController
|
|||||||
.destroy_all
|
.destroy_all
|
||||||
else
|
else
|
||||||
# ADICIONA o pilar às entregas (não remove os demais já marcados).
|
# ADICIONA o pilar às entregas (não remove os demais já marcados).
|
||||||
valor = valor_classificacao(tipo)
|
valor = valor_classificacao(tipo)
|
||||||
|
veiculos = veiculos_dos_trackings(tracking_ids) # tracking_id => vehicle (snapshot)
|
||||||
tracking_ids.each do |tid|
|
tracking_ids.each do |tid|
|
||||||
ce = @consolidacao.consolidacao_entregas.find_or_initialize_by(tracking_id: tid, tipo: tipo)
|
ce = @consolidacao.consolidacao_entregas.find_or_initialize_by(tracking_id: tid, tipo: tipo)
|
||||||
ce.assign_attributes(motorista_nome: motorista,
|
ce.assign_attributes(motorista_nome: motorista,
|
||||||
valor_aplicado: valor, created_by: current_user.id)
|
valor_aplicado: valor, created_by: current_user.id)
|
||||||
|
ce.vehicle = veiculos[tid] if ce.vehicle.blank?
|
||||||
ce.save!
|
ce.save!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -246,6 +249,7 @@ class ConsolidacaoEntregasController < ApplicationController
|
|||||||
nf: params[:nf],
|
nf: params[:nf],
|
||||||
obs: params[:obs],
|
obs: params[:obs],
|
||||||
valor_extra: valor_extraordinaria_param,
|
valor_extra: valor_extraordinaria_param,
|
||||||
|
vehicle: params[:vehicle], # veículo escolhido no lançamento (Q3)
|
||||||
user: current_user
|
user: current_user
|
||||||
)
|
)
|
||||||
auditar!(:editar, @consolidacao,
|
auditar!(:editar, @consolidacao,
|
||||||
@@ -271,6 +275,7 @@ class ConsolidacaoEntregasController < ApplicationController
|
|||||||
motorista = @consolidacao.adicionar_termos!(
|
motorista = @consolidacao.adicionar_termos!(
|
||||||
motorista: params[:motorista].to_s.strip,
|
motorista: params[:motorista].to_s.strip,
|
||||||
quantidade: params[:quantidade],
|
quantidade: params[:quantidade],
|
||||||
|
vehicle: params[:vehicle], # veículo escolhido no lançamento (Q3)
|
||||||
user: current_user
|
user: current_user
|
||||||
)
|
)
|
||||||
auditar!(:editar, @consolidacao,
|
auditar!(:editar, @consolidacao,
|
||||||
@@ -315,12 +320,104 @@ class ConsolidacaoEntregasController < ApplicationController
|
|||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
def set_consolidacao
|
def set_consolidacao
|
||||||
@consolidacao = Consolidacao.find(params[:consolidacao_id])
|
@consolidacao = Consolidacao.find(params[:consolidacao_id])
|
||||||
end
|
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
|
def bloquear_se_finalizada
|
||||||
return unless @consolidacao.finalizada? && current_user.operador?
|
return unless @consolidacao.finalizada? && current_user.operador?
|
||||||
redirect_to @consolidacao, alert: 'Consolidação finalizada — apenas admin/gerente podem editar.'
|
redirect_to @consolidacao, alert: 'Consolidação finalizada — apenas admin/gerente podem editar.'
|
||||||
@@ -367,6 +464,11 @@ class ConsolidacaoEntregasController < ApplicationController
|
|||||||
# { vehicle:, eligible:, classificadas:, fechado: }. Fonte única do filtro de
|
# { vehicle:, eligible:, classificadas:, fechado: }. Fonte única do filtro de
|
||||||
# carros e do acompanhamento (chips). 2 queries: elegíveis (com a coluna do
|
# carros e do acompanhamento (chips). 2 queries: elegíveis (com a coluna do
|
||||||
# veículo) e as já classificadas (não-manuais).
|
# 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)
|
def veiculos_status(motorista)
|
||||||
return [] unless motorista.present?
|
return [] unless motorista.present?
|
||||||
|
|
||||||
@@ -380,12 +482,17 @@ class ConsolidacaoEntregasController < ApplicationController
|
|||||||
classificadas = @consolidacao.consolidacao_entregas
|
classificadas = @consolidacao.consolidacao_entregas
|
||||||
.where(motorista_nome: motorista, manual: false)
|
.where(motorista_nome: motorista, manual: false)
|
||||||
.distinct.pluck(:tracking_id).to_set
|
.distinct.pluck(:tracking_id).to_set
|
||||||
|
estados = @consolidacao.consolidacao_veiculos.where(motorista_nome: motorista).index_by(&:vehicle)
|
||||||
|
|
||||||
por_veiculo.map do |veiculo, linhas|
|
veiculos = (por_veiculo.keys + estados.keys).uniq
|
||||||
ids = linhas.map(&:first)
|
veiculos.map do |veiculo|
|
||||||
|
ids = (por_veiculo[veiculo] || []).map(&:first)
|
||||||
feito = ids.count { |id| classificadas.include?(id) }
|
feito = ids.count { |id| classificadas.include?(id) }
|
||||||
|
cv = estados[veiculo]
|
||||||
{ vehicle: veiculo, eligible: ids.size, classificadas: feito,
|
{ vehicle: veiculo, eligible: ids.size, classificadas: feito,
|
||||||
fechado: ids.size.positive? && feito >= ids.size }
|
completo: ids.size.positive? && feito >= ids.size,
|
||||||
|
fechado: cv&.fechado? || false,
|
||||||
|
arquivado: cv&.arquivado? || false }
|
||||||
end.sort_by { |v| v[:vehicle] }
|
end.sort_by { |v| v[:vehicle] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,10 @@ class ConsolidacoesController < ApplicationController
|
|||||||
@apontamentos = @consolidacao.consolidacao_entregas.apontamentos
|
@apontamentos = @consolidacao.consolidacao_entregas.apontamentos
|
||||||
.order(:motorista_nome, :tracking_id)
|
.order(:motorista_nome, :tracking_id)
|
||||||
.group_by { |ce| [ce.motorista_nome, ce.tracking_id] }
|
.group_by { |ce| [ce.motorista_nome, ce.tracking_id] }
|
||||||
|
# Veículos consolidados por motorista (breakdown informativo no card).
|
||||||
|
@veiculos_por_motorista = @consolidacao.consolidacao_veiculos.ativos
|
||||||
|
.order(:vehicle)
|
||||||
|
.group_by(&:motorista_nome)
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /consolidacoes/:id/wizard — PASSO 1: selecionar motorista
|
# GET /consolidacoes/:id/wizard — PASSO 1: selecionar motorista
|
||||||
@@ -177,14 +181,20 @@ class ConsolidacoesController < ApplicationController
|
|||||||
.apontamentos
|
.apontamentos
|
||||||
.where(motorista_nome: cm.motorista_nome)
|
.where(motorista_nome: cm.motorista_nome)
|
||||||
.distinct.count(:tracking_id)
|
.distinct.count(:tracking_id)
|
||||||
|
# Status por veículo (para o breakdown do card) e estado de fechamento.
|
||||||
|
veiculos = @consolidacao.consolidacao_veiculos.ativos
|
||||||
|
.where(motorista_nome: cm.motorista_nome)
|
||||||
|
.order(:vehicle)
|
||||||
{
|
{
|
||||||
registro: cm,
|
registro: cm,
|
||||||
total: total,
|
total: total,
|
||||||
classificadas: classificadas,
|
classificadas: classificadas,
|
||||||
apontamentos: apontamentos,
|
apontamentos: apontamentos,
|
||||||
# Espelha o gate do servidor (todas_entregas_classificadas?): um motorista
|
veiculos: veiculos,
|
||||||
# só com apontamento manual tem 0 entregas elegíveis e já está "completo".
|
veiculos_fechados: veiculos.count(&:fechado?),
|
||||||
completo: total.zero? || classificadas >= total
|
# Espelha o gate do servidor (todas_entregas_classificadas?): exige tudo
|
||||||
|
# classificado E todos os veículos ativos fechados (consolidados).
|
||||||
|
completo: @consolidacao.motorista_completo?(cm.motorista_nome)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -199,7 +209,7 @@ class ConsolidacoesController < ApplicationController
|
|||||||
redirect_to @consolidacao, notice: 'Consolidação finalizada! Motoristas notificados.'
|
redirect_to @consolidacao, notice: 'Consolidação finalizada! Motoristas notificados.'
|
||||||
else
|
else
|
||||||
redirect_to wizard_consolidacao_path(@consolidacao),
|
redirect_to wizard_consolidacao_path(@consolidacao),
|
||||||
alert: 'Não é possível finalizar: existem entregas não classificadas.'
|
alert: 'Não é possível finalizar: ainda há entregas não classificadas. Classifique-as ou arquive o veículo pendente.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -320,6 +330,7 @@ class ConsolidacoesController < ApplicationController
|
|||||||
nome = cm.motorista_nome
|
nome = cm.motorista_nome
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@consolidacao.consolidacao_entregas.where(motorista_nome: nome).delete_all
|
@consolidacao.consolidacao_entregas.where(motorista_nome: nome).delete_all
|
||||||
|
@consolidacao.consolidacao_veiculos.where(motorista_nome: nome).delete_all
|
||||||
cm.destroy!
|
cm.destroy!
|
||||||
end
|
end
|
||||||
# cm já não existe — atualiza o total da consolidação direto (soma dos ativos).
|
# cm já não existe — atualiza o total da consolidação direto (soma dos ativos).
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ["modal", "motorista", "nf", "obs", "erro", "tipoCheck", "valorExtra"]
|
static targets = ["modal", "motorista", "nf", "obs", "erro", "tipoCheck", "valorExtra", "veiculo"]
|
||||||
static values = { apontarManualUrl: String }
|
static values = { apontarManualUrl: String }
|
||||||
|
|
||||||
abrir() {
|
abrir() {
|
||||||
@@ -52,7 +52,8 @@ export default class extends Controller {
|
|||||||
nf: this.nfTarget.value.trim(),
|
nf: this.nfTarget.value.trim(),
|
||||||
obs: this.obsTarget.value.trim(),
|
obs: this.obsTarget.value.trim(),
|
||||||
tipos: tipos,
|
tipos: tipos,
|
||||||
valor: valor
|
valor: valor,
|
||||||
|
vehicle: this.hasVeiculoTarget ? this.veiculoTarget.value : ""
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ["modal", "quantidade", "erro"]
|
static targets = ["modal", "quantidade", "erro", "veiculo"]
|
||||||
static values = { apontarTermoUrl: String, motorista: String }
|
static values = { apontarTermoUrl: String, motorista: String }
|
||||||
|
|
||||||
abrir() {
|
abrir() {
|
||||||
@@ -55,7 +55,11 @@ export default class extends Controller {
|
|||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
|
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ motorista: motorista, quantidade: quantidade })
|
body: JSON.stringify({
|
||||||
|
motorista: motorista,
|
||||||
|
quantidade: quantidade,
|
||||||
|
vehicle: this.hasVeiculoTarget ? this.veiculoTarget.value : ""
|
||||||
|
})
|
||||||
})
|
})
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
if (!resp.ok || data.ok === false) {
|
if (!resp.ok || data.ok === false) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export default class extends Controller {
|
|||||||
"linha", "checkbox", "selecionarTodos", "busca", "buscaVazia",
|
"linha", "checkbox", "selecionarTodos", "busca", "buscaVazia",
|
||||||
"resumo", "listaCol", "btnResumoLabel",
|
"resumo", "listaCol", "btnResumoLabel",
|
||||||
"toggles", "barra", "barraManuais", "percentual", "chipVeiculo",
|
"toggles", "barra", "barraManuais", "percentual", "chipVeiculo",
|
||||||
|
"consolidarBtn", "consolidarHint",
|
||||||
"contadorClassificadas", "contadorTotal", "contadorManuais", "manuaisInfo",
|
"contadorClassificadas", "contadorTotal", "contadorManuais", "manuaisInfo",
|
||||||
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria",
|
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria",
|
||||||
"qtdTermo", "qtdManuais", "valorTotal"
|
"qtdTermo", "qtdManuais", "valorTotal"
|
||||||
@@ -217,6 +218,23 @@ export default class extends Controller {
|
|||||||
this.barraTarget.classList.toggle("bg-green-500", pct >= 100)
|
this.barraTarget.classList.toggle("bg-green-500", pct >= 100)
|
||||||
this.barraTarget.classList.toggle("bg-orange-500", pct < 100)
|
this.barraTarget.classList.toggle("bg-orange-500", pct < 100)
|
||||||
|
|
||||||
|
// Botão "Consolidar este veículo" (só existe quando UM veículo está
|
||||||
|
// selecionado e ainda não fechado): habilita quando o escopo chega a 100%.
|
||||||
|
if (this.hasConsolidarBtnTarget) {
|
||||||
|
const pronto = pct >= 100
|
||||||
|
this.consolidarBtnTarget.disabled = !pronto
|
||||||
|
this.consolidarBtnTarget.classList.toggle("bg-green-600", pronto)
|
||||||
|
this.consolidarBtnTarget.classList.toggle("hover:bg-green-500", pronto)
|
||||||
|
this.consolidarBtnTarget.classList.toggle("text-white", pronto)
|
||||||
|
this.consolidarBtnTarget.classList.toggle("cursor-pointer", pronto)
|
||||||
|
this.consolidarBtnTarget.classList.toggle("bg-[#2a2a2a]", !pronto)
|
||||||
|
this.consolidarBtnTarget.classList.toggle("text-gray-600", !pronto)
|
||||||
|
this.consolidarBtnTarget.classList.toggle("cursor-not-allowed", !pronto)
|
||||||
|
if (this.hasConsolidarHintTarget)
|
||||||
|
this.consolidarHintTarget.textContent =
|
||||||
|
pronto ? "Pronto para consolidar." : "Classifique 100% deste veículo para consolidar."
|
||||||
|
}
|
||||||
|
|
||||||
if (dados.veiculos_status) this.atualizarChips(dados.veiculos_status)
|
if (dados.veiculos_status) this.atualizarChips(dados.veiculos_status)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,9 +245,12 @@ export default class extends Controller {
|
|||||||
const porVeiculo = {}
|
const porVeiculo = {}
|
||||||
status.forEach(v => { porVeiculo[v.vehicle] = v })
|
status.forEach(v => { porVeiculo[v.vehicle] = v })
|
||||||
|
|
||||||
const VERDE = ["border-green-500", "text-green-400", "bg-green-500/10"]
|
const LARANJA = ["border-orange-500", "text-orange-400", "bg-orange-500/10"]
|
||||||
const LARANJA = ["border-orange-500", "text-orange-400", "bg-orange-500/10"]
|
const VERDE_FECHADO = ["border-green-500", "text-green-400", "bg-green-500/10"]
|
||||||
const NEUTRO = ["border-[#2a2a2a]", "text-gray-300"]
|
const VERDE_PRONTO = ["border-green-600/50", "text-green-400"]
|
||||||
|
const ARQUIVADO = ["border-dashed", "border-[#3a3a3a]", "text-gray-500"]
|
||||||
|
const NEUTRO = ["border-[#2a2a2a]", "text-gray-300"]
|
||||||
|
const TODAS = [...LARANJA, ...VERDE_FECHADO, ...VERDE_PRONTO, ...ARQUIVADO, ...NEUTRO]
|
||||||
|
|
||||||
this.chipVeiculoTargets.forEach(chip => {
|
this.chipVeiculoTargets.forEach(chip => {
|
||||||
const dados = porVeiculo[chip.dataset.vehicle]
|
const dados = porVeiculo[chip.dataset.vehicle]
|
||||||
@@ -237,17 +258,23 @@ export default class extends Controller {
|
|||||||
const feito = chip.querySelector("[data-chip-feito]")
|
const feito = chip.querySelector("[data-chip-feito]")
|
||||||
const total = chip.querySelector("[data-chip-total]")
|
const total = chip.querySelector("[data-chip-total]")
|
||||||
const check = chip.querySelector("[data-chip-check]")
|
const check = chip.querySelector("[data-chip-check]")
|
||||||
|
const lock = chip.querySelector("[data-chip-lock]")
|
||||||
if (feito) feito.textContent = dados.classificadas
|
if (feito) feito.textContent = dados.classificadas
|
||||||
if (total) total.textContent = dados.eligible
|
if (total) total.textContent = dados.eligible
|
||||||
if (check) check.textContent = dados.fechado ? "✓" : ""
|
if (check) check.textContent = dados.completo ? "✓" : ""
|
||||||
|
if (lock) lock.classList.toggle("hidden", !dados.fechado)
|
||||||
|
|
||||||
// Estado de cor determinístico: laranja (selecionado) > verde (fechado) > neutro.
|
// Cor determinística: selecionado(laranja) > consolidado(verde+🔒) >
|
||||||
// Seleção tem prioridade para o usuário sempre enxergar quais carros somam.
|
// pronto(verde) > arquivado(cinza) > neutro.
|
||||||
chip.classList.remove(...VERDE, ...LARANJA, ...NEUTRO)
|
chip.classList.remove(...TODAS)
|
||||||
if (this.veiculosValue.includes(chip.dataset.vehicle)) {
|
if (this.veiculosValue.includes(chip.dataset.vehicle)) {
|
||||||
chip.classList.add(...LARANJA)
|
chip.classList.add(...LARANJA)
|
||||||
} else if (dados.fechado) {
|
} else if (dados.fechado) {
|
||||||
chip.classList.add(...VERDE)
|
chip.classList.add(...VERDE_FECHADO)
|
||||||
|
} else if (dados.completo) {
|
||||||
|
chip.classList.add(...VERDE_PRONTO)
|
||||||
|
} else if (dados.arquivado) {
|
||||||
|
chip.classList.add(...ARQUIVADO)
|
||||||
} else {
|
} else {
|
||||||
chip.classList.add(...NEUTRO)
|
chip.classList.add(...NEUTRO)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ class Consolidacao < ApplicationRecord
|
|||||||
|
|
||||||
has_many :consolidacao_motoristas, dependent: :destroy
|
has_many :consolidacao_motoristas, dependent: :destroy
|
||||||
has_many :consolidacao_entregas, dependent: :destroy
|
has_many :consolidacao_entregas, dependent: :destroy
|
||||||
|
has_many :consolidacao_veiculos, dependent: :destroy
|
||||||
|
|
||||||
|
# Bucket dos lançamentos sem veículo (termo/apontamento manual legado). Mesmo
|
||||||
|
# sentinel do controller e de ConsolidacaoVeiculo.
|
||||||
|
SEM_VEICULO = ConsolidacaoVeiculo::SEM_VEICULO
|
||||||
|
|
||||||
# ── Enums ───────────────────────────────────────────────────
|
# ── Enums ───────────────────────────────────────────────────
|
||||||
enum status: { rascunho: 0, finalizada: 1, arquivada: 2 }
|
enum status: { rascunho: 0, finalizada: 1, arquivada: 2 }
|
||||||
@@ -111,21 +116,34 @@ class Consolidacao < ApplicationRecord
|
|||||||
def finalizar!(user)
|
def finalizar!(user)
|
||||||
return false unless todas_entregas_classificadas?
|
return false unless todas_entregas_classificadas?
|
||||||
|
|
||||||
update!(
|
transaction do
|
||||||
status: :finalizada,
|
# Finalizar a consolidação fecha (consolida) os veículos que ainda estavam
|
||||||
finalizado_por: user.id,
|
# abertos — mantém o estado por veículo coerente com o todo finalizado.
|
||||||
finalizado_em: Time.current
|
consolidacao_veiculos.ativos.abertos.update_all(
|
||||||
)
|
fechado_em: Time.current, fechado_por: user.id, updated_at: Time.current
|
||||||
|
)
|
||||||
|
update!(
|
||||||
|
status: :finalizada,
|
||||||
|
finalizado_por: user.id,
|
||||||
|
finalizado_em: Time.current
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Só está completa quando CADA motorista tem todas as suas entregas elegíveis
|
# Só está completa quando CADA motorista ativo está completo (nada por
|
||||||
# classificadas. (Antes era uma soma agregada com >=, que mascarava motoristas
|
# classificar, descontados os veículos arquivados). Fechar veículo é um passo
|
||||||
# pendentes quando outro tinha entregas a mais.)
|
# independente; ao finalizar, os veículos abertos são consolidados juntos.
|
||||||
def todas_entregas_classificadas?
|
def todas_entregas_classificadas?
|
||||||
consolidacao_motoristas.ativos.all? do |cm|
|
consolidacao_motoristas.ativos.all? { |cm| motorista_completo?(cm.motorista_nome) }
|
||||||
total = entregas_elegiveis_count(cm.motorista_nome)
|
end
|
||||||
total.zero? || classificadas_count(cm.motorista_nome) >= total
|
|
||||||
end
|
# Completude de UM motorista: não sobra entrega elegível por classificar. Os
|
||||||
|
# veículos ARQUIVADOS saem da conta (suas entregas deixam de ser exigidas),
|
||||||
|
# permitindo finalizar deixando um carro problemático de fora. Fechar um veículo
|
||||||
|
# é um passo independente (lock) — a finalização em si fecha os que restarem.
|
||||||
|
def motorista_completo?(motorista)
|
||||||
|
total = entregas_elegiveis_count(motorista) - eligible_veiculos_arquivados(motorista)
|
||||||
|
total.zero? || classificadas_count(motorista) >= total
|
||||||
end
|
end
|
||||||
|
|
||||||
def percentual_classificado
|
def percentual_classificado
|
||||||
@@ -153,6 +171,28 @@ class Consolidacao < ApplicationRecord
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Elegíveis de UM veículo específico do motorista (respeita filtros da
|
||||||
|
# consolidação). O bucket "(sem veículo)" casa as entregas com vehicle NULL/vazio.
|
||||||
|
def entregas_elegiveis_count_veiculo(motorista, vehicle)
|
||||||
|
rel = Entrega.atendidas.da_conta_gade
|
||||||
|
.no_periodo_checkout(data_inicio, data_fim)
|
||||||
|
.do_motorista(motorista)
|
||||||
|
rel = rel.da_veiculo(vehicle_ids) if vehicle_ids.present?
|
||||||
|
rel = rel.da_operacoes(operacoes) if operacoes.present?
|
||||||
|
rel = vehicle == SEM_VEICULO ? rel.where(vehicle: [nil, '']) : rel.where(vehicle: vehicle)
|
||||||
|
rel.count
|
||||||
|
end
|
||||||
|
|
||||||
|
# Total de elegíveis dos veículos ARQUIVADOS do motorista — descontado do gate
|
||||||
|
# para que arquivar um carro problemático libere a finalização. Só toca a base
|
||||||
|
# externa quando existe algum veículo arquivado (mantém os testes isolados).
|
||||||
|
def eligible_veiculos_arquivados(motorista)
|
||||||
|
arquivados = consolidacao_veiculos.arquivados.where(motorista_nome: motorista)
|
||||||
|
return 0 if arquivados.empty?
|
||||||
|
|
||||||
|
arquivados.sum { |cv| entregas_elegiveis_count_veiculo(motorista, cv.vehicle) }
|
||||||
|
end
|
||||||
|
|
||||||
# Rótulos amigáveis das operações vinculadas (para exibição).
|
# Rótulos amigáveis das operações vinculadas (para exibição).
|
||||||
def operacoes_label
|
def operacoes_label
|
||||||
Array(operacoes).map { |t| Operacao.label(t) }
|
Array(operacoes).map { |t| Operacao.label(t) }
|
||||||
@@ -190,6 +230,7 @@ class Consolidacao < ApplicationRecord
|
|||||||
motorista_nome: motorista,
|
motorista_nome: motorista,
|
||||||
valor_aplicado: ConsolidacaoEntrega.valor_para(tipo),
|
valor_aplicado: ConsolidacaoEntrega.valor_para(tipo),
|
||||||
manual: true,
|
manual: true,
|
||||||
|
vehicle: entrega.try(:vehicle).presence, # snapshot do veículo da rastreio
|
||||||
created_by: user&.id
|
created_by: user&.id
|
||||||
)
|
)
|
||||||
ce.save!
|
ce.save!
|
||||||
@@ -208,7 +249,7 @@ class Consolidacao < ApplicationRecord
|
|||||||
# O motorista vem digitado (não de uma Entrega) e é incluído se ainda não
|
# O motorista vem digitado (não de uma Entrega) e é incluído se ainda não
|
||||||
# estiver. tracking_id é sintético (não há Entrega), e a NF/observação ficam
|
# estiver. tracking_id é sintético (não há Entrega), e a NF/observação ficam
|
||||||
# guardadas em nf_manual/obs_manual.
|
# guardadas em nf_manual/obs_manual.
|
||||||
def adicionar_apontamento_manual!(motorista:, tipos:, user:, nf: nil, obs: nil, valor_extra: nil)
|
def adicionar_apontamento_manual!(motorista:, tipos:, user:, nf: nil, obs: nil, valor_extra: nil, vehicle: nil)
|
||||||
tipos_validos = Array(tipos).map(&:to_s).select { |t| ConsolidacaoEntrega.tipos.key?(t) }.uniq
|
tipos_validos = Array(tipos).map(&:to_s).select { |t| ConsolidacaoEntrega.tipos.key?(t) }.uniq
|
||||||
raise ArgumentError, 'Selecione ao menos uma classificação.' if tipos_validos.empty?
|
raise ArgumentError, 'Selecione ao menos uma classificação.' if tipos_validos.empty?
|
||||||
|
|
||||||
@@ -232,6 +273,7 @@ class Consolidacao < ApplicationRecord
|
|||||||
motorista_nome: nome,
|
motorista_nome: nome,
|
||||||
valor_aplicado: valor,
|
valor_aplicado: valor,
|
||||||
manual: true,
|
manual: true,
|
||||||
|
vehicle: vehicle.presence, # veículo escolhido no lançamento (Q3)
|
||||||
nf_manual: nf.presence,
|
nf_manual: nf.presence,
|
||||||
obs_manual: obs.presence,
|
obs_manual: obs.presence,
|
||||||
created_by: user&.id
|
created_by: user&.id
|
||||||
@@ -249,7 +291,7 @@ class Consolidacao < ApplicationRecord
|
|||||||
# para atrelar (só motorista + quantidade, a um preço fixo configurável). Fica
|
# para atrelar (só motorista + quantidade, a um preço fixo configurável). Fica
|
||||||
# numa única linha (manual: true): quantidade guarda o N e valor_aplicado guarda
|
# numa única linha (manual: true): quantidade guarda o N e valor_aplicado guarda
|
||||||
# o total do lote, então recalcular_motorista! e demais somas seguem inalteradas.
|
# o total do lote, então recalcular_motorista! e demais somas seguem inalteradas.
|
||||||
def adicionar_termos!(motorista:, quantidade:, user:)
|
def adicionar_termos!(motorista:, quantidade:, user:, vehicle: nil)
|
||||||
nome = motorista.to_s.strip
|
nome = motorista.to_s.strip
|
||||||
raise ArgumentError, 'Informe o motorista do termo.' if nome.blank?
|
raise ArgumentError, 'Informe o motorista do termo.' if nome.blank?
|
||||||
|
|
||||||
@@ -266,6 +308,7 @@ class Consolidacao < ApplicationRecord
|
|||||||
quantidade: qtd,
|
quantidade: qtd,
|
||||||
valor_aplicado: qtd * ConsolidacaoEntrega.valor_para('termo'), # total do lote
|
valor_aplicado: qtd * ConsolidacaoEntrega.valor_para('termo'), # total do lote
|
||||||
manual: true,
|
manual: true,
|
||||||
|
vehicle: vehicle.presence, # veículo escolhido no lançamento (Q3)
|
||||||
created_by: user&.id
|
created_by: user&.id
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -286,6 +329,27 @@ class Consolidacao < ApplicationRecord
|
|||||||
end
|
end
|
||||||
cm.update_column(:valor_total, total)
|
cm.update_column(:valor_total, total)
|
||||||
update_column(:valor_total, consolidacao_motoristas.ativos.sum(:valor_total))
|
update_column(:valor_total, consolidacao_motoristas.ativos.sum(:valor_total))
|
||||||
|
sincronizar_veiculos!(nome)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Mantém as linhas de consolidacao_veiculos do motorista em dia a partir dos
|
||||||
|
# pilares já classificados (100% DB — não toca a base externa). Cria/atualiza uma
|
||||||
|
# linha por veículo presente nas classificações e remove as que ficaram sem
|
||||||
|
# pilares e ainda não foram fechadas/arquivadas. Veículos elegíveis mas 100% sem
|
||||||
|
# classificar não geram linha aqui — o gate já os pega por classificadas < total.
|
||||||
|
def sincronizar_veiculos!(nome)
|
||||||
|
entregas = consolidacao_entregas.where(motorista_nome: nome)
|
||||||
|
presentes = entregas.pluck(:vehicle).map { |v| v.presence || SEM_VEICULO }.uniq
|
||||||
|
|
||||||
|
presentes.each do |veic|
|
||||||
|
cv = consolidacao_veiculos.find_or_initialize_by(motorista_nome: nome, vehicle: veic)
|
||||||
|
cv.save! # before_save :recalcular_valor atualiza o valor_total do veículo
|
||||||
|
end
|
||||||
|
|
||||||
|
consolidacao_veiculos.where(motorista_nome: nome)
|
||||||
|
.where(fechado_em: nil, arquivado_em: nil)
|
||||||
|
.where.not(vehicle: presentes)
|
||||||
|
.destroy_all
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
71
app/models/consolidacao_veiculo.rb
Normal file
71
app/models/consolidacao_veiculo.rb
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# app/models/consolidacao_veiculo.rb
|
||||||
|
# Unidade de FECHAMENTO por veículo dentro de uma consolidação. Espelha os padrões
|
||||||
|
# de ConsolidacaoMotorista (arquivamento reversível + valor_total recalculado dos
|
||||||
|
# pilares), mas o que ela controla é o estado "fechado/consolidado" de cada carro.
|
||||||
|
# O PAGAMENTO continua no motorista (ConsolidacaoMotorista) — ver [[consolidacao]].
|
||||||
|
class ConsolidacaoVeiculo < ApplicationRecord
|
||||||
|
# Nome da tabela fixado (mesmo motivo dos outros: inflector pt-BR).
|
||||||
|
self.table_name = 'consolidacao_veiculos'
|
||||||
|
|
||||||
|
# Bucket dos lançamentos sem veículo (termo/apontamento manual legado). Igual ao
|
||||||
|
# sentinel usado no controller e na migração — NULL quebraria o índice único.
|
||||||
|
SEM_VEICULO = '(sem veículo)'
|
||||||
|
|
||||||
|
belongs_to :consolidacao
|
||||||
|
belongs_to :fechador, class_name: 'User', foreign_key: :fechado_por, optional: true
|
||||||
|
belongs_to :arquivador, class_name: 'User', foreign_key: :arquivado_por, optional: true
|
||||||
|
|
||||||
|
validates :motorista_nome, presence: true
|
||||||
|
validates :vehicle, presence: true
|
||||||
|
validates :vehicle, uniqueness: { scope: %i[consolidacao_id motorista_nome] }
|
||||||
|
|
||||||
|
before_save :recalcular_valor
|
||||||
|
|
||||||
|
# ── Fechamento (consolidação do veículo, reversível) ────────
|
||||||
|
scope :fechados, -> { where.not(fechado_em: nil) }
|
||||||
|
scope :abertos, -> { where(fechado_em: nil) }
|
||||||
|
|
||||||
|
# ── Arquivamento (reversível) — sai do gate, mantém os dados ─
|
||||||
|
scope :ativos, -> { where(arquivado_em: nil) }
|
||||||
|
scope :arquivados, -> { where.not(arquivado_em: nil) }
|
||||||
|
|
||||||
|
def fechado?
|
||||||
|
fechado_em.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def arquivado?
|
||||||
|
arquivado_em.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
# update_columns: não dispara before_save (o valor já está correto no fechamento).
|
||||||
|
def fechar!(user)
|
||||||
|
update_columns(fechado_em: Time.current, fechado_por: user&.id, updated_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reabrir!(_user)
|
||||||
|
update_columns(fechado_em: nil, fechado_por: nil, updated_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
def arquivar!(user)
|
||||||
|
update_columns(arquivado_em: Time.current, arquivado_por: user&.id, updated_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
def desarquivar!(_user)
|
||||||
|
update_columns(arquivado_em: nil, arquivado_por: nil, updated_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Soma dos pilares deste (motorista + veículo). Mesma regra do ConsolidacaoMotorista:
|
||||||
|
# o valor_aplicado capturado na classificação é a fonte da verdade e o desconto
|
||||||
|
# subtrai. O bucket "(sem veículo)" casa os pilares com vehicle NULL/vazio.
|
||||||
|
def recalcular_valor
|
||||||
|
self.valor_total = escopo_entregas
|
||||||
|
.sum { |e| e.desconto? ? -e.valor_aplicado : e.valor_aplicado }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def escopo_entregas
|
||||||
|
rel = consolidacao.consolidacao_entregas.where(motorista_nome: motorista_nome)
|
||||||
|
vehicle == SEM_VEICULO ? rel.where(vehicle: [nil, '']) : rel.where(vehicle: vehicle)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -46,13 +46,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%# Fechar por veículo — filtra a tela por carro e mostra quais já estão fechados.
|
<%# Fechar por veículo — filtra a tela por carro, mostra o estado (pronto/
|
||||||
|
consolidado/arquivado) e permite CONSOLIDAR o veículo selecionado.
|
||||||
Só aparece quando o motorista usou mais de um veículo no período. %>
|
Só aparece quando o motorista usou mais de um veículo no período. %>
|
||||||
|
<% pode_fechar_veiculo = !(@consolidacao.finalizada? && current_user.operador?) %>
|
||||||
<% if @veiculos_status.size > 1 %>
|
<% if @veiculos_status.size > 1 %>
|
||||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-3 mb-3">
|
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-3 mb-3">
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2 flex-wrap">
|
||||||
<span class="text-gray-400 text-sm font-semibold">🚗 Fechar por veículo:</span>
|
<span class="text-gray-400 text-sm font-semibold">🚗 Fechar por veículo:</span>
|
||||||
<span class="text-gray-600 text-xs">clique para filtrar/somar um ou vários carros (clique de novo p/ tirar)</span>
|
<span class="text-gray-600 text-xs">clique no nome para filtrar; consolide o carro quando estiver 100% (🔒)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<%# Chip "Todos" — limpa a seleção (mostra todos os carros) %>
|
<%# Chip "Todos" — limpa a seleção (mostra todos os carros) %>
|
||||||
@@ -66,18 +68,68 @@
|
|||||||
<% selecionado = @veiculos_sel.include?(v[:vehicle]) %>
|
<% selecionado = @veiculos_sel.include?(v[:vehicle]) %>
|
||||||
<%# Alterna este veículo dentro da seleção atual (multi-seleção) %>
|
<%# Alterna este veículo dentro da seleção atual (multi-seleção) %>
|
||||||
<% nova_sel = selecionado ? (@veiculos_sel - [v[:vehicle]]) : (@veiculos_sel + [v[:vehicle]]) %>
|
<% nova_sel = selecionado ? (@veiculos_sel - [v[:vehicle]]) : (@veiculos_sel + [v[:vehicle]]) %>
|
||||||
|
<%# Cor determinística: selecionado(laranja) > consolidado(verde+🔒) >
|
||||||
|
pronto(verde) > arquivado(cinza tracejado) > neutro. %>
|
||||||
|
<% cor = if selecionado then 'border-orange-500 text-orange-400 bg-orange-500/10'
|
||||||
|
elsif v[:fechado] then 'border-green-500 text-green-400 bg-green-500/10'
|
||||||
|
elsif v[:completo] then 'border-green-600/50 text-green-400'
|
||||||
|
elsif v[:arquivado] then 'border-dashed border-[#3a3a3a] text-gray-500'
|
||||||
|
else 'border-[#2a2a2a] text-gray-300 hover:border-orange-500' end %>
|
||||||
<%= link_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista, vehicle: nova_sel.presence),
|
<%= link_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista, vehicle: nova_sel.presence),
|
||||||
data: { 'validacao-target': 'chipVeiculo', vehicle: v[:vehicle], eligible: v[:eligible] },
|
data: { 'validacao-target': 'chipVeiculo', vehicle: v[:vehicle], eligible: v[:eligible] },
|
||||||
class: "px-3 py-2 rounded-lg text-sm font-semibold border min-h-[40px] flex items-center gap-2 transition-colors
|
class: "px-3 py-2 rounded-lg text-sm font-semibold border min-h-[40px] flex items-center gap-2 transition-colors #{cor}" do %>
|
||||||
#{selecionado ? 'border-orange-500 text-orange-400 bg-orange-500/10' : (v[:fechado] ? 'border-green-500 text-green-400 bg-green-500/10' : 'border-[#2a2a2a] text-gray-300 hover:border-orange-500')}" do %>
|
<span data-chip-lock class="<%= 'hidden' unless v[:fechado] %>">🔒</span>
|
||||||
<span><%= v[:vehicle] %></span>
|
<span><%= v[:vehicle] %></span>
|
||||||
<span class="text-xs <%= v[:fechado] ? 'text-green-400' : 'text-gray-500' %>">
|
<span class="text-xs <%= v[:completo] ? 'text-green-400' : 'text-gray-500' %>">
|
||||||
<span data-chip-feito><%= v[:classificadas] %></span>/<span data-chip-total><%= v[:eligible] %></span>
|
<span data-chip-feito><%= v[:classificadas] %></span>/<span data-chip-total><%= v[:eligible] %></span>
|
||||||
<span data-chip-check><%= '✓' if v[:fechado] %></span>
|
<span data-chip-check><%= '✓' if v[:completo] %></span>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%# Ação de consolidação — opera sobre o veículo único selecionado. %>
|
||||||
|
<% if pode_fechar_veiculo %>
|
||||||
|
<% sel_unico = @veiculos_sel.size == 1 ? @veiculos_sel.first : nil %>
|
||||||
|
<% status_unico = sel_unico && @veiculos_status.find { |v| v[:vehicle] == sel_unico } %>
|
||||||
|
<div class="mt-3 pt-3 border-t border-[#2a2a2a]">
|
||||||
|
<% if status_unico.nil? %>
|
||||||
|
<p class="text-gray-500 text-xs">Selecione <strong>um</strong> veículo (clique no nome) para consolidá-lo separadamente.</p>
|
||||||
|
<% elsif status_unico[:fechado] %>
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<span class="text-green-400 text-sm font-semibold">🔒 <%= sel_unico %> consolidado.</span>
|
||||||
|
<%= button_to '🔓 Reabrir veículo',
|
||||||
|
reabrir_veiculo_consolidacao_consolidacao_entregas_path(@consolidacao),
|
||||||
|
method: :post,
|
||||||
|
params: { motorista: @motorista, vehicle: sel_unico, vehicle_sel: @veiculos_sel },
|
||||||
|
data: { turbo_confirm: "Reabrir o veículo #{sel_unico} para edição?" },
|
||||||
|
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] hover:border-orange-500 px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<%# Habilita só quando o carro está 100% classificado; o JS liga/desliga
|
||||||
|
conforme a barra do escopo selecionado chega (ou não) a 100%. %>
|
||||||
|
<%= button_to '🔒 Consolidar este veículo',
|
||||||
|
fechar_veiculo_consolidacao_consolidacao_entregas_path(@consolidacao),
|
||||||
|
method: :post,
|
||||||
|
params: { motorista: @motorista, vehicle: sel_unico, vehicle_sel: @veiculos_sel },
|
||||||
|
disabled: !status_unico[:completo],
|
||||||
|
data: { 'validacao-target': 'consolidarBtn',
|
||||||
|
turbo_confirm: "Consolidar o veículo #{sel_unico}? Você poderá reabrir depois." },
|
||||||
|
class: "font-bold px-4 py-2 rounded-lg text-xs min-h-[40px] #{status_unico[:completo] ? 'bg-green-600 hover:bg-green-500 text-white cursor-pointer' : 'bg-[#2a2a2a] text-gray-600 cursor-not-allowed'}" %>
|
||||||
|
<%= button_to '🗄️ Arquivar veículo',
|
||||||
|
arquivar_veiculo_consolidacao_consolidacao_entregas_path(@consolidacao),
|
||||||
|
method: :post,
|
||||||
|
params: { motorista: @motorista, vehicle: sel_unico, vehicle_sel: @veiculos_sel },
|
||||||
|
data: { turbo_confirm: "Arquivar o veículo #{sel_unico}? Ele sai da finalização (as entregas já classificadas continuam no pagamento)." },
|
||||||
|
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] hover:border-red-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
|
||||||
|
<span class="text-gray-500 text-xs" data-validacao-target="consolidarHint">
|
||||||
|
<%= status_unico[:completo] ? 'Pronto para consolidar.' : 'Classifique 100% deste veículo para consolidar.' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
@@ -172,6 +224,16 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</datalist>
|
</datalist>
|
||||||
|
|
||||||
|
<%# Veículo (Q3) — a qual carro este lançamento pertence (soma no balde dele) %>
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">Veículo <span class="text-gray-600">(opcional)</span></label>
|
||||||
|
<select data-apontamento-manual-target="veiculo"
|
||||||
|
class="w-full bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-3 text-base focus:border-purple-500 focus:outline-none mb-3">
|
||||||
|
<option value="">— sem veículo —</option>
|
||||||
|
<% @veiculos_status.map { |v| v[:vehicle] }.reject { |x| x == '(sem veículo)' }.each do |veic| %>
|
||||||
|
<option value="<%= veic %>"><%= veic %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
|
||||||
<%# NF + observação (texto livre) %>
|
<%# NF + observação (texto livre) %>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -242,6 +304,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%# Veículo (Q3) — a qual carro estes termos pertencem %>
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">Veículo <span class="text-gray-600">(opcional)</span></label>
|
||||||
|
<select data-apontamento-termo-target="veiculo"
|
||||||
|
class="w-full bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-3 text-base focus:border-blue-500 focus:outline-none mb-4">
|
||||||
|
<option value="">— sem veículo —</option>
|
||||||
|
<% @veiculos_status.map { |v| v[:vehicle] }.reject { |x| x == '(sem veículo)' }.each do |veic| %>
|
||||||
|
<option value="<%= veic %>"><%= veic %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
|
||||||
<%# Stepper de quantidade %>
|
<%# Stepper de quantidade %>
|
||||||
<label class="block text-gray-400 text-sm mb-2">Quantidade de termos</label>
|
<label class="block text-gray-400 text-sm mb-2">Quantidade de termos</label>
|
||||||
<div class="flex items-center justify-center gap-3 mb-5">
|
<div class="flex items-center justify-center gap-3 mb-5">
|
||||||
|
|||||||
@@ -91,6 +91,18 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-orange-500 font-black text-xl"><%= moeda(cm.valor_total) %></p>
|
<p class="text-orange-500 font-black text-xl"><%= moeda(cm.valor_total) %></p>
|
||||||
|
<%# Breakdown por veículo (consolidados 🔒 / abertos). Informativo — o
|
||||||
|
pagamento continua sendo do motorista inteiro. %>
|
||||||
|
<% veiculos = @veiculos_por_motorista[cm.motorista_nome] %>
|
||||||
|
<% if veiculos.present? %>
|
||||||
|
<div class="flex flex-wrap gap-1.5 mt-1">
|
||||||
|
<% veiculos.each do |cv| %>
|
||||||
|
<span class="text-xs px-2 py-0.5 rounded border <%= cv.fechado? ? 'border-green-500/50 text-green-400' : 'border-[#2a2a2a] text-gray-400' %>">
|
||||||
|
<%= cv.fechado? ? '🔒' : '○' %> <%= cv.vehicle %> · <%= moeda(cv.valor_total) %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% if cm.pago? %>
|
<% if cm.pago? %>
|
||||||
<p class="text-green-400 text-xs mt-1">
|
<p class="text-green-400 text-xs mt-1">
|
||||||
💰 Pago em <%= l cm.pago_em, format: :short %>
|
💰 Pago em <%= l cm.pago_em, format: :short %>
|
||||||
|
|||||||
@@ -187,6 +187,13 @@
|
|||||||
· <span class="text-orange-400">➕ <%= m[:apontamentos] %> apontamento<%= 's' if m[:apontamentos] > 1 %></span>
|
· <span class="text-orange-400">➕ <%= m[:apontamentos] %> apontamento<%= 's' if m[:apontamentos] > 1 %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
|
<% if m[:veiculos].any? %>
|
||||||
|
<p class="text-xs mt-0.5">
|
||||||
|
<span class="<%= m[:veiculos_fechados] == m[:veiculos].size ? 'text-green-400' : 'text-gray-400' %>">
|
||||||
|
🚗 <%= m[:veiculos_fechados] %>/<%= m[:veiculos].size %> veículo<%= 's' if m[:veiculos].size > 1 %> consolidado<%= 's' if m[:veiculos_fechados] != 1 %>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right <%= 'pr-10' if pode_arquivar_motorista %>">
|
<div class="text-right <%= 'pr-10' if pode_arquivar_motorista %>">
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ Rails.application.routes.draw do
|
|||||||
post :classificar # toggle individual (JSON)
|
post :classificar # toggle individual (JSON)
|
||||||
post :classificar_em_massa # ações em massa (JSON)
|
post :classificar_em_massa # ações em massa (JSON)
|
||||||
get :revisar # Passo 3 — revisão do motorista
|
get :revisar # Passo 3 — revisão do motorista
|
||||||
|
post :fechar_veiculo # consolida (fecha) um veículo do motorista
|
||||||
|
post :reabrir_veiculo # reabre um veículo já consolidado
|
||||||
|
post :arquivar_veiculo # tira um veículo do gate (reversível)
|
||||||
|
post :desarquivar_veiculo # restaura um veículo arquivado
|
||||||
get :buscar_nf # JSON — busca a NF na SimpleRoute (apontamento manual)
|
get :buscar_nf # JSON — busca a NF na SimpleRoute (apontamento manual)
|
||||||
post :apontar # cria apontamento manual a partir da NF
|
post :apontar # cria apontamento manual a partir da NF
|
||||||
post :apontar_manual # apontamento 100% manual (sem rastreio)
|
post :apontar_manual # apontamento 100% manual (sem rastreio)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Snapshot do veículo em cada pilar da consolidação. O veículo vive só na tabela
|
||||||
|
# externa de rastreio (db_reem_simplerout_2026), acessível via tracking_id. Guardar
|
||||||
|
# uma cópia aqui permite fechar/consolidar POR VEÍCULO sem N+1 e mantém o histórico
|
||||||
|
# estável mesmo que a base externa mude depois. Lançamentos sintéticos (apontamento
|
||||||
|
# 100% manual "MAN-*" e entrega de termo "TERMO-*") não têm veículo na rastreio e
|
||||||
|
# ficam NULL — o agrupamento trata NULL como "(sem veículo)".
|
||||||
|
class AddVehicleToConsolidacaoEntregas < ActiveRecord::Migration[7.1]
|
||||||
|
def up
|
||||||
|
unless column_exists?(:consolidacao_entregas, :vehicle)
|
||||||
|
add_column :consolidacao_entregas, :vehicle, :string
|
||||||
|
add_index :consolidacao_entregas, %i[consolidacao_id motorista_nome vehicle],
|
||||||
|
name: 'idx_consolidacao_entrega_veiculo'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Backfill dos lançamentos já existentes a partir da rastreio. Só roda onde a
|
||||||
|
# tabela externa existe (dev/prod) — no banco de teste ela é isolada, então
|
||||||
|
# pulamos para não quebrar o db:migrate.
|
||||||
|
return unless connection.table_exists?('db_reem_simplerout_2026')
|
||||||
|
|
||||||
|
say_with_time 'backfill vehicle em consolidacao_entregas' do
|
||||||
|
execute(<<~SQL.squish)
|
||||||
|
UPDATE consolidacao_entregas ce
|
||||||
|
SET vehicle = e.vehicle
|
||||||
|
FROM db_reem_simplerout_2026 e
|
||||||
|
WHERE e.tracking_id = ce.tracking_id
|
||||||
|
AND ce.vehicle IS NULL
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
if index_exists?(:consolidacao_entregas, %i[consolidacao_id motorista_nome vehicle],
|
||||||
|
name: 'idx_consolidacao_entrega_veiculo')
|
||||||
|
remove_index :consolidacao_entregas, name: 'idx_consolidacao_entrega_veiculo'
|
||||||
|
end
|
||||||
|
remove_column :consolidacao_entregas, :vehicle if column_exists?(:consolidacao_entregas, :vehicle)
|
||||||
|
end
|
||||||
|
end
|
||||||
61
db/migrate/20260703000002_create_consolidacao_veiculos.rb
Normal file
61
db/migrate/20260703000002_create_consolidacao_veiculos.rb
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Unidade de FECHAMENTO por veículo dentro de uma consolidação. Espelha o padrão de
|
||||||
|
# consolidacao_motoristas (arquivamento reversível + valor_total), mas o que ela
|
||||||
|
# controla é o estado "fechado/consolidado" de cada carro — o PAGAMENTO continua no
|
||||||
|
# motorista (consolidacao_motoristas). Assim dá para consolidar um veículo pronto
|
||||||
|
# sem esperar os outros. O bucket sem veículo (termo/apontamento manual legado) usa
|
||||||
|
# o sentinel '(sem veículo)' para que o índice único funcione (NULL quebraria).
|
||||||
|
class CreateConsolidacaoVeiculos < ActiveRecord::Migration[7.1]
|
||||||
|
SEM_VEICULO = '(sem veículo)'
|
||||||
|
|
||||||
|
def up
|
||||||
|
unless table_exists?(:consolidacao_veiculos)
|
||||||
|
create_table :consolidacao_veiculos do |t|
|
||||||
|
t.references :consolidacao, null: false, foreign_key: { to_table: :consolidacoes }
|
||||||
|
t.string :motorista_nome, null: false
|
||||||
|
t.string :vehicle, null: false
|
||||||
|
t.decimal :valor_total, precision: 10, scale: 2, default: 0
|
||||||
|
|
||||||
|
t.datetime :fechado_em # NULL = aberto (ainda não consolidado)
|
||||||
|
t.integer :fechado_por
|
||||||
|
t.datetime :arquivado_em # NULL = ativo
|
||||||
|
t.integer :arquivado_por
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :consolidacao_veiculos, %i[consolidacao_id motorista_nome vehicle],
|
||||||
|
unique: true, name: 'idx_consolidacao_veiculo_unique'
|
||||||
|
add_index :consolidacao_veiculos, :arquivado_em
|
||||||
|
end
|
||||||
|
|
||||||
|
# Backfill: uma linha por (consolidação · motorista · veículo) já classificado,
|
||||||
|
# somando o valor (desconto = tipo 3 subtrai). Consolidações já finalizadas
|
||||||
|
# (status 1) nascem FECHADAS, herdando quem/quando finalizou — para não
|
||||||
|
# "reabrir" o que já foi pago.
|
||||||
|
say_with_time 'backfill consolidacao_veiculos a partir das entregas' do
|
||||||
|
execute(<<~SQL.squish)
|
||||||
|
INSERT INTO consolidacao_veiculos
|
||||||
|
(consolidacao_id, motorista_nome, vehicle, valor_total,
|
||||||
|
fechado_em, fechado_por, created_at, updated_at)
|
||||||
|
SELECT ce.consolidacao_id,
|
||||||
|
ce.motorista_nome,
|
||||||
|
COALESCE(NULLIF(ce.vehicle, ''), '#{SEM_VEICULO}') AS vehicle,
|
||||||
|
COALESCE(SUM(CASE WHEN ce.tipo = 3
|
||||||
|
THEN -ce.valor_aplicado
|
||||||
|
ELSE ce.valor_aplicado END), 0) AS valor_total,
|
||||||
|
CASE WHEN c.status = 1 THEN c.finalizado_em END AS fechado_em,
|
||||||
|
CASE WHEN c.status = 1 THEN c.finalizado_por END AS fechado_por,
|
||||||
|
NOW(), NOW()
|
||||||
|
FROM consolidacao_entregas ce
|
||||||
|
JOIN consolidacoes c ON c.id = ce.consolidacao_id
|
||||||
|
GROUP BY ce.consolidacao_id, ce.motorista_nome,
|
||||||
|
COALESCE(NULLIF(ce.vehicle, ''), '#{SEM_VEICULO}'),
|
||||||
|
c.status, c.finalizado_em, c.finalizado_por
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_table :consolidacao_veiculos if table_exists?(:consolidacao_veiculos)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -39,6 +39,12 @@ FactoryBot.define do
|
|||||||
sequence(:motorista_nome) { |n| "Motorista #{n}" }
|
sequence(:motorista_nome) { |n| "Motorista #{n}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
factory :consolidacao_veiculo do
|
||||||
|
association :consolidacao
|
||||||
|
sequence(:motorista_nome) { |n| "Motorista #{n}" }
|
||||||
|
sequence(:vehicle) { |n| "GADE_#{format('%03d', n)}" }
|
||||||
|
end
|
||||||
|
|
||||||
factory :consolidacao_entrega do
|
factory :consolidacao_entrega do
|
||||||
association :consolidacao
|
association :consolidacao
|
||||||
sequence(:tracking_id) { |n| "TRK-#{n}" }
|
sequence(:tracking_id) { |n| "TRK-#{n}" }
|
||||||
|
|||||||
@@ -232,4 +232,85 @@ RSpec.describe Consolidacao, type: :model do
|
|||||||
expect(Consolidacao.arquivadas).to include(arquivada)
|
expect(Consolidacao.arquivadas).to include(arquivada)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#sincronizar_veiculos! (via recalcular_motorista!)' do
|
||||||
|
let(:consolidacao) { create(:consolidacao) }
|
||||||
|
|
||||||
|
before { create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'A') }
|
||||||
|
|
||||||
|
it 'cria uma linha por veículo classificado, somando o valor do veículo' do
|
||||||
|
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
|
||||||
|
vehicle: 'GADE_1', tipo: :entrega_normal, valor_aplicado: 15)
|
||||||
|
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
|
||||||
|
vehicle: 'GADE_1', tipo: :bonus, valor_aplicado: 10)
|
||||||
|
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
|
||||||
|
vehicle: 'GADE_2', tipo: :entrega_normal, valor_aplicado: 20)
|
||||||
|
|
||||||
|
consolidacao.recalcular_motorista!('A')
|
||||||
|
|
||||||
|
veics = consolidacao.consolidacao_veiculos.where(motorista_nome: 'A').order(:vehicle)
|
||||||
|
expect(veics.map(&:vehicle)).to eq(%w[GADE_1 GADE_2])
|
||||||
|
expect(veics.find_by(vehicle: 'GADE_1').valor_total).to eq(25)
|
||||||
|
expect(veics.find_by(vehicle: 'GADE_2').valor_total).to eq(20)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'remove o veículo aberto que ficou sem pilares, mas preserva o fechado' do
|
||||||
|
e1 = create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
|
||||||
|
vehicle: 'GADE_1', valor_aplicado: 15)
|
||||||
|
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
|
||||||
|
vehicle: 'GADE_2', valor_aplicado: 20)
|
||||||
|
consolidacao.recalcular_motorista!('A')
|
||||||
|
consolidacao.consolidacao_veiculos.find_by(vehicle: 'GADE_2').fechar!(create(:gerente))
|
||||||
|
|
||||||
|
e1.destroy
|
||||||
|
consolidacao.recalcular_motorista!('A')
|
||||||
|
|
||||||
|
veics = consolidacao.consolidacao_veiculos.where(motorista_nome: 'A')
|
||||||
|
expect(veics.pluck(:vehicle)).to contain_exactly('GADE_2') # GADE_1 (aberto e vazio) removido
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'gate: veículo arquivado libera a finalização' do
|
||||||
|
let(:consolidacao) { create(:consolidacao) }
|
||||||
|
|
||||||
|
before { create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'A') }
|
||||||
|
|
||||||
|
it 'desconta as elegíveis do veículo arquivado' do
|
||||||
|
allow(consolidacao).to receive(:entregas_elegiveis_count).with('A').and_return(3)
|
||||||
|
allow(consolidacao).to receive(:entregas_elegiveis_count_veiculo).with('A', 'GADE_2').and_return(1)
|
||||||
|
|
||||||
|
# 2 classificadas (GADE_1). Falta 1 (GADE_2) => incompleto.
|
||||||
|
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
|
||||||
|
vehicle: 'GADE_1', tracking_id: 'T1')
|
||||||
|
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
|
||||||
|
vehicle: 'GADE_1', tracking_id: 'T2', tipo: :bonus)
|
||||||
|
expect(consolidacao.motorista_completo?('A')).to be false
|
||||||
|
|
||||||
|
# Arquiva GADE_2 (o carro sem classificar): total cai p/ 2 => completo.
|
||||||
|
cv = consolidacao.consolidacao_veiculos.create!(motorista_nome: 'A', vehicle: 'GADE_2')
|
||||||
|
cv.arquivar!(create(:gerente))
|
||||||
|
expect(consolidacao.motorista_completo?('A')).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#finalizar! fecha os veículos ainda abertos' do
|
||||||
|
let(:consolidacao) { create(:consolidacao) }
|
||||||
|
let(:gerente) { create(:gerente) }
|
||||||
|
|
||||||
|
before { create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'A') }
|
||||||
|
|
||||||
|
it 'consolida os veículos abertos ao finalizar' do
|
||||||
|
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
|
||||||
|
vehicle: 'GADE_1', valor_aplicado: 15)
|
||||||
|
consolidacao.recalcular_motorista!('A')
|
||||||
|
cv = consolidacao.consolidacao_veiculos.find_by(vehicle: 'GADE_1')
|
||||||
|
expect(cv).not_to be_fechado
|
||||||
|
|
||||||
|
allow(consolidacao).to receive(:todas_entregas_classificadas?).and_return(true)
|
||||||
|
consolidacao.finalizar!(gerente)
|
||||||
|
|
||||||
|
expect(cv.reload).to be_fechado
|
||||||
|
expect(cv.fechado_por).to eq(gerente.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
74
spec/models/consolidacao_veiculo_spec.rb
Normal file
74
spec/models/consolidacao_veiculo_spec.rb
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ConsolidacaoVeiculo, type: :model do
|
||||||
|
let(:consolidacao) { create(:consolidacao) }
|
||||||
|
|
||||||
|
describe 'validações' do
|
||||||
|
it 'exige motorista_nome e vehicle' do
|
||||||
|
expect(build(:consolidacao_veiculo, motorista_nome: nil)).not_to be_valid
|
||||||
|
expect(build(:consolidacao_veiculo, vehicle: nil)).not_to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'não permite o mesmo veículo duas vezes para o mesmo motorista' do
|
||||||
|
create(:consolidacao_veiculo, consolidacao: consolidacao, motorista_nome: 'A', vehicle: 'GADE_1')
|
||||||
|
dup = build(:consolidacao_veiculo, consolidacao: consolidacao, motorista_nome: 'A', vehicle: 'GADE_1')
|
||||||
|
expect(dup).not_to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#recalcular_valor (before_save)' do
|
||||||
|
before do
|
||||||
|
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'Joao',
|
||||||
|
vehicle: 'GADE_1', tipo: :entrega_normal, valor_aplicado: 15)
|
||||||
|
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'Joao',
|
||||||
|
vehicle: 'GADE_1', tipo: :desconto, valor_aplicado: 5)
|
||||||
|
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'Joao',
|
||||||
|
vehicle: 'GADE_2', tipo: :entrega_normal, valor_aplicado: 99)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'soma só os pilares do veículo e trata desconto como negativo' do
|
||||||
|
cv = create(:consolidacao_veiculo, consolidacao: consolidacao, motorista_nome: 'Joao', vehicle: 'GADE_1')
|
||||||
|
expect(cv.valor_total).to eq(10) # 15 - 5 (GADE_2 fica de fora)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'o bucket "(sem veículo)" casa pilares com vehicle NULL/vazio' do
|
||||||
|
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'Joao',
|
||||||
|
vehicle: nil, tipo: :bonus, valor_aplicado: 7)
|
||||||
|
cv = create(:consolidacao_veiculo, consolidacao: consolidacao, motorista_nome: 'Joao',
|
||||||
|
vehicle: ConsolidacaoVeiculo::SEM_VEICULO)
|
||||||
|
expect(cv.valor_total).to eq(7)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'fechamento e arquivamento' do
|
||||||
|
let(:gerente) { create(:gerente) }
|
||||||
|
|
||||||
|
it 'fecha e reabre registrando quem/quando' do
|
||||||
|
cv = create(:consolidacao_veiculo, consolidacao: consolidacao, motorista_nome: 'A', vehicle: 'GADE_1')
|
||||||
|
expect(cv).not_to be_fechado
|
||||||
|
|
||||||
|
cv.fechar!(gerente)
|
||||||
|
cv.reload
|
||||||
|
expect(cv).to be_fechado
|
||||||
|
expect(cv.fechado_por).to eq(gerente.id)
|
||||||
|
expect(cv.fechado_em).to be_present
|
||||||
|
|
||||||
|
cv.reabrir!(gerente)
|
||||||
|
expect(cv.reload).not_to be_fechado
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'arquiva e desarquiva, e os escopos separam os estados' do
|
||||||
|
aberto = create(:consolidacao_veiculo, consolidacao: consolidacao, motorista_nome: 'A', vehicle: 'GADE_1')
|
||||||
|
fechado = create(:consolidacao_veiculo, consolidacao: consolidacao, motorista_nome: 'B', vehicle: 'GADE_2')
|
||||||
|
arquivado = create(:consolidacao_veiculo, consolidacao: consolidacao, motorista_nome: 'C', vehicle: 'GADE_3')
|
||||||
|
fechado.fechar!(gerente)
|
||||||
|
arquivado.arquivar!(gerente)
|
||||||
|
|
||||||
|
expect(ConsolidacaoVeiculo.fechados).to include(fechado)
|
||||||
|
expect(ConsolidacaoVeiculo.fechados).not_to include(aberto)
|
||||||
|
expect(ConsolidacaoVeiculo.abertos).to include(aberto)
|
||||||
|
expect(ConsolidacaoVeiculo.ativos).not_to include(arquivado)
|
||||||
|
expect(ConsolidacaoVeiculo.arquivados).to contain_exactly(arquivado)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user