Criação da tela de relatório com IA e integração backend

This commit is contained in:
RafaelMTA13 2025-11-26 15:50:28 -03:00
parent b0ba36507b
commit 220e436fa0
6 changed files with 616 additions and 633 deletions

552
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,215 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import html2pdf from 'html2pdf.js';
import './styleMedico/NovoRelatorioAudio.css';
const NovoRelatorioAudio = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
paciente_nome: '',
data_exame: new Date().toISOString().split('T')[0],
exam: '',
diagnostico: '',
conclusao: '',
medico_nome: 'Dr.______________________'
});
const handleAudioUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
setLoading(true);
const data = new FormData();
data.append('audio', file);
try {
const response = await fetch('http://localhost:3001/api/transcrever-relatorio', {
method: 'POST',
body: data,
});
if (!response.ok) throw new Error('Erro na comunicação com a API');
const result = await response.json();
setFormData(prev => ({
...prev,
exam: result.exam || prev.exam,
diagnostico: result.diagnostico || prev.diagnostico,
conclusao: result.conclusao || prev.conclusao
}));
} catch (error) {
console.error(error);
alert("Erro: Verifique se o backend (porta 3001) está rodando.");
} finally {
setLoading(false);
e.target.value = null;
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handlePrintPDF = () => {
const element = document.getElementById('documento-final');
const opt = {
margin: 0,
filename: `Laudo_${formData.paciente_nome || 'SemNome'}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
html2pdf().set(opt).from(element).save();
};
return (
<div className="ai-editor-container">
<div className="editor-sidebar">
<div>
<h4><i className="bi bi-robot me-2"></i>AI Report</h4>
<p className="text-muted small">Gerador de laudos automatizado</p>
</div>
<div className="ai-upload-box">
{loading ? (
<div className="spinner-border text-info" role="status"></div>
) : (
<>
<label htmlFor="audioFile" style={{cursor:'pointer', width:'100%', display:'block'}}>
<i className="bi bi-mic fs-2 text-info"></i>
<div style={{color:'#fff', fontWeight:'bold', marginTop:'10px'}}>Gravar / Enviar Áudio</div>
<div className="small text-muted mt-1">Clique para selecionar</div>
</label>
<input
id="audioFile"
type="file"
accept="audio/*"
style={{display:'none'}}
onChange={handleAudioUpload}
/>
</>
)}
</div>
<hr className="border-secondary" />
<div className="form-group">
<label>NOME DO PACIENTE</label>
<input
type="text"
name="paciente_nome"
className="form-control dark-input"
value={formData.paciente_nome}
onChange={handleChange}
placeholder="Ex: Maria Silva"
/>
</div>
<div className="form-group">
<label>EXAME REALIZADO</label>
<input
type="text"
name="exam"
className="form-control dark-input"
value={formData.exam}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label>DIAGNÓSTICO (TEXTO)</label>
<textarea
name="diagnostico"
rows="5"
className="form-control dark-input"
value={formData.diagnostico}
onChange={handleChange}
></textarea>
</div>
<div className="form-group">
<label>CONCLUSÃO</label>
<textarea
name="conclusao"
rows="3"
className="form-control dark-input"
value={formData.conclusao}
onChange={handleChange}
></textarea>
</div>
<div className="mt-auto d-flex gap-2">
<button className="btn btn-outline-light flex-grow-1" onClick={() => navigate(-1)}>Voltar</button>
<button className="btn btn-info flex-grow-1 fw-bold" onClick={handlePrintPDF}>
<i className="bi bi-printer me-2"></i>PDF
</button>
</div>
</div>
<div className="preview-area">
<div id="documento-final" className="paper-a4">
<div style={{borderBottom: '2px solid #000', paddingBottom: '20px', marginBottom: '30px', display:'flex', justifyContent:'space-between'}}>
<div>
<h2 style={{margin:0, fontSize:'18pt', fontWeight:'bold'}}>CLÍNICA RISE UP</h2>
<p style={{margin:0, fontSize:'10pt'}}>Excelência em Diagnóstico por Imagem</p>
</div>
<div style={{textAlign:'right', fontSize:'10pt'}}>
<p style={{margin:0}}><strong>{formData.medico_nome}</strong></p>
<p style={{margin:0}}>CRM/SP 123456</p>
<p style={{margin:0}}>{new Date().toLocaleDateString()}</p>
</div>
</div>
<div style={{backgroundColor:'#f8f9fa', padding:'15px', borderRadius:'4px', marginBottom:'30px'}}>
<table style={{width:'100%', fontSize:'11pt'}}>
<tbody>
<tr>
<td style={{width:'15%', fontWeight:'bold'}}>Paciente:</td>
<td>{formData.paciente_nome || '__________________________'}</td>
</tr>
<tr>
<td style={{fontWeight:'bold'}}>Exame:</td>
<td>{formData.exam || '__________________________'}</td>
</tr>
<tr>
<td style={{fontWeight:'bold'}}>Data:</td>
<td>{new Date(formData.data_exame).toLocaleDateString('pt-BR')}</td>
</tr>
</tbody>
</table>
</div>
<div className="laudo-body">
<h4 style={{textTransform:'uppercase', borderBottom:'1px solid #ccc', paddingBottom:'5px', marginBottom:'15px'}}>Relatório Médico</h4>
<p style={{fontWeight:'bold', marginBottom:'5px'}}>Análise:</p>
<p style={{marginBottom:'25px', textAlign:'justify', whiteSpace:'pre-wrap'}}>
{formData.diagnostico || <span style={{color:'#ccc'}}>O diagnóstico aparecerá aqui após o processamento do áudio...</span>}
</p>
<p style={{fontWeight:'bold', marginBottom:'5px'}}>Conclusão:</p>
<p style={{marginBottom:'40px', textAlign:'justify', fontWeight:'500'}}>
{formData.conclusao}
</p>
</div>
<div style={{marginTop:'50px', textAlign:'center', pageBreakInside:'avoid'}}>
<p style={{marginBottom:0}}>________________________________________</p>
<p style={{fontWeight:'bold', margin:0}}>{formData.medico_nome}</p>
<p style={{fontSize:'10pt', color:'#666'}}>Assinatura Eletrônica</p>
</div>
</div>
</div>
</div>
);
};
export default NovoRelatorioAudio;

View File

@ -0,0 +1,97 @@
/* Container que ocupa toda a altura da tela (menos o header do site) */
.ai-editor-container {
display: flex;
min-height: calc(100vh - 80px); /* Ajuste conforme seu header */
background-color: #e9ecef; /* Cor de "Mesa" */
overflow: hidden;
}
/* --- LADO ESQUERDO: PAINEL DE CONTROLE --- */
.editor-sidebar {
width: 400px;
background-color: #212529; /* Dark Mode para o painel */
color: #fff;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 15px;
box-shadow: 4px 0 10px rgba(0,0,0,0.1);
}
.editor-sidebar h4 {
color: #0dcaf0; /* Ciano para destaque */
margin-bottom: 20px;
font-weight: 700;
}
.editor-sidebar label {
font-size: 0.85rem;
color: #adb5bd;
margin-bottom: 5px;
}
/* Estilo customizado para inputs no fundo escuro */
.dark-input {
background-color: #343a40;
border: 1px solid #495057;
color: #fff;
border-radius: 6px;
}
.dark-input:focus {
background-color: #3b4248;
color: #fff;
border-color: #0dcaf0;
box-shadow: none;
}
/* Área de Upload Destacada */
.ai-upload-box {
border: 2px dashed #0dcaf0;
border-radius: 10px;
padding: 20px;
text-align: center;
background: rgba(13, 202, 240, 0.1);
transition: 0.3s;
cursor: pointer;
}
.ai-upload-box:hover {
background: rgba(13, 202, 240, 0.2);
}
/* --- LADO DIREITO: PREVIEW A4 --- */
.preview-area {
flex: 1;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 40px;
overflow-y: auto;
}
.paper-a4 {
width: 210mm;
min-height: 297mm;
background: white;
padding: 25mm;
box-shadow: 0 0 20px rgba(0,0,0,0.15);
color: #000;
font-family: 'Georgia', serif; /* Fonte mais séria para o documento */
font-size: 12pt;
line-height: 1.6;
}
/* Responsividade: Em celular vira coluna única */
@media (max-width: 900px) {
.ai-editor-container {
flex-direction: column;
}
.editor-sidebar {
width: 100%;
height: auto;
}
.paper-a4 {
width: 100%;
padding: 15mm;
}
}

View File

@ -1,6 +1,5 @@
import React from 'react' import React, { useState } from 'react'
import '../PagesMedico/styleMedico/FormNovoRelatorio.css' import '../PagesMedico/styleMedico/FormNovoRelatorio.css'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useAuth } from '../components/utils/AuthProvider' import { useAuth } from '../components/utils/AuthProvider'
import { GetPatientByCPF } from '../components/utils/Functions-Endpoints/Patient' import { GetPatientByCPF } from '../components/utils/Functions-Endpoints/Patient'
@ -14,6 +13,49 @@ const FormRelatorio = ({onSave, DictInfo, setDictInfo }) => {
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
// --- NOVO: Estado para controlar o loading da transcrição ---
const [isTranscribing, setIsTranscribing] = useState(false);
// --- NOVA FUNÇÃO: Envia o áudio e preenche o formulário ---
const handleAudioUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
setIsTranscribing(true); // Ativa o spinner
const formData = new FormData();
formData.append('audio', file); // 'audio' deve ser o nome esperado no backend
try {
// ATENÇÃO: Substitua essa URL pela rota do seu backend que criamos
const response = await fetch('http://localhost:3001/api/transcrever-relatorio', {
method: 'POST',
body: formData,
// headers: { 'Authorization': authHeader } // Descomente se seu backend precisar de token
});
if (!response.ok) throw new Error("Falha na transcrição");
const data = await response.json();
// Atualiza o DictInfo com os dados vindos da IA
setDictInfo((prev) => ({
...prev,
exam: data.exam || prev.exam, // Preenche se a IA achou, senão mantém o antigo
diagnostico: data.diagnostico || prev.diagnostico,
conclusao: data.conclusao || prev.conclusao
}));
} catch (error) {
console.error("Erro no upload de áudio:", error);
alert("Não foi possível gerar o relatório por áudio. Verifique o backend.");
} finally {
setIsTranscribing(false); // Desativa o spinner
e.target.value = null; // Limpa o input para permitir enviar o mesmo arquivo novamente se quiser
}
};
// -----------------------------------------------------------
const BaixarPDFdoRelatorio = () => { const BaixarPDFdoRelatorio = () => {
const elemento = document.getElementById("folhaA4"); // tua div do relatório const elemento = document.getElementById("folhaA4"); // tua div do relatório
const opt = { const opt = {
@ -58,20 +100,16 @@ const FormRelatorio = ({onSave, DictInfo, setDictInfo }) => {
console.log(DictInfo) console.log(DictInfo)
setShowModal(true) setShowModal(true)
onSave({ onSave({
"patient_id": DictInfo.paciente_id, "patient_id": DictInfo.paciente_id,
"exam": DictInfo.exam, "exam": DictInfo.exam,
"diagnosis": DictInfo.diagnosis, "diagnosis": DictInfo.diagnostico, // Garanta que o backend espera 'diagnosis' mas seu state usa 'diagnostico'
"conclusion": DictInfo.conclusao, "conclusion": DictInfo.conclusao,
"status": "draft", "status": "draft",
"requested_by": DictInfo.requested_by, "requested_by": DictInfo.requested_by,
"hide_date": false, "hide_date": false,
"hide_signature": false, "hide_signature": false,
}); });
} }
return ( return (
@ -110,6 +148,28 @@ onSave({
<div className='card'> <div className='card'>
{/* --- ÁREA DE UPLOAD DE ÁUDIO (INSERIDA AQUI) --- */}
<div className="p-3 mb-3 border-bottom bg-light">
<label className="form-label fw-bold">🎙 Preenchimento Automático via Áudio</label>
<div className="d-flex align-items-center gap-3">
{isTranscribing ? (
<div className="d-flex align-items-center text-primary">
<div className="spinner-border spinner-border-sm me-2" role="status"></div>
<span>A IA está gerando o relatório... aguarde.</span>
</div>
) : (
<input
type="file"
className="form-control"
accept="audio/*"
onChange={handleAudioUpload}
/>
)}
</div>
<small className="text-muted">Envie um áudio ditando o exame, diagnóstico e conclusão.</small>
</div>
{/* ----------------------------------------------- */}
<form action="" onSubmit={handleSubmit}> <form action="" onSubmit={handleSubmit}>
<div id='primeiraLinha'> <div id='primeiraLinha'>
@ -131,7 +191,7 @@ onSave({
<div className="col-md-2 mb-3"> <div className="col-md-2 mb-3">
<label >Exame:</label> <label >Exame:</label>
<input type="text" className="form-control" name="exam" onChange={handleChange} /> <input type="text" className="form-control" name="exam" onChange={handleChange} value={DictInfo.exam || ''} />
</div> </div>
@ -177,7 +237,8 @@ onSave({
<p>Paciente: {DictInfo?.paciente_nome}</p> <p>Paciente: {DictInfo?.paciente_nome}</p>
<p>Data de nascimento: </p> <p>Data de nascimento: </p>
<p>Data do exame: {DictInfo.data_exam}</p> {/* Corrigi de data_exam para data_exame para bater com o state */}
<p>Data do exame: {DictInfo.data_exame}</p>
<p>Exame: {DictInfo.exam}</p> <p>Exame: {DictInfo.exam}</p>
@ -189,7 +250,7 @@ onSave({
<div> <div>
<p>Dr {DictInfo.requested_by}</p> <p>Dr {DictInfo.requested_by}</p>
<p>Emitido em: 0</p> <p>Emitido em: {new Date().toLocaleDateString()}</p>
</div> </div>
</div> </div>

View File

@ -5,12 +5,21 @@
"url": "/medico/agendamento" "url": "/medico/agendamento"
}, },
{
"name": "Relatório por Áudio",
"icon": "file-earmark-plus-fill",
"url": "/medico/novo-relatorio-audio"
},
{ {
"name": "Relatórios", "name": "Relatórios",
"icon": "file-earmark-text-fill", "icon": "file-earmark-text-fill",
"url": "/medico/relatorios" "url": "/medico/relatorios"
}, },
{ {
"name": "Chat com pacientes", "name": "Chat com pacientes",
"icon": "chat-dots-fill", "icon": "chat-dots-fill",

View File

@ -9,6 +9,7 @@ import Chat from "../../PagesMedico/Chat";
import DoctorItems from "../../data/sidebar-items-medico.json"; import DoctorItems from "../../data/sidebar-items-medico.json";
import FormNovoRelatorio from "../../PagesMedico/FormNovoRelatorio"; import FormNovoRelatorio from "../../PagesMedico/FormNovoRelatorio";
import EditPageRelatorio from "../../PagesMedico/EditPageRelatorio"; import EditPageRelatorio from "../../PagesMedico/EditPageRelatorio";
import NovoRelatorioAudio from "../../PagesMedico/NovoRelatorioAudio";
import BotaoVideoChamada from '../../components/BotaoVideoChamada'; import BotaoVideoChamada from '../../components/BotaoVideoChamada';
import DoctorAgendamentoEditPage from "../../PagesMedico/DoctorAgendamentoEditPage"; import DoctorAgendamentoEditPage from "../../PagesMedico/DoctorAgendamentoEditPage";
@ -25,6 +26,8 @@ function PerfilMedico() {
<div id="main"> <div id="main">
<Routes> <Routes>
<Route path="/" element={<DoctorRelatorioManager />} /> <Route path="/" element={<DoctorRelatorioManager />} />
<Route path="/novo-relatorio" element={<FormNovoRelatorio />} />
<Route path="/novo-relatorio-audio" element={<NovoRelatorioAudio />} />
<Route path="/relatorios/criar" element={<FormNovoRelatorio />} /> <Route path="/relatorios/criar" element={<FormNovoRelatorio />} />
<Route path="/relatorios/:id/edit" element={<EditPageRelatorio />} /> <Route path="/relatorios/:id/edit" element={<EditPageRelatorio />} />
<Route path="/prontuario" element={<Prontuario />} /> <Route path="/prontuario" element={<Prontuario />} />