mergerelatoriosnovo

This commit is contained in:
Jessica_Faro 2025-10-30 11:34:58 -03:00
commit 52ae210ff7
6 changed files with 254 additions and 115 deletions

View File

@ -19,45 +19,59 @@ const DoctorRelatorioManager = () => {
const [showModal, setShowModal] = useState(false);
const [index, setIndex] = useState();
// busca lista de relatórios
useEffect(() => {
let mounted = true;
const fetchReports = async () => {
try {
var myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
myHeaders.append('Authorization', authHeader);
if (authHeader) myHeaders.append('Authorization', authHeader);
var requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
const res = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*", requestOptions);
const data = await res.json();
setRelatorios(data || []);
const uniqueMap = new Map();
(Array.isArray(data) ? data : []).forEach(r => {
if (r && r.id) uniqueMap.set(r.id, r);
});
const unique = Array.from(uniqueMap.values())
.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
if (mounted) setRelatorios(unique);
} catch (err) {
console.error('Erro listar relatórios', err);
setRelatorios([]);
if (mounted) setRelatorios([]);
}
};
fetchReports();
const refreshHandler = () => fetchReports();
window.addEventListener('reports:refresh', refreshHandler);
return () => {
mounted = false;
window.removeEventListener('reports:refresh', refreshHandler);
};
}, [authHeader]);
// depois que RelatoriosFiltrados mudar, busca pacientes e médicos correspondentes
useEffect(() => {
const fetchRelData = async () => {
const pacientes = [];
const medicos = [];
for (let i = 0; i < RelatoriosFiltrados.length; i++) {
const rel = RelatoriosFiltrados[i];
// paciente
try {
const pacienteRes = await GetPatientByID(rel.patient_id, authHeader);
pacientes.push(Array.isArray(pacienteRes) ? pacienteRes[0] : pacienteRes);
} catch (err) {
pacientes.push(null);
}
// médico: tenta created_by ou requested_by id se existir
try {
const doctorId = rel.created_by || rel.requested_by || null;
if (doctorId) {
// se created_by é id (uuid) usamos GetDoctorByID, senão se requested_by for nome, guardamos nome
const docRes = await GetDoctorByID(doctorId, authHeader);
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes);
} else {
@ -77,54 +91,73 @@ const DoctorRelatorioManager = () => {
}
}, [RelatoriosFiltrados, authHeader]);
const BaixarPDFdoRelatorio = (nome_paciente) => {
const elemento = document.getElementById("folhaA4");
const opt = { margin: 0, filename: `relatorio_${nome_paciente || "paciente"}.pdf`, html2canvas: { scale: 2 }, jsPDF: { unit: "mm", format: "a4", orientation: "portrait" } };
const BaixarPDFdoRelatorio = (nome_paciente, idx) => {
const elemento = document.getElementById(`folhaA4-${idx}`);
if (!elemento) {
console.error('Elemento para gerar PDF não encontrado:', `folhaA4-${idx}`);
return;
}
const opt = {
margin: 0,
filename: `relatorio_${nome_paciente || "paciente"}.pdf`,
html2canvas: { scale: 2 },
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" }
};
html2pdf().set(opt).from(elemento).save();
};
return (
<div>
{showModal && (
<div className="modal">
<div className="modal-dialog modal-tabela-relatorio">
<div className="modal modal-centered" role="dialog" aria-modal="true" onClick={() => setShowModal(false)}>
{/* aqui: classe modal-dialog-square para ficar quadrado */}
<div className="modal-dialog modal-dialog-square" role="document" onClick={(e) => e.stopPropagation()}>
<div className="modal-content">
<div className="modal-header text-white">
<h5 className="modal-title ">Relatório de {PacientesComRelatorios[index]?.full_name}</h5>
<button type="button" className="btn-close" onClick={() => setShowModal(false)}></button>
<div className="modal-header custom-modal-header">
<h5 className="modal-title">Relatório de {PacientesComRelatorios[index]?.full_name}</h5>
<button type="button" className="btn-close modal-close-btn" aria-label="Close" onClick={() => setShowModal(false)}></button>
</div>
<div className="modal-body">
<div id="folhaA4">
<div id='header-relatorio'>
<p>Clinica Rise up</p>
<p>Dr - CRM/SP 123456</p>
<p>Avenida - (79) 9 4444-4444</p>
<div id={`folhaA4-${index}`} className="folhaA4">
<div id='header-relatorio' style={{ textAlign: 'center', marginBottom: 24 }}>
<p style={{ margin: 0 }}>Clinica Rise up</p>
<p style={{ margin: 0 }}>Dr - CRM/SP 123456</p>
<p style={{ margin: 0 }}>Avenida - (79) 9 4444-4444</p>
</div>
<div id='infoPaciente'>
<p>Paciente: {PacientesComRelatorios[index]?.full_name}</p>
<p>Data de nascimento: {PacientesComRelatorios[index]?.birth_date}</p>
<p>Data do exame: {RelatoriosFiltrados[index]?.due_at || ''}</p>
{/* Exibe conteúdo salvo (content_html) */}
<p style={{ marginTop: '15px', fontWeight: 'bold' }}>Conteúdo do Relatório:</p>
<TiptapViewer htmlContent={RelatoriosFiltrados[index]?.content_html || RelatoriosFiltrados[index]?.content || 'Relatório não preenchido.'} />
<div id='infoPaciente' style={{ padding: '0 6px' }}>
<p><strong>Paciente:</strong> {PacientesComRelatorios[index]?.full_name}</p>
<p><strong>Data de nascimento:</strong> {PacientesComRelatorios[index]?.birth_date || '—'}</p>
<p><strong>Data do exame:</strong> {RelatoriosFiltrados[index]?.due_at || '—'}</p>
<p style={{ marginTop: 12, fontWeight: '700' }}>Conteúdo do Relatório:</p>
<div className="tiptap-viewer-wrapper">
<TiptapViewer htmlContent={RelatoriosFiltrados[index]?.content_html || RelatoriosFiltrados[index]?.content || 'Relatório não preenchido.'} />
</div>
</div>
<div>
<div style={{ marginTop: 20, padding: '0 6px' }}>
<p>Dr {MedicosComRelatorios[index]?.full_name || RelatoriosFiltrados[index]?.requested_by}</p>
<p>Emitido em: {RelatoriosFiltrados[index]?.created_at || '—'}</p>
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {RelatoriosFiltrados[index]?.created_at || '—'}</p>
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(PacientesComRelatorios[index]?.full_name)}><i className='bi bi-file-pdf-fill'></i> baixar em pdf</button>
<button type="button" className="btn btn-primary" onClick={() => { setShowModal(false) }}>Fechar</button>
<div className="modal-footer custom-modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(PacientesComRelatorios[index]?.full_name, index)}>
<i className='bi bi-file-pdf-fill'></i> baixar em pdf
</button>
<button type="button" className="btn btn-outline-secondary" onClick={() => { setShowModal(false) }}>
Fechar
</button>
</div>
</div>
</div>
</div>
)}
{/* restante da página (lista) permanece igual */}
<div className="page-heading"><h3>Lista de Relatórios</h3></div>
<div className="page-content">
<section className="row">
@ -173,7 +206,7 @@ const DoctorRelatorioManager = () => {
</tr>
))
) : (
<tr><td colSpan="8" className="text-center">Nenhum paciente encontrado.</td></tr>
<tr><td colSpan="3" className="text-center">Nenhum paciente encontrado.</td></tr>
)}
</tbody>
</table>

View File

@ -1,4 +1,4 @@
// EditPageRelatorio.jsx
// src/PagesMedico/EditPageRelatorio.jsx
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import API_KEY from '../components/utils/apiKeys';
@ -7,7 +7,6 @@ import TiptapEditor from '../PagesMedico/TiptapEditor';
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient';
import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor';
const EditPageRelatorio = () => {
const params = useParams();
const navigate = useNavigate();
@ -19,7 +18,6 @@ const EditPageRelatorio = () => {
const [doctor, setDoctor] = useState(null);
const [html, setHtml] = useState('');
const generateTemplate = (r = {}, p = {}, d = {}) => {
const patientName = p?.full_name || '[NOME DO PACIENTE]';
const birthDate = p?.birth_date || '';
@ -48,34 +46,29 @@ const EditPageRelatorio = () => {
`;
};
useEffect(() => {
const load = async () => {
setLoading(true);
try {
const myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
if (authHeader) myHeaders.append("Authorization", authHeader);
const requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
// Pega relatório por id (supabase geralmente retorna array para ?id=eq.X)
const resp = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?id=eq.${params.id}`, requestOptions);
const data = await resp.json();
const rep = Array.isArray(data) ? data[0] : data;
if (!rep) throw new Error('Relatório não encontrado');
setReport(rep);
// busca paciente
if (rep.patient_id) {
const p = await GetPatientByID(rep.patient_id, authHeader);
setPatient(Array.isArray(p) ? p[0] : p);
}
// busca doctor se tiver created_by/requested_by id (tentamos fallback)
if (rep.created_by) {
try {
@ -86,7 +79,6 @@ const EditPageRelatorio = () => {
}
}
// content_html preferencial
let initial = rep.content_html || rep.content || rep.diagnosis || rep.conclusion || '';
if (!initial || initial.trim() === '') {
@ -104,16 +96,15 @@ const EditPageRelatorio = () => {
// eslint-disable-next-line
}, [params.id, authHeader]);
const handleSave = async () => {
setLoading(true);
try {
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
myHeaders.append('Authorization', authHeader);
if (authHeader) myHeaders.append('Authorization', authHeader);
myHeaders.append('Content-Type', 'application/json');
// Adicionado para que a API retorne o registro atualizado
myHeaders.append('Accept', 'application/json');
// pedir que o Supabase retorne a representação do registro atualizado (opcional)
myHeaders.append('Prefer', 'return=representation');
const body = JSON.stringify({ content_html: html });
@ -125,15 +116,21 @@ const EditPageRelatorio = () => {
});
if (!res.ok) {
const txt = await res.text();
let txt;
try { txt = await res.text(); } catch (e) { txt = 'erro lendo resposta'; }
console.error('Erro PATCH', res.status, txt);
throw new Error('Erro na API');
}
// Recebe o dado atualizado e atualiza o estado do componente
const updatedData = await res.json();
let updatedData;
try {
updatedData = await res.json();
} catch (e) {
updatedData = null;
}
const updatedReport = Array.isArray(updatedData) ? updatedData[0] : updatedData;
if (updatedReport) {
setReport(updatedReport);
setHtml(updatedReport.content_html || '');
@ -150,21 +147,17 @@ const EditPageRelatorio = () => {
}
};
if (loading) return <div>Carregando...</div>;
return (
<div className='container'>
<h3 className='mb-4'>Editar Relatório do Paciente: {patient?.full_name || '...'}</h3>
<div className='mb-3'>
<h5 className='mb-2'>Conteúdo do Relatório</h5>
<TiptapEditor content={html} onChange={(newHtml) => setHtml(newHtml)} />
</div>
<div className='d-flex justify-content-center mt-4'>
<button className='btn btn-success' onClick={handleSave} disabled={loading}>
{loading ? 'Salvando...' : 'Salvar Relatório'}
@ -174,5 +167,4 @@ const EditPageRelatorio = () => {
);
};
export default EditPageRelatorio;

View File

@ -1,3 +1,4 @@
// src/PagesMedico/FormNovoRelatorio.jsx
import React, { useEffect, useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import API_KEY from '../components/utils/apiKeys';
@ -39,7 +40,7 @@ const FormNovoRelatorio = () => {
const doctorRef = useRef();
useEffect(() => {
// carregar pacientes
// carregar pacientes e médicos
let mounted = true;
const loadPatients = async () => {
setLoadingPatients(true);
@ -109,7 +110,7 @@ const FormNovoRelatorio = () => {
patient_id: patient.id,
patient_name: patient.full_name || '',
patient_birth: patient.birth_date || '',
contentHtml: generateTemplate(patient.full_name || '', patient.birth_date || '', form.doctor_name)
contentHtml: generateTemplate(patient.full_name || '', patient.birth_date || '', prev.doctor_name)
}));
setPatientQuery('');
setShowPatientDropdown(false);
@ -120,7 +121,7 @@ const FormNovoRelatorio = () => {
...prev,
doctor_id: doctor.id,
doctor_name: doctor.full_name || '',
contentHtml: generateTemplate(form.patient_name, form.patient_birth, doctor.full_name || '')
contentHtml: generateTemplate(prev.patient_name, prev.patient_birth, doctor.full_name || '')
}));
setDoctorQuery('');
setShowDoctorDropdown(false);
@ -137,7 +138,7 @@ const FormNovoRelatorio = () => {
const handleEditorChange = (html) => setForm(prev => ({ ...prev, contentHtml: html }));
// salvar novo relatório
// salvar novo relatório (agora com Prefer: return=representation e dispatch para refresh)
const handleSubmit = async (e) => {
e.preventDefault();
if (!form.patient_id) return alert('Selecione o paciente (clicando no item) antes de salvar.');
@ -146,35 +147,51 @@ const FormNovoRelatorio = () => {
try {
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
myHeaders.append('Authorization', authHeader);
if (authHeader) myHeaders.append('Authorization', authHeader);
myHeaders.append('Content-Type', 'application/json');
myHeaders.append('Accept', 'application/json');
// pedir que o Supabase retorne a representação do registro criado
myHeaders.append('Prefer', 'return=representation');
const body = JSON.stringify({
// monta o payload apenas com campos válidos
const payload = {
patient_id: form.patient_id,
content: form.contentHtml,
content_html: form.contentHtml,
requested_by: form.doctor_name || '',
created_by: form.doctor_id || null,
status: 'draft'
});
requested_by: form.doctor_name || ''
};
// só inclui created_by se tiver um id válido
if (form.doctor_id) payload.created_by = form.doctor_id;
payload.status = 'draft';
const res = await fetch('https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports', {
method: 'POST',
headers: myHeaders,
body,
body: JSON.stringify(payload),
});
if (!res.ok) {
const txt = await res.text();
// tenta ler JSON, se não for JSON lê o texto
let txt;
try {
txt = await res.json();
} catch (err) {
txt = await res.text();
}
console.error('Erro POST criar relatório:', res.status, txt);
// mostra mensagem mais útil
return alert(`Erro ao criar relatório (ver console). Status ${res.status}`);
return alert(`Erro ao criar relatório (ver console). Status ${res.status}\nMensagem: ${JSON.stringify(txt)}`);
}
const created = await res.json();
console.log('Relatório criado:', created);
// dispara refresh global para a lista (DoctorRelatorioManager está escutando)
window.dispatchEvent(new Event('reports:refresh'));
alert('Relatório criado com sucesso!');
navigate('/medico/relatorios');
} catch (err) {
console.error('Erro salvar relatório:', err);
console.error('Erro salvar relatório (catch):', err);
alert('Erro ao salvar relatório. Veja console.');
}
};
@ -185,7 +202,7 @@ const FormNovoRelatorio = () => {
<form onSubmit={handleSubmit} className="card p-4 mb-4">
<div className="row g-3 align-items-end">
<div className="col-md-6" ref={patientRef}>
<div className="col-md-6" ref={patientRef} style={{ position: 'relative' }}>
<label className="form-label">Buscar paciente (digite para filtrar)</label>
<input
className="form-control"
@ -195,7 +212,7 @@ const FormNovoRelatorio = () => {
onFocus={() => setShowPatientDropdown(true)}
/>
{showPatientDropdown && patientQuery && (
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '45%' }}>
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '100%' }}>
{filteredPatients.length > 0 ? filteredPatients.map(p => (
<li key={p.id} className="list-group-item list-group-item-action" onClick={() => choosePatient(p)}>
{p.full_name} {p.cpf ? `- ${p.cpf}` : ''}
@ -206,7 +223,7 @@ const FormNovoRelatorio = () => {
<div className="form-text">Clique no paciente desejado para selecioná-lo e preencher o template.</div>
</div>
<div className="col-md-6" ref={doctorRef}>
<div className="col-md-6" ref={doctorRef} style={{ position: 'relative' }}>
<label className="form-label">Buscar médico (digite para filtrar)</label>
<input
className="form-control"
@ -216,7 +233,7 @@ const FormNovoRelatorio = () => {
onFocus={() => setShowDoctorDropdown(true)}
/>
{showDoctorDropdown && doctorQuery && (
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '45%', right: 0 }}>
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '100%' }}>
{filteredDoctors.length > 0 ? filteredDoctors.map(d => (
<li key={d.id} className="list-group-item list-group-item-action" onClick={() => chooseDoctor(d)}>
{d.full_name} {d.crm ? `- CRM ${d.crm}` : ''}

View File

@ -12,4 +12,4 @@ const TiptapViewer = ({ htmlContent }) => {
);
};
export default TiptapViewer;
export default TiptapViewer;

View File

@ -112,4 +112,4 @@ function Relatorio() {
);
}
export default Relatorio;
export default Relatorio;

View File

@ -1,55 +1,152 @@
#folhaA4 {
width: 210mm;
min-height: 207mm;
padding: 20mm;
margin: 10mm auto;
border: 1px solid #ccc;
background: white;
/* src/PagesMedico/styleMedico/FormNovoRelatorio.css */
/* --- Modal centralizada e quadrada (ajustada para ser mais larga e sem quadrado branco no botão fechar) --- */
/* backdrop + center */
.modal.modal-centered {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(10, 20, 30, 0.45);
z-index: 2000;
padding: 20px;
}
#primeiraLinha{
display: flex;
flex-direction: row;
gap: 20px;
margin-bottom: 20px;
/* dialog box — maior horizontalmente e altura automática para scroll interno */
.modal-dialog.modal-dialog-square {
width: 880px; /* largura aumentada */
max-width: 96vw;
height: auto; /* deixa altura automática (melhor para conteúdo longo) */
max-height: 92vh;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
input,textarea,label{
font-size: 1.1rem;
/* caixa branca que contém o conteúdo - ocupa 100% da dialog */
.modal-dialog.modal-dialog-square .modal-content {
width: 100%;
height: auto;
border-radius: 12px;
box-shadow: 0 12px 30px rgba(11,22,35,0.18);
overflow: hidden;
display: flex;
flex-direction: column;
background: #fff;
}
textarea{
width: 100%;
height: 100px;
/* header */
.custom-modal-header {
position: relative;
background: linear-gradient(90deg, #203B75 0%, #274A8A 100%);
color: #fff;
padding: 14px 18px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.submitButton{
display: flex;
margin-left: auto;
height:50% ;
padding: 8px 20px;
font-size: medium;
.custom-modal-header .modal-title {
margin: 0;
font-size: 1.05rem;
font-weight: 700;
}
.bi-download{
font-size: 1.2rem;
margin-right: 5px;
font-weight: bold;
/* botão fechar no header — sem quadrado branco por trás */
.modal-close-btn {
background: transparent !important;
border: none;
width: 40px;
height: 40px;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: none;
outline: none;
position: relative;
z-index: 5;
}
#infoPaciente{
margin-top: 50px;
margin-bottom: 40px;
.modal-close-btn::after {
content: '✕';
color: #fff;
font-weight: 700;
font-size: 16px;
}
#header-relatorio{
text-align: center;
margin-bottom: 30px;
/* body - faz scroll interno se for longo */
.modal-body {
padding: 18px;
overflow: auto;
flex: 1 1 auto;
}
.info-paciente{
font-weight: bold;
/* footer */
.custom-modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 14px 18px;
border-top: 1px solid #eee;
background: #fafafa;
}
/* folhaA4 dentro da modal — adapta para caber */
.folhaA4 {
width: 100%;
box-sizing: border-box;
background: transparent;
padding: 0;
}
/* melhor espaçamento e leitura do conteúdo */
#header-relatorio p {
color: #374151;
margin: 6px 0;
text-align: center;
}
#infoPaciente p {
margin: 10px 0;
color: #3d4650;
}
/* tornar o viewer responsivo */
.tiptap-viewer-wrapper {
border: 1px dashed #e7e7e7;
padding: 12px;
margin-top: 10px;
background: #fff;
}
/* barra de scroll customizada (opcional) */
.modal-body::-webkit-scrollbar {
width: 10px;
}
.modal-body::-webkit-scrollbar-thumb {
background: rgba(100,100,100,0.18);
border-radius: 8px;
}
.modal-body::-webkit-scrollbar-track {
background: rgba(0,0,0,0.02);
}
/* responsividade para telas pequenas: mantém centralizado, ajusta proporção */
@media (max-width: 680px) {
.modal-dialog.modal-dialog-square {
width: 92vw;
height: 86vh;
}
.modal-close-btn { width: 36px; height: 36px; }
.custom-modal-footer { padding: 10px; }
.modal-body { padding: 12px; }
}