const PICONS = { dashboard: '', patients: '', settings: '', sessions: '', search: '', plus: '', check: '', checkCircle: '', circle: '', chevR: '', chevD: '', chevL: '', arrowR: '', x: '', trash: '', grip: '', play: '', user: '', briefcase: '', arrowDown: '', arrowUp: '', minus: '', info: '', alert: '', logout: '', clock: '', calendar: '', headset: '', send: '', copy: '', edit: '', archive: '', restore: '', spark: '', eye: '', speech: '', filter: '', dots: '', shield: '', pulse: '', sun: '', moon: '', menu: '', unlock: '', // Fonte: Health Icons (healthicons.org / resolvetosavelives/healthicons), licença MIT/CC0. // Pictogramas preenchidos (não stroke-line como o restante do registro); por isso vêm // envolvidos em para não herdar o stroke do pai. pelvis: '', joints: '', diagnosis: '', }; function Icon({ name, size = 18, stroke = 2, style, className }) { return ( ); } const STATUS_LABELS = { draft: "Rascunho", published: "Publicado", archived: "Arquivado" }; function Badge({ status, children }) { return {children || STATUS_LABELS[status] || status}; } function Btn(props) { const { kind = "", size = "", icon, iconR, children, type, disabled, onClick, style, autoFocus } = props; return ( ); } function Tag({ kind = "", icon, children }) { return {icon && }{children}; } function ThemeToggle({ theme, onToggle }) { const dark = theme === "dark"; return ( ); } function NavToggle({ onClick }) { return ( ); } function Wordmark({ size }) { return NAZIDE PRESSUS; } function BrandGlyph({ size = 22, color }) { return ( ); } function ProlapseGlyph({ grade, on }) { const map = { ausente: 26, grau1: 31, grau2: 37, grau3: 43, grau4: 50 }; const cy = map[grade] ?? 26; const sev = grade === "ausente"; const col = on ? "var(--accent)" : (sev ? "var(--text-faint)" : "var(--anat)"); return ( ); } function DiastaseGlyph({ level, on }) { const gapMap = { ausente: 4, leve: 9, moderada: 15, grave: 22 }; const g = gapMap[level] ?? 4; const sev = level === "ausente"; const col = on ? "var(--accent)" : (sev ? "var(--text-faint)" : "var(--anat)"); const cx = 28; return ( ); } function Toast({ msg, onDone }) { React.useEffect(() => { const id = setTimeout(onDone, 2600); return () => clearTimeout(id); }, []); return (
{msg}
); } /* --- Campos de formulário ------------------------------------------------ Vieram de `view_patient_form.jsx` em 2026-07-30, onde eram locais, sem alteração de comportamento. O formulário de usuário precisa dos mesmos campos, e um segundo conjunto divergiria do primeiro na primeira correção feita só de um lado. */ function Stepper({ value, onChange, min = 0, max = 99 }) { const current = Number.isFinite(Number(value)) ? Number(value) : min; const update = (next) => onChange(Math.max(min, Math.min(max, next))); const handleInput = (raw) => { if (raw === "") { onChange(""); return; } onChange(raw); }; return (
handleInput(e.target.value)} onBlur={() => update(value === "" ? min : Number(value))} aria-label="Valor numerico" />
); } function SegBinary({ value, on = "Sim", off = "Não", onChange, fill, disabled }) { return (
); } function CheckRow({ on, onClick, children }) { return ( ); } function RadioRow({ on, onClick, children }) { return ( ); } function FSelect({ label, opt, value, onChange, options, lead, disabled }) { const id = React.useId(); return (
{label && }
{lead && }
); } function formatDate(value) { const digits = String(value || "").replace(/\D/g, "").slice(0, 8); if (digits.length <= 2) return digits; if (digits.length <= 4) return digits.slice(0, 2) + "/" + digits.slice(2); return digits.slice(0, 2) + "/" + digits.slice(2, 4) + "/" + digits.slice(4); } function FInput({ label, opt, required, value, onChange, placeholder, mask, error, type }) { const id = React.useId(); return (
{label && }
onChange(mask === "date" ? formatDate(e.target.value) : e.target.value)} />
{error &&
{error}
}
); } // Botão de ação de linha em tabela. Veio de `view_patients.jsx`, onde era // local, quando a tela de usuários passou a precisar dos mesmos. function ActionIcon({ icon, label, onClick, go, disabled }) { return ; } /* --- Notificação e paginação --------------------------------------------- Cards `notification` e `pagination` do design system, sincronizados em 2026-07-30. */ function InlineNote({ kind = "info", title, children, onClose }) { const icone = { error: "alert", warning: "alert", success: "check", info: "info" }[kind] || "info"; return (
{title &&
{title}
} {children &&
{children}
}
{onClose && }
); } // RF-AUDITORIA-15. Variante à parte da nota de erro, e não a mesma com outro // texto: é a única classe de operação sem volta, e a interface deve custar a // ser atravessada. O botão de prosseguir fica a cargo de quem usa. function AlertaVermelho({ children, ciente, onCiente }) { return (
Alerta vermelho
{children}
); } function Pagination({ total, skip, limit, onSkip, onLimit, opcoes = [25, 50, 100] }) { const pagina = Math.floor(skip / limit) + 1; const paginas = Math.max(1, Math.ceil(total / limit)); const primeiro = total === 0 ? 0 : skip + 1; const ultimo = Math.min(skip + limit, total); return (
{onLimit && ( <> Itens por página: )} {total === 0 ? "0 itens" : `${primeiro}–${ultimo} de ${total.toLocaleString("pt-BR")} itens`}
Página {pagina} de {paginas} {/* Desabilitado, nunca oculto: esconder move os botões de lugar a cada navegação. */}
); } const PUBLICATION_FIELD_LABELS = { nome: "Nome ou identificação do caso", ref: "Referência", "data.pessoais.dataNascimento": "Data de nascimento", "data.hda.historia": "Queixa principal", "data.diagnostico.queixaClassificada": "Classificação da queixa principal", "data.diagnostico.diagnosticoFisioterapeutico": "Diagnóstico fisioterapêutico", "data.simulador.manometriaNivel": "Nível de manometria", data: "Dados do caso", }; const PUBLICATION_ISSUE_MESSAGES = { "nome:placeholder": "Informe um nome válido.", "ref:placeholder": "Informe uma referência válida.", "data.pessoais.dataNascimento:invalid_date": "Informe uma data válida no formato DD/MM/AAAA.", "data.hda.historia:required": "Preencha a queixa principal.", "data.diagnostico.queixaClassificada:required": "Selecione uma classificação diagnóstica.", "data.diagnostico.diagnosticoFisioterapeutico:required": "Preencha o diagnóstico fisioterapêutico.", "data.simulador.manometriaNivel:out_of_range": "Selecione um nível entre 1 e 5.", "data:unity_serialization": "Não foi possível preparar o caso para o simulador.", }; const API_ERROR_MESSAGES = { case_publication_invalid: "O caso possui campos obrigatórios pendentes.", invalid_case_transition: "Esta ação não está disponível para o estado atual do caso.", case_not_editable: "Este caso está somente para leitura.", fala_nao_aplicavel: "Este item não se aplica aos dados atuais do caso.", fala_sem_dados_suficientes: "Preencha ao menos um campo relacionado a este item antes de gerar a fala.", geracao_falhou: "Não foi possível gerar a fala com o provedor de IA. Tente novamente.", }; function publicationIssueLabel(issue) { return PUBLICATION_FIELD_LABELS[issue.path] || "Campo do caso"; } function publicationIssueMessage(issue) { return PUBLICATION_ISSUE_MESSAGES[issue.path + ":" + issue.code] || (issue.code === "required" ? "Preencha este campo." : "Revise o valor informado."); } function userErrorMessage(error) { if (!error) return ""; if (typeof error === "string") return error; return API_ERROR_MESSAGES[error.code] || "Não foi possível concluir a solicitação. Tente novamente."; } function PublicationIssueList({ issues = [] }) { if (!issues.length) return null; return (
    {issues.map((issue, index) => (
  • {publicationIssueLabel(issue)}: {publicationIssueMessage(issue)}
  • ))}
); } // Avisos não bloqueantes: o caso pode ser publicado, mas há algo a conferir. // A mensagem vem pronta do backend (pt-BR), diferente dos erros bloqueantes, // que são traduzidos por publicationIssueMessage a partir de path + code. function PublicationWarningList({ warnings = [] }) { if (!warnings.length) return null; return (
Atenção
    {warnings.map((warning, index) => (
  • {warning.message}
  • ))}
); } function ConfirmModal({ open, title, children, confirmLabel, confirmIcon, busy, error, onConfirm, onClose }) { React.useEffect(() => { if (!open) return; const onKeyDown = (event) => { if (event.key === "Escape" && !busy) onClose(); }; document.addEventListener("keydown", onKeyDown); return () => document.removeEventListener("keydown", onKeyDown); }, [open, busy, onClose]); if (!open) return null; const titleId = "confirm-modal-title"; return (
!busy && onClose()}>
event.stopPropagation()}>

{title}

{children} {error && (
{userErrorMessage(error)}
)}
Cancelar {busy ? "Processando..." : confirmLabel}
); } // RF-AUTH-11: diálogo de troca obrigatória, sem caminho de dispensa. // // O `ConfirmModal` acima fecha por `Esc`, por clique no overlay e sempre oferece // "Cancelar" — as três saídas que este diálogo não pode ter. Não é o mesmo // componente com uma opção a menos: a ausência de saída é a razão de ele // existir, e uma opção `dispensavel={false}` no ConfirmModal deixaria a saída a // um descuido de distância em qualquer chamada futura. function ModalSemSaida({ title, children, footer }) { React.useEffect(() => { const bloquear = (event) => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); } }; document.addEventListener("keydown", bloquear, true); return () => document.removeEventListener("keydown", bloquear, true); }, []); const titleId = React.useId(); return (

{title}

{children}
{footer}
); } Object.assign(window, { PICONS, Icon, Badge, Btn, Tag, ThemeToggle, NavToggle, Wordmark, BrandGlyph, ProlapseGlyph, DiastaseGlyph, Toast, ConfirmModal, ModalSemSaida, PublicationIssueList, PublicationWarningList, publicationIssueMessage, userErrorMessage, STATUS_LABELS, Stepper, SegBinary, CheckRow, RadioRow, FSelect, FInput, formatDate, ActionIcon, InlineNote, AlertaVermelho, Pagination, });