fix: searchable client combobox in Nova instalação
Replaces the native list with a combobox (name/code/CNPJ search, accent- and case-insensitive, keyboard + mouse, custom dropdown, hidden client_id = the Gerenciador's real CLIENTE.ID). The backend resolves client_id against the active GTI_GERENCIADOR mirror and rejects free text/unknown ids. Adds Tenant.external_cnpj (migration 0025) mirrored via the internal endpoint. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,16 @@
|
||||
{% else %}
|
||||
<form method="post" action="/agents/new">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label>Cliente
|
||||
<input type="text" id="filtro-cliente" placeholder="Buscar cliente..." autocomplete="off" oninput="filtrarClientes(this.value)">
|
||||
<select name="tenant_id" id="select-cliente" size="8" required>
|
||||
{% for tenant in tenants %}<option value="{{ tenant.id }}">{{ tenant.name }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<div class="combo-field">
|
||||
<label for="cliente-busca">Cliente</label>
|
||||
<div class="combo" id="combo-cliente">
|
||||
<input type="text" id="cliente-busca" placeholder="Buscar por nome, código ou CNPJ..." autocomplete="off" role="combobox" aria-expanded="false" aria-controls="cliente-lista" aria-autocomplete="list">
|
||||
<input type="hidden" name="client_id" id="cliente-id" value="">
|
||||
<ul id="cliente-lista" class="combo-lista" role="listbox" hidden></ul>
|
||||
</div>
|
||||
<p class="error combo-erro" id="cliente-erro" role="alert" hidden>Selecione um cliente da lista.</p>
|
||||
</div>
|
||||
<script type="application/json" id="clientes-json">{{ clients_json|safe }}</script>
|
||||
<label>Nome da instalação
|
||||
<input name="display_name" placeholder="ex. Servidor Principal" maxlength="160" required>
|
||||
</label>
|
||||
@@ -30,13 +34,72 @@
|
||||
</label>
|
||||
<p><button type="submit">Criar instalação</button></p>
|
||||
</form>
|
||||
<style>
|
||||
.combo { position: relative; }
|
||||
.combo-lista { position: absolute; z-index: 20; left: 0; right: 0; top: 100%; margin: 4px 0 0; padding: 4px; list-style: none; max-height: 280px; overflow-y: auto; background: var(--cor-superficie); border: 1px solid var(--cor-borda); border-radius: 8px; box-shadow: var(--sombra-card); }
|
||||
.combo-lista li { padding: 8px 10px; border-radius: 6px; cursor: pointer; display: flex; flex-direction: column; gap: 2px; }
|
||||
.combo-lista li small { color: var(--cor-texto-secundario); }
|
||||
.combo-lista li:hover, .combo-lista li.ativo { background: var(--cor-superficie-alt); }
|
||||
.combo-lista li.selecionado { background: var(--cor-acento-suave); color: var(--cor-acento-texto); font-weight: 600; }
|
||||
.combo-lista li.vazio { cursor: default; color: var(--cor-texto-secundario); }
|
||||
.combo.invalido input[type=text] { border-color: var(--cor-perigo); }
|
||||
.combo-field { margin-bottom: 12px; }
|
||||
</style>
|
||||
<script>
|
||||
function filtrarClientes(termo) {
|
||||
var alvo = termo.trim().toLowerCase();
|
||||
document.querySelectorAll("#select-cliente option").forEach(function (opcao) {
|
||||
opcao.hidden = alvo.length > 0 && opcao.textContent.toLowerCase().indexOf(alvo) === -1;
|
||||
});
|
||||
(function () {
|
||||
var clientes = JSON.parse(document.getElementById("clientes-json").textContent);
|
||||
var box = document.getElementById("cliente-busca"), hid = document.getElementById("cliente-id");
|
||||
var lista = document.getElementById("cliente-lista"), erro = document.getElementById("cliente-erro");
|
||||
var combo = document.getElementById("combo-cliente"), form = box.form;
|
||||
var visiveis = [], ativo = -1;
|
||||
function norm(t) { return (t || "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase(); }
|
||||
function cnpjFmt(d) {
|
||||
if (!d) return "";
|
||||
if (d.length === 14) return d.replace(/^(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})$/, "$1.$2.$3/$4-$5");
|
||||
if (d.length === 11) return d.replace(/^(\d{3})(\d{3})(\d{3})(\d{2})$/, "$1.$2.$3-$4");
|
||||
return d;
|
||||
}
|
||||
function abrir(v) { lista.hidden = !v; box.setAttribute("aria-expanded", v ? "true" : "false"); }
|
||||
function render() {
|
||||
var q = norm(box.value), qd = q.replace(/\D/g, "");
|
||||
visiveis = clientes.filter(function (c) {
|
||||
if (!q) return true;
|
||||
return norm(c.name).indexOf(q) !== -1 || norm(c.code).indexOf(q) !== -1 || (qd && (c.cnpj || "").indexOf(qd) !== -1);
|
||||
}).slice(0, 100);
|
||||
lista.innerHTML = "";
|
||||
if (!visiveis.length) {
|
||||
var vazio = document.createElement("li"); vazio.className = "vazio"; vazio.textContent = "Nenhum cliente ativo encontrado."; lista.appendChild(vazio);
|
||||
}
|
||||
visiveis.forEach(function (c, i) {
|
||||
var li = document.createElement("li"); li.setAttribute("role", "option"); li.dataset.i = i;
|
||||
if (String(c.id) === hid.value) li.classList.add("selecionado");
|
||||
var nome = document.createElement("span"); nome.textContent = c.name; li.appendChild(nome);
|
||||
if (c.cnpj) { var sm = document.createElement("small"); sm.textContent = cnpjFmt(c.cnpj); li.appendChild(sm); }
|
||||
li.addEventListener("mousedown", function (e) { e.preventDefault(); escolher(c); });
|
||||
lista.appendChild(li);
|
||||
});
|
||||
ativo = -1; abrir(true);
|
||||
}
|
||||
function destacar(n) {
|
||||
var itens = lista.querySelectorAll("li[role=option]"); if (!itens.length) return;
|
||||
ativo = (n + itens.length) % itens.length;
|
||||
itens.forEach(function (li, i) { li.classList.toggle("ativo", i === ativo); });
|
||||
itens[ativo].scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
function escolher(c) { box.value = c.name; hid.value = c.id; erro.hidden = true; combo.classList.remove("invalido"); abrir(false); }
|
||||
box.addEventListener("input", function () { hid.value = ""; render(); });
|
||||
box.addEventListener("focus", render);
|
||||
box.addEventListener("blur", function () { setTimeout(function () { abrir(false); }, 120); });
|
||||
box.addEventListener("keydown", function (e) {
|
||||
if (e.key === "ArrowDown") { e.preventDefault(); if (lista.hidden) render(); destacar(ativo + 1); }
|
||||
else if (e.key === "ArrowUp") { e.preventDefault(); destacar(ativo - 1); }
|
||||
else if (e.key === "Enter") { if (!lista.hidden && ativo >= 0 && visiveis[ativo]) { e.preventDefault(); escolher(visiveis[ativo]); } else if (!hid.value) { e.preventDefault(); } }
|
||||
else if (e.key === "Escape") { abrir(false); }
|
||||
});
|
||||
form.addEventListener("submit", function (e) {
|
||||
if (!hid.value) { e.preventDefault(); erro.hidden = false; combo.classList.add("invalido"); box.focus(); }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user