Compare commits
No commits in common. "6e3c11c5d359e839c1942c81bc64903ea4600009" and "da8ee7244b4be4e18b5837c7c225eddadc1c2e44" have entirely different histories.
6e3c11c5d3
...
da8ee7244b
@ -6,8 +6,6 @@ import Link from "next/link";
|
|||||||
import ProtectedRoute from "@/components/shared/ProtectedRoute";
|
import ProtectedRoute from "@/components/shared/ProtectedRoute";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useAvatarUrl } from "@/hooks/useAvatarUrl";
|
|
||||||
import { UploadAvatar } from '@/components/ui/upload-avatar';
|
|
||||||
import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, listarAgendamentos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api";
|
import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, listarAgendamentos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api";
|
||||||
import { ENV_CONFIG } from '@/lib/env-config';
|
import { ENV_CONFIG } from '@/lib/env-config';
|
||||||
import { useReports } from "@/hooks/useReports";
|
import { useReports } from "@/hooks/useReports";
|
||||||
@ -117,7 +115,6 @@ const colorsByType = {
|
|||||||
|
|
||||||
const ProfissionalPage = () => {
|
const ProfissionalPage = () => {
|
||||||
const { logout, user, token } = useAuth();
|
const { logout, user, token } = useAuth();
|
||||||
const { toast } = useToast();
|
|
||||||
const [activeSection, setActiveSection] = useState('calendario');
|
const [activeSection, setActiveSection] = useState('calendario');
|
||||||
const [pacienteSelecionado, setPacienteSelecionado] = useState<any>(null);
|
const [pacienteSelecionado, setPacienteSelecionado] = useState<any>(null);
|
||||||
|
|
||||||
@ -128,9 +125,6 @@ const ProfissionalPage = () => {
|
|||||||
// Estados para o perfil do médico
|
// Estados para o perfil do médico
|
||||||
const [isEditingProfile, setIsEditingProfile] = useState(false);
|
const [isEditingProfile, setIsEditingProfile] = useState(false);
|
||||||
const [doctorId, setDoctorId] = useState<string | null>(null);
|
const [doctorId, setDoctorId] = useState<string | null>(null);
|
||||||
|
|
||||||
// Hook para carregar automaticamente o avatar do médico
|
|
||||||
const { avatarUrl: retrievedAvatarUrl } = useAvatarUrl(doctorId);
|
|
||||||
// Removemos o placeholder extenso — inicializamos com valores minimalistas e vazios.
|
// Removemos o placeholder extenso — inicializamos com valores minimalistas e vazios.
|
||||||
const [profileData, setProfileData] = useState({
|
const [profileData, setProfileData] = useState({
|
||||||
nome: '',
|
nome: '',
|
||||||
@ -273,15 +267,6 @@ const ProfissionalPage = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [user?.email, user?.id]);
|
}, [user?.email, user?.id]);
|
||||||
|
|
||||||
// Sincroniza a URL do avatar recuperada com o profileData
|
|
||||||
useEffect(() => {
|
|
||||||
if (retrievedAvatarUrl) {
|
|
||||||
setProfileData(prev => ({
|
|
||||||
...prev,
|
|
||||||
fotoUrl: retrievedAvatarUrl
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [retrievedAvatarUrl]);
|
|
||||||
|
|
||||||
|
|
||||||
// Estados para campos principais da consulta
|
// Estados para campos principais da consulta
|
||||||
@ -2981,42 +2966,17 @@ const ProfissionalPage = () => {
|
|||||||
<h3 className="text-base sm:text-lg font-semibold mb-4">Foto do Perfil</h3>
|
<h3 className="text-base sm:text-lg font-semibold mb-4">Foto do Perfil</h3>
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
{isEditingProfile ? (
|
<Avatar className="h-20 w-20 sm:h-24 sm:w-24">
|
||||||
<UploadAvatar
|
<AvatarFallback className="bg-primary text-primary-foreground text-lg sm:text-2xl font-bold">
|
||||||
userId={String(doctorId || (user && (user as any).id) || '')}
|
{profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'MD'}
|
||||||
currentAvatarUrl={(profileData as any).fotoUrl}
|
</AvatarFallback>
|
||||||
userName={(profileData as any).nome}
|
</Avatar>
|
||||||
onAvatarChange={async (newUrl: string) => {
|
|
||||||
try {
|
|
||||||
setProfileData((prev) => ({ ...prev, fotoUrl: newUrl }));
|
|
||||||
// Foto foi salva no Supabase Storage - atualizar apenas o estado local
|
|
||||||
// Para persistir no banco, o usuário deve clicar em "Salvar" após isso
|
|
||||||
try { toast({ title: 'Foto enviada', description: 'Clique em "Salvar" para confirmar as alterações.', variant: 'default' }); } catch (e) { /* ignore toast errors */ }
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[ProfissionalPage] erro ao processar upload de foto:', err);
|
|
||||||
try { toast({ title: 'Erro ao processar foto', description: (err as any)?.message || 'Falha ao processar a foto do perfil.', variant: 'destructive' }); } catch (e) {}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Avatar className="h-20 w-20 sm:h-24 sm:w-24">
|
|
||||||
{(profileData as any).fotoUrl ? (
|
|
||||||
<AvatarImage src={(profileData as any).fotoUrl} alt={(profileData as any).nome} />
|
|
||||||
) : (
|
|
||||||
<AvatarFallback className="bg-primary text-primary-foreground text-lg sm:text-2xl font-bold">
|
|
||||||
{profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'MD'}
|
|
||||||
</AvatarFallback>
|
|
||||||
)}
|
|
||||||
</Avatar>
|
|
||||||
|
|
||||||
<div className="text-center space-y-2">
|
<div className="text-center space-y-2">
|
||||||
<p className="text-xs sm:text-sm text-muted-foreground">
|
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||||
{profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'MD'}
|
{profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'MD'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -30,7 +30,6 @@ import {
|
|||||||
criarPaciente,
|
criarPaciente,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
import { getAvatarPublicUrl } from '@/lib/api';
|
import { getAvatarPublicUrl } from '@/lib/api';
|
||||||
import { useAvatarUrl } from '@/hooks/useAvatarUrl';
|
|
||||||
|
|
||||||
import { validarCPFLocal } from "@/lib/utils";
|
import { validarCPFLocal } from "@/lib/utils";
|
||||||
import { verificarCpfDuplicado } from "@/lib/api";
|
import { verificarCpfDuplicado } from "@/lib/api";
|
||||||
@ -132,9 +131,6 @@ export function PatientRegistrationForm({
|
|||||||
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
||||||
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
|
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
|
||||||
|
|
||||||
// Hook para carregar automaticamente o avatar do paciente
|
|
||||||
const { avatarUrl: retrievedAvatarUrl } = useAvatarUrl(mode === "edit" ? patientId : null);
|
|
||||||
|
|
||||||
const [showCredentialsDialog, setShowCredentialsDialog] = useState(false);
|
const [showCredentialsDialog, setShowCredentialsDialog] = useState(false);
|
||||||
const [credentials, setCredentials] = useState<{
|
const [credentials, setCredentials] = useState<{
|
||||||
email: string;
|
email: string;
|
||||||
@ -265,14 +261,7 @@ export function PatientRegistrationForm({
|
|||||||
if (patientId == null) throw new Error("Paciente inexistente para edição");
|
if (patientId == null) throw new Error("Paciente inexistente para edição");
|
||||||
const payload = toPayload(); const saved = await atualizarPaciente(String(patientId), payload);
|
const payload = toPayload(); const saved = await atualizarPaciente(String(patientId), payload);
|
||||||
if (form.photo) {
|
if (form.photo) {
|
||||||
try {
|
try { setUploadingPhoto(true); try { await removerFotoPaciente(String(patientId)); setPhotoPreview(null); } catch (remErr) { console.warn('[PatientForm] aviso: falha ao remover avatar antes do upload:', remErr); } await uploadFotoPaciente(String(patientId), form.photo); }
|
||||||
setUploadingPhoto(true);
|
|
||||||
try { await removerFotoPaciente(String(patientId)); setPhotoPreview(null); } catch (remErr) { console.warn('[PatientForm] aviso: falha ao remover avatar antes do upload:', remErr); }
|
|
||||||
const uploadResult = await uploadFotoPaciente(String(patientId), form.photo);
|
|
||||||
// Upload realizado com sucesso - a foto está armazenada no Supabase Storage
|
|
||||||
// Não é necessário fazer PATCH para persistir a URL no banco
|
|
||||||
console.debug('[PatientForm] foto_url obtida do upload:', uploadResult.foto_url);
|
|
||||||
}
|
|
||||||
catch (upErr) { console.warn('[PatientForm] Falha ao enviar foto do paciente:', upErr); alert('Paciente atualizado, mas falha ao enviar a foto. Tente novamente.'); }
|
catch (upErr) { console.warn('[PatientForm] Falha ao enviar foto do paciente:', upErr); alert('Paciente atualizado, mas falha ao enviar a foto. Tente novamente.'); }
|
||||||
finally { setUploadingPhoto(false); }
|
finally { setUploadingPhoto(false); }
|
||||||
}
|
}
|
||||||
@ -366,15 +355,7 @@ export function PatientRegistrationForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (form.photo) {
|
if (form.photo) {
|
||||||
try {
|
try { setUploadingPhoto(true); const pacienteId = savedPatientProfile?.id || (savedPatientProfile && (savedPatientProfile as any).id); if (pacienteId) await uploadFotoPaciente(String(pacienteId), form.photo); }
|
||||||
setUploadingPhoto(true);
|
|
||||||
const pacienteId = savedPatientProfile?.id || (savedPatientProfile && (savedPatientProfile as any).id);
|
|
||||||
if (pacienteId) {
|
|
||||||
const uploadResult = await uploadFotoPaciente(String(pacienteId), form.photo);
|
|
||||||
// Upload realizado com sucesso - a foto está armazenada no Supabase Storage
|
|
||||||
console.debug('[PatientForm] foto_url obtida do upload após criação:', uploadResult.foto_url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (upErr) { console.warn('[PatientForm] Falha ao enviar foto do paciente após criação:', upErr); alert('Paciente criado, mas falha ao enviar a foto. Você pode tentar novamente no perfil.'); }
|
catch (upErr) { console.warn('[PatientForm] Falha ao enviar foto do paciente após criação:', upErr); alert('Paciente criado, mas falha ao enviar a foto. Você pode tentar novamente no perfil.'); }
|
||||||
finally { setUploadingPhoto(false); }
|
finally { setUploadingPhoto(false); }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,106 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { getAvatarPublicUrl } from '@/lib/api'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook que gerencia a URL do avatar de um usuário
|
|
||||||
* Recupera automaticamente a URL baseada no userId
|
|
||||||
* Tenta múltiplas extensões (jpg, png, webp) até encontrar o arquivo
|
|
||||||
* @param userId - ID do usuário (string ou number)
|
|
||||||
* @returns { avatarUrl: string | null, isLoading: boolean }
|
|
||||||
*/
|
|
||||||
export function useAvatarUrl(userId: string | number | null | undefined) {
|
|
||||||
const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!userId) {
|
|
||||||
console.debug('[useAvatarUrl] userId é vazio, limpando avatar')
|
|
||||||
setAvatarUrl(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
const extensions = ['jpg', 'png', 'webp']
|
|
||||||
let foundUrl: string | null = null
|
|
||||||
let testedExtensions = 0
|
|
||||||
|
|
||||||
const tryNextExtension = () => {
|
|
||||||
const ext = extensions[testedExtensions]
|
|
||||||
if (!ext) {
|
|
||||||
// Nenhuma extensão funcionou
|
|
||||||
console.warn('[useAvatarUrl] Nenhuma extensão de avatar encontrada para userId:', userId)
|
|
||||||
setAvatarUrl(null)
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = getAvatarPublicUrl(userId, ext)
|
|
||||||
console.debug('[useAvatarUrl] Testando extensão:', { userId, ext, url })
|
|
||||||
|
|
||||||
// Valida se a imagem existe fazendo um HEAD request
|
|
||||||
fetch(url, { method: 'HEAD', mode: 'cors' })
|
|
||||||
.then((response) => {
|
|
||||||
console.debug('[useAvatarUrl] HEAD response:', {
|
|
||||||
userId,
|
|
||||||
ext,
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
contentType: response.headers.get('content-type'),
|
|
||||||
contentLength: response.headers.get('content-length'),
|
|
||||||
ok: response.ok,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
console.log('[useAvatarUrl] Avatar encontrado:', url)
|
|
||||||
foundUrl = url
|
|
||||||
setAvatarUrl(url)
|
|
||||||
setIsLoading(false)
|
|
||||||
} else {
|
|
||||||
// Tenta próxima extensão
|
|
||||||
testedExtensions++
|
|
||||||
tryNextExtension()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.debug('[useAvatarUrl] Erro no HEAD request para ext', ext, ':', error.message)
|
|
||||||
|
|
||||||
// Tenta GET como fallback se HEAD falhar (pode ser CORS issue)
|
|
||||||
fetch(url)
|
|
||||||
.then((response) => {
|
|
||||||
console.debug('[useAvatarUrl] GET fallback response para', ext, ':', {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
contentType: response.headers.get('content-type'),
|
|
||||||
})
|
|
||||||
if (response.ok) {
|
|
||||||
console.log('[useAvatarUrl] Avatar encontrado via GET fallback:', ext)
|
|
||||||
foundUrl = url
|
|
||||||
setAvatarUrl(url)
|
|
||||||
setIsLoading(false)
|
|
||||||
} else {
|
|
||||||
// Tenta próxima extensão
|
|
||||||
testedExtensions++
|
|
||||||
tryNextExtension()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.debug('[useAvatarUrl] Erro no GET fallback para ext', ext, ':', err.message)
|
|
||||||
// Tenta próxima extensão
|
|
||||||
testedExtensions++
|
|
||||||
tryNextExtension()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[useAvatarUrl] Erro ao construir URL para ext', ext, ':', error)
|
|
||||||
testedExtensions++
|
|
||||||
tryNextExtension()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tryNextExtension()
|
|
||||||
}, [userId])
|
|
||||||
|
|
||||||
return { avatarUrl, isLoading }
|
|
||||||
}
|
|
||||||
@ -2779,8 +2779,7 @@ export async function removerAnexo(_id: string | number, _anexoId: string | numb
|
|||||||
* Envia uma foto de avatar do paciente ao Supabase Storage.
|
* Envia uma foto de avatar do paciente ao Supabase Storage.
|
||||||
* - Valida tipo (jpeg/png/webp) e tamanho (<= 2MB)
|
* - Valida tipo (jpeg/png/webp) e tamanho (<= 2MB)
|
||||||
* - Faz POST multipart/form-data para /storage/v1/object/avatars/{userId}/avatar
|
* - Faz POST multipart/form-data para /storage/v1/object/avatars/{userId}/avatar
|
||||||
* - Inclui JWT token automaticamente se disponível
|
* - Retorna o objeto { Key } quando upload for bem-sucedido
|
||||||
* - Retorna { foto_url } quando upload for bem-sucedido
|
|
||||||
*/
|
*/
|
||||||
export async function uploadFotoPaciente(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string; Key?: string }> {
|
export async function uploadFotoPaciente(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string; Key?: string }> {
|
||||||
const userId = String(_id);
|
const userId = String(_id);
|
||||||
@ -2814,15 +2813,14 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
|
|||||||
form.append('file', _file, `avatar.${ext}`);
|
form.append('file', _file, `avatar.${ext}`);
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
// Supabase requer o anon key no header 'apikey'
|
// Supabase requires the anon key in 'apikey' header for client-side uploads
|
||||||
apikey: ENV_CONFIG.SUPABASE_ANON_KEY,
|
apikey: ENV_CONFIG.SUPABASE_ANON_KEY,
|
||||||
|
// Accept json
|
||||||
|
Accept: 'application/json',
|
||||||
};
|
};
|
||||||
|
// if user is logged in, include Authorization header
|
||||||
// Incluir JWT token se disponível (para autenticar como usuário logado)
|
|
||||||
const jwt = getAuthToken();
|
const jwt = getAuthToken();
|
||||||
if (jwt) {
|
if (jwt) headers.Authorization = `Bearer ${jwt}`;
|
||||||
headers.Authorization = `Bearer ${jwt}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug('[uploadFotoPaciente] Iniciando upload:', {
|
console.debug('[uploadFotoPaciente] Iniciando upload:', {
|
||||||
url: uploadUrl,
|
url: uploadUrl,
|
||||||
@ -2837,61 +2835,81 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
|
|||||||
body: form as any,
|
body: form as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Supabase storage returns 200/201 com info do objeto ou erro
|
// Supabase storage returns 200/201 with object info or error
|
||||||
// 409 (Duplicate) é esperado quando o arquivo já existe e queremos sobrescrever
|
if (!res.ok) {
|
||||||
if (!res.ok && res.status !== 409) {
|
|
||||||
const raw = await res.text().catch(() => '');
|
const raw = await res.text().catch(() => '');
|
||||||
console.error('[uploadFotoPaciente] upload falhou', {
|
console.error('[uploadFotoPaciente] upload falhou', {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
raw,
|
raw,
|
||||||
|
headers: Object.fromEntries(res.headers.entries()),
|
||||||
url: uploadUrl,
|
url: uploadUrl,
|
||||||
objectPath,
|
requestHeaders: headers,
|
||||||
hasAuth: !!jwt
|
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. Verifique as políticas de RLS no Supabase.');
|
if (res.status === 403) throw new Error('Sem permissão para fazer upload');
|
||||||
if (res.status === 404) throw new Error('Bucket de avatars não encontrado. Verifique se o bucket "avatars" existe no Supabase');
|
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'}`);
|
throw new Error(`Falha no upload da imagem (${res.status}): ${raw || 'Sem detalhes do erro'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construir URL pública do arquivo
|
// Try to parse JSON response
|
||||||
// Importante: codificar userId e nome do arquivo separadamente, não o caminho inteiro
|
let json: any = null;
|
||||||
const publicUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/public/${bucket}/${encodeURIComponent(userId)}/avatar.${ext}`;
|
try { json = await res.json(); } catch { json = null; }
|
||||||
|
|
||||||
console.debug('[uploadFotoPaciente] upload concluído:', { publicUrl, objectPath });
|
// The API may not return a structured body; return the Key we constructed
|
||||||
|
const key = (json && (json.Key || json.key)) ?? objectPath;
|
||||||
return { foto_url: publicUrl, Key: objectPath };
|
const publicUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/public/avatars/${encodeURIComponent(userId)}/avatar.${ext}`;
|
||||||
|
return { foto_url: publicUrl, Key: key };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retorna a URL pública do avatar do usuário (acesso público)
|
* Retorna a URL pública do avatar do usuário (acesso público)
|
||||||
* ⚠️ IMPORTANTE: O arquivo é armazenado como "avatar.{ext}" (jpg, png ou webp)
|
* Path conforme OpenAPI: /storage/v1/object/public/avatars/{userId}/avatar.{ext}
|
||||||
* Este helper retorna a URL COM extensão, não sem.
|
|
||||||
* @param userId - ID do usuário (UUID)
|
* @param userId - ID do usuário (UUID)
|
||||||
* @param ext - Extensão do arquivo (jpg, png, webp). Se não fornecida, tenta jpg por padrão.
|
* @param ext - extensão do arquivo: 'jpg' | 'png' | 'webp' (default 'jpg')
|
||||||
* @returns URL pública completa do avatar
|
|
||||||
*/
|
*/
|
||||||
export function getAvatarPublicUrl(userId: string | number, ext: string = 'jpg'): string {
|
export function getAvatarPublicUrl(userId: string | number): string {
|
||||||
|
// Build the public avatar URL without file extension.
|
||||||
|
// Example: https://<project>.supabase.co/storage/v1/object/public/avatars/{userId}/avatar
|
||||||
const id = String(userId || '').trim();
|
const id = String(userId || '').trim();
|
||||||
if (!id) throw new Error('userId é obrigatório para obter URL pública do avatar');
|
if (!id) throw new Error('userId é obrigatório para obter URL pública do avatar');
|
||||||
const base = String(ENV_CONFIG.SUPABASE_URL).replace(/\/$/, '');
|
const base = String(ENV_CONFIG.SUPABASE_URL).replace(/\/$/, '');
|
||||||
|
// Note: Supabase public object path does not require an extension in some setups
|
||||||
// IMPORTANTE: Deve corresponder exatamente ao objectPath usado no upload:
|
return `${base}/storage/v1/object/public/${encodeURIComponent('avatars')}/${encodeURIComponent(id)}/avatar`;
|
||||||
// uploadFotoPaciente() salva como: `${userId}/avatar.${ext}`
|
|
||||||
// Então aqui retornamos: `/storage/v1/object/public/avatars/${userId}/avatar.${ext}`
|
|
||||||
const cleanExt = ext.toLowerCase().replace(/^\./, ''); // Remove ponto se presente
|
|
||||||
return `${base}/storage/v1/object/public/avatars/${encodeURIComponent(id)}/avatar.${cleanExt}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removerFotoPaciente(_id: string | number): Promise<void> {
|
export async function removerFotoPaciente(_id: string | number): Promise<void> {
|
||||||
const userId = String(_id || '').trim();
|
const userId = String(_id || '').trim();
|
||||||
if (!userId) throw new Error('ID do paciente é obrigatório para remover foto');
|
if (!userId) throw new Error('ID do paciente é obrigatório para remover foto');
|
||||||
|
const deleteUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/avatars/${encodeURIComponent(userId)}/avatar`;
|
||||||
// Na prática, o upload usa upsert: true, então não é necessário fazer DELETE explícito.
|
const headers: Record<string,string> = {
|
||||||
// Apenas log e retorna com sucesso para compatibilidade.
|
apikey: ENV_CONFIG.SUPABASE_ANON_KEY,
|
||||||
console.debug('[removerFotoPaciente] Remoção de foto não necessária (upload usa upsert: true)', { userId });
|
Accept: 'application/json',
|
||||||
return;
|
};
|
||||||
|
const jwt = getAuthToken();
|
||||||
|
if (jwt) headers.Authorization = `Bearer ${jwt}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.debug('[removerFotoPaciente] Deleting avatar for user:', userId, 'url:', deleteUrl);
|
||||||
|
const res = await fetch(deleteUrl, { method: 'DELETE', headers });
|
||||||
|
if (!res.ok) {
|
||||||
|
const raw = await res.text().catch(() => '');
|
||||||
|
console.warn('[removerFotoPaciente] remoção falhou', { status: res.status, raw });
|
||||||
|
// Treat 404 as success (object already absent)
|
||||||
|
if (res.status === 404) return;
|
||||||
|
// Include status and server body in the error message to aid debugging
|
||||||
|
const bodySnippet = raw && raw.length > 0 ? raw : '<sem corpo na resposta>';
|
||||||
|
if (res.status === 401) throw new Error(`Não autenticado (401). Resposta: ${bodySnippet}`);
|
||||||
|
if (res.status === 403) throw new Error(`Sem permissão para remover a foto (403). Resposta: ${bodySnippet}`);
|
||||||
|
throw new Error(`Falha ao remover a foto do storage (status ${res.status}). Resposta: ${bodySnippet}`);
|
||||||
|
}
|
||||||
|
// success
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
// bubble up for the caller to handle
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export async function listarAnexosMedico(_id: string | number): Promise<any[]> { return []; }
|
export async function listarAnexosMedico(_id: string | number): Promise<any[]> { return []; }
|
||||||
export async function adicionarAnexoMedico(_id: string | number, _file: File): Promise<any> { return {}; }
|
export async function adicionarAnexoMedico(_id: string | number, _file: File): Promise<any> { return {}; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user