fix permissao Meus dados

This commit is contained in:
StsDanilo 2025-11-13 16:07:45 -03:00
parent 945e9b49cd
commit fcbcb9988f
3 changed files with 284 additions and 234 deletions

View File

@ -114,7 +114,7 @@ export default function NovoUsuarioPage() {
password: formData.senha, password: formData.senha,
full_name: formData.nomeCompleto, full_name: formData.nomeCompleto,
phone: formData.telefone || null, phone: formData.telefone || null,
role: formData.papel, roles: [formData.papel, "paciente"],
cpf: formData.cpf, cpf: formData.cpf,
create_patient_record: isPatient, create_patient_record: isPatient,
phone_mobile: isPatient ? formData.telefone || null : undefined, phone_mobile: isPatient ? formData.telefone || null : undefined,
@ -126,7 +126,7 @@ export default function NovoUsuarioPage() {
console.error("Erro ao criar usuário:", e); console.error("Erro ao criar usuário:", e);
// 3. MENSAGEM DE ERRO MELHORADA // 3. MENSAGEM DE ERRO MELHORADA
const detail = e.message?.split('detail:"')[1]?.split('"')[0] || e.message; const detail = e.message?.split('detail:"')[1]?.split('"')[0] || e.message;
setError(detail.replace(/\\/g, '') || "Não foi possível criar o usuário. Verifique os dados e tente novamente."); setError(detail.replace(/\\/g, "") || "Não foi possível criar o usuário. Verifique os dados e tente novamente.");
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }

View File

@ -3,7 +3,7 @@
"use client"; "use client";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import Sidebar from "@/components/Sidebar" import Sidebar from "@/components/Sidebar";
import { useAuthLayout } from "@/hooks/useAuthLayout"; import { useAuthLayout } from "@/hooks/useAuthLayout";
import { patientsService } from "@/services/patientsApi.mjs"; import { patientsService } from "@/services/patientsApi.mjs";
import { api } from "@/services/api.mjs"; import { api } from "@/services/api.mjs";
@ -31,7 +31,7 @@ interface PatientProfileData {
} }
export default function PatientProfile() { export default function PatientProfile() {
const { user, isLoading: isAuthLoading } = useAuthLayout({ requiredRole: 'patient' }); const { user, isLoading: isAuthLoading } = useAuthLayout({ requiredRole: ["paciente", "admin", "medico", "gestor", "secretaria"] });
const [patientData, setPatientData] = useState<PatientProfileData | null>(null); const [patientData, setPatientData] = useState<PatientProfileData | null>(null);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
@ -45,13 +45,13 @@ export default function PatientProfile() {
setPatientData({ setPatientData({
name: patientDetails.full_name || user.name, name: patientDetails.full_name || user.name,
email: user.email, email: user.email,
phone: patientDetails.phone_mobile || '', phone: patientDetails.phone_mobile || "",
cpf: patientDetails.cpf || '', cpf: patientDetails.cpf || "",
birthDate: patientDetails.birth_date || '', birthDate: patientDetails.birth_date || "",
cep: patientDetails.cep || '', cep: patientDetails.cep || "",
street: patientDetails.street || '', street: patientDetails.street || "",
number: patientDetails.number || '', number: patientDetails.number || "",
city: patientDetails.city || '', city: patientDetails.city || "",
avatarFullUrl: user.avatarFullUrl, avatarFullUrl: user.avatarFullUrl,
}); });
} catch (error) { } catch (error) {
@ -100,18 +100,18 @@ export default function PatientProfile() {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (!file || !user) return; if (!file || !user) return;
const fileExt = file.name.split('.').pop(); const fileExt = file.name.split(".").pop();
// *** A CORREÇÃO ESTÁ AQUI *** // *** A CORREÇÃO ESTÁ AQUI ***
// O caminho salvo no banco de dados não deve conter o nome do bucket. // O caminho salvo no banco de dados não deve conter o nome do bucket.
const filePath = `${user.id}/avatar.${fileExt}`; const filePath = `${user.id}/avatar.${fileExt}`;
try { try {
await api.storage.upload('avatars', filePath, file); await api.storage.upload("avatars", filePath, file);
await api.patch(`/rest/v1/profiles?id=eq.${user.id}`, { avatar_url: filePath }); await api.patch(`/rest/v1/profiles?id=eq.${user.id}`, { avatar_url: filePath });
const newFullUrl = `https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/public/avatars/${filePath}?t=${new Date().getTime()}`; const newFullUrl = `https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/public/avatars/${filePath}?t=${new Date().getTime()}`;
setPatientData(prev => prev ? { ...prev, avatarFullUrl: newFullUrl } : null); setPatientData((prev) => (prev ? { ...prev, avatarFullUrl: newFullUrl } : null));
toast({ title: "Sucesso!", description: "Sua foto de perfil foi atualizada." }); toast({ title: "Sucesso!", description: "Sua foto de perfil foi atualizada." });
} catch (error) { } catch (error) {
@ -121,7 +121,11 @@ export default function PatientProfile() {
}; };
if (isAuthLoading || !patientData) { if (isAuthLoading || !patientData) {
return <Sidebar><div>Carregando seus dados...</div></Sidebar>; return (
<Sidebar>
<div>Carregando seus dados...</div>
</Sidebar>
);
} }
return ( return (
@ -140,29 +144,66 @@ export default function PatientProfile() {
<div className="grid lg:grid-cols-3 gap-6"> <div className="grid lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6"> <div className="lg:col-span-2 space-y-6">
<Card> <Card>
<CardHeader><CardTitle className="flex items-center"><User className="mr-2 h-5 w-5" />Informações Pessoais</CardTitle></CardHeader> <CardHeader>
<CardTitle className="flex items-center">
<User className="mr-2 h-5 w-5" />
Informações Pessoais
</CardTitle>
</CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div><Label htmlFor="name">Nome Completo</Label><Input id="name" value={patientData.name} onChange={(e) => handleInputChange("name", e.target.value)} disabled={!isEditing} /></div> <div>
<div><Label htmlFor="cpf">CPF</Label><Input id="cpf" value={patientData.cpf} onChange={(e) => handleInputChange("cpf", e.target.value)} disabled={!isEditing} /></div> <Label htmlFor="name">Nome Completo</Label>
<Input id="name" value={patientData.name} onChange={(e) => handleInputChange("name", e.target.value)} disabled={!isEditing} />
</div>
<div>
<Label htmlFor="cpf">CPF</Label>
<Input id="cpf" value={patientData.cpf} onChange={(e) => handleInputChange("cpf", e.target.value)} disabled={!isEditing} />
</div>
</div>
<div>
<Label htmlFor="birthDate">Data de Nascimento</Label>
<Input id="birthDate" type="date" value={patientData.birthDate} onChange={(e) => handleInputChange("birthDate", e.target.value)} disabled={!isEditing} />
</div> </div>
<div><Label htmlFor="birthDate">Data de Nascimento</Label><Input id="birthDate" type="date" value={patientData.birthDate} onChange={(e) => handleInputChange("birthDate", e.target.value)} disabled={!isEditing} /></div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader><CardTitle className="flex items-center"><Mail className="mr-2 h-5 w-5" />Contato e Endereço</CardTitle></CardHeader> <CardHeader>
<CardTitle className="flex items-center">
<Mail className="mr-2 h-5 w-5" />
Contato e Endereço
</CardTitle>
</CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div><Label htmlFor="email">Email</Label><Input id="email" type="email" value={patientData.email} disabled /></div> <div>
<div><Label htmlFor="phone">Telefone</Label><Input id="phone" value={patientData.phone} onChange={(e) => handleInputChange("phone", e.target.value)} disabled={!isEditing} /></div> <Label htmlFor="email">Email</Label>
<Input id="email" type="email" value={patientData.email} disabled />
</div>
<div>
<Label htmlFor="phone">Telefone</Label>
<Input id="phone" value={patientData.phone} onChange={(e) => handleInputChange("phone", e.target.value)} disabled={!isEditing} />
</div>
</div> </div>
<div className="grid md:grid-cols-3 gap-4"> <div className="grid md:grid-cols-3 gap-4">
<div><Label htmlFor="cep">CEP</Label><Input id="cep" value={patientData.cep} onChange={(e) => handleInputChange("cep", e.target.value)} disabled={!isEditing} /></div> <div>
<div className="md:col-span-2"><Label htmlFor="street">Rua / Logradouro</Label><Input id="street" value={patientData.street} onChange={(e) => handleInputChange("street", e.target.value)} disabled={!isEditing} /></div> <Label htmlFor="cep">CEP</Label>
<Input id="cep" value={patientData.cep} onChange={(e) => handleInputChange("cep", e.target.value)} disabled={!isEditing} />
</div>
<div className="md:col-span-2">
<Label htmlFor="street">Rua / Logradouro</Label>
<Input id="street" value={patientData.street} onChange={(e) => handleInputChange("street", e.target.value)} disabled={!isEditing} />
</div>
</div> </div>
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div><Label htmlFor="number">Número</Label><Input id="number" value={patientData.number} onChange={(e) => handleInputChange("number", e.target.value)} disabled={!isEditing} /></div> <div>
<div><Label htmlFor="city">Cidade</Label><Input id="city" value={patientData.city} onChange={(e) => handleInputChange("city", e.target.value)} disabled={!isEditing} /></div> <Label htmlFor="number">Número</Label>
<Input id="number" value={patientData.number} onChange={(e) => handleInputChange("number", e.target.value)} disabled={!isEditing} />
</div>
<div>
<Label htmlFor="city">Cidade</Label>
<Input id="city" value={patientData.city} onChange={(e) => handleInputChange("city", e.target.value)} disabled={!isEditing} />
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -170,13 +211,20 @@ export default function PatientProfile() {
<div className="space-y-6"> <div className="space-y-6">
<Card> <Card>
<CardHeader><CardTitle>Resumo do Perfil</CardTitle></CardHeader> <CardHeader>
<CardTitle>Resumo do Perfil</CardTitle>
</CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="relative"> <div className="relative">
<Avatar className="w-16 h-16 cursor-pointer" onClick={handleAvatarClick}> <Avatar className="w-16 h-16 cursor-pointer" onClick={handleAvatarClick}>
<AvatarImage src={patientData.avatarFullUrl} /> <AvatarImage src={patientData.avatarFullUrl} />
<AvatarFallback className="text-2xl">{patientData.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback> <AvatarFallback className="text-2xl">
{patientData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar> </Avatar>
<div className="absolute bottom-0 right-0 bg-primary text-primary-foreground rounded-full p-1 cursor-pointer hover:bg-primary/80" onClick={handleAvatarClick}> <div className="absolute bottom-0 right-0 bg-primary text-primary-foreground rounded-full p-1 cursor-pointer hover:bg-primary/80" onClick={handleAvatarClick}>
<Upload className="w-3 h-3" /> <Upload className="w-3 h-3" />
@ -189,9 +237,18 @@ export default function PatientProfile() {
</div> </div>
</div> </div>
<div className="space-y-3 pt-4 border-t"> <div className="space-y-3 pt-4 border-t">
<div className="flex items-center text-sm"><Mail className="mr-2 h-4 w-4 text-gray-500" /><span className="truncate">{patientData.email}</span></div> <div className="flex items-center text-sm">
<div className="flex items-center text-sm"><Phone className="mr-2 h-4 w-4 text-gray-500" /><span>{patientData.phone || "Não informado"}</span></div> <Mail className="mr-2 h-4 w-4 text-gray-500" />
<div className="flex items-center text-sm"><Calendar className="mr-2 h-4 w-4 text-gray-500" /><span>{patientData.birthDate ? new Date(patientData.birthDate).toLocaleDateString("pt-BR", { timeZone: 'UTC' }) : "Não informado"}</span></div> <span className="truncate">{patientData.email}</span>
</div>
<div className="flex items-center text-sm">
<Phone className="mr-2 h-4 w-4 text-gray-500" />
<span>{patientData.phone || "Não informado"}</span>
</div>
<div className="flex items-center text-sm">
<Calendar className="mr-2 h-4 w-4 text-gray-500" />
<span>{patientData.birthDate ? new Date(patientData.birthDate).toLocaleDateString("pt-BR", { timeZone: "UTC" }) : "Não informado"}</span>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -199,5 +256,5 @@ export default function PatientProfile() {
</div> </div>
</div> </div>
</Sidebar> </Sidebar>
) );
} }

View File

@ -1,8 +1,8 @@
// ARQUIVO COMPLETO PARA: hooks/useAuthLayout.ts // ARQUIVO COMPLETO PARA: hooks/useAuthLayout.ts
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { useRouter } from 'next/navigation'; import { useRouter } from "next/navigation";
import { usersService } from '@/services/usersApi.mjs'; import { usersService } from "@/services/usersApi.mjs";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/use-toast";
interface UserLayoutData { interface UserLayoutData {
@ -15,7 +15,7 @@ interface UserLayoutData {
} }
interface UseAuthLayoutOptions { interface UseAuthLayoutOptions {
requiredRole?: string; requiredRole?: string[];
} }
export function useAuthLayout({ requiredRole }: UseAuthLayoutOptions = {}) { export function useAuthLayout({ requiredRole }: UseAuthLayoutOptions = {}) {
@ -28,18 +28,14 @@ export function useAuthLayout({ requiredRole }: UseAuthLayoutOptions = {}) {
try { try {
const fullUserData = await usersService.getMe(); const fullUserData = await usersService.getMe();
if ( if (!fullUserData.roles.some((role) => requiredRole?.includes(role))) {
requiredRole && console.error(`Acesso negado. Requer perfil '${requiredRole}', mas o usuário tem '${fullUserData.roles.join(", ")}'.`);
!fullUserData.roles.includes(requiredRole) &&
!fullUserData.roles.includes('admin')
) {
console.error(`Acesso negado. Requer perfil '${requiredRole}', mas o usuário tem '${fullUserData.roles.join(', ')}'.`);
toast({ toast({
title: "Acesso Negado", title: "Acesso Negado",
description: "Você não tem permissão para acessar esta página.", description: "Você não tem permissão para acessar esta página.",
variant: "destructive", variant: "destructive",
}); });
router.push('/'); router.push("/");
return; return;
} }
@ -47,19 +43,16 @@ export function useAuthLayout({ requiredRole }: UseAuthLayoutOptions = {}) {
// *** A CORREÇÃO ESTÁ AQUI *** // *** A CORREÇÃO ESTÁ AQUI ***
// Adicionamos o nome do bucket 'avatars' na URL final. // Adicionamos o nome do bucket 'avatars' na URL final.
const avatarFullUrl = avatarPath const avatarFullUrl = avatarPath ? `https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/public/avatars/${avatarPath}` : undefined;
? `https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/public/avatars/${avatarPath}`
: undefined;
setUser({ setUser({
id: fullUserData.user.id, id: fullUserData.user.id,
name: fullUserData.profile.full_name || 'Usuário', name: fullUserData.profile.full_name || "Usuário",
email: fullUserData.user.email, email: fullUserData.user.email,
roles: fullUserData.roles, roles: fullUserData.roles,
avatar_url: avatarPath, avatar_url: avatarPath,
avatarFullUrl: avatarFullUrl, avatarFullUrl: avatarFullUrl,
}); });
} catch (error) { } catch (error) {
console.error("Falha na autenticação do layout:", error); console.error("Falha na autenticação do layout:", error);
router.push("/login"); router.push("/login");