From 7de65147c1333d77d94ab90e17d26aabd8a620d6 Mon Sep 17 00:00:00 2001 From: Gabriel Lira Figueira Date: Sun, 30 Nov 2025 00:53:27 -0300 Subject: [PATCH] fix: loop infinito no perfil e sincronia de avatar na sidebar --- app/patient/profile/page.tsx | 113 ++++++++++++++++++++++------- components/Sidebar.tsx | 132 +++++++++++++++++++++++++--------- components/ui/userToolTip.tsx | 29 ++++++-- 3 files changed, 207 insertions(+), 67 deletions(-) diff --git a/app/patient/profile/page.tsx b/app/patient/profile/page.tsx index 46911b9..2174452 100644 --- a/app/patient/profile/page.tsx +++ b/app/patient/profile/page.tsx @@ -1,18 +1,17 @@ -// ARQUIVO COMPLETO PARA: app/patient/profile/page.tsx - +// Caminho: app/patient/profile/page.tsx "use client"; import { useState, useEffect, useRef } from "react"; import Sidebar from "@/components/Sidebar"; import { useAuthLayout } from "@/hooks/useAuthLayout"; import { patientsService } from "@/services/patientsApi.mjs"; +import { usersService } from "@/services/usersApi.mjs"; // Adicionado import import { api } from "@/services/api.mjs"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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 { User, Mail, Phone, Calendar, Upload } from "lucide-react"; import { toast } from "@/hooks/use-toast"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; @@ -41,11 +40,39 @@ export default function PatientProfile() { const [isSaving, setIsSaving] = useState(false); const fileInputRef = useRef(null); + const getInitials = (name: string) => { + if (!name) return "U"; + return name + .split(" ") + .map((n) => n[0]) + .slice(0, 2) + .join("") + .toUpperCase(); + }; + + // Função auxiliar para construir URL do avatar + const buildAvatarUrl = (path: string | null | undefined) => { + if (!path) return undefined; + const baseUrl = "https://yuanqfswhberkoevtmfr.supabase.co"; + const cleanPath = path.startsWith('/') ? path.slice(1) : path; + const separator = cleanPath.includes('?') ? '&' : '?'; + return `${baseUrl}/storage/v1/object/avatars/${cleanPath}${separator}t=${new Date().getTime()}`; + }; + useEffect(() => { if (user?.id) { - const fetchPatientDetails = async () => { + const loadData = async () => { try { + // 1. Busca dados médicos (Tabela Patients) const patientDetails = await patientsService.getById(user.id); + + // 2. Busca dados de sistema frescos (Tabela Profiles via getMe) + // Isso garante que pegamos o avatar real do banco, não do cache local + const userSystemData = await usersService.getMe(); + + const freshAvatarPath = userSystemData?.profile?.avatar_url; + const freshAvatarUrl = buildAvatarUrl(freshAvatarPath); + setPatientData({ name: patientDetails.full_name || user.name, email: user.email, @@ -56,10 +83,10 @@ export default function PatientProfile() { street: patientDetails.street || "", number: patientDetails.number || "", city: patientDetails.city || "", - avatarFullUrl: user.avatarFullUrl, + avatarFullUrl: freshAvatarUrl, // Usa a URL fresca do banco }); } catch (error) { - console.error("Erro ao buscar detalhes do paciente:", error); + console.error("Erro ao buscar detalhes:", error); toast({ title: "Erro", description: "Não foi possível carregar seus dados completos.", @@ -67,9 +94,9 @@ export default function PatientProfile() { }); } }; - fetchPatientDetails(); + loadData(); } - }, [user]); + }, [user?.id, user?.email, user?.name]); // Removi user.avatarFullUrl para não depender do cache const handleInputChange = ( field: keyof PatientProfileData, @@ -78,6 +105,27 @@ export default function PatientProfile() { setPatientData((prev) => (prev ? { ...prev, [field]: value } : null)); }; + const updateLocalSession = (updates: { full_name?: string; avatar_url?: string }) => { + try { + const storedUserString = localStorage.getItem("user_info"); + if (storedUserString) { + const storedUser = JSON.parse(storedUserString); + + if (!storedUser.user_metadata) storedUser.user_metadata = {}; + if (updates.full_name) storedUser.user_metadata.full_name = updates.full_name; + if (updates.avatar_url) storedUser.user_metadata.avatar_url = updates.avatar_url; + + if (!storedUser.profile) storedUser.profile = {}; + if (updates.full_name) storedUser.profile.full_name = updates.full_name; + if (updates.avatar_url) storedUser.profile.avatar_url = updates.avatar_url; + + localStorage.setItem("user_info", JSON.stringify(storedUser)); + } + } catch (e) { + console.error("Erro ao atualizar sessão local:", e); + } + }; + const handleSave = async () => { if (!patientData || !user) return; setIsSaving(true); @@ -92,12 +140,22 @@ export default function PatientProfile() { number: patientData.number, city: patientData.city, }; + await patientsService.update(user.id, patientPayload); + await api.patch(`/rest/v1/profiles?id=eq.${user.id}`, { + full_name: patientData.name, + }); + + updateLocalSession({ full_name: patientData.name }); + toast({ title: "Sucesso!", - description: "Seus dados foram atualizados.", + description: "Seus dados foram atualizados. A página será recarregada.", }); + setIsEditing(false); + setTimeout(() => window.location.reload(), 1000); + } catch (error) { console.error("Erro ao salvar dados:", error); toast({ @@ -121,9 +179,6 @@ export default function PatientProfile() { if (!file || !user) return; const fileExt = file.name.split(".").pop(); - - // *** A CORREÇÃO ESTÁ AQUI *** - // O caminho salvo no banco de dados não deve conter o nome do bucket. const filePath = `${user.id}/avatar.${fileExt}`; try { @@ -132,15 +187,21 @@ export default function PatientProfile() { avatar_url: filePath, }); - const newFullUrl = `https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/public/avatars/${filePath}?t=${new Date().getTime()}`; + const newFullUrl = buildAvatarUrl(filePath); + setPatientData((prev) => prev ? { ...prev, avatarFullUrl: newFullUrl } : null ); + updateLocalSession({ avatar_url: filePath }); + toast({ title: "Sucesso!", description: "Sua foto de perfil foi atualizada.", }); + + setTimeout(() => window.location.reload(), 1000); + } catch (error) { console.error("Erro no upload do avatar:", error); toast({ @@ -154,7 +215,9 @@ export default function PatientProfile() { if (isAuthLoading || !patientData) { return ( -
Carregando seus dados...
+
+

Carregando seus dados...

+
); } @@ -313,22 +376,20 @@ export default function PatientProfile() {
-
+
- - - {patientData.name - .split(" ") - .map((n) => n[0]) - .join("")} + + + {getInitials(patientData.name)}
@@ -337,11 +398,11 @@ export default function PatientProfile() { ref={fileInputRef} onChange={handleAvatarUpload} className="hidden" - accept="image/png, image/jpeg" + accept="image/png, image/jpeg, image/jpg" />
-

{patientData.name}

+

{patientData.name}

Paciente

@@ -373,4 +434,4 @@ export default function PatientProfile() {
); -} +} \ No newline at end of file diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index aecf916..6055224 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -1,4 +1,3 @@ -// Caminho: [seu-caminho]/ManagerLayout.tsx "use client"; import type React from "react"; @@ -7,6 +6,7 @@ import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; import Cookies from "js-cookie"; import { api } from "@/services/api.mjs"; +import { usersService } from "@/services/usersApi.mjs"; // Importando usersService import { Button } from "@/components/ui/button"; import { @@ -47,6 +47,7 @@ interface UserData { full_name: string; phone_mobile: string; role: string; + avatar_url?: string; }; identities: { identity_id: string; @@ -72,39 +73,100 @@ export default function Sidebar({ children }: SidebarProps) { const [role, setRole] = useState(); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [showLogoutDialog, setShowLogoutDialog] = useState(false); + const [avatarFullUrl, setAvatarFullUrl] = useState(undefined); const router = useRouter(); const pathname = usePathname(); + // Função auxiliar para construir URL + const buildAvatarUrl = (path: string) => { + if (!path) return undefined; + const baseUrl = "https://yuanqfswhberkoevtmfr.supabase.co"; + const cleanPath = path.startsWith('/') ? path.slice(1) : path; + const separator = cleanPath.includes('?') ? '&' : '?'; + return `${baseUrl}/storage/v1/object/avatars/${cleanPath}${separator}t=${new Date().getTime()}`; + }; + useEffect(() => { const userInfoString = localStorage.getItem("user_info"); const token = localStorage.getItem("token"); if (userInfoString && token) { - const userInfo = JSON.parse(userInfoString); + try { + const userInfo = JSON.parse(userInfoString); + + // 1. Tenta pegar o avatar do cache local + let rawAvatarPath = + userInfo.profile?.avatar_url || + userInfo.user_metadata?.avatar_url || + userInfo.app_metadata?.avatar_url || + ""; + + // Configura estado inicial com o que tem no cache + setUserData({ + id: userInfo.id ?? "", + email: userInfo.email ?? "", + app_metadata: { + user_role: userInfo.app_metadata?.user_role ?? "patient", + }, + user_metadata: { + cpf: userInfo.user_metadata?.cpf ?? "", + email_verified: userInfo.user_metadata?.email_verified ?? false, + full_name: userInfo.user_metadata?.full_name || userInfo.profile?.full_name || "Usuário", + phone_mobile: userInfo.user_metadata?.phone_mobile ?? "", + role: userInfo.user_metadata?.role ?? "", + avatar_url: rawAvatarPath, + }, + identities: userInfo.identities ?? [], + is_anonymous: userInfo.is_anonymous ?? false, + }); + + setRole(userInfo.user_metadata?.role); + + if (rawAvatarPath) { + setAvatarFullUrl(buildAvatarUrl(rawAvatarPath)); + } + + // 2. AUTO-REPARO: Se não tiver avatar ou profile no cache, busca na API e atualiza + if (!rawAvatarPath || !userInfo.profile) { + console.log("[Sidebar] Cache incompleto. Buscando dados frescos..."); + usersService.getMe().then((freshData) => { + if (freshData && freshData.profile) { + const freshAvatar = freshData.profile.avatar_url; + + // Atualiza o objeto local + const updatedUserInfo = { + ...userInfo, + profile: freshData.profile, // Injeta o profile completo + user_metadata: { + ...userInfo.user_metadata, + avatar_url: freshAvatar || userInfo.user_metadata.avatar_url + } + }; + + // Salva no localStorage para a próxima vez + localStorage.setItem("user_info", JSON.stringify(updatedUserInfo)); + console.log("[Sidebar] LocalStorage sincronizado com sucesso."); + + // Atualiza visualmente se achou um avatar novo + if (freshAvatar && freshAvatar !== rawAvatarPath) { + setAvatarFullUrl(buildAvatarUrl(freshAvatar)); + // Atualiza o userData também para refletir no tooltip + setUserData(prev => prev ? ({ + ...prev, + user_metadata: { + ...prev.user_metadata, + avatar_url: freshAvatar + } + }) : undefined); + } + } + }).catch(err => console.error("[Sidebar] Falha no auto-reparo:", err)); + } + + } catch (e) { + console.error("Erro ao processar dados do usuário na Sidebar:", e); + } - setUserData({ - id: userInfo.id ?? "", - email: userInfo.email ?? "", - app_metadata: { - user_role: userInfo.app_metadata?.user_role ?? "patient", - }, - user_metadata: { - cpf: userInfo.user_metadata?.cpf ?? "", - email_verified: userInfo.user_metadata?.email_verified ?? false, - full_name: userInfo.user_metadata?.full_name ?? "", - phone_mobile: userInfo.user_metadata?.phone_mobile ?? "", - role: userInfo.user_metadata?.role ?? "", - }, - identities: - userInfo.identities?.map((identity: any) => ({ - identity_id: identity.identity_id ?? "", - id: identity.id ?? "", - user_id: identity.user_id ?? "", - provider: identity.provider ?? "", - })) ?? [], - is_anonymous: userInfo.is_anonymous ?? false, - }); - setRole(userInfo.user_metadata?.role); } else { router.push("/login"); } @@ -129,6 +191,7 @@ export default function Sidebar({ children }: SidebarProps) { try { await api.logout(); } catch (error) { + console.error("Erro ao fazer logout", error); } finally { localStorage.removeItem("user_info"); localStorage.removeItem("token"); @@ -188,14 +251,14 @@ export default function Sidebar({ children }: SidebarProps) { }, ]; - const managerItems: MenuItem[] = [ - { href: "/manager/dashboard", icon: Home, label: "Dashboard" }, - { href: "/manager/usuario", icon: Users, label: "Gestão de Usuários" }, - { href: "/manager/home", icon: Stethoscope, label: "Gestão de Médicos" }, - { href: "/manager/pacientes", icon: Users, label: "Gestão de Pacientes" }, - { href: "/secretary/appointments", icon: CalendarCheck2, label: "Consultas" }, - { href: "/manager/disponibilidade", icon: ClipboardList, label: "Disponibilidade" }, - ]; + const managerItems: MenuItem[] = [ + { href: "/manager/dashboard", icon: Home, label: "Dashboard" }, + { href: "/manager/usuario", icon: Users, label: "Gestão de Usuários" }, + { href: "/manager/home", icon: Stethoscope, label: "Gestão de Médicos" }, + { href: "/manager/pacientes", icon: Users, label: "Gestão de Pacientes" }, + { href: "/secretary/appointments", icon: CalendarCheck2, label: "Consultas" }, + { href: "/manager/disponibilidade", icon: ClipboardList, label: "Disponibilidade" }, + ]; switch (role) { case "gestor": @@ -295,6 +358,7 @@ export default function Sidebar({ children }: SidebarProps) { sidebarCollapsed={sidebarCollapsed} handleLogout={handleLogout} isActive={role !== "paciente"} + avatarUrl={avatarFullUrl} /> @@ -328,4 +392,4 @@ export default function Sidebar({ children }: SidebarProps) {   ); -} +} \ No newline at end of file diff --git a/components/ui/userToolTip.tsx b/components/ui/userToolTip.tsx index f579653..9d1fabf 100644 --- a/components/ui/userToolTip.tsx +++ b/components/ui/userToolTip.tsx @@ -33,6 +33,7 @@ interface Props { sidebarCollapsed: boolean; handleLogout: () => void; isActive: boolean; + avatarUrl?: string; } export default function SidebarUserSection({ @@ -40,6 +41,7 @@ export default function SidebarUserSection({ sidebarCollapsed, handleLogout, isActive, + avatarUrl, }: Props) { const pathname = usePathname(); const menuItems: any[] = [ @@ -56,6 +58,18 @@ export default function SidebarUserSection({ { href: "/patient/reports", icon: ClipboardPlus, label: "Meus Laudos" }, { href: "/patient/profile", icon: SquareUser, label: "Meus Dados" }, ]; + + // Função auxiliar para obter iniciais + const getInitials = (name: string) => { + if (!name) return "U"; + return name + .split(" ") + .map((n) => n[0]) + .slice(0, 2) + .join("") + .toUpperCase(); + }; + return (
{/* POPUP DE INFORMAÇÕES DO USUÁRIO */} @@ -67,12 +81,13 @@ export default function SidebarUserSection({ }`} > - - - {userData.user_metadata.full_name - .split(" ") - .map((n) => n[0]) - .join("")} + + + {getInitials(userData.user_metadata.full_name)} @@ -140,4 +155,4 @@ export default function SidebarUserSection({    
); -} +} \ No newline at end of file