diff --git a/package-lock.json b/package-lock.json index bb50c8d..a672c1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "express": "^5.1.0", "firebase": "^12.5.0", "flatpickr": "^4.6.13", + "helmet": "^8.1.0", "html2pdf.js": "^0.12.1", "lucide-react": "^0.543.0", "node-fetch": "^3.3.2", @@ -25979,6 +25980,15 @@ "he": "bin/he" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", diff --git a/package.json b/package.json index b12170f..d9aad38 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "express": "^5.1.0", "firebase": "^12.5.0", "flatpickr": "^4.6.13", + "helmet": "^8.1.0", "html2pdf.js": "^0.12.1", "lucide-react": "^0.543.0", "node-fetch": "^3.3.2", diff --git a/src/PagesPaciente/ConsultasPaciente.jsx b/src/PagesPaciente/ConsultasPaciente.jsx index 24a4811..45de7a0 100644 --- a/src/PagesPaciente/ConsultasPaciente.jsx +++ b/src/PagesPaciente/ConsultasPaciente.jsx @@ -1,429 +1,371 @@ import React, { useState, useMemo, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import API_KEY from '../components/utils/apiKeys.js'; -import AgendamentoCadastroManager from '../pages/AgendamentoCadastroManager.jsx'; +import AgendamentoCadastroManager from '../pages/AgendamentoCadastroManager.jsx'; import { useAuth } from '../components/utils/AuthProvider.js'; import dayjs from 'dayjs'; import 'dayjs/locale/pt-br'; import isBetween from 'dayjs/plugin/isBetween'; -import localeData from 'dayjs/plugin/localeData'; -import { ChevronLeft, ChevronRight, Edit, Trash2 } from 'lucide-react'; -import "../pages/style/Agendamento.css"; +import localeData from 'dayjs/plugin/localeData'; +import { ChevronLeft, ChevronRight, Edit, Trash2 } from 'lucide-react'; +import "../pages/style/Agendamento.css"; import '../pages/style/FilaEspera.css'; import Spinner from '../components/Spinner.jsx'; - dayjs.locale('pt-br'); dayjs.extend(isBetween); -dayjs.extend(localeData); - +dayjs.extend(localeData); const Agendamento = ({ setDictInfo }) => { - const navigate = useNavigate(); - const { getAuthorizationHeader, user } = useAuth(); + const navigate = useNavigate(); + const { getAuthorizationHeader, user } = useAuth(); + const [isLoading, setIsLoading] = useState(true); + const [DictAgendamentosOrganizados, setDictAgendamentosOrganizados] = useState({}); + const [filaEsperaData, setFilaDeEsperaData] = useState([]); + const [FiladeEspera, setFiladeEspera] = useState(false); + const [PageNovaConsulta, setPageConsulta] = useState(false); - - const [isLoading, setIsLoading] = useState(true); - const [DictAgendamentosOrganizados, setDictAgendamentosOrganizados] = useState({}); - - - const [filaEsperaData, setFilaDeEsperaData] = useState([]); - - const [FiladeEspera, setFiladeEspera] = useState(false); - const [PageNovaConsulta, setPageConsulta] = useState(false); - + const [currentDate, setCurrentDate] = useState(dayjs()); + const [selectedDay, setSelectedDay] = useState(dayjs()); + const [quickJump, setQuickJump] = useState({ + month: currentDate.month(), + year: currentDate.year() + }); - const [currentDate, setCurrentDate] = useState(dayjs()); - const [selectedDay, setSelectedDay] = useState(dayjs()); - const [quickJump, setQuickJump] = useState({ - month: currentDate.month(), - year: currentDate.year() + const [isCancelModalOpen, setIsCancelModalOpen] = useState(false); + const [appointmentToCancel, setAppointmentToCancel] = useState(null); + const [cancellationReason, setCancellationReason] = useState(''); + + const authHeader = useMemo(() => getAuthorizationHeader(), [getAuthorizationHeader]); + + useEffect(() => { + const carregarDados = async () => { + const patientId = user?.patient_id || "6e7f8829-0574-42df-9290-8dbb70f75ada"; + + if (!authHeader) { + console.warn("Header de autorização não disponível."); + setIsLoading(false); + return; + } + + setIsLoading(true); + try { + const myHeaders = new Headers({ "Authorization": authHeader, "apikey": API_KEY }); + const requestOptions = { method: 'GET', headers: myHeaders }; + const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select=*,doctors(full_name)&patient_id=eq.${patientId}`, requestOptions); + + if (!response.ok) throw new Error(`Erro na requisição: ${response.statusText}`); + + const consultasBrutas = await response.json() || []; + + const newDict = {}; + const newFila = []; + + for (const agendamento of consultasBrutas) { + const agendamentoMelhorado = { + ...agendamento, + medico_nome: agendamento.doctors?.full_name || 'Médico não informado' + }; + + 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].push(agendamentoMelhorado); + } else { + newDict[diaAgendamento] = [agendamentoMelhorado]; + } + } + } + + for (const key in newDict) { + newDict[key].sort((a, b) => a.scheduled_at.localeCompare(b.scheduled_at)); + } + + setDictAgendamentosOrganizados(newDict); + setFilaDeEsperaData(newFila); + + } catch (err) { + console.error('Falha ao buscar ou processar agendamentos:', err); + setDictAgendamentosOrganizados({}); + setFilaDeEsperaData([]); + } finally { + setIsLoading(false); + } + }; + + carregarDados(); + }, [authHeader, user]); + + const updateAppointmentStatus = async (id, updates) => { + const myHeaders = new Headers({ + "Authorization": authHeader, "apikey": API_KEY, "Content-Type": "application/json" + }); + const requestOptions = { method: 'PATCH', headers: myHeaders, body: JSON.stringify(updates) }; + + try { + const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${id}`, requestOptions); + if (!response.ok) throw new Error('Falha ao atualizar o status.'); + return true; + } catch (error) { + console.error('Erro de rede/servidor:', error); + return false; + } + }; + + const handleCancelClick = (appointmentId) => { + setAppointmentToCancel(appointmentId); + setCancellationReason(''); + setIsCancelModalOpen(true); + }; + + const executeCancellation = async () => { + if (!appointmentToCancel) return; + setIsLoading(true); + const motivo = cancellationReason.trim() || "Cancelado pelo paciente (motivo não especificado)"; + const success = await updateAppointmentStatus(appointmentToCancel, { + status: "cancelled", + cancellation_reason: motivo, + updated_at: new Date().toISOString() }); + setIsCancelModalOpen(false); + setAppointmentToCancel(null); + setCancellationReason(''); - - const [isCancelModalOpen, setIsCancelModalOpen] = useState(false); - const [appointmentToCancel, setAppointmentToCancel] = useState(null); - const [cancellationReason, setCancellationReason] = useState(''); - - const authHeader = useMemo(() => getAuthorizationHeader(), [getAuthorizationHeader]); + if (success) { + alert("Solicitação cancelada com sucesso!"); - - - useEffect(() => { - const carregarDados = async () => { - - const patientId = user?.patient_id || "6e7f8829-0574-42df-9290-8dbb70f75ada"; - - if (!authHeader) { - console.warn("Header de autorização não disponível."); - setIsLoading(false); - return; - } - - - setIsLoading(true); - try { - - const myHeaders = new Headers({ "Authorization": authHeader, "apikey": API_KEY }); - const requestOptions = { method: 'GET', headers: myHeaders }; - const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select=*,doctors(full_name)&patient_id=eq.${patientId}`, requestOptions); - - if (!response.ok) throw new Error(`Erro na requisição: ${response.statusText}`); - - const consultasBrutas = await response.json() || []; - - - - const newDict = {}; - const newFila = []; - - - for (const agendamento of consultasBrutas) { - const agendamentoMelhorado = { - ...agendamento, - medico_nome: agendamento.doctors?.full_name || 'Médico não informado' - }; - - 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].push(agendamentoMelhorado); - } else { - newDict[diaAgendamento] = [agendamentoMelhorado]; - } - } - } - - for (const key in newDict) { - newDict[key].sort((a, b) => a.scheduled_at.localeCompare(b.scheduled_at)); - } - - setDictAgendamentosOrganizados(newDict); - setFilaDeEsperaData(newFila); - - - } catch (err) { - console.error('Falha ao buscar ou processar agendamentos:', err); - setDictAgendamentosOrganizados({}); - setFilaDeEsperaData([]); - } finally { - setIsLoading(false); - } - }; - - - carregarDados(); - }, [authHeader, user]); - - - const updateAppointmentStatus = async (id, updates) => { - const myHeaders = new Headers({ - "Authorization": authHeader, "apikey": API_KEY, "Content-Type": "application/json" - }); - const requestOptions = { method: 'PATCH', headers: myHeaders, body: JSON.stringify(updates) }; - - try { - const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${id}`, requestOptions); - if (!response.ok) throw new Error('Falha ao atualizar o status.'); - return true; - } catch (error) { - console.error('Erro de rede/servidor:', error); - return false; + setDictAgendamentosOrganizados(prev => { + const newDict = { ...prev }; + for (const date in newDict) { + newDict[date] = newDict[date].filter(app => app.id !== appointmentToCancel); } - }; - - - const handleCancelClick = (appointmentId) => { - setAppointmentToCancel(appointmentId); - setCancellationReason(''); - setIsCancelModalOpen(true); - }; - - - - const executeCancellation = async () => { - if (!appointmentToCancel) return; - - setIsLoading(true); - - - const motivo = cancellationReason.trim() || "Cancelado pelo paciente (motivo não especificado)"; - - const success = await updateAppointmentStatus(appointmentToCancel, { - status: "cancelled", - cancellation_reason: motivo, - updated_at: new Date().toISOString() - }); - - - setIsCancelModalOpen(false); - setAppointmentToCancel(null); - setCancellationReason(''); - - - if (success) { - alert("Solicitação cancelada com sucesso!"); - - setDictAgendamentosOrganizados(prev => { - const newDict = { ...prev }; - for (const date in newDict) { - newDict[date] = newDict[date].filter(app => app.id !== appointmentToCancel); - } - return newDict; - }); - setFilaDeEsperaData(prev => prev.filter(item => item.agendamento.id !== appointmentToCancel)); - } else { - alert("Falha ao cancelar a solicitação."); - } - setIsLoading(false); - }; - - - const handleQuickJumpChange = (type, value) => setQuickJump(prev => ({ ...prev, [type]: Number(value) })); - const applyQuickJump = () => { - const newDate = dayjs().year(quickJump.year).month(quickJump.month).date(1); - setCurrentDate(newDate); - setSelectedDay(newDate); - }; - const dateGrid = useMemo(() => { - 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; - }, [currentDate]); - const weekDays = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb']; - const handleDateClick = (day) => setSelectedDay(day); - - const activeButtonStyle = { - backgroundColor: '#1B2A41', - color: 'white', - padding: '6px 12px', - fontSize: '0.875rem', - borderRadius: '8px', - border: '1px solid white', - display: 'flex', - alignItems: 'center', - gap: '8px', - cursor: 'pointer' - }; - - const inactiveButtonStyle = { - backgroundColor: '#1B2A41', - color: 'white', - padding: '6px 12px', - fontSize: '0.875rem', - borderRadius: '8px', - border: '1px solid #1B2A41', - display: 'flex', - alignItems: 'center', - gap: '8px', - cursor: 'pointer' - }; - - - - if (isLoading) { - return ( -
- -
- ); + return newDict; + }); + setFilaDeEsperaData(prev => prev.filter(item => item.agendamento.id !== appointmentToCancel)); + } else { + alert("Falha ao cancelar a solicitação."); } + setIsLoading(false); + }; + const handleQuickJumpChange = (type, value) => setQuickJump(prev => ({ ...prev, [type]: Number(value) })); + const applyQuickJump = () => { + const newDate = dayjs().year(quickJump.year).month(quickJump.month).date(1); + setCurrentDate(newDate); + setSelectedDay(newDate); + }; + const dateGrid = useMemo(() => { + 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; + }, [currentDate]); + const weekDays = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb']; + const handleDateClick = (day) => setSelectedDay(day); + if (isLoading) { return ( -
-

Minhas consultas

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

{selectedDay.format('dddd')}

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

-
-

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

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

Nenhuma consulta agendada para esta data.

)} -
-
-
-
-
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}
} -
- ); - })} -
-
-
- ) : ( -
-
-
-
-

Minhas Solicitações em Fila de Espera

-
-
- - - - - - - - - - {filaEsperaData.length > 0 ? (filaEsperaData.map((item) => ( - - - - - - ))) : ( - - - - )} - -
Médico SolicitadoData da SolicitaçãoAções
Dr(a). {item.Infos?.medico_nome}{dayjs(item.agendamento.created_at).format('DD/MM/YYYY HH:mm')} - -
-
Nenhuma solicitação na fila de espera.
-
-
-
-
-
-
-
- )} -
+
+ +
+ ); + } + + return ( +
+

Minhas consultas

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

{selectedDay.format('dddd')}

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

+
+

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

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

Nenhuma consulta agendada para esta data.

)} +
- ) : ( - - )} - - - {} - {isCancelModalOpen && ( -
-
-
-

Confirmação de Cancelamento

- -
-
-

Qual o motivo do cancelamento?

- -
-
- - -
+
+
+
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}
} +
+ ); + })} +
+
+ ) : ( +
+
+
+
+

Minhas Solicitações em Fila de Espera

+
+
+ + + + + + + + + + {filaEsperaData.length > 0 ? (filaEsperaData.map((item) => ( + + + + + + ))) : ( + + + + )} + +
Médico SolicitadoData da SolicitaçãoAções
Dr(a). {item.Infos?.medico_nome}{dayjs(item.agendamento.created_at).format('DD/MM/YYYY HH:mm')} + +
+
Nenhuma solicitação na fila de espera.
+
+
+
+
+
+
+
)} - {} +
- ) + ) : ( + + )} + + {isCancelModalOpen && ( +
+
+
+

Confirmação de Cancelamento

+ +
+
+

Qual o motivo do cancelamento?

+ +
+
+ + +
+
+
+ )} +
+ ) } - export default Agendamento; diff --git a/src/components/Header/Header.css b/src/components/Header/Header.css index f51e931..9ff5a50 100644 --- a/src/components/Header/Header.css +++ b/src/components/Header/Header.css @@ -487,4 +487,15 @@ width: calc(100vw - 20px); max-width: none; } -} \ No newline at end of file +} + +/* 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; +} diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index b1d2806..45b4d56 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -1,8 +1,11 @@ +// src/components/Header/Header.jsx import React, { useState, useRef, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { createPortal } from 'react-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import './Header.css'; const Header = () => { + // --- Hooks (sempre chamados na mesma ordem) --- const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isSuporteCardOpen, setIsSuporteCardOpen] = useState(false); const [isChatOpen, setIsChatOpen] = useState(false); @@ -11,9 +14,11 @@ const Header = () => { const [showLogoutModal, setShowLogoutModal] = useState(false); const [avatarUrl, setAvatarUrl] = useState(null); const navigate = useNavigate(); + const location = useLocation(); const chatInputRef = useRef(null); const mensagensContainerRef = useRef(null); + // --- Efeitos --- useEffect(() => { const loadAvatar = () => { const localAvatar = localStorage.getItem('user_avatar'); @@ -44,7 +49,18 @@ const Header = () => { } }, [mensagens]); - // --- Logout --- + // Fecha modal com ESC (útil para logout) + useEffect(() => { + const onKey = (e) => { + if (e.key === 'Escape' && showLogoutModal) { + setShowLogoutModal(false); + } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [showLogoutModal]); + + // --- Lógica e handlers --- const handleLogoutClick = () => { setShowLogoutModal(true); setIsDropdownOpen(false); @@ -65,26 +81,21 @@ const Header = () => { sessionStorage.getItem("authToken"); if (token) { - const response = 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); - } + // tentativa de logout no backend (se houver) + try { + await fetch( + "https://mock.apidog.com/m1/1053378-0-default/auth/v1/logout", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + } catch (err) { + // não interrompe o fluxo se a API falhar — prosseguimos para limpar local + console.warn('Erro ao chamar endpoint de logout (ignorado):', err); } } @@ -105,12 +116,13 @@ const Header = () => { sessionStorage.removeItem(key); }); + // tenta limpar caches relacionados se existirem if (window.caches) { caches.keys().then(names => { names.forEach(name => { if (name.includes("auth") || name.includes("api")) caches.delete(name); }); - }); + }).catch(()=>{ /* ignore */ }); } }; @@ -157,7 +169,6 @@ const Header = () => { e.preventDefault(); if (mensagem.trim() === '') return; - // Mensagem do usuário const novaMensagemUsuario = { id: Date.now(), texto: mensagem, @@ -177,7 +188,6 @@ const Header = () => { const data = await response.json(); - // Resposta da IA const respostaSuporte = { id: Date.now() + 1, texto: data.resposta || data.reply || "Desculpe, não consegui processar sua pergunta no momento 😅", @@ -198,6 +208,7 @@ const Header = () => { } }; + // --- Subcomponentes --- const SuporteCard = () => (

Suporte

@@ -257,6 +268,82 @@ const Header = () => {
); + // --- Modal de logout renderizado via Portal (garante top-most e clique) --- + const LogoutModalPortal = ({ onCancel, onConfirm }) => { + const modalContent = ( +
+
e.stopPropagation()} + > +

