From d21ed347158912ad712ad947ddff2cbb605d1332 Mon Sep 17 00:00:00 2001 From: pedrogomes5913 Date: Thu, 23 Oct 2025 14:07:32 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20upload/download=20de=20avatar=20e=20cor?= =?UTF-8?q?re=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- susconecta/app/paciente/page.tsx | 42 ++++--- susconecta/components/ui/upload-avatar.tsx | 132 +++++++++++++++++++++ susconecta/lib/api.ts | 28 ++++- 3 files changed, 180 insertions(+), 22 deletions(-) create mode 100644 susconecta/components/ui/upload-avatar.tsx diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx index 8bd8e24..5287b3d 100644 --- a/susconecta/app/paciente/page.tsx +++ b/susconecta/app/paciente/page.tsx @@ -1,7 +1,7 @@ 'use client' -// import { useAuth } from '@/hooks/useAuth' // removido duplicado import { useState } from 'react' +import type { ReactNode } from 'react' import { useRouter } from 'next/navigation' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog' @@ -12,6 +12,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' @@ -73,7 +74,8 @@ export default function PacientePage() { // Estado para edição do perfil const [isEditingProfile, setIsEditingProfile] = useState(false) - const [profileData, setProfileData] = useState({ + const [profileData, setProfileData] = useState({ + id: user?.id || '', nome: "Maria Silva Santos", email: user?.email || "paciente@example.com", telefone: "(11) 99999-9999", @@ -81,10 +83,23 @@ export default function PacientePage() { cidade: "São Paulo", cep: "01234-567", biografia: "Paciente desde 2020. Histórico de consultas e exames regulares.", + foto_url: user?.profile?.foto_url }) - const handleProfileChange = (field: string, value: string) => { - setProfileData(prev => ({ ...prev, [field]: value })) + interface ProfileData { + id: string; + nome: string; + email: string; + telefone: string; + endereco: string; + cidade: string; + cep: string; + biografia: string; + foto_url?: string; + } + + const handleProfileChange = (field: keyof ProfileData | string, value: any) => { + setProfileData((prev: ProfileData) => ({ ...prev, [field]: value })) } const handleSaveProfile = () => { setIsEditingProfile(false) @@ -674,19 +689,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 703ccd9..c6b198f 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -2597,8 +2597,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(); @@ -2614,6 +2616,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, @@ -2623,10 +2632,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 @@ -2635,7 +2653,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 }; }