618 lines
18 KiB
TypeScript
618 lines
18 KiB
TypeScript
/**
|
|
* Product Service
|
|
* Serviço para gerenciar operações relacionadas a produtos, departamentos e filtros
|
|
*/
|
|
|
|
import { env } from '../config/env';
|
|
import { authService } from './auth.service';
|
|
|
|
const API_URL = env.API_URL;
|
|
|
|
export interface Categoria {
|
|
codigoSecao: number;
|
|
codigoCategoria: number;
|
|
descricaoCategoria: string;
|
|
tituloECommerce: string;
|
|
url: string;
|
|
}
|
|
|
|
export interface Secao {
|
|
codigoSecao: number;
|
|
codigoDepartamento: number;
|
|
descricaoSecao: string;
|
|
tituloEcommerce: string;
|
|
url: string;
|
|
categorias: Categoria[];
|
|
}
|
|
|
|
export interface ClasseMercadologica {
|
|
codigoDepartamento: number;
|
|
descricaoDepartamento: string;
|
|
tituloEcommerce: string;
|
|
url: string;
|
|
secoes: Secao[];
|
|
}
|
|
|
|
export interface FilterProduct {
|
|
brands?: string[];
|
|
text?: string;
|
|
urlCategory?: string;
|
|
outLine?: boolean;
|
|
campaign?: boolean;
|
|
onlyWithStock?: boolean;
|
|
promotion?: boolean;
|
|
oportunity?: boolean;
|
|
markdown?: boolean;
|
|
productPromotion?: boolean;
|
|
offers?: boolean;
|
|
storeStock?: string;
|
|
orderBy?: string;
|
|
percentOffMin?: number;
|
|
percentOffMax?: number;
|
|
}
|
|
|
|
export interface SaleProduct {
|
|
id: number;
|
|
brand: string;
|
|
category: string;
|
|
[key: string]: any;
|
|
}
|
|
|
|
export interface StoreERP {
|
|
id: string;
|
|
name: string;
|
|
shortName: string;
|
|
}
|
|
|
|
class ProductService {
|
|
/**
|
|
* Obtém o token de autenticação
|
|
*/
|
|
private getAuthHeaders(): HeadersInit {
|
|
const token = authService.getToken();
|
|
const headers: HeadersInit = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
/**
|
|
* Obtém os headers com informações da loja
|
|
*/
|
|
private getStoreHeaders(store?: string): HeadersInit {
|
|
const headers = this.getAuthHeaders();
|
|
const storeName = store || authService.getStore() || '';
|
|
|
|
if (!storeName || storeName.trim() === '') {
|
|
throw new Error('Loja não informada. É necessário informar a loja para buscar produtos.');
|
|
}
|
|
|
|
headers['x-store'] = storeName;
|
|
|
|
return headers;
|
|
}
|
|
|
|
/**
|
|
* Carrega as classes mercadológicas (departamentos, seções e categorias)
|
|
*/
|
|
async getClasseMercadologica(): Promise<ClasseMercadologica[]> {
|
|
try {
|
|
const response = await fetch(`${API_URL}sales/departments`, {
|
|
method: 'GET',
|
|
headers: this.getAuthHeaders(),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Erro ao carregar departamentos: ${response.statusText}`);
|
|
}
|
|
|
|
const data: ClasseMercadologica[] = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Erro ao carregar departamentos:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Busca produtos com filtros
|
|
*/
|
|
async getProductByFilter(
|
|
store: string,
|
|
page: number,
|
|
size: number,
|
|
filterProduct?: FilterProduct
|
|
): Promise<SaleProduct[]> {
|
|
try {
|
|
// Validar se a loja foi fornecida
|
|
if (!store || store.trim() === '') {
|
|
throw new Error('Loja não informada. É necessário informar a loja para buscar produtos.');
|
|
}
|
|
|
|
const headers = this.getStoreHeaders(store);
|
|
headers['x-page'] = page.toString();
|
|
headers['x-size-count'] = size.toString();
|
|
|
|
let response: Response;
|
|
const url = `${API_URL}sales/products`;
|
|
|
|
if (filterProduct) {
|
|
response = await fetch(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(filterProduct),
|
|
});
|
|
} else {
|
|
// GET sem body, apenas headers
|
|
response = await fetch(url, {
|
|
method: 'GET',
|
|
headers,
|
|
});
|
|
}
|
|
|
|
if (!response.ok) {
|
|
let errorMessage = response.statusText;
|
|
let errorDetails: any = null;
|
|
try {
|
|
errorDetails = await response.json();
|
|
errorMessage = errorDetails?.message || errorDetails?.error || errorMessage;
|
|
} catch {
|
|
// Se não conseguir parsear o JSON, usa a mensagem padrão
|
|
}
|
|
console.error('Erro na API:', {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
errorDetails,
|
|
});
|
|
throw new Error(`Erro ao buscar produtos: ${errorMessage}`);
|
|
}
|
|
|
|
let result: any;
|
|
try {
|
|
result = await response.json();
|
|
} catch (error) {
|
|
console.error('Erro ao parsear resposta JSON:', error);
|
|
throw new Error('Resposta da API não é um JSON válido');
|
|
}
|
|
|
|
// A API pode retornar diretamente o array ou dentro de um wrapper { data: [...] }
|
|
let data: SaleProduct[] = [];
|
|
|
|
// Verificar se result existe e é válido
|
|
if (!result) {
|
|
console.warn('Resposta da API é nula ou indefinida');
|
|
return [];
|
|
}
|
|
|
|
if (Array.isArray(result)) {
|
|
data = result;
|
|
} else if (result && typeof result === 'object' && result.data !== undefined) {
|
|
// Verificar se result.data é um array antes de atribuir
|
|
if (Array.isArray(result.data)) {
|
|
data = result.data;
|
|
} else {
|
|
console.warn('result.data não é um array:', result.data);
|
|
return [];
|
|
}
|
|
} else if (result && typeof result === 'object') {
|
|
// Se for um objeto mas não tiver data, pode ser um erro ou formato inesperado
|
|
console.warn('Resposta da API em formato inesperado:', result);
|
|
return [];
|
|
} else {
|
|
console.warn('Resposta da API não é um array ou objeto válido:', result);
|
|
return [];
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Erro ao buscar produtos:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Busca produtos por departamento/categoria
|
|
* Segue o padrão do Angular: getProductByDepartment(store, page, size, urlDepartment)
|
|
* Endpoint: GET /sales/product/category/{urlDepartment}
|
|
*/
|
|
async getProductByDepartment(
|
|
store: string,
|
|
page: number,
|
|
size: number,
|
|
urlDepartment: string
|
|
): Promise<SaleProduct[]> {
|
|
try {
|
|
// Validar se a loja foi fornecida
|
|
if (!store || store.trim() === '') {
|
|
throw new Error('Loja não informada. É necessário informar a loja para buscar produtos.');
|
|
}
|
|
|
|
const headers = this.getStoreHeaders(store);
|
|
headers['x-page'] = page.toString();
|
|
headers['x-size-count'] = size.toString();
|
|
|
|
// No Angular, sempre usa 'category/' mesmo que seja department
|
|
// Se urlDepartment contém '/', já está no formato correto
|
|
// Caso contrário, usa 'category/' + urlDepartment
|
|
let url = '';
|
|
if (urlDepartment.includes('/')) {
|
|
url = `category/${urlDepartment}`;
|
|
} else {
|
|
url = `category/${urlDepartment}`;
|
|
}
|
|
|
|
const response = await fetch(`${API_URL}sales/product/${url}`, {
|
|
method: 'GET',
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let errorMessage = response.statusText;
|
|
let errorDetails: any = null;
|
|
try {
|
|
errorDetails = await response.json();
|
|
errorMessage = errorDetails?.message || errorDetails?.error || errorMessage;
|
|
} catch {
|
|
// Se não conseguir parsear o JSON, usa a mensagem padrão
|
|
}
|
|
console.error('Erro na API:', {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
errorDetails,
|
|
});
|
|
throw new Error(`Erro ao buscar produtos por departamento: ${errorMessage}`);
|
|
}
|
|
|
|
let result: any;
|
|
try {
|
|
result = await response.json();
|
|
} catch (error) {
|
|
console.error('Erro ao parsear resposta JSON:', error);
|
|
throw new Error('Resposta da API não é um JSON válido');
|
|
}
|
|
|
|
// A API pode retornar diretamente o array ou dentro de um wrapper
|
|
let data: SaleProduct[] = [];
|
|
|
|
if (!result) {
|
|
console.warn('Resposta da API é nula ou indefinida');
|
|
return [];
|
|
}
|
|
|
|
if (Array.isArray(result)) {
|
|
data = result;
|
|
} else if (result && typeof result === 'object' && result.data !== undefined) {
|
|
if (Array.isArray(result.data)) {
|
|
data = result.data;
|
|
} else {
|
|
console.warn('result.data não é um array:', result.data);
|
|
return [];
|
|
}
|
|
} else if (result && typeof result === 'object') {
|
|
console.warn('Resposta da API em formato inesperado:', result);
|
|
return [];
|
|
} else {
|
|
console.warn('Resposta da API não é um array ou objeto válido:', result);
|
|
return [];
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Erro ao buscar produtos por departamento:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Busca produtos por termo de pesquisa
|
|
*/
|
|
async searchProduct(
|
|
store: string,
|
|
page: number,
|
|
size: number,
|
|
search: string
|
|
): Promise<SaleProduct[]> {
|
|
try {
|
|
const headers = this.getStoreHeaders(store);
|
|
headers['x-page'] = page.toString();
|
|
headers['x-size-count'] = size.toString();
|
|
|
|
const response = await fetch(`${API_URL}sales/products/${encodeURIComponent(search)}`, {
|
|
method: 'GET',
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let errorMessage = response.statusText;
|
|
let errorDetails: any = null;
|
|
try {
|
|
errorDetails = await response.json();
|
|
errorMessage = errorDetails?.message || errorDetails?.error || errorMessage;
|
|
} catch {
|
|
// Se não conseguir parsear o JSON, usa a mensagem padrão
|
|
}
|
|
console.error('Erro na API:', {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
errorDetails,
|
|
});
|
|
throw new Error(`Erro ao buscar produtos: ${errorMessage}`);
|
|
}
|
|
|
|
let result: any;
|
|
try {
|
|
result = await response.json();
|
|
} catch (error) {
|
|
console.error('Erro ao parsear resposta JSON:', error);
|
|
throw new Error('Resposta da API não é um JSON válido');
|
|
}
|
|
|
|
// A API pode retornar diretamente o array ou dentro de um wrapper
|
|
let data: SaleProduct[] = [];
|
|
|
|
if (!result) {
|
|
console.warn('Resposta da API é nula ou indefinida');
|
|
return [];
|
|
}
|
|
|
|
if (Array.isArray(result)) {
|
|
data = result;
|
|
} else if (result && typeof result === 'object' && result.data !== undefined) {
|
|
if (Array.isArray(result.data)) {
|
|
data = result.data;
|
|
} else {
|
|
console.warn('result.data não é um array:', result.data);
|
|
return [];
|
|
}
|
|
} else if (result && typeof result === 'object') {
|
|
console.warn('Resposta da API em formato inesperado:', result);
|
|
return [];
|
|
} else {
|
|
console.warn('Resposta da API não é um array ou objeto válido:', result);
|
|
return [];
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Erro ao buscar produtos:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extrai marcas únicas de uma lista de produtos
|
|
*/
|
|
extractBrandsFromProducts(products: SaleProduct[]): string[] {
|
|
const brandsSet = new Set<string>();
|
|
|
|
products.forEach((product) => {
|
|
if (product.brand) {
|
|
const brand = product.brand.replace('#', '').trim();
|
|
if (brand) {
|
|
brandsSet.add(brand.toUpperCase());
|
|
}
|
|
}
|
|
});
|
|
|
|
return Array.from(brandsSet).sort();
|
|
}
|
|
|
|
/**
|
|
* Carrega as filiais disponíveis para o usuário
|
|
* Segue o mesmo padrão do Angular: lookupService.getStore()
|
|
*/
|
|
async getStores(): Promise<StoreERP[]> {
|
|
try {
|
|
const user = authService.getUser();
|
|
if (!user || !user.id) {
|
|
throw new Error('Usuário não encontrado. É necessário estar autenticado.');
|
|
}
|
|
|
|
// No Angular: lookupService.getStore() chama lists/store/user/${this.authService.getUser()}
|
|
// onde getUser() retorna apenas o ID do usuário
|
|
const userId = user.id;
|
|
const response = await fetch(`${API_URL}lists/store/user/${userId}`, {
|
|
method: 'GET',
|
|
headers: this.getAuthHeaders(),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
|
throw new Error(`Erro ao carregar filiais: ${errorData.message || response.statusText}`);
|
|
}
|
|
|
|
// A API pode retornar diretamente o array ou dentro de um wrapper
|
|
const result = await response.json();
|
|
|
|
// Se retornar dentro de um wrapper { data: [...] }, extrair o data
|
|
// Caso contrário, retornar diretamente
|
|
const data: StoreERP[] = Array.isArray(result) ? result : (result.data || result);
|
|
|
|
if (!Array.isArray(data)) {
|
|
throw new Error('Formato de resposta inválido da API');
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Erro ao carregar filiais:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtém detalhes completos de um produto de venda
|
|
* Endpoint: GET /sales/product/:id
|
|
*/
|
|
async getProductDetail(store: string, id: number): Promise<SaleProduct> {
|
|
try {
|
|
if (!store || store.trim() === "") {
|
|
throw new Error("Loja não informada. É necessário informar a loja.");
|
|
}
|
|
|
|
const headers = this.getStoreHeaders(store);
|
|
const response = await fetch(`${API_URL}sales/product/${id}`, {
|
|
method: "GET",
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
|
throw new Error(`Erro ao buscar detalhes do produto: ${errorData.message || response.statusText}`);
|
|
}
|
|
|
|
const data: SaleProduct = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error("Erro ao buscar detalhes do produto:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtém produtos que compram junto
|
|
* Endpoint: GET /sales/product/bytogether/:id
|
|
*/
|
|
async getProductsBuyTogether(store: string, id: number): Promise<SaleProduct[]> {
|
|
try {
|
|
if (!store || store.trim() === "") {
|
|
throw new Error("Loja não informada. É necessário informar a loja.");
|
|
}
|
|
|
|
const headers = this.getStoreHeaders(store);
|
|
const response = await fetch(`${API_URL}sales/product/bytogether/${id}`, {
|
|
method: "GET",
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
|
throw new Error(`Erro ao buscar produtos compre junto: ${errorData.message || response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
const data: SaleProduct[] = Array.isArray(result) ? result : (result.data || []);
|
|
return data;
|
|
} catch (error) {
|
|
console.error("Erro ao buscar produtos compre junto:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtém produtos similares
|
|
* Endpoint: GET /sales/product/simil/:id
|
|
*/
|
|
async getProductsSimilar(store: string, id: number): Promise<SaleProduct[]> {
|
|
try {
|
|
if (!store || store.trim() === "") {
|
|
throw new Error("Loja não informada. É necessário informar a loja.");
|
|
}
|
|
|
|
const headers = this.getStoreHeaders(store);
|
|
const response = await fetch(`${API_URL}sales/product/simil/${id}`, {
|
|
method: "GET",
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
|
throw new Error(`Erro ao buscar produtos similares: ${errorData.message || response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
const data: SaleProduct[] = Array.isArray(result) ? result : (result.data || []);
|
|
return data;
|
|
} catch (error) {
|
|
console.error("Erro ao buscar produtos similares:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtém estoques de um produto em todas as filiais
|
|
* Endpoint: GET /sales/stock/:storeId/:id
|
|
*/
|
|
async getProductStocks(store: string, id: number): Promise<Array<{
|
|
store: string;
|
|
storeName: string;
|
|
quantity: number;
|
|
work: boolean;
|
|
blocked: string;
|
|
breakdown: number;
|
|
transfer: number;
|
|
allowDelivery: number;
|
|
}>> {
|
|
try {
|
|
if (!store || store.trim() === "") {
|
|
throw new Error("Loja não informada. É necessário informar a loja.");
|
|
}
|
|
|
|
const headers = this.getAuthHeaders();
|
|
const response = await fetch(`${API_URL}sales/stock/${store}/${id}`, {
|
|
method: "GET",
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
|
throw new Error(`Erro ao buscar estoques: ${errorData.message || response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
const data = Array.isArray(result) ? result : (result.data || []);
|
|
return data;
|
|
} catch (error) {
|
|
console.error("Erro ao buscar estoques:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtém opções de parcelamento de um produto
|
|
* Endpoint: GET /sales/installment/:id?quantity=:quantity
|
|
*/
|
|
async getProductInstallments(
|
|
store: string,
|
|
id: number,
|
|
quantity: number = 1
|
|
): Promise<Array<{
|
|
installment: number;
|
|
installmentValue: number;
|
|
}>> {
|
|
try {
|
|
if (!store || store.trim() === "") {
|
|
throw new Error("Loja não informada. É necessário informar a loja.");
|
|
}
|
|
|
|
const headers = this.getStoreHeaders(store);
|
|
const url = new URL(`${API_URL}sales/installment/${id}`);
|
|
url.searchParams.append("quantity", quantity.toString());
|
|
|
|
const response = await fetch(url.toString(), {
|
|
method: "GET",
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
|
throw new Error(`Erro ao buscar parcelamento: ${errorData.message || response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
const data = Array.isArray(result) ? result : (result.data || []);
|
|
return data;
|
|
} catch (error) {
|
|
console.error("Erro ao buscar parcelamento:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const productService = new ProductService();
|
|
|