887 lines
36 KiB
JavaScript
887 lines
36 KiB
JavaScript
import React, { useState, useEffect } from "react";
|
||
import { Link } from "react-router-dom";
|
||
import { getAccessToken } from "../../utils/auth.js";
|
||
import { getFullName, getUserId } from "../../utils/userInfo";
|
||
import "../../assets/css/index.css";
|
||
import { getUserRole } from "../../utils/userInfo";
|
||
|
||
const AvatarForm = "/img/AvatarForm.jpg";
|
||
const banner = "/img/banner.png";
|
||
|
||
export default function PatientDashboard() {
|
||
const [appointments, setAppointments] = useState([]);
|
||
const [reports, setReports] = useState([]);
|
||
const [nextConsultations, setNextConsultations] = useState([]);
|
||
const [recentExams, setRecentExams] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [currentTime, setCurrentTime] = useState(new Date());
|
||
const role = getUserRole();
|
||
const tokenUsuario = getAccessToken();
|
||
const userId = getUserId();
|
||
const patientName = getFullName() || "Paciente";
|
||
|
||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
||
const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||
|
||
const API_KEY = supabaseAK;
|
||
|
||
const requestOptions = {
|
||
method: "GET",
|
||
headers: {
|
||
apikey: API_KEY,
|
||
Authorization: `Bearer ${tokenUsuario}`,
|
||
},
|
||
redirect: "follow",
|
||
};
|
||
|
||
useEffect(() => {
|
||
const loadPatientData = async () => {
|
||
try {
|
||
setLoading(true);
|
||
console.log("🔄 Carregando dados do paciente...", { userId, tokenUsuario: !!tokenUsuario });
|
||
|
||
// Buscar todas as consultas primeiro (sem filtrar por patient_id se não existir na tabela)
|
||
const appointmentsResponse = await fetch(
|
||
`${supabaseUrl}/rest/v1/appointments`,
|
||
requestOptions
|
||
);
|
||
|
||
const reportsResponse = await fetch(
|
||
`${supabaseUrl}/rest/v1/reports`,
|
||
requestOptions
|
||
);
|
||
|
||
const doctorsResponse = await fetch(
|
||
`${supabaseUrl}/rest/v1/doctors?select=id,full_name`,
|
||
requestOptions
|
||
);
|
||
|
||
console.log("📡 Status das respostas:", {
|
||
appointments: appointmentsResponse.status,
|
||
reports: reportsResponse.status,
|
||
doctors: doctorsResponse.status
|
||
});
|
||
|
||
const [appointmentsData, reportsData, doctorsData] = await Promise.all([
|
||
appointmentsResponse.json(),
|
||
reportsResponse.json(),
|
||
doctorsResponse.json()
|
||
]);
|
||
|
||
console.log("📊 Dados recebidos:", {
|
||
appointments: appointmentsData,
|
||
reports: reportsData,
|
||
doctors: doctorsData
|
||
});
|
||
|
||
const appointmentsArr = Array.isArray(appointmentsData) ? appointmentsData : [];
|
||
const reportsArr = Array.isArray(reportsData) ? reportsData : [];
|
||
const doctorsArr = Array.isArray(doctorsData) ? doctorsData : [];
|
||
|
||
// Filtrar consultas por patient_id (se o campo existir)
|
||
const patientAppointments = appointmentsArr.filter(apt =>
|
||
apt.patient_id === userId ||
|
||
apt.patient_id === parseInt(userId) ||
|
||
// Se não tiver patient_id, mostrar algumas para demonstração
|
||
!apt.patient_id
|
||
);
|
||
|
||
// Filtrar relatórios por patient_id (se o campo existir)
|
||
const patientReports = reportsArr.filter(rep =>
|
||
rep.patient_id === userId ||
|
||
rep.patient_id === parseInt(userId) ||
|
||
// Se não tiver patient_id, mostrar alguns para demonstração
|
||
!rep.patient_id
|
||
);
|
||
|
||
// Enriquecer consultas com nomes dos médicos
|
||
const enrichedAppointments = patientAppointments.map(appointment => {
|
||
const doctor = doctorsArr.find(doc => doc.id === appointment.doctor_id);
|
||
return {
|
||
...appointment,
|
||
doctor_name: doctor ? doctor.full_name : 'Médico não informado'
|
||
};
|
||
});
|
||
|
||
console.log("✅ Dados processados:", {
|
||
enrichedAppointments,
|
||
patientReports,
|
||
totalDoctors: doctorsArr.length
|
||
});
|
||
|
||
setAppointments(enrichedAppointments);
|
||
setReports(patientReports);
|
||
|
||
// Processar dados
|
||
console.log("🔥 TESTE: Chamando processNextConsultations com:", enrichedAppointments.length, "consultas");
|
||
|
||
// FORÇAR para teste
|
||
if (enrichedAppointments.length === 0) {
|
||
forceShowConsultations();
|
||
} else {
|
||
// Filtrar consultas não canceladas
|
||
const nonCancelledConsultations = enrichedAppointments.filter(apt =>
|
||
apt.status !== 'cancelled' &&
|
||
apt.status !== 'cancelada' &&
|
||
apt.status !== 'canceled'
|
||
);
|
||
|
||
console.log("📋 Consultas não canceladas:", nonCancelledConsultations.length, "de", enrichedAppointments.length);
|
||
|
||
if (nonCancelledConsultations.length > 0) {
|
||
// Ordenar por proximidade da data atual (mais próximas primeiro)
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
const sortedByProximity = nonCancelledConsultations
|
||
.map(apt => {
|
||
const dateField = apt.scheduled_at || apt.date;
|
||
const timeField = apt.time;
|
||
|
||
if (dateField) {
|
||
let consultationDateTime;
|
||
|
||
if (dateField.includes('T')) {
|
||
// Data já inclui horário
|
||
consultationDateTime = new Date(dateField);
|
||
} else {
|
||
// Combinar data com horário
|
||
consultationDateTime = new Date(dateField);
|
||
if (timeField) {
|
||
const [hours, minutes] = timeField.split(':');
|
||
consultationDateTime.setHours(parseInt(hours), parseInt(minutes), 0, 0);
|
||
} else {
|
||
consultationDateTime.setHours(12, 0, 0, 0); // Default meio-dia se não houver horário
|
||
}
|
||
}
|
||
|
||
const now = new Date();
|
||
const diffInMinutes = Math.abs((consultationDateTime - now) / (1000 * 60));
|
||
return { ...apt, proximityScore: diffInMinutes, consultationDateTime };
|
||
}
|
||
return { ...apt, proximityScore: 999999 }; // Consultas sem data vão para o final
|
||
})
|
||
.sort((a, b) => a.proximityScore - b.proximityScore)
|
||
.slice(0, 2);
|
||
|
||
console.log("✅ Mostrando 2 consultas mais próximas da data atual:", sortedByProximity);
|
||
setNextConsultations(sortedByProximity);
|
||
} else {
|
||
console.log("⚠️ Todas as consultas estão canceladas - usando dados de teste");
|
||
forceShowConsultations();
|
||
}
|
||
}
|
||
|
||
processRecentExams(patientReports);
|
||
|
||
} catch (error) {
|
||
console.error("❌ Erro ao carregar dados do paciente:", error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
if (tokenUsuario) {
|
||
loadPatientData();
|
||
}
|
||
}, [userId, tokenUsuario]);
|
||
|
||
// Processar próximas consultas
|
||
const processNextConsultations = (appointments) => {
|
||
console.log("🔄 Processando consultas:", appointments);
|
||
console.log("📊 Total de consultas recebidas:", appointments.length);
|
||
|
||
// Análise detalhada de cada consulta
|
||
appointments.forEach((apt, index) => {
|
||
console.log(`📋 Consulta ${index + 1}:`, {
|
||
id: apt.id,
|
||
scheduled_at: apt.scheduled_at,
|
||
date: apt.date,
|
||
time: apt.time,
|
||
doctor_name: apt.doctor_name,
|
||
status: apt.status
|
||
});
|
||
});
|
||
|
||
// Data de hoje em formato string para comparação
|
||
const today = new Date();
|
||
const todayString = today.toISOString().split('T')[0]; // YYYY-MM-DD
|
||
|
||
console.log("<22> Data de hoje (string):", todayString);
|
||
|
||
// Filtrar consultas futuras (incluindo hoje)
|
||
const futureConsultations = appointments.filter(apt => {
|
||
// Usar scheduled_at como data principal
|
||
const dateField = apt.scheduled_at || apt.date;
|
||
|
||
if (!dateField) {
|
||
console.log("⚠️ Consulta sem data:", apt.id);
|
||
return false;
|
||
}
|
||
|
||
// Normalizar a data da consulta
|
||
let consultationDate = dateField;
|
||
|
||
// Se a data contém horário, pegar apenas a parte da data
|
||
if (consultationDate.includes('T')) {
|
||
consultationDate = consultationDate.split('T')[0];
|
||
}
|
||
|
||
const isFuture = consultationDate >= todayString;
|
||
console.log(`📅 Consulta ${apt.id}: ${consultationDate} >= ${todayString} = ${isFuture}`);
|
||
|
||
return isFuture;
|
||
});
|
||
|
||
console.log("🔮 Consultas futuras encontradas:", futureConsultations.length);
|
||
console.log("📋 Lista de consultas futuras:", futureConsultations);
|
||
|
||
// Mostrar as 2 consultas mais próximas do horário atual (futuras ou passadas)
|
||
const consultationsWithProximity = appointments
|
||
.map(apt => {
|
||
const dateField = apt.scheduled_at || apt.date;
|
||
const timeField = apt.time;
|
||
|
||
if (dateField) {
|
||
let consultationDateTime;
|
||
|
||
if (dateField.includes('T')) {
|
||
// Data já inclui horário
|
||
consultationDateTime = new Date(dateField);
|
||
} else {
|
||
// Combinar data com horário
|
||
consultationDateTime = new Date(dateField);
|
||
if (timeField) {
|
||
const [hours, minutes] = timeField.split(':');
|
||
consultationDateTime.setHours(parseInt(hours), parseInt(minutes), 0, 0);
|
||
} else {
|
||
consultationDateTime.setHours(12, 0, 0, 0); // Default meio-dia se não houver horário
|
||
}
|
||
}
|
||
|
||
const now = new Date();
|
||
const diffInMinutes = Math.abs((consultationDateTime - now) / (1000 * 60));
|
||
return { ...apt, proximityScore: diffInMinutes, consultationDateTime };
|
||
}
|
||
return { ...apt, proximityScore: 999999 }; // Consultas sem data vão para o final
|
||
})
|
||
.sort((a, b) => a.proximityScore - b.proximityScore)
|
||
.slice(0, 2);
|
||
|
||
console.log("✅ 2 consultas mais próximas da data atual:", consultationsWithProximity);
|
||
setNextConsultations(consultationsWithProximity);
|
||
};
|
||
|
||
// FUNÇÃO DE TESTE - FORÇAR EXIBIÇÃO
|
||
|
||
// Processar exames recentes
|
||
const processRecentExams = (reports) => {
|
||
console.log("🔬 Processando exames:", reports);
|
||
|
||
// Ordenar por data de criação (mais recentes primeiro)
|
||
const recent = reports
|
||
.filter(report => report.created_at) // Apenas com data válida
|
||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
|
||
.slice(0, 5);
|
||
|
||
console.log("✅ Exames recentes processados:", recent);
|
||
setRecentExams(recent);
|
||
};
|
||
|
||
// Atualizar relógio
|
||
useEffect(() => {
|
||
const timer = setInterval(() => {
|
||
setCurrentTime(new Date());
|
||
}, 1000);
|
||
return () => clearInterval(timer);
|
||
}, []);
|
||
|
||
// Funções auxiliares para status das consultas
|
||
const getStatusColor = (status) => {
|
||
switch (status) {
|
||
case 'confirmed': case 'confirmada': return 'bg-success';
|
||
case 'pending': case 'pendente': return 'bg-warning';
|
||
case 'cancelled': case 'cancelada': return 'bg-danger';
|
||
case 'completed': case 'finalizada': return 'bg-info';
|
||
default: return 'bg-primary';
|
||
}
|
||
};
|
||
|
||
const getStatusBorderColor = (status) => {
|
||
switch (status) {
|
||
case 'confirmed': case 'confirmada': return '#28a745';
|
||
case 'pending': case 'pendente': return '#ffc107';
|
||
case 'cancelled': case 'cancelada': return '#dc3545';
|
||
case 'completed': case 'finalizada': return '#17a2b8';
|
||
default: return '#007bff';
|
||
}
|
||
};
|
||
|
||
const getStatusIcon = (status) => {
|
||
switch (status) {
|
||
case 'confirmed': case 'confirmada': return 'fa-check';
|
||
case 'pending': case 'pendente': return 'fa-clock-o';
|
||
case 'cancelled': case 'cancelada': return 'fa-times';
|
||
case 'completed': case 'finalizada': return 'fa-check-circle';
|
||
default: return 'fa-calendar';
|
||
}
|
||
};
|
||
|
||
|
||
|
||
|
||
|
||
// Funções auxiliares para status dos exames
|
||
const getExamBorderColor = (status) => {
|
||
switch (status) {
|
||
case 'completed': case 'finalizado': return '#28a745';
|
||
case 'draft': case 'rascunho': return '#ffc107';
|
||
case 'pending': case 'pendente': return '#17a2b8';
|
||
default: return '#6c757d';
|
||
}
|
||
};
|
||
|
||
const getExamIconColor = (status) => {
|
||
switch (status) {
|
||
case 'completed': case 'finalizado': return 'bg-success';
|
||
case 'draft': case 'rascunho': return 'bg-warning';
|
||
case 'pending': case 'pendente': return 'bg-info';
|
||
default: return 'bg-secondary';
|
||
}
|
||
};
|
||
|
||
const getExamIcon = (status) => {
|
||
switch (status) {
|
||
case 'completed': case 'finalizado': return 'fa-check';
|
||
case 'draft': case 'rascunho': return 'fa-clock-o';
|
||
case 'pending': case 'pendente': return 'fa-file-text';
|
||
default: return 'fa-file-o';
|
||
}
|
||
};
|
||
|
||
const getExamBadgeClass = (status) => {
|
||
switch (status) {
|
||
case 'completed': case 'finalizado': return 'bg-success';
|
||
case 'draft': case 'rascunho': return 'bg-warning';
|
||
case 'pending': case 'pendente': return 'bg-info';
|
||
default: return 'bg-secondary';
|
||
}
|
||
};
|
||
|
||
const getExamStatusText = (status) => {
|
||
switch (status) {
|
||
case 'completed': case 'finalizado': return 'Concluído';
|
||
case 'draft': case 'rascunho': return 'Em análise';
|
||
case 'pending': case 'pendente': return 'Disponível';
|
||
default: return 'Processando';
|
||
}
|
||
};
|
||
|
||
// Estatísticas calculadas baseadas nos dados reais da API + demonstração (apenas consultas não canceladas)
|
||
const nonCancelledAppointments = appointments.filter(apt =>
|
||
apt.status !== 'cancelled' &&
|
||
apt.status !== 'cancelada' &&
|
||
apt.status !== 'canceled'
|
||
);
|
||
|
||
const totalConsultas = nonCancelledAppointments.length > 0 ? nonCancelledAppointments.length : 5;
|
||
const consultasRealizadas = nonCancelledAppointments.length > 0
|
||
? nonCancelledAppointments.filter(apt => apt.status === 'completed' || apt.status === 'finalizada').length
|
||
: 3;
|
||
const proximasConsultas = nextConsultations.length;
|
||
const examesRealizados = reports.length > 0 ? reports.length : 3;
|
||
const [previewUrl, setPreviewUrl] = useState(AvatarForm);
|
||
useEffect(() => {
|
||
const loadAvatar = async () => {
|
||
if (!userId) return;
|
||
|
||
const myHeaders = new Headers();
|
||
myHeaders.append("apikey", supabaseAK);
|
||
myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
|
||
|
||
const requestOptions = {
|
||
headers: myHeaders,
|
||
method: 'GET',
|
||
redirect: 'follow'
|
||
};
|
||
|
||
try {
|
||
const response = await fetch(`${supabaseUrl}/storage/v1/object/avatars/${userId}/avatar.png`, requestOptions);
|
||
|
||
if (response.ok) {
|
||
const blob = await response.blob();
|
||
const imageUrl = URL.createObjectURL(blob);
|
||
setPreviewUrl(imageUrl);
|
||
return; // Avatar encontrado
|
||
}
|
||
} catch (error) {
|
||
|
||
}
|
||
|
||
// Se chegou até aqui, não encontrou avatar - mantém o padrão
|
||
|
||
};
|
||
|
||
loadAvatar();
|
||
}, [userId]);
|
||
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="page-wrapper">
|
||
<div className="content">
|
||
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: "400px" }}>
|
||
<div className="text-center">
|
||
<div className="spinner-border text-primary" role="status">
|
||
<span className="sr-only">Carregando...</span>
|
||
</div>
|
||
<p className="text-muted mt-3">Carregando dashboard...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="page-wrapper">
|
||
<div className="content">
|
||
{/* Header com informações do paciente */}
|
||
<div className="page-header">
|
||
<div className="row">
|
||
<div className="col-sm-12">
|
||
<div className="user-info-banner" style={{
|
||
background: `linear-gradient(135deg, #004a99, #0077cc), url(${banner})`,
|
||
backgroundSize: 'cover',
|
||
borderRadius: '15px',
|
||
padding: '30px',
|
||
color: 'white',
|
||
marginBottom: '20px'
|
||
}}>
|
||
<div className="row align-items-center">
|
||
<div className="col-md-8">
|
||
<h2 className="mb-2" style={{color: 'white'}}>👋 Olá, {patientName}!</h2>
|
||
<p className="mb-2" style={{ color: 'white' }}>Acompanhe suas consultas, resultados e tudo o que precisa em um só lugar.
|
||
Cuide-se, e deixe o resto com a gente 💙</p>
|
||
<small className="opacity-75">
|
||
🕒 {currentTime.toLocaleString('pt-BR')}
|
||
</small>
|
||
</div>
|
||
<div className="col-md-4 text-right">
|
||
<img
|
||
src={previewUrl}
|
||
alt="Avatar"
|
||
className="rounded-circle"
|
||
style={{ width: '80px', height: '80px', objectFit: 'cover', border: '3px solid white' }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Cards de estatísticas */}
|
||
<div className="row">
|
||
<div className="col-md-6 col-sm-6 col-lg-6 col-xl-3">
|
||
<div className="dash-widget" style={{ borderRadius: '15px', boxShadow: '0 4px 20px rgba(0,0,0,0.1)' }}>
|
||
<span className="dash-widget-bg1">
|
||
<i className="fa fa-calendar-check-o" aria-hidden="true"></i>
|
||
</span>
|
||
<div className="dash-widget-info text-right">
|
||
<h3>{totalConsultas}</h3>
|
||
<span className="widget-title1">Total Consultas</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="col-md-6 col-sm-6 col-lg-6 col-xl-3">
|
||
<div className="dash-widget" style={{ borderRadius: '15px', boxShadow: '0 4px 20px rgba(0,0,0,0.1)' }}>
|
||
<span className="dash-widget-bg2">
|
||
<i className="fa fa-clock-o" aria-hidden="true"></i>
|
||
</span>
|
||
<div className="dash-widget-info text-right">
|
||
<h3>{proximasConsultas}</h3>
|
||
<span className="widget-title2">Próximas Consultas</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="col-md-6 col-sm-6 col-lg-6 col-xl-3">
|
||
<div className="dash-widget" style={{ borderRadius: '15px', boxShadow: '0 4px 20px rgba(0,0,0,0.1)' }}>
|
||
<span className="dash-widget-bg3">
|
||
<i className="fa fa-stethoscope" aria-hidden="true"></i>
|
||
</span>
|
||
<div className="dash-widget-info text-right">
|
||
<h3>{examesRealizados}</h3>
|
||
<span className="widget-title3">Laudos</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="col-md-6 col-sm-6 col-lg-6 col-xl-3">
|
||
<div className="dash-widget" style={{ borderRadius: '15px', boxShadow: '0 4px 20px rgba(0,0,0,0.1)' }}>
|
||
<span className="dash-widget-bg4">
|
||
<i className="fa fa-check-circle" aria-hidden="true"></i>
|
||
</span>
|
||
<div className="dash-widget-info text-right">
|
||
<h3>{consultasRealizadas}</h3>
|
||
<span className="widget-title4">Consultas Realizadas</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Ações rápidas */}
|
||
<div className="row mb-4">
|
||
<div className="col-12">
|
||
<div className="card" style={{ borderRadius: '15px', boxShadow: '0 4px 20px rgba(0,0,0,0.1)' }}>
|
||
|
||
<div className="card-body">
|
||
<div className="row">
|
||
<div className="col-md-3 col-sm-6 mb-3">
|
||
<Link to="/paciente/medicosdisponiveis" className="btn btn-outline-primary btn-lg w-100" style={{ borderRadius: '10px' }}>
|
||
<i className="fa fa-user-md mb-2" style={{ fontSize: '24px', display: 'block' }}></i>
|
||
Agendar Consulta
|
||
</Link>
|
||
</div>
|
||
<div className="col-md-3 col-sm-6 mb-3">
|
||
<Link to="/paciente/consultalist" className="btn btn-outline-success btn-lg w-100" style={{ borderRadius: '10px' }}>
|
||
<i className="fa fa-calendar mb-2" style={{ fontSize: '24px', display: 'block' }}></i>
|
||
Minhas Consultas
|
||
</Link>
|
||
</div>
|
||
<div className="col-md-3 col-sm-6 mb-3">
|
||
<Link to="/paciente/laudolist" className="btn btn-outline-info btn-lg w-100" style={{ borderRadius: '10px' }}>
|
||
<i className="fa fa-file-text mb-2" style={{ fontSize: '24px', display: 'block' }}></i>
|
||
Meus Laudos
|
||
</Link>
|
||
</div>
|
||
<div className="col-md-3 col-sm-6 mb-3">
|
||
<button className="btn btn-outline-warning btn-lg w-100" style={{ borderRadius: '10px' }}>
|
||
<i className="fa fa-phone mb-2" style={{ fontSize: '24px', display: 'block' }}></i>
|
||
Emergência
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="row">
|
||
{/* Próximas Consultas */}
|
||
<div className="col-12 col-lg-6 mb-4">
|
||
<div className="card" style={{ borderRadius: '15px', boxShadow: '0 4px 20px rgba(0,0,0,0.1)' }}>
|
||
<div className="card-header d-flex justify-content-between align-items-center">
|
||
<h4 className="card-title">📅 Próximas Consultas</h4>
|
||
<Link className="btn btn-primary btn-sm" to="/paciente/consultalist" style={{ borderRadius: '8px' }}>
|
||
Ver todas
|
||
</Link>
|
||
</div>
|
||
<div className="card-body">
|
||
{nextConsultations.length > 0 ? (
|
||
<div className="row">
|
||
{nextConsultations.map((consultation, index) => (
|
||
<div key={consultation.id} className="col-12 mb-2">
|
||
<div className="d-flex align-items-center p-2 rounded" style={{
|
||
background: '#f8f9fa',
|
||
borderRadius: '10px',
|
||
color: '#333',
|
||
border: '1px solid #dee2e6',
|
||
borderLeftWidth: '4px',
|
||
borderLeftColor: getStatusBorderColor(consultation.status)
|
||
}}>
|
||
<div className="consultation-icon me-3">
|
||
<div className={`${getStatusColor(consultation.status)} rounded-circle d-flex align-items-center justify-content-center`} style={{ width: '35px', height: '35px' }}>
|
||
<i className={`fa ${getStatusIcon(consultation.status)} text-white`}></i>
|
||
</div>
|
||
</div>
|
||
<div className="flex-grow-1">
|
||
<h6 className="mb-0">{consultation.doctor_name || 'Médico não informado'}</h6>
|
||
<small className="text-muted">
|
||
{(() => {
|
||
const dateToShow = consultation.scheduled_at || consultation.date;
|
||
if (dateToShow) {
|
||
// Extrair data e hora sem conversão de fuso horário
|
||
let dateStr = '';
|
||
let timeStr = '';
|
||
|
||
if (dateToShow.includes('T')) {
|
||
// Formato ISO: 2025-11-15T14:30:00
|
||
const [datePart, timePart] = dateToShow.split('T');
|
||
const [year, month, day] = datePart.split('-');
|
||
dateStr = `${day}/${month}/${year}`;
|
||
|
||
if (timePart) {
|
||
const [hour, minute] = timePart.split(':');
|
||
timeStr = `${hour}:${minute}`;
|
||
}
|
||
} else {
|
||
// Formato simples: 2025-11-15
|
||
const [year, month, day] = dateToShow.split('-');
|
||
dateStr = `${day}/${month}/${year}`;
|
||
}
|
||
|
||
// Usar horário do campo time se existir, senão usar o extraído
|
||
const finalTime = consultation.time || timeStr || 'Horário a confirmar';
|
||
|
||
return `${dateStr} às ${finalTime}`;
|
||
}
|
||
return 'Data a confirmar';
|
||
})()}
|
||
</small>
|
||
</div>
|
||
<span
|
||
className={`custom-badge ${
|
||
consultation.status === 'requested' ? 'status-orange' :
|
||
consultation.status === 'confirmed' ? 'status-blue' :
|
||
consultation.status === 'completed' ? 'status-green' :
|
||
consultation.status === 'cancelled' ? 'status-red' :
|
||
'status-gray'
|
||
}`}
|
||
style={{ minWidth: '110px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
|
||
>
|
||
{consultation.status === 'requested' ? (
|
||
<>
|
||
<i className="fa fa-clock-o" style={{ marginRight: '6px' }}></i>
|
||
Solicitado
|
||
</>
|
||
) : consultation.status === 'confirmed' ? (
|
||
<>
|
||
<i className="fa fa-check-circle" style={{ marginRight: '6px' }}></i>
|
||
Confirmado
|
||
</>
|
||
) : consultation.status === 'completed' ? (
|
||
<>
|
||
<i className="fa fa-check" style={{ marginRight: '6px' }}></i>
|
||
Concluído
|
||
</>
|
||
) : consultation.status === 'cancelled' ? (
|
||
<>
|
||
<i className="fa fa-times-circle" style={{ marginRight: '6px' }}></i>
|
||
Cancelado
|
||
</>
|
||
) : (
|
||
<>
|
||
<i className="fa fa-question-circle" style={{ marginRight: '6px' }}></i>
|
||
{consultation.status}
|
||
</>
|
||
)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-center text-muted py-4">
|
||
<i className="fa fa-calendar-o fa-3x mb-3"></i>
|
||
<p>Nenhuma consulta agendada</p>
|
||
<Link to="/paciente/medicosdisponiveis" className="btn btn-primary btn-sm">
|
||
Agendar primeira consulta
|
||
</Link>
|
||
</div>
|
||
)}
|
||
|
||
{/* Botão para ver mais */}
|
||
<div className="text-center mt-3">
|
||
<Link to="/paciente/medicosdisponiveis" className="btn btn-outline-primary btn-sm">
|
||
<i className="fa fa-plus me-2"></i>
|
||
Agendar nova consulta
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Exames Recentes */}
|
||
<div className="col-12 col-lg-6 mb-4">
|
||
<div className="card" style={{ borderRadius: '15px', boxShadow: '0 4px 20px rgba(0,0,0,0.1)' }}>
|
||
<div className="card-header d-flex justify-content-between align-items-center">
|
||
<h4 className="card-title">🔬 Laudos Recentes</h4>
|
||
<Link className="btn btn-primary btn-sm" to="/paciente/laudolist" style={{ borderRadius: '8px' }}>
|
||
Ver todos
|
||
</Link>
|
||
</div>
|
||
<div className="card-body">
|
||
{recentExams.length > 0 ? (
|
||
<div className="row">
|
||
{recentExams.map((exam) => (
|
||
<div key={exam.id} className="col-12 mb-2">
|
||
<div className="d-flex align-items-center p-2 rounded" style={{
|
||
background: '#f8f9fa',
|
||
borderRadius: '10px',
|
||
color: '#333',
|
||
border: '1px solid #dee2e6',
|
||
borderLeftWidth: '4px',
|
||
borderLeftColor: getExamBorderColor(exam.status)
|
||
}}>
|
||
<div className="exam-icon me-3">
|
||
<div className={`${getExamIconColor(exam.status)} rounded-circle d-flex align-items-center justify-content-center`} style={{ width: '35px', height: '35px' }}>
|
||
<i className={`fa ${getExamIcon(exam.status)} text-white`}></i>
|
||
</div>
|
||
</div>
|
||
<div className="flex-grow-1">
|
||
<h6 className="mb-0">{exam.exam || 'Exame'}</h6>
|
||
<small className="text-muted">
|
||
{new Date(exam.created_at).toLocaleDateString('pt-BR')} - {exam.requested_by || 'Médico não informado'}
|
||
</small>
|
||
</div>
|
||
<span
|
||
className={`custom-badge ${
|
||
exam.status === 'draft' ? 'status-orange' :
|
||
exam.status === 'completed' ? 'status-green' :
|
||
'status-gray'
|
||
}`}
|
||
style={{ minWidth: '110px', display: 'inline-block', textAlign: 'center' }}
|
||
>
|
||
{exam.status === 'draft' ? (
|
||
<>
|
||
<i className="fa fa-edit" style={{ marginRight: '6px' }}></i>
|
||
Rascunho
|
||
</>
|
||
) : exam.status === 'completed' ? (
|
||
<>
|
||
<i className="fa fa-check-circle" style={{ marginRight: '6px' }}></i>
|
||
Concluído
|
||
</>
|
||
) : (
|
||
exam.status
|
||
)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-center text-muted py-4">
|
||
<i className="fa fa-file-text-o fa-3x mb-3"></i>
|
||
<p>Nenhum laudo realizado ainda</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="text-center mt-3">
|
||
<Link to="/paciente/laudolist" className="btn btn-outline-info btn-sm">
|
||
<i className="fa fa-eye me-2"></i>
|
||
Ver todos os laudos
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Informações de saúde */}
|
||
<div className="row">
|
||
<div className="col-12">
|
||
<div className="card" style={{ borderRadius: '15px', boxShadow: '0 4px 20px rgba(0,0,0,0.1)' }}>
|
||
<div className="card-header">
|
||
<h4 className="card-title">💡 Dicas de Saúde</h4>
|
||
</div>
|
||
<div className="card-body">
|
||
<div className="row">
|
||
<div className="col-md-4 text-center mb-3">
|
||
<div className="health-tip p-3" style={{ backgroundColor: '#f8f9fa', borderRadius: '10px' }}>
|
||
<i className="fa fa-heart text-danger fa-2x mb-2"></i>
|
||
<h6>Exercite-se Regularmente</h6>
|
||
<p className="text-muted small">30 minutos de atividade física por dia fazem a diferença</p>
|
||
</div>
|
||
</div>
|
||
<div className="col-md-4 text-center mb-3">
|
||
<div className="health-tip p-3" style={{ backgroundColor: '#f8f9fa', borderRadius: '10px' }}>
|
||
<i className="fa fa-coffee text-success fa-2x mb-2"></i>
|
||
<h6>Alimentação Saudável</h6>
|
||
<p className="text-muted small">Consuma frutas e vegetais todos os dias</p>
|
||
</div>
|
||
</div>
|
||
<div className="col-md-4 text-center mb-3">
|
||
<div className="health-tip p-3" style={{ backgroundColor: '#f8f9fa', borderRadius: '10px' }}>
|
||
<i className="fa fa-bed text-primary fa-2x mb-2"></i>
|
||
<h6>Durma Bem</h6>
|
||
<p className="text-muted small">7-8 horas de sono são essenciais para sua saúde</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// CSS customizado para o PatientDashboard
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
.timeline {
|
||
position: relative;
|
||
}
|
||
|
||
.timeline-item {
|
||
position: relative;
|
||
}
|
||
|
||
.timeline-marker {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
margin-top: 6px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.timeline-content {
|
||
background: #f8f9fa;
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
border-left: 3px solid #007bff;
|
||
width: 100%;
|
||
}
|
||
|
||
.exam-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.health-tip {
|
||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
.health-tip:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.dash-widget {
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.dash-widget:hover {
|
||
transform: translateY(-3px);
|
||
}
|
||
|
||
.user-info-banner {
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.user-info-banner::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 20"><defs><radialGradient id="a" cx="50%" cy="0%" r="100%"><stop offset="0%" style="stop-color:rgba(255,255,255,0.1)"/><stop offset="100%" style="stop-color:rgba(255,255,255,0)"/></radialGradient></defs><rect width="100" height="20" fill="url(%23a)"/></svg>');
|
||
pointer-events: none;
|
||
}
|
||
`;
|
||
|
||
if (!document.head.querySelector('[data-patient-dashboard-styles]')) {
|
||
style.setAttribute('data-patient-dashboard-styles', 'true');
|
||
document.head.appendChild(style);
|
||
} |