Compare commits

...

4 Commits

14 changed files with 1189 additions and 447 deletions

31
package-lock.json generated
View File

@ -31881,23 +31881,6 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-bytes": { "node_modules/pretty-bytes": {
"version": "5.6.0", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@ -33023,20 +33006,6 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/react-scripts/node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/react-toastify": { "node_modules/react-toastify": {
"version": "11.0.5", "version": "11.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",

View File

@ -199,3 +199,14 @@
padding: 0.6rem 1.2rem; padding: 0.6rem 1.2rem;
} }
} }
@media (max-width: 576px) {
.doctor-form-container { padding: 0.75rem; }
.doctor-form-title { font-size: 1.75rem; }
.form-section { padding: 0.75rem; }
.section-header { font-size: 1.25rem; }
.form-label { font-size: 1rem; }
.form-control-custom { font-size: 1rem; }
.btns-container { display: flex; flex-direction: column; gap: 8px; }
.btn-submit, .btn-cancel { width: 100%; margin-right: 0; }
}

View File

@ -135,14 +135,11 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
} }
}; };
const handleAvailabilityUpdate = useCallback((newAvailability) => { const handleAvailabilityUpdate = useCallback((newAvailability) => {
setFormData((prev) => { setFormData((prev) => {
if (JSON.stringify(prev.availability) !== JSON.stringify(newAvailability)) {
return { ...prev, availability: newAvailability }; return { ...prev, availability: newAvailability };
}
return prev;
}); });
}, []); }, [setFormData]);
const handleCepBlur = async () => { const handleCepBlur = async () => {
const cep = formData.cep?.replace(/\D/g, ""); const cep = formData.cep?.replace(/\D/g, "");
@ -326,17 +323,9 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
} }
try { try {
const savedDoctor = await onSave({ ...formData }); // Chama a função onSave (handleSave no DoctorEditPage) com o formData completo.
// A lógica de salvamento do médico e da disponibilidade é responsabilidade do componente pai.
if (formData.availability && formData.availability.length > 0 && savedDoctor.id) { await onSave({ ...formData });
if (formData.availabilityId) {
await handlePatchAvailability(formData.availabilityId, formData.availability);
} else {
await handleCreateAvailability(savedDoctor.id, formData.availability);
}
}
} catch (error) { } catch (error) {
console.error("Erro ao salvar médico ou disponibilidade:", error); console.error("Erro ao salvar médico ou disponibilidade:", error);

View File

@ -152,3 +152,17 @@
.btn-add:hover { .btn-add:hover {
background-color: #059669; background-color: #059669;
} }
@media (max-width: 768px) {
.horarios-container { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 576px) {
.horarios-container { grid-template-columns: 1fr; gap: 10px; }
.day-card { height: auto; }
.day-header label { font-size: 13px; }
.time-inputs { flex-direction: column; }
.input-wrapper input { font-size: 12px; }
.btn-add { font-size: 12px; }
.btn-remove { font-size: 12px; }
}

View File

@ -10,13 +10,13 @@ const initialBlockTemplate = {
}; };
const emptyAvailabilityTemplate = [ const emptyAvailabilityTemplate = [
{ dia: "Segunda-feira", isChecked: false, blocos: [] }, { dia: "Domingo", weekday: 0, isChecked: false, blocos: [] },
{ dia: "Terça-feira", isChecked: false, blocos: [] }, { dia: "Segunda-feira", weekday: 1, isChecked: false, blocos: [] },
{ dia: "Quarta-feira", isChecked: false, blocos: [] }, { dia: "Terça-feira", weekday: 2, isChecked: false, blocos: [] },
{ dia: "Quinta-feira", isChecked: false, blocos: [] }, { dia: "Quarta-feira", weekday: 3, isChecked: false, blocos: [] },
{ dia: "Sexta-feira", isChecked: false, blocos: [] }, { dia: "Quinta-feira", weekday: 4, isChecked: false, blocos: [] },
{ dia: "Sábado", isChecked: false, blocos: [] }, { dia: "Sexta-feira", weekday: 5, isChecked: false, blocos: [] },
{ dia: "Domingo", isChecked: false, blocos: [] }, { dia: "Sábado", weekday: 6, isChecked: false, blocos: [] },
]; ];
const HorariosDisponibilidade = ({ const HorariosDisponibilidade = ({
@ -35,10 +35,18 @@ const HorariosDisponibilidade = ({
} }
}, [initialAvailability]); }, [initialAvailability]);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
if (onUpdate) onUpdate(availability);
}, [availability, onUpdate]);
const handleDayCheck = useCallback((dayIndex, currentIsChecked) => { const handleDayCheck = useCallback((dayIndex, currentIsChecked) => {
const isChecked = !currentIsChecked; const isChecked = !currentIsChecked;
setAvailability((prev) => setAvailability((prev) => {
prev.map((day, i) => const updated = prev.map((day, i) =>
i === dayIndex i === dayIndex
? { ? {
...day, ...day,
@ -56,8 +64,10 @@ const HorariosDisponibilidade = ({
: [], : [],
} }
: day : day
)
); );
console.log('handleDayCheck - updated availability:', updated);
return updated;
});
}, []); }, []);
const handleAddBlock = useCallback((dayIndex) => { const handleAddBlock = useCallback((dayIndex) => {
@ -107,9 +117,7 @@ const HorariosDisponibilidade = ({
); );
}, []); }, []);
const handleSave = useCallback(() => {
if (onUpdate) onUpdate(availability);
}, [availability, onUpdate]);
return ( return (
<div className="horarios-container"> <div className="horarios-container">

View File

@ -1,285 +1,405 @@
import React, { useState, useEffect, useCallback, useMemo } from "react"; import React, { useState, useEffect, useMemo } from "react";
import HorariosDisponibilidade from "../components/doctors/HorariosDisponibilidade"; import HorariosDisponibilidade from "../components/doctors/HorariosDisponibilidade";
import { useAuth } from "../components/utils/AuthProvider"; import { useAuth } from "../components/utils/AuthProvider";
import API_KEY from "../components/utils/apiKeys"; import API_KEY from "../components/utils/apiKeys";
import { GetAllDoctors } from "../components/utils/Functions-Endpoints/Doctor"; import "./style/DisponibilidadesDoctorPage.css";
const ENDPOINT = const ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
"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 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 DisponibilidadesDoctorPage = () => {
const { getAuthorizationHeader } = useAuth(); const { getAuthorizationHeader } = useAuth();
const [disponibilidades, setDisponibilidades] = useState([]); const [disponibilidades, setDisponibilidades] = useState([]);
const [loading, setLoading] = useState(false);
const [doctors, setDoctors] = useState([]); const [doctors, setDoctors] = useState([]);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [selectedDoctor, setSelectedDoctor] = useState(null);
const [editando, setEditando] = useState(null); const [editando, setEditando] = useState(null);
const [doctorsLoading, setDoctorsLoading] = useState(true); 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(() => { useEffect(() => {
const fetchDoctors = async () => { const fetchDoctors = async () => {
try { try {
setDoctorsLoading(true); const requestOptions = {
const data = await GetAllDoctors(); method: "GET",
console.log("Médicos recebidos:", data); headers: getHeaders(),
setDoctors(Array.isArray(data) ? data : []); };
const response = await fetch(DOCTORS_ENDPOINT, requestOptions);
const result = await response.json();
setDoctors(Array.isArray(result) ? result : []);
} catch (error) { } catch (error) {
console.error("Erro ao carregar médicos:", error);
setDoctors([]); setDoctors([]);
} finally {
setDoctorsLoading(false);
} }
}; };
fetchDoctors(); fetchDoctors();
}, []); }, [getAuthorizationHeader]);
const resolveAuthHeader = () => {
try {
const h = getAuthorizationHeader();
return h || "";
} catch {
return "";
}
};
const getHeaders = () => {
const myHeaders = new Headers();
const authHeader = resolveAuthHeader();
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(() => { useEffect(() => {
if (selectedDoctor) { const fetchDisponibilidades = async () => {
fetchDisponibilidades(selectedDoctor.id);
} else {
fetchDisponibilidades(null);
}
}, [selectedDoctor, fetchDisponibilidades]);
const atualizarDisponibilidade = async (id, dadosAtualizados) => {
try { try {
const res = await fetch(`${ENDPOINT}?id=eq.${id}`, { const res = await fetch(ENDPOINT, { method: "GET", headers: getHeaders() });
method: "PATCH",
headers: getHeaders(),
body: JSON.stringify(dadosAtualizados),
});
if (res.ok) { if (res.ok) {
alert("Disponibilidade atualizada com sucesso!"); const data = await res.json();
setEditando(null); setDisponibilidades(Array.isArray(data) ? data : []);
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) { } catch (error) {
console.error("Erro:", error); setDisponibilidades([]);
alert("Falha ao conectar com o servidor"); }
};
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) => { const deletarDisponibilidade = async (id) => {
if (!window.confirm("Deseja realmente excluir esta disponibilidade?")) if (!window.confirm("Deseja realmente excluir esta disponibilidade?")) return;
return;
try { try {
const res = await fetch(`${ENDPOINT}?id=eq.${id}`, { const res = await fetch(`${ENDPOINT}?id=eq.${id}`, { method: "DELETE", headers: getHeaders() });
method: "DELETE", if (res.ok) setDisponibilidades((prev) => prev.filter((d) => d.id !== id));
headers: getHeaders(), } catch (error) {}
});
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);
alert("Erro ao conectar com o servidor");
}
}; };
const initialAvailabilityParaEdicao = useMemo( const disponibilidadesAgrupadas = useMemo(() => {
() => const agrupadas = {};
diasDaSemana.map((dia, weekdayIndex) => { doctors.forEach((doctor) => {
const blocosDoDia = disponibilidades agrupadas[doctor.id] = {
.filter((d) => d.weekday === weekdayIndex && d.active !== false) doctor_id: doctor.id,
.map((d) => ({ 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, id: d.id,
inicio: d.start_time ? d.start_time.substring(0, 5) : "07:00", inicio: formatTime(d.start_time) || "07:00",
termino: d.end_time ? d.end_time.substring(0, 5) : "17:00", termino: formatTime(d.end_time) || "17:00",
isNew: false,
slot_minutes: d.slot_minutes || 30, slot_minutes: d.slot_minutes || 30,
appointment_type: d.appointment_type || "presencial", appointment_type: d.appointment_type || "presencial",
active: d.active !== false, isNew: false,
})); });
}
});
const resultado = [1, 2, 3, 4, 5, 6, 0].map((weekday) => {
const blocosDoDia = blocosPorDia[weekday] || [];
return { return {
dia, dia: diasDaSemana[weekday],
weekday: weekdayIndex, weekday: weekday,
isChecked: blocosDoDia.length > 0, isChecked: blocosDoDia.length > 0,
blocos: blocosDoDia, blocos:
blocosDoDia.length > 0
? blocosDoDia
: [
{
id: null,
inicio: "07:00",
termino: "17:00",
slot_minutes: 30,
appointment_type: "presencial",
isNew: true,
},
],
}; };
}), });
[disponibilidades] return resultado;
); }, [disponibilidades, editando]);
const handleUpdateHorarios = (horariosAtualizados) => { const handleUpdateHorarios = (horariosAtualizados) => {
const bloco = horariosAtualizados if (!editando) return;
.flatMap((d) => d.blocos) setAvailabilityEdit(horariosAtualizados || []);
.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);
}; };
const filteredDoctors = useMemo(() => { const filteredDoctors = useMemo(() => {
if (!searchTerm) return doctors; if (!searchTerm) return doctors;
return doctors.filter((doc) => return doctors.filter((doc) => (doc.full_name || doc.name).toLowerCase().includes(searchTerm.toLowerCase()));
doc.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [doctors, searchTerm]); }, [doctors, searchTerm]);
return ( const handleCancelarEdicao = () => {
<div id="main-content"> setEditando(null);
<h1 style={{ fontSize: "1.5rem", fontWeight: "bold", color: "#333" }}> setAvailabilityEdit([]);
Disponibilidades dos Médicos };
</h1>
<div style={{ marginTop: "10px", marginBottom: "10px" }}> 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 <input
type="text" type="text"
placeholder="Buscar médico por nome..." placeholder="Buscar médico por nome..."
value={searchTerm} value={searchTerm}
onChange={(e) => { onChange={(e) => {
setSearchTerm(e.target.value); setSearchTerm(e.target.value);
setSelectedDoctor(null); setShowSuggestions(true);
}}
style={{
border: "1px solid #ccc",
borderRadius: "4px",
padding: "6px",
width: "300px",
}} }}
onFocus={() => setShowSuggestions(true)}
className="search-input"
/> />
{searchTerm && ( {searchTerm && (
<ul <button onClick={handleClearSearch} className="clear-search-btn">
style={{ ×
border: "1px solid #ddd", </button>
borderRadius: "4px",
backgroundColor: "white",
position: "absolute",
zIndex: 10,
width: "300px",
maxHeight: "150px",
overflowY: "auto",
marginTop: "4px",
listStyle: "none",
padding: 0,
}}
>
{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> </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> </div>
<section className="calendario-ou-filaespera"> <section className="calendario-ou-filaespera">
<div className="fila-container"> <div className="fila-container">
<h2 className="fila-titulo"> <h2 className="section-title">{editando ? `Editar Horários` : "Lista de Disponibilidades"}</h2>
{editando ? "Editar Disponibilidade" : "Lista de Disponibilidades"}{" "}
({disponibilidades.length})
</h2>
{loading ? ( {doctors.length === 0 ? (
<p>Carregando...</p> <p className="loading-text">Carregando médicos...</p>
) : disponibilidades.length === 0 ? (
<p>Nenhuma disponibilidade encontrada.</p>
) : editando ? ( ) : editando ? (
<> <>
<HorariosDisponibilidade <div className="edit-container">
initialAvailability={initialAvailabilityParaEdicao} {initialAvailabilityParaEdicao.length > 0 ? (
onUpdate={handleUpdateHorarios} <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 <button
onClick={() => onClick={() =>
handleUpdateHorarios(initialAvailabilityParaEdicao) salvarTodasDisponibilidades(editando, availabilityEdit.length > 0 ? availabilityEdit : initialAvailabilityParaEdicao)
} }
style={{ className="disp-btn-primary"
marginTop: "20px",
padding: "10px 20px",
fontSize: "16px",
fontWeight: "bold",
borderRadius: "8px",
backgroundColor: "#3b82f6",
color: "white",
border: "none",
cursor: "pointer",
}}
> >
Salvar Alterações Salvar Alterações
</button> </button>
<button onClick={handleCancelarEdicao} className="disp-btn-danger">
Cancelar
</button>
</div>
</> </>
) : ( ) : (
<table className="fila-tabela"> <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> <thead>
<tr> <tr>
<th>Médico</th>
<th>Dia da Semana</th> <th>Dia da Semana</th>
<th>Início</th> <th>Início</th>
<th>Término</th> <th>Término</th>
@ -290,67 +410,28 @@ const DisponibilidadesDoctorPage = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{disponibilidades.map((disp) => { {grupo.disponibilidades.map((disp) => (
const medico = doctors.find((d) => d.id === disp.doctor_id);
return (
<tr key={disp.id}> <tr key={disp.id}>
<td>{medico ? medico.name : disp.doctor_id}</td> <td>{disp.is_empty ? "Nenhum horário cadastrado" : getDiaSemana(disp.weekday)}</td>
<td>{diasDaSemana[disp.weekday]}</td> <td>{disp.is_empty ? "-" : formatTime(disp.start_time)}</td>
<td>{disp.start_time}</td> <td>{disp.is_empty ? "-" : formatTime(disp.end_time)}</td>
<td>{disp.end_time}</td> <td>{disp.is_empty ? "-" : disp.slot_minutes || 30}</td>
<td>{disp.slot_minutes || 30}</td> <td>{disp.is_empty ? "-" : disp.appointment_type || "presencial"}</td>
<td>{disp.appointment_type || "presencial"}</td>
<td> <td>
<span <span className={getStatusBadgeClass(disp)}>{getStatusText(disp)}</span>
className={`badge ${
disp.active === false
? "badge-inactive"
: "badge-active"
}`}
>
{disp.active === false ? "Inativa" : "Ativa"}
</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>
<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>
</div>
</td> </td>
<td>{!disp.is_empty && <button onClick={() => deletarDisponibilidade(disp.id)} className="disp-btn-delete">Excluir</button>}</td>
</tr> </tr>
); ))}
})}
</tbody> </tbody>
</table> </table>
</div>
</div>
)}
</div>
))
)}
</div>
)} )}
</div> </div>
</section> </section>

View File

@ -1,104 +1,333 @@
import React, { useEffect, useState, useCallback } from "react"; import React, { useState, useEffect, useMemo } from "react";
import { useParams, useSearchParams } from "react-router-dom"; import { useParams, useNavigate, useLocation } from "react-router-dom";
import { GetDoctorByID } from "../components/utils/Functions-Endpoints/Doctor";
import DoctorForm from "../components/doctors/DoctorForm"; import DoctorForm from "../components/doctors/DoctorForm";
import { useAuth } from "../components/utils/AuthProvider"; import { useAuth } from "../components/utils/AuthProvider";
import API_KEY from "../components/utils/apiKeys"; import API_KEY from "../components/utils/apiKeys";
const ENDPOINT_AVAILABILITY = const ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors";
"https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability"; const ENDPOINT_AVAILABILITY = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
const DoctorEditPage = ({DictInfo}) => { 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 EditDoctorPage = () => {
const { id } = useParams();
const navigate = useNavigate();
const location = useLocation();
const { getAuthorizationHeader } = useAuth(); const { getAuthorizationHeader } = useAuth();
const [DoctorToPUT, setDoctorPUT] = useState({});
const Parametros = useParams(); const [doctor, setDoctor] = useState(null);
const [searchParams] = useSearchParams(); const [availability, setAvailability] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const DoctorID = "b24c88b2-1d51-4c04-8fe8-e75c3f2817d1"; const effectiveId = id;
const availabilityId = searchParams.get("availabilityId");
const [availabilityToPATCH, setAvailabilityToPATCH] = useState(null); const getHeaders = () => {
const [mode, setMode] = useState("doctor"); const myHeaders = new Headers();
console.log("teste", DictInfo)
useEffect(() => {
setDoctorPUT(DictInfo)
}, [DictInfo]);
const HandlePutDoctor = async () => {
const authHeader = getAuthorizationHeader(); const authHeader = getAuthorizationHeader();
if (authHeader) myHeaders.append("Authorization", authHeader);
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json"); myHeaders.append("Content-Type", "application/json");
if (API_KEY) myHeaders.append("apikey", API_KEY);
var raw = JSON.stringify(DoctorToPUT); myHeaders.append("Prefer", "return=representation");
return myHeaders;
console.log("Enviando médico para atualização (PUT):", DoctorToPUT);
var requestOptions = {
method: "PUT",
headers: myHeaders,
body: raw,
redirect: "follow",
};
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?id=eq.${DictInfo.id}`,requestOptions)
.then(response => console.log(response))
};
// 2. Função para Atualizar DISPONIBILIDADE (PATCH)
const HandlePatchAvailability = async (data) => {
const authHeader = getAuthorizationHeader();
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
var raw = JSON.stringify(data);
console.log("Enviando disponibilidade para atualização (PATCH):", data);
var requestOptions = {
method: "PATCH",
headers: myHeaders,
body: raw,
redirect: "follow",
}; };
const salvarDisponibilidades = async (doctorId, horariosAtualizados) => {
try { try {
const response = await fetch( const headers = getHeaders();
`${ENDPOINT_AVAILABILITY}?id=eq.${availabilityId}`, const promises = [];
requestOptions 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_AVAILABILITY}?id=eq.${bloco.id}`, {
method: "PATCH",
headers,
body: JSON.stringify(payload),
}).then((res) => {
if (!res.ok) throw new Error(`Erro no PATCH: ${res.status}`);
return { type: "PATCH", id: bloco.id };
})
); );
console.log("Resposta PATCH Disponibilidade:", response); } else {
alert("Disponibilidade atualizada com sucesso!"); promises.push(
// Opcional: Redirecionar de volta para a lista de disponibilidades fetch(ENDPOINT_AVAILABILITY, {
// navigate('/disponibilidades'); method: "POST",
headers,
body: JSON.stringify(payload),
})
.then((res) => res.json())
.then((data) => {
const createdItem = Array.isArray(data) ? data[0] : data;
if (createdItem && createdItem.id) {
return { type: "POST", id: createdItem.id };
}
return { type: "POST", id: null };
})
);
}
}
}
}
const results = await Promise.all(promises);
results.forEach((res) => {
if (res.type === "POST" && res.id) currentIds.add(res.id);
});
const existingDisponibilidadesRes = await fetch(
`${ENDPOINT_AVAILABILITY}?doctor_id=eq.${String(doctorId)}`,
{ method: "GET", headers }
);
if (existingDisponibilidadesRes.ok) {
const existingDisponibilidades = await existingDisponibilidadesRes.json();
const deletePromises = existingDisponibilidades
.filter((disp) => !currentIds.has(disp.id))
.map((disp) =>
fetch(`${ENDPOINT_AVAILABILITY}?id=eq.${disp.id}`, {
method: "DELETE",
headers,
})
);
await Promise.all(deletePromises);
}
const updatedResponse = await fetch(
`${ENDPOINT_AVAILABILITY}?doctor_id=eq.${doctorId}&order=weekday.asc,start_time.asc`,
{ method: "GET", headers }
);
if (updatedResponse.ok) {
const updatedData = await updatedResponse.json();
setAvailability(updatedData);
}
} catch (error) { } catch (error) {
console.error("Erro ao atualizar disponibilidade:", error);
alert("Erro ao atualizar disponibilidade.");
throw error; throw error;
} }
}; };
const normalizeAvailabilityForForm = (availabilityData) => {
if (!Array.isArray(availabilityData)) return [];
const disponibilidadesMedico = availabilityData.filter((d) =>
String(d.doctor_id) === String(effectiveId) && d.active !== false
);
const blocosPorDia = {};
disponibilidadesMedico.forEach((d) => {
const num = typeof d.weekday === "string" ? weekdayStrToNum[d.weekday.toLowerCase()] : d.weekday;
if (num === undefined || num === null) return;
if (!blocosPorDia[num]) blocosPorDia[num] = [];
blocosPorDia[num].push({
id: d.id,
inicio: d.start_time?.substring(0, 5) || "07:00",
termino: d.end_time?.substring(0, 5) || "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;
};
const availabilityFormatted = useMemo(() => {
return normalizeAvailabilityForForm(availability);
}, [availability, effectiveId]);
useEffect(() => {
const fetchDoctorData = async () => {
if (!effectiveId || effectiveId === "edit") {
alert("ID do médico não encontrado");
navigate("/secretaria/medicos");
return;
}
try {
const doctorResponse = await fetch(`${ENDPOINT}?id=eq.${effectiveId}`, {
method: "GET",
headers: getHeaders(),
});
if (!doctorResponse.ok) {
throw new Error("Erro ao carregar dados do médico");
}
const doctorData = await doctorResponse.json();
if (doctorData.length === 0) {
throw new Error("Médico não encontrado");
}
setDoctor(doctorData[0]);
const availabilityResponse = await fetch(
`${ENDPOINT_AVAILABILITY}?doctor_id=eq.${effectiveId}&order=weekday.asc,start_time.asc`,
{
method: "GET",
headers: getHeaders(),
}
);
if (availabilityResponse.ok) {
const availabilityData = await availabilityResponse.json();
setAvailability(availabilityData);
} else {
setAvailability([]);
}
} catch (error) {
alert("Erro ao carregar dados do médico");
navigate("/secretaria/medicos");
} finally {
setIsLoading(false);
}
};
if (effectiveId) {
fetchDoctorData();
}
}, [effectiveId, navigate]);
const handleSave = async (formData) => {
const { availability: updatedAvailability, ...doctorDataToSave } = formData;
try {
setIsSaving(true);
const response = await fetch(`${ENDPOINT}?id=eq.${effectiveId}`, {
method: "PATCH",
headers: getHeaders(),
body: JSON.stringify(doctorDataToSave),
});
if (!response.ok) {
throw new Error("Erro ao salvar dados do médico");
}
if (updatedAvailability && updatedAvailability.length > 0) {
await salvarDisponibilidades(effectiveId, updatedAvailability);
}
alert("Médico e horários atualizados com sucesso!");
navigate("/secretaria/medicos");
} catch (error) {
alert(`Erro ao salvar dados: ${error.message}`);
} finally {
setIsSaving(false);
}
console.log('Horários a serem salvos:', updatedAvailability);
};
const handleCancel = () => {
navigate("/secretaria/medicos");
};
if (isLoading) {
return ( return (
<div> <div className="container mt-4">
<div className="d-flex justify-content-center">
<div className="spinner-border" role="status">
<span className="visually-hidden">Carregando...</span>
</div>
</div>
<p className="text-center mt-2">
Carregando dados do médico ID: {effectiveId || "..."}
</p>
</div>
);
}
if (!doctor) {
if (!isLoading) {
return (
<div className="container mt-4">
<div className="alert alert-danger">
Médico não encontrado
</div>
</div>
);
}
return null;
}
const formData = {
...doctor,
availability: (doctor && doctor.availability) ? doctor.availability : availabilityFormatted,
};
return (
<div className="container mt-4">
<div className="row">
<div className="col-12">
<h1>Editar Médico</h1>
<DoctorForm <DoctorForm
onSave={ formData={formData}
mode === "availability" ? HandlePatchAvailability : HandlePutDoctor setFormData={setDoctor}
} onSave={handleSave}
formData={mode === "availability" ? availabilityToPATCH : DoctorToPUT} onCancel={handleCancel}
setFormData={ isLoading={isSaving}
mode === "availability" ? setAvailabilityToPATCH : setDoctorPUT isEditing={true}
}
isEditingAvailability={mode === "availability"}
/> />
</div> </div>
</div>
</div>
); );
}; };
export default DoctorEditPage; export default EditDoctorPage;

View File

@ -147,7 +147,7 @@ function TableDoctor({setDictInfo}) {
return resultado; return resultado;
}) : []; }) : [];
// Aplica ordenação rápida
const applySorting = (arr) => { const applySorting = (arr) => {
if (!Array.isArray(arr) || !sortKey) return arr; if (!Array.isArray(arr) || !sortKey) return arr;
const copy = [...arr]; const copy = [...arr];
@ -437,13 +437,13 @@ function TableDoctor({setDictInfo}) {
<td>{medico.email || 'Não informado'}</td> <td>{medico.email || 'Não informado'}</td>
<td> <td>
<div className="d-flex gap-2"> <div className="d-flex gap-2">
<Link to={`details`}> <Link to={`details/${medico.id}`}>
<button className="btn btn-sm btn-view" onClick={() => setDictInfo({...medico})}> <button className="btn btn-sm btn-view" onClick={() => setDictInfo({...medico})}>
<i className="bi bi-eye me-1"></i> Ver Detalhes <i className="bi bi-eye me-1"></i> Ver Detalhes
</button> </button>
</Link> </Link>
<Link to={`edit`}> <Link to={`edit/${medico.id}`}>
<button className="btn btn-sm btn-edit" onClick={() => setDictInfo({...medico})}> <button className="btn btn-sm btn-edit" onClick={() => setDictInfo({...medico})}>
<i className="bi bi-pencil me-1"></i> Editar <i className="bi bi-pencil me-1"></i> Editar
</button> </button>

View File

@ -223,6 +223,23 @@
.btn-action.btn-delete:hover { .btn-action.btn-delete:hover {
background-color: #C53030; background-color: #C53030;
} }
@media (max-width: 768px) {
.unidade-selecionarprofissional { flex-direction: column; align-items: stretch; gap: 12px; }
.calendar-wrapper { flex-direction: column; padding: 16px; }
.calendar-info-panel { flex: 0 0 auto; border-right: none; border-bottom: 1px solid #E2E8F0; padding-right: 0; padding-bottom: 16px; }
.calendar-grid { grid-template-columns: repeat(4, 1fr); }
.calendar-controls { flex-direction: column; align-items: flex-start; gap: 8px; }
}
@media (max-width: 576px) {
.calendar-grid { grid-template-columns: repeat(2, 1fr); }
.date-indicator h2 { font-size: 1.25rem; }
.legend-item { font-size: 0.75rem; padding: 4px 8px; }
.appointment-item { flex-direction: column; align-items: stretch; gap: 8px; }
.appointment-actions { width: 100%; }
.btn-action { width: 100%; }
}
.btn-adicionar-consulta { .btn-adicionar-consulta {
background-color: #2a67e2; background-color: #2a67e2;
color: #fff; color: #fff;

View File

@ -0,0 +1,394 @@
.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;
}
@media (max-width: 576px) {
.disponibilidades-container { padding: 12px; }
.disponibilidades-title { font-size: 1.25rem; }
.search-container { padding: 12px; }
.doctor-header { padding: 12px; }
.doctor-name { font-size: 1rem; }
.doctor-hours { display: none; }
.expand-icon { font-size: 0.9rem; }
.table-container { padding: 0 6px 6px 6px; }
.disponibilidades-table thead th { padding: 6px 8px; }
.disponibilidades-table td { padding: 10px 8px; font-size: 0.8125rem; }
.disponibilidades-table thead th:nth-child(4),
.disponibilidades-table thead th:nth-child(5),
.disponibilidades-table thead th:nth-child(7),
.disponibilidades-table tbody td:nth-child(4),
.disponibilidades-table tbody td:nth-child(5),
.disponibilidades-table tbody td:nth-child(7) {
display: none;
}
.disp-buttons-container { flex-direction: column; gap: 10px; }
.disp-btn-primary, .disp-btn-danger { width: 100%; }
.suggestions-dropdown { width: calc(100% - 30px); left: 15px; right: 15px; }
}
.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: #fa273c;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.2s;
}
.disp-btn-danger:hover {
background-color: #f41936;
}
/* 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%;
}
}

View File

@ -256,6 +256,15 @@ html, body {
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
@media (max-width: 576px) {
.unidade-selecionarprofissional { flex-direction: column; gap: 10px; }
.unidade-selecionarprofissional input,
.unidade-selecionarprofissional select { width: 100%; margin-left: 0; }
.busca-fila-espera { position: static; width: 100%; margin-bottom: 8px; }
.fila-header { height: auto; flex-direction: column; gap: 8px; }
.btns-e-legenda-container { flex-direction: column; gap: 10px; }
.legenda-tabela { justify-content: center; flex-wrap: wrap; }
}
.fila-header { .fila-header {
position: relative; position: relative;
display: flex; display: flex;

View File

@ -160,6 +160,17 @@
gap: 0.75rem; gap: 0.75rem;
} }
@media (max-width: 576px) {
.table-doctor-card .card-header { padding: 0.75rem 1rem; }
.table-doctor-table th, .table-doctor-table td { padding: 8px 6px; }
.table-doctor-table thead th:nth-child(2),
.table-doctor-table thead th:nth-child(4),
.table-doctor-table tbody td:nth-child(2),
.table-doctor-table tbody td:nth-child(4) { display: none; }
.filter-buttons-container { width: 100%; }
.filter-btn { width: 100%; }
}
@media (max-width: 768px) { @media (max-width: 768px) {
.table-doctor-table { .table-doctor-table {
font-size: 0.875rem; font-size: 0.875rem;

View File

@ -338,3 +338,13 @@
padding: 0.4em 0.65em; padding: 0.4em 0.65em;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
@media (max-width: 576px) {
.table-paciente-card .card-header { padding: 0.75rem 1rem; }
.table-paciente-table th, .table-paciente-table td { padding: 8px 6px; }
.table-paciente-table thead th:nth-child(2),
.table-paciente-table thead th:nth-child(4),
.table-paciente-table tbody td:nth-child(2),
.table-paciente-table tbody td:nth-child(4) { display: none; }
.table-paciente-filters .btn-sm { width: 100%; }
}

View File

@ -34,8 +34,8 @@ function PerfilSecretaria({ onLogout }) {
<Route path="medicos" element={<DoctorTable setDictInfo={setDictInfo} />} /> <Route path="medicos" element={<DoctorTable setDictInfo={setDictInfo} />} />
<Route path="pacientes/details" element={<Details DictInfo={DictInfo}/>} /> <Route path="pacientes/details" element={<Details DictInfo={DictInfo}/>} />
<Route path="pacientes/edit" element={<EditPage DictInfo={DictInfo}/>} /> <Route path="pacientes/edit" element={<EditPage DictInfo={DictInfo}/>} />
<Route path="medicos/details" element={<DoctorDetails doctor={DictInfo} />} /> <Route path="medicos/details/:id" element={<DoctorDetails doctor={DictInfo} />} />
<Route path="medicos/edit" element={<DoctorEditPage DictInfo={DictInfo} />} /> <Route path="medicos/edit/:id" element={<DoctorEditPage DictInfo={DictInfo} />} />
<Route path="agendamento" element={<Agendamento setDictInfo={setDictInfo}/>} /> <Route path="agendamento" element={<Agendamento setDictInfo={setDictInfo}/>} />
<Route path="agendamento/edit" element={<AgendamentoEditPage setDictInfo={setDictInfo} DictInfo={DictInfo}/>} /> <Route path="agendamento/edit" element={<AgendamentoEditPage setDictInfo={setDictInfo} DictInfo={DictInfo}/>} />
<Route path="laudo" element={<LaudoManager />} /> <Route path="laudo" element={<LaudoManager />} />