- 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
571 lines
21 KiB
TypeScript
571 lines
21 KiB
TypeScript
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>
|
||
);
|
||
}
|