636 lines
22 KiB
JavaScript
636 lines
22 KiB
JavaScript
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 { 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, 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);
|
|
|
|
const Agendamento = ({ setDictInfo }) => {
|
|
const navigate = useNavigate();
|
|
const { getAuthorizationHeader, user } = useAuth();
|
|
|
|
console.log('USER NO AGENDAMENTO:', user);
|
|
|
|
const [patientId, setPatientId] = useState('bf7d8323-05e1-437a-817c-f08eb5f174ef');
|
|
const [isLoading, setIsLoading] = useState(false); // começa false
|
|
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 [isCancelModalOpen, setIsCancelModalOpen] = useState(false);
|
|
const [appointmentToCancel, setAppointmentToCancel] = useState(null);
|
|
const [cancellationReason, setCancellationReason] = useState('');
|
|
|
|
const authHeader = useMemo(
|
|
() => getAuthorizationHeader(),
|
|
[getAuthorizationHeader]
|
|
);
|
|
|
|
// Buscar consultas desse paciente
|
|
const carregarDados = async () => {
|
|
// só tenta buscar quando tiver header e patientId
|
|
if (!authHeader) {
|
|
console.warn('Header de autorização não disponível.');
|
|
return;
|
|
}
|
|
|
|
if (!patientId) {
|
|
console.warn('patientId ainda não carregado, aguardando contexto.');
|
|
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()) || [];
|
|
console.log('CONSULTAS BRUTAS PACIENTE:', consultasBrutas);
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
// roda quando authHeader ou patientId mudarem
|
|
useEffect(() => {
|
|
carregarDados();
|
|
}, [authHeader, patientId]); // padrão recomendado para fetch com useEffect [web:46][web:82]
|
|
|
|
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('');
|
|
|
|
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);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div
|
|
className="form-container"
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
height: '50vh',
|
|
}}
|
|
>
|
|
<Spinner />
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<div>
|
|
<h1>Minhas consultas</h1>
|
|
<div
|
|
className="btns-gerenciamento-e-consulta"
|
|
style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}
|
|
>
|
|
<button
|
|
className="btn-adicionar-consulta"
|
|
onClick={() => {
|
|
setPageConsulta(true);
|
|
setFiladeEspera(false);
|
|
}}
|
|
style={{ backgroundColor: PageNovaConsulta ? '#1d4ed8' : undefined }}
|
|
>
|
|
<i className="bi bi-plus-circle"></i> Solicitar Agendamento
|
|
</button>
|
|
<button
|
|
className="btn-adicionar-consulta"
|
|
onClick={() => {
|
|
setFiladeEspera(!FiladeEspera);
|
|
setPageConsulta(false);
|
|
}}
|
|
style={{
|
|
backgroundColor:
|
|
FiladeEspera && !PageNovaConsulta ? '#1d4ed8' : undefined,
|
|
}}
|
|
>
|
|
<i className="bi bi-list-task me-1"></i> Fila de Espera (
|
|
{filaEsperaData.length})
|
|
</button>
|
|
</div>
|
|
|
|
{!PageNovaConsulta ? (
|
|
<div className="atendimento-eprocura">
|
|
<section className="calendario-ou-filaespera">
|
|
{!FiladeEspera ? (
|
|
<div className="calendar-wrapper">
|
|
<div className="calendar-info-panel">
|
|
<div className="info-date-display">
|
|
<span>{selectedDay.format('MMM')}</span>
|
|
<strong>{selectedDay.format('DD')}</strong>
|
|
</div>
|
|
<div className="info-details">
|
|
<h3>{selectedDay.format('dddd')}</h3>
|
|
<p>{selectedDay.format('D [de] MMMM [de] YYYY')}</p>
|
|
</div>
|
|
<div className="appointments-list">
|
|
<div
|
|
className="calendar-legend compact-legend"
|
|
style={{
|
|
marginTop: 4,
|
|
gap: 3,
|
|
fontSize: 8,
|
|
lineHeight: 1.1,
|
|
}}
|
|
>
|
|
<div
|
|
className="legend-item"
|
|
data-status="completed"
|
|
style={{ padding: '0px 5px' }}
|
|
>
|
|
Realizado
|
|
</div>
|
|
<div
|
|
className="legend-item"
|
|
data-status="confirmed"
|
|
style={{ padding: '0px 5px' }}
|
|
>
|
|
Confirmado
|
|
</div>
|
|
<div
|
|
className="legend-item"
|
|
data-status="agendado"
|
|
style={{ padding: '0px 5px' }}
|
|
>
|
|
Agendado
|
|
</div>
|
|
<div
|
|
className="legend-item"
|
|
data-status="cancelled"
|
|
style={{ padding: '0px 5px' }}
|
|
>
|
|
Cancelado
|
|
</div>
|
|
</div>
|
|
|
|
<h4>Consultas para {selectedDay.format('DD/MM')}</h4>
|
|
{DictAgendamentosOrganizados[
|
|
selectedDay.format('YYYY-MM-DD')
|
|
]?.length > 0 ? (
|
|
DictAgendamentosOrganizados[
|
|
selectedDay.format('YYYY-MM-DD')
|
|
].map((app) => (
|
|
<div
|
|
key={app.id}
|
|
className="appointment-item"
|
|
data-status={app.status}
|
|
>
|
|
<div className="item-time">
|
|
{dayjs(app.scheduled_at).add(3, 'hour').format('HH:mm')}
|
|
|
|
</div>
|
|
<div className="item-details">
|
|
<span>Consulta com Dr(a). {app.medico_nome}</span>
|
|
</div>
|
|
<div className="item-actions">
|
|
{app.status !== 'cancelled' &&
|
|
dayjs(app.scheduled_at).isAfter(dayjs()) && (
|
|
<button
|
|
className="btn btn-sm btn-outline-danger"
|
|
onClick={() => handleCancelClick(app.id)}
|
|
title="Cancelar Consulta"
|
|
>
|
|
<Trash2 size={16} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="no-appointments-info">
|
|
<p>Nenhuma consulta agendada para esta data.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="calendar-main">
|
|
<div className="calendar-legend"></div>
|
|
<div className="calendar-controls">
|
|
<div className="date-indicator">
|
|
<h2>{currentDate.format('MMMM [de] YYYY')}</h2>
|
|
<div
|
|
className="quick-jump-controls"
|
|
style={{
|
|
display: 'flex',
|
|
gap: '5px',
|
|
marginTop: '10px',
|
|
}}
|
|
>
|
|
<select
|
|
value={quickJump.month}
|
|
onChange={(e) =>
|
|
handleQuickJumpChange('month', e.target.value)
|
|
}
|
|
className="form-select form-select-sm w-auto"
|
|
>
|
|
{dayjs.months().map((month, index) => (
|
|
<option key={index} value={index}>
|
|
{month.charAt(0).toUpperCase() + month.slice(1)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
value={quickJump.year}
|
|
onChange={(e) =>
|
|
handleQuickJumpChange('year', e.target.value)
|
|
}
|
|
className="form-select form-select-sm w-auto"
|
|
>
|
|
{Array.from({ length: 11 }, (_, i) =>
|
|
dayjs().year() - 5 + i
|
|
).map((year) => (
|
|
<option key={year} value={year}>
|
|
{year}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<button
|
|
className="btn btn-sm btn-outline-primary"
|
|
onClick={applyQuickJump}
|
|
disabled={
|
|
quickJump.month === currentDate.month() &&
|
|
quickJump.year === currentDate.year()
|
|
}
|
|
>
|
|
Ir
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="nav-buttons">
|
|
<button
|
|
onClick={() => setCurrentDate((c) => c.subtract(1, 'month'))}
|
|
>
|
|
<ChevronLeft size={20} />
|
|
</button>
|
|
<button onClick={() => setCurrentDate(dayjs())}>
|
|
Hoje
|
|
</button>
|
|
<button
|
|
onClick={() => setCurrentDate((c) => c.add(1, 'month'))}
|
|
>
|
|
<ChevronRight size={20} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="calendar-grid">
|
|
{weekDays.map((day) => (
|
|
<div key={day} className="day-header">
|
|
{day}
|
|
</div>
|
|
))}
|
|
{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 (
|
|
<div
|
|
key={index}
|
|
className={cellClasses}
|
|
onClick={() => handleDateClick(day)}
|
|
>
|
|
<span>{day.format('D')}</span>
|
|
{appointmentsOnDay.length > 0 && (
|
|
<div className="appointments-indicator">
|
|
{appointmentsOnDay.length}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="page-content table-paciente-container">
|
|
<section className="row">
|
|
<div className="col-12">
|
|
<div className="card table-paciente-card">
|
|
<div className="card-header">
|
|
<h4 className="card-title mb-0">
|
|
Minhas Solicitações em Fila de Espera
|
|
</h4>
|
|
</div>
|
|
<div className="card-body">
|
|
<div className="table-responsive">
|
|
<table className="table table-striped table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Médico Solicitado</th>
|
|
<th>Data da Solicitação</th>
|
|
<th>Ações</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filaEsperaData.length > 0 ? (
|
|
filaEsperaData.map((item) => (
|
|
<tr key={item.agendamento.id}>
|
|
<td>Dr(a). {item.Infos?.medico_nome}</td>
|
|
<td>
|
|
{dayjs(
|
|
item.agendamento.created_at
|
|
).format('DD/MM/YYYY HH:mm')}
|
|
</td>
|
|
<td>
|
|
<button
|
|
className="btn btn-sm btn-danger"
|
|
onClick={() =>
|
|
handleCancelClick(item.agendamento.id)
|
|
}
|
|
>
|
|
<i className="bi bi-trash me-1"></i>{' '}
|
|
Cancelar
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))
|
|
) : (
|
|
<tr>
|
|
<td
|
|
colSpan="3"
|
|
className="text-center py-4"
|
|
>
|
|
<div className="text-muted">
|
|
Nenhuma solicitação na fila de espera.
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
) : (
|
|
<AgendamentoCadastroManager
|
|
setPageConsulta={setPageConsulta}
|
|
onSaved={() => {
|
|
carregarDados(); // recarrega consultas do paciente
|
|
setPageConsulta(false);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{isCancelModalOpen && (
|
|
<div className="modal-overlay">
|
|
<div className="modal-content" style={{ maxWidth: '400px' }}>
|
|
<div
|
|
className="modal-header"
|
|
style={{
|
|
backgroundColor: '#fee2e2',
|
|
borderBottom: '1px solid #fca5a5',
|
|
padding: '15px',
|
|
borderRadius: '8px 8px 0 0',
|
|
}}
|
|
>
|
|
<h4 style={{ margin: 0, color: '#dc2626' }}>
|
|
Confirmação de Cancelamento
|
|
</h4>
|
|
<button
|
|
className="close-button"
|
|
onClick={() => setIsCancelModalOpen(false)}
|
|
style={{
|
|
background: 'none',
|
|
border: 'none',
|
|
fontSize: '1.5rem',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
<div className="modal-body" style={{ padding: '20px' }}>
|
|
<p>Qual o motivo do cancelamento?</p>
|
|
<textarea
|
|
value={cancellationReason}
|
|
onChange={(e) => setCancellationReason(e.target.value)}
|
|
placeholder="Ex: Precisei viajar, motivo pessoal, etc."
|
|
rows="4"
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px',
|
|
resize: 'none',
|
|
border: '1px solid #ccc',
|
|
borderRadius: '4px',
|
|
}}
|
|
></textarea>
|
|
</div>
|
|
<div
|
|
className="modal-footer"
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'flex-end',
|
|
gap: '10px',
|
|
padding: '15px',
|
|
borderTop: '1px solid #eee',
|
|
}}
|
|
>
|
|
<button
|
|
className="btn btn-secondary"
|
|
onClick={() => setIsCancelModalOpen(false)}
|
|
style={{
|
|
backgroundColor: '#6c757d',
|
|
color: 'white',
|
|
border: 'none',
|
|
padding: '8px 15px',
|
|
borderRadius: '4px',
|
|
}}
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
className="btn btn-danger"
|
|
onClick={executeCancellation}
|
|
style={{
|
|
backgroundColor: '#dc3545',
|
|
color: 'white',
|
|
border: 'none',
|
|
padding: '8px 15px',
|
|
borderRadius: '4px',
|
|
}}
|
|
>
|
|
<Trash2 size={16} style={{ marginRight: '5px' }} /> Excluir
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Agendamento;
|