442 lines
16 KiB
JavaScript
442 lines
16 KiB
JavaScript
import React, { useState, useEffect, useMemo } from "react";
|
||
import HorariosDisponibilidade from "../components/doctors/HorariosDisponibilidade";
|
||
import { useAuth } from "../components/utils/AuthProvider";
|
||
import API_KEY from "../components/utils/apiKeys";
|
||
import "./style/DisponibilidadesDoctorPage.css";
|
||
|
||
const ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
|
||
const DOCTORS_ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors";
|
||
|
||
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 [doctors, setDoctors] = useState([]);
|
||
const [searchTerm, setSearchTerm] = useState("");
|
||
const [editando, setEditando] = useState(null);
|
||
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 {
|
||
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) {
|
||
setDoctors([]);
|
||
}
|
||
};
|
||
fetchDoctors();
|
||
}, [getAuthorizationHeader]);
|
||
|
||
useEffect(() => {
|
||
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 toggleExpandDoctor = (doctorId) => {
|
||
setExpandedDoctors((prev) => ({ ...prev, [doctorId]: !prev[doctorId] }));
|
||
};
|
||
|
||
const salvarTodasDisponibilidades = async (doctorId, horariosAtualizados) => {
|
||
try {
|
||
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) {
|
||
const data = await res.json();
|
||
setDisponibilidades(Array.isArray(data) ? data : []);
|
||
}
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
};
|
||
|
||
const deletarDisponibilidade = async (id) => {
|
||
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) setDisponibilidades((prev) => prev.filter((d) => d.id !== id));
|
||
} catch (error) {}
|
||
};
|
||
|
||
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) => {
|
||
if (!editando) return;
|
||
setAvailabilityEdit(horariosAtualizados || []);
|
||
};
|
||
|
||
const filteredDoctors = useMemo(() => {
|
||
if (!searchTerm) return doctors;
|
||
return doctors.filter((doc) => (doc.full_name || doc.name).toLowerCase().includes(searchTerm.toLowerCase()));
|
||
}, [doctors, searchTerm]);
|
||
|
||
const handleCancelarEdicao = () => {
|
||
setEditando(null);
|
||
setAvailabilityEdit([]);
|
||
};
|
||
|
||
const handleDoctorSelect = (doctor) => {
|
||
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 (
|
||
<div className="disponibilidades-container">
|
||
<h1 className="disponibilidades-title">Disponibilidades dos Médicos</h1>
|
||
|
||
<div className="search-container">
|
||
<div className="search-input-container">
|
||
<input
|
||
type="text"
|
||
placeholder="Buscar médico por nome..."
|
||
value={searchTerm}
|
||
onChange={(e) => {
|
||
setSearchTerm(e.target.value);
|
||
setShowSuggestions(true);
|
||
}}
|
||
onFocus={() => setShowSuggestions(true)}
|
||
className="search-input"
|
||
/>
|
||
{searchTerm && (
|
||
<button onClick={handleClearSearch} className="clear-search-btn">
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{showSuggestions && searchTerm && filteredDoctors.length > 0 && (
|
||
<div className="suggestions-dropdown">
|
||
{filteredDoctors.map((doc) => (
|
||
<div key={doc.id} onClick={() => handleDoctorSelect(doc)} className="suggestion-item">
|
||
{doc.full_name || doc.name}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<section className="calendario-ou-filaespera">
|
||
<div className="fila-container">
|
||
<h2 className="section-title">{editando ? `Editar Horários` : "Lista de Disponibilidades"}</h2>
|
||
|
||
{doctors.length === 0 ? (
|
||
<p className="loading-text">Carregando médicos...</p>
|
||
) : editando ? (
|
||
<>
|
||
<div className="edit-container">
|
||
{initialAvailabilityParaEdicao.length > 0 ? (
|
||
<HorariosDisponibilidade initialAvailability={initialAvailabilityParaEdicao} onUpdate={handleUpdateHorarios} onCancel={handleCancelarEdicao} />
|
||
) : (
|
||
<p className="loading-text">Carregando horários para edição...</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="disp-buttons-container">
|
||
<button
|
||
onClick={() =>
|
||
salvarTodasDisponibilidades(editando, availabilityEdit.length > 0 ? availabilityEdit : initialAvailabilityParaEdicao)
|
||
}
|
||
className="disp-btn-primary"
|
||
>
|
||
Salvar Alterações
|
||
</button>
|
||
|
||
<button onClick={handleCancelarEdicao} className="disp-btn-danger">
|
||
Cancelar
|
||
</button>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="doctor-group-container">
|
||
{disponibilidadesAgrupadas.length === 0 ? (
|
||
<p className="no-results">Nenhum médico encontrado</p>
|
||
) : (
|
||
disponibilidadesAgrupadas.map((grupo) => (
|
||
<div key={grupo.doctor_id} className={`doctor-group ${expandedDoctors[grupo.doctor_id] ? "expanded" : ""}`}>
|
||
<div className="doctor-header" onClick={() => toggleExpandDoctor(grupo.doctor_id)}>
|
||
<h3 className="doctor-name">
|
||
{grupo.doctor_name}
|
||
<span className="doctor-hours">({grupo.disponibilidades.filter((d) => !d.is_empty).length} horários)</span>
|
||
</h3>
|
||
<span className={`expand-icon ${expandedDoctors[grupo.doctor_id] ? "expanded" : ""}`}>▼</span>
|
||
</div>
|
||
|
||
{expandedDoctors[grupo.doctor_id] && (
|
||
<div className="doctor-content">
|
||
<div className="edit-btn-container">
|
||
<button onClick={() => setEditando(grupo.doctor_id)} className="disp-btn-edit">
|
||
{grupo.disponibilidades.some((d) => !d.is_empty) ? "Editar" : "Cadastrar Horários"}
|
||
</button>
|
||
</div>
|
||
|
||
<div className="table-container">
|
||
<table className="disponibilidades-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Dia da Semana</th>
|
||
<th>Início</th>
|
||
<th>Término</th>
|
||
<th>Intervalo (min)</th>
|
||
<th>Tipo</th>
|
||
<th>Status</th>
|
||
<th>Ações</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{grupo.disponibilidades.map((disp) => (
|
||
<tr key={disp.id}>
|
||
<td>{disp.is_empty ? "Nenhum horário cadastrado" : getDiaSemana(disp.weekday)}</td>
|
||
<td>{disp.is_empty ? "-" : formatTime(disp.start_time)}</td>
|
||
<td>{disp.is_empty ? "-" : formatTime(disp.end_time)}</td>
|
||
<td>{disp.is_empty ? "-" : disp.slot_minutes || 30}</td>
|
||
<td>{disp.is_empty ? "-" : disp.appointment_type || "presencial"}</td>
|
||
<td>
|
||
<span className={getStatusBadgeClass(disp)}>{getStatusText(disp)}</span>
|
||
</td>
|
||
<td>{!disp.is_empty && <button onClick={() => deletarDisponibilidade(disp.id)} className="disp-btn-delete">Excluir</button>}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default DisponibilidadesDoctorPage; |