guisilvagomes 6b9bfbbd29 feat: implementa sistema de agendamento com API de slots
- 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
2025-10-30 12:56:52 -03:00

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();