Files
Reem-Notas/app/javascript/controllers/validacao_controller.js
2026-06-22 19:24:30 -03:00

219 lines
8.0 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", "modoRemover", "rotuloAcao",
"toggles", "barra", "percentual",
"contadorClassificadas", "contadorTotal",
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria", "valorTotal"
]
static values = {
url: String, // POST classificar
massaUrl: String, // POST classificar_em_massa
motorista: String,
total: Number,
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,
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
}
}
// ── Marcar todos como Normal ───────────────────────────────
async marcarTodos(event) {
const tipo = event.currentTarget.dataset.tipo
const remover = this.modoRemover
const verbo = remover ? "REMOVER de TODAS as entregas" : "Marcar TODAS as entregas como"
if (!confirm(`${verbo} ${this.labelTipo(tipo)}?`)) return
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,
tracking_ids: [], // vazio = todas
acao: remover ? "remover" : "adicionar",
valor: valor
})
this.linhaTargets.forEach(l => this.setToggle(l.dataset.tracking, tipo, !remover))
this.atualizarResumo(resp)
}
// ── Selecionar / desmarcar todos os checkboxes ─────────────
selecionarTodos(event) {
const marcar = event.currentTarget.checked
this.checkboxTargets.forEach(c => { c.checked = marcar })
}
// ── Modo das ações em massa: adicionar (padrão) ou remover ──
get modoRemover() {
return this.hasModoRemoverTarget && this.modoRemoverTarget.checked
}
alternarModo() {
if (this.hasRotuloAcaoTarget) {
this.rotuloAcaoTarget.textContent =
this.modoRemover ? "Remover dos selecionados:" : "Selecionados como:"
}
}
// ── Marcar selecionados como X ─────────────────────────────
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
}
const remover = this.modoRemover
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,
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
const valor = dados.valor_total || 0
this.valorTotalTarget.textContent =
"R$ " + valor.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
const classificadas = 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.barraTarget.style.width = `${Math.min(pct, 100)}%`
this.percentualTarget.textContent = `${Math.min(pct, 100)}%`
if (pct >= 100) {
this.barraTarget.classList.remove("bg-orange-500")
this.barraTarget.classList.add("bg-green-500")
}
}
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
}
}