fix: excessão medica painel secretaria
This commit is contained in:
parent
6c0c7d75b8
commit
f2a9dc7b70
@ -15,6 +15,7 @@ import {
|
||||
type Doctor,
|
||||
type Appointment,
|
||||
type DoctorAvailability,
|
||||
type DoctorException,
|
||||
type Weekday,
|
||||
} from "../../services";
|
||||
|
||||
@ -41,6 +42,7 @@ interface DayCell {
|
||||
date: Date;
|
||||
isCurrentMonth: boolean;
|
||||
appointments: Appointment[];
|
||||
exceptions: DoctorException[];
|
||||
}
|
||||
|
||||
// Helper para formatar nome do médico sem duplicar "Dr."
|
||||
@ -61,6 +63,8 @@ export function SecretaryDoctorSchedule() {
|
||||
const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>(
|
||||
[]
|
||||
);
|
||||
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||
const [exceptions, setExceptions] = useState<DoctorException[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Modal states
|
||||
@ -87,6 +91,9 @@ export function SecretaryDoctorSchedule() {
|
||||
const [exceptionStartDate, setExceptionStartDate] = useState("");
|
||||
const [exceptionEndDate, setExceptionEndDate] = useState("");
|
||||
const [exceptionReason, setExceptionReason] = useState("");
|
||||
const [exceptionIsFullDay, setExceptionIsFullDay] = useState(true);
|
||||
const [exceptionStartTime, setExceptionStartTime] = useState("08:00");
|
||||
const [exceptionEndTime, setExceptionEndTime] = useState("18:00");
|
||||
|
||||
useEffect(() => {
|
||||
loadDoctors();
|
||||
@ -127,8 +134,25 @@ export function SecretaryDoctorSchedule() {
|
||||
|
||||
setAvailabilities(Array.isArray(availData) ? availData : []);
|
||||
|
||||
// Load appointments for the month (will be used for calendar display)
|
||||
await appointmentService.list();
|
||||
// Load appointments for the doctor
|
||||
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) {
|
||||
console.error("[SecretaryDoctorSchedule] Erro ao carregar agenda:", error);
|
||||
toast.error("Erro ao carregar agenda do médico");
|
||||
@ -154,16 +178,32 @@ export function SecretaryDoctorSchedule() {
|
||||
const currentDatePointer = new Date(startDate);
|
||||
|
||||
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({
|
||||
date: new Date(currentDatePointer),
|
||||
date: dayDate,
|
||||
isCurrentMonth: currentDatePointer.getMonth() === month,
|
||||
appointments: [],
|
||||
appointments: dayAppointments,
|
||||
exceptions: dayExceptions,
|
||||
});
|
||||
currentDatePointer.setDate(currentDatePointer.getDate() + 1);
|
||||
}
|
||||
|
||||
setCalendarDays(days);
|
||||
}, [currentDate]);
|
||||
}, [currentDate, appointments, exceptions]);
|
||||
|
||||
useEffect(() => {
|
||||
generateCalendar();
|
||||
@ -274,14 +314,58 @@ export function SecretaryDoctorSchedule() {
|
||||
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 {
|
||||
// TODO: Implement exception creation
|
||||
toast.success("Exceção adicionada com sucesso");
|
||||
const start = new Date(exceptionStartDate);
|
||||
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);
|
||||
setExceptionType("férias");
|
||||
setExceptionStartDate("");
|
||||
setExceptionEndDate("");
|
||||
setExceptionReason("");
|
||||
setExceptionIsFullDay(true);
|
||||
setExceptionStartTime("08:00");
|
||||
setExceptionEndTime("18:00");
|
||||
|
||||
loadDoctorSchedule();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
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 */}
|
||||
<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">
|
||||
{formatMonthYear(currentDate)}
|
||||
</h2>
|
||||
@ -429,6 +513,30 @@ export function SecretaryDoctorSchedule() {
|
||||
</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">
|
||||
{["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => (
|
||||
<div
|
||||
@ -441,7 +549,7 @@ export function SecretaryDoctorSchedule() {
|
||||
{calendarDays.map((day, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`bg-white p-2 min-h-[80px] ${
|
||||
className={`bg-white p-2 min-h-[100px] ${
|
||||
day.isCurrentMonth ? "" : "opacity-40"
|
||||
} ${
|
||||
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()}
|
||||
</div>
|
||||
{day.appointments.map((apt, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-xs bg-green-100 text-green-800 p-1 rounded mb-1 truncate"
|
||||
>
|
||||
{apt.patient_id}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 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
|
||||
key={`exc-${i}`}
|
||||
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}
|
||||
>
|
||||
{exc.kind === "bloqueio" ? "🚫" : "➕"} {timeRange}
|
||||
</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>
|
||||
@ -532,6 +685,81 @@ export function SecretaryDoctorSchedule() {
|
||||
)}
|
||||
</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 */}
|
||||
{showAvailabilityDialog && (
|
||||
<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"
|
||||
/>
|
||||
</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 className="flex gap-3 mt-6">
|
||||
<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"
|
||||
>
|
||||
Cancelar
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user