diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx
index 52f2714..30e7f61 100644
--- a/susconecta/app/paciente/page.tsx
+++ b/susconecta/app/paciente/page.tsx
@@ -1,6 +1,7 @@
'use client'
-// import { useAuth } from '@/hooks/useAuth' // removido duplicado
+import { useState } from 'react'
+import type { ReactNode } from 'react'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
@@ -12,6 +13,7 @@ import { Textarea } from '@/components/ui/textarea'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { User, LogOut, Calendar, FileText, MessageCircle, UserCog, Home, Clock, FolderOpen, ChevronLeft, ChevronRight, MapPin, Stethoscope } from 'lucide-react'
import { SimpleThemeToggle } from '@/components/simple-theme-toggle'
+import { UploadAvatar } from '@/components/ui/upload-avatar'
import Link from 'next/link'
import ProtectedRoute from '@/components/ProtectedRoute'
import { useAuth } from '@/hooks/useAuth'
@@ -743,19 +745,12 @@ export default function PacientePage() {
{/* Foto do Perfil */}
Foto do Perfil
-
-
-
- {profileData.nome.split(' ').map(n => n[0]).join('').toUpperCase()}
-
-
- {isEditingProfile && (
-
-
-
Formatos aceitos: JPG, PNG (máx. 2MB)
-
- )}
-
+
handleProfileChange('foto_url', newUrl)}
+ userName={profileData.nome}
+ />
)
diff --git a/susconecta/components/ui/upload-avatar.tsx b/susconecta/components/ui/upload-avatar.tsx
new file mode 100644
index 0000000..f2e7a72
--- /dev/null
+++ b/susconecta/components/ui/upload-avatar.tsx
@@ -0,0 +1,132 @@
+"use client"
+
+import React, { useState } from 'react'
+import { Button } from './button'
+import { Input } from './input'
+import { Avatar, AvatarFallback, AvatarImage } from './avatar'
+import { Upload, Download } from 'lucide-react'
+import { uploadFotoPaciente } from '@/lib/api'
+
+interface UploadAvatarProps {
+ userId: string
+ currentAvatarUrl?: string
+ onAvatarChange?: (newUrl: string) => void
+ userName?: string
+ className?: string
+}
+
+export function UploadAvatar({ userId, currentAvatarUrl, onAvatarChange, userName }: UploadAvatarProps) {
+ const [isUploading, setIsUploading] = useState(false)
+ const [error, setError] = useState('')
+
+ const handleUpload = async (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0]
+ if (!file) return
+
+ try {
+ setIsUploading(true)
+ setError('')
+
+ console.debug('[UploadAvatar] Iniciando upload:', {
+ fileName: file.name,
+ fileType: file.type,
+ fileSize: file.size,
+ userId
+ })
+
+ const result = await uploadFotoPaciente(userId, file)
+
+ if (result.foto_url) {
+ console.debug('[UploadAvatar] Upload concluído:', result)
+ onAvatarChange?.(result.foto_url)
+ }
+ } catch (err) {
+ console.error('[UploadAvatar] Erro no upload:', err)
+ setError(err instanceof Error ? err.message : 'Erro ao fazer upload do avatar')
+ } finally {
+ setIsUploading(false)
+ // Limpa o input para permitir selecionar o mesmo arquivo novamente
+ event.target.value = ''
+ }
+ }
+
+ const handleDownload = async () => {
+ if (!currentAvatarUrl) return
+
+ try {
+ const response = await fetch(currentAvatarUrl)
+ const blob = await response.blob()
+ const url = window.URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `avatar-${userId}.${blob.type.split('/')[1] || 'jpg'}`
+ document.body.appendChild(a)
+ a.click()
+ window.URL.revokeObjectURL(url)
+ document.body.removeChild(a)
+ } catch (err) {
+ setError('Erro ao baixar o avatar')
+ }
+ }
+
+ const initials = userName
+ ? userName.split(' ').map(n => n[0]).join('').toUpperCase()
+ : 'U'
+
+ return (
+
+
+
+
+
+ {initials}
+
+
+
+
+
+
+
+ {currentAvatarUrl && (
+
+ )}
+
+
+
+
+
+ Formatos aceitos: JPG, PNG, WebP (máx. 2MB)
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts
index 200d9d1..d779169 100644
--- a/susconecta/lib/api.ts
+++ b/susconecta/lib/api.ts
@@ -2676,8 +2676,10 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
};
const ext = extMap[_file.type] || 'jpg';
- const objectPath = `avatars/${userId}/avatar.${ext}`;
- const uploadUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/avatars/${encodeURIComponent(userId)}/avatar`;
+ // O bucket deve ser 'avatars' e o caminho do objeto será userId/avatar.ext
+ const bucket = 'avatars';
+ const objectPath = `${userId}/avatar.${ext}`;
+ const uploadUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/${bucket}/${encodeURIComponent(objectPath)}`;
// Build multipart form data
const form = new FormData();
@@ -2693,6 +2695,13 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
const jwt = getAuthToken();
if (jwt) headers.Authorization = `Bearer ${jwt}`;
+ console.debug('[uploadFotoPaciente] Iniciando upload:', {
+ url: uploadUrl,
+ fileType: _file.type,
+ fileSize: _file.size,
+ hasAuth: !!jwt
+ });
+
const res = await fetch(uploadUrl, {
method: 'POST',
headers,
@@ -2702,10 +2711,19 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
// Supabase storage returns 200/201 with object info or error
if (!res.ok) {
const raw = await res.text().catch(() => '');
- console.error('[uploadFotoPaciente] upload falhou', { status: res.status, raw });
+ console.error('[uploadFotoPaciente] upload falhou', {
+ status: res.status,
+ raw,
+ headers: Object.fromEntries(res.headers.entries()),
+ url: uploadUrl,
+ requestHeaders: headers,
+ objectPath
+ });
+
if (res.status === 401) throw new Error('Não autenticado');
if (res.status === 403) throw new Error('Sem permissão para fazer upload');
- throw new Error('Falha no upload da imagem');
+ if (res.status === 404) throw new Error('Bucket de avatars não encontrado. Verifique se o bucket "avatars" existe no Supabase');
+ throw new Error(`Falha no upload da imagem (${res.status}): ${raw || 'Sem detalhes do erro'}`);
}
// Try to parse JSON response
@@ -2714,7 +2732,7 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
// The API may not return a structured body; return the Key we constructed
const key = (json && (json.Key || json.key)) ?? objectPath;
- const publicUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/public/${encodeURIComponent('avatars')}/${encodeURIComponent(userId)}/avatar.${ext}`;
+ const publicUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/public/avatars/${encodeURIComponent(userId)}/avatar.${ext}`;
return { foto_url: publicUrl, Key: key };
}