add-avatar-endpoint

This commit is contained in:
João Gustavo 2025-10-17 18:30:15 -03:00
parent f95fb7d3ce
commit 60630cd9db
4 changed files with 336 additions and 30 deletions

View File

@ -53,7 +53,7 @@ export default function PacientesPage() {
async function loadAll() { async function loadAll() {
try { try {
setLoading(true); setLoading(true);
const data = await listarPacientes({ page: 1, limit: 20 }); const data = await listarPacientes({ page: 1, limit: 50 });
if (Array.isArray(data)) { if (Array.isArray(data)) {
setPatients(data.map(normalizePaciente)); setPatients(data.map(normalizePaciente));

View File

@ -22,11 +22,13 @@ import {
listarAnexosMedico, listarAnexosMedico,
adicionarAnexoMedico, adicionarAnexoMedico,
removerAnexoMedico, removerAnexoMedico,
removerFotoMedico,
MedicoInput, MedicoInput,
Medico, Medico,
criarUsuarioMedico, criarUsuarioMedico,
gerarSenhaAleatoria, gerarSenhaAleatoria,
} from "@/lib/api"; } from "@/lib/api";
import { getAvatarPublicUrl } from '@/lib/api';
; ;
import { buscarCepAPI } from "@/lib/api"; import { buscarCepAPI } from "@/lib/api";
@ -150,6 +152,7 @@ export function DoctorRegistrationForm({
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false, formacao: false, admin: false }); const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false, formacao: false, admin: false });
const [isSubmitting, setSubmitting] = useState(false); const [isSubmitting, setSubmitting] = useState(false);
const [isUploadingPhoto, setUploadingPhoto] = useState(false);
const [isSearchingCEP, setSearchingCEP] = useState(false); const [isSearchingCEP, setSearchingCEP] = useState(false);
const [photoPreview, setPhotoPreview] = useState<string | null>(null); const [photoPreview, setPhotoPreview] = useState<string | null>(null);
const [serverAnexos, setServerAnexos] = useState<any[]>([]); const [serverAnexos, setServerAnexos] = useState<any[]>([]);
@ -242,6 +245,22 @@ export function DoctorRegistrationForm({
} catch (err) { } catch (err) {
console.error("[DoctorForm] Erro ao carregar anexos:", err); console.error("[DoctorForm] Erro ao carregar anexos:", err);
} }
// Try to detect existing public avatar (no file extension) and set preview
try {
const url = getAvatarPublicUrl(String(doctorId));
try {
const head = await fetch(url, { method: 'HEAD' });
if (head.ok) { setPhotoPreview(url); }
else {
const get = await fetch(url, { method: 'GET' });
if (get.ok) { setPhotoPreview(url); }
}
} catch (inner) {
// ignore network/CORS errors while detecting
}
} catch (detectErr) {
// ignore detection errors
}
} catch (err) { } catch (err) {
console.error("[DoctorForm] Erro ao carregar médico:", err); console.error("[DoctorForm] Erro ao carregar médico:", err);
} }
@ -345,6 +364,27 @@ function setField<T extends keyof FormData>(k: T, v: FormData[T]) {
return Object.keys(e).length === 0; return Object.keys(e).length === 0;
} }
async function handleRemoverFotoServidor() {
if (mode !== 'edit' || !doctorId) return;
try {
setUploadingPhoto(true);
await removerFotoMedico(String(doctorId));
setPhotoPreview(null);
alert('Foto removida com sucesso.');
} catch (e: any) {
console.warn('[DoctorForm] erro ao remover foto do servidor', e);
if (String(e?.message || '').includes('401')) {
alert('Falha ao remover a foto: não autenticado. Faça login novamente e tente novamente.\nDetalhe: ' + (e?.message || ''));
} else if (String(e?.message || '').includes('403')) {
alert('Falha ao remover a foto: sem permissão. Verifique as permissões do token e se o storage aceita esse usuário.\nDetalhe: ' + (e?.message || ''));
} else {
alert(e?.message || 'Não foi possível remover a foto do storage. Veja console para detalhes.');
}
} finally {
setUploadingPhoto(false);
}
}
function toPayload(): MedicoInput { function toPayload(): MedicoInput {
// Converte dd/MM/yyyy para ISO (yyyy-MM-dd) se possível // Converte dd/MM/yyyy para ISO (yyyy-MM-dd) se possível
let isoDate: string | null = null; let isoDate: string | null = null;
@ -396,6 +436,18 @@ async function handleSubmit(ev: React.FormEvent) {
if (!doctorId) throw new Error("ID do médico não fornecido para edição"); if (!doctorId) throw new Error("ID do médico não fornecido para edição");
const payload = toPayload(); const payload = toPayload();
const saved = await atualizarMedico(String(doctorId), payload); const saved = await atualizarMedico(String(doctorId), payload);
// If user selected a new photo, upload it
if (form.photo) {
try {
setUploadingPhoto(true);
await uploadFotoMedico(String(doctorId), form.photo);
} catch (upErr) {
console.warn('[DoctorForm] Falha ao enviar foto do médico:', upErr);
alert('Médico atualizado, mas falha ao enviar a foto. Tente novamente.');
} finally {
setUploadingPhoto(false);
}
}
onSaved?.(saved); onSaved?.(saved);
alert("Médico atualizado com sucesso!"); alert("Médico atualizado com sucesso!");
if (inline) onClose?.(); if (inline) onClose?.();
@ -458,6 +510,20 @@ async function handleSubmit(ev: React.FormEvent) {
setPhotoPreview(null); setPhotoPreview(null);
setServerAnexos([]); setServerAnexos([]);
// If a photo was selected during creation, upload it now
if (form.photo) {
try {
setUploadingPhoto(true);
const docId = (savedDoctorProfile && (savedDoctorProfile.id || (Array.isArray(savedDoctorProfile) ? savedDoctorProfile[0]?.id : undefined))) || null;
if (docId) await uploadFotoMedico(String(docId), form.photo);
} catch (upErr) {
console.warn('[DoctorForm] Falha ao enviar foto do médico após criação:', upErr);
alert('Médico criado, mas falha ao enviar a foto. Você pode tentar novamente no perfil.');
} finally {
setUploadingPhoto(false);
}
}
// 5. Notifica componente pai // 5. Notifica componente pai
onSaved?.(savedDoctorProfile); onSaved?.(savedDoctorProfile);
} else { } else {
@ -582,6 +648,11 @@ async function handleSubmit(ev: React.FormEvent) {
</Button> </Button>
</Label> </Label>
<Input id="photo" type="file" accept="image/*" className="hidden" onChange={handlePhoto} /> <Input id="photo" type="file" accept="image/*" className="hidden" onChange={handlePhoto} />
{mode === "edit" && (
<Button type="button" variant="ghost" onClick={handleRemoverFotoServidor}>
<Trash2 className="mr-2 h-4 w-4" /> Remover foto
</Button>
)}
{errors.photo && <p className="text-sm text-destructive">{errors.photo}</p>} {errors.photo && <p className="text-sm text-destructive">{errors.photo}</p>}
<p className="text-xs text-muted-foreground">Máximo 5MB</p> <p className="text-xs text-muted-foreground">Máximo 5MB</p>
</div> </div>

View File

@ -27,6 +27,7 @@ import {
criarUsuarioPaciente, criarUsuarioPaciente,
criarPaciente, criarPaciente,
} from "@/lib/api"; } from "@/lib/api";
import { getAvatarPublicUrl } from '@/lib/api';
import { validarCPFLocal } from "@/lib/utils"; import { validarCPFLocal } from "@/lib/utils";
import { verificarCpfDuplicado } from "@/lib/api"; import { verificarCpfDuplicado } from "@/lib/api";
@ -99,6 +100,7 @@ export function PatientRegistrationForm({
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false }); const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false });
const [isSubmitting, setSubmitting] = useState(false); const [isSubmitting, setSubmitting] = useState(false);
const [isUploadingPhoto, setUploadingPhoto] = useState(false);
const [isSearchingCEP, setSearchingCEP] = useState(false); const [isSearchingCEP, setSearchingCEP] = useState(false);
const [photoPreview, setPhotoPreview] = useState<string | null>(null); const [photoPreview, setPhotoPreview] = useState<string | null>(null);
const [serverAnexos, setServerAnexos] = useState<any[]>([]); const [serverAnexos, setServerAnexos] = useState<any[]>([]);
@ -145,6 +147,22 @@ export function PatientRegistrationForm({
const ax = await listarAnexos(String(patientId)).catch(() => []); const ax = await listarAnexos(String(patientId)).catch(() => []);
setServerAnexos(Array.isArray(ax) ? ax : []); setServerAnexos(Array.isArray(ax) ? ax : []);
// Try to detect existing public avatar (no file extension) and set preview
try {
const url = getAvatarPublicUrl(String(patientId));
try {
const head = await fetch(url, { method: 'HEAD' });
if (head.ok) { setPhotoPreview(url); }
else {
const get = await fetch(url, { method: 'GET' });
if (get.ok) { setPhotoPreview(url); }
}
} catch (inner) {
// ignore network/CORS errors while detecting
}
} catch (detectErr) {
// ignore detection errors
}
} catch (err) { } catch (err) {
console.error("[PatientForm] Erro ao carregar paciente:", err); console.error("[PatientForm] Erro ao carregar paciente:", err);
} }
@ -260,6 +278,28 @@ 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 payload = toPayload();
const saved = await atualizarPaciente(String(patientId), payload); const saved = await atualizarPaciente(String(patientId), payload);
// If a new photo was selected locally, remove existing public avatar (if any) then upload the new one
if (form.photo) {
try {
setUploadingPhoto(true);
// Attempt to remove existing avatar first (no-op if none)
try {
await removerFotoPaciente(String(patientId));
// clear any cached preview so upload result will repopulate it
setPhotoPreview(null);
} catch (remErr) {
// If removal fails (permissions/CORS), continue to attempt upload — we don't want to block the user
console.warn('[PatientForm] aviso: falha ao remover avatar antes do upload:', remErr);
}
await uploadFotoPaciente(String(patientId), form.photo);
} catch (upErr) {
console.warn('[PatientForm] Falha ao enviar foto do paciente:', upErr);
// don't block the main update — show a warning
alert('Paciente atualizado, mas falha ao enviar a foto. Tente novamente.');
} finally {
setUploadingPhoto(false);
}
}
onSaved?.(saved); onSaved?.(saved);
alert("Paciente atualizado com sucesso!"); alert("Paciente atualizado com sucesso!");
@ -335,6 +375,22 @@ export function PatientRegistrationForm({
setForm(initial); setForm(initial);
setPhotoPreview(null); setPhotoPreview(null);
setServerAnexos([]); setServerAnexos([]);
// If a photo was selected during creation, upload it now using the created patient id
if (form.photo) {
try {
setUploadingPhoto(true);
const pacienteId = savedPatientProfile?.id || (savedPatientProfile && (savedPatientProfile as any).id);
if (pacienteId) {
await uploadFotoPaciente(String(pacienteId), form.photo);
}
} catch (upErr) {
console.warn('[PatientForm] Falha ao enviar foto do paciente após criação:', upErr);
// Non-blocking: inform user
alert('Paciente criado, mas falha ao enviar a foto. Você pode tentar novamente no perfil.');
} finally {
setUploadingPhoto(false);
}
}
onSaved?.(savedPatientProfile); onSaved?.(savedPatientProfile);
return; return;
} else { } else {
@ -419,10 +475,23 @@ export function PatientRegistrationForm({
async function handleRemoverFotoServidor() { async function handleRemoverFotoServidor() {
if (mode !== "edit" || !patientId) return; if (mode !== "edit" || !patientId) return;
try { try {
setUploadingPhoto(true);
await removerFotoPaciente(String(patientId)); await removerFotoPaciente(String(patientId));
alert("Foto removida."); // clear preview and inform user
setPhotoPreview(null);
alert('Foto removida com sucesso.');
} catch (e: any) { } catch (e: any) {
alert(e?.message || "Não foi possível remover a foto."); console.warn('[PatientForm] erro ao remover foto do servidor', e);
// Show detailed guidance for common cases
if (String(e?.message || '').includes('401')) {
alert('Falha ao remover a foto: não autenticado. Faça login novamente e tente novamente.\nDetalhe: ' + (e?.message || ''));
} else if (String(e?.message || '').includes('403')) {
alert('Falha ao remover a foto: sem permissão. Verifique as permissões do token e se o storage aceita esse usuário.\nDetalhe: ' + (e?.message || ''));
} else {
alert(e?.message || 'Não foi possível remover a foto do storage. Veja console para detalhes.');
}
} finally {
setUploadingPhoto(false);
} }
} }

View File

@ -599,20 +599,24 @@ export async function deletarExcecao(id: string): Promise<void> {
// ===== CONFIG =====
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? ENV_CONFIG.SUPABASE_URL; const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? ENV_CONFIG.SUPABASE_URL;
const REST = `${API_BASE}/rest/v1`; const REST = `${API_BASE}/rest/v1`;
const DEFAULT_AUTH_CALLBACK = 'https://mediconecta-app-liart.vercel.app/auth/callback';
const DEFAULT_LANDING = 'https://mediconecta-app-liart.vercel.app';
// Helper to build/normalize redirect URLs // Helper to build/normalize redirect URLs
function buildRedirectUrl(target?: 'paciente' | 'medico' | 'admin' | 'default', explicit?: string, redirectBase?: string) { function buildRedirectUrl(target?: 'paciente' | 'medico' | 'admin' | 'default', explicit?: string, redirectBase?: string) {
const DEFAULT_REDIRECT_BASE = redirectBase ?? 'https://mediconecta-app-liart.vercel.app'; const DEFAULT_REDIRECT_BASE = redirectBase ?? DEFAULT_LANDING;
if (explicit) { if (explicit) {
// If explicit is already absolute, return trimmed
try { try {
const u = new URL(explicit); const u = new URL(explicit);
return u.toString().replace(/\/$/, ''); return u.toString().replace(/\/$/, '');
} catch (e) { } catch (e) {
// Not an absolute URL, fall through to build from base
} }
} }
@ -692,21 +696,35 @@ async function fetchWithFallback<T = any>(url: string, headers: Record<string, s
// Parse genérico // Parse genérico
async function parse<T>(res: Response): Promise<T> { async function parse<T>(res: Response): Promise<T> {
let json: any = null; let json: any = null;
let rawText = '';
try { try {
// Attempt to parse JSON; many endpoints may return empty bodies (204/204) or plain text
// so guard against unexpected EOF during json parsing
json = await res.json(); json = await res.json();
} catch (err) { } catch (err) {
console.error("Erro ao parsear a resposta como JSON:", err); // Try to capture raw text for better diagnostics
}
if (!res.ok) {
// Tenta também ler o body como texto cru para obter mensagens detalhadas
let rawText = '';
try { try {
rawText = await res.clone().text(); rawText = await res.clone().text();
} catch (tErr) { } catch (tErr) {
// ignore rawText = '';
} }
console.error("[API ERROR]", res.url, res.status, json, "raw:", rawText); if (rawText) {
console.warn('Resposta não-JSON recebida do servidor. raw text:', rawText);
} else {
console.warn('Resposta vazia ou inválida recebida do servidor; não foi possível parsear JSON:', err);
}
}
if (!res.ok) {
// If we didn't already collect rawText above, try to get it now for error messaging
if (!rawText) {
try {
rawText = await res.clone().text();
} catch (tErr) {
rawText = '';
}
}
console.error('[API ERROR]', res.url, res.status, json, 'raw:', rawText);
const code = (json && (json.error?.code || json.code)) ?? res.status; const code = (json && (json.error?.code || json.code)) ?? res.status;
const msg = (json && (json.error?.message || json.message || json.error)) ?? res.statusText; const msg = (json && (json.error?.message || json.message || json.error)) ?? res.statusText;
@ -1423,15 +1441,41 @@ export async function vincularUserIdMedico(medicoId: string | number, userId: st
* Retorna o paciente atualizado. * Retorna o paciente atualizado.
*/ */
export async function vincularUserIdPaciente(pacienteId: string | number, userId: string): Promise<Paciente> { export async function vincularUserIdPaciente(pacienteId: string | number, userId: string): Promise<Paciente> {
const url = `${REST}/patients?id=eq.${encodeURIComponent(String(pacienteId))}`; // Validate pacienteId looks like a UUID (basic check) or at least a non-empty string/number
const idStr = String(pacienteId || '').trim();
if (!idStr) throw new Error('ID do paciente inválido ao tentar vincular user_id.');
const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
const looksLikeUuid = uuidRegex.test(idStr);
// Allow non-UUID ids (legacy) but log a debug warning when it's not UUID
if (!looksLikeUuid) console.warn('[vincularUserIdPaciente] pacienteId does not look like a UUID:', idStr);
const url = `${REST}/patients?id=eq.${encodeURIComponent(idStr)}`;
const payload = { user_id: String(userId) }; const payload = { user_id: String(userId) };
// Debug-friendly masked headers
const headers = withPrefer({ ...baseHeaders(), 'Content-Type': 'application/json' }, 'return=representation');
const maskedHeaders = { ...headers } as Record<string, string>;
if (maskedHeaders.Authorization) {
const a = maskedHeaders.Authorization as string;
maskedHeaders.Authorization = a.slice(0,6) + '...' + a.slice(-6);
}
console.debug('[vincularUserIdPaciente] PATCH', url, 'payload:', { ...payload }, 'headers(masked):', maskedHeaders);
const res = await fetch(url, { const res = await fetch(url, {
method: 'PATCH', method: 'PATCH',
headers: withPrefer({ ...baseHeaders(), 'Content-Type': 'application/json' }, 'return=representation'), headers,
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
// If parse throws, the existing parse() will log response details; ensure we also surface helpful context
try {
const arr = await parse<Paciente[] | Paciente>(res); const arr = await parse<Paciente[] | Paciente>(res);
return Array.isArray(arr) ? arr[0] : (arr as Paciente); return Array.isArray(arr) ? arr[0] : (arr as Paciente);
} catch (err) {
console.error('[vincularUserIdPaciente] erro ao vincular:', { pacienteId: idStr, userId, url });
throw err;
}
} }
@ -1789,11 +1833,10 @@ export async function criarUsuarioDirectAuth(input: {
// Criar usuário para MÉDICO no Supabase Auth (sistema de autenticação) // Criar usuário para MÉDICO no Supabase Auth (sistema de autenticação)
export async function criarUsuarioMedico(medico: { email: string; full_name: string; phone_mobile: string; }): Promise<any> { export async function criarUsuarioMedico(medico: { email: string; full_name: string; phone_mobile: string; }): Promise<any> {
// Rely on the server-side create-user endpoint (POST /create-user). The const redirectBase = DEFAULT_LANDING;
// backend is responsible for role assignment and sending the magic link.
// Any error should be surfaced to the caller so it can be handled there.
const redirectBase = 'https://mediconecta-app-liart.vercel.app';
const emailRedirectTo = `${redirectBase.replace(/\/$/, '')}/profissional`; const emailRedirectTo = `${redirectBase.replace(/\/$/, '')}/profissional`;
// Use the role-specific landing as the redirect_url so the magic link
// redirects users directly to the app path (e.g. /profissional).
const redirect_url = emailRedirectTo; const redirect_url = emailRedirectTo;
// generate a secure-ish random password on the client so the caller can receive it // generate a secure-ish random password on the client so the caller can receive it
const password = gerarSenhaAleatoria(); const password = gerarSenhaAleatoria();
@ -1804,9 +1847,10 @@ export async function criarUsuarioMedico(medico: { email: string; full_name: str
// Criar usuário para PACIENTE no Supabase Auth (sistema de autenticação) // Criar usuário para PACIENTE no Supabase Auth (sistema de autenticação)
export async function criarUsuarioPaciente(paciente: { email: string; full_name: string; phone_mobile: string; }): Promise<any> { export async function criarUsuarioPaciente(paciente: { email: string; full_name: string; phone_mobile: string; }): Promise<any> {
// Rely on the server-side create-user endpoint (POST /create-user). const redirectBase = DEFAULT_LANDING;
const redirectBase = 'https://mediconecta-app-liart.vercel.app';
const emailRedirectTo = `${redirectBase.replace(/\/$/, '')}/paciente`; const emailRedirectTo = `${redirectBase.replace(/\/$/, '')}/paciente`;
// Use the role-specific landing as the redirect_url so the magic link
// redirects users directly to the app path (e.g. /paciente).
const redirect_url = emailRedirectTo; const redirect_url = emailRedirectTo;
// generate a secure-ish random password on the client so the caller can receive it // generate a secure-ish random password on the client so the caller can receive it
const password = gerarSenhaAleatoria(); const password = gerarSenhaAleatoria();
@ -1886,13 +1930,135 @@ export async function buscarCepAPI(cep: string): Promise<{
export async function listarAnexos(_id: string | number): Promise<any[]> { return []; } export async function listarAnexos(_id: string | number): Promise<any[]> { return []; }
export async function adicionarAnexo(_id: string | number, _file: File): Promise<any> { return {}; } export async function adicionarAnexo(_id: string | number, _file: File): Promise<any> { return {}; }
export async function removerAnexo(_id: string | number, _anexoId: string | number): Promise<void> {} export async function removerAnexo(_id: string | number, _anexoId: string | number): Promise<void> {}
export async function uploadFotoPaciente(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> { return {}; } /**
export async function removerFotoPaciente(_id: string | number): Promise<void> {} * Envia uma foto de avatar do paciente ao Supabase Storage.
* - Valida tipo (jpeg/png/webp) e tamanho (<= 2MB)
* - Faz POST multipart/form-data para /storage/v1/object/avatars/{userId}/avatar
* - Retorna o objeto { Key } quando upload for bem-sucedido
*/
export async function uploadFotoPaciente(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string; Key?: string }> {
const userId = String(_id);
if (!userId) throw new Error('ID do paciente é obrigatório para upload de foto');
if (!_file) throw new Error('Arquivo ausente');
// validações de formato e tamanho
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowed.includes(_file.type)) {
throw new Error('Formato inválido. Aceitamos JPG, PNG ou WebP.');
}
const maxBytes = 2 * 1024 * 1024; // 2MB
if (_file.size > maxBytes) {
throw new Error('Arquivo muito grande. Máx 2MB.');
}
const extMap: Record<string, string> = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/webp': 'webp',
};
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`;
// Build multipart form data
const form = new FormData();
form.append('file', _file, `avatar.${ext}`);
const headers: Record<string, string> = {
// Supabase requires the anon key in 'apikey' header for client-side uploads
apikey: ENV_CONFIG.SUPABASE_ANON_KEY,
// Accept json
Accept: 'application/json',
};
// if user is logged in, include Authorization header
const jwt = getAuthToken();
if (jwt) headers.Authorization = `Bearer ${jwt}`;
const res = await fetch(uploadUrl, {
method: 'POST',
headers,
body: form as any,
});
// 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 });
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');
}
// Try to parse JSON response
let json: any = null;
try { json = await res.json(); } catch { json = null; }
// 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}`;
return { foto_url: publicUrl, Key: key };
}
/**
* Retorna a URL pública do avatar do usuário (acesso público)
* Path conforme OpenAPI: /storage/v1/object/public/avatars/{userId}/avatar.{ext}
* @param userId - ID do usuário (UUID)
* @param ext - extensão do arquivo: 'jpg' | 'png' | 'webp' (default 'jpg')
*/
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();
if (!id) throw new Error('userId é obrigatório para obter URL pública do avatar');
const base = String(ENV_CONFIG.SUPABASE_URL).replace(/\/$/, '');
// Note: Supabase public object path does not require an extension in some setups
return `${base}/storage/v1/object/public/${encodeURIComponent('avatars')}/${encodeURIComponent(id)}/avatar`;
}
export async function removerFotoPaciente(_id: string | number): Promise<void> {
const userId = String(_id || '').trim();
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`;
const headers: Record<string,string> = {
apikey: ENV_CONFIG.SUPABASE_ANON_KEY,
Accept: 'application/json',
};
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 {}; }
export async function removerAnexoMedico(_id: string | number, _anexoId: string | number): Promise<void> {} export async function removerAnexoMedico(_id: string | number, _anexoId: string | number): Promise<void> {}
export async function uploadFotoMedico(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> { return {}; } export async function uploadFotoMedico(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string; Key?: string }> {
export async function removerFotoMedico(_id: string | number): Promise<void> {} // reuse same implementation as paciente but place under avatars/{userId}/avatar
return await uploadFotoPaciente(_id, _file);
}
export async function removerFotoMedico(_id: string | number): Promise<void> {
// reuse samme implementation
return await removerFotoPaciente(_id);
}
// ===== PERFIS DE USUÁRIOS ===== // ===== PERFIS DE USUÁRIOS =====
export async function listarPerfis(params?: { page?: number; limit?: number; q?: string; }): Promise<Profile[]> { export async function listarPerfis(params?: { page?: number; limit?: number; q?: string; }): Promise<Profile[]> {