// Painel SUPERADMIN — visível só pra usuários role=SUPERADMIN.
// Gerencia tipos de perícia (bancária, veicular, tributária, cartão RMC, etc.)
// e versiona prompts/templates/fórmulas.
function AdminPanel() {
const [types, setTypes] = useState([]);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState(null);
const [selectedTypeId, setSelectedTypeId] = useState(null);
const [showNewType, setShowNewType] = useState(false);
const reloadTypes = async () => {
setLoading(true);
setErr(null);
try {
const res = await window.apiFetch('/admin/pericia-types');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setTypes(await res.json());
} catch (e) {
setErr(`Não foi possível carregar tipos de perícia: ${e.message}`);
} finally {
setLoading(false);
}
};
useEffect(() => { reloadTypes(); }, []);
return (
Painel administrativo
Gerencie tipos de perícia e versione prompts, planilhas e fórmulas da metodologia.
setShowNewType(true)}> Nova perícia
{err && (
{err}
)}
{selectedTypeId ? (
{ setSelectedTypeId(null); reloadTypes(); }}
/>
) : (
)}
setShowNewType(false)}
onCreated={() => { setShowNewType(false); reloadTypes(); }}
/>
);
}
// ─── Lista de tipos ───────────────────────────────────────────────────────────
function PericiaTypesList({ types, loading, onOpen }) {
if (loading) {
return (
Carregando tipos de perícia…
);
}
if (!types.length) {
return (
Nenhum tipo de perícia cadastrado.
Clique em "Nova perícia" pra criar o primeiro (ex.: bancária, tributária, cartão RMC).
);
}
return (
<>
{/* Mobile: cards stackados */}
{types.map((t) => (
onOpen(t.id)}
className="w-full text-left px-4 py-3 flex items-start gap-3 active:bg-ink-50"
>
{t.code}
·
{t.active_version_label
? {t.active_version_label}
: sem versão ativa }
·
{fmtDate.format(new Date(t.created_at))}
))}
{/* Desktop: tabela completa */}
Code
Label
Versão ativa
Status
Criada em
{types.map((t) => (
onOpen(t.id)}
className="border-b border-ink-100 last:border-0 hover:bg-ink-50 cursor-pointer transition-colors"
>
{t.code}
{t.label}
{t.active_version_label
? {t.active_version_label}
: (nenhuma ativa) }
{t.status}
{fmtDate.format(new Date(t.created_at))}
))}
>
);
}
// ─── Modal: criar tipo de perícia ─────────────────────────────────────────────
function NewPericiaTypeModal({ open, onClose, onCreated }) {
const [code, setCode] = useState('');
const [label, setLabel] = useState('');
const [description, setDescription] = useState('');
const [submitting, setSubmitting] = useState(false);
const [err, setErr] = useState(null);
useEffect(() => {
if (open) { setCode(''); setLabel(''); setDescription(''); setErr(null); }
}, [open]);
const submit = async (e) => {
e.preventDefault();
if (!code.match(/^[a-z][a-z0-9_]*$/)) {
setErr('Code precisa começar com letra minúscula e usar só letras, números e _');
return;
}
if (!label.trim()) { setErr('Informe um label.'); return; }
setSubmitting(true);
setErr(null);
try {
const res = await window.apiFetch('/admin/pericia-types', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: code.trim(), label: label.trim(), description: description.trim() || null }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.detail || `HTTP ${res.status}`);
}
onCreated();
} catch (e2) {
setErr(e2.message);
} finally {
setSubmitting(false);
}
};
return (
);
}
// ─── Detalhe do tipo: lista versões + upload ──────────────────────────────────
function PericiaTypeDetail({ typeId, onBack }) {
const [versions, setVersions] = useState([]);
const [type, setType] = useState(null);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState(null);
const [showUpload, setShowUpload] = useState(false);
const [showVersion, setShowVersion] = useState(null);
const reload = async () => {
setLoading(true);
setErr(null);
try {
const types = await (await window.apiFetch('/admin/pericia-types')).json();
setType(types.find((t) => t.id === typeId) || null);
const v = await (await window.apiFetch(`/admin/pericia-types/${typeId}/versions`)).json();
setVersions(v);
} catch (e) {
setErr(`Falha ao carregar: ${e.message}`);
} finally { setLoading(false); }
};
useEffect(() => { reload(); }, [typeId]);
const activate = async (vid) => {
if (!confirm('Ativar esta versão? A versão atualmente ativa será deprecada automaticamente.')) return;
try {
const res = await window.apiFetch(`/admin/pericia-types/${typeId}/versions/${vid}/activate`, { method: 'POST' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
reload();
} catch (e) { alert(`Falha: ${e.message}`); }
};
const deprecate = async (vid) => {
if (!confirm('Deprecar esta versão? Análises futuras não a usarão mais.')) return;
try {
const res = await window.apiFetch(`/admin/pericia-types/${typeId}/versions/${vid}/deprecate`, { method: 'POST' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
reload();
} catch (e) { alert(`Falha: ${e.message}`); }
};
if (loading) {
return (
Carregando…
);
}
return (
Voltar
{type?.label || typeId}
{type?.code}
{type?.description &&
{type.description}
}
setShowUpload(true)}> Nova versão
{err && (
{err}
)}
{/* Mobile: cards stackados */}
{versions.length === 0 && (
Nenhuma versão. Suba a primeira clicando em "Nova versão".
)}
{versions.map((v) => (
{v.version_label}
{v.status}
{v.activated_at
? <>Ativada {fmtDateTime.format(new Date(v.activated_at))}>
: <>Criada {fmtDateTime.format(new Date(v.created_at))}>}
setShowVersion(v.id)} className="text-navy-800 active:underline">Inspecionar
{v.status !== 'ACTIVE' && (
activate(v.id)} className="text-ok-700 active:underline">Ativar
)}
{v.status === 'ACTIVE' && (
deprecate(v.id)} className="text-danger-700 active:underline">Deprecar
)}
))}
{/* Desktop: tabela */}
Versão
Status
Ativada em
Criada em
Ações
{versions.length === 0 && (
Nenhuma versão. Suba a primeira clicando em "Nova versão".
)}
{versions.map((v) => (
{v.version_label}
{v.status}
{v.activated_at ? fmtDateTime.format(new Date(v.activated_at)) : — }
{fmtDateTime.format(new Date(v.created_at))}
setShowVersion(v.id)} className="text-[12.5px] text-navy-800 hover:underline font-medium">Inspecionar
{v.status !== 'ACTIVE' && (
activate(v.id)} className="text-[12.5px] text-ok-700 hover:underline font-medium">Ativar
)}
{v.status === 'ACTIVE' && (
deprecate(v.id)} className="text-[12.5px] text-danger-700 hover:underline font-medium">Deprecar
)}
))}
setShowUpload(false)}
onUploaded={() => { setShowUpload(false); reload(); }}
/>
setShowVersion(null)}
/>
);
}
// ─── Modal: upload nova versão ────────────────────────────────────────────────
function UploadVersionModal({ open, typeId, onClose, onUploaded }) {
const [versionLabel, setVersionLabel] = useState('');
const [notes, setNotes] = useState('');
const [files, setFiles] = useState({
prompt: null,
extraction_tools: null,
opinion_template: null,
calculation: null,
spreadsheet: null,
});
const [submitting, setSubmitting] = useState(false);
const [err, setErr] = useState(null);
useEffect(() => {
if (open) {
setVersionLabel(''); setNotes(''); setErr(null);
setFiles({ prompt: null, extraction_tools: null, opinion_template: null, calculation: null, spreadsheet: null });
}
}, [open]);
const slots = [
{ key: 'prompt', label: 'prompt.md', hint: 'System prompt da metodologia (texto em Markdown).' },
{ key: 'extraction_tools', label: 'extraction_tools.json', hint: 'JSONSchema da tool de extração que o Claude vai chamar.' },
{ key: 'opinion_template', label: 'opinion_template.md', hint: 'Template Jinja2 do texto de considerações no painel PDF.' },
{ key: 'calculation', label: 'calculation.yaml', hint: 'Espelho declarativo das fórmulas (referência/audit).' },
{ key: 'spreadsheet', label: 'spreadsheet.xlsx', hint: 'Planilha-modelo (com 11+ inputs editáveis e fórmulas preservadas).' },
];
const allFilesIn = slots.every((s) => files[s.key]);
const submit = async (e) => {
e.preventDefault();
if (!versionLabel.trim()) { setErr('Informe a versão (ex.: v1.0.0).'); return; }
if (!allFilesIn) { setErr('Anexe os 5 arquivos pra subir a versão.'); return; }
setSubmitting(true);
setErr(null);
try {
const fd = new FormData();
fd.append('version_label', versionLabel.trim());
if (notes.trim()) fd.append('notes', notes.trim());
for (const s of slots) fd.append(s.key, files[s.key]);
const res = await window.apiFetch(`/admin/pericia-types/${typeId}/versions`, {
method: 'POST',
body: fd,
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.detail || `HTTP ${res.status}`);
}
onUploaded();
} catch (e2) {
setErr(e2.message);
} finally { setSubmitting(false); }
};
return (
setVersionLabel(e.target.value)}
placeholder="v1.0.0"
className="font-mono"
autoFocus
/>
setNotes(e.target.value)}
placeholder="ex.: ajuste em B.9 + atualização Bacen 2026-05"
/>
{err && (
{err}
)}
Cancelar
{submitting ? 'Enviando…' : 'Subir versão (como DRAFT)'}
);
}
// ─── Modal: inspecionar versão ───────────────────────────────────────────────
function InspectVersionModal({ versionId, typeId, onClose }) {
const [data, setData] = useState(null);
const [tab, setTab] = useState('prompt');
const [err, setErr] = useState(null);
useEffect(() => {
if (!versionId) { setData(null); return; }
(async () => {
setErr(null);
try {
const res = await window.apiFetch(`/admin/pericia-types/${typeId}/versions/${versionId}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setData(await res.json());
} catch (e) { setErr(e.message); }
})();
}, [versionId, typeId]);
const open = !!versionId;
const tabs = [
{ id: 'prompt', label: 'prompt.md' },
{ id: 'tools', label: 'extraction_tools.json' },
{ id: 'opinion', label: 'opinion_template.md' },
{ id: 'calc', label: 'calculation.yaml' },
];
let body = '';
if (data) {
body = tab === 'prompt' ? data.system_prompt_md
: tab === 'tools' ? JSON.stringify(data.extraction_tools, null, 2)
: tab === 'opinion' ? data.opinion_template_md
: tab === 'calc' ? data.calculation_yaml
: '';
}
return (
{tabs.map((t) => (
setTab(t.id)}
className={`px-3 py-2 text-[12.5px] font-medium border-b-2 transition-colors whitespace-nowrap shrink-0 ${
tab === t.id ? 'border-navy-800 text-navy-800' : 'border-transparent text-ink-500 hover:text-ink-900'
}`}
>{t.label}
))}
{err &&
{err}
}
{!data && !err &&
Carregando…
}
{data && (
{body}
)}
Fechar
);
}
window.AdminPanel = AdminPanel;