482 lines
15 KiB
TypeScript
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 };
|