import api from './api'; import { PRODUCTS, Product, PetType } from "../data/products"; import { Banner } from "../types"; const CATEGORY_SLUG_TO_NAME: Record = { 'joints': 'مفاصل و استخوان', 'immune': 'تقویت سیستم ایمنی و گوارش', 'energy': 'ویتامین‌ها و انرژی‌بخش‌ها', 'special-care': 'مراقبت‌های ویژه (پوست، دندان و چشم)', 'general': 'تقویت عمومی', 'nutrition': 'تغذیه تخصصی', 'supplements': 'مکمل‌های غذایی و درمانی', }; interface BackendProduct { id?: string; artNo?: string; nameFa?: string; name?: string; nameEn?: string; scientificTagline?: string; description?: string; shortDescription?: string; priceDisplay?: string; priceValue?: number; category?: { name?: string } | string; categorySlug?: string; unit?: string; packageSize?: number; dosageLogic?: string; benefits?: string; suitableFor?: string; requiresRx?: boolean; storage?: string; specialBadge?: string; onSetOfAction?: string; optimisticTemplate?: string; feedingAdvice?: string; slug?: string; imageUrl?: string; image?: string; images?: string[]; podcastUrl?: string; podcastTitle?: string; podcastDescription?: string; podcastCover?: string; videoUrl?: string; videoTitle?: string; videoDescription?: string; videoCover?: string; pdfUrl?: string; pdfTitle?: string; pdfDescription?: string; pdfCover?: string; productGroup?: string; ingredientList?: Array<{ ingredient: string }>; ingredients?: string; symptoms?: Array<{ symptom?: string } | string>; keyBenefits?: unknown; expectedResults?: unknown; scientificValidation?: unknown; doctorNotes?: unknown; scientificArticles?: unknown; compositionTable?: unknown; analyticalConstituents?: unknown; additivesPerKg?: unknown; faqList?: unknown; specialist?: unknown; } export class ProductService { private static instance: ProductService; private constructor() {} public static getInstance(): ProductService { if (!ProductService.instance) { ProductService.instance = new ProductService(); } return ProductService.instance; } private mapBackendToFrontend(inputData: Record): Product { const data = inputData as unknown as BackendProduct; // Find the local static product to inherit functions like calculateDosage const local = PRODUCTS.find(p => p.artNo === data.artNo || p.id === data.productGroup || (typeof data.slug === 'string' && data.slug.includes(p.id)) ); const safeParse = (val: unknown, fallback: T): T => { if (!val) return fallback; if (typeof val === 'string') { try { return JSON.parse(val) as T; } catch { return fallback; } } return val as T; }; const categoryName = typeof data.category === 'object' && data.category !== null ? data.category.name : (typeof data.category === 'string' ? data.category : (CATEGORY_SLUG_TO_NAME[data.categorySlug || ''] || data.categorySlug || '')); const priceVal = Number(data.priceValue || 0); const defaultSpecialist = local?.specialist || { name: 'تیم تخصصی کنینا', title: 'مشاور علمی و دارویی', image: '/images/vets/vet1.webp', message: 'تمامی محصولات مکمل کنینا دارای استانداردهای دارویی آلمان و تاییدیه کلینیکی هستند.' }; return { id: data.id || '', artNo: data.artNo || '', name: data.nameFa || data.name || '', nameFa: data.nameFa || data.name || '', nameEn: data.nameEn || '', scientificTagline: data.scientificTagline || '', description: data.description || '', shortDescription: data.shortDescription || '', price: data.priceDisplay || `${priceVal.toLocaleString('fa-IR')} تومان`, priceValue: priceVal, category: categoryName || '', categorySlug: data.categorySlug || '', unit: data.unit || 'عدد', packageSize: data.packageSize !== undefined && data.packageSize !== null ? Number(data.packageSize) : 100, dosage_logic: data.dosageLogic || '', benefits: data.benefits || '', suitableFor: (data.suitableFor as PetType) || 'سگ', requiresRx: Boolean(data.requiresRx), storage: data.storage, specialBadge: data.specialBadge, onSetOfAction: data.onSetOfAction, optimisticTemplate: data.optimisticTemplate, feedingAdvice: data.dosageLogic || data.feedingAdvice || '', slug: data.slug, image: (() => { const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, ''); if (!data.imageUrl) return local?.image || '/assets/images/hero-section-image.png'; if (data.imageUrl.startsWith('http://') || data.imageUrl.startsWith('https://')) return data.imageUrl; if (data.imageUrl.startsWith('/uploads/')) return `${apiBase}${data.imageUrl}`; if (data.imageUrl.startsWith('/products/')) return local?.image || '/assets/images/hero-section-image.png'; return local?.image || data.imageUrl || '/assets/images/hero-section-image.png'; })(), images: Array.isArray(data.images) && data.images.length > 0 ? data.images.map((img: string) => { const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, ''); if (img.startsWith('http://') || img.startsWith('https://')) return img; if (img.startsWith('/uploads/')) return `${apiBase}${img}`; return img; }) : (local?.images || []), main_ingredients: data.ingredientList?.map(i => i.ingredient) || data.ingredients?.split(/[،,-]/).map(s => s.trim()).filter(Boolean) || [], symptoms: data.symptoms?.map(s => typeof s === 'string' ? s : s.symptom || '').filter(Boolean) || [], keyBenefits: safeParse(data.keyBenefits, local?.keyBenefits || []), expectedResults: safeParse(data.expectedResults, local?.expectedResults || []), analysis: safeParse>(data.compositionTable, local?.analysis || {}), specialist: safeParse(data.specialist, defaultSpecialist), calculateDosage: local?.calculateDosage, podcastUrl: (() => { const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, ''); if (!data.podcastUrl) return undefined; if (data.podcastUrl.startsWith('http://') || data.podcastUrl.startsWith('https://')) return data.podcastUrl; if (data.podcastUrl.startsWith('/uploads/')) return `${apiBase}${data.podcastUrl}`; return data.podcastUrl; })(), podcastTitle: data.podcastTitle || undefined, podcastDescription: data.podcastDescription || undefined, podcastCover: (() => { const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, ''); if (!data.podcastCover) return undefined; if (data.podcastCover.startsWith('http://') || data.podcastCover.startsWith('https://')) return data.podcastCover; if (data.podcastCover.startsWith('/uploads/')) return `${apiBase}${data.podcastCover}`; return data.podcastCover; })(), videoUrl: (() => { const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, ''); if (!data.videoUrl) return undefined; if (data.videoUrl.startsWith('http://') || data.videoUrl.startsWith('https://')) return data.videoUrl; if (data.videoUrl.startsWith('/uploads/')) return `${apiBase}${data.videoUrl}`; return data.videoUrl; })(), videoTitle: data.videoTitle || undefined, videoDescription: data.videoDescription || undefined, videoCover: (() => { const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, ''); if (!data.videoCover) return undefined; if (data.videoCover.startsWith('http://') || data.videoCover.startsWith('https://')) return data.videoCover; if (data.videoCover.startsWith('/uploads/')) return `${apiBase}${data.videoCover}`; return data.videoCover; })(), pdfUrl: (() => { const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, ''); if (!data.pdfUrl) return undefined; if (data.pdfUrl.startsWith('http://') || data.pdfUrl.startsWith('https://')) return data.pdfUrl; if (data.pdfUrl.startsWith('/uploads/')) return `${apiBase}${data.pdfUrl}`; return data.pdfUrl; })(), pdfTitle: data.pdfTitle || undefined, pdfDescription: data.pdfDescription || undefined, pdfCover: (() => { const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, ''); if (!data.pdfCover) return undefined; if (data.pdfCover.startsWith('http://') || data.pdfCover.startsWith('https://')) return data.pdfCover; if (data.pdfCover.startsWith('/uploads/')) return `${apiBase}${data.pdfCover}`; return data.pdfCover; })(), barcode: (inputData.barcode as string) || undefined, metaTitle: (inputData.metaTitle as string) || undefined, metaDescription: (inputData.metaDescription as string) || undefined, canonicalUrl: (inputData.canonicalUrl as string) || undefined, keywords: typeof inputData.keywords === 'string' ? inputData.keywords : undefined, stockStatus: (inputData.stockStatus as 'IN_STOCK' | 'OUT_OF_STOCK' | 'PRE_ORDER' | 'DISCONTINUED') || 'IN_STOCK', noIndex: Boolean(inputData.noIndex), noFollow: Boolean(inputData.noFollow), ogImage: (() => { const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, ''); const og = inputData.ogImage as string; if (!og) return undefined; if (og.startsWith('http://') || og.startsWith('https://')) return og; if (og.startsWith('/uploads/')) return `${apiBase}${og}`; return og; })(), featuredImageAlt: (inputData.featuredImageAlt as string) || undefined, updatedAt: (inputData.updatedAt as string) || (inputData.createdAt as string) || undefined, reviews: Array.isArray(inputData.reviews) ? inputData.reviews as Product['reviews'] : undefined, }; } private async serverFetch(endpoint: string, tags: string[] = ['products'], revalidate = 3600) { const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4001/api'; const apiBase = apiUrl.endsWith('/api') ? apiUrl : `${apiUrl.replace(/\/$/, '')}/api`; const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; const res = await fetch(`${apiBase}${cleanEndpoint}`, { next: { tags, revalidate }, }); if (!res.ok) throw new Error(`Fetch failed: ${res.status}`); return res.json(); } public async getProducts(filters?: { category?: string; petType?: PetType | "all"; query?: string; symptom?: string; page?: number; limit?: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; }): Promise<{ data: Product[]; meta: { total: number; page: number; lastPage: number; limit: number } }> { const params = new URLSearchParams(); if (filters?.category && filters.category !== "all") params.append('category', filters.category); if (filters?.petType && filters.petType !== "all") params.append('petType', filters.petType); if (filters?.query) params.append('search', filters.query); if (filters?.symptom) params.append('symptom', filters.symptom); if (filters?.sortBy) params.append('sortBy', filters.sortBy); if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder); params.append('page', String(filters?.page || 1)); params.append('limit', String(filters?.limit || 12)); try { if (typeof window === 'undefined') { const json = await this.serverFetch(`/products?${params.toString()}`, ['products'], 3600); return { data: json.data.map((item: Record) => this.mapBackendToFrontend(item)), meta: json.meta, }; } const response = await api.get(`/products?${params.toString()}`); return { data: response.data.data.map((item: Record) => this.mapBackendToFrontend(item)), meta: response.data.meta, }; } catch (error) { console.error("[ProductService] Failed to fetch products:", error); return { data: [], meta: { total: 0, page: 1, lastPage: 0, limit: 12 } }; } } public async getProductById(id: string): Promise { try { if (typeof window === 'undefined') { const data = await this.serverFetch(`/products/${id}`, ['products'], 3600); return this.mapBackendToFrontend(data); } const response = await api.get(`/products/${id}`); return this.mapBackendToFrontend(response.data); } catch (error) { console.error(`[ProductService] Failed to fetch product ${id}:`, error); return null; } } public async getProductBySlug(slug: string): Promise { try { if (typeof window === 'undefined') { const data = await this.serverFetch(`/products/${encodeURIComponent(slug)}`, ['products', `product-${slug}`], 3600); return this.mapBackendToFrontend(data); } const response = await api.get(`/products/${slug}`); return this.mapBackendToFrontend(response.data); } catch (error) { console.error(`[ProductService] Failed to fetch product ${slug}:`, error); return null; } } public async getFeaturedProducts(): Promise { try { const response = await api.get('/products?limit=30'); const all = response.data.data.map((item: Record) => this.mapBackendToFrontend(item)); const featured: Product[] = []; const categoriesSeen = new Set(); for (const p of all) { if (p.category && !categoriesSeen.has(p.category)) { featured.push(p); categoriesSeen.add(p.category); } if (featured.length === 4) break; } if (featured.length < 4) { for (const p of all) { if (!featured.some(f => f.id === p.id)) { featured.push(p); } if (featured.length === 4) break; } } return featured; } catch (error) { console.error("[ProductService] Failed to fetch featured products:", error); return []; } } public async getActiveFilters(): Promise<{ categories: { id: string; name: string; slug: string }[]; symptoms: string[]; petTypes: string[]; }> { try { const response = await api.get('/products/filters'); return response.data; } catch (error) { console.error("[ProductService] Failed to fetch active filters:", error); return { categories: [], symptoms: [], petTypes: [] }; } } public async getNavigationFilters(): Promise<{ id: string; name: string; slug: string; symptoms: string[]; }[]> { try { const response = await api.get('/products/navigation-filters'); return response.data; } catch (error) { console.error("[ProductService] Failed to fetch navigation filters:", error); return []; } } public async getBanners(): Promise { try { const response = await api.get('/banners'); return response.data || []; } catch (error) { console.error("[ProductService] Failed to fetch banners:", error); return []; } } } export const productService = ProductService.getInstance();