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) => `
${escapeHtml(item.label)}
${escapeHtml(item.description)}
`).join("") : `
Nenhuma etapa disponivel.
`; parameterTypes.innerHTML = parameterTypeList.length > 0 ? parameterTypeList.map((item) => `${escapeHtml(item.label)}`).join("") : `Sem tipos`; } function renderLockedLifecycle(message) { lifecycleList.innerHTML = `
Leitura indisponivel
${escapeHtml(message || "A sessao atual nao pode ler o contrato compartilhado.")}
`; parameterTypes.innerHTML = `Bloqueado`; } 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) => `
${escapeHtml(item.gate || "revisao")}

${escapeHtml(item.display_name || item.tool_name || "Tool")}

${escapeHtml(item.tool_name || "")}
${escapeHtml(item.status || "pendente")}

${escapeHtml(item.summary || payload?.message || "Item aguardando analise do time.")}

`).join("") : `

Fila sem itens no momento

${escapeHtml(payload?.message || "Nenhuma tool aguardando revisao agora.")}

`; } function renderLockedQueue(message) { setText("[data-tool-review-queue-count]", "0"); setText("[data-tool-review-queue-mode]", "Bloqueado"); queueList.innerHTML = `

Fila indisponivel

${escapeHtml(message || "A sessao atual nao pode acessar a fila de revisao.")}

`; } 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) => `
${escapeHtml(item.domain || "tool")}

${escapeHtml(item.display_name || item.tool_name || "Tool")}

${escapeHtml(item.tool_name || "")}
v${escapeHtml(String(item.version || 1))}

${escapeHtml(item.description || "Publicacao ativa no catalogo do produto.")}

${escapeHtml(item.implementation_module || "")}
`).join("") : `

Catalogo ativo vazio

Nenhuma publicacao ativa retornada pela sessao web.

`; } function renderLockedPublications(message) { setText("[data-tool-review-publication-count]", "0"); setText("[data-tool-publication-source]", "Bloqueado"); publicationList.innerHTML = `

Catalogo protegido

${escapeHtml(message || "A sessao atual nao possui permissao para ler as publicacoes ativas.")}

`; } } 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 = `
${escapeHtml(draft?.domain || "tool")}

${escapeHtml(draft?.display_name || "Nova tool")}

${escapeHtml(draft?.tool_name || "")}
${escapeHtml(draft?.status || "draft")}

${escapeHtml(draft?.summary || "")}

Objetivo: ${escapeHtml(draft?.business_goal || "")}
Parametros: ${escapeHtml(String(draft?.parameter_count || 0))}
Obrigatorios: ${escapeHtml(String(draft?.required_parameter_count || 0))}
Aprovacao: ${draft?.requires_director_approval ? "Diretor obrigatorio" : "Nao"}
${parameters.length > 0 ? parameters.map((item) => `
${escapeHtml(item.name)}
${escapeHtml(item.description)}
${escapeHtml(item.parameter_type)}
`).join("") : `
Sem parametros
A tool foi cadastrada sem parametros de entrada.
`}
Avisos
${warnings.length > 0 ? `
    ${warnings.map((item) => `
  • ${escapeHtml(item)}
  • `).join("")}
` : `
Nenhum aviso extra para este pre-cadastro.
`}
Proximos passos
${nextSteps.length > 0 ? `
    ${nextSteps.map((item) => `
  • ${escapeHtml(item)}
  • `).join("")}
` : `
Sem orientacoes adicionais.
`}
`; } } 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 = `

Leitura indisponivel

${escapeHtml(result.message || "Nao foi possivel carregar os colaboradores.")}

`; 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 `
${escapeHtml(item?.role || "colaborador")}

${escapeHtml(item?.display_name || "Colaborador")}

${escapeHtml(item?.email || "")}
${item?.is_active ? "Ativo" : "Inativo"}
Ultimo login: ${escapeHtml(lastLogin)}
ID: ${escapeHtml(String(item?.id || "-"))}
`; }).join("") : `

Nenhum colaborador cadastrado ainda

Use o formulario ao lado para criar o primeiro colaborador administrativo.

`; } 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", }); }