From 5d1716923ee16f043473538f5a4709982caf46b8 Mon Sep 17 00:00:00 2001 From: Eduarda-SS <137419071+Eduarda-SS@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:58:20 -0300 Subject: [PATCH] Atualizando --- src/_assets/css/Header.css | 38 + src/_assets/css/LaudoStyle.css | 37 +- src/_assets/css/doctor/DoctorForm.css | 11 + .../css/doctor/HorariosDisponibilidade.css | 14 + src/components/Header/Header.jsx | 350 +++-- src/components/Sidebar/Sidebar.jsx | 50 +- src/components/agendamento/Calendario.jsx | 165 +++ src/components/medico/FormCadastroMedico.jsx | 33 +- .../medico/HorariosDisponibilidade.jsx | 38 +- .../paciente/FormCadastroPaciente.jsx | 26 +- src/data/sidebar-items-paciente.json | 10 +- src/pages/medico/CadastroAgendamento.jsx | 1054 ++++++++------ src/pages/medico/CadastroRelatorio.jsx | 129 +- src/pages/paciente/ListaConsulta.jsx | 891 +++++------- src/pages/secretaria/CadastroAgendamento.jsx | 238 +++- src/pages/secretaria/CadastroLaudos.jsx | 744 ++++++---- src/pages/secretaria/Dashboard.jsx | 192 ++- src/pages/secretaria/DetalhesMedico.jsx | 76 +- src/pages/secretaria/DetalhesPaciente.jsx | 21 +- .../secretaria/DisponibilidadesMedico.jsx | 673 +++++---- src/pages/secretaria/EditarMedico.jsx | 415 ++++-- src/pages/secretaria/EditarPaciente.jsx | 51 +- src/pages/secretaria/ListaAgendamentos.jsx | 1223 ++++++----------- src/pages/secretaria/ListaMedicos.jsx | 15 +- src/pages/usuario/Login.jsx | 9 +- 25 files changed, 3607 insertions(+), 2896 deletions(-) create mode 100644 src/components/agendamento/Calendario.jsx diff --git a/src/_assets/css/Header.css b/src/_assets/css/Header.css index f51e931..2335a2e 100644 --- a/src/_assets/css/Header.css +++ b/src/_assets/css/Header.css @@ -487,4 +487,42 @@ width: calc(100vw - 20px); 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; } \ No newline at end of file diff --git a/src/_assets/css/LaudoStyle.css b/src/_assets/css/LaudoStyle.css index 31f1542..5a08f68 100644 --- a/src/_assets/css/LaudoStyle.css +++ b/src/_assets/css/LaudoStyle.css @@ -309,4 +309,39 @@ html[data-bs-theme="dark"] .notice-card { background: #232323 !important; color: #e0e0e0 !important; box-shadow: 0 8px 30px rgba(10,20,40,0.32) !important; -} \ No newline at end of file +} + +/* 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; +} diff --git a/src/_assets/css/doctor/DoctorForm.css b/src/_assets/css/doctor/DoctorForm.css index d5f2379..81ef2e4 100644 --- a/src/_assets/css/doctor/DoctorForm.css +++ b/src/_assets/css/doctor/DoctorForm.css @@ -198,4 +198,15 @@ font-size: 1.1rem; 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; } } \ No newline at end of file diff --git a/src/_assets/css/doctor/HorariosDisponibilidade.css b/src/_assets/css/doctor/HorariosDisponibilidade.css index c726b67..e5b0921 100644 --- a/src/_assets/css/doctor/HorariosDisponibilidade.css +++ b/src/_assets/css/doctor/HorariosDisponibilidade.css @@ -151,4 +151,18 @@ .btn-add:hover { 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; } } \ No newline at end of file diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index 1a1c4cc..707ea4a 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -1,10 +1,12 @@ //Nesta página falta: ajustar caminho do CSS import { useState, useRef, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -//import './Header.css'; +import { createPortal } from 'react-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; +// import './Header.css'; const Header = () => { + // --- hooks (sempre na mesma ordem) --- const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isSuporteCardOpen, setIsSuporteCardOpen] = useState(false); const [isChatOpen, setIsChatOpen] = useState(false); @@ -12,48 +14,70 @@ const Header = () => { const [mensagens, setMensagens] = useState([]); const [showLogoutModal, setShowLogoutModal] = useState(false); const [avatarUrl, setAvatarUrl] = useState(null); + const navigate = useNavigate(); + const location = useLocation(); + const chatInputRef = useRef(null); const mensagensContainerRef = useRef(null); - useEffect(() => { - 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); - }, []); - + // foco quando abre chat useEffect(() => { if (isChatOpen && chatInputRef.current) { chatInputRef.current.focus(); } }, [isChatOpen]); + // scroll automático quando nova mensagem useEffect(() => { if (mensagensContainerRef.current) { mensagensContainerRef.current.scrollTop = mensagensContainerRef.current.scrollHeight; } }, [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 = () => { setShowLogoutModal(true); setIsDropdownOpen(false); }; - const handleLogoutCancel = () => { - setShowLogoutModal(false); + const clearAuthData = () => { + ["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 () => { @@ -67,55 +91,34 @@ const Header = () => { sessionStorage.getItem("authToken"); if (token) { - const response = await fetch( - "https://mock.apidog.com/m1/1053378-0-default/auth/v1/logout", - { + try { + await fetch("https://mock.apidog.com/m1/1053378-0-default/auth/v1/logout", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - } - ); - - if (response.status === 204) console.log("Logout realizado com sucesso"); - 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); - } + }); + } catch (err) { + // ignora erro de rede / endpoint — prossegue para limpar local + console.warn('logout endpoint error (ignored):', err); } } clearAuthData(); - navigate("/login"); - } catch (error) { - console.error("Erro durante logout:", error); + navigate('/login'); + } catch (err) { + console.error('Erro no logout:', err); clearAuthData(); - navigate("/login"); + navigate('/login'); } finally { setShowLogoutModal(false); } }; - const clearAuthData = () => { - ["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); - }); - }); - } - }; + const handleLogoutCancel = () => setShowLogoutModal(false); + // --- profile / suporte / chat handlers --- const handleProfileClick = () => { setIsDropdownOpen(!isDropdownOpen); if (isSuporteCardOpen) setIsSuporteCardOpen(false); @@ -128,14 +131,12 @@ const Header = () => { }; const handleSuporteClick = () => { - setIsSuporteCardOpen(!isSuporteCardOpen); - if (isDropdownOpen) setIsDropdownOpen(false); + setIsSuporteCardOpen((s) => !s); + setIsDropdownOpen(false); if (isChatOpen) setIsChatOpen(false); }; - const handleCloseSuporteCard = () => { - setIsSuporteCardOpen(false); - }; + const handleCloseSuporteCard = () => setIsSuporteCardOpen(false); const handleChatClick = () => { setIsChatOpen(true); @@ -143,7 +144,7 @@ const Header = () => { setMensagens([ { 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', hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) } @@ -155,11 +156,10 @@ const Header = () => { setMensagem(''); }; - const handleEnviarMensagem = async (e) => { + const handleEnviarMensagem = (e) => { e.preventDefault(); if (mensagem.trim() === '') return; - // Mensagem do usuário const novaMensagemUsuario = { id: Date.now(), texto: mensagem, @@ -170,37 +170,30 @@ const Header = () => { setMensagens(prev => [...prev, novaMensagemUsuario]); setMensagem(''); - try { - const response = await fetch("http://localhost:5000/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ message: mensagem }), - }); + setTimeout(() => { + if (chatInputRef.current) chatInputRef.current.focus(); + }, 0); - const data = await response.json(); - - // Resposta da IA + setTimeout(() => { + const respostas = [ + '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 = { 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', hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) }; - setMensagens(prev => [...prev, respostaSuporte]); - } catch (error) { - 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]); - } + }, 900); }; - const SuporteCard = () => ( + // --- subcomponentes (UI) --- + const SuporteCardContent = ({ onOpenChat }) => (

Suporte

Entre em contato conosco através dos canais abaixo

@@ -219,7 +212,7 @@ const Header = () => {
-
+
Chat Online
Disponível 24/7
@@ -228,11 +221,11 @@ const Header = () => {
); - const ChatOnline = () => ( -
+ const ChatOnlineContent = ({ mensagens, onSend, onClose }) => ( +

Chat de Suporte

- +
@@ -244,7 +237,7 @@ const Header = () => { ))}
-
+ {
); + // --- portals: Logout / Suporte / Chat (garante top-most e clickable) --- + const PortalWrapper = ({ children, z = 99999 }) => { + if (typeof document === 'undefined') return null; + return createPortal( +
+ {children} +
, + document.body + ); + }; + + const LogoutModalPortal = ({ onCancel, onConfirm }) => { + if (typeof document === 'undefined') return null; + return createPortal( +
+
e.stopPropagation()} + > +

Confirmar Logout

+

Tem certeza que deseja encerrar a sessão?

+
+ + +
+
+
, + document.body + ); + }; + + const SuportePortal = ({ onClose, children }) => { + if (typeof document === 'undefined') return null; + return createPortal( +
+
e.stopPropagation()} + > + {children} +
+
, + document.body + ); + }; + + const ChatPortal = ({ onClose, children }) => { + if (typeof document === 'undefined') return null; + return createPortal( +
+
e.stopPropagation()} + > + {children} +
+
, + document.body + ); + }; + + // --- evita render na rota de login (mantendo hooks invocados) --- + if (location.pathname === '/login') { + return null; + } + + // --- JSX principal (header visual) --- return ( -
+
-
+
📞
-
-
+
+
{isDropdownOpen && ( -
- - +
e.stopPropagation()}> + +
)}
- {/* Modal de Logout */} - {showLogoutModal && ( -
-
-

Confirmar Logout

-

Tem certeza que deseja encerrar a sessão?

-
- - -
-
-
- )} + {/* logout modal via portal */} + {showLogoutModal && } + {/* suporte portal */} {isSuporteCardOpen && ( -
-
e.stopPropagation()}> - -
-
+ + + )} + {/* chat portal */} {isChatOpen && ( -
-
- -
-
+ + + )}
); diff --git a/src/components/Sidebar/Sidebar.jsx b/src/components/Sidebar/Sidebar.jsx index 7e3184d..5fff25c 100644 --- a/src/components/Sidebar/Sidebar.jsx +++ b/src/components/Sidebar/Sidebar.jsx @@ -1,15 +1,14 @@ import { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; -import { useAuth } from "../../_assets/utils/AuthProvider"; -import { UserInfos } from "../../_assets/utils/Functions-Endpoints/General"; +import { useAuth } from "./utils/AuthProvider"; import MobileMenuToggle from "./MobileMenuToggle"; import ToggleSidebar from "./ToggleSidebar"; -import PacienteItems from "../../data/sidebar-items-paciente.json" -import DoctorItems from "../../data/sidebar-items-medico.json" -import admItems from "../../data/sidebar-items-adm.json" -import SecretariaItems from "../../data/sidebar-items-secretaria.json" -import FinanceiroItems from "../../data/sidebar-items-financeiro.json" +import PacienteItems from "../data/sidebar-items-paciente.json" +import DoctorItems from "../data/sidebar-items-medico.json" +import admItems from "../data/sidebar-items-adm.json" +import SecretariaItems from "../data/sidebar-items-secretaria.json" +import FinanceiroItems from "../data/sidebar-items-financeiro.json" function Sidebar({ menuItems }) { @@ -18,15 +17,23 @@ function Sidebar({ menuItems }) { const [isMobile, setIsMobile] = useState(false); const [showLogoutModal, setShowLogoutModal] = useState(false); const navigate = useNavigate(); - + const [roleUser, setRoleUser] = useState([]) const {getAuthorizationHeader} = useAuth(); 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 @@ -37,14 +44,6 @@ function Sidebar({ menuItems }) { setIsActive(!mobile); }; - const fetchInfoUser = async () => { - const InfoUser = await UserInfos(authHeader); - console.log(InfoUser.roles, "dados") - - setRoleUser(InfoUser.roles) - } - - fetchInfoUser() 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); @@ -251,26 +243,26 @@ function Sidebar({ menuItems }) {
    {roleUser.includes("admin") && - + } {roleUser.includes("admin") || roleUser.includes("secretaria") ? - + : null } {roleUser.includes("admin") || roleUser.includes("medico") ? - + :null } {roleUser.includes("admin") || roleUser.includes("financeiro") ? - + :null } {roleUser.includes("admin") || roleUser.includes("paciente") ? - + : null } @@ -296,4 +288,4 @@ function Sidebar({ menuItems }) { ); } -export default Sidebar; +export default Sidebar; \ No newline at end of file diff --git a/src/components/agendamento/Calendario.jsx b/src/components/agendamento/Calendario.jsx new file mode 100644 index 0000000..3458719 --- /dev/null +++ b/src/components/agendamento/Calendario.jsx @@ -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 ( +
    + {/* Painel lateral de informações */} +
    +
    + {selectedDay.format('MMM')} + {selectedDay.format('DD')} +
    +
    +

    {selectedDay.format('dddd')}

    +

    {selectedDay.format('D [de] MMMM [de] YYYY')}

    +
    +
    +

    Consultas para {selectedDay.format('DD/MM')}

    + {showSpinner ? : (DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')]?.length > 0) ? ( + DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')].map(app => ( +
    +
    {dayjs(app.scheduled_at).format('HH:mm')}
    +
    + {app.paciente_nome} + Dr(a). {app.medico_nome} +
    +
    + {app.status === 'cancelled' ? ( + + ) : ( + + )} +
    +
    + )) + ) : ( +

    Nenhuma consulta agendada.

    + )} +
    +
    + + {/* Calendário Principal */} +
    + {/* Legenda */} +
    +
    Realizado
    +
    Confirmado
    +
    Agendado
    +
    Cancelado
    +
    + + {/* Controles de navegação e Jump */} +
    +
    +

    {currentDate.format('MMMM [de] YYYY')}

    +
    + + + +
    +
    + +
    + + + +
    +
    + + {/* Grid dos dias */} +
    + {weekDays.map(day =>
    {day}
    )} + {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 ( +
    handleDateClick(day)} + > + {day.format('D')} + {count > 0 &&
    {count}
    } +
    + ); + })} +
    +
    +
    + ); +}; + +export default CalendarComponent; \ No newline at end of file diff --git a/src/components/medico/FormCadastroMedico.jsx b/src/components/medico/FormCadastroMedico.jsx index 12c511e..b2e8556 100644 --- a/src/components/medico/FormCadastroMedico.jsx +++ b/src/components/medico/FormCadastroMedico.jsx @@ -3,11 +3,11 @@ import { useState, useRef, useCallback } from "react"; import { Link, useNavigate, useLocation } from "react-router-dom"; -import { useAuth } from '../../_assets/utils/AuthProvider'; -import API_KEY from '../../_assets/utils/apiKeys'; -import HorariosDisponibilidade from "../medico/HorariosDisponibilidade"; +import { useAuth } from '../utils/AuthProvider'; +import API_KEY from '../utils/apiKeys'; -//import "./DoctorForm.css"; +import HorariosDisponibilidade from "../doctors/HorariosDisponibilidade"; +import "./DoctorForm.css"; 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) => { - setFormData((prev) => { - if (JSON.stringify(prev.availability) !== JSON.stringify(newAvailability)) { +const handleAvailabilityUpdate = useCallback((newAvailability) => { + setFormData((prev) => { return { ...prev, availability: newAvailability }; - } - return prev; }); - }, []); +}, [setFormData]); const handleCepBlur = async () => { const cep = formData.cep?.replace(/\D/g, ""); @@ -330,17 +327,9 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) { } try { - const savedDoctor = await onSave({ ...formData }); - - if (formData.availability && formData.availability.length > 0 && savedDoctor.id) { - if (formData.availabilityId) { - - await handlePatchAvailability(formData.availabilityId, formData.availability); - } else { - - await handleCreateAvailability(savedDoctor.id, formData.availability); - } - } + // 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. + await onSave({ ...formData }); } catch (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. Marque um dia para começar a adicionar blocos de tempo.

    - diff --git a/src/components/medico/HorariosDisponibilidade.jsx b/src/components/medico/HorariosDisponibilidade.jsx index 69bb805..41bb27f 100644 --- a/src/components/medico/HorariosDisponibilidade.jsx +++ b/src/components/medico/HorariosDisponibilidade.jsx @@ -12,13 +12,13 @@ const initialBlockTemplate = { }; const emptyAvailabilityTemplate = [ - { dia: "Segunda-feira", isChecked: false, blocos: [] }, - { dia: "Terça-feira", isChecked: false, blocos: [] }, - { dia: "Quarta-feira", isChecked: false, blocos: [] }, - { dia: "Quinta-feira", isChecked: false, blocos: [] }, - { dia: "Sexta-feira", isChecked: false, blocos: [] }, - { dia: "Sábado", isChecked: false, blocos: [] }, - { dia: "Domingo", isChecked: false, blocos: [] }, + { dia: "Domingo", weekday: 0, isChecked: false, blocos: [] }, + { dia: "Segunda-feira", weekday: 1, isChecked: false, blocos: [] }, + { dia: "Terça-feira", weekday: 2, isChecked: false, blocos: [] }, + { dia: "Quarta-feira", weekday: 3, isChecked: false, blocos: [] }, + { dia: "Quinta-feira", weekday: 4, isChecked: false, blocos: [] }, + { dia: "Sexta-feira", weekday: 5, isChecked: false, blocos: [] }, + { dia: "Sábado", weekday: 6, isChecked: false, blocos: [] }, ]; const HorariosDisponibilidade = ({ @@ -37,10 +37,18 @@ const HorariosDisponibilidade = ({ } }, [initialAvailability]); + useEffect(() => { + if (isFirstRun.current) { + isFirstRun.current = false; + return; + } + if (onUpdate) onUpdate(availability); + }, [availability, onUpdate]); + const handleDayCheck = useCallback((dayIndex, currentIsChecked) => { const isChecked = !currentIsChecked; - setAvailability((prev) => - prev.map((day, i) => + setAvailability((prev) => { + const updated = prev.map((day, i) => i === dayIndex ? { ...day, @@ -58,8 +66,10 @@ const HorariosDisponibilidade = ({ : [], } : day - ) - ); + ); + console.log('handleDayCheck - updated availability:', updated); + return updated; + }); }, []); const handleAddBlock = useCallback((dayIndex) => { @@ -109,9 +119,7 @@ const HorariosDisponibilidade = ({ ); }, []); - const handleSave = useCallback(() => { - if (onUpdate) onUpdate(availability); - }, [availability, onUpdate]); + return (
    @@ -199,4 +207,4 @@ const HorariosDisponibilidade = ({ ); }; -export default HorariosDisponibilidade; +export default HorariosDisponibilidade; \ No newline at end of file diff --git a/src/components/paciente/FormCadastroPaciente.jsx b/src/components/paciente/FormCadastroPaciente.jsx index 938b3c5..95a3483 100644 --- a/src/components/paciente/FormCadastroPaciente.jsx +++ b/src/components/paciente/FormCadastroPaciente.jsx @@ -4,7 +4,6 @@ import { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; import { FormatTelefones, FormatPeso, FormatCPF } from '../../_assets/utils/Formatar/Format'; - //import './PatientForm.css'; function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) { @@ -56,6 +55,8 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) { })); }; + + useEffect(() => { const peso = parseFloat(formData.weight_kg); const altura = parseFloat(formData.height_m); @@ -67,9 +68,20 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) { } }, [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 { name, value, type, checked, files } = e.target; - + console.log(name, value) if (value && emptyFields.includes(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')) { setFormData(prev => ({ ...prev, [name]: FormatPeso(value) })); } else if (name === 'rn_in_insurance' || name === 'vip' || name === 'validadeIndeterminada') { - setFormData(prev => ({ ...prev, [name]: checked })); - } else { + setFormData(prev => ({ ...prev, [name]: checked })); + } else if(name === 'cep'){ + handleCep(value) + setFormData(prev => ({...prev, [name]: value})) + }else { setFormData(prev => ({ ...prev, [name]: value })); } }; @@ -193,9 +208,6 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) { } 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 }); }; diff --git a/src/data/sidebar-items-paciente.json b/src/data/sidebar-items-paciente.json index bb15caf..d0442e4 100644 --- a/src/data/sidebar-items-paciente.json +++ b/src/data/sidebar-items-paciente.json @@ -1,13 +1,17 @@ [ -{ + { + "name": "Início", + "icon": "house-fill", + "url": "/paciente" + }, + { "name": "Minhas consulta", "icon": "calendar-plus-fill", "url": "/paciente/agendamento" }, - { "name": "Meus laudos", "icon": "table", "url": "/paciente/laudo" } -] +] \ No newline at end of file diff --git a/src/pages/medico/CadastroAgendamento.jsx b/src/pages/medico/CadastroAgendamento.jsx index 186f371..2d248d3 100644 --- a/src/pages/medico/CadastroAgendamento.jsx +++ b/src/pages/medico/CadastroAgendamento.jsx @@ -1,476 +1,658 @@ //DoctorAgendamentoManager.jsx //Nesta página falta: mudar nomes, ajustar caminho do CSS -import { useState, useMemo, useEffect } from 'react'; +import { useState, useMemo, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { GetPatientByID } from '../../_assets/utils/Functions-Endpoints/Patient.js'; -import { GetDoctorByID } from '../../_assets/utils/Functions-Endpoints/Doctor.js'; +import { GetAllDoctors } from '../../_assets/utils/Functions-Endpoints/Doctor.js'; import { useAuth } from '../../_assets/utils/AuthProvider.js'; -import { UserInfos } from '../../_assets/utils/Functions-Endpoints/General.js'; +import { Search, ChevronLeft, ChevronRight, Edit, Trash2, CheckCircle } from 'lucide-react'; import API_KEY from '../../_assets/utils/apiKeys.js'; -import AgendamentoCadastroManager from '../secretaria/CadastroAgendamento.jsx'; -import TabelaAgendamentoDia from '../../components/agendamento/TabelaAgendamentoDia.jsx'; -import TabelaAgendamentoSemana from '../../components/agendamento/TabelaAgendamentoSemana.jsx'; -import TabelaAgendamentoMes from '../../components/agendamento/TabelaAgendamentoMes.jsx'; -import AgendamentosMes from '../../components/agendamento/DadosConsultasMock.js'; import dayjs from 'dayjs'; +import 'dayjs/locale/pt-br'; +import isBetween from 'dayjs/plugin/isBetween'; +import localeData from 'dayjs/plugin/localeData'; + +import AgendamentoCadastroManager from '../secretaria/CadastroAgendamento.jsx'; +import Spinner from '../../components/Spinner.jsx'; //import "../pages/style/Agendamento.css"; //import '../pages/style/FilaEspera.css'; +dayjs.locale('pt-br'); +dayjs.extend(isBetween); +dayjs.extend(localeData); -const Agendamento = ({setDictInfo}) => { - const navigate = useNavigate(); - const [selectedID, setSelectedId] = useState('0') - const [filaEsperaData, setfilaEsperaData] = useState([]) +const Agendamento = () => { + const navigate = useNavigate(); + const { getAuthorizationHeader, user } = useAuth(); + const authHeader = getAuthorizationHeader(); + + // ID do médico que você quer visualizar + const ID_MEDICO_ESPECIFICO = "078d2a67-b4c1-43c8-ae32-c1e75bb5b3df"; + + const [listaTodosAgendamentos, setListaTodosAgendamentos] = useState([]); + const [selectedID, setSelectedId] = useState('0'); + const [filaEsperaData, setFilaEsperaData] = useState([]); const [FiladeEspera, setFiladeEspera] = useState(false); - const [tabela, setTabela] = useState('diario'); - const [PageNovaConsulta, setPageConsulta] = useState(false); - const [searchTerm, setSearchTerm] = useState(''); - const [agendamentos, setAgendamentos] = useState() - const {getAuthorizationHeader} = useAuth() - const [DictAgendamentosOrganizados, setAgendamentosOrganizados ] = useState({}) + const [PageNovaConsulta, setPageConsulta] = useState(false); + const [DictAgendamentosOrganizados, setAgendamentosOrganizados] = useState({}); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [ListaDeMedicos, setListaDeMedicos] = useState([]); + const [FiltredTodosMedicos, setFiltredTodosMedicos] = useState([]); + const [searchTermDoctor, setSearchTermDoctor] = useState(''); + const [MedicoFiltrado, setMedicoFiltrado] = useState({ id: "vazio" }); + const [motivoCancelamento, setMotivoCancelamento] = useState(""); + const [showSpinner, setShowSpinner] = useState(true); + const [waitlistSearch, setWaitlistSearch] = useState(''); + const [waitSortKey, setWaitSortKey] = useState(null); + const [waitSortDir, setWaitSortDir] = useState('asc'); + const [waitPage, setWaitPage] = useState(1); + const [waitPerPage, setWaitPerPage] = useState(10); + const [cacheMedicos, setCacheMedicos] = useState({}); + const [cachePacientes, setCachePacientes] = useState({}); + const [currentDate, setCurrentDate] = useState(dayjs()); + const [selectedDay, setSelectedDay] = useState(dayjs()); + const [agendamentoParaEdicao, setAgendamentoParaEdicao] = useState(null); + const [quickJump, setQuickJump] = useState({ + month: currentDate.month(), + year: currentDate.year() + }); - const [showDeleteModal, setShowDeleteModal] = useState(false) - const [showConfirmModal, setShowConfirmModal] = useState(false) - - const [coresConsultas, setCoresConsultas] = useState([]) + // ✨ ALTERAÇÃO PRINCIPAL: A busca agora filtra pelo ID do médico direto na API + const fetchAppointments = useCallback(async () => { + if (!authHeader) return; + setShowSpinner(true); + const myHeaders = new Headers(); + myHeaders.append("Authorization", authHeader); + myHeaders.append("apikey", API_KEY); + const requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' }; - const [listaConsultasID, setListaConsultaID] = useState([]) + // A URL agora contém o filtro para o ID do médico específico + const apiUrl = `https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?doctor_id=eq.${ID_MEDICO_ESPECIFICO}&select=*`; - const [motivoCancelamento, setMotivoCancelamento] = useState("") - - const [user, setUser] = useState({}) + try { + const res = await fetch(apiUrl, requestOptions); + const data = await res.json(); + setListaTodosAgendamentos(data || []); + } catch (err) { + console.error('Erro ao buscar agendamentos', err); + setListaTodosAgendamentos([]); + } finally { + setShowSpinner(false); + } + }, [authHeader, ID_MEDICO_ESPECIFICO]); - let authHeader = getAuthorizationHeader() - - const FiltrarAgendamentos = async (listaTodosAgendamentos) => { - const ConfigurarFiladeEspera = async (patient_id, doctor_id, agendamento) => { - // Assumindo que GetDoctorByID e GetPatientByID estão definidos no seu escopo - let medico = await GetDoctorByID(doctor_id, authHeader); - let paciente = await GetPatientByID(patient_id, authHeader); - - let dicionario = { - ...agendamento, - - nome_medico: medico[0].full_name, - doctor_id: medico.id, - patient_id: paciente[0].id, - paciente_nome: paciente[0].full_name, - paciente_cpf: paciente[0].cpf - - }; - return dicionario; - }; - - let DictAgendamentosOrganizados = {}; - let ListaFilaDeEspera = []; - - // 1. Agrupamento (igual ao seu código original) - for (const agendamento of listaTodosAgendamentos) { - if (agendamento.status === 'requested') { - - let v = await ConfigurarFiladeEspera(agendamento.patient_id, agendamento.doctor_id, agendamento); - ListaFilaDeEspera.push(v); - } else { - - const DiaAgendamento = agendamento.scheduled_at?.split("T")[0]; - - let novoAgendamento = await ConfigurarFiladeEspera(agendamento.patient_id, agendamento.doctor_id, agendamento); - - if (DiaAgendamento in DictAgendamentosOrganizados) { - DictAgendamentosOrganizados[DiaAgendamento].push(novoAgendamento); + const updateAppointmentStatus = useCallback(async (id, updates) => { + setShowSpinner(true); + const myHeaders = new Headers(); + myHeaders.append("Authorization", authHeader); + myHeaders.append("apikey", API_KEY); + myHeaders.append("Content-Type", "application/json"); + myHeaders.append("Prefer", "return=representation"); + 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) { + await fetchAppointments(); + return true; } else { - DictAgendamentosOrganizados[DiaAgendamento] = [novoAgendamento]; + console.error('Erro ao atualizar agendamento:', await response.text()); + return false; + } + } catch (error) { + console.error('Erro de rede/servidor:', error); + return false; + } finally { + setShowSpinner(false); + } + }, [authHeader, fetchAppointments]); + + + const deleteConsulta = useCallback(async (id) => { + const success = await updateAppointmentStatus(id, { status: "cancelled", cancellation_reason: motivoCancelamento, updated_at: new Date().toISOString() }); + if (success) { + setShowDeleteModal(false); + setMotivoCancelamento(""); + setSelectedId('0'); + } else { + alert("Falha ao cancelar a consulta."); + } + }, [motivoCancelamento, updateAppointmentStatus]); + + + const confirmConsulta = useCallback(async (id) => { + const success = await updateAppointmentStatus(id, { status: "agendado", cancellation_reason: null, updated_at: new Date().toISOString() }); + if (success) { + setSelectedId('0'); + } else { + alert("Falha ao reverter o cancelamento."); + } + }, [updateAppointmentStatus]); + + useEffect(() => { + if(authHeader) { + fetchAppointments(); + // A busca de todos os médicos pode continuar caso a secretária precise ver a lista + if (user?.role !== 'doctor') { + GetAllDoctors(authHeader).then(docs => { + if (docs) { + setListaDeMedicos(docs.map(d => ({ nomeMedico: d.full_name, idMedico: d.id }))); + } + }); } } - } + }, [authHeader, fetchAppointments, user?.role]); -// ---------------------------------------------------------------------- - // 2. Ordenação Interna: Ordenar os agendamentos por HORÁRIO (do menor para o maior) - for (const DiaAgendamento in DictAgendamentosOrganizados) { - DictAgendamentosOrganizados[DiaAgendamento].sort((a, b) => { - // Compara as strings de data/hora (ISO 8601) diretamente, - // que funcionam para ordenação cronológica. - if (a.scheduled_at < b.scheduled_at) return -1; - if (a.scheduled_at > b.scheduled_at) return 1; + useEffect(() => { + const processData = async () => { + // Como os dados já vêm filtrados da API, não precisamos mais da verificação de 'user' aqui + if (!listaTodosAgendamentos.length) { + setAgendamentosOrganizados({}); + setFilaEsperaData([]); + return; + } + + setShowSpinner(true); + + // ✨ SIMPLIFICAÇÃO: Não é mais necessário filtrar por `user.role`, + // pois a API já retornou apenas os agendamentos do médico desejado. + const appointmentsToShow = listaTodosAgendamentos; + + const patientIdsToFetch = new Set(); + const doctorIdsToFetch = new Set(); + + appointmentsToShow.forEach(ag => { + if (ag.patient_id && !cachePacientes[ag.patient_id]) { + patientIdsToFetch.add(ag.patient_id); + } + if (ag.doctor_id && !cacheMedicos[ag.doctor_id]) { + doctorIdsToFetch.add(ag.doctor_id); + } + }); + + const fetchPromises = []; + + if (patientIdsToFetch.size > 0) { + const query = `id=in.(${Array.from(patientIdsToFetch).join(',')})`; + fetchPromises.push( + fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients?${query}&select=*`, { + headers: { apikey: API_KEY, Authorization: authHeader } + }).then(res => res.json()) + ); + } else { + fetchPromises.push(Promise.resolve(null)); // Mantém a ordem do Promise.all + } + + if (doctorIdsToFetch.size > 0) { + const query = `id=in.(${Array.from(doctorIdsToFetch).join(',')})`; + fetchPromises.push( + fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?${query}&select=id,full_name`, { + headers: { apikey: API_KEY, Authorization: authHeader } + }).then(res => res.json()) + ); + } else { + fetchPromises.push(Promise.resolve(null)); + } + + const [newPatients, newDoctors] = await Promise.all(fetchPromises); + + const updatedPatientCache = { ...cachePacientes }; + if (newPatients) newPatients.forEach(p => updatedPatientCache[p.id] = p); + + const updatedDoctorCache = { ...cacheMedicos }; + if (newDoctors) newDoctors.forEach(d => updatedDoctorCache[d.id] = d); + + setCachePacientes(updatedPatientCache); + setCacheMedicos(updatedDoctorCache); + + const newDict = {}; + const newFila = []; + + for (const agendamento of appointmentsToShow) { + const medico = updatedDoctorCache[agendamento.doctor_id]; + const paciente = updatedPatientCache[agendamento.patient_id]; + + if (!medico || !paciente) continue; + + const agendamentoMelhorado = { + ...agendamento, + medico_nome: medico.full_name || 'N/A', + paciente_nome: paciente.full_name || 'N/A', + paciente_cpf: paciente.cpf || 'N/A' + }; + + if (agendamento.status === "requested") { + newFila.push({ agendamento: agendamentoMelhorado, Infos: agendamentoMelhorado }); + } else { + const DiaAgendamento = dayjs(agendamento.scheduled_at).format("YYYY-MM-DD"); + if (!newDict[DiaAgendamento]) newDict[DiaAgendamento] = []; + newDict[DiaAgendamento].push(agendamentoMelhorado); + } + } + + for (const key in newDict) { + newDict[key].sort((a, b) => new Date(a.scheduled_at) - new Date(b.scheduled_at)); + } + + setAgendamentosOrganizados(newDict); + setFilaEsperaData(newFila); + setShowSpinner(false); + }; + + processData(); + }, [listaTodosAgendamentos, authHeader]); // Removido 'user' das dependências pois não é mais usado aqui + + // O restante do código permanece o mesmo... + + const handleEditConsulta = (agendamento) => { + setAgendamentoParaEdicao(agendamento); + setPageConsulta(true); + }; + + const handleSearchMedicos = (term) => { + setSearchTermDoctor(term); + if (term.trim()) { + const filtered = ListaDeMedicos.filter(medico => + medico.nomeMedico.toLowerCase().includes(term.toLowerCase()) + ); + setFiltredTodosMedicos(filtered); + } else { + setFiltredTodosMedicos([]); + setMedicoFiltrado({ id: "vazio" }); + } + }; + + const generateDateGrid = () => { + const grid = []; + const startOfMonth = currentDate.startOf('month'); + let currentDay = startOfMonth.subtract(startOfMonth.day(), 'day'); + 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); +const DeleteModal = () => ( +
    +
    +
    +
    +
    Confirmação de Cancelamento
    + +
    +
    +

    Qual o motivo do cancelamento? (Opcional)

    + +
    +
    + + {/* ✨ Botão sempre ativo ✨ */} + +
    +
    +
    +
    +); + + + useEffect(() => { + setQuickJump({ + month: currentDate.month(), + year: currentDate.year() + }); + }, [currentDate]); + + const filaEsperaFiltrada = useMemo(() => { + if (!waitlistSearch.trim()) return filaEsperaData; + const term = waitlistSearch.toLowerCase(); + return filaEsperaData.filter(item => + (item?.Infos?.paciente_nome?.toLowerCase() || '').includes(term) || + (item?.Infos?.paciente_cpf?.toLowerCase() || '').includes(term) || + (item?.Infos?.medico_nome?.toLowerCase() || '').includes(term) + ); + }, [waitlistSearch, filaEsperaData]); + + const applySortingWaitlist = useCallback((arr) => { + if (!Array.isArray(arr) || !waitSortKey) return arr; + const copy = [...arr]; + const key = waitSortKey; + const dir = waitSortDir === 'asc' ? 1 : -1; + copy.sort((a, b) => { + const valA = key === 'data' ? new Date(a.agendamento.scheduled_at) : (a.Infos?.[`${key}_nome`] || ''); + const valB = key === 'data' ? new Date(b.agendamento.scheduled_at) : (b.Infos?.[`${key}_nome`] || ''); + if (valA < valB) return -1 * dir; + if (valA > valB) return 1 * dir; return 0; }); - } + return copy; + }, [waitSortKey, waitSortDir]); -// ---------------------------------------------------------------------- - // 3. Ordenação Externa: Ordenar os DIAS (as chaves do objeto) - // Para garantir que as chaves fiquem na sequência cronológica correta. + const filaEsperaOrdenada = useMemo(() => applySortingWaitlist(filaEsperaFiltrada), [filaEsperaFiltrada, applySortingWaitlist]); + const waitTotalPages = Math.ceil(filaEsperaOrdenada.length / waitPerPage) || 1; + const waitIndiceInicial = (waitPage - 1) * waitPerPage; + const waitIndiceFinal = waitIndiceInicial + waitPerPage; + const filaEsperaPaginada = filaEsperaOrdenada.slice(waitIndiceInicial, waitIndiceFinal); - // Pega as chaves (datas) - const chavesOrdenadas = Object.keys(DictAgendamentosOrganizados).sort((a, b) => { - // Compara as chaves de data (strings 'YYYY-MM-DD') - if (a < b) return -1; - if (a > b) return 1; - return 0; - }); - - // Cria um novo objeto no formato desejado, garantindo a ordem das chaves - let DictAgendamentosFinal = {}; - for (const data of chavesOrdenadas) { - DictAgendamentosFinal[data] = DictAgendamentosOrganizados[data]; - } - setAgendamentosOrganizados(DictAgendamentosFinal); // Use o objeto final ordenado - setfilaEsperaData(ListaFilaDeEspera); -}; - -useEffect(() => { - -console.log(user, "usuario") - -}, [user]) - - // Requisição inicial para mostrar os agendamentos do banco de dados - useEffect(() => { - - async function fetchDadosUser (){ - let dado = await UserInfos(authHeader) - setUser(dado) - } - - fetchDadosUser() - - - var myHeaders = new Headers(); - myHeaders.append("Authorization", authHeader); - myHeaders.append("apikey", API_KEY) - - var requestOptions = { - method: 'GET', - headers: myHeaders, - redirect: 'follow' + const gerarNumerosWaitPages = () => { + const paginas = []; + const paginasParaMostrar = 5; + let inicio = Math.max(1, waitPage - Math.floor(paginasParaMostrar / 2)); + let fim = Math.min(waitTotalPages, inicio + paginasParaMostrar - 1); + inicio = Math.max(1, fim - paginasParaMostrar + 1); + for (let i = inicio; i <= fim; i++) paginas.push(i); + return paginas; }; - fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?doctor_id=eq.${"078d2a67-b4c1-43c8-ae32-c1e75bb5b3df"}`, requestOptions) - .then(response => response.json()) - .then(result => {FiltrarAgendamentos(result); console.log(result, "RESULTRADO DA API")}) - .catch(error => console.log('error', error)); + useEffect(() => { setWaitPage(1); }, [waitlistSearch, waitSortKey, waitSortDir]); - - - }, []) - - -const deleteConsulta = (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":"cancelled", - "cancellation_reason": motivoCancelamento - }); - - - var requestOptions = { - method: 'PATCH', - headers: myHeaders, - body: raw, - redirect: 'follow' + const handleQuickJumpChange = (type, value) => { + setQuickJump(prev => ({ ...prev, [type]: Number(value) })); }; - 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 applyQuickJump = () => { + let newDate = dayjs().year(quickJump.year).month(quickJump.month).date(1); + setCurrentDate(newDate); + setSelectedDay(newDate); + }; - // Lógica para filtrar os dados da AGENDA (AgendamentosMes) - const filteredAgendamentos = useMemo(() => { - if (!searchTerm.trim()) { - return AgendamentosMes; - } - const lowerCaseSearchTerm = searchTerm.toLowerCase(); - const filteredData = {}; - - for (const semana in AgendamentosMes) { - filteredData[semana] = {}; - for (const dia in AgendamentosMes[semana]) { - filteredData[semana][dia] = AgendamentosMes[semana][dia].filter(agendamento => - agendamento.status === 'vazio' || - (agendamento.paciente && agendamento.paciente.toLowerCase().includes(lowerCaseSearchTerm)) - ); - } - } - return filteredData; - }, [searchTerm]); - - const ListarDiasdoMes = (ano, mes) => { - let segundas = []; let tercas = []; let quartas = []; let quintas = []; let sextas = [] - const base = dayjs(`${ano}-${mes}-01`) - const DiasnoMes = base.daysInMonth() - for (let d = 1; d <= DiasnoMes; d++) { - const data = dayjs(`${ano}-${mes}-${d}`) - const dia = data.format('dddd') - switch (dia) { - case 'Monday': segundas.push(d); break - case 'Tuesday': tercas.push(d); break - case 'Wednesday': quartas.push(d); break - case 'Thursday': quintas.push(d); break - case 'Friday': sextas.push(d); break - default: break - } - } - let ListaDiasDatas = {segundas:segundas,tercas:tercas,quartas: quartas,quintas: quintas,sextas: sextas} - return ListaDiasDatas - } - - -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 handleClickCancel = () => setPageConsulta(false) - - return ( -
    -

    Agendar nova consulta

    - - - - - - {!PageNovaConsulta ? ( -
    - -
    -
    - -
    - -
    -
    -
    -
    - - - -
    -
    -
    Realizado
    -
    Confirmado
    -
    Agendado
    -
    Cancelado
    -
    + return ( +
    +

    Agendar nova consulta

    +
    + + + +
    + {!PageNovaConsulta ? ( +
    + {user?.role !== 'doctor' && ( +
    +
    +
    +
    +
    + handleSearchMedicos(e.target.value)} /> +
    +
    + {searchTermDoctor && FiltredTodosMedicos.length > 0 && ( +
    + {FiltredTodosMedicos.map((medico) => ( +
    { + setSearchTermDoctor(medico.nomeMedico); + setFiltredTodosMedicos([]); + setMedicoFiltrado({ id: medico.idMedico }); + }} + > +

    {medico.nomeMedico}

    +
    + ))} +
    + )} +
    +
    +
    + )} +
    + + +
    +
    + {!FiladeEspera ? ( +
    +
    +
    {selectedDay.format('MMM')}{selectedDay.format('DD')}
    +

    {selectedDay.format('dddd')}

    {selectedDay.format('D [de] MMMM [de] YYYY')}

    +
    +

    Consultas para {selectedDay.format('DD/MM')}

    + {showSpinner ? : (DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')]?.filter(app => MedicoFiltrado.id === "vazio" || app.doctor_id === MedicoFiltrado.id).length > 0) ? ( + DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')] + .filter(app => MedicoFiltrado.id === "vazio" || app.doctor_id === MedicoFiltrado.id) + .map(app => ( +
    +
    {dayjs(app.scheduled_at).format('HH:mm')}
    +
    {app.paciente_nome}Dr(a). {app.medico_nome}
    +
    + {app.status === 'cancelled' ? ( + + ) : ( + + )} + {app.status !== 'cancelled' && ( + + )} +
    +
    + )) + ) : (

    Nenhuma consulta agendada.

    )} +
    +
    +
    +
    +
    Realizado
    +
    Confirmado
    +
    Agendado
    +
    Cancelado
    +
    +
    +
    +

    {currentDate.format('MMMM [de] YYYY')}

    +
    + + + +
    +
    +
    + + + +
    +
    +
    + {weekDays.map(day =>
    {day}
    )} + {dateGrid.map((day, index) => { + const dayString = day.format('YYYY-MM-DD'); + const appointmentsOnDay = DictAgendamentosOrganizados[dayString] || []; + const filteredAppointments = appointmentsOnDay.filter(app => MedicoFiltrado.id === "vazio" || app.doctor_id === MedicoFiltrado.id); + const cellClasses = `day-cell ${day.isSame(currentDate, 'month') ? 'current-month' : 'other-month'} ${day.isSame(dayjs(), 'day') ? 'today' : ''} ${day.isSame(selectedDay, 'day') ? 'selected' : ''}`; + return ( +
    handleDateClick(day)}> + {day.format('D')} + {filteredAppointments.length > 0 &&
    {filteredAppointments.length}
    } +
    + ); + })} +
    +
    +
    + ) : ( +
    +
    +
    +
    +

    Fila de Espera

    +
    +
    +
    Filtros
    +
    setWaitlistSearch(e.target.value)} />Digite o nome do paciente, CPF ou nome do médico
    +
    +
    + Ordenar por: + +
    +
    +
    +
    {filaEsperaFiltrada.length} DE {filaEsperaData.length} SOLICITAÇÕES ENCONTRADAS
    +
    +
    +
    + + + + + + + + + + + + {filaEsperaPaginada.length > 0 ? ( + filaEsperaPaginada.map((item, index) => ( + + + + + + + + )) + ) : ( + + + + )} + +
    Nome do PacienteCPFMédico SolicitadoData da SolicitaçãoAções
    {item?.Infos?.paciente_nome}{item?.Infos?.paciente_cpf}{item?.Infos?.medico_nome}{dayjs(item.agendamento.scheduled_at).format('DD/MM/YYYY')} + +
    +
    + {showSpinner ? : (<>

    Nenhuma solicitação encontrada.

    )} +
    +
    + {filaEsperaFiltrada.length > 0 && ( +
    +
    + Itens por página: + +
    +
    + Página {waitPage} de {waitTotalPages} • Mostrando {waitIndiceInicial + 1}-{Math.min(waitIndiceFinal, filaEsperaFiltrada.length)} de {filaEsperaFiltrada.length} + +
    +
    + )} +
    +
    +
    +
    +
    +
    + )}
    - - {tabela === "diario" && } - - - {tabela === 'semanal' && } - - - {tabela === 'mensal' && } -
    - -
    + ) : ( + { + setPageConsulta(false); + fetchAppointments(); + }} + /> + )} + {showDeleteModal && }
    - ) : ( - - )} - - - {showConfirmModal &&( -
    - e.target.classList.contains("modal") && setShowDeleteModal(false) - } - > -
    -
    - -
    -
    - Confirmação de edição -
    - -
    - -
    -

    - Tem certeza que deseja retirar o cancelamento ? -

    -
    - -
    - - - - - -
    -
    -
    -
    )} - - {showDeleteModal && ( -
    - e.target.classList.contains("modal") && setShowDeleteModal(false) - } - > -
    -
    - -
    -
    - Confirmação de Cancelamento -
    - -
    - -
    -

    - Qual o motivo do cancelamento? -

    -
    - - +
    +
    + + +
    +
    +
    + )} +
    + ) } -export default ConsultasPaciente; \ No newline at end of file +export default Agendamento; \ No newline at end of file diff --git a/src/pages/secretaria/CadastroAgendamento.jsx b/src/pages/secretaria/CadastroAgendamento.jsx index 8fb0b0a..2ecbc2f 100644 --- a/src/pages/secretaria/CadastroAgendamento.jsx +++ b/src/pages/secretaria/CadastroAgendamento.jsx @@ -1,88 +1,188 @@ //AgendamentoCadastroManager.jsx //Nesta página falta: mudar nomes -import { useEffect,useState } from 'react' -import { useAuth } from '../../_assets/utils/AuthProvider' -import { UserInfos } from '../../_assets/utils/Functions-Endpoints/General' -import API_KEY from '../../_assets/utils/apiKeys' +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useAuth } from '../../_assets/utils/AuthProvider'; +import { UserInfos } from '../../_assets/utils/Functions-Endpoints/General'; +import API_KEY from '../../_assets/utils/apiKeys'; -import FormNovaConsulta from '../../components/agendamento/FormNovaConsulta' - -import dayjs from 'dayjs' +import FormNovaConsulta from '../../components/agendamento/FormNovaConsulta'; +import dayjs from 'dayjs'; -const AgendamentoCadastroManager = ({setPageConsulta, Dict}) => { +const AgendamentoCadastroManager = ({ setPageConsulta, agendamentoInicial }) => { - const {getAuthorizationHeader} = useAuth() - const [agendamento, setAgendamento] = useState({status:'confirmed'}) - const [idUsuario, setIDusuario] = useState('0') + const { getAuthorizationHeader } = useAuth(); + + 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{ - console.log(Dict) - setAgendamento(Dict) - } - - const ColherInfoUsuario =async () => { - const result = await UserInfos(authHeader) - - setIDusuario(result?.profile?.id) - - } - ColherInfoUsuario() - - - }, []) - - - - const handleSave = (Dict) => { - let DataAtual = dayjs() + + const ColherInfoUsuario = async () => { + const result = await UserInfos(authHeader); + setIDusuario(result?.profile?.id); + }; + ColherInfoUsuario(); + + + }, [agendamentoInicial, authHeader]); + const handleSave = async (Dict) => { 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({ + "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({ - - "patient_id": Dict.patient_id, - "doctor_id": Dict.doctor_id, - "scheduled_at": `${Dict.dataAtendimento}T${Dict.horarioInicio}:00.000Z`, - "duration_minutes": 30, - "appointment_type": Dict.tipo_consulta, - - "patient_notes": "", - "insurance_provider": Dict.convenio, - "status": Dict.status, - "created_by": idUsuario - }); - - var requestOptions = { - method: 'POST', - headers: myHeaders, - body: raw, - redirect: 'follow' - }; - - fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments", requestOptions) - .then(response => response.text()) - .then(result => console.log(result)) - .catch(error => console.log('error', error)); - - } + 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, + "updated_at": dayjs().toISOString(), + "updated_by": idUsuario, + }); + + var requestOptions = { + method: 'PATCH', + headers: myHeaders, + body: raw, + redirect: 'follow' + }; + + try { + const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${appointmentId}`, requestOptions); + if (response.ok) { + toast.success("Agendamento atualizado com sucesso! 📝"); + setPageConsulta(false); + } 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 ( -
    - setPageConsulta(false)}/> + return ( +
    + {} + setPageConsulta(false)} + /> +
    + ); +}; -
    - ) -} - -export default AgendamentoCadastroManager \ No newline at end of file +export default AgendamentoCadastroManager; \ No newline at end of file diff --git a/src/pages/secretaria/CadastroLaudos.jsx b/src/pages/secretaria/CadastroLaudos.jsx index 48dde4a..feb864b 100644 --- a/src/pages/secretaria/CadastroLaudos.jsx +++ b/src/pages/secretaria/CadastroLaudos.jsx @@ -1,338 +1,518 @@ // LaudoManager.jsx //Nesta página falta: ajustar caminho do CSS -import { useState, useEffect } from "react"; -//import "./LaudoStyle.css"; // Importa o CSS externo +import { useState, useEffect } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; -/* ===== Mock data (simula APIDOG) ===== */ -function mockFetchLaudos() { - return [ - { - id: "LAU-300551296", - pedido: 300551296, - data: "29/07/2025", - paciente: { nome: "Sarah Mariana Oliveira", cpf: "616.869.070-**", nascimento: "1990-03-25", convenio: "Unimed" }, - solicitante: "Sandro Rangel Santos", - exame: "US - Abdome Total", - conteudo: "RELATÓRIO MÉDICO\n\nAchados: Imagens compatíveis com ...\nConclusão: Órgãos sem alterações significativas.", - status: "rascunho" - }, - { - id: "LAU-300659170", - pedido: 300659170, - data: "29/07/2025", - paciente: { nome: "Laissa Helena Marquetti", cpf: "950.684.57-**", nascimento: "1986-09-12", convenio: "Bradesco" }, - solicitante: "Sandro Rangel Santos", - exame: "US - Mamária Bilateral", - conteudo: "RELATÓRIO MÉDICO\n\nAchados: text...", - status: "liberado" - }, - { - id: "LAU-300658301", - pedido: 300658301, - data: "28/07/2025", - paciente: { nome: "Vera Lúcia Oliveira Santos", cpf: "928.005.**", nascimento: "1979-02-02", convenio: "Particular" }, - solicitante: "Dr. Fulano", - exame: "US - Transvaginal", - conteudo: "RELATÓRIO MÉDICO\n\nAchados: ...", - status: "entregue" - } - ]; -} +import { GetPatientByID } from '../../_assets/utils/Functions-Endpoints/Patient'; +import { GetDoctorByID } from '../../_assets/utils/Functions-Endpoints/Doctor'; +import { useAuth } from '../../_assets/utils/AuthProvider'; +import API_KEY from '../../_assets/utils/apiKeys'; -function mockDeleteLaudo(id) { - return new Promise((res) => setTimeout(() => res({ ok: true }), 500)); -} +import html2pdf from 'html2pdf.js'; +import TiptapViewer from '../../components/medico/TiptapViewer' -/* ===== Componente ===== */ -export default function LaudoManager() { - const [laudos, setLaudos] = useState([]); - const [openDropdownId, setOpenDropdownId] = useState(null); +//import '../PagesMedico/styleMedico/DoctorRelatorioManager.css'; - /* viewerLaudo é usado para mostrar o editor/leitura; - previewLaudo é usado para a pré-visualização (sem bloquear) */ - const [viewerLaudo, setViewerLaudo] = useState(null); - const [previewLaudo, setPreviewLaudo] = useState(null); - const [showPreview, setShowPreview] = useState(false); +const LaudoManager = () => { + const navigate = useNavigate(); + const { getAuthorizationHeader } = useAuth(); + const authHeader = getAuthorizationHeader(); - const [showConfirmDelete, setShowConfirmDelete] = useState(false); - const [toDelete, setToDelete] = useState(null); - const [loadingDelete, setLoadingDelete] = useState(false); + const [relatoriosOriginais, setRelatoriosOriginais] = useState([]); + const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]); + 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 [showNoPermission, setShowNoPermission] = useState(false); + const [showProtocolModal, setShowProtocolModal] = useState(false); + const [protocolForIndex, setProtocolForIndex] = useState(null); - /* pesquisa */ - const [query, setQuery] = useState(""); + const [paginaAtual, setPaginaAtual] = useState(1); + const [itensPorPagina, setItensPorPagina] = useState(10); - /* Para simplificar: eu assumo aqui que estamos na visão da secretaria */ - const isSecretary = true; // permanece true (somente leitura) + // agora guardamos a mensagem (null = sem aviso) + 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(() => { - // Importa os dados mock apenas - const data = mockFetchLaudos(); - setLaudos(data); - }, []); + let mounted = true; - // Fecha dropdown ao clicar fora - useEffect(() => { - function onDocClick(e) { - if (e.target.closest && e.target.closest('.action-btn')) return; - if (e.target.closest && e.target.closest('.dropdown')) return; - setOpenDropdownId(null); - } - document.addEventListener('click', onDocClick); - return () => document.removeEventListener('click', onDocClick); - }, []); + const fetchReports = async () => { + try { + const myHeaders = new Headers(); + myHeaders.append('apikey', API_KEY); + if (authHeader) myHeaders.append('Authorization', authHeader); + const requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' }; - function toggleDropdown(id, e) { - e.stopPropagation(); - setOpenDropdownId(prev => (prev === id ? null : id)); - } + const res = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*", requestOptions); + const data = await res.json(); - /* (botao editar) */ - function handleOpenViewer(laudo) { - setOpenDropdownId(null); - if (isSecretary) { - // (notificação sem bloquear) - setShowNoPermission(true); - return; - } - setViewerLaudo(laudo); - } + const uniqueMap = new Map(); + (Array.isArray(data) ? data : []).forEach(r => { + if (r && r.id) uniqueMap.set(r.id, r); + }); + const unique = Array.from(uniqueMap.values()) + .sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)); - /* (botao imprimir) */ - function handlePrint(laudo) { - // evitar bug: fechar viewer antes de abrir preview - setViewerLaudo(null); - setPreviewLaudo(laudo); - setShowPreview(true); - setOpenDropdownId(null); - } - - /* (botao excluir) */ - function handleRequestDelete(laudo) { - 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."); + if (mounted) { + setRelatoriosOriginais(unique); + setRelatoriosFiltrados(unique); + setRelatoriosFinais(unique); + } + } catch (err) { + console.error('Erro listar relatórios', err); + if (mounted) { + setRelatoriosOriginais([]); + setRelatoriosFiltrados([]); + setRelatoriosFinais([]); + } } - } catch (err) { - alert("Erro de rede ao excluir."); - } finally { - setLoadingDelete(false); - } - } + }; - /* filtro de pesquisa (por pedido ou nome do paciente) */ - const normalized = (s = "") => String(s).toLowerCase(); - const filteredLaudos = laudos.filter(l => { - const q = normalized(query).trim(); - if (!q) return true; - if (normalized(l.pedido).includes(q)) return true; - if (normalized(l.paciente?.nome).includes(q)) return true; - return false; - }); + fetchReports(); + const refreshHandler = () => fetchReports(); + window.addEventListener('reports:refresh', refreshHandler); + return () => { + mounted = false; + window.removeEventListener('reports:refresh', refreshHandler); + }; + }, [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 ( -
    -
    -
    -
    -
    Gerenciamento de Laudo
    -
    -
    - -
    - setQuery(e.target.value)} - style={{ width:"100%", padding:12, borderRadius:8, border:"1px solid #e6eef8" }} - /> -
    - - {filteredLaudos.length === 0 ? ( -
    Nenhum laudo encontrado.
    - ) : ( -
    - {filteredLaudos.map((l) => ( -
    -
    -
    {l.pedido}
    -
    {l.data}
    +
    +

    Lista de Relatórios

    +
    +
    +
    +
    +
    +

    Relatórios Cadastrados

    +
    +
    -
    -
    {l.paciente.nome}
    -
    {l.paciente.cpf} • {l.paciente.convenio}
    -
    -
    {l.exame}
    -
    {l.solicitante}
    -
    {l.status}
    +
    -
    -
    toggleDropdown(l.id, e)} title="Ações"> - +
    +
    +
    + Filtros +
    +
    +
    +
    + + setTermoPesquisa(e.target.value)} + /> +
    +
    +
    +
    + + setFiltroExame(e.target.value)} + /> +
    +
    +
    + +
    +
    +
    + {relatoriosFinais.length} DE {relatoriosOriginais.length} RELATÓRIOS ENCONTRADOS +
    +
    +
    - {openDropdownId === l.id && ( -
    -
    handleOpenViewer(l)}>Editar
    -
    handlePrint(l)}>Imprimir
    -
    { alert("Protocolo de entrega: formulário (não implementado)."); setOpenDropdownId(null); }}>Protocolo de entrega
    -
    { alert("Liberar laudo: requer permissão de médico. (não implementado)"); setOpenDropdownId(null); }}>Liberar laudo
    -
    handleRequestDelete(l)} style={{ color:"#c23b3b" }}>Excluir laudo
    +
    + + + + + + + + + + + {relatoriosPaginados.length > 0 ? ( + relatoriosPaginados.map((relatorio, index) => { + const paciente = pacientesComRelatorios[index] || {}; + return ( + + + + + + + ); + }) + ) : ( + + )} + +
    PacienteCPFExame
    {paciente?.full_name || relatorio.patient_name || 'Carregando...'}{paciente?.cpf || 'Carregando...'}{relatorio.exam || relatorio.exame || '—'} +
    + + + + + {/* Removido o botão "Imprimir" daqui (agora no Ver Detalhes) */} + + + + +
    +
    Nenhum relatório encontrado.
    + + {relatoriosFinais.length > 0 && ( +
    +
    + Itens por página: + +
    + +
    + + Página {paginaAtual} de {totalPaginas} • + Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, relatoriosFinais.length)} de {relatoriosFinais.length} itens + + + +
    )}
    - ))} +
    - )} +
    - {/* Viewer modal (modo leitura) — só abre para quem tem permissão */} - {viewerLaudo && !showPreview && !isSecretary && ( -
    -
    setViewerLaudo(null)} /> -
    -
    -
    -
    {viewerLaudo.paciente.nome}
    -
    - Nasc.: {viewerLaudo.paciente.nascimento} • {computeAge(viewerLaudo.paciente.nascimento)} anos • {viewerLaudo.paciente.cpf} • {viewerLaudo.paciente.convenio} + {/* Modal principal (detalhes) */} + {showModal && relatorioModal && ( +
    setShowModal(false)}> +
    e.stopPropagation()}> +
    +
    +
    Relatório de {pacientesComRelatorios[modalIndex]?.full_name || relatorioModal.patient_name || 'Paciente'}
    + +
    + +
    +
    +
    +

    Clinica Rise up

    +

    Dr - CRM/SP 123456

    +

    Avenida - (79) 9 4444-4444

    +
    + +
    +

    Paciente: {pacientesComRelatorios[modalIndex]?.full_name || relatorioModal.patient_name || '—'}

    +

    Data de nascimento: {pacientesComRelatorios[modalIndex]?.birth_date || '—'}

    +

    Data do exame: {relatorioModal?.due_at || relatorioModal?.date || '—'}

    + +

    Conteúdo do Relatório:

    +
    + +
    +
    + +
    +

    Dr {medicosComRelatorios[modalIndex]?.full_name || relatorioModal?.requested_by || '—'}

    +

    Emitido em: {relatorioModal?.created_at || '—'}

    +
    -
    - - -
    -
    - -
    -
    B
    -
    I
    -
    U
    -
    Fonte
    -
    Tamanho
    -
    Lista
    -
    Campos
    -
    Modelos
    -
    Imagens
    -
    - -
    - {viewerLaudo.conteudo.split("\n").map((line, i) => ( -

    {line}

    - ))} -
    - -
    -
    - - - -
    - -
    - - +
    + +
    )} - {/* Preview modal — agora não bloqueia a tela (sem backdrop escuro), botão imprimir é interativo */} - {showPreview && previewLaudo && ( -
    -
    -
    -
    -
    Pré-visualização - {previewLaudo.paciente.nome}
    -
    - - -
    -
    - -
    -
    - RELATÓRIO MÉDICO -
    -
    - {previewLaudo.paciente.nome} • Nasc.: {previewLaudo.paciente.nascimento} • CPF: {previewLaudo.paciente.cpf} + {/* Modal Protocolo */} + {showProtocolModal && protocolForIndex && ( +
    setShowProtocolModal(false)}> +
    e.stopPropagation()}> +
    +
    +
    Protocolo de Entrega - {protocolForIndex.relatorio?.patient_name || 'Paciente'}
    +
    -
    - {previewLaudo.conteudo} +
    +
    +

    Pedido: {protocolForIndex.relatorio?.id || protocolForIndex.relatorio?.pedido}

    +

    Paciente: {protocolForIndex.relatorio?.patient_name || '—'}

    +

    Data: {protocolForIndex.relatorio?.due_at || protocolForIndex.relatorio?.date || '—'}

    +
    +

    Protocolo de entrega gerado automaticamente. (Substitua pelo endpoint real se houver)

    +
    +
    + +
    + +
    )} - {/* Notificação simples: Sem permissão (exibe sem backdrop escuro) - centralizada */} - {showNoPermission && ( -
    -
    Sem permissão para editar
    -
    Você está na visualização da secretaria. Edição disponível somente para médicos autorizados.
    -
    - -
    -
    - )} - - {/* Confirm delete modal (simples: Sim / Não) */} - {showConfirmDelete && toDelete && ( -
    -
    -
    Confirmar exclusão
    -
    Você tem certeza que quer excluir o laudo {toDelete.pedido} - {toDelete.paciente.nome} ? Esta ação é irreversível.
    - -
    - - + {/* Variável de aviso: mostra texto personalizado */} + {noPermissionText && ( +
    setNoPermissionText(null)}> +
    e.stopPropagation()}> +
    +
    +
    {noPermissionText}
    +

    {/* opcional descrição aqui */}

    +
    + +
    +
    )} +
    ); -} +}; -/* ===== Helpers ===== */ -function computeAge(birth) { - if (!birth) return "-"; - const [y,m,d] = birth.split("-").map(x => parseInt(x,10)); - if (!y) return "-"; - const today = new Date(); - let age = today.getFullYear() - y; - const mm = today.getMonth() + 1; - const dd = today.getDate(); - if (mm < m || (mm === m && dd < d)) age--; - return age; -} +export default LaudoManager; diff --git a/src/pages/secretaria/Dashboard.jsx b/src/pages/secretaria/Dashboard.jsx index f58fb11..3477964 100644 --- a/src/pages/secretaria/Dashboard.jsx +++ b/src/pages/secretaria/Dashboard.jsx @@ -1,26 +1,152 @@ //inicio.jsx //Nesta página falta: ajustar caminho do CSS -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useState, useEffect } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; 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() { const navigate = useNavigate(); + const { getAuthorizationHeader, isAuthenticated } = useAuth(); const [pacientes, setPacientes] = useState([]); + const [medicos, setMedicos] = 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 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 agendamentosDoDia = agendamentos.filter( - a => a.data && new Date(a.data).getDate() === hoje.getDate() - ); + hoje.setHours(0, 0, 0, 0); + + 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 pendencias = agendamentos.filter(a => a.status === 'pending' || a.status === 'scheduled').length; return (
    @@ -58,7 +184,7 @@ function Inicio() {
    PENDÊNCIAS - 0 + {loading ? '...' : pendencias}
    @@ -93,14 +219,54 @@ function Inicio() {

    Próximos Agendamentos

    - {agendamentosHoje > 0 ? ( -
    - {agendamentosDoDia.map(agendamento => ( + {loading ? ( +
    +

    Carregando agendamentos...

    +
    + ) : agendamentosHoje > 0 ? ( +
    + {agendamentosDoDia.slice(0, 5).map(agendamento => (
    -

    {agendamento.nomePaciente}

    -

    {new Date(agendamento.data).toLocaleTimeString()}

    +
    +
    +

    + {new Date(agendamento.scheduled_at).toLocaleTimeString('pt-BR', { + hour: '2-digit', + minute: '2-digit' + })} +

    +

    + {new Date(agendamento.scheduled_at).toLocaleDateString('pt-BR', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + })} +

    +
    +
    +

    + Paciente: {agendamento.nomePaciente} +

    +

    + Dr(a): {agendamento.nomeMedico} + {agendamento.especialidadeMedico && ` - ${agendamento.especialidadeMedico}`} +

    +
    + + {agendamento.status === 'scheduled' ? 'Agendado' : + agendamento.status === 'completed' ? 'Concluído' : + agendamento.status === 'pending' ? 'Pendente' : + agendamento.status === 'cancelled' ? 'Cancelado' : + agendamento.status === 'requested' ? '' : agendamento.status} + +
    ))} + {agendamentosHoje > 5 && ( + + )}
    ) : (
    diff --git a/src/pages/secretaria/DetalhesMedico.jsx b/src/pages/secretaria/DetalhesMedico.jsx index 44e3b50..dde5158 100644 --- a/src/pages/secretaria/DetalhesMedico.jsx +++ b/src/pages/secretaria/DetalhesMedico.jsx @@ -1,47 +1,33 @@ //DoctorDetails.jsx 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 { useAuth } from "../../_assets/utils/AuthProvider"; import avatarPlaceholder from '../../_assets/images/avatar_placeholder.png'; - -const Details = () => { + +const DoctorDetails = ({DictInfo}) => { const {getAuthorizationHeader} = useAuth(); - const [doctor, setDoctor] = useState({}); const Parametros = useParams() const navigate = useNavigate(); 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]; 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

    Carregando...

    ; - return ( <>

    MediConnect

    +
    - {doctor.full_name || "Nome Completo"} -

    {doctor.cpf || "CPF"}

    + {DictInfo.full_name || "Nome Completo"} +

    {DictInfo.cpf || "CPF"}

    - - -
    @@ -71,29 +55,29 @@ const Details = () => {
    -

    {doctor.full_name || "-"}

    +

    {DictInfo.full_name || "-"}

    -

    {doctor.birth_date || "-"}

    +

    {DictInfo.birth_date || "-"}

    -

    {doctor.cpf || "-"}

    +

    {DictInfo.cpf || "-"}

    -

    {doctor.crm || "-"}

    +

    {DictInfo.crm || "-"}

    -

    {doctor.crm_uf || "-"}

    +

    {DictInfo.crm_uf || "-"}

    -

    {doctor.specialty || "-"}

    +

    {DictInfo.specialty || "-"}

    @@ -105,31 +89,31 @@ const Details = () => {
    -

    {doctor.cep || "-"}

    +

    {DictInfo.cep || "-"}

    -

    {doctor.street || "-"}

    +

    {DictInfo.street || "-"}

    -

    {doctor.neighborhood || "-"}

    +

    {DictInfo.neighborhood || "-"}

    -

    {doctor.city || "-"}

    +

    {DictInfo.city || "-"}

    -

    {doctor.state || "-"}

    +

    {DictInfo.state || "-"}

    -

    {doctor.number || "-"}

    +

    {DictInfo.number || "-"}

    -

    {doctor.complement || "-"}

    +

    {DictInfo.complement || "-"}

    @@ -141,15 +125,15 @@ const Details = () => {
    -

    {doctor.email || "-"}

    +

    {DictInfo.email || "-"}

    -

    {doctor.phone_mobile || "-"}

    +

    {DictInfo.phone_mobile || "-"}

    -

    {doctor.phone2 || "-"}

    +

    {DictInfo.phone2 || "-"}

    @@ -158,4 +142,4 @@ const Details = () => { ); }; -export default Details; \ No newline at end of file +export default DoctorDetails; \ No newline at end of file diff --git a/src/pages/secretaria/DetalhesPaciente.jsx b/src/pages/secretaria/DetalhesPaciente.jsx index 43a2bba..865eeed 100644 --- a/src/pages/secretaria/DetalhesPaciente.jsx +++ b/src/pages/secretaria/DetalhesPaciente.jsx @@ -2,16 +2,18 @@ import { useEffect, useState } from "react"; 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 avatarPlaceholder from '../../_assets/images/avatar_placeholder.png'; + const Details = (DictInfo) => { const parametros = useParams(); const {getAuthorizationHeader, isAuthenticated} = useAuth(); const [paciente, setPaciente] = useState({}); const [anexos, setAnexos] = useState([]); + const [selectedFile, setSelectedFile] = useState(null); const location = useLocation(); const navigate = useNavigate(); const patientID = parametros.id @@ -21,6 +23,16 @@ const Details = (DictInfo) => { navigate(`/${prefixo}/pacientes`); } + +const navigateEdit = () => { + const prefixo = location.pathname.split("/")[1]; + navigate(`/${prefixo}/medicos/edit`); + } + + + + + useEffect(() => { if (!DictInfo) return; console.log(patientID, 'teu id') @@ -62,6 +74,7 @@ const Details = (DictInfo) => { <>

    MediConnect

    +
    - - -
    @@ -283,4 +294,4 @@ const Details = (DictInfo) => { ); }; -export default Details; +export default Details; \ No newline at end of file diff --git a/src/pages/secretaria/DisponibilidadesMedico.jsx b/src/pages/secretaria/DisponibilidadesMedico.jsx index c32cb75..a6c1e57 100644 --- a/src/pages/secretaria/DisponibilidadesMedico.jsx +++ b/src/pages/secretaria/DisponibilidadesMedico.jsx @@ -1,359 +1,440 @@ //DisponibilidadesDoctorPage.jsx -import { useState, useEffect, useCallback, useMemo } from "react"; -import { GetAllDoctors } from "../../_assets/utils/Functions-Endpoints/Doctor"; +import { useState, useEffect, useMemo } from "react"; import { useAuth } from "../../_assets/utils/AuthProvider"; import API_KEY from "../../_assets/utils/apiKeys"; import HorariosDisponibilidade from "../../components/medico/HorariosDisponibilidade"; +// import "./style/DisponibilidadesDoctorPage.css"; -const ENDPOINT = - "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability"; +const ENDPOINT = "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 { getAuthorizationHeader } = useAuth(); const [disponibilidades, setDisponibilidades] = useState([]); - const [loading, setLoading] = useState(false); const [doctors, setDoctors] = useState([]); const [searchTerm, setSearchTerm] = useState(""); - const [selectedDoctor, setSelectedDoctor] = 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(() => { const fetchDoctors = async () => { try { - setDoctorsLoading(true); - const data = await GetAllDoctors(); - console.log("Médicos recebidos:", data); - setDoctors(Array.isArray(data) ? data : []); + const requestOptions = { + method: "GET", + headers: getHeaders(), + }; + const response = await fetch(DOCTORS_ENDPOINT, requestOptions); + const result = await response.json(); + setDoctors(Array.isArray(result) ? result : []); } catch (error) { - console.error("Erro ao carregar médicos:", error); setDoctors([]); - } finally { - setDoctorsLoading(false); } }; fetchDoctors(); - }, []); - - 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); - } - }, []); + }, [getAuthorizationHeader]); useEffect(() => { - if (selectedDoctor) { - fetchDisponibilidades(selectedDoctor.id); - } else { - fetchDisponibilidades(null); - } - }, [selectedDoctor, fetchDisponibilidades]); + const fetchDisponibilidades = async () => { + try { + const res = await fetch(ENDPOINT, { method: "GET", headers: getHeaders() }); + if (res.ok) { + const data = await res.json(); + 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 { - const res = await fetch(`${ENDPOINT}?id=eq.${id}`, { - method: "PATCH", - headers: getHeaders(), - body: JSON.stringify(dadosAtualizados), + const headers = getHeaders(); + const promises = []; + 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}?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) { - alert("Disponibilidade atualizada com sucesso!"); - setEditando(null); - if (selectedDoctor) fetchDisponibilidades(selectedDoctor.id); - else fetchDisponibilidades(); - } else { - const errorData = await res.json(); - console.error("Erro na resposta:", errorData); - alert("Erro ao atualizar disponibilidade"); + const data = await res.json(); + setDisponibilidades(Array.isArray(data) ? data : []); } } catch (error) { - console.error("Erro:", error); - alert("Falha ao conectar com o servidor"); + console.error(error); } }; const deletarDisponibilidade = async (id) => { - if (!window.confirm("Deseja realmente excluir esta disponibilidade?")) - return; + if (!window.confirm("Deseja realmente excluir esta disponibilidade?")) return; try { - const res = await fetch(`${ENDPOINT}?id=eq.${id}`, { - method: "DELETE", - headers: getHeaders(), - }); - 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 res = await fetch(`${ENDPOINT}?id=eq.${id}`, { method: "DELETE", headers: getHeaders() }); + if (res.ok) setDisponibilidades((prev) => prev.filter((d) => d.id !== id)); + } catch (error) {} }; - const initialAvailabilityParaEdicao = useMemo( - () => - diasDaSemana.map((dia, weekdayIndex) => { - const blocosDoDia = disponibilidades - .filter((d) => d.weekday === weekdayIndex && d.active !== false) - .map((d) => ({ - id: d.id, - 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, - slot_minutes: d.slot_minutes || 30, - appointment_type: d.appointment_type || "presencial", - active: d.active !== false, - })); - return { - dia, - weekday: weekdayIndex, - isChecked: blocosDoDia.length > 0, - blocos: blocosDoDia, - }; - }), - [disponibilidades] - ); + const disponibilidadesAgrupadas = useMemo(() => { + const agrupadas = {}; + doctors.forEach((doctor) => { + agrupadas[doctor.id] = { + doctor_id: doctor.id, + doctor_name: doctor.full_name || doctor.name, + disponibilidades: [], + }; + }); + disponibilidades.forEach((disp) => { + if (agrupadas[disp.doctor_id]) agrupadas[disp.doctor_id].disponibilidades.push(disp); + }); + Object.values(agrupadas).forEach((grupo) => { + if (grupo.disponibilidades.length === 0) { + grupo.disponibilidades.push({ + id: `empty-${grupo.doctor_id}`, + doctor_id: grupo.doctor_id, + doctor_name: grupo.doctor_name, + is_empty: true, + }); + } + }); + 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 bloco = horariosAtualizados - .flatMap((d) => d.blocos) - .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); + if (!editando) return; + setAvailabilityEdit(horariosAtualizados || []); }; const filteredDoctors = useMemo(() => { if (!searchTerm) return doctors; - return doctors.filter((doc) => - doc.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); + return doctors.filter((doc) => (doc.full_name || doc.name).toLowerCase().includes(searchTerm.toLowerCase())); }, [doctors, searchTerm]); - return ( -
    -

    - Disponibilidades dos Médicos -

    + const handleCancelarEdicao = () => { + setEditando(null); + setAvailabilityEdit([]); + }; -
    - { - setSearchTerm(e.target.value); - setSelectedDoctor(null); - }} - style={{ - border: "1px solid #ccc", - borderRadius: "4px", - padding: "6px", - width: "300px", - }} - /> - {searchTerm && ( -
      { + setSearchTerm(doctor.full_name || doctor.name); + setShowSuggestions(false); + }; + + const handleClearSearch = () => { + setSearchTerm(""); + setShowSuggestions(false); + }; + + const getStatusBadgeClass = (disp) => { + if (disp.is_empty) return "status-badge status-not-configured"; + if (disp.active === false) return "status-badge status-inactive"; + return "status-badge status-active"; + }; + + const getStatusText = (disp) => { + if (disp.is_empty) return "Não configurado"; + if (disp.active === false) return "Inativa"; + return "Ativa"; + }; + + return ( +
      +

      Disponibilidades dos Médicos

      + +
      +
      + { + setSearchTerm(e.target.value); + setShowSuggestions(true); }} - > - {filteredDoctors.length > 0 ? ( - filteredDoctors.map((doc) => ( -
    • { - setSelectedDoctor(doc); - setSearchTerm(doc.name); - }} - style={{ - padding: "6px 8px", - cursor: "pointer", - borderBottom: "1px solid #eee", - }} - > - {doc.name} -
    • - )) - ) : ( -
    • - Nenhum médico encontrado -
    • - )} -
    + onFocus={() => setShowSuggestions(true)} + className="search-input" + /> + {searchTerm && ( + + )} +
    + + {showSuggestions && searchTerm && filteredDoctors.length > 0 && ( +
    + {filteredDoctors.map((doc) => ( +
    handleDoctorSelect(doc)} className="suggestion-item"> + {doc.full_name || doc.name} +
    + ))} +
    )}
    -

    - {editando ? "Editar Disponibilidade" : "Lista de Disponibilidades"}{" "} - ({disponibilidades.length}) -

    +

    {editando ? `Editar Horários` : "Lista de Disponibilidades"}

    - {loading ? ( -

    Carregando...

    - ) : disponibilidades.length === 0 ? ( -

    Nenhuma disponibilidade encontrada.

    + {doctors.length === 0 ? ( +

    Carregando médicos...

    ) : editando ? ( <> - - +
    + {initialAvailabilityParaEdicao.length > 0 ? ( + + ) : ( +

    Carregando horários para edição...

    + )} +
    + +
    + + + +
    ) : ( - - - - - - - - - - - - - - - {disponibilidades.map((disp) => { - const medico = doctors.find((d) => d.id === disp.doctor_id); - return ( - - - - - - - - - - - ); - })} - -
    MédicoDia da SemanaInícioTérminoIntervalo (min)TipoStatusAções
    {medico ? medico.name : disp.doctor_id}{diasDaSemana[disp.weekday]}{disp.start_time}{disp.end_time}{disp.slot_minutes || 30}{disp.appointment_type || "presencial"} - - {disp.active === false ? "Inativa" : "Ativa"} - - -
    - +
    + {disponibilidadesAgrupadas.length === 0 ? ( +

    Nenhum médico encontrado

    + ) : ( + disponibilidadesAgrupadas.map((grupo) => ( +
    +
    toggleExpandDoctor(grupo.doctor_id)}> +

    + {grupo.doctor_name} + ({grupo.disponibilidades.filter((d) => !d.is_empty).length} horários) +

    + +
    - + {expandedDoctors[grupo.doctor_id] && ( +
    +
    + +
    + +
    + + + + + + + + + + + + + + {grupo.disponibilidades.map((disp) => ( + + + + + + + + + + ))} + +
    Dia da SemanaInícioTérminoIntervalo (min)TipoStatusAções
    {disp.is_empty ? "Nenhum horário cadastrado" : getDiaSemana(disp.weekday)}{disp.is_empty ? "-" : formatTime(disp.start_time)}{disp.is_empty ? "-" : formatTime(disp.end_time)}{disp.is_empty ? "-" : disp.slot_minutes || 30}{disp.is_empty ? "-" : disp.appointment_type || "presencial"} + {getStatusText(disp)} + {!disp.is_empty && }
    +
    -
    + )} +
    + )) + )} +
    )}
    @@ -361,4 +442,4 @@ const DisponibilidadesDoctorPage = () => { ); }; -export default DisponibilidadesDoctorPage; +export default DisponibilidadesDoctorPage; \ No newline at end of file diff --git a/src/pages/secretaria/EditarMedico.jsx b/src/pages/secretaria/EditarMedico.jsx index cf2908c..1a43cbe 100644 --- a/src/pages/secretaria/EditarMedico.jsx +++ b/src/pages/secretaria/EditarMedico.jsx @@ -1,150 +1,337 @@ //DoctorEditPage.jsx //Nesta página falta: mudar nomes -import { useEffect, useState } from "react"; -import { useParams, useSearchParams } from "react-router-dom"; -import { GetDoctorByID } from "../../_assets/utils/Functions-Endpoints/Doctor"; +import { useState, useEffect, useMemo } from "react"; +import { useParams, useNavigate, useLocation } from "react-router-dom"; import { useAuth } from "../../_assets/utils/AuthProvider"; import API_KEY from "../../_assets/utils/apiKeys"; import DoctorForm from "../../components/medico/FormCadastroMedico"; -const ENDPOINT_AVAILABILITY = - "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability"; +const ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors"; +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 [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 [searchParams] = useSearchParams(); - const DoctorID = Parametros.id; - const availabilityId = searchParams.get("availabilityId"); + const effectiveId = id; - const [availabilityToPATCH, setAvailabilityToPATCH] = useState(null); - const [mode, setMode] = useState("doctor"); - - useEffect(() => { + const getHeaders = () => { + const myHeaders = new Headers(); const authHeader = getAuthorizationHeader(); - - 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); + 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; + }; - var raw = JSON.stringify(DoctorToPUT); - - console.log("Enviando médico para atualização (PUT):", DoctorToPUT); - - var requestOptions = { - method: "PUT", - headers: myHeaders, - body: raw, - redirect: "follow", - }; - + const salvarDisponibilidades = async (doctorId, horariosAtualizados) => { try { - const response = await fetch( - `https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?id=eq.${DoctorID}`, - requestOptions + const headers = getHeaders(); + const promises = []; + 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) { - console.error("Erro ao atualizar médico:", error); - alert("Erro ao atualizar dados do médico."); throw error; } }; - // 2. Função para Atualizar DISPONIBILIDADE (PATCH) - const HandlePatchAvailability = async (data) => { - const authHeader = getAuthorizationHeader(); + const normalizeAvailabilityForForm = (availabilityData) => { + if (!Array.isArray(availabilityData)) return []; - var myHeaders = new Headers(); - myHeaders.append("apikey", API_KEY); - myHeaders.append("Authorization", authHeader); - myHeaders.append("Content-Type", "application/json"); + const disponibilidadesMedico = availabilityData.filter((d) => + String(d.doctor_id) === String(effectiveId) && d.active !== false + ); + 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 = { - method: "PATCH", - headers: myHeaders, - body: raw, - redirect: "follow", + try { + const doctorResponse = await fetch(`${ENDPOINT}?id=eq.${effectiveId}`, { + method: "GET", + headers: getHeaders(), + }); + + 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 { - const response = await fetch( - `${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; + if (effectiveId) { + fetchDoctorData(); } + }, [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 ( +
    +
    +
    + Carregando... +
    +
    +

    + Carregando dados do médico ID: {effectiveId || "..."} +

    +
    + ); + } + + if (!doctor) { + if (!isLoading) { + return ( +
    +
    + Médico não encontrado +
    +
    + ); + } + return null; + } + + const formData = { + ...doctor, + availability: (doctor && doctor.availability) ? doctor.availability : availabilityFormatted, }; return ( -
    -

    - {mode === "availability" - ? `Editar Horário Disponível (ID: ${availabilityId.substring(0, 8)})` - : `Editar Médico (ID: ${DoctorID})`} -

    - - +
    +
    +
    +

    Editar Médico

    + +
    +
    ); }; -export default DoctorEditPage; +export default EditDoctorPage; \ No newline at end of file diff --git a/src/pages/secretaria/EditarPaciente.jsx b/src/pages/secretaria/EditarPaciente.jsx index 1cf7c84..68c83b2 100644 --- a/src/pages/secretaria/EditarPaciente.jsx +++ b/src/pages/secretaria/EditarPaciente.jsx @@ -9,34 +9,16 @@ import API_KEY from '../../_assets/utils/apiKeys' import PatientForm from '../../components/paciente/FormCadastroPaciente' - -const EditPage = (DictInfo) => { +const EditPage = ({DictInfo}) => { const navigate = useNavigate() - const Parametros = useParams() const [PatientToPUT, setPatientPUT] = useState({}) const { getAuthorizationHeader, isAuthenticated } = useAuth(); -console.log(DictInfo, "usuario vindo do set") -const PatientID = Parametros.id - - useEffect(() => { - const authHeader = getAuthorizationHeader() - 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)); - - - + setPatientPUT(DictInfo) }, [DictInfo]) - const HandlePutPatient = async () => { const authHeader = getAuthorizationHeader() @@ -46,9 +28,9 @@ const HandlePutPatient = async () => { myHeaders.append("Authorization", authHeader); 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 = { method: 'PATCH', @@ -57,26 +39,11 @@ const HandlePutPatient = async () => { redirect: 'follow' }; - try { - const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients?id=eq.${PatientID}`,requestOptions); - console.log(response) - + fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients?id=eq.${PatientToPUT.id}`,requestOptions) + .then(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 ( @@ -93,4 +60,4 @@ const HandlePutPatient = async () => { ) } -export default EditPage +export default EditPage \ No newline at end of file diff --git a/src/pages/secretaria/ListaAgendamentos.jsx b/src/pages/secretaria/ListaAgendamentos.jsx index 238225f..2022e82 100644 --- a/src/pages/secretaria/ListaAgendamentos.jsx +++ b/src/pages/secretaria/ListaAgendamentos.jsx @@ -3,7 +3,7 @@ import { useState, useMemo, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -// Importação de endpoints para lógica da Fila de Espera e Médicos (versão main) +import { Search, ChevronLeft, ChevronRight, Edit, Trash2 } from 'lucide-react'; import { GetPatientByID } from '../../_assets/utils/Functions-Endpoints/Patient.js'; import { GetAllDoctors, GetDoctorByID } from '../../_assets/utils/Functions-Endpoints/Doctor.js'; import { useAuth } from '../../_assets/utils/AuthProvider.js'; @@ -13,818 +13,455 @@ import AgendamentoCadastroManager from '../secretaria/CadastroAgendamento.jsx'; import TabelaAgendamentoDia from '../../components/agendamento/TabelaAgendamentoDia.jsx'; import TabelaAgendamentoSemana from '../../components/agendamento/TabelaAgendamentoSemana.jsx'; import TabelaAgendamentoMes from '../../components/agendamento/TabelaAgendamentoMes.jsx'; -import AgendamentosMes from '../../components/agendamento/DadosConsultasMock.js'; +import FormNovaConsulta from '../../components/agendamento/FormNovaConsulta.jsx'; +import CalendarComponent from '../../components/agendamento/Calendario.jsx'; import Spinner from '../../components/Spinner.jsx'; +// import "./style/Agendamento.css"; +// import './style/FilaEspera.css'; + import dayjs from 'dayjs'; -//import "./style/Agendamento.css"; -//import './style/FilaEspera.css'; +import 'dayjs/locale/pt-br'; +import isBetween from 'dayjs/plugin/isBetween'; +import localeData from 'dayjs/plugin/localeData'; + +dayjs.locale('pt-br'); +dayjs.extend(isBetween); +dayjs.extend(localeData); const Agendamento = ({ setDictInfo }) => { - const navigate = useNavigate(); - - const [listaTodosAgendamentos, setListaTodosAgendamentos] = useState([]) - - const [selectedID, setSelectedId] = useState('0') - const [filaEsperaData, setFilaEsperaData] = useState([]) - const [FiladeEspera, setFiladeEspera] = useState(false); - const [tabela, setTabela] = useState('diario'); - const [PageNovaConsulta, setPageConsulta] = useState(false); - const [searchTerm, setSearchTerm] = useState(''); - const [agendamentos, setAgendamentos] = useState() - const { getAuthorizationHeader } = useAuth() - const [DictAgendamentosOrganizados, setAgendamentosOrganizados] = useState({}) - - const [showDeleteModal, setShowDeleteModal] = useState(false) - const [AgendamentoFiltrado, setAgendamentoFiltrado] = useState() - - const [ListaDeMedicos, setListaDeMedicos] = useState([]) - const [FiltredTodosMedicos, setFiltredTodosMedicos] = useState([]) - const [searchTermDoctor, setSearchTermDoctor] = useState(''); - - const [MedicoFiltrado, setMedicoFiltrado] = useState({ id: "vazio" }) - - - const [cacheAgendamentos, setCacheAgendamentos] = useState([]) - - const [showConfirmModal, setShowConfirmModal] = useState(false) - const [motivoCancelamento, setMotivoCancelamento] = useState("") - - const [listaConsultasID, setListaConsultaID] = useState([]) - const [coresConsultas,setCoresConsultas] = useState([]) - - const [showSpinner,setShowSpinner] = useState(true) - - // Estados para Fila de Espera (padrão TablePaciente) - const [waitlistSearch, setWaitlistSearch] = useState(''); - const [waitSortKey, setWaitSortKey] = useState(null); // 'paciente' | 'medico' | 'data' | null - const [waitSortDir, setWaitSortDir] = useState('asc'); // 'asc' | 'desc' - const [waitPage, setWaitPage] = useState(1); - const [waitPerPage, setWaitPerPage] = useState(10); + const navigate = useNavigate(); + const [listaTodosAgendamentos, setListaTodosAgendamentos] = useState([]); + const [selectedID, setSelectedId] = useState('0'); + const [filaEsperaData, setFilaEsperaData] = useState([]); + const [FiladeEspera, setFiladeEspera] = useState(false); + const [PageNovaConsulta, setPageConsulta] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const { getAuthorizationHeader } = useAuth(); + const [DictAgendamentosOrganizados, setAgendamentosOrganizados] = useState({}); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [ListaDeMedicos, setListaDeMedicos] = useState([]); + const [FiltredTodosMedicos, setFiltredTodosMedicos] = useState([]); + const [searchTermDoctor, setSearchTermDoctor] = useState(''); + const [MedicoFiltrado, setMedicoFiltrado] = useState({ id: "vazio" }); + const [cacheAgendamentos, setCacheAgendamentos] = useState([]); + const [appointmentToEdit, setAppointmentToEdit] = useState(null); + const [showConfirmModal, setShowConfirmModal] = useState(false); + const [motivoCancelamento, setMotivoCancelamento] = useState(""); + const [showSpinner, setShowSpinner] = useState(true); + const [waitlistSearch, setWaitlistSearch] = useState(''); + const [waitSortKey, setWaitSortKey] = useState(null); + const [waitSortDir, setWaitSortDir] = useState('asc'); + const [waitPage, setWaitPage] = useState(1); + const [waitPerPage, setWaitPerPage] = useState(10); + const authHeader = getAuthorizationHeader(); + const cacheMedicos = useMemo(() => ({}), []); + const cachePacientes = useMemo(() => ({}), []); + const [currentDate, setCurrentDate] = useState(dayjs()); + const [selectedDay, setSelectedDay] = useState(dayjs()); - let authHeader = getAuthorizationHeader() - -const cacheMedicos = {}; -const cachePacientes = {}; - - useEffect(() => { - if (!listaTodosAgendamentos.length) return; - console.log("recarregando") - const DictAgendamentosOrganizados = {}; - const ListaFilaDeEspera = []; - - const fetchDados = async () => { - for (const agendamento of listaTodosAgendamentos) { - // Cache de médico e paciente - if (!cacheMedicos[agendamento.doctor_id]) { - cacheMedicos[agendamento.doctor_id] = await GetDoctorByID(agendamento.doctor_id, authHeader); - } - if (!cachePacientes[agendamento.patient_id]) { - cachePacientes[agendamento.patient_id] = await GetPatientByID(agendamento.patient_id, authHeader); - } - - const medico = cacheMedicos[agendamento.doctor_id]; - const paciente = cachePacientes[agendamento.patient_id]; - - if (agendamento.status === "requested") { - ListaFilaDeEspera.push({ - agendamento, - Infos: { - nome_medico: medico[0]?.full_name, - doctor_id: medico[0]?.id, - patient_id: paciente[0]?.id, - paciente_nome: paciente[0]?.full_name, - paciente_cpf: paciente[0]?.cpf, - }, - }); - } else { - const DiaAgendamento = agendamento.scheduled_at.split("T")[0]; - - let agendamentoMelhorado = {...agendamento, medico_nome: medico[0]?.full_name, - doctor_id: medico[0]?.id, - patient_id: paciente[0]?.id, - paciente_nome: paciente[0]?.full_name, - paciente_cpf: paciente[0]?.cpf, } - - if (DiaAgendamento in DictAgendamentosOrganizados) { - DictAgendamentosOrganizados[DiaAgendamento].push(agendamentoMelhorado); - } else { - DictAgendamentosOrganizados[DiaAgendamento] = [agendamentoMelhorado]; - } - } - } - - // Ordenar por data - for (const DiaAgendamento in DictAgendamentosOrganizados) { - DictAgendamentosOrganizados[DiaAgendamento].sort((a, b) => a.scheduled_at.localeCompare(b.scheduled_at)); - } - - const chavesOrdenadas = Object.keys(DictAgendamentosOrganizados).sort(); - - const DictAgendamentosFinal = {}; - for (const data of chavesOrdenadas) { - DictAgendamentosFinal[data] = DictAgendamentosOrganizados[data]; - } - - setAgendamentosOrganizados(DictAgendamentosFinal); - setFilaEsperaData(ListaFilaDeEspera); - }; - - fetchDados(); - }, [listaTodosAgendamentos]); // 👉 só recalcula quando a lista muda - - - useEffect(() => { - var myHeaders = new Headers(); - myHeaders.append("Authorization", authHeader); - myHeaders.append("apikey", API_KEY) - - var requestOptions = { - method: 'GET', - headers: myHeaders, - redirect: 'follow' - }; - - fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select&doctor_id&patient_id&status&scheduled_at&order&limit&offset", requestOptions) - .then(response => response.json()) - .then(result => { setListaTodosAgendamentos(result); console.log(result) }) - .catch(error => console.log('error', error)); - - const PegarTodosOsMedicos = async () => { - let lista = [] - const TodosOsMedicos = await GetAllDoctors(authHeader) - - for (let d = 0; TodosOsMedicos.length > d; d++) { - lista.push({ nomeMedico: TodosOsMedicos[d].full_name, idMedico: TodosOsMedicos[d].id }) - } - setListaDeMedicos(lista) - } - PegarTodosOsMedicos() - - }, []) - - - const deleteConsulta = (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":"cancelled", - "cancellation_reason": motivoCancelamento + const [editingAppointmentId, setEditingAppointmentId] = useState(null); + + const [quickJump, setQuickJump] = useState({ + month: currentDate.month(), + year: currentDate.year() }); - var requestOptions = { - method: 'PATCH', - headers: myHeaders, - body: raw, - redirect: 'follow' + + const fetchAppointments = async () => { + const myHeaders = new Headers(); myHeaders.append("Authorization", authHeader); myHeaders.append("apikey", API_KEY); + const requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' }; + try { + const res = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select=*", requestOptions); + const data = await res.json(); + setListaTodosAgendamentos(data); + } catch (err) { + console.error('Erro ao buscar agendamentos', err); + } }; - 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 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 filteredAgendamentos = useMemo(() => { - if (!searchTerm.trim()) { - return AgendamentosMes; - } - const lowerCaseSearchTerm = searchTerm.toLowerCase(); - const filteredData = {}; - - for (const semana in AgendamentosMes) { - filteredData[semana] = {}; - for (const dia in AgendamentosMes[semana]) { - filteredData[semana][dia] = AgendamentosMes[semana][dia].filter(agendamento => - agendamento.status === 'vazio' || - (agendamento.paciente && agendamento.paciente.toLowerCase().includes(lowerCaseSearchTerm)) - ); - } - } - return filteredData; - }, [searchTerm]); - - const ListarDiasdoMes = (ano, mes) => { - let segundas = []; let tercas = []; let quartas = []; let quintas = []; let sextas = [] - const base = dayjs(`${ano}-${mes}-01`) - const DiasnoMes = base.daysInMonth() - for (let d = 1; d <= DiasnoMes; d++) { - const data = dayjs(`${ano}-${mes}-${d}`) - const dia = data.format('dddd') - switch (dia) { - case 'Monday': segundas.push(d); break - case 'Tuesday': tercas.push(d); break - case 'Wednesday': quartas.push(d); break - case 'Thursday': quintas.push(d); break - case 'Friday': sextas.push(d); break - default: break - } - } - let ListaDiasDatas = { segundas: segundas, tercas: tercas, quartas: quartas, quintas: quintas, sextas: sextas } - return ListaDiasDatas - } - - - useEffect(() => { - console.log("mudou FiltredTodosMedicos:", FiltredTodosMedicos); - if (MedicoFiltrado.id != "vazio" ) { - const unicoMedico = MedicoFiltrado; - console.log(unicoMedico) - const idMedicoFiltrado = unicoMedico.idMedico; - console.log(`Médico único encontrado: ${unicoMedico.nomeMedico}. ID: ${idMedicoFiltrado}`); - - const agendamentosDoMedico = filtrarAgendamentosPorMedico( - DictAgendamentosOrganizados, - idMedicoFiltrado - ); - console.log(`Total de agendamentos filtrados para este médico: ${agendamentosDoMedico.length}`); - console.log("Lista completa de Agendamentos do Médico:", agendamentosDoMedico); - - //FiltrarAgendamentos(agendamentosDoMedico) - - setListaTodosAgendamentos(agendamentosDoMedico) - - } - }, [FiltredTodosMedicos, MedicoFiltrado]); - - const filtrarAgendamentosPorMedico = (dictAgendamentos, idMedicoFiltrado) => { - setCacheAgendamentos(DictAgendamentosOrganizados); - - const todasAsListasDeAgendamentos = Object.values(dictAgendamentos); - const todosOsAgendamentos = todasAsListasDeAgendamentos.flat(); - - const agendamentosFiltrados = todosOsAgendamentos.filter(agendamento => - agendamento.doctor_id === idMedicoFiltrado - ); - - return agendamentosFiltrados; - }; - - const handleSearchMedicos = (term) => { - setSearchTermDoctor(term); - if (term.trim() === '') { - if (MedicoFiltrado.id !== "vazio") { - console.log("Medico escolhido, mas vai ser apagado") - console.log(cacheAgendamentos, "cache ") - - } - - - setFiltredTodosMedicos([]); - setMedicoFiltrado({ id: "vazio" }) - - //2 FiltrarAgendamentos() - return; - } - if (FiltredTodosMedicos.length === 1) { - setMedicoFiltrado({ ...FiltredTodosMedicos[0] }) - } - - const filtered = ListaDeMedicos.filter(medico => - medico.nomeMedico.toLowerCase().includes(term.toLowerCase()) - ); - setFiltredTodosMedicos(filtered); -}; - -useEffect(() => { - setShowSpinner(false) -},[filaEsperaData]) - - // Filtrar Fila de Espera - const filaEsperaFiltrada = useMemo(() => { - if (!waitlistSearch.trim()) return filaEsperaData; - const term = waitlistSearch.toLowerCase(); - return filaEsperaData.filter(item => { - const paciente = item?.Infos?.paciente_nome?.toLowerCase() || ''; - const cpf = item?.Infos?.paciente_cpf?.toLowerCase() || ''; - const medico = item?.Infos?.nome_medico?.toLowerCase() || ''; - return paciente.includes(term) || cpf.includes(term) || medico.includes(term); - }); - }, [waitlistSearch, filaEsperaData]); - - // Ordenar Fila de Espera - const applySortingWaitlist = (arr) => { - if (!Array.isArray(arr) || !waitSortKey) return arr; - const copy = [...arr]; - if (waitSortKey === 'paciente') { - copy.sort((a, b) => (a?.Infos?.paciente_nome || '').localeCompare((b?.Infos?.paciente_nome || ''), undefined, { sensitivity: 'base' })); - } else if (waitSortKey === 'medico') { - copy.sort((a, b) => (a?.Infos?.nome_medico || '').localeCompare((b?.Infos?.nome_medico || ''), undefined, { sensitivity: 'base' })); - } else if (waitSortKey === 'data') { - copy.sort((a, b) => new Date(a?.agendamento?.scheduled_at || 0) - new Date(b?.agendamento?.scheduled_at || 0)); - } - if (waitSortDir === 'desc') copy.reverse(); - return copy; - }; - - const filaEsperaOrdenada = applySortingWaitlist(filaEsperaFiltrada); - - // Paginação Fila de Espera - const waitTotalPages = Math.ceil(filaEsperaFiltrada.length / waitPerPage) || 1; - const waitIndiceInicial = (waitPage - 1) * waitPerPage; - const waitIndiceFinal = waitIndiceInicial + waitPerPage; - const filaEsperaPaginada = filaEsperaOrdenada.slice(waitIndiceInicial, waitIndiceFinal); - - const gerarNumerosWaitPages = () => { - const paginas = []; - const paginasParaMostrar = 5; - let inicio = Math.max(1, waitPage - Math.floor(paginasParaMostrar / 2)); - let fim = Math.min(waitTotalPages, inicio + paginasParaMostrar - 1); - inicio = Math.max(1, fim - paginasParaMostrar + 1); - for (let i = inicio; i <= fim; i++) { - paginas.push(i); - } - return paginas; - }; - - // Reset página quando filtros/ordenação mudarem - useEffect(() => { - setWaitPage(1); - }, [waitlistSearch, waitSortKey, waitSortDir]); - - const limparFiltrosWaitlist = () => { - setWaitlistSearch(''); - setWaitSortKey(null); - setWaitSortDir('asc'); - setWaitPage(1); - }; - - return ( -
    - -

    Agendar nova consulta

    - -
    - - - - - -
    - - {!PageNovaConsulta ? ( -
    -
    - - {/* Bloco de busca por médico */} -
    -
    -
    - -
    - handleSearchMedicos(e.target.value)} - /> -
    -
    - - {/* DROPDOWN (RENDERIZAÇÃO CONDICIONAL) */} - {searchTermDoctor && FiltredTodosMedicos.length > 0 && ( -
    - {FiltredTodosMedicos.map((medico) => ( -
    { - setSearchTermDoctor(medico.nomeMedico); - setFiltredTodosMedicos([]); - setMedicoFiltrado(medico) - - }} - > -

    {medico.nomeMedico}

    -
    - ))} -
    - )} -
    -
    - -
    - -
    - - -
    - -
    - {FiladeEspera === false ? - ( -
    -
    -
    -
    - - - -
    -
    -
    Realizado
    -
    Confirmado
    -
    Agendado
    -
    Cancelado
    -
    -
    - - {/* Componentes de Tabela - Adicionado props de delete da main */} - {tabela === "diario" && } - {tabela === 'semanal' && } - {tabela === 'mensal' && } -
    -
    - ) - : - ( -
    -
    -
    -
    -
    -

    Fila de Espera

    -
    -
    - {/* Filtros */} -
    -
    - Filtros -
    - -
    - setWaitlistSearch(e.target.value)} - /> - - Digite o nome do paciente, CPF ou nome do médico - -
    - -
    - {/* Ordenação rápida */} -
    - Ordenar por: - {(() => { - const sortValue = waitSortKey ? `${waitSortKey}-${waitSortDir}` : ''; - return ( - - ); - })()} -
    -
    - -
    -
    - {filaEsperaFiltrada.length} DE {filaEsperaData.length} SOLICITAÇÕES ENCONTRADAS -
    -
    -
    -
    - - - - - - - - - - - - {filaEsperaPaginada.length > 0 ? ( - filaEsperaPaginada.map((item, index) => ( - - - - - - - - )) - ) : ( - - - - )} - -
    Nome do PacienteCPFMédico SolicitadoData da SolicitaçãoAções
    {item?.Infos?.paciente_nome}{item?.Infos?.paciente_cpf}{item?.Infos?.nome_medico}{dayjs(item.agendamento.scheduled_at).format('DD/MM/YYYY')} - -
    -
    - {showSpinner ? ( - - ) : ( - <> - -

    Nenhuma solicitação encontrada.

    - - )} -
    -
    - - {/* Paginação */} - {filaEsperaFiltrada.length > 0 && ( -
    -
    - Itens por página: - -
    - -
    - - Página {waitPage} de {waitTotalPages} • - Mostrando {waitIndiceInicial + 1}-{Math.min(waitIndiceFinal, filaEsperaFiltrada.length)} de {filaEsperaFiltrada.length} - - - -
    -
    - )} -
    -
    -
    -
    -
    -
    - ) + const updateAppointmentStatus = async (id, updates) => { + const myHeaders = new Headers(); myHeaders.append("Authorization", authHeader); myHeaders.append("apikey", API_KEY); myHeaders.append("Content-Type", "application/json"); myHeaders.append("Prefer", "return=representation"); + 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) { + await fetchAppointments(); + return true; + } else { + console.error('Erro ao atualizar agendamento:', await response.text()); + return false; } -
    -
    - ) : ( - - )} + } catch (error) { + console.error('Erro de rede/servidor:', error); + return false; + } + }; - {/* Modal de Confirmação de Exclusão */} - {showDeleteModal && ( -
    - e.target.classList.contains("modal") && setShowDeleteModal(false) - } - > -
    -
    -
    -
    - Confirmação de Cancelamento -
    - -
    + const deleteConsulta = async (id) => { + setShowSpinner(true); + const success = await updateAppointmentStatus(id, { status: "cancelled", cancellation_reason: motivoCancelamento, updated_at: new Date().toISOString() }); + setShowSpinner(false); + if (success) { + setShowDeleteModal(false); + setMotivoCancelamento(""); + setSelectedId('0'); + } else { + alert("Falha ao cancelar a consulta."); + } + }; + + const confirmConsulta = async (id) => { + setShowSpinner(true); + const success = await updateAppointmentStatus(id, { status: "agendado", cancellation_reason: null, updated_at: new Date().toISOString() }); + setShowSpinner(false); + if (success) { + setShowConfirmModal(false); + setSelectedId('0'); + } else { + alert("Falha ao reverter o cancelamento."); + } + }; -
    -

    - Qual o motivo do cancelamento? -

    -
    - +
    +
    + + +
    -
    - -
    - - - - - -
    -
    -
    )} - - {showConfirmModal &&( -
    - e.target.classList.contains("modal") && setShowDeleteModal(false) - } - > -
    -
    - -
    -
    - Confirmação de edição -
    - -
    - -
    -

    - Tem certeza que deseja retirar o cancelamento ? -

    -
    - -
    - - - - - -
    -
    -
    - )} + ); -
    - ) + + const ConfirmEditModal = () => ( +
    +
    +
    +
    +
    Confirmação de edição
    + +
    +
    +

    Tem certeza que deseja retirar o cancelamento?

    + Isso reverterá o status do agendamento para Agendado. +
    +
    + + +
    +
    +
    +
    + ); + + + + return ( +
    +

    Agendar nova consulta

    +
    + {/* LIMPA O OBJETO DE EDIÇÃO AO CLICAR EM "ADICIONAR" */} + + + +
    + {!PageNovaConsulta ? ( +
    +
    +
    +
    +
    +
    + handleSearchMedicos(e.target.value)} /> +
    +
    + {searchTermDoctor && FiltredTodosMedicos.length > 0 && (
    {FiltredTodosMedicos.map((medico) => (
    { setSearchTermDoctor(medico.nomeMedico); setFiltredTodosMedicos([]); setMedicoFiltrado(medico); }}>

    {medico.nomeMedico}

    ))}
    )} +
    +
    +
    +
    + + +
    +
    + {FiladeEspera === false ? ( +
    +
    +
    {selectedDay.format('MMM')}{selectedDay.format('DD')}
    +

    {selectedDay.format('dddd')}

    {selectedDay.format('D [de] MMMM [de] YYYY')}

    +
    +

    Consultas para {selectedDay.format('DD/MM')}

    + {showSpinner ? : (DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')]?.length > 0) ? (DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')].map(app => ( +
    +
    {dayjs(app.scheduled_at).format('HH:mm')}
    +
    {app.paciente_nome}Dr(a). {app.medico_nome}
    +
    + {app.status === 'cancelled' ? ( + + ) : ( + <> + {/* MUDANÇA: PASSA O OBJETO COMPLETO 'app' PARA O ESTADO DE EDIÇÃO */} + + {/* Botão de Cancelar */} + + + )} +
    +
    + ))) : (

    Nenhuma consulta agendada.

    )} +
    +
    +
    +
    +
    Realizado
    Confirmado
    Agendado
    Cancelado
    +
    +
    +
    +

    {currentDate.format('MMMM [de] YYYY')}

    +
    + + + +
    +
    + +
    + + + +
    +
    + +
    + {weekDays.map(day =>
    {day}
    )} + {dateGrid.map((day, index) => { + const appointmentsOnDay = DictAgendamentosOrganizados[day.format('YYYY-MM-DD')] || []; + const cellClasses = `day-cell ${day.isSame(currentDate, 'month') ? 'current-month' : 'other-month'} ${day.isSame(dayjs(), 'day') ? 'today' : ''} ${day.isSame(selectedDay, 'day') ? 'selected' : ''}`; + return (
    handleDateClick(day)}>{day.format('D')}{appointmentsOnDay.length > 0 &&
    {appointmentsOnDay.length}
    }
    ); + })} +
    +
    +
    + ) : ( +
    +
    +
    +
    +

    Fila de Espera

    +
    +
    +
    Filtros
    +
    setWaitlistSearch(e.target.value)} />Digite o nome do paciente, CPF ou nome do médico
    +
    +
    + Ordenar por: + {(() => { const sortValue = waitSortKey ? `${waitSortKey}-${waitSortDir}` : ''; return ();})()} +
    +
    +
    {filaEsperaFiltrada.length} DE {filaEsperaData.length} SOLICITAÇÕES ENCONTRADAS
    +
    +
    + + + + {filaEsperaPaginada.length > 0 ? (filaEsperaPaginada.map((item, index) => ( + + ))) : ()} + +
    Nome do PacienteCPFMédico SolicitadoData da SolicitaçãoAções
    {item?.Infos?.paciente_nome}{item?.Infos?.paciente_cpf}{item?.Infos?.nome_medico}{dayjs(item.agendamento.scheduled_at).format('DD/MM/YYYY')}
    {showSpinner ? : (<>

    Nenhuma solicitação encontrada.

    )}
    + {filaEsperaFiltrada.length > 0 && (
    +
    Itens por página:
    +
    Página {waitPage} de {waitTotalPages} • Mostrando {waitIndiceInicial + 1}-{Math.min(waitIndiceFinal, filaEsperaFiltrada.length)} de {filaEsperaFiltrada.length}
    +
    )} +
    +
    +
    +
    +
    +
    + )} +
    +
    + ) : ( + + + )} + {showDeleteModal && } + {showConfirmModal && } +
    + ) } - + export default Agendamento; \ No newline at end of file diff --git a/src/pages/secretaria/ListaMedicos.jsx b/src/pages/secretaria/ListaMedicos.jsx index 4a7e733..2223f28 100644 --- a/src/pages/secretaria/ListaMedicos.jsx +++ b/src/pages/secretaria/ListaMedicos.jsx @@ -5,9 +5,10 @@ import { useState, useEffect } from "react"; import { Link } from "react-router-dom"; import { useAuth } from "../../_assets/utils/AuthProvider"; import API_KEY from "../../_assets/utils/apiKeys"; + //import "./style/TableDoctor.css"; -function TableDoctor() { +function TableDoctor({setDictInfo}) { const { getAuthorizationHeader, isAuthenticated } = useAuth(); const [medicos, setMedicos] = useState([]); @@ -150,7 +151,7 @@ function TableDoctor() { return resultado; }) : []; - // Aplica ordenação rápida + const applySorting = (arr) => { if (!Array.isArray(arr) || !sortKey) return arr; const copy = [...arr]; @@ -216,7 +217,7 @@ function TableDoctor() {
    -
    +

    Médicos Cadastrados

    @@ -440,14 +441,14 @@ function TableDoctor() { {medico.email || 'Não informado'}
    - - - - diff --git a/src/pages/usuario/Login.jsx b/src/pages/usuario/Login.jsx index eb9800e..a1cf907 100644 --- a/src/pages/usuario/Login.jsx +++ b/src/pages/usuario/Login.jsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; import { useAuth } from "../../_assets/utils/AuthProvider"; import { UserInfos } from "../../_assets/utils/Functions-Endpoints/General"; @@ -118,7 +118,8 @@ function Login() { if (data.access_token) { const UserData = await UserInfos(`bearer ${data.access_token}`); console.log(UserData, "Dados do usuário"); - + localStorage.setItem("roleUser", UserData.roles) + if (UserData?.roles?.includes("admin")) { navigate(`/admin/`); } else if (UserData?.roles?.includes("secretaria")) { @@ -131,7 +132,7 @@ function Login() { navigate(`/paciente/`); } }else{ - console.log("ERROROROROROOR") + console.log("Erro na tentativa de login") setShowCabecalho(true) } } else { @@ -251,4 +252,4 @@ function Login() { ); } -export default Login; +export default Login; \ No newline at end of file