riseup-squad18/src/pages/PerfilMedico.tsx
Fernando Pirichowski Aguiar d082028c5a feat: Sistema completo de agendamento médico com correções RLS e melhorias UI
- Fix: Avatar upload usando Supabase Client com RLS policies
- Fix: Profile update usando Supabase Client
- Fix: Timezone handling em datas de consultas
- Fix: Filtros de consultas passadas/futuras
- Fix: Appointment cancellation com Supabase Client
- Fix: Navegação após booking de consulta
- Fix: Report service usando Supabase Client
- Fix: Campo created_by em relatórios
- Fix: URL pública de avatares no Storage
- Fix: Modal de criação de usuário com scroll
- Feat: Sistema completo de gestão de consultas
- Feat: Painéis para paciente, médico, secretária e admin
- Feat: Upload de avatares
- Feat: Sistema de relatórios médicos
- Feat: Gestão de disponibilidade de médicos
2025-11-05 23:38:31 -03:00

571 lines
21 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from "react";
import { Save, ArrowLeft } from "lucide-react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
import { doctorService } from "../services";
import { AvatarUpload } from "../components/ui/AvatarUpload";
export default function PerfilMedico() {
const { user } = useAuth();
const navigate = useNavigate();
const [isEditing, setIsEditing] = useState(false);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<
"personal" | "professional" | "security"
>("personal");
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
const [formData, setFormData] = useState({
full_name: "",
email: "",
phone: "",
cpf: "",
birth_date: "",
gender: "",
specialty: "",
crm: "",
crm_state: "",
bio: "",
education: "",
experience_years: "",
});
const [passwordData, setPasswordData] = useState({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
useEffect(() => {
if (user?.id) {
loadDoctorData();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user?.id]);
const loadDoctorData = async () => {
if (!user?.id) {
console.error("[PerfilMedico] Sem user.id:", user);
toast.error("Usuário não identificado");
return;
}
try {
setLoading(true);
console.log("[PerfilMedico] Buscando dados do médico...");
// Tentar buscar por user_id primeiro
let doctor = await doctorService.getByUserId(user.id);
// Se não encontrar por user_id, tentar por email
if (!doctor && user.email) {
console.log(
"[PerfilMedico] Médico não encontrado por user_id, tentando por email:",
user.email
);
doctor = await doctorService.getByEmail(user.email);
}
if (doctor) {
console.log("[PerfilMedico] Dados do médico carregados:", doctor);
setFormData({
full_name: doctor.full_name || "",
email: doctor.email || "",
phone: doctor.phone_mobile || "",
cpf: doctor.cpf || "",
birth_date: doctor.birth_date || "",
gender: "", // Doctor type não tem gender
specialty: doctor.specialty || "",
crm: doctor.crm || "",
crm_state: doctor.crm_uf || "",
bio: "", // Doctor type não tem bio
education: "", // Doctor type não tem education
experience_years: "", // Doctor type não tem experience_years
});
setAvatarUrl(undefined);
} else {
console.warn("[PerfilMedico] Médico não encontrado na tabela doctors");
// Usar dados básicos do usuário logado
setFormData({
full_name: user.nome || "",
email: user.email || "",
phone: "",
cpf: "",
birth_date: "",
gender: "",
specialty: "",
crm: "",
crm_state: "",
bio: "",
education: "",
experience_years: "",
});
toast("Preencha seus dados para completar o cadastro", { icon: "" });
}
} catch (error) {
console.error("[PerfilMedico] Erro ao carregar dados do médico:", error);
toast.error("Erro ao carregar dados do perfil");
} finally {
setLoading(false);
}
};
const handleSave = async () => {
if (!user?.id) return;
try {
const dataToSave = {
...formData,
experience_years: formData.experience_years
? parseInt(formData.experience_years)
: undefined,
};
await doctorService.update(user.id, dataToSave);
toast.success("Perfil atualizado com sucesso!");
setIsEditing(false);
} catch (error) {
console.error("Erro ao salvar perfil:", error);
toast.error("Erro ao salvar perfil");
}
};
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handlePasswordChange = async () => {
if (passwordData.newPassword !== passwordData.confirmPassword) {
toast.error("As senhas não coincidem");
return;
}
if (passwordData.newPassword.length < 6) {
toast.error("A senha deve ter pelo menos 6 caracteres");
return;
}
try {
// TODO: Implementar mudança de senha via API
toast.success("Senha alterada com sucesso!");
setPasswordData({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
} catch (error) {
console.error("Erro ao alterar senha:", error);
toast.error("Erro ao alterar senha");
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="w-16 h-16 border-4 border-green-600 border-t-transparent rounded-full animate-spin" />
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-4 sm:py-6 lg:py-8 px-4 sm:px-6">
<div className="max-w-4xl mx-auto space-y-4 sm:space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex items-start sm:items-center gap-2 sm:gap-3 w-full sm:w-auto">
<button
onClick={() => navigate(-1)}
className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors flex-shrink-0"
title="Voltar"
>
<ArrowLeft className="w-5 h-5 sm:w-6 sm:h-6" />
</button>
<div className="min-w-0 flex-1">
<h1 className="text-xl sm:text-2xl lg:text-3xl font-bold text-gray-900 truncate">
Meu Perfil
</h1>
<p className="text-xs sm:text-sm text-gray-600 mt-0.5 sm:mt-1">
Gerencie suas informações pessoais e profissionais
</p>
</div>
</div>
{!isEditing ? (
<button
onClick={() => setIsEditing(true)}
className="w-full sm:w-auto px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm sm:text-base whitespace-nowrap"
>
Editar Perfil
</button>
) : (
<div className="flex gap-2 w-full sm:w-auto">
<button
onClick={() => {
setIsEditing(false);
loadDoctorData();
}}
className="flex-1 sm:flex-none px-3 sm:px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm sm:text-base"
>
Cancelar
</button>
<button
onClick={handleSave}
className="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center justify-center gap-2 text-sm sm:text-base"
>
<Save className="w-4 h-4" />
Salvar
</button>
</div>
)}
</div>
{/* Avatar Card */}
<div className="bg-white rounded-lg shadow p-4 sm:p-6">
<h2 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">
Foto de Perfil
</h2>
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-4 sm:gap-6">
<AvatarUpload
userId={user?.id}
currentAvatarUrl={avatarUrl}
name={formData.full_name || "Médico"}
color="green"
size="xl"
editable={true}
onAvatarUpdate={(url) => setAvatarUrl(url || undefined)}
/>
<div className="text-center sm:text-left min-w-0 flex-1">
<p className="font-medium text-gray-900 text-sm sm:text-base truncate">
{formData.full_name}
</p>
<p className="text-gray-500 text-xs sm:text-sm truncate">
{formData.specialty}
</p>
<p className="text-xs sm:text-sm text-gray-500 truncate">
CRM: {formData.crm} - {formData.crm_state}
</p>
</div>
</div>
</div>
{/* Tabs */}
<div className="bg-white rounded-lg shadow">
<div className="border-b border-gray-200 overflow-x-auto">
<nav className="flex -mb-px min-w-max">
<button
onClick={() => setActiveTab("personal")}
className={`px-4 sm:px-6 py-2.5 sm:py-3 text-xs sm:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
activeTab === "personal"
? "border-green-600 text-green-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
Dados Pessoais
</button>
<button
onClick={() => setActiveTab("professional")}
className={`px-4 sm:px-6 py-2.5 sm:py-3 text-xs sm:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
activeTab === "professional"
? "border-green-600 text-green-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
Info. Profissionais
</button>
<button
onClick={() => setActiveTab("security")}
className={`px-4 sm:px-6 py-2.5 sm:py-3 text-xs sm:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
activeTab === "security"
? "border-green-600 text-green-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
Segurança
</button>
</nav>
</div>
<div className="p-4 sm:p-6">
{/* Tab: Dados Pessoais */}
{activeTab === "personal" && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">
Informações Pessoais
</h3>
<p className="text-sm text-gray-500 mb-4">
Mantenha seus dados atualizados
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nome Completo
</label>
<input
type="text"
value={formData.full_name}
onChange={(e) =>
handleChange("full_name", e.target.value)
}
disabled={!isEditing}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
type="email"
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
disabled={!isEditing}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Telefone
</label>
<input
type="tel"
value={formData.phone}
onChange={(e) => handleChange("phone", e.target.value)}
disabled={!isEditing}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
CPF
</label>
<input
type="text"
value={formData.cpf}
disabled
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Data de Nascimento
</label>
<input
type="date"
value={formData.birth_date}
onChange={(e) =>
handleChange("birth_date", e.target.value)
}
disabled={!isEditing}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Gênero
</label>
<select
value={formData.gender}
onChange={(e) => handleChange("gender", e.target.value)}
disabled={!isEditing}
className="form-input"
>
<option value="">Selecione</option>
<option value="male">Masculino</option>
<option value="female">Feminino</option>
<option value="other">Outro</option>
</select>
</div>
</div>
</div>
</div>
)}
{/* Tab: Informações Profissionais */}
{activeTab === "professional" && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">
Informações Profissionais
</h3>
<p className="text-sm text-gray-500 mb-4">
Dados da sua carreira médica
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Especialidade
</label>
<input
type="text"
value={formData.specialty}
onChange={(e) =>
handleChange("specialty", e.target.value)
}
disabled={!isEditing}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
CRM
</label>
<input
type="text"
value={formData.crm}
disabled
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Estado do CRM
</label>
<input
type="text"
value={formData.crm_state}
disabled
maxLength={2}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Anos de Experiência
</label>
<input
type="number"
value={formData.experience_years}
onChange={(e) =>
handleChange("experience_years", e.target.value)
}
disabled={!isEditing}
min="0"
className="form-input"
/>
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Biografia
</label>
<textarea
value={formData.bio}
onChange={(e) => handleChange("bio", e.target.value)}
disabled={!isEditing}
placeholder="Conte um pouco sobre sua trajetória profissional..."
rows={4}
className="form-input"
/>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Formação Acadêmica
</label>
<textarea
value={formData.education}
onChange={(e) =>
handleChange("education", e.target.value)
}
disabled={!isEditing}
placeholder="Universidades, residências, especializações..."
rows={4}
className="form-input"
/>
</div>
</div>
</div>
)}
{/* Tab: Segurança */}
{activeTab === "security" && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">Alterar Senha</h3>
<p className="text-sm text-gray-500 mb-4">
Mantenha sua conta segura
</p>
<div className="max-w-md space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Senha Atual
</label>
<input
type="password"
value={passwordData.currentPassword}
onChange={(e) =>
setPasswordData({
...passwordData,
currentPassword: e.target.value,
})
}
placeholder="Digite sua senha atual"
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nova Senha
</label>
<input
type="password"
value={passwordData.newPassword}
onChange={(e) =>
setPasswordData({
...passwordData,
newPassword: e.target.value,
})
}
placeholder="Digite a nova senha"
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Confirmar Nova Senha
</label>
<input
type="password"
value={passwordData.confirmPassword}
onChange={(e) =>
setPasswordData({
...passwordData,
confirmPassword: e.target.value,
})
}
placeholder="Confirme a nova senha"
className="form-input"
/>
</div>
<button
onClick={handlePasswordChange}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Alterar Senha
</button>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
}