// Stepper, SegBinary, CheckRow, RadioRow, FSelect, FInput e formatDate moraram // aqui até 2026-07-30. Foram para `ui-portal.jsx` sem alteração de // comportamento, porque o formulário de usuário (`view_users.jsx`) precisa dos // mesmos campos — e um segundo conjunto divergiria do primeiro na primeira // correção que alguém fizesse só de um lado. function MultiCheck({ value, options, onChange }) { const selected = value || []; const toggle = (code) => onChange(selected.includes(code) ? selected.filter(item => item !== code) : [...selected, code]); return
{options.map(([code, label]) => toggle(code)}>{label})}
; } function calcularIdade(dataNascimento) { const match = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(dataNascimento); if (!match) return null; const [, dia, mes, ano] = match.map(Number); const nascimento = new Date(ano, mes - 1, dia); if (nascimento.getFullYear() !== ano || nascimento.getMonth() !== mes - 1 || nascimento.getDate() !== dia) return null; const hoje = new Date(); if (nascimento > hoje) return null; return hoje.getFullYear() - ano - (hoje < new Date(hoje.getFullYear(), mes - 1, dia) ? 1 : 0); } const OXFORD_CAP = ["Ausência de contração perceptível", "Esboço de contração, não sustentável (flicker)", "Contração fraca, sem elevação perineal", "Contração moderada com elevação perineal presente", "Contração boa com elevação contra resistência parcial", "Contração forte com elevação contra resistência total"]; function imcFlag(v) { const n = parseFloat(String(v).replace(",", ".")); if (isNaN(n)) return null; if (n < 18.5) return ["Baixo peso", "imc-over"]; if (n < 25) return ["Eutrófico", "imc-normal"]; if (n < 30) return ["Sobrepeso", "imc-over"]; return ["Obesidade", "imc-over"]; } function calcularImc(peso, altura) { const pesoNumero = Number(String(peso).replace(",", ".")); const alturaNumero = Number(String(altura).replace(",", ".")); if (!Number.isFinite(pesoNumero) || !Number.isFinite(alturaNumero) || pesoNumero <= 0 || alturaNumero <= 0) return ""; return (pesoNumero / (alturaNumero * alturaNumero)).toFixed(1).replace(".", ","); } function calcularMaiorPesoRN(partos) { const pesos = (partos || []).map((parto) => String(parto.pesoRN || "").replace(",", ".").replace(/[^\d.]/g, "")) .filter(Boolean).map(Number).filter(Number.isFinite); if (!pesos.length) return ""; return pesos.reduce((maior, peso) => Math.max(maior, peso), 0).toLocaleString("pt-BR", { minimumFractionDigits: 1, maximumFractionDigits: 2 }) + " kg"; } const ESTADO_CIVIL = [["solteira", "Solteira"], ["casada", "Casada"], ["uniao", "União estável"], ["divorciada", "Divorciada"], ["viuva", "Viúva"]]; const ESCOLARIDADE = [["fundamental", "Ensino fundamental"], ["medio", "Ensino médio"], ["superior", "Ensino superior"], ["pos", "Pós-graduação"]]; const COR_RACA = [["branca", "Branca"], ["preta", "Preta"], ["parda", "Parda"], ["amarela", "Amarela"], ["indigena", "Indígena"]]; const BRISTOL = [["1", "Tipo 1 · cíbalos duros"], ["2", "Tipo 2 · grumoso"], ["3", "Tipo 3 · fissurado"], ["4", "Tipo 4 · liso e macio"], ["5", "Tipo 5 · fragmentos macios"], ["6", "Tipo 6 · pastoso"], ["7", "Tipo 7 · líquido"]]; const CARATER_ESCAPE = [["jato", "Jato"], ["gota", "Gota"], ["insensivel", "Insensível (sem perceber)"]]; const DIU = [["nao", "Não"], ["cobre", "Cobre"], ["prata", "Prata"], ["mirena", "Mirena"]]; const TIPO_PARTO = [["normal", "Normal"], ["cesarea", "Cesárea"], ["forceps", "Fórceps"], ["vacuo", "Vácuo"]]; // Item 2.2 da ficha — ordem e itens idênticos (21 sintomas). const SINTOMAS_GERAIS = [ ["iue", "Incontinência urinária de esforço (IUE)"], ["if", "Incontinência fecal (IF)"], ["ia", "Incontinência anal (IA)"], ["urgenciaEvac", "Urgência para evacuar"], ["ifUrgencia", "IF por urgência"], ["soiling", "Soiling"], ["frequenciaEvac", "Aumento da frequência evacuatória"], ["flatosAnais", "Flatos anais involuntários"], ["flatosVaginais", "Flatos vaginais involuntários"], ["frequenciaUrinaria", "Aumento da frequência urinária"], ["urgenciaMiccional", "Urgência miccional"], ["iuUrgencia", "IU por urgência"], ["pesoVaginal", "Sensação de peso vaginal / pressão pélvica"], ["nocturia", "Noctúria"], ["enurese", "Enurese noturna"], ["hesitacao", "Hesitação miccional"], ["fluxoIntercortado", "Fluxo urinário intercortado"], ["dorRelacao", "Dor durante a relação sexual"], ["esvaziamentoIncompleto", "Esvaziamento vesical incompleto"], ["gotejamento", "Gotejamento pós-miccional"], ["constipacao", "Constipação intestinal"], ]; const GATILHOS_IUE = [["salto", "Salto / impacto"], ["corrida", "Corrida"], ["danca", "Dança"], ["gargalhada", "Gargalhada"], ["tosse", "Tosse"], ["espirro", "Espirro"], ["ginastica", "Ginástica"], ["caminhada", "Caminhada"], ["posicao", "Troca de posição"], ["relacao", "Relação sexual"]]; const GATILHOS_URGENCIA = [["aguaFria", "Contato com água fria"], ["torneira", "Barulho de torneira"], ["frio", "Frio"], ["estresse", "Estresse emocional"]]; const SINTOMAS_ANORRETAIS = [["esforco", "Esforço evacuatório"], ["esvaziamento", "Sensação de esvaziamento retal incompleto"], ["dor", "Dor à evacuação"], ["sangue", "Sangue nas fezes"], ["manobras", "Manobras evacuatórias (digital ou pressão perineal)"], ["urgencia", "Urgência fecal"], ["ifUrgencia", "IF por urgência"], ["soiling", "Soiling (sujidade involuntária)"], ["flatos", "Flatos anais involuntários"], ["plicoma", "Plicoma anal"], ["hemorroida", "Hemorroida"]]; const ESCAPE_FECAL = [["", "Não se aplica"], ["liquida", "Líquidas"], ["pastosa", "Pastosas"], ["solida", "Sólidas"]]; const COMORBIDADES = [["diabetes", "Diabetes mellitus"], ["hipertensao", "Hipertensão arterial"], ["obesidade", "Obesidade"], ["ansiedade", "Depressão / ansiedade"], ["neurologica", "Doença neurológica"], ["cancer", "Câncer"], ["tvp", "Trombose venosa profunda (TVP)"], ["coagulopatia", "Hemofilia / coagulopatia"], ["hepatite", "Hepatite"], ["renal", "Doença renal"], ["cardiaca", "Doença cardíaca"], ["epilepsia", "Epilepsia"], ["respiratoria", "Bronquite / DPOC"], ["rinite", "Rinite / sinusite"], ["ciatico", "Ciático / radiculopatia"], ["ist", "IST / DST"], ["refluxo", "Refluxo gastroesofágico"], ["dermica", "Patologia dérmica"]]; const INVESTIGACOES = [["fluxometria", "Fluxometria com eletromiografia"], ["urodinamica", "Urodinâmica completa"], ["urocultura", "Sumário de urina / urocultura"], ["citobacteriologico", "Exame citobacteriológico / pH vaginal"], ["usg", "Ultrassonografia pélvica"], ["rm", "Ressonância magnética da pelve"]]; const DOR_REGIOES = [["lombar", "Dor lombar"], ["pubica", "Dor púbica"], ["baixoVentre", "Dor em baixo ventre"], ["mmii", "Dor em membros inferiores"], ["sacralCoccigea", "Dor sacral / coccígea"]]; const POSTURAL_ACHADOS = [["retificacaoLombar", "Retificação lombar"], ["hiperlordoseLombar", "Hiperlordose lombar"], ["hipercifoseToracica", "Hipercifose torácica"], ["anteversaoPelvica", "Anteversão pélvica"], ["retroversaoPelvica", "Retroversão pélvica"], ["escoliose", "Escoliose"], ["semiflexaoJoelhos", "Semiflexão de joelhos"], ["hiperextensaoJoelhos", "Hiperextensão de joelhos"], ["assimetriaOmbros", "Assimetria de ombros"]]; const MUSCULOS = [["iliopsoas", "Iliopsoas"], ["tfl", "Tensor da fáscia lata (TFL)"], ["gluteoMaximo", "Glúteo máximo"], ["gluteoMedio", "Glúteo médio"], ["adutores", "Adutores"], ["isquiotibiais", "Ísquiotibiais"]]; const MRC = [["", "—"], ["0", "0"], ["1", "1"], ["2", "2"], ["3", "3"], ["4", "4"], ["5", "5"]]; const TESTES_ESPECIAIS = [["diastase", "Avaliação da diástase abdominal", true], ["thomas", "Teste de Thomas", false], ["ober", "Teste de Ober", false], ["extensibilidadeAdutores", "Extensibilidade passiva dos adutores", false]]; const RESULTADO_TESTE = [["", "Selecione"], ["positivo", "Positivo"], ["negativo", "Negativo"], ["naoRealizado", "Não realizado"]]; const INSPECAO_ITENS = [ ["coloracao", "Coloração da pele", [["normal", "Normal"], ["alterada", "Alterada"]]], ["eritema", "Eritema", [["ausente", "Ausente"], ["presente", "Presente"]]], ["feridas", "Feridas / fissura vaginal ou anal", [["ausente", "Ausentes"], ["presente", "Presentes"]]], ["prolapso", "Prolapso de órgão pélvico", [["ausente", "Ausente"], ["paredeAnterior", "Parede anterior"], ["paredePosterior", "Parede posterior"], ["cupula", "Cúpula"]]], ["atrofia", "Atrofia vulvovaginal / ressecamento", [["ausente", "Ausente"], ["presente", "Presente"]]], ["secrecao", "Secreção vaginal", [["ausente", "Ausente"], ["presente", "Presente"]]], ["aberturaVulvogenital", "Abertura vulvogenital", [["normal", "Normal"], ["aumentada", "Aumentada"], ["diminuida", "Diminuída"]]], ]; const REFLEXOS_ITENS = [["cutaneoAnal", "Cutâneo-anal"], ["clitoridiano", "Clitoridiano"], ["bulbocavernoso", "Bulbocavernoso"], ["tosseReflexa", "Tosse (contração reflexa do AP)"]]; const REFLEXO_OPTS = [["", "Selecione"], ["presente", "Presente"], ["ausente", "Ausente"], ["hiperativo", "Hiperativo"]]; const PERINEAL_ITENS = [ ["centroTendineo", "Centro tendíneo — palpação", [["", "Selecione"], ["normal", "Normal"], ["encurtado", "Encurtado"], ["doloroso", "Doloroso"]]], ["contracaoVoluntaria", "Contração voluntária (percepção)", [["", "Selecione"], ["presente", "Presente"], ["ausente", "Ausente"], ["invertida", "Invertida"]]], ["relaxamentoVoluntario", "Relaxamento voluntário", [["", "Selecione"], ["completo", "Completo"], ["parcial", "Parcial"], ["ausente", "Ausente"]]], ["contracaoInvoluntariaTosse", "Contração involuntária (tosse)", [["", "Selecione"], ["presente", "Presente"], ["ausente", "Ausente"], ["atrasada", "Atrasada"]]], ["relaxamentoInvoluntarioValsalva", "Relaxamento involuntário (Valsalva)", [["", "Selecione"], ["presente", "Presente"], ["ausente", "Ausente"]]], ["descensoPerineal", "Descenso perineal (Valsalva)", [["", "Selecione"], ["presente", "Presente"], ["ausente", "Ausente"]]], ]; const RELOGIO_POSICOES = [["3h", "3 h (direita)"], ["5h", "5 h (direita)"], ["7h", "7 h (esquerda)"], ["9h", "9 h (esquerda)"]]; const OBTURADOR_OPTS = [["", "Selecione"], ["normal", "Normal"], ["tensao", "Tensão"], ["dor", "Dor"], ["espasmo", "Espasmo"]]; const TIMING_OPTS = [["", "Selecione"], ["adequado", "Adequado"], ["ausente", "Ausente"], ["tardio", "Tardio"]]; const LINHA_ALBA = [["", "Selecione"], ["preservada", "Preservada"], ["comprometida", "Comprometida"]]; const ASPECTOS_SEXUAIS = [["frequencia", "Frequência das relações"], ["desejo", "Desejo sexual presente"], ["lubrificacao", "Lubrificação presente"], ["usaLubrificante", "Uso de lubrificante artificial"], ["prazer", "Prazer durante a relação"], ["orgasmo", "Orgasmo presente"]]; const MANO = [["11.2", 1], ["21.2", 2], ["32.4", 3], ["47.0", 4], ["61.3", 5]].map(([v, n]) => ({ n, v })); const QUEIXA_CLASS = [ ["iue", "IUE — Incontinência Urinária de Esforço"], ["iu", "IU — Incontinência por Urgência"], ["ium", "IUM — Incontinência Urinária Mista"], ["naoSeAplica", "Não se aplica"], ]; // Espelha 1:1 o CaseData do backend (schemas/case.py), que por sua vez espelha // a Ficha de Avaliação Fisioterapêutica Pélvica da contratante (Ficha-Caso01-v2). function blankCase() { return { nome: "", ref: "CASO-NOVO", pessoais: { idade: "", dataNascimento: "", dataAvaliacao: "", estadoCivil: "casada", escolaridade: "medio", corRaca: "parda", profissao: "", medicoResponsavel: "", diagnosticoMedico: "", peso: "", altura: "", imc: "", atividadeFisica: false, atividadeFisicaQual: "", }, antecedentes: { familiar: "", gineco: { gravidezes: 0, partos: 0, partosNormais: 0, cesarianas: 0, abortos: 0, forceps: 0, contE: 0, contL: 0, contS: 0, pesoRN: "", complicacoes: "", partosDetalhados: [], }, }, ginecologico: { itu: "", vaginites: "", dataPapanicolau: "", menstruacaoRegular: null, dum: "", menopausa: null, menopausaDesde: "", reposicaoHormonal: null, diu: "nao", cuidadosVulvares: "" }, saude: { medicamentos: "", cirurgiasPrevias: "", comorbidades: [] }, qualidadeVida: { evaImpacto: null, instrumento: "", escoreInstrumento: "", reavaliacao: "", investigacoesComplementares: [], achadosComplementares: "" }, hda: { historia: "", tempoQueixa: "", tratamentoAnterior: null, tratamentoAnteriorQual: "", sintomasGerais: [], dorVesical: null }, digestivo: { evacuacaoHabitual: "", bristol: "4", usaBanquinho: null, sintomasAnorretais: [], inicioSintomas: "", situacaoDesencadeante: "", escapeFecalConsistencia: "", doencaIntestinal: null, doencaIntestinalQual: "", balonete: { indicada: null, limiarSensibilidade: "", primeiroDesejo: "", urgencia: "", capacidadeMaxima: "" }, }, urinario: { incontinencia: false, caraterEscape: "", gatilhosIUE: [], gatilhosUrgencia: {} }, sexual: { ativa: true, idadeInicio: null, tempoInativa: "", motivoInativa: "", dorRelacao: false, inicioDor: "", tipoDor: "", frequencia: { presente: null, obs: "" }, desejo: { presente: null, obs: "" }, lubrificacao: { presente: null, obs: "" }, usaLubrificante: { presente: null, obs: "" }, prazer: { presente: null, obs: "" }, orgasmo: { presente: null, obs: "" }, }, musculoesqueletica: { quedasFraturas: "", dores: {} }, exameFisico: { sinaisVitais: { pa: "", fc: "", fr: "" }, postural: { achados: [], observacoes: "" }, abdominal: { padraoRespiratorio: "", flacidez: "", cicatrizes: "", aderencias: "", massaPalpavel: "", diastase: { supra: { largura: "", profundidade: "" }, umbilical: { largura: "", profundidade: "" }, infra: { largura: "", profundidade: "" } }, linhaAlba: "", linhaAlbaObs: "", }, muscular: {}, testesEspeciais: {}, inspecao: { coloracao: "normal", eritema: "ausente", feridas: "ausente", prolapso: "ausente", atrofia: "ausente", secrecao: "ausente", aberturaVulvogenital: "normal" }, reflexos: { cutaneoAnal: "", clitoridiano: "", bulbocavernoso: "", tosseReflexa: "", testeTosse: "", sensibilidadeExterna: "" }, perinealFuncional: { centroTendineo: "", contracaoVoluntaria: "", relaxamentoVoluntario: "", contracaoInvoluntariaTosse: "", relaxamentoInvoluntarioValsalva: "", descensoPerineal: "" }, tonusOxford: null, espasmosSuperficiais: "", espasmosProfundos: "", triggerPoints: "", relogio: {}, }, perfect: { power: null, enduranceSegundos: null, repeticoes: null, fastPorSegundo: null, elevacao: null, timing: "", obturadorDireito: "", obturadorEsquerdo: "", }, simulador: { manometriaNivel: 3 }, diagnostico: { queixaClassificada: "naoSeAplica", diagnosticoFisioterapeutico: "", prognostico: "", plano: { objetivos: "", recursos: "", frequenciaDuracao: "" }, contraEletro: { implanteMetal: false, diuCobrePrata: false, tvp: false, marcapasso: false }, }, referencias: "", anotacoes: "", }; } function deepMerge(base, over) { const out = JSON.parse(JSON.stringify(base)); if (!over || typeof over !== "object") return out; for (const k in over) { const ov = over[k]; if (ov && typeof ov === "object" && !Array.isArray(ov) && out[k] && typeof out[k] === "object") out[k] = deepMerge(out[k], ov); else if (ov !== undefined) out[k] = ov; } return out; } function caseFormSnapshot(nome, form) { const normalized = JSON.parse(JSON.stringify(form)); const idade = calcularIdade(normalized.pessoais.dataNascimento); normalized.pessoais.idade = idade === null ? "" : idade; normalized.pessoais.imc = calcularImc(normalized.pessoais.peso, normalized.pessoais.altura); normalized.antecedentes.gineco.pesoRN = calcularMaiorPesoRN(normalized.antecedentes.gineco.partosDetalhados); return JSON.stringify({ nome, form: normalized }); } // Abas 0-8 espelham as seções da ficha da contratante (1 a 7). A aba 9 // ("falas") é editorial — não faz parte da ficha nem do CaseData espelhado // por `blankCase()`; tem fonte própria (ESP-0012, `/api/cases/{id}/falas`). const TABS = [ { id: "identificacao", label: "Identificação", icon: "user" }, { id: "historia", label: "História Clínica", icon: "speech" }, { id: "sistemas", label: "Sexual e Anorretal", icon: "pelvis" }, { id: "historicos", label: "Históricos", icon: "clock" }, { id: "impacto", label: "Impacto e Musculoesquelética", icon: "joints" }, { id: "exame", label: "Exame Físico", icon: "eye" }, { id: "perfect", label: "PERFECT e Simulador", icon: "headset" }, { id: "diagnostico", label: "Diagnóstico e Plano", icon: "diagnosis" }, { id: "notas", label: "Referências e Anotações", icon: "edit" }, { id: "falas", label: "Falas da Paciente", icon: "spark" }, ]; // ESP-0012: roteiro de fala da Paciente, gerado por IA ou editado à mão. // Vive em `case_speech_scripts` (backend), não em `CaseData` — por isso não // entra em `blankCase()`/`set()` nem no payload Unity nesta versão // (RNF-FALAS-01). Cada card carrega sua própria requisição, independente do // botão "Salvar rascunho" do formulário. function FalaCard({ item, editavel, onGerar, onSalvar, onGerarAudio, onObterAudio }) { const [texto, setTexto] = React.useState(item.texto || ""); const [busy, setBusy] = React.useState(false); const [erro, setErro] = React.useState(null); const [audioUrl, setAudioUrl] = React.useState(null); const [audioBusy, setAudioBusy] = React.useState(false); const [audioErro, setAudioErro] = React.useState(null); const audioUrlRef = React.useRef(null); React.useEffect(() => { setTexto(item.texto || ""); }, [item.texto, item.chave]); // Ao trocar de item, ou se o áudio deixar de existir (regenerado por outro // meio), descarta o player já carregado — evita tocar o áudio de outro // item por engano. React.useEffect(() => { if (audioUrlRef.current) { URL.revokeObjectURL(audioUrlRef.current); audioUrlRef.current = null; } setAudioUrl(null); }, [item.chave, item.tem_audio]); React.useEffect(() => () => { if (audioUrlRef.current) URL.revokeObjectURL(audioUrlRef.current); }, []); const alterado = texto !== (item.texto || ""); const origemLabel = item.origem === "ia" ? "Gerado por IA" : item.origem === "manual" ? "Editado manualmente" : "Ainda sem roteiro"; const gerar = () => { setBusy(true); setErro(null); onGerar(item.chave).catch(e => setErro(e)).finally(() => setBusy(false)); }; const salvar = () => { setBusy(true); setErro(null); onSalvar(item.chave, texto).catch(e => setErro(e)).finally(() => setBusy(false)); }; const carregarAudio = () => { setAudioBusy(true); setAudioErro(null); return onObterAudio(item.chave).then(url => { if (audioUrlRef.current) URL.revokeObjectURL(audioUrlRef.current); audioUrlRef.current = url; setAudioUrl(url); }).catch(e => setAudioErro(e)).finally(() => setAudioBusy(false)); }; const gerarAudio = () => { setAudioBusy(true); setAudioErro(null); onGerarAudio(item.chave).then(() => carregarAudio()).catch(e => { setAudioErro(e); setAudioBusy(false); }); }; return (
{item.rotulo} {item.obrigatorio && Obrigatório} {!item.aplicavel && Não se aplica a este caso}
{origemLabel}
{item.aplicavel ? ( <>