Confirmar Logout

+

Tem certeza que deseja encerrar a sessão?

+
+ + +
+
+
+ ); + + // garante que exista document antes de criar portal (SSRed apps podem não ter) + if (typeof document === 'undefined') return null; + return createPortal(modalContent, document.body); + }; + + // --- Agora sim: condicional de render baseado na rota --- + if (location.pathname === '/login') { + return null; + } + + // --- JSX principal --- return (
@@ -282,22 +369,9 @@ const Header = () => {
- {/* Modal de Logout */} + {/* Modal de Logout via portal */} {showLogoutModal && ( -
-
-

Confirmar Logout

-

Tem certeza que deseja encerrar a sessão?

-
- - -
-
-
+ )} {isSuporteCardOpen && ( @@ -319,4 +393,4 @@ const Header = () => { ); }; -export default Header; \ No newline at end of file +export default Header; diff --git a/src/pages/style/Agendamento.css b/src/pages/style/Agendamento.css index 1237a37..a5e90b2 100644 --- a/src/pages/style/Agendamento.css +++ b/src/pages/style/Agendamento.css @@ -240,3 +240,31 @@ .appointment-actions { width: 100%; } .btn-action { width: 100%; } } +.btn-adicionar-consulta { + background-color: #2a67e2; + color: #fff; + padding: 10px 24px; + border-radius: 8px; + border: none; + font-weight: 600; + font-size: 1rem; + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + transition: background 0.2s; +} +.btn-adicionar-consulta:hover { + background-color: #1d4ed8; +} +.btn-adicionar-consulta i { + font-size: 1.2em; + vertical-align: middle; +} +.btn-adicionar-consulta i { + font-size: 1.2em; + vertical-align: middle; + display: flex; + align-items: center; + justify-content: center; +}