Fases 4, 5 e 6 — Job 1h + Auditoria + Consolidação + Wizard de validação
This commit is contained in:
145
app/javascript/controllers/validacao_controller.js
Normal file
145
app/javascript/controllers/validacao_controller.js
Normal file
@@ -0,0 +1,145 @@
|
||||
// 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", "toggles", "barra", "percentual",
|
||||
"contadorClassificadas", "contadorTotal",
|
||||
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "valorTotal"
|
||||
]
|
||||
|
||||
static values = {
|
||||
url: String, // POST classificar
|
||||
massaUrl: String, // POST classificar_em_massa
|
||||
motorista: String,
|
||||
total: Number
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
btn.disabled = true
|
||||
try {
|
||||
const resp = await this.post(this.urlValue, {
|
||||
tracking_id: tracking,
|
||||
tipo: tipo,
|
||||
motorista: this.motoristaValue
|
||||
})
|
||||
this.marcarToggleAtivo(tracking, tipo)
|
||||
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
|
||||
if (!confirm(`Marcar TODAS as entregas como ${this.labelTipo(tipo)}?`)) return
|
||||
|
||||
const resp = await this.post(this.massaUrlValue, {
|
||||
tipo: tipo,
|
||||
motorista: this.motoristaValue,
|
||||
tracking_ids: [] // vazio = todas
|
||||
})
|
||||
this.linhaTargets.forEach(l => this.marcarToggleAtivo(l.dataset.tracking, tipo))
|
||||
this.atualizarResumo(resp)
|
||||
}
|
||||
|
||||
// ── 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 resp = await this.post(this.massaUrlValue, {
|
||||
tipo: tipo,
|
||||
motorista: this.motoristaValue,
|
||||
tracking_ids: ids
|
||||
})
|
||||
ids.forEach(id => this.marcarToggleAtivo(id, tipo))
|
||||
this.checkboxTargets.forEach(c => c.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()
|
||||
}
|
||||
|
||||
marcarToggleAtivo(tracking, tipo) {
|
||||
const grupo = this.togglesTargets.find(t => t.dataset.tracking === tracking)
|
||||
if (!grupo) return
|
||||
grupo.querySelectorAll(".toggle-btn").forEach(b => {
|
||||
const ativo = b.dataset.tipo === tipo
|
||||
b.classList.toggle("ring-2", ativo)
|
||||
b.classList.toggle("ring-green-400", ativo)
|
||||
b.classList.toggle("scale-105", ativo)
|
||||
b.classList.toggle("opacity-50", !ativo)
|
||||
})
|
||||
const linha = this.linhaTargets.find(l => l.dataset.tracking === tracking)
|
||||
if (linha) linha.dataset.classificada = "1"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
const valor = dados.valor_total || 0
|
||||
this.valorTotalTarget.textContent =
|
||||
"R$ " + valor.toFixed(2).replace(".", ",")
|
||||
|
||||
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" }[tipo] || tipo
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user