const { Card, DataTable, Button, Icon, IconButton, SearchField, Select, Input, Badge, Tabs, Avatar, EmptyState, StatCard, ImportExportBar } = window.ACRFlowDesignSystem_ba1e9c;
const SEVERIDADE = { info: { tone: "neutral", label: "Informativo" }, aviso: { tone: "warning", label: "Atenção" }, critico: { tone: "danger", label: "Crítico" } };
const ROTULO_ACAO = {
"login.local": "Entrada (conta local)",
"login.google": "Entrada (Google)",
"login.falhou": "Tentativa de entrada recusada",
logout: "Saída do sistema",
"sessao.encerrada_pelo_supervisor": "Sessão encerrada pelo supervisor",
"login.bloqueado": "Conta bloqueada por tentativas",
"acesso.ip_bloqueado": "Estação bloqueada por tentativas",
"demanda.exclusao_negada": "Tentativa de exclusão negada",
"demanda.cancelamento_negada": "Tentativa de cancelamento negada",
"demanda.reabertura_negada": "Tentativa de reabertura negada",
"acesso.ip_liberado": "Estação liberada pelo supervisor",
"demanda.prazo_corrigido": "Prazo corrigido pelo sistema",
"demanda.gerada_por_recorrencia": "Demanda gerada por recorrência",
"demanda.criada": "Demanda aberta",
"demanda.status_alterado": "Status alterado",
"demanda.prioridade_alterada": "Prioridade alterada",
"demanda.prazo_alterado": "Prazo alterado",
"demanda.responsaveis_alterados": "Responsáveis alterados",
"demanda.encerramento_solicitado": "Encerramento solicitado",
"demanda.encerrada": "Encerramento aprovado",
"demanda.devolvida": "Demanda devolvida",
"demanda.cancelada": "Demanda cancelada",
"demanda.reaberta": "Demanda reaberta",
"demanda.excluida": "Demanda excluída",
"usuario.criado": "Colaborador cadastrado",
"usuario.perfil_alterado": "Perfil alterado",
"usuario.senha_redefinida": "Senha redefinida",
"usuario.desativado": "Colaborador desativado",
"usuario.reativado": "Colaborador reativado",
"cadastro.criado": "Cadastro criado",
"cadastro.editado": "Cadastro alterado",
"cadastro.removido": "Cadastro removido",
};
const ICONE_ACAO = (a) => {
if (a.startsWith("login") || a === "logout" || a.startsWith("sessao")) return "key-round";
if (a.includes("excluida") || a.includes("removido") || a.includes("desativado")) return "trash-2";
if (a.includes("cancelada") || a.includes("devolvida")) return "undo-2";
if (a.includes("encerrada")) return "shield-check";
if (a.startsWith("usuario")) return "user-cog";
if (a.startsWith("cadastro")) return "database";
return "activity";
};
const dataHora = (iso) => (iso ? new Date(iso).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" }) : "—");
const desde = (iso) => {
if (!iso) return "nunca acessou";
const min = Math.round((Date.now() - new Date(iso)) / 60000);
if (min < 1) return "agora mesmo";
if (min < 60) return "há " + min + " min";
const h = Math.round(min / 60);
if (h < 24) return "há " + h + (h === 1 ? " hora" : " horas");
const dias = Math.round(h / 24);
return "há " + dias + (dias === 1 ? " dia" : " dias");
};
function AuditScreen({ user, onToast }) {
const supervisor = user && user.profile === "supervisor";
const [aba, setAba] = React.useState("eventos");
const [resumo, setResumo] = React.useState(null);
const [sessoes, setSessoes] = React.useState([]);
const [acessos, setAcessos] = React.useState([]);
const [eventos, setEventos] = React.useState([]);
const [prod, setProd] = React.useState(null);
const [bloqueios, setBloqueios] = React.useState([]);
const [janela, setJanela] = React.useState(30);
const [carregando, setCarregando] = React.useState(true);
const hoje = new Date().toISOString().slice(0, 10);
const trintaDias = new Date(Date.now() - 29 * 86400000).toISOString().slice(0, 10);
const [de, setDe] = React.useState(trintaDias);
const [ate, setAte] = React.useState(hoje);
const [acao, setAcao] = React.useState("Todas");
const [sev, setSev] = React.useState("Todas");
const [q, setQ] = React.useState("");
const online = window.ACRStore && window.ACRStore.online();
const carregar = React.useCallback(async () => {
if (!online) { setCarregando(false); return; }
setCarregando(true);
const API = window.ACRApi.audit;
const filtros = { from: de, to: ate, limit: 500 };
if (acao !== "Todas") filtros.action = Object.keys(ROTULO_ACAO).find((k) => ROTULO_ACAO[k] === acao) || acao;
if (sev !== "Todas") filtros.severity = Object.keys(SEVERIDADE).find((k) => SEVERIDADE[k].label === sev);
if (q.trim()) filtros.q = q.trim();
try {
const [r, s, a, e, b] = await Promise.all([API.summary(), API.sessions(), API.access(), API.events(filtros), API.blocks().catch(() => [])]);
setResumo(r); setSessoes(s); setAcessos(a); setEventos(e); setBloqueios(b || []);
} catch (err) {
onToast && onToast({ tone: "danger", title: "Não foi possível carregar a auditoria", message: err.message });
} finally { setCarregando(false); }
}, [de, ate, acao, sev, q, online]);
React.useEffect(() => { carregar(); }, [carregar]);
/** Produtividade carrega sozinha: uma falha aqui nao pode esvaziar as outras abas. */
const [erroProd, setErroProd] = React.useState(null);
React.useEffect(() => {
if (!online) return;
let vivo = true;
setErroProd(null);
window.ACRApi.audit.productivity(janela)
.then((p) => { if (vivo) setProd(p); })
.catch((err) => { if (vivo) { setProd({ dias: janela, linhas: [] }); setErroProd(err.message || "falha ao consultar"); } });
return () => { vivo = false; };
}, [janela, online]);
if (!supervisor) {
return } title="Acesso restrito"
description="O módulo de auditoria é exclusivo do perfil Supervisor." />;
}
const ativos = sessoes.filter((s) => s.ativa);
const acoesDisponiveis = ["Todas"].concat([...new Set((resumo ? resumo.porAcao : []).map((a) => ROTULO_ACAO[a.action] || a.action))]);
const vazio = (t, d) => } title={t} description={d} />;
return (
Auditoria
Sessões, acessos e o registro de tudo que acontece no sistema — visível apenas ao supervisor.
dataHora(r.created_at) },
{ key: "user_name", label: "Usuário" },
{ key: "action", label: "Evento", valor: (r) => ROTULO_ACAO[r.action] || r.action },
{ key: "severity", label: "Gravidade", valor: (r) => (SEVERIDADE[r.severity] || {}).label || r.severity },
{ key: "entity", label: "Tipo" },
{ key: "entity_id", label: "Registro" },
{ key: "description", label: "Descrição" },
{ key: "ip", label: "Origem (IP)" },
]}
linhas={eventos} />
} disabled={carregando} onClick={carregar}>
{carregando ? "Carregando…" : "Atualizar"}
{!online ?
} title="Sem conexão com o servidor"
description="A auditoria só existe no banco de dados — reconecte para consultar." /> : (
<>
} hint="Sessão vista nos últimos 30 min" />
} />
} hint="Exclusões e redefinições de senha" />
} />
b.ativo).length ? " (" + bloqueios.filter((b) => b.ativo).length + ")" : "") },
]} />
{aba === "eventos" ? (
<>
{eventos.length === 0 ? vazio("Nenhum evento encontrado", "Amplie o período ou limpe os filtros.") : (
(
{dataHora(r.created_at)}
{desde(r.created_at)}
) },
{ key: "action", label: "Evento", width: 230, render: (r) => (
{ROTULO_ACAO[r.action] || r.action}
) },
{ key: "user_name", label: "Usuário", width: 170, render: (r) => (r.user_name
?
: Sistema) },
{ key: "description", label: "Descrição", render: (r) => (
{r.description || "—"}
) },
{ key: "severity", label: "Gravidade", width: 120, render: (r) => (
{(SEVERIDADE[r.severity] || {}).label || r.severity}
) },
{ key: "ip", label: "Origem", width: 130, render: (r) => (
{r.ip || "—"}
) },
]} />
)}
>
) : null}
{aba === "sessoes" ? (
{sessoes.length === 0 ? vazio("Nenhuma sessão aberta", "Ninguém está conectado no momento.") : (
(
{r.name}
{r.role_title || (r.profile === "supervisor" ? "Supervisor" : "Operacional")}
) },
{ key: "ativa", label: "Situação", width: 120, render: (r) => (
{r.ativa ? "Ativa" : "Inativa"}
) },
{ key: "started_at", label: "Entrou", width: 150, render: (r) => (
{dataHora(r.started_at)}
) },
{ key: "last_seen_at", label: "Visto por último", width: 150, render: (r) => (
{desde(r.last_seen_at)}
) },
{ key: "auth_type", label: "Acesso", width: 110, render: (r) => (
{r.auth_type === "google" ? "Google" : "Local"}
) },
{ key: "ip", label: "Origem", width: 130, render: (r) => (
{r.ip || "—"}
) },
{ key: "acao", label: "", width: 60, align: "right", render: (r) => (
}
onClick={async () => {
await window.ACRApi.audit.endSessions(r.user_id);
onToast && onToast({ tone: "info", title: "Sessão encerrada", message: r.name + " precisará entrar novamente." });
carregar();
}} />
) },
]} />
)}
) : null}
{aba === "acessos" ? (
{acessos.length === 0 ? vazio("Nenhum colaborador cadastrado", "Cadastre a equipe em Controle de Acesso.") : (
(
{r.name}
{r.username || r.email || "—"}
) },
{ key: "profile", label: "Perfil", width: 130, render: (r) => (
{r.profile === "supervisor" ? "Supervisor" : "Operacional"}
) },
{ key: "online", label: "Agora", width: 100, render: (r) => (
{r.online ? "Online" : "Offline"}
) },
{ key: "last_login_at", label: "Último acesso", width: 200, render: (r) => {
// Sem login gravado, mas com rastro na auditoria: mostra a última atividade.
const ativ = r.ultima_atividade && new Date(r.ultima_atividade).getFullYear() > 1970 ? r.ultima_atividade : null;
const inferido = !r.last_login_at && !!ativ;
const quando = r.last_login_at || ativ;
return (
{dataHora(quando)}
{quando ? desde(quando) + (inferido ? " · por atividade" : "") : "nunca acessou"}
);
} },
{ key: "login_count", label: "Entradas", width: 100, align: "right", render: (r) => (
r.login_count ? {r.login_count}
: {r.n_eventos ? "—" : "0"}
) },
{ key: "last_login_ip", label: "Origem", width: 130, render: (r) => (
{r.last_login_ip || "—"}
) },
{ key: "active", label: "Conta", width: 100, render: (r) => (
{r.active ? "Ativa" : "Inativa"}
) },
]} />
)}
) : null}
{aba === "bloqueios" ? (
{bloqueios.length === 0 ? vazio("Nenhum bloqueio registrado", "Nenhuma estação atingiu o limite de tentativas.") : (
(
{r.ip}
) },
{ key: "ativo", label: "Situação", width: 150, render: (r) => (
{r.ativo ? (r.minutos_restantes > 60 ? "Bloqueado · " + Math.ceil(r.minutos_restantes / 60) + " h" : "Bloqueado · " + r.minutos_restantes + " min") : "Liberado"}
) },
{ key: "strikes", label: "Reincidências", width: 130, align: "right", render: (r) => (
1 ? "var(--text-strong)" : "var(--text-body)" }}>{r.strikes}
) },
{ key: "updated_at", label: "Último bloqueio", width: 190, render: (r) => (
{dataHora(r.updated_at)}
) },
{ key: "last_reason", label: "Motivo", render: (r) => (
{r.last_reason || "—"}
) },
{ key: "acao", label: "", width: 70, align: "right", render: (r) => (
r.ativo ? (
}
onClick={async () => {
await window.ACRApi.audit.releaseBlock(r.ip);
onToast && onToast({ tone: "info", title: "IP liberado", message: r.ip + " pode tentar entrar novamente." });
carregar();
}} />
) : null
) },
]} />
)}
) : null}
{aba === "produtividade" ? (() => {
if (erroProd) return } title="Não foi possível calcular a produtividade" description={erroProd} />;
const linhas = (prod && prod.linhas) || [];
const ativas = linhas.filter((r) => r.active);
const top = ativas.filter((r) => r.concluidas > 0 || r.eventos > 0).slice(0, 5);
const maior = Math.max(1, ...top.map((r) => r.concluidas || 0));
const soma = (k) => ativas.reduce((a, r) => a + (Number(r[k]) || 0), 0);
const pct = (r) => (r.concluidas ? Math.round((r.no_prazo / r.concluidas) * 100) : null);
return (
Janela de apuração — conta apenas o que o sistema registra: entradas, demandas, comentários e anexos.
} hint={"no período de " + janela + " dias"} />
} />
} hint="Comentários, anexos e atualizações" />
!r.entradas).length} tone={ativas.some((r) => !r.entradas) ? "warning" : "neutral"} icon={} hint="Conta ativa, nenhuma entrada no período" />
{top.length === 0 ? vazio("Sem movimento no período", "Nenhuma atividade registrada na janela escolhida.") : (
{top.map((r, i) => (
{i + 1}
{r.name}
{r.concluidas} concluída{r.concluidas === 1 ? "" : "s"}
{pct(r) !== null ? " · " + pct(r) + "% no prazo" : ""}
))}
)}
{linhas.length === 0 ? vazio("Nenhum dado no período", "Escolha uma janela maior ou aguarde o registro de atividades.") : (
(
{r.name}
{r.role_title || r.username || "—"}
) },
{ key: "last_login_at", label: "Último acesso", width: 150, render: (r) => (
{desde(r.last_login_at)}
) },
{ key: "dias_ativos", label: "Dias ativos", width: 110, align: "right", render: (r) => (
{r.dias_ativos}/{janela}
) },
{ key: "entradas", label: "Entradas", width: 95, align: "right", render: (r) => (
{r.entradas}
) },
{ key: "criadas", label: "Abriu", width: 85, align: "right", render: (r) => (
{r.criadas}
) },
{ key: "concluidas", label: "Concluiu", width: 100, align: "right", render: (r) => (
{r.concluidas}
) },
{ key: "no_prazo", label: "No prazo", width: 100, align: "right", render: (r) => (
pct(r) === null ? —
: = 80 ? "success" : pct(r) >= 50 ? "warning" : "danger"} size="sm">{pct(r)}%
) },
{ key: "horas_media", label: "Tempo médio", width: 120, align: "right", render: (r) => (
{r.horas_media ? (Number(r.horas_media) >= 48 ? Math.round(Number(r.horas_media) / 24) + " dias" : Number(r.horas_media) + " h") : "—"}
) },
{ key: "interacoes", label: "Interações", width: 110, align: "right", render: (r) => (
{(Number(r.comentarios) || 0) + (Number(r.anexos) || 0) + (Number(r.atualizacoes) || 0)}
) },
{ key: "em_aberto", label: "Em aberto", width: 100, align: "right", render: (r) => (
{r.em_aberto}
) },
{ key: "active", label: "Conta", width: 95, render: (r) => (
{r.active ? "Ativa" : "Inativa"}
) },
]} />
)}
Estes números medem uso do sistema, não desempenho isolado: uma demanda complexa pesa igual a uma simples, e trabalho feito fora do FlowTix não aparece aqui.
);
})() : null}
>
)}
);
}
Object.assign(window, { AuditScreen });