255 lines
10 KiB
TypeScript
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>
|
|
);
|
|
} |