Laudo do Paciente
This commit is contained in:
parent
709cd4e13d
commit
f3e74702e1
34
package-lock.json
generated
34
package-lock.json
generated
@ -20,6 +20,7 @@
|
|||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"perfect-scrollbar": "^1.5.6",
|
"perfect-scrollbar": "^1.5.6",
|
||||||
|
"powershell": "^2.3.3",
|
||||||
"quill": "^2.0.3",
|
"quill": "^2.0.3",
|
||||||
"rater-js": "^1.0.1",
|
"rater-js": "^1.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@ -25314,6 +25315,11 @@
|
|||||||
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
|
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/is-undefined": {
|
||||||
|
"version": "1.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-undefined/-/is-undefined-1.0.12.tgz",
|
||||||
|
"integrity": "sha512-qaX2mymwUhMq+NQPnx5iR/u2PgqhL6jLzDunMmonOgVofqoFhxzd6kOmiL0DLYZUkN/RvNWYPenoANVn5phlaA=="
|
||||||
|
},
|
||||||
"node_modules/is-weakmap": {
|
"node_modules/is-weakmap": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
|
||||||
@ -25357,6 +25363,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-win": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-win/-/is-win-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-+XpgpizPqNzohXiqme7pfhAhpoG0Eo+CtuSx/XYW4enarERuheDbNbFrm4+XYylpV1w/eI+si5itFA0RfCWjog=="
|
||||||
|
},
|
||||||
"node_modules/is-wsl": {
|
"node_modules/is-wsl": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
@ -30018,6 +30029,16 @@
|
|||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/powershell": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/powershell/-/powershell-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-xLEFA2BWxlhrcp2wecH3rGVhG/z1kQDFvie1ynHZVjXdcYWaIaUrshCa8kep7Sj8c0EdNcNnyZU79oTbJRFDsQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-undefined": "^1.0.0",
|
||||||
|
"is-win": "^1.0.2",
|
||||||
|
"spawno": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@ -30069,6 +30090,11 @@
|
|||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/proc-output": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/proc-output/-/proc-output-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-XARWwM2pPNU/U8V4OuQNQLyjFqvHk1FRB5sFd1CCyT2vLLfDlLRLE4f6njcvm4Kyek1VzvF8MQRAYK1uLOlZmw=="
|
||||||
|
},
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
@ -32146,6 +32172,14 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/spawno": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/spawno/-/spawno-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-euy9JLkCC2SvXNYZAi9WBTHDxbjSWNCaeLhLIH+BGW1Xb/3yKxoWOT2kanSS1a5wB0iukDYu79FJMNJsGW7azA==",
|
||||||
|
"dependencies": {
|
||||||
|
"proc-output": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/spdy": {
|
"node_modules/spdy": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"perfect-scrollbar": "^1.5.6",
|
"perfect-scrollbar": "^1.5.6",
|
||||||
|
"powershell": "^2.3.3",
|
||||||
"quill": "^2.0.3",
|
"quill": "^2.0.3",
|
||||||
"rater-js": "^1.0.1",
|
"rater-js": "^1.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import Details from './pages/Details';
|
|||||||
//import DoctorEditPage from './components/doctors/DoctorEditPage';
|
//import DoctorEditPage from './components/doctors/DoctorEditPage';
|
||||||
import DoctorTable from './pages/DoctorTable';
|
import DoctorTable from './pages/DoctorTable';
|
||||||
import DoctorFormLayout from './pages/DoctorFormLayout';
|
import DoctorFormLayout from './pages/DoctorFormLayout';
|
||||||
|
import LaudoManager from "./pages/LaudoManager";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isSidebarActive, setIsSidebarActive] = useState(true);
|
const [isSidebarActive, setIsSidebarActive] = useState(true);
|
||||||
@ -34,6 +35,9 @@ const renderPageContent = () => {
|
|||||||
}
|
}
|
||||||
else if(currentPage === 'doctor-form-layout'){
|
else if(currentPage === 'doctor-form-layout'){
|
||||||
return <DoctorFormLayout/>
|
return <DoctorFormLayout/>
|
||||||
|
}
|
||||||
|
else if (currentPage === 'laudo-manager') {
|
||||||
|
return <LaudoManager />;
|
||||||
}
|
}
|
||||||
else if (currentPage === 'table') {
|
else if (currentPage === 'table') {
|
||||||
return <Table setCurrentPage={setCurrentPage} setPatientID={setPatientID}/>;
|
return <Table setCurrentPage={setCurrentPage} setPatientID={setPatientID}/>;
|
||||||
|
|||||||
@ -26,6 +26,12 @@
|
|||||||
"name": "Lista de Médico",
|
"name": "Lista de Médico",
|
||||||
"icon": "table",
|
"icon": "table",
|
||||||
"url": "doctor-table"
|
"url": "doctor-table"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Laudo do Paciente",
|
||||||
|
"icon": "table",
|
||||||
|
"url": "laudo-manager"
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
327
src/pages/LaudoManager.jsx
Normal file
327
src/pages/LaudoManager.jsx
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
|
||||||
|
/* ===== Estilos embutidos ===== */
|
||||||
|
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:#f7fbff; border-radius:8px; padding:18px; box-shadow: 0 1px 0 rgba(0,0,0,0.03);}
|
||||||
|
.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-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; }
|
||||||
|
.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; }
|
||||||
|
.modal-backdrop { position:absolute; inset:0; background: rgba(9,20,40,0.45); }
|
||||||
|
.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; }
|
||||||
|
.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; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
/* ===== 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: "rascunho"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: "rascunho"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
const [viewerLaudo, setViewerLaudo] = useState(null);
|
||||||
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
|
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
|
||||||
|
const [toDelete, setToDelete] = useState(null);
|
||||||
|
const [loadingDelete, setLoadingDelete] = useState(false);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// se clicar em um botão de ação (ícone), não fecha
|
||||||
|
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;
|
||||||
|
// caso contrário, fecha qualquer dropdown aberto
|
||||||
|
setOpenDropdownId(null);
|
||||||
|
}
|
||||||
|
document.addEventListener('click', onDocClick);
|
||||||
|
return () => document.removeEventListener('click', onDocClick);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function toggleDropdown(id, e) {
|
||||||
|
e.stopPropagation(); // evita que o document click feche imediatamente
|
||||||
|
setOpenDropdownId(prev => (prev === id ? null : id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenViewer(laudo) {
|
||||||
|
setViewerLaudo(laudo);
|
||||||
|
setOpenDropdownId(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequestDelete(laudo) {
|
||||||
|
setToDelete(laudo);
|
||||||
|
setOpenDropdownId(null);
|
||||||
|
setShowConfirmDelete(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDelete(typed) {
|
||||||
|
if (!toDelete) return;
|
||||||
|
if (typed.trim().toUpperCase() !== "EXCLUIR") {
|
||||||
|
alert("Confirmação inválida. Digite EXCLUIR para confirmar.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoadingDelete(true);
|
||||||
|
try {
|
||||||
|
const resp = await mockDeleteLaudo(toDelete.id);
|
||||||
|
if (resp.ok || resp === true) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePrint(laudo) {
|
||||||
|
setViewerLaudo(laudo);
|
||||||
|
setShowPreview(true);
|
||||||
|
setOpenDropdownId(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="laudo-wrap">
|
||||||
|
<div className="left-col">
|
||||||
|
<div className="title-row">
|
||||||
|
<div className="page-title">Gerenciamento de Laudo</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{laudos.length === 0 ? (
|
||||||
|
<div className="empty">Nenhum laudo encontrado.</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ borderRadius:8, overflow:"visible", boxShadow:"0 0 0 1px #eef6ff" }}>
|
||||||
|
{laudos.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="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="row-actions">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* dropdown associado ao laudo (não será cortado) */}
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Viewer modal (modo leitura) */}
|
||||||
|
{viewerLaudo && !showPreview && (
|
||||||
|
<div className="viewer-modal">
|
||||||
|
<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}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display:"flex", gap:8 }}>
|
||||||
|
<button className="tool-btn" onClick={() => { setShowPreview(true); }}>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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Preview modal */}
|
||||||
|
{showPreview && viewerLaudo && (
|
||||||
|
<div className="preview-modal">
|
||||||
|
<div className="modal-backdrop" onClick={() => setShowPreview(false)} />
|
||||||
|
<div className="modal-card" style={{ maxWidth:900 }}>
|
||||||
|
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:12 }}>
|
||||||
|
<div style={{ fontWeight:700 }}>Pré-visualização - {viewerLaudo.paciente.nome}</div>
|
||||||
|
<div style={{ display:"flex", gap:8 }}>
|
||||||
|
<button className="tool-btn" onClick={() => window.print()}>Imprimir / Download</button>
|
||||||
|
<button className="tool-btn" onClick={() => setShowPreview(false)}>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" }}>
|
||||||
|
{viewerLaudo.paciente.nome} • Nasc.: {viewerLaudo.paciente.nascimento} • CPF: {viewerLaudo.paciente.cpf}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ whiteSpace:"pre-wrap", fontSize:15, color:"#1f2d3d", lineHeight:1.5 }}>
|
||||||
|
{viewerLaudo.conteudo}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Confirm delete modal */}
|
||||||
|
{showConfirmDelete && toDelete && (
|
||||||
|
<div className="confirm-modal">
|
||||||
|
<div className="modal-backdrop" onClick={() => { if (!loadingDelete) setShowConfirmDelete(false); }} />
|
||||||
|
<div className="modal-card" style={{ maxWidth:480 }}>
|
||||||
|
<div style={{ fontWeight:700, marginBottom:8 }}>Excluir laudo</div>
|
||||||
|
<div style={{ marginBottom:12 }}>Você está prestes a excluir o laudo <strong>{toDelete.pedido} - {toDelete.paciente.nome}</strong>. Esta ação é irreversível.</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom:8 }}>
|
||||||
|
<div style={{ fontSize:13, color:"#61788f", marginBottom:6 }}>Para confirmar, digite <strong>EXCLUIR</strong> abaixo e clique em Confirmar.</div>
|
||||||
|
<ConfirmDeleteInput loading={loadingDelete} onConfirm={confirmDelete} onCancel={() => setShowConfirmDelete(false)} />
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user