Melhorias da disponibilidade
This commit is contained in:
parent
310bccbe6f
commit
3dac02b650
@ -1,115 +1,166 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
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 { GetAllDoctors } from "../components/utils/Functions-Endpoints/Doctor";
|
||||
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 DisponibilidadesDoctorPage = () => {
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDoctors = async () => {
|
||||
try {
|
||||
setDoctorsLoading(true);
|
||||
const data = await GetAllDoctors();
|
||||
console.log("Médicos recebidos:", data);
|
||||
setDoctors(Array.isArray(data) ? data : []);
|
||||
} 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 [expandedDoctors, setExpandedDoctors] = useState({});
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
|
||||
const getHeaders = () => {
|
||||
const myHeaders = new Headers();
|
||||
const authHeader = resolveAuthHeader();
|
||||
const authHeader = getAuthorizationHeader();
|
||||
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);
|
||||
}
|
||||
}, []);
|
||||
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) {
|
||||
console.error("Erro ao carregar médicos:", error);
|
||||
setDoctors([]);
|
||||
}
|
||||
};
|
||||
fetchDoctors();
|
||||
}, [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) {
|
||||
console.error("Erro ao buscar disponibilidades:", 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();
|
||||
|
||||
// 1. Obter as disponibilidades existentes para este médico
|
||||
const existingDisponibilidadesRes = await fetch(`${ENDPOINT}?doctor_id=eq.${String(doctorId)}`, {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
const existingDisponibilidades = existingDisponibilidadesRes.ok ? await existingDisponibilidadesRes.json() : [];
|
||||
|
||||
const disponibilidadesParaManter = new Set();
|
||||
|
||||
for (const dia of horariosAtualizados) {
|
||||
if (dia.isChecked && dia.blocos.length > 0) {
|
||||
// Processar blocos de horários
|
||||
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: 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) {
|
||||
// Atualizar disponibilidade existente
|
||||
await fetch(`${ENDPOINT}?id=eq.${bloco.id}`, {
|
||||
method: "PATCH",
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
disponibilidadesParaManter.add(bloco.id);
|
||||
} else {
|
||||
// Criar nova disponibilidade
|
||||
const postRes = await fetch(ENDPOINT, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (postRes.ok) {
|
||||
const newDisp = await postRes.json();
|
||||
// Adicionar o ID da nova disponibilidade para evitar exclusão acidental
|
||||
if (newDisp && newDisp.length > 0) {
|
||||
disponibilidadesParaManter.add(newDisp[0].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Excluir disponibilidades antigas que não foram mantidas
|
||||
const disponibilidadesParaExcluir = existingDisponibilidades.filter(
|
||||
(disp) => !disponibilidadesParaManter.has(disp.id)
|
||||
);
|
||||
|
||||
for (const disp of disponibilidadesParaExcluir) {
|
||||
await fetch(`${ENDPOINT}?id=eq.${disp.id}`, {
|
||||
method: "DELETE",
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
alert("Horários atualizados com sucesso!");
|
||||
setEditando(null);
|
||||
|
||||
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");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erro:", error);
|
||||
alert("Falha ao conectar com o servidor");
|
||||
const data = await res.json();
|
||||
setDisponibilidades(Array.isArray(data) ? data : []);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Erro ao salvar disponibilidades:", error);
|
||||
alert("Erro ao salvar os horários");
|
||||
}
|
||||
};
|
||||
|
||||
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",
|
||||
@ -118,10 +169,6 @@ const DisponibilidadesDoctorPage = () => {
|
||||
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);
|
||||
@ -129,228 +176,322 @@ const DisponibilidadesDoctorPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
return [1, 2, 3, 4, 5, 6, 0].map((weekday) => {
|
||||
const blocosDoDia = disponibilidadesMedico
|
||||
.filter((d) => d.weekday === weekday && d.active !== false)
|
||||
.map((d) => ({
|
||||
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,
|
||||
}));
|
||||
|
||||
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,
|
||||
}],
|
||||
};
|
||||
});
|
||||
}, [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;
|
||||
salvarTodasDisponibilidades(editando, horariosAtualizados);
|
||||
};
|
||||
|
||||
const filteredDoctors = useMemo(() => {
|
||||
if (!searchTerm) return doctors;
|
||||
return doctors.filter((doc) =>
|
||||
doc.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
(doc.full_name || doc.name).toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}, [doctors, searchTerm]);
|
||||
|
||||
const handleCancelarEdicao = () => {
|
||||
setEditando(null);
|
||||
};
|
||||
|
||||
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 id="main-content">
|
||||
<h1 style={{ fontSize: "1.5rem", fontWeight: "bold", color: "#333" }}>
|
||||
<div className="disponibilidades-container">
|
||||
<h1 className="disponibilidades-title">
|
||||
Disponibilidades dos Médicos
|
||||
</h1>
|
||||
|
||||
<div style={{ marginTop: "10px", marginBottom: "10px" }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar médico por nome..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setSelectedDoctor(null);
|
||||
}}
|
||||
style={{
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "4px",
|
||||
padding: "6px",
|
||||
width: "300px",
|
||||
}}
|
||||
/>
|
||||
{searchTerm && (
|
||||
<ul
|
||||
style={{
|
||||
border: "1px solid #ddd",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: "white",
|
||||
position: "absolute",
|
||||
zIndex: 10,
|
||||
width: "300px",
|
||||
maxHeight: "150px",
|
||||
overflowY: "auto",
|
||||
marginTop: "4px",
|
||||
listStyle: "none",
|
||||
padding: 0,
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
{filteredDoctors.length > 0 ? (
|
||||
filteredDoctors.map((doc) => (
|
||||
<li
|
||||
key={doc.id}
|
||||
onClick={() => {
|
||||
setSelectedDoctor(doc);
|
||||
setSearchTerm(doc.name);
|
||||
}}
|
||||
style={{
|
||||
padding: "6px 8px",
|
||||
cursor: "pointer",
|
||||
borderBottom: "1px solid #eee",
|
||||
}}
|
||||
>
|
||||
{doc.name}
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li style={{ padding: "6px 8px", color: "#888" }}>
|
||||
Nenhum médico encontrado
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
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="fila-titulo">
|
||||
{editando ? "Editar Disponibilidade" : "Lista de Disponibilidades"}{" "}
|
||||
({disponibilidades.length})
|
||||
<h2 className="section-title">
|
||||
{editando ? `Editar Horários` : "Lista de Disponibilidades"}
|
||||
</h2>
|
||||
|
||||
{loading ? (
|
||||
<p>Carregando...</p>
|
||||
) : disponibilidades.length === 0 ? (
|
||||
<p>Nenhuma disponibilidade encontrada.</p>
|
||||
{doctors.length === 0 ? (
|
||||
<p className="loading-text">Carregando médicos...</p>
|
||||
) : editando ? (
|
||||
<>
|
||||
<HorariosDisponibilidade
|
||||
initialAvailability={initialAvailabilityParaEdicao}
|
||||
onUpdate={handleUpdateHorarios}
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleUpdateHorarios(initialAvailabilityParaEdicao)
|
||||
}
|
||||
style={{
|
||||
marginTop: "20px",
|
||||
padding: "10px 20px",
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "#3b82f6",
|
||||
color: "white",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Salvar Alterações
|
||||
</button>
|
||||
<div className="edit-container">
|
||||
{initialAvailabilityParaEdicao.length > 0 ? (
|
||||
<HorariosDisponibilidade
|
||||
initialAvailability={initialAvailabilityParaEdicao}
|
||||
onUpdate={handleUpdateHorarios}
|
||||
readOnly={false}
|
||||
showHeader={false}
|
||||
compact={false}
|
||||
/>
|
||||
) : (
|
||||
<p className="loading-text">Carregando horários para edição...</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="disp-buttons-container">
|
||||
<button
|
||||
onClick={() => handleUpdateHorarios(initialAvailabilityParaEdicao)}
|
||||
className="disp-btn-primary"
|
||||
>
|
||||
Salvar Alterações
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleCancelarEdicao}
|
||||
className="disp-btn-danger"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<table className="fila-tabela">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Médico</th>
|
||||
<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>
|
||||
{disponibilidades.map((disp) => {
|
||||
const medico = doctors.find((d) => d.id === disp.doctor_id);
|
||||
return (
|
||||
<tr key={disp.id}>
|
||||
<td>{medico ? medico.name : disp.doctor_id}</td>
|
||||
<td>{diasDaSemana[disp.weekday]}</td>
|
||||
<td>{disp.start_time}</td>
|
||||
<td>{disp.end_time}</td>
|
||||
<td>{disp.slot_minutes || 30}</td>
|
||||
<td>{disp.appointment_type || "presencial"}</td>
|
||||
<td>
|
||||
<span
|
||||
className={`badge ${
|
||||
disp.active === false
|
||||
? "badge-inactive"
|
||||
: "badge-active"
|
||||
}`}
|
||||
>
|
||||
{disp.active === false ? "Inativa" : "Ativa"}
|
||||
<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>
|
||||
</td>
|
||||
<td>
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<button
|
||||
className="btn btn-sm btn-edit"
|
||||
style={{
|
||||
backgroundColor: "#3b82f6",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
padding: "6px 10px",
|
||||
cursor: "pointer",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
onClick={() => console.log("Editar clicado")}
|
||||
>
|
||||
Editar
|
||||
</button>
|
||||
</h3>
|
||||
<span className={`expand-icon ${expandedDoctors[grupo.doctor_id] ? 'expanded' : ''}`}>
|
||||
▼
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-sm btn-delete"
|
||||
style={{
|
||||
backgroundColor: "#ef4444",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
padding: "6px 10px",
|
||||
cursor: "pointer",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
onClick={() => console.log("Excluir clicado")}
|
||||
>
|
||||
Excluir
|
||||
</button>
|
||||
{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>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
370
src/pages/style/DisponibilidadesDoctorPage.css
Normal file
370
src/pages/style/DisponibilidadesDoctorPage.css
Normal file
@ -0,0 +1,370 @@
|
||||
.disponibilidades-container {
|
||||
padding: 20px;
|
||||
background: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.disponibilidades-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin: 10px 0 25px 0;
|
||||
position: relative;
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
border: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.search-input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border: 1px solid #dce1e6;
|
||||
border-radius: 6px;
|
||||
padding: 10px 40px 10px 12px;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
|
||||
}
|
||||
|
||||
.clear-search-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: #7f8c8d;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.clear-search-btn:hover {
|
||||
background-color: #f8f9fa;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.suggestions-dropdown {
|
||||
border: 1px solid #e1e8ed;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
width: calc(100% - 30px);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-top: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
transition: background-color 0.2s;
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.suggestion-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.suggestion-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Collapsible Styles */
|
||||
.doctor-group {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.doctor-header {
|
||||
padding: 16px 20px;
|
||||
background-color: #f1f3f5;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.doctor-header:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
.doctor-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.doctor-hours {
|
||||
font-size: 0.9rem;
|
||||
font-weight: normal;
|
||||
color: #7f8c8d;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
font-size: 1rem;
|
||||
color: #7f8c8d;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.expand-icon.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.doctor-content {
|
||||
padding: 0;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease-out;
|
||||
}
|
||||
|
||||
.doctor-group.expanded .doctor-content {
|
||||
max-height: 5000px;
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
padding: 0 10px 10px 10px;
|
||||
}
|
||||
|
||||
.disponibilidades-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.disponibilidades-table thead {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.disponibilidades-table thead th {
|
||||
padding: 6px 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.disponibilidades-table tbody tr {
|
||||
transition: background-color 0.2s;
|
||||
border-bottom: 1px solid #f1f3f5;
|
||||
}
|
||||
|
||||
.disponibilidades-table tbody tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.disponibilidades-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.disponibilidades-table td {
|
||||
padding: 12px 16px;
|
||||
color: #495057;
|
||||
border-bottom: 1px solid #f1f3f5;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: #27ae60;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
|
||||
.status-not-configured {
|
||||
background-color: #7f8c8d;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.edit-btn-container {
|
||||
padding: 8px 10px 0px 10px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.disp-btn-edit {
|
||||
background-color: #ffe8a1;
|
||||
color: #2c3e50;
|
||||
border: 1px solid #f0d860;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.disp-btn-edit:hover {
|
||||
background-color: #ffcc00;
|
||||
border-color: #e6b800;
|
||||
}
|
||||
|
||||
.disp-btn-delete {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.disp-btn-delete:hover {
|
||||
background-color: #f1b0b7;
|
||||
border-color: #e89ca6;
|
||||
}
|
||||
|
||||
/* Edit Mode Styles */
|
||||
.edit-container {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.disp-buttons-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.disp-btn-primary {
|
||||
padding: 10px 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.disp-btn-primary:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
.disp-btn-danger {
|
||||
padding: 10px 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
background-color: #95a5a6;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.disp-btn-danger:hover {
|
||||
background-color: #7f8c8d;
|
||||
}
|
||||
|
||||
/* Section Titles */
|
||||
.section-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
/* Loading and Empty States */
|
||||
.loading-text, .no-results {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #7f8c8d;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.disponibilidades-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.disponibilidades-table {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.disponibilidades-table th,
|
||||
.disponibilidades-table td {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.doctor-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.disp-buttons-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.disp-btn-primary,
|
||||
.disp-btn-danger {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user