Compare commits
3 Commits
f67ff8df8c
...
2c39f404d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c39f404d8 | ||
|
|
6a8a4af756 | ||
| d21ed34715 |
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
// import { useAuth } from '@/hooks/useAuth' // removido duplicado
|
|
||||||
|
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@ -12,6 +12,7 @@ import { Textarea } from '@/components/ui/textarea'
|
|||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
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 { User, LogOut, Calendar, FileText, MessageCircle, UserCog, Home, Clock, FolderOpen, ChevronLeft, ChevronRight, MapPin, Stethoscope } from 'lucide-react'
|
||||||
import { SimpleThemeToggle } from '@/components/simple-theme-toggle'
|
import { SimpleThemeToggle } from '@/components/simple-theme-toggle'
|
||||||
|
import { UploadAvatar } from '@/components/ui/upload-avatar'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import ProtectedRoute from '@/components/ProtectedRoute'
|
import ProtectedRoute from '@/components/ProtectedRoute'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
@ -73,7 +74,7 @@ export default function PacientePage() {
|
|||||||
|
|
||||||
// Estado para edição do perfil
|
// Estado para edição do perfil
|
||||||
const [isEditingProfile, setIsEditingProfile] = useState(false)
|
const [isEditingProfile, setIsEditingProfile] = useState(false)
|
||||||
const [profileData, setProfileData] = useState({
|
const [profileData, setProfileData] = useState<any>({
|
||||||
nome: '',
|
nome: '',
|
||||||
email: user?.email || '',
|
email: user?.email || '',
|
||||||
telefone: '',
|
telefone: '',
|
||||||
@ -81,6 +82,8 @@ export default function PacientePage() {
|
|||||||
cidade: '',
|
cidade: '',
|
||||||
cep: '',
|
cep: '',
|
||||||
biografia: '',
|
biografia: '',
|
||||||
|
id: undefined,
|
||||||
|
foto_url: undefined,
|
||||||
})
|
})
|
||||||
const [patientId, setPatientId] = useState<string | null>(null)
|
const [patientId, setPatientId] = useState<string | null>(null)
|
||||||
|
|
||||||
@ -216,7 +219,7 @@ export default function PacientePage() {
|
|||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') console.debug('[PacientePage] paciente row', paciente)
|
if (process.env.NODE_ENV !== 'production') console.debug('[PacientePage] paciente row', paciente)
|
||||||
|
|
||||||
setProfileData(prev => ({ ...prev, nome, email: emailFromRow, telefone, endereco, cidade, cep, biografia }))
|
setProfileData((prev: any) => ({ ...prev, nome, email: emailFromRow, telefone, endereco, cidade, cep, biografia }))
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[PacientePage] erro ao carregar paciente', err)
|
console.warn('[PacientePage] erro ao carregar paciente', err)
|
||||||
@ -231,7 +234,7 @@ export default function PacientePage() {
|
|||||||
}, [user?.id, user?.email])
|
}, [user?.id, user?.email])
|
||||||
|
|
||||||
const handleProfileChange = (field: string, value: string) => {
|
const handleProfileChange = (field: string, value: string) => {
|
||||||
setProfileData(prev => ({ ...prev, [field]: value }))
|
setProfileData((prev: any) => ({ ...prev, [field]: value }))
|
||||||
}
|
}
|
||||||
const handleSaveProfile = () => {
|
const handleSaveProfile = () => {
|
||||||
setIsEditingProfile(false)
|
setIsEditingProfile(false)
|
||||||
@ -743,19 +746,12 @@ export default function PacientePage() {
|
|||||||
{/* Foto do Perfil */}
|
{/* Foto do Perfil */}
|
||||||
<div className="border-t border-border pt-6">
|
<div className="border-t border-border pt-6">
|
||||||
<h3 className="text-lg font-semibold mb-4 text-foreground">Foto do Perfil</h3>
|
<h3 className="text-lg font-semibold mb-4 text-foreground">Foto do Perfil</h3>
|
||||||
<div className="flex items-center gap-4">
|
<UploadAvatar
|
||||||
<Avatar className="h-20 w-20">
|
userId={profileData.id}
|
||||||
<AvatarFallback className="text-lg">
|
currentAvatarUrl={profileData.foto_url}
|
||||||
{profileData.nome.split(' ').map(n => n[0]).join('').toUpperCase()}
|
onAvatarChange={(newUrl) => handleProfileChange('foto_url', newUrl)}
|
||||||
</AvatarFallback>
|
userName={profileData.nome}
|
||||||
</Avatar>
|
/>
|
||||||
{isEditingProfile && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Button variant="outline" size="sm">Alterar Foto</Button>
|
|
||||||
<p className="text-xs text-muted-foreground">Formatos aceitos: JPG, PNG (máx. 2MB)</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
132
susconecta/components/ui/upload-avatar.tsx
Normal file
132
susconecta/components/ui/upload-avatar.tsx
Normal file
@ -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<boolean>(false)
|
||||||
|
const [error, setError] = useState<string>('')
|
||||||
|
|
||||||
|
const handleUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Avatar className="h-20 w-20">
|
||||||
|
<AvatarImage src={currentAvatarUrl} alt={userName || 'Avatar'} />
|
||||||
|
<AvatarFallback className="text-lg">
|
||||||
|
{initials}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => document.getElementById('avatar-upload')?.click()}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
|
<Upload className="h-4 w-4 mr-2" />
|
||||||
|
{isUploading ? 'Enviando...' : 'Upload'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{currentAvatarUrl && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDownload}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="avatar-upload"
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
accept="image/jpeg,image/png,image/webp"
|
||||||
|
onChange={handleUpload}
|
||||||
|
disabled={isUploading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Formatos aceitos: JPG, PNG, WebP (máx. 2MB)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -2676,8 +2676,10 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
|
|||||||
};
|
};
|
||||||
const ext = extMap[_file.type] || 'jpg';
|
const ext = extMap[_file.type] || 'jpg';
|
||||||
|
|
||||||
const objectPath = `avatars/${userId}/avatar.${ext}`;
|
// O bucket deve ser 'avatars' e o caminho do objeto será userId/avatar.ext
|
||||||
const uploadUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/avatars/${encodeURIComponent(userId)}/avatar`;
|
const bucket = 'avatars';
|
||||||
|
const objectPath = `${userId}/avatar.${ext}`;
|
||||||
|
const uploadUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/${bucket}/${encodeURIComponent(objectPath)}`;
|
||||||
|
|
||||||
// Build multipart form data
|
// Build multipart form data
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
@ -2693,6 +2695,13 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
|
|||||||
const jwt = getAuthToken();
|
const jwt = getAuthToken();
|
||||||
if (jwt) headers.Authorization = `Bearer ${jwt}`;
|
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, {
|
const res = await fetch(uploadUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
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
|
// Supabase storage returns 200/201 with object info or error
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text().catch(() => '');
|
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 === 401) throw new Error('Não autenticado');
|
||||||
if (res.status === 403) throw new Error('Sem permissão para fazer upload');
|
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
|
// 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
|
// The API may not return a structured body; return the Key we constructed
|
||||||
const key = (json && (json.Key || json.key)) ?? objectPath;
|
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 };
|
return { foto_url: publicUrl, Key: key };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user