Atualizacão do laudo

This commit is contained in:
Jessica_Faro 2025-09-12 15:04:17 -03:00
parent f3e74702e1
commit 1af8268943

View File

@ -1,13 +1,14 @@
// src/pages/LaudoManager.jsx
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
/* ===== Estilos embutidos ===== */ /* ===== Estilos embutidos ===== */
/* Eu coloquei os estilos aqui para simplificar a edição. */
const styles = ` const styles = `
.laudo-wrap { display:flex; gap:24px; padding:18px; font-family: Inter, Roboto, Arial, sans-serif; } .laudo-wrap { display:flex; gap:24px; padding:18px; font-family: Inter, Roboto, Arial, sans-serif; }
.left-col { width: 100%; max-width: 1160px; background:#f7fbff; border-radius:8px; padding:18px; box-shadow: 0 1px 0 rgba(0,0,0,0.03);} .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; } .title-row { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
.page-title { font-size:20px; color:#2b4a78; font-weight:700; } .page-title { font-size:20px; color:#2b4a78; font-weight:700; }
.laudo-table { width:100%; border-collapse:collapse; background:#fff; border-radius:8px; overflow:visible; } .laudo-row { display:flex; padding:14px 12px; align-items:center; border-bottom:1px solid #eef3f8; position:relative; overflow:visible; background: transparent; }
.laudo-row { display:flex; padding:14px 12px; align-items:center; border-bottom:1px solid #eef3f8; position:relative; overflow:visible; }
.col { flex:1; padding:0 8px; font-size:14px; color:#2e3a4b; } .col { flex:1; padding:0 8px; font-size:14px; color:#2e3a4b; }
.col.small { flex:0 0 90px; text-align:right; } .col.small { flex:0 0 90px; text-align:right; }
.row-actions { position:relative; flex: 0 0 88px; display:flex; justify-content:flex-end; } .row-actions { position:relative; flex: 0 0 88px; display:flex; justify-content:flex-end; }
@ -15,9 +16,9 @@ const styles = `
.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 { 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 { padding:12px 18px; cursor:pointer; font-size:15px; color:#244056; }
.dropdown .item:hover { background:#f6fbff; } .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; } .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); } .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; } .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; } .viewer-header { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; margin-bottom:12px; }
.patient-info { font-size:13px; color:#3a556b; } .patient-info { font-size:13px; color:#3a556b; }
.toolbar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:12px; } .toolbar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:12px; }
@ -30,6 +31,9 @@ const styles = `
.btn.primary { background:#2f63a6; color:white; } .btn.primary { background:#2f63a6; color:white; }
.small-muted { color:#7f95a8; font-size:13px; } .small-muted { color:#7f95a8; font-size:13px; }
.empty { padding:40px; text-align:center; color:#7d97b4; } .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) ===== */ /* ===== Mock data (simula APIDOG) ===== */
@ -53,7 +57,7 @@ function mockFetchLaudos() {
solicitante: "Sandro Rangel Santos", solicitante: "Sandro Rangel Santos",
exame: "US - Mamária Bilateral", exame: "US - Mamária Bilateral",
conteudo: "RELATÓRIO MÉDICO\n\nAchados: text...", conteudo: "RELATÓRIO MÉDICO\n\nAchados: text...",
status: "rascunho" status: "liberado"
}, },
{ {
id: "LAU-300658301", id: "LAU-300658301",
@ -63,7 +67,7 @@ function mockFetchLaudos() {
solicitante: "Dr. Fulano", solicitante: "Dr. Fulano",
exame: "US - Transvaginal", exame: "US - Transvaginal",
conteudo: "RELATÓRIO MÉDICO\n\nAchados: ...", conteudo: "RELATÓRIO MÉDICO\n\nAchados: ...",
status: "rascunho" status: "entregue"
} }
]; ];
} }
@ -76,12 +80,23 @@ function mockDeleteLaudo(id) {
export default function LaudoManager() { export default function LaudoManager() {
const [laudos, setLaudos] = useState([]); const [laudos, setLaudos] = useState([]);
const [openDropdownId, setOpenDropdownId] = useState(null); 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 [viewerLaudo, setViewerLaudo] = useState(null);
const [previewLaudo, setPreviewLaudo] = useState(null);
const [showPreview, setShowPreview] = useState(false); const [showPreview, setShowPreview] = useState(false);
const [showConfirmDelete, setShowConfirmDelete] = useState(false); const [showConfirmDelete, setShowConfirmDelete] = useState(false);
const [toDelete, setToDelete] = useState(null); const [toDelete, setToDelete] = useState(null);
const [loadingDelete, setLoadingDelete] = useState(false); 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(() => { useEffect(() => {
const el = document.createElement("style"); const el = document.createElement("style");
el.innerHTML = styles; el.innerHTML = styles;
@ -94,11 +109,8 @@ export default function LaudoManager() {
// Fecha dropdown ao clicar fora // Fecha dropdown ao clicar fora
useEffect(() => { useEffect(() => {
function onDocClick(e) { function onDocClick(e) {
// se clicar em um botão de ação (ícone), não fecha
if (e.target.closest && e.target.closest('.action-btn')) return; if (e.target.closest && e.target.closest('.action-btn')) return;
// se clicar dentro de um dropdown, não fecha
if (e.target.closest && e.target.closest('.dropdown')) return; if (e.target.closest && e.target.closest('.dropdown')) return;
// caso contrário, fecha qualquer dropdown aberto
setOpenDropdownId(null); setOpenDropdownId(null);
} }
document.addEventListener('click', onDocClick); document.addEventListener('click', onDocClick);
@ -106,12 +118,30 @@ export default function LaudoManager() {
}, []); }, []);
function toggleDropdown(id, e) { function toggleDropdown(id, e) {
e.stopPropagation(); // evita que o document click feche imediatamente e.stopPropagation();
setOpenDropdownId(prev => (prev === id ? null : id)); 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) { function handleOpenViewer(laudo) {
setOpenDropdownId(null);
if (isSecretary) {
// eu mostro um aviso simples (sem modal que bloqueia)
setShowNoPermission(true);
return;
}
setViewerLaudo(laudo); 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); setOpenDropdownId(null);
} }
@ -121,16 +151,18 @@ export default function LaudoManager() {
setShowConfirmDelete(true); setShowConfirmDelete(true);
} }
async function confirmDelete(typed) { async function doConfirmDelete(confirm) {
if (!toDelete) return; if (!toDelete) return;
if (typed.trim().toUpperCase() !== "EXCLUIR") { if (!confirm) {
alert("Confirmação inválida. Digite EXCLUIR para confirmar."); setShowConfirmDelete(false);
setToDelete(null);
return; return;
} }
setLoadingDelete(true); setLoadingDelete(true);
try { try {
const resp = await mockDeleteLaudo(toDelete.id); const resp = await mockDeleteLaudo(toDelete.id);
if (resp.ok || resp === true) { if (resp.ok || resp === true) {
// eu removo o laudo da lista local
setLaudos(curr => curr.filter(l => l.id !== toDelete.id)); setLaudos(curr => curr.filter(l => l.id !== toDelete.id));
setShowConfirmDelete(false); setShowConfirmDelete(false);
setToDelete(null); setToDelete(null);
@ -145,17 +177,18 @@ export default function LaudoManager() {
} }
} }
function handlePrint(laudo) {
setViewerLaudo(laudo);
setShowPreview(true);
setOpenDropdownId(null);
}
return ( return (
<div className="laudo-wrap"> <div className="laudo-wrap">
<div className="left-col"> <div className="left-col">
<div className="title-row"> <div className="title-row">
<div>
<div className="page-title">Gerenciamento de Laudo</div> <div className="page-title">Gerenciamento de Laudo</div>
<div style={{color:"#7f95a8", marginTop:6}}>Visualização: Secretaria (Somente leitura)</div>
</div>
</div>
<div style={{ marginBottom:12 }}>
<input placeholder="Pesquisar paciente ou pedido..." style={{ width:"100%", padding:12, borderRadius:8, border:"1px solid #e6eef8" }} />
</div> </div>
{laudos.length === 0 ? ( {laudos.length === 0 ? (
@ -174,13 +207,13 @@ export default function LaudoManager() {
</div> </div>
<div className="col" style={{ flex:1 }}>{l.exame}</div> <div className="col" style={{ flex:1 }}>{l.exame}</div>
<div className="col small">{l.solicitante}</div> <div className="col small">{l.solicitante}</div>
<div className="col small" style={{ flex: "0 0 80px", textAlign:"left" }}>{l.status}</div>
<div className="row-actions"> <div className="row-actions">
<div className="action-btn" onClick={(e)=> toggleDropdown(l.id, e)} title="Ações"> <div className="action-btn" onClick={(e)=> toggleDropdown(l.id, e)} title="Ações">
<svg width="16" height="16" viewBox="0 0 24 24"><circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="12" cy="19" r="1.6"/></svg> <svg width="16" height="16" viewBox="0 0 24 24"><circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="12" cy="19" r="1.6"/></svg>
</div> </div>
{/* dropdown associado ao laudo (não será cortado) */}
{openDropdownId === l.id && ( {openDropdownId === l.id && (
<div className="dropdown" data-laudo-dropdown={l.id}> <div className="dropdown" data-laudo-dropdown={l.id}>
<div className="item" onClick={() => handleOpenViewer(l)}>Editar</div> <div className="item" onClick={() => handleOpenViewer(l)}>Editar</div>
@ -197,9 +230,9 @@ export default function LaudoManager() {
)} )}
</div> </div>
{/* Viewer modal (modo leitura) */} {/* Viewer modal (modo leitura) — só abre para quem tem permissão */}
{viewerLaudo && !showPreview && ( {viewerLaudo && !showPreview && !isSecretary && (
<div className="viewer-modal"> <div className="viewer-modal" style={{ pointerEvents:"auto" }}>
<div className="modal-backdrop" onClick={() => setViewerLaudo(null)} /> <div className="modal-backdrop" onClick={() => setViewerLaudo(null)} />
<div className="modal-card" role="dialog" aria-modal="true"> <div className="modal-card" role="dialog" aria-modal="true">
<div className="viewer-header"> <div className="viewer-header">
@ -211,7 +244,7 @@ export default function LaudoManager() {
</div> </div>
<div style={{ display:"flex", gap:8 }}> <div style={{ display:"flex", gap:8 }}>
<button className="tool-btn" onClick={() => { setShowPreview(true); }}>Pré-visualizar / Imprimir</button> <button className="tool-btn" onClick={() => { setPreviewLaudo(viewerLaudo); setShowPreview(true); setViewerLaudo(null); }}>Pré-visualizar / Imprimir</button>
<button className="tool-btn" onClick={() => setViewerLaudo(null)}>Fechar</button> <button className="tool-btn" onClick={() => setViewerLaudo(null)}>Fechar</button>
</div> </div>
</div> </div>
@ -250,16 +283,16 @@ export default function LaudoManager() {
</div> </div>
)} )}
{/* Preview modal */} {/* Preview modal — agora não bloqueia a tela (sem backdrop escuro), botão imprimir é interativo */}
{showPreview && viewerLaudo && ( {showPreview && previewLaudo && (
<div className="preview-modal"> <div className="preview-modal" style={{ pointerEvents:"none" /* container não bloqueia */ }}>
<div className="modal-backdrop" onClick={() => setShowPreview(false)} /> <div /* sem backdrop, assim não deixa a tela escura/blocked */ />
<div className="modal-card" style={{ maxWidth:900 }}> <div className="modal-card" style={{ maxWidth:900, pointerEvents:"auto" }}>
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:12 }}> <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:12 }}>
<div style={{ fontWeight:700 }}>Pré-visualização - {viewerLaudo.paciente.nome}</div> <div style={{ fontWeight:700 }}>Pré-visualização - {previewLaudo.paciente.nome}</div>
<div style={{ display:"flex", gap:8 }}> <div style={{ display:"flex", gap:8 }}>
<button className="tool-btn" onClick={() => window.print()}>Imprimir / Download</button> <button className="tool-btn" onClick={() => alert("Imprimir (simulado).")}>Imprimir / Download</button>
<button className="tool-btn" onClick={() => setShowPreview(false)}>Fechar</button> <button className="tool-btn" onClick={() => { setShowPreview(false); setPreviewLaudo(null); }}>Fechar</button>
</div> </div>
</div> </div>
@ -268,28 +301,40 @@ export default function LaudoManager() {
<strong>RELATÓRIO MÉDICO</strong> <strong>RELATÓRIO MÉDICO</strong>
</div> </div>
<div style={{ marginBottom:14, fontSize:13, color:"#546b7f" }}> <div style={{ marginBottom:14, fontSize:13, color:"#546b7f" }}>
{viewerLaudo.paciente.nome} Nasc.: {viewerLaudo.paciente.nascimento} CPF: {viewerLaudo.paciente.cpf} {previewLaudo.paciente.nome} Nasc.: {previewLaudo.paciente.nascimento} CPF: {previewLaudo.paciente.cpf}
</div> </div>
<div style={{ whiteSpace:"pre-wrap", fontSize:15, color:"#1f2d3d", lineHeight:1.5 }}> <div style={{ whiteSpace:"pre-wrap", fontSize:15, color:"#1f2d3d", lineHeight:1.5 }}>
{viewerLaudo.conteudo} {previewLaudo.conteudo}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)} )}
{/* Confirm delete modal */} {/* Notificação simples: Sem permissão (exibe sem backdrop escuro) */}
{showConfirmDelete && toDelete && ( {showNoPermission && (
<div className="confirm-modal"> <div className="notice-card" role="alert" aria-live="polite">
<div className="modal-backdrop" onClick={() => { if (!loadingDelete) setShowConfirmDelete(false); }} /> <div style={{ fontWeight:700, marginBottom:6 }}>Sem permissão para editar</div>
<div className="modal-card" style={{ maxWidth:480 }}> <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={{ fontWeight:700, marginBottom:8 }}>Excluir laudo</div> <div style={{ textAlign:"right" }}>
<div style={{ marginBottom:12 }}>Você está prestes a excluir o laudo <strong>{toDelete.pedido} - {toDelete.paciente.nome}</strong>. Esta ação é irreversível.</div> <button className="tool-btn" onClick={() => setShowNoPermission(false)}>Fechar</button>
</div>
</div>
)}
<div style={{ marginBottom:8 }}> {/* Confirm delete modal (simples: Sim / Não) */}
<div style={{ fontSize:13, color:"#61788f", marginBottom:6 }}>Para confirmar, digite <strong>EXCLUIR</strong> abaixo e clique em Confirmar.</div> {showConfirmDelete && toDelete && (
<ConfirmDeleteInput loading={loadingDelete} onConfirm={confirmDelete} onCancel={() => setShowConfirmDelete(false)} /> <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>
</div> </div>
</div> </div>
</div> </div>
@ -310,18 +355,3 @@ function computeAge(birth) {
if (mm < m || (mm === m && dd < d)) age--; if (mm < m || (mm === m && dd < d)) age--;
return age; return age;
} }
function ConfirmDeleteInput({ onConfirm, onCancel, loading }) {
const [txt, setTxt] = useState("");
return (
<div>
<input value={txt} onChange={e => setTxt(e.target.value)} placeholder="Digite EXCLUIR" style={{ width:"100%", padding:"10px", borderRadius:6, border:"1px solid #e1ecfb", marginBottom:8 }} />
<div style={{ display:"flex", gap:8, justifyContent:"flex-end" }}>
<button className="tool-btn" onClick={onCancel} disabled={loading}>Cancelar</button>
<button className="tool-btn" onClick={() => onConfirm(txt)} disabled={loading} style={{ background: loading ? "#d7e8ff" : "#ffecec", border: "1px solid #ffd7d7" }}>
{loading ? "Excluindo..." : "Confirmar Exclusão"}
</button>
</div>
</div>
);
}