248 lines
9.9 KiB
JavaScript
248 lines
9.9 KiB
JavaScript
// app/javascript/controllers/validacao_controller.js
|
|
// Stimulus controller — Wizard Passo 2 (validação de entregas)
|
|
// Atualização em tempo real: toggles, ações em massa, barra de progresso, resumo lateral.
|
|
|
|
import { Controller } from "@hotwired/stimulus"
|
|
|
|
export default class extends Controller {
|
|
static targets = [
|
|
"linha", "checkbox", "selecionarTodos",
|
|
"toggles", "barra", "barraManuais", "percentual", "chipVeiculo",
|
|
"contadorClassificadas", "contadorTotal", "contadorManuais", "manuaisInfo",
|
|
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria",
|
|
"qtdTermo", "qtdManuais", "valorTotal"
|
|
]
|
|
|
|
static values = {
|
|
url: String, // POST classificar
|
|
massaUrl: String, // POST classificar_em_massa
|
|
motorista: String,
|
|
total: Number,
|
|
veiculos: Array, // veículo(s) selecionado(s) — filtro multi-seleção
|
|
precoExtraordinaria: Number // preço padrão sugerido no prompt
|
|
}
|
|
|
|
connect() {
|
|
// Carrega estado inicial renderizado pelo servidor
|
|
const el = document.getElementById("validacao-estado-inicial")
|
|
if (el) {
|
|
const estado = JSON.parse(el.textContent)
|
|
this.atualizarResumo(estado)
|
|
}
|
|
}
|
|
|
|
// ── Toggle individual ──────────────────────────────────────
|
|
async classificar(event) {
|
|
const btn = event.currentTarget
|
|
const tracking = btn.dataset.tracking
|
|
const tipo = btn.dataset.tipo
|
|
|
|
// Pilar extraordinária: ao ATIVAR (botão ainda sem ring-2), pergunta o valor.
|
|
const ativando = !btn.classList.contains("ring-2")
|
|
let valor = null
|
|
if (tipo === "extraordinaria" && ativando) {
|
|
valor = this.pedirValor()
|
|
if (valor === null) return // admin cancelou
|
|
}
|
|
|
|
btn.disabled = true
|
|
try {
|
|
const resp = await this.post(this.urlValue, {
|
|
tracking_id: tracking,
|
|
tipo: tipo,
|
|
motorista: this.motoristaValue,
|
|
vehicle: this.veiculosValue,
|
|
valor: valor
|
|
})
|
|
// resp.ativo indica se o pilar ficou ligado ou desligado (toggle)
|
|
this.setToggle(tracking, tipo, resp.ativo)
|
|
this.atualizarResumo(resp)
|
|
} catch (e) {
|
|
alert("Erro ao classificar entrega. Tente novamente.")
|
|
} finally {
|
|
btn.disabled = false
|
|
}
|
|
}
|
|
|
|
// ── Selecionar / desmarcar todos os checkboxes ─────────────
|
|
selecionarTodos(event) {
|
|
const marcar = event.currentTarget.checked
|
|
this.checkboxTargets.forEach(c => { c.checked = marcar })
|
|
}
|
|
|
|
// O pilar está ativo (ring-2) na entrega indicada?
|
|
pilarAtivo(tracking, tipo) {
|
|
const grupo = this.togglesTargets.find(t => t.dataset.tracking === tracking)
|
|
if (!grupo) return false
|
|
const btn = grupo.querySelector(`.toggle-btn[data-tipo="${tipo}"]`)
|
|
return !!btn && btn.classList.contains("ring-2")
|
|
}
|
|
|
|
// ── Aplicar/remover o pilar nos selecionados (toggle em massa) ──
|
|
async marcarSelecionados(event) {
|
|
const tipo = event.currentTarget.dataset.tipo
|
|
const ids = this.checkboxTargets.filter(c => c.checked).map(c => c.value)
|
|
|
|
if (ids.length === 0) {
|
|
alert("Selecione ao menos uma entrega (checkbox à esquerda).")
|
|
return
|
|
}
|
|
|
|
// Toggle em massa: se TODAS as selecionadas já têm o pilar, remove de todas;
|
|
// caso contrário, aplica em todas.
|
|
const remover = ids.every(id => this.pilarAtivo(id, tipo))
|
|
|
|
let valor = null
|
|
if (tipo === "extraordinaria" && !remover) {
|
|
valor = this.pedirValor()
|
|
if (valor === null) return
|
|
}
|
|
|
|
const resp = await this.post(this.massaUrlValue, {
|
|
tipo: tipo,
|
|
motorista: this.motoristaValue,
|
|
vehicle: this.veiculosValue,
|
|
tracking_ids: ids,
|
|
acao: remover ? "remover" : "adicionar",
|
|
valor: valor
|
|
})
|
|
ids.forEach(id => this.setToggle(id, tipo, !remover))
|
|
this.checkboxTargets.forEach(c => c.checked = false)
|
|
if (this.hasSelecionarTodosTarget) this.selecionarTodosTarget.checked = false
|
|
this.atualizarResumo(resp)
|
|
}
|
|
|
|
// ── Helpers ────────────────────────────────────────────────
|
|
|
|
async post(url, body) {
|
|
const resp = await fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
|
|
},
|
|
body: JSON.stringify(body)
|
|
})
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
|
return resp.json()
|
|
}
|
|
|
|
// Liga/desliga UM pilar (os demais pilares da entrega ficam como estão)
|
|
setToggle(tracking, tipo, ativo) {
|
|
const grupo = this.togglesTargets.find(t => t.dataset.tracking === tracking)
|
|
if (!grupo) return
|
|
|
|
const btn = grupo.querySelector(`.toggle-btn[data-tipo="${tipo}"]`)
|
|
if (btn) {
|
|
btn.classList.toggle("ring-2", ativo)
|
|
btn.classList.toggle("ring-green-400", ativo)
|
|
btn.classList.toggle("scale-105", ativo)
|
|
btn.classList.toggle("opacity-50", !ativo)
|
|
}
|
|
|
|
// a linha está "classificada" se tiver qualquer pilar ligado
|
|
const algumAtivo = grupo.querySelectorAll(".toggle-btn.ring-2").length > 0
|
|
const linha = this.linhaTargets.find(l => l.dataset.tracking === tracking)
|
|
if (linha) linha.dataset.classificada = algumAtivo ? "1" : "0"
|
|
}
|
|
|
|
atualizarResumo(dados) {
|
|
const porTipo = dados.por_tipo || {}
|
|
this.qtdNormalTarget.textContent = porTipo["entrega_normal"] || 0
|
|
this.qtdRetiradaTarget.textContent = porTipo["retirada"] || 0
|
|
this.qtdBonusTarget.textContent = porTipo["bonus"] || 0
|
|
this.qtdDescontoTarget.textContent = porTipo["desconto"] || 0
|
|
if (this.hasQtdExtraordinariaTarget)
|
|
this.qtdExtraordinariaTarget.textContent = porTipo["extraordinaria"] || 0
|
|
if (this.hasQtdTermoTarget)
|
|
this.qtdTermoTarget.textContent = porTipo["termo"] || 0
|
|
|
|
const valor = dados.valor_total || 0
|
|
this.valorTotalTarget.textContent =
|
|
"R$ " + valor.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
|
|
// Lançamentos manuais do motorista (apontamentos + termo): feedback visual.
|
|
// Ficam fora do denominador de elegíveis (o gate de finalização não muda).
|
|
const manuais = dados.manuais || 0
|
|
if (this.hasQtdManuaisTarget) this.qtdManuaisTarget.textContent = manuais
|
|
if (this.hasContadorManuaisTarget) this.contadorManuaisTarget.textContent = manuais
|
|
if (this.hasManuaisInfoTarget) this.manuaisInfoTarget.classList.toggle("hidden", manuais <= 0)
|
|
|
|
// Barra/contador refletem o ESCOPO atual (veículo selecionado, se houver).
|
|
const classificadas = (dados.classificadas_escopo ?? dados.classificadas) || 0
|
|
this.contadorClassificadasTarget.textContent = classificadas
|
|
|
|
const total = this.totalValue || parseInt(this.contadorTotalTarget.textContent)
|
|
const pct = total > 0 ? Math.round((classificadas / total) * 100) : 0
|
|
this.percentualTarget.textContent = `${Math.min(pct, 100)}%`
|
|
|
|
// Barra com 2 segmentos, proporcionais ao total geral (elegíveis + manuais):
|
|
// laranja/verde = elegíveis classificadas · azul = lançamentos manuais.
|
|
const denom = total + manuais
|
|
this.barraTarget.style.width = denom > 0 ? `${(classificadas / denom) * 100}%` : "0%"
|
|
if (this.hasBarraManuaisTarget)
|
|
this.barraManuaisTarget.style.width = denom > 0 ? `${(manuais / denom) * 100}%` : "0%"
|
|
|
|
// Cor do segmento elegível: verde quando todas as elegíveis estão classificadas.
|
|
this.barraTarget.classList.toggle("bg-green-500", pct >= 100)
|
|
this.barraTarget.classList.toggle("bg-orange-500", pct < 100)
|
|
|
|
if (dados.veiculos_status) this.atualizarChips(dados.veiculos_status)
|
|
}
|
|
|
|
// Atualiza os chips "fechar por veículo" (contador + estado fechado) a partir
|
|
// do status devolvido pelo servidor.
|
|
atualizarChips(status) {
|
|
if (!this.hasChipVeiculoTarget) return
|
|
const porVeiculo = {}
|
|
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 NEUTRO = ["border-[#2a2a2a]", "text-gray-300"]
|
|
|
|
this.chipVeiculoTargets.forEach(chip => {
|
|
const dados = porVeiculo[chip.dataset.vehicle]
|
|
if (!dados) return
|
|
const feito = chip.querySelector("[data-chip-feito]")
|
|
const total = chip.querySelector("[data-chip-total]")
|
|
const check = chip.querySelector("[data-chip-check]")
|
|
if (feito) feito.textContent = dados.classificadas
|
|
if (total) total.textContent = dados.eligible
|
|
if (check) check.textContent = dados.fechado ? "✓" : ""
|
|
|
|
// Estado de cor determinístico: laranja (selecionado) > verde (fechado) > neutro.
|
|
// Seleção tem prioridade para o usuário sempre enxergar quais carros somam.
|
|
chip.classList.remove(...VERDE, ...LARANJA, ...NEUTRO)
|
|
if (this.veiculosValue.includes(chip.dataset.vehicle)) {
|
|
chip.classList.add(...LARANJA)
|
|
} else if (dados.fechado) {
|
|
chip.classList.add(...VERDE)
|
|
} else {
|
|
chip.classList.add(...NEUTRO)
|
|
}
|
|
})
|
|
}
|
|
|
|
labelTipo(tipo) {
|
|
return { entrega_normal: "Normal", retirada: "Retirada",
|
|
bonus: "Bônus", desconto: "Desconto",
|
|
extraordinaria: "Extraordinária" }[tipo] || tipo
|
|
}
|
|
|
|
// Pergunta o valor da entrega extraordinária (pré-preenchido com o padrão).
|
|
// Retorna o número (>= 0) ou null se o admin cancelar.
|
|
pedirValor() {
|
|
const padrao = (this.precoExtraordinariaValue || 0).toFixed(2).replace(".", ",")
|
|
const entrada = prompt("Valor desta entrega extraordinária (R$):", padrao)
|
|
if (entrada === null) return null
|
|
const num = parseFloat(entrada.replace(",", "."))
|
|
if (isNaN(num) || num < 0) {
|
|
alert("Valor inválido. Informe um número maior ou igual a zero.")
|
|
return null
|
|
}
|
|
return num
|
|
}
|
|
}
|