You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
613 lines
27 KiB
JavaScript
613 lines
27 KiB
JavaScript
document.documentElement.dataset.panelReady = "true";
|
|
|
|
const loginForm = document.querySelector('[data-admin-login-form="true"]');
|
|
const reviewBoard = document.querySelector('[data-admin-tool-review-board="true"]');
|
|
const toolIntakePage = document.querySelector('[data-admin-tool-intake="true"]');
|
|
const collaboratorBoard = document.querySelector('[data-admin-collaborator-board="true"]');
|
|
|
|
if (loginForm) {
|
|
mountLoginForm(loginForm);
|
|
}
|
|
|
|
if (reviewBoard) {
|
|
mountToolReviewBoard(reviewBoard);
|
|
}
|
|
|
|
if (toolIntakePage) {
|
|
mountToolIntakePage(toolIntakePage);
|
|
}
|
|
|
|
if (collaboratorBoard) {
|
|
mountCollaboratorBoard(collaboratorBoard);
|
|
}
|
|
|
|
function mountLoginForm(form) {
|
|
const feedback = document.getElementById("admin-login-feedback");
|
|
const submitButton = form.querySelector('button[type="submit"]');
|
|
const submitLabel = form.querySelector("[data-submit-label]");
|
|
const submitSpinner = form.querySelector("[data-submit-spinner]");
|
|
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
toggleLoading(true);
|
|
clearFeedback();
|
|
|
|
const formData = new FormData(form);
|
|
const payload = {
|
|
email: String(formData.get("email") || "").trim(),
|
|
password: String(formData.get("password") || ""),
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(form.dataset.authEndpoint, {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const authBody = await readJson(response);
|
|
if (!response.ok) {
|
|
throw new Error(authBody?.detail || "Nao foi possivel autenticar no admin.");
|
|
}
|
|
|
|
showFeedback("success", authBody?.message || "Sessao administrativa web iniciada com sucesso.");
|
|
form.reset();
|
|
|
|
const redirectTo = authBody?.redirect_to || form.dataset.dashboardHref;
|
|
if (redirectTo) {
|
|
window.setTimeout(() => {
|
|
window.location.assign(redirectTo);
|
|
}, 250);
|
|
}
|
|
} catch (error) {
|
|
showFeedback("danger", error instanceof Error ? error.message : "Erro inesperado durante o login.");
|
|
} finally {
|
|
toggleLoading(false);
|
|
}
|
|
});
|
|
|
|
function toggleLoading(isLoading) {
|
|
submitButton.disabled = isLoading;
|
|
submitSpinner.classList.toggle("d-none", !isLoading);
|
|
submitLabel.textContent = isLoading ? "Validando acesso..." : "Entrar no painel";
|
|
}
|
|
|
|
function clearFeedback() {
|
|
feedback.className = "alert d-none mt-4 mb-0 rounded-4";
|
|
feedback.textContent = "";
|
|
}
|
|
|
|
function showFeedback(variant, message) {
|
|
feedback.className = `alert alert-${variant} mt-4 mb-0 rounded-4`;
|
|
feedback.textContent = message;
|
|
}
|
|
}
|
|
|
|
function mountToolReviewBoard(board) {
|
|
const refreshButton = board.querySelector("[data-admin-tool-refresh]");
|
|
const refreshLabel = board.querySelector("[data-tool-refresh-label]");
|
|
const refreshSpinner = board.querySelector("[data-tool-refresh-spinner]");
|
|
const feedback = document.getElementById("admin-tool-review-feedback");
|
|
const queueList = board.querySelector("[data-tool-review-queue-list]");
|
|
const publicationList = board.querySelector("[data-tool-publication-list]");
|
|
const lifecycleList = board.querySelector("[data-tool-contract-lifecycle]");
|
|
const parameterTypes = board.querySelector("[data-tool-parameter-types]");
|
|
|
|
if (refreshButton) {
|
|
refreshButton.addEventListener("click", () => {
|
|
void loadBoard();
|
|
});
|
|
}
|
|
|
|
void loadBoard();
|
|
|
|
async function loadBoard() {
|
|
toggleRefreshing(true);
|
|
clearFeedback();
|
|
|
|
const overviewResult = await fetchPanelJson(board.dataset.overviewEndpoint);
|
|
const contractsResult = await fetchPanelJson(board.dataset.contractsEndpoint);
|
|
const reviewQueueResult = await fetchPanelJson(board.dataset.reviewQueueEndpoint);
|
|
const publicationsResult = await fetchPanelJson(board.dataset.publicationsEndpoint);
|
|
|
|
if (!overviewResult.ok && !contractsResult.ok && !reviewQueueResult.ok && !publicationsResult.ok) {
|
|
showFeedback("warning", overviewResult.message || "Entre com uma sessao administrativa web para carregar esta tela.");
|
|
}
|
|
|
|
if (overviewResult.ok) {
|
|
renderOverview(overviewResult.body);
|
|
}
|
|
if (contractsResult.ok) {
|
|
renderContracts(contractsResult.body);
|
|
} else {
|
|
renderLockedLifecycle(contractsResult.message);
|
|
}
|
|
if (reviewQueueResult.ok) {
|
|
renderReviewQueue(reviewQueueResult.body);
|
|
} else {
|
|
renderLockedQueue(reviewQueueResult.message);
|
|
}
|
|
if (publicationsResult.ok) {
|
|
renderPublications(publicationsResult.body);
|
|
} else {
|
|
renderLockedPublications(publicationsResult.message);
|
|
}
|
|
|
|
setText("[data-tool-review-last-sync]", formatNow());
|
|
toggleRefreshing(false);
|
|
}
|
|
|
|
function toggleRefreshing(isLoading) {
|
|
if (!refreshButton || !refreshLabel || !refreshSpinner) {
|
|
return;
|
|
}
|
|
refreshButton.disabled = isLoading;
|
|
refreshSpinner.classList.toggle("d-none", !isLoading);
|
|
refreshLabel.textContent = isLoading ? "Atualizando..." : "Atualizar leitura";
|
|
}
|
|
|
|
function clearFeedback() {
|
|
feedback.className = "alert d-none rounded-4 mb-4";
|
|
feedback.textContent = "";
|
|
}
|
|
|
|
function showFeedback(variant, message) {
|
|
feedback.className = `alert alert-${variant} rounded-4 mb-4`;
|
|
feedback.textContent = message;
|
|
}
|
|
|
|
function renderOverview(payload) {
|
|
const workflow = Array.isArray(payload?.workflow) ? payload.workflow : [];
|
|
const nextSteps = Array.isArray(payload?.next_steps) ? payload.next_steps : [];
|
|
setText("[data-tool-review-lifecycle-count]", String(workflow.length || 0));
|
|
if (nextSteps.length > 0 && !feedback.textContent) {
|
|
showFeedback("info", `Proximos passos: ${nextSteps[0]}`);
|
|
}
|
|
}
|
|
|
|
function renderContracts(payload) {
|
|
const lifecycle = Array.isArray(payload?.lifecycle_statuses) ? payload.lifecycle_statuses : [];
|
|
const parameterTypeList = Array.isArray(payload?.parameter_types) ? payload.parameter_types : [];
|
|
lifecycleList.innerHTML = lifecycle.length > 0
|
|
? lifecycle.map((item) => `<div class="admin-tool-inline-note rounded-4 p-3"><div class="fw-semibold">${escapeHtml(item.label)}</div><div class="small text-secondary mt-1">${escapeHtml(item.description)}</div></div>`).join("")
|
|
: `<div class="small text-secondary">Nenhuma etapa disponivel.</div>`;
|
|
parameterTypes.innerHTML = parameterTypeList.length > 0
|
|
? parameterTypeList.map((item) => `<span class="badge rounded-pill bg-body-tertiary text-secondary border">${escapeHtml(item.label)}</span>`).join("")
|
|
: `<span class="badge rounded-pill bg-body-tertiary text-secondary border">Sem tipos</span>`;
|
|
}
|
|
|
|
function renderLockedLifecycle(message) {
|
|
lifecycleList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3"><div class="fw-semibold">Leitura indisponivel</div><div class="small text-secondary mt-1">${escapeHtml(message || "A sessao atual nao pode ler o contrato compartilhado.")}</div></div>`;
|
|
parameterTypes.innerHTML = `<span class="badge rounded-pill bg-body-tertiary text-secondary border">Bloqueado</span>`;
|
|
}
|
|
|
|
function renderReviewQueue(payload) {
|
|
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
setText("[data-tool-review-queue-count]", String(items.length));
|
|
setText("[data-tool-review-queue-mode]", payload?.queue_mode || "Fila web");
|
|
queueList.innerHTML = items.length > 0
|
|
? items.map((item) => `<article class="admin-tool-review-card rounded-4 p-4"><div class="d-flex justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item.gate || "revisao")}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item.display_name || item.tool_name || "Tool")}</h4><div class="small text-secondary">${escapeHtml(item.tool_name || "")}</div></div><span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border border-warning-subtle">${escapeHtml(item.status || "pendente")}</span></div><p class="text-secondary mb-0">${escapeHtml(item.summary || payload?.message || "Item aguardando analise do time.")}</p></article>`).join("")
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Fila sem itens no momento</h4><p class="text-secondary mb-0">${escapeHtml(payload?.message || "Nenhuma tool aguardando revisao agora.")}</p></div>`;
|
|
}
|
|
|
|
function renderLockedQueue(message) {
|
|
setText("[data-tool-review-queue-count]", "0");
|
|
setText("[data-tool-review-queue-mode]", "Bloqueado");
|
|
queueList.innerHTML = `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Fila indisponivel</h4><p class="text-secondary mb-0">${escapeHtml(message || "A sessao atual nao pode acessar a fila de revisao.")}</p></div>`;
|
|
}
|
|
|
|
function renderPublications(payload) {
|
|
const items = Array.isArray(payload?.publications) ? payload.publications : [];
|
|
setText("[data-tool-review-publication-count]", String(items.length));
|
|
setText("[data-tool-publication-source]", payload?.source || "Catalogo web");
|
|
publicationList.innerHTML = items.length > 0
|
|
? items.slice(0, 9).map((item) => `<div class="col-12 col-md-6 col-xxl-4"><article class="admin-tool-publication-card rounded-4 p-4 h-100"><div class="d-flex justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item.domain || "tool")}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item.display_name || item.tool_name || "Tool")}</h4><div class="small text-secondary">${escapeHtml(item.tool_name || "")}</div></div><span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle">v${escapeHtml(String(item.version || 1))}</span></div><p class="text-secondary mb-3">${escapeHtml(item.description || "Publicacao ativa no catalogo do produto.")}</p><div class="small text-secondary">${escapeHtml(item.implementation_module || "")}</div></article></div>`).join("")
|
|
: `<div class="col-12"><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Catalogo ativo vazio</h4><p class="text-secondary mb-0">Nenhuma publicacao ativa retornada pela sessao web.</p></div></div>`;
|
|
}
|
|
|
|
function renderLockedPublications(message) {
|
|
setText("[data-tool-review-publication-count]", "0");
|
|
setText("[data-tool-publication-source]", "Bloqueado");
|
|
publicationList.innerHTML = `<div class="col-12"><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Catalogo protegido</h4><p class="text-secondary mb-0">${escapeHtml(message || "A sessao atual nao possui permissao para ler as publicacoes ativas.")}</p></div></div>`;
|
|
}
|
|
}
|
|
|
|
function mountToolIntakePage(page) {
|
|
const form = page.querySelector('[data-admin-tool-intake-form="true"]');
|
|
const addButton = page.querySelector("[data-add-parameter-row]");
|
|
const parameterList = page.querySelector("[data-parameter-list]");
|
|
const template = page.querySelector("#admin-tool-parameter-row-template");
|
|
const feedback = document.getElementById("admin-tool-intake-feedback");
|
|
const preview = page.querySelector("[data-tool-intake-preview]");
|
|
const storageStatus = page.querySelector("[data-tool-intake-storage-status]");
|
|
const submitLabel = page.querySelector("[data-intake-submit-label]");
|
|
const submitSpinner = page.querySelector("[data-intake-submit-spinner]");
|
|
const submitButton = form?.querySelector('button[type="submit"]');
|
|
|
|
if (!form || !parameterList || !template || !feedback || !preview || !storageStatus || !submitLabel || !submitSpinner || !submitButton) {
|
|
return;
|
|
}
|
|
|
|
if (addButton) {
|
|
addButton.addEventListener("click", () => {
|
|
appendParameterRow();
|
|
});
|
|
}
|
|
|
|
parameterList.addEventListener("click", (event) => {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLElement) || !target.matches("[data-remove-parameter-row]")) {
|
|
return;
|
|
}
|
|
|
|
const rows = parameterList.querySelectorAll("[data-parameter-row]");
|
|
const row = target.closest("[data-parameter-row]");
|
|
if (!row) {
|
|
return;
|
|
}
|
|
|
|
if (rows.length === 1) {
|
|
clearParameterRow(row);
|
|
return;
|
|
}
|
|
|
|
row.remove();
|
|
});
|
|
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
toggleSubmitting(true);
|
|
clearFeedback();
|
|
|
|
const payload = buildPayload();
|
|
|
|
try {
|
|
const response = await fetch(page.dataset.intakeEndpoint, {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const body = await readJson(response);
|
|
if (!response.ok) {
|
|
throw new Error(body?.detail || "Nao foi possivel validar o pre-cadastro da tool.");
|
|
}
|
|
|
|
renderDraftPreview(body);
|
|
showFeedback("success", body?.message || "Pre-cadastro validado com sucesso.");
|
|
} catch (error) {
|
|
storageStatus.textContent = "Falha";
|
|
showFeedback("danger", error instanceof Error ? error.message : "Erro inesperado ao validar a nova tool.");
|
|
} finally {
|
|
toggleSubmitting(false);
|
|
}
|
|
});
|
|
|
|
function appendParameterRow() {
|
|
const fragment = template.content.cloneNode(true);
|
|
parameterList.appendChild(fragment);
|
|
}
|
|
|
|
function buildPayload() {
|
|
const formData = new FormData(form);
|
|
const parameters = Array.from(parameterList.querySelectorAll("[data-parameter-row]"))
|
|
.map((row) => {
|
|
const name = row.querySelector('[name="parameter_name"]')?.value?.trim() || "";
|
|
const parameterType = row.querySelector('[name="parameter_type"]')?.value || "string";
|
|
const description = row.querySelector('[name="parameter_description"]')?.value?.trim() || "";
|
|
const required = Boolean(row.querySelector('[name="parameter_required"]')?.checked);
|
|
return { name, parameter_type: parameterType, description, required };
|
|
})
|
|
.filter((item) => item.name || item.description);
|
|
|
|
return {
|
|
domain: String(formData.get("domain") || "").trim(),
|
|
tool_name: String(formData.get("tool_name") || "").trim(),
|
|
display_name: String(formData.get("display_name") || "").trim(),
|
|
description: String(formData.get("description") || "").trim(),
|
|
business_goal: String(formData.get("business_goal") || "").trim(),
|
|
parameters,
|
|
};
|
|
}
|
|
|
|
function clearParameterRow(row) {
|
|
row.querySelectorAll("input").forEach((input) => {
|
|
if (input.type === "checkbox") {
|
|
input.checked = true;
|
|
return;
|
|
}
|
|
input.value = "";
|
|
});
|
|
const select = row.querySelector('select[name="parameter_type"]');
|
|
if (select) {
|
|
select.value = "string";
|
|
}
|
|
}
|
|
|
|
function toggleSubmitting(isLoading) {
|
|
submitButton.disabled = isLoading;
|
|
submitSpinner.classList.toggle("d-none", !isLoading);
|
|
submitLabel.textContent = isLoading ? "Validando..." : "Validar pre-cadastro";
|
|
}
|
|
|
|
function clearFeedback() {
|
|
feedback.className = "alert d-none rounded-4 mb-4";
|
|
feedback.textContent = "";
|
|
}
|
|
|
|
function showFeedback(variant, message) {
|
|
feedback.className = `alert alert-${variant} rounded-4 mb-4`;
|
|
feedback.textContent = message;
|
|
}
|
|
|
|
function renderDraftPreview(payload) {
|
|
const draft = payload?.draft_preview;
|
|
const warnings = Array.isArray(payload?.warnings) ? payload.warnings : [];
|
|
const nextSteps = Array.isArray(payload?.next_steps) ? payload.next_steps : [];
|
|
const parameters = Array.isArray(draft?.parameters) ? draft.parameters : [];
|
|
|
|
storageStatus.textContent = payload?.storage_status || "Validado";
|
|
preview.innerHTML = `
|
|
<article class="admin-tool-preview-card rounded-4 p-4">
|
|
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
|
|
<div>
|
|
<div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(draft?.domain || "tool")}</div>
|
|
<h4 class="h4 fw-semibold mb-1">${escapeHtml(draft?.display_name || "Nova tool")}</h4>
|
|
<div class="small text-secondary">${escapeHtml(draft?.tool_name || "")}</div>
|
|
</div>
|
|
<span class="badge rounded-pill bg-info-subtle text-info-emphasis border border-info-subtle">${escapeHtml(draft?.status || "draft")}</span>
|
|
</div>
|
|
<p class="text-secondary mb-3">${escapeHtml(draft?.summary || "")}</p>
|
|
<div class="admin-tool-preview-meta small text-secondary mb-3">
|
|
<div><strong>Objetivo:</strong> ${escapeHtml(draft?.business_goal || "")}</div>
|
|
<div><strong>Parametros:</strong> ${escapeHtml(String(draft?.parameter_count || 0))}</div>
|
|
<div><strong>Obrigatorios:</strong> ${escapeHtml(String(draft?.required_parameter_count || 0))}</div>
|
|
<div><strong>Aprovacao:</strong> ${draft?.requires_director_approval ? "Diretor obrigatorio" : "Nao"}</div>
|
|
</div>
|
|
<div class="vstack gap-2 mb-3">
|
|
${parameters.length > 0
|
|
? parameters.map((item) => `<div class="admin-tool-inline-note rounded-4 p-3"><div class="d-flex justify-content-between gap-3"><div><div class="fw-semibold">${escapeHtml(item.name)}</div><div class="small text-secondary mt-1">${escapeHtml(item.description)}</div></div><span class="badge rounded-pill bg-body-tertiary text-secondary border">${escapeHtml(item.parameter_type)}</span></div></div>`).join("")
|
|
: `<div class="admin-tool-inline-note rounded-4 p-3"><div class="fw-semibold">Sem parametros</div><div class="small text-secondary mt-1">A tool foi cadastrada sem parametros de entrada.</div></div>`}
|
|
</div>
|
|
<div class="admin-tool-preview-stack vstack gap-3">
|
|
<div>
|
|
<div class="small text-uppercase fw-semibold text-secondary mb-2">Avisos</div>
|
|
${warnings.length > 0
|
|
? `<ul class="small text-secondary ps-3 mb-0">${warnings.map((item) => `<li class="mb-2">${escapeHtml(item)}</li>`).join("")}</ul>`
|
|
: `<div class="small text-secondary">Nenhum aviso extra para este pre-cadastro.</div>`}
|
|
</div>
|
|
<div>
|
|
<div class="small text-uppercase fw-semibold text-secondary mb-2">Proximos passos</div>
|
|
${nextSteps.length > 0
|
|
? `<ul class="small text-secondary ps-3 mb-0">${nextSteps.map((item) => `<li class="mb-2">${escapeHtml(item)}</li>`).join("")}</ul>`
|
|
: `<div class="small text-secondary">Sem orientacoes adicionais.</div>`}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function mountCollaboratorBoard(board) {
|
|
const form = board.querySelector('[data-admin-collaborator-form="true"]');
|
|
const feedback = document.getElementById("admin-collaborator-feedback");
|
|
const list = board.querySelector("[data-collaborator-list]");
|
|
const refreshButton = board.querySelector("[data-admin-collaborator-refresh]");
|
|
const refreshLabel = board.querySelector("[data-collaborator-refresh-label]");
|
|
const refreshSpinner = board.querySelector("[data-collaborator-refresh-spinner]");
|
|
const submitButton = form?.querySelector('button[type="submit"]');
|
|
const submitLabel = form?.querySelector("[data-collaborator-submit-label]");
|
|
const submitSpinner = form?.querySelector("[data-collaborator-submit-spinner]");
|
|
|
|
if (!form || !feedback || !list || !refreshButton || !refreshLabel || !refreshSpinner || !submitButton || !submitLabel || !submitSpinner) {
|
|
return;
|
|
}
|
|
|
|
refreshButton.addEventListener("click", () => {
|
|
void loadCollaborators();
|
|
});
|
|
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
toggleSubmitting(true);
|
|
clearFeedback();
|
|
|
|
const formData = new FormData(form);
|
|
const payload = {
|
|
display_name: String(formData.get("display_name") || "").trim(),
|
|
email: String(formData.get("email") || "").trim(),
|
|
password: String(formData.get("password") || ""),
|
|
is_active: Boolean(formData.get("is_active")),
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(board.dataset.collaboratorCollectionEndpoint, {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const body = await readJson(response);
|
|
if (!response.ok) {
|
|
throw new Error(body?.detail || "Nao foi possivel criar o colaborador.");
|
|
}
|
|
|
|
showFeedback("success", body?.message || "Colaborador criado com sucesso.");
|
|
form.reset();
|
|
const activeField = form.querySelector('[name="is_active"]');
|
|
if (activeField instanceof HTMLInputElement) {
|
|
activeField.checked = true;
|
|
}
|
|
await loadCollaborators();
|
|
} catch (error) {
|
|
showFeedback("danger", error instanceof Error ? error.message : "Erro inesperado ao cadastrar colaborador.");
|
|
} finally {
|
|
toggleSubmitting(false);
|
|
}
|
|
});
|
|
|
|
list.addEventListener("click", async (event) => {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLElement) || !target.matches("[data-collaborator-toggle]")) {
|
|
return;
|
|
}
|
|
|
|
const collaboratorId = target.dataset.collaboratorId;
|
|
const nextState = target.dataset.collaboratorNextState === "true";
|
|
if (!collaboratorId) {
|
|
return;
|
|
}
|
|
|
|
toggleRefreshing(true);
|
|
clearFeedback();
|
|
|
|
try {
|
|
const response = await fetch(`${board.dataset.collaboratorCollectionEndpoint}/${collaboratorId}/status`, {
|
|
method: "PATCH",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify({ is_active: nextState }),
|
|
});
|
|
const body = await readJson(response);
|
|
if (!response.ok) {
|
|
throw new Error(body?.detail || "Nao foi possivel atualizar o status do colaborador.");
|
|
}
|
|
|
|
showFeedback("success", body?.message || "Status do colaborador atualizado com sucesso.");
|
|
await loadCollaborators();
|
|
} catch (error) {
|
|
showFeedback("danger", error instanceof Error ? error.message : "Erro inesperado ao atualizar o colaborador.");
|
|
toggleRefreshing(false);
|
|
}
|
|
});
|
|
|
|
void loadCollaborators();
|
|
|
|
async function loadCollaborators() {
|
|
toggleRefreshing(true);
|
|
const result = await fetchPanelJson(board.dataset.collaboratorCollectionEndpoint);
|
|
|
|
if (result.ok) {
|
|
renderCollaborators(result.body);
|
|
} else {
|
|
list.innerHTML = `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Leitura indisponivel</h4><p class="text-secondary mb-0">${escapeHtml(result.message || "Nao foi possivel carregar os colaboradores.")}</p></div>`;
|
|
setText("[data-collaborator-total]", "0");
|
|
setText("[data-collaborator-active-count]", "0");
|
|
setText("[data-collaborator-inactive-count]", "0");
|
|
showFeedback("warning", result.message || "A sessao atual nao pode consultar os colaboradores.");
|
|
}
|
|
|
|
toggleRefreshing(false);
|
|
}
|
|
|
|
function renderCollaborators(payload) {
|
|
const collaborators = Array.isArray(payload?.collaborators) ? payload.collaborators : [];
|
|
setText("[data-collaborator-total]", String(payload?.total || 0));
|
|
setText("[data-collaborator-active-count]", String(payload?.active_count || 0));
|
|
setText("[data-collaborator-inactive-count]", String(payload?.inactive_count || 0));
|
|
|
|
list.innerHTML = collaborators.length > 0
|
|
? collaborators.map((item) => {
|
|
const statusBadge = item?.is_active
|
|
? "bg-success-subtle text-success-emphasis border border-success-subtle"
|
|
: "bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle";
|
|
const actionLabel = item?.is_active ? "Desativar acesso" : "Reativar acesso";
|
|
const buttonClass = item?.is_active ? "btn-outline-secondary" : "btn-outline-dark";
|
|
const lastLogin = item?.last_login_at ? formatDateTime(item.last_login_at) : "Ainda nao acessou";
|
|
return `<article class="admin-collaborator-card rounded-4 p-4"><div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item?.role || "colaborador")}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item?.display_name || "Colaborador")}</h4><div class="small text-secondary">${escapeHtml(item?.email || "")}</div></div><span class="badge rounded-pill ${statusBadge}">${item?.is_active ? "Ativo" : "Inativo"}</span></div><div class="small text-secondary admin-collaborator-meta mb-3"><div><strong>Ultimo login:</strong> ${escapeHtml(lastLogin)}</div><div><strong>ID:</strong> ${escapeHtml(String(item?.id || "-"))}</div></div><button class="btn btn-sm ${buttonClass} rounded-pill px-3" type="button" data-collaborator-toggle="true" data-collaborator-id="${escapeHtml(String(item?.id || ""))}" data-collaborator-next-state="${item?.is_active ? "false" : "true"}">${actionLabel}</button></article>`;
|
|
}).join("")
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Nenhum colaborador cadastrado ainda</h4><p class="text-secondary mb-0">Use o formulario ao lado para criar o primeiro colaborador administrativo.</p></div>`;
|
|
}
|
|
|
|
function toggleSubmitting(isLoading) {
|
|
submitButton.disabled = isLoading;
|
|
submitSpinner.classList.toggle("d-none", !isLoading);
|
|
submitLabel.textContent = isLoading ? "Criando..." : "Criar colaborador";
|
|
}
|
|
|
|
function toggleRefreshing(isLoading) {
|
|
refreshButton.disabled = isLoading;
|
|
refreshSpinner.classList.toggle("d-none", !isLoading);
|
|
refreshLabel.textContent = isLoading ? "Atualizando..." : "Atualizar lista";
|
|
}
|
|
|
|
function clearFeedback() {
|
|
feedback.className = "alert d-none rounded-4 mb-4";
|
|
feedback.textContent = "";
|
|
}
|
|
|
|
function showFeedback(variant, message) {
|
|
feedback.className = `alert alert-${variant} rounded-4 mb-4`;
|
|
feedback.textContent = message;
|
|
}
|
|
}
|
|
|
|
async function fetchPanelJson(url) {
|
|
const response = await fetch(url, {
|
|
credentials: "same-origin",
|
|
headers: { Accept: "application/json" },
|
|
});
|
|
const body = await readJson(response);
|
|
if (response.ok) {
|
|
return { ok: true, body };
|
|
}
|
|
const defaultMessage = response.status === 401
|
|
? "Entre com uma sessao administrativa web para visualizar esta area."
|
|
: body?.detail || "Nao foi possivel carregar os dados desta superficie.";
|
|
return { ok: false, body, message: defaultMessage };
|
|
}
|
|
|
|
async function readJson(response) {
|
|
try {
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function setText(selector, value) {
|
|
const target = document.querySelector(selector);
|
|
if (target) {
|
|
target.textContent = value;
|
|
}
|
|
}
|
|
|
|
function formatNow() {
|
|
return new Date().toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value || "")
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
function formatDateTime(value) {
|
|
const parsed = new Date(value);
|
|
if (Number.isNaN(parsed.getTime())) {
|
|
return String(value || "");
|
|
}
|
|
return parsed.toLocaleString("pt-BR", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|