fix: excessão medica painel secretaria
This commit is contained in:
parent
6c0c7d75b8
commit
f2a9dc7b70
@ -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) => (
|
|
||||||
<div
|
{/* Exceções (bloqueios e disponibilidades extras) */}
|
||||||
key={i}
|
{day.exceptions.map((exc, i) => {
|
||||||
className="text-xs bg-green-100 text-green-800 p-1 rounded mb-1 truncate"
|
const timeRange = exc.start_time && exc.end_time
|
||||||
>
|
? `${exc.start_time} - ${exc.end_time}`
|
||||||
{apt.patient_id}
|
: "Dia inteiro";
|
||||||
</div>
|
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>
|
||||||
))}
|
))}
|
||||||
</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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user