Correção de horario e implantação da geração de Romaneio
This commit is contained in:
163
app/javascript/controllers/romaneio_controller.js
Normal file
163
app/javascript/controllers/romaneio_controller.js
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
// Editor do romaneio: salva cada campo assim que ele muda e reatualiza a prévia
|
||||
// (que é o PDF de verdade, num iframe).
|
||||
//
|
||||
// Por que salvar POR CAMPO e não com um submit de formulário: `campos_editados`
|
||||
// precisa registrar exatamente o campo que um humano tocou. Um submit em bloco não
|
||||
// distingue "redigitei o mesmo valor" de "nunca olhei" — marcaria os 5 campos das 29
|
||||
// linhas como editados, e a reimportação seguinte preservaria endereços velhos no
|
||||
// plano inteiro. A regra de reconciliação (Romaneios::Importador) só funciona com
|
||||
// escrita por campo.
|
||||
export default class extends Controller {
|
||||
static targets = ["celula", "preview", "veiculo", "motorista", "rotulo", "status", "linha"]
|
||||
static values = { url: String, pdfUrl: String, veiculo: String }
|
||||
|
||||
// Recarregar o PDF a cada tecla travaria a tela; 600ms depois do último save é o
|
||||
// suficiente para parecer imediato sem regerar o documento à toa.
|
||||
static ATRASO_PREVIEW = 600
|
||||
|
||||
connect() {
|
||||
this.timerPreview = null
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.timerPreview) clearTimeout(this.timerPreview)
|
||||
}
|
||||
|
||||
// ── Edição ──────────────────────────────────────────────────────────────
|
||||
salvarCelula(event) {
|
||||
const input = event.currentTarget
|
||||
// Sair do campo sem mudar nada não é edição — marcar como editado aqui
|
||||
// congelaria o valor contra a próxima reimportação sem o operador querer.
|
||||
if (input.value === input.dataset.original) return
|
||||
|
||||
this.gravar(input.dataset.linhaId, input.dataset.campo, input.value, input)
|
||||
}
|
||||
|
||||
restaurar(event) {
|
||||
const botao = event.currentTarget
|
||||
const { linhaId, campo } = botao.dataset
|
||||
const input = this.celulaTargets.find(
|
||||
(c) => c.dataset.linhaId === linhaId && c.dataset.campo === campo
|
||||
)
|
||||
// valor null no corpo = "devolve o que o plano trouxe e destrava o campo"
|
||||
this.gravar(linhaId, campo, null, input, botao)
|
||||
}
|
||||
|
||||
alternarAparelho(event) {
|
||||
const botao = event.currentTarget
|
||||
const novo = botao.dataset.valor === "SIM" ? "" : "SIM"
|
||||
this.gravar(botao.dataset.linhaId, "aparelho", novo, null, null, (dados) => {
|
||||
botao.dataset.valor = dados.valor
|
||||
botao.textContent = dados.valor || "—"
|
||||
const estilo = dados.valor === "SIM"
|
||||
? "bg-green-600/20 text-green-300 border-green-600/40"
|
||||
: "bg-[#1a1a1a] text-gray-500 border-white/10"
|
||||
botao.className = `${estilo} w-full min-h-[40px] rounded-lg border font-semibold text-xs hover:border-orange-500 transition-colors`
|
||||
this.marcar(botao.dataset.linhaId, "aparelho", dados.editado)
|
||||
})
|
||||
}
|
||||
|
||||
salvarRotulo() {
|
||||
this.enviar(`${this.urlValue}/atualizar_rotulo`, { rotulo_plano: this.rotuloTarget.value })
|
||||
.then(() => this.agendarPreview())
|
||||
}
|
||||
|
||||
salvarMotorista() {
|
||||
// Não mexe na prévia: o nome do motorista não sai impresso — a linha do
|
||||
// rodapé é preenchida à mão na retirada.
|
||||
this.enviar(`${this.urlValue}/atualizar_veiculo`, {
|
||||
veiculo: this.veiculoValue,
|
||||
motorista: this.motoristaTarget.value
|
||||
})
|
||||
}
|
||||
|
||||
// ── Navegação e filtro ──────────────────────────────────────────────────
|
||||
trocarVeiculo() {
|
||||
window.location.search = `?veiculo=${encodeURIComponent(this.veiculoTarget.value)}`
|
||||
}
|
||||
|
||||
anterior() { this.mover(-1) }
|
||||
proximo() { this.mover(1) }
|
||||
|
||||
mover(passo) {
|
||||
const select = this.veiculoTarget
|
||||
const destino = select.selectedIndex + passo
|
||||
if (destino < 0 || destino >= select.options.length) return
|
||||
select.selectedIndex = destino
|
||||
this.trocarVeiculo()
|
||||
}
|
||||
|
||||
filtrar(event) {
|
||||
const termo = event.currentTarget.value.trim().toLowerCase()
|
||||
this.linhaTargets.forEach((linha) => {
|
||||
linha.hidden = termo !== "" && !linha.dataset.busca.includes(termo)
|
||||
})
|
||||
}
|
||||
|
||||
atualizarPreview() {
|
||||
if (!this.hasPreviewTarget) return
|
||||
// Cache-bust em vez de reload(): reload reinicializa o visualizador de PDF do
|
||||
// navegador e a tela pisca a cada salvamento.
|
||||
this.previewTarget.src =
|
||||
`${this.pdfUrlValue}?veiculo=${encodeURIComponent(this.veiculoValue)}&t=${Date.now()}`
|
||||
}
|
||||
|
||||
// ── Infra ───────────────────────────────────────────────────────────────
|
||||
gravar(linhaId, campo, valor, input, botao, aoConcluir) {
|
||||
this.status("Salvando…")
|
||||
|
||||
this.enviar(`${this.urlValue}/atualizar_linha`, { linha_id: linhaId, campo, valor })
|
||||
.then((dados) => {
|
||||
if (!dados || !dados.ok) return
|
||||
if (input) {
|
||||
// Escrevemos de volta o valor QUE O SERVIDOR GRAVOU: a escrita é
|
||||
// last-write-wins, e assim uma sobreposição de duas abas fica visível em
|
||||
// vez de silenciosa.
|
||||
input.value = dados.valor
|
||||
input.dataset.original = dados.valor
|
||||
}
|
||||
this.marcar(linhaId, campo, dados.editado)
|
||||
if (aoConcluir) aoConcluir(dados)
|
||||
this.status("Salvo")
|
||||
this.agendarPreview()
|
||||
})
|
||||
.catch(() => this.status("Não deu para salvar — tente de novo"))
|
||||
}
|
||||
|
||||
marcar(linhaId, campo, editado) {
|
||||
const alvo = this.element.querySelector(`[data-romaneio-marcador="${linhaId}-${campo}"]`)
|
||||
if (!alvo) return
|
||||
|
||||
if (!editado) {
|
||||
alvo.innerHTML = ""
|
||||
return
|
||||
}
|
||||
alvo.innerHTML =
|
||||
`<button type="button" title="Editado — não será sobrescrito na reimportação. Clique para restaurar o valor do plano." ` +
|
||||
`data-action="romaneio#restaurar" data-linha-id="${linhaId}" data-campo="${campo}" ` +
|
||||
`class="text-orange-400 hover:text-orange-200 text-xs leading-none">●</button>`
|
||||
}
|
||||
|
||||
agendarPreview() {
|
||||
if (this.timerPreview) clearTimeout(this.timerPreview)
|
||||
this.timerPreview = setTimeout(() => this.atualizarPreview(), this.constructor.ATRASO_PREVIEW)
|
||||
}
|
||||
|
||||
enviar(url, corpo) {
|
||||
return fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')?.content
|
||||
},
|
||||
body: JSON.stringify(corpo)
|
||||
}).then((r) => (r.ok ? r.json() : Promise.reject(r)))
|
||||
}
|
||||
|
||||
status(texto) {
|
||||
if (this.hasStatusTarget) this.statusTarget.textContent = texto
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user