diff --git a/package-lock.json b/package-lock.json
index 3a03d4c..668ad01 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"bootstrap-icons": "^1.13.1",
"flatpickr": "^4.6.13",
"perfect-scrollbar": "^1.5.6",
+ "powershell": "^2.3.3",
"quill": "^2.0.3",
"rater-js": "^1.0.1",
"react": "^18.2.0",
@@ -25314,6 +25315,11 @@
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@@ -25357,6 +25363,11 @@
"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": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -30018,6 +30029,16 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -30069,6 +30090,11 @@
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"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": {
"version": "2.0.1",
"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"
}
},
+ "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": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
diff --git a/package.json b/package.json
index 2ee1efb..e3e4a86 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"bootstrap-icons": "^1.13.1",
"flatpickr": "^4.6.13",
"perfect-scrollbar": "^1.5.6",
+ "powershell": "^2.3.3",
"quill": "^2.0.3",
"rater-js": "^1.0.1",
"react": "^18.2.0",
diff --git a/src/App.js b/src/App.js
index 165f091..0c3d186 100644
--- a/src/App.js
+++ b/src/App.js
@@ -14,6 +14,7 @@ import Details from './pages/Details';
//import DoctorEditPage from './components/doctors/DoctorEditPage';
import DoctorTable from './pages/DoctorTable';
import DoctorFormLayout from './pages/DoctorFormLayout';
+import LaudoManager from "./pages/LaudoManager";
function App() {
const [isSidebarActive, setIsSidebarActive] = useState(true);
@@ -35,6 +36,9 @@ const renderPageContent = () => {
else if(currentPage === 'doctor-form-layout'){
return
}
+ else if (currentPage === 'laudo-manager') {
+ return ;
+}
else if (currentPage === 'table') {
return
;
}
diff --git a/src/data/sidebar-items.json b/src/data/sidebar-items.json
index 9d39584..1f18dee 100644
--- a/src/data/sidebar-items.json
+++ b/src/data/sidebar-items.json
@@ -26,6 +26,12 @@
"name": "Lista de Médico",
"icon": "table",
"url": "doctor-table"
- }
+ },
+
+ {
+ "name": "Laudo do Paciente",
+ "icon": "table",
+ "url": "laudo-manager"
+ }
]
\ No newline at end of file
diff --git a/src/pages/LaudoManager.jsx b/src/pages/LaudoManager.jsx
new file mode 100644
index 0000000..fb73c59
--- /dev/null
+++ b/src/pages/LaudoManager.jsx
@@ -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 (
+
+
+
+
Gerenciamento de Laudo
+
+
+ {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}
+
+
+
toggleDropdown(l.id, e)} title="Ações">
+
+
+
+ {/* dropdown associado ao laudo (não será cortado) */}
+ {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) */}
+ {viewerLaudo && !showPreview && (
+
+
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 */}
+ {showPreview && viewerLaudo && (
+
+
setShowPreview(false)} />
+
+
+
Pré-visualização - {viewerLaudo.paciente.nome}
+
+
+
+
+
+
+
+
+ RELATÓRIO MÉDICO
+
+
+ {viewerLaudo.paciente.nome} • Nasc.: {viewerLaudo.paciente.nascimento} • CPF: {viewerLaudo.paciente.cpf}
+
+
+
+ {viewerLaudo.conteudo}
+
+
+
+
+ )}
+
+ {/* Confirm delete modal */}
+ {showConfirmDelete && toDelete && (
+
+
{ if (!loadingDelete) setShowConfirmDelete(false); }} />
+
+
Excluir laudo
+
Você está prestes a excluir o laudo {toDelete.pedido} - {toDelete.paciente.nome}. Esta ação é irreversível.
+
+
+
Para confirmar, digite EXCLUIR abaixo e clique em Confirmar.
+
setShowConfirmDelete(false)} />
+
+
+
+ )}
+
+ );
+}
+
+/* ===== 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 (
+
+
setTxt(e.target.value)} placeholder="Digite EXCLUIR" style={{ width:"100%", padding:"10px", borderRadius:6, border:"1px solid #e1ecfb", marginBottom:8 }} />
+
+
+
+
+
+ );
+}