Arummando o agendamento

This commit is contained in:
GilenoNeto901 2025-12-02 14:04:36 -03:00
parent 07ed113291
commit 6f3a49575c
3 changed files with 314 additions and 200 deletions

View File

@ -30,6 +30,7 @@ const Agendamento = () => {
const authHeader = getAuthorizationHeader();
const ID_MEDICO_ESPECIFICO = "078d2a67-b4c1-43c8-ae32-c1e75bb5b3df";
const [listaTodosAgendamentos, setListaTodosAgendamentos] = useState([]);
const [selectedID, setSelectedId] = useState("0");
@ -39,12 +40,16 @@ const Agendamento = () => {
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 [searchTermPatient, setSearchTermPatient] = useState("");
const [patientDropdownOpen, setPatientDropdownOpen] = useState(false);
const [showSpinner, setShowSpinner] = useState(true);
const [waitlistSearch, setWaitlistSearch] = useState("");
const [waitSortKey, setWaitSortKey] = useState(null);
@ -302,6 +307,14 @@ const Agendamento = () => {
setMedicoFiltrado({ id: "vazio" });
}
};
const filtrarPorPaciente = (appointments) => {
if (!searchTermPatient.trim()) return appointments;
const term = searchTermPatient.toLowerCase();
return appointments.filter((app) =>
app.paciente_nome?.toLowerCase().includes(term)
);
};
const generateDateGrid = () => {
const grid = [];
@ -411,6 +424,13 @@ const Agendamento = () => {
() => applySortingWaitlist(filaEsperaFiltrada),
[filaEsperaFiltrada, applySortingWaitlist]
);
const listaPacientesUnicos = useMemo(() => {
const pacientes = Object.values(cachePacientes || {});
return pacientes.sort((a, b) =>
(a.full_name || "").localeCompare(b.full_name || "")
);
}, [cachePacientes]);
const waitTotalPages =
Math.ceil(filaEsperaOrdenada.length / waitPerPage) || 1;
const waitIndiceInicial = (waitPage - 1) * waitPerPage;
@ -449,71 +469,11 @@ const Agendamento = () => {
<h1>Agendar nova consulta</h1>
{!PageNovaConsulta ? (
<div className="atendimento-eprocura">
{user?.role !== "doctor" && (
<div className="card p-3 mb-3 table-paciente-filters">
<h5 className="mb-3">
<i className="bi bi-funnel-fill me-2 text-primary"></i>
Filtrar por Médico
</h5>
<div className="position-relative">
<input
type="text"
className="form-control"
placeholder="Digite o nome do médico..."
value={searchTermDoctor}
onChange={(e) => handleSearchMedicos(e.target.value)}
/>
<small className="text-muted">
Buscar médico para filtrar consultas
</small>
{searchTermDoctor && FiltredTodosMedicos.length > 0 && (
<div
className="list-group position-absolute w-100"
style={{
zIndex: 1000,
maxHeight: "200px",
overflowY: "auto",
}}
>
{FiltredTodosMedicos.map((medico) => (
<button
key={medico.idMedico}
type="button"
className="list-group-item list-group-item-action"
onClick={() => {
setSearchTermDoctor(medico.nomeMedico);
setFiltredTodosMedicos([]);
setMedicoFiltrado({ id: medico.idMedico });
}}
>
{medico.nomeMedico}
</button>
))}
</div>
)}
</div>
{MedicoFiltrado.id !== "vazio" && (
<div className="mt-3">
<span className="badge bg-primary me-2">
<i className="bi bi-person-check me-1"></i>
{searchTermDoctor}
<button
type="button"
className="btn-close btn-close-white ms-2"
style={{ fontSize: "0.6rem" }}
onClick={() => {
setMedicoFiltrado({ id: "vazio" });
setSearchTermDoctor("");
}}
></button>
</span>
</div>
)}
</div>
)}
<div className="container-btns-agenda-fila_esepera">
<div className="tabs-agenda-fila">
<button
className={`btn-agenda ${
@ -538,7 +498,7 @@ const Agendamento = () => {
</div>
<div
className="btns-gerenciamento-e-consulta"
style={{ display: "flex", gap: "10px", marginBottom: "20px" }}
style={{ display: "flex", gap: "10px" }}
>
<button
className="btn btn-primary"
@ -575,99 +535,191 @@ const Agendamento = () => {
<div className="info-details">
<h3>{selectedDay.format("dddd")}</h3>
<p>{selectedDay.format("D [de] MMMM [de] YYYY")}</p>
<div className="calendar-legend">
<div className="legend-item" data-status="completed">Realizado</div>
<div className="legend-item" data-status="confirmed">Confirmado</div>
<div className="legend-item" data-status="agendado">Agendado</div>
<div className="legend-item" data-status="cancelled">Cancelado</div>
</div>
</div>
<div className="appointments-list">
<h4>Consultas para {selectedDay.format("DD/MM")}</h4>
{showSpinner ? (
<Spinner />
) : 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) => (
<div
key={app.id}
className="appointment-item"
data-status={app.status}
>
<div className="item-time">
{dayjs(app.scheduled_at).format("HH:mm")}
</div>
<div className="item-details">
<span>{app.paciente_nome}</span>
<small>Dr(a). {app.medico_nome}</small>
</div>
<div className="appointment-actions">
{app.status === "cancelled" ? (
<button
className="btn-action btn-edit"
onClick={() => {
setSelectedId(app.id);
confirmConsulta(app.id);
}}
>
<CheckCircle
size={16}
title="Reverter Cancelamento"
/>
</button>
) : (
<button
className="btn-action btn-edit"
onClick={() => handleEditConsulta(app)}
title="Editar Agendamento"
>
<Edit size={16} />
</button>
)}
{app.status !== "cancelled" && (
<button
className="btn-action btn-delete"
onClick={() => {
setSelectedId(app.id);
setShowDeleteModal(true);
}}
title="Cancelar Agendamento"
>
<Trash2 size={16} />
</button>
)}
</div>
</div>
))
) : (
<div className="no-appointments-info">
<p>Nenhuma consulta agendada.</p>
</div>
)}
{showSpinner ? (
<Spinner />
) : (() => {
const appointmentsForDay =
DictAgendamentosOrganizados[selectedDay.format("YYYY-MM-DD")] || [];
// 1º filtro: por médico (se tiver)
const filteredByDoctor = appointmentsForDay.filter(
(app) =>
MedicoFiltrado.id === "vazio" ||
app.doctor_id === MedicoFiltrado.id
);
// 2º filtro: por paciente (campo de cima)
const filteredByPatient = filtrarPorPaciente(filteredByDoctor);
if (filteredByPatient.length === 0) {
return (
<div className="no-appointments-info">
<p>
{searchTermPatient
? `Nenhuma consulta encontrada para "${searchTermPatient}".`
: "Nenhuma consulta agendada."}
</p>
</div>
);
}
return filteredByPatient.map((app) => (
<div
key={app.id}
className="appointment-item"
data-status={app.status}
>
<div className="item-time">
{dayjs(app.scheduled_at).format("HH:mm")}
</div>
<div className="item-details">
<span>{app.paciente_nome}</span>
<small>Dr(a). {app.medico_nome}</small>
</div>
<div className="appointment-actions">
{app.status === "cancelled" ? (
<button
className="btn-action btn-edit"
onClick={() => {
setSelectedId(app.id);
confirmConsulta(app.id);
}}
>
<CheckCircle
size={16}
title="Reverter Cancelamento"
/>
</button>
) : (
<button
className="btn-action btn-edit"
onClick={() => handleEditConsulta(app)}
title="Editar Agendamento"
>
<Edit size={16} />
</button>
)}
{app.status !== "cancelled" && (
<button
className="btn-action btn-delete"
onClick={() => {
setSelectedId(app.id);
setShowDeleteModal(true);
}}
title="Cancelar Agendamento"
>
<Trash2 size={16} />
</button>
)}
</div>
</div>
));
})()}
</div>
</div>
<div className="calendar-main">
<div className="patient-filter-container" style={{ marginBottom: "20px" }}>
<div className="card p-3">
<div className="filter-header d-flex align-items-center gap-2 mb-2">
<i className="bi bi-funnel-fill text-primary"></i>
<h5 className="mb-0">Filtrar por Paciente</h5>
</div>
<div className="position-relative">
<Search
size={20}
className="position-absolute"
style={{ left: "10px", top: "50%", transform: "translateY(-50%)", color: "#6c757d" }}
/>
<input
type="text"
className="form-control ps-5"
placeholder="Buscar paciente para filtrar consultas"
value={searchTermPatient}
onChange={(e) => {
setSearchTermPatient(e.target.value);
setPatientDropdownOpen(true);
}}
onFocus={() => {
if (searchTermPatient.trim()) {
setPatientDropdownOpen(true);
}
}}
style={{ paddingLeft: "40px" }}
/>
{patientDropdownOpen && searchTermPatient.trim() && (
<div
className="patient-suggestions"
style={{
position: "absolute",
top: "100%",
left: 0,
right: 0,
zIndex: 10,
backgroundColor: "white",
border: "1px solid #ced4da",
borderTop: "none",
maxHeight: "250px",
overflowY: "auto",
}}
>
{listaPacientesUnicos
.filter((p) => {
const nome = (p.full_name || "").toLowerCase();
const term = searchTermPatient.toLowerCase();
return nome.startsWith(term); // só começo do nome
})
.slice(0, 20)
.map((p) => (
<button
key={p.id}
type="button"
className="w-100 text-start px-3 py-1"
style={{
border: "none",
background: "white",
fontSize: "0.9rem",
}}
onClick={() => {
setSearchTermPatient(p.full_name);
setPatientDropdownOpen(false);
}}
>
{p.full_name} - {p.cpf}
</button>
))}
</div>
)}
</div>
{searchTermPatient && (
<div className="mt-2">
<button
className="btn btn-sm btn-outline-secondary"
onClick={() => setSearchTermPatient("")}
>
<i className="bi bi-x-circle me-1"></i>
Limpar filtro
</button>
</div>
)}
</div>
</div>
<div className="calendar-legend">
<div className="legend-item" data-status="completed">
Realizado
</div>
<div className="legend-item" data-status="confirmed">
Confirmado
</div>
<div className="legend-item" data-status="agendado">
Agendado
</div>
<div className="legend-item" data-status="cancelled">
Cancelado
</div>
</div>
</div>
<div className="calendar-controls">
<div className="date-indicator">
<h2>{currentDate.format("MMMM [de] YYYY")}</h2>
@ -755,13 +807,17 @@ const Agendamento = () => {
))}
{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 appointmentsOnDay =
DictAgendamentosOrganizados[dayString] || [];
const filteredAppointments = filtrarPorPaciente(
appointmentsOnDay.filter(
(app) =>
MedicoFiltrado.id === "vazio" ||
app.doctor_id === MedicoFiltrado.id
)
);
const cellClasses = `day-cell ${
day.isSame(currentDate, "month")
? "current-month"

View File

@ -166,13 +166,20 @@
}
.container-btns-agenda-fila_esepera {
margin-top: 30px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
gap: 20px;
margin-left: 20px;
margin-top: 0;
margin-bottom: 0;
margin-left: 0;
padding: 0 20px 0;
border-bottom: 1px solid #edf1f7;
}
.btn-fila-espera,
.btn-agenda {
background-color: transparent;
@ -269,4 +276,10 @@ html[data-bs-theme="dark"] .legenda-item-agendado {
background-color: #2e2e1e;
border: 3px solid #4d4d2e;
color: #f7f7c4;
}
}
.calendar-legend {
margin-top: 8px;
display: flex;
gap: 8px;
justify-content: center;
}

View File

@ -24,6 +24,7 @@ dayjs.extend(isBetween);
dayjs.extend(localeData);
const Agendamento = ({ setDictInfo }) => {
const navigate = useNavigate();
const [listaTodosAgendamentos, setListaTodosAgendamentos] = useState([]);
const [selectedID, setSelectedId] = useState('0');
@ -37,6 +38,7 @@ const Agendamento = ({ setDictInfo }) => {
const [ListaDeMedicos, setListaDeMedicos] = useState([]);
const [FiltredTodosMedicos, setFiltredTodosMedicos] = useState([]);
const [searchTermDoctor, setSearchTermDoctor] = useState('');
const [doctorDropdownOpen, setDoctorDropdownOpen] = useState(false);
const [MedicoFiltrado, setMedicoFiltrado] = useState({ id: "vazio" });
const [cacheAgendamentos, setCacheAgendamentos] = useState([]);
const [appointmentToEdit, setAppointmentToEdit] = useState(null);
@ -412,22 +414,11 @@ const Agendamento = ({ setDictInfo }) => {
<h3>{selectedDay.format('dddd')}</h3>
<p>{selectedDay.format('D [de] MMMM [de] YYYY')}</p>
<div
className="calendar-legend compact-legend"
style={{ marginTop: 6, gap: 4, fontSize: 10 }}
>
<div className="legend-item" data-status="completed" style={{ padding: '2px 8px' }}>
Realizado
</div>
<div className="legend-item" data-status="confirmed" style={{ padding: '2px 8px' }}>
Confirmado
</div>
<div className="legend-item" data-status="agendado" style={{ padding: '2px 8px' }}>
Agendado
</div>
<div className="legend-item" data-status="cancelled" style={{ padding: '2px 8px' }}>
Cancelado
</div>
<div className="calendar-legend">
<div className="legend-item" data-status="completed">Realizado</div>
<div className="legend-item" data-status="confirmed">Confirmado</div>
<div className="legend-item" data-status="agendado">Agendado</div>
<div className="legend-item" data-status="cancelled">Cancelado</div>
</div>
<h4 className="subtitle-consultas">
@ -440,12 +431,13 @@ const Agendamento = ({ setDictInfo }) => {
) : (() => {
const allApps =
DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')] || [];
const filtered = allApps.filter(app =>
!searchTermDoctor
? true
: (app.medico_nome || '').toLowerCase()
.includes(searchTermDoctor.toLowerCase())
);
const termDoc = searchTermDoctor.toLowerCase();
const filtered = allApps.filter(app =>
!termDoc
? true
: (app.medico_nome || '').toLowerCase().startsWith(termDoc)
);
return filtered.length > 0 ? (
filtered.map(app => (
<div key={app.id} className="appointment-item" data-status={app.status}>
@ -494,19 +486,72 @@ const Agendamento = ({ setDictInfo }) => {
);
})()}
</div>
</div>
<div className="calendar-main">
<div className="calendar-header-filter">
<input
type="text"
className="form-control"
placeholder="Filtrar atendimento do médico"
value={searchTermDoctor}
onChange={(e) => setSearchTermDoctor(e.target.value)}
/>
</div>
</div>
<div className="calendar-main">
{/* SUBSTITUIR POR ESTE BLOCO 👇 */}
<div className="calendar-header-filter">
<div className="position-relative">
<input
type="text"
className="form-control"
placeholder="Filtrar atendimento do médico"
value={searchTermDoctor}
onChange={(e) => {
setSearchTermDoctor(e.target.value);
setDoctorDropdownOpen(true);
}}
onFocus={() => {
if (searchTermDoctor.trim()) setDoctorDropdownOpen(true);
}}
/>
{doctorDropdownOpen && searchTermDoctor.trim() && (
<div
className="doctor-suggestions"
style={{
position: "absolute",
top: "100%",
left: 0,
right: 0,
zIndex: 10,
backgroundColor: "white",
border: "1px solid #ced4da",
borderTop: "none",
maxHeight: "250px",
overflowY: "auto",
}}
>
{ListaDeMedicos
.filter((m) => {
const nome = (m.nomeMedico || "").toLowerCase();
const term = searchTermDoctor.toLowerCase();
return nome.startsWith(term);
})
.slice(0, 20)
.map((m) => (
<button
key={m.idMedico}
type="button"
className="w-100 text-start px-3 py-1"
style={{
border: "none",
background: "white",
fontSize: "0.9rem",
}}
onClick={() => {
setSearchTermDoctor(m.nomeMedico);
setDoctorDropdownOpen(false);
}}
>
{m.nomeMedico}
</button>
))}
</div>
)}
</div>
</div>
<div className="calendar-controls">
<div className="date-indicator">
<h2>{currentDate.format('MMMM [de] YYYY')}</h2>
@ -578,12 +623,12 @@ const Agendamento = ({ setDictInfo }) => {
{dateGrid.map((day, index) => {
const dayKey = day.format('YYYY-MM-DD');
const appointmentsOnDay = DictAgendamentosOrganizados[dayKey] || [];
const filteredAppointments = appointmentsOnDay.filter(app =>
!searchTermDoctor
? true
: (app.medico_nome || '').toLowerCase()
.includes(searchTermDoctor.toLowerCase())
);
const termDoc = searchTermDoctor.toLowerCase();
const filteredAppointments = appointmentsOnDay.filter(app =>
!termDoc
? true
: (app.medico_nome || '').toLowerCase().startsWith(termDoc)
);
const cellClasses = `day-cell ${
day.isSame(currentDate, 'month') ? 'current-month' : 'other-month'
} ${day.isSame(dayjs(), 'day') ? 'today' : ''} ${