riseup-squad18/src/pages/AgendamentoPaciente.tsx
Fernando Pirichowski Aguiar 389a191f20 fix: corrige persistência de avatar, agendamento de consulta e download de PDF
- Avatar do paciente agora persiste após reload (adiciona timestamp para evitar cache)
- Agendamento usa patient_id correto ao invés de user_id
- Botão de download de PDF desbloqueado com logs detalhados
2025-11-15 08:36:41 -03:00

580 lines
20 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { Calendar, User, FileText, CheckCircle, LogOut } from "lucide-react";
// import consultaService from "../services/consultaService"; // não utilizado após integração com appointmentService
import { appointmentService, patientService } from "../services";
import AvailableSlotsPicker from "../components/agenda/AvailableSlotsPicker";
import { doctorService } from "../services";
import toast from "react-hot-toast";
import { format, addDays } from "date-fns";
import { ptBR } from "date-fns/locale";
import { useNavigate } from "react-router-dom";
interface Medico {
_id: string;
nome: string;
especialidade: string;
valorConsulta: number;
horarioAtendimento: Record<string, string[]>;
}
interface Paciente {
_id: string;
nome: string;
cpf: string;
telefone: string;
email: string;
}
const AgendamentoPaciente: React.FC = () => {
const [medicos, setMedicos] = useState<Medico[]>([]);
const [pacienteLogado, setPacienteLogado] = useState<Paciente | null>(null);
const [patientTableId, setPatientTableId] = useState<string | null>(null); // ID da tabela patients
const [loading, setLoading] = useState(false);
const [etapa, setEtapa] = useState(1);
const [agendamento, setAgendamento] = useState({
medicoId: "",
data: "",
horario: "",
tipoConsulta: "primeira-vez",
motivoConsulta: "",
observacoes: "",
});
// Slots são carregados diretamente pelo AvailableSlotsPicker
const navigate = useNavigate();
useEffect(() => {
// Verificar se paciente está logado
const pacienteData = localStorage.getItem("pacienteLogado");
if (!pacienteData) {
console.log(
"[AgendamentoPaciente] Paciente não logado, redirecionando..."
);
navigate("/paciente");
return;
}
try {
const paciente = JSON.parse(pacienteData);
console.log("[AgendamentoPaciente] Paciente logado:", paciente);
setPacienteLogado(paciente);
// Buscar o patient.id (ID da tabela) usando o user_id
const fetchPatientTableId = async () => {
try {
const patientData = await patientService.getByUserId(paciente._id);
console.log(
"[AgendamentoPaciente] Patient data da tabela:",
patientData
);
if (patientData?.id) {
setPatientTableId(patientData.id);
console.log(
"[AgendamentoPaciente] Patient table ID:",
patientData.id
);
} else {
console.error(
"[AgendamentoPaciente] ❌ Paciente não encontrado na tabela"
);
}
} catch (error) {
console.error(
"[AgendamentoPaciente] Erro ao buscar patient.id:",
error
);
}
};
void fetchPatientTableId();
void fetchMedicos();
} catch (error) {
console.error(
"[AgendamentoPaciente] Erro ao carregar dados do paciente:",
error
);
navigate("/paciente");
}
}, [navigate]);
// As consultas locais agora aparecem na Dashboard (AcompanhamentoPaciente)
const fetchMedicos = async () => {
try {
console.log("[AgendamentoPaciente] Iniciando busca de médicos...");
const doctors = await doctorService.list({ active: true });
console.log("[AgendamentoPaciente] Médicos recebidos:", doctors);
const mapped: Medico[] = doctors.map((m: any) => ({
_id: m.id,
nome: m.full_name,
especialidade: m.specialty || "",
valorConsulta: 0,
horarioAtendimento: {},
}));
console.log("[AgendamentoPaciente] Médicos mapeados:", mapped);
setMedicos(mapped);
if (mapped.length === 0) {
toast.error(
"Nenhum médico ativo encontrado. Por favor, cadastre médicos primeiro."
);
}
} catch (error) {
console.error("[AgendamentoPaciente] Erro ao carregar médicos:", error);
toast.error("Erro ao carregar lista de médicos");
}
};
// Horários disponíveis agora são resolvidos no componente de slots
const handleMedicoChange = (medicoId: string) => {
setAgendamento((prev) => ({ ...prev, medicoId, data: "", horario: "" }));
};
const handleDataChange = (data: string) => {
setAgendamento((prev) => ({ ...prev, data, horario: "" }));
};
const confirmarAgendamento = async () => {
if (!pacienteLogado) return;
// Verificar se temos o patient.id da tabela
if (!patientTableId) {
console.error(
"[AgendamentoPaciente] ❌ Patient table ID não encontrado!"
);
toast.error(
"Erro: Dados do paciente não carregados. Recarregue a página."
);
return;
}
try {
setLoading(true);
// Verificar se há token de autenticação
const token = localStorage.getItem("mediconnect_access_token");
console.log("[AgendamentoPaciente] Token presente?", !!token);
if (!token) {
console.error(
"[AgendamentoPaciente] ❌ Token não encontrado! Redirecionando para login..."
);
toast.error("Sessão expirada. Faça login novamente.");
navigate("/paciente");
return;
}
// NOTE: Removed remote CPF validation to avoid false negatives
// NOTE: remote CEP validation removed to avoid false negatives
const dataHora = new Date(
`${agendamento.data}T${agendamento.horario}:00.000Z`
);
// Payload conforme documentação da API Supabase
const payload = {
doctor_id: agendamento.medicoId,
patient_id: patientTableId,
scheduled_at: dataHora.toISOString(),
duration_minutes: 30,
created_by: pacienteLogado._id,
};
console.log("[AgendamentoPaciente] 📋 Dados para criar consulta:", {
patient_id: patientTableId,
patient_user_id: pacienteLogado._id,
doctor_id: agendamento.medicoId,
scheduled_at: dataHora.toISOString(),
chief_complaint: agendamento.motivoConsulta,
token_presente: !!token,
payload_completo: payload,
});
const resultado = await appointmentService.create(payload);
console.log(
"[AgendamentoPaciente] ✅ Consulta criada com sucesso:",
resultado
);
toast.success("Consulta agendada com sucesso!");
setEtapa(4); // Etapa de confirmação
} catch (error) {
console.error(
"[AgendamentoPaciente] ❌ Erro ao agendar consulta:",
error
);
toast.error("Erro ao agendar consulta. Tente novamente.");
} finally {
setLoading(false);
}
};
const resetarAgendamento = () => {
setAgendamento({
medicoId: "",
data: "",
horario: "",
tipoConsulta: "primeira-vez",
motivoConsulta: "",
observacoes: "",
});
setEtapa(1);
};
// Removido: criação/visualização local aqui. Use a Dashboard para ver.
const logout = () => {
localStorage.removeItem("pacienteLogado");
navigate("/paciente");
};
const proximosSeteDias = () => {
const dias = [];
for (let i = 1; i <= 7; i++) {
const data = addDays(new Date(), i);
dias.push({
valor: format(data, "yyyy-MM-dd"),
label: format(data, "EEEE, dd/MM", { locale: ptBR }),
});
}
return dias;
};
const medicoSelecionado = medicos.find((m) => m._id === agendamento.medicoId);
if (!pacienteLogado) {
return (
<div className="flex justify-center items-center min-h-screen">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
if (etapa === 4) {
return (
<div className="min-h-screen bg-gray-50 p-4 sm:p-6 lg:p-8">
<div className="max-w-2xl mx-auto">
<div className="bg-white rounded-lg sm:rounded-xl shadow-md p-6 sm:p-8 text-center">
<CheckCircle className="w-12 h-12 sm:w-16 sm:h-16 text-green-500 mx-auto mb-3 sm:mb-4" />
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 mb-3 sm:mb-4">
Consulta Agendada com Sucesso!
</h2>
<div className="bg-gray-50 rounded-lg p-4 sm:p-6 mb-4 sm:mb-6 text-left">
<h3 className="text-sm sm:text-base font-semibold mb-2 sm:mb-3">
Detalhes do Agendamento:
</h3>
<div className="space-y-1.5 sm:space-y-2 text-xs sm:text-sm">
<p className="break-words">
<strong>Paciente:</strong> {pacienteLogado.nome}
</p>
<p className="break-words">
<strong>Médico:</strong> {medicoSelecionado?.nome}
</p>
<p className="break-words">
<strong>Especialidade:</strong>{" "}
{medicoSelecionado?.especialidade}
</p>
<p>
<strong>Data:</strong>{" "}
{format(new Date(agendamento.data), "dd/MM/yyyy", {
locale: ptBR,
})}
</p>
<p>
<strong>Horário:</strong> {agendamento.horario}
</p>
<p>
<strong>Tipo:</strong> {agendamento.tipoConsulta}
</p>
{agendamento.motivoConsulta && (
<p className="break-words">
<strong>Motivo:</strong> {agendamento.motivoConsulta}
</p>
)}
</div>
</div>
<button
onClick={resetarAgendamento}
className="btn-primary w-full sm:w-auto text-sm sm:text-base"
>
Fazer Novo Agendamento
</button>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 p-4 sm:p-6 lg:p-8">
<div className="max-w-4xl mx-auto space-y-4 sm:space-y-6 lg:space-y-8">
{/* Header com informações do paciente */}
<div className="bg-gradient-to-r from-blue-700 to-blue-500 rounded-lg sm:rounded-xl p-4 sm:p-6 text-white shadow">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
<div className="min-w-0 flex-1">
<h1 className="text-lg sm:text-xl lg:text-2xl font-bold truncate">
Bem-vindo(a), {pacienteLogado.nome}!
</h1>
<p className="opacity-90 text-sm sm:text-base">
Agende sua consulta médica
</p>
</div>
<button
onClick={logout}
className="w-full sm:w-auto flex items-center justify-center space-x-2 bg-white/20 hover:bg-white/30 px-3 sm:px-4 py-2 rounded-lg transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white/70 text-sm sm:text-base whitespace-nowrap"
>
<LogOut className="w-4 h-4" />
<span>Sair</span>
</button>
</div>
</div>
{/* As consultas locais serão exibidas na Dashboard do paciente */}
{/* Indicador de Etapas */}
<div className="flex items-center justify-center mb-6 sm:mb-8">
{[1, 2, 3].map((numero) => (
<React.Fragment key={numero}>
<div
className={`w-7 h-7 sm:w-8 sm:h-8 rounded-full flex items-center justify-center text-sm sm:text-base font-medium ${
etapa >= numero
? "bg-blue-600 text-white"
: "bg-gray-300 text-gray-600"
}`}
>
{numero}
</div>
{numero < 3 && (
<div
className={`w-12 sm:w-16 h-1 ${
etapa > numero ? "bg-blue-600" : "bg-gray-300"
}`}
/>
)}
</React.Fragment>
))}
</div>
<div className="bg-white rounded-lg sm:rounded-xl shadow border border-gray-200 p-4 sm:p-6">
{/* Etapa 1: Seleção de Médico */}
{etapa === 1 && (
<div className="space-y-4 sm:space-y-6">
<h2 className="text-lg sm:text-xl font-semibold flex items-center">
<User className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
Selecione o Médico
</h2>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Médico/Especialidade
</label>
<select
value={agendamento.medicoId}
onChange={(e) => handleMedicoChange(e.target.value)}
className="form-input text-sm sm:text-base"
required
>
<option value="">Selecione um médico</option>
{medicos.map((medico) => (
<option key={medico._id} value={medico._id}>
{medico.nome} - {medico.especialidade} (R${" "}
{medico.valorConsulta})
</option>
))}
</select>
</div>
<div className="flex justify-end pt-2">
<button
onClick={() => setEtapa(2)}
disabled={!agendamento.medicoId}
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed w-full sm:w-auto text-sm sm:text-base"
>
Próximo
</button>
</div>
</div>
)}
{/* Etapa 2: Seleção de Data e Horário */}
{etapa === 2 && (
<div className="space-y-4 sm:space-y-6">
<h2 className="text-lg sm:text-xl font-semibold flex items-center">
<Calendar className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
Selecione Data e Horário
</h2>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Data da Consulta
</label>
<select
value={agendamento.data}
onChange={(e) => handleDataChange(e.target.value)}
className="form-input text-sm sm:text-base"
required
>
<option value="">Selecione uma data</option>
{proximosSeteDias().map((dia) => (
<option key={dia.valor} value={dia.valor}>
{dia.label}
</option>
))}
</select>
</div>
{agendamento.data && agendamento.medicoId && (
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Horários Disponíveis
</label>
<AvailableSlotsPicker
doctorId={agendamento.medicoId}
date={agendamento.data}
onSelect={(t) =>
setAgendamento((prev) => ({ ...prev, horario: t }))
}
/>
</div>
)}
<div className="flex flex-col sm:flex-row justify-between gap-3 pt-2">
<button
onClick={() => setEtapa(1)}
className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300 w-full sm:w-auto text-sm sm:text-base order-2 sm:order-1"
>
Voltar
</button>
<button
onClick={() => setEtapa(3)}
disabled={!agendamento.horario}
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 w-full sm:w-auto text-sm sm:text-base order-1 sm:order-2"
>
Próximo
</button>
</div>
</div>
)}
{/* Etapa 3: Informações Adicionais */}
{etapa === 3 && (
<div className="space-y-4 sm:space-y-6">
<h2 className="text-lg sm:text-xl font-semibold flex items-center">
<FileText className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
Informações da Consulta
</h2>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Tipo de Consulta
</label>
<select
value={agendamento.tipoConsulta}
onChange={(e) =>
setAgendamento((prev) => ({
...prev,
tipoConsulta: e.target.value,
}))
}
className="form-input text-sm sm:text-base"
>
<option value="primeira-vez">Primeira Consulta</option>
<option value="retorno">Retorno</option>
<option value="urgencia">Urgência</option>
</select>
</div>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Motivo da Consulta
</label>
<textarea
value={agendamento.motivoConsulta}
onChange={(e) =>
setAgendamento((prev) => ({
...prev,
motivoConsulta: e.target.value,
}))
}
className="form-input text-sm sm:text-base"
rows={3}
placeholder="Descreva brevemente o motivo da consulta"
/>
</div>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Observações (opcional)
</label>
<textarea
value={agendamento.observacoes}
onChange={(e) =>
setAgendamento((prev) => ({
...prev,
observacoes: e.target.value,
}))
}
className="form-input text-sm sm:text-base"
rows={2}
placeholder="Informações adicionais relevantes"
/>
</div>
{/* Resumo do Agendamento */}
<div className="bg-gray-50 rounded-lg p-3 sm:p-4 border border-gray-200">
<h3 className="text-sm sm:text-base font-semibold mb-2 sm:mb-3">
Resumo do Agendamento:
</h3>
<div className="space-y-1 sm:space-y-1.5 text-xs sm:text-sm">
<p className="break-words">
<strong>Paciente:</strong> {pacienteLogado.nome}
</p>
<p className="break-words">
<strong>Médico:</strong> {medicoSelecionado?.nome}
</p>
<p>
<strong>Data:</strong>{" "}
{format(new Date(agendamento.data), "dd/MM/yyyy", {
locale: ptBR,
})}
</p>
<p>
<strong>Horário:</strong> {agendamento.horario}
</p>
<p>
<strong>Valor:</strong> R${" "}
{medicoSelecionado?.valorConsulta}
</p>
</div>
</div>
<div className="flex flex-col sm:flex-row justify-between gap-3 pt-2">
<button
onClick={() => setEtapa(2)}
className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300 w-full sm:w-auto text-sm sm:text-base order-2 sm:order-1"
>
Voltar
</button>
<button
onClick={confirmarAgendamento}
disabled={loading}
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 w-full sm:w-auto text-sm sm:text-base order-1 sm:order-2"
>
{loading ? "Agendando..." : "Confirmar Agendamento"}
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default AgendamentoPaciente;