feat: Sistema completo de agendamento com disponibilidade de médicos e reserva de consultas para pacientes

This commit is contained in:
guisilvagomes 2025-10-13 11:24:25 -03:00
parent e7aa76df75
commit 6ebbfae4f2
5 changed files with 1560 additions and 66 deletions

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:50fbfa0b9343001fbf0b803364ec8a0fe48bcc62537eb59a31abc74674d80d0a
size 807015226

View File

@ -0,0 +1,821 @@
import { useState, useEffect, useCallback } from "react";
import {
format,
addMonths,
subMonths,
startOfMonth,
endOfMonth,
eachDayOfInterval,
isSameMonth,
isSameDay,
isToday,
parseISO,
isBefore,
startOfDay,
} from "date-fns";
import { ptBR } from "date-fns/locale";
import {
Search,
Star,
MapPin,
Video,
Clock,
CalendarDays,
ChevronLeft,
ChevronRight,
Stethoscope,
AlertCircle,
CheckCircle2,
} from "lucide-react";
import { medicoService } from "../services/medicoService";
import { availabilityService } from "../services/availabilityService";
import { exceptionService } from "../services/exceptionService";
import { consultaService } from "../services/consultaService";
interface Medico {
id: string;
nome: string;
especialidade: string;
crm: string;
foto?: string;
email?: string;
telefone?: string;
valorConsulta?: number;
}
interface TimeSlot {
inicio: string;
fim: string;
ativo: boolean;
}
interface DaySchedule {
ativo: boolean;
horarios: TimeSlot[];
}
interface Availability {
domingo: DaySchedule;
segunda: DaySchedule;
terca: DaySchedule;
quarta: DaySchedule;
quinta: DaySchedule;
sexta: DaySchedule;
sabado: DaySchedule;
}
interface Exception {
id: string;
data: string;
motivo?: string;
}
const dayOfWeekMap: { [key: number]: keyof Availability } = {
0: "domingo",
1: "segunda",
2: "terca",
3: "quarta",
4: "quinta",
5: "sexta",
6: "sabado",
};
export default function AgendamentoConsulta() {
const [medicos, setMedicos] = useState<Medico[]>([]);
const [filteredMedicos, setFilteredMedicos] = useState<Medico[]>([]);
const [selectedMedico, setSelectedMedico] = useState<Medico | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const [selectedSpecialty, setSelectedSpecialty] = useState("all");
const [loading, setLoading] = useState(true);
// Calendar and scheduling states
const [currentMonth, setCurrentMonth] = useState(new Date());
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
const [availability, setAvailability] = useState<Availability | null>(null);
const [exceptions, setExceptions] = useState<Exception[]>([]);
const [availableSlots, setAvailableSlots] = useState<string[]>([]);
const [selectedTime, setSelectedTime] = useState("");
const [appointmentType, setAppointmentType] = useState<
"presencial" | "online"
>("presencial");
const [motivo, setMotivo] = useState("");
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [bookingSuccess, setBookingSuccess] = useState(false);
const [bookingError, setBookingError] = useState("");
// Load doctors on mount
useEffect(() => {
loadMedicos();
}, []);
const loadMedicos = async () => {
try {
setLoading(true);
const data = await medicoService.listarMedicos();
setMedicos(data);
setFilteredMedicos(data);
} catch (error) {
console.error("Erro ao carregar médicos:", error);
} finally {
setLoading(false);
}
};
// Filter doctors based on search and specialty
useEffect(() => {
let filtered = medicos;
if (searchTerm) {
filtered = filtered.filter(
(medico) =>
medico.nome.toLowerCase().includes(searchTerm.toLowerCase()) ||
medico.especialidade.toLowerCase().includes(searchTerm.toLowerCase())
);
}
if (selectedSpecialty !== "all") {
filtered = filtered.filter(
(medico) => medico.especialidade === selectedSpecialty
);
}
setFilteredMedicos(filtered);
}, [searchTerm, selectedSpecialty, medicos]);
// Get unique specialties
const specialties = Array.from(new Set(medicos.map((m) => m.especialidade)));
// Load availability and exceptions when doctor is selected
useEffect(() => {
if (selectedMedico) {
loadDoctorAvailability();
loadDoctorExceptions();
}
}, [selectedMedico]);
const loadDoctorAvailability = async () => {
if (!selectedMedico) return;
try {
const data = await availabilityService.getAvailability(selectedMedico.id);
if (data && data.length > 0) {
const avail = data[0];
setAvailability({
domingo: avail.domingo || { ativo: false, horarios: [] },
segunda: avail.segunda || { ativo: false, horarios: [] },
terca: avail.terca || { ativo: false, horarios: [] },
quarta: avail.quarta || { ativo: false, horarios: [] },
quinta: avail.quinta || { ativo: false, horarios: [] },
sexta: avail.sexta || { ativo: false, horarios: [] },
sabado: avail.sabado || { ativo: false, horarios: [] },
});
} else {
setAvailability(null);
}
} catch (error) {
console.error("Erro ao carregar disponibilidade:", error);
setAvailability(null);
}
};
const loadDoctorExceptions = async () => {
if (!selectedMedico) return;
try {
const data = await exceptionService.listExceptions(selectedMedico.id);
setExceptions(data || []);
} catch (error) {
console.error("Erro ao carregar exceções:", error);
setExceptions([]);
}
};
// Calculate available slots when date is selected
useEffect(() => {
if (selectedDate && availability && selectedMedico) {
calculateAvailableSlots();
} else {
setAvailableSlots([]);
}
}, [selectedDate, availability, exceptions]);
const calculateAvailableSlots = () => {
if (!selectedDate || !availability) return;
// Check if date is an exception (blocked)
const dateStr = format(selectedDate, "yyyy-MM-dd");
const isBlocked = exceptions.some((exc) => exc.data === dateStr);
if (isBlocked) {
setAvailableSlots([]);
return;
}
// Get day of week schedule
const dayOfWeek = selectedDate.getDay();
const dayKey = dayOfWeekMap[dayOfWeek];
const daySchedule = availability[dayKey];
if (!daySchedule || !daySchedule.ativo) {
setAvailableSlots([]);
return;
}
// Extract active time slots
const slots = daySchedule.horarios
.filter((slot) => slot.ativo)
.map((slot) => slot.inicio);
setAvailableSlots(slots);
};
const isDateBlocked = (date: Date): boolean => {
const dateStr = format(date, "yyyy-MM-dd");
return exceptions.some((exc) => exc.data === dateStr);
};
const isDateAvailable = (date: Date): boolean => {
if (!availability) return false;
// Check if in the past
if (isBefore(date, startOfDay(new Date()))) return false;
// Check if blocked
if (isDateBlocked(date)) return false;
// Check if day has available schedule
const dayOfWeek = date.getDay();
const dayKey = dayOfWeekMap[dayOfWeek];
const daySchedule = availability[dayKey];
return (
daySchedule?.ativo && daySchedule.horarios.some((slot) => slot.ativo)
);
};
// Calendar generation
const generateCalendarDays = () => {
const start = startOfMonth(currentMonth);
const end = endOfMonth(currentMonth);
const days = eachDayOfInterval({ start, end });
// Add padding days from previous month
const startDay = start.getDay();
const prevMonthDays = [];
for (let i = startDay - 1; i >= 0; i--) {
const day = new Date(start);
day.setDate(day.getDate() - (i + 1));
prevMonthDays.push(day);
}
return [...prevMonthDays, ...days];
};
const handlePrevMonth = () => {
setCurrentMonth(subMonths(currentMonth, 1));
};
const handleNextMonth = () => {
setCurrentMonth(addMonths(currentMonth, 1));
};
const handleSelectDoctor = (medico: Medico) => {
setSelectedMedico(medico);
setSelectedDate(undefined);
setSelectedTime("");
setMotivo("");
setBookingSuccess(false);
setBookingError("");
};
const handleBookAppointment = () => {
if (selectedMedico && selectedDate && selectedTime && motivo) {
setShowConfirmDialog(true);
}
};
const confirmAppointment = async () => {
if (!selectedMedico || !selectedDate || !selectedTime) return;
try {
setBookingError("");
// Get current user from localStorage
const userStr = localStorage.getItem("user");
if (!userStr) {
setBookingError("Usuário não autenticado");
return;
}
const user = JSON.parse(userStr);
// Create date-time string
const dataHora = `${format(
selectedDate,
"yyyy-MM-dd"
)}T${selectedTime}:00`;
// Book appointment via API
await consultaService.criarConsulta({
medicoId: selectedMedico.id,
pacienteId: user.id,
dataHora,
tipoConsulta: appointmentType,
motivoConsulta: motivo,
status: "agendada",
});
setBookingSuccess(true);
setShowConfirmDialog(false);
// Reset form after 3 seconds
setTimeout(() => {
setSelectedMedico(null);
setSelectedDate(undefined);
setSelectedTime("");
setMotivo("");
setBookingSuccess(false);
}, 3000);
} catch (error: any) {
console.error("Erro ao agendar consulta:", error);
setBookingError(
error.message || "Erro ao agendar consulta. Tente novamente."
);
setShowConfirmDialog(false);
}
};
const calendarDays = generateCalendarDays();
return (
<div className="space-y-6 p-6">
{/* Success Message */}
{bookingSuccess && (
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 flex items-center gap-3">
<CheckCircle2 className="h-5 w-5 text-green-600 dark:text-green-400" />
<div>
<p className="font-medium text-green-900 dark:text-green-100">
Consulta agendada com sucesso!
</p>
<p className="text-sm text-green-700 dark:text-green-300">
Você receberá uma confirmação por e-mail em breve.
</p>
</div>
</div>
)}
{/* Error Message */}
{bookingError && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 flex items-center gap-3">
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
<p className="text-red-900 dark:text-red-100">{bookingError}</p>
</div>
)}
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Agendar Consulta
</h1>
<p className="text-gray-600 dark:text-gray-400">
Escolha um médico e horário disponível
</p>
</div>
{/* Search and Filters */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 space-y-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Buscar Médicos
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Buscar por nome ou especialidade
</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Ex: Cardiologia, Dr. Silva..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Especialidade
</label>
<select
value={selectedSpecialty}
onChange={(e) => setSelectedSpecialty(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="all">Todas as especialidades</option>
{specialties.map((specialty) => (
<option key={specialty} value={specialty}>
{specialty}
</option>
))}
</select>
</div>
</div>
</div>
{/* Doctors List */}
{loading ? (
<div className="text-center py-12">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<p className="mt-2 text-gray-600 dark:text-gray-400">
Carregando médicos...
</p>
</div>
) : filteredMedicos.length === 0 ? (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-12 text-center">
<Stethoscope className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600 dark:text-gray-400">
Nenhum médico encontrado
</p>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{filteredMedicos.map((medico) => (
<div
key={medico.id}
className={`bg-white dark:bg-gray-800 rounded-lg shadow p-6 transition-all ${
selectedMedico?.id === medico.id ? "ring-2 ring-blue-500" : ""
}`}
>
<div className="flex gap-4">
<div className="h-16 w-16 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-300 text-xl font-bold">
{medico.nome
.split(" ")
.map((n) => n[0])
.join("")
.substring(0, 2)}
</div>
<div className="flex-1 space-y-2">
<div>
<h3 className="font-semibold text-gray-900 dark:text-white">
{medico.nome}
</h3>
<p className="text-gray-600 dark:text-gray-400">
{medico.especialidade}
</p>
<p className="text-sm text-gray-500 dark:text-gray-500">
CRM: {medico.crm}
</p>
</div>
<div className="flex items-center justify-between">
<span className="text-lg font-semibold text-blue-600 dark:text-blue-400">
{medico.valorConsulta
? `R$ ${medico.valorConsulta.toFixed(2)}`
: "Consultar valor"}
</span>
<button
onClick={() => handleSelectDoctor(medico)}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
selectedMedico?.id === medico.id
? "bg-blue-600 text-white"
: "bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-600"
}`}
>
{selectedMedico?.id === medico.id
? "Selecionado"
: "Selecionar"}
</button>
</div>
</div>
</div>
</div>
))}
</div>
)}
{/* Appointment Details */}
{selectedMedico && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 space-y-6">
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Detalhes do Agendamento
</h2>
<p className="text-gray-600 dark:text-gray-400">
Consulta com {selectedMedico.nome} -{" "}
{selectedMedico.especialidade}
</p>
</div>
{/* Appointment Type */}
<div className="flex gap-2">
<button
onClick={() => setAppointmentType("presencial")}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-lg border-2 transition-colors ${
appointmentType === "presencial"
? "border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400"
: "border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400"
}`}
>
<MapPin className="h-5 w-5" />
<span className="font-medium">Presencial</span>
</button>
<button
onClick={() => setAppointmentType("online")}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-lg border-2 transition-colors ${
appointmentType === "online"
? "border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400"
: "border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400"
}`}
>
<Video className="h-5 w-5" />
<span className="font-medium">Online</span>
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Calendar */}
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Selecione a Data
</label>
<div className="mt-2">
{/* Month/Year Navigation */}
<div className="flex items-center justify-between mb-4">
<button
onClick={handlePrevMonth}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg"
>
<ChevronLeft className="h-5 w-5" />
</button>
<span className="font-semibold text-gray-900 dark:text-white">
{format(currentMonth, "MMMM yyyy", { locale: ptBR })}
</span>
<button
onClick={handleNextMonth}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg"
>
<ChevronRight className="h-5 w-5" />
</button>
</div>
{/* Calendar Grid */}
<div className="border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
{/* Days of week header */}
<div className="grid grid-cols-7 bg-gray-50 dark:bg-gray-700">
{["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map(
(day) => (
<div
key={day}
className="text-center py-2 text-sm font-medium text-gray-600 dark:text-gray-400"
>
{day}
</div>
)
)}
</div>
{/* Calendar days */}
<div className="grid grid-cols-7">
{calendarDays.map((day, index) => {
const isCurrentMonth = isSameMonth(day, currentMonth);
const isSelected =
selectedDate && isSameDay(day, selectedDate);
const isTodayDate = isToday(day);
const isAvailable =
isCurrentMonth && isDateAvailable(day);
const isBlocked = isCurrentMonth && isDateBlocked(day);
const isPast = isBefore(day, startOfDay(new Date()));
return (
<button
key={index}
onClick={() => isAvailable && setSelectedDate(day)}
disabled={!isAvailable}
className={`
aspect-square p-2 text-sm border-r border-b border-gray-200 dark:border-gray-700
${
!isCurrentMonth
? "text-gray-300 dark:text-gray-600 bg-gray-50 dark:bg-gray-800"
: ""
}
${
isSelected
? "bg-blue-600 text-white font-bold"
: ""
}
${
isTodayDate && !isSelected
? "font-bold text-blue-600 dark:text-blue-400"
: ""
}
${
isAvailable && !isSelected
? "hover:bg-blue-50 dark:hover:bg-blue-900/20 cursor-pointer"
: ""
}
${
isBlocked
? "bg-red-50 dark:bg-red-900/20 text-red-400 line-through"
: ""
}
${
isPast && !isBlocked
? "text-gray-400 dark:text-gray-600"
: ""
}
${
!isAvailable &&
!isBlocked &&
isCurrentMonth &&
!isPast
? "text-gray-300 dark:text-gray-600"
: ""
}
`}
>
{format(day, "d")}
</button>
);
})}
</div>
</div>
{/* Legend */}
<div className="mt-3 space-y-1 text-xs text-gray-600 dark:text-gray-400">
<p>🟢 Datas disponíveis</p>
<p>🔴 Datas bloqueadas</p>
</div>
</div>
</div>
</div>
{/* Time Slots and Details */}
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Horários Disponíveis
</label>
{selectedDate ? (
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{format(selectedDate, "EEEE, d 'de' MMMM 'de' yyyy", {
locale: ptBR,
})}
</p>
) : (
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Selecione uma data
</p>
)}
</div>
{selectedDate && availableSlots.length > 0 ? (
<div className="grid grid-cols-3 gap-2">
{availableSlots.map((slot) => (
<button
key={slot}
onClick={() => setSelectedTime(slot)}
className={`flex items-center justify-center gap-1 py-2 rounded-lg border-2 transition-colors ${
selectedTime === slot
? "border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium"
: "border-gray-300 dark:border-gray-600 hover:border-blue-300 dark:hover:border-blue-700"
}`}
>
<Clock className="h-3 w-3" />
{slot}
</button>
))}
</div>
) : selectedDate ? (
<div className="p-4 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-center">
<p className="text-gray-600 dark:text-gray-400">
Nenhum horário disponível para esta data
</p>
</div>
) : (
<div className="p-4 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 text-center">
<p className="text-gray-600 dark:text-gray-400">
Selecione uma data para ver os horários
</p>
</div>
)}
{/* Reason */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Motivo da Consulta *
</label>
<textarea
placeholder="Descreva brevemente o motivo da consulta..."
value={motivo}
onChange={(e) => setMotivo(e.target.value)}
rows={4}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white resize-none"
/>
</div>
{/* Summary */}
{selectedDate && selectedTime && (
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg space-y-2">
<h4 className="font-semibold text-gray-900 dark:text-white">
Resumo
</h4>
<div className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
<p>📅 Data: {format(selectedDate, "dd/MM/yyyy")}</p>
<p> Horário: {selectedTime}</p>
<p>
📍 Tipo:{" "}
{appointmentType === "online" ? "Online" : "Presencial"}
</p>
{selectedMedico.valorConsulta && (
<p>
💰 Valor: R$ {selectedMedico.valorConsulta.toFixed(2)}
</p>
)}
</div>
</div>
)}
{/* Confirm Button */}
<button
onClick={handleBookAppointment}
disabled={!selectedTime || !motivo.trim()}
className="w-full py-3 rounded-lg font-semibold bg-blue-600 text-white hover:bg-blue-700 disabled:bg-gray-300 dark:disabled:bg-gray-700 disabled:cursor-not-allowed transition-colors"
>
Confirmar Agendamento
</button>
</div>
</div>
</div>
)}
{/* Confirmation Dialog */}
{showConfirmDialog && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6 space-y-4">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
Confirmar Agendamento
</h3>
<p className="text-gray-600 dark:text-gray-400">
Revise os detalhes da sua consulta antes de confirmar
</p>
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-300 font-bold">
{selectedMedico?.nome
.split(" ")
.map((n) => n[0])
.join("")
.substring(0, 2)}
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">
{selectedMedico?.nome}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{selectedMedico?.especialidade}
</p>
</div>
</div>
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<p>
📅 Data: {selectedDate && format(selectedDate, "dd/MM/yyyy")}
</p>
<p> Horário: {selectedTime}</p>
<p>
📍 Tipo:{" "}
{appointmentType === "online"
? "Consulta Online"
: "Consulta Presencial"}
</p>
{selectedMedico?.valorConsulta && (
<p>💰 Valor: R$ {selectedMedico.valorConsulta.toFixed(2)}</p>
)}
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<p className="font-medium text-gray-900 dark:text-white mb-1">
Motivo:
</p>
<p className="text-gray-600 dark:text-gray-400">{motivo}</p>
</div>
</div>
</div>
<div className="flex gap-3 pt-4">
<button
onClick={() => setShowConfirmDialog(false)}
className="flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
Cancelar
</button>
<button
onClick={confirmAppointment}
className="flex-1 px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors font-medium"
>
Confirmar
</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,698 @@
import React, { useState, useEffect } from "react";
import {
Clock,
Plus,
Trash2,
Save,
Copy,
Calendar as CalendarIcon,
AlertCircle,
} from "lucide-react";
import toast from "react-hot-toast";
import { format, addDays, startOfWeek } from "date-fns";
import { ptBR } from "date-fns/locale";
import availabilityService from "../services/availabilityService";
import exceptionService from "../services/exceptionService";
import { useAuth } from "../hooks/useAuth";
interface TimeSlot {
id: string;
inicio: string;
fim: string;
ativo: boolean;
}
interface DaySchedule {
day: string;
dayOfWeek: number;
enabled: boolean;
slots: TimeSlot[];
}
const daysOfWeek = [
{ key: 0, label: "Domingo", dbKey: "domingo" },
{ key: 1, label: "Segunda-feira", dbKey: "segunda" },
{ key: 2, label: "Terça-feira", dbKey: "terca" },
{ key: 3, label: "Quarta-feira", dbKey: "quarta" },
{ key: 4, label: "Quinta-feira", dbKey: "quinta" },
{ key: 5, label: "Sexta-feira", dbKey: "sexta" },
{ key: 6, label: "Sábado", dbKey: "sabado" },
];
const DisponibilidadeMedico: React.FC = () => {
const { user } = useAuth();
const medicoId = user?.id || "";
const [schedule, setSchedule] = useState<Record<number, DaySchedule>>({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [activeTab, setActiveTab] = useState<"weekly" | "blocked" | "settings">(
"weekly"
);
// States for adding slots
const [showAddSlotDialog, setShowAddSlotDialog] = useState(false);
const [selectedDay, setSelectedDay] = useState<number | null>(null);
const [newSlot, setNewSlot] = useState({ inicio: "09:00", fim: "10:00" });
// States for blocked dates
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
new Date()
);
const [blockedDates, setBlockedDates] = useState<Date[]>([]);
const [exceptions, setExceptions] = useState<any[]>([]);
// Settings
const [consultationDuration, setConsultationDuration] = useState("60");
const [breakTime, setBreakTime] = useState("0");
useEffect(() => {
if (medicoId) {
loadAvailability();
loadExceptions();
}
}, [medicoId]);
const loadAvailability = async () => {
try {
setLoading(true);
const response = await availabilityService.getAvailability(medicoId);
if (response.success && response.data && response.data.length > 0) {
const availabilityData = response.data[0];
const newSchedule: Record<number, DaySchedule> = {};
daysOfWeek.forEach(({ key, label, dbKey }) => {
const dayData = availabilityData[dbKey];
newSchedule[key] = {
day: label,
dayOfWeek: key,
enabled: dayData?.ativo || false,
slots:
dayData?.horarios?.map((h: any, index: number) => ({
id: `${key}-${index}`,
inicio: h.inicio,
fim: h.fim,
ativo: h.ativo !== false,
})) || [],
};
});
setSchedule(newSchedule);
} else {
// Initialize empty schedule
const newSchedule: Record<number, DaySchedule> = {};
daysOfWeek.forEach(({ key, label }) => {
newSchedule[key] = {
day: label,
dayOfWeek: key,
enabled: false,
slots: [],
};
});
setSchedule(newSchedule);
}
} catch (error) {
console.error("Erro ao carregar disponibilidade:", error);
toast.error("Erro ao carregar disponibilidade");
} finally {
setLoading(false);
}
};
const loadExceptions = async () => {
try {
const response = await exceptionService.listExceptions(medicoId);
if (response.success && response.data) {
setExceptions(response.data);
const blocked = response.data
.filter((exc: any) => exc.tipo === "bloqueio")
.map((exc: any) => new Date(exc.data));
setBlockedDates(blocked);
}
} catch (error) {
console.error("Erro ao carregar exceções:", error);
}
};
const toggleDay = (dayKey: number) => {
setSchedule((prev) => ({
...prev,
[dayKey]: {
...prev[dayKey],
enabled: !prev[dayKey].enabled,
},
}));
};
const addTimeSlot = () => {
if (selectedDay !== null) {
const newSlotId = `${selectedDay}-${Date.now()}`;
setSchedule((prev) => ({
...prev,
[selectedDay]: {
...prev[selectedDay],
slots: [
...prev[selectedDay].slots,
{
id: newSlotId,
inicio: newSlot.inicio,
fim: newSlot.fim,
ativo: true,
},
],
},
}));
setShowAddSlotDialog(false);
setNewSlot({ inicio: "09:00", fim: "10:00" });
setSelectedDay(null);
}
};
const removeTimeSlot = (dayKey: number, slotId: string) => {
setSchedule((prev) => ({
...prev,
[dayKey]: {
...prev[dayKey],
slots: prev[dayKey].slots.filter((slot) => slot.id !== slotId),
},
}));
};
const toggleSlotAvailability = (dayKey: number, slotId: string) => {
setSchedule((prev) => ({
...prev,
[dayKey]: {
...prev[dayKey],
slots: prev[dayKey].slots.map((slot) =>
slot.id === slotId ? { ...slot, ativo: !slot.ativo } : slot
),
},
}));
};
const copySchedule = (fromDay: number) => {
const sourceSchedule = schedule[fromDay];
if (!sourceSchedule.enabled || sourceSchedule.slots.length === 0) {
toast.error("Dia não tem horários configurados");
return;
}
const updatedSchedule = { ...schedule };
Object.keys(updatedSchedule).forEach((key) => {
const dayKey = Number(key);
if (dayKey !== fromDay && updatedSchedule[dayKey].enabled) {
updatedSchedule[dayKey].slots = sourceSchedule.slots.map((slot) => ({
...slot,
id: `${dayKey}-${slot.id}`,
}));
}
});
setSchedule(updatedSchedule);
toast.success("Horários copiados com sucesso!");
};
const handleSaveSchedule = async () => {
try {
setSaving(true);
// Build availability object
const availabilityData: any = {
medico_id: medicoId,
};
daysOfWeek.forEach(({ key, dbKey }) => {
const daySchedule = schedule[key];
availabilityData[dbKey] = {
ativo: daySchedule.enabled,
horarios: daySchedule.slots.map((slot) => ({
inicio: slot.inicio,
fim: slot.fim,
ativo: slot.ativo,
})),
};
});
const response = await availabilityService.createAvailability(
availabilityData
);
if (response.success) {
toast.success("Disponibilidade salva com sucesso!");
loadAvailability();
} else {
throw new Error(response.message || "Erro ao salvar");
}
} catch (error: any) {
console.error("Erro ao salvar disponibilidade:", error);
toast.error(error.message || "Erro ao salvar disponibilidade");
} finally {
setSaving(false);
}
};
const toggleBlockedDate = async () => {
if (!selectedDate) return;
const dateString = format(selectedDate, "yyyy-MM-dd");
const dateExists = blockedDates.some(
(d) => format(d, "yyyy-MM-dd") === dateString
);
try {
if (dateExists) {
// Remove block
const exception = exceptions.find(
(exc) => format(new Date(exc.data), "yyyy-MM-dd") === dateString
);
if (exception) {
await exceptionService.deleteException(exception._id);
setBlockedDates(
blockedDates.filter((d) => format(d, "yyyy-MM-dd") !== dateString)
);
toast.success("Data desbloqueada");
}
} else {
// Add block
const response = await exceptionService.createException({
medicoId,
data: dateString,
tipo: "bloqueio",
motivo: "Data bloqueada pelo médico",
});
if (response.success) {
setBlockedDates([...blockedDates, selectedDate]);
toast.success("Data bloqueada");
}
}
loadExceptions();
} catch (error) {
console.error("Erro ao alternar bloqueio de data:", error);
toast.error("Erro ao bloquear/desbloquear data");
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Gerenciar Disponibilidade
</h2>
<p className="text-gray-600 dark:text-gray-400">
Configure seus horários de atendimento
</p>
</div>
<button
onClick={handleSaveSchedule}
disabled={saving}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors disabled:opacity-50"
>
<Save className="h-4 w-4" />
{saving ? "Salvando..." : "Salvar Alterações"}
</button>
</div>
{/* Tabs */}
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab("weekly")}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === "weekly"
? "border-indigo-500 text-indigo-600 dark:text-indigo-400"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
Horário Semanal
</button>
<button
onClick={() => setActiveTab("blocked")}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === "blocked"
? "border-indigo-500 text-indigo-600 dark:text-indigo-400"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
Datas Bloqueadas ({blockedDates.length})
</button>
<button
onClick={() => setActiveTab("settings")}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === "settings"
? "border-indigo-500 text-indigo-600 dark:text-indigo-400"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
Configurações
</button>
</nav>
</div>
{/* Tab Content - Weekly Schedule */}
{activeTab === "weekly" && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Horários por Dia da Semana
</h3>
<p className="text-gray-600 dark:text-gray-400">
Defina seus horários de atendimento para cada dia da semana
</p>
</div>
{daysOfWeek.map(({ key, label }) => (
<div key={key} className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={schedule[key]?.enabled || false}
onChange={() => toggleDay(key)}
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 dark:peer-focus:ring-indigo-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-indigo-600"></div>
</label>
<span className="text-gray-900 dark:text-white font-medium">
{label}
</span>
{schedule[key]?.enabled && (
<span className="px-2 py-1 bg-indigo-100 dark:bg-indigo-900 text-indigo-600 dark:text-indigo-400 text-xs rounded">
{schedule[key]?.slots.length || 0} horário(s)
</span>
)}
</div>
{schedule[key]?.enabled && (
<div className="flex gap-2">
<button
onClick={() => copySchedule(key)}
className="flex items-center gap-1 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<Copy className="h-4 w-4" />
Copiar
</button>
<button
onClick={() => {
setSelectedDay(key);
setShowAddSlotDialog(true);
}}
className="flex items-center gap-1 px-3 py-1.5 text-sm bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors"
>
<Plus className="h-4 w-4" />
Adicionar Horário
</button>
</div>
)}
</div>
{schedule[key]?.enabled && (
<div className="ml-14 space-y-2">
{schedule[key]?.slots.length === 0 ? (
<p className="text-gray-500 dark:text-gray-400">
Nenhum horário configurado
</p>
) : (
schedule[key]?.slots.map((slot) => (
<div
key={slot.id}
className="flex items-center justify-between p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50"
>
<div className="flex items-center gap-3">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={slot.ativo}
onChange={() =>
toggleSlotAvailability(key, slot.id)
}
/>
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 dark:peer-focus:ring-indigo-800 rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-indigo-600"></div>
</label>
<Clock className="h-4 w-4 text-gray-400" />
<span className="text-gray-900 dark:text-white">
{slot.inicio} - {slot.fim}
</span>
{!slot.ativo && (
<span className="px-2 py-1 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 text-xs rounded">
Bloqueado
</span>
)}
</div>
<button
onClick={() => removeTimeSlot(key, slot.id)}
className="p-2 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
>
<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />
</button>
</div>
))
)}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Tab Content - Blocked Dates */}
{activeTab === "blocked" && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Selecionar Datas
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Clique em uma data no calendário e depois no botão para
bloquear/desbloquear
</p>
<div className="space-y-4">
<input
type="date"
value={selectedDate ? format(selectedDate, "yyyy-MM-dd") : ""}
onChange={(e) => setSelectedDate(new Date(e.target.value))}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
<button
onClick={toggleBlockedDate}
disabled={!selectedDate}
className="w-full py-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors disabled:opacity-50"
>
{selectedDate &&
blockedDates.some(
(d) =>
format(d, "yyyy-MM-dd") ===
format(selectedDate, "yyyy-MM-dd")
)
? "Desbloquear Data"
: "Bloquear Data"}
</button>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Datas Bloqueadas
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{blockedDates.length} data(s) bloqueada(s)
</p>
{blockedDates.length === 0 ? (
<p className="text-gray-500 dark:text-gray-400 text-center py-8">
Nenhuma data bloqueada
</p>
) : (
<div className="space-y-2 max-h-96 overflow-y-auto">
{blockedDates.map((date, index) => (
<div
key={index}
className="flex items-center justify-between p-3 rounded-lg border border-gray-200 dark:border-gray-700"
>
<span className="text-gray-900 dark:text-white">
{format(date, "EEEE, dd 'de' MMMM 'de' yyyy", {
locale: ptBR,
})}
</span>
<button
onClick={() => {
setSelectedDate(date);
toggleBlockedDate();
}}
className="p-2 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
>
<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />
</button>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* Tab Content - Settings */}
{activeTab === "settings" && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Configurações de Consulta
</h3>
<p className="text-gray-600 dark:text-gray-400">
Defina as configurações padrão para suas consultas
</p>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Duração Padrão da Consulta
</label>
<select
value={consultationDuration}
onChange={(e) => setConsultationDuration(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
>
<option value="30">30 minutos</option>
<option value="45">45 minutos</option>
<option value="60">1 hora</option>
<option value="90">1 hora e 30 minutos</option>
<option value="120">2 horas</option>
</select>
<p className="text-gray-500 dark:text-gray-400 text-sm mt-1">
Esta duração será usada para calcular os horários disponíveis
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Intervalo entre Consultas
</label>
<select
value={breakTime}
onChange={(e) => setBreakTime(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
>
<option value="0">Sem intervalo</option>
<option value="15">15 minutos</option>
<option value="30">30 minutos</option>
</select>
<p className="text-gray-500 dark:text-gray-400 text-sm mt-1">
Tempo de descanso entre consultas
</p>
</div>
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div>
<p className="text-gray-900 dark:text-white font-medium">
Aceitar consultas online
</p>
<p className="text-gray-500 dark:text-gray-400 text-sm">
Permitir agendamento de teleconsultas
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
defaultChecked
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 dark:peer-focus:ring-indigo-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-indigo-600"></div>
</label>
</div>
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div>
<p className="text-gray-900 dark:text-white font-medium">
Confirmação automática
</p>
<p className="text-gray-500 dark:text-gray-400 text-sm">
Aprovar agendamentos automaticamente
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 dark:peer-focus:ring-indigo-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-indigo-600"></div>
</label>
</div>
</div>
</div>
</div>
)}
{/* Add Time Slot Dialog */}
{showAddSlotDialog && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Adicionar Horário
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Defina o período de atendimento para{" "}
{selectedDay !== null ? schedule[selectedDay]?.day : ""}
</p>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Horário de Início
</label>
<input
type="time"
value={newSlot.inicio}
onChange={(e) =>
setNewSlot({ ...newSlot, inicio: e.target.value })
}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Horário de Término
</label>
<input
type="time"
value={newSlot.fim}
onChange={(e) =>
setNewSlot({ ...newSlot, fim: e.target.value })
}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
<div className="flex gap-2 mt-6">
<button
onClick={() => {
setShowAddSlotDialog(false);
setSelectedDay(null);
}}
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
Cancelar
</button>
<button
onClick={addTimeSlot}
className="flex-1 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors"
>
Adicionar
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default DisponibilidadeMedico;

View File

@ -24,6 +24,7 @@ import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import consultaService from "../services/consultaService"; import consultaService from "../services/consultaService";
import medicoService from "../services/medicoService"; import medicoService from "../services/medicoService";
import AgendamentoConsulta from "../components/AgendamentoConsulta";
interface Consulta { interface Consulta {
_id: string; _id: string;
@ -40,10 +41,12 @@ interface Consulta {
} }
interface Medico { interface Medico {
_id: string; _id?: string;
id?: string;
nome: string; nome: string;
especialidade: string; especialidade: string;
valorConsulta: number; valorConsulta?: number;
valor_consulta?: number;
} }
const AcompanhamentoPaciente: React.FC = () => { const AcompanhamentoPaciente: React.FC = () => {
@ -68,58 +71,61 @@ const AcompanhamentoPaciente: React.FC = () => {
if (!pacienteId) return; if (!pacienteId) return;
setLoading(true); setLoading(true);
try { try {
// Buscar consultas locais // Buscar consultas da API
const raw = localStorage.getItem("consultas_local"); const consultasResp = await consultaService.listarConsultas({
let lista: any[] = []; paciente_id: pacienteId,
if (raw) { });
try {
lista = JSON.parse(raw);
} catch {
lista = [];
}
}
const minhasConsultas = lista.filter(
(c) => c.pacienteId === pacienteId || c.pacienteId === user?.email
);
// Buscar médicos // Buscar médicos
const medicosResp = await medicoService.listarMedicos({}); const medicosResp = await medicoService.listarMedicos({});
if (medicosResp.success && medicosResp.data) { if (medicosResp.success && medicosResp.data) {
setMedicos(medicosResp.data.data); setMedicos(medicosResp.data.data as Medico[]);
} }
setConsultas( if (consultasResp.success && consultasResp.data) {
minhasConsultas.map((c) => ({ const consultasData = Array.isArray(consultasResp.data)
_id: c.id, ? consultasResp.data
pacienteId: c.pacienteId, : consultasResp.data.data || [];
medicoId: c.medicoId,
dataHora: c.dataHora, setConsultas(
status: c.status || "agendada", consultasData.map((c) => ({
tipoConsulta: c.tipo || "presencial", _id: c._id || c.id,
motivoConsulta: c.observacoes || "Consulta médica", pacienteId: c.pacienteId,
observacoes: c.observacoes, medicoId: c.medicoId,
})) dataHora: c.dataHora,
); status: c.status || "agendada",
tipoConsulta: c.tipoConsulta || c.tipo || "presencial",
motivoConsulta:
c.motivoConsulta || c.observacoes || "Consulta médica",
observacoes: c.observacoes,
resultados: c.resultados,
prescricoes: c.prescricoes,
proximaConsulta: c.proximaConsulta,
}))
);
} else {
setConsultas([]);
}
} catch (error) { } catch (error) {
console.error("Erro ao carregar consultas:", error); console.error("Erro ao carregar consultas:", error);
toast.error("Erro ao carregar consultas"); toast.error("Erro ao carregar consultas");
setConsultas([]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [pacienteId, user?.email]); }, [pacienteId]);
useEffect(() => { useEffect(() => {
fetchConsultas(); fetchConsultas();
}, [fetchConsultas]); }, [fetchConsultas]);
const getMedicoNome = (medicoId: string) => { const getMedicoNome = (medicoId: string) => {
const medico = medicos.find((m) => m._id === medicoId); const medico = medicos.find((m) => m._id === medicoId || m.id === medicoId);
return medico?.nome || "Médico"; return medico?.nome || "Médico";
}; };
const getMedicoEspecialidade = (medicoId: string) => { const getMedicoEspecialidade = (medicoId: string) => {
const medico = medicos.find((m) => m._id === medicoId); const medico = medicos.find((m) => m._id === medicoId || m.id === medicoId);
return medico?.especialidade || "Especialidade"; return medico?.especialidade || "Especialidade";
}; };
@ -625,24 +631,7 @@ const AcompanhamentoPaciente: React.FC = () => {
); );
// Book Appointment Content // Book Appointment Content
const renderBookAppointment = () => ( const renderBookAppointment = () => <AgendamentoConsulta />;
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Agendar Consulta
</h1>
<p className="text-gray-600 dark:text-gray-400">
Escolha um médico e horário disponível
</p>
</div>
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
<p className="text-center py-16 text-gray-600 dark:text-gray-400">
Funcionalidade de agendamento em desenvolvimento
</p>
</div>
</div>
);
// Messages Content // Messages Content
const renderMessages = () => ( const renderMessages = () => (

View File

@ -28,6 +28,7 @@ import { useAuth } from "../hooks/useAuth";
import relatorioService, { import relatorioService, {
RelatorioCreate, RelatorioCreate,
} from "../services/relatorioService"; } from "../services/relatorioService";
import DisponibilidadeMedico from "../components/DisponibilidadeMedico";
import ConsultaModal from "../components/consultas/ConsultaModal"; import ConsultaModal from "../components/consultas/ConsultaModal";
import AvailabilityManager from "../components/agenda/AvailabilityManager"; import AvailabilityManager from "../components/agenda/AvailabilityManager";
import ExceptionsManager from "../components/agenda/ExceptionsManager"; import ExceptionsManager from "../components/agenda/ExceptionsManager";
@ -758,19 +759,7 @@ const PainelMedico: React.FC = () => {
</div> </div>
); );
const renderAvailability = () => ( const renderAvailability = () => <DisponibilidadeMedico />;
<div className="space-y-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Gerenciar Disponibilidade
</h1>
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
<AvailabilityManager doctorId={medicoId} />
</div>
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
<ExceptionsManager doctorId={medicoId} />
</div>
</div>
);
const renderReports = () => ( const renderReports = () => (
<div className="space-y-6"> <div className="space-y-6">