diff --git a/susconecta/app/(main-routes)/perfil/loading.tsx b/susconecta/app/(main-routes)/perfil/loading.tsx new file mode 100644 index 0000000..9b7a1af --- /dev/null +++ b/susconecta/app/(main-routes)/perfil/loading.tsx @@ -0,0 +1,34 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function PerfillLoading() { + return ( +
+
+ +
+ + +
+
+ +
+
+ +
+ + + +
+
+ +
+ +
+ + +
+
+
+
+ ); +} diff --git a/susconecta/app/(main-routes)/perfil/page.tsx b/susconecta/app/(main-routes)/perfil/page.tsx new file mode 100644 index 0000000..2db4cf9 --- /dev/null +++ b/susconecta/app/(main-routes)/perfil/page.tsx @@ -0,0 +1,653 @@ +"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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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(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 ( +
+
+ + + + Você não tem permissão para acessar esta página. + + + +
+
+ ); + } + + if (loading) { + return ( +
+
+
+
+
+
+
+
+ ); + } + + if (error) { + return ( +
+
+ + + {error} + + +
+
+ ); + } + + if (!userInfo) { + return ( +
+
+ + + + Nenhuma informação de perfil disponível. + + +
+
+ ); + } + + 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 ( +
+
+
+ {/* Header com Título e Botão */} +
+
+

Meu Perfil

+

Bem-vindo à sua área exclusiva.

+
+ {!isEditing ? ( + + ) : ( +
+ + +
+ )} +
+ + {/* Grid de 2 colunas */} +
+ {/* Coluna Esquerda - Informações Pessoais */} +
+ {/* Informações Pessoais */} +
+

Informações Pessoais

+ +
+ {/* Nome Completo */} +
+ + {isEditing ? ( + setEditingData({...editingData, full_name: e.target.value})} + className="mt-2" + /> + ) : ( + <> +
+ {userInfo.profile?.full_name || "Não preenchido"} +
+

+ Este campo não pode ser alterado +

+ + )} +
+ + {/* Email */} +
+ +
+ {userInfo.user.email} +
+

+ Este campo não pode ser alterado +

+
+ + {/* UUID */} +
+ +
+ {userInfo.user.id} +
+

+ Este campo não pode ser alterado +

+
+ + {/* Permissões */} +
+ +
+ {userInfo.roles && userInfo.roles.length > 0 ? ( + userInfo.roles.map((role) => ( + + {role} + + )) + ) : ( + + Nenhuma permissão atribuída + + )} +
+
+
+
+ + {/* Endereço e Contato */} +
+

Endereço e Contato

+ +
+ {/* Telefone */} +
+ + {isEditing ? ( + handlePhoneChange(e.target.value)} + className="mt-2" + placeholder="(00) 00000-0000" + maxLength={15} + /> + ) : ( +
+ {userInfo.profile?.phone || "Não preenchido"} +
+ )} +
+ + {/* Endereço */} +
+ + {isEditing ? ( + setEditingData({...editingData, street: e.target.value})} + className="mt-2" + placeholder="Rua, avenida, etc." + /> + ) : ( +
+ {userInfo.profile?.street || "Não preenchido"} +
+ )} +
+ + {/* Número */} +
+ + {isEditing ? ( + setEditingData({...editingData, number: e.target.value})} + className="mt-2" + placeholder="123" + /> + ) : ( +
+ {userInfo.profile?.number || "Não preenchido"} +
+ )} +
+ + {/* Complemento */} +
+ + {isEditing ? ( + setEditingData({...editingData, complement: e.target.value})} + className="mt-2" + placeholder="Apto 42, Bloco B, etc." + /> + ) : ( +
+ {userInfo.profile?.complement || "Não preenchido"} +
+ )} +
+ + {/* Bairro */} +
+ + {isEditing ? ( + setEditingData({...editingData, neighborhood: e.target.value})} + className="mt-2" + placeholder="Vila, bairro, etc." + /> + ) : ( +
+ {userInfo.profile?.neighborhood || "Não preenchido"} +
+ )} +
+ + {/* Cidade */} +
+ + {isEditing ? ( + setEditingData({...editingData, city: e.target.value})} + className="mt-2" + placeholder="São Paulo" + /> + ) : ( +
+ {userInfo.profile?.city || "Não preenchido"} +
+ )} +
+ + {/* Estado */} +
+ + {isEditing ? ( + setEditingData({...editingData, state: e.target.value})} + className="mt-2" + placeholder="SP" + maxLength={2} + /> + ) : ( +
+ {userInfo.profile?.state || "Não preenchido"} +
+ )} +
+ + {/* CEP */} +
+ + {isEditing ? ( +
+
+
+ handleCepChange(e.target.value)} + className="mt-2" + placeholder="00000-000" + maxLength={9} + disabled={cepLoading} + /> +
+ {cepValid === true && ( + + )} + {cepValid === false && ( + + )} +
+ {cepLoading && ( +

Buscando CEP...

+ )} + {cepValid === false && ( +

CEP inválido ou não encontrado

+ )} + {cepValid === true && ( +

✓ CEP preenchido com sucesso

+ )} +
+ ) : ( +
+ {userInfo.profile?.cep || "Não preenchido"} +
+ )} +
+
+
+
+ + {/* Coluna Direita - Foto do Perfil */} +
+
+

Foto do Perfil

+ + {isEditing ? ( +
+ setEditingData({...editingData, avatar_url: newUrl})} + userName={editingData.full_name || userInfo.profile?.full_name || "Usuário"} + /> +
+ ) : ( +
+ + + + {getInitials(userInfo.profile?.full_name)} + + + +
+

+ {getInitials(userInfo.profile?.full_name)} +

+
+
+ )} + + {/* Informações de Status */} +
+
+ +
+ + {userInfo.profile?.disabled ? "Desabilitado" : "Ativo"} + +
+
+
+
+
+
+ + {/* Botão Voltar */} +
+ +
+
+
+
+ ); +} diff --git a/susconecta/components/dashboard/header.tsx b/susconecta/components/dashboard/header.tsx index 0872b66..002d16e 100644 --- a/susconecta/components/dashboard/header.tsx +++ b/susconecta/components/dashboard/header.tsx @@ -6,11 +6,13 @@ 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(null); @@ -84,7 +86,14 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
-