canina/frontend/application/lib/services/productService.ts
parsa aghaei 8d1b786a24
All checks were successful
Deploy Canina / deploy (push) Successful in 1m50s
chore(brand): rename all Persian occurrences of کانینا to کنینا across the project
2026-08-16 16:40:08 +03:30

257 lines
9.2 KiB
TypeScript

import api from './api';
import { PRODUCTS, Product, PetType } from "../data/products";
const CATEGORY_SLUG_TO_NAME: Record<string, string> = {
'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;
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<string, unknown>): 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 = <T>(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: 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';
})(),
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<Product['keyBenefits']>(data.keyBenefits, local?.keyBenefits || []),
expectedResults: safeParse<Product['expectedResults']>(data.expectedResults, local?.expectedResults || []),
analysis: safeParse<Record<string, string>>(data.compositionTable, local?.analysis || {}),
specialist: safeParse<Product['specialist']>(data.specialist, defaultSpecialist),
calculateDosage: local?.calculateDosage,
};
}
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 {
const response = await api.get(`/products?${params.toString()}`);
return {
data: response.data.data.map((item: Record<string, unknown>) => 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<Product | null> {
try {
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<Product | null> {
try {
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<Product[]> {
try {
const response = await api.get('/products?limit=30');
const all = response.data.data.map((item: Record<string, unknown>) => this.mapBackendToFrontend(item));
const featured: Product[] = [];
const categoriesSeen = new Set<string>();
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 [];
}
}
}
export const productService = ProductService.getInstance();