// src/pages/LaudoManager.jsx import React, { useState, useRef, useEffect } from "react"; /* ===== Estilos embutidos ===== */ /* Eu coloquei os estilos aqui para simplificar a edição. */ const styles = ` .laudo-wrap { display:flex; gap:24px; padding:18px; font-family: Inter, Roboto, Arial, sans-serif; } .left-col { width: 100%; max-width: 1160px; background:#ffffff; border-radius:8px; padding:18px; box-shadow: 0 1px 0 rgba(0,0,0,0.03); } /* <-- fundo branco */ .title-row { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; } .page-title { font-size:20px; color:#2b4a78; font-weight:700; } .laudo-row { display:flex; padding:14px 12px; align-items:center; border-bottom:1px solid #eef3f8; position:relative; overflow:visible; background: transparent; } .col { flex:1; padding:0 8px; font-size:14px; color:#2e3a4b; } .col.small { flex:0 0 90px; text-align:right; } .row-actions { position:relative; flex: 0 0 88px; display:flex; justify-content:flex-end; } .action-btn { background:transparent; border:1px solid #d7e6fb; border-radius:8px; height:40px; width:40px; display:flex; align-items:center; justify-content:center; cursor:pointer; } .dropdown { position:absolute; right:0; top:48px; background:white; border-radius:8px; box-shadow: 0 10px 30px rgba(20,30,50,0.12); min-width:220px; padding:8px 0; z-index:9999; } .dropdown .item { padding:12px 18px; cursor:pointer; font-size:15px; color:#244056; } .dropdown .item:hover { background:#f6fbff; } .viewer-modal, .preview-modal, .confirm-modal { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; z-index:12000; pointer-events:none; } /* deixar pointer-events none para que não bloqueie por padrão */ .modal-backdrop { position:absolute; inset:0; background: rgba(9,20,40,0.45); pointer-events:auto; } /* usado apenas quando necessário */ .modal-card { position:relative; width:92%; max-width:1100px; background:white; border-radius:10px; padding:18px; box-shadow: 0 10px 60px rgba(10,20,40,0.25); max-height:88vh; overflow:auto; pointer-events:auto; } .viewer-header { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; margin-bottom:12px; } .patient-info { font-size:13px; color:#3a556b; } .toolbar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:12px; } .tool-btn { padding:8px 10px; border-radius:6px; border:1px solid #e6eef8; cursor:pointer; background:#fff; font-size:13px; } .editor-area { border:1px solid #e6eef8; border-radius:8px; padding:14px; min-height:360px; background: #fff; color:#1f2d3d; font-size:15px; line-height:1.5; } .footer-controls { display:flex; justify-content:space-between; align-items:center; margin-top:12px; } .toggle { display:flex; align-items:center; gap:8px; } .btn { padding:8px 12px; border-radius:8px; border:none; cursor:pointer; font-weight:600; } .btn.secondary { background:#eef6ff; color:#2f63a6; border:1px solid #d6e9ff; } .btn.primary { background:#2f63a6; color:white; } .small-muted { color:#7f95a8; font-size:13px; } .empty { padding:40px; text-align:center; color:#7d97b4; } /* notificação simples (sem backdrop escuro) */ .notice-card { position:fixed; top:20vh; left:50%; transform:translateX(-50%); background:#fff; border-radius:8px; padding:14px 18px; box-shadow:0 8px 30px rgba(10,20,40,0.12); z-index:13000; pointer-events:auto; } `; /* ===== 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" } ]; } function mockDeleteLaudo(id) { return new Promise((res) => setTimeout(() => res({ ok: true }), 500)); } /* ===== Componente ===== */ export default function LaudoManager() { const [laudos, setLaudos] = useState([]); const [openDropdownId, setOpenDropdownId] = 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 [showConfirmDelete, setShowConfirmDelete] = useState(false); const [toDelete, setToDelete] = useState(null); const [loadingDelete, setLoadingDelete] = useState(false); /* notificação simples (sem backdrop) para 'sem permissão' */ const [showNoPermission, setShowNoPermission] = useState(false); /* Para simplificar: eu assumo aqui que estamos na visão da secretaria */ const isSecretary = true; // eu deixei true para forçar o comportamento "somente leitura" useEffect(() => { const el = document.createElement("style"); el.innerHTML = styles; document.head.appendChild(el); const data = mockFetchLaudos(); setLaudos(data); return () => document.head.removeChild(el); }, []); // 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); }, []); function toggleDropdown(id, e) { e.stopPropagation(); setOpenDropdownId(prev => (prev === id ? null : id)); } /* Quando clicar em Editar: - se for secretaria eu mostro um aviso simples sem fundo escuro - se for médico (isSecretary=false) eu abriria o editor (aqui eu deixei o mecanismo) */ function handleOpenViewer(laudo) { setOpenDropdownId(null); if (isSecretary) { // eu mostro um aviso simples (sem modal que bloqueia) setShowNoPermission(true); return; } setViewerLaudo(laudo); } /* Ao pedir impressão: eu fecho qualquer viewer aberto e abro a preview - a pré-visualização NÃO bloqueia a página (removi backdrop) */ function handlePrint(laudo) { // Evito o bug: fechar viewer antes de abrir preview setViewerLaudo(null); setPreviewLaudo(laudo); setShowPreview(true); setOpenDropdownId(null); } function handleRequestDelete(laudo) { setToDelete(laudo); setOpenDropdownId(null); setShowConfirmDelete(true); } 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) { // eu 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."); } } catch (err) { alert("Erro de rede ao excluir."); } finally { setLoadingDelete(false); } } return (
Gerenciamento de Laudo
Visualização: Secretaria (Somente leitura)
{laudos.length === 0 ? (
Nenhum laudo encontrado.
) : (
{laudos.map((l) => (
{l.pedido}
{l.data}
{l.paciente.nome}
{l.paciente.cpf} • {l.paciente.convenio}
{l.exame}
{l.solicitante}
{l.status}
toggleDropdown(l.id, e)} title="Ações">
{openDropdownId === l.id && (
handleOpenViewer(l)}>Editar
handlePrint(l)}>Imprimir
{ alert("Protocolo de entrega: formulário (não implementado)."); setOpenDropdownId(null); }}>Protocolo de entrega
{ alert("Liberar laudo: requer permissão de médico. (não implementado)"); setOpenDropdownId(null); }}>Liberar laudo
handleRequestDelete(l)} style={{ color:"#c23b3b" }}>Excluir laudo
)}
))}
)}
{/* Viewer modal (modo leitura) — só abre para quem tem permissão */} {viewerLaudo && !showPreview && !isSecretary && (
setViewerLaudo(null)} />
{viewerLaudo.paciente.nome}
Nasc.: {viewerLaudo.paciente.nascimento} • {computeAge(viewerLaudo.paciente.nascimento)} anos • {viewerLaudo.paciente.cpf} • {viewerLaudo.paciente.convenio}
B
I
U
Fonte
Tamanho
Lista
Campos
Modelos
Imagens
{viewerLaudo.conteudo.split("\n").map((line, i) => (

{line}

))}
)} {/* Preview modal — agora não bloqueia a tela (sem backdrop escuro), botão imprimir é interativo */} {showPreview && previewLaudo && (
Pré-visualização - {previewLaudo.paciente.nome}
RELATÓRIO MÉDICO
{previewLaudo.paciente.nome} • Nasc.: {previewLaudo.paciente.nascimento} • CPF: {previewLaudo.paciente.cpf}
{previewLaudo.conteudo}
)} {/* Notificação simples: Sem permissão (exibe sem backdrop escuro) */} {showNoPermission && (
Sem permissão para editar
Você está na visualização da secretaria. Edição disponível somente para médicos autorizados.
)} {/* Confirm delete modal (simples: Sim / Não) */} {showConfirmDelete && toDelete && (
Confirmar exclusão
Você tem certeza que quer excluir o laudo {toDelete.pedido} - {toDelete.paciente.nome} ? Esta ação é irreversível.
)}
); } /* ===== 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; }