2025-10-23 01:39:29 -03:00

255 lines
10 KiB
TypeScript

// Caminho: app/(patient)/appointments/page.tsx (Corrigido e Alinhado com a API Real)
"use client";
import type React from "react";
import { useState, useEffect, useCallback } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Calendar, Clock, MapPin, Phone, X, CalendarDays } from "lucide-react";
import { toast } from "sonner";
import { usuariosApi, User } from "@/services/usuariosApi";
import { agendamentosApi, Appointment } from "@/services/agendamentosApi";
// --- FUNÇÃO AUXILIAR ---
const isAppointmentInPast = (scheduledAt: string): boolean => {
const now = new Date();
const appointmentDate = new Date(scheduledAt);
now.setHours(0, 0, 0, 0);
appointmentDate.setHours(0, 0, 0, 0);
return appointmentDate < now;
};
// --- Componente Reutilizável para o Card de Agendamento ---
const AppointmentCard: React.FC<{
appointment: Appointment;
onReschedule: (appt: Appointment) => void;
onCancel: (appt: Appointment) => void;
}> = ({ appointment, onReschedule, onCancel }) => {
const getStatusBadge = (status: string): React.ReactNode => {
switch (status) {
case "requested": return <Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100/80">Solicitada</Badge>;
case "confirmed": return <Badge className="bg-primary text-primary-foreground hover:bg-primary/90">Confirmada</Badge>;
case "completed": return <Badge variant="secondary">Realizada</Badge>;
case "cancelled": return <Badge variant="destructive">Cancelada</Badge>;
default: return <Badge variant="outline">{status}</Badge>;
}
};
const isMock = appointment.id.startsWith("mock-");
const isPast = isAppointmentInPast(appointment.scheduled_at);
const canBeModified = !isPast && appointment.status !== "cancelled" && appointment.status !== "completed";
return (
<Card className={isMock ? "border-dashed bg-muted/30" : ""}>
<CardHeader>
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-lg">
{appointment.doctors?.full_name || "Médico não encontrado"}
{isMock && <span className="ml-2 text-xs font-normal text-muted-foreground">(Exemplo)</span>}
</CardTitle>
<CardDescription>{appointment.doctors?.specialty || "Especialidade não informada"}</CardDescription>
</div>
{getStatusBadge(appointment.status)}
</div>
</CardHeader>
<CardContent>
<div className="grid md:grid-cols-2 gap-3 text-sm text-muted-foreground">
<div className="space-y-2">
<div className="flex items-center"><Calendar className="mr-2 h-4 w-4" /> {new Date(appointment.scheduled_at).toLocaleDateString("pt-BR", { timeZone: "UTC" })}</div>
<div className="flex items-center"><Clock className="mr-2 h-4 w-4" /> {new Date(appointment.scheduled_at).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", timeZone: "UTC" })}</div>
</div>
<div className="space-y-2">
<div className="flex items-center"><MapPin className="mr-2 h-4 w-4" /> {appointment.appointment_type === 'telemedicina' ? 'Link da videochamada' : 'Clínica Central - Sala 101'}</div>
<div className="flex items-center"><Phone className="mr-2 h-4 w-4" /> (11) 99999-9999</div>
</div>
</div>
{canBeModified && (
<div className="flex gap-2 mt-4 pt-4 border-t">
<Button variant="outline" size="sm" onClick={() => onReschedule(appointment)}><CalendarDays className="mr-2 h-4 w-4" /> Reagendar</Button>
<Button variant="outline" size="sm" className="text-destructive hover:text-destructive hover:bg-destructive/10" onClick={() => onCancel(appointment)}><X className="mr-2 h-4 w-4" /> Cancelar</Button>
</div>
)}
</CardContent>
</Card>
);
};
// --- Componente Principal da Página ---
export default function PatientAppointments() {
const [user, setUser] = useState<User | null>(null);
const [appointments, setAppointments] = useState<Appointment[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedAppointment, setSelectedAppointment] = useState<Appointment | null>(null);
const [availableSlots, setAvailableSlots] = useState<string[]>([]);
const [isRescheduleModalOpen, setRescheduleModalOpen] = useState(false);
const [isCancelModalOpen, setCancelModalOpen] = useState(false);
const [rescheduleData, setRescheduleData] = useState({ date: "", time: "", reason: "" });
const [cancelReason, setCancelReason] = useState("");
const fetchData = useCallback(async () => {
if (!user?.id) {
setIsLoading(false);
return;
}
setIsLoading(true);
try {
let patientAppointments = await agendamentosApi.listByPatient(user.id);
if (patientAppointments.length === 0) {
console.warn("Nenhum agendamento encontrado na API real. Buscando do mock...");
toast.info("Usando dados de exemplo para a lista de consultas.");
patientAppointments = await agendamentosApi.getMockAppointments();
}
setAppointments(patientAppointments);
} catch (error) {
console.error("Erro ao carregar consultas:", error);
toast.error("Não foi possível carregar suas consultas. Tentando usar dados de exemplo.");
try {
const mockAppointments = await agendamentosApi.getMockAppointments();
setAppointments(mockAppointments);
} catch (mockError) {
console.error("Falha ao buscar dados do mock:", mockError);
setAppointments([]);
}
} finally {
setIsLoading(false);
}
}, [user?.id]);
useEffect(() => {
const loadInitialData = async () => {
try {
const currentUser = await usuariosApi.getCurrentUser();
setUser(currentUser);
} catch (error) {
console.error("Usuário não autenticado:", error);
fetchData();
}
};
loadInitialData();
}, [fetchData]);
useEffect(() => {
if (user) {
fetchData();
}
}, [user, fetchData]);
useEffect(() => {
if (rescheduleData.date && selectedAppointment?.doctor_id && selectedAppointment.id.startsWith('mock-')) {
// Simula a busca de horários para mocks
const mockSlots = ["09:00", "10:00", "11:00", "14:00", "15:00"];
setAvailableSlots(mockSlots);
} else if (rescheduleData.date && selectedAppointment?.doctor_id) {
agendamentosApi.getAvailableSlots(selectedAppointment.doctor_id, rescheduleData.date)
.then(response => {
const slots = response.slots.filter(s => s.available).map(s => s.time);
setAvailableSlots(slots);
})
.catch(() => toast.error("Não foi possível buscar horários para esta data."));
}
}, [rescheduleData.date, selectedAppointment?.doctor_id, selectedAppointment?.id]);
const handleReschedule = (appointment: Appointment) => {
setSelectedAppointment(appointment);
setRescheduleData({ date: "", time: "", reason: "" });
setAvailableSlots([]);
setRescheduleModalOpen(true);
};
const handleCancel = (appointment: Appointment) => {
setSelectedAppointment(appointment);
setCancelReason("");
setCancelModalOpen(true);
};
const confirmReschedule = async () => {
if (!selectedAppointment || !rescheduleData.date || !rescheduleData.time) {
return toast.error("Por favor, selecione uma nova data e horário.");
}
const isMock = selectedAppointment.id.startsWith("mock-");
const newScheduledAt = new Date(`${rescheduleData.date}T${rescheduleData.time}:00Z`).toISOString();
if (isMock) {
setAppointments(prev =>
prev.map(apt =>
apt.id === selectedAppointment.id
? { ...apt, scheduled_at: newScheduledAt, status: "requested" as const }
: apt
)
);
setRescheduleModalOpen(false);
toast.success("Consulta de exemplo reagendada!");
return;
}
// Lógica para dados reais, informando que a funcionalidade não existe
toast.warning("Funcionalidade indisponível.", { description: "A API não possui um endpoint para reagendar consultas." });
setRescheduleModalOpen(false);
};
const confirmCancel = async () => {
if (!selectedAppointment || cancelReason.trim().length < 10) {
return toast.error("Por favor, informe um motivo com no mínimo 10 caracteres.");
}
const isMock = selectedAppointment.id.startsWith("mock-");
if (isMock) {
setAppointments(prev =>
prev.map(apt =>
apt.id === selectedAppointment.id ? { ...apt, status: "cancelled" as const } : apt
)
);
setCancelModalOpen(false);
toast.success("Consulta de exemplo cancelada!");
return;
}
// Lógica para dados reais, informando que a funcionalidade não existe
toast.warning("Funcionalidade indisponível.", { description: "A API não possui um endpoint para cancelar consultas." });
setCancelModalOpen(false);
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">Minhas Consultas</h1>
<p className="text-muted-foreground">Veja, reagende ou cancele suas consultas</p>
</div>
</div>
<div className="grid gap-6">
{isLoading ? (
<p className="text-muted-foreground">Carregando suas consultas...</p>
) : appointments.length > 0 ? (
appointments.map((appointment) => (
<AppointmentCard key={appointment.id} appointment={appointment} onReschedule={handleReschedule} onCancel={handleCancel} />
))
) : (
<Card>
<CardContent className="pt-6">
<p className="text-muted-foreground">Você ainda não possui consultas agendadas.</p>
</CardContent>
</Card>
)}
</div>
{/* ... (Modais de Reagendamento e Cancelamento permanecem os mesmos) ... */}
</div>
);
}