saveinmed/saveinmed-frontend/src/services/produtosVendaService.ts
Tiago Yamamoto b39caf0fd0 first commit
2025-12-17 13:58:26 -03:00

482 lines
15 KiB
TypeScript

interface ProdutoVenda {
id: string;
catalogo_id: string;
preco_venda: number;
preco_final?: number; // ✅ Preço final calculado pela API
observacoes?: string;
data_validade: string;
qtdade_estoque?: number; // ✅ Agora incluído no endpoint unificado
empresa_id?: string; // ✅ ID da empresa que está vendendo o produto
createdAt: string;
updatedAt: string;
}
interface ProdutoEstoque {
id: string;
catalogo_id: string;
venda_id: string;
quantidade: number;
createdAt: string;
updatedAt: string;
}
interface ProdutoCatalogo {
$id: string;
codigo_ean: string;
codigo_interno: string;
nome: string;
descricao?: string;
preco_base: number;
preco_fabrica?: number;
pmc?: number;
desconto_comercial: number;
laboratorio?: string;
categoria?: string;
subcategoria?: string;
lab_nome?: string; // ✅ Nome do laboratório
cat_nome?: string; // ✅ Nome da categoria
}
interface ProdutoCompleto {
id: string;
$id?: string; // ✅ Para compatibilidade com Appwrite
catalogo_id: string;
nome: string;
descricao?: string;
preco_venda: number;
preco_base: number;
preco_final?: number; // ✅ Preço final para exibição na listagem
quantidade_estoque: number;
observacoes?: string;
data_validade: string;
codigo_ean: string;
codigo_interno: string;
laboratorio?: string;
categoria?: string;
lab_nome?: string; // ✅ Nome do laboratório
cat_nome?: string; // ✅ Nome da categoria
empresa_id?: string; // ✅ ID da empresa que está vendendo o produto
}
interface ResponseVenda {
items: ProdutoVenda[];
total: number;
}
interface ResponseEstoque {
items: ProdutoEstoque[];
total: number;
}
interface ResponseCatalogo {
documents: ProdutoCatalogo[];
total: number;
}
class ProdutosVendaService {
private baseUrl = process.env.NEXT_PUBLIC_BFF_API_URL!;
private getAuthHeaders(): Record<string, string> {
const token = localStorage.getItem('access_token');
if (!token) {
console.warn("⚠️ Nenhum token de acesso encontrado. Usuário pode não estar autenticado.");
}
return {
"accept": "application/json",
"Content-Type": "application/json",
...(token && { "Authorization": `Bearer ${token}` })
};
}
/**
* Busca dados do usuário logado via endpoint /auth/me
* @returns Promise com os dados do usuário ou null se erro
*/
private async buscarUsuarioLogado(): Promise<any | null> {
try {
const token = localStorage.getItem('access_token');
if (!token) {
console.error('🔑 Token de acesso não encontrado');
return null;
}
const response = await fetch(`${this.baseUrl}/auth/me`, {
method: 'GET',
headers: this.getAuthHeaders(),
});
if (!response.ok) {
console.error('❌ Erro ao buscar dados do usuário:', response.status, response.statusText);
return null;
}
const userData = await response.json();
return userData;
} catch (error) {
console.error('❌ Erro exception ao buscar dados do usuário:', error);
return null;
}
}
/**
* Extrai o IDs de empresa dos dados do usuário
* @param userData - Dados do usuário do endpoint /auth/me
* @returns Array de string com os IDs das empresas ou array vazio se não encontrado
*/
private extrairEmpresasIds(userData: any | null): string[] {
if (!userData) {
return [];
}
// Buscar o campo empresasDados
const empresasDados = userData.empresasDados || userData["empresas-dados"] || userData.empresas_dados;
if (Array.isArray(empresasDados)) {
return empresasDados;
}
return [];
}
async buscarProdutosVenda(page: number = 1): Promise<ResponseVenda> {
try {
const response = await fetch(`${this.baseUrl}/produtos-venda?page=${page}&limit=100`, {
method: "GET",
headers: this.getAuthHeaders(),
});
if (!response.ok) {
if (response.status === 401) {
throw new Error("Não autorizado. Por favor, faça login novamente.");
}
throw new Error(`Erro ao buscar produtos de venda: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Erro ao buscar produtos de venda:", error);
throw error;
}
}
async buscarProdutoVendaPorId(id: string): Promise<ProdutoVenda | null> {
try {
const response = await fetch(`${this.baseUrl}/produtos-venda/${id}`, {
method: "GET",
headers: this.getAuthHeaders(),
});
if (!response.ok) {
if (response.status === 404) {
console.warn(`⚠️ [ProdutosVenda] Produto de venda não encontrado: ${id}`);
return null;
}
if (response.status === 401) {
throw new Error("Não autorizado. Por favor, faça login novamente.");
}
throw new Error(`Erro ao buscar produto de venda: ${response.status}`);
}
const produto = await response.json();
return produto;
} catch (error) {
console.error(`❌ [ProdutosVenda] Erro ao buscar produto de venda ${id}:`, error);
throw error;
}
}
// NOTA: Método removido pois agora usamos apenas o endpoint unificado /produtos-venda para estoque
// Mantemos buscarProdutosCatalogo() pois ainda precisamos dos dados de laboratório e categoria
async buscarProdutosCatalogo(page: number = 1): Promise<ResponseCatalogo> {
try {
const response = await fetch(`${this.baseUrl}/produtos-catalogo?page=${page}`, {
method: "GET",
headers: this.getAuthHeaders(),
});
if (!response.ok) {
if (response.status === 401) {
throw new Error("Não autorizado. Por favor, faça login novamente.");
}
throw new Error(`Erro ao buscar produtos do catálogo: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Erro ao buscar produtos do catálogo:", error);
throw error;
}
}
async buscarProdutosCompletos(page: number = 1): Promise<ProdutoCompleto[]> {
try {
// Buscar dados de venda (que agora inclui estoque) e catálogo em paralelo
const [vendaResponse, catalogoResponse] = await Promise.all([
this.buscarProdutosVenda(page),
this.buscarProdutosCatalogo(page),
]);
// Criar um mapa de produtos do catálogo por id para busca rápida
const catalogoMap = new Map<string, ProdutoCatalogo>();
catalogoResponse.documents.forEach(catalogo => {
catalogoMap.set(catalogo.$id, catalogo);
});
// Combinar os dados
const produtosCompletos: ProdutoCompleto[] = vendaResponse.items.map(venda => {
const catalogo = catalogoMap.get(venda.catalogo_id);
return {
id: venda.id,
$id: venda.id, // ✅ Para compatibilidade
catalogo_id: venda.catalogo_id,
nome: catalogo?.nome || `Produto ${venda.catalogo_id}`,
descricao: catalogo?.descricao || venda.observacoes,
preco_venda: venda.preco_venda,
preco_final: venda.preco_final, // ✅ Incluir preco_final da API
preco_base: catalogo?.preco_base || 0,
quantidade_estoque: venda.qtdade_estoque || 0, // ✅ Agora vem do endpoint /produtos-venda
observacoes: venda.observacoes,
data_validade: venda.data_validade,
codigo_ean: catalogo?.codigo_ean || "",
codigo_interno: catalogo?.codigo_interno || "",
laboratorio: catalogo?.laboratorio,
categoria: catalogo?.categoria,
lab_nome: catalogo?.lab_nome, // ✅ Incluir lab_nome do catálogo
cat_nome: catalogo?.cat_nome, // ✅ Incluir cat_nome do catálogo
empresa_id: venda.empresa_id, // ✅ Incluir empresa_id do produto de venda
};
});
return produtosCompletos;
} catch (error) {
console.error("❌ Erro ao buscar produtos completos:", error);
throw error;
}
}
/**
* Busca produtos completos excluindo os da própria empresa do usuário logado
* @param page - Número da página
* @returns Promise<ProdutoCompleto[]> - Lista de produtos filtrados
*/
async buscarProdutosCompletosParaCompra(page: number = 1): Promise<ProdutoCompleto[]> {
try {
// Buscar dados do usuário logado em paralelo com os produtos
const [produtosCompletos, usuarioLogado] = await Promise.all([
this.buscarProdutosCompletos(page),
this.buscarUsuarioLogado()
]);
// Se não conseguir buscar dados do usuário, retornar todos os produtos
if (!usuarioLogado) {
console.warn("⚠️ Não foi possível buscar dados do usuário, retornando todos os produtos");
return produtosCompletos;
}
// Extrair IDs das empresas do usuário
const empresasUsuario = this.extrairEmpresasIds(usuarioLogado);
if (empresasUsuario.length === 0) {
console.warn("⚠️ Usuário não possui empresas associadas, retornando todos os produtos");
return produtosCompletos;
}
// Filtrar produtos que NÃO são da empresa do usuário
const produtosFiltrados = produtosCompletos.filter(produto => {
// Se o produto não tem empresa_id, manter na lista
if (!produto.empresa_id) {
return true;
}
// Excluir produtos das empresas do usuário
const isProdutoDaPropriaEmpresa = empresasUsuario.includes(produto.empresa_id);
return !isProdutoDaPropriaEmpresa;
});
return produtosFiltrados;
} catch (error) {
console.error("❌ Erro ao buscar produtos para compra:", error);
// Em caso de erro, retornar produtos sem filtro para não quebrar a aplicação
console.warn("⚠️ Retornando produtos sem filtro devido ao erro");
return this.buscarProdutosCompletos(page);
}
}
async buscarTodosProdutosCompletos(): Promise<ProdutoCompleto[]> {
try {
let todosProdutos: ProdutoCompleto[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const produtosPagina = await this.buscarProdutosCompletosParaCompra(page);
if (produtosPagina.length > 0) {
todosProdutos.push(...produtosPagina);
page++;
// Se retornou menos que o esperado, provavelmente é a última página
if (produtosPagina.length < 20) { // assumindo que API retorna ~20 por página
hasMore = false;
}
} else {
hasMore = false;
}
}
return todosProdutos;
} catch (error) {
console.error("❌ Erro ao buscar todos os produtos:", error);
throw error;
}
}
async buscarProdutosPorNome(nome: string, page: number = 1): Promise<ProdutoCompleto[]> {
try {
// Por enquanto, vamos buscar todos os produtos e filtrar pelo nome
// Em uma implementação real, a API deveria suportar filtros
const produtos = await this.buscarProdutosCompletosParaCompra(page);
if (!nome.trim()) {
return produtos;
}
const termoBusca = nome.toLowerCase().trim();
return produtos.filter(produto =>
produto.nome.toLowerCase().includes(termoBusca) ||
produto.descricao?.toLowerCase().includes(termoBusca) ||
produto.codigo_ean.toLowerCase().includes(termoBusca) ||
produto.codigo_interno.toLowerCase().includes(termoBusca)
);
} catch (error) {
console.error("❌ Erro ao buscar produtos por nome:", error);
throw error;
}
}
/**
* Atualiza a quantidade de estoque de um produto
*/
async atualizarQuantidadeEstoque(produtoId: string, novaQuantidade: number): Promise<{ success: boolean, data?: any, error?: string }> {
try {
const response = await fetch(`${this.baseUrl}/produtos-venda/${produtoId}`, {
method: "PUT",
headers: this.getAuthHeaders(),
body: JSON.stringify({
qtdade_estoque: novaQuantidade
}),
});
if (!response.ok) {
if (response.status === 401) {
throw new Error("Não autorizado. Por favor, faça login novamente.");
}
const errorData = await response.json();
throw new Error(`Erro ao atualizar estoque: ${errorData.message || response.status}`);
}
const data = await response.json();
return { success: true, data };
} catch (error: any) {
console.error(`❌ Erro ao atualizar estoque do produto ${produtoId}:`, error);
return { success: false, error: error.message };
}
}
/**
* Reduz a quantidade de estoque de um produto
*/
async reduzirEstoque(produtoId: string, quantidadeReduzir: number): Promise<{ success: boolean, data?: any, error?: string }> {
try {
// Primeiro buscar o produto atual para obter a quantidade atual
const produtoAtual = await this.buscarProdutoVendaPorId(produtoId);
if (!produtoAtual) {
return { success: false, error: "Produto não encontrado" };
}
// Buscar dados completos para obter quantidade atual de estoque
const produtosCompletos = await this.buscarProdutosCompletos();
const produtoCompleto = produtosCompletos.find(p => p.id === produtoId);
if (!produtoCompleto) {
return { success: false, error: "Produto completo não encontrado" };
}
const quantidadeAtual = produtoCompleto.quantidade_estoque;
const novaQuantidade = Math.max(0, quantidadeAtual - quantidadeReduzir);
return await this.atualizarQuantidadeEstoque(produtoId, novaQuantidade);
} catch (error: any) {
console.error(`❌ Erro ao reduzir estoque do produto ${produtoId}:`, error);
return { success: false, error: error.message };
}
}
/**
* Processa ajuste de estoque para múltiplos produtos (para checkout)
*/
async processarAjusteEstoquePedido(itens: Array<{ produto: any, quantidade: number }>): Promise<{ sucessos: number, erros: number, detalhes: Array<{ produto: string, sucesso: boolean, erro?: string }> }> {
let sucessos = 0;
let erros = 0;
const detalhes: Array<{ produto: string, sucesso: boolean, erro?: string }> = [];
for (const item of itens) {
try {
const produtoId = item.produto.id;
const quantidadeReduzir = item.quantidade;
const resultado = await this.reduzirEstoque(produtoId, quantidadeReduzir);
if (resultado.success) {
sucessos++;
detalhes.push({
produto: item.produto.nome,
sucesso: true
});
} else {
erros++;
detalhes.push({
produto: item.produto.nome,
sucesso: false,
erro: resultado.error
});
console.error(`❌ Erro ao ajustar estoque: ${item.produto.nome} - ${resultado.error}`);
}
} catch (error: any) {
erros++;
detalhes.push({
produto: item.produto.nome,
sucesso: false,
erro: error.message
});
console.error(`💥 Erro ao processar estoque do produto ${item.produto.nome}:`, error);
}
}
return { sucessos, erros, detalhes };
}
}
export const produtosVendaService = new ProdutosVendaService();
export type { ProdutoCompleto, ProdutoVenda, ProdutoEstoque, ProdutoCatalogo };