forked from RiseUP/riseup-squad23
Resolvendo conflito no package-lock
This commit is contained in:
commit
b208d2ac73
537
package-lock.json
generated
537
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
215
src/PagesMedico/NovoRelatorioAudio.jsx
Normal file
215
src/PagesMedico/NovoRelatorioAudio.jsx
Normal 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;
|
||||
97
src/PagesMedico/styleMedico/NovoRelatorioAudio.css
Normal file
97
src/PagesMedico/styleMedico/NovoRelatorioAudio.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import '../PagesMedico/styleMedico/FormNovoRelatorio.css'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../components/utils/AuthProvider'
|
||||
import { GetPatientByCPF } from '../components/utils/Functions-Endpoints/Patient'
|
||||
@ -14,6 +13,49 @@ const FormRelatorio = ({onSave, DictInfo, setDictInfo }) => {
|
||||
|
||||
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 elemento = document.getElementById("folhaA4"); // tua div do relatório
|
||||
const opt = {
|
||||
@ -58,20 +100,16 @@ const FormRelatorio = ({onSave, DictInfo, setDictInfo }) => {
|
||||
console.log(DictInfo)
|
||||
setShowModal(true)
|
||||
|
||||
|
||||
onSave({
|
||||
"patient_id": DictInfo.paciente_id,
|
||||
|
||||
"exam": DictInfo.exam,
|
||||
"diagnosis": DictInfo.diagnosis,
|
||||
"diagnosis": DictInfo.diagnostico, // Garanta que o backend espera 'diagnosis' mas seu state usa 'diagnostico'
|
||||
"conclusion": DictInfo.conclusao,
|
||||
"status": "draft",
|
||||
"requested_by": DictInfo.requested_by,
|
||||
|
||||
"hide_date": false,
|
||||
"hide_signature": false,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
@ -110,6 +148,28 @@ onSave({
|
||||
|
||||
<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}>
|
||||
<div id='primeiraLinha'>
|
||||
|
||||
@ -131,7 +191,7 @@ onSave({
|
||||
|
||||
<div className="col-md-2 mb-3">
|
||||
<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>
|
||||
|
||||
|
||||
@ -177,7 +237,8 @@ onSave({
|
||||
<p>Paciente: {DictInfo?.paciente_nome}</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>
|
||||
|
||||
@ -189,7 +250,7 @@ onSave({
|
||||
|
||||
<div>
|
||||
<p>Dr {DictInfo.requested_by}</p>
|
||||
<p>Emitido em: 0</p>
|
||||
<p>Emitido em: {new Date().toLocaleDateString()}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -5,12 +5,21 @@
|
||||
"url": "/medico/agendamento"
|
||||
},
|
||||
|
||||
|
||||
|
||||
{
|
||||
"name": "Relatório por Áudio",
|
||||
"icon": "file-earmark-plus-fill",
|
||||
"url": "/medico/novo-relatorio-audio"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Relatórios",
|
||||
"icon": "file-earmark-text-fill",
|
||||
"url": "/medico/relatorios"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"name": "Chat com pacientes",
|
||||
"icon": "chat-dots-fill",
|
||||
|
||||
@ -9,6 +9,7 @@ import Chat from "../../PagesMedico/Chat";
|
||||
import DoctorItems from "../../data/sidebar-items-medico.json";
|
||||
import FormNovoRelatorio from "../../PagesMedico/FormNovoRelatorio";
|
||||
import EditPageRelatorio from "../../PagesMedico/EditPageRelatorio";
|
||||
import NovoRelatorioAudio from "../../PagesMedico/NovoRelatorioAudio";
|
||||
import BotaoVideoChamada from '../../components/BotaoVideoChamada';
|
||||
|
||||
import DoctorAgendamentoEditPage from "../../PagesMedico/DoctorAgendamentoEditPage";
|
||||
@ -25,6 +26,8 @@ function PerfilMedico() {
|
||||
<div id="main">
|
||||
<Routes>
|
||||
<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/:id/edit" element={<EditPageRelatorio />} />
|
||||
<Route path="/prontuario" element={<Prontuario />} />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user