merge detalhesrelatorio

This commit is contained in:
Jessica_Faro 2025-11-12 12:24:45 -03:00
commit d6bd5b955a
3 changed files with 601 additions and 311 deletions

View File

@ -1,9 +1,11 @@
// src/PagesMedico/DoctorRelatorioManager.jsx
import API_KEY from '../components/utils/apiKeys';
import { Link } from 'react-router-dom';
import { 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 { UserInfos } from '../components/utils/Functions-Endpoints/General';
import { useNavigate } from 'react-router-dom';
import html2pdf from 'html2pdf.js';
import TiptapViewer from './TiptapViewer';
@ -12,12 +14,11 @@ import './styleMedico/DoctorRelatorioManager.css';
const DoctorRelatorioManager = () => {
const navigate = useNavigate();
const { getAuthorizationHeader } = useAuth();
let authHeader = getAuthorizationHeader();
const authHeader = getAuthorizationHeader();
const [relatoriosOriginais, setRelatoriosOriginais] = useState([]);
const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]);
const [relatoriosFinais, setRelatoriosFinais] = useState([]);
const [pacientesData, setPacientesData] = useState({});
const [pacientesComRelatorios, setPacientesComRelatorios] = useState([]);
const [medicosComRelatorios, setMedicosComRelatorios] = useState([]);
const [showModal, setShowModal] = useState(false);
@ -30,8 +31,7 @@ const DoctorRelatorioManager = () => {
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const totalPaginas = Math.ceil(relatoriosFinais.length / itensPorPagina);
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);
@ -41,14 +41,76 @@ const DoctorRelatorioManager = () => {
const fetchReports = async () => {
try {
var myHeaders = new Headers();
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
if (authHeader) myHeaders.append('Authorization', authHeader);
var requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
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();
// Tenta descobrir o ID do usuário logado para aplicar filtro por médico
let userId = null;
let userFullName = null;
try {
const token = authHeader ? authHeader.replace(/^Bearer\s+/i, '') : '';
if (token) {
const userInfo = await UserInfos(token);
userId = userInfo?.id || userInfo?.user?.id || userInfo?.sub || null;
userFullName = userInfo?.full_name || (userInfo?.user && userInfo.user.full_name) || null;
}
} catch (err) {
console.warn('Não foi possível obter UserInfos (pode não estar logado):', err);
}
// Monta a URL com possíveis filtros preferenciais
const baseUrl = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*";
let data = [];
if (userId) {
// 1) tenta por doctor_id
try {
const res = await fetch(`${baseUrl}&doctor_id=eq.${userId}`, requestOptions);
data = await res.json();
} catch (e) {
console.warn('Erro ao buscar por doctor_id:', e);
data = [];
}
// 2) fallback para created_by (se vazio)
if ((!Array.isArray(data) || data.length === 0) && userId) {
try {
const res2 = await fetch(`${baseUrl}&created_by=eq.${userId}`, requestOptions);
data = await res2.json();
} catch (e) {
console.warn('Erro ao buscar por created_by:', e);
data = [];
}
}
// 3) fallback para requested_by com nome completo (se ainda vazio)
if ((!Array.isArray(data) || data.length === 0) && userFullName) {
try {
// encode para evitar problemas com espaços/caracteres especiais
const encodedName = encodeURIComponent(userFullName);
const res3 = await fetch(`${baseUrl}&requested_by=eq.${encodedName}`, requestOptions);
data = await res3.json();
} catch (e) {
console.warn('Erro ao buscar por requested_by:', e);
data = [];
}
}
}
// Se não obteve userId ou nenhuma das tentativas acima retornou algo, busca tudo (comportamento anterior)
if (!userId || (!Array.isArray(data) || data.length === 0)) {
try {
const resAll = await fetch(baseUrl, requestOptions);
data = await resAll.json();
} catch (e) {
console.error('Erro listar relatórios (busca completa):', e);
data = [];
}
}
// garante unicidade e ordenação por criação
const uniqueMap = new Map();
(Array.isArray(data) ? data : []).forEach(r => {
if (r && r.id) uniqueMap.set(r.id, r);
@ -62,7 +124,7 @@ const DoctorRelatorioManager = () => {
setRelatoriosFinais(unique);
}
} catch (err) {
console.error('Erro listar relatórios', err);
console.error('Erro listar relatórios (catch):', err);
if (mounted) {
setRelatoriosOriginais([]);
setRelatoriosFiltrados([]);
@ -82,43 +144,57 @@ const DoctorRelatorioManager = () => {
};
}, [authHeader]);
// Busca dados de pacientes e médicos baseados na lista final que aparece na tela
useEffect(() => {
const fetchRelData = async () => {
const pacientes = [];
const medicos = [];
for (let i = 0; i < relatoriosFiltrados.length; i++) {
const rel = relatoriosFiltrados[i];
for (let i = 0; i < relatoriosFinais.length; i++) {
const rel = relatoriosFinais[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: prioriza campos com id (doctor_id ou created_by). Se tiver somente requested_by (nome), usa nome.
try {
const doctorId = rel.created_by || rel.requested_by || null;
if (doctorId) {
const docRes = await GetDoctorByID(doctorId, authHeader);
if (rel.doctor_id) {
const docRes = await GetDoctorByID(rel.doctor_id, authHeader);
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes);
} else if (rel.created_by) {
// created_by costuma ser id
const docRes = await GetDoctorByID(rel.created_by, authHeader);
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes);
} else if (rel.requested_by) {
medicos.push({ full_name: rel.requested_by });
} else {
medicos.push({ full_name: rel.requested_by || '' });
medicos.push({ full_name: '' });
}
} catch (err) {
// fallback para requested_by se houver
medicos.push({ full_name: rel.requested_by || '' });
}
}
setPacientesComRelatorios(pacientes);
setMedicosComRelatorios(medicos);
};
if (relatoriosFiltrados.length > 0) fetchRelData();
if (relatoriosFinais.length > 0) fetchRelData();
else {
setPacientesComRelatorios([]);
setMedicosComRelatorios([]);
}
}, [relatoriosFiltrados, authHeader]);
}, [relatoriosFinais, authHeader]);
const abrirModal = (relatorio, index) => {
const abrirModal = (relatorio, pageIndex) => {
// encontra índice global do relatório no array relatoriosFinais (para alinhar com pacientes/medicos)
const globalIndex = relatoriosFinais.findIndex(r => r.id === relatorio.id);
const indexToUse = globalIndex >= 0 ? globalIndex : (indiceInicial + pageIndex);
setRelatorioModal(relatorio);
setModalIndex(index);
setModalIndex(indexToUse);
setShowModal(true);
};
@ -127,6 +203,7 @@ const DoctorRelatorioManager = () => {
setTermoPesquisa('');
setFiltroExame('');
setRelatoriosFinais(relatoriosOriginais);
setPaginaAtual(1);
};
const BaixarPDFdoRelatorio = (nome_paciente, idx) => {
@ -198,17 +275,17 @@ const DoctorRelatorioManager = () => {
<div id='infoPaciente' style={{ padding: '0 6px' }}>
<p><strong>Paciente:</strong> {pacientesComRelatorios[modalIndex]?.full_name}</p>
<p><strong>Data de nascimento:</strong> {pacientesComRelatorios[modalIndex]?.birth_date || '—'}</p>
<p><strong>Data do exame:</strong> {relatoriosFiltrados[modalIndex]?.due_at || '—'}</p>
<p><strong>Data do exame:</strong> {relatoriosFinais[modalIndex]?.due_at || '—'}</p>
<p style={{ marginTop: 12, fontWeight: '700' }}>Conteúdo do Relatório:</p>
<div className="tiptap-viewer-wrapper">
<TiptapViewer htmlContent={relatoriosFiltrados[modalIndex]?.content_html || relatoriosFiltrados[modalIndex]?.content || 'Relatório não preenchido.'} />
<TiptapViewer htmlContent={relatoriosFinais[modalIndex]?.content_html || relatoriosFinais[modalIndex]?.content || 'Relatório não preenchido.'} />
</div>
</div>
<div style={{ marginTop: 20, padding: '0 6px' }}>
<p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatoriosFiltrados[modalIndex]?.requested_by}</p>
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {relatoriosFiltrados[modalIndex]?.created_at || '—'}</p>
<p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatoriosFinais[modalIndex]?.requested_by}</p>
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {relatoriosFinais[modalIndex]?.created_at || '—'}</p>
</div>
</div>
</div>
@ -295,7 +372,8 @@ const DoctorRelatorioManager = () => {
<tbody>
{relatoriosPaginados.length > 0 ? (
relatoriosPaginados.map((relatorio, index) => {
const paciente = pacientesData[relatorio.patient_id];
const globalIndex = relatoriosFinais.findIndex(r => r.id === relatorio.id);
const paciente = pacientesComRelatorios[globalIndex];
return (
<tr key={relatorio.id}>
<td>{paciente?.full_name || 'Carregando...'}</td>
@ -381,4 +459,4 @@ const DoctorRelatorioManager = () => {
);
};
export default DoctorRelatorioManager;
export default DoctorRelatorioManager;

View File

@ -1,337 +1,514 @@
// src/pages/LaudoManager.jsx
import React, { useState, useEffect } from "react";
import "./LaudoStyle.css"; // Importa o CSS externo
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';
/* ===== Mock data (simula APIDOG) ===== */
function mockFetchLaudos() {
return [
{
id: "LAU-300551296",
pedido: 300551296,
data: "29/07/2025",
paciente: { nome: "Sarah Mariana Oliveira", cpf: "616.869.070-**", nascimento: "1990-03-25", convenio: "Unimed" },
solicitante: "Sandro Rangel Santos",
exame: "US - Abdome Total",
conteudo: "RELATÓRIO MÉDICO\n\nAchados: Imagens compatíveis com ...\nConclusão: Órgãos sem alterações significativas.",
status: "rascunho"
},
{
id: "LAU-300659170",
pedido: 300659170,
data: "29/07/2025",
paciente: { nome: "Laissa Helena Marquetti", cpf: "950.684.57-**", nascimento: "1986-09-12", convenio: "Bradesco" },
solicitante: "Sandro Rangel Santos",
exame: "US - Mamária Bilateral",
conteudo: "RELATÓRIO MÉDICO\n\nAchados: text...",
status: "liberado"
},
{
id: "LAU-300658301",
pedido: 300658301,
data: "28/07/2025",
paciente: { nome: "Vera Lúcia Oliveira Santos", cpf: "928.005.**", nascimento: "1979-02-02", convenio: "Particular" },
solicitante: "Dr. Fulano",
exame: "US - Transvaginal",
conteudo: "RELATÓRIO MÉDICO\n\nAchados: ...",
status: "entregue"
}
];
}
const LaudoManager = () => {
const navigate = useNavigate();
const { getAuthorizationHeader } = useAuth();
const authHeader = getAuthorizationHeader();
function mockDeleteLaudo(id) {
return new Promise((res) => setTimeout(() => res({ ok: true }), 500));
}
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);
/* ===== Componente ===== */
export default function LaudoManager() {
const [laudos, setLaudos] = useState([]);
const [openDropdownId, setOpenDropdownId] = useState(null);
const [showProtocolModal, setShowProtocolModal] = useState(false);
const [protocolForIndex, setProtocolForIndex] = useState(null);
/* viewerLaudo é usado para mostrar o editor/leitura;
previewLaudo é usado para a pré-visualização (sem bloquear) */
const [viewerLaudo, setViewerLaudo] = useState(null);
const [previewLaudo, setPreviewLaudo] = useState(null);
const [showPreview, setShowPreview] = useState(false);
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
const [toDelete, setToDelete] = useState(null);
const [loadingDelete, setLoadingDelete] = useState(false);
// agora guardamos a mensagem (null = sem aviso)
const [noPermissionText, setNoPermissionText] = useState(null);
/* notificação simples (sem backdrop) para 'sem permissão' */
const [showNoPermission, setShowNoPermission] = useState(false);
const isSecretary = true;
/* pesquisa */
const [query, setQuery] = useState("");
/* Para simplificar: eu assumo aqui que estamos na visão da secretaria */
const isSecretary = true; // permanece true (somente leitura)
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(() => {
// Importa os dados mock apenas
const data = mockFetchLaudos();
setLaudos(data);
}, []);
let mounted = true;
// Fecha dropdown ao clicar fora
useEffect(() => {
function onDocClick(e) {
if (e.target.closest && e.target.closest('.action-btn')) return;
if (e.target.closest && e.target.closest('.dropdown')) return;
setOpenDropdownId(null);
}
document.addEventListener('click', onDocClick);
return () => document.removeEventListener('click', onDocClick);
}, []);
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' };
function toggleDropdown(id, e) {
e.stopPropagation();
setOpenDropdownId(prev => (prev === id ? null : id));
}
const res = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*", requestOptions);
const data = await res.json();
/* (botao editar) */
function handleOpenViewer(laudo) {
setOpenDropdownId(null);
if (isSecretary) {
// (notificação sem bloquear)
setShowNoPermission(true);
return;
}
setViewerLaudo(laudo);
}
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));
/* (botao imprimir) */
function handlePrint(laudo) {
// evitar bug: fechar viewer antes de abrir preview
setViewerLaudo(null);
setPreviewLaudo(laudo);
setShowPreview(true);
setOpenDropdownId(null);
}
/* (botao excluir) */
function handleRequestDelete(laudo) {
setToDelete(laudo);
setOpenDropdownId(null);
setShowConfirmDelete(true);
}
/* (funcionalidade do botao de excluir) */
async function doConfirmDelete(confirm) {
if (!toDelete) return;
if (!confirm) {
setShowConfirmDelete(false);
setToDelete(null);
return;
}
setLoadingDelete(true);
try {
const resp = await mockDeleteLaudo(toDelete.id);
if (resp.ok || resp === true) {
// removo o laudo da lista local
setLaudos(curr => curr.filter(l => l.id !== toDelete.id));
setShowConfirmDelete(false);
setToDelete(null);
alert("Laudo excluído com sucesso.");
} else {
alert("Erro ao excluir. Tente novamente.");
if (mounted) {
setRelatoriosOriginais(unique);
setRelatoriosFiltrados(unique);
setRelatoriosFinais(unique);
}
} catch (err) {
console.error('Erro listar relatórios', err);
if (mounted) {
setRelatoriosOriginais([]);
setRelatoriosFiltrados([]);
setRelatoriosFinais([]);
}
}
} catch (err) {
alert("Erro de rede ao excluir.");
} finally {
setLoadingDelete(false);
}
}
};
/* filtro de pesquisa (por pedido ou nome do paciente) */
const normalized = (s = "") => String(s).toLowerCase();
const filteredLaudos = laudos.filter(l => {
const q = normalized(query).trim();
if (!q) return true;
if (normalized(l.pedido).includes(q)) return true;
if (normalized(l.paciente?.nome).includes(q)) return true;
return false;
});
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) {
// MUDANÇA: mostrar "Ainda não implementado"
setNoPermissionText('Ainda não implementado');
return;
}
// para médicos: implementação real já estava antes (mantive o bloco, caso queira)
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);
}
// refetch simples
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 className="laudo-wrap">
<div className="left-col">
<div className="title-row">
<div>
<div className="page-title">Gerenciamento de Laudo</div>
{/* removi a linha "Visualização: Secretaria" conforme pedido */}
</div>
</div>
<div style={{ marginBottom:12 }}>
<input
placeholder="Pesquisar paciente ou pedido..."
value={query}
onChange={e => setQuery(e.target.value)}
style={{ width:"100%", padding:12, borderRadius:8, border:"1px solid #e6eef8" }}
/>
</div>
{filteredLaudos.length === 0 ? (
<div className="empty">Nenhum laudo encontrado.</div>
) : (
<div style={{ borderRadius:8, overflow:"visible", boxShadow:"0 0 0 1px #eef6ff" }}>
{filteredLaudos.map((l) => (
<div className="laudo-row" key={l.id}>
<div className="col" style={{ flex: "0 0 160px" }}>
<div style={{ fontWeight:700 }}>{l.pedido}</div>
<div className="small-muted">{l.data}</div>
<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 className="col" style={{ flex:2 }}>
<div style={{ fontWeight:600 }}>{l.paciente.nome}</div>
<div className="small-muted">{l.paciente.cpf} {l.paciente.convenio}</div>
</div>
<div className="col" style={{ flex:1 }}>{l.exame}</div>
<div className="col small">{l.solicitante}</div>
<div className="col small" style={{ flex: "0 0 80px", textAlign:"left" }}>{l.status}</div>
</div>
<div className="row-actions">
<div className="action-btn" onClick={(e)=> toggleDropdown(l.id, e)} title="Ações">
<i class="bi bi-three-dots-vertical"></i>
<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>
{openDropdownId === l.id && (
<div className="dropdown" data-laudo-dropdown={l.id}>
<div className="item" onClick={() => handleOpenViewer(l)}>Editar</div>
<div className="item" onClick={() => handlePrint(l)}>Imprimir</div>
<div className="item" onClick={() => { alert("Protocolo de entrega: formulário (não implementado)."); setOpenDropdownId(null); }}>Protocolo de entrega</div>
<div className="item" onClick={() => { alert("Liberar laudo: requer permissão de médico. (não implementado)"); setOpenDropdownId(null); }}>Liberar laudo</div>
<div className="item" onClick={() => handleRequestDelete(l)} style={{ color:"#c23b3b" }}>Excluir laudo</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>
{/* Removido o botão "Imprimir" daqui (agora no Ver Detalhes) */}
<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>
{/* Viewer modal (modo leitura) — só abre para quem tem permissão */}
{viewerLaudo && !showPreview && !isSecretary && (
<div className="viewer-modal" style={{ pointerEvents:"auto" }}>
<div className="modal-backdrop" onClick={() => setViewerLaudo(null)} />
<div className="modal-card" role="dialog" aria-modal="true">
<div className="viewer-header">
<div>
<div style={{ fontSize:18, fontWeight:700 }}>{viewerLaudo.paciente.nome}</div>
<div className="patient-info">
Nasc.: {viewerLaudo.paciente.nascimento} {computeAge(viewerLaudo.paciente.nascimento)} anos {viewerLaudo.paciente.cpf} {viewerLaudo.paciente.convenio}
{/* Modal principal (detalhes) */}
{showModal && relatorioModal && (
<div className="modal modal-centered" role="dialog" aria-modal="true" onClick={() => setShowModal(false)}>
<div className="modal-dialog modal-dialog-square" role="document" onClick={(e) => e.stopPropagation()}>
<div className="modal-content">
<div className="modal-header custom-modal-header">
<h5 className="modal-title">Relatório de {pacientesComRelatorios[modalIndex]?.full_name || relatorioModal.patient_name || 'Paciente'}</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-${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 style={{ display:"flex", gap:8 }}>
<button className="tool-btn" onClick={() => { setPreviewLaudo(viewerLaudo); setShowPreview(true); setViewerLaudo(null); }}>Pré-visualizar / Imprimir</button>
<button className="tool-btn" onClick={() => setViewerLaudo(null)}>Fechar</button>
</div>
</div>
<div className="toolbar">
<div className="tool-btn">B</div>
<div className="tool-btn"><i>I</i></div>
<div className="tool-btn"><u>U</u></div>
<div className="tool-btn">Fonte</div>
<div className="tool-btn">Tamanho</div>
<div className="tool-btn">Lista</div>
<div className="tool-btn">Campos</div>
<div className="tool-btn">Modelos</div>
<div className="tool-btn">Imagens</div>
</div>
<div className="editor-area" aria-readonly>
{viewerLaudo.conteudo.split("\n").map((line, i) => (
<p key={i} style={{ margin: line.trim()==="" ? "8px 0" : "6px 0" }}>{line}</p>
))}
</div>
<div className="footer-controls">
<div className="toggle small-muted">
<label><input type="checkbox" disabled /> Pré-visualização</label>
<label style={{ marginLeft:12 }}><input type="checkbox" disabled /> Ocultar data</label>
<label style={{ marginLeft:12 }}><input type="checkbox" disabled /> Ocultar assinatura</label>
</div>
<div style={{ display:"flex", gap:8 }}>
<button className="btn secondary" onClick={() => { if(window.confirm("Cancelar e voltar à lista? Todas alterações não salvas serão perdidas.")) setViewerLaudo(null); }}>Cancelar</button>
<button className="btn primary" onClick={() => alert("Salvar (não implementado para secretaria).")}>Salvar laudo</button>
<div className="modal-footer custom-modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(pacientesComRelatorios[modalIndex]?.full_name || 'paciente', modalIndex)}>
<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>
)}
{/* Preview modal — agora não bloqueia a tela (sem backdrop escuro), botão imprimir é interativo */}
{showPreview && previewLaudo && (
<div className="preview-modal" style={{ pointerEvents:"none" /* container não bloqueia */ }}>
<div /* sem backdrop, assim não deixa a tela escura/blocked */ />
<div className="modal-card" style={{ maxWidth:900, pointerEvents:"auto" }}>
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:12 }}>
<div style={{ fontWeight:700 }}>Pré-visualização - {previewLaudo.paciente.nome}</div>
<div style={{ display:"flex", gap:8 }}>
<button className="tool-btn" onClick={() => alert("Imprimir (simulado).")}>Imprimir / Download</button>
<button className="tool-btn" onClick={() => { setShowPreview(false); setPreviewLaudo(null); }}>Fechar</button>
</div>
</div>
<div style={{ border: "1px solid #e6eef8", borderRadius:6, padding:18, background:"#fff" }}>
<div style={{ marginBottom:8, fontSize:14, color:"#33475b" }}>
<strong>RELATÓRIO MÉDICO</strong>
</div>
<div style={{ marginBottom:14, fontSize:13, color:"#546b7f" }}>
{previewLaudo.paciente.nome} Nasc.: {previewLaudo.paciente.nascimento} CPF: {previewLaudo.paciente.cpf}
{/* Modal Protocolo */}
{showProtocolModal && protocolForIndex && (
<div className="modal modal-centered" role="dialog" aria-modal="true" onClick={() => setShowProtocolModal(false)}>
<div className="modal-dialog modal-dialog-tabela-relatorio" role="document" onClick={(e) => e.stopPropagation()}>
<div className="modal-content">
<div className="modal-header custom-modal-header">
<h5 className="modal-title">Protocolo de Entrega - {protocolForIndex.relatorio?.patient_name || 'Paciente'}</h5>
<button type="button" className="btn-close modal-close-btn" aria-label="Close" onClick={() => setShowProtocolModal(false)}></button>
</div>
<div style={{ whiteSpace:"pre-wrap", fontSize:15, color:"#1f2d3d", lineHeight:1.5 }}>
{previewLaudo.conteudo}
<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 custom-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'></i> baixar protocolo (PDF)
</button>
<button type="button" className="btn btn-outline-secondary" onClick={() => setShowProtocolModal(false)}>
Fechar
</button>
</div>
</div>
</div>
</div>
)}
{/* Notificação simples: Sem permissão (exibe sem backdrop escuro) - centralizada */}
{showNoPermission && (
<div className="notice-card" role="alert" aria-live="polite">
<div style={{ fontWeight:700, marginBottom:6 }}>Sem permissão para editar</div>
<div style={{ marginBottom:10, color:"#5a6f80" }}>Você está na visualização da secretaria. Edição disponível somente para médicos autorizados.</div>
<div style={{ textAlign:"right" }}>
<button className="tool-btn" onClick={() => setShowNoPermission(false)}>Fechar</button>
</div>
</div>
)}
{/* Confirm delete modal (simples: Sim / Não) */}
{showConfirmDelete && toDelete && (
<div className="confirm-modal" style={{ pointerEvents:"auto" }}>
<div className="modal-card" style={{ maxWidth:480 }}>
<div style={{ fontWeight:700, marginBottom:8 }}>Confirmar exclusão</div>
<div style={{ marginBottom:12 }}>Você tem certeza que quer excluir o laudo <strong>{toDelete.pedido} - {toDelete.paciente.nome}</strong> ? Esta ação é irreversível.</div>
<div style={{ display:"flex", justifyContent:"flex-end", gap:8 }}>
<button className="tool-btn" onClick={() => doConfirmDelete(false)} disabled={loadingDelete}>Não</button>
<button className="tool-btn" onClick={() => doConfirmDelete(true)} disabled={loadingDelete} style={{ background: loadingDelete ? "#d7e8ff" : "#ffecec", border: "1px solid #ffd7d7" }}>
{loadingDelete ? "Excluindo..." : "Sim, excluir"}
</button>
{/* Variável de aviso: mostra texto personalizado */}
{noPermissionText && (
<div className="modal modal-centered" role="dialog" aria-modal="true" onClick={() => setNoPermissionText(null)}>
<div className="modal-dialog modal-dialog-square" role="document" onClick={(e) => e.stopPropagation()}>
<div className="modal-content">
<div style={{ padding: 18 }}>
<h5 style={{ marginBottom: 8 }}>{noPermissionText}</h5>
<p style={{ color: '#6c757d' }}>{/* opcional descrição aqui */}</p>
<div style={{ textAlign: 'right', marginTop: 12 }}>
<button className="btn btn-outline-secondary" onClick={() => setNoPermissionText(null)}>Fechar</button>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
}
};
/* ===== Helpers ===== */
function computeAge(birth) {
if (!birth) return "-";
const [y,m,d] = birth.split("-").map(x => parseInt(x,10));
if (!y) return "-";
const today = new Date();
let age = today.getFullYear() - y;
const mm = today.getMonth() + 1;
const dd = today.getDate();
if (mm < m || (mm === m && dd < d)) age--;
return age;
}
export default LaudoManager;

View File

@ -309,4 +309,39 @@ html[data-bs-theme="dark"] .notice-card {
background: #232323 !important;
color: #e0e0e0 !important;
box-shadow: 0 8px 30px rgba(10,20,40,0.32) !important;
}
}
/* Botões coloridos para Protocolo e Liberar (combina com estilo dos outros botões) */
.btn-protocolo {
background-color: #E6F2FF;
color: #004085;
border: 1px solid #d6e9ff;
padding: 8px 12px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}
.btn-protocolo:hover {
background-color: #cce5ff;
}
/* Liberar laudo - estilo parecido com o botão editar (amarelo claro) */
.btn-liberar {
background-color: #FFF3CD;
color: #856404;
border: 1px solid #ffeaa7;
padding: 8px 12px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}
.btn-liberar:hover {
background-color: #ffeaa7;
}
/* Ajuste visual (pequeno) para espaçamento horizontal dos botões da linha */
.table-responsive .d-flex.gap-2 .btn {
display: inline-flex;
align-items: center;
gap: 6px;
}