153 lines
5.7 KiB
JavaScript
153 lines
5.7 KiB
JavaScript
// Widget genérico de vinculação em duas tabelas — não vinculados (esquerda)
|
|
// e vinculados (direita), com checkbox de seleção múltipla, busca por coluna,
|
|
// ordenação por nome e um botão de ação em lote por painel ("Vincular"/
|
|
// "Desvincular"). Mesmo espírito visual/de interação do painel "Usuários do
|
|
// Escritório" de perfis-acesso.html (profiles.js), mas genérico o bastante
|
|
// pra rodar em qualquer shell — hoje reaproveitado por "Usuários sob
|
|
// liderança" em usuarios.html (users-admin.js) e no modal "Gerenciar
|
|
// Usuário" (account.js, presente em todo shell).
|
|
//
|
|
// A vinculação aqui é só em memória — quem chama decide quando persistir
|
|
// (chamando getVinculadosIds() no momento de salvar), não dispara nenhuma
|
|
// requisição por conta própria.
|
|
//
|
|
// config = {
|
|
// available: { selectAll, sortHeader, searchPrincipal, searchSecundaria, body, footer, acaoBtn },
|
|
// linked: { ...mesmo formato... },
|
|
// secundariaValor: (candidato) => string,
|
|
// }
|
|
// candidato = { id, nome, ...outros campos usados por secundariaValor }
|
|
function pidCriarSeletorDuplo(config) {
|
|
const paineis = { available: config.available, linked: config.linked };
|
|
const secundariaValor = config.secundariaValor || (() => "");
|
|
const estado = {
|
|
available: { sortDir: 1, selected: new Set() },
|
|
linked: { sortDir: 1, selected: new Set() },
|
|
};
|
|
|
|
let candidatos = [];
|
|
let vinculadosIds = new Set();
|
|
|
|
function secundariaTexto(candidato) {
|
|
return secundariaValor(candidato) || "—";
|
|
}
|
|
|
|
function listaPara(kind) {
|
|
return kind === "linked"
|
|
? candidatos.filter((c) => vinculadosIds.has(c.id))
|
|
: candidatos.filter((c) => !vinculadosIds.has(c.id));
|
|
}
|
|
|
|
function filtradosOrdenados(kind) {
|
|
const el = paineis[kind];
|
|
const state = estado[kind];
|
|
const termoNome = el.searchPrincipal.value.trim().toLowerCase();
|
|
const termoSecundaria = el.searchSecundaria.value.trim().toLowerCase();
|
|
return listaPara(kind)
|
|
.filter((c) => !termoNome || c.nome.toLowerCase().includes(termoNome))
|
|
.filter((c) => !termoSecundaria || secundariaTexto(c).toLowerCase().includes(termoSecundaria))
|
|
.slice()
|
|
.sort((a, b) => a.nome.localeCompare(b.nome, "pt-BR", { sensitivity: "base" }) * state.sortDir);
|
|
}
|
|
|
|
function renderPainel(kind) {
|
|
const el = paineis[kind];
|
|
const state = estado[kind];
|
|
|
|
// Poda a seleção de ids que saíram da lista (ex.: acabou de ser
|
|
// vinculado/desvinculado e migrou pro outro painel).
|
|
const idsAtuais = new Set(listaPara(kind).map((c) => c.id));
|
|
Array.from(state.selected).forEach((id) => {
|
|
if (!idsAtuais.has(id)) state.selected.delete(id);
|
|
});
|
|
|
|
const filtrados = filtradosOrdenados(kind);
|
|
|
|
el.body.innerHTML = filtrados.length
|
|
? filtrados
|
|
.map(
|
|
(c) => `
|
|
<tr>
|
|
<td><input type="checkbox" data-dual-select="${c.id}" ${state.selected.has(c.id) ? "checked" : ""} /></td>
|
|
<td>${pidEscapeHtml(c.nome)}</td>
|
|
<td>${pidEscapeHtml(secundariaTexto(c))}</td>
|
|
</tr>`
|
|
)
|
|
.join("")
|
|
: `<tr class="dual-select__empty-row"><td colspan="3">Nenhum usuário encontrado.</td></tr>`;
|
|
|
|
const selecionadosVisiveis = filtrados.filter((c) => state.selected.has(c.id)).length;
|
|
el.selectAll.disabled = filtrados.length === 0;
|
|
el.selectAll.checked = filtrados.length > 0 && selecionadosVisiveis === filtrados.length;
|
|
el.selectAll.indeterminate = selecionadosVisiveis > 0 && selecionadosVisiveis < filtrados.length;
|
|
|
|
el.sortHeader.classList.toggle("is-sort-asc", state.sortDir === 1);
|
|
el.sortHeader.classList.toggle("is-sort-desc", state.sortDir === -1);
|
|
|
|
el.footer.textContent = `${filtrados.length} ${filtrados.length === 1 ? "usuário" : "usuários"}`;
|
|
el.acaoBtn.disabled = state.selected.size === 0;
|
|
}
|
|
|
|
function renderAmbos() {
|
|
renderPainel("available");
|
|
renderPainel("linked");
|
|
}
|
|
|
|
function mover(kind) {
|
|
const state = estado[kind];
|
|
const ids = Array.from(state.selected);
|
|
if (!ids.length) return;
|
|
ids.forEach((id) => {
|
|
if (kind === "available") vinculadosIds.add(id);
|
|
else vinculadosIds.delete(id);
|
|
});
|
|
state.selected.clear();
|
|
renderAmbos();
|
|
}
|
|
|
|
["available", "linked"].forEach((kind) => {
|
|
const el = paineis[kind];
|
|
el.searchPrincipal.addEventListener("input", () => renderPainel(kind));
|
|
el.searchSecundaria.addEventListener("input", () => renderPainel(kind));
|
|
el.sortHeader.addEventListener("click", () => {
|
|
estado[kind].sortDir *= -1;
|
|
renderPainel(kind);
|
|
});
|
|
el.selectAll.addEventListener("change", () => {
|
|
const filtrados = filtradosOrdenados(kind);
|
|
filtrados.forEach((c) => {
|
|
if (el.selectAll.checked) estado[kind].selected.add(c.id);
|
|
else estado[kind].selected.delete(c.id);
|
|
});
|
|
renderPainel(kind);
|
|
});
|
|
el.body.addEventListener("change", (event) => {
|
|
const checkbox = event.target.closest("[data-dual-select]");
|
|
if (!checkbox) return;
|
|
const id = Number(checkbox.getAttribute("data-dual-select"));
|
|
if (checkbox.checked) estado[kind].selected.add(id);
|
|
else estado[kind].selected.delete(id);
|
|
renderPainel(kind);
|
|
});
|
|
el.acaoBtn.addEventListener("click", () => mover(kind));
|
|
});
|
|
|
|
function setDados(novosCandidatos, vinculadosIniciais) {
|
|
candidatos = novosCandidatos;
|
|
vinculadosIds = new Set(vinculadosIniciais);
|
|
["available", "linked"].forEach((kind) => {
|
|
estado[kind].selected.clear();
|
|
estado[kind].sortDir = 1;
|
|
paineis[kind].searchPrincipal.value = "";
|
|
paineis[kind].searchSecundaria.value = "";
|
|
});
|
|
renderAmbos();
|
|
}
|
|
|
|
function getVinculadosIds() {
|
|
return Array.from(vinculadosIds);
|
|
}
|
|
|
|
return { setDados, getVinculadosIds };
|
|
}
|