riseup-squad18/src/components/secretaria/SecretaryDoctorSchedule.tsx
2025-11-02 22:41:13 -03:00

812 lines
29 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from "react";
import toast from "react-hot-toast";
import {
ChevronLeft,
ChevronRight,
Plus,
Edit,
Trash2,
Calendar as CalendarIcon,
} from "lucide-react";
import {
doctorService,
appointmentService,
availabilityService,
type Doctor,
type Appointment,
type DoctorAvailability,
type Weekday,
} from "../../services";
// Helper para converter weekday (string em inglês) para texto legível em português
const weekdayToText = (weekday: Weekday | undefined | null): string => {
if (weekday === undefined || weekday === null) {
return "Desconhecido";
}
const weekdayMap: Record<Weekday, string> = {
sunday: "Domingo",
monday: "Segunda-feira",
tuesday: "Terça-feira",
wednesday: "Quarta-feira",
thursday: "Quinta-feira",
friday: "Sexta-feira",
saturday: "Sábado",
};
return weekdayMap[weekday] || "Desconhecido";
};
interface DayCell {
date: Date;
isCurrentMonth: boolean;
appointments: Appointment[];
}
// Helper para formatar nome do médico sem duplicar "Dr."
const formatDoctorName = (fullName: string): string => {
const name = fullName.trim();
// Verifica se já começa com Dr. ou Dr (case insensitive)
if (/^dr\.?\s/i.test(name)) {
return name;
}
return `Dr. ${name}`;
};
export function SecretaryDoctorSchedule() {
const [doctors, setDoctors] = useState<Doctor[]>([]);
const [selectedDoctorId, setSelectedDoctorId] = useState<string>("");
const [currentDate, setCurrentDate] = useState(new Date());
const [calendarDays, setCalendarDays] = useState<DayCell[]>([]);
const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>(
[]
);
const [loading, setLoading] = useState(false);
// Modal states
const [showAvailabilityDialog, setShowAvailabilityDialog] = useState(false);
const [showExceptionDialog, setShowExceptionDialog] = useState(false);
const [showEditDialog, setShowEditDialog] = useState(false);
const [editingAvailability, setEditingAvailability] =
useState<DoctorAvailability | null>(null);
// Availability form
const [selectedWeekdays, setSelectedWeekdays] = useState<string[]>([]);
const [startTime, setStartTime] = useState("08:00");
const [endTime, setEndTime] = useState("18:00");
const [duration, setDuration] = useState(30);
// Edit form
const [editStartTime, setEditStartTime] = useState("08:00");
const [editEndTime, setEditEndTime] = useState("18:00");
const [editDuration, setEditDuration] = useState(30);
const [editActive, setEditActive] = useState(true);
// Exception form
const [exceptionType, setExceptionType] = useState("férias");
const [exceptionStartDate, setExceptionStartDate] = useState("");
const [exceptionEndDate, setExceptionEndDate] = useState("");
const [exceptionReason, setExceptionReason] = useState("");
useEffect(() => {
loadDoctors();
}, []);
// If a doctor id was requested by other components (via sessionStorage), select it
useEffect(() => {
const requested = sessionStorage.getItem("selectedDoctorForSchedule");
if (requested) {
setSelectedDoctorId(requested);
sessionStorage.removeItem("selectedDoctorForSchedule");
}
}, [doctors]);
useEffect(() => {
console.log("[SecretaryDoctorSchedule] Estado availabilities atualizado:", {
count: availabilities.length,
data: availabilities,
});
}, [availabilities]);
const loadDoctorSchedule = useCallback(async () => {
if (!selectedDoctorId) return;
console.log("[SecretaryDoctorSchedule] Carregando agenda do médico:", selectedDoctorId);
setLoading(true);
try {
// Load availabilities
const availData = await availabilityService.list({
doctor_id: selectedDoctorId,
});
console.log("[SecretaryDoctorSchedule] Disponibilidades recebidas:", {
count: availData?.length || 0,
data: availData,
});
setAvailabilities(Array.isArray(availData) ? availData : []);
// Load appointments for the month (will be used for calendar display)
await appointmentService.list();
} catch (error) {
console.error("[SecretaryDoctorSchedule] Erro ao carregar agenda:", error);
toast.error("Erro ao carregar agenda do médico");
} finally {
setLoading(false);
}
}, [selectedDoctorId]);
useEffect(() => {
loadDoctorSchedule();
}, [loadDoctorSchedule]);
const generateCalendar = useCallback(() => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const firstDay = new Date(year, month, 1);
const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - firstDay.getDay());
const days: DayCell[] = [];
const currentDatePointer = new Date(startDate);
for (let i = 0; i < 42; i++) {
days.push({
date: new Date(currentDatePointer),
isCurrentMonth: currentDatePointer.getMonth() === month,
appointments: [],
});
currentDatePointer.setDate(currentDatePointer.getDate() + 1);
}
setCalendarDays(days);
}, [currentDate]);
useEffect(() => {
generateCalendar();
}, [generateCalendar]);
const loadDoctors = async () => {
try {
const data = await doctorService.list();
setDoctors(Array.isArray(data) ? data : []);
if (data.length > 0) {
setSelectedDoctorId(data[0].id);
}
} catch (error) {
console.error("Erro ao carregar médicos:", error);
toast.error("Erro ao carregar médicos");
}
};
const previousMonth = () => {
setCurrentDate(
new Date(currentDate.getFullYear(), currentDate.getMonth() - 1)
);
};
const nextMonth = () => {
setCurrentDate(
new Date(currentDate.getFullYear(), currentDate.getMonth() + 1)
);
};
const goToToday = () => {
setCurrentDate(new Date());
};
const formatMonthYear = (date: Date) => {
return date.toLocaleDateString("pt-BR", { month: "long", year: "numeric" });
};
const handleAddAvailability = async () => {
if (selectedWeekdays.length === 0) {
toast.error("Selecione pelo menos um dia da semana");
return;
}
if (!selectedDoctorId) {
toast.error("Selecione um médico");
return;
}
try {
console.log("[SecretaryDoctorSchedule] Criando disponibilidades:", {
doctor_id: selectedDoctorId,
weekdays: selectedWeekdays,
start_time: startTime,
end_time: endTime,
slot_minutes: duration,
});
// Os dias da semana já estão no formato correto (sunday, monday, etc.)
const promises = selectedWeekdays.map((weekdayStr) => {
const payload = {
doctor_id: selectedDoctorId,
weekday: weekdayStr as Weekday,
start_time: startTime,
end_time: endTime,
slot_minutes: duration,
appointment_type: "presencial" as const,
active: true,
};
console.log("[SecretaryDoctorSchedule] Payload para criação:", payload);
return availabilityService.create(payload);
});
await Promise.all(promises);
toast.success(
`Disponibilidade${selectedWeekdays.length > 1 ? "s" : ""} adicionada${
selectedWeekdays.length > 1 ? "s" : ""
} com sucesso`
);
setShowAvailabilityDialog(false);
setSelectedWeekdays([]);
setStartTime("08:00");
setEndTime("18:00");
setDuration(30);
loadDoctorSchedule();
} catch (error: any) {
console.error("[SecretaryDoctorSchedule] Erro ao adicionar disponibilidade:", {
error,
message: error?.message,
response: error?.response?.data,
});
const errorMsg = error?.response?.data?.message ||
error?.response?.data?.hint ||
error?.message ||
"Erro ao adicionar disponibilidade";
toast.error(errorMsg);
}
};
const handleAddException = async () => {
if (!exceptionStartDate || !exceptionEndDate) {
toast.error("Preencha as datas de início e fim");
return;
}
try {
// TODO: Implement exception creation
toast.success("Exceção adicionada com sucesso");
setShowExceptionDialog(false);
loadDoctorSchedule();
} catch (error) {
console.error("Erro ao adicionar exceção:", error);
toast.error("Erro ao adicionar exceção");
}
};
const handleEditAvailability = (availability: DoctorAvailability) => {
setEditingAvailability(availability);
setEditStartTime(availability.start_time);
setEditEndTime(availability.end_time);
setEditDuration(availability.slot_minutes || 30);
setEditActive(availability.active ?? true);
setShowEditDialog(true);
};
const handleSaveEdit = async () => {
if (!editingAvailability?.id) return;
console.log("[SecretaryDoctorSchedule] Salvando edição:", {
id: editingAvailability.id,
start_time: editStartTime,
end_time: editEndTime,
slot_minutes: editDuration,
active: editActive,
});
try {
const updateData = {
start_time: editStartTime,
end_time: editEndTime,
slot_minutes: editDuration,
active: editActive,
};
console.log("[SecretaryDoctorSchedule] Dados de atualização:", updateData);
const result = await availabilityService.update(editingAvailability.id, updateData);
console.log("[SecretaryDoctorSchedule] Resultado da atualização:", result);
toast.success("Disponibilidade atualizada com sucesso");
setShowEditDialog(false);
setEditingAvailability(null);
loadDoctorSchedule();
} catch (error: any) {
console.error("[SecretaryDoctorSchedule] Erro ao atualizar disponibilidade:", {
error,
message: error?.message,
response: error?.response,
data: error?.response?.data,
});
const errorMessage = error?.response?.data?.message ||
error?.message ||
"Erro ao atualizar disponibilidade";
toast.error(errorMessage);
}
};
const handleDeleteAvailability = async (availability: DoctorAvailability) => {
if (!availability.id) return;
const confirmDelete = window.confirm(
`Tem certeza que deseja deletar a disponibilidade de ${weekdayToText(
availability.weekday
)} (${availability.start_time} - ${
availability.end_time
})?\n\n⚠ Esta ação é permanente e não pode ser desfeita.`
);
if (!confirmDelete) return;
try {
await availabilityService.delete(availability.id);
toast.success("Disponibilidade deletada com sucesso");
loadDoctorSchedule();
} catch (error) {
console.error("Erro ao deletar disponibilidade:", error);
toast.error("Erro ao deletar disponibilidade");
}
};
const weekdays = [
{ value: "monday", label: "Segunda" },
{ value: "tuesday", label: "Terça" },
{ value: "wednesday", label: "Quarta" },
{ value: "thursday", label: "Quinta" },
{ value: "friday", label: "Sexta" },
{ value: "saturday", label: "Sábado" },
{ value: "sunday", label: "Domingo" },
];
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Agenda Médica</h1>
<p className="text-gray-600 mt-1">
Gerencie disponibilidades e exceções
</p>
</div>
</div>
{/* Doctor Selector */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Selecione o Médico
</label>
<select
value={selectedDoctorId}
onChange={(e) => setSelectedDoctorId(e.target.value)}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
{doctors.map((doctor) => (
<option key={doctor.id} value={doctor.id}>
{formatDoctorName(doctor.full_name)} - {doctor.specialty}
</option>
))}
</select>
</div>
{/* Calendar */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-gray-900 capitalize">
{formatMonthYear(currentDate)}
</h2>
<div className="flex items-center gap-2">
<button
onClick={goToToday}
className="px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Hoje
</button>
<button
onClick={previousMonth}
className="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
<ChevronLeft className="h-4 w-4" />
</button>
<button
onClick={nextMonth}
className="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
<ChevronRight className="h-4 w-4" />
</button>
</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
key={day}
className="bg-gray-50 px-2 py-3 text-center text-sm font-semibold text-gray-700"
>
{day}
</div>
))}
{calendarDays.map((day, index) => (
<div
key={index}
className={`bg-white p-2 min-h-[80px] ${
day.isCurrentMonth ? "" : "opacity-40"
} ${
day.date.toDateString() === new Date().toDateString()
? "bg-blue-50"
: ""
}`}
>
<div className="text-sm text-gray-700 mb-1">
{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>
))}
</div>
))}
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-4">
<button
onClick={() => setShowAvailabilityDialog(true)}
className="flex-1 flex items-center justify-center gap-2 px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
<Plus className="h-5 w-5" />
Adicionar Disponibilidade
</button>
<button
onClick={() => setShowExceptionDialog(true)}
className="flex-1 flex items-center justify-center gap-2 px-6 py-3 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition-colors"
>
<CalendarIcon className="h-5 w-5" />
Adicionar Exceção (Férias/Bloqueio)
</button>
</div>
{/* Current Availability */}
<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">
Disponibilidade Atual
</h3>
{loading ? (
<p className="text-gray-500">Carregando...</p>
) : availabilities.length === 0 ? (
<p className="text-gray-500">Nenhuma disponibilidade configurada</p>
) : (
<div className="space-y-3">
{availabilities.map((avail) => (
<div
key={avail.id}
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"
>
<div>
<p className="font-medium text-gray-900">
{weekdayToText(avail.weekday)}
</p>
<p className="text-sm text-gray-600">
{avail.start_time} - {avail.end_time}
</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 bg-green-100 text-green-700">
{avail.active ? "Ativo" : "Inativo"}
</span>
<button
onClick={() => handleEditAvailability(avail)}
title="Editar"
className="p-2 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => handleDeleteAvailability(avail)}
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">
<div className="bg-white rounded-xl shadow-xl max-w-md w-full mx-4 p-6">
<h3 className="text-xl font-semibold text-gray-900 mb-4">
Adicionar Disponibilidade
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Dias da Semana
</label>
<div className="space-y-2">
{weekdays.map((day) => (
<label
key={day.value}
className="flex items-center gap-2 cursor-pointer"
>
<input
type="checkbox"
checked={selectedWeekdays.includes(day.value)}
onChange={(e) => {
if (e.target.checked) {
setSelectedWeekdays([
...selectedWeekdays,
day.value,
]);
} else {
setSelectedWeekdays(
selectedWeekdays.filter((d) => d !== day.value)
);
}
}}
className="h-4 w-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
/>
<span className="text-sm text-gray-700">{day.label}</span>
</label>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Hora Início
</label>
<input
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-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={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Duração da Consulta (minutos)
</label>
<input
type="number"
value={duration}
onChange={(e) => setDuration(parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
</div>
<div className="flex gap-3 mt-6">
<button
onClick={() => setShowAvailabilityDialog(false)}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Cancelar
</button>
<button
onClick={handleAddAvailability}
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Adicionar
</button>
</div>
</div>
</div>
)}
{/* Exception Dialog */}
{showExceptionDialog && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-md w-full mx-4 p-6">
<h3 className="text-xl font-semibold text-gray-900 mb-4">
Adicionar Exceção
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tipo de Exceção
</label>
<select
value={exceptionType}
onChange={(e) => setExceptionType(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"
>
<option value="férias">Férias</option>
<option value="licença">Licença Médica</option>
<option value="congresso">Congresso</option>
<option value="outro">Outro</option>
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Data Início
</label>
<input
type="date"
value={exceptionStartDate}
onChange={(e) => setExceptionStartDate(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">
Data Fim
</label>
<input
type="date"
value={exceptionEndDate}
onChange={(e) => setExceptionEndDate(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>
<label className="block text-sm font-medium text-gray-700 mb-2">
Motivo (Opcional)
</label>
<input
type="text"
value={exceptionReason}
onChange={(e) => setExceptionReason(e.target.value)}
placeholder="Ex: Férias anuais, Conferência médica..."
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 className="flex gap-3 mt-6">
<button
onClick={() => setShowExceptionDialog(false)}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Cancelar
</button>
<button
onClick={handleAddException}
className="flex-1 px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition-colors"
>
Adicionar
</button>
</div>
</div>
</div>
)}
{/* Edit Dialog */}
{showEditDialog && editingAvailability && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-md w-full mx-4 p-6">
<h3 className="text-xl font-semibold text-gray-900 mb-4">
Editar Disponibilidade
</h3>
<div className="mb-4 p-3 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-900 font-medium">
{weekdayToText(editingAvailability.weekday)}
</p>
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Hora Início
</label>
<input
type="time"
value={editStartTime}
onChange={(e) => setEditStartTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-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={editEndTime}
onChange={(e) => setEditEndTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Duração da Consulta (minutos)
</label>
<select
value={editDuration}
onChange={(e) => setEditDuration(Number(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
<option value={15}>15 minutos</option>
<option value={20}>20 minutos</option>
<option value={30}>30 minutos</option>
<option value={45}>45 minutos</option>
<option value={60}>60 minutos</option>
</select>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="editActive"
checked={editActive}
onChange={(e) => setEditActive(e.target.checked)}
className="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
/>
<label
htmlFor="editActive"
className="text-sm font-medium text-gray-700"
>
Disponibilidade ativa
</label>
</div>
</div>
<div className="flex gap-3 mt-6">
<button
onClick={() => {
setShowEditDialog(false);
setEditingAvailability(null);
}}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Cancelar
</button>
<button
onClick={handleSaveEdit}
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Salvar
</button>
</div>
</div>
</div>
)}
</div>
);
}