229 lines
9.4 KiB
TypeScript
229 lines
9.4 KiB
TypeScript
"use client";
|
|
|
|
import type React from "react";
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { agendamentosApi, Appointment } from "@/services/agendamentosApi";
|
|
import { Patient } from "@/services/pacientesApi";
|
|
import { Doctor } from "@/services/medicosApi";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Clock, Calendar as CalendarIcon, MapPin, Phone, User, X, RefreshCw, Loader2 } from "lucide-react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { toast } from "sonner";
|
|
import { Calendar } from "@/components/ui/calendar";
|
|
import { format, parseISO, isSameDay } from "date-fns";
|
|
|
|
// Interface corrigida para incluir os tipos completos de Patient e Doctor
|
|
interface AppointmentWithDetails extends Appointment {
|
|
patients: Patient;
|
|
doctors: Doctor;
|
|
}
|
|
|
|
export default function DoctorAppointmentsPage() {
|
|
const [allAppointments, setAllAppointments] = useState<AppointmentWithDetails[]>([]);
|
|
const [filteredAppointments, setFilteredAppointments] = useState<AppointmentWithDetails[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [bookedDays, setBookedDays] = useState<Date[]>([]);
|
|
const [selectedCalendarDate, setSelectedCalendarDate] = useState<Date | undefined>(new Date());
|
|
|
|
const fetchAppointments = useCallback(async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
try {
|
|
// A camada de serviço deve ser ajustada para buscar os dados aninhados
|
|
// Ex: api.get('/rest/v1/appointments?select=*,patients(*),doctors(*)')
|
|
const data = await agendamentosApi.list() as AppointmentWithDetails[];
|
|
setAllAppointments(data || []);
|
|
|
|
const uniqueBookedDates = Array.from(new Set(data.map(app => app.scheduled_at.split('T')[0])));
|
|
const dateObjects = uniqueBookedDates.map(dateString => parseISO(dateString));
|
|
setBookedDays(dateObjects);
|
|
|
|
toast.success("Agenda atualizada com sucesso!");
|
|
} catch (e) {
|
|
console.error("Erro ao carregar a agenda:", e);
|
|
setError("Não foi possível carregar sua agenda. Verifique a conexão.");
|
|
setAllAppointments([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchAppointments();
|
|
}, [fetchAppointments]);
|
|
|
|
useEffect(() => {
|
|
if (selectedCalendarDate) {
|
|
const todayAppointments = allAppointments
|
|
.filter(app => isSameDay(parseISO(app.scheduled_at), selectedCalendarDate))
|
|
.sort((a, b) => a.scheduled_at.localeCompare(b.scheduled_at));
|
|
setFilteredAppointments(todayAppointments);
|
|
} else {
|
|
const todayAppointments = allAppointments
|
|
.filter(app => isSameDay(parseISO(app.scheduled_at), new Date()))
|
|
.sort((a, b) => a.scheduled_at.localeCompare(b.scheduled_at));
|
|
setFilteredAppointments(todayAppointments);
|
|
}
|
|
}, [allAppointments, selectedCalendarDate]);
|
|
|
|
const getStatusVariant = (status: Appointment['status']) => {
|
|
switch (status) {
|
|
case "confirmed":
|
|
case "requested":
|
|
return "default";
|
|
case "completed":
|
|
return "secondary";
|
|
case "cancelled":
|
|
return "destructive";
|
|
default:
|
|
return "outline";
|
|
}
|
|
};
|
|
|
|
const handleCancel = async (id: string) => {
|
|
try {
|
|
await agendamentosApi.update(id, { status: "cancelled" });
|
|
toast.info(`Consulta cancelada com sucesso.`);
|
|
await fetchAppointments();
|
|
} catch (error) {
|
|
console.error("Erro ao cancelar consulta:", error);
|
|
toast.error("Não foi possível cancelar a consulta.");
|
|
}
|
|
};
|
|
|
|
const handleReSchedule = (id: string) => {
|
|
toast.info(`Reagendamento da Consulta ID: ${id}. Navegar para a página de agendamento.`);
|
|
};
|
|
|
|
const displayDate = selectedCalendarDate
|
|
? format(selectedCalendarDate, "EEEE, dd 'de' MMMM")
|
|
: "Selecione uma data";
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900">Agenda Médica</h1>
|
|
<p className="text-gray-600">Visualize e gerencie todas as suas consultas.</p>
|
|
</div>
|
|
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-xl font-semibold">Consultas para: {displayDate}</h2>
|
|
<Button onClick={fetchAppointments} disabled={isLoading} variant="outline" size="sm">
|
|
<RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
|
Atualizar Agenda
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-1">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center">
|
|
<CalendarIcon className="mr-2 h-5 w-5" />
|
|
Calendário
|
|
</CardTitle>
|
|
<p className="text-sm text-gray-500">Dias em azul possuem agendamentos.</p>
|
|
</CardHeader>
|
|
<CardContent className="flex justify-center p-2">
|
|
<Calendar
|
|
mode="single"
|
|
selected={selectedCalendarDate}
|
|
onSelect={setSelectedCalendarDate}
|
|
initialFocus
|
|
modifiers={{ booked: bookedDays }}
|
|
modifiersClassNames={{
|
|
booked: "bg-blue-600 text-white aria-selected:!bg-blue-700 hover:!bg-blue-700/90"
|
|
}}
|
|
className="rounded-md border p-2"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="lg:col-span-2 space-y-4">
|
|
{isLoading ? (
|
|
<div className="text-center text-lg text-gray-500 p-8">
|
|
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-blue-600" />
|
|
Carregando a agenda...
|
|
</div>
|
|
) : error ? (
|
|
<div className="text-center text-lg text-red-600 p-8">{error}</div>
|
|
) : filteredAppointments.length === 0 ? (
|
|
<p className="text-center text-lg text-gray-500 p-8">Nenhuma consulta encontrada para a data selecionada.</p>
|
|
) : (
|
|
filteredAppointments.map((appointment) => {
|
|
const showActions = appointment.status === "requested" || appointment.status === "confirmed";
|
|
|
|
return (
|
|
<Card key={appointment.id} className="shadow-lg">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-xl font-semibold flex items-center">
|
|
<User className="mr-2 h-5 w-5 text-blue-600" />
|
|
{appointment.patients?.full_name || 'Paciente não informado'}
|
|
</CardTitle>
|
|
<Badge variant={getStatusVariant(appointment.status)} className="uppercase">
|
|
{appointment.status}
|
|
</Badge>
|
|
</CardHeader>
|
|
|
|
<CardContent className="grid md:grid-cols-3 gap-4 pt-4">
|
|
<div className="space-y-3">
|
|
<div className="flex items-center text-sm text-gray-700">
|
|
<User className="mr-2 h-4 w-4 text-gray-500" />
|
|
<span className="font-semibold">Médico:</span> {appointment.doctors?.full_name || 'N/A'}
|
|
</div>
|
|
<div className="flex items-center text-sm text-gray-700">
|
|
<CalendarIcon className="mr-2 h-4 w-4 text-gray-500" />
|
|
{format(parseISO(appointment.scheduled_at), 'dd/MM/yyyy')}
|
|
</div>
|
|
<div className="flex items-center text-sm text-gray-700">
|
|
<Clock className="mr-2 h-4 w-4 text-gray-500" />
|
|
{format(parseISO(appointment.scheduled_at), 'HH:mm')}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div className="flex items-center text-sm text-gray-700">
|
|
<MapPin className="mr-2 h-4 w-4 text-gray-500" />
|
|
{appointment.appointment_type || 'N/A'}
|
|
</div>
|
|
<div className="flex items-center text-sm text-gray-700">
|
|
<Phone className="mr-2 h-4 w-4 text-gray-500" />
|
|
{appointment.patients?.phone_mobile || "N/A"}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col justify-center items-end">
|
|
{showActions && (
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleReSchedule(appointment.id)}
|
|
>
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
Reagendar
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={() => handleCancel(appointment.id)}
|
|
>
|
|
<X className="mr-2 h-4 w-4" />
|
|
Cancelar
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |