fix: excessão medica painel secretaria

This commit is contained in:
Pedro Araujo da Silveira 2025-11-02 23:37:02 -03:00
parent 6c0c7d75b8
commit f2a9dc7b70

View File

@ -15,6 +15,7 @@ import {
type Doctor, type Doctor,
type Appointment, type Appointment,
type DoctorAvailability, type DoctorAvailability,
type DoctorException,
type Weekday, type Weekday,
} from "../../services"; } from "../../services";
@ -41,6 +42,7 @@ interface DayCell {
date: Date; date: Date;
isCurrentMonth: boolean; isCurrentMonth: boolean;
appointments: Appointment[]; appointments: Appointment[];
exceptions: DoctorException[];
} }
// Helper para formatar nome do médico sem duplicar "Dr." // Helper para formatar nome do médico sem duplicar "Dr."
@ -61,6 +63,8 @@ export function SecretaryDoctorSchedule() {
const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>( const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>(
[] []
); );
const [appointments, setAppointments] = useState<Appointment[]>([]);
const [exceptions, setExceptions] = useState<DoctorException[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// Modal states // Modal states
@ -87,6 +91,9 @@ export function SecretaryDoctorSchedule() {
const [exceptionStartDate, setExceptionStartDate] = useState(""); const [exceptionStartDate, setExceptionStartDate] = useState("");
const [exceptionEndDate, setExceptionEndDate] = useState(""); const [exceptionEndDate, setExceptionEndDate] = useState("");
const [exceptionReason, setExceptionReason] = useState(""); const [exceptionReason, setExceptionReason] = useState("");
const [exceptionIsFullDay, setExceptionIsFullDay] = useState(true);
const [exceptionStartTime, setExceptionStartTime] = useState("08:00");
const [exceptionEndTime, setExceptionEndTime] = useState("18:00");
useEffect(() => { useEffect(() => {
loadDoctors(); loadDoctors();
@ -127,8 +134,25 @@ export function SecretaryDoctorSchedule() {
setAvailabilities(Array.isArray(availData) ? availData : []); setAvailabilities(Array.isArray(availData) ? availData : []);
// Load appointments for the month (will be used for calendar display) // Load appointments for the doctor
await appointmentService.list(); const appointmentsData = await appointmentService.list({
doctor_id: selectedDoctorId,
});
console.log("[SecretaryDoctorSchedule] Consultas recebidas:", {
count: appointmentsData?.length || 0,
data: appointmentsData,
});
setAppointments(Array.isArray(appointmentsData) ? appointmentsData : []);
// Load exceptions for the doctor
const exceptionsData = await availabilityService.listExceptions({
doctor_id: selectedDoctorId,
});
console.log("[SecretaryDoctorSchedule] Exceções recebidas:", {
count: exceptionsData?.length || 0,
data: exceptionsData,
});
setExceptions(Array.isArray(exceptionsData) ? exceptionsData : []);
} catch (error) { } catch (error) {
console.error("[SecretaryDoctorSchedule] Erro ao carregar agenda:", error); console.error("[SecretaryDoctorSchedule] Erro ao carregar agenda:", error);
toast.error("Erro ao carregar agenda do médico"); toast.error("Erro ao carregar agenda do médico");
@ -154,16 +178,32 @@ export function SecretaryDoctorSchedule() {
const currentDatePointer = new Date(startDate); const currentDatePointer = new Date(startDate);
for (let i = 0; i < 42; i++) { for (let i = 0; i < 42; i++) {
const dayDate = new Date(currentDatePointer);
const dayDateStr = dayDate.toISOString().split('T')[0];
// Filter appointments for this day
const dayAppointments = appointments.filter(apt => {
if (!apt.scheduled_at) return false;
const aptDate = new Date(apt.scheduled_at).toISOString().split('T')[0];
return aptDate === dayDateStr;
});
// Filter exceptions for this day
const dayExceptions = exceptions.filter(exc => {
return exc.date === dayDateStr;
});
days.push({ days.push({
date: new Date(currentDatePointer), date: dayDate,
isCurrentMonth: currentDatePointer.getMonth() === month, isCurrentMonth: currentDatePointer.getMonth() === month,
appointments: [], appointments: dayAppointments,
exceptions: dayExceptions,
}); });
currentDatePointer.setDate(currentDatePointer.getDate() + 1); currentDatePointer.setDate(currentDatePointer.getDate() + 1);
} }
setCalendarDays(days); setCalendarDays(days);
}, [currentDate]); }, [currentDate, appointments, exceptions]);
useEffect(() => { useEffect(() => {
generateCalendar(); generateCalendar();
@ -274,14 +314,58 @@ export function SecretaryDoctorSchedule() {
return; return;
} }
if (!selectedDoctorId) {
toast.error("Selecione um médico");
return;
}
if (!exceptionIsFullDay && (!exceptionStartTime || !exceptionEndTime)) {
toast.error("Preencha os horários de início e fim");
return;
}
try { try {
// TODO: Implement exception creation const start = new Date(exceptionStartDate);
toast.success("Exceção adicionada com sucesso"); const end = new Date(exceptionEndDate);
// Criar exceções para cada dia no intervalo
const promises = [];
for (let date = new Date(start); date <= end; date.setDate(date.getDate() + 1)) {
const dateStr = date.toISOString().split('T')[0];
promises.push(
availabilityService.createException({
doctor_id: selectedDoctorId,
date: dateStr,
kind: "bloqueio",
start_time: exceptionIsFullDay ? null : exceptionStartTime,
end_time: exceptionIsFullDay ? null : exceptionEndTime,
reason: exceptionReason || exceptionType,
created_by: selectedDoctorId, // Idealmente deveria ser o ID da secretária
})
);
}
await Promise.all(promises);
const days = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
toast.success(`Exceção adicionada para ${days} dia${days > 1 ? 's' : ''} com sucesso`);
setShowExceptionDialog(false); setShowExceptionDialog(false);
setExceptionType("férias");
setExceptionStartDate("");
setExceptionEndDate("");
setExceptionReason("");
setExceptionIsFullDay(true);
setExceptionStartTime("08:00");
setExceptionEndTime("18:00");
loadDoctorSchedule(); loadDoctorSchedule();
} catch (error) { } catch (error: any) {
console.error("Erro ao adicionar exceção:", error); console.error("Erro ao adicionar exceção:", error);
toast.error("Erro ao adicionar exceção"); const errorMsg = error?.response?.data?.message ||
error?.message ||
"Erro ao adicionar exceção";
toast.error(errorMsg);
} }
}; };
@ -403,7 +487,7 @@ export function SecretaryDoctorSchedule() {
{/* Calendar */} {/* Calendar */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 capitalize"> <h2 className="text-lg font-semibold text-gray-900 capitalize">
{formatMonthYear(currentDate)} {formatMonthYear(currentDate)}
</h2> </h2>
@ -429,6 +513,30 @@ export function SecretaryDoctorSchedule() {
</div> </div>
</div> </div>
{/* Legenda */}
<div className="mb-4 flex flex-wrap gap-3 text-xs">
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-yellow-100 border border-yellow-300"></div>
<span className="text-gray-600">Solicitada</span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-green-100 border border-green-300"></div>
<span className="text-gray-600">Confirmada</span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-blue-100 border border-blue-300"></div>
<span className="text-gray-600">Concluída</span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-red-100 border border-red-300"></div>
<span className="text-gray-600">Bloqueio</span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-purple-100 border border-purple-300"></div>
<span className="text-gray-600">Disponibilidade Extra</span>
</div>
</div>
<div className="grid grid-cols-7 gap-px bg-gray-200 border border-gray-200 rounded-lg overflow-hidden"> <div className="grid grid-cols-7 gap-px bg-gray-200 border border-gray-200 rounded-lg overflow-hidden">
{["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => ( {["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => (
<div <div
@ -441,7 +549,7 @@ export function SecretaryDoctorSchedule() {
{calendarDays.map((day, index) => ( {calendarDays.map((day, index) => (
<div <div
key={index} key={index}
className={`bg-white p-2 min-h-[80px] ${ className={`bg-white p-2 min-h-[100px] ${
day.isCurrentMonth ? "" : "opacity-40" day.isCurrentMonth ? "" : "opacity-40"
} ${ } ${
day.date.toDateString() === new Date().toDateString() day.date.toDateString() === new Date().toDateString()
@ -449,17 +557,62 @@ export function SecretaryDoctorSchedule() {
: "" : ""
}`} }`}
> >
<div className="text-sm text-gray-700 mb-1"> <div className="text-sm text-gray-700 mb-1 font-medium">
{day.date.getDate()} {day.date.getDate()}
</div> </div>
{day.appointments.map((apt, i) => (
{/* Exceções (bloqueios e disponibilidades extras) */}
{day.exceptions.map((exc, i) => {
const timeRange = exc.start_time && exc.end_time
? `${exc.start_time} - ${exc.end_time}`
: "Dia inteiro";
const tooltipText = exc.reason
? `${timeRange} - ${exc.reason}`
: timeRange;
return (
<div <div
key={i} key={`exc-${i}`}
className="text-xs bg-green-100 text-green-800 p-1 rounded mb-1 truncate" className={`text-xs p-1 rounded mb-1 truncate ${
exc.kind === "bloqueio"
? "bg-red-100 text-red-800"
: "bg-purple-100 text-purple-800"
}`}
title={tooltipText}
> >
{apt.patient_id} {exc.kind === "bloqueio" ? "🚫" : ""} {timeRange}
</div> </div>
))} );
})}
{/* Consultas agendadas */}
{day.appointments.map((apt, i) => {
const time = apt.scheduled_at
? new Date(apt.scheduled_at).toLocaleTimeString('pt-BR', {
hour: '2-digit',
minute: '2-digit',
})
: '';
return (
<div
key={`apt-${i}`}
className={`text-xs p-1 rounded mb-1 truncate ${
apt.status === "requested"
? "bg-yellow-100 text-yellow-800"
: apt.status === "confirmed"
? "bg-green-100 text-green-800"
: apt.status === "completed"
? "bg-blue-100 text-blue-800"
: apt.status === "cancelled"
? "bg-gray-100 text-gray-600"
: "bg-orange-100 text-orange-800"
}`}
title={`${time} - ${apt.patient_id}`}
>
📅 {time}
</div>
);
})}
</div> </div>
))} ))}
</div> </div>
@ -532,6 +685,81 @@ export function SecretaryDoctorSchedule() {
)} )}
</div> </div>
{/* Exceções (Bloqueios e Disponibilidades Extras) */}
{exceptions.length > 0 && (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Exceções Cadastradas
</h3>
<div className="space-y-3">
{exceptions
.sort((a, b) => a.date.localeCompare(b.date))
.map((exc) => (
<div
key={exc.id}
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"
>
<div>
<p className="font-medium text-gray-900">
{new Date(exc.date).toLocaleDateString('pt-BR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</p>
<p className="text-sm text-gray-600">
{exc.start_time && exc.end_time
? `${exc.start_time} - ${exc.end_time}`
: "Dia inteiro"}
</p>
{exc.reason && (
<p className="text-sm text-gray-500 mt-1">{exc.reason}</p>
)}
</div>
<div className="flex items-center gap-2">
<span
className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${
exc.kind === "bloqueio"
? "bg-red-100 text-red-700"
: "bg-purple-100 text-purple-700"
}`}
>
{exc.kind === "bloqueio" ? "Bloqueio" : "Disponibilidade Extra"}
</span>
<button
onClick={async () => {
if (
window.confirm(
`Tem certeza que deseja remover esta exceção?\n\nData: ${new Date(
exc.date
).toLocaleDateString('pt-BR')}`
)
) {
try {
if (exc.id) {
await availabilityService.deleteException(exc.id);
toast.success("Exceção removida com sucesso");
loadDoctorSchedule();
}
} catch (error) {
console.error("Erro ao remover exceção:", error);
toast.error("Erro ao remover exceção");
}
}
}}
title="Deletar"
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
))}
</div>
</div>
)}
{/* Availability Dialog */} {/* Availability Dialog */}
{showAvailabilityDialog && ( {showAvailabilityDialog && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
@ -692,11 +920,56 @@ export function SecretaryDoctorSchedule() {
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-transparent"
/> />
</div> </div>
{/* Opção de dia inteiro ou horário específico */}
<div className="border-t border-gray-200 pt-4">
<label className="flex items-center gap-2 cursor-pointer mb-3">
<input
type="checkbox"
checked={exceptionIsFullDay}
onChange={(e) => setExceptionIsFullDay(e.target.checked)}
className="h-4 w-4 text-orange-600 border-gray-300 rounded focus:ring-orange-500"
/>
<span className="text-sm font-medium text-gray-700">
Bloqueio do dia inteiro
</span>
</label>
{!exceptionIsFullDay && (
<div className="grid grid-cols-2 gap-4 animate-in fade-in duration-200">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Hora Início
</label>
<input
type="time"
value={exceptionStartTime}
onChange={(e) => setExceptionStartTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Hora Fim
</label>
<input
type="time"
value={exceptionEndTime}
onChange={(e) => setExceptionEndTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-transparent"
/>
</div>
</div>
)}
</div>
</div> </div>
<div className="flex gap-3 mt-6"> <div className="flex gap-3 mt-6">
<button <button
onClick={() => setShowExceptionDialog(false)} onClick={() => {
setShowExceptionDialog(false);
setExceptionIsFullDay(true);
}}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors" className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
> >
Cancelar Cancelar