ifixkart-storefront/services/api/catalogService.ts

405 lines
11 KiB
TypeScript

import { apiFetch } from './client';
import { API_ENDPOINTS } from './config';
export interface BrandResponse {
brand_id: string;
name: string;
slug: string;
logo_url: string | null;
is_active: boolean;
created_at: string;
}
export interface CategoryResponse {
category_id: string;
parent_category_id: string | null;
name: string;
slug: string;
description: string | null;
image_url: string | null;
sort_order: string;
is_parent_feature?: boolean;
is_active: boolean;
product_count?: number;
created_at: string;
}
// --- Parent Hierarchy Types ---
export interface HierarchyModelItem {
model_id: string;
name: string;
slug: string;
image_url: string | null;
}
export interface HierarchySeriesItem {
series_id: string;
name: string;
slug: string;
models: HierarchyModelItem[];
}
export interface HierarchyBrandItem {
brand_id: string;
name: string;
slug: string;
logo_url: string | null;
series: HierarchySeriesItem[];
}
export interface CategoryParentHierarchyResponse {
category_id: string;
brands: HierarchyBrandItem[];
}
export interface DeviceSeriesResponse {
series_id: string;
brand_id: string;
name: string;
slug: string;
sort_order: number;
is_active: boolean;
}
export interface VariantAttributeResponse {
id: string;
variant_id: string;
attribute_id: string;
attribute_value: string;
attribute_name?: string;
attribute_code?: string;
}
export interface VariantImageResponse {
image_id: string;
variant_id: string;
image_url: string;
sort_order: number;
is_primary: boolean;
}
export interface ProductVariantResponse {
variant_id: string;
product_id: string;
sku: string;
barcode: string | null;
/** API may return decimal strings like "199.00" */
price: number | string;
compare_price: number | string | null;
cost_price: number | string;
low_stock_threshold: number;
status: string;
available_stock?: number;
attributes?: VariantAttributeResponse[];
images?: VariantImageResponse[];
}
export interface ProductImageResponse {
image_id: string;
product_id: string;
image_url: string;
alt_text: string | null;
sort_order: number;
is_banner: boolean;
}
export interface ProductResponse {
product_id: string;
category_id: string;
brand_id: string | null;
device_series_id: string | null;
device_model_id: string | null;
name: string;
slug: string;
full_path: string;
description: string | null;
status: string;
created_at: string;
images: ProductImageResponse[];
variants: ProductVariantResponse[];
rating?: number;
review_count?: number;
badge?: 'HOT' | 'NEW' | 'SALE' | null;
/** Present on list cards; used for Sale / Hot highlight filters */
discount_percent?: number;
color?: string | null;
colors?: string[];
brand_name?: string | null;
}
export interface DeviceModelResponse {
model_id: string;
series_id: string;
brand_id: string;
name: string;
slug: string;
full_path: string;
release_year: number | null;
image_url: string | null;
is_active: boolean;
}
export interface ServiceTypeResponse {
service_type_id: string;
name: string;
slug: string;
icon_url: string | null;
description: string | null;
is_active: boolean;
}
export interface RepairServiceResponse {
repair_service_id: string;
model_id: string;
service_type_id: string;
slug: string;
full_path: string;
description: string | null;
}
export interface ProductCardResponse {
product_id: string;
category_id?: string;
slug: string;
name: string;
thumbnail_url: string | null;
/** List API returns decimal strings like "199.00" */
price: number | string;
compare_price: number | string | null;
discount_percent: number;
rating: number;
stock_count: number;
first_variant_id?: string | null;
badge: string | null;
brand_name: string | null;
color?: string | null;
colors?: string[];
}
export interface ProductPaginatedResponse {
total: number;
page: number;
limit: number;
cursor: string | null;
products: ProductCardResponse[];
}
export const catalogService = {
async getBrands(categoryId?: string): Promise<BrandResponse[]> {
try {
const url = categoryId
? `${API_ENDPOINTS.CATALOG_BRANDS_ALL}?category_id=${categoryId}`
: API_ENDPOINTS.CATALOG_BRANDS_ALL;
return await apiFetch<BrandResponse[]>(url);
} catch {
return MOCK_BRANDS;
}
},
async getCategories(): Promise<CategoryResponse[]> {
try {
return await apiFetch<CategoryResponse[]>(API_ENDPOINTS.CATALOG_CATEGORIES_ALL);
} catch {
return MOCK_CATEGORIES;
}
},
async getProducts(): Promise<ProductResponse[]> {
try {
const data = await apiFetch<any>(`${API_ENDPOINTS.CATALOG_PRODUCTS_ALL}?page=1&limit=48`);
const products = Array.isArray(data) ? data : data?.products || [];
return products;
} catch {
return [];
}
},
async getProductsPaginated(params: Record<string, any> = {}): Promise<ProductPaginatedResponse> {
const query = new URLSearchParams();
Object.entries(params).forEach(([key, val]) => {
if (val !== undefined && val !== null && val !== '') {
query.append(key, String(val));
}
});
try {
const queryString = query.toString();
const endpoint = `/api/v1/catalog/products${queryString ? '?' + queryString : ''}`;
return await apiFetch<ProductPaginatedResponse>(endpoint);
} catch (e) {
const limit = Number(params.limit) || 24;
const page = Number(params.page) || 1;
let cards = MOCK_PRODUCTS.map((p) => {
const firstVar = p.variants[0];
const price = firstVar?.price ? Number(firstVar.price) : 0;
const compare_price = firstVar?.compare_price ? Number(firstVar.compare_price) : null;
let discount_percent = 0;
if (price && compare_price && compare_price > price) {
discount_percent = Math.round(((compare_price - price) / compare_price) * 100);
}
const colorAttr = firstVar?.attributes?.find((a) =>
`${a.attribute_code || ''} ${a.attribute_name || ''}`.toLowerCase().includes('color')
);
return {
product_id: p.product_id,
category_id: p.category_id,
slug: p.slug,
name: p.name,
thumbnail_url: p.images[0]?.image_url || null,
price,
compare_price,
discount_percent,
rating: p.rating || 4.8,
stock_count: 15,
badge: p.badge || null,
brand_name: MOCK_BRANDS.find((b) => b.brand_id === p.brand_id)?.name || 'Apple',
brand_id: p.brand_id,
color: colorAttr?.attribute_value || null,
colors: colorAttr?.attribute_value ? [colorAttr.attribute_value] : [],
};
});
if (params.category) {
cards = cards.filter((p) => p.category_id === params.category);
}
if (params.brand) {
cards = cards.filter(
(p) =>
p.brand_id === params.brand ||
String(p.brand_name).toLowerCase() === String(params.brand).toLowerCase()
);
}
if (params.search) {
const q = String(params.search).toLowerCase();
cards = cards.filter(
(p) =>
p.name.toLowerCase().includes(q) ||
String(p.color || '').toLowerCase().includes(q) ||
String(p.brand_name || '').toLowerCase().includes(q)
);
}
const total = cards.length;
const start = (page - 1) * limit;
return {
total,
page,
limit,
cursor: null,
products: cards.slice(start, start + limit),
};
}
},
async getProductDetail(slug: string): Promise<any> {
try {
return await apiFetch<any>(`/api/v1/catalog/products/detail/${slug}`);
} catch (e) {
const found = MOCK_PRODUCTS.find(p => p.slug === slug) || MOCK_PRODUCTS[0];
return {
product: found,
reviews: [
{ review_id: 'rev_1', author_name: 'Alex Morgan', rating: 5, verified_buyer: true, review_date: '2026-07-20', title: 'Exceptional Quality!', comment: 'Exceptional build quality and super-fast delivery!' }
],
related_products: MOCK_PRODUCTS.filter(p => p.product_id !== found.product_id),
compatible_devices: [
{ model_id: 'm1', name: 'iPhone 15 Pro Max', slug: 'iphone-15-pro-max' }
],
specifications: [
{ label: 'Manufacturer', value: 'Apple' },
{ label: 'Model Reference', value: found.name }
]
};
}
},
async getProductsBySlugs(slugs: string[]): Promise<ProductCardResponse[]> {
const unique = [...new Set(slugs.map((s) => String(s || '').trim()).filter(Boolean))];
if (!unique.length) return [];
const cards = await Promise.all(
unique.map(async (slug) => {
try {
const data = await apiFetch<any>(`/api/v1/catalog/products/detail/${slug}`);
const p = data?.product || data;
if (!p?.slug && !p?.product_id) return null;
const variant = Array.isArray(p.variants) ? p.variants[0] : null;
const thumb =
p.thumbnail_url ||
p.images?.[0]?.image_url ||
variant?.images?.[0]?.image_url ||
null;
return {
product_id: p.product_id || slug,
category_id: p.category_id,
slug: p.slug || slug,
name: p.name || slug,
thumbnail_url: thumb,
price: variant?.price ?? p.price ?? 0,
compare_price: variant?.compare_price ?? p.compare_price ?? null,
discount_percent: Number(p.discount_percent) || 0,
rating: Number(p.rating) || 5,
stock_count: Number(p.stock_count) || 0,
first_variant_id: variant?.variant_id || null,
badge: p.badge || null,
brand_name: p.brand_name || null,
} as ProductCardResponse;
} catch {
return null;
}
})
);
return cards.filter((c): c is ProductCardResponse => Boolean(c));
},
async getDeviceModels(): Promise<DeviceModelResponse[]> {
try {
return await apiFetch<DeviceModelResponse[]>(API_ENDPOINTS.CATALOG_DEVICE_MODELS_ALL);
} catch {
return MOCK_DEVICE_MODELS;
}
},
async getServiceTypes(): Promise<ServiceTypeResponse[]> {
try {
return await apiFetch<ServiceTypeResponse[]>(API_ENDPOINTS.CATALOG_SERVICE_TYPES_ALL);
} catch {
return MOCK_SERVICE_TYPES;
}
},
async getRepairServices(): Promise<RepairServiceResponse[]> {
try {
return await apiFetch<RepairServiceResponse[]>(API_ENDPOINTS.CATALOG_REPAIR_SERVICES_ALL);
} catch {
return [];
}
},
async getParentHierarchy(categoryId: string): Promise<CategoryParentHierarchyResponse | null> {
try {
return await apiFetch<CategoryParentHierarchyResponse>(
`/api/v1/storefront/catalog/categories/${categoryId}/parent-hierarchy`
);
} catch {
return null;
}
},
async getDeviceSeries(): Promise<DeviceSeriesResponse[]> {
try {
return await apiFetch<DeviceSeriesResponse[]>('/api/v1/catalog/device-series/all');
} catch {
return [];
}
},
};
// Fallback catalog mock dataset (empty by default to ensure only real API data renders)
export const MOCK_BRANDS: BrandResponse[] = [];
export const MOCK_CATEGORIES: CategoryResponse[] = [];
export const MOCK_PRODUCTS: ProductResponse[] = [];
export const MOCK_DEVICE_MODELS: DeviceModelResponse[] = [];
export const MOCK_SERVICE_TYPES: ServiceTypeResponse[] = [];