812 lines
29 KiB
TypeScript
812 lines
29 KiB
TypeScript
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>
|
||
);
|
||
}
|