riseup-squad18/src/components/secretaria/SecretaryAppointmentList.tsx

1088 lines
42 KiB
TypeScript

import { useState, useEffect } from "react";
import toast from "react-hot-toast";
import { Search, Plus, Eye, Edit, Trash2, X, RefreshCw } from "lucide-react";
import {
appointmentService,
type Appointment,
patientService,
type Patient,
doctorService,
type Doctor,
} from "../../services";
import { Avatar } from "../ui/Avatar";
import { CalendarPicker } from "../agenda/CalendarPicker";
import AvailableSlotsPicker from "../agenda/AvailableSlotsPicker";
import { CheckInButton } from "../consultas/CheckInButton";
import { ConfirmAppointmentButton } from "../consultas/ConfirmAppointmentButton";
import { RescheduleModal } from "../consultas/RescheduleModal";
import { isToday, parseISO, format } from "date-fns";
interface AppointmentWithDetails extends Appointment {
patient?: Patient;
doctor?: Doctor;
}
export function SecretaryAppointmentList() {
const [appointments, setAppointments] = useState<AppointmentWithDetails[]>(
[]
);
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("Todos");
const [typeFilter, setTypeFilter] = useState("Todos");
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(10);
const [showCreateModal, setShowCreateModal] = useState(false);
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
const [selectedAppointment, setSelectedAppointment] =
useState<AppointmentWithDetails | null>(null);
const [patients, setPatients] = useState<Patient[]>([]);
const [doctors, setDoctors] = useState<Doctor[]>([]);
const [formData, setFormData] = useState<any>({
id: undefined,
patient_id: "",
doctor_id: "",
scheduled_at: "",
appointment_type: "presencial",
notes: "",
});
const [selectedDate, setSelectedDate] = useState<string>("");
const [selectedTime, setSelectedTime] = useState<string>("");
const [showRescheduleModal, setShowRescheduleModal] = useState(false);
const [rescheduleAppointment, setRescheduleAppointment] =
useState<AppointmentWithDetails | null>(null);
const loadAppointments = async () => {
setLoading(true);
try {
const data = await appointmentService.list();
// Buscar detalhes de pacientes e médicos
const appointmentsWithDetails = await Promise.all(
(Array.isArray(data) ? data : []).map(async (appointment) => {
try {
const [patient, doctor] = await Promise.all([
appointment.patient_id
? patientService.getById(appointment.patient_id)
: null,
appointment.doctor_id
? doctorService.getById(appointment.doctor_id)
: null,
]);
return {
...appointment,
patient: patient || undefined,
doctor: doctor || undefined,
};
} catch (error) {
console.error("Erro ao carregar detalhes:", error);
return appointment;
}
})
);
setAppointments(appointmentsWithDetails);
console.log("✅ Consultas carregadas:", appointmentsWithDetails);
} catch (error) {
console.error("❌ Erro ao carregar consultas:", error);
toast.error("Erro ao carregar consultas");
setAppointments([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadAppointments();
loadDoctorsAndPatients();
}, []);
// Se outro componente pediu para abrir o modal de criação com paciente pré-selecionado
useEffect(() => {
const openFromSession = () => {
const patientId = sessionStorage.getItem("selectedPatientForAppointment");
if (patientId) {
setFormData((prev: any) => ({ ...prev, patient_id: patientId }));
setModalMode("create");
setShowCreateModal(true);
sessionStorage.removeItem("selectedPatientForAppointment");
}
};
// Try on mount
openFromSession();
// Listen for explicit events
const handler = () => openFromSession();
window.addEventListener("open-create-appointment", handler);
return () => window.removeEventListener("open-create-appointment", handler);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Função de filtro
const filteredAppointments = appointments.filter((appointment) => {
// Filtro de busca por nome do paciente ou médico
const searchLower = searchTerm.toLowerCase();
const matchesSearch =
!searchTerm ||
appointment.patient?.full_name?.toLowerCase().includes(searchLower) ||
appointment.doctor?.full_name?.toLowerCase().includes(searchLower) ||
appointment.order_number?.toString().includes(searchTerm);
// Mapeia o valor selecionado no select para o valor real usado na API/data
const mapStatusFilterToValue = (label: string) => {
if (label === "Todos") return null;
// Se já é um valor de API, retorna ele mesmo
if (
[
"requested",
"confirmed",
"checked_in",
"in_progress",
"completed",
"cancelled",
"no_show",
].includes(label)
) {
return label;
}
// Mantém mapeamento legado por compatibilidade
const map: Record<string, string> = {
Confirmada: "confirmed",
Agendada: "requested",
Cancelada: "cancelled",
Concluída: "completed",
Concluida: "completed",
};
return map[label] || label.toLowerCase();
};
const mapTypeFilterToValue = (label: string) => {
if (label === "Todos") return null;
const map: Record<string, string> = {
Presencial: "presencial",
Telemedicina: "telemedicina",
};
return map[label] || label.toLowerCase();
};
const statusValue = mapStatusFilterToValue(statusFilter);
const typeValue = mapTypeFilterToValue(typeFilter);
// Filtro de status
const matchesStatus =
statusValue === null || appointment.status === statusValue;
// Filtro de tipo
const matchesType =
typeValue === null || appointment.appointment_type === typeValue;
return matchesSearch && matchesStatus && matchesType;
});
// Cálculos de paginação
const totalPages = Math.ceil(filteredAppointments.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedAppointments = filteredAppointments.slice(
startIndex,
endIndex
);
const loadDoctorsAndPatients = async () => {
try {
const [patientsData, doctorsData] = await Promise.all([
patientService.list(),
doctorService.list(),
]);
setPatients(Array.isArray(patientsData) ? patientsData : []);
setDoctors(Array.isArray(doctorsData) ? doctorsData : []);
} catch (error) {
console.error("Erro ao carregar pacientes e médicos:", error);
}
};
const handleOpenCreateModal = () => {
setFormData({
patient_id: "",
doctor_id: "",
scheduled_at: "",
appointment_type: "presencial",
notes: "",
});
setSelectedDate("");
setSelectedTime("");
setShowCreateModal(true);
};
const handleCreateAppointment = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.patient_id || !formData.doctor_id || !formData.scheduled_at) {
toast.error("Preencha todos os campos obrigatórios");
return;
}
try {
if (modalMode === "edit" && formData.id) {
// Update only allowed fields per API types
const updatePayload: any = {};
if (formData.scheduled_at)
updatePayload.scheduled_at = new Date(
formData.scheduled_at
).toISOString();
if (formData.notes) updatePayload.notes = formData.notes;
await appointmentService.update(formData.id, updatePayload);
toast.success("Consulta atualizada com sucesso!");
} else {
// Buscar user ID para created_by
const userStr = localStorage.getItem("mediconnect_user");
let userId: string | undefined;
if (userStr) {
try {
const userData = JSON.parse(userStr);
userId = userData.id;
} catch (e) {
console.warn("Erro ao parsear user do localStorage");
}
}
// Payload conforme documentação da API Supabase
await appointmentService.create({
doctor_id: formData.doctor_id,
patient_id: formData.patient_id,
scheduled_at: new Date(formData.scheduled_at).toISOString(),
duration_minutes: 30,
created_by: userId,
});
toast.success("Consulta agendada com sucesso!");
}
setShowCreateModal(false);
loadAppointments();
} catch (error) {
console.error("Erro ao criar consulta:", error);
toast.error("Erro ao agendar consulta");
}
};
const handleSearch = () => {
loadAppointments();
};
const handleViewAppointment = (appointment: AppointmentWithDetails) => {
setSelectedAppointment(appointment);
};
const handleEditAppointment = (appointment: AppointmentWithDetails) => {
setModalMode("edit");
setFormData({
id: appointment.id,
patient_id: appointment.patient_id || "",
doctor_id: appointment.doctor_id || "",
scheduled_at: appointment.scheduled_at || "",
appointment_type: appointment.appointment_type || "presencial",
notes: appointment.notes || "",
});
setShowCreateModal(true);
};
const handleClear = () => {
setSearchTerm("");
setStatusFilter("Todos");
setTypeFilter("Todos");
setCurrentPage(1);
loadAppointments();
};
const handleStatusChange = async (
appointmentId: string,
newStatus: string
) => {
try {
console.log(
`[SecretaryAppointmentList] Atualizando status da consulta ${appointmentId} para ${newStatus}`
);
await appointmentService.update(appointmentId, {
status: newStatus as any,
});
toast.success(
`Status atualizado para: ${
newStatus === "requested"
? "Solicitada"
: newStatus === "confirmed"
? "Confirmada"
: newStatus === "checked_in"
? "Check-in"
: newStatus === "in_progress"
? "Em Atendimento"
: newStatus === "completed"
? "Concluída"
: newStatus === "cancelled"
? "Cancelada"
: newStatus === "no_show"
? "Não Compareceu"
: newStatus
}`
);
loadAppointments();
} catch (error) {
console.error("Erro ao atualizar status:", error);
toast.error("Erro ao atualizar status da consulta");
}
};
const handleDeleteAppointment = async (appointmentId: string) => {
if (!confirm("Tem certeza que deseja cancelar esta consulta?")) {
return;
}
try {
await appointmentService.update(appointmentId, {
status: "cancelled",
cancelled_at: new Date().toISOString(),
cancellation_reason: "Cancelado pela secretaria",
});
toast.success("Consulta cancelada com sucesso!");
loadAppointments();
} catch (error) {
console.error("Erro ao cancelar consulta:", error);
toast.error("Erro ao cancelar consulta");
}
};
// Reset página quando filtros mudarem
useEffect(() => {
setCurrentPage(1);
}, [searchTerm, statusFilter, typeFilter]);
const getStatusBadge = (status: string) => {
const statusMap: Record<string, { label: string; className: string }> = {
confirmada: {
label: "Confirmada",
className: "bg-green-100 text-green-700",
},
agendada: { label: "Agendada", className: "bg-blue-100 text-blue-700" },
cancelada: { label: "Cancelada", className: "bg-red-100 text-red-700" },
concluida: { label: "Concluída", className: "bg-gray-100 text-gray-700" },
};
const config = statusMap[status] || {
label: status,
className: "bg-gray-100 text-gray-700",
};
return (
<span
className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${config.className}`}
>
{config.label}
</span>
);
};
const formatDate = (dateString: string) => {
try {
const date = new Date(dateString);
return date.toLocaleDateString("pt-BR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch {
return "—";
}
};
const formatTime = (dateString: string) => {
try {
const date = new Date(dateString);
return date.toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
});
} catch {
return "—";
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">
Consultas
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Gerencie as consultas agendadas
</p>
</div>
<button
onClick={handleOpenCreateModal}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
<Plus className="h-4 w-4" />
Nova Consulta
</button>
</div>
{/* Search and Filters */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 space-y-4">
<div className="flex gap-3">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 dark:text-gray-500" />
<input
type="text"
placeholder="Buscar consultas por paciente ou médico..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400"
/>
</div>
<button
onClick={handleSearch}
className="px-6 py-2.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Buscar
</button>
<button
onClick={handleClear}
className="px-6 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
Limpar
</button>
</div>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">
Status:
</span>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<option>Todos</option>
<option value="requested">Solicitada</option>
<option value="confirmed">Confirmada</option>
<option value="checked_in">Check-in</option>
<option value="in_progress">Em Atendimento</option>
<option value="completed">Concluída</option>
<option value="cancelled">Cancelada</option>
<option value="no_show">Não Compareceu</option>
</select>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">
Tipo:
</span>
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<option>Todos</option>
<option>Presencial</option>
<option>Telemedicina</option>
</select>
</div>
</div>
</div>
{/* Table */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
<tr>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Paciente
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Médico
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Data/Hora
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Tipo
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Ações
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{loading ? (
<tr>
<td
colSpan={6}
className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
>
Carregando consultas...
</td>
</tr>
) : filteredAppointments.length === 0 ? (
<tr>
<td
colSpan={6}
className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
>
{searchTerm ||
statusFilter !== "Todos" ||
typeFilter !== "Todos"
? "Nenhuma consulta encontrada com esses filtros"
: "Nenhuma consulta encontrada"}
</td>
</tr>
) : (
paginatedAppointments.map((appointment) => (
<tr
key={appointment.id}
className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<Avatar
src={appointment.patient}
name={appointment.patient?.full_name || ""}
size="md"
color="blue"
/>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{appointment.patient?.full_name ||
"Paciente não encontrado"}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{appointment.patient?.email || "—"}
</p>
</div>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<Avatar
src={appointment.doctor}
name={appointment.doctor?.full_name || ""}
size="md"
color="green"
/>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{appointment.doctor?.full_name ||
"Médico não encontrado"}
</p>
<p className="text-xs text-gray-500">
{appointment.doctor?.specialty || "—"}
</p>
</div>
</div>
</td>
<td className="px-6 py-4 text-sm text-gray-900">
{appointment.scheduled_at ? (
<>
<div className="font-medium">
{formatDate(appointment.scheduled_at)}
</div>
<div className="text-gray-500 text-xs">
{formatTime(appointment.scheduled_at)}
</div>
</>
) : (
"—"
)}
</td>
<td className="px-6 py-4 text-sm text-gray-700">
<span
className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${
appointment.appointment_type === "telemedicina"
? "bg-purple-100 text-purple-700"
: "bg-blue-100 text-blue-700"
}`}
>
{appointment.appointment_type === "telemedicina"
? "Telemedicina"
: "Presencial"}
</span>
</td>
<td className="px-6 py-4">
<select
value={appointment.status || "requested"}
onChange={(e) =>
handleStatusChange(appointment.id, e.target.value)
}
className="px-3 py-1.5 text-xs font-medium rounded-full border-0 focus:ring-2 focus:ring-green-500 cursor-pointer"
style={{
backgroundColor:
appointment.status === "requested"
? "#fef3c7"
: appointment.status === "confirmed"
? "#d1fae5"
: appointment.status === "checked_in"
? "#fed7aa"
: appointment.status === "in_progress"
? "#fed7aa"
: appointment.status === "completed"
? "#dbeafe"
: appointment.status === "cancelled"
? "#f3f4f6"
: appointment.status === "no_show"
? "#fee2e2"
: "#f3f4f6",
color:
appointment.status === "requested"
? "#92400e"
: appointment.status === "confirmed"
? "#065f46"
: appointment.status === "checked_in"
? "#9a3412"
: appointment.status === "in_progress"
? "#9a3412"
: appointment.status === "completed"
? "#1e40af"
: appointment.status === "cancelled"
? "#4b5563"
: appointment.status === "no_show"
? "#991b1b"
: "#4b5563",
}}
>
<option value="requested">Solicitada</option>
<option value="confirmed">Confirmada</option>
<option value="checked_in">Check-in</option>
<option value="in_progress">Em Atendimento</option>
<option value="completed">Concluída</option>
<option value="cancelled">Cancelada</option>
<option value="no_show">Não Compareceu</option>
</select>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
{/* Confirm Button - Mostra apenas para consultas requested (aguardando confirmação) */}
<ConfirmAppointmentButton
appointmentId={appointment.id}
currentStatus={appointment.status || "requested"}
patientName={appointment.patient?.full_name}
patientPhone={appointment.patient?.phone}
scheduledAt={
appointment.scheduled_at
? format(
parseISO(appointment.scheduled_at),
"dd/MM/yyyy 'às' HH:mm"
)
: undefined
}
/>
{/* Check-in Button - Mostra apenas para consultas confirmadas do dia */}
{appointment.status === "confirmed" &&
appointment.scheduled_at &&
isToday(parseISO(appointment.scheduled_at)) && (
<CheckInButton
appointmentId={appointment.id}
patientName={
appointment.patient?.full_name || "Paciente"
}
disabled={loading}
/>
)}
{/* Reschedule Button - Mostra apenas para consultas canceladas */}
{appointment.status === "cancelled" &&
appointment.scheduled_at && (
<button
onClick={() => {
setRescheduleAppointment(appointment);
setShowRescheduleModal(true);
}}
title="Reagendar"
className="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-purple-600 bg-purple-50 hover:bg-purple-100 dark:bg-purple-900/20 dark:hover:bg-purple-900/40 rounded-lg transition-colors"
>
<RefreshCw className="w-4 h-4" />
Reagendar
</button>
)}
<button
onClick={() => handleViewAppointment(appointment)}
title="Visualizar"
className="p-2 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900 rounded-lg transition-colors"
>
<Eye className="h-4 w-4" />
</button>
<button
onClick={() => handleEditAppointment(appointment)}
title="Editar"
className="p-2 text-orange-600 hover:bg-orange-50 dark:hover:bg-orange-900 rounded-lg transition-colors"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => handleDeleteAppointment(appointment.id)}
title="Cancelar"
className="p-2 text-red-600 hover:bg-red-50 dark:hover:bg-red-900 rounded-lg transition-colors"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Paginação */}
{filteredAppointments.length > 0 && (
<div className="flex items-center justify-between bg-white dark:bg-gray-800 px-6 py-4 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
Mostrando {startIndex + 1} até{" "}
{Math.min(endIndex, filteredAppointments.length)} de{" "}
{filteredAppointments.length} consultas
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300"
>
Anterior
</button>
<div className="flex items-center gap-1">
{(() => {
const maxPagesToShow = 4;
let startPage = Math.max(
1,
currentPage - Math.floor(maxPagesToShow / 2)
);
let endPage = Math.min(
totalPages,
startPage + maxPagesToShow - 1
);
// Ajusta startPage se estivermos próximos do fim
if (endPage - startPage < maxPagesToShow - 1) {
startPage = Math.max(1, endPage - maxPagesToShow + 1);
}
const pages = [];
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages.map((page) => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-3 py-2 text-sm rounded-lg transition-colors ${
currentPage === page
? "bg-green-600 text-white"
: "border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
}`}
>
{page}
</button>
));
})()}
</div>
<button
onClick={() =>
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
}
disabled={currentPage === totalPages}
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300"
>
Próxima
</button>
</div>
</div>
)}
{/* Modal de Criar Consulta */}
{showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
{modalMode === "edit" ? "Editar Consulta" : "Nova Consulta"}
</h2>
</div>
<form onSubmit={handleCreateAppointment} className="p-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Paciente *
</label>
<select
value={formData.patient_id}
onChange={(e) =>
setFormData({ ...formData, patient_id: e.target.value })
}
className="form-input"
required
>
<option value="">Selecione um paciente</option>
{patients.map((patient) => (
<option key={patient.id} value={patient.id}>
{patient.full_name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Médico *
</label>
<select
value={formData.doctor_id}
onChange={(e) =>
setFormData({ ...formData, doctor_id: e.target.value })
}
className="form-input"
required
>
<option value="">Selecione um médico</option>
{doctors.map((doctor) => (
<option key={doctor.id} value={doctor.id}>
{doctor.full_name} - {doctor.specialty}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tipo de Consulta *
</label>
<select
value={formData.appointment_type}
onChange={(e) =>
setFormData({
...formData,
appointment_type: e.target.value,
})
}
className="form-input"
required
>
<option value="presencial">Presencial</option>
<option value="telemedicina">Telemedicina</option>
</select>
</div>
</div>
{/* Calendário Visual */}
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Data da Consulta *
</label>
{formData.doctor_id ? (
<CalendarPicker
doctorId={formData.doctor_id}
selectedDate={selectedDate}
onSelectDate={(date) => {
setSelectedDate(date);
setSelectedTime(""); // Resetar horário ao mudar data
setFormData({ ...formData, scheduled_at: "" });
}}
/>
) : (
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 text-center text-gray-500">
Selecione um médico primeiro para ver a disponibilidade
</div>
)}
</div>
{/* Seletor de Horários */}
{selectedDate && formData.doctor_id && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Horário *{" "}
{selectedTime && (
<span className="text-blue-600 font-semibold">
({selectedTime})
</span>
)}
</label>
<AvailableSlotsPicker
doctorId={formData.doctor_id}
date={selectedDate}
onSelect={(time) => {
setSelectedTime(time);
// Combinar data + horário no formato ISO
const datetime = `${selectedDate}T${time}:00`;
setFormData({ ...formData, scheduled_at: datetime });
}}
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Observações
</label>
<textarea
value={formData.notes || ""}
onChange={(e) =>
setFormData({ ...formData, notes: e.target.value })
}
className="form-input"
rows={3}
placeholder="Observações da consulta"
/>
</div>
</div>
<div className="flex justify-end gap-3 pt-4">
<button
type="button"
onClick={() => setShowCreateModal(false)}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Cancelar
</button>
<button
type="submit"
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
{modalMode === "edit"
? "Salvar Alterações"
: "Agendar Consulta"}
</button>
</div>
</form>
</div>
</div>
)}
{/* Modal de Visualizar Consulta */}
{selectedAppointment && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Visualizar Consulta
</h2>
<button
onClick={() => setSelectedAppointment(null)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<X className="h-5 w-5 text-gray-500" />
</button>
</div>
<div className="p-6 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">
Paciente
</label>
<p className="text-gray-900 font-medium">
{selectedAppointment.patient?.full_name || "—"}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">
Médico
</label>
<p className="text-gray-900">
{selectedAppointment.doctor?.full_name || "—"}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">
Data
</label>
<p className="text-gray-900">
{selectedAppointment.scheduled_at
? formatDate(selectedAppointment.scheduled_at)
: "—"}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">
Hora
</label>
<p className="text-gray-900">
{selectedAppointment.scheduled_at
? formatTime(selectedAppointment.scheduled_at)
: "—"}
</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">
Tipo
</label>
<p className="text-gray-900">
{selectedAppointment.appointment_type === "telemedicina"
? "Telemedicina"
: "Presencial"}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">
Status
</label>
<div>
{getStatusBadge(selectedAppointment.status || "agendada")}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">
Observações
</label>
<p className="text-gray-900 whitespace-pre-wrap">
{selectedAppointment.notes || "—"}
</p>
</div>
<div className="flex justify-end gap-3 pt-4">
<button
onClick={() => setSelectedAppointment(null)}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Fechar
</button>
</div>
</div>
</div>
</div>
)}
{/* Modal de Reagendar */}
{showRescheduleModal && rescheduleAppointment && (
<RescheduleModal
appointmentId={rescheduleAppointment.id}
appointmentDate={rescheduleAppointment.scheduled_at || ""}
doctorId={rescheduleAppointment.doctor_id || ""}
doctorName={rescheduleAppointment.doctor?.full_name || "Médico"}
patientName={rescheduleAppointment.patient?.full_name || "Paciente"}
onClose={() => {
setShowRescheduleModal(false);
setRescheduleAppointment(null);
loadAppointments(); // Recarregar lista
}}
/>
)}
</div>
);
}