- Adiciona Edge Function para calcular slots disponíveis - Implementa método callFunction() no apiClient para Edge Functions - Atualiza appointmentService com getAvailableSlots() e create() - Simplifica AgendamentoConsulta removendo lógica manual de slots - Remove arquivos de teste e documentação temporária - Atualiza README com documentação completa - Adiciona AGENDAMENTO-SLOTS-API.md com detalhes da implementação - Corrige formatação de dados (telefone, CPF, nomes) - Melhora diálogos de confirmação e feedback visual - Otimiza performance e user experience
161 lines
5.1 KiB
TypeScript
161 lines
5.1 KiB
TypeScript
/**
|
|
* Serviço de Avatars (Frontend)
|
|
*/
|
|
|
|
import axios from "axios";
|
|
import { API_CONFIG } from "../api/config";
|
|
import type {
|
|
UploadAvatarInput,
|
|
UploadAvatarResponse,
|
|
DeleteAvatarInput,
|
|
GetAvatarUrlInput,
|
|
} from "./types";
|
|
|
|
class AvatarService {
|
|
private readonly SUPABASE_URL = API_CONFIG.SUPABASE_URL;
|
|
private readonly STORAGE_URL = `${this.SUPABASE_URL}/storage/v1/object`;
|
|
private readonly BUCKET_NAME = "avatars";
|
|
|
|
/**
|
|
* Cria uma instância limpa do axios sem baseURL
|
|
* Para evitar conflitos com configurações globais
|
|
*/
|
|
private createAxiosInstance() {
|
|
return axios.create({
|
|
// NÃO definir baseURL aqui - usaremos URL completa
|
|
timeout: 30000,
|
|
maxContentLength: 2 * 1024 * 1024, // 2MB
|
|
maxBodyLength: 2 * 1024 * 1024, // 2MB
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Faz upload de avatar do usuário
|
|
*/
|
|
async upload(data: UploadAvatarInput): Promise<UploadAvatarResponse> {
|
|
try {
|
|
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
|
|
|
|
if (!token) {
|
|
throw new Error("Token de autenticação não encontrado");
|
|
}
|
|
|
|
// Determina a extensão do arquivo
|
|
const ext = data.file.name.split(".").pop()?.toLowerCase() || "jpg";
|
|
const filePath = `${data.userId}/avatar.${ext}`;
|
|
|
|
// Cria FormData para o upload
|
|
const formData = new FormData();
|
|
formData.append("file", data.file);
|
|
|
|
// URL COMPLETA (sem baseURL do axios)
|
|
const uploadUrl = `${this.STORAGE_URL}/${this.BUCKET_NAME}/${filePath}`;
|
|
|
|
console.log("[AvatarService] 🚀 Upload iniciado:", {
|
|
uploadUrl,
|
|
STORAGE_URL: this.STORAGE_URL,
|
|
BUCKET_NAME: this.BUCKET_NAME,
|
|
filePath,
|
|
userId: data.userId,
|
|
fileName: data.file.name,
|
|
fileSize: data.file.size,
|
|
fileType: data.file.type,
|
|
token: token ? `${token.substring(0, 20)}...` : "null",
|
|
});
|
|
|
|
// Cria instância limpa do axios
|
|
const axiosInstance = this.createAxiosInstance();
|
|
|
|
console.log("[AvatarService] 🔍 Verificando URL antes do POST:");
|
|
console.log(" - URL completa:", uploadUrl);
|
|
console.log(" - Deve começar com:", this.SUPABASE_URL);
|
|
console.log(" - Deve conter: /storage/v1/object/avatars/");
|
|
|
|
// Upload usando Supabase Storage API
|
|
// Importante: NÃO definir Content-Type manualmente
|
|
const response = await axiosInstance.post(uploadUrl, formData, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
apikey: API_CONFIG.SUPABASE_ANON_KEY,
|
|
"x-upsert": "true",
|
|
},
|
|
});
|
|
|
|
console.log("[AvatarService] ✅ Upload bem-sucedido:", response.data);
|
|
console.log("[AvatarService] 📍 URL real usada:", response.config?.url);
|
|
|
|
// Retorna a URL pública
|
|
const publicUrl = this.getPublicUrl({
|
|
userId: data.userId,
|
|
ext: ext as "jpg" | "png" | "webp",
|
|
});
|
|
|
|
return {
|
|
Key: publicUrl,
|
|
};
|
|
} catch (error) {
|
|
console.error("❌ [AvatarService] Erro ao fazer upload:", error);
|
|
if (axios.isAxiosError(error)) {
|
|
console.error("📋 Detalhes do erro:", {
|
|
status: error.response?.status,
|
|
statusText: error.response?.statusText,
|
|
data: error.response?.data,
|
|
message: error.message,
|
|
requestUrl: error.config?.url,
|
|
requestMethod: error.config?.method,
|
|
headers: error.config?.headers,
|
|
});
|
|
|
|
console.error("🔍 URL que foi enviada:", error.config?.url);
|
|
console.error(
|
|
"🔍 URL esperada:",
|
|
`${this.STORAGE_URL}/${this.BUCKET_NAME}/{user_id}/avatar.{ext}`
|
|
);
|
|
|
|
// Mensagens de erro mais específicas
|
|
if (error.response?.status === 400) {
|
|
console.error(
|
|
"💡 Erro 400: Verifique se o bucket 'avatars' existe e está configurado corretamente"
|
|
);
|
|
console.error(
|
|
" OU: Verifique se a URL está correta (deve ter /storage/v1/object/avatars/)"
|
|
);
|
|
} else if (error.response?.status === 401) {
|
|
console.error("💡 Erro 401: Token inválido ou expirado");
|
|
} else if (error.response?.status === 403) {
|
|
console.error(
|
|
"💡 Erro 403: Sem permissão. Verifique as políticas RLS do Storage"
|
|
);
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove avatar do usuário (sobrescreve com imagem vazia ou remove do perfil)
|
|
*/
|
|
async delete(_data: DeleteAvatarInput): Promise<void> {
|
|
try {
|
|
// Não há endpoint de delete, então apenas removemos a referência do perfil
|
|
// O upload futuro irá sobrescrever a imagem antiga
|
|
console.log(
|
|
"Avatar será removido do perfil. Upload futuro sobrescreverá a imagem."
|
|
);
|
|
} catch (error) {
|
|
console.error("Erro ao deletar avatar:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retorna a URL pública do avatar
|
|
* Não precisa de autenticação pois é endpoint público
|
|
*/
|
|
getPublicUrl(data: GetAvatarUrlInput): string {
|
|
return `${this.STORAGE_URL}/${this.BUCKET_NAME}/${data.userId}/avatar.${data.ext}`;
|
|
}
|
|
}
|
|
|
|
export const avatarService = new AvatarService();
|