diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts
index 9027df8..5c3fd07 100644
--- a/backend/src/admin/admin.service.ts
+++ b/backend/src/admin/admin.service.ts
@@ -508,6 +508,14 @@ export class AdminService {
: {}),
};
+ let resolvedCategorySlug = data.categorySlug;
+ if (data.categoryId && (!resolvedCategorySlug || resolvedCategorySlug === 'general')) {
+ const cat = await this.prisma.category.findUnique({ where: { id: data.categoryId } });
+ if (cat) {
+ resolvedCategorySlug = cat.slug;
+ }
+ }
+
const product = await this.prisma.product.create({
data: {
artNo: data.artNo || `ART-${Date.now()}`,
@@ -518,7 +526,7 @@ export class AdminService {
shortDescription: data.shortDescription || '',
ingredients: data.ingredients || null,
categoryId: data.categoryId || '',
- categorySlug: data.categorySlug || 'general',
+ categorySlug: resolvedCategorySlug || 'general',
buyPrice: data.buyPrice !== undefined ? data.buyPrice : 0,
marginRetailPercent:
marginRetail !== undefined ? marginRetail : undefined,
@@ -662,6 +670,14 @@ export class AdminService {
: {}),
};
+ let resolvedCategorySlug = data.categorySlug;
+ if (data.categoryId && !resolvedCategorySlug) {
+ const cat = await this.prisma.category.findUnique({ where: { id: data.categoryId } });
+ if (cat) {
+ resolvedCategorySlug = cat.slug;
+ }
+ }
+
const product = await this.prisma.product.update({
where: { id },
data: {
@@ -673,7 +689,7 @@ export class AdminService {
shortDescription: data.shortDescription,
ingredients: data.ingredients !== undefined ? data.ingredients : undefined,
categoryId: data.categoryId,
- categorySlug: data.categorySlug,
+ categorySlug: resolvedCategorySlug !== undefined ? resolvedCategorySlug : undefined,
buyPrice: data.buyPrice !== undefined ? data.buyPrice : undefined,
marginRetailPercent:
marginRetail !== undefined ? marginRetail : undefined,
diff --git a/backend/src/admin/categories.service.ts b/backend/src/admin/categories.service.ts
index 84a2fb3..a276d32 100644
--- a/backend/src/admin/categories.service.ts
+++ b/backend/src/admin/categories.service.ts
@@ -1,4 +1,4 @@
-import { Injectable, NotFoundException } from '@nestjs/common';
+import { Injectable, NotFoundException, OnModuleInit } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
@@ -9,9 +9,37 @@ export class CategoryQuery {
}
@Injectable()
-export class CategoriesService {
+export class CategoriesService implements OnModuleInit {
constructor(private prisma: PrismaService) {}
+ async onModuleInit() {
+ await this.seedDefaultCategories();
+ }
+
+ private async seedDefaultCategories() {
+ try {
+ const defaultCategories = [
+ { name: 'مفاصل و استخوان', slug: 'joints', description: 'مکملهای ارتوپدی و مفاصل حاوی عصاره صدف سبز و GAG' },
+ { name: 'سیستم ایمنی و گوارش', slug: 'immune', description: 'تقویت سیستم دفاعی بدن و پروبیوتیکهای تخصصی' },
+ { name: 'ویتامینها و انرژیبخشها', slug: 'energy', description: 'مولتیویتامینها و تونیکهای انرژیبخش' },
+ { name: 'مراقبتهای ویژه (پوست، دندان و چشم)', slug: 'special-care', description: 'محلولهای نقره، قطرههای شستشو و ژل دندان' },
+ { name: 'تقویت عمومی', slug: 'general', description: 'مکملهای پایهای و روزانه سگ و گربه' },
+ { name: 'تغذیه تخصصی', slug: 'nutrition', description: 'شیر خشک، پودر خون گاو و مکملهای تخصصی پرورش' },
+ { name: 'مکملهای غذایی و درمانی', slug: 'supplements', description: 'طیف کامل مکملهای گرید دارویی Canina' },
+ ];
+
+ for (const cat of defaultCategories) {
+ await this.prisma.category.upsert({
+ where: { slug: cat.slug },
+ update: { name: cat.name, description: cat.description },
+ create: { name: cat.name, slug: cat.slug, description: cat.description },
+ });
+ }
+ } catch (err) {
+ console.warn('[CategoriesService] Failed to seed default categories:', err);
+ }
+ }
+
async getCategories(query: CategoryQuery = {}) {
const page = Number(query.page) || 1;
const limit = Number(query.limit) || 10;
diff --git a/backend/src/products/dto/get-products.dto.ts b/backend/src/products/dto/get-products.dto.ts
index 66df75d..9f72321 100644
--- a/backend/src/products/dto/get-products.dto.ts
+++ b/backend/src/products/dto/get-products.dto.ts
@@ -15,10 +15,10 @@ export class GetProductsDto extends PaginationDto {
@ApiPropertyOptional({
description: 'فیلتر بر اساس نوع حیوان',
- enum: ['سگ', 'گربه', 'all'],
+ enum: ['سگ', 'گربه', 'سگ و گربه', 'هر دو', 'all'],
})
@IsOptional()
- @IsEnum(['سگ', 'گربه', 'all'])
+ @IsEnum(['سگ', 'گربه', 'سگ و گربه', 'هر دو', 'all'])
petType?: string;
@ApiPropertyOptional({ description: 'فیلتر بر اساس یک علامت درمانی خاص' })
diff --git a/backend/src/products/products.service.ts b/backend/src/products/products.service.ts
index 6ce64d5..c040c0b 100644
--- a/backend/src/products/products.service.ts
+++ b/backend/src/products/products.service.ts
@@ -21,7 +21,7 @@ export class ProductsService {
minPrice,
maxPrice,
page = 1,
- limit = 10,
+ limit = 50,
sortBy = 'createdAt',
sortOrder = 'desc',
} = filters;
@@ -39,7 +39,9 @@ export class ProductsService {
}
if (petType && petType !== 'all') {
- andConditions.push({ suitableFor: { in: [petType, 'هر دو'] } });
+ andConditions.push({
+ suitableFor: { in: [petType, 'سگ و گربه', 'هر دو'] },
+ });
}
if (symptom) {
@@ -230,7 +232,13 @@ export class ProductsService {
return {
categories,
symptoms: symptomsDb.map((s) => s.symptom).filter(Boolean),
- petTypes: petTypesDb.map((p) => p.suitableFor).filter(Boolean),
+ petTypes: Array.from(
+ new Set(
+ petTypesDb
+ .map((p) => (p.suitableFor === 'هر دو' ? 'سگ و گربه' : p.suitableFor))
+ .filter(Boolean),
+ ),
+ ),
};
}
diff --git a/frontend/admin-panel/src/pages/Products.tsx b/frontend/admin-panel/src/pages/Products.tsx
index 72a68f9..abbb7f9 100644
--- a/frontend/admin-panel/src/pages/Products.tsx
+++ b/frontend/admin-panel/src/pages/Products.tsx
@@ -652,11 +652,16 @@ export default function Products() {
// Fetch categories separately
try {
- const catRes = await api.get('/admin/categories');
- if (catRes.data?.data) {
+ const catRes = await api.get('/admin/categories/all');
+ if (catRes.data?.data && Array.isArray(catRes.data.data)) {
setCategories(catRes.data.data);
} else if (Array.isArray(catRes.data)) {
setCategories(catRes.data);
+ } else {
+ const fallbackRes = await api.get('/admin/categories', { params: { limit: 100 } });
+ if (fallbackRes.data?.data && Array.isArray(fallbackRes.data.data)) {
+ setCategories(fallbackRes.data.data);
+ }
}
} catch (err) {
console.warn('Could not fetch categories', err);
diff --git a/frontend/application/app/shop/[slug]/page.tsx b/frontend/application/app/shop/[slug]/page.tsx
index 02f1245..62931ff 100644
--- a/frontend/application/app/shop/[slug]/page.tsx
+++ b/frontend/application/app/shop/[slug]/page.tsx
@@ -21,10 +21,20 @@ export async function generateMetadata(
const nameFa = product.nameFa || product.name;
const nameEn = product.nameEn ? ` (${product.nameEn})` : '';
- const rawTitle = product.metaTitle || `${nameFa}${nameEn}`;
- const title = formatPageTitle(rawTitle, config.brandNameFa, config.titleSeparator, config.titlePosition, false);
- const ogTitle = `${title} ${config.titleSeparator} ${config.brandNameFa}`;
- const description = product.metaDescription || (product.shortDescription || product.description || '').substring(0, 155).trim() || config.defaultMetaDescription;
+
+ // B1: If custom metaTitle is specified for the product, use it directly (absolute to bypass layout template)
+ const metaTitleTrimmed = product.metaTitle?.trim();
+ const rawTitle = metaTitleTrimmed || `${nameFa}${nameEn}`;
+ const title = metaTitleTrimmed
+ ? { absolute: metaTitleTrimmed }
+ : formatPageTitle(rawTitle, config.brandNameFa, config.titleSeparator, config.titlePosition, false);
+
+ const ogTitle = metaTitleTrimmed || `${typeof title === 'string' ? title : rawTitle} ${config.titleSeparator} ${config.brandNameFa}`;
+
+ // B2: Connect description directly to metaDescription written for all 35 products
+ const description = product.metaDescription?.trim() ||
+ (product.shortDescription || product.description || '').substring(0, 155).trim() ||
+ config.defaultMetaDescription;
const rawKeywords = product.keywords;
const keywords = typeof rawKeywords === 'string'
@@ -90,7 +100,7 @@ export async function generateMetadata(
},
other: {
product_id: String(product.artNo || product.id || resolvedParams.slug),
- product_name: title,
+ product_name: rawTitle,
product_price: String(product.priceValue || 0),
product_old_price: String(product.priceValue || 0),
availability: (product.stockStatus === 'DISCONTINUED' || String(product.stockStatus || '').toLowerCase().includes('out')) ? 'outofstock' : 'instock',
diff --git a/frontend/application/components/ProductPage.tsx b/frontend/application/components/ProductPage.tsx
index 92fd224..b77f729 100644
--- a/frontend/application/components/ProductPage.tsx
+++ b/frontend/application/components/ProductPage.tsx
@@ -526,7 +526,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
>
- {isMounted && pets.length > 0 ? (
- product.optimisticTemplate?.replace("[PetName]", activePet?.name || "").replace("[OnSet]", product.onSetOfAction || "به زودی")
- ) : (
- product.scientificTagline
+ {product.scientificTagline && (
+
+ {product.optimisticTemplate?.replace("[PetName]", activePet?.name || "").replace("[OnSet]", product.onSetOfAction || "به زودی")} +
+ )} + + {/* A9: Veterinary Prescription Notice (requiresRx) */} + {product.requiresRx && ( ++ این فرآورده دارویی بالینی با دوز درمانی ویژه میباشد. جهت تضمین سلامت پت، مصرف آن تحت نظر دامپزشک توصیه شده و ارسال سفارش پس از هماهنگی واحد پشتیبانی تخصصی انجام خواهد گرفت. +
+