248 lines
9.5 KiB
TypeScript
248 lines
9.5 KiB
TypeScript
// Caminho: app/patient/dashboard/page.tsx
|
|
|
|
"use client";
|
|
|
|
import type React from "react";
|
|
import { useState, useEffect } from "react";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Calendar, Clock, User, Plus, LucideIcon } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { toast } from "sonner";
|
|
|
|
// Importando TODOS os serviços de API necessários
|
|
import { usuariosApi } from "@/services/usuariosApi";
|
|
import { agendamentosApi, Appointment as ApiAppointment } from "@/services/agendamentosApi";
|
|
import { pacientesApi, Patient } from "@/services/pacientesApi";
|
|
|
|
// --- Componentes Reutilizáveis ---
|
|
|
|
interface DashboardStatCardProps {
|
|
title: string;
|
|
value: string;
|
|
description: string;
|
|
icon: LucideIcon;
|
|
}
|
|
|
|
const DashboardStatCard: React.FC<DashboardStatCardProps> = ({ title, value, description, icon: Icon }) => (
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{value}</div>
|
|
<p className="text-xs text-muted-foreground">{description}</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
|
|
interface AppointmentDisplay {
|
|
doctorName: string;
|
|
specialty: string;
|
|
date: string;
|
|
time: string;
|
|
}
|
|
|
|
interface UpcomingAppointmentItemProps {
|
|
appointment: AppointmentDisplay;
|
|
}
|
|
|
|
const UpcomingAppointmentItem: React.FC<UpcomingAppointmentItemProps> = ({ appointment }) => (
|
|
<div className="flex items-center justify-between p-3 bg-accent/50 rounded-lg">
|
|
<div>
|
|
<p className="font-medium">{appointment.doctorName}</p>
|
|
<p className="text-sm text-muted-foreground">{appointment.specialty}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="font-medium">{appointment.date}</p>
|
|
<p className="text-sm text-muted-foreground">{appointment.time}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// --- Tipos e Dados Estáticos ---
|
|
|
|
interface QuickAction {
|
|
href: string;
|
|
label: string;
|
|
icon: LucideIcon;
|
|
variant?: "outline";
|
|
}
|
|
|
|
const quickActions: QuickAction[] = [
|
|
{ href: "/patient/schedule", label: "Agendar Nova Consulta", icon: Plus, variant: "outline" },
|
|
{ href: "/patient/appointments", label: "Ver Minhas Consultas", icon: Calendar, variant: "outline" },
|
|
{ href: "/patient/profile", label: "Atualizar Dados", icon: User, variant: "outline" },
|
|
];
|
|
|
|
// --- Componente da Página ---
|
|
export default function PatientDashboard() {
|
|
const [statsData, setStatsData] = useState<DashboardStatCardProps[]>([]);
|
|
const [upcomingAppointments, setUpcomingAppointments] = useState<ApiAppointment[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
const user = await usuariosApi.getCurrentUser();
|
|
if (!user || !user.id) {
|
|
throw new Error("Usuário não autenticado.");
|
|
}
|
|
|
|
const [appointmentsResponse, patientResponse] = await Promise.allSettled([
|
|
agendamentosApi.listByPatient(user.id),
|
|
pacientesApi.getById(user.id)
|
|
]);
|
|
|
|
let appointments: ApiAppointment[] = [];
|
|
if (appointmentsResponse.status === 'fulfilled') {
|
|
appointments = appointmentsResponse.value;
|
|
// LÓGICA DE FALLBACK PARA AGENDAMENTOS
|
|
if (appointments.length === 0) {
|
|
console.warn("Nenhum agendamento encontrado na API real. Buscando do mock...");
|
|
toast.info("Usando dados de exemplo para os agendamentos.");
|
|
appointments = await agendamentosApi.getMockAppointments();
|
|
}
|
|
} else {
|
|
console.error("Falha ao buscar agendamentos:", appointmentsResponse.reason);
|
|
setError("Não foi possível carregar seus agendamentos. Tentando usar dados de exemplo.");
|
|
appointments = await agendamentosApi.getMockAppointments(); // Fallback em caso de erro
|
|
}
|
|
|
|
const upcoming = appointments
|
|
.filter(appt => new Date(appt.scheduled_at) > new Date() && appt.status !== 'cancelled')
|
|
.sort((a, b) => new Date(a.scheduled_at).getTime() - new Date(b.scheduled_at).getTime());
|
|
|
|
setUpcomingAppointments(upcoming);
|
|
|
|
let patientData: Patient | null = null;
|
|
if (patientResponse.status === 'fulfilled') {
|
|
patientData = patientResponse.value;
|
|
} else {
|
|
console.warn("Paciente não encontrado na API real. Tentando buscar do mock...", patientResponse.reason);
|
|
try {
|
|
patientData = await pacientesApi.getMockPatient();
|
|
toast.info("Usando dados de exemplo para o perfil do paciente.");
|
|
} catch (mockError) {
|
|
console.error("Falha ao buscar dados do mock:", mockError);
|
|
}
|
|
}
|
|
|
|
const nextAppointment = upcoming[0];
|
|
const appointmentsThisMonth = appointments.filter(appt => {
|
|
const apptDate = new Date(appt.scheduled_at);
|
|
const now = new Date();
|
|
return apptDate.getMonth() === now.getMonth() && apptDate.getFullYear() === now.getFullYear();
|
|
});
|
|
|
|
let profileCompleteness = 0;
|
|
let profileDescription = "Dados não encontrados";
|
|
if (patientData) {
|
|
const profileFields = ['nome_completo', 'cpf', 'email', 'telefone', 'data_nascimento', 'endereco', 'cidade', 'estado', 'cep', 'convenio'];
|
|
const filledFields = profileFields.filter(field => patientData[field]).length;
|
|
profileCompleteness = Math.round((filledFields / profileFields.length) * 100);
|
|
profileDescription = profileCompleteness === 100 ? "Dados completos" : `${filledFields} de ${profileFields.length} campos preenchidos`;
|
|
}
|
|
|
|
setStatsData([
|
|
{
|
|
title: "Próxima Consulta",
|
|
value: nextAppointment ? new Date(nextAppointment.scheduled_at).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }) : "Nenhuma",
|
|
description: nextAppointment ? `${nextAppointment.doctors?.full_name || 'Médico'} - ${new Date(nextAppointment.scheduled_at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}` : "Sem consultas futuras",
|
|
icon: Calendar
|
|
},
|
|
{
|
|
title: "Consultas Este Mês",
|
|
value: appointmentsThisMonth.length.toString(),
|
|
description: `${appointmentsThisMonth.filter(a => a.status === 'completed').length} realizadas`,
|
|
icon: Clock
|
|
},
|
|
{
|
|
title: "Perfil",
|
|
value: `${profileCompleteness}%`,
|
|
description: profileDescription,
|
|
icon: User
|
|
},
|
|
]);
|
|
|
|
} catch (err) {
|
|
console.error("Erro geral ao carregar dados do dashboard:", err);
|
|
setError("Não foi possível carregar as informações. Tente novamente mais tarde.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, []);
|
|
|
|
if (isLoading) {
|
|
return <div className="text-center text-muted-foreground">Carregando dashboard...</div>;
|
|
}
|
|
|
|
if (error) {
|
|
return <div className="text-center text-destructive p-4 bg-destructive/10 rounded-md">{error}</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Dashboard</h1>
|
|
<p className="text-muted-foreground">Bem-vindo ao seu portal de consultas médicas.</p>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{statsData.map((stat) => (
|
|
<DashboardStatCard key={stat.title} {...stat} />
|
|
))}
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 gap-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Ações Rápidas</CardTitle>
|
|
<CardDescription>Acesse rapidamente as principais funcionalidades.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{quickActions.map((action) => {
|
|
const Icon = action.icon;
|
|
return (
|
|
<Link key={action.href} href={action.href}>
|
|
<Button variant={action.variant} className="w-full justify-start bg-transparent">
|
|
<Icon className="mr-2 h-4 w-4" />
|
|
{action.label}
|
|
</Button>
|
|
</Link>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Próximas Consultas</CardTitle>
|
|
<CardDescription>Suas consultas agendadas para o futuro.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{upcomingAppointments.length > 0 ? (
|
|
upcomingAppointments.slice(0, 5).map((appointment) => (
|
|
<UpcomingAppointmentItem key={appointment.id} appointment={{
|
|
doctorName: appointment.doctors?.full_name || 'Médico a confirmar',
|
|
specialty: appointment.doctors?.specialty || 'Especialidade',
|
|
date: new Date(appointment.scheduled_at).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }),
|
|
time: new Date(appointment.scheduled_at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
|
|
}} />
|
|
))
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">Você não tem nenhuma consulta agendada.</p>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |