Compare commits

..

No commits in common. "3c52ec5e3a172ad194d9f5f3fd122a4d5055f8ff" and "770eab9afeddd7d8d63837a854a25e745f83e0f1" have entirely different histories.

16 changed files with 3928 additions and 3313 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,403 +1,41 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import {
countTotalPatients,
countTotalDoctors,
countAppointmentsToday,
getUpcomingAppointments,
getAppointmentsByDateRange,
getNewUsersLastDays,
getPendingReports,
getDisabledUsers,
getDoctorsAvailabilityToday,
getPatientById,
getDoctorById,
} from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { AlertCircle, Calendar, Users, Stethoscope, Clock, FileText, AlertTriangle, Plus, ArrowLeft } from 'lucide-react';
import Link from 'next/link';
import { PatientRegistrationForm } from '@/components/forms/patient-registration-form';
import { DoctorRegistrationForm } from '@/components/forms/doctor-registration-form';
interface DashboardStats {
totalPatients: number;
totalDoctors: number;
appointmentsToday: number;
}
interface UpcomingAppointment {
id: string;
scheduled_at: string;
status: string;
doctor_id: string;
patient_id: string;
doctor?: { full_name?: string };
patient?: { full_name?: string };
}
export default function DashboardPage() {
const router = useRouter();
const [stats, setStats] = useState<DashboardStats>({
totalPatients: 0,
totalDoctors: 0,
appointmentsToday: 0,
});
const [appointments, setAppointments] = useState<UpcomingAppointment[]>([]);
const [appointmentData, setAppointmentData] = useState<any[]>([]);
const [newUsers, setNewUsers] = useState<any[]>([]);
const [pendingReports, setPendingReports] = useState<any[]>([]);
const [disabledUsers, setDisabledUsers] = useState<any[]>([]);
const [doctors, setDoctors] = useState<Map<string, any>>(new Map());
const [patients, setPatients] = useState<Map<string, any>>(new Map());
const [loading, setLoading] = useState(true);
// Estados para os modais de formulário
const [showPatientForm, setShowPatientForm] = useState(false);
const [showDoctorForm, setShowDoctorForm] = useState(false);
const [editingPatientId, setEditingPatientId] = useState<string | null>(null);
const [editingDoctorId, setEditingDoctorId] = useState<string | null>(null);
useEffect(() => {
loadDashboardData();
}, []);
const loadDashboardData = async () => {
try {
setLoading(true);
// 1. Carrega stats
const [patientCount, doctorCount, todayCount] = await Promise.all([
countTotalPatients(),
countTotalDoctors(),
countAppointmentsToday(),
]);
setStats({
totalPatients: patientCount,
totalDoctors: doctorCount,
appointmentsToday: todayCount,
});
// 2. Carrega dados dos widgets em paralelo
const [upcomingAppts, appointmentDataRange, newUsersList, pendingReportsList, disabledUsersList] = await Promise.all([
getUpcomingAppointments(5),
getAppointmentsByDateRange(7),
getNewUsersLastDays(7),
getPendingReports(5),
getDisabledUsers(5),
]);
setAppointments(upcomingAppts);
setAppointmentData(appointmentDataRange);
setNewUsers(newUsersList);
setPendingReports(pendingReportsList);
setDisabledUsers(disabledUsersList);
// 3. Busca detalhes de pacientes e médicos para as próximas consultas
const doctorMap = new Map();
const patientMap = new Map();
for (const appt of upcomingAppts) {
if (appt.doctor_id && !doctorMap.has(appt.doctor_id)) {
const doctor = await getDoctorById(appt.doctor_id);
if (doctor) doctorMap.set(appt.doctor_id, doctor);
}
if (appt.patient_id && !patientMap.has(appt.patient_id)) {
const patient = await getPatientById(appt.patient_id);
if (patient) patientMap.set(appt.patient_id, patient);
}
}
setDoctors(doctorMap);
setPatients(patientMap);
} catch (err) {
console.error('[Dashboard] Erro ao carregar dados:', err);
} finally {
setLoading(false);
}
};
const handlePatientFormSaved = () => {
setShowPatientForm(false);
setEditingPatientId(null);
loadDashboardData();
};
const handleDoctorFormSaved = () => {
setShowDoctorForm(false);
setEditingDoctorId(null);
loadDashboardData();
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};
const getStatusBadge = (status: string) => {
const statusMap: Record<string, { variant: any; label: string }> = {
confirmed: { variant: 'default', label: 'Confirmado' },
completed: { variant: 'secondary', label: 'Concluído' },
cancelled: { variant: 'destructive', label: 'Cancelado' },
requested: { variant: 'outline', label: 'Solicitado' },
};
const s = statusMap[status] || { variant: 'outline', label: status };
return <Badge variant={s.variant as any}>{s.label}</Badge>;
};
if (loading) {
return (
<div className="space-y-6 p-6 bg-background">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-muted rounded w-1/4"></div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
{[1, 2, 3, 4].map(i => (
<div key={i} className="h-32 bg-muted rounded"></div>
))}
</div>
</div>
</div>
);
}
// Se está exibindo formulário de paciente
if (showPatientForm) {
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center gap-4">
<Button variant="ghost" onClick={() => {
setShowPatientForm(false);
setEditingPatientId(null);
}}>
<ArrowLeft className="h-4 w-4" />
</Button>
<h1 className="text-2xl font-bold">{editingPatientId ? "Editar paciente" : "Novo paciente"}</h1>
</div>
<PatientRegistrationForm
inline
mode={editingPatientId ? "edit" : "create"}
patientId={editingPatientId}
onSaved={handlePatientFormSaved}
onClose={() => {
setShowPatientForm(false);
setEditingPatientId(null);
}}
/>
</div>
);
}
// Se está exibindo formulário de médico
if (showDoctorForm) {
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => {
setShowDoctorForm(false);
setEditingDoctorId(null);
}}>
<ArrowLeft className="h-4 w-4" />
</Button>
<h1 className="text-2xl font-bold">{editingDoctorId ? "Editar Médico" : "Novo Médico"}</h1>
</div>
<DoctorRegistrationForm
inline
mode={editingDoctorId ? "edit" : "create"}
doctorId={editingDoctorId}
onSaved={handleDoctorFormSaved}
onClose={() => {
setShowDoctorForm(false);
setEditingDoctorId(null);
}}
/>
</div>
);
}
return (
<div className="space-y-6 p-6 bg-background">
{/* Header */}
<div>
<h1 className="text-3xl font-bold text-foreground">Dashboard</h1>
<p className="text-muted-foreground mt-2">Bem-vindo ao painel de controle</p>
</div>
{/* 1. CARDS RESUMO */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-card p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Total de Pacientes</h3>
<p className="text-3xl font-bold text-foreground mt-2">{stats.totalPatients}</p>
</div>
<Users className="h-8 w-8 text-blue-500 opacity-20" />
</div>
<>
<div className="space-y-6 p-6 bg-background">
<div>
<h1 className="text-2xl font-bold text-foreground">Dashboard</h1>
<p className="text-muted-foreground">
Bem-vindo ao painel de controle
</p>
</div>
<div className="bg-card p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Total de Médicos</h3>
<p className="text-3xl font-bold text-foreground mt-2">{stats.totalDoctors}</p>
</div>
<Stethoscope className="h-8 w-8 text-green-500 opacity-20" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-card p-6 rounded-lg border">
<h3 className="text-sm font-medium text-muted-foreground">
Total de Pacientes
</h3>
<p className="text-2xl font-bold text-foreground">1,234</p>
</div>
</div>
<div className="bg-card p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Consultas Hoje</h3>
<p className="text-3xl font-bold text-foreground mt-2">{stats.appointmentsToday}</p>
</div>
<Calendar className="h-8 w-8 text-purple-500 opacity-20" />
<div className="bg-card p-6 rounded-lg border">
<h3 className="text-sm font-medium text-muted-foreground">
Consultas Hoje
</h3>
<p className="text-2xl font-bold text-foreground">28</p>
</div>
</div>
<div className="bg-card p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Relatórios Pendentes</h3>
<p className="text-3xl font-bold text-foreground mt-2">{pendingReports.length}</p>
</div>
<FileText className="h-8 w-8 text-orange-500 opacity-20" />
<div className="bg-card p-6 rounded-lg border">
<h3 className="text-sm font-medium text-muted-foreground">
Próximas Consultas
</h3>
<p className="text-2xl font-bold text-foreground">45</p>
</div>
<div className="bg-card p-6 rounded-lg border">
<h3 className="text-sm font-medium text-muted-foreground">
Receita Mensal
</h3>
<p className="text-2xl font-bold text-foreground">R$ 45.230</p>
</div>
</div>
</div>
{/* 6. AÇÕES RÁPIDAS */}
<div className="bg-card p-6 rounded-lg border border-border">
<h2 className="text-lg font-semibold text-foreground mb-4">Ações Rápidas</h2>
<div className="flex flex-wrap gap-3">
<Button onClick={() => setShowPatientForm(true)} className="gap-2">
<Plus className="h-4 w-4" />
Novo Paciente
</Button>
<Button onClick={() => router.push('/agenda')} variant="outline" className="gap-2">
<Calendar className="h-4 w-4" />
Novo Agendamento
</Button>
<Button onClick={() => setShowDoctorForm(true)} variant="outline" className="gap-2">
<Stethoscope className="h-4 w-4" />
Novo Médico
</Button>
<Button onClick={() => router.push('/dashboard/relatorios')} variant="outline" className="gap-2">
<FileText className="h-4 w-4" />
Ver Relatórios
</Button>
</div>
</div>
{/* 2. PRÓXIMAS CONSULTAS */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 bg-card p-6 rounded-lg border border-border">
<h2 className="text-lg font-semibold text-foreground mb-4">Próximas Consultas (7 dias)</h2>
{appointments.length > 0 ? (
<div className="space-y-3">
{appointments.map(appt => (
<div key={appt.id} className="flex items-center justify-between p-3 bg-muted rounded-lg hover:bg-muted/80 transition">
<div className="flex-1">
<p className="font-medium text-foreground">
{patients.get(appt.patient_id)?.full_name || 'Paciente desconhecido'}
</p>
<p className="text-sm text-muted-foreground">
Médico: {doctors.get(appt.doctor_id)?.full_name || 'Médico desconhecido'}
</p>
<p className="text-xs text-muted-foreground mt-1">{formatDate(appt.scheduled_at)}</p>
</div>
<div className="flex items-center gap-2">
{getStatusBadge(appt.status)}
</div>
</div>
))}
</div>
) : (
<p className="text-muted-foreground">Nenhuma consulta agendada para os próximos 7 dias</p>
)}
</div>
{/* 5. RELATÓRIOS PENDENTES */}
<div className="bg-card p-6 rounded-lg border border-border">
<h2 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<FileText className="h-5 w-5" />
Relatórios Pendentes
</h2>
{pendingReports.length > 0 ? (
<div className="space-y-2">
{pendingReports.map(report => (
<div key={report.id} className="p-3 bg-muted rounded-lg hover:bg-muted/80 transition cursor-pointer text-sm">
<p className="font-medium text-foreground truncate">{report.order_number}</p>
<p className="text-xs text-muted-foreground">{report.exam || 'Sem descrição'}</p>
</div>
))}
<Button onClick={() => router.push('/dashboard/relatorios')} variant="ghost" className="w-full mt-2" size="sm">
Ver Todos
</Button>
</div>
) : (
<p className="text-muted-foreground text-sm">Sem relatórios pendentes</p>
)}
</div>
</div>
{/* 4. NOVOS USUÁRIOS */}
<div className="bg-card p-6 rounded-lg border border-border">
<h2 className="text-lg font-semibold text-foreground mb-4">Novos Usuários (últimos 7 dias)</h2>
{newUsers.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{newUsers.map(user => (
<div key={user.id} className="p-3 bg-muted rounded-lg">
<p className="font-medium text-foreground truncate">{user.full_name || 'Sem nome'}</p>
<p className="text-sm text-muted-foreground truncate">{user.email}</p>
</div>
))}
</div>
) : (
<p className="text-muted-foreground">Nenhum novo usuário nos últimos 7 dias</p>
)}
</div>
{/* 8. ALERTAS */}
{disabledUsers.length > 0 && (
<div className="bg-card p-6 rounded-lg border border-destructive/50">
<h2 className="text-lg font-semibold text-destructive mb-4 flex items-center gap-2">
<AlertTriangle className="h-5 w-5" />
Alertas - Usuários Desabilitados
</h2>
<div className="space-y-2">
{disabledUsers.map(user => (
<Alert key={user.id} variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<strong>{user.full_name}</strong> ({user.email}) está desabilitado
</AlertDescription>
</Alert>
))}
</div>
</div>
)}
{/* 11. LINK PARA RELATÓRIOS */}
<div className="bg-gradient-to-r from-blue-500/10 to-purple-500/10 p-6 rounded-lg border border-blue-500/20">
<h2 className="text-lg font-semibold text-foreground mb-2">Seção de Relatórios</h2>
<p className="text-muted-foreground text-sm mb-4">
Acesse a seção de relatórios médicos para gerenciar, visualizar e exportar documentos.
</p>
<Button asChild>
<Link href="/dashboard/relatorios">Ir para Relatórios</Link>
</Button>
</div>
</div>
</>
);
}

View File

@ -1,34 +0,0 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function PerfillLoading() {
return (
<div className="space-y-6 p-6">
<div className="flex items-center gap-4 mb-8">
<Skeleton className="h-20 w-20 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-6 w-64" />
<Skeleton className="h-4 w-48" />
</div>
</div>
<div className="grid gap-6">
<div className="rounded-lg border border-border p-6 space-y-4">
<Skeleton className="h-6 w-32" />
<div className="space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
</div>
<div className="rounded-lg border border-border p-6 space-y-4">
<Skeleton className="h-6 w-32" />
<div className="space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
</div>
</div>
</div>
);
}

View File

@ -1,653 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { UploadAvatar } from "@/components/ui/upload-avatar";
import { AlertCircle, ArrowLeft, CheckCircle, XCircle } from "lucide-react";
import { getUserInfoById } from "@/lib/api";
import { useAuth } from "@/hooks/useAuth";
import { formatTelefone, formatCEP, validarCEP, buscarCEP } from "@/lib/utils";
interface UserProfile {
user: {
id: string;
email: string;
created_at: string;
last_sign_in_at: string | null;
email_confirmed_at: string | null;
};
profile: {
id: string;
full_name: string | null;
email: string | null;
phone: string | null;
avatar_url: string | null;
cep?: string | null;
street?: string | null;
number?: string | null;
complement?: string | null;
neighborhood?: string | null;
city?: string | null;
state?: string | null;
disabled: boolean;
created_at: string;
updated_at: string;
} | null;
roles: string[];
permissions: {
isAdmin: boolean;
isManager: boolean;
isDoctor: boolean;
isSecretary: boolean;
isAdminOrManager: boolean;
};
}
export default function PerfilPage() {
const router = useRouter();
const { user: authUser } = useAuth();
const [userInfo, setUserInfo] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [editingData, setEditingData] = useState<{
phone?: string;
full_name?: string;
avatar_url?: string;
cep?: string;
street?: string;
number?: string;
complement?: string;
neighborhood?: string;
city?: string;
state?: string;
}>({});
const [cepLoading, setCepLoading] = useState(false);
const [cepValid, setCepValid] = useState<boolean | null>(null);
useEffect(() => {
async function loadUserInfo() {
try {
setLoading(true);
if (!authUser?.id) {
throw new Error("ID do usuário não encontrado");
}
console.log('[PERFIL] Chamando getUserInfoById com ID:', authUser.id);
// Para admin/gestor, usar getUserInfoById com o ID do usuário logado
const info = await getUserInfoById(authUser.id);
console.log('[PERFIL] Sucesso ao carregar info:', info);
setUserInfo(info as UserProfile);
setError(null);
} catch (err: any) {
console.error('[PERFIL] Erro ao carregar:', err);
setError(err?.message || "Erro ao carregar informações do perfil");
setUserInfo(null);
} finally {
setLoading(false);
}
}
if (authUser) {
console.log('[PERFIL] useEffect acionado, authUser:', authUser);
loadUserInfo();
}
}, [authUser]);
if (authUser?.userType !== 'administrador') {
return (
<div className="flex flex-col h-screen">
<div className="flex-1 p-6">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Você não tem permissão para acessar esta página.
</AlertDescription>
</Alert>
<Button
variant="outline"
onClick={() => router.back()}
className="mt-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Voltar
</Button>
</div>
</div>
);
}
if (loading) {
return (
<div className="flex flex-col h-screen">
<div className="flex-1 p-6">
<div className="space-y-4">
<div className="h-20 bg-muted rounded-lg animate-pulse" />
<div className="h-64 bg-muted rounded-lg animate-pulse" />
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="flex flex-col h-screen">
<div className="flex-1 p-6">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
<Button
variant="outline"
onClick={() => window.location.reload()}
className="mt-4"
>
Tentar Novamente
</Button>
</div>
</div>
);
}
if (!userInfo) {
return (
<div className="flex flex-col h-screen">
<div className="flex-1 p-6">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Nenhuma informação de perfil disponível.
</AlertDescription>
</Alert>
</div>
</div>
);
}
const getInitials = (name: string | null | undefined) => {
if (!name) return "AD";
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2);
};
const handleEditClick = () => {
if (!isEditing && userInfo) {
setEditingData({
full_name: userInfo.profile?.full_name || "",
phone: userInfo.profile?.phone || "",
avatar_url: userInfo.profile?.avatar_url || "",
cep: userInfo.profile?.cep || "",
street: userInfo.profile?.street || "",
number: userInfo.profile?.number || "",
complement: userInfo.profile?.complement || "",
neighborhood: userInfo.profile?.neighborhood || "",
city: userInfo.profile?.city || "",
state: userInfo.profile?.state || "",
});
// Se já existe CEP, marcar como válido
if (userInfo.profile?.cep) {
setCepValid(true);
}
}
setIsEditing(!isEditing);
};
const handleSaveEdit = async () => {
try {
// Aqui você implementaria a chamada para atualizar o perfil
console.log('[PERFIL] Salvando alterações:', editingData);
// await atualizarPerfil(userInfo?.user.id, editingData);
setIsEditing(false);
setUserInfo((prev) =>
prev ? {
...prev,
profile: prev.profile ? {
...prev.profile,
full_name: editingData.full_name || prev.profile.full_name,
phone: editingData.phone || prev.profile.phone,
avatar_url: editingData.avatar_url || prev.profile.avatar_url,
cep: editingData.cep || prev.profile.cep,
street: editingData.street || prev.profile.street,
number: editingData.number || prev.profile.number,
complement: editingData.complement || prev.profile.complement,
neighborhood: editingData.neighborhood || prev.profile.neighborhood,
city: editingData.city || prev.profile.city,
state: editingData.state || prev.profile.state,
} : null,
} : null
);
} catch (err: any) {
console.error('[PERFIL] Erro ao salvar:', err);
}
};
const handleCancelEdit = () => {
setIsEditing(false);
setEditingData({});
setCepValid(null);
};
const handleCepChange = async (cepValue: string) => {
// Formatar CEP
const formatted = formatCEP(cepValue);
setEditingData({...editingData, cep: formatted});
// Validar CEP
const isValid = validarCEP(cepValue);
setCepValid(isValid ? null : false); // null = não validado ainda, false = inválido
if (isValid) {
setCepLoading(true);
try {
const resultado = await buscarCEP(cepValue);
if (resultado) {
setCepValid(true);
// Preencher campos automaticamente
setEditingData(prev => ({
...prev,
street: resultado.street,
neighborhood: resultado.neighborhood,
city: resultado.city,
state: resultado.state,
}));
console.log('[PERFIL] CEP preenchido com sucesso:', resultado);
} else {
setCepValid(false);
}
} catch (err) {
console.error('[PERFIL] Erro ao buscar CEP:', err);
setCepValid(false);
} finally {
setCepLoading(false);
}
}
};
const handlePhoneChange = (phoneValue: string) => {
const formatted = formatTelefone(phoneValue);
setEditingData({...editingData, phone: formatted});
};
return (
<div className="flex flex-col h-screen bg-background">
<div className="flex-1 overflow-y-auto">
<div className="p-6 space-y-6">
{/* Header com Título e Botão */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold">Meu Perfil</h2>
<p className="text-muted-foreground mt-1">Bem-vindo à sua área exclusiva.</p>
</div>
{!isEditing ? (
<Button
className="bg-blue-600 hover:bg-blue-700"
onClick={handleEditClick}
>
Editar Perfil
</Button>
) : (
<div className="flex gap-2">
<Button
className="bg-green-600 hover:bg-green-700"
onClick={handleSaveEdit}
>
Salvar
</Button>
<Button
variant="outline"
onClick={handleCancelEdit}
>
Cancelar
</Button>
</div>
)}
</div>
{/* Grid de 2 colunas */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Coluna Esquerda - Informações Pessoais */}
<div className="lg:col-span-2 space-y-6">
{/* Informações Pessoais */}
<div className="border border-border rounded-lg p-6">
<h3 className="text-lg font-semibold mb-4">Informações Pessoais</h3>
<div className="space-y-4">
{/* Nome Completo */}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Nome Completo
</Label>
{isEditing ? (
<Input
value={editingData.full_name || ""}
onChange={(e) => setEditingData({...editingData, full_name: e.target.value})}
className="mt-2"
/>
) : (
<>
<div className="mt-2 p-3 bg-muted rounded text-foreground font-medium">
{userInfo.profile?.full_name || "Não preenchido"}
</div>
<p className="text-xs text-muted-foreground mt-1">
Este campo não pode ser alterado
</p>
</>
)}
</div>
{/* Email */}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Email
</Label>
<div className="mt-2 p-3 bg-muted rounded text-foreground">
{userInfo.user.email}
</div>
<p className="text-xs text-muted-foreground mt-1">
Este campo não pode ser alterado
</p>
</div>
{/* UUID */}
<div>
<Label className="text-sm font-medium text-muted-foreground">
UUID
</Label>
<div className="mt-2 p-3 bg-muted rounded text-foreground font-mono text-xs break-all">
{userInfo.user.id}
</div>
<p className="text-xs text-muted-foreground mt-1">
Este campo não pode ser alterado
</p>
</div>
{/* Permissões */}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Permissões
</Label>
<div className="mt-2 flex flex-wrap gap-2">
{userInfo.roles && userInfo.roles.length > 0 ? (
userInfo.roles.map((role) => (
<Badge key={role} variant="outline">
{role}
</Badge>
))
) : (
<span className="text-sm text-muted-foreground">
Nenhuma permissão atribuída
</span>
)}
</div>
</div>
</div>
</div>
{/* Endereço e Contato */}
<div className="border border-border rounded-lg p-6">
<h3 className="text-lg font-semibold mb-4">Endereço e Contato</h3>
<div className="space-y-4">
{/* Telefone */}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Telefone
</Label>
{isEditing ? (
<Input
value={editingData.phone || ""}
onChange={(e) => handlePhoneChange(e.target.value)}
className="mt-2"
placeholder="(00) 00000-0000"
maxLength={15}
/>
) : (
<div className="mt-2 p-3 bg-muted rounded text-foreground">
{userInfo.profile?.phone || "Não preenchido"}
</div>
)}
</div>
{/* Endereço */}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Logradouro
</Label>
{isEditing ? (
<Input
value={editingData.street || ""}
onChange={(e) => setEditingData({...editingData, street: e.target.value})}
className="mt-2"
placeholder="Rua, avenida, etc."
/>
) : (
<div className="mt-2 p-3 bg-muted rounded text-foreground">
{userInfo.profile?.street || "Não preenchido"}
</div>
)}
</div>
{/* Número */}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Número
</Label>
{isEditing ? (
<Input
value={editingData.number || ""}
onChange={(e) => setEditingData({...editingData, number: e.target.value})}
className="mt-2"
placeholder="123"
/>
) : (
<div className="mt-2 p-3 bg-muted rounded text-foreground">
{userInfo.profile?.number || "Não preenchido"}
</div>
)}
</div>
{/* Complemento */}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Complemento
</Label>
{isEditing ? (
<Input
value={editingData.complement || ""}
onChange={(e) => setEditingData({...editingData, complement: e.target.value})}
className="mt-2"
placeholder="Apto 42, Bloco B, etc."
/>
) : (
<div className="mt-2 p-3 bg-muted rounded text-foreground">
{userInfo.profile?.complement || "Não preenchido"}
</div>
)}
</div>
{/* Bairro */}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Bairro
</Label>
{isEditing ? (
<Input
value={editingData.neighborhood || ""}
onChange={(e) => setEditingData({...editingData, neighborhood: e.target.value})}
className="mt-2"
placeholder="Vila, bairro, etc."
/>
) : (
<div className="mt-2 p-3 bg-muted rounded text-foreground">
{userInfo.profile?.neighborhood || "Não preenchido"}
</div>
)}
</div>
{/* Cidade */}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Cidade
</Label>
{isEditing ? (
<Input
value={editingData.city || ""}
onChange={(e) => setEditingData({...editingData, city: e.target.value})}
className="mt-2"
placeholder="São Paulo"
/>
) : (
<div className="mt-2 p-3 bg-muted rounded text-foreground">
{userInfo.profile?.city || "Não preenchido"}
</div>
)}
</div>
{/* Estado */}
<div>
<Label className="text-sm font-medium text-muted-foreground">
Estado
</Label>
{isEditing ? (
<Input
value={editingData.state || ""}
onChange={(e) => setEditingData({...editingData, state: e.target.value})}
className="mt-2"
placeholder="SP"
maxLength={2}
/>
) : (
<div className="mt-2 p-3 bg-muted rounded text-foreground">
{userInfo.profile?.state || "Não preenchido"}
</div>
)}
</div>
{/* CEP */}
<div>
<Label className="text-sm font-medium text-muted-foreground">
CEP
</Label>
{isEditing ? (
<div className="space-y-2">
<div className="flex gap-2 items-end">
<div className="flex-1">
<Input
value={editingData.cep || ""}
onChange={(e) => handleCepChange(e.target.value)}
className="mt-2"
placeholder="00000-000"
maxLength={9}
disabled={cepLoading}
/>
</div>
{cepValid === true && (
<CheckCircle className="h-5 w-5 text-green-500 mb-2" />
)}
{cepValid === false && (
<XCircle className="h-5 w-5 text-red-500 mb-2" />
)}
</div>
{cepLoading && (
<p className="text-xs text-muted-foreground">Buscando CEP...</p>
)}
{cepValid === false && (
<p className="text-xs text-red-500">CEP inválido ou não encontrado</p>
)}
{cepValid === true && (
<p className="text-xs text-green-500"> CEP preenchido com sucesso</p>
)}
</div>
) : (
<div className="mt-2 p-3 bg-muted rounded text-foreground">
{userInfo.profile?.cep || "Não preenchido"}
</div>
)}
</div>
</div>
</div>
</div>
{/* Coluna Direita - Foto do Perfil */}
<div>
<div className="border border-border rounded-lg p-6">
<h3 className="text-lg font-semibold mb-4">Foto do Perfil</h3>
{isEditing ? (
<div className="space-y-4">
<UploadAvatar
userId={userInfo.user.id}
currentAvatarUrl={editingData.avatar_url || userInfo.profile?.avatar_url || "/avatars/01.png"}
onAvatarChange={(newUrl) => setEditingData({...editingData, avatar_url: newUrl})}
userName={editingData.full_name || userInfo.profile?.full_name || "Usuário"}
/>
</div>
) : (
<div className="flex flex-col items-center gap-4">
<Avatar className="h-24 w-24">
<AvatarImage
src={userInfo.profile?.avatar_url || "/avatars/01.png"}
alt={userInfo.profile?.full_name || "Usuário"}
/>
<AvatarFallback className="bg-primary text-primary-foreground text-2xl font-bold">
{getInitials(userInfo.profile?.full_name)}
</AvatarFallback>
</Avatar>
<div className="text-center space-y-2">
<p className="text-sm text-muted-foreground">
{getInitials(userInfo.profile?.full_name)}
</p>
</div>
</div>
)}
{/* Informações de Status */}
<div className="mt-6 pt-6 border-t border-border space-y-4">
<div>
<Label className="text-sm font-medium text-muted-foreground">
Status
</Label>
<div className="mt-2">
<Badge
variant={
userInfo.profile?.disabled ? "destructive" : "default"
}
>
{userInfo.profile?.disabled ? "Desabilitado" : "Ativo"}
</Badge>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Botão Voltar */}
<div className="flex gap-3 pb-6">
<Button
variant="outline"
onClick={() => router.back()}
>
<ArrowLeft className="h-4 w-4 mr-2" />
Voltar
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,6 +1,6 @@
"use client";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form";
import HeaderAgenda from "@/components/agenda/HeaderAgenda";
import FooterAgenda from "@/components/agenda/FooterAgenda";
@ -38,6 +38,7 @@ interface FormData {
export default function NovoAgendamentoPage() {
const router = useRouter();
const searchParams = useSearchParams();
const [formData, setFormData] = useState<FormData>({});
const handleFormChange = (data: FormData) => {
@ -87,7 +88,7 @@ export default function NovoAgendamentoPage() {
const handleCancel = () => {
// If origin was provided (eg: consultas), return there. Default to calendar.
try {
const origin = (typeof window !== 'undefined') ? new URLSearchParams(window.location.search).get('origin') : null;
const origin = searchParams?.get?.('origin');
if (origin === 'consultas') {
router.push('/consultas');
return;

View File

@ -17,10 +17,8 @@ import Link from 'next/link'
import ProtectedRoute from '@/components/ProtectedRoute'
import { useAuth } from '@/hooks/useAuth'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, atualizarPaciente, buscarPacientePorId } from '@/lib/api'
import { ENV_CONFIG } from '@/lib/env-config'
import { listarRelatoriosPorPaciente } from '@/lib/reports'
// reports are rendered statically for now
import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarMensagensPorPaciente } from '@/lib/api'
import { useReports } from '@/hooks/useReports'
// Simulação de internacionalização básica
const strings = {
dashboard: 'Dashboard',
@ -55,7 +53,7 @@ const strings = {
export default function PacientePage() {
const { logout, user } = useAuth()
const [tab, setTab] = useState<'dashboard'|'consultas'|'exames'|'perfil'>('dashboard')
const [tab, setTab] = useState<'dashboard'|'consultas'|'exames'|'mensagens'|'perfil'>('dashboard')
// Simulação de loaders, empty states e erro
const [loading, setLoading] = useState(false)
@ -238,142 +236,44 @@ export default function PacientePage() {
const handleProfileChange = (field: string, value: string) => {
setProfileData((prev: any) => ({ ...prev, [field]: value }))
}
const handleSaveProfile = async () => {
if (!patientId) {
setToast({ type: 'error', msg: 'Paciente não identificado. Não foi possível salvar.' })
setIsEditingProfile(false)
return
}
setLoading(true)
try {
const payload: any = {}
if (profileData.email) payload.email = profileData.email
if (profileData.telefone) payload.phone_mobile = profileData.telefone
if (profileData.endereco) payload.street = profileData.endereco
if (profileData.cidade) payload.city = profileData.cidade
if (profileData.cep) payload.cep = profileData.cep
if (profileData.biografia) payload.notes = profileData.biografia
await atualizarPaciente(String(patientId), payload)
// refresh patient row
const refreshed = await buscarPacientePorId(String(patientId)).catch(() => null)
if (refreshed) {
const getFirst = (obj: any, keys: string[]) => {
if (!obj) return undefined
for (const k of keys) {
const v = obj[k]
if (v !== undefined && v !== null && String(v).trim() !== '') return String(v)
}
return undefined
}
const nome = getFirst(refreshed, ['full_name','fullName','name','nome','social_name']) || profileData.nome
const telefone = getFirst(refreshed, ['phone_mobile','phone','telefone','mobile']) || profileData.telefone
const rua = getFirst(refreshed, ['street','logradouro','endereco','address'])
const numero = getFirst(refreshed, ['number','numero'])
const bairro = getFirst(refreshed, ['neighborhood','bairro'])
const endereco = rua ? (numero ? `${rua}, ${numero}` : rua) + (bairro ? ` - ${bairro}` : '') : profileData.endereco
const cidade = getFirst(refreshed, ['city','cidade','localidade']) || profileData.cidade
const cep = getFirst(refreshed, ['cep','postal_code','zip']) || profileData.cep
const biografia = getFirst(refreshed, ['biography','bio','notes']) || profileData.biografia || ''
const emailFromRow = getFirst(refreshed, ['email']) || profileData.email
const foto = getFirst(refreshed, ['foto_url','avatar_url','fotoUrl']) || profileData.foto_url
setProfileData((prev:any) => ({ ...prev, nome, email: emailFromRow, telefone, endereco, cidade, cep, biografia, foto_url: foto }))
}
setIsEditingProfile(false)
setToast({ type: 'success', msg: strings.sucesso })
} catch (err: any) {
console.warn('[PacientePage] erro ao atualizar paciente', err)
setToast({ type: 'error', msg: err?.message || strings.erroSalvar })
} finally {
setLoading(false)
}
const handleSaveProfile = () => {
setIsEditingProfile(false)
setToast({ type: 'success', msg: strings.sucesso })
}
const handleCancelEdit = () => {
setIsEditingProfile(false)
}
function DashboardCards() {
const [nextAppt, setNextAppt] = useState<string | null>(null)
const [examsCount, setExamsCount] = useState<number | null>(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
let mounted = true
async function load() {
if (!patientId) {
setNextAppt(null)
setExamsCount(null)
return
}
setLoading(true)
try {
// Load appointments for this patient (upcoming)
const q = `patient_id=eq.${encodeURIComponent(String(patientId))}&order=scheduled_at.asc&limit=200`
const ags = await listarAgendamentos(q).catch(() => [])
if (!mounted) return
const now = Date.now()
// find the first appointment with scheduled_at >= now
const upcoming = (ags || []).map((a: any) => ({ ...a, _sched: a.scheduled_at ? new Date(a.scheduled_at).getTime() : null }))
.filter((a: any) => a._sched && a._sched >= now)
.sort((x: any, y: any) => Number(x._sched) - Number(y._sched))
if (upcoming && upcoming.length) {
setNextAppt(new Date(upcoming[0]._sched).toLocaleDateString('pt-BR'))
} else {
setNextAppt(null)
}
// Load reports/laudos count
const reports = await listarRelatoriosPorPaciente(String(patientId)).catch(() => [])
if (!mounted) return
setExamsCount(Array.isArray(reports) ? reports.length : 0)
} catch (e) {
console.warn('[DashboardCards] erro ao carregar dados', e)
if (!mounted) return
setNextAppt(null)
setExamsCount(null)
} finally {
if (mounted) setLoading(false)
}
}
load()
return () => { mounted = false }
}, [patientId])
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<Card className="flex flex-col items-center justify-center p-4">
<Calendar className="mb-2 text-primary" aria-hidden />
<span className="font-semibold">{strings.proximaConsulta}</span>
<span className="text-2xl">{loading ? '...' : (nextAppt ?? '-')}</span>
<span className="text-2xl">12/10/2025</span>
</Card>
<Card className="flex flex-col items-center justify-center p-4">
<FileText className="mb-2 text-primary" aria-hidden />
<span className="font-semibold">{strings.ultimosExames}</span>
<span className="text-2xl">{loading ? '...' : (examsCount !== null ? String(examsCount) : '-')}</span>
<span className="text-2xl">2</span>
</Card>
</div>
<Card className="flex flex-col items-center justify-center p-4">
<MessageCircle className="mb-2 text-primary" aria-hidden />
<span className="font-semibold">{strings.mensagensNaoLidas}</span>
<span className="text-2xl">1</span>
</Card>
</div>
)
}
// Consultas fictícias
const [currentDate, setCurrentDate] = useState(new Date())
// helper: produce a local YYYY-MM-DD key (uses local timezone, not toISOString UTC)
const localDateKey = (d: Date) => {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
const consultasFicticias = [
{
id: 1,
medico: "Dr. Carlos Andrade",
especialidade: "Cardiologia",
local: "Clínica Coração Feliz",
data: localDateKey(new Date()),
data: new Date().toISOString().split('T')[0],
hora: "09:00",
status: "Confirmada"
},
@ -382,7 +282,7 @@ export default function PacientePage() {
medico: "Dra. Fernanda Lima",
especialidade: "Dermatologia",
local: "Clínica Pele Viva",
data: localDateKey(new Date()),
data: new Date().toISOString().split('T')[0],
hora: "14:30",
status: "Pendente"
},
@ -391,7 +291,7 @@ export default function PacientePage() {
medico: "Dr. João Silva",
especialidade: "Ortopedia",
local: "Hospital Ortopédico",
data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return localDateKey(d) })(),
data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return d.toISOString().split('T')[0] })(),
hora: "11:00",
status: "Cancelada"
},
@ -410,7 +310,7 @@ export default function PacientePage() {
setCurrentDate(new Date());
}
const todayStr = localDateKey(currentDate)
const todayStr = currentDate.toISOString().split('T')[0];
const consultasDoDia = consultasFicticias.filter(c => c.data === todayStr);
function Consultas() {
@ -423,132 +323,19 @@ export default function PacientePage() {
const activeToggleClass = "w-full transition duration-200 focus-visible:ring-2 focus-visible:ring-[#2563eb]/60 active:scale-[0.97] bg-[#2563eb] text-white hover:bg-[#2563eb] hover:text-white"
const inactiveToggleClass = "w-full transition duration-200 bg-slate-50 text-[#2563eb] border border-[#2563eb]/30 hover:bg-slate-100 hover:text-[#2563eb] dark:bg-white/5 dark:text-white dark:hover:bg-white/10 dark:border-white/20"
const hoverPrimaryIconClass = "rounded-xl bg-white text-[#1e293b] border border-black/10 shadow-[0_2px_8px_rgba(0,0,0,0.03)] transition duration-200 hover:bg-[#2563eb] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563eb] dark:bg-slate-800 dark:text-slate-100 dark:border-white/10 dark:shadow-none dark:hover:bg-[#2563eb] dark:hover:text-white"
const today = new Date(); today.setHours(0, 0, 0, 0);
const selectedDate = new Date(currentDate); selectedDate.setHours(0, 0, 0, 0);
const isSelectedDateToday = selectedDate.getTime() === today.getTime()
const today = new Date(); today.setHours(0, 0, 0, 0);
const selectedDate = new Date(currentDate); selectedDate.setHours(0, 0, 0, 0);
const isSelectedDateToday = selectedDate.getTime() === today.getTime()
// Appointments state (loaded when "Ver consultas agendadas" is opened)
const [appointments, setAppointments] = useState<any[] | null>(null)
const [loadingAppointments, setLoadingAppointments] = useState(false)
const [appointmentsError, setAppointmentsError] = useState<string | null>(null)
useEffect(() => {
let mounted = true
if (!mostrarAgendadas) return
if (!patientId) {
setAppointmentsError('Paciente não identificado. Faça login novamente.')
return
}
async function loadAppointments() {
try {
setLoadingAppointments(true)
setAppointmentsError(null)
setAppointments(null)
// Try `eq.` first, then fallback to `in.(id)` which some views expect
const baseEncoded = encodeURIComponent(String(patientId))
const queriesToTry = [
`patient_id=eq.${baseEncoded}&order=scheduled_at.asc&limit=200`,
`patient_id=in.(${baseEncoded})&order=scheduled_at.asc&limit=200`,
];
let rows: any[] = []
for (const q of queriesToTry) {
try {
// Debug: also fetch raw response to inspect headers/response body in the browser
try {
const token = (typeof window !== 'undefined') ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null
const headers: Record<string,string> = {
apikey: ENV_CONFIG.SUPABASE_ANON_KEY,
Accept: 'application/json',
}
if (token) headers.Authorization = `Bearer ${token}`
const rawUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/appointments?${q}`
console.debug('[Consultas][debug] GET', rawUrl, 'Headers(masked):', { ...headers, Authorization: headers.Authorization ? `${String(headers.Authorization).slice(0,6)}...${String(headers.Authorization).slice(-6)}` : undefined })
const rawRes = await fetch(rawUrl, { method: 'GET', headers })
const rawText = await rawRes.clone().text().catch(() => '')
console.debug('[Consultas][debug] raw response', { url: rawUrl, status: rawRes.status, bodyPreview: (typeof rawText === 'string' && rawText.length > 0) ? rawText.slice(0, 200) : rawText })
} catch (dbgErr) {
console.debug('[Consultas][debug] não foi possível capturar raw response', dbgErr)
}
const r = await listarAgendamentos(q)
if (r && Array.isArray(r) && r.length) {
rows = r
break
}
// if r is empty array, continue to next query format
} catch (e) {
// keep trying next format
console.debug('[Consultas] tentativa listarAgendamentos falhou para query', q, e)
}
}
if (!mounted) return
if (!rows || rows.length === 0) {
// no appointments found for this patient using either filter
setAppointments([])
return
}
const doctorIds = Array.from(new Set(rows.map((r: any) => r.doctor_id).filter(Boolean)))
const doctorsMap: Record<string, any> = {}
if (doctorIds.length) {
try {
const docs = await buscarMedicosPorIds(doctorIds).catch(() => [])
for (const d of docs || []) doctorsMap[d.id] = d
} catch (e) {
// ignore
}
}
const mapped = (rows || []).map((a: any) => {
const sched = a.scheduled_at ? new Date(a.scheduled_at) : null
const doc = a.doctor_id ? doctorsMap[String(a.doctor_id)] : null
return {
id: a.id,
medico: doc?.full_name || a.doctor_id || '---',
especialidade: doc?.specialty || '',
local: a.location || a.place || '',
data: sched ? localDateKey(sched) : '',
hora: sched ? sched.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '',
status: a.status ? String(a.status) : 'Pendente',
}
})
setAppointments(mapped)
} catch (err: any) {
console.warn('[Consultas] falha ao carregar agendamentos', err)
if (!mounted) return
setAppointmentsError(err?.message ?? 'Falha ao carregar agendamentos.')
setAppointments([])
} finally {
if (mounted) setLoadingAppointments(false)
}
}
loadAppointments()
return () => { mounted = false }
}, [mostrarAgendadas, patientId])
// Monta a URL de resultados com os filtros atuais
const buildResultadosHref = () => {
const qs = new URLSearchParams()
qs.set('tipo', tipoConsulta) // 'teleconsulta' | 'presencial'
if (especialidade) qs.set('especialidade', especialidade)
if (localizacao) qs.set('local', localizacao)
// indicate navigation origin so destination can alter UX (e.g., show modal instead of redirect)
qs.set('origin', 'paciente')
return `/resultados?${qs.toString()}`
const handlePesquisar = () => {
const params = new URLSearchParams({
tipo: tipoConsulta,
especialidade,
local: localizacao
})
router.push(`/resultados?${params.toString()}`)
}
// derived lists for the "Ver consultas agendadas" dialog (computed after appointments state is declared)
const _dialogSource = (appointments !== null ? appointments : consultasFicticias)
const _todaysAppointments = (_dialogSource || []).filter((c: any) => c.data === todayStr)
return (
<section className="bg-card shadow-md rounded-lg border border-border p-6">
<div className="max-w-3xl mx-auto space-y-8">
@ -611,11 +398,11 @@ export default function PacientePage() {
</div>
</div>
{/* Botão agora redireciona direto para /resultados */}
<Button asChild className={`w-full md:w-auto md:self-start ${hoverPrimaryClass}`}>
<Link href={buildResultadosHref()} prefetch={false}>
Pesquisar
</Link>
<Button
className={`w-full md:w-auto md:self-start ${hoverPrimaryClass}`}
onClick={handlePesquisar}
>
Pesquisar
</Button>
</div>
@ -632,7 +419,7 @@ export default function PacientePage() {
</div>
<Dialog open={mostrarAgendadas} onOpenChange={open => setMostrarAgendadas(open)}>
<DialogContent className="max-w-3xl space-y-6 sm:max-h-[85vh] overflow-hidden flex flex-col">
<DialogContent className="max-w-3xl space-y-6 sm:max-h-[85vh] overflow-hidden">
<DialogHeader>
<DialogTitle className="text-2xl font-semibold text-foreground">Consultas agendadas</DialogTitle>
<DialogDescription>Gerencie suas consultas confirmadas, pendentes ou canceladas.</DialogDescription>
@ -644,7 +431,7 @@ export default function PacientePage() {
type="button"
variant="outline"
size="icon"
onClick={(e: any) => { e.stopPropagation(); e.preventDefault(); navigateDate('prev') }}
onClick={() => navigateDate('prev')}
aria-label="Dia anterior"
className={`group shadow-sm ${hoverPrimaryIconClass}`}
>
@ -655,7 +442,7 @@ export default function PacientePage() {
type="button"
variant="outline"
size="icon"
onClick={(e: any) => { e.stopPropagation(); e.preventDefault(); navigateDate('next') }}
onClick={() => navigateDate('next')}
aria-label="Próximo dia"
className={`group shadow-sm ${hoverPrimaryIconClass}`}
>
@ -675,95 +462,84 @@ export default function PacientePage() {
)}
</div>
<div className="text-sm text-muted-foreground">
{`${_todaysAppointments.length} consulta${_todaysAppointments.length !== 1 ? 's' : ''} agendada${_todaysAppointments.length !== 1 ? 's' : ''}`}
{consultasDoDia.length} consulta{consultasDoDia.length !== 1 ? 's' : ''} agendada{consultasDoDia.length !== 1 ? 's' : ''}
</div>
</div>
<div className="flex-1 flex flex-col gap-4 overflow-y-auto pr-1 sm:pr-2 pb-6">
{loadingAppointments && mostrarAgendadas ? (
<div className="text-center py-10 text-muted-foreground">Carregando consultas...</div>
) : appointmentsError ? (
<div className="text-center py-10 text-red-600">{appointmentsError}</div>
<div className="flex flex-col gap-4 overflow-y-auto max-h-[70vh] pr-1 sm:pr-2">
{consultasDoDia.length === 0 ? (
<div className="text-center py-10 text-muted-foreground">
<Calendar className="h-12 w-12 mx-auto mb-4 opacity-60" />
<p className="text-lg font-medium">Nenhuma consulta agendada para este dia</p>
<p className="text-sm">Use a busca para marcar uma nova consulta.</p>
</div>
) : (
// prefer appointments (client-loaded) when present; fallback to fictitious list
(() => {
const todays = _todaysAppointments
if (!todays || todays.length === 0) {
return (
<div className="text-center py-10 text-muted-foreground">
<Calendar className="h-12 w-12 mx-auto mb-4 opacity-60" />
<p className="text-lg font-medium">Nenhuma consulta agendada para este dia</p>
<p className="text-sm">Use a busca para marcar uma nova consulta.</p>
</div>
)
}
return todays.map((consulta: any) => (
<div
key={consulta.id}
className="rounded-xl border border-black/5 dark:border-white/10 bg-card shadow-[0_4px_12px_rgba(0,0,0,0.05)] dark:shadow-none p-5"
>
<div className="grid gap-4 md:grid-cols-[minmax(0,2fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.4fr)] items-start">
<div className="flex items-start gap-3">
<span
className="mt-1 h-3 w-3 flex-shrink-0 rounded-full"
style={{ backgroundColor: consulta.status === 'Confirmada' ? '#22c55e' : consulta.status === 'Pendente' ? '#fbbf24' : '#ef4444' }}
/>
<div className="space-y-1">
<div className="font-medium flex items-center gap-2 text-foreground">
<Stethoscope className="h-4 w-4 text-muted-foreground" />
{consulta.medico}
</div>
<p className="text-sm text-muted-foreground break-words">
{consulta.especialidade} {consulta.local}
</p>
consultasDoDia.map(consulta => (
<div
key={consulta.id}
className="rounded-xl border border-black/5 dark:border-white/10 bg-card shadow-[0_4px_12px_rgba(0,0,0,0.05)] dark:shadow-none p-5"
>
<div className="grid gap-4 md:grid-cols-[minmax(0,2fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.4fr)] items-start">
<div className="flex items-start gap-3">
<span
className="mt-1 h-3 w-3 flex-shrink-0 rounded-full"
style={{ backgroundColor: consulta.status === 'Confirmada' ? '#22c55e' : consulta.status === 'Pendente' ? '#fbbf24' : '#ef4444' }}
/>
<div className="space-y-1">
<div className="font-medium flex items-center gap-2 text-foreground">
<Stethoscope className="h-4 w-4 text-muted-foreground" />
{consulta.medico}
</div>
<p className="text-sm text-muted-foreground break-words">
{consulta.especialidade} {consulta.local}
</p>
</div>
</div>
<div className="flex items-center gap-2 text-foreground">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{consulta.hora}</span>
</div>
<div className="flex items-center gap-2 text-foreground">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{consulta.hora}</span>
</div>
<div className="flex items-center">
<span className={`px-3 py-1 rounded-full text-sm font-medium text-white ${consulta.status === 'Confirmada' ? 'bg-green-600' : consulta.status === 'Pendente' ? 'bg-yellow-500' : 'bg-red-600'}`}>
{consulta.status}
</span>
</div>
<div className="flex items-center">
<span className={`px-3 py-1 rounded-full text-sm font-medium text-white ${consulta.status === 'Confirmada' ? 'bg-green-600' : consulta.status === 'Pendente' ? 'bg-yellow-500' : 'bg-red-600'}`}>
{consulta.status}
</span>
</div>
<div className="flex flex-wrap items-center justify-end gap-2">
<div className="flex flex-wrap items-center justify-end gap-2">
<Button
type="button"
variant="outline"
size="sm"
className="border border-[#2563eb]/40 text-[#2563eb] hover:bg-transparent hover:text-[#2563eb] focus-visible:ring-2 focus-visible:ring-[#2563eb]/40 active:scale-[0.97]"
>
Detalhes
</Button>
{consulta.status !== 'Cancelada' && (
<Button type="button" variant="secondary" size="sm" className={hoverPrimaryClass}>
Reagendar
</Button>
)}
{consulta.status !== 'Cancelada' && (
<Button
type="button"
variant="outline"
variant="destructive"
size="sm"
className="border border-[#2563eb]/40 text-[#2563eb] hover:bg-transparent hover:text-[#2563eb] focus-visible:ring-2 focus-visible:ring-[#2563eb]/40 active:scale-[0.97]"
className="transition duration-200 hover:bg-[#dc2626] focus-visible:ring-2 focus-visible:ring-[#dc2626]/60 active:scale-[0.97]"
>
Detalhes
Cancelar
</Button>
{consulta.status !== 'Cancelada' && (
<Button type="button" variant="secondary" size="sm" className={hoverPrimaryClass}>
Reagendar
</Button>
)}
{consulta.status !== 'Cancelada' && (
<Button
type="button"
variant="destructive"
size="sm"
className="transition duration-200 hover:bg-[#dc2626] focus-visible:ring-2 focus-visible:ring-[#dc2626]/60 active:scale-[0.97]"
>
Cancelar
</Button>
)}
</div>
)}
</div>
</div>
))
})()
</div>
))
)}
</div>
<DialogFooter className="justify-center border-t border-border pt-4 mt-2">
<Button variant="outline" onClick={() => { setMostrarAgendadas(false) }} className="w-full sm:w-auto">
<Button variant="outline" onClick={() => setMostrarAgendadas(false)} className="w-full sm:w-auto">
Fechar
</Button>
</DialogFooter>
@ -773,164 +549,120 @@ export default function PacientePage() {
)
}
// Selected report state
// Reports (laudos) hook
const { reports, loadReportsByPatient, loading: reportsLoading } = useReports()
const [selectedReport, setSelectedReport] = useState<any | null>(null)
function ExamesLaudos() {
const [reports, setReports] = useState<any[] | null>(null)
const [loadingReports, setLoadingReports] = useState(false)
const [reportsError, setReportsError] = useState<string | null>(null)
const [reportDoctorName, setReportDoctorName] = useState<string | null>(null)
useEffect(() => {
let mounted = true
if (!patientId) return
setLoadingReports(true)
setReportsError(null)
listarRelatoriosPorPaciente(String(patientId))
.then(res => {
if (!mounted) return
setReports(Array.isArray(res) ? res : [])
})
.catch(err => {
console.warn('[ExamesLaudos] erro ao carregar laudos', err)
if (!mounted) return
setReportsError('Falha ao carregar laudos.')
})
.finally(() => { if (mounted) setLoadingReports(false) })
return () => { mounted = false }
// load laudos for this patient
loadReportsByPatient(patientId).catch(() => {})
}, [patientId])
// When a report is selected, try to fetch doctor name if we have an id
useEffect(() => {
let mounted = true
if (!selectedReport) {
setReportDoctorName(null)
return
}
const maybeDoctorId = selectedReport.doctor_id || selectedReport.created_by || null
if (!maybeDoctorId) {
setReportDoctorName(null)
return
}
(async () => {
try {
const docs = await buscarMedicosPorIds([String(maybeDoctorId)]).catch(() => [])
if (!mounted) return
if (docs && docs.length) {
const doc0: any = docs[0]
setReportDoctorName(doc0.full_name || doc0.name || doc0.fullName || null)
}
} catch (e) {
// ignore
}
})()
return () => { mounted = false }
}, [selectedReport])
return (
<section className="bg-card shadow-md rounded-lg border border-border p-6">
<h2 className="text-2xl font-bold mb-6">Laudos</h2>
<div className="space-y-3">
{loadingReports ? (
<div className="text-center py-8 text-muted-foreground">{strings.carregando}</div>
) : reportsError ? (
<div className="text-center py-8 text-red-600">{reportsError}</div>
) : (!reports || reports.length === 0) ? (
<div className="text-center py-8 text-muted-foreground">Nenhum laudo encontrado para este paciente.</div>
) : (
reports.map((r) => (
<div key={r.id || JSON.stringify(r)} className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4">
{reportsLoading ? (
<div className="text-center py-8 text-muted-foreground">Carregando laudos...</div>
) : (!reports || reports.length === 0) ? (
<div className="text-center py-8 text-muted-foreground">Nenhum laudo salvo.</div>
) : (
<div className="space-y-3">
{reports.map((r: any) => (
<div key={r.id || r.order_number || JSON.stringify(r)} className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4">
<div>
<div className="font-medium text-foreground">{r.title || r.name || r.report_name || 'Laudo'}</div>
<div className="text-sm text-muted-foreground">Data: {new Date(r.report_date || r.created_at || Date.now()).toLocaleDateString('pt-BR')}</div>
<div className="font-medium text-foreground">{r.title || r.report_type || r.exame || r.name || 'Laudo'}</div>
<div className="text-sm text-muted-foreground">Data: {new Date(r.report_date || r.data || r.created_at || Date.now()).toLocaleDateString('pt-BR')}</div>
</div>
<div className="flex gap-2 mt-2 md:mt-0">
<Button variant="outline" onClick={async () => { setSelectedReport(r); }}>{strings.visualizarLaudo}</Button>
<Button variant="secondary" onClick={async () => { try { await navigator.clipboard.writeText(JSON.stringify(r)); setToast({ type: 'success', msg: 'Laudo copiado.' }) } catch { setToast({ type: 'error', msg: 'Falha ao copiar.' }) } }}>{strings.compartilhar}</Button>
<Button variant="outline" onClick={async () => { setSelectedReport(r); }}>Visualizar</Button>
<Button variant="secondary" onClick={async () => { try { await navigator.clipboard.writeText(JSON.stringify(r)); setToast({ type: 'success', msg: 'Laudo copiado (debug).' }) } catch { setToast({ type: 'error', msg: 'Falha ao copiar.' }) } }}>Compartilhar</Button>
</div>
</div>
))
)}
</div>
))}
</div>
)}
<Dialog open={!!selectedReport} onOpenChange={open => !open && setSelectedReport(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Laudo Médico</DialogTitle>
<DialogDescription>
{selectedReport && (
<>
<div className="mb-2">
<div className="font-semibold text-lg">{selectedReport.title || selectedReport.name || 'Laudo'}</div>
<div className="text-sm text-muted-foreground">Data: {new Date(selectedReport.report_date || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}</div>
{reportDoctorName && <div className="text-sm text-muted-foreground">Profissional: <strong className="text-foreground">{reportDoctorName}</strong></div>}
</div>
{/* Standardized laudo sections: CID, Exame, Diagnóstico, Conclusão, Notas (prefer HTML when available) */}
{(() => {
const cid = selectedReport.cid ?? selectedReport.cid_code ?? selectedReport.cidCode ?? selectedReport.cie ?? '-'
const exam = selectedReport.exam ?? selectedReport.exame ?? selectedReport.especialidade ?? selectedReport.report_type ?? '-'
const diagnosis = selectedReport.diagnosis ?? selectedReport.diagnostico ?? selectedReport.diagnosis_text ?? selectedReport.diagnostico_text ?? ''
const conclusion = selectedReport.conclusion ?? selectedReport.conclusao ?? selectedReport.conclusion_text ?? selectedReport.conclusao_text ?? ''
const notesHtml = selectedReport.content_html ?? selectedReport.conteudo_html ?? selectedReport.contentHtml ?? null
const notesText = selectedReport.content ?? selectedReport.body ?? selectedReport.conteudo ?? selectedReport.notes ?? selectedReport.observacoes ?? ''
return (
<div className="space-y-3 mb-4">
<div>
<div className="text-xs text-muted-foreground">CID</div>
<div className="text-foreground">{cid || '-'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Exame</div>
<div className="text-foreground">{exam || '-'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Diagnóstico</div>
<div className="whitespace-pre-line text-foreground">{diagnosis || '-'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Conclusão</div>
<div className="whitespace-pre-line text-foreground">{conclusion || '-'}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Notas do Profissional</div>
{notesHtml ? (
<div className="prose max-w-none p-2 bg-muted rounded" dangerouslySetInnerHTML={{ __html: String(notesHtml) }} />
) : (
<div className="whitespace-pre-line text-foreground p-2 bg-muted rounded">{notesText || '-'}</div>
)}
</div>
</div>
)
})()}
{/* Optional: doctor signature or footer */}
{selectedReport.doctor_signature && (
<div className="mt-4 text-sm text-muted-foreground">Assinatura: <img src={selectedReport.doctor_signature} alt="assinatura" className="inline-block h-10" /></div>
)}
</>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setSelectedReport(null)}>Fechar</Button>
</DialogFooter>
</DialogContent>
<DialogHeader>
<DialogTitle>Laudo Médico</DialogTitle>
<DialogDescription>
{selectedReport && (
<>
<div className="font-semibold mb-2">{selectedReport.title || selectedReport.report_type || selectedReport.exame || 'Laudo'}</div>
<div className="text-sm text-muted-foreground mb-4">Data: {new Date(selectedReport.report_date || selectedReport.data || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}</div>
<div className="mb-4 whitespace-pre-line">{selectedReport.content || selectedReport.laudo || selectedReport.body || JSON.stringify(selectedReport, null, 2)}</div>
</>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setSelectedReport(null)}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</section>
)
}
function Mensagens() {
const [msgs, setMsgs] = useState<any[]>([])
const [loadingMsgs, setLoadingMsgs] = useState(false)
const [msgsError, setMsgsError] = useState<string | null>(null)
useEffect(() => {
let mounted = true
if (!patientId) return
setLoadingMsgs(true)
setMsgsError(null)
listarMensagensPorPaciente(String(patientId))
.then(res => {
if (!mounted) return
setMsgs(Array.isArray(res) ? res : [])
})
.catch(err => {
console.warn('[Mensagens] erro ao carregar mensagens', err)
if (!mounted) return
setMsgsError('Falha ao carregar mensagens.')
})
.finally(() => { if (mounted) setLoadingMsgs(false) })
return () => { mounted = false }
}, [patientId])
return (
<section className="bg-card shadow-md rounded-lg border border-border p-6">
<h2 className="text-2xl font-bold mb-6">Mensagens Recebidas</h2>
<div className="space-y-3">
{loadingMsgs ? (
<div className="text-center py-8 text-muted-foreground">Carregando mensagens...</div>
) : msgsError ? (
<div className="text-center py-8 text-red-600">{msgsError}</div>
) : (!msgs || msgs.length === 0) ? (
<div className="text-center py-8 text-muted-foreground">Nenhuma mensagem encontrada.</div>
) : (
msgs.map((msg: any) => (
<div key={msg.id || JSON.stringify(msg)} className="bg-muted rounded p-4">
<div className="font-medium text-foreground flex items-center gap-2">
<User className="h-4 w-4 text-primary" />
{msg.sender_name || msg.from || msg.doctor_name || 'Remetente'}
{!msg.read && <span className="ml-2 px-2 py-0.5 rounded-full text-xs bg-primary text-white">Nova</span>}
</div>
<div className="text-sm text-muted-foreground mb-2">{new Date(msg.created_at || msg.data || Date.now()).toLocaleString('pt-BR')}</div>
<div className="text-foreground whitespace-pre-line">{msg.body || msg.content || msg.text || JSON.stringify(msg)}</div>
</div>
))
)}
</div>
</section>
)
}
function Perfil() {
const hasAddress = Boolean(profileData.endereco || profileData.cidade || profileData.cep)
const hasAddress = Boolean(profileData.endereco || profileData.cidade || profileData.cep || profileData.biografia)
return (
<div className="space-y-6 max-w-2xl mx-auto">
<div className="flex items-center justify-between">
@ -1000,7 +732,14 @@ export default function PacientePage() {
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.cep}</p>
)}
</div>
{/* Biografia removed: not used */}
<div className="space-y-2">
<Label htmlFor="biografia">Biografia</Label>
{isEditingProfile ? (
<Textarea id="biografia" value={profileData.biografia} onChange={e => handleProfileChange('biografia', e.target.value)} rows={4} placeholder="Conte um pouco sobre você..." />
) : (
<p className="p-2 bg-muted/50 rounded min-h-[100px] text-foreground">{profileData.biografia}</p>
)}
</div>
</div>
)}
</div>
@ -1045,7 +784,7 @@ export default function PacientePage() {
<Button variant={tab==='dashboard'?'secondary':'ghost'} aria-current={tab==='dashboard'} onClick={()=>setTab('dashboard')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.dashboard}</Button>
<Button variant={tab==='consultas'?'secondary':'ghost'} aria-current={tab==='consultas'} onClick={()=>setTab('consultas')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.consultas}</Button>
<Button variant={tab==='exames'?'secondary':'ghost'} aria-current={tab==='exames'} onClick={()=>setTab('exames')} className="justify-start"><FileText className="mr-2 h-5 w-5" />{strings.exames}</Button>
<Button variant={tab==='mensagens'?'secondary':'ghost'} aria-current={tab==='mensagens'} onClick={()=>setTab('mensagens')} className="justify-start"><MessageCircle className="mr-2 h-5 w-5" />{strings.mensagens}</Button>
<Button variant={tab==='perfil'?'secondary':'ghost'} aria-current={tab==='perfil'} onClick={()=>setTab('perfil')} className="justify-start"><UserCog className="mr-2 h-5 w-5" />{strings.perfil}</Button>
</nav>
{/* Conteúdo principal */}
@ -1065,7 +804,7 @@ export default function PacientePage() {
{tab==='dashboard' && <DashboardCards />}
{tab==='consultas' && <Consultas />}
{tab==='exames' && <ExamesLaudos />}
{tab==='mensagens' && <Mensagens />}
{tab==='perfil' && <Perfil />}
</main>
)}

View File

@ -11,7 +11,6 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select";
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
import {
Table,
@ -21,7 +20,13 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
import { User, FolderOpen, X, Users, MessageSquare, ClipboardList, Plus, Edit, Trash2, ChevronLeft, ChevronRight, Clock, FileCheck, Upload, Download, Eye, History, Stethoscope, Pill, Activity, Search } from "lucide-react"
import { Calendar as CalendarIcon, FileText, Settings } from "lucide-react";
@ -34,7 +39,6 @@ import {
import dynamic from "next/dynamic";
import { ENV_CONFIG } from '@/lib/env-config';
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
@ -78,20 +82,6 @@ const colorsByType = {
return p?.idade ?? p?.age ?? '';
};
// Normaliza número de telefone para E.164 básico (prioriza +55 quando aplicável)
const normalizePhoneNumber = (raw?: string) => {
if (!raw || typeof raw !== 'string') return '';
// Remover tudo que não for dígito
const digits = raw.replace(/\D+/g, '');
if (!digits) return '';
// Já tem código de país (começa com 55)
if (digits.startsWith('55') && digits.length >= 11) return '+' + digits;
// Se tiver 10 ou 11 dígitos (DDD + número), assume Brasil e prefixa +55
if (digits.length === 10 || digits.length === 11) return '+55' + digits;
// Se tiver outros formatos pequenos, apenas prefixa +
return '+' + digits;
};
// Helpers para normalizar campos do laudo/relatório
const getReportPatientName = (r: any) => r?.paciente?.full_name ?? r?.paciente?.nome ?? r?.patient?.full_name ?? r?.patient?.nome ?? r?.patient_name ?? r?.patient_full_name ?? '';
const getReportPatientId = (r: any) => r?.paciente?.id ?? r?.patient?.id ?? r?.patient_id ?? r?.patientId ?? r?.patient_id_raw ?? r?.patient_id ?? r?.id ?? '';
@ -111,7 +101,7 @@ const colorsByType = {
};
const ProfissionalPage = () => {
const { logout, user, token } = useAuth();
const { logout, user } = useAuth();
const [activeSection, setActiveSection] = useState('calendario');
const [pacienteSelecionado, setPacienteSelecionado] = useState<any>(null);
@ -384,98 +374,10 @@ const ProfissionalPage = () => {
const [selectedEvent, setSelectedEvent] = useState<any>(null);
const [currentCalendarDate, setCurrentCalendarDate] = useState(new Date());
const [commPhoneNumber, setCommPhoneNumber] = useState('');
const [commMessage, setCommMessage] = useState('');
const [commPatientId, setCommPatientId] = useState<string | null>(null);
const [smsSending, setSmsSending] = useState(false);
const handleSave = async (event: React.MouseEvent<HTMLButtonElement>) => {
const handleSave = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
setSmsSending(true);
try {
// Validate required fields
if (!commPhoneNumber || !commPhoneNumber.trim()) throw new Error('O campo phone_number é obrigatório');
if (!commMessage || !commMessage.trim()) throw new Error('O campo message é obrigatório');
const payload: any = { phone_number: commPhoneNumber.trim(), message: commMessage.trim() };
if (commPatientId) payload.patient_id = commPatientId;
const headers: Record<string,string> = { 'Content-Type': 'application/json' };
// include any default headers from ENV_CONFIG if present (e.g. apikey)
if ((ENV_CONFIG as any)?.DEFAULT_HEADERS) Object.assign(headers, (ENV_CONFIG as any).DEFAULT_HEADERS);
// include Authorization if we have a token (user session)
if (token) headers['Authorization'] = `Bearer ${token}`;
// Ensure apikey is present (frontend only has ANON key in this project)
if (!headers.apikey && (ENV_CONFIG as any)?.SUPABASE_ANON_KEY) {
headers.apikey = (ENV_CONFIG as any).SUPABASE_ANON_KEY;
}
// Ensure Accept header
headers['Accept'] = 'application/json';
// Normalizar número antes de enviar (E.164 básico)
const normalized = normalizePhoneNumber(commPhoneNumber);
if (!normalized) throw new Error('Número inválido após normalização');
payload.phone_number = normalized;
// Debug: log payload and headers with secrets masked to help diagnose issues
try {
const masked = { ...headers } as Record<string, any>;
if (masked.apikey && typeof masked.apikey === 'string') masked.apikey = `${masked.apikey.slice(0,4)}...${masked.apikey.slice(-4)}`;
if (masked.Authorization) masked.Authorization = 'Bearer <<token-present>>';
console.debug('[ProfissionalPage] Enviando SMS -> url:', `${(ENV_CONFIG as any).SUPABASE_URL}/functions/v1/send-sms`, 'payload:', payload, 'headers(masked):', masked);
} catch (e) {
// ignore logging errors
}
const res = await fetch(`${(ENV_CONFIG as any).SUPABASE_URL}/functions/v1/send-sms`, {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
const body = await res.json().catch(() => null);
if (!res.ok) {
// If server returned 5xx and we sent a patient_id, try a single retry without patient_id
if (res.status >= 500 && payload.patient_id) {
try {
const fallback = { phone_number: payload.phone_number, message: payload.message };
console.debug('[ProfissionalPage] 5xx ao enviar com patient_id — tentando reenviar sem patient_id', { fallback });
const retryRes = await fetch(`${(ENV_CONFIG as any).SUPABASE_URL}/functions/v1/send-sms`, {
method: 'POST',
headers,
body: JSON.stringify(fallback),
});
const retryBody = await retryRes.json().catch(() => null);
if (retryRes.ok) {
alert('SMS enviado com sucesso (sem patient_id)');
setCommPhoneNumber('');
setCommMessage('');
setCommPatientId(null);
return;
} else {
throw new Error(retryBody?.message || retryBody?.error || `Erro ao enviar SMS (retry ${retryRes.status})`);
}
} catch (retryErr) {
console.warn('[ProfissionalPage] Reenvio sem patient_id falhou', retryErr);
throw new Error(body?.message || body?.error || `Erro ao enviar SMS (${res.status})`);
}
}
throw new Error(body?.message || body?.error || `Erro ao enviar SMS (${res.status})`);
}
// success feedback
alert('SMS enviado com sucesso');
// clear fields
setCommPhoneNumber('');
setCommMessage('');
setCommPatientId(null);
} catch (err: any) {
alert(String(err?.message || err || 'Falha ao enviar SMS'));
} finally {
setSmsSending(false);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
console.log("Laudo salvo!");
window.scrollTo({ top: 0, behavior: "smooth" });
};
@ -2578,65 +2480,61 @@ const ProfissionalPage = () => {
<div className="bg-card shadow-md rounded-lg p-6">
<h2 className="text-2xl font-bold mb-4 text-foreground">Comunicação com o Paciente</h2>
<div className="space-y-6">
<div className="grid grid-cols-1 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="patientSelect">Paciente *</Label>
<Select
value={commPatientId ?? ''}
onValueChange={(val: string) => {
// Radix Select does not allow an Item with empty string as value.
// Use a sentinel value "__none" for the "-- nenhum --" choice and map it to null here.
const v = val === "__none" ? null : (val || null);
setCommPatientId(v);
if (!v) {
setCommPhoneNumber('');
return;
}
try {
const found = (pacientes || []).find((p: any) => String(p.id ?? p.uuid ?? p.email ?? '') === String(v));
if (found) {
setCommPhoneNumber(
found.phone_mobile ?? found.celular ?? found.telefone ?? found.phone ?? found.mobile ?? found.phone_number ?? ''
);
} else {
setCommPhoneNumber('');
}
} catch (e) {
console.warn('[ProfissionalPage] erro ao preencher telefone do paciente selecionado', e);
setCommPhoneNumber('');
}
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="-- nenhum --" />
<Label htmlFor="destinatario">Destinatário</Label>
<Select>
<SelectTrigger id="destinatario" className="hover:border-primary focus:border-primary cursor-pointer">
<SelectValue placeholder="Selecione o paciente" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">-- nenhum --</SelectItem>
{pacientes && pacientes.map((p:any) => (
<SelectItem key={String(p.id || p.uuid || p.cpf || p.email)} value={String(p.id ?? p.uuid ?? p.email ?? '')}>
{p.full_name ?? p.nome ?? p.name ?? p.email ?? String(p.id ?? p.cpf ?? '')}
<SelectContent className="bg-popover border">
{pacientes.map((paciente) => (
<SelectItem
key={paciente.cpf}
value={paciente.nome}
className="hover:bg-blue-50 focus:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground dark:focus:bg-primary dark:focus:text-primary-foreground"
>
{paciente.nome} - {paciente.cpf}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="phoneNumber">Número (phone_number)</Label>
<Input id="phoneNumber" placeholder="+5511999999999" value={commPhoneNumber} readOnly disabled className="bg-muted/50" />
<Label htmlFor="tipoMensagem">Tipo de mensagem</Label>
<Select>
<SelectTrigger id="tipoMensagem" className="hover:border-primary focus:border-primary cursor-pointer">
<SelectValue placeholder="Selecione o tipo" />
</SelectTrigger>
<SelectContent className="bg-popover border">
<SelectItem value="lembrete" className="hover:bg-blue-50 focus:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground dark:focus:bg-primary dark:focus:text-primary-foreground">Lembrete de Consulta</SelectItem>
<SelectItem value="resultado" className="hover:bg-blue-50 focus:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground dark:focus:bg-primary dark:focus:text-primary-foreground">Resultado de Exame</SelectItem>
<SelectItem value="instrucao" className="hover:bg-blue-50 focus:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground dark:focus:bg-primary dark:focus:text-primary-foreground">Instruções Pós-Consulta</SelectItem>
<SelectItem value="outro" className="hover:bg-blue-50 focus:bg-blue-50 cursor-pointer dark:hover:bg-primary dark:hover:text-primary-foreground dark:focus:bg-primary dark:focus:text-primary-foreground">Outro</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="message">Mensagem (message)</Label>
<textarea id="message" className="w-full p-2 border rounded" rows={5} value={commMessage} onChange={(e) => setCommMessage(e.target.value)} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label htmlFor="dataEnvio">Data de envio</Label>
<p id="dataEnvio" className="text-sm text-muted-foreground">03/09/2025</p>
</div>
<div className="flex justify-end mt-6">
<Button onClick={handleSave} disabled={smsSending}>
{smsSending ? 'Enviando...' : 'Enviar SMS'}
</Button>
<div>
<Label htmlFor="statusEntrega">Status da entrega</Label>
<p id="statusEntrega" className="text-sm text-muted-foreground">Pendente</p>
</div>
</div>
<div className="space-y-2">
<Label>Resposta do paciente</Label>
<div className="border rounded-md p-3 bg-muted/40 space-y-2">
<p className="text-sm">"Ok, obrigado pelo lembrete!"</p>
<p className="text-xs text-muted-foreground">03/09/2025 14:30</p>
</div>
</div>
<div className="flex justify-end mt-6">
<Button onClick={handleSave}>Registrar Comunicação</Button>
</div>
</div>
</div>
);

File diff suppressed because it is too large Load Diff

View File

@ -6,13 +6,11 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { useState, useEffect, useRef } from "react"
import { useRouter } from "next/navigation"
import { SidebarTrigger } from "../ui/sidebar"
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
const { logout, user } = useAuth();
const router = useRouter();
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
@ -86,14 +84,7 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
</div>
<div className="py-1">
<button
onClick={(e) => {
e.preventDefault();
setDropdownOpen(false);
router.push('/perfil');
}}
className="w-full text-left px-4 py-2 text-sm hover:bg-accent cursor-pointer"
>
<button className="w-full text-left px-4 py-2 text-sm hover:bg-accent cursor-pointer">
👤 Perfil
</button>
<button className="w-full text-left px-4 py-2 text-sm hover:bg-accent cursor-pointer">

View File

@ -25,13 +25,14 @@ import {
FileText,
BarChart3,
Stethoscope,
User,
} from "lucide-react"
const navigation = [
{ name: "Dashboard", href: "/dashboard", icon: Home },
{ name: "Calendario", href: "/calendar", icon: Calendar },
{ name: "Pacientes", href: "/pacientes", icon: Users },
{ name: "Médicos", href: "/doutores", icon: Stethoscope },
{ name: "Médicos", href: "/doutores", icon: User },
{ name: "Consultas", href: "/consultas", icon: UserCheck },
{ name: "Relatórios", href: "/dashboard/relatorios", icon: BarChart3 },
]

View File

@ -2,7 +2,7 @@
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { loginUser, logoutUser, AuthenticationError } from '@/lib/auth'
import { getUserInfo, getCurrentUser } from '@/lib/api'
import { getUserInfo } from '@/lib/api'
import { ENV_CONFIG } from '@/lib/env-config'
import { isExpired, parseJwt } from '@/lib/jwt'
import { httpClient } from '@/lib/http'
@ -132,16 +132,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
// Restaurar sessão válida
const userData = JSON.parse(storedUser) as UserData
setToken(storedToken)
// Também buscar o usuário autenticado (/auth/v1/user) para garantir id/email atualizados
try {
const authUser = await getCurrentUser().catch(() => null)
if (authUser) {
userData.id = authUser.id ?? userData.id
userData.email = authUser.email ?? userData.email
}
} catch (e) {
console.warn('[AUTH] Falha ao buscar /auth/v1/user durante restauração de sessão:', e)
}
// Tentar buscar profile consolidado (user-info) e mesclar
try {
const info = await getUserInfo()
@ -257,31 +247,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
console.warn('[AUTH] Falha ao buscar user-info após login (não crítico):', err)
}
// Também chamar /auth/v1/user para obter dados básicos do usuário autenticado
try {
const curRes = await fetch(`${ENV_CONFIG.SUPABASE_URL}/auth/v1/user`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${response.access_token}`,
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY,
}
})
if (curRes.ok) {
const cu = await curRes.json().catch(() => null)
if (cu && response.user) {
response.user.id = cu.id ?? response.user.id
response.user.email = cu.email ?? response.user.email
}
} else {
// não crítico
console.warn('[AUTH] /auth/v1/user retornou', curRes.status)
}
} catch (e) {
console.warn('[AUTH] Erro ao chamar /auth/v1/user após login (não crítico):', e)
}
saveAuthData(
response.access_token,
response.user,

View File

@ -1196,60 +1196,6 @@ export async function criarAgendamento(input: AppointmentCreate): Promise<Appoin
return created;
}
/**
* Cria um agendamento direto no endpoint REST sem realizar validações locais
* como checagem de disponibilidade ou exceções. Use com cautela.
*/
export async function criarAgendamentoDireto(input: AppointmentCreate & { created_by?: string | null }): Promise<Appointment> {
if (!input || !input.patient_id || !input.doctor_id || !input.scheduled_at) {
throw new Error('Parâmetros inválidos para criar agendamento. patient_id, doctor_id e scheduled_at são obrigatórios.');
}
// Determine created_by: prefer explicit, then localStorage, then user-info
let createdBy: string | null = input.created_by ?? null;
if (!createdBy && typeof window !== 'undefined') {
try {
const raw = localStorage.getItem(AUTH_STORAGE_KEYS.USER);
if (raw) {
const parsed = JSON.parse(raw);
createdBy = parsed?.id ?? parsed?.user?.id ?? null;
}
} catch (e) {
// ignore
}
}
if (!createdBy) {
try {
const info = await getUserInfo().catch(() => null);
createdBy = info?.user?.id ?? null;
} catch (e) {
// ignore
}
}
const payload: any = {
patient_id: input.patient_id,
doctor_id: input.doctor_id,
scheduled_at: new Date(input.scheduled_at).toISOString(),
duration_minutes: input.duration_minutes ?? 30,
appointment_type: input.appointment_type ?? 'presencial',
chief_complaint: input.chief_complaint ?? null,
patient_notes: input.patient_notes ?? null,
insurance_provider: input.insurance_provider ?? null,
};
if (createdBy) payload.created_by = createdBy;
const url = `${REST}/appointments`;
const res = await fetch(url, {
method: 'POST',
headers: withPrefer({ ...baseHeaders(), 'Content-Type': 'application/json' }, 'return=representation'),
body: JSON.stringify(payload),
});
const created = await parse<Appointment>(res);
return created;
}
// Payload for updating an appointment (PATCH /rest/v1/appointments/{id})
export type AppointmentUpdate = Partial<{
scheduled_at: string;
@ -2390,47 +2336,6 @@ export async function getUserInfo(): Promise<UserInfo> {
return await parse<UserInfo>(res);
}
/**
* Retorna dados de usuário específico (apenas admin/gestor)
*
* Endpoint: POST /functions/v1/user-info-by-id/{userId}
*
* @param userId - UUID do usuário a ser consultado
* @returns Informações do usuário (user, profile, roles)
* @throws Erro se não autenticado (401) ou sem permissão (403)
*
* Documentação: https://docs.mediconnect.com
*/
export async function getUserInfoById(userId: string): Promise<UserInfo> {
const jwt = getAuthToken();
if (!jwt) {
// No token available — avoid calling the protected function and throw a friendly error
throw new Error('Você não está autenticado. Faça login para acessar informações do usuário.');
}
if (!userId) {
throw new Error('userId é obrigatório');
}
// ID na URL path, não no body
const url = `${API_BASE}/functions/v1/user-info-by-id/${encodeURIComponent(userId)}`;
const res = await fetch(url, {
method: "POST",
headers: baseHeaders(),
});
// Avoid calling parse() for auth errors to prevent noisy console dumps
if (!res.ok && res.status === 401) {
throw new Error('Você não está autenticado. Faça login novamente.');
}
if (!res.ok && res.status === 403) {
throw new Error('Você não tem permissão para acessar informações de outro usuário. Apenas admin/gestor podem.');
}
return await parse<UserInfo>(res);
}
export type CreateUserInput = {
email: string;
password: string;
@ -2961,206 +2866,3 @@ export async function excluirPerfil(id: string | number): Promise<void> {
await parse<any>(res);
}
// ===== DASHBOARD WIDGETS =====
/**
* Busca contagem total de pacientes
*/
export async function countTotalPatients(): Promise<number> {
try {
const url = `${REST}/patients?select=id&limit=1`;
const res = await fetch(url, {
headers: {
...baseHeaders(),
'Prefer': 'count=exact'
}
});
const countHeader = res.headers.get('content-range');
if (countHeader) {
const match = countHeader.match(/\/(\d+)$/);
return match ? parseInt(match[1]) : 0;
}
return 0;
} catch (err) {
console.error('[countTotalPatients] Erro:', err);
return 0;
}
}
/**
* Busca contagem total de médicos
*/
export async function countTotalDoctors(): Promise<number> {
try {
const url = `${REST}/doctors?select=id&limit=1`;
const res = await fetch(url, {
headers: {
...baseHeaders(),
'Prefer': 'count=exact'
}
});
const countHeader = res.headers.get('content-range');
if (countHeader) {
const match = countHeader.match(/\/(\d+)$/);
return match ? parseInt(match[1]) : 0;
}
return 0;
} catch (err) {
console.error('[countTotalDoctors] Erro:', err);
return 0;
}
}
/**
* Busca contagem de agendamentos para hoje
*/
export async function countAppointmentsToday(): Promise<number> {
try {
const today = new Date().toISOString().split('T')[0];
const tomorrow = new Date(Date.now() + 86400000).toISOString().split('T')[0];
const url = `${REST}/appointments?scheduled_at=gte.${today}T00:00:00&scheduled_at=lt.${tomorrow}T00:00:00&select=id&limit=1`;
const res = await fetch(url, {
headers: {
...baseHeaders(),
'Prefer': 'count=exact'
}
});
const countHeader = res.headers.get('content-range');
if (countHeader) {
const match = countHeader.match(/\/(\d+)$/);
return match ? parseInt(match[1]) : 0;
}
return 0;
} catch (err) {
console.error('[countAppointmentsToday] Erro:', err);
return 0;
}
}
/**
* Busca próximas consultas (próximos 7 dias)
*/
export async function getUpcomingAppointments(limit: number = 10): Promise<any[]> {
try {
const today = new Date().toISOString();
const nextWeek = new Date(Date.now() + 7 * 86400000).toISOString();
const url = `${REST}/appointments?scheduled_at=gte.${today}&scheduled_at=lt.${nextWeek}&order=scheduled_at.asc&limit=${limit}&select=id,scheduled_at,status,doctor_id,patient_id`;
const res = await fetch(url, { headers: baseHeaders() });
return await parse<any[]>(res);
} catch (err) {
console.error('[getUpcomingAppointments] Erro:', err);
return [];
}
}
/**
* Busca agendamentos por data (para gráfico)
*/
export async function getAppointmentsByDateRange(days: number = 14): Promise<any[]> {
try {
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const endDate = new Date().toISOString();
const url = `${REST}/appointments?scheduled_at=gte.${startDate.toISOString()}&scheduled_at=lt.${endDate}&select=scheduled_at,status&order=scheduled_at.asc`;
const res = await fetch(url, { headers: baseHeaders() });
return await parse<any[]>(res);
} catch (err) {
console.error('[getAppointmentsByDateRange] Erro:', err);
return [];
}
}
/**
* Busca novos usuários (últimos 7 dias)
*/
export async function getNewUsersLastDays(days: number = 7): Promise<any[]> {
try {
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const url = `${REST}/profiles?created_at=gte.${startDate.toISOString()}&order=created_at.desc&limit=10&select=id,full_name,email`;
const res = await fetch(url, { headers: baseHeaders() });
return await parse<any[]>(res);
} catch (err) {
console.error('[getNewUsersLastDays] Erro:', err);
return [];
}
}
/**
* Busca relatórios pendentes (draft)
*/
export async function getPendingReports(limit: number = 5): Promise<any[]> {
try {
const url = `${REST}/reports?status=eq.draft&order=created_at.desc&limit=${limit}&select=id,order_number,patient_id,exam,requested_by,created_at`;
const res = await fetch(url, { headers: baseHeaders() });
return await parse<any[]>(res);
} catch (err) {
console.error('[getPendingReports] Erro:', err);
return [];
}
}
/**
* Busca usuários desabilitados (alertas)
*/
export async function getDisabledUsers(limit: number = 5): Promise<any[]> {
try {
const url = `${REST}/profiles?disabled=eq.true&order=updated_at.desc&limit=${limit}&select=id,full_name,email,disabled`;
const res = await fetch(url, { headers: baseHeaders() });
return await parse<any[]>(res);
} catch (err) {
console.error('[getDisabledUsers] Erro:', err);
return [];
}
}
/**
* Busca disponibilidade de médicos (para hoje)
*/
export async function getDoctorsAvailabilityToday(): Promise<any[]> {
try {
const today = new Date();
const weekday = today.getDay();
const url = `${REST}/doctor_availability?weekday=eq.${weekday}&active=eq.true&select=id,doctor_id,start_time,end_time,slot_minutes,appointment_type`;
const res = await fetch(url, { headers: baseHeaders() });
return await parse<any[]>(res);
} catch (err) {
console.error('[getDoctorsAvailabilityToday] Erro:', err);
return [];
}
}
/**
* Busca detalhes de paciente por ID
*/
export async function getPatientById(patientId: string): Promise<any> {
try {
const url = `${REST}/patients?id=eq.${patientId}&select=*&limit=1`;
const res = await fetch(url, { headers: baseHeaders() });
const arr = await parse<any[]>(res);
return arr && arr.length > 0 ? arr[0] : null;
} catch (err) {
console.error('[getPatientById] Erro:', err);
return null;
}
}
/**
* Busca detalhes de médico por ID
*/
export async function getDoctorById(doctorId: string): Promise<any> {
try {
const url = `${REST}/doctors?id=eq.${doctorId}&select=*&limit=1`;
const res = await fetch(url, { headers: baseHeaders() });
const arr = await parse<any[]>(res);
return arr && arr.length > 0 ? arr[0] : null;
} catch (err) {
console.error('[getDoctorById] Erro:', err);
return null;
}
}

View File

@ -19,19 +19,8 @@ function extractProjectRef(url: string): string | null {
*/
function extractProjectRefFromKey(apiKey: string): string | null {
try {
const part = apiKey.split('.')[1];
if (!part) return null;
// Decode base64 payload in both browser and Node environments
let jsonStr: string | null = null;
if (typeof atob === 'function') {
try { jsonStr = atob(part); } catch (e) { jsonStr = null; }
}
if (!jsonStr && typeof Buffer !== 'undefined') {
try { jsonStr = Buffer.from(part, 'base64').toString('utf8'); } catch (e) { jsonStr = null; }
}
if (!jsonStr) return null;
const payload = JSON.parse(jsonStr);
return payload?.ref ?? payload?.project_ref ?? null;
const payload = JSON.parse(atob(apiKey.split('.')[1]));
return payload.ref || null;
} catch {
return null;
}

View File

@ -271,9 +271,7 @@ export async function deletarRelatorio(id: string): Promise<void> {
export async function listarRelatoriosPorPaciente(idPaciente: string): Promise<Report[]> {
try {
console.log('👤 [API RELATÓRIOS] Buscando relatórios do paciente:', idPaciente);
// Try a strict eq lookup first (encode the id)
const encodedId = encodeURIComponent(String(idPaciente));
let url = `${BASE_API_RELATORIOS}?patient_id=eq.${encodedId}`;
const url = `${BASE_API_RELATORIOS}?patient_id=eq.${idPaciente}`;
const headers = obterCabecalhos();
const masked = (headers as any)['Authorization'] ? `${String((headers as any)['Authorization']).slice(0,6)}...${String((headers as any)['Authorization']).slice(-6)}` : null;
console.debug('[listarRelatoriosPorPaciente] URL:', url);
@ -283,24 +281,8 @@ export async function listarRelatoriosPorPaciente(idPaciente: string): Promise<R
headers,
});
const resultado = await tratarRespostaApi<Report[]>(resposta);
console.log('✅ [API RELATÓRIOS] Relatórios do paciente encontrados (eq):', resultado.length);
// If eq returned results, return them. Otherwise retry using `in.(id)` which some setups prefer.
if (Array.isArray(resultado) && resultado.length) return resultado;
// Retry with in.(id) clause as a fallback
try {
const inClause = encodeURIComponent(`(${String(idPaciente)})`);
const urlIn = `${BASE_API_RELATORIOS}?patient_id=in.${inClause}`;
console.debug('[listarRelatoriosPorPaciente] retrying with IN clause URL:', urlIn);
const resp2 = await fetch(urlIn, { method: 'GET', headers });
const res2 = await tratarRespostaApi<Report[]>(resp2);
console.log('✅ [API RELATÓRIOS] Relatórios do paciente encontrados (in):', Array.isArray(res2) ? res2.length : 0);
return Array.isArray(res2) ? res2 : [];
} catch (e) {
console.warn('[listarRelatoriosPorPaciente] fallback in.() failed', e);
}
return [];
console.log('✅ [API RELATÓRIOS] Relatórios do paciente encontrados:', resultado.length);
return resultado;
} catch (erro) {
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios do paciente:', erro);
throw erro;

View File

@ -23,69 +23,3 @@ export function validarCPFLocal(cpf: string): boolean {
return true;
}
// ===== FORMATAÇÃO DE CEP =====
export function formatCEP(cep: string): string {
if (!cep) return "";
const cleaned = cep.replace(/\D/g, "");
if (cleaned.length !== 8) return cleaned;
return `${cleaned.slice(0, 5)}-${cleaned.slice(5)}`;
}
export function validarCEP(cep: string): boolean {
if (!cep) return false;
const cleaned = cep.replace(/\D/g, "");
return cleaned.length === 8 && /^\d{8}$/.test(cleaned);
}
// ===== FORMATAÇÃO DE TELEFONE =====
export function formatTelefone(telefone: string): string {
if (!telefone) return "";
const cleaned = telefone.replace(/\D/g, "");
// Formatar em (00) 9XXXX-XXXX ou (00) XXXX-XXXX
if (cleaned.length === 11) {
return `(${cleaned.slice(0, 2)}) ${cleaned.slice(2, 7)}-${cleaned.slice(7)}`;
} else if (cleaned.length === 10) {
return `(${cleaned.slice(0, 2)}) ${cleaned.slice(2, 6)}-${cleaned.slice(6)}`;
}
return cleaned;
}
// ===== BUSCA DE CEP =====
export async function buscarCEP(cep: string): Promise<{
street: string;
neighborhood: string;
city: string;
state: string;
} | null> {
try {
if (!validarCEP(cep)) {
throw new Error("CEP inválido");
}
const cleaned = cep.replace(/\D/g, "");
const response = await fetch(`https://viacep.com.br/ws/${cleaned}/json/`);
if (!response.ok) {
throw new Error("Erro ao buscar CEP");
}
const data = await response.json();
if (data.erro) {
throw new Error("CEP não encontrado");
}
return {
street: data.logradouro || "",
neighborhood: data.bairro || "",
city: data.localidade || "",
state: data.uf || "",
};
} catch (err) {
console.error("[buscarCEP] Erro:", err);
return null;
}
}

File diff suppressed because it is too large Load Diff