512 lines
22 KiB
JavaScript
512 lines
22 KiB
JavaScript
// src/pages/LaudoManager.jsx
|
|
import API_KEY from '../components/utils/apiKeys';
|
|
import { Link } from 'react-router-dom';
|
|
import React, { useState, useEffect } from 'react';
|
|
import { useAuth } from '../components/utils/AuthProvider';
|
|
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient';
|
|
import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import html2pdf from 'html2pdf.js';
|
|
import TiptapViewer from '../PagesMedico/TiptapViewer'
|
|
import '../PagesMedico/styleMedico/DoctorRelatorioManager.css';
|
|
|
|
const LaudoManager = () => {
|
|
const navigate = useNavigate();
|
|
const { getAuthorizationHeader } = useAuth();
|
|
const authHeader = getAuthorizationHeader();
|
|
|
|
const [relatoriosOriginais, setRelatoriosOriginais] = useState([]);
|
|
const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]);
|
|
const [relatoriosFinais, setRelatoriosFinais] = useState([]);
|
|
const [pacientesComRelatorios, setPacientesComRelatorios] = useState([]);
|
|
const [medicosComRelatorios, setMedicosComRelatorios] = useState([]);
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [relatorioModal, setRelatorioModal] = useState(null);
|
|
const [termoPesquisa, setTermoPesquisa] = useState('');
|
|
const [filtroExame, setFiltroExame] = useState('');
|
|
const [modalIndex, setModalIndex] = useState(0);
|
|
|
|
const [showProtocolModal, setShowProtocolModal] = useState(false);
|
|
const [protocolForIndex, setProtocolForIndex] = useState(null);
|
|
|
|
const [paginaAtual, setPaginaAtual] = useState(1);
|
|
const [itensPorPagina, setItensPorPagina] = useState(10);
|
|
|
|
const [noPermissionText, setNoPermissionText] = useState(null);
|
|
|
|
const isSecretary = true;
|
|
|
|
const totalPaginas = Math.max(1, Math.ceil(relatoriosFinais.length / itensPorPagina));
|
|
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
|
|
const indiceFinal = indiceInicial + itensPorPagina;
|
|
const relatoriosPaginados = relatoriosFinais.slice(indiceInicial, indiceFinal);
|
|
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
|
|
const fetchReports = async () => {
|
|
try {
|
|
const myHeaders = new Headers();
|
|
myHeaders.append('apikey', API_KEY);
|
|
if (authHeader) myHeaders.append('Authorization', authHeader);
|
|
const 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();
|
|
|
|
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) {
|
|
setRelatoriosOriginais(unique);
|
|
setRelatoriosFiltrados(unique);
|
|
setRelatoriosFinais(unique);
|
|
}
|
|
} catch (err) {
|
|
console.error('Erro listar relatórios', err);
|
|
if (mounted) {
|
|
setRelatoriosOriginais([]);
|
|
setRelatoriosFiltrados([]);
|
|
setRelatoriosFinais([]);
|
|
}
|
|
}
|
|
};
|
|
|
|
fetchReports();
|
|
const refreshHandler = () => fetchReports();
|
|
window.addEventListener('reports:refresh', refreshHandler);
|
|
return () => {
|
|
mounted = false;
|
|
window.removeEventListener('reports:refresh', refreshHandler);
|
|
};
|
|
}, [authHeader]);
|
|
|
|
useEffect(() => {
|
|
const fetchRelData = async () => {
|
|
const pacientes = [];
|
|
const medicos = [];
|
|
for (let i = 0; i < relatoriosFiltrados.length; i++) {
|
|
const rel = relatoriosFiltrados[i];
|
|
try {
|
|
const pacienteRes = await GetPatientByID(rel.patient_id, authHeader);
|
|
pacientes.push(Array.isArray(pacienteRes) ? pacienteRes[0] : pacienteRes);
|
|
} catch (err) {
|
|
pacientes.push(null);
|
|
}
|
|
try {
|
|
const doctorId = rel.created_by || rel.requested_by || null;
|
|
if (doctorId) {
|
|
const docRes = await GetDoctorByID(doctorId, authHeader);
|
|
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes);
|
|
} else {
|
|
medicos.push({ full_name: rel.requested_by || '' });
|
|
}
|
|
} catch (err) {
|
|
medicos.push({ full_name: rel.requested_by || '' });
|
|
}
|
|
}
|
|
setPacientesComRelatorios(pacientes);
|
|
setMedicosComRelatorios(medicos);
|
|
};
|
|
if (relatoriosFiltrados.length > 0) fetchRelData();
|
|
else {
|
|
setPacientesComRelatorios([]);
|
|
setMedicosComRelatorios([]);
|
|
}
|
|
}, [relatoriosFiltrados, authHeader]);
|
|
|
|
const abrirModal = (relatorio, index) => {
|
|
setRelatorioModal(relatorio);
|
|
setModalIndex(index);
|
|
setShowModal(true);
|
|
};
|
|
|
|
const limparFiltros = () => {
|
|
setTermoPesquisa('');
|
|
setFiltroExame('');
|
|
setRelatoriosFinais(relatoriosOriginais);
|
|
};
|
|
|
|
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();
|
|
};
|
|
|
|
const handleEditClick = (relatorio) => {
|
|
if (isSecretary) {
|
|
setNoPermissionText('Sem permissão para editar/criar laudo.');
|
|
return;
|
|
}
|
|
navigate(`/medico/relatorios/${relatorio.id}/edit`);
|
|
};
|
|
|
|
const handleOpenProtocol = (relatorio, index) => {
|
|
setProtocolForIndex({ relatorio, index });
|
|
setShowProtocolModal(true);
|
|
};
|
|
|
|
const handleLiberarLaudo = async (relatorio) => {
|
|
if (isSecretary) {
|
|
|
|
setNoPermissionText('Ainda não implementado');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const myHeaders = new Headers();
|
|
myHeaders.append('apikey', API_KEY);
|
|
if (authHeader) myHeaders.append('Authorization', authHeader);
|
|
myHeaders.append('Content-Type', 'application/json');
|
|
myHeaders.append('Prefer', 'return=representation');
|
|
|
|
const body = JSON.stringify({ status: 'liberado' });
|
|
|
|
const res = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?id=eq.${relatorio.id}`, {
|
|
method: 'PATCH',
|
|
headers: myHeaders,
|
|
body
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const txt = await res.text().catch(()=> '');
|
|
throw new Error('Erro ao liberar laudo: ' + res.status + ' ' + txt);
|
|
}
|
|
|
|
const refreshed = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*", {
|
|
method: 'GET',
|
|
headers: (() => { const h=new Headers(); h.append('apikey', API_KEY); if(authHeader) h.append('Authorization', authHeader); return h; })(),
|
|
});
|
|
const data = await refreshed.json();
|
|
setRelatoriosOriginais(Array.isArray(data)? data : []);
|
|
setRelatoriosFiltrados(Array.isArray(data)? data : []);
|
|
setRelatoriosFinais(Array.isArray(data)? data : []);
|
|
alert('Laudo liberado com sucesso.');
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Erro ao liberar laudo. Veja console.');
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const q = (termoPesquisa || '').toLowerCase().trim();
|
|
const ex = (filtroExame || '').toLowerCase().trim();
|
|
|
|
let items = relatoriosOriginais || [];
|
|
if (q) {
|
|
items = items.filter(r => {
|
|
const patientName = (r.patient_name || r.patient_fullname || '').toString().toLowerCase();
|
|
const pedido = (r.id || r.request_id || r.request || '').toString().toLowerCase();
|
|
return patientName.includes(q) || pedido.includes(q) || (r.patient_id && r.patient_id.toString().includes(q));
|
|
});
|
|
}
|
|
if (ex) items = items.filter(r => (r.exam || r.exame || '').toLowerCase().includes(ex));
|
|
|
|
setRelatoriosFiltrados(items);
|
|
setRelatoriosFinais(items);
|
|
setPaginaAtual(1);
|
|
}, [termoPesquisa, filtroExame, relatoriosOriginais]);
|
|
|
|
const irParaPagina = (pagina) => setPaginaAtual(pagina);
|
|
const avancarPagina = () => { if (paginaAtual < totalPaginas) setPaginaAtual(paginaAtual + 1); };
|
|
const voltarPagina = () => { if (paginaAtual > 1) setPaginaAtual(paginaAtual - 1); };
|
|
const gerarNumerosPaginas = () => {
|
|
const paginas = [];
|
|
const paginasParaMostrar = 5;
|
|
let inicio = Math.max(1, paginaAtual - Math.floor(paginasParaMostrar / 2));
|
|
let fim = Math.min(totalPaginas, inicio + paginasParaMostrar - 1);
|
|
inicio = Math.max(1, fim - paginasParaMostrar + 1);
|
|
for (let i = inicio; i <= fim; i++) paginas.push(i);
|
|
return paginas;
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div className="page-heading"><h3>Lista de Relatórios</h3></div>
|
|
<div className="page-content">
|
|
<section className="row">
|
|
<div className="col-12">
|
|
<div className="card">
|
|
<div className="card-header d-flex justify-content-between align-items-center">
|
|
<h4 className="card-title mb-0">Relatórios Cadastrados</h4>
|
|
<div>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={() => setNoPermissionText('Sem permissão para editar/criar laudo.')}
|
|
title="Secretaria não pode criar relatórios"
|
|
>
|
|
<i className="bi bi-plus-circle"></i> Adicionar Relatório
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card-body">
|
|
<div className="card p-3 mb-3">
|
|
<h5 className="mb-3">
|
|
<i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros
|
|
</h5>
|
|
<div className="row">
|
|
<div className="col-md-5">
|
|
<div className="mb-3">
|
|
<label className="form-label">Buscar por nome ou CPF do paciente</label>
|
|
<input
|
|
type="text"
|
|
className="form-control"
|
|
placeholder="Digite nome ou CPF do paciente..."
|
|
value={termoPesquisa}
|
|
onChange={(e) => setTermoPesquisa(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-md-5">
|
|
<div className="mb-3">
|
|
<label className="form-label">Filtrar por tipo de exame</label>
|
|
<input
|
|
type="text"
|
|
className="form-control"
|
|
placeholder="Digite o tipo de exame..."
|
|
value={filtroExame}
|
|
onChange={(e) => setFiltroExame(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-md-2 d-flex align-items-end">
|
|
<button className="btn btn-outline-secondary w-100" onClick={limparFiltros}>
|
|
<i className="bi bi-arrow-clockwise"></i> Limpar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="mt-2">
|
|
<div className="contador-relatorios">
|
|
{relatoriosFinais.length} DE {relatoriosOriginais.length} RELATÓRIOS ENCONTRADOS
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="table-responsive">
|
|
<table className="table table-striped table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Paciente</th>
|
|
<th>CPF</th>
|
|
<th>Exame</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{relatoriosPaginados.length > 0 ? (
|
|
relatoriosPaginados.map((relatorio, index) => {
|
|
const paciente = pacientesComRelatorios[index] || {};
|
|
return (
|
|
<tr key={relatorio.id || index}>
|
|
<td>{paciente?.full_name || relatorio.patient_name || 'Carregando...'}</td>
|
|
<td>{paciente?.cpf || 'Carregando...'}</td>
|
|
<td>{relatorio.exam || relatorio.exame || '—'}</td>
|
|
<td>
|
|
<div className="d-flex gap-2">
|
|
<button className="btn btn-sm btn-ver-detalhes" onClick={() => abrirModal(relatorio, index)}>
|
|
<i className="bi bi-eye me-1"></i> Ver Detalhes
|
|
</button>
|
|
|
|
<button className="btn btn-sm btn-editar" onClick={() => handleEditClick(relatorio)}>
|
|
<i className="bi bi-pencil me-1"></i> Editar
|
|
</button>
|
|
|
|
|
|
|
|
<button className="btn btn-sm btn-protocolo" onClick={() => handleOpenProtocol(relatorio, index)}>
|
|
<i className="bi bi-send me-1"></i> Protocolo
|
|
</button>
|
|
|
|
<button className="btn btn-sm btn-liberar" onClick={() => handleLiberarLaudo(relatorio)}>
|
|
<i className="bi bi-unlock me-1"></i> Liberar laudo
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})
|
|
) : (
|
|
<tr><td colSpan="4" className="text-center">Nenhum relatório encontrado.</td></tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
|
|
{relatoriosFinais.length > 0 && (
|
|
<div className="d-flex justify-content-between align-items-center mt-3">
|
|
<div className="d-flex align-items-center">
|
|
<span className="me-2 text-muted">Itens por página:</span>
|
|
<select
|
|
className="form-select form-select-sm w-auto"
|
|
value={itensPorPagina}
|
|
onChange={(e) => {
|
|
setItensPorPagina(Number(e.target.value));
|
|
setPaginaAtual(1);
|
|
}}
|
|
>
|
|
<option value={5}>5</option>
|
|
<option value={10}>10</option>
|
|
<option value={25}>25</option>
|
|
<option value={50}>50</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="d-flex align-items-center">
|
|
<span className="me-3 text-muted">
|
|
Página {paginaAtual} de {totalPaginas} •
|
|
Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, relatoriosFinais.length)} de {relatoriosFinais.length} itens
|
|
</span>
|
|
|
|
<nav>
|
|
<ul className="pagination pagination-sm mb-0">
|
|
<li className={`page-item ${paginaAtual === 1 ? 'disabled' : ''}`}>
|
|
<button className="page-link" onClick={voltarPagina}>
|
|
<i className="bi bi-chevron-left"></i>
|
|
</button>
|
|
</li>
|
|
|
|
{gerarNumerosPaginas().map(pagina => (
|
|
<li key={pagina} className={`page-item ${pagina === paginaAtual ? 'active' : ''}`}>
|
|
<button className="page-link" onClick={() => irParaPagina(pagina)}>
|
|
{pagina}
|
|
</button>
|
|
</li>
|
|
))}
|
|
|
|
<li className={`page-item ${paginaAtual === totalPaginas ? 'disabled' : ''}`}>
|
|
<button className="page-link" onClick={avancarPagina}>
|
|
<i className="bi bi-chevron-right"></i>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
|
|
{showModal && relatorioModal && (
|
|
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0, 0, 0, 0.5)" }} tabIndex="-1">
|
|
<div className="modal-dialog modal-dialog-centered modal-lg">
|
|
<div className="modal-content">
|
|
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}>
|
|
<h5 className="modal-title">Relatório de {pacientesComRelatorios[modalIndex]?.full_name || relatorioModal.patient_name || 'Paciente'}</h5>
|
|
</div>
|
|
|
|
<div className="modal-body">
|
|
<div id={`folhaA4-${modalIndex}`} 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' style={{ padding: '0 6px' }}>
|
|
<p><strong>Paciente:</strong> {pacientesComRelatorios[modalIndex]?.full_name || relatorioModal.patient_name || '—'}</p>
|
|
<p><strong>Data de nascimento:</strong> {pacientesComRelatorios[modalIndex]?.birth_date || '—'}</p>
|
|
<p><strong>Data do exame:</strong> {relatorioModal?.due_at || relatorioModal?.date || '—'}</p>
|
|
|
|
<p style={{ marginTop: 12, fontWeight: '700' }}>Conteúdo do Relatório:</p>
|
|
<div className="tiptap-viewer-wrapper">
|
|
<TiptapViewer htmlContent={relatorioModal?.content_html || relatorioModal?.content || relatorioModal?.diagnosis || 'Relatório não preenchido.'} />
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ marginTop: 20, padding: '0 6px' }}>
|
|
<p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatorioModal?.requested_by || '—'}</p>
|
|
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {relatorioModal?.created_at || '—'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="modal-footer">
|
|
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(pacientesComRelatorios[modalIndex]?.full_name || 'paciente', modalIndex)}>
|
|
<i className='bi bi-file-pdf-fill me-1'></i> Baixar em PDF
|
|
</button>
|
|
<button type="button" className="btn btn-secondary" onClick={() => { setShowModal(false) }}>
|
|
Fechar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
|
|
{showProtocolModal && protocolForIndex && (
|
|
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0, 0, 0, 0.5)" }} tabIndex="-1">
|
|
<div className="modal-dialog modal-dialog-centered">
|
|
<div className="modal-content">
|
|
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}>
|
|
<h5 className="modal-title">Protocolo de Entrega - {protocolForIndex.relatorio?.patient_name || 'Paciente'}</h5>
|
|
</div>
|
|
|
|
<div className="modal-body">
|
|
<div style={{ padding: '0 6px' }}>
|
|
<p><strong>Pedido:</strong> {protocolForIndex.relatorio?.id || protocolForIndex.relatorio?.pedido}</p>
|
|
<p><strong>Paciente:</strong> {protocolForIndex.relatorio?.patient_name || '—'}</p>
|
|
<p><strong>Data:</strong> {protocolForIndex.relatorio?.due_at || protocolForIndex.relatorio?.date || '—'}</p>
|
|
<hr />
|
|
<p>Protocolo de entrega gerado automaticamente. (Substitua pelo endpoint real se houver)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="modal-footer">
|
|
<button className="btn btn-primary" onClick={() => {
|
|
const idx = protocolForIndex.index ?? 0;
|
|
BaixarPDFdoRelatorio(protocolForIndex.relatorio?.patient_name || 'paciente', idx);
|
|
}}>
|
|
<i className='bi bi-file-earmark-pdf-fill me-1'></i> Baixar Protocolo (PDF)
|
|
</button>
|
|
<button type="button" className="btn btn-secondary" onClick={() => setShowProtocolModal(false)}>
|
|
Fechar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{noPermissionText && (
|
|
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0, 0, 0, 0.5)" }} tabIndex="-1">
|
|
<div className="modal-dialog modal-dialog-centered">
|
|
<div className="modal-content">
|
|
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}>
|
|
<h5 className="modal-title">Aviso</h5>
|
|
</div>
|
|
<div className="modal-body">
|
|
<p>{noPermissionText}</p>
|
|
</div>
|
|
<div className="modal-footer">
|
|
<button className="btn btn-primary" onClick={() => setNoPermissionText(null)}>Fechar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LaudoManager;
|