Atualizando
This commit is contained in:
parent
cb41907160
commit
5d1716923e
@ -487,4 +487,42 @@
|
|||||||
width: calc(100vw - 20px);
|
width: calc(100vw - 20px);
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* permite que cliques "passem" através do header (exceto para os elementos interativos) */
|
||||||
|
.header-container {
|
||||||
|
pointer-events: none; /* header não captura cliques */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* mas permite que os controles no canto (telefone e profile) continuem clicáveis */
|
||||||
|
.phone-icon-container,
|
||||||
|
.profile-section {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garantir pointer-events nos elementos do header e overlays criados por portal */
|
||||||
|
.header-container { pointer-events: auto; }
|
||||||
|
.phone-icon-container, .profile-section { pointer-events: auto; }
|
||||||
|
|
||||||
|
/* Força que os overlays criados por portal fiquem por cima */
|
||||||
|
.logout-modal-overlay, .suporte-card-overlay, .chat-overlay {
|
||||||
|
z-index: 110000 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pequeno ajuste visual dos botões do modal (pode se misturar com seu CSS atual) */
|
||||||
|
.logout-cancel-button {
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.logout-confirm-button {
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
background: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -309,4 +309,39 @@ html[data-bs-theme="dark"] .notice-card {
|
|||||||
background: #232323 !important;
|
background: #232323 !important;
|
||||||
color: #e0e0e0 !important;
|
color: #e0e0e0 !important;
|
||||||
box-shadow: 0 8px 30px rgba(10,20,40,0.32) !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;
|
||||||
|
}
|
||||||
|
|||||||
@ -198,4 +198,15 @@
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
padding: 0.6rem 1.2rem;
|
padding: 0.6rem 1.2rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.doctor-form-container { padding: 0.75rem; }
|
||||||
|
.doctor-form-title { font-size: 1.75rem; }
|
||||||
|
.form-section { padding: 0.75rem; }
|
||||||
|
.section-header { font-size: 1.25rem; }
|
||||||
|
.form-label { font-size: 1rem; }
|
||||||
|
.form-control-custom { font-size: 1rem; }
|
||||||
|
.btns-container { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.btn-submit, .btn-cancel { width: 100%; margin-right: 0; }
|
||||||
}
|
}
|
||||||
@ -151,4 +151,18 @@
|
|||||||
|
|
||||||
.btn-add:hover {
|
.btn-add:hover {
|
||||||
background-color: #059669;
|
background-color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.horarios-container { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.horarios-container { grid-template-columns: 1fr; gap: 10px; }
|
||||||
|
.day-card { height: auto; }
|
||||||
|
.day-header label { font-size: 13px; }
|
||||||
|
.time-inputs { flex-direction: column; }
|
||||||
|
.input-wrapper input { font-size: 12px; }
|
||||||
|
.btn-add { font-size: 12px; }
|
||||||
|
.btn-remove { font-size: 12px; }
|
||||||
}
|
}
|
||||||
@ -1,10 +1,12 @@
|
|||||||
//Nesta página falta: ajustar caminho do CSS
|
//Nesta página falta: ajustar caminho do CSS
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { createPortal } from 'react-dom';
|
||||||
//import './Header.css';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
// import './Header.css';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
|
// --- hooks (sempre na mesma ordem) ---
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
const [isSuporteCardOpen, setIsSuporteCardOpen] = useState(false);
|
const [isSuporteCardOpen, setIsSuporteCardOpen] = useState(false);
|
||||||
const [isChatOpen, setIsChatOpen] = useState(false);
|
const [isChatOpen, setIsChatOpen] = useState(false);
|
||||||
@ -12,48 +14,70 @@ const Header = () => {
|
|||||||
const [mensagens, setMensagens] = useState([]);
|
const [mensagens, setMensagens] = useState([]);
|
||||||
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
||||||
const [avatarUrl, setAvatarUrl] = useState(null);
|
const [avatarUrl, setAvatarUrl] = useState(null);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const chatInputRef = useRef(null);
|
const chatInputRef = useRef(null);
|
||||||
const mensagensContainerRef = useRef(null);
|
const mensagensContainerRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
// foco quando abre chat
|
||||||
const loadAvatar = () => {
|
|
||||||
const localAvatar = localStorage.getItem('user_avatar');
|
|
||||||
if (localAvatar) {
|
|
||||||
setAvatarUrl(localAvatar);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadAvatar();
|
|
||||||
|
|
||||||
const handleStorageChange = () => {
|
|
||||||
loadAvatar();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('storage', handleStorageChange);
|
|
||||||
return () => window.removeEventListener('storage', handleStorageChange);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isChatOpen && chatInputRef.current) {
|
if (isChatOpen && chatInputRef.current) {
|
||||||
chatInputRef.current.focus();
|
chatInputRef.current.focus();
|
||||||
}
|
}
|
||||||
}, [isChatOpen]);
|
}, [isChatOpen]);
|
||||||
|
|
||||||
|
// scroll automático quando nova mensagem
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mensagensContainerRef.current) {
|
if (mensagensContainerRef.current) {
|
||||||
mensagensContainerRef.current.scrollTop = mensagensContainerRef.current.scrollHeight;
|
mensagensContainerRef.current.scrollTop = mensagensContainerRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
}, [mensagens]);
|
}, [mensagens]);
|
||||||
|
|
||||||
// --- Logout ---
|
// carrega avatar se existir
|
||||||
|
useEffect(() => {
|
||||||
|
const loadAvatar = () => {
|
||||||
|
const localAvatar = localStorage.getItem('user_avatar');
|
||||||
|
if (localAvatar) setAvatarUrl(localAvatar);
|
||||||
|
};
|
||||||
|
loadAvatar();
|
||||||
|
const onStorage = () => loadAvatar();
|
||||||
|
window.addEventListener('storage', onStorage);
|
||||||
|
return () => window.removeEventListener('storage', onStorage);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ESC fecha qualquer overlay/portal aberto (logout / suporte / chat)
|
||||||
|
useEffect(() => {
|
||||||
|
const onKey = (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
if (showLogoutModal) setShowLogoutModal(false);
|
||||||
|
if (isSuporteCardOpen) setIsSuporteCardOpen(false);
|
||||||
|
if (isChatOpen) setIsChatOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
}, [showLogoutModal, isSuporteCardOpen, isChatOpen]);
|
||||||
|
|
||||||
|
// --- handlers logout (mantive comportamento) ---
|
||||||
const handleLogoutClick = () => {
|
const handleLogoutClick = () => {
|
||||||
setShowLogoutModal(true);
|
setShowLogoutModal(true);
|
||||||
setIsDropdownOpen(false);
|
setIsDropdownOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogoutCancel = () => {
|
const clearAuthData = () => {
|
||||||
setShowLogoutModal(false);
|
["token","authToken","userToken","access_token","user","auth","userData"].forEach(key => {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
sessionStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
if (window.caches) {
|
||||||
|
caches.keys().then(names => {
|
||||||
|
names.forEach(name => {
|
||||||
|
if (name.includes("auth") || name.includes("api")) caches.delete(name);
|
||||||
|
});
|
||||||
|
}).catch(()=>{});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogoutConfirm = async () => {
|
const handleLogoutConfirm = async () => {
|
||||||
@ -67,55 +91,34 @@ const Header = () => {
|
|||||||
sessionStorage.getItem("authToken");
|
sessionStorage.getItem("authToken");
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
const response = await fetch(
|
try {
|
||||||
"https://mock.apidog.com/m1/1053378-0-default/auth/v1/logout",
|
await fetch("https://mock.apidog.com/m1/1053378-0-default/auth/v1/logout", {
|
||||||
{
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
);
|
} catch (err) {
|
||||||
|
// ignora erro de rede / endpoint — prossegue para limpar local
|
||||||
if (response.status === 204) console.log("Logout realizado com sucesso");
|
console.warn('logout endpoint error (ignored):', err);
|
||||||
else if (response.status === 401) console.log("Token inválido ou expirado");
|
|
||||||
else {
|
|
||||||
try {
|
|
||||||
const errorData = await response.json();
|
|
||||||
console.error("Erro no logout:", errorData);
|
|
||||||
} catch {
|
|
||||||
console.error("Erro no logout - status:", response.status);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAuthData();
|
clearAuthData();
|
||||||
navigate("/login");
|
navigate('/login');
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Erro durante logout:", error);
|
console.error('Erro no logout:', err);
|
||||||
clearAuthData();
|
clearAuthData();
|
||||||
navigate("/login");
|
navigate('/login');
|
||||||
} finally {
|
} finally {
|
||||||
setShowLogoutModal(false);
|
setShowLogoutModal(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearAuthData = () => {
|
const handleLogoutCancel = () => setShowLogoutModal(false);
|
||||||
["token", "authToken", "userToken", "access_token", "user", "auth", "userData"].forEach(key => {
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
sessionStorage.removeItem(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.caches) {
|
|
||||||
caches.keys().then(names => {
|
|
||||||
names.forEach(name => {
|
|
||||||
if (name.includes("auth") || name.includes("api")) caches.delete(name);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// --- profile / suporte / chat handlers ---
|
||||||
const handleProfileClick = () => {
|
const handleProfileClick = () => {
|
||||||
setIsDropdownOpen(!isDropdownOpen);
|
setIsDropdownOpen(!isDropdownOpen);
|
||||||
if (isSuporteCardOpen) setIsSuporteCardOpen(false);
|
if (isSuporteCardOpen) setIsSuporteCardOpen(false);
|
||||||
@ -128,14 +131,12 @@ const Header = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSuporteClick = () => {
|
const handleSuporteClick = () => {
|
||||||
setIsSuporteCardOpen(!isSuporteCardOpen);
|
setIsSuporteCardOpen((s) => !s);
|
||||||
if (isDropdownOpen) setIsDropdownOpen(false);
|
setIsDropdownOpen(false);
|
||||||
if (isChatOpen) setIsChatOpen(false);
|
if (isChatOpen) setIsChatOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseSuporteCard = () => {
|
const handleCloseSuporteCard = () => setIsSuporteCardOpen(false);
|
||||||
setIsSuporteCardOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChatClick = () => {
|
const handleChatClick = () => {
|
||||||
setIsChatOpen(true);
|
setIsChatOpen(true);
|
||||||
@ -143,7 +144,7 @@ const Header = () => {
|
|||||||
setMensagens([
|
setMensagens([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
texto: 'Olá! Me chamo Ágatha e sou sua assistente virtual. 👋 Bem-vindo ao suporte Mediconnect. Como posso te ajudar hoje?',
|
texto: 'Olá! Bem-vindo ao suporte Mediconnect. Como podemos ajudar você hoje?',
|
||||||
remetente: 'suporte',
|
remetente: 'suporte',
|
||||||
hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
|
hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
|
||||||
}
|
}
|
||||||
@ -155,11 +156,10 @@ const Header = () => {
|
|||||||
setMensagem('');
|
setMensagem('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEnviarMensagem = async (e) => {
|
const handleEnviarMensagem = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (mensagem.trim() === '') return;
|
if (mensagem.trim() === '') return;
|
||||||
|
|
||||||
// Mensagem do usuário
|
|
||||||
const novaMensagemUsuario = {
|
const novaMensagemUsuario = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
texto: mensagem,
|
texto: mensagem,
|
||||||
@ -170,37 +170,30 @@ const Header = () => {
|
|||||||
setMensagens(prev => [...prev, novaMensagemUsuario]);
|
setMensagens(prev => [...prev, novaMensagemUsuario]);
|
||||||
setMensagem('');
|
setMensagem('');
|
||||||
|
|
||||||
try {
|
setTimeout(() => {
|
||||||
const response = await fetch("http://localhost:5000/api/chat", {
|
if (chatInputRef.current) chatInputRef.current.focus();
|
||||||
method: "POST",
|
}, 0);
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ message: mensagem }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
setTimeout(() => {
|
||||||
|
const respostas = [
|
||||||
// Resposta da IA
|
'Entendi sua dúvida. Vou verificar isso para você.',
|
||||||
|
'Obrigado pela informação. Estou analisando seu caso.',
|
||||||
|
'Pode me dar mais detalhes sobre o problema?',
|
||||||
|
'Já encaminhei sua solicitação para nossa equipe técnica.',
|
||||||
|
'Vou ajudar você a resolver isso!'
|
||||||
|
];
|
||||||
const respostaSuporte = {
|
const respostaSuporte = {
|
||||||
id: Date.now() + 1,
|
id: Date.now() + 1,
|
||||||
texto: data.resposta || data.reply || "Desculpe, não consegui processar sua pergunta no momento 😅",
|
texto: respostas[Math.floor(Math.random() * respostas.length)],
|
||||||
remetente: 'suporte',
|
remetente: 'suporte',
|
||||||
hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
|
hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
|
||||||
};
|
};
|
||||||
|
|
||||||
setMensagens(prev => [...prev, respostaSuporte]);
|
setMensagens(prev => [...prev, respostaSuporte]);
|
||||||
} catch (error) {
|
}, 900);
|
||||||
console.error("Erro ao conectar com o servidor:", error);
|
|
||||||
const erroMsg = {
|
|
||||||
id: Date.now() + 1,
|
|
||||||
texto: "Ops! Ocorreu um erro ao tentar falar com o suporte.",
|
|
||||||
remetente: 'suporte',
|
|
||||||
hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
|
|
||||||
};
|
|
||||||
setMensagens(prev => [...prev, erroMsg]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SuporteCard = () => (
|
// --- subcomponentes (UI) ---
|
||||||
|
const SuporteCardContent = ({ onOpenChat }) => (
|
||||||
<div className="suporte-card">
|
<div className="suporte-card">
|
||||||
<h2 className="suporte-titulo">Suporte</h2>
|
<h2 className="suporte-titulo">Suporte</h2>
|
||||||
<p className="suporte-subtitulo">Entre em contato conosco através dos canais abaixo</p>
|
<p className="suporte-subtitulo">Entre em contato conosco através dos canais abaixo</p>
|
||||||
@ -219,7 +212,7 @@ const Header = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="contato-item clickable" onClick={handleChatClick}>
|
<div className="contato-item clickable" onClick={onOpenChat} role="button" tabIndex={0}>
|
||||||
<div className="contato-info">
|
<div className="contato-info">
|
||||||
<div className="contato-nome">Chat Online</div>
|
<div className="contato-nome">Chat Online</div>
|
||||||
<div className="contato-descricao">Disponível 24/7</div>
|
<div className="contato-descricao">Disponível 24/7</div>
|
||||||
@ -228,11 +221,11 @@ const Header = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ChatOnline = () => (
|
const ChatOnlineContent = ({ mensagens, onSend, onClose }) => (
|
||||||
<div className="chat-online">
|
<div className="chat-online" role="dialog" aria-modal="true">
|
||||||
<div className="chat-header">
|
<div className="chat-header">
|
||||||
<h3 className="chat-titulo">Chat de Suporte</h3>
|
<h3 className="chat-titulo">Chat de Suporte</h3>
|
||||||
<button type="button" className="fechar-chat" onClick={handleCloseChat}>×</button>
|
<button type="button" className="fechar-chat" onClick={onClose} aria-label="Fechar chat">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="chat-mensagens" ref={mensagensContainerRef}>
|
<div className="chat-mensagens" ref={mensagensContainerRef}>
|
||||||
@ -244,7 +237,7 @@ const Header = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form className="chat-input" onSubmit={handleEnviarMensagem}>
|
<form className="chat-input" onSubmit={onSend}>
|
||||||
<input
|
<input
|
||||||
ref={chatInputRef}
|
ref={chatInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
@ -259,63 +252,162 @@ const Header = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- portals: Logout / Suporte / Chat (garante top-most e clickable) ---
|
||||||
|
const PortalWrapper = ({ children, z = 99999 }) => {
|
||||||
|
if (typeof document === 'undefined') return null;
|
||||||
|
return createPortal(
|
||||||
|
<div style={{ position: 'fixed', inset: 0, zIndex: z }}>
|
||||||
|
{children}
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LogoutModalPortal = ({ onCancel, onConfirm }) => {
|
||||||
|
if (typeof document === 'undefined') return null;
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className="logout-modal-overlay"
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0, left: 0, right: 0, bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
zIndex: 110000
|
||||||
|
}}
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="logout-modal-content"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
padding: '1.6rem',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 8px 24px rgba(0,0,0,0.2)',
|
||||||
|
maxWidth: '480px',
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h3 style={{ marginTop: 0 }}>Confirmar Logout</h3>
|
||||||
|
<p>Tem certeza que deseja encerrar a sessão?</p>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'center', marginTop: '1rem' }}>
|
||||||
|
<button onClick={onCancel} className="logout-cancel-button">Cancelar</button>
|
||||||
|
<button onClick={onConfirm} className="logout-confirm-button">Sair</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SuportePortal = ({ onClose, children }) => {
|
||||||
|
if (typeof document === 'undefined') return null;
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className="suporte-card-overlay"
|
||||||
|
style={{
|
||||||
|
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
zIndex: 105000,
|
||||||
|
pointerEvents: 'auto'
|
||||||
|
}}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="suporte-card-container"
|
||||||
|
style={{ marginTop: '80px', marginRight: '20px' }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChatPortal = ({ onClose, children }) => {
|
||||||
|
if (typeof document === 'undefined') return null;
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className="chat-overlay"
|
||||||
|
style={{
|
||||||
|
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
zIndex: 115000,
|
||||||
|
pointerEvents: 'auto'
|
||||||
|
}}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="chat-container"
|
||||||
|
style={{ marginTop: '80px', marginRight: '20px' }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- evita render na rota de login (mantendo hooks invocados) ---
|
||||||
|
if (location.pathname === '/login') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- JSX principal (header visual) ---
|
||||||
return (
|
return (
|
||||||
<div className="header-container">
|
<div className="header-container" style={{ pointerEvents: 'auto' }}>
|
||||||
<div className="right-corner-elements">
|
<div className="right-corner-elements">
|
||||||
<div className="phone-icon-container" onClick={handleSuporteClick}>
|
<div
|
||||||
|
className="phone-icon-container"
|
||||||
|
onClick={handleSuporteClick}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
style={{ pointerEvents: 'auto' }}
|
||||||
|
>
|
||||||
<span className="phone-icon" role="img" aria-label="telefone">📞</span>
|
<span className="phone-icon" role="img" aria-label="telefone">📞</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="profile-section">
|
<div className="profile-section" style={{ pointerEvents: 'auto' }}>
|
||||||
<div className="profile-picture-container" onClick={handleProfileClick}>
|
<div className="profile-picture-container" onClick={handleProfileClick} role="button" tabIndex={0}>
|
||||||
<div className="profile-placeholder"></div>
|
<div className="profile-placeholder"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isDropdownOpen && (
|
{isDropdownOpen && (
|
||||||
<div className="profile-dropdown">
|
<div className="profile-dropdown" onClick={(e) => e.stopPropagation()}>
|
||||||
<button type="button" onClick={handleViewProfile} className="dropdown-button">
|
<button type="button" onClick={handleViewProfile} className="dropdown-button">Ver Perfil</button>
|
||||||
Ver Perfil
|
<button type="button" onClick={handleLogoutClick} className="dropdown-button logout-button">Sair (Logout)</button>
|
||||||
</button>
|
|
||||||
<button type="button" onClick={handleLogoutClick} className="dropdown-button logout-button">
|
|
||||||
Sair (Logout)
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal de Logout */}
|
{/* logout modal via portal */}
|
||||||
{showLogoutModal && (
|
{showLogoutModal && <LogoutModalPortal onCancel={handleLogoutCancel} onConfirm={handleLogoutConfirm} />}
|
||||||
<div className="logout-modal-overlay">
|
|
||||||
<div className="logout-modal-content">
|
|
||||||
<h3>Confirmar Logout</h3>
|
|
||||||
<p>Tem certeza que deseja encerrar a sessão?</p>
|
|
||||||
<div className="logout-modal-buttons">
|
|
||||||
<button onClick={handleLogoutCancel} className="logout-cancel-button">
|
|
||||||
Cancelar
|
|
||||||
</button>
|
|
||||||
<button onClick={handleLogoutConfirm} className="logout-confirm-button">
|
|
||||||
Sair
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
{/* suporte portal */}
|
||||||
{isSuporteCardOpen && (
|
{isSuporteCardOpen && (
|
||||||
<div className="suporte-card-overlay" onClick={handleCloseSuporteCard}>
|
<SuportePortal onClose={handleCloseSuporteCard}>
|
||||||
<div className="suporte-card-container" onClick={(e) => e.stopPropagation()}>
|
<SuporteCardContent onOpenChat={handleChatClick} />
|
||||||
<SuporteCard />
|
</SuportePortal>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* chat portal */}
|
||||||
{isChatOpen && (
|
{isChatOpen && (
|
||||||
<div className="chat-overlay">
|
<ChatPortal onClose={handleCloseChat}>
|
||||||
<div className="chat-container">
|
<ChatOnlineContent mensagens={mensagens} onSend={handleEnviarMensagem} onClose={handleCloseChat} />
|
||||||
<ChatOnline />
|
</ChatPortal>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../../_assets/utils/AuthProvider";
|
import { useAuth } from "./utils/AuthProvider";
|
||||||
import { UserInfos } from "../../_assets/utils/Functions-Endpoints/General";
|
|
||||||
import MobileMenuToggle from "./MobileMenuToggle";
|
import MobileMenuToggle from "./MobileMenuToggle";
|
||||||
import ToggleSidebar from "./ToggleSidebar";
|
import ToggleSidebar from "./ToggleSidebar";
|
||||||
|
|
||||||
import PacienteItems from "../../data/sidebar-items-paciente.json"
|
import PacienteItems from "../data/sidebar-items-paciente.json"
|
||||||
import DoctorItems from "../../data/sidebar-items-medico.json"
|
import DoctorItems from "../data/sidebar-items-medico.json"
|
||||||
import admItems from "../../data/sidebar-items-adm.json"
|
import admItems from "../data/sidebar-items-adm.json"
|
||||||
import SecretariaItems from "../../data/sidebar-items-secretaria.json"
|
import SecretariaItems from "../data/sidebar-items-secretaria.json"
|
||||||
import FinanceiroItems from "../../data/sidebar-items-financeiro.json"
|
import FinanceiroItems from "../data/sidebar-items-financeiro.json"
|
||||||
|
|
||||||
|
|
||||||
function Sidebar({ menuItems }) {
|
function Sidebar({ menuItems }) {
|
||||||
@ -18,15 +17,23 @@ function Sidebar({ menuItems }) {
|
|||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [roleUser, setRoleUser] = useState([])
|
const [roleUser, setRoleUser] = useState([])
|
||||||
|
|
||||||
const {getAuthorizationHeader} = useAuth();
|
const {getAuthorizationHeader} = useAuth();
|
||||||
|
|
||||||
const authHeader = getAuthorizationHeader();
|
const authHeader = getAuthorizationHeader();
|
||||||
|
|
||||||
|
let pathname = window.location.pathname.split("/")[1]
|
||||||
|
|
||||||
|
|
||||||
|
// useEffect para definir quais toggle da sidebar devem aparecer
|
||||||
|
useEffect(() => {
|
||||||
|
let teste = localStorage.getItem("roleUser")
|
||||||
|
setRoleUser(teste)
|
||||||
|
|
||||||
|
}, [authHeader])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Detecta se é mobile/tablet
|
// Detecta se é mobile/tablet
|
||||||
@ -37,14 +44,6 @@ function Sidebar({ menuItems }) {
|
|||||||
setIsActive(!mobile);
|
setIsActive(!mobile);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInfoUser = async () => {
|
|
||||||
const InfoUser = await UserInfos(authHeader);
|
|
||||||
console.log(InfoUser.roles, "dados")
|
|
||||||
|
|
||||||
setRoleUser(InfoUser.roles)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchInfoUser()
|
|
||||||
|
|
||||||
checkScreenSize();
|
checkScreenSize();
|
||||||
window.addEventListener("resize", checkScreenSize);
|
window.addEventListener("resize", checkScreenSize);
|
||||||
@ -117,13 +116,6 @@ function Sidebar({ menuItems }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if(roleUser.includes("admin")){
|
|
||||||
console.log("tem")
|
|
||||||
}
|
|
||||||
console.log(roleUser)
|
|
||||||
}, [roleUser])
|
|
||||||
|
|
||||||
const handleLogoutCancel = () => setShowLogoutModal(false);
|
const handleLogoutCancel = () => setShowLogoutModal(false);
|
||||||
|
|
||||||
|
|
||||||
@ -251,26 +243,26 @@ function Sidebar({ menuItems }) {
|
|||||||
<ul className="menu">
|
<ul className="menu">
|
||||||
|
|
||||||
{roleUser.includes("admin") &&
|
{roleUser.includes("admin") &&
|
||||||
<ToggleSidebar perfil={"administrador"} items={admItems}/>
|
<ToggleSidebar perfil={"administrador"} items={admItems} defaultOpen={pathname.includes("admin") } />
|
||||||
}
|
}
|
||||||
{roleUser.includes("admin") || roleUser.includes("secretaria") ?
|
{roleUser.includes("admin") || roleUser.includes("secretaria") ?
|
||||||
<ToggleSidebar perfil={"secretaria"} items={SecretariaItems}/>
|
<ToggleSidebar perfil={"secretaria"} items={SecretariaItems} defaultOpen={pathname.includes("secretaria")} />
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{roleUser.includes("admin") || roleUser.includes("medico") ?
|
{roleUser.includes("admin") || roleUser.includes("medico") ?
|
||||||
<ToggleSidebar perfil={"medico"} items={DoctorItems}/>
|
<ToggleSidebar perfil={"medico"} items={DoctorItems} defaultOpen={pathname.includes("medico") } />
|
||||||
:null
|
:null
|
||||||
}
|
}
|
||||||
|
|
||||||
{roleUser.includes("admin") || roleUser.includes("financeiro") ?
|
{roleUser.includes("admin") || roleUser.includes("financeiro") ?
|
||||||
<ToggleSidebar perfil={"financeiro"} items={FinanceiroItems}/>
|
<ToggleSidebar perfil={"financeiro"} items={FinanceiroItems} defaultOpen={pathname.includes("financeiro") } />
|
||||||
:null
|
:null
|
||||||
}
|
}
|
||||||
|
|
||||||
{roleUser.includes("admin") || roleUser.includes("paciente") ?
|
{roleUser.includes("admin") || roleUser.includes("paciente") ?
|
||||||
<ToggleSidebar perfil={"paciente"} items={PacienteItems}/>
|
<ToggleSidebar perfil={"paciente"} items={PacienteItems} defaultOpen={pathname.includes("paciente") } />
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,4 +288,4 @@ function Sidebar({ menuItems }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Sidebar;
|
export default Sidebar;
|
||||||
165
src/components/agendamento/Calendario.jsx
Normal file
165
src/components/agendamento/Calendario.jsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ChevronLeft, ChevronRight, Edit, Trash2 } from 'lucide-react';
|
||||||
|
import Spinner from '../Spinner.jsx'; // Certifique-se de que o caminho está correto
|
||||||
|
|
||||||
|
const CalendarComponent = ({
|
||||||
|
currentDate,
|
||||||
|
setCurrentDate,
|
||||||
|
selectedDay,
|
||||||
|
setSelectedDay,
|
||||||
|
DictAgendamentosOrganizados,
|
||||||
|
showSpinner,
|
||||||
|
setSelectedId,
|
||||||
|
setShowDeleteModal,
|
||||||
|
setShowConfirmModal,
|
||||||
|
quickJump,
|
||||||
|
handleQuickJumpChange,
|
||||||
|
applyQuickJump
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
// Gera os 42 dias para o grid do calendário
|
||||||
|
const generateDateGrid = () => {
|
||||||
|
const grid = [];
|
||||||
|
const startOfMonth = currentDate.startOf('month');
|
||||||
|
// Começa no domingo da semana em que o mês começa (day() retorna 0 para domingo)
|
||||||
|
let currentDay = startOfMonth.subtract(startOfMonth.day(), 'day');
|
||||||
|
|
||||||
|
// Gera 6 semanas (6*7 = 42 dias)
|
||||||
|
for (let i = 0; i < 42; i++) {
|
||||||
|
grid.push(currentDay);
|
||||||
|
currentDay = currentDay.add(1, 'day');
|
||||||
|
}
|
||||||
|
return grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateGrid = useMemo(() => generateDateGrid(), [currentDate]);
|
||||||
|
const weekDays = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
|
||||||
|
|
||||||
|
const handleDateClick = (day) => {
|
||||||
|
setSelectedDay(day);
|
||||||
|
// Opcional: Adicionar lógica para abrir o agendamento do dia/semana/mês (sua primeira imagem)
|
||||||
|
// Se você quiser que o clique abra a tela de agendamento de slots:
|
||||||
|
// Exemplo: onSelectDate(day);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Função para obter o status de agendamentos para o indicador (ponto azul)
|
||||||
|
const getAppointmentCount = (day) => {
|
||||||
|
return DictAgendamentosOrganizados[day.format('YYYY-MM-DD')]?.length || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="calendar-wrapper">
|
||||||
|
{/* Painel lateral de informações */}
|
||||||
|
<div className="calendar-info-panel">
|
||||||
|
<div className="info-date-display">
|
||||||
|
<span>{selectedDay.format('MMM')}</span>
|
||||||
|
<strong>{selectedDay.format('DD')}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="info-details">
|
||||||
|
<h3>{selectedDay.format('dddd')}</h3>
|
||||||
|
<p>{selectedDay.format('D [de] MMMM [de] YYYY')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="appointments-list">
|
||||||
|
<h4>Consultas para {selectedDay.format('DD/MM')}</h4>
|
||||||
|
{showSpinner ? <Spinner/> : (DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')]?.length > 0) ? (
|
||||||
|
DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')].map(app => (
|
||||||
|
<div key={app.id} className="appointment-item" data-status={app.status}>
|
||||||
|
<div className="item-time">{dayjs(app.scheduled_at).format('HH:mm')}</div>
|
||||||
|
<div className="item-details">
|
||||||
|
<span>{app.paciente_nome}</span>
|
||||||
|
<small>Dr(a). {app.medico_nome}</small>
|
||||||
|
</div>
|
||||||
|
<div className="appointment-actions">
|
||||||
|
{app.status === 'cancelled' ? (
|
||||||
|
<button className="btn-action btn-edit" onClick={() => { setSelectedId(app.id); setShowConfirmModal(true); }}>
|
||||||
|
<Edit size={16} />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button className="btn-action btn-delete" onClick={() => { setSelectedId(app.id); setShowDeleteModal(true); }}>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="no-appointments-info"><p>Nenhuma consulta agendada.</p></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendário Principal */}
|
||||||
|
<div className="calendar-main">
|
||||||
|
{/* Legenda */}
|
||||||
|
<div className="calendar-legend">
|
||||||
|
<div className="legend-item" data-status="completed">Realizado</div>
|
||||||
|
<div className="legend-item" data-status="confirmed">Confirmado</div>
|
||||||
|
<div className="legend-item" data-status="agendado">Agendado</div>
|
||||||
|
<div className="legend-item" data-status="cancelled">Cancelado</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controles de navegação e Jump */}
|
||||||
|
<div className="calendar-controls">
|
||||||
|
<div className="date-indicator">
|
||||||
|
<h2>{currentDate.format('MMMM [de] YYYY')}</h2>
|
||||||
|
<div className="quick-jump-controls" style={{ display: 'flex', gap: '5px', marginTop: '10px' }}>
|
||||||
|
<select
|
||||||
|
value={quickJump.month}
|
||||||
|
onChange={(e) => handleQuickJumpChange('month', e.target.value)}
|
||||||
|
className="form-select form-select-sm w-auto"
|
||||||
|
>
|
||||||
|
{dayjs.months().map((month, index) => (
|
||||||
|
<option key={index} value={index}>{month.charAt(0).toUpperCase() + month.slice(1)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={quickJump.year}
|
||||||
|
onChange={(e) => handleQuickJumpChange('year', e.target.value)}
|
||||||
|
className="form-select form-select-sm w-auto"
|
||||||
|
>
|
||||||
|
{Array.from({ length: 11 }, (_, i) => dayjs().year() - 5 + i).map(year => (
|
||||||
|
<option key={year} value={year}>{year}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-outline-primary"
|
||||||
|
onClick={applyQuickJump}
|
||||||
|
disabled={quickJump.month === currentDate.month() && quickJump.year === currentDate.year()}
|
||||||
|
>
|
||||||
|
Ir
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="nav-buttons">
|
||||||
|
<button onClick={() => { setCurrentDate(currentDate.subtract(1, 'month')); setSelectedDay(currentDate.subtract(1, 'month')); }}><ChevronLeft size={20} /></button>
|
||||||
|
<button onClick={() => { setCurrentDate(dayjs()); setSelectedDay(dayjs()); }}>Hoje</button>
|
||||||
|
<button onClick={() => { setCurrentDate(currentDate.add(1, 'month')); setSelectedDay(currentDate.add(1, 'month')); }}><ChevronRight size={20} /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid dos dias */}
|
||||||
|
<div className="calendar-grid">
|
||||||
|
{weekDays.map(day => <div key={day} className="day-header">{day}</div>)}
|
||||||
|
{dateGrid.map((day, index) => {
|
||||||
|
const count = getAppointmentCount(day);
|
||||||
|
const cellClasses = `day-cell ${day.isSame(currentDate, 'month') ? 'current-month' : 'other-month'} ${day.isSame(dayjs(), 'day') ? 'today' : ''} ${day.isSame(selectedDay, 'day') ? 'selected' : ''}`;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={cellClasses}
|
||||||
|
onClick={() => handleDateClick(day)}
|
||||||
|
>
|
||||||
|
<span>{day.format('D')}</span>
|
||||||
|
{count > 0 && <div className="appointments-indicator">{count}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CalendarComponent;
|
||||||
@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
import { useState, useRef, useCallback } from "react";
|
import { useState, useRef, useCallback } from "react";
|
||||||
import { Link, useNavigate, useLocation } from "react-router-dom";
|
import { Link, useNavigate, useLocation } from "react-router-dom";
|
||||||
import { useAuth } from '../../_assets/utils/AuthProvider';
|
import { useAuth } from '../utils/AuthProvider';
|
||||||
import API_KEY from '../../_assets/utils/apiKeys';
|
import API_KEY from '../utils/apiKeys';
|
||||||
import HorariosDisponibilidade from "../medico/HorariosDisponibilidade";
|
|
||||||
|
|
||||||
//import "./DoctorForm.css";
|
import HorariosDisponibilidade from "../doctors/HorariosDisponibilidade";
|
||||||
|
import "./DoctorForm.css";
|
||||||
|
|
||||||
const ENDPOINT_AVAILABILITY = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
|
const ENDPOINT_AVAILABILITY = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
|
||||||
|
|
||||||
@ -139,14 +139,11 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAvailabilityUpdate = useCallback((newAvailability) => {
|
const handleAvailabilityUpdate = useCallback((newAvailability) => {
|
||||||
setFormData((prev) => {
|
setFormData((prev) => {
|
||||||
if (JSON.stringify(prev.availability) !== JSON.stringify(newAvailability)) {
|
|
||||||
return { ...prev, availability: newAvailability };
|
return { ...prev, availability: newAvailability };
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, [setFormData]);
|
||||||
|
|
||||||
const handleCepBlur = async () => {
|
const handleCepBlur = async () => {
|
||||||
const cep = formData.cep?.replace(/\D/g, "");
|
const cep = formData.cep?.replace(/\D/g, "");
|
||||||
@ -330,17 +327,9 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedDoctor = await onSave({ ...formData });
|
// Chama a função onSave (handleSave no DoctorEditPage) com o formData completo.
|
||||||
|
// A lógica de salvamento do médico e da disponibilidade é responsabilidade do componente pai.
|
||||||
if (formData.availability && formData.availability.length > 0 && savedDoctor.id) {
|
await onSave({ ...formData });
|
||||||
if (formData.availabilityId) {
|
|
||||||
|
|
||||||
await handlePatchAvailability(formData.availabilityId, formData.availability);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
await handleCreateAvailability(savedDoctor.id, formData.availability);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao salvar médico ou disponibilidade:", error);
|
console.error("Erro ao salvar médico ou disponibilidade:", error);
|
||||||
@ -764,7 +753,7 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
|
|||||||
Defina seus horários de atendimento para cada dia da semana.
|
Defina seus horários de atendimento para cada dia da semana.
|
||||||
Marque um dia para começar a adicionar blocos de tempo.
|
Marque um dia para começar a adicionar blocos de tempo.
|
||||||
</p>
|
</p>
|
||||||
<HorariosDisponibilidade
|
<HorariosDisponibilidade
|
||||||
initialAvailability={formData.availability}
|
initialAvailability={formData.availability}
|
||||||
onUpdate={handleAvailabilityUpdate}
|
onUpdate={handleAvailabilityUpdate}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -12,13 +12,13 @@ const initialBlockTemplate = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const emptyAvailabilityTemplate = [
|
const emptyAvailabilityTemplate = [
|
||||||
{ dia: "Segunda-feira", isChecked: false, blocos: [] },
|
{ dia: "Domingo", weekday: 0, isChecked: false, blocos: [] },
|
||||||
{ dia: "Terça-feira", isChecked: false, blocos: [] },
|
{ dia: "Segunda-feira", weekday: 1, isChecked: false, blocos: [] },
|
||||||
{ dia: "Quarta-feira", isChecked: false, blocos: [] },
|
{ dia: "Terça-feira", weekday: 2, isChecked: false, blocos: [] },
|
||||||
{ dia: "Quinta-feira", isChecked: false, blocos: [] },
|
{ dia: "Quarta-feira", weekday: 3, isChecked: false, blocos: [] },
|
||||||
{ dia: "Sexta-feira", isChecked: false, blocos: [] },
|
{ dia: "Quinta-feira", weekday: 4, isChecked: false, blocos: [] },
|
||||||
{ dia: "Sábado", isChecked: false, blocos: [] },
|
{ dia: "Sexta-feira", weekday: 5, isChecked: false, blocos: [] },
|
||||||
{ dia: "Domingo", isChecked: false, blocos: [] },
|
{ dia: "Sábado", weekday: 6, isChecked: false, blocos: [] },
|
||||||
];
|
];
|
||||||
|
|
||||||
const HorariosDisponibilidade = ({
|
const HorariosDisponibilidade = ({
|
||||||
@ -37,10 +37,18 @@ const HorariosDisponibilidade = ({
|
|||||||
}
|
}
|
||||||
}, [initialAvailability]);
|
}, [initialAvailability]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFirstRun.current) {
|
||||||
|
isFirstRun.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onUpdate) onUpdate(availability);
|
||||||
|
}, [availability, onUpdate]);
|
||||||
|
|
||||||
const handleDayCheck = useCallback((dayIndex, currentIsChecked) => {
|
const handleDayCheck = useCallback((dayIndex, currentIsChecked) => {
|
||||||
const isChecked = !currentIsChecked;
|
const isChecked = !currentIsChecked;
|
||||||
setAvailability((prev) =>
|
setAvailability((prev) => {
|
||||||
prev.map((day, i) =>
|
const updated = prev.map((day, i) =>
|
||||||
i === dayIndex
|
i === dayIndex
|
||||||
? {
|
? {
|
||||||
...day,
|
...day,
|
||||||
@ -58,8 +66,10 @@ const HorariosDisponibilidade = ({
|
|||||||
: [],
|
: [],
|
||||||
}
|
}
|
||||||
: day
|
: day
|
||||||
)
|
);
|
||||||
);
|
console.log('handleDayCheck - updated availability:', updated);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAddBlock = useCallback((dayIndex) => {
|
const handleAddBlock = useCallback((dayIndex) => {
|
||||||
@ -109,9 +119,7 @@ const HorariosDisponibilidade = ({
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
|
||||||
if (onUpdate) onUpdate(availability);
|
|
||||||
}, [availability, onUpdate]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="horarios-container">
|
<div className="horarios-container">
|
||||||
@ -199,4 +207,4 @@ const HorariosDisponibilidade = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HorariosDisponibilidade;
|
export default HorariosDisponibilidade;
|
||||||
@ -4,7 +4,6 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { FormatTelefones, FormatPeso, FormatCPF } from '../../_assets/utils/Formatar/Format';
|
import { FormatTelefones, FormatPeso, FormatCPF } from '../../_assets/utils/Formatar/Format';
|
||||||
|
|
||||||
//import './PatientForm.css';
|
//import './PatientForm.css';
|
||||||
|
|
||||||
function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
|
function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
|
||||||
@ -56,6 +55,8 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const peso = parseFloat(formData.weight_kg);
|
const peso = parseFloat(formData.weight_kg);
|
||||||
const altura = parseFloat(formData.height_m);
|
const altura = parseFloat(formData.height_m);
|
||||||
@ -67,9 +68,20 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
|
|||||||
}
|
}
|
||||||
}, [formData.weight_kg, formData.height_m, setFormData]);
|
}, [formData.weight_kg, formData.height_m, setFormData]);
|
||||||
|
|
||||||
|
const handleCep = (value) => {
|
||||||
|
if(value.length === 8){
|
||||||
|
fetch(`https://viacep.com.br/ws/${value}/json/`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {setFormData(prev => ({...prev, street:result.logradouro,neighborhood:result.bairro,city:result.localidade,
|
||||||
|
state:result.estado
|
||||||
|
|
||||||
|
}))})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const { name, value, type, checked, files } = e.target;
|
const { name, value, type, checked, files } = e.target;
|
||||||
|
console.log(name, value)
|
||||||
if (value && emptyFields.includes(name)) {
|
if (value && emptyFields.includes(name)) {
|
||||||
setEmptyFields(prev => prev.filter(field => field !== name));
|
setEmptyFields(prev => prev.filter(field => field !== name));
|
||||||
}
|
}
|
||||||
@ -105,8 +117,11 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
|
|||||||
} else if (name.includes('weight_kg') || name.includes('height_m')) {
|
} else if (name.includes('weight_kg') || name.includes('height_m')) {
|
||||||
setFormData(prev => ({ ...prev, [name]: FormatPeso(value) }));
|
setFormData(prev => ({ ...prev, [name]: FormatPeso(value) }));
|
||||||
} else if (name === 'rn_in_insurance' || name === 'vip' || name === 'validadeIndeterminada') {
|
} else if (name === 'rn_in_insurance' || name === 'vip' || name === 'validadeIndeterminada') {
|
||||||
setFormData(prev => ({ ...prev, [name]: checked }));
|
setFormData(prev => ({ ...prev, [name]: checked }));
|
||||||
} else {
|
} else if(name === 'cep'){
|
||||||
|
handleCep(value)
|
||||||
|
setFormData(prev => ({...prev, [name]: value}))
|
||||||
|
}else {
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -193,9 +208,6 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
if (!emailRegex.test(formData.email)) {
|
|
||||||
throw new Error('Email inválido. Por favor, verifique o email digitado.');
|
|
||||||
}
|
|
||||||
|
|
||||||
await onSave({ ...formData, bmi: parseFloat(formData.bmi) || null });
|
await onSave({ ...formData, bmi: parseFloat(formData.bmi) || null });
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
"name": "Início",
|
||||||
|
"icon": "house-fill",
|
||||||
|
"url": "/paciente"
|
||||||
|
},
|
||||||
|
{
|
||||||
"name": "Minhas consulta",
|
"name": "Minhas consulta",
|
||||||
"icon": "calendar-plus-fill",
|
"icon": "calendar-plus-fill",
|
||||||
"url": "/paciente/agendamento"
|
"url": "/paciente/agendamento"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Meus laudos",
|
"name": "Meus laudos",
|
||||||
"icon": "table",
|
"icon": "table",
|
||||||
"url": "/paciente/laudo"
|
"url": "/paciente/laudo"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
@ -6,21 +6,21 @@ import { Link, useNavigate } from 'react-router-dom';
|
|||||||
import { useAuth } from '../../_assets/utils/AuthProvider';
|
import { useAuth } from '../../_assets/utils/AuthProvider';
|
||||||
import { GetPatientByID } from '../../_assets/utils/Functions-Endpoints/Patient';
|
import { GetPatientByID } from '../../_assets/utils/Functions-Endpoints/Patient';
|
||||||
import { GetDoctorByID } from '../../_assets/utils/Functions-Endpoints/Doctor';
|
import { GetDoctorByID } from '../../_assets/utils/Functions-Endpoints/Doctor';
|
||||||
|
import { UserInfos } from '../../_assets/utils/Functions-Endpoints/General';
|
||||||
import API_KEY from '../../_assets/utils/apiKeys';
|
import API_KEY from '../../_assets/utils/apiKeys';
|
||||||
|
|
||||||
import html2pdf from 'html2pdf.js';
|
import html2pdf from 'html2pdf.js';
|
||||||
import TiptapViewer from '../../components/medico/TiptapViewer';
|
import TiptapViewer from './TiptapViewer';
|
||||||
//import './styleMedico/DoctorRelatorioManager.css';
|
//import './styleMedico/DoctorRelatorioManager.css';
|
||||||
|
|
||||||
const DoctorRelatorioManager = () => {
|
const DoctorRelatorioManager = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { getAuthorizationHeader } = useAuth();
|
const { getAuthorizationHeader } = useAuth();
|
||||||
let authHeader = getAuthorizationHeader();
|
const authHeader = getAuthorizationHeader();
|
||||||
|
|
||||||
const [relatoriosOriginais, setRelatoriosOriginais] = useState([]);
|
const [relatoriosOriginais, setRelatoriosOriginais] = useState([]);
|
||||||
const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]);
|
const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]);
|
||||||
const [relatoriosFinais, setRelatoriosFinais] = useState([]);
|
const [relatoriosFinais, setRelatoriosFinais] = useState([]);
|
||||||
const [pacientesData, setPacientesData] = useState({});
|
|
||||||
const [pacientesComRelatorios, setPacientesComRelatorios] = useState([]);
|
const [pacientesComRelatorios, setPacientesComRelatorios] = useState([]);
|
||||||
const [medicosComRelatorios, setMedicosComRelatorios] = useState([]);
|
const [medicosComRelatorios, setMedicosComRelatorios] = useState([]);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
@ -33,8 +33,7 @@ const DoctorRelatorioManager = () => {
|
|||||||
const [paginaAtual, setPaginaAtual] = useState(1);
|
const [paginaAtual, setPaginaAtual] = useState(1);
|
||||||
const [itensPorPagina, setItensPorPagina] = useState(10);
|
const [itensPorPagina, setItensPorPagina] = useState(10);
|
||||||
|
|
||||||
|
const totalPaginas = Math.max(1, Math.ceil(relatoriosFinais.length / itensPorPagina));
|
||||||
const totalPaginas = Math.ceil(relatoriosFinais.length / itensPorPagina);
|
|
||||||
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
|
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
|
||||||
const indiceFinal = indiceInicial + itensPorPagina;
|
const indiceFinal = indiceInicial + itensPorPagina;
|
||||||
const relatoriosPaginados = relatoriosFinais.slice(indiceInicial, indiceFinal);
|
const relatoriosPaginados = relatoriosFinais.slice(indiceInicial, indiceFinal);
|
||||||
@ -44,14 +43,76 @@ const DoctorRelatorioManager = () => {
|
|||||||
|
|
||||||
const fetchReports = async () => {
|
const fetchReports = async () => {
|
||||||
try {
|
try {
|
||||||
var myHeaders = new Headers();
|
const myHeaders = new Headers();
|
||||||
myHeaders.append('apikey', API_KEY);
|
myHeaders.append('apikey', API_KEY);
|
||||||
if (authHeader) myHeaders.append('Authorization', authHeader);
|
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);
|
// Tenta descobrir o ID do usuário logado para aplicar filtro por médico
|
||||||
const data = await res.json();
|
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();
|
const uniqueMap = new Map();
|
||||||
(Array.isArray(data) ? data : []).forEach(r => {
|
(Array.isArray(data) ? data : []).forEach(r => {
|
||||||
if (r && r.id) uniqueMap.set(r.id, r);
|
if (r && r.id) uniqueMap.set(r.id, r);
|
||||||
@ -65,7 +126,7 @@ const DoctorRelatorioManager = () => {
|
|||||||
setRelatoriosFinais(unique);
|
setRelatoriosFinais(unique);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erro listar relatórios', err);
|
console.error('Erro listar relatórios (catch):', err);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setRelatoriosOriginais([]);
|
setRelatoriosOriginais([]);
|
||||||
setRelatoriosFiltrados([]);
|
setRelatoriosFiltrados([]);
|
||||||
@ -85,43 +146,57 @@ const DoctorRelatorioManager = () => {
|
|||||||
};
|
};
|
||||||
}, [authHeader]);
|
}, [authHeader]);
|
||||||
|
|
||||||
|
// Busca dados de pacientes e médicos baseados na lista final que aparece na tela
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchRelData = async () => {
|
const fetchRelData = async () => {
|
||||||
const pacientes = [];
|
const pacientes = [];
|
||||||
const medicos = [];
|
const medicos = [];
|
||||||
for (let i = 0; i < relatoriosFiltrados.length; i++) {
|
for (let i = 0; i < relatoriosFinais.length; i++) {
|
||||||
const rel = relatoriosFiltrados[i];
|
const rel = relatoriosFinais[i];
|
||||||
|
// paciente
|
||||||
try {
|
try {
|
||||||
const pacienteRes = await GetPatientByID(rel.patient_id, authHeader);
|
const pacienteRes = await GetPatientByID(rel.patient_id, authHeader);
|
||||||
pacientes.push(Array.isArray(pacienteRes) ? pacienteRes[0] : pacienteRes);
|
pacientes.push(Array.isArray(pacienteRes) ? pacienteRes[0] : pacienteRes);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
pacientes.push(null);
|
pacientes.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// médico: prioriza campos com id (doctor_id ou created_by). Se tiver somente requested_by (nome), usa nome.
|
||||||
try {
|
try {
|
||||||
const doctorId = rel.created_by || rel.requested_by || null;
|
if (rel.doctor_id) {
|
||||||
if (doctorId) {
|
const docRes = await GetDoctorByID(rel.doctor_id, authHeader);
|
||||||
const docRes = await GetDoctorByID(doctorId, authHeader);
|
|
||||||
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes);
|
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 {
|
} else {
|
||||||
medicos.push({ full_name: rel.requested_by || '' });
|
medicos.push({ full_name: '' });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// fallback para requested_by se houver
|
||||||
medicos.push({ full_name: rel.requested_by || '' });
|
medicos.push({ full_name: rel.requested_by || '' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setPacientesComRelatorios(pacientes);
|
setPacientesComRelatorios(pacientes);
|
||||||
setMedicosComRelatorios(medicos);
|
setMedicosComRelatorios(medicos);
|
||||||
};
|
};
|
||||||
if (relatoriosFiltrados.length > 0) fetchRelData();
|
|
||||||
|
if (relatoriosFinais.length > 0) fetchRelData();
|
||||||
else {
|
else {
|
||||||
setPacientesComRelatorios([]);
|
setPacientesComRelatorios([]);
|
||||||
setMedicosComRelatorios([]);
|
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);
|
setRelatorioModal(relatorio);
|
||||||
setModalIndex(index);
|
setModalIndex(indexToUse);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -130,6 +205,7 @@ const DoctorRelatorioManager = () => {
|
|||||||
setTermoPesquisa('');
|
setTermoPesquisa('');
|
||||||
setFiltroExame('');
|
setFiltroExame('');
|
||||||
setRelatoriosFinais(relatoriosOriginais);
|
setRelatoriosFinais(relatoriosOriginais);
|
||||||
|
setPaginaAtual(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const BaixarPDFdoRelatorio = (nome_paciente, idx) => {
|
const BaixarPDFdoRelatorio = (nome_paciente, idx) => {
|
||||||
@ -201,17 +277,17 @@ const DoctorRelatorioManager = () => {
|
|||||||
<div id='infoPaciente' style={{ padding: '0 6px' }}>
|
<div id='infoPaciente' style={{ padding: '0 6px' }}>
|
||||||
<p><strong>Paciente:</strong> {pacientesComRelatorios[modalIndex]?.full_name}</p>
|
<p><strong>Paciente:</strong> {pacientesComRelatorios[modalIndex]?.full_name}</p>
|
||||||
<p><strong>Data de nascimento:</strong> {pacientesComRelatorios[modalIndex]?.birth_date || '—'}</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>
|
<p style={{ marginTop: 12, fontWeight: '700' }}>Conteúdo do Relatório:</p>
|
||||||
<div className="tiptap-viewer-wrapper">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 20, padding: '0 6px' }}>
|
<div style={{ marginTop: 20, padding: '0 6px' }}>
|
||||||
<p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatoriosFiltrados[modalIndex]?.requested_by}</p>
|
<p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatoriosFinais[modalIndex]?.requested_by}</p>
|
||||||
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {relatoriosFiltrados[modalIndex]?.created_at || '—'}</p>
|
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {relatoriosFinais[modalIndex]?.created_at || '—'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -298,7 +374,8 @@ const DoctorRelatorioManager = () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{relatoriosPaginados.length > 0 ? (
|
{relatoriosPaginados.length > 0 ? (
|
||||||
relatoriosPaginados.map((relatorio, index) => {
|
relatoriosPaginados.map((relatorio, index) => {
|
||||||
const paciente = pacientesData[relatorio.patient_id];
|
const globalIndex = relatoriosFinais.findIndex(r => r.id === relatorio.id);
|
||||||
|
const paciente = pacientesComRelatorios[globalIndex];
|
||||||
return (
|
return (
|
||||||
<tr key={relatorio.id}>
|
<tr key={relatorio.id}>
|
||||||
<td>{paciente?.full_name || 'Carregando...'}</td>
|
<td>{paciente?.full_name || 'Carregando...'}</td>
|
||||||
|
|||||||
@ -1,600 +1,377 @@
|
|||||||
//ConsultasPaciente.jsx
|
//ConsultasPaciente.jsx
|
||||||
//Nesta página falta: mudar nomes, ajustar caminho do CSS
|
//Nesta página falta: mudar nomes, ajustar caminho do CSS
|
||||||
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useState, useMemo, useEffect } from 'react';
|
||||||
import { useEffect, useState, useMemo } from 'react'
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../../_assets/utils/AuthProvider'
|
import { useAuth } from '../../_assets/utils/AuthProvider.js';
|
||||||
import { GetPatientByID } from '../../_assets/utils/Functions-Endpoints/Patient'
|
import { ChevronLeft, ChevronRight, Edit, Trash2 } from 'lucide-react';
|
||||||
import { GetDoctorByID } from '../../_assets/utils/Functions-Endpoints/Doctor'
|
import API_KEY from '../../_assets/utils/apiKeys.js';
|
||||||
import { UserInfos } from '../../_assets/utils/Functions-Endpoints/General'
|
|
||||||
import API_KEY from '../../_assets/utils/apiKeys'
|
|
||||||
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs';
|
||||||
import TabelaAgendamentoDia from "../../components/agendamento/TabelaAgendamentoDia"
|
import 'dayjs/locale/pt-br';
|
||||||
|
import isBetween from 'dayjs/plugin/isBetween';
|
||||||
|
import localeData from 'dayjs/plugin/localeData';
|
||||||
|
|
||||||
//import "./style.css"
|
import AgendamentoCadastroManager from '../secretaria/CadastroAgendamento.jsx';
|
||||||
//import "../pages/style/TablePaciente.css"
|
import Spinner from '../../components/Spinner.jsx';
|
||||||
|
|
||||||
const ConsultasPaciente = ({ setDictInfo }) => {
|
// import "../pages/style/Agendamento.css";
|
||||||
const { getAuthorizationHeader } = useAuth()
|
// import '../pages/style/FilaEspera.css';
|
||||||
const [agendamentosOrganizados, setAgendamentosOrganizados] = useState({})
|
|
||||||
const [listaTodasConsultas, setListaTodasConsultas] = useState([])
|
|
||||||
const [patientID, setPatientID] = useState("")
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
|
||||||
const [selectedID, setSelectedId] = useState("")
|
|
||||||
let authHeader = getAuthorizationHeader()
|
|
||||||
|
|
||||||
const [motivoCancelamento, setMotivoCancelamento] = useState("")
|
dayjs.locale('pt-br');
|
||||||
|
dayjs.extend(isBetween);
|
||||||
|
dayjs.extend(localeData);
|
||||||
|
|
||||||
const [consultas, setConsultas] = useState([])
|
const Agendamento = ({ setDictInfo }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [consultasOrganizadas, setConsultasOrganizadas] = useState({})
|
const { getAuthorizationHeader, user } = useAuth();
|
||||||
const [filaDeEspera, setFilaDeEspera] = useState([])
|
|
||||||
const [viewFila, setViewFila] = useState(false)
|
|
||||||
|
|
||||||
const [listaConsultasID, setListaConsultaID] = useState([])
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [coresConsultas,setCoresConsultas] = useState([])
|
const [DictAgendamentosOrganizados, setDictAgendamentosOrganizados] = useState({});
|
||||||
|
const [filaEsperaData, setFilaDeEsperaData] = useState([]);
|
||||||
|
const [FiladeEspera, setFiladeEspera] = useState(false);
|
||||||
|
const [PageNovaConsulta, setPageConsulta] = useState(false);
|
||||||
|
|
||||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
const [currentDate, setCurrentDate] = useState(dayjs());
|
||||||
|
const [selectedDay, setSelectedDay] = useState(dayjs());
|
||||||
|
const [quickJump, setQuickJump] = useState({
|
||||||
|
month: currentDate.month(),
|
||||||
|
year: currentDate.year()
|
||||||
|
});
|
||||||
|
|
||||||
const [waitlistSearch, setWaitlistSearch] = useState('');
|
const [isCancelModalOpen, setIsCancelModalOpen] = useState(false);
|
||||||
const [waitSortKey, setWaitSortKey] = useState(null); // 'paciente' | 'medico' | 'data' | null
|
const [appointmentToCancel, setAppointmentToCancel] = useState(null);
|
||||||
const [waitSortDir, setWaitSortDir] = useState('asc'); // 'asc' | 'desc'
|
const [cancellationReason, setCancellationReason] = useState('');
|
||||||
const [waitPage, setWaitPage] = useState(1);
|
|
||||||
const [waitPerPage, setWaitPerPage] = useState(10);
|
|
||||||
|
|
||||||
|
const authHeader = useMemo(() => getAuthorizationHeader(), [getAuthorizationHeader]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const carregarDados = async () => {
|
||||||
|
const patientId = user?.patient_id || "6e7f8829-0574-42df-9290-8dbb70f75ada";
|
||||||
|
|
||||||
console.log(listaConsultasID, coresConsultas, "ojwhdofigewfey7few0fr74r")
|
if (!authHeader) {
|
||||||
|
console.warn("Header de autorização não disponível.");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
}, [coresConsultas, listaConsultasID])
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const myHeaders = new Headers({ "Authorization": authHeader, "apikey": API_KEY });
|
||||||
|
const requestOptions = { method: 'GET', headers: myHeaders };
|
||||||
|
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select=*,doctors(full_name)&patient_id=eq.${patientId}`, requestOptions);
|
||||||
|
|
||||||
useMemo(() => {
|
if (!response.ok) throw new Error(`Erro na requisição: ${response.statusText}`);
|
||||||
let conjuntoConsultas = {}
|
|
||||||
let filaEspera = []
|
|
||||||
|
|
||||||
const fetchInfosConsultas = async (consulta) => {
|
const consultasBrutas = await response.json() || [];
|
||||||
//console.log(doctor, "PACIENTE TRAZIDO PELO ")
|
|
||||||
|
|
||||||
//let consultaMelhorada = {...consulta, paciente_nome:paciente[0].full_name, medico_nome:doctor[0].full_name }
|
const newDict = {};
|
||||||
|
const newFila = [];
|
||||||
|
|
||||||
//console.log(consultaMelhorada,"ID DO MEDICO")
|
for (const agendamento of consultasBrutas) {
|
||||||
|
const agendamentoMelhorado = {
|
||||||
|
...agendamento,
|
||||||
|
medico_nome: agendamento.doctors?.full_name || 'Médico não informado'
|
||||||
|
};
|
||||||
|
|
||||||
for(let i = 0; listaTodasConsultas.length > i; i++){
|
if (agendamento.status === "requested") {
|
||||||
|
newFila.push({ agendamento: agendamentoMelhorado, Infos: agendamentoMelhorado });
|
||||||
let consulta = listaTodasConsultas[i]
|
} else {
|
||||||
|
const diaAgendamento = dayjs(agendamento.scheduled_at).format("YYYY-MM-DD");
|
||||||
let doctor = await GetDoctorByID(consulta.doctor_id, authHeader)
|
if (newDict[diaAgendamento]) {
|
||||||
let paciente = await GetPatientByID(consulta.patient_id, authHeader)
|
newDict[diaAgendamento].push(agendamentoMelhorado);
|
||||||
|
} else {
|
||||||
consulta = {...consulta, medico_nome:doctor[0]?.full_name, paciente_nome:paciente[0]?.full_name}
|
newDict[diaAgendamento] = [agendamentoMelhorado];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(consulta.status === "requested"){
|
|
||||||
|
|
||||||
filaEspera.push(consulta)
|
|
||||||
|
|
||||||
}else{
|
|
||||||
|
|
||||||
let data = consulta.scheduled_at.split("T")[0]
|
|
||||||
let chavesConsultas = Object.keys(conjuntoConsultas)
|
|
||||||
|
|
||||||
if(chavesConsultas.includes(data)){
|
|
||||||
let lista = conjuntoConsultas[data]
|
|
||||||
|
|
||||||
lista.push(consulta)
|
|
||||||
|
|
||||||
conjuntoConsultas = {...conjuntoConsultas, [data]:lista}
|
|
||||||
}else{
|
|
||||||
conjuntoConsultas = {...conjuntoConsultas, [data]:[consulta] }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setConsultasOrganizadas(conjuntoConsultas)
|
|
||||||
setFilaDeEspera(filaEspera)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("so muda")
|
for (const key in newDict) {
|
||||||
if(!listaTodasConsultas.length) return
|
newDict[key].sort((a, b) => a.scheduled_at.localeCompare(b.scheduled_at));
|
||||||
|
|
||||||
console.log(filaEspera, "fila de espera")
|
|
||||||
fetchInfosConsultas();
|
|
||||||
|
|
||||||
}, [listaTodasConsultas])
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let userInfos = UserInfos(authHeader)
|
|
||||||
|
|
||||||
const fetchConsultas = async () => {
|
|
||||||
try {
|
|
||||||
const myHeaders = new Headers();
|
|
||||||
myHeaders.append("Authorization", authHeader);
|
|
||||||
myHeaders.append("apikey", API_KEY)
|
|
||||||
|
|
||||||
const requestOptions = {
|
|
||||||
method: 'GET',
|
|
||||||
headers: myHeaders,
|
|
||||||
redirect: 'follow'
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?patient_id=eq.${"6e7f8829-0574-42df-9290-8dbb70f75ada"}`, requestOptions);
|
|
||||||
const result = await response.json();
|
|
||||||
setListaTodasConsultas(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('error', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchConsultas();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
|
|
||||||
const confirmConsulta = (selectedPatientId) => {
|
|
||||||
var myHeaders = new Headers();
|
|
||||||
myHeaders.append("Content-Type", "application/json");
|
|
||||||
myHeaders.append('apikey', API_KEY)
|
|
||||||
myHeaders.append("authorization", authHeader)
|
|
||||||
|
|
||||||
|
|
||||||
var raw = JSON.stringify({ "status":"confirmed"
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var requestOptions = {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: myHeaders,
|
|
||||||
body: raw,
|
|
||||||
redirect: 'follow'
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${selectedPatientId}`, requestOptions)
|
|
||||||
.then(response => {if(response.status !== 200)(console.log(response))})
|
|
||||||
.then(result => console.log(result))
|
|
||||||
.catch(error => console.log('error', error));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteConsulta = async (ID) => {
|
|
||||||
try {
|
|
||||||
const myHeaders = new Headers();
|
|
||||||
myHeaders.append("Content-Type", "application/json");
|
|
||||||
myHeaders.append('apikey', API_KEY);
|
|
||||||
myHeaders.append("authorization", authHeader);
|
|
||||||
|
|
||||||
const raw = JSON.stringify({ "status": "cancelled", "cancellation_reason":motivoCancelamento });
|
|
||||||
|
|
||||||
const requestOptions = {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: myHeaders,
|
|
||||||
body: raw,
|
|
||||||
redirect: 'follow'
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${ID}`, requestOptions);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Falha ao cancelar consulta: ${response.status} - ${errorText}`);
|
|
||||||
}
|
|
||||||
setConsultas(prevConsultas => prevConsultas.filter(consulta => consulta.id !== ID));
|
|
||||||
|
|
||||||
console.log("Consulta cancelada com sucesso!");
|
|
||||||
alert("Consulta cancelada com sucesso!");
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erro ao cancelar a consulta:', error);
|
|
||||||
alert('Erro ao cancelar a consulta. Veja o console.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDictAgendamentosOrganizados(newDict);
|
||||||
|
setFilaDeEsperaData(newFila);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Falha ao buscar ou processar agendamentos:', err);
|
||||||
|
setDictAgendamentosOrganizados({});
|
||||||
|
setFilaDeEsperaData([]);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
carregarDados();
|
||||||
|
}, [authHeader, user]);
|
||||||
|
|
||||||
|
const updateAppointmentStatus = async (id, updates) => {
|
||||||
|
const myHeaders = new Headers({
|
||||||
|
"Authorization": authHeader, "apikey": API_KEY, "Content-Type": "application/json"
|
||||||
|
});
|
||||||
|
const requestOptions = { method: 'PATCH', headers: myHeaders, body: JSON.stringify(updates) };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${id}`, requestOptions);
|
||||||
|
if (!response.ok) throw new Error('Falha ao atualizar o status.');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro de rede/servidor:', error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelClick = (appointmentId) => {
|
||||||
|
setAppointmentToCancel(appointmentId);
|
||||||
|
setCancellationReason('');
|
||||||
|
setIsCancelModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const filaEsperaFiltrada = useMemo(() => {
|
const executeCancellation = async () => {
|
||||||
if (!waitlistSearch.trim()) return filaDeEspera;
|
if (!appointmentToCancel) return;
|
||||||
const term = waitlistSearch.toLowerCase();
|
setIsLoading(true);
|
||||||
return filaDeEspera.filter(item => {
|
const motivo = cancellationReason.trim() || "Cancelado pelo paciente (motivo não especificado)";
|
||||||
const paciente = item?.paciente_nome?.toLowerCase() || '';
|
const success = await updateAppointmentStatus(appointmentToCancel, {
|
||||||
const medico = item?.medico_nome?.toLowerCase() || '';
|
status: "cancelled",
|
||||||
return paciente.includes(term) || medico.includes(term);
|
cancellation_reason: motivo,
|
||||||
});
|
updated_at: new Date().toISOString()
|
||||||
}, [waitlistSearch, filaDeEspera]);
|
});
|
||||||
|
|
||||||
|
setIsCancelModalOpen(false);
|
||||||
|
setAppointmentToCancel(null);
|
||||||
|
setCancellationReason('');
|
||||||
|
|
||||||
const applySortingWaitlist = (arr) => {
|
if (success) {
|
||||||
if (!Array.isArray(arr) || !waitSortKey) return arr;
|
alert("Solicitação cancelada com sucesso!");
|
||||||
const copy = [...arr];
|
|
||||||
if (waitSortKey === 'paciente') {
|
setDictAgendamentosOrganizados(prev => {
|
||||||
copy.sort((a, b) => (a?.paciente_nome || '').localeCompare((b?.paciente_nome || ''), undefined, { sensitivity: 'base' }));
|
const newDict = { ...prev };
|
||||||
} else if (waitSortKey === 'medico') {
|
for (const date in newDict) {
|
||||||
copy.sort((a, b) => (a?.medico_nome || '').localeCompare((b?.medico_nome || ''), undefined, { sensitivity: 'base' }));
|
newDict[date] = newDict[date].filter(app => app.id !== appointmentToCancel);
|
||||||
} else if (waitSortKey === 'data') {
|
|
||||||
copy.sort((a, b) => new Date(a?.created_at || 0) - new Date(b?.created_at || 0));
|
|
||||||
}
|
}
|
||||||
if (waitSortDir === 'desc') copy.reverse();
|
return newDict;
|
||||||
return copy;
|
});
|
||||||
};
|
setFilaDeEsperaData(prev => prev.filter(item => item.agendamento.id !== appointmentToCancel));
|
||||||
|
} else {
|
||||||
|
alert("Falha ao cancelar a solicitação.");
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const filaEsperaOrdenada = applySortingWaitlist(filaEsperaFiltrada);
|
const handleQuickJumpChange = (type, value) => setQuickJump(prev => ({ ...prev, [type]: Number(value) }));
|
||||||
|
const applyQuickJump = () => {
|
||||||
|
const newDate = dayjs().year(quickJump.year).month(quickJump.month).date(1);
|
||||||
const waitTotalPages = Math.ceil(filaEsperaFiltrada.length / waitPerPage) || 1;
|
setCurrentDate(newDate);
|
||||||
const waitIndiceInicial = (waitPage - 1) * waitPerPage;
|
setSelectedDay(newDate);
|
||||||
const waitIndiceFinal = waitIndiceInicial + waitPerPage;
|
};
|
||||||
const filaEsperaPaginada = filaEsperaOrdenada.slice(waitIndiceInicial, waitIndiceFinal);
|
const dateGrid = useMemo(() => {
|
||||||
|
const grid = [];
|
||||||
const gerarNumerosWaitPages = () => {
|
const startOfMonth = currentDate.startOf('month');
|
||||||
const paginas = [];
|
let currentDay = startOfMonth.subtract(startOfMonth.day(), 'day');
|
||||||
const paginasParaMostrar = 5;
|
for (let i = 0; i < 42; i++) {
|
||||||
let inicio = Math.max(1, waitPage - Math.floor(paginasParaMostrar / 2));
|
grid.push(currentDay);
|
||||||
let fim = Math.min(waitTotalPages, inicio + paginasParaMostrar - 1);
|
currentDay = currentDay.add(1, 'day');
|
||||||
inicio = Math.max(1, fim - paginasParaMostrar + 1);
|
}
|
||||||
for (let i = inicio; i <= fim; i++) {
|
return grid;
|
||||||
paginas.push(i);
|
}, [currentDate]);
|
||||||
}
|
const weekDays = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
|
||||||
return paginas;
|
const handleDateClick = (day) => setSelectedDay(day);
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setWaitPage(1);
|
|
||||||
}, [waitlistSearch, waitSortKey, waitSortDir]);
|
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="form-container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
|
||||||
<h1> Gerencie suas consultas</h1>
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<div className='form-container'>
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Minhas consultas</h1>
|
||||||
|
<div className="btns-gerenciamento-e-consulta" style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
|
||||||
|
<button
|
||||||
|
className="btn-adicionar-consulta"
|
||||||
|
onClick={() => {
|
||||||
|
setPageConsulta(true);
|
||||||
|
setFiladeEspera(false);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
backgroundColor: PageNovaConsulta ? '#1d4ed8' : undefined
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="bi bi-plus-circle"></i> Solicitar Agendamento
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-adicionar-consulta"
|
||||||
|
onClick={() => {
|
||||||
|
setFiladeEspera(!FiladeEspera);
|
||||||
|
setPageConsulta(false);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
backgroundColor: FiladeEspera && !PageNovaConsulta ? '#1d4ed8' : undefined
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="bi bi-list-task me-1"></i> Fila de Espera ({filaEsperaData.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='btns-container'>
|
{!PageNovaConsulta ? (
|
||||||
<button className="btn btn-primary" onClick={() => { navigate("criar") }}>
|
<div className='atendimento-eprocura'>
|
||||||
<i className="bi bi-plus-circle"></i> Adicionar Consulta
|
<section className='calendario-ou-filaespera'>
|
||||||
</button>
|
{!FiladeEspera ? (
|
||||||
{!viewFila ?
|
<div className="calendar-wrapper">
|
||||||
<button onClick={() => setViewFila(true)} className="btn btn-primary">Ver fila de espera</button>
|
<div className="calendar-info-panel">
|
||||||
:
|
<div className="info-date-display"><span>{selectedDay.format('MMM')}</span><strong>{selectedDay.format('DD')}</strong></div>
|
||||||
<button onClick={() => setViewFila(false)} className="btn btn-primary">Ver consultas </button>
|
<div className="info-details"><h3>{selectedDay.format('dddd')}</h3><p>{selectedDay.format('D [de] MMMM [de] YYYY')}</p></div>
|
||||||
}
|
<div className="appointments-list">
|
||||||
</div>
|
<h4>Consultas para {selectedDay.format('DD/MM')}</h4>
|
||||||
|
{(DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')]?.length > 0) ? (
|
||||||
|
DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')].map(app => (
|
||||||
|
<div key={app.id} className="appointment-item" data-status={app.status}>
|
||||||
{viewFila ?
|
<div className="item-time">{dayjs(app.scheduled_at).format('HH:mm')}</div>
|
||||||
<div className="page-content table-paciente-container">
|
<div className="item-details">
|
||||||
<section className="row">
|
<span>Consulta com Dr(a). {app.medico_nome}</span>
|
||||||
<div className="col-12">
|
</div>
|
||||||
<div className="card table-paciente-card">
|
<div className='item-actions'>
|
||||||
<div className="card-header">
|
{app.status !== 'cancelled' && dayjs(app.scheduled_at).isAfter(dayjs()) && (
|
||||||
<h4 className="card-title mb-0">Fila de Espera</h4>
|
<button className="btn btn-sm btn-outline-danger" onClick={() => handleCancelClick(app.id)} title="Cancelar Consulta">
|
||||||
</div>
|
<Trash2 size={16} />
|
||||||
<div className="card-body">
|
</button>
|
||||||
{/* Filtros */}
|
)}
|
||||||
<div className="card p-3 mb-3 table-paciente-filters">
|
</div>
|
||||||
<h5 className="mb-3">
|
|
||||||
<i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros
|
|
||||||
</h5>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="Buscar por paciente ou médico..."
|
|
||||||
value={waitlistSearch}
|
|
||||||
onChange={(e) => setWaitlistSearch(e.target.value)}
|
|
||||||
/>
|
|
||||||
<small className="text-muted">
|
|
||||||
Digite o nome do paciente ou nome do médico
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-flex flex-wrap align-items-center gap-2 mb-3">
|
|
||||||
{/* Ordenação rápida */}
|
|
||||||
<div className="d-flex align-items-center gap-2">
|
|
||||||
<span className="text-muted small">Ordenar por:</span>
|
|
||||||
{(() => {
|
|
||||||
const sortValue = waitSortKey ? `${waitSortKey}-${waitSortDir}` : '';
|
|
||||||
return (
|
|
||||||
<select
|
|
||||||
className="form-select compact-select sort-select w-auto"
|
|
||||||
value={sortValue}
|
|
||||||
onChange={(e) => {
|
|
||||||
const v = e.target.value;
|
|
||||||
if (!v) { setWaitSortKey(null); setWaitSortDir('asc'); return; }
|
|
||||||
const [k, d] = v.split('-');
|
|
||||||
setWaitSortKey(k);
|
|
||||||
setWaitSortDir(d);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="">Sem ordenação</option>
|
|
||||||
<option value="paciente-asc">Paciente (A-Z)</option>
|
|
||||||
<option value="paciente-desc">Paciente (Z-A)</option>
|
|
||||||
<option value="medico-asc">Médico (A-Z)</option>
|
|
||||||
<option value="medico-desc">Médico (Z-A)</option>
|
|
||||||
<option value="data-asc">Data (mais antiga)</option>
|
|
||||||
<option value="data-desc">Data (mais recente)</option>
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3">
|
|
||||||
<div className="contador-pacientes">
|
|
||||||
{filaEsperaFiltrada.length} DE {filaDeEspera.length} SOLICITAÇÕES ENCONTRADAS
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="table-responsive">
|
|
||||||
<table className="table table-striped table-hover table-paciente-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Nome do Paciente</th>
|
|
||||||
<th>CPF</th>
|
|
||||||
<th>Médico Solicitado</th>
|
|
||||||
<th>Data da Solicitação</th>
|
|
||||||
<th>Ações</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{filaEsperaPaginada.length > 0 ? (
|
|
||||||
filaEsperaPaginada.map((item, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{item?.paciente_nome}</td>
|
|
||||||
<td>{item?.paciente_cpf}</td>
|
|
||||||
<td>{item?.medico_nome}</td>
|
|
||||||
<td>{dayjs(item?.created_at).format('DD/MM/YYYY HH:mm')}</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-delete"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedId(item.id)
|
|
||||||
setShowDeleteModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className="bi bi-trash me-1"></i> Excluir
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td colSpan="5" className="text-center py-4">
|
|
||||||
<div className="text-muted">
|
|
||||||
<i className="bi bi-inbox display-4"></i>
|
|
||||||
<p className="mt-2">Nenhuma solicitação encontrada.</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{/* Paginação */}
|
|
||||||
{filaEsperaFiltrada.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={waitPerPage}
|
|
||||||
onChange={(e) => {
|
|
||||||
setWaitPerPage(Number(e.target.value));
|
|
||||||
setWaitPage(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 {waitPage} de {waitTotalPages} •
|
|
||||||
Mostrando {waitIndiceInicial + 1}-{Math.min(waitIndiceFinal, filaEsperaFiltrada.length)} de {filaEsperaFiltrada.length}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<ul className="pagination pagination-sm mb-0">
|
|
||||||
<li className={`page-item ${waitPage === 1 ? 'disabled' : ''}`}>
|
|
||||||
<button className="page-link" onClick={() => setWaitPage(p => Math.max(1, p - 1))}>
|
|
||||||
<i className="bi bi-chevron-left"></i>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{gerarNumerosWaitPages().map(pagina => (
|
|
||||||
<li key={pagina} className={`page-item ${pagina === waitPage ? 'active' : ''}`}>
|
|
||||||
<button className="page-link" onClick={() => setWaitPage(pagina)}>
|
|
||||||
{pagina}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<li className={`page-item ${waitPage === waitTotalPages ? 'disabled' : ''}`}>
|
|
||||||
<button className="page-link" onClick={() => setWaitPage(p => Math.min(waitTotalPages, p + 1))}>
|
|
||||||
<i className="bi bi-chevron-right"></i>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
))
|
||||||
|
) : (<div className="no-appointments-info"><p>Nenhuma consulta agendada para esta data.</p></div>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="calendar-main">
|
||||||
|
<div className="calendar-legend">
|
||||||
|
<div className="legend-item" data-status="completed">Realizado</div>
|
||||||
|
<div className="legend-item" data-status="confirmed">Confirmado</div>
|
||||||
|
<div className="legend-item" data-status="agendado">Agendado</div>
|
||||||
|
<div className="legend-item" data-status="cancelled">Cancelado</div>
|
||||||
|
</div>
|
||||||
|
<div className="calendar-controls">
|
||||||
|
<div className="date-indicator">
|
||||||
|
<h2>{currentDate.format('MMMM [de] YYYY')}</h2>
|
||||||
|
<div className="quick-jump-controls" style={{ display: 'flex', gap: '5px', marginTop: '10px' }}>
|
||||||
|
<select value={quickJump.month} onChange={(e) => handleQuickJumpChange('month', e.target.value)} className="form-select form-select-sm w-auto">
|
||||||
|
{dayjs.months().map((month, index) => (<option key={index} value={index}>{month.charAt(0).toUpperCase() + month.slice(1)}</option>))}
|
||||||
|
</select>
|
||||||
|
<select value={quickJump.year} onChange={(e) => handleQuickJumpChange('year', e.target.value)} className="form-select form-select-sm w-auto">
|
||||||
|
{Array.from({ length: 11 }, (_, i) => dayjs().year() - 5 + i).map(year => (<option key={year} value={year}>{year}</option>))}
|
||||||
|
</select>
|
||||||
|
<button className="btn btn-sm btn-outline-primary" onClick={applyQuickJump} disabled={quickJump.month === currentDate.month() && quickJump.year === currentDate.year()}>Ir</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="nav-buttons">
|
||||||
</section>
|
<button onClick={() => setCurrentDate(c => c.subtract(1, 'month'))}><ChevronLeft size={20} /></button>
|
||||||
</div>
|
<button onClick={() => setCurrentDate(dayjs())}>Hoje</button>
|
||||||
:
|
<button onClick={() => setCurrentDate(c => c.add(1, 'month'))}><ChevronRight size={20} /></button>
|
||||||
<div>
|
</div>
|
||||||
<h2 className='fila-titulo'>Suas proximas consultas</h2>
|
</div>
|
||||||
|
<div className="calendar-grid">
|
||||||
<TabelaAgendamentoDia agendamentos={consultasOrganizadas} setDictInfo={setDictInfo}
|
{weekDays.map(day => <div key={day} className="day-header">{day}</div>)}
|
||||||
selectedID={selectedID} setSelectedId={setSelectedId} setShowDeleteModal={setShowDeleteModal}
|
{dateGrid.map((day, index) => {
|
||||||
coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID}
|
const appointmentsOnDay = DictAgendamentosOrganizados[day.format('YYYY-MM-DD')] || [];
|
||||||
listaConsultasID={listaConsultasID} setShowConfirmModal={setShowConfirmModal}
|
const cellClasses = `day-cell ${day.isSame(currentDate, 'month') ? 'current-month' : 'other-month'} ${day.isSame(dayjs(), 'day') ? 'today' : ''} ${day.isSame(selectedDay, 'day') ? 'selected' : ''}`;
|
||||||
/>
|
return (
|
||||||
</div>
|
<div key={index} className={cellClasses} onClick={() => handleDateClick(day)}>
|
||||||
}
|
<span>{day.format('D')}</span>
|
||||||
|
{appointmentsOnDay.length > 0 && <div className="appointments-indicator">{appointmentsOnDay.length}</div>}
|
||||||
{showDeleteModal && (
|
</div>
|
||||||
<div
|
);
|
||||||
className="modal fade show"
|
})}
|
||||||
style={{
|
</div>
|
||||||
display: "block",
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
||||||
}}
|
|
||||||
tabIndex="-1"
|
|
||||||
onClick={(e) =>
|
|
||||||
e.target.classList.contains("modal") && setShowDeleteModal(false)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="modal-dialog modal-dialog-centered">
|
|
||||||
<div className="modal-content">
|
|
||||||
|
|
||||||
<div className="modal-header bg-danger bg-opacity-25">
|
|
||||||
<h5 className="modal-title text-danger">
|
|
||||||
Confirmação de Cancelamento
|
|
||||||
</h5>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn-close"
|
|
||||||
onClick={() => setShowDeleteModal(false)}
|
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal-body">
|
|
||||||
<p className="mb-0 fs-5">
|
|
||||||
Qual o motivo do cancelamento?
|
|
||||||
</p>
|
|
||||||
<div className='campo-de-input'>
|
|
||||||
|
|
||||||
<textarea className='input-modal' value={motivoCancelamento} onChange={(e) => setMotivoCancelamento(e.target.value)} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
<div className="modal-footer">
|
<div className="page-content table-paciente-container">
|
||||||
|
<section className="row">
|
||||||
<button
|
<div className="col-12">
|
||||||
type="button"
|
<div className="card table-paciente-card">
|
||||||
className="btn btn-primary"
|
<div className="card-header"><h4 className="card-title mb-0">Minhas Solicitações em Fila de Espera</h4></div>
|
||||||
onClick={() => {setShowDeleteModal(false);
|
<div className="card-body">
|
||||||
|
<div className="table-responsive">
|
||||||
}}
|
<table className="table table-striped table-hover">
|
||||||
>
|
<thead>
|
||||||
Cancelar
|
<tr>
|
||||||
</button>
|
<th>Médico Solicitado</th>
|
||||||
|
<th>Data da Solicitação</th>
|
||||||
|
<th>Ações</th>
|
||||||
<button
|
</tr>
|
||||||
type="button"
|
</thead>
|
||||||
className="btn btn-danger"
|
<tbody>
|
||||||
onClick={() => {
|
{filaEsperaData.length > 0 ? (filaEsperaData.map((item) => (
|
||||||
|
<tr key={item.agendamento.id}>
|
||||||
deleteConsulta(selectedID)
|
<td>Dr(a). {item.Infos?.medico_nome}</td>
|
||||||
setShowDeleteModal(false)
|
<td>{dayjs(item.agendamento.created_at).format('DD/MM/YYYY HH:mm')}</td>
|
||||||
let lista_cores = coresConsultas
|
<td>
|
||||||
|
<button className="btn btn-sm btn-danger" onClick={() => handleCancelClick(item.agendamento.id)}>
|
||||||
let lista = listaConsultasID
|
<i className="bi bi-trash me-1"></i> Cancelar
|
||||||
|
</button>
|
||||||
lista.push(selectedID)
|
</td>
|
||||||
lista_cores.push("cancelled")
|
</tr>
|
||||||
|
))) : (
|
||||||
setCoresConsultas(lista_cores)
|
<tr>
|
||||||
|
<td colSpan="3" className="text-center py-4">
|
||||||
setListaConsultaID(lista)
|
<div className="text-muted">Nenhuma solicitação na fila de espera.</div>
|
||||||
|
</td>
|
||||||
console.log("lista", lista)
|
</tr>
|
||||||
|
)}
|
||||||
}}
|
</tbody>
|
||||||
|
</table>
|
||||||
>
|
</div>
|
||||||
<i className="bi bi-trash me-1"></i> Excluir
|
</div>
|
||||||
</button>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</section>
|
||||||
</div>)}
|
|
||||||
|
|
||||||
{showConfirmModal &&(
|
|
||||||
<div
|
|
||||||
className="modal fade show"
|
|
||||||
style={{
|
|
||||||
display: "block",
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
||||||
}}
|
|
||||||
tabIndex="-1"
|
|
||||||
onClick={(e) =>
|
|
||||||
e.target.classList.contains("modal") && setShowDeleteModal(false)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="modal-dialog modal-dialog-centered">
|
|
||||||
<div className="modal-content">
|
|
||||||
|
|
||||||
<div className="modal-header bg-success">
|
|
||||||
<h5 className="modal-title">
|
|
||||||
Confirmação de edição
|
|
||||||
</h5>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal-body">
|
|
||||||
<p className="mb-0 fs-5">
|
|
||||||
Tem certeza que deseja retirar o cancelamento ?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal-footer">
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={() => {setShowConfirmModal(false); setSelectedId("")}}
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-success"
|
|
||||||
onClick={() => {confirmConsulta(selectedID);setShowConfirmModal(false)
|
|
||||||
let lista_cores = coresConsultas
|
|
||||||
|
|
||||||
let lista = listaConsultasID
|
|
||||||
|
|
||||||
lista.push(selectedID)
|
|
||||||
lista_cores.push("confirmed")
|
|
||||||
|
|
||||||
setCoresConsultas(lista_cores)
|
|
||||||
|
|
||||||
setListaConsultaID(lista)
|
|
||||||
}}
|
|
||||||
|
|
||||||
>
|
|
||||||
<i className="bi bi-trash me-1"></i> Confirmar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
) : (
|
||||||
|
<AgendamentoCadastroManager setPageConsulta={setPageConsulta} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isCancelModalOpen && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal-content" style={{ maxWidth: '400px' }}>
|
||||||
|
<div className="modal-header" style={{ backgroundColor: '#fee2e2', borderBottom: '1px solid #fca5a5', padding: '15px', borderRadius: '8px 8px 0 0' }}>
|
||||||
|
<h4 style={{ margin: 0, color: '#dc2626' }}>Confirmação de Cancelamento</h4>
|
||||||
|
<button className="close-button" onClick={() => setIsCancelModalOpen(false)} style={{ background: 'none', border: 'none', fontSize: '1.5rem', cursor: 'pointer' }}>×</button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body" style={{ padding: '20px' }}>
|
||||||
|
<p>Qual o motivo do cancelamento?</p>
|
||||||
|
<textarea
|
||||||
|
value={cancellationReason}
|
||||||
|
onChange={(e) => setCancellationReason(e.target.value)}
|
||||||
|
placeholder="Ex: Precisei viajar, motivo pessoal, etc."
|
||||||
|
rows="4"
|
||||||
|
style={{ width: '100%', padding: '10px', resize: 'none', border: '1px solid #ccc', borderRadius: '4px' }}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer" style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', padding: '15px', borderTop: '1px solid #eee' }}>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => setIsCancelModalOpen(false)}
|
||||||
|
style={{ backgroundColor: '#6c757d', color: 'white', border: 'none', padding: '8px 15px', borderRadius: '4px' }}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-danger"
|
||||||
|
onClick={executeCancellation}
|
||||||
|
style={{ backgroundColor: '#dc3545', color: 'white', border: 'none', padding: '8px 15px', borderRadius: '4px' }}
|
||||||
|
>
|
||||||
|
<Trash2 size={16} style={{ marginRight: '5px' }} /> Excluir
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConsultasPaciente;
|
export default Agendamento;
|
||||||
@ -1,88 +1,188 @@
|
|||||||
//AgendamentoCadastroManager.jsx
|
//AgendamentoCadastroManager.jsx
|
||||||
//Nesta página falta: mudar nomes
|
//Nesta página falta: mudar nomes
|
||||||
|
|
||||||
import { useEffect,useState } from 'react'
|
import { useEffect, useState } from 'react';
|
||||||
import { useAuth } from '../../_assets/utils/AuthProvider'
|
import { toast } from 'react-toastify';
|
||||||
import { UserInfos } from '../../_assets/utils/Functions-Endpoints/General'
|
import { useAuth } from '../../_assets/utils/AuthProvider';
|
||||||
import API_KEY from '../../_assets/utils/apiKeys'
|
import { UserInfos } from '../../_assets/utils/Functions-Endpoints/General';
|
||||||
|
import API_KEY from '../../_assets/utils/apiKeys';
|
||||||
|
|
||||||
import FormNovaConsulta from '../../components/agendamento/FormNovaConsulta'
|
import FormNovaConsulta from '../../components/agendamento/FormNovaConsulta';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import dayjs from 'dayjs'
|
|
||||||
|
|
||||||
|
|
||||||
const AgendamentoCadastroManager = ({setPageConsulta, Dict}) => {
|
const AgendamentoCadastroManager = ({ setPageConsulta, agendamentoInicial }) => {
|
||||||
|
|
||||||
const {getAuthorizationHeader} = useAuth()
|
const { getAuthorizationHeader } = useAuth();
|
||||||
const [agendamento, setAgendamento] = useState({status:'confirmed'})
|
|
||||||
const [idUsuario, setIDusuario] = useState('0')
|
const [agendamento, setAgendamento] = useState({ status: 'agendado' });
|
||||||
|
const [idUsuario, setIDusuario] = useState('0');
|
||||||
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
|
||||||
let authHeader = getAuthorizationHeader()
|
let authHeader = getAuthorizationHeader();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
|
||||||
|
if (agendamentoInicial && agendamentoInicial.id) {
|
||||||
|
|
||||||
|
const scheduled_at = dayjs(agendamentoInicial.scheduled_at);
|
||||||
|
|
||||||
useEffect(() => {
|
setAgendamento({
|
||||||
|
...agendamentoInicial,
|
||||||
|
|
||||||
|
dataAtendimento: scheduled_at.format('YYYY-MM-DD'),
|
||||||
|
horarioInicio: scheduled_at.format('HH:mm'),
|
||||||
|
|
||||||
|
|
||||||
|
tipo_consulta: agendamentoInicial.appointment_type || 'Presencial',
|
||||||
|
convenio: agendamentoInicial.insurance_provider || 'Público',
|
||||||
|
|
||||||
|
|
||||||
|
paciente_nome: agendamentoInicial.paciente_nome || '',
|
||||||
|
paciente_cpf: agendamentoInicial.paciente_cpf || '',
|
||||||
|
medico_nome: agendamentoInicial.medico_nome || '',
|
||||||
|
|
||||||
|
|
||||||
|
patient_id: agendamentoInicial.patient_id,
|
||||||
|
doctor_id: agendamentoInicial.doctor_id,
|
||||||
|
status: agendamentoInicial.status,
|
||||||
|
});
|
||||||
|
setIsEditMode(true);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
setAgendamento({
|
||||||
|
status: 'agendado',
|
||||||
|
patient_id: null,
|
||||||
|
doctor_id: null,
|
||||||
|
dataAtendimento: dayjs().format('YYYY-MM-DD'),
|
||||||
|
horarioInicio: '',
|
||||||
|
tipo_consulta: 'Presencial',
|
||||||
|
convenio: 'Público',
|
||||||
|
paciente_nome: '',
|
||||||
|
paciente_cpf: '',
|
||||||
|
medico_nome: ''
|
||||||
|
});
|
||||||
|
setIsEditMode(false);
|
||||||
|
}
|
||||||
|
|
||||||
if(!Dict){setAgendamento({})}
|
|
||||||
else{
|
const ColherInfoUsuario = async () => {
|
||||||
console.log(Dict)
|
const result = await UserInfos(authHeader);
|
||||||
setAgendamento(Dict)
|
setIDusuario(result?.profile?.id);
|
||||||
}
|
};
|
||||||
|
ColherInfoUsuario();
|
||||||
const ColherInfoUsuario =async () => {
|
|
||||||
const result = await UserInfos(authHeader)
|
|
||||||
|
}, [agendamentoInicial, authHeader]);
|
||||||
setIDusuario(result?.profile?.id)
|
const handleSave = async (Dict) => {
|
||||||
|
|
||||||
}
|
|
||||||
ColherInfoUsuario()
|
|
||||||
|
|
||||||
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleSave = (Dict) => {
|
|
||||||
let DataAtual = dayjs()
|
|
||||||
var myHeaders = new Headers();
|
var myHeaders = new Headers();
|
||||||
myHeaders.append("apikey", API_KEY);
|
myHeaders.append("apikey", API_KEY);
|
||||||
myHeaders.append("Authorization", authHeader);
|
myHeaders.append("Authorization", authHeader);
|
||||||
myHeaders.append("Content-Type", "application/json");
|
myHeaders.append("Content-Type", "application/json");
|
||||||
|
myHeaders.append("Prefer", "return=representation");
|
||||||
|
|
||||||
|
var raw = JSON.stringify({
|
||||||
|
"patient_id": Dict.patient_id,
|
||||||
|
"doctor_id": Dict.doctor_id,
|
||||||
|
"scheduled_at": `${Dict.dataAtendimento}T${Dict.horarioInicio}:00.000Z`,
|
||||||
|
"duration_minutes": Dict.duration_minutes || 30,
|
||||||
|
"appointment_type": Dict.tipo_consulta,
|
||||||
|
"insurance_provider": Dict.convenio,
|
||||||
|
"status": Dict.status || 'agendado',
|
||||||
|
"created_by": idUsuario,
|
||||||
|
"created_at": dayjs().toISOString(),
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var requestOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: myHeaders,
|
||||||
|
body: raw,
|
||||||
|
redirect: 'follow'
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments", requestOptions);
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success("Agendamento criado com sucesso! ✅");
|
||||||
|
setPageConsulta(false);
|
||||||
|
} else {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('Erro ao cadastrar agendamento:', errorText);
|
||||||
|
toast.error(`Falha ao criar agendamento: ${JSON.parse(errorText)?.message || 'Erro desconhecido'}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro de rede:', error);
|
||||||
|
toast.error("Erro de rede ao salvar. ❌");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleUpdate = async (Dict) => {
|
||||||
|
const appointmentId = agendamentoInicial.id;
|
||||||
|
|
||||||
|
var myHeaders = new Headers();
|
||||||
|
myHeaders.append("apikey", API_KEY);
|
||||||
|
myHeaders.append("Authorization", authHeader);
|
||||||
|
myHeaders.append("Content-Type", "application/json");
|
||||||
|
myHeaders.append("Prefer", "return=representation");
|
||||||
|
|
||||||
var raw = JSON.stringify({
|
var raw = JSON.stringify({
|
||||||
|
"patient_id": Dict.patient_id,
|
||||||
"patient_id": Dict.patient_id,
|
"doctor_id": Dict.doctor_id,
|
||||||
"doctor_id": Dict.doctor_id,
|
"scheduled_at": `${Dict.dataAtendimento}T${Dict.horarioInicio}:00.000Z`,
|
||||||
"scheduled_at": `${Dict.dataAtendimento}T${Dict.horarioInicio}:00.000Z`,
|
"duration_minutes": Dict.duration_minutes || 30,
|
||||||
"duration_minutes": 30,
|
"appointment_type": Dict.tipo_consulta,
|
||||||
"appointment_type": Dict.tipo_consulta,
|
"insurance_provider": Dict.convenio,
|
||||||
|
"status": Dict.status,
|
||||||
"patient_notes": "",
|
"updated_at": dayjs().toISOString(),
|
||||||
"insurance_provider": Dict.convenio,
|
"updated_by": idUsuario,
|
||||||
"status": Dict.status,
|
});
|
||||||
"created_by": idUsuario
|
|
||||||
});
|
var requestOptions = {
|
||||||
|
method: 'PATCH',
|
||||||
var requestOptions = {
|
headers: myHeaders,
|
||||||
method: 'POST',
|
body: raw,
|
||||||
headers: myHeaders,
|
redirect: 'follow'
|
||||||
body: raw,
|
};
|
||||||
redirect: 'follow'
|
|
||||||
};
|
try {
|
||||||
|
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${appointmentId}`, requestOptions);
|
||||||
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments", requestOptions)
|
if (response.ok) {
|
||||||
.then(response => response.text())
|
toast.success("Agendamento atualizado com sucesso! 📝");
|
||||||
.then(result => console.log(result))
|
setPageConsulta(false);
|
||||||
.catch(error => console.log('error', error));
|
} else {
|
||||||
|
const errorText = await response.text();
|
||||||
}
|
console.error('Erro ao atualizar agendamento:', errorText);
|
||||||
|
toast.error(`Falha ao atualizar agendamento: ${JSON.parse(errorText)?.message || 'Erro desconhecido'}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro de rede:', error);
|
||||||
|
toast.error("Erro de rede ao atualizar. ❌");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleFormSubmit = (Dict) => {
|
||||||
|
if (isEditMode) {
|
||||||
|
|
||||||
|
handleUpdate(Dict);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
handleSave(Dict);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<FormNovaConsulta onSave={handleSave} agendamento={agendamento} setAgendamento={setAgendamento} onCancel={() => setPageConsulta(false)}/>
|
return (
|
||||||
|
<div>
|
||||||
|
{}
|
||||||
|
<FormNovaConsulta
|
||||||
|
onSave={handleFormSubmit}
|
||||||
|
agendamento={agendamento}
|
||||||
|
setAgendamento={setAgendamento}
|
||||||
|
onCancel={() => setPageConsulta(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
</div>
|
export default AgendamentoCadastroManager;
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AgendamentoCadastroManager
|
|
||||||
@ -1,338 +1,518 @@
|
|||||||
// LaudoManager.jsx
|
// LaudoManager.jsx
|
||||||
//Nesta página falta: ajustar caminho do CSS
|
//Nesta página falta: ajustar caminho do CSS
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
//import "./LaudoStyle.css"; // Importa o CSS externo
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
/* ===== Mock data (simula APIDOG) ===== */
|
import { GetPatientByID } from '../../_assets/utils/Functions-Endpoints/Patient';
|
||||||
function mockFetchLaudos() {
|
import { GetDoctorByID } from '../../_assets/utils/Functions-Endpoints/Doctor';
|
||||||
return [
|
import { useAuth } from '../../_assets/utils/AuthProvider';
|
||||||
{
|
import API_KEY from '../../_assets/utils/apiKeys';
|
||||||
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) {
|
import html2pdf from 'html2pdf.js';
|
||||||
return new Promise((res) => setTimeout(() => res({ ok: true }), 500));
|
import TiptapViewer from '../../components/medico/TiptapViewer'
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== Componente ===== */
|
//import '../PagesMedico/styleMedico/DoctorRelatorioManager.css';
|
||||||
export default function LaudoManager() {
|
|
||||||
const [laudos, setLaudos] = useState([]);
|
|
||||||
const [openDropdownId, setOpenDropdownId] = useState(null);
|
|
||||||
|
|
||||||
/* viewerLaudo é usado para mostrar o editor/leitura;
|
const LaudoManager = () => {
|
||||||
previewLaudo é usado para a pré-visualização (sem bloquear) */
|
const navigate = useNavigate();
|
||||||
const [viewerLaudo, setViewerLaudo] = useState(null);
|
const { getAuthorizationHeader } = useAuth();
|
||||||
const [previewLaudo, setPreviewLaudo] = useState(null);
|
const authHeader = getAuthorizationHeader();
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
|
||||||
|
|
||||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
|
const [relatoriosOriginais, setRelatoriosOriginais] = useState([]);
|
||||||
const [toDelete, setToDelete] = useState(null);
|
const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]);
|
||||||
const [loadingDelete, setLoadingDelete] = useState(false);
|
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);
|
||||||
|
|
||||||
/* notificação simples (sem backdrop) para 'sem permissão' */
|
const [showProtocolModal, setShowProtocolModal] = useState(false);
|
||||||
const [showNoPermission, setShowNoPermission] = useState(false);
|
const [protocolForIndex, setProtocolForIndex] = useState(null);
|
||||||
|
|
||||||
/* pesquisa */
|
const [paginaAtual, setPaginaAtual] = useState(1);
|
||||||
const [query, setQuery] = useState("");
|
const [itensPorPagina, setItensPorPagina] = useState(10);
|
||||||
|
|
||||||
/* Para simplificar: eu assumo aqui que estamos na visão da secretaria */
|
// agora guardamos a mensagem (null = sem aviso)
|
||||||
const isSecretary = true; // permanece true (somente leitura)
|
const [noPermissionText, setNoPermissionText] = useState(null);
|
||||||
|
|
||||||
|
const isSecretary = true;
|
||||||
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
// Importa os dados mock apenas
|
let mounted = true;
|
||||||
const data = mockFetchLaudos();
|
|
||||||
setLaudos(data);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Fecha dropdown ao clicar fora
|
const fetchReports = async () => {
|
||||||
useEffect(() => {
|
try {
|
||||||
function onDocClick(e) {
|
const myHeaders = new Headers();
|
||||||
if (e.target.closest && e.target.closest('.action-btn')) return;
|
myHeaders.append('apikey', API_KEY);
|
||||||
if (e.target.closest && e.target.closest('.dropdown')) return;
|
if (authHeader) myHeaders.append('Authorization', authHeader);
|
||||||
setOpenDropdownId(null);
|
const requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
|
||||||
}
|
|
||||||
document.addEventListener('click', onDocClick);
|
|
||||||
return () => document.removeEventListener('click', onDocClick);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function toggleDropdown(id, e) {
|
const res = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*", requestOptions);
|
||||||
e.stopPropagation();
|
const data = await res.json();
|
||||||
setOpenDropdownId(prev => (prev === id ? null : id));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (botao editar) */
|
const uniqueMap = new Map();
|
||||||
function handleOpenViewer(laudo) {
|
(Array.isArray(data) ? data : []).forEach(r => {
|
||||||
setOpenDropdownId(null);
|
if (r && r.id) uniqueMap.set(r.id, r);
|
||||||
if (isSecretary) {
|
});
|
||||||
// (notificação sem bloquear)
|
const unique = Array.from(uniqueMap.values())
|
||||||
setShowNoPermission(true);
|
.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
|
||||||
return;
|
|
||||||
}
|
|
||||||
setViewerLaudo(laudo);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (botao imprimir) */
|
if (mounted) {
|
||||||
function handlePrint(laudo) {
|
setRelatoriosOriginais(unique);
|
||||||
// evitar bug: fechar viewer antes de abrir preview
|
setRelatoriosFiltrados(unique);
|
||||||
setViewerLaudo(null);
|
setRelatoriosFinais(unique);
|
||||||
setPreviewLaudo(laudo);
|
}
|
||||||
setShowPreview(true);
|
} catch (err) {
|
||||||
setOpenDropdownId(null);
|
console.error('Erro listar relatórios', err);
|
||||||
}
|
if (mounted) {
|
||||||
|
setRelatoriosOriginais([]);
|
||||||
/* (botao excluir) */
|
setRelatoriosFiltrados([]);
|
||||||
function handleRequestDelete(laudo) {
|
setRelatoriosFinais([]);
|
||||||
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.");
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
};
|
||||||
alert("Erro de rede ao excluir.");
|
|
||||||
} finally {
|
|
||||||
setLoadingDelete(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* filtro de pesquisa (por pedido ou nome do paciente) */
|
fetchReports();
|
||||||
const normalized = (s = "") => String(s).toLowerCase();
|
const refreshHandler = () => fetchReports();
|
||||||
const filteredLaudos = laudos.filter(l => {
|
window.addEventListener('reports:refresh', refreshHandler);
|
||||||
const q = normalized(query).trim();
|
return () => {
|
||||||
if (!q) return true;
|
mounted = false;
|
||||||
if (normalized(l.pedido).includes(q)) return true;
|
window.removeEventListener('reports:refresh', refreshHandler);
|
||||||
if (normalized(l.paciente?.nome).includes(q)) return true;
|
};
|
||||||
return false;
|
}, [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 (
|
return (
|
||||||
<div className="laudo-wrap">
|
<div>
|
||||||
<div className="left-col">
|
<div className="page-heading"><h3>Lista de Relatórios</h3></div>
|
||||||
<div className="title-row">
|
<div className="page-content">
|
||||||
<div>
|
<section className="row">
|
||||||
<div className="page-title">Gerenciamento de Laudo</div>
|
<div className="col-12">
|
||||||
</div>
|
<div className="card">
|
||||||
</div>
|
<div className="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h4 className="card-title mb-0">Relatórios Cadastrados</h4>
|
||||||
<div style={{ marginBottom:12 }}>
|
<div>
|
||||||
<input
|
<button
|
||||||
placeholder="Pesquisar paciente ou pedido..."
|
className="btn btn-primary"
|
||||||
value={query}
|
onClick={() => setNoPermissionText('Sem permissão para editar/criar laudo.')}
|
||||||
onChange={e => setQuery(e.target.value)}
|
title="Secretaria não pode criar relatórios"
|
||||||
style={{ width:"100%", padding:12, borderRadius:8, border:"1px solid #e6eef8" }}
|
>
|
||||||
/>
|
<i className="bi bi-plus-circle"></i> Adicionar Relatório
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
{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>
|
||||||
<div className="col" style={{ flex:2 }}>
|
</div>
|
||||||
<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 className="row-actions">
|
<div className="card-body">
|
||||||
<div className="action-btn" onClick={(e)=> toggleDropdown(l.id, e)} title="Ações">
|
<div className="card p-3 mb-3">
|
||||||
<i class="bi bi-three-dots-vertical"></i>
|
<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>
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="contador-relatorios">
|
||||||
|
{relatoriosFinais.length} DE {relatoriosOriginais.length} RELATÓRIOS ENCONTRADOS
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{openDropdownId === l.id && (
|
<div className="table-responsive">
|
||||||
<div className="dropdown" data-laudo-dropdown={l.id}>
|
<table className="table table-striped table-hover">
|
||||||
<div className="item" onClick={() => handleOpenViewer(l)}>Editar</div>
|
<thead>
|
||||||
<div className="item" onClick={() => handlePrint(l)}>Imprimir</div>
|
<tr>
|
||||||
<div className="item" onClick={() => { alert("Protocolo de entrega: formulário (não implementado)."); setOpenDropdownId(null); }}>Protocolo de entrega</div>
|
<th>Paciente</th>
|
||||||
<div className="item" onClick={() => { alert("Liberar laudo: requer permissão de médico. (não implementado)"); setOpenDropdownId(null); }}>Liberar laudo</div>
|
<th>CPF</th>
|
||||||
<div className="item" onClick={() => handleRequestDelete(l)} style={{ color:"#c23b3b" }}>Excluir laudo</div>
|
<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>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Viewer modal (modo leitura) — só abre para quem tem permissão */}
|
{/* Modal principal (detalhes) */}
|
||||||
{viewerLaudo && !showPreview && !isSecretary && (
|
{showModal && relatorioModal && (
|
||||||
<div className="viewer-modal" style={{ pointerEvents:"auto" }}>
|
<div className="modal modal-centered" role="dialog" aria-modal="true" onClick={() => setShowModal(false)}>
|
||||||
<div className="modal-backdrop" onClick={() => setViewerLaudo(null)} />
|
<div className="modal-dialog modal-dialog-square" role="document" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="modal-card" role="dialog" aria-modal="true">
|
<div className="modal-content">
|
||||||
<div className="viewer-header">
|
<div className="modal-header custom-modal-header">
|
||||||
<div>
|
<h5 className="modal-title">Relatório de {pacientesComRelatorios[modalIndex]?.full_name || relatorioModal.patient_name || 'Paciente'}</h5>
|
||||||
<div style={{ fontSize:18, fontWeight:700 }}>{viewerLaudo.paciente.nome}</div>
|
<button type="button" className="btn-close modal-close-btn" aria-label="Close" onClick={() => setShowModal(false)}></button>
|
||||||
<div className="patient-info">
|
</div>
|
||||||
Nasc.: {viewerLaudo.paciente.nascimento} • {computeAge(viewerLaudo.paciente.nascimento)} anos • {viewerLaudo.paciente.cpf} • {viewerLaudo.paciente.convenio}
|
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<div style={{ display:"flex", gap:8 }}>
|
<div className="modal-footer custom-modal-footer">
|
||||||
<button className="tool-btn" onClick={() => { setPreviewLaudo(viewerLaudo); setShowPreview(true); setViewerLaudo(null); }}>Pré-visualizar / Imprimir</button>
|
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(pacientesComRelatorios[modalIndex]?.full_name || 'paciente', modalIndex)}>
|
||||||
<button className="tool-btn" onClick={() => setViewerLaudo(null)}>Fechar</button>
|
<i className='bi bi-file-pdf-fill'></i> baixar em pdf
|
||||||
</div>
|
</button>
|
||||||
</div>
|
<button type="button" className="btn btn-outline-secondary" onClick={() => { setShowModal(false) }}>
|
||||||
|
Fechar
|
||||||
<div className="toolbar">
|
</button>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Preview modal — agora não bloqueia a tela (sem backdrop escuro), botão imprimir é interativo */}
|
{/* Modal Protocolo */}
|
||||||
{showPreview && previewLaudo && (
|
{showProtocolModal && protocolForIndex && (
|
||||||
<div className="preview-modal" style={{ pointerEvents:"none" /* container não bloqueia */ }}>
|
<div className="modal modal-centered" role="dialog" aria-modal="true" onClick={() => setShowProtocolModal(false)}>
|
||||||
<div /* sem backdrop, assim não deixa a tela escura/blocked */ />
|
<div className="modal-dialog modal-dialog-tabela-relatorio" role="document" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="modal-card" style={{ maxWidth:900, pointerEvents:"auto" }}>
|
<div className="modal-content">
|
||||||
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:12 }}>
|
<div className="modal-header custom-modal-header">
|
||||||
<div style={{ fontWeight:700 }}>Pré-visualização - {previewLaudo.paciente.nome}</div>
|
<h5 className="modal-title">Protocolo de Entrega - {protocolForIndex.relatorio?.patient_name || 'Paciente'}</h5>
|
||||||
<div style={{ display:"flex", gap:8 }}>
|
<button type="button" className="btn-close modal-close-btn" aria-label="Close" onClick={() => setShowProtocolModal(false)}></button>
|
||||||
<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}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ whiteSpace:"pre-wrap", fontSize:15, color:"#1f2d3d", lineHeight:1.5 }}>
|
<div className="modal-body">
|
||||||
{previewLaudo.conteudo}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Notificação simples: Sem permissão (exibe sem backdrop escuro) - centralizada */}
|
{/* Variável de aviso: mostra texto personalizado */}
|
||||||
{showNoPermission && (
|
{noPermissionText && (
|
||||||
<div className="notice-card" role="alert" aria-live="polite">
|
<div className="modal modal-centered" role="dialog" aria-modal="true" onClick={() => setNoPermissionText(null)}>
|
||||||
<div style={{ fontWeight:700, marginBottom:6 }}>Sem permissão para editar</div>
|
<div className="modal-dialog modal-dialog-square" role="document" onClick={(e) => e.stopPropagation()}>
|
||||||
<div style={{ marginBottom:10, color:"#5a6f80" }}>Você está na visualização da secretaria. Edição disponível somente para médicos autorizados.</div>
|
<div className="modal-content">
|
||||||
<div style={{ textAlign:"right" }}>
|
<div style={{ padding: 18 }}>
|
||||||
<button className="tool-btn" onClick={() => setShowNoPermission(false)}>Fechar</button>
|
<h5 style={{ marginBottom: 8 }}>{noPermissionText}</h5>
|
||||||
</div>
|
<p style={{ color: '#6c757d' }}>{/* opcional descrição aqui */}</p>
|
||||||
</div>
|
<div style={{ textAlign: 'right', marginTop: 12 }}>
|
||||||
)}
|
<button className="btn btn-outline-secondary" onClick={() => setNoPermissionText(null)}>Fechar</button>
|
||||||
|
</div>
|
||||||
{/* Confirm delete modal (simples: Sim / Não) */}
|
</div>
|
||||||
{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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
/* ===== Helpers ===== */
|
export default LaudoManager;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,26 +1,152 @@
|
|||||||
//inicio.jsx
|
//inicio.jsx
|
||||||
//Nesta página falta: ajustar caminho do CSS
|
//Nesta página falta: ajustar caminho do CSS
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { FaUser, FaUserPlus, FaCalendarAlt, FaCalendarCheck } from 'react-icons/fa';
|
import { FaUser, FaUserPlus, FaCalendarAlt, FaCalendarCheck } from 'react-icons/fa';
|
||||||
//import './style/Inicio.css';
|
import { useAuth } from '../../_assets/utils/AuthProvider';
|
||||||
|
import API_KEY from '../../_assets/utils/apiKeys';
|
||||||
|
// import './style/Inicio.css';
|
||||||
|
|
||||||
|
|
||||||
function Inicio() {
|
function Inicio() {
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { getAuthorizationHeader, isAuthenticated } = useAuth();
|
||||||
const [pacientes, setPacientes] = useState([]);
|
const [pacientes, setPacientes] = useState([]);
|
||||||
|
const [medicos, setMedicos] = useState([]);
|
||||||
const [agendamentos, setAgendamentos] = useState([]);
|
const [agendamentos, setAgendamentos] = useState([]);
|
||||||
|
const [agendamentosComPacientes, setAgendamentosComPacientes] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPacientes = async () => {
|
||||||
|
try {
|
||||||
|
const authHeader = getAuthorizationHeader();
|
||||||
|
|
||||||
|
const myHeaders = new Headers();
|
||||||
|
myHeaders.append("apikey", API_KEY);
|
||||||
|
myHeaders.append("Authorization", authHeader);
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: myHeaders,
|
||||||
|
redirect: 'follow'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setPacientes(data);
|
||||||
|
console.log('Pacientes carregados:', data.length);
|
||||||
|
} else {
|
||||||
|
console.error(' Erro ao buscar pacientes:', response.status);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(' Erro ao buscar pacientes:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchMedicos = async () => {
|
||||||
|
try {
|
||||||
|
const authHeader = getAuthorizationHeader();
|
||||||
|
|
||||||
|
const myHeaders = new Headers();
|
||||||
|
myHeaders.append("apikey", API_KEY);
|
||||||
|
myHeaders.append("Authorization", authHeader);
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: myHeaders,
|
||||||
|
redirect: 'follow'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setMedicos(data);
|
||||||
|
console.log(' Médicos carregados:', data.length);
|
||||||
|
} else {
|
||||||
|
console.error('Erro ao buscar médicos:', response.status);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(' Erro ao buscar médicos:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAgendamentos = async () => {
|
||||||
|
try {
|
||||||
|
const authHeader = getAuthorizationHeader();
|
||||||
|
|
||||||
|
const myHeaders = new Headers();
|
||||||
|
myHeaders.append("apikey", API_KEY);
|
||||||
|
myHeaders.append("Authorization", authHeader);
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: myHeaders,
|
||||||
|
redirect: 'follow'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments", requestOptions);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setAgendamentos(data);
|
||||||
|
console.log(' Agendamentos carregados:', data.length);
|
||||||
|
} else {
|
||||||
|
console.error(' Erro ao buscar agendamentos:', response.status);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(' Erro ao buscar agendamentos:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isAuthenticated) {
|
||||||
|
fetchPacientes();
|
||||||
|
fetchMedicos();
|
||||||
|
fetchAgendamentos();
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, getAuthorizationHeader]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (agendamentos.length > 0 && pacientes.length > 0 && medicos.length > 0) {
|
||||||
|
const agendamentosComNomes = agendamentos.map(agendamento => {
|
||||||
|
const paciente = pacientes.find(p => p.id === agendamento.patient_id);
|
||||||
|
const medico = medicos.find(m => m.id === agendamento.doctor_id);
|
||||||
|
return {
|
||||||
|
...agendamento,
|
||||||
|
nomePaciente: paciente?.full_name || 'Paciente não encontrado',
|
||||||
|
nomeMedico: medico?.full_name || 'Médico não encontrado',
|
||||||
|
especialidadeMedico: medico?.specialty || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setAgendamentosComPacientes(agendamentosComNomes);
|
||||||
|
}
|
||||||
|
}, [agendamentos, pacientes, medicos]);
|
||||||
|
|
||||||
const totalPacientes = pacientes.length;
|
const totalPacientes = pacientes.length;
|
||||||
const novosEsseMes = pacientes.filter(p => p.createdAt && new Date(p.createdAt).getMonth() === new Date().getMonth()).length;
|
const novosEsseMes = pacientes.filter(p => p.created_at && new Date(p.created_at).getMonth() === new Date().getMonth()).length;
|
||||||
|
|
||||||
const hoje = new Date();
|
const hoje = new Date();
|
||||||
const agendamentosDoDia = agendamentos.filter(
|
hoje.setHours(0, 0, 0, 0);
|
||||||
a => a.data && new Date(a.data).getDate() === hoje.getDate()
|
|
||||||
);
|
const agendamentosDoDia = agendamentosComPacientes.filter(a => {
|
||||||
|
if (!a.scheduled_at) return false;
|
||||||
|
const dataAgendamento = new Date(a.scheduled_at);
|
||||||
|
dataAgendamento.setHours(0, 0, 0, 0);
|
||||||
|
return dataAgendamento.getTime() === hoje.getTime();
|
||||||
|
});
|
||||||
|
|
||||||
const agendamentosHoje = agendamentosDoDia.length;
|
const agendamentosHoje = agendamentosDoDia.length;
|
||||||
|
|
||||||
|
|
||||||
|
const pendencias = agendamentos.filter(a => a.status === 'pending' || a.status === 'scheduled').length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-container">
|
<div className="dashboard-container">
|
||||||
@ -58,7 +184,7 @@ function Inicio() {
|
|||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-info">
|
<div className="stat-info">
|
||||||
<span className="stat-label">PENDÊNCIAS</span>
|
<span className="stat-label">PENDÊNCIAS</span>
|
||||||
<span className="stat-value">0</span>
|
<span className="stat-value">{loading ? '...' : pendencias}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-icon-wrapper orange"><FaCalendarAlt className="stat-icon" /></div>
|
<div className="stat-icon-wrapper orange"><FaCalendarAlt className="stat-icon" /></div>
|
||||||
</div>
|
</div>
|
||||||
@ -93,14 +219,54 @@ function Inicio() {
|
|||||||
|
|
||||||
<div className="appointments-section">
|
<div className="appointments-section">
|
||||||
<h2>Próximos Agendamentos</h2>
|
<h2>Próximos Agendamentos</h2>
|
||||||
{agendamentosHoje > 0 ? (
|
{loading ? (
|
||||||
<div>
|
<div className="no-appointments-content">
|
||||||
{agendamentosDoDia.map(agendamento => (
|
<p>Carregando agendamentos...</p>
|
||||||
|
</div>
|
||||||
|
) : agendamentosHoje > 0 ? (
|
||||||
|
<div className="agendamentos-list">
|
||||||
|
{agendamentosDoDia.slice(0, 5).map(agendamento => (
|
||||||
<div key={agendamento.id} className="agendamento-item">
|
<div key={agendamento.id} className="agendamento-item">
|
||||||
<p>{agendamento.nomePaciente}</p>
|
<div className="agendamento-info">
|
||||||
<p>{new Date(agendamento.data).toLocaleTimeString()}</p>
|
<div className="agendamento-time-date">
|
||||||
|
<p className="agendamento-hora">
|
||||||
|
{new Date(agendamento.scheduled_at).toLocaleTimeString('pt-BR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<p className="agendamento-data">
|
||||||
|
{new Date(agendamento.scheduled_at).toLocaleDateString('pt-BR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="agendamento-detalhes">
|
||||||
|
<p className="agendamento-paciente">
|
||||||
|
<strong>Paciente:</strong> {agendamento.nomePaciente}
|
||||||
|
</p>
|
||||||
|
<p className="agendamento-medico">
|
||||||
|
<strong>Dr(a):</strong> {agendamento.nomeMedico}
|
||||||
|
{agendamento.especialidadeMedico && ` - ${agendamento.especialidadeMedico}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className={`agendamento-status status-${agendamento.status}`}>
|
||||||
|
{agendamento.status === 'scheduled' ? 'Agendado' :
|
||||||
|
agendamento.status === 'completed' ? 'Concluído' :
|
||||||
|
agendamento.status === 'pending' ? 'Pendente' :
|
||||||
|
agendamento.status === 'cancelled' ? 'Cancelado' :
|
||||||
|
agendamento.status === 'requested' ? '' : agendamento.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{agendamentosHoje > 5 && (
|
||||||
|
<button className="view-all-button" onClick={() => navigate('/secretaria/agendamento')}>
|
||||||
|
Ver todos os {agendamentosHoje} agendamentos
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="no-appointments-content">
|
<div className="no-appointments-content">
|
||||||
|
|||||||
@ -1,47 +1,33 @@
|
|||||||
//DoctorDetails.jsx
|
//DoctorDetails.jsx
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams,Link, useNavigate, useLocation } from "react-router-dom";
|
import { useParams, Link, useNavigate, useLocation } from "react-router-dom";
|
||||||
import { GetDoctorByID } from "../../_assets/utils/Functions-Endpoints/Doctor";
|
import { GetDoctorByID } from "../../_assets/utils/Functions-Endpoints/Doctor";
|
||||||
import { useAuth } from "../../_assets/utils/AuthProvider";
|
import { useAuth } from "../../_assets/utils/AuthProvider";
|
||||||
|
|
||||||
import avatarPlaceholder from '../../_assets/images/avatar_placeholder.png';
|
import avatarPlaceholder from '../../_assets/images/avatar_placeholder.png';
|
||||||
|
|
||||||
const Details = () => {
|
const DoctorDetails = ({DictInfo}) => {
|
||||||
const {getAuthorizationHeader} = useAuth();
|
const {getAuthorizationHeader} = useAuth();
|
||||||
const [doctor, setDoctor] = useState({});
|
|
||||||
const Parametros = useParams()
|
const Parametros = useParams()
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const Voltar = () => {
|
const navigateEdit = () => {
|
||||||
|
const prefixo = location.pathname.split("/")[1];
|
||||||
|
navigate(`/${prefixo}/medicos/edit`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Voltar = () => {
|
||||||
const prefixo = location.pathname.split("/")[1];
|
const prefixo = location.pathname.split("/")[1];
|
||||||
navigate(`/${prefixo}/medicos`);
|
navigate(`/${prefixo}/medicos`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const doctorID = Parametros.id
|
|
||||||
useEffect(() => {
|
|
||||||
if (!doctorID) return;
|
|
||||||
|
|
||||||
const authHeader = getAuthorizationHeader()
|
|
||||||
|
|
||||||
GetDoctorByID(doctorID, authHeader)
|
|
||||||
.then((data) => {
|
|
||||||
console.log(data, "médico vindo da API");
|
|
||||||
setDoctor(data[0])
|
|
||||||
; // supabase retorna array
|
|
||||||
})
|
|
||||||
.catch((err) => console.error("Erro ao buscar paciente:", err));
|
|
||||||
|
|
||||||
|
|
||||||
}, [doctorID]);
|
|
||||||
|
|
||||||
//if (!doctor) return <p style={{ textAlign: "center" }}>Carregando...</p>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="card p-3 shadow-sm">
|
<div className="card p-3 shadow-sm">
|
||||||
<h3 className="mb-3 text-center">MediConnect</h3>
|
<h3 className="mb-3 text-center">MediConnect</h3>
|
||||||
|
<hr />
|
||||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||||
<button className="btn btn-success me-2" onClick={() => Voltar()}>
|
<button className="btn btn-success me-2" onClick={() => Voltar()}>
|
||||||
<i className="bi bi-chevron-left"></i> Voltar
|
<i className="bi bi-chevron-left"></i> Voltar
|
||||||
@ -52,15 +38,13 @@ const Details = () => {
|
|||||||
<img src={avatarPlaceholder} alt="" />
|
<img src={avatarPlaceholder} alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div className="media-body ms-3 font-extrabold">
|
<div className="media-body ms-3 font-extrabold">
|
||||||
<span>{doctor.full_name || "Nome Completo"}</span>
|
<span>{DictInfo.full_name || "Nome Completo"}</span>
|
||||||
<p>{doctor.cpf || "CPF"}</p>
|
<p>{DictInfo.cpf || "CPF"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link to={`edit`}>
|
<button className="btn btn-light" onClick={() => {navigateEdit()}} >
|
||||||
<button className="btn btn-light" onClick={() => {console.log(doctor.id)}} >
|
|
||||||
<i className="bi bi-pencil-square"></i> Editar
|
<i className="bi bi-pencil-square"></i> Editar
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -71,29 +55,29 @@ const Details = () => {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-md-6 mb-3">
|
||||||
<label className="font-extrabold">Nome:</label>
|
<label className="font-extrabold">Nome:</label>
|
||||||
<p>{doctor.full_name || "-"}</p>
|
<p>{DictInfo.full_name || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-md-6 mb-3">
|
||||||
<label className="font-extrabold">Data de nascimento:</label>
|
<label className="font-extrabold">Data de nascimento:</label>
|
||||||
<p>{doctor.birth_date || "-"}</p>
|
<p>{DictInfo.birth_date || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-md-6 mb-3">
|
||||||
<label className="font-extrabold">CPF:</label>
|
<label className="font-extrabold">CPF:</label>
|
||||||
<p>{doctor.cpf || "-"}</p>
|
<p>{DictInfo.cpf || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-md-6 mb-3">
|
||||||
<label className="font-extrabold">CRM:</label>
|
<label className="font-extrabold">CRM:</label>
|
||||||
<p>{doctor.crm || "-"}</p>
|
<p>{DictInfo.crm || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-md-6 mb-3">
|
||||||
<label className="font-extrabold">Estado do CRM:</label>
|
<label className="font-extrabold">Estado do CRM:</label>
|
||||||
<p>{doctor.crm_uf || "-"}</p>
|
<p>{DictInfo.crm_uf || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-md-6 mb-3">
|
||||||
<label className="font-extrabold">Especialização:</label>
|
<label className="font-extrabold">Especialização:</label>
|
||||||
<p>{doctor.specialty || "-"}</p>
|
<p>{DictInfo.specialty || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -105,31 +89,31 @@ const Details = () => {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-4 mb-3">
|
<div className="col-md-4 mb-3">
|
||||||
<label className="font-extrabold">CEP:</label>
|
<label className="font-extrabold">CEP:</label>
|
||||||
<p>{doctor.cep || "-"}</p>
|
<p>{DictInfo.cep || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-8 mb-3">
|
<div className="col-md-8 mb-3">
|
||||||
<label className="font-extrabold">Rua:</label>
|
<label className="font-extrabold">Rua:</label>
|
||||||
<p>{doctor.street || "-"}</p>
|
<p>{DictInfo.street || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-4 mb-3">
|
<div className="col-md-4 mb-3">
|
||||||
<label className="font-extrabold">Bairro:</label>
|
<label className="font-extrabold">Bairro:</label>
|
||||||
<p>{doctor.neighborhood || "-"}</p>
|
<p>{DictInfo.neighborhood || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-4 mb-3">
|
<div className="col-md-4 mb-3">
|
||||||
<label className="font-extrabold">Cidade:</label>
|
<label className="font-extrabold">Cidade:</label>
|
||||||
<p>{doctor.city || "-"}</p>
|
<p>{DictInfo.city || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-2 mb-3">
|
<div className="col-md-2 mb-3">
|
||||||
<label className="font-extrabold">Estado:</label>
|
<label className="font-extrabold">Estado:</label>
|
||||||
<p>{doctor.state || "-"}</p>
|
<p>{DictInfo.state || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-4 mb-3">
|
<div className="col-md-4 mb-3">
|
||||||
<label className="font-extrabold">Número:</label>
|
<label className="font-extrabold">Número:</label>
|
||||||
<p>{doctor.number || "-"}</p>
|
<p>{DictInfo.number || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-8 mb-3">
|
<div className="col-md-8 mb-3">
|
||||||
<label className="font-extrabold">Complemento:</label>
|
<label className="font-extrabold">Complemento:</label>
|
||||||
<p>{doctor.complement || "-"}</p>
|
<p>{DictInfo.complement || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -141,15 +125,15 @@ const Details = () => {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-md-6 mb-3">
|
||||||
<label className="font-extrabold">Email:</label>
|
<label className="font-extrabold">Email:</label>
|
||||||
<p>{doctor.email || "-"}</p>
|
<p>{DictInfo.email || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-md-6 mb-3">
|
||||||
<label className="font-extrabold">Telefone:</label>
|
<label className="font-extrabold">Telefone:</label>
|
||||||
<p>{doctor.phone_mobile || "-"}</p>
|
<p>{DictInfo.phone_mobile || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-md-6 mb-3">
|
||||||
<label className="font-extrabold">Telefone 2:</label>
|
<label className="font-extrabold">Telefone 2:</label>
|
||||||
<p>{doctor.phone2 || "-"}</p>
|
<p>{DictInfo.phone2 || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -158,4 +142,4 @@ const Details = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Details;
|
export default DoctorDetails;
|
||||||
@ -2,16 +2,18 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams, useNavigate, useLocation, Link } from "react-router-dom";
|
import { useParams, useNavigate, useLocation, Link } from "react-router-dom";
|
||||||
import {GetPatientByID} from "../../_assets/utils/Functions-Endpoints/Patient"
|
import { GetPatientByID } from "../../_assets/utils/Functions-Endpoints/Patient"
|
||||||
import { useAuth } from "../../_assets/utils/AuthProvider";
|
import { useAuth } from "../../_assets/utils/AuthProvider";
|
||||||
|
|
||||||
import avatarPlaceholder from '../../_assets/images/avatar_placeholder.png';
|
import avatarPlaceholder from '../../_assets/images/avatar_placeholder.png';
|
||||||
|
|
||||||
|
|
||||||
const Details = (DictInfo) => {
|
const Details = (DictInfo) => {
|
||||||
const parametros = useParams();
|
const parametros = useParams();
|
||||||
const {getAuthorizationHeader, isAuthenticated} = useAuth();
|
const {getAuthorizationHeader, isAuthenticated} = useAuth();
|
||||||
const [paciente, setPaciente] = useState({});
|
const [paciente, setPaciente] = useState({});
|
||||||
const [anexos, setAnexos] = useState([]);
|
const [anexos, setAnexos] = useState([]);
|
||||||
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const patientID = parametros.id
|
const patientID = parametros.id
|
||||||
@ -21,6 +23,16 @@ const Details = (DictInfo) => {
|
|||||||
navigate(`/${prefixo}/pacientes`);
|
navigate(`/${prefixo}/pacientes`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const navigateEdit = () => {
|
||||||
|
const prefixo = location.pathname.split("/")[1];
|
||||||
|
navigate(`/${prefixo}/medicos/edit`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!DictInfo) return;
|
if (!DictInfo) return;
|
||||||
console.log(patientID, 'teu id')
|
console.log(patientID, 'teu id')
|
||||||
@ -62,6 +74,7 @@ const Details = (DictInfo) => {
|
|||||||
<>
|
<>
|
||||||
<div className="card p-3 shadow-sm">
|
<div className="card p-3 shadow-sm">
|
||||||
<h3 className="mb-3 text-center">MediConnect</h3>
|
<h3 className="mb-3 text-center">MediConnect</h3>
|
||||||
|
<hr />
|
||||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||||
<button className="btn btn-success me-2" onClick={() => Voltar()}>
|
<button className="btn btn-success me-2" onClick={() => Voltar()}>
|
||||||
<i className="bi bi-chevron-left"></i> Voltar
|
<i className="bi bi-chevron-left"></i> Voltar
|
||||||
@ -80,11 +93,9 @@ const Details = (DictInfo) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to={`edit`}>
|
<button className="btn btn-light" onClick={() => navigateEdit()} >
|
||||||
<button className="btn btn-light" >
|
|
||||||
<i className="bi bi-pencil-square"></i> Editar
|
<i className="bi bi-pencil-square"></i> Editar
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -283,4 +294,4 @@ const Details = (DictInfo) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Details;
|
export default Details;
|
||||||
@ -1,359 +1,440 @@
|
|||||||
//DisponibilidadesDoctorPage.jsx
|
//DisponibilidadesDoctorPage.jsx
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { GetAllDoctors } from "../../_assets/utils/Functions-Endpoints/Doctor";
|
|
||||||
import { useAuth } from "../../_assets/utils/AuthProvider";
|
import { useAuth } from "../../_assets/utils/AuthProvider";
|
||||||
import API_KEY from "../../_assets/utils/apiKeys";
|
import API_KEY from "../../_assets/utils/apiKeys";
|
||||||
|
|
||||||
import HorariosDisponibilidade from "../../components/medico/HorariosDisponibilidade";
|
import HorariosDisponibilidade from "../../components/medico/HorariosDisponibilidade";
|
||||||
|
// import "./style/DisponibilidadesDoctorPage.css";
|
||||||
|
|
||||||
const ENDPOINT =
|
const ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
|
||||||
"https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
|
const DOCTORS_ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors";
|
||||||
|
|
||||||
const diasDaSemana = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"];
|
const diasDaSemana = [
|
||||||
|
"Domingo",
|
||||||
|
"Segunda",
|
||||||
|
"Terça",
|
||||||
|
"Quarta",
|
||||||
|
"Quinta",
|
||||||
|
"Sexta",
|
||||||
|
"Sábado"
|
||||||
|
];
|
||||||
|
const weekdayNumToStr = {
|
||||||
|
0: "sunday",
|
||||||
|
1: "monday",
|
||||||
|
2: "tuesday",
|
||||||
|
3: "wednesday",
|
||||||
|
4: "thursday",
|
||||||
|
5: "friday",
|
||||||
|
6: "saturday",
|
||||||
|
};
|
||||||
|
const weekdayStrToNum = Object.fromEntries(
|
||||||
|
Object.entries(weekdayNumToStr).map(([num, str]) => [str, Number(num)])
|
||||||
|
);
|
||||||
|
|
||||||
const DisponibilidadesDoctorPage = () => {
|
const DisponibilidadesDoctorPage = () => {
|
||||||
const { getAuthorizationHeader } = useAuth();
|
const { getAuthorizationHeader } = useAuth();
|
||||||
const [disponibilidades, setDisponibilidades] = useState([]);
|
const [disponibilidades, setDisponibilidades] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [doctors, setDoctors] = useState([]);
|
const [doctors, setDoctors] = useState([]);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [selectedDoctor, setSelectedDoctor] = useState(null);
|
|
||||||
const [editando, setEditando] = useState(null);
|
const [editando, setEditando] = useState(null);
|
||||||
const [doctorsLoading, setDoctorsLoading] = useState(true);
|
const [expandedDoctors, setExpandedDoctors] = useState({});
|
||||||
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
|
const [availabilityEdit, setAvailabilityEdit] = useState([]);
|
||||||
|
|
||||||
|
const getHeaders = () => {
|
||||||
|
const myHeaders = new Headers();
|
||||||
|
const authHeader = getAuthorizationHeader();
|
||||||
|
if (authHeader) myHeaders.append("Authorization", authHeader);
|
||||||
|
myHeaders.append("Content-Type", "application/json");
|
||||||
|
if (API_KEY) myHeaders.append("apikey", API_KEY);
|
||||||
|
myHeaders.append("Prefer", "return=representation");
|
||||||
|
return myHeaders;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDoctors = async () => {
|
const fetchDoctors = async () => {
|
||||||
try {
|
try {
|
||||||
setDoctorsLoading(true);
|
const requestOptions = {
|
||||||
const data = await GetAllDoctors();
|
method: "GET",
|
||||||
console.log("Médicos recebidos:", data);
|
headers: getHeaders(),
|
||||||
setDoctors(Array.isArray(data) ? data : []);
|
};
|
||||||
|
const response = await fetch(DOCTORS_ENDPOINT, requestOptions);
|
||||||
|
const result = await response.json();
|
||||||
|
setDoctors(Array.isArray(result) ? result : []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao carregar médicos:", error);
|
|
||||||
setDoctors([]);
|
setDoctors([]);
|
||||||
} finally {
|
|
||||||
setDoctorsLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchDoctors();
|
fetchDoctors();
|
||||||
}, []);
|
}, [getAuthorizationHeader]);
|
||||||
|
|
||||||
const resolveAuthHeader = () => {
|
|
||||||
try {
|
|
||||||
const h = getAuthorizationHeader();
|
|
||||||
return h || "";
|
|
||||||
} catch {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHeaders = () => {
|
|
||||||
const myHeaders = new Headers();
|
|
||||||
const authHeader = resolveAuthHeader();
|
|
||||||
if (authHeader) myHeaders.append("Authorization", authHeader);
|
|
||||||
myHeaders.append("Content-Type", "application/json");
|
|
||||||
if (API_KEY) myHeaders.append("apikey", API_KEY);
|
|
||||||
return myHeaders;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchDisponibilidades = useCallback(async (doctorId = null) => {
|
|
||||||
setLoading(true);
|
|
||||||
let url = ENDPOINT;
|
|
||||||
if (doctorId) {
|
|
||||||
url += `?doctor_id=eq.${doctorId}&select=*&order=weekday.asc,start_time.asc`;
|
|
||||||
} else {
|
|
||||||
url += `?select=*&order=doctor_id.asc,weekday.asc,start_time.asc`;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, { method: "GET", headers: getHeaders() });
|
|
||||||
if (!res.ok) throw new Error(`Erro HTTP: ${res.status}`);
|
|
||||||
const data = await res.json();
|
|
||||||
setDisponibilidades(Array.isArray(data) ? data : []);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Erro ao buscar disponibilidades:", e);
|
|
||||||
alert("Erro ao carregar disponibilidades");
|
|
||||||
setDisponibilidades([]);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedDoctor) {
|
const fetchDisponibilidades = async () => {
|
||||||
fetchDisponibilidades(selectedDoctor.id);
|
try {
|
||||||
} else {
|
const res = await fetch(ENDPOINT, { method: "GET", headers: getHeaders() });
|
||||||
fetchDisponibilidades(null);
|
if (res.ok) {
|
||||||
}
|
const data = await res.json();
|
||||||
}, [selectedDoctor, fetchDisponibilidades]);
|
setDisponibilidades(Array.isArray(data) ? data : []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setDisponibilidades([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchDisponibilidades();
|
||||||
|
}, [getAuthorizationHeader]);
|
||||||
|
|
||||||
const atualizarDisponibilidade = async (id, dadosAtualizados) => {
|
const toggleExpandDoctor = (doctorId) => {
|
||||||
|
setExpandedDoctors((prev) => ({ ...prev, [doctorId]: !prev[doctorId] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const salvarTodasDisponibilidades = async (doctorId, horariosAtualizados) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${ENDPOINT}?id=eq.${id}`, {
|
const headers = getHeaders();
|
||||||
method: "PATCH",
|
const promises = [];
|
||||||
headers: getHeaders(),
|
const currentIds = new Set();
|
||||||
body: JSON.stringify(dadosAtualizados),
|
|
||||||
|
for (const dia of horariosAtualizados) {
|
||||||
|
if (dia.isChecked && dia.blocos.length > 0) {
|
||||||
|
for (const bloco of dia.blocos) {
|
||||||
|
const inicio = bloco.inicio.includes(":") ? bloco.inicio : bloco.inicio + ":00";
|
||||||
|
const termino = bloco.termino.includes(":") ? bloco.termino : bloco.termino + ":00";
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
doctor_id: doctorId,
|
||||||
|
weekday: weekdayNumToStr[dia.weekday],
|
||||||
|
start_time: inicio,
|
||||||
|
end_time: termino,
|
||||||
|
slot_minutes: bloco.slot_minutes || 30,
|
||||||
|
appointment_type: bloco.appointment_type || "presencial",
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bloco.id && !bloco.isNew) {
|
||||||
|
currentIds.add(bloco.id);
|
||||||
|
promises.push(
|
||||||
|
fetch(`${ENDPOINT}?id=eq.${bloco.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}).then(() => ({ type: 'PATCH', id: bloco.id }))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
promises.push(
|
||||||
|
fetch(ENDPOINT, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
const createdItem = Array.isArray(data) ? data[0] : data;
|
||||||
|
return { type: 'POST', id: createdItem?.id };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
|
results.forEach(res => {
|
||||||
|
if (res.type === 'POST' && res.id) currentIds.add(res.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const existingRes = await fetch(`${ENDPOINT}?doctor_id=eq.${String(doctorId)}`, {
|
||||||
|
method: "GET", headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingRes.ok) {
|
||||||
|
const existingData = await existingRes.json();
|
||||||
|
const deletePromises = existingData
|
||||||
|
.filter(dbItem => !currentIds.has(dbItem.id))
|
||||||
|
.map(dbItem =>
|
||||||
|
fetch(`${ENDPOINT}?id=eq.${dbItem.id}`, { method: "DELETE", headers })
|
||||||
|
);
|
||||||
|
await Promise.all(deletePromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditando(null);
|
||||||
|
setAvailabilityEdit([]);
|
||||||
|
const res = await fetch(ENDPOINT, { method: "GET", headers: getHeaders() });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
alert("Disponibilidade atualizada com sucesso!");
|
const data = await res.json();
|
||||||
setEditando(null);
|
setDisponibilidades(Array.isArray(data) ? data : []);
|
||||||
if (selectedDoctor) fetchDisponibilidades(selectedDoctor.id);
|
|
||||||
else fetchDisponibilidades();
|
|
||||||
} else {
|
|
||||||
const errorData = await res.json();
|
|
||||||
console.error("Erro na resposta:", errorData);
|
|
||||||
alert("Erro ao atualizar disponibilidade");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro:", error);
|
console.error(error);
|
||||||
alert("Falha ao conectar com o servidor");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletarDisponibilidade = async (id) => {
|
const deletarDisponibilidade = async (id) => {
|
||||||
if (!window.confirm("Deseja realmente excluir esta disponibilidade?"))
|
if (!window.confirm("Deseja realmente excluir esta disponibilidade?")) return;
|
||||||
return;
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${ENDPOINT}?id=eq.${id}`, {
|
const res = await fetch(`${ENDPOINT}?id=eq.${id}`, { method: "DELETE", headers: getHeaders() });
|
||||||
method: "DELETE",
|
if (res.ok) setDisponibilidades((prev) => prev.filter((d) => d.id !== id));
|
||||||
headers: getHeaders(),
|
} catch (error) {}
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
alert("Disponibilidade excluída com sucesso!");
|
|
||||||
setDisponibilidades((prev) => prev.filter((d) => d.id !== id));
|
|
||||||
} else {
|
|
||||||
const errorData = await res.json();
|
|
||||||
console.error("Erro na resposta:", errorData);
|
|
||||||
alert("Erro ao excluir disponibilidade");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro:", error);
|
|
||||||
alert("Erro ao conectar com o servidor");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialAvailabilityParaEdicao = useMemo(
|
const disponibilidadesAgrupadas = useMemo(() => {
|
||||||
() =>
|
const agrupadas = {};
|
||||||
diasDaSemana.map((dia, weekdayIndex) => {
|
doctors.forEach((doctor) => {
|
||||||
const blocosDoDia = disponibilidades
|
agrupadas[doctor.id] = {
|
||||||
.filter((d) => d.weekday === weekdayIndex && d.active !== false)
|
doctor_id: doctor.id,
|
||||||
.map((d) => ({
|
doctor_name: doctor.full_name || doctor.name,
|
||||||
id: d.id,
|
disponibilidades: [],
|
||||||
inicio: d.start_time ? d.start_time.substring(0, 5) : "07:00",
|
};
|
||||||
termino: d.end_time ? d.end_time.substring(0, 5) : "17:00",
|
});
|
||||||
isNew: false,
|
disponibilidades.forEach((disp) => {
|
||||||
slot_minutes: d.slot_minutes || 30,
|
if (agrupadas[disp.doctor_id]) agrupadas[disp.doctor_id].disponibilidades.push(disp);
|
||||||
appointment_type: d.appointment_type || "presencial",
|
});
|
||||||
active: d.active !== false,
|
Object.values(agrupadas).forEach((grupo) => {
|
||||||
}));
|
if (grupo.disponibilidades.length === 0) {
|
||||||
return {
|
grupo.disponibilidades.push({
|
||||||
dia,
|
id: `empty-${grupo.doctor_id}`,
|
||||||
weekday: weekdayIndex,
|
doctor_id: grupo.doctor_id,
|
||||||
isChecked: blocosDoDia.length > 0,
|
doctor_name: grupo.doctor_name,
|
||||||
blocos: blocosDoDia,
|
is_empty: true,
|
||||||
};
|
});
|
||||||
}),
|
}
|
||||||
[disponibilidades]
|
});
|
||||||
);
|
let resultado = Object.values(agrupadas);
|
||||||
|
if (searchTerm) resultado = resultado.filter((grupo) => grupo.doctor_name.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||||
|
return resultado;
|
||||||
|
}, [disponibilidades, doctors, searchTerm]);
|
||||||
|
|
||||||
|
const formatTime = (timeString) => {
|
||||||
|
if (!timeString) return "";
|
||||||
|
return timeString.includes(":") ? timeString.substring(0, 5) : timeString;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDiaSemana = (weekday) => {
|
||||||
|
const dias = {
|
||||||
|
0: "Domingo",
|
||||||
|
1: "Segunda",
|
||||||
|
2: "Terça",
|
||||||
|
3: "Quarta",
|
||||||
|
4: "Quinta",
|
||||||
|
5: "Sexta",
|
||||||
|
6: "Sábado",
|
||||||
|
sunday: "Domingo",
|
||||||
|
monday: "Segunda",
|
||||||
|
tuesday: "Terça",
|
||||||
|
wednesday: "Quarta",
|
||||||
|
thursday: "Quinta",
|
||||||
|
friday: "Sexta",
|
||||||
|
saturday: "Sábado",
|
||||||
|
};
|
||||||
|
const key = typeof weekday === "string" ? weekday.toLowerCase() : weekday;
|
||||||
|
return dias[key] || "Desconhecido";
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialAvailabilityParaEdicao = useMemo(() => {
|
||||||
|
if (!editando) return [];
|
||||||
|
const disponibilidadesMedico = disponibilidades.filter((d) => String(d.doctor_id) === String(editando));
|
||||||
|
const blocosPorDia = {};
|
||||||
|
disponibilidadesMedico.forEach((d) => {
|
||||||
|
const num = typeof d.weekday === "string" ? weekdayStrToNum[d.weekday.toLowerCase()] : d.weekday;
|
||||||
|
if (num === undefined) return;
|
||||||
|
if (!blocosPorDia[num]) blocosPorDia[num] = [];
|
||||||
|
if (d.active !== false) {
|
||||||
|
blocosPorDia[num].push({
|
||||||
|
id: d.id,
|
||||||
|
inicio: formatTime(d.start_time) || "07:00",
|
||||||
|
termino: formatTime(d.end_time) || "17:00",
|
||||||
|
slot_minutes: d.slot_minutes || 30,
|
||||||
|
appointment_type: d.appointment_type || "presencial",
|
||||||
|
isNew: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const resultado = [1, 2, 3, 4, 5, 6, 0].map((weekday) => {
|
||||||
|
const blocosDoDia = blocosPorDia[weekday] || [];
|
||||||
|
return {
|
||||||
|
dia: diasDaSemana[weekday],
|
||||||
|
weekday: weekday,
|
||||||
|
isChecked: blocosDoDia.length > 0,
|
||||||
|
blocos:
|
||||||
|
blocosDoDia.length > 0
|
||||||
|
? blocosDoDia
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
id: null,
|
||||||
|
inicio: "07:00",
|
||||||
|
termino: "17:00",
|
||||||
|
slot_minutes: 30,
|
||||||
|
appointment_type: "presencial",
|
||||||
|
isNew: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return resultado;
|
||||||
|
}, [disponibilidades, editando]);
|
||||||
|
|
||||||
const handleUpdateHorarios = (horariosAtualizados) => {
|
const handleUpdateHorarios = (horariosAtualizados) => {
|
||||||
const bloco = horariosAtualizados
|
if (!editando) return;
|
||||||
.flatMap((d) => d.blocos)
|
setAvailabilityEdit(horariosAtualizados || []);
|
||||||
.find((b) => b.id === editando);
|
|
||||||
if (!bloco) return alert("Bloco não encontrado.");
|
|
||||||
const dadosAtualizados = {
|
|
||||||
start_time: bloco.inicio + ":00",
|
|
||||||
end_time: bloco.termino + ":00",
|
|
||||||
slot_minutes: bloco.slot_minutes,
|
|
||||||
appointment_type: bloco.appointment_type,
|
|
||||||
active: bloco.active,
|
|
||||||
};
|
|
||||||
atualizarDisponibilidade(editando, dadosAtualizados);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredDoctors = useMemo(() => {
|
const filteredDoctors = useMemo(() => {
|
||||||
if (!searchTerm) return doctors;
|
if (!searchTerm) return doctors;
|
||||||
return doctors.filter((doc) =>
|
return doctors.filter((doc) => (doc.full_name || doc.name).toLowerCase().includes(searchTerm.toLowerCase()));
|
||||||
doc.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
);
|
|
||||||
}, [doctors, searchTerm]);
|
}, [doctors, searchTerm]);
|
||||||
|
|
||||||
return (
|
const handleCancelarEdicao = () => {
|
||||||
<div id="main-content">
|
setEditando(null);
|
||||||
<h1 style={{ fontSize: "1.5rem", fontWeight: "bold", color: "#333" }}>
|
setAvailabilityEdit([]);
|
||||||
Disponibilidades dos Médicos
|
};
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div style={{ marginTop: "10px", marginBottom: "10px" }}>
|
const handleDoctorSelect = (doctor) => {
|
||||||
<input
|
setSearchTerm(doctor.full_name || doctor.name);
|
||||||
type="text"
|
setShowSuggestions(false);
|
||||||
placeholder="Buscar médico por nome..."
|
};
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => {
|
const handleClearSearch = () => {
|
||||||
setSearchTerm(e.target.value);
|
setSearchTerm("");
|
||||||
setSelectedDoctor(null);
|
setShowSuggestions(false);
|
||||||
}}
|
};
|
||||||
style={{
|
|
||||||
border: "1px solid #ccc",
|
const getStatusBadgeClass = (disp) => {
|
||||||
borderRadius: "4px",
|
if (disp.is_empty) return "status-badge status-not-configured";
|
||||||
padding: "6px",
|
if (disp.active === false) return "status-badge status-inactive";
|
||||||
width: "300px",
|
return "status-badge status-active";
|
||||||
}}
|
};
|
||||||
/>
|
|
||||||
{searchTerm && (
|
const getStatusText = (disp) => {
|
||||||
<ul
|
if (disp.is_empty) return "Não configurado";
|
||||||
style={{
|
if (disp.active === false) return "Inativa";
|
||||||
border: "1px solid #ddd",
|
return "Ativa";
|
||||||
borderRadius: "4px",
|
};
|
||||||
backgroundColor: "white",
|
|
||||||
position: "absolute",
|
return (
|
||||||
zIndex: 10,
|
<div className="disponibilidades-container">
|
||||||
width: "300px",
|
<h1 className="disponibilidades-title">Disponibilidades dos Médicos</h1>
|
||||||
maxHeight: "150px",
|
|
||||||
overflowY: "auto",
|
<div className="search-container">
|
||||||
marginTop: "4px",
|
<div className="search-input-container">
|
||||||
listStyle: "none",
|
<input
|
||||||
padding: 0,
|
type="text"
|
||||||
|
placeholder="Buscar médico por nome..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchTerm(e.target.value);
|
||||||
|
setShowSuggestions(true);
|
||||||
}}
|
}}
|
||||||
>
|
onFocus={() => setShowSuggestions(true)}
|
||||||
{filteredDoctors.length > 0 ? (
|
className="search-input"
|
||||||
filteredDoctors.map((doc) => (
|
/>
|
||||||
<li
|
{searchTerm && (
|
||||||
key={doc.id}
|
<button onClick={handleClearSearch} className="clear-search-btn">
|
||||||
onClick={() => {
|
×
|
||||||
setSelectedDoctor(doc);
|
</button>
|
||||||
setSearchTerm(doc.name);
|
)}
|
||||||
}}
|
</div>
|
||||||
style={{
|
|
||||||
padding: "6px 8px",
|
{showSuggestions && searchTerm && filteredDoctors.length > 0 && (
|
||||||
cursor: "pointer",
|
<div className="suggestions-dropdown">
|
||||||
borderBottom: "1px solid #eee",
|
{filteredDoctors.map((doc) => (
|
||||||
}}
|
<div key={doc.id} onClick={() => handleDoctorSelect(doc)} className="suggestion-item">
|
||||||
>
|
{doc.full_name || doc.name}
|
||||||
{doc.name}
|
</div>
|
||||||
</li>
|
))}
|
||||||
))
|
</div>
|
||||||
) : (
|
|
||||||
<li style={{ padding: "6px 8px", color: "#888" }}>
|
|
||||||
Nenhum médico encontrado
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="calendario-ou-filaespera">
|
<section className="calendario-ou-filaespera">
|
||||||
<div className="fila-container">
|
<div className="fila-container">
|
||||||
<h2 className="fila-titulo">
|
<h2 className="section-title">{editando ? `Editar Horários` : "Lista de Disponibilidades"}</h2>
|
||||||
{editando ? "Editar Disponibilidade" : "Lista de Disponibilidades"}{" "}
|
|
||||||
({disponibilidades.length})
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{loading ? (
|
{doctors.length === 0 ? (
|
||||||
<p>Carregando...</p>
|
<p className="loading-text">Carregando médicos...</p>
|
||||||
) : disponibilidades.length === 0 ? (
|
|
||||||
<p>Nenhuma disponibilidade encontrada.</p>
|
|
||||||
) : editando ? (
|
) : editando ? (
|
||||||
<>
|
<>
|
||||||
<HorariosDisponibilidade
|
<div className="edit-container">
|
||||||
initialAvailability={initialAvailabilityParaEdicao}
|
{initialAvailabilityParaEdicao.length > 0 ? (
|
||||||
onUpdate={handleUpdateHorarios}
|
<HorariosDisponibilidade initialAvailability={initialAvailabilityParaEdicao} onUpdate={handleUpdateHorarios} onCancel={handleCancelarEdicao} />
|
||||||
/>
|
) : (
|
||||||
<button
|
<p className="loading-text">Carregando horários para edição...</p>
|
||||||
onClick={() =>
|
)}
|
||||||
handleUpdateHorarios(initialAvailabilityParaEdicao)
|
</div>
|
||||||
}
|
|
||||||
style={{
|
<div className="disp-buttons-container">
|
||||||
marginTop: "20px",
|
<button
|
||||||
padding: "10px 20px",
|
onClick={() =>
|
||||||
fontSize: "16px",
|
salvarTodasDisponibilidades(editando, availabilityEdit.length > 0 ? availabilityEdit : initialAvailabilityParaEdicao)
|
||||||
fontWeight: "bold",
|
}
|
||||||
borderRadius: "8px",
|
className="disp-btn-primary"
|
||||||
backgroundColor: "#3b82f6",
|
>
|
||||||
color: "white",
|
Salvar Alterações
|
||||||
border: "none",
|
</button>
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
<button onClick={handleCancelarEdicao} className="disp-btn-danger">
|
||||||
>
|
Cancelar
|
||||||
Salvar Alterações
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<table className="fila-tabela">
|
<div className="doctor-group-container">
|
||||||
<thead>
|
{disponibilidadesAgrupadas.length === 0 ? (
|
||||||
<tr>
|
<p className="no-results">Nenhum médico encontrado</p>
|
||||||
<th>Médico</th>
|
) : (
|
||||||
<th>Dia da Semana</th>
|
disponibilidadesAgrupadas.map((grupo) => (
|
||||||
<th>Início</th>
|
<div key={grupo.doctor_id} className={`doctor-group ${expandedDoctors[grupo.doctor_id] ? "expanded" : ""}`}>
|
||||||
<th>Término</th>
|
<div className="doctor-header" onClick={() => toggleExpandDoctor(grupo.doctor_id)}>
|
||||||
<th>Intervalo (min)</th>
|
<h3 className="doctor-name">
|
||||||
<th>Tipo</th>
|
{grupo.doctor_name}
|
||||||
<th>Status</th>
|
<span className="doctor-hours">({grupo.disponibilidades.filter((d) => !d.is_empty).length} horários)</span>
|
||||||
<th>Ações</th>
|
</h3>
|
||||||
</tr>
|
<span className={`expand-icon ${expandedDoctors[grupo.doctor_id] ? "expanded" : ""}`}>▼</span>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
|
||||||
{disponibilidades.map((disp) => {
|
|
||||||
const medico = doctors.find((d) => d.id === disp.doctor_id);
|
|
||||||
return (
|
|
||||||
<tr key={disp.id}>
|
|
||||||
<td>{medico ? medico.name : disp.doctor_id}</td>
|
|
||||||
<td>{diasDaSemana[disp.weekday]}</td>
|
|
||||||
<td>{disp.start_time}</td>
|
|
||||||
<td>{disp.end_time}</td>
|
|
||||||
<td>{disp.slot_minutes || 30}</td>
|
|
||||||
<td>{disp.appointment_type || "presencial"}</td>
|
|
||||||
<td>
|
|
||||||
<span
|
|
||||||
className={`badge ${
|
|
||||||
disp.active === false
|
|
||||||
? "badge-inactive"
|
|
||||||
: "badge-active"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{disp.active === false ? "Inativa" : "Ativa"}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div style={{ display: "flex", gap: "8px" }}>
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-edit"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#3b82f6",
|
|
||||||
color: "white",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "6px",
|
|
||||||
padding: "6px 10px",
|
|
||||||
cursor: "pointer",
|
|
||||||
fontWeight: "bold",
|
|
||||||
}}
|
|
||||||
onClick={() => console.log("Editar clicado")}
|
|
||||||
>
|
|
||||||
Editar
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
{expandedDoctors[grupo.doctor_id] && (
|
||||||
className="btn btn-sm btn-delete"
|
<div className="doctor-content">
|
||||||
style={{
|
<div className="edit-btn-container">
|
||||||
backgroundColor: "#ef4444",
|
<button onClick={() => setEditando(grupo.doctor_id)} className="disp-btn-edit">
|
||||||
color: "white",
|
{grupo.disponibilidades.some((d) => !d.is_empty) ? "Editar" : "Cadastrar Horários"}
|
||||||
border: "none",
|
</button>
|
||||||
borderRadius: "6px",
|
</div>
|
||||||
padding: "6px 10px",
|
|
||||||
cursor: "pointer",
|
<div className="table-container">
|
||||||
fontWeight: "bold",
|
<table className="disponibilidades-table">
|
||||||
}}
|
<thead>
|
||||||
onClick={() => console.log("Excluir clicado")}
|
<tr>
|
||||||
>
|
<th>Dia da Semana</th>
|
||||||
Excluir
|
<th>Início</th>
|
||||||
</button>
|
<th>Término</th>
|
||||||
|
<th>Intervalo (min)</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{grupo.disponibilidades.map((disp) => (
|
||||||
|
<tr key={disp.id}>
|
||||||
|
<td>{disp.is_empty ? "Nenhum horário cadastrado" : getDiaSemana(disp.weekday)}</td>
|
||||||
|
<td>{disp.is_empty ? "-" : formatTime(disp.start_time)}</td>
|
||||||
|
<td>{disp.is_empty ? "-" : formatTime(disp.end_time)}</td>
|
||||||
|
<td>{disp.is_empty ? "-" : disp.slot_minutes || 30}</td>
|
||||||
|
<td>{disp.is_empty ? "-" : disp.appointment_type || "presencial"}</td>
|
||||||
|
<td>
|
||||||
|
<span className={getStatusBadgeClass(disp)}>{getStatusText(disp)}</span>
|
||||||
|
</td>
|
||||||
|
<td>{!disp.is_empty && <button onClick={() => deletarDisponibilidade(disp.id)} className="disp-btn-delete">Excluir</button>}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
)}
|
||||||
</tr>
|
</div>
|
||||||
);
|
))
|
||||||
})}
|
)}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -361,4 +442,4 @@ const DisponibilidadesDoctorPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DisponibilidadesDoctorPage;
|
export default DisponibilidadesDoctorPage;
|
||||||
@ -1,150 +1,337 @@
|
|||||||
//DoctorEditPage.jsx
|
//DoctorEditPage.jsx
|
||||||
//Nesta página falta: mudar nomes
|
//Nesta página falta: mudar nomes
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { useParams, useSearchParams } from "react-router-dom";
|
import { useParams, useNavigate, useLocation } from "react-router-dom";
|
||||||
import { GetDoctorByID } from "../../_assets/utils/Functions-Endpoints/Doctor";
|
|
||||||
import { useAuth } from "../../_assets/utils/AuthProvider";
|
import { useAuth } from "../../_assets/utils/AuthProvider";
|
||||||
import API_KEY from "../../_assets/utils/apiKeys";
|
import API_KEY from "../../_assets/utils/apiKeys";
|
||||||
|
|
||||||
import DoctorForm from "../../components/medico/FormCadastroMedico";
|
import DoctorForm from "../../components/medico/FormCadastroMedico";
|
||||||
|
|
||||||
const ENDPOINT_AVAILABILITY =
|
const ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors";
|
||||||
"https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
|
const ENDPOINT_AVAILABILITY = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
|
||||||
|
|
||||||
const DoctorEditPage = () => {
|
const diasDaSemana = ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"];
|
||||||
|
const weekdayNumToStr = {
|
||||||
|
0: "sunday",
|
||||||
|
1: "monday",
|
||||||
|
2: "tuesday",
|
||||||
|
3: "wednesday",
|
||||||
|
4: "thursday",
|
||||||
|
5: "friday",
|
||||||
|
6: "saturday",
|
||||||
|
};
|
||||||
|
const weekdayStrToNum = Object.fromEntries(
|
||||||
|
Object.entries(weekdayNumToStr).map(([num, str]) => [str, Number(num)])
|
||||||
|
);
|
||||||
|
|
||||||
|
const EditDoctorPage = () => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const { getAuthorizationHeader } = useAuth();
|
const { getAuthorizationHeader } = useAuth();
|
||||||
const [DoctorToPUT, setDoctorPUT] = useState({});
|
|
||||||
|
const [doctor, setDoctor] = useState(null);
|
||||||
|
const [availability, setAvailability] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
const Parametros = useParams();
|
const effectiveId = id;
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const DoctorID = Parametros.id;
|
|
||||||
const availabilityId = searchParams.get("availabilityId");
|
|
||||||
|
|
||||||
const [availabilityToPATCH, setAvailabilityToPATCH] = useState(null);
|
const getHeaders = () => {
|
||||||
const [mode, setMode] = useState("doctor");
|
const myHeaders = new Headers();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const authHeader = getAuthorizationHeader();
|
const authHeader = getAuthorizationHeader();
|
||||||
|
if (authHeader) myHeaders.append("Authorization", authHeader);
|
||||||
if (availabilityId) {
|
|
||||||
setMode("availability");
|
|
||||||
|
|
||||||
fetch(`${ENDPOINT_AVAILABILITY}?id=eq.${availabilityId}&select=*`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
apikey: API_KEY,
|
|
||||||
Authorization: authHeader,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data && data.length > 0) {
|
|
||||||
setAvailabilityToPATCH(data[0]);
|
|
||||||
console.log("Disponibilidade vinda da API:", data[0]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => console.error("Erro ao buscar disponibilidade:", err));
|
|
||||||
} else {
|
|
||||||
setMode("doctor");
|
|
||||||
GetDoctorByID(DoctorID, authHeader)
|
|
||||||
.then((data) => {
|
|
||||||
console.log(data, "médico vindo da API");
|
|
||||||
setDoctorPUT(data[0]);
|
|
||||||
})
|
|
||||||
.catch((err) => console.error("Erro ao buscar paciente:", err));
|
|
||||||
}
|
|
||||||
}, [DoctorID, availabilityId, getAuthorizationHeader]);
|
|
||||||
|
|
||||||
const HandlePutDoctor = async () => {
|
|
||||||
const authHeader = getAuthorizationHeader();
|
|
||||||
|
|
||||||
var myHeaders = new Headers();
|
|
||||||
myHeaders.append("apikey", API_KEY);
|
|
||||||
myHeaders.append("Authorization", authHeader);
|
|
||||||
myHeaders.append("Content-Type", "application/json");
|
myHeaders.append("Content-Type", "application/json");
|
||||||
|
if (API_KEY) myHeaders.append("apikey", API_KEY);
|
||||||
|
myHeaders.append("Prefer", "return=representation");
|
||||||
|
return myHeaders;
|
||||||
|
};
|
||||||
|
|
||||||
var raw = JSON.stringify(DoctorToPUT);
|
const salvarDisponibilidades = async (doctorId, horariosAtualizados) => {
|
||||||
|
|
||||||
console.log("Enviando médico para atualização (PUT):", DoctorToPUT);
|
|
||||||
|
|
||||||
var requestOptions = {
|
|
||||||
method: "PUT",
|
|
||||||
headers: myHeaders,
|
|
||||||
body: raw,
|
|
||||||
redirect: "follow",
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const headers = getHeaders();
|
||||||
`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?id=eq.${DoctorID}`,
|
const promises = [];
|
||||||
requestOptions
|
const currentIds = new Set();
|
||||||
|
|
||||||
|
for (const dia of horariosAtualizados) {
|
||||||
|
if (dia.isChecked && dia.blocos.length > 0) {
|
||||||
|
for (const bloco of dia.blocos) {
|
||||||
|
const inicio = bloco.inicio.includes(":") ? bloco.inicio : bloco.inicio + ":00";
|
||||||
|
const termino = bloco.termino.includes(":") ? bloco.termino : bloco.termino + ":00";
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
doctor_id: doctorId,
|
||||||
|
weekday: weekdayNumToStr[dia.weekday],
|
||||||
|
start_time: inicio,
|
||||||
|
end_time: termino,
|
||||||
|
slot_minutes: bloco.slot_minutes || 30,
|
||||||
|
appointment_type: bloco.appointment_type || "presencial",
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bloco.id && !bloco.isNew) {
|
||||||
|
currentIds.add(bloco.id);
|
||||||
|
promises.push(
|
||||||
|
fetch(`${ENDPOINT_AVAILABILITY}?id=eq.${bloco.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}).then((res) => {
|
||||||
|
if (!res.ok) throw new Error(`Erro no PATCH: ${res.status}`);
|
||||||
|
return { type: "PATCH", id: bloco.id };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
promises.push(
|
||||||
|
fetch(ENDPOINT_AVAILABILITY, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
const createdItem = Array.isArray(data) ? data[0] : data;
|
||||||
|
if (createdItem && createdItem.id) {
|
||||||
|
return { type: "POST", id: createdItem.id };
|
||||||
|
}
|
||||||
|
return { type: "POST", id: null };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
|
results.forEach((res) => {
|
||||||
|
if (res.type === "POST" && res.id) currentIds.add(res.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingDisponibilidadesRes = await fetch(
|
||||||
|
`${ENDPOINT_AVAILABILITY}?doctor_id=eq.${String(doctorId)}`,
|
||||||
|
{ method: "GET", headers }
|
||||||
);
|
);
|
||||||
console.log("Resposta PUT Doutor:", response);
|
|
||||||
alert("Dados do médico atualizados com sucesso!");
|
if (existingDisponibilidadesRes.ok) {
|
||||||
|
const existingDisponibilidades = await existingDisponibilidadesRes.json();
|
||||||
|
|
||||||
|
const deletePromises = existingDisponibilidades
|
||||||
|
.filter((disp) => !currentIds.has(disp.id))
|
||||||
|
.map((disp) =>
|
||||||
|
fetch(`${ENDPOINT_AVAILABILITY}?id=eq.${disp.id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(deletePromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedResponse = await fetch(
|
||||||
|
`${ENDPOINT_AVAILABILITY}?doctor_id=eq.${doctorId}&order=weekday.asc,start_time.asc`,
|
||||||
|
{ method: "GET", headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updatedResponse.ok) {
|
||||||
|
const updatedData = await updatedResponse.json();
|
||||||
|
setAvailability(updatedData);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao atualizar médico:", error);
|
|
||||||
alert("Erro ao atualizar dados do médico.");
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Função para Atualizar DISPONIBILIDADE (PATCH)
|
const normalizeAvailabilityForForm = (availabilityData) => {
|
||||||
const HandlePatchAvailability = async (data) => {
|
if (!Array.isArray(availabilityData)) return [];
|
||||||
const authHeader = getAuthorizationHeader();
|
|
||||||
|
|
||||||
var myHeaders = new Headers();
|
const disponibilidadesMedico = availabilityData.filter((d) =>
|
||||||
myHeaders.append("apikey", API_KEY);
|
String(d.doctor_id) === String(effectiveId) && d.active !== false
|
||||||
myHeaders.append("Authorization", authHeader);
|
);
|
||||||
myHeaders.append("Content-Type", "application/json");
|
const blocosPorDia = {};
|
||||||
|
|
||||||
|
disponibilidadesMedico.forEach((d) => {
|
||||||
|
const num = typeof d.weekday === "string" ? weekdayStrToNum[d.weekday.toLowerCase()] : d.weekday;
|
||||||
|
if (num === undefined || num === null) return;
|
||||||
|
if (!blocosPorDia[num]) blocosPorDia[num] = [];
|
||||||
|
blocosPorDia[num].push({
|
||||||
|
id: d.id,
|
||||||
|
inicio: d.start_time?.substring(0, 5) || "07:00",
|
||||||
|
termino: d.end_time?.substring(0, 5) || "17:00",
|
||||||
|
slot_minutes: d.slot_minutes || 30,
|
||||||
|
appointment_type: d.appointment_type || "presencial",
|
||||||
|
isNew: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultado = [1, 2, 3, 4, 5, 6, 0].map((weekday) => {
|
||||||
|
const blocosDoDia = blocosPorDia[weekday] || [];
|
||||||
|
return {
|
||||||
|
dia: diasDaSemana[weekday],
|
||||||
|
weekday: weekday,
|
||||||
|
isChecked: blocosDoDia.length > 0,
|
||||||
|
blocos:
|
||||||
|
blocosDoDia.length > 0
|
||||||
|
? blocosDoDia
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
id: null,
|
||||||
|
inicio: "07:00",
|
||||||
|
termino: "17:00",
|
||||||
|
slot_minutes: 30,
|
||||||
|
appointment_type: "presencial",
|
||||||
|
isNew: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return resultado;
|
||||||
|
};
|
||||||
|
|
||||||
var raw = JSON.stringify(data);
|
const availabilityFormatted = useMemo(() => {
|
||||||
|
return normalizeAvailabilityForForm(availability);
|
||||||
|
}, [availability, effectiveId]);
|
||||||
|
|
||||||
console.log("Enviando disponibilidade para atualização (PATCH):", data);
|
useEffect(() => {
|
||||||
|
const fetchDoctorData = async () => {
|
||||||
|
if (!effectiveId || effectiveId === "edit") {
|
||||||
|
alert("ID do médico não encontrado");
|
||||||
|
navigate("/secretaria/medicos");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var requestOptions = {
|
try {
|
||||||
method: "PATCH",
|
const doctorResponse = await fetch(`${ENDPOINT}?id=eq.${effectiveId}`, {
|
||||||
headers: myHeaders,
|
method: "GET",
|
||||||
body: raw,
|
headers: getHeaders(),
|
||||||
redirect: "follow",
|
});
|
||||||
|
|
||||||
|
if (!doctorResponse.ok) {
|
||||||
|
throw new Error("Erro ao carregar dados do médico");
|
||||||
|
}
|
||||||
|
|
||||||
|
const doctorData = await doctorResponse.json();
|
||||||
|
if (doctorData.length === 0) {
|
||||||
|
throw new Error("Médico não encontrado");
|
||||||
|
}
|
||||||
|
|
||||||
|
setDoctor(doctorData[0]);
|
||||||
|
|
||||||
|
const availabilityResponse = await fetch(
|
||||||
|
`${ENDPOINT_AVAILABILITY}?doctor_id=eq.${effectiveId}&order=weekday.asc,start_time.asc`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: getHeaders(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (availabilityResponse.ok) {
|
||||||
|
const availabilityData = await availabilityResponse.json();
|
||||||
|
setAvailability(availabilityData);
|
||||||
|
} else {
|
||||||
|
setAvailability([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert("Erro ao carregar dados do médico");
|
||||||
|
navigate("/secretaria/medicos");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
if (effectiveId) {
|
||||||
const response = await fetch(
|
fetchDoctorData();
|
||||||
`${ENDPOINT_AVAILABILITY}?id=eq.${availabilityId}`,
|
|
||||||
requestOptions
|
|
||||||
);
|
|
||||||
console.log("Resposta PATCH Disponibilidade:", response);
|
|
||||||
alert("Disponibilidade atualizada com sucesso!");
|
|
||||||
// Opcional: Redirecionar de volta para a lista de disponibilidades
|
|
||||||
// navigate('/disponibilidades');
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro ao atualizar disponibilidade:", error);
|
|
||||||
alert("Erro ao atualizar disponibilidade.");
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
}, [effectiveId, navigate]);
|
||||||
|
|
||||||
|
const handleSave = async (formData) => {
|
||||||
|
const { availability: updatedAvailability, ...doctorDataToSave } = formData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
const response = await fetch(`${ENDPOINT}?id=eq.${effectiveId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: getHeaders(),
|
||||||
|
body: JSON.stringify(doctorDataToSave),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erro ao salvar dados do médico");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedAvailability && updatedAvailability.length > 0) {
|
||||||
|
await salvarDisponibilidades(effectiveId, updatedAvailability);
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("Médico e horários atualizados com sucesso!");
|
||||||
|
navigate("/secretaria/medicos");
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert(`Erro ao salvar dados: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
console.log('Horários a serem salvos:', updatedAvailability);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
navigate("/secretaria/medicos");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="container mt-4">
|
||||||
|
<div className="d-flex justify-content-center">
|
||||||
|
<div className="spinner-border" role="status">
|
||||||
|
<span className="visually-hidden">Carregando...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-center mt-2">
|
||||||
|
Carregando dados do médico ID: {effectiveId || "..."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doctor) {
|
||||||
|
if (!isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="container mt-4">
|
||||||
|
<div className="alert alert-danger">
|
||||||
|
Médico não encontrado
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
...doctor,
|
||||||
|
availability: (doctor && doctor.availability) ? doctor.availability : availabilityFormatted,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="container mt-4">
|
||||||
<h1 className="text-2xl font-bold mb-4">
|
<div className="row">
|
||||||
{mode === "availability"
|
<div className="col-12">
|
||||||
? `Editar Horário Disponível (ID: ${availabilityId.substring(0, 8)})`
|
<h1>Editar Médico</h1>
|
||||||
: `Editar Médico (ID: ${DoctorID})`}
|
<DoctorForm
|
||||||
</h1>
|
formData={formData}
|
||||||
|
setFormData={setDoctor}
|
||||||
<DoctorForm
|
onSave={handleSave}
|
||||||
onSave={
|
onCancel={handleCancel}
|
||||||
mode === "availability" ? HandlePatchAvailability : HandlePutDoctor
|
isLoading={isSaving}
|
||||||
}
|
isEditing={true}
|
||||||
formData={mode === "availability" ? availabilityToPATCH : DoctorToPUT}
|
/>
|
||||||
setFormData={
|
</div>
|
||||||
mode === "availability" ? setAvailabilityToPATCH : setDoctorPUT
|
</div>
|
||||||
}
|
|
||||||
isEditingAvailability={mode === "availability"}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DoctorEditPage;
|
export default EditDoctorPage;
|
||||||
@ -9,34 +9,16 @@ import API_KEY from '../../_assets/utils/apiKeys'
|
|||||||
|
|
||||||
import PatientForm from '../../components/paciente/FormCadastroPaciente'
|
import PatientForm from '../../components/paciente/FormCadastroPaciente'
|
||||||
|
|
||||||
|
const EditPage = ({DictInfo}) => {
|
||||||
const EditPage = (DictInfo) => {
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const Parametros = useParams()
|
|
||||||
const [PatientToPUT, setPatientPUT] = useState({})
|
const [PatientToPUT, setPatientPUT] = useState({})
|
||||||
|
|
||||||
const { getAuthorizationHeader, isAuthenticated } = useAuth();
|
const { getAuthorizationHeader, isAuthenticated } = useAuth();
|
||||||
console.log(DictInfo, "usuario vindo do set")
|
|
||||||
const PatientID = Parametros.id
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const authHeader = getAuthorizationHeader()
|
setPatientPUT(DictInfo)
|
||||||
console.log(DictInfo.DictInfo.id, "id do cabra")
|
|
||||||
|
|
||||||
GetPatientByID(DictInfo.DictInfo.id, authHeader)
|
|
||||||
.then((data) => {
|
|
||||||
console.log(data[0], "paciente vindo da API");
|
|
||||||
setPatientPUT(data[0]); // supabase retorna array
|
|
||||||
})
|
|
||||||
.catch((err) => console.error("Erro ao buscar paciente:", err));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}, [DictInfo])
|
}, [DictInfo])
|
||||||
|
|
||||||
|
|
||||||
const HandlePutPatient = async () => {
|
const HandlePutPatient = async () => {
|
||||||
const authHeader = getAuthorizationHeader()
|
const authHeader = getAuthorizationHeader()
|
||||||
|
|
||||||
@ -46,9 +28,9 @@ const HandlePutPatient = async () => {
|
|||||||
myHeaders.append("Authorization", authHeader);
|
myHeaders.append("Authorization", authHeader);
|
||||||
myHeaders.append("Content-Type", "application/json");
|
myHeaders.append("Content-Type", "application/json");
|
||||||
|
|
||||||
var raw = JSON.stringify(PatientToPUT);
|
var raw = JSON.stringify({...PatientToPUT, bmi:Number(PatientToPUT) || null});
|
||||||
|
|
||||||
console.log("Enviando paciente para atualização:", PatientToPUT);
|
console.log("Enviando atualização:", PatientToPUT);
|
||||||
|
|
||||||
var requestOptions = {
|
var requestOptions = {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@ -57,26 +39,11 @@ const HandlePutPatient = async () => {
|
|||||||
redirect: 'follow'
|
redirect: 'follow'
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients?id=eq.${PatientToPUT.id}`,requestOptions)
|
||||||
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients?id=eq.${PatientID}`,requestOptions);
|
.then(response => console.log(response))
|
||||||
console.log(response)
|
.then(result => console.log(result))
|
||||||
|
.catch(console.log("erro"))
|
||||||
|
|
||||||
if(response.ok === false){
|
|
||||||
console.error("Erro ao atualizar paciente:");
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
console.log("ATUALIZADO COM SUCESSO");
|
|
||||||
navigate('/secretaria/pacientes')
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro ao atualizar paciente:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -93,4 +60,4 @@ const HandlePutPatient = async () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditPage
|
export default EditPage
|
||||||
File diff suppressed because it is too large
Load Diff
@ -5,9 +5,10 @@ import { useState, useEffect } from "react";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useAuth } from "../../_assets/utils/AuthProvider";
|
import { useAuth } from "../../_assets/utils/AuthProvider";
|
||||||
import API_KEY from "../../_assets/utils/apiKeys";
|
import API_KEY from "../../_assets/utils/apiKeys";
|
||||||
|
|
||||||
//import "./style/TableDoctor.css";
|
//import "./style/TableDoctor.css";
|
||||||
|
|
||||||
function TableDoctor() {
|
function TableDoctor({setDictInfo}) {
|
||||||
const { getAuthorizationHeader, isAuthenticated } = useAuth();
|
const { getAuthorizationHeader, isAuthenticated } = useAuth();
|
||||||
|
|
||||||
const [medicos, setMedicos] = useState([]);
|
const [medicos, setMedicos] = useState([]);
|
||||||
@ -150,7 +151,7 @@ function TableDoctor() {
|
|||||||
return resultado;
|
return resultado;
|
||||||
}) : [];
|
}) : [];
|
||||||
|
|
||||||
// Aplica ordenação rápida
|
|
||||||
const applySorting = (arr) => {
|
const applySorting = (arr) => {
|
||||||
if (!Array.isArray(arr) || !sortKey) return arr;
|
if (!Array.isArray(arr) || !sortKey) return arr;
|
||||||
const copy = [...arr];
|
const copy = [...arr];
|
||||||
@ -216,7 +217,7 @@ function TableDoctor() {
|
|||||||
<div className="page-content table-doctor-container">
|
<div className="page-content table-doctor-container">
|
||||||
<section className="row">
|
<section className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="table-doctor-card">
|
<div className="card table-doctor-card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h4 className="card-title mb-0">Médicos Cadastrados</h4>
|
<h4 className="card-title mb-0">Médicos Cadastrados</h4>
|
||||||
<Link to={'cadastro'}>
|
<Link to={'cadastro'}>
|
||||||
@ -440,14 +441,14 @@ function TableDoctor() {
|
|||||||
<td>{medico.email || 'Não informado'}</td>
|
<td>{medico.email || 'Não informado'}</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2">
|
||||||
<Link to={`${medico.id}`}>
|
<Link to={`details/${medico.id}`}>
|
||||||
<button className="btn btn-sm btn-view">
|
<button className="btn btn-sm btn-view" onClick={() => setDictInfo({...medico})}>
|
||||||
<i className="bi bi-eye me-1"></i> Ver Detalhes
|
<i className="bi bi-eye me-1"></i> Ver Detalhes
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link to={`${medico.id}/edit`}>
|
<Link to={`edit/${medico.id}`}>
|
||||||
<button className="btn btn-sm btn-edit">
|
<button className="btn btn-sm btn-edit" onClick={() => setDictInfo({...medico})}>
|
||||||
<i className="bi bi-pencil me-1"></i> Editar
|
<i className="bi bi-pencil me-1"></i> Editar
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../../_assets/utils/AuthProvider";
|
import { useAuth } from "../../_assets/utils/AuthProvider";
|
||||||
import { UserInfos } from "../../_assets/utils/Functions-Endpoints/General";
|
import { UserInfos } from "../../_assets/utils/Functions-Endpoints/General";
|
||||||
@ -118,7 +118,8 @@ function Login() {
|
|||||||
if (data.access_token) {
|
if (data.access_token) {
|
||||||
const UserData = await UserInfos(`bearer ${data.access_token}`);
|
const UserData = await UserInfos(`bearer ${data.access_token}`);
|
||||||
console.log(UserData, "Dados do usuário");
|
console.log(UserData, "Dados do usuário");
|
||||||
|
localStorage.setItem("roleUser", UserData.roles)
|
||||||
|
|
||||||
if (UserData?.roles?.includes("admin")) {
|
if (UserData?.roles?.includes("admin")) {
|
||||||
navigate(`/admin/`);
|
navigate(`/admin/`);
|
||||||
} else if (UserData?.roles?.includes("secretaria")) {
|
} else if (UserData?.roles?.includes("secretaria")) {
|
||||||
@ -131,7 +132,7 @@ function Login() {
|
|||||||
navigate(`/paciente/`);
|
navigate(`/paciente/`);
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
console.log("ERROROROROROOR")
|
console.log("Erro na tentativa de login")
|
||||||
setShowCabecalho(true)
|
setShowCabecalho(true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -251,4 +252,4 @@ function Login() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
Loading…
x
Reference in New Issue
Block a user