feat(admin): add comprehensive product attributes management and dynamic analysis table
Some checks failed
Deploy Canina / deploy (push) Successful in 2m18s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 6s

This commit is contained in:
parsa aghaei 2026-09-02 12:09:57 +03:30
parent 68840fbdf2
commit ceaab10fdd
14 changed files with 2362 additions and 1873 deletions

View File

@ -460,6 +460,22 @@ export class AdminService {
...(data.keyHighlights !== undefined
? { keyHighlights: data.keyHighlights }
: {}),
...(data.keyBenefits !== undefined
? { keyBenefits: data.keyBenefits }
: {}),
...(data.analysis !== undefined ? { analysis: data.analysis } : {}),
...(data.onSetOfAction !== undefined
? { onSetOfAction: data.onSetOfAction }
: {}),
...(data.specialBadge !== undefined
? { specialBadge: data.specialBadge }
: {}),
...(data.contraindications !== undefined
? { contraindications: data.contraindications }
: {}),
...(data.expectedResults !== undefined
? { expectedResults: data.expectedResults }
: {}),
...(data.isPreorder !== undefined ? { isPreorder: data.isPreorder } : {}),
...(data.preorderDeposit !== undefined
? { preorderDeposit: data.preorderDeposit }
@ -584,6 +600,22 @@ export class AdminService {
...(data.keyHighlights !== undefined
? { keyHighlights: data.keyHighlights }
: {}),
...(data.keyBenefits !== undefined
? { keyBenefits: data.keyBenefits }
: {}),
...(data.analysis !== undefined ? { analysis: data.analysis } : {}),
...(data.onSetOfAction !== undefined
? { onSetOfAction: data.onSetOfAction }
: {}),
...(data.specialBadge !== undefined
? { specialBadge: data.specialBadge }
: {}),
...(data.contraindications !== undefined
? { contraindications: data.contraindications }
: {}),
...(data.expectedResults !== undefined
? { expectedResults: data.expectedResults }
: {}),
...(data.isPreorder !== undefined ? { isPreorder: data.isPreorder } : {}),
...(data.preorderDeposit !== undefined
? { preorderDeposit: data.preorderDeposit }

View File

@ -304,6 +304,34 @@ export class ProductDto {
@IsArray()
keyHighlights?: string[];
@ApiPropertyOptional({ description: 'کارت‌های شاخص «چرا این محصول؟» (۳ کارت مزایا)' })
@IsOptional()
@IsArray()
keyBenefits?: Array<{ icon: string; title: string; description: string }>;
@ApiPropertyOptional({ description: 'جدول آنالیز و درصد ترکیبات (Analytical Constituents)' })
@IsOptional()
analysis?: Record<string, string>;
@ApiPropertyOptional({ description: 'زمان شروع اثربخشی (Onset of Action)' })
@IsOptional()
@IsString()
onSetOfAction?: string;
@ApiPropertyOptional({ description: 'برچسب ویژه محصول (Special Badge)' })
@IsOptional()
@IsString()
specialBadge?: string;
@ApiPropertyOptional({ description: 'موارد منع مصرف (Contraindications)' })
@IsOptional()
contraindications?: string[] | string;
@ApiPropertyOptional({ description: 'نتایج بالینی مورد انتظار (Expected Clinical Results)' })
@IsOptional()
@IsArray()
expectedResults?: Array<{ icon: string; text: string }>;
@ApiPropertyOptional({ description: 'پیکربندی جامع دوز و داده‌های تکمیلی' })
@IsOptional()
dosageConfig?: Record<string, any>;

View File

@ -33,6 +33,14 @@ import {
Thermometer,
AlertTriangle,
Sparkles,
Star,
FlaskConical,
Shield,
Heart,
Zap,
Clock,
Tag,
Activity,
} from 'lucide-react';
import { toast } from 'react-hot-toast';
import api, { BASE_DOMAIN } from '../services/api';
@ -279,6 +287,11 @@ export default function Products() {
precautions: '',
lifeStage: '',
keyHighlights: ['', '', '', ''] as string[],
keyBenefits: [] as Array<{ icon: string; title: string; description: string }>,
analysis: [] as Array<{ key: string; value: string }>,
onSetOfAction: '',
specialBadge: '',
contraindications: '',
symptoms: [] as string[],
isPreorder: false,
preorderDeposit: '' as number | string,
@ -422,6 +435,38 @@ export default function Products() {
initialHighlights[3] || '',
];
// Parse keyBenefits (Why this product / 3 Cards)
let initialBenefits: Array<{ icon: string; title: string; description: string }> = [];
const rawBenefits = dosageConfig.keyBenefits || (product as unknown as { keyBenefits?: any }).keyBenefits;
if (Array.isArray(rawBenefits)) {
initialBenefits = rawBenefits.map((b: any) => ({
icon: b.icon || 'Star',
title: b.title || '',
description: b.description || '',
}));
}
// Parse analytical constituents table (Key-Value)
let initialAnalysis: Array<{ key: string; value: string }> = [];
const rawAnalysis = dosageConfig.analysis || (product as unknown as { analysis?: any }).analysis || (product as unknown as { compositionTable?: any }).compositionTable;
if (rawAnalysis && typeof rawAnalysis === 'object' && !Array.isArray(rawAnalysis)) {
initialAnalysis = Object.entries(rawAnalysis).map(([k, v]) => ({
key: k,
value: String(v || '')
}));
} else if (Array.isArray(rawAnalysis)) {
initialAnalysis = rawAnalysis.map((item: any) => ({
key: item.key || item.title || '',
value: String(item.value || item.percent || '')
}));
}
const initialContraindications = dosageConfig.contraindications
? (Array.isArray(dosageConfig.contraindications) ? dosageConfig.contraindications.join('\n') : String(dosageConfig.contraindications))
: ((product as unknown as { contraindications?: any }).contraindications
? (Array.isArray((product as unknown as { contraindications?: any }).contraindications) ? (product as unknown as { contraindications?: any }).contraindications.join('\n') : String((product as unknown as { contraindications?: any }).contraindications))
: '');
setFormData({
artNo: product.artNo || '',
nameFa: product.nameFa || '',
@ -442,11 +487,16 @@ export default function Products() {
feedingAdvice: dosageConfig.feedingAdvice || product.feedingAdvice || product.dosageLogic || '',
showFaqs: dosageConfig.showFaqs !== undefined ? Boolean(dosageConfig.showFaqs) : (product.showFaqs !== undefined ? Boolean(product.showFaqs) : true),
faqs: Array.isArray(initialFaqs) ? initialFaqs : [],
paoMonths: dosageConfig.paoMonths !== undefined ? dosageConfig.paoMonths : (product.paoMonths !== undefined ? product.paoMonths : '12'),
paoMonths: dosageConfig.paoMonths !== undefined ? dosageConfig.paoMonths : (product.paoMonths !== undefined ? product.paoMonths : ''),
storageInfo: dosageConfig.storageInfo || product.storageInfo || '',
precautions: dosageConfig.precautions || product.precautions || '',
lifeStage: dosageConfig.lifeStage || product.lifeStage || 'مناسب تمام سنین و دوران بارداری و شیردهی',
lifeStage: dosageConfig.lifeStage || product.lifeStage || '',
keyHighlights: paddedHighlights,
keyBenefits: initialBenefits,
analysis: initialAnalysis,
onSetOfAction: dosageConfig.onSetOfAction || (product as unknown as { onSetOfAction?: string }).onSetOfAction || '',
specialBadge: dosageConfig.specialBadge || (product as unknown as { specialBadge?: string }).specialBadge || '',
contraindications: initialContraindications,
suitableFor: product.suitableFor || 'سگ و گربه',
imageUrl: product.imageUrl || '',
slug: product.slug || '',
@ -497,6 +547,11 @@ export default function Products() {
precautions: '',
lifeStage: '',
keyHighlights: ['', '', '', ''],
keyBenefits: [],
analysis: [],
onSetOfAction: '',
specialBadge: '',
contraindications: '',
suitableFor: 'سگ و گربه',
imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: '',
stockStatus: 'IN_STOCK', noIndex: false, noFollow: false, ogImage: '', featuredImageAlt: '',
@ -704,6 +759,22 @@ export default function Products() {
try {
const derivedSlug = (formData.slug || '').trim() || slugify(formData.nameEn || '');
// Prepare analysis object
const analysisObj: Record<string, string> = {};
if (formData.analysis && Array.isArray(formData.analysis)) {
formData.analysis.forEach(item => {
if (item && item.key?.trim() && item.value?.trim()) {
analysisObj[item.key.trim()] = item.value.trim();
}
});
}
// Prepare keyBenefits array
const validBenefits = formData.keyBenefits
? formData.keyBenefits.filter(b => b && (b.title?.trim() || b.description?.trim()))
: [];
const finalPayload: Record<string, unknown> = {
artNo: (formData.artNo || '').trim(),
nameFa: (formData.nameFa || '').trim(),
@ -754,6 +825,11 @@ export default function Products() {
precautions: (formData.precautions || '').trim() || undefined,
lifeStage: (formData.lifeStage || '').trim() || undefined,
keyHighlights: formData.keyHighlights ? formData.keyHighlights.filter(h => h && h.trim().length > 0) : [],
keyBenefits: validBenefits,
analysis: Object.keys(analysisObj).length > 0 ? analysisObj : undefined,
onSetOfAction: (formData.onSetOfAction || '').trim() || undefined,
specialBadge: (formData.specialBadge || '').trim() || undefined,
contraindications: (formData.contraindications || '').trim() || undefined,
slug: derivedSlug,
symptoms: formData.symptoms || [],
isPreorder: Boolean(formData.isPreorder),
@ -1175,6 +1251,7 @@ export default function Products() {
{[
{ id: 'general', label: 'اطلاعات پایه' },
{ id: 'technical', label: 'مشخصات علمی و نگهداری' },
{ id: 'benefits', label: 'مزایا و آنالیز ترکیبات' },
{ id: 'faq', label: 'پرسش‌های متداول (FAQ)' },
{ id: 'pricing', label: 'موجودی و قیمت' },
{ id: 'seo', label: 'سئو (SEO)' },
@ -1587,6 +1664,339 @@ export default function Products() {
))}
</div>
</div>
{/* Clinical Presentation & Contraindications Card */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-4">
<h4 className="text-sm font-black text-gray-800 flex items-center gap-2 border-b pb-2">
<Shield className="w-4 h-4 text-canina-blue" />
<span>برچسب ویژه، سرعت اثر و موارد منع مصرف</span>
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700 flex items-center justify-between">
<span>برچسب ویژه محصول (Special Badge)</span>
<span className="text-[10px] text-gray-400">بج نمایشی روی کارت کالا</span>
</label>
<input
type="text"
value={formData.specialBadge}
onChange={(e) => setFormData({ ...formData, specialBadge: e.target.value })}
placeholder="مثال: فرمولاسیون دارویی، پیشنهاد ویژه دامپزشک یا پرفروش‌ترین"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-bold font-vazir"
/>
<div className="flex items-center gap-1.5 flex-wrap pt-1">
{['فرمولاسیون دارویی', 'پیشنهاد کلینیکی', 'پرفروش‌ترین کاتالوگ', 'تاییدیه GMP آلمان'].map((badge) => (
<button
key={badge}
type="button"
onClick={() => setFormData({ ...formData, specialBadge: badge })}
className="px-2 py-0.5 text-[10px] font-bold rounded-md bg-gray-100 text-gray-600 hover:bg-purple-50 hover:text-purple-700 transition-colors"
>
{badge}
</button>
))}
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700 flex items-center justify-between">
<span>زمان شروع اثربخشی (Onset of Action)</span>
<span className="text-[10px] text-gray-400">دوره مشاهده نتایج بالینی</span>
</label>
<input
type="text"
value={formData.onSetOfAction}
onChange={(e) => setFormData({ ...formData, onSetOfAction: e.target.value })}
placeholder="مثال: ۲ الی ۳ هفته پس از شروع مصرف مداوم"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-bold font-vazir"
/>
<div className="flex items-center gap-1.5 flex-wrap pt-1">
{['۲ الی ۴ هفته پس از شروع مصرف', 'تأثیر سریع در هفته اول', 'دوره کامل ۶۰ الی ۹۰ روزه'].map((onset) => (
<button
key={onset}
type="button"
onClick={() => setFormData({ ...formData, onSetOfAction: onset })}
className="px-2 py-0.5 text-[10px] font-bold rounded-md bg-gray-100 text-gray-600 hover:bg-purple-50 hover:text-purple-700 transition-colors"
>
{onset}
</button>
))}
</div>
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-xs font-bold text-gray-700 block">
موارد منع مصرف و هشدارهای خاص (Contraindications)
</label>
<textarea
rows={2}
value={formData.contraindications}
onChange={(e) => setFormData({ ...formData, contraindications: e.target.value })}
placeholder="در صورت وجود حساسیت به پروتئین‌های خاص یا نارسایی حاد با دامپزشک مشورت شود..."
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs leading-relaxed font-vazir"
/>
</div>
</div>
</div>
</div>
{/* Benefits & Analysis Tab */}
<div className={activeTab === 'benefits' ? 'block space-y-5' : 'hidden'}>
{/* Why this product? Key Benefits Cards */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b pb-3">
<div>
<h4 className="text-sm font-black text-gray-800 flex items-center gap-2">
<Star className="w-4 h-4 text-amber-500" />
<span>کارتهای برجسته «چرا این محصول؟» (Why This Product / Key Benefits)</span>
</h4>
<p className="text-xs text-gray-500 mt-0.5">
کارتهای سهگانه اصلی با آیکون، عنوان شاخص و توضیح کوتاه که در بالای صفحه محصول نمایش داده میشوند.
</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
<Button
type="button"
variant="outline"
size="sm"
startIcon={Plus}
onClick={() => {
setFormData({
...formData,
keyBenefits: [...(formData.keyBenefits || []), { icon: 'Star', title: '', description: '' }]
});
}}
className="border-amber-200 text-amber-700 hover:bg-amber-50 text-xs font-bold"
>
افزودن کارت مزیت
</Button>
<button
type="button"
onClick={() => {
setFormData({
...formData,
keyBenefits: [
{
icon: 'Shield',
title: 'فرمولاسیون تخصصی آلمان',
description: 'تولید شده طبق سخت‌گیرانه‌ترین استانداردهای دارویی اروپا جهت تضمین خلوص و اثربخشی مواد مؤثره.'
},
{
icon: 'Sparkles',
title: 'اثربخشی سریع و هدفمند',
description: 'جذب بیولوژیک بالا در بافت‌های هدف و بهبود چشمگیر علائم فیزیولوژیک حیوان.'
},
{
icon: 'Heart',
title: 'ایمن و بدون عارضه جانبی',
description: 'فاقد افزودنی‌های شیمیایی مضر، نگهدارنده‌های صنعتی و بدون ایجاد بار کلیوی و کبدی.'
}
]
});
toast.success('۳ کارت مزیت پیشنهادی کاتالوگ بارگذاری شدند');
}}
className="text-xs text-amber-600 hover:text-amber-800 font-bold flex items-center gap-1 hover:underline cursor-pointer"
>
درج ۳ مزیت پیشنهادی کاتالوگ
</button>
</div>
</div>
{formData.keyBenefits && formData.keyBenefits.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2">
{formData.keyBenefits.map((benefit, bIdx) => (
<div key={bIdx} className="bg-gray-50/70 p-4 rounded-2xl border border-gray-200 space-y-3 relative group">
<div className="flex items-center justify-between">
<span className="text-xs font-black text-amber-700 bg-amber-50 px-2 py-0.5 rounded-md">
کارت {bIdx + 1}
</span>
<button
type="button"
onClick={() => {
const newBenefits = formData.keyBenefits.filter((_, i) => i !== bIdx);
setFormData({ ...formData, keyBenefits: newBenefits });
}}
className="text-gray-400 hover:text-red-600 transition-colors p-1 cursor-pointer"
title="حذف این کارت"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
<div className="space-y-1">
<label className="text-[11px] font-bold text-gray-600">انتخاب آیکون</label>
<select
value={benefit.icon}
onChange={(e) => {
const newBenefits = [...formData.keyBenefits];
newBenefits[bIdx].icon = e.target.value;
setFormData({ ...formData, keyBenefits: newBenefits });
}}
className="w-full px-3 py-1.5 rounded-xl border border-gray-200 bg-white text-xs font-bold font-vazir outline-none focus:border-amber-500"
>
<option value="Star"> ستاره (Star)</option>
<option value="Shield">🛡 سپر ایمنی (Shield)</option>
<option value="Sparkles"> درخشش (Sparkles)</option>
<option value="Heart"> قلب و سلامت (Heart)</option>
<option value="Zap"> سرعت اثر (Zap)</option>
<option value="CheckCircle2"> تیک سبز (Check)</option>
<option value="Activity">📈 بهبود عملکرد (Activity)</option>
</select>
</div>
<div className="space-y-1">
<label className="text-[11px] font-bold text-gray-600">عنوان شاخص (Title)</label>
<input
type="text"
value={benefit.title}
onChange={(e) => {
const newBenefits = [...formData.keyBenefits];
newBenefits[bIdx].title = e.target.value;
setFormData({ ...formData, keyBenefits: newBenefits });
}}
placeholder="مثال: نقره میکروسیلور یا صدف لب‌سبز"
className="w-full px-3 py-1.5 rounded-xl border border-gray-200 bg-white text-xs font-bold font-vazir outline-none focus:border-amber-500"
/>
</div>
<div className="space-y-1">
<label className="text-[11px] font-bold text-gray-600">توضیح کوتاه (Description)</label>
<textarea
rows={2}
value={benefit.description}
onChange={(e) => {
const newBenefits = [...formData.keyBenefits];
newBenefits[bIdx].description = e.target.value;
setFormData({ ...formData, keyBenefits: newBenefits });
}}
placeholder="مثال: اثر آنتی‌باکتریال ۲۴ ساعته و جلوگیری از پلاک دندانی"
className="w-full px-3 py-1.5 rounded-xl border border-gray-200 bg-white text-xs font-medium font-vazir outline-none focus:border-amber-500 leading-relaxed"
/>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-6 border-2 border-dashed border-gray-200 rounded-2xl bg-gray-50/50 space-y-2">
<Star className="w-8 h-8 text-gray-300 mx-auto" />
<p className="text-xs text-gray-500 font-bold">هنوز هیچ کارت مزیتی برای این محصول اضافه نشده است.</p>
<p className="text-[11px] text-gray-400">میتوانید به صورت دستی کارت اضافه کنید یا از دکمه درج ۳ مزیت پیشنهادی استفاده نمایید.</p>
</div>
)}
</div>
{/* Analytical Constituents Table */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b pb-3">
<div>
<h4 className="text-sm font-black text-gray-800 flex items-center gap-2">
<FlaskConical className="w-4 h-4 text-canina-blue" />
<span>جدول آنالیز و درصد ترکیبات (Analytical Constituents Table)</span>
</h4>
<p className="text-xs text-gray-500 mt-0.5">
جدول دقیق درصدهای آنالیز آزمایشگاهی (پروتئین، چربی، فیبر، کلسیم، مواد مؤثره) در کاتالوگ دارویی آلمان.
</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
<Button
type="button"
variant="outline"
size="sm"
startIcon={Plus}
onClick={() => {
setFormData({
...formData,
analysis: [...(formData.analysis || []), { key: '', value: '' }]
});
}}
className="border-blue-200 text-blue-700 hover:bg-blue-50 text-xs font-bold"
>
افزودن سطر آنالیز
</Button>
<button
type="button"
onClick={() => {
setFormData({
...formData,
analysis: [
{ key: 'پروتئین خام (Crude Protein)', value: '۳۰.۰ ٪' },
{ key: 'چربی خام (Crude Fat)', value: '۱۳.۰ ٪' },
{ key: 'فیبر خام (Crude Fiber)', value: '۴.۰ ٪' },
{ key: 'خاکستر خام (Crude Ash)', value: '۸.۵ ٪' },
{ key: 'کلسیم (Calcium)', value: '۲.۲ ٪' },
{ key: 'فسفر (Phosphorus)', value: '۱.۳ ٪' },
{ key: 'رطوبت (Moisture)', value: '۹.۰ ٪' }
]
});
toast.success('سطرهای استاندارد آنالیز ترکیبات بارگذاری شدند');
}}
className="text-xs text-canina-blue hover:text-blue-800 font-bold flex items-center gap-1 hover:underline cursor-pointer"
>
درج جدول استاندارد Canina
</button>
</div>
</div>
{formData.analysis && formData.analysis.length > 0 ? (
<div className="space-y-2">
<div className="grid grid-cols-12 gap-2 text-[11px] font-black text-gray-500 px-2 pb-1 border-b border-gray-100">
<div className="col-span-6 sm:col-span-5">عنوان پارامتر / ترکیب (Key)</div>
<div className="col-span-5 sm:col-span-6">مقدار / درصد (Value)</div>
<div className="col-span-1 text-center">حذف</div>
</div>
{formData.analysis.map((row, rIdx) => (
<div key={rIdx} className="grid grid-cols-12 gap-2 items-center">
<div className="col-span-6 sm:col-span-5">
<input
type="text"
value={row.key}
onChange={(e) => {
const newAnalysis = [...formData.analysis];
newAnalysis[rIdx].key = e.target.value;
setFormData({ ...formData, analysis: newAnalysis });
}}
placeholder="مثال: پروتئین خام یا نقره"
className="w-full px-3 py-2 rounded-xl border border-gray-200 text-xs font-bold font-vazir outline-none focus:border-blue-500"
/>
</div>
<div className="col-span-5 sm:col-span-6">
<input
type="text"
value={row.value}
onChange={(e) => {
const newAnalysis = [...formData.analysis];
newAnalysis[rIdx].value = e.target.value;
setFormData({ ...formData, analysis: newAnalysis });
}}
placeholder="مثال: ۲۸٪ یا بسیار بالا"
className="w-full px-3 py-2 rounded-xl border border-gray-200 text-xs font-bold font-vazir outline-none focus:border-blue-500"
/>
</div>
<div className="col-span-1 flex justify-center">
<button
type="button"
onClick={() => {
const newAnalysis = formData.analysis.filter((_, i) => i !== rIdx);
setFormData({ ...formData, analysis: newAnalysis });
}}
className="w-8 h-8 rounded-lg text-gray-400 hover:text-red-600 hover:bg-red-50 flex items-center justify-center transition-colors cursor-pointer"
title="حذف این سطر"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-6 border-2 border-dashed border-gray-200 rounded-2xl bg-gray-50/50 space-y-2">
<FlaskConical className="w-8 h-8 text-gray-300 mx-auto" />
<p className="text-xs text-gray-500 font-bold">هنوز هیچ سطری در جدول آنالیز ترکیبات ثبت نشده است.</p>
<p className="text-[11px] text-gray-400">میتوانید به صورت اختیاری پارامترهای آزمایشگاهی کالا را در این بخش وارد کنید.</p>
</div>
)}
</div>
</div>
{/* FAQ Tab */}

View File

@ -133,8 +133,6 @@ export class ProductService {
suitableFor: (data.suitableFor as PetType) || 'سگ',
requiresRx: Boolean(data.requiresRx),
storage: (data.dosageConfig as Record<string, any>)?.storageInfo || data.storage,
specialBadge: data.specialBadge,
onSetOfAction: data.onSetOfAction,
optimisticTemplate: data.optimisticTemplate,
feedingAdvice: (data.dosageConfig as Record<string, any>)?.feedingAdvice || data.dosageLogic || data.feedingAdvice || '',
showFaqs: (data.dosageConfig as Record<string, any>)?.showFaqs !== undefined ? Boolean((data.dosageConfig as Record<string, any>).showFaqs) : true,
@ -159,9 +157,20 @@ export class ProductService {
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 || {}),
keyBenefits: (data.dosageConfig as Record<string, any>)?.keyBenefits && Array.isArray((data.dosageConfig as Record<string, any>).keyBenefits) && (data.dosageConfig as Record<string, any>).keyBenefits.length > 0
? (data.dosageConfig as Record<string, any>).keyBenefits
: safeParse<Product['keyBenefits']>(data.keyBenefits, local?.keyBenefits || []),
expectedResults: (data.dosageConfig as Record<string, any>)?.expectedResults && Array.isArray((data.dosageConfig as Record<string, any>).expectedResults) && (data.dosageConfig as Record<string, any>).expectedResults.length > 0
? (data.dosageConfig as Record<string, any>).expectedResults
: safeParse<Product['expectedResults']>(data.expectedResults, local?.expectedResults || []),
analysis: (data.dosageConfig as Record<string, any>)?.analysis && typeof (data.dosageConfig as Record<string, any>).analysis === 'object' && Object.keys((data.dosageConfig as Record<string, any>).analysis).length > 0
? (data.dosageConfig as Record<string, any>).analysis
: safeParse<Record<string, string>>(data.compositionTable, local?.analysis || {}),
onSetOfAction: (data.dosageConfig as Record<string, any>)?.onSetOfAction || data.onSetOfAction || local?.onSetOfAction || undefined,
specialBadge: (data.dosageConfig as Record<string, any>)?.specialBadge || data.specialBadge || local?.specialBadge || undefined,
contraindications: (data.dosageConfig as Record<string, any>)?.contraindications
? (Array.isArray((data.dosageConfig as Record<string, any>).contraindications) ? (data.dosageConfig as Record<string, any>).contraindications : [(data.dosageConfig as Record<string, any>).contraindications])
: (local?.contraindications || undefined),
specialist: safeParse<Product['specialist']>(data.specialist, defaultSpecialist),
calculateDosage: local?.calculateDosage,

View File

@ -121,13 +121,13 @@
"119": "compilerOptions",
"120": "compilerOptions",
"121": "backend/README.md",
"122": "AdminController",
"122": "AdminService",
"123": "UsersService",
"124": "Repository Map",
"125": "validate_integrity.js",
"126": "admin-panel/package.json",
"127": "Sahel-Font",
"128": "AdminService",
"128": "ProductDto",
"129": "Reports.tsx",
"130": "Sahel-Font",
"131": "Role & Core Objective",
@ -197,7 +197,7 @@
"195": "React + TypeScript + Vite",
"196": "Select.tsx",
"197": "userStore.ts",
"198": "@tailwindcss/postcss",
"198": "Body",
"199": "SmsLogQueryDto",
"200": "application/README.md",
"201": "deploy.sh",
@ -300,7 +300,7 @@
"298": ".initiateOrderPayment",
"299": "@tailwindcss/postcss",
"300": "typescript",
"301": "bcrypt",
"301": "@nestjs/jwt",
"302": "typescript-eslint",
"303": "@nestjs/throttler",
"304": "passport",
@ -323,12 +323,13 @@
"321": "orders/page.tsx",
"322": "pets/page.tsx",
"323": "prettier",
"324": "ts-jest",
"324": "eslint",
"325": "@types/react-dom",
"326": "@types/js-yaml",
"327": "eslint-plugin-react-refresh",
"328": "@types/supertest",
"329": "typescript-eslint",
"330": "@nestjs/swagger",
"331": "tailwindcss"
"331": "tailwindcss",
"332": "eslint-config-next"
}

File diff suppressed because one or more lines are too long

View File

@ -300,7 +300,7 @@
"298": ".initiateOrderPayment",
"299": "@tailwindcss/postcss",
"300": "typescript",
"301": "@nestjs/jwt",
"301": "bcrypt",
"302": "typescript-eslint",
"303": "@nestjs/throttler",
"304": "passport",
@ -330,5 +330,5 @@
"328": "@types/supertest",
"329": "typescript-eslint",
"330": "@nestjs/swagger",
"331": "eslint-config-next"
"331": "tailwindcss"
}

View File

@ -1,7 +1,7 @@
# Graph Report - canina (2026-09-02)
## Corpus Check
- 599 files · ~1,082,383 words
- 599 files · ~1,082,386 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
@ -10,7 +10,7 @@
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `f0457b57`
- Built from commit: `9744aa81`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@ -299,7 +299,7 @@
- .initiateOrderPayment
- @tailwindcss/postcss
- typescript
- @nestjs/jwt
- bcrypt
- typescript-eslint
- @nestjs/throttler
- passport
@ -325,7 +325,7 @@
- @types/supertest
- typescript-eslint
- @nestjs/swagger
- eslint-config-next
- tailwindcss
## God Nodes (most connected - your core abstractions)
1. `Roles()` - 106 edges
@ -352,8 +352,8 @@
backend/src/orders/orders.controller.ts → frontend/admin-panel/src/types/admin.ts
## Import Cycles
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/services/authService.ts -> frontend/application/lib/services/api.ts`
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/api.ts`
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/services/authService.ts -> frontend/application/lib/services/api.ts`
- 4-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/orderService.ts -> frontend/application/lib/services/api.ts`
## Communities (332 total, 120 thin omitted)
@ -379,8 +379,8 @@ Cohesion: 0.12
Nodes (18): metadata, FeaturedProducts(), ProductCard(), OrderSuccess(), PetProfile(), SearchResultsPage(), CURRENT_SYMPTOMS, MEDICAL_HISTORIES (+10 more)
### Community 5 - "CmsController"
Cohesion: 0.12
Nodes (12): CmsController, ApiBearerAuth, ApiOperation, ApiTags, Controller, Delete, Get, Param (+4 more)
Cohesion: 0.10
Nodes (14): CmsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
### Community 6 - "TicketsService"
Cohesion: 0.09
@ -552,7 +552,7 @@ Nodes (19): autoprefixer, devDependencies, autoprefixer, eslint, @eslint/js, glo
### Community 50 - "devDependencies"
Cohesion: 0.12
Nodes (17): devDependencies, eslint, jsdom, tailwindcss, @testing-library/jest-dom, @testing-library/react, @types/node, @types/react (+9 more)
Nodes (17): eslint-config-next, devDependencies, eslint, eslint-config-next, jsdom, @testing-library/jest-dom, @testing-library/react, @types/node (+9 more)
### Community 51 - "BlogsController"
Cohesion: 0.07
@ -600,7 +600,7 @@ Nodes (19): ProductImageZoomModalProps, CalculatorState, GalleryMediaItem, ICON_
### Community 63 - "dependencies"
Cohesion: 0.09
Nodes (23): dependencies, bcrypt, bcryptjs, class-validator, compression, ioredis, @nestjs/common, @nestjs/passport (+15 more)
Nodes (23): dependencies, bcryptjs, class-validator, compression, ioredis, @nestjs/common, @nestjs/jwt, @nestjs/passport (+15 more)
### Community 64 - "compilerOptions"
Cohesion: 0.10
@ -763,8 +763,8 @@ Cohesion: 0.17
Nodes (11): 1. Detect Test Runner from Tech Stack, 2. Execution Output Capture (Mandatory), 3. Acceptance Criteria Verification, 4. Failure Routing Protocol, 5. Success Routing Protocol, 6. Forbidden Actions, Expected JSON Output Schema, Operational Rules & Boundaries (+3 more)
### Community 106 - "cms.controller.ts"
Cohesion: 0.21
Nodes (12): Body, Post, CreateHeroBannerDto, CreateSmartAdvisorRuleDto, CreateVetTestimonialDto, ApiProperty, ApiPropertyOptional, IsBoolean (+4 more)
Cohesion: 0.37
Nodes (10): CreateHeroBannerDto, CreateSmartAdvisorRuleDto, CreateVetTestimonialDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNumber, IsOptional (+2 more)
### Community 107 - "PaginationDto"
Cohesion: 0.12

View File

@ -384,7 +384,7 @@
"label": "Category",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L51",
"source_location": "L54",
"_callable": true,
"_callable_class": true,
"_origin": "ast",
@ -397,7 +397,7 @@
"label": "Product",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L58",
"source_location": "L61",
"_callable": true,
"_callable_class": true,
"_origin": "ast",
@ -6188,7 +6188,7 @@
"label": "Products()",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L120",
"source_location": "L123",
"_callable": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_products_products",
@ -6220,78 +6220,6 @@
"community_name": "Products.tsx",
"norm_label": "calculatesellingprice()"
},
{
"label": ".createHeroBanner()",
"file_type": "code",
"source_file": "backend/src/cms/cms.controller.ts",
"source_location": "L39",
"_callable": true,
"_origin": "ast",
"id": "backend_src_cms_cms_controller_cmscontroller_createherobanner",
"community": 106,
"community_name": "cms.controller.ts",
"norm_label": ".createherobanner()"
},
{
"label": ".createSmartAdvisorRule()",
"file_type": "code",
"source_file": "backend/src/cms/cms.controller.ts",
"source_location": "L95",
"_callable": true,
"_origin": "ast",
"id": "backend_src_cms_cms_controller_cmscontroller_createsmartadvisorrule",
"community": 106,
"community_name": "cms.controller.ts",
"norm_label": ".createsmartadvisorrule()"
},
{
"label": ".createVetTestimonial()",
"file_type": "code",
"source_file": "backend/src/cms/cms.controller.ts",
"source_location": "L67",
"_callable": true,
"_origin": "ast",
"id": "backend_src_cms_cms_controller_cmscontroller_createvettestimonial",
"community": 106,
"community_name": "cms.controller.ts",
"norm_label": ".createvettestimonial()"
},
{
"label": ".createHeroBanner()",
"file_type": "code",
"source_file": "backend/src/cms/cms.service.ts",
"source_location": "L20",
"_callable": true,
"_origin": "ast",
"id": "backend_src_cms_cms_service_cmsservice_createherobanner",
"community": 106,
"community_name": "cms.controller.ts",
"norm_label": ".createherobanner()"
},
{
"label": ".createSmartAdvisorRule()",
"file_type": "code",
"source_file": "backend/src/cms/cms.service.ts",
"source_location": "L67",
"_callable": true,
"_origin": "ast",
"id": "backend_src_cms_cms_service_cmsservice_createsmartadvisorrule",
"community": 106,
"community_name": "cms.controller.ts",
"norm_label": ".createsmartadvisorrule()"
},
{
"label": ".createVetTestimonial()",
"file_type": "code",
"source_file": "backend/src/cms/cms.service.ts",
"source_location": "L41",
"_callable": true,
"_origin": "ast",
"id": "backend_src_cms_cms_service_cmsservice_createvettestimonial",
"community": 106,
"community_name": "cms.controller.ts",
"norm_label": ".createvettestimonial()"
},
{
"label": ".constructor()",
"file_type": "code",
@ -13504,6 +13432,42 @@
"community_name": "CmsController",
"norm_label": ".constructor()"
},
{
"label": ".createHeroBanner()",
"file_type": "code",
"source_file": "backend/src/cms/cms.controller.ts",
"source_location": "L39",
"_callable": true,
"_origin": "ast",
"id": "backend_src_cms_cms_controller_cmscontroller_createherobanner",
"community": 5,
"community_name": "CmsController",
"norm_label": ".createherobanner()"
},
{
"label": ".createSmartAdvisorRule()",
"file_type": "code",
"source_file": "backend/src/cms/cms.controller.ts",
"source_location": "L95",
"_callable": true,
"_origin": "ast",
"id": "backend_src_cms_cms_controller_cmscontroller_createsmartadvisorrule",
"community": 5,
"community_name": "CmsController",
"norm_label": ".createsmartadvisorrule()"
},
{
"label": ".createVetTestimonial()",
"file_type": "code",
"source_file": "backend/src/cms/cms.controller.ts",
"source_location": "L67",
"_callable": true,
"_origin": "ast",
"id": "backend_src_cms_cms_controller_cmscontroller_createvettestimonial",
"community": 5,
"community_name": "CmsController",
"norm_label": ".createvettestimonial()"
},
{
"label": ".deleteHeroBanner()",
"file_type": "code",
@ -13624,6 +13588,42 @@
"community_name": "CmsController",
"norm_label": ".constructor()"
},
{
"label": ".createHeroBanner()",
"file_type": "code",
"source_file": "backend/src/cms/cms.service.ts",
"source_location": "L20",
"_callable": true,
"_origin": "ast",
"id": "backend_src_cms_cms_service_cmsservice_createherobanner",
"community": 5,
"community_name": "CmsController",
"norm_label": ".createherobanner()"
},
{
"label": ".createSmartAdvisorRule()",
"file_type": "code",
"source_file": "backend/src/cms/cms.service.ts",
"source_location": "L67",
"_callable": true,
"_origin": "ast",
"id": "backend_src_cms_cms_service_cmsservice_createsmartadvisorrule",
"community": 5,
"community_name": "CmsController",
"norm_label": ".createsmartadvisorrule()"
},
{
"label": ".createVetTestimonial()",
"file_type": "code",
"source_file": "backend/src/cms/cms.service.ts",
"source_location": "L41",
"_callable": true,
"_origin": "ast",
"id": "backend_src_cms_cms_service_cmsservice_createvettestimonial",
"community": 5,
"community_name": "CmsController",
"norm_label": ".createvettestimonial()"
},
{
"label": ".deleteHeroBanner()",
"file_type": "code",
@ -19504,28 +19504,6 @@
"community_name": "cms.controller.ts",
"norm_label": "cms.controller.ts"
},
{
"label": "Body",
"file_type": "code",
"source_file": "",
"source_location": "",
"_origin": "ast",
"id": "backend_src_cms_cms_controller_ts_body",
"community": 106,
"community_name": "cms.controller.ts",
"norm_label": "body"
},
{
"label": "Post",
"file_type": "code",
"source_file": "",
"source_location": "",
"_origin": "ast",
"id": "backend_src_cms_cms_controller_ts_post",
"community": 106,
"community_name": "cms.controller.ts",
"norm_label": "post"
},
{
"label": "cms.service.ts",
"file_type": "code",
@ -33854,26 +33832,26 @@
"norm_label": "typescript"
},
{
"label": "@nestjs/jwt",
"label": "bcrypt",
"file_type": "code",
"source_file": "backend/package.json",
"source_location": "L27",
"source_location": "L33",
"_origin": "ast",
"id": "backend_package_dependencies_nestjs_jwt",
"id": "backend_package_dependencies_bcrypt",
"community": 301,
"community_name": "@nestjs/jwt",
"norm_label": "@nestjs/jwt"
"community_name": "bcrypt",
"norm_label": "bcrypt"
},
{
"label": "@nestjs/jwt",
"label": "bcrypt",
"file_type": "concept",
"source_file": "backend/package.json",
"source_location": "L27",
"source_location": "L33",
"_origin": "ast",
"id": "nestjs_jwt",
"id": "bcrypt",
"community": 301,
"community_name": "@nestjs/jwt",
"norm_label": "@nestjs/jwt"
"community_name": "bcrypt",
"norm_label": "bcrypt"
},
{
"label": "typescript-eslint",
@ -35251,26 +35229,26 @@
"norm_label": "@nestjs/swagger"
},
{
"label": "eslint-config-next",
"label": "tailwindcss",
"file_type": "code",
"source_file": "frontend/application/package.json",
"source_location": "L31",
"source_location": "L33",
"_origin": "ast",
"id": "frontend_application_package_devdependencies_eslint_config_next",
"id": "frontend_application_package_devdependencies_tailwindcss",
"community": 331,
"community_name": "eslint-config-next",
"norm_label": "eslint-config-next"
"community_name": "tailwindcss",
"norm_label": "tailwindcss"
},
{
"label": "eslint-config-next",
"label": "tailwindcss",
"file_type": "concept",
"source_file": "frontend/application/package.json",
"source_location": "L31",
"source_location": "L33",
"_origin": "ast",
"id": "eslint_config_next",
"id": "frontend_application_package_json_tailwindcss",
"community": 331,
"community_name": "eslint-config-next",
"norm_label": "eslint-config-next"
"community_name": "tailwindcss",
"norm_label": "tailwindcss"
},
{
"label": "ApiBearerAuth",
@ -38561,6 +38539,17 @@
"community_name": "CmsController",
"norm_label": "apitags"
},
{
"label": "Body",
"file_type": "code",
"source_file": "",
"source_location": "",
"_origin": "ast",
"id": "backend_src_cms_cms_controller_ts_body",
"community": 5,
"community_name": "CmsController",
"norm_label": "body"
},
{
"label": "Controller",
"file_type": "code",
@ -38605,6 +38594,17 @@
"community_name": "CmsController",
"norm_label": "param"
},
{
"label": "Post",
"file_type": "code",
"source_file": "",
"source_location": "",
"_origin": "ast",
"id": "backend_src_cms_cms_controller_ts_post",
"community": 5,
"community_name": "CmsController",
"norm_label": "post"
},
{
"label": "Put",
"file_type": "code",
@ -38660,6 +38660,17 @@
"community_name": "devDependencies",
"norm_label": "eslint"
},
{
"label": "eslint-config-next",
"file_type": "code",
"source_file": "frontend/application/package.json",
"source_location": "L31",
"_origin": "ast",
"id": "frontend_application_package_devdependencies_eslint_config_next",
"community": 50,
"community_name": "devDependencies",
"norm_label": "eslint-config-next"
},
{
"label": "jsdom",
"file_type": "code",
@ -38671,17 +38682,6 @@
"community_name": "devDependencies",
"norm_label": "jsdom"
},
{
"label": "tailwindcss",
"file_type": "code",
"source_file": "frontend/application/package.json",
"source_location": "L33",
"_origin": "ast",
"id": "frontend_application_package_devdependencies_tailwindcss",
"community": 50,
"community_name": "devDependencies",
"norm_label": "tailwindcss"
},
{
"label": "@testing-library/jest-dom",
"file_type": "code",
@ -38737,6 +38737,17 @@
"community_name": "devDependencies",
"norm_label": "vitest"
},
{
"label": "eslint-config-next",
"file_type": "concept",
"source_file": "frontend/application/package.json",
"source_location": "L31",
"_origin": "ast",
"id": "eslint_config_next",
"community": 50,
"community_name": "devDependencies",
"norm_label": "eslint-config-next"
},
{
"label": "eslint",
"file_type": "concept",
@ -38748,17 +38759,6 @@
"community_name": "devDependencies",
"norm_label": "eslint"
},
{
"label": "tailwindcss",
"file_type": "concept",
"source_file": "frontend/application/package.json",
"source_location": "L33",
"_origin": "ast",
"id": "frontend_application_package_json_tailwindcss",
"community": 50,
"community_name": "devDependencies",
"norm_label": "tailwindcss"
},
{
"label": "@types/node",
"file_type": "concept",
@ -40882,17 +40882,6 @@
"community_name": "dependencies",
"norm_label": "dependencies"
},
{
"label": "bcrypt",
"file_type": "code",
"source_file": "backend/package.json",
"source_location": "L33",
"_origin": "ast",
"id": "backend_package_dependencies_bcrypt",
"community": 63,
"community_name": "dependencies",
"norm_label": "bcrypt"
},
{
"label": "bcryptjs",
"file_type": "code",
@ -40948,6 +40937,17 @@
"community_name": "dependencies",
"norm_label": "@nestjs/common"
},
{
"label": "@nestjs/jwt",
"file_type": "code",
"source_file": "backend/package.json",
"source_location": "L27",
"_origin": "ast",
"id": "backend_package_dependencies_nestjs_jwt",
"community": 63,
"community_name": "dependencies",
"norm_label": "@nestjs/jwt"
},
{
"label": "@nestjs/passport",
"file_type": "code",
@ -41003,17 +41003,6 @@
"community_name": "dependencies",
"norm_label": "rxjs"
},
{
"label": "bcrypt",
"file_type": "concept",
"source_file": "backend/package.json",
"source_location": "L33",
"_origin": "ast",
"id": "bcrypt",
"community": 63,
"community_name": "dependencies",
"norm_label": "bcrypt"
},
{
"label": "bcryptjs",
"file_type": "concept",
@ -41069,6 +41058,17 @@
"community_name": "dependencies",
"norm_label": "@nestjs/common"
},
{
"label": "@nestjs/jwt",
"file_type": "concept",
"source_file": "backend/package.json",
"source_location": "L27",
"_origin": "ast",
"id": "nestjs_jwt",
"community": 63,
"community_name": "dependencies",
"norm_label": "@nestjs/jwt"
},
{
"label": "@nestjs/passport",
"file_type": "concept",
@ -50293,7 +50293,7 @@
"confidence": "EXTRACTED",
"confidence_score": 1.0,
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L1775",
"source_location": "L1778",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products_products",
@ -81637,7 +81637,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L35",
"source_location": "L38",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81649,7 +81649,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L35",
"source_location": "L38",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81661,7 +81661,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L36",
"source_location": "L39",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81673,7 +81673,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L37",
"source_location": "L40",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81685,7 +81685,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L38",
"source_location": "L41",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81697,7 +81697,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L39",
"source_location": "L42",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81709,7 +81709,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L40",
"source_location": "L43",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81721,7 +81721,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L41",
"source_location": "L44",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81733,7 +81733,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L42",
"source_location": "L45",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81745,7 +81745,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L43",
"source_location": "L46",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81757,7 +81757,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L44",
"source_location": "L47",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81769,7 +81769,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L45",
"source_location": "L48",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81781,7 +81781,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L45",
"source_location": "L48",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -81793,7 +81793,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L45",
"source_location": "L48",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -94705,7 +94705,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L35",
"source_location": "L38",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -94717,7 +94717,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L36",
"source_location": "L39",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -94729,7 +94729,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L37",
"source_location": "L40",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -94741,7 +94741,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L38",
"source_location": "L41",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -94753,7 +94753,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L39",
"source_location": "L42",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -94765,7 +94765,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L40",
"source_location": "L43",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -94777,7 +94777,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L41",
"source_location": "L44",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -94789,7 +94789,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L42",
"source_location": "L45",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -94801,7 +94801,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L43",
"source_location": "L46",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -94813,7 +94813,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L44",
"source_location": "L47",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -94825,7 +94825,7 @@
"context": "import",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L45",
"source_location": "L48",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -118097,7 +118097,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L120",
"source_location": "L123",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -118108,7 +118108,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L51",
"source_location": "L54",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -118119,7 +118119,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "frontend/admin-panel/src/pages/Products.tsx",
"source_location": "L58",
"source_location": "L61",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_admin_panel_src_pages_products",
@ -137022,5 +137022,5 @@
}
],
"hyperedges": [],
"built_at_commit": "f0457b57484a35308f030c1760128a55245c389f"
"built_at_commit": "9744aa814f71b8e124acff84ce0c3384e993a60f"
}

View File

@ -1,16 +1,16 @@
# Graph Report - canina (2026-09-02)
## Corpus Check
- 599 files · ~1,082,386 words
- 599 files · ~1,084,324 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 4208 nodes · 7643 edges · 332 communities (212 shown, 120 thin omitted)
- 4208 nodes · 7643 edges · 333 communities (214 shown, 119 thin omitted)
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 288 edges (avg confidence: 0.79)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `9744aa81`
- Built from commit: `68840fbd`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@ -136,13 +136,13 @@
- compilerOptions
- compilerOptions
- backend/README.md
- AdminController
- AdminService
- UsersService
- Repository Map
- validate_integrity.js
- admin-panel/package.json
- Sahel-Font
- AdminService
- ProductDto
- Reports.tsx
- Sahel-Font
- Role & Core Objective
@ -211,7 +211,7 @@
- React + TypeScript + Vite
- Select.tsx
- userStore.ts
- @tailwindcss/postcss
- Body
- SmsLogQueryDto
- application/README.md
- deploy.sh
@ -299,7 +299,7 @@
- .initiateOrderPayment
- @tailwindcss/postcss
- typescript
- bcrypt
- @nestjs/jwt
- typescript-eslint
- @nestjs/throttler
- passport
@ -318,7 +318,7 @@
- @nestjs/cli
- @nestjs/testing
- prettier
- ts-jest
- eslint
- @types/react-dom
- @types/js-yaml
- eslint-plugin-react-refresh
@ -326,6 +326,7 @@
- typescript-eslint
- @nestjs/swagger
- tailwindcss
- eslint-config-next
## God Nodes (most connected - your core abstractions)
1. `Roles()` - 106 edges
@ -356,7 +357,7 @@
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/services/authService.ts -> frontend/application/lib/services/api.ts`
- 4-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/orderService.ts -> frontend/application/lib/services/api.ts`
## Communities (332 total, 120 thin omitted)
## Communities (333 total, 119 thin omitted)
### Community 0 - "Roles"
Cohesion: 0.24
@ -396,15 +397,15 @@ Nodes (17): SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiT
### Community 9 - "devDependencies"
Cohesion: 0.22
Nodes (9): devDependencies, eslint, @types/bcryptjs, @types/node, typescript, eslint, @types/node, typescript (+1 more)
Nodes (9): devDependencies, ts-jest, @types/bcryptjs, @types/node, typescript, @types/node, typescript, ts-jest (+1 more)
### Community 10 - "CreateReviewDto"
Cohesion: 0.07
Nodes (30): CreateReviewDto, ApiProperty, IsInt, IsNotEmpty, IsOptional, IsString, Min, ApiProperty (+22 more)
### Community 11 - "WikiController"
Cohesion: 0.21
Nodes (9): ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, Controller, Get, Param, Query (+1 more)
Cohesion: 0.13
Nodes (13): ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, Controller, Get, Param, Query (+5 more)
### Community 12 - "index.ts"
Cohesion: 0.06
@ -435,7 +436,7 @@ Cohesion: 0.19
Nodes (8): JwtAuthGuard, Injectable, ROLES_KEY, RequestWithUser, RolesGuard, Injectable, UserReqPayload, ScientificTermData
### Community 19 - "admin.controller.ts"
Cohesion: 0.35
Cohesion: 0.29
Nodes (12): ApplyGlobalMarginsDto, BulkPriceAdjustmentDto, ApiProperty, ApiPropertyOptional, IsArray, IsBoolean, IsIn, IsNumber (+4 more)
### Community 20 - "CreateVideoDto"
@ -552,7 +553,7 @@ Nodes (19): autoprefixer, devDependencies, autoprefixer, eslint, @eslint/js, glo
### Community 50 - "devDependencies"
Cohesion: 0.12
Nodes (17): eslint-config-next, devDependencies, eslint, eslint-config-next, jsdom, @testing-library/jest-dom, @testing-library/react, @types/node (+9 more)
Nodes (17): devDependencies, eslint, jsdom, @tailwindcss/postcss, @testing-library/jest-dom, @testing-library/react, @types/node, @types/react (+9 more)
### Community 51 - "BlogsController"
Cohesion: 0.07
@ -591,7 +592,7 @@ Cohesion: 0.06
Nodes (30): compilerOptions, allowSyntheticDefaultImports, baseUrl, declaration, emitDecoratorMetadata, esModuleInterop, experimentalDecorators, forceConsistentCasingInFileNames (+22 more)
### Community 60 - "CreateUserDto"
Cohesion: 0.23
Cohesion: 0.24
Nodes (10): CreateUserDto, ApiProperty, ApiPropertyOptional, IsEmail, IsNotEmpty, IsNumber, IsOptional, IsString (+2 more)
### Community 61 - "ProductPage.tsx"
@ -600,19 +601,19 @@ Nodes (19): ProductImageZoomModalProps, CalculatorState, GalleryMediaItem, ICON_
### Community 63 - "dependencies"
Cohesion: 0.09
Nodes (23): dependencies, bcryptjs, class-validator, compression, ioredis, @nestjs/common, @nestjs/jwt, @nestjs/passport (+15 more)
Nodes (23): dependencies, bcrypt, bcryptjs, class-validator, compression, ioredis, @nestjs/common, @nestjs/passport (+15 more)
### Community 64 - "compilerOptions"
Cohesion: 0.10
Nodes (20): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, lib, module, moduleDetection, moduleResolution, noEmit (+12 more)
### Community 65 - "admin.service.ts"
Cohesion: 0.14
Nodes (15): CouponTargetInput, PaginationQuery, ProductDto, ApiPropertyOptional, IsArray, IsBoolean, IsNumber, IsOptional (+7 more)
Cohesion: 0.22
Nodes (8): CouponTargetInput, PaginationQuery, applyRounding(), calculateSellingPrice(), DEFAULT_PRICING_SETTINGS, PricingSettings, RoundingMode, CANONICAL_ALIASES
### Community 66 - "AdminQueryDto"
Cohesion: 0.13
Nodes (8): ApiQuery, Get, Query, AdminProductQueryDto, AdminQueryDto, ApiPropertyOptional, IsOptional, IsString
Cohesion: 0.18
Nodes (7): ApiQuery, Query, AdminProductQueryDto, AdminQueryDto, ApiPropertyOptional, IsOptional, IsString
### Community 67 - "ReportsController"
Cohesion: 0.14
@ -767,8 +768,8 @@ Cohesion: 0.37
Nodes (10): CreateHeroBannerDto, CreateSmartAdvisorRuleDto, CreateVetTestimonialDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNumber, IsOptional (+2 more)
### Community 107 - "PaginationDto"
Cohesion: 0.12
Nodes (15): AdminModule, Module, PaginationDto, SortOrder, ApiPropertyOptional, IsEnum, IsInt, IsOptional (+7 more)
Cohesion: 0.18
Nodes (11): AdminModule, Module, PaginationDto, SortOrder, ApiPropertyOptional, IsEnum, IsInt, IsOptional (+3 more)
### Community 108 - "PrismaService"
Cohesion: 0.06
@ -826,9 +827,9 @@ Nodes (9): compilerOptions, esModuleInterop, module, moduleResolution, skipLibCh
Cohesion: 0.20
Nodes (9): Compile and run the project, Deployment, Description, License, Project setup, Resources, Run tests, Stay in touch (+1 more)
### Community 122 - "AdminController"
Cohesion: 0.17
Nodes (12): AdminController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Param (+4 more)
### Community 122 - "AdminService"
Cohesion: 0.10
Nodes (12): AdminController, ApiBearerAuth, ApiOperation, ApiTags, Controller, Delete, Get, Param (+4 more)
### Community 123 - "UsersService"
Cohesion: 0.07
@ -850,6 +851,10 @@ Nodes (9): name, private, scripts, build, dev, lint, preview, type (+1 more)
Cohesion: 0.20
Nodes (9): Arch Linux, Contributors, Install, Known problems for variable version, License, Sahel-Font, To Do (variable), طریقه استفاده از نسخه متغیر variable (+1 more)
### Community 128 - "ProductDto"
Cohesion: 0.16
Nodes (7): ProductDto, ApiPropertyOptional, IsArray, IsBoolean, IsNumber, IsOptional, IsString
### Community 129 - "Reports.tsx"
Cohesion: 0.20
Nodes (7): BestSellerItem, CategoryDistItem, COLORS, CustomTooltipProps, ReportData, TopCouponItem, Reports
@ -1086,6 +1091,10 @@ Nodes (3): Select, SelectOption, SelectProps
Cohesion: 0.08
Nodes (37): AuthModal, B2BPortal, CartDrawer, ClientLayout(), LoginModal, AuthModal(), AuthModalProps, extractOtpFromText() (+29 more)
### Community 198 - "Body"
Cohesion: 0.21
Nodes (3): Body, Post, CouponInput
### Community 199 - "SmsLogQueryDto"
Cohesion: 0.25
Nodes (7): SmsLogQueryDto, ApiPropertyOptional, IsIn, IsNumber, IsOptional, IsString, Type
@ -1125,7 +1134,7 @@ Nodes (3): GET(), handleRevalidate(), POST()
## Knowledge Gaps
- **1347 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1342 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **120 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **119 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
@ -1134,7 +1143,7 @@ _Questions this graph is uniquely positioned to answer:_
_High betweenness centrality (0.065) - this node is a cross-community bridge._
- **Why does `ApiResponse` connect `src/services/api.ts` to `BlogsController`, `PetsController`, `ProductsService`, `WikiController`, `auth.controller.ts`, `HomeController`, `UsersService`, `OrdersService`?**
_High betweenness centrality (0.057) - this node is a cross-community bridge._
- **Why does `PrismaService` connect `PrismaService` to `app.module.ts`, `CmsController`, `TicketsService`, `CreateReviewDto`, `DoctorQueryDto`, `HomeController`, `CreateVideoDto`, `BlogsService`, `menu.module.ts`, `auth.service.ts`, `MenuService`, `B2BService`, `FaqService`, `ZibalService`, `CategoriesController`, `MediaController`, `ZibalEBankService`, `BannersService`, `TestimonialsService`, `IngredientsService`, `SmartAdvisorService`, `MetricsController`, `WikiService`, `admin.service.ts`, `ReportsController`, `PetsController`, `zibal-ebank.service.ts`, `ProductsService`, `seo.module.ts`, `zibal.service.ts`, `OrdersService`, `BlogsController`, `cms.controller.ts`, `PaginationDto`, `PetsController`, `UsersService`?**
- **Why does `PrismaService` connect `PrismaService` to `app.module.ts`, `CmsController`, `TicketsService`, `CreateReviewDto`, `WikiController`, `DoctorQueryDto`, `HomeController`, `CreateVideoDto`, `BlogsService`, `menu.module.ts`, `auth.service.ts`, `MenuService`, `B2BService`, `FaqService`, `ZibalService`, `CategoriesController`, `MediaController`, `ZibalEBankService`, `BannersService`, `TestimonialsService`, `IngredientsService`, `SmartAdvisorService`, `MetricsController`, `WikiService`, `admin.service.ts`, `ReportsController`, `PetsController`, `zibal-ebank.service.ts`, `ProductsService`, `seo.module.ts`, `zibal.service.ts`, `OrdersService`, `BlogsController`, `cms.controller.ts`, `PaginationDto`, `PetsController`, `UsersService`?**
_High betweenness centrality (0.029) - this node is a cross-community bridge._
- **What connects `$schema`, `collection`, `sourceRoot` to the rest of the system?**
_1347 weakly-connected nodes found - possible documentation gaps or missing edges._

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff