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>
107 lines
6.7 KiB
HTML
107 lines
6.7 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Nova instalação | GTI Fiscal{% endblock %}
|
|
{% block content %}
|
|
<div class="page-heading"><div><p class="eyebrow">DISTRIBUIÇÃO DO AGENT</p><h1>Nova instalação</h1><p class="muted">Cria uma instalação pendente; o pacote e o vínculo de enrollment só existem depois de gerados aqui.</p></div><div><a href="/agents" class="secondary">Voltar</a></div></div>
|
|
<section class="panel" style="max-width:560px">
|
|
{% if error %}<p class="error" role="alert">{{ error }}</p>{% endif %}
|
|
{% if not releases %}
|
|
<p class="muted">Nenhuma versão do Agent está publicada ainda. Publique uma release primeiro (<code>python central/scripts/publish_agent_release.py</code>) e ela aparecerá aqui.</p>
|
|
{% elif not tenants %}
|
|
<p class="muted">Nenhum cliente ativo encontrado no GTI Gerenciador. Verifique o cadastro por lá, ou rode a sincronização (<code>python central/scripts/sync_clients_from_gerenciador.py</code>).</p>
|
|
{% else %}
|
|
<form method="post" action="/agents/new">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<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>
|
|
<label>Hostname esperado (opcional)
|
|
<input name="expected_hostname" placeholder="ex. SRV-CONSTRUCAO" maxlength="255">
|
|
</label>
|
|
<label>Versão do Agent
|
|
<select name="release_id" required>
|
|
{% for release in releases %}<option value="{{ release.id }}" {% if loop.first %}selected{% endif %}>{{ release.version }} ({{ release.platform }}/{{ release.architecture }})</option>{% endfor %}
|
|
</select>
|
|
</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 () {
|
|
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>
|
|
{% endblock %}
|