Compare commits

..

4 Commits

Author SHA1 Message Date
parsa aghaei
29f5122f14 chore(graphify): update knowledge graph
Some checks failed
Deploy Canina / deploy (push) Successful in 2m23s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 0s
2026-09-05 15:16:28 +03:30
parsa aghaei
2c377cab88 fix(app): improve wallet history display, fix order details modal layout and lint errors 2026-09-05 15:16:20 +03:30
parsa aghaei
56e8029fff feat(admin): update order management and SMS rules settings UI with lint fixes 2026-09-05 15:16:12 +03:30
parsa aghaei
5e3e3958c5 feat(backend): add wallet refund transactions and enhance SMS automation rules 2026-09-05 15:16:04 +03:30
32 changed files with 158475 additions and 9718 deletions

View File

@ -402,6 +402,9 @@ model Order {
paymentMethod String? @default("card") @map("payment_method") @db.VarChar(30)
shippingAddress String? @map("shipping_address") @db.Text
trackingNumber String? @unique @map("tracking_number") @db.VarChar(100)
refundStatus String? @default("none") @map("refund_status") @db.VarChar(30) // none, wallet, gateway, partial
refundedAmount Decimal @default(0.00) @map("refunded_amount") @db.Decimal(15, 2)
refundMethod String? @map("refund_method") @db.VarChar(30) // wallet, zibal
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

View File

@ -204,7 +204,8 @@ export class AdminController {
@Body('trackingCode') trackingCode?: string,
@Body('trackingNumber') trackingNumber?: string,
) {
const finalTracking = trackingNumber !== undefined ? trackingNumber : trackingCode;
const finalTracking =
trackingNumber !== undefined ? trackingNumber : trackingCode;
const order = await this.adminService.updateOrderStatus(
id,
status,

View File

@ -859,22 +859,31 @@ export class AdminService {
}
if (!shipping.province || !shipping.address) {
let text = trimmed;
const recipientMatch = text.match(/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/);
const recipientMatch = text.match(
/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/,
);
if (recipientMatch) {
if (!shipping.fullName) shipping.fullName = recipientMatch[1]?.trim();
if (!shipping.phone && recipientMatch[2]) shipping.phone = recipientMatch[2]?.trim();
if (!shipping.fullName)
shipping.fullName = recipientMatch[1]?.trim();
if (!shipping.phone && recipientMatch[2])
shipping.phone = recipientMatch[2]?.trim();
text = text.replace(recipientMatch[0], '').trim();
}
const postalMatch = text.match(/\(کد پستی:\s*([^)]+)\)/);
if (postalMatch) {
if (!shipping.postalCode) shipping.postalCode = postalMatch[1]?.trim();
if (!shipping.postalCode)
shipping.postalCode = postalMatch[1]?.trim();
text = text.replace(postalMatch[0], '').trim();
}
const parts = text.split(/[،,]/).map((p) => p.trim()).filter(Boolean);
const parts = text
.split(/[،,]/)
.map((p) => p.trim())
.filter(Boolean);
if (parts.length >= 3) {
if (!shipping.province) shipping.province = parts[0];
if (!shipping.city) shipping.city = parts[1];
if (!shipping.address) shipping.address = parts.slice(2).join('، ');
if (!shipping.address)
shipping.address = parts.slice(2).join('، ');
} else if (parts.length === 2) {
if (!shipping.province) shipping.province = parts[0];
if (!shipping.address) shipping.address = parts[1];
@ -897,25 +906,35 @@ export class AdminService {
customerFullName
).trim();
const finalTracking = trackingNumber || updatedOrder.trackingNumber || '';
const finalTracking =
trackingNumber || updatedOrder.trackingNumber || '';
const orderData = {
orderNumber: updatedOrder.trackingNumber || updatedOrder.id.slice(0, 8),
orderNumber:
updatedOrder.trackingNumber || updatedOrder.id.slice(0, 8),
amount: Number(updatedOrder.totalAmount || 0).toLocaleString('fa-IR'),
customerName: customerFullName,
customerPhone: user?.mobile || shipping.phone || '',
recipientName: recipientFullName,
recipientPhone: shipping.phone || user?.mobile || '',
provinceCity: (shipping.province && shipping.city ? `${shipping.province} - ${shipping.city}` : shipping.province || shipping.city || 'ثبت نشده').trim(),
provinceCity: (shipping.province && shipping.city
? `${shipping.province} - ${shipping.city}`
: shipping.province || shipping.city || 'ثبت نشده'
).trim(),
postalCode: shipping.postalCode || shipping.zipCode || '',
address: (shipping.address || (typeof rawShipping === 'string' ? rawShipping : '')).trim(),
address: (
shipping.address ||
(typeof rawShipping === 'string' ? rawShipping : '')
).trim(),
trackingCode: finalTracking || 'ارسال با پیک شهری',
shippingTrackingCode: finalTracking || 'ارسال با پیک شهری',
shippingMethod: finalTracking ? 'پست پیشتاز / تیپاکس' : 'پیک شهری',
orderDate: new Date().toLocaleDateString('fa-IR'),
};
this.smsService.triggerEvent('ORDER_SHIPPED', orderData).catch(() => {});
this.smsService
.triggerEvent('ORDER_SHIPPED', orderData)
.catch(() => {});
} catch (smsErr) {
// Silently catch SMS errors to avoid breaking status update
}
@ -944,6 +963,12 @@ export class AdminService {
);
}
if (order.refundStatus === 'wallet' || order.refundStatus === 'gateway') {
throw new BadRequestException(
'برای این سفارش قبلاً استرداد وجه انجام شده است',
);
}
const refundAmount =
customAmount !== undefined && customAmount > 0
? customAmount
@ -980,6 +1005,9 @@ export class AdminService {
where: { id: order.id },
data: {
status: 'cancelled',
refundStatus: 'wallet',
refundedAmount: refundAmount,
refundMethod: 'wallet',
},
});
});
@ -1000,11 +1028,15 @@ export class AdminService {
customerPhone: order.user.mobile,
amount: Number(refundAmount || 0).toLocaleString('fa-IR'),
balance: Number(user?.walletBalance || 0).toLocaleString('fa-IR'),
reason: reason || `استرداد وجه سفارش #${order.trackingNumber || order.id.slice(0, 8)}`,
reason:
reason ||
`استرداد وجه سفارش #${order.trackingNumber || order.id.slice(0, 8)}`,
date: new Date().toLocaleDateString('fa-IR'),
};
this.smsService.triggerEvent('WALLET_CHARGED', refundData).catch(() => {});
this.smsService
.triggerEvent('WALLET_CHARGED', refundData)
.catch(() => {});
} catch (err) {
// Silently catch SMS errors
}
@ -1290,8 +1322,14 @@ export class AdminService {
customerName: customerFullName,
customerPhone: updatedUser.mobile,
amount: Number(amount || 0).toLocaleString('fa-IR'),
balance: Number(updatedUser.walletBalance || 0).toLocaleString('fa-IR'),
reason: description || (type === 'refund' ? 'استرداد وجه به کیف پول' : 'شارژ کیف پول توسط مدیریت'),
balance: Number(updatedUser.walletBalance || 0).toLocaleString(
'fa-IR',
),
reason:
description ||
(type === 'refund'
? 'استرداد وجه به کیف پول'
: 'شارژ کیف پول توسط مدیریت'),
date: new Date().toLocaleDateString('fa-IR'),
};

View File

@ -304,12 +304,16 @@ export class ProductDto {
@IsArray()
keyHighlights?: string[];
@ApiPropertyOptional({ description: 'کارت‌های شاخص «چرا این محصول؟» (۳ کارت مزایا)' })
@ApiPropertyOptional({
description: 'کارت‌های شاخص «چرا این محصول؟» (۳ کارت مزایا)',
})
@IsOptional()
@IsArray()
keyBenefits?: Array<{ icon: string; title: string; description: string }>;
@ApiPropertyOptional({ description: 'جدول آنالیز و درصد ترکیبات (Analytical Constituents)' })
@ApiPropertyOptional({
description: 'جدول آنالیز و درصد ترکیبات (Analytical Constituents)',
})
@IsOptional()
analysis?: Record<string, string>;
@ -327,12 +331,16 @@ export class ProductDto {
@IsOptional()
contraindications?: string[] | string;
@ApiPropertyOptional({ description: 'نتایج بالینی مورد انتظار (Expected Clinical Results)' })
@ApiPropertyOptional({
description: 'نتایج بالینی مورد انتظار (Expected Clinical Results)',
})
@IsOptional()
@IsArray()
expectedResults?: Array<{ icon: string; text: string }>;
@ApiPropertyOptional({ description: 'توصیه و نظر پزشک متخصص (Specialist Endorsement)' })
@ApiPropertyOptional({
description: 'توصیه و نظر پزشک متخصص (Specialist Endorsement)',
})
@IsOptional()
specialist?: {
name?: string;

View File

@ -27,39 +27,93 @@ export const SMS_EVENT_DEFINITIONS: SmsEventDefinition[] = [
{
event: 'ORDER_PAID',
label: 'پرداخت موفق سفارش (Order Paid)',
description: 'هنگام پرداخت موفق آنلاین در درگاه بانکی یا تسویه حساب از طریق کیف پول',
description:
'هنگام پرداخت موفق آنلاین در درگاه بانکی یا تسویه حساب از طریق کیف پول',
variables: [
{ key: 'orderNumber', label: 'شماره سفارش / فاکتور', sample: 'CN-140308-01' },
{
key: 'orderNumber',
label: 'شماره سفارش / فاکتور',
sample: 'CN-140308-01',
},
{ key: 'amount', label: 'مبلغ پرداختی (تومان)', sample: '۱,۴۵۰,۰۰۰' },
{ key: 'customerName', label: 'نام خریدار', sample: 'علی محمدی' },
{ key: 'customerPhone', label: 'شماره موبایل خریدار', sample: '09123456789' },
{
key: 'customerPhone',
label: 'شماره موبایل خریدار',
sample: '09123456789',
},
{ key: 'recipientName', label: 'نام تحویل‌گیرنده', sample: 'رضا حسینی' },
{ key: 'recipientPhone', label: 'تلفن تماس تحویل‌گیرنده', sample: '09351234567' },
{ key: 'provinceCity', label: 'استان و شهر مقصد', sample: 'تهران - تهران' },
{
key: 'recipientPhone',
label: 'تلفن تماس تحویل‌گیرنده',
sample: '09351234567',
},
{
key: 'provinceCity',
label: 'استان و شهر مقصد',
sample: 'تهران - تهران',
},
{ key: 'postalCode', label: 'کد پستی ۱۰ رقمی', sample: '1967812345' },
{ key: 'address', label: 'نشانی کامل پستی', sample: 'خیابان ولیعصر، کوچه بهار، پلاک ۴' },
{ key: 'itemsSummary', label: 'خلاصه اقلام سفارش', sample: 'شیر خشک توله سگ (۲)' },
{
key: 'address',
label: 'نشانی کامل پستی',
sample: 'خیابان ولیعصر، کوچه بهار، پلاک ۴',
},
{
key: 'itemsSummary',
label: 'خلاصه اقلام سفارش',
sample: 'شیر خشک توله سگ (۲)',
},
{ key: 'itemCount', label: 'تعداد کل اقلام', sample: '۲' },
{ key: 'paymentMethod', label: 'روش پرداخت', sample: 'آنلاین' },
{ key: 'orderDate', label: 'تاریخ ثبت سفارش', sample: '۱۴۰۳/۰۶/۱۵' },
{ key: 'refNumber', label: 'شماره پیگیری پرداخت بانکی', sample: '83920192' },
{
key: 'refNumber',
label: 'شماره پیگیری پرداخت بانکی',
sample: '83920192',
},
],
},
{
event: 'ORDER_CREATED',
label: 'ثبت سفارش کارت به کارت یا غیرآنلاین',
description: 'هنگامی که سفارش با روش کارت به کارت در انتظار پرداخت ثبت می‌شود',
description:
'هنگامی که سفارش با روش کارت به کارت در انتظار پرداخت ثبت می‌شود',
variables: [
{ key: 'orderNumber', label: 'شماره سفارش / فاکتور', sample: 'CN-140308-01' },
{
key: 'orderNumber',
label: 'شماره سفارش / فاکتور',
sample: 'CN-140308-01',
},
{ key: 'amount', label: 'مبلغ کل سفارش', sample: '۱,۴۵۰,۰۰۰' },
{ key: 'customerName', label: 'نام خریدار', sample: 'علی محمدی' },
{ key: 'customerPhone', label: 'شماره موبایل خریدار', sample: '09123456789' },
{
key: 'customerPhone',
label: 'شماره موبایل خریدار',
sample: '09123456789',
},
{ key: 'recipientName', label: 'نام تحویل‌گیرنده', sample: 'رضا حسینی' },
{ key: 'recipientPhone', label: 'تلفن تحویل‌گیرنده', sample: '09351234567' },
{ key: 'provinceCity', label: 'استان و شهر مقصد', sample: 'تهران - تهران' },
{
key: 'recipientPhone',
label: 'تلفن تحویل‌گیرنده',
sample: '09351234567',
},
{
key: 'provinceCity',
label: 'استان و شهر مقصد',
sample: 'تهران - تهران',
},
{ key: 'postalCode', label: 'کد پستی ۱۰ رقمی', sample: '1967812345' },
{ key: 'address', label: 'نشانی پستی', sample: 'خیابان ولیعصر، کوچه بهار، پلاک ۴' },
{ key: 'itemsSummary', label: 'خلاصه اقلام سفارش', sample: 'شیر خشک توله سگ (۲)' },
{
key: 'address',
label: 'نشانی پستی',
sample: 'خیابان ولیعصر، کوچه بهار، پلاک ۴',
},
{
key: 'itemsSummary',
label: 'خلاصه اقلام سفارش',
sample: 'شیر خشک توله سگ (۲)',
},
{ key: 'itemCount', label: 'تعداد کل اقلام', sample: '۲' },
{ key: 'paymentMethod', label: 'روش پرداخت', sample: 'کارت به کارت' },
{ key: 'orderDate', label: 'تاریخ ثبت سفارش', sample: '۱۴۰۳/۰۶/۱۵' },
@ -71,11 +125,23 @@ export const SMS_EVENT_DEFINITIONS: SmsEventDefinition[] = [
description: 'هنگام تغییر وضعیت سفارش به ارسال شده یا درج کد رهگیری پستی',
variables: [
{ key: 'orderNumber', label: 'شماره سفارش', sample: 'CN-140308-01' },
{ key: 'shippingTrackingCode', label: 'کد رهگیری ۲۴ رقمی پست', sample: '184920394857283948571928' },
{ key: 'trackingCode', label: 'کد رهگیری پستی (مترادف)', sample: '184920394857283948571928' },
{
key: 'shippingTrackingCode',
label: 'کد رهگیری ۲۴ رقمی پست',
sample: '184920394857283948571928',
},
{
key: 'trackingCode',
label: 'کد رهگیری پستی (مترادف)',
sample: '184920394857283948571928',
},
{ key: 'customerName', label: 'نام خریدار', sample: 'علی محمدی' },
{ key: 'recipientName', label: 'نام تحویل‌گیرنده', sample: 'رضا حسینی' },
{ key: 'customerPhone', label: 'شماره همراه مشتری', sample: '09123456789' },
{
key: 'customerPhone',
label: 'شماره همراه مشتری',
sample: '09123456789',
},
{ key: 'shippingMethod', label: 'روش ارسال', sample: 'پست پیشتاز' },
],
},
@ -84,9 +150,21 @@ export const SMS_EVENT_DEFINITIONS: SmsEventDefinition[] = [
label: 'درخواست همکاری و عمده‌فروشی B2B',
description: 'هنگام ارسال فرم همکاری همکاران و کلینیک‌های دامپزشکی',
variables: [
{ key: 'applicantName', label: 'نام متقاضی / فروشگاه', sample: 'پت‌شاپ آلفا' },
{ key: 'applicantPhone', label: 'شماره همراه متقاضی', sample: '09123456789' },
{ key: 'companyName', label: 'نام شرکت یا مجموعه', sample: 'فروشگاه آلفا' },
{
key: 'applicantName',
label: 'نام متقاضی / فروشگاه',
sample: 'پت‌شاپ آلفا',
},
{
key: 'applicantPhone',
label: 'شماره همراه متقاضی',
sample: '09123456789',
},
{
key: 'companyName',
label: 'نام شرکت یا مجموعه',
sample: 'فروشگاه آلفا',
},
{ key: 'businessType', label: 'نوع فعالیت', sample: 'پت‌شاپ' },
{ key: 'date', label: 'تاریخ ثبت درخواست', sample: '۱۴۰۳/۰۶/۱۵' },
],
@ -97,23 +175,44 @@ export const SMS_EVENT_DEFINITIONS: SmsEventDefinition[] = [
description: 'هنگامی که کاربری در فرم تماس پیامی ارسال می‌کند',
variables: [
{ key: 'senderName', label: 'نام فرستنده پیام', sample: 'سارا احمدی' },
{ key: 'senderPhone', label: 'شماره همراه فرستنده', sample: '09123456789' },
{
key: 'senderPhone',
label: 'شماره همراه فرستنده',
sample: '09123456789',
},
{ key: 'email', label: 'ایمیل فرستنده', sample: 'user@example.com' },
{ key: 'subject', label: 'موضوع پیام', sample: 'مشاوره خرید مکمل' },
{ key: 'message', label: 'متن پیام', sample: 'درخواست راهنمایی در مورد دوز کلسیم' },
{
key: 'message',
label: 'متن پیام',
sample: 'درخواست راهنمایی در مورد دوز کلسیم',
},
{ key: 'date', label: 'تاریخ ارسال', sample: '۱۴۰۳/۰۶/۱۵' },
],
},
{
event: 'WALLET_CHARGED',
label: 'شارژ و افزایش موجودی کیف پول',
description: 'هنگام افزایش موجودی کیف پول کاربر (درگاه آنلاین، پنل مدیریت، کش‌بک یا استرداد)',
description:
'هنگام افزایش موجودی کیف پول کاربر (درگاه آنلاین، پنل مدیریت، کش‌بک یا استرداد)',
variables: [
{ key: 'customerName', label: 'نام مشتری', sample: 'علی محمدی' },
{ key: 'amount', label: 'مبلغ شارژ (تومان)', sample: '۵۰,۰۰۰' },
{ key: 'balance', label: 'موجودی جدید کیف پول (تومان)', sample: '۱۵۰,۰۰۰' },
{ key: 'reason', label: 'علت / نوع شارژ', sample: 'شارژ آنلاین / پاداش خرید (Cashback)' },
{ key: 'customerPhone', label: 'شماره همراه مشتری', sample: '09123456789' },
{
key: 'balance',
label: 'موجودی جدید کیف پول (تومان)',
sample: '۱۵۰,۰۰۰',
},
{
key: 'reason',
label: 'علت / نوع شارژ',
sample: 'شارژ آنلاین / پاداش خرید (Cashback)',
},
{
key: 'customerPhone',
label: 'شماره همراه مشتری',
sample: '09123456789',
},
{ key: 'date', label: 'تاریخ شارژ', sample: '۱۴۰۳/۰۶/۱۵' },
],
},
@ -128,7 +227,8 @@ export const DEFAULT_SMS_RULES: SmsRule[] = [
recipientType: 'customer',
patternId: 508081,
variables: ['customerName', 'orderNumber', 'amount'],
description: 'ارسال شماره فاکتور و مبلغ سفارش به شماره همراه مشتری بلافاصله پس از پرداخت',
description:
'ارسال شماره فاکتور و مبلغ سفارش به شماره همراه مشتری بلافاصله پس از پرداخت',
},
{
id: 'default-order-admin',

View File

@ -163,9 +163,7 @@ export class SmsService {
const activeRules = (config.rules || []).filter(
(rule) =>
rule.enabled &&
rule.event === event &&
Number(rule.patternId) > 0,
rule.enabled && rule.event === event && Number(rule.patternId) > 0,
);
if (activeRules.length > 0) {
@ -297,7 +295,8 @@ export class SmsService {
if (!config.username || !config.password) {
return {
success: false,
message: 'مشخصات نام کاربری و رمز عبور سامانه ملی پیامک تنظیم نشده است.',
message:
'مشخصات نام کاربری و رمز عبور سامانه ملی پیامک تنظیم نشده است.',
};
}
@ -323,7 +322,8 @@ export class SmsService {
if (!phone) {
return {
success: false,
message: 'شماره گیرنده جهت ارسال تست یافت نشد. لطفاً یک شماره موبایل وارد کنید.',
message:
'شماره گیرنده جهت ارسال تست یافت نشد. لطفاً یک شماره موبایل وارد کنید.',
};
}
@ -496,7 +496,8 @@ export class SmsService {
const patterns: MeliPayamakPattern[] = [];
// MeliPayamak uses <ShareServiceBody> and child tags <BodyID>, <Title>, <Body>, <BodyStatus>
// We match both <ShareServiceBody> and <SharedServiceBody> for backwards/cross compatibility
const itemRegex = /<(?:Share|Shared)ServiceBody>([\s\S]*?)<\/(?:Share|Shared)ServiceBody>/gi;
const itemRegex =
/<(?:Share|Shared)ServiceBody>([\s\S]*?)<\/(?:Share|Shared)ServiceBody>/gi;
let match: RegExpExecArray | null;
while ((match = itemRegex.exec(xml)) !== null) {
@ -504,7 +505,9 @@ export class SmsService {
const idMatch = block.match(/<(?:BodyID|Id)>(\d+)<\/(?:BodyID|Id)>/i);
const titleMatch = block.match(/<Title>([\s\S]*?)<\/Title>/i);
const bodyMatch = block.match(/<Body>([\s\S]*?)<\/Body>/i);
const statusMatch = block.match(/<(?:BodyStatus|Status)>(-?\d+)<\/(?:BodyStatus|Status)>/i);
const statusMatch = block.match(
/<(?:BodyStatus|Status)>(-?\d+)<\/(?:BodyStatus|Status)>/i,
);
if (idMatch) {
const id = parseInt(idMatch[1], 10);
@ -616,7 +619,9 @@ export class SmsService {
const patterns = await this.getPatterns();
const target = patterns.find((p) => p.id === patternId);
if (!target || !target.body) {
return args.length > 0 ? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}` : `الگو ${patternId}`;
return args.length > 0
? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}`
: `الگو ${patternId}`;
}
let rendered = target.body;
@ -626,7 +631,9 @@ export class SmsService {
});
return rendered;
} catch {
return args.length > 0 ? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}` : `الگو ${patternId}`;
return args.length > 0
? `الگو ${patternId} با مقادیر: ${args.join(' ، ')}`
: `الگو ${patternId}`;
}
}
@ -761,19 +768,34 @@ export class SmsService {
}
// Check legacy default bindings
if (p.id === config.otpBodyId && !assigned.some((a) => a.includes('OTP'))) {
if (
p.id === config.otpBodyId &&
!assigned.some((a) => a.includes('OTP'))
) {
assigned.push('کد تایید OTP');
}
if (p.id === config.orderBodyId && !assigned.some((a) => a.includes('سفارش'))) {
if (
p.id === config.orderBodyId &&
!assigned.some((a) => a.includes('سفارش'))
) {
assigned.push('پترن پیش‌فرض سفارش');
}
if (p.id === config.shippingBodyId && !assigned.some((a) => a.includes('پست'))) {
if (
p.id === config.shippingBodyId &&
!assigned.some((a) => a.includes('پست'))
) {
assigned.push('پترن پیش‌فرض پست');
}
if (p.id === config.b2bBodyId && !assigned.some((a) => a.includes('B2B'))) {
if (
p.id === config.b2bBodyId &&
!assigned.some((a) => a.includes('B2B'))
) {
assigned.push('پترن پیش‌فرض B2B');
}
if (p.id === config.petCareBodyId && !assigned.some((a) => a.includes('پت'))) {
if (
p.id === config.petCareBodyId &&
!assigned.some((a) => a.includes('پت'))
) {
assigned.push('پترن پیش‌فرض پرونده پت');
}
@ -1029,43 +1051,62 @@ export class SmsService {
});
// Pre-render message text for logging
void this.renderPatternMessage(options.bodyId, options.args).then((renderedText) => {
const req = https.request(
'https://rest.payamak-panel.com/api/SendSMS/BaseServiceNumber',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
void this.renderPatternMessage(options.bodyId, options.args).then(
(renderedText) => {
const req = https.request(
'https://rest.payamak-panel.com/api/SendSMS/BaseServiceNumber',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
},
},
},
(res) => {
let data = '';
res.on('data', (chunk: Buffer | string) => (data += String(chunk)));
res.on('end', () => {
void (async () => {
try {
const json = JSON.parse(data) as MeliPayamakResponse;
const val = json.Value ?? 0;
if (json && (val > 15 || json.RetStatus === 1)) {
this.logger.log(
`[SMS Sent] Pattern SMS ${options.bodyId} dispatched to ${options.to} (RecId: ${val})`,
);
await this.recordLog({
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
messageText: renderedText,
status: 'SUCCESS',
recId: String(val),
});
resolve(true);
} else {
const errMsg = `خطای درگاه ملی پیامک با کد بازگشتی: ${val}`;
this.logger.error(
`[SMS Error] Failed sending to ${options.to}. Code: ${val}`,
);
(res) => {
let data = '';
res.on(
'data',
(chunk: Buffer | string) => (data += String(chunk)),
);
res.on('end', () => {
void (async () => {
try {
const json = JSON.parse(data) as MeliPayamakResponse;
const val = json.Value ?? 0;
if (json && (val > 15 || json.RetStatus === 1)) {
this.logger.log(
`[SMS Sent] Pattern SMS ${options.bodyId} dispatched to ${options.to} (RecId: ${val})`,
);
await this.recordLog({
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
messageText: renderedText,
status: 'SUCCESS',
recId: String(val),
});
resolve(true);
} else {
const errMsg = `خطای درگاه ملی پیامک با کد بازگشتی: ${val}`;
this.logger.error(
`[SMS Error] Failed sending to ${options.to}. Code: ${val}`,
);
await this.recordLog({
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
messageText: renderedText,
status: 'FAILED',
recId: String(val),
errorMessage: errMsg,
});
resolve(false);
}
} catch (err: unknown) {
const msg =
err instanceof Error ? err.message : String(err);
await this.recordLog({
receptor: options.to,
type: msgType,
@ -1073,50 +1114,37 @@ export class SmsService {
args: options.args,
messageText: renderedText,
status: 'FAILED',
recId: String(val),
errorMessage: errMsg,
errorMessage: `خطای پارس پاسخ سرور: ${data || msg}`,
});
resolve(false);
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
await this.recordLog({
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
messageText: renderedText,
status: 'FAILED',
errorMessage: `خطای پارس پاسخ سرور: ${data || msg}`,
});
resolve(false);
}
})();
});
},
);
})();
});
},
);
req.on('error', (err: Error) => {
void (async () => {
this.logger.error(
`[SMS Exception] MeliPayamak HTTP Error: ${err.message}`,
);
await this.recordLog({
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
messageText: renderedText,
status: 'FAILED',
errorMessage: `خطای شبکه: ${err.message}`,
});
resolve(false);
})();
});
req.on('error', (err: Error) => {
void (async () => {
this.logger.error(
`[SMS Exception] MeliPayamak HTTP Error: ${err.message}`,
);
await this.recordLog({
receptor: options.to,
type: msgType,
patternId: options.bodyId,
args: options.args,
messageText: renderedText,
status: 'FAILED',
errorMessage: `خطای شبکه: ${err.message}`,
});
resolve(false);
})();
});
req.write(payload);
req.end();
});
req.write(payload);
req.end();
},
);
});
}

View File

@ -257,11 +257,9 @@ export class OrdersService {
// Send Order Confirmation SMS for wallet orders
if (user.mobile) {
const orderData = this.buildOrderEventData(createdWalletOrder, user);
this.smsService
.triggerEvent('ORDER_PAID', orderData)
.catch((err) => {
this.logger.warn(`Failed to trigger wallet order SMS: ${err}`);
});
this.smsService.triggerEvent('ORDER_PAID', orderData).catch((err) => {
this.logger.warn(`Failed to trigger wallet order SMS: ${err}`);
});
}
return createdWalletOrder;
@ -324,29 +322,36 @@ export class OrdersService {
shipping = {};
}
}
// If not parsed as JSON or fields are missing, extract from formatted address string:
// Pattern: "استان، شهر، آدرس پستی (کد پستی: ۱۲۳۴۵۶۷۸۹۰) (گیرنده: نام گیرنده - ۰۹۱۲۰۰۰۰۰۰۰)"
if (!shipping.province || !shipping.address) {
let text = trimmed;
// 1. Extract recipient & phone: (گیرنده: نام گیرنده - ۰۹۱۲۰۰۰۰۰۰۰)
const recipientMatch = text.match(/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/);
const recipientMatch = text.match(
/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/,
);
if (recipientMatch) {
if (!shipping.fullName) shipping.fullName = recipientMatch[1]?.trim();
if (!shipping.phone && recipientMatch[2]) shipping.phone = recipientMatch[2]?.trim();
if (!shipping.phone && recipientMatch[2])
shipping.phone = recipientMatch[2]?.trim();
text = text.replace(recipientMatch[0], '').trim();
}
// 2. Extract postal code: (کد پستی: ۱۲۳۴۵۶۷۸۹۰)
const postalMatch = text.match(/\(کد پستی:\s*([^)]+)\)/);
if (postalMatch) {
if (!shipping.postalCode) shipping.postalCode = postalMatch[1]?.trim();
if (!shipping.postalCode)
shipping.postalCode = postalMatch[1]?.trim();
text = text.replace(postalMatch[0], '').trim();
}
// 3. Extract province and city: "استان، شهر، بقیه آدرس" or "استان - شهر - بقیه آدرس"
const parts = text.split(/[،,]/).map((p) => p.trim()).filter(Boolean);
const parts = text
.split(/[،,]/)
.map((p) => p.trim())
.filter(Boolean);
if (parts.length >= 3) {
if (!shipping.province) shipping.province = parts[0];
if (!shipping.city) shipping.city = parts[1];
@ -362,10 +367,7 @@ export class OrdersService {
const items = order.orderItems || [];
const itemsSummary = items
.map(
(i: any) =>
`${i.product?.nameFa || 'محصول'} (${i.quantity})`,
)
.map((i: any) => `${i.product?.nameFa || 'محصول'} (${i.quantity})`)
.join('، ');
const itemCount = items.reduce(
(sum: number, i: any) => sum + (i.quantity || 1),
@ -384,11 +386,7 @@ export class OrdersService {
customerFullName
).trim();
const recipientMobile = (
shipping.phone ||
user?.mobile ||
''
).trim();
const recipientMobile = (shipping.phone || user?.mobile || '').trim();
const provinceCity = (
shipping.province && shipping.city
@ -447,14 +445,17 @@ export class OrdersService {
const mobile = updatedOrder.user?.mobile;
const finalTracking = trackingCode || updatedOrder.trackingNumber || '';
if (status === 'shipped' && mobile) {
const orderData = this.buildOrderEventData(updatedOrder, updatedOrder.user);
const orderData = this.buildOrderEventData(
updatedOrder,
updatedOrder.user,
);
orderData.trackingCode = finalTracking || 'ارسال با پیک شهری';
orderData.shippingTrackingCode = finalTracking || 'ارسال با پیک شهری';
orderData.shippingMethod = finalTracking ? 'پست پیشتاز / تیپاکس' : 'پیک شهری';
orderData.shippingMethod = finalTracking
? 'پست پیشتاز / تیپاکس'
: 'پیک شهری';
this.smsService
.triggerEvent('ORDER_SHIPPED', orderData)
.catch(() => {});
this.smsService.triggerEvent('ORDER_SHIPPED', orderData).catch(() => {});
}
return updatedOrder;

View File

@ -251,9 +251,10 @@ export class PaymentController {
tryReverse?: boolean;
cardNumber?: string;
description?: string;
orderId?: string;
},
) {
return this.zibalService.requestRefund(body);
return this.paymentService.handleAdminGatewayRefund(body);
}
@UseGuards(JwtAuthGuard, RolesGuard)

View File

@ -511,22 +511,31 @@ export class PaymentService {
}
if (!shipping.province || !shipping.address) {
let text = trimmed;
const recipientMatch = text.match(/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/);
const recipientMatch = text.match(
/\(گیرنده:\s*([^-)]+)(?:-\s*([^)]+))?\)/,
);
if (recipientMatch) {
if (!shipping.fullName) shipping.fullName = recipientMatch[1]?.trim();
if (!shipping.phone && recipientMatch[2]) shipping.phone = recipientMatch[2]?.trim();
if (!shipping.fullName)
shipping.fullName = recipientMatch[1]?.trim();
if (!shipping.phone && recipientMatch[2])
shipping.phone = recipientMatch[2]?.trim();
text = text.replace(recipientMatch[0], '').trim();
}
const postalMatch = text.match(/\(کد پستی:\s*([^)]+)\)/);
if (postalMatch) {
if (!shipping.postalCode) shipping.postalCode = postalMatch[1]?.trim();
if (!shipping.postalCode)
shipping.postalCode = postalMatch[1]?.trim();
text = text.replace(postalMatch[0], '').trim();
}
const parts = text.split(/[،,]/).map((p) => p.trim()).filter(Boolean);
const parts = text
.split(/[،,]/)
.map((p) => p.trim())
.filter(Boolean);
if (parts.length >= 3) {
if (!shipping.province) shipping.province = parts[0];
if (!shipping.city) shipping.city = parts[1];
if (!shipping.address) shipping.address = parts.slice(2).join('، ');
if (!shipping.address)
shipping.address = parts.slice(2).join('، ');
} else if (parts.length === 2) {
if (!shipping.province) shipping.province = parts[0];
if (!shipping.address) shipping.address = parts[1];
@ -538,10 +547,7 @@ export class PaymentService {
const items = order?.orderItems || [];
const itemsSummary = items
.map(
(i: any) =>
`${i.product?.nameFa || 'محصول'} (${i.quantity})`,
)
.map((i: any) => `${i.product?.nameFa || 'محصول'} (${i.quantity})`)
.join('، ');
const itemCount = items.reduce(
(sum: number, i: any) => sum + (i.quantity || 1),
@ -562,11 +568,7 @@ export class PaymentService {
customerName
).trim();
const recipientMobile = (
shipping.phone ||
customerMobile ||
''
).trim();
const recipientMobile = (shipping.phone || customerMobile || '').trim();
const provinceCity = (
shipping.province && shipping.city
@ -601,11 +603,9 @@ export class PaymentService {
orderDate: new Date().toLocaleDateString('fa-IR'),
};
this.smsService
.triggerEvent('ORDER_PAID', orderData)
.catch((err) => {
this.logger.warn(`Could not trigger SMS event ORDER_PAID: ${err}`);
});
this.smsService.triggerEvent('ORDER_PAID', orderData).catch((err) => {
this.logger.warn(`Could not trigger SMS event ORDER_PAID: ${err}`);
});
}
// Send SMS notification if wallet top-up
@ -632,7 +632,9 @@ export class PaymentService {
this.smsService
.triggerEvent('WALLET_CHARGED', topupData)
.catch((err) => {
this.logger.warn(`Could not trigger SMS event WALLET_CHARGED: ${err}`);
this.logger.warn(
`Could not trigger SMS event WALLET_CHARGED: ${err}`,
);
});
}
} catch (err) {
@ -1130,7 +1132,9 @@ export class PaymentService {
this.smsService
.triggerEvent('WALLET_CHARGED', topupData)
.catch((err) => {
this.logger.warn(`Could not trigger SMS event WALLET_CHARGED: ${err}`);
this.logger.warn(
`Could not trigger SMS event WALLET_CHARGED: ${err}`,
);
});
}
} catch (err) {
@ -1176,4 +1180,99 @@ export class PaymentService {
async checkGatewayHealth() {
return this.zibalService.checkHealth();
}
/**
* 12. Admin: Process Gateway Refund (Zibal) with Wallet Balance Deduction if previously refunded to wallet
*/
async handleAdminGatewayRefund(options: {
trackId: string | number;
amount?: number; // Rial
tryReverse?: boolean;
cardNumber?: string;
description?: string;
orderId?: string;
}) {
// 1. If orderId is not passed, attempt to find by trackId
let order = options.orderId
? await this.prisma.order.findUnique({
where: { id: options.orderId },
include: { user: true },
})
: null;
if (!order && options.trackId) {
const tx = await this.prisma.paymentTransaction.findUnique({
where: { trackId: String(options.trackId) },
include: { order: { include: { user: true } } },
});
if (tx?.order) {
order = tx.order;
}
}
if (order && order.refundStatus === 'gateway') {
throw new BadRequestException(
'برای این سفارش قبلاً استرداد درگاه بانکی انجام شده است',
);
}
// 2. Execute refund via Zibal
const zibalRes = await this.zibalService.requestRefund({
trackId: options.trackId,
amount: options.amount,
tryReverse: options.tryReverse,
cardNumber: options.cardNumber,
description: options.description,
});
const isSuccess =
zibalRes?.result === 1 ||
zibalRes?.status === 1 ||
zibalRes?.result === 100 ||
zibalRes?.success === true;
if (isSuccess && order) {
const refundAmountTomans = options.amount
? Math.round(Number(options.amount) / 10)
: Number(order.totalAmount || 0);
await this.prisma.$transaction(async (tx) => {
// If order was previously refunded to wallet, deduct that amount from user's wallet!
if (order.refundStatus === 'wallet' && order.userId) {
const deductionAmount = Number(
order.refundedAmount || refundAmountTomans,
);
await tx.user.update({
where: { id: order.userId },
data: {
walletBalance: { decrement: deductionAmount },
},
});
await tx.walletTransaction.create({
data: {
userId: order.userId,
amount: deductionAmount,
type: 'withdraw',
status: 'completed',
description: `کسر موجودی کیف پول به علت استرداد بانکی سفارش #${order.trackingNumber || order.id.slice(0, 8)} از طریق زیبال`,
},
});
}
// Update order refund status to gateway
await tx.order.update({
where: { id: order.id },
data: {
status: 'cancelled',
refundStatus: 'gateway',
refundedAmount: refundAmountTomans,
refundMethod: 'zibal',
},
});
});
}
return zibalRes;
}
}

View File

@ -223,7 +223,10 @@ export class SettingsController {
@Roles('Admin')
@ApiBearerAuth()
@Get('sms/events')
@ApiOperation({ summary: 'دریافت لیست تمامی رویدادهای سیستم و متغیرهای داینامیک قابل استفاده در پترن‌ها' })
@ApiOperation({
summary:
'دریافت لیست تمامی رویدادهای سیستم و متغیرهای داینامیک قابل استفاده در پترن‌ها',
})
getSmsEvents() {
return this.settingsService.getSmsEvents();
}
@ -232,11 +235,10 @@ export class SettingsController {
@Roles('Admin')
@ApiBearerAuth()
@Post('sms/test-rule')
@ApiOperation({ summary: 'تست ارسال پیامک برای یک سناریوی تعریف‌شده با دیتای تستی' })
testSmsRule(
@Body('rule') rule: any,
@Body('testPhone') testPhone?: string,
) {
@ApiOperation({
summary: 'تست ارسال پیامک برای یک سناریوی تعریف‌شده با دیتای تستی',
})
testSmsRule(@Body('rule') rule: any, @Body('testPhone') testPhone?: string) {
return this.settingsService.testSmsRule(rule, testPhone);
}

View File

@ -608,8 +608,12 @@ export class SettingsService implements OnModuleInit {
updates['ogImageUrl'] = String(obj.ogImageUrl);
}
if ('googleSiteVerification' in obj) {
updates['googleSiteVerification'] = String(obj.googleSiteVerification ?? '');
updates['google_site_verification'] = String(obj.googleSiteVerification ?? '');
updates['googleSiteVerification'] = String(
obj.googleSiteVerification ?? '',
);
updates['google_site_verification'] = String(
obj.googleSiteVerification ?? '',
);
}
if ('googleAnalyticsId' in obj) {
updates['googleAnalyticsId'] = String(obj.googleAnalyticsId ?? '');
@ -620,10 +624,14 @@ export class SettingsService implements OnModuleInit {
updates['gtm_id'] = String(obj.googleTagManagerId ?? '');
}
if ('enableGoogleAnalytics' in obj) {
updates['enableGoogleAnalytics'] = String(Boolean(obj.enableGoogleAnalytics));
updates['enableGoogleAnalytics'] = String(
Boolean(obj.enableGoogleAnalytics),
);
}
if ('enableGoogleTagManager' in obj) {
updates['enableGoogleTagManager'] = String(Boolean(obj.enableGoogleTagManager));
updates['enableGoogleTagManager'] = String(
Boolean(obj.enableGoogleTagManager),
);
}
if ('clarityProjectId' in obj) {
updates['clarityProjectId'] = String(obj.clarityProjectId ?? '');
@ -659,7 +667,7 @@ export class SettingsService implements OnModuleInit {
return this.smsService.testSms(targetPhone, patternId, args);
}
async getSmsEvents() {
getSmsEvents() {
return this.smsService.getEventDefinitions();
}

View File

@ -309,12 +309,16 @@ export class UsersService {
customerName: customerFullName,
customerPhone: updatedUser.mobile,
amount: Number(amount || 0).toLocaleString('fa-IR'),
balance: Number(updatedUser.walletBalance || 0).toLocaleString('fa-IR'),
balance: Number(updatedUser.walletBalance || 0).toLocaleString(
'fa-IR',
),
reason: 'افزایش موجودی کیف پول',
date: new Date().toLocaleDateString('fa-IR'),
};
this.smsService.triggerEvent('WALLET_CHARGED', topupData).catch(() => {});
this.smsService
.triggerEvent('WALLET_CHARGED', topupData)
.catch(() => {});
} catch (err) {
// Silently catch SMS error
}

View File

@ -102,6 +102,9 @@ export interface Order {
total?: number;
isRefill?: boolean;
shippingAddress?: string;
refundStatus?: string;
refundedAmount?: number;
refundMethod?: string;
}
export const ORDER_STATUS_MAP: Record<string, { label: string; color: string; icon: React.ElementType }> = {
@ -242,8 +245,12 @@ export default function Orders() {
setRefundModalOrder(order);
const amt = Number(order.totalAmount || order.total || 0);
setRefundAmount(String(amt));
// If order was paid by wallet, default to wallet. If online, let admin choose.
setRefundTarget(order.paymentMethod === 'wallet' ? 'wallet' : 'zibal');
// If order was already refunded to wallet, default/force to gateway
if (order.refundStatus === 'wallet') {
setRefundTarget('zibal');
} else {
setRefundTarget(order.paymentMethod === 'wallet' ? 'wallet' : 'zibal');
}
setRefundReason(`استرداد سفارش #${order.trackingNumber || order.id.slice(0, 8)}`);
updateUrlParams({ modal: 'refund', orderId: order.id });
};
@ -440,16 +447,15 @@ export default function Orders() {
}
const res = await api.post('/payment/admin/refund', {
orderId: refundModalOrder.id,
trackId,
amount: refundAmount ? Number(refundAmount) * 10 : undefined,
tryReverse: true,
description: refundReason || `استرداد وجه سفارش #${refundModalOrder.trackingNumber || refundModalOrder.id.slice(0, 8)}`,
});
if (res.data?.result === 1 || res.data?.status === 1 || res.data?.success) {
toast.success('درخواست استرداد وجه به درگاه زیبال با موفقیت ارسال شد.');
// Also mark order as cancelled
await api.put(`/admin/orders/${refundModalOrder.id}/status`, { status: 'cancelled' });
if (res.data?.result === 1 || res.data?.status === 1 || res.data?.result === 100 || res.data?.success) {
toast.success('درخواست استرداد وجه به درگاه زیبال با موفقیت ارسال شد و وضعیت سفارش به‌روزرسانی شد.');
setRefundModalOrder(null);
await fetchOrders();
} else {
@ -790,6 +796,22 @@ export default function Orders() {
<CreditCard className="w-3 h-3 shrink-0" />
<span className="font-vazir">{payInfo.label}</span>
</span>
{order.refundStatus && (
<span
className={`text-[10px] font-bold font-vazir px-2 py-0.5 rounded-md border flex items-center gap-1 shadow-sm ${
order.refundStatus === 'gateway'
? 'bg-rose-50 text-rose-700 border-rose-200'
: 'bg-amber-50 text-amber-700 border-amber-200'
}`}
>
<Undo2 className="w-3 h-3 shrink-0" />
<span className="font-vazir">
{order.refundStatus === 'gateway'
? 'مسترد به درگاه'
: 'مسترد به کیف‌پول'}
</span>
</span>
)}
{order.isRefill && (
<span className="text-[10px] font-bold font-vazir px-2 py-0.5 rounded-md bg-emerald-50 text-emerald-700 border border-emerald-200 shadow-sm">
تمدید ۵٪-
@ -855,16 +877,16 @@ export default function Orders() {
className="text-gray-600 hover:bg-gray-100"
/>
{(order.status === 'cancelled' || order.status === 'pending_payment') && (
{(order.status === 'cancelled' || order.status === 'pending_payment') && order.refundStatus !== 'gateway' && (
<Button
variant="ghost"
size="xs"
startIcon={Undo2}
onClick={() => openOrderRefundModal(order)}
disabled={isProcessing}
className="text-rose-600 hover:bg-rose-50"
className={`hover:bg-rose-50 ${order.refundStatus === 'wallet' ? 'text-amber-700 hover:bg-amber-50' : 'text-rose-600'}`}
>
استرداد
{order.refundStatus === 'wallet' ? 'استرداد بانکی' : 'استرداد'}
</Button>
)}
</div>
@ -1018,17 +1040,15 @@ export default function Orders() {
{(() => {
let addrText = selectedOrder.address?.fullAddress || selectedOrder.shippingAddress || 'ثبت نشده';
let postCode = selectedOrder.address?.postalCode;
let prov = selectedOrder.address?.province;
let ct = selectedOrder.address?.city;
if (typeof selectedOrder.shippingAddress === 'string' && selectedOrder.shippingAddress.trim().startsWith('{')) {
try {
const parsed = JSON.parse(selectedOrder.shippingAddress);
addrText = parsed.formatted || `${parsed.province || ''}، ${parsed.city || ''}، ${parsed.address || ''}`;
if (parsed.postalCode) postCode = parsed.postalCode;
if (parsed.province) prov = parsed.province;
if (parsed.city) ct = parsed.city;
} catch {}
} catch {
// ignore malformed json
}
}
return (
@ -1115,6 +1135,16 @@ export default function Orders() {
<span>روش پرداخت:</span>
<span className="text-white font-bold">{selectedOrder.paymentMethod === 'wallet' ? 'کیف پول الکترونیک' : 'درگاه پرداخت آنلاین'}</span>
</div>
{selectedOrder.refundStatus && (
<div className="flex justify-between font-bold text-amber-300 pt-1 border-t border-gray-800/80">
<span>وضعیت استرداد وجه:</span>
<span>
{selectedOrder.refundStatus === 'gateway'
? `مسترد به درگاه بانکی (${toPersianDigits(Number(selectedOrder.refundedAmount || selectedOrder.totalAmount || 0).toLocaleString())} تومان)`
: `مسترد به کیف‌پول (${toPersianDigits(Number(selectedOrder.refundedAmount || selectedOrder.totalAmount || 0).toLocaleString())} تومان)`}
</span>
</div>
)}
<div className="flex justify-between text-sm sm:text-base font-black text-amber-400 pt-2 border-t border-gray-800">
<span>مبلغ نهایی پرداخت شده:</span>
<span>{toPersianDigits(Number(selectedOrder.totalAmount || selectedOrder.total || 0).toLocaleString())} تومان</span>
@ -1157,14 +1187,22 @@ export default function Orders() {
{/* Destination selector */}
<div>
<label className="block font-bold text-gray-700 mb-2">روش عودت و بازگشت وجه:</label>
{refundModalOrder.refundStatus === 'wallet' && (
<div className="mb-3 p-3 bg-amber-50 border border-amber-200 rounded-xl text-amber-800 text-[11px] leading-relaxed">
<strong>توجه:</strong> وجه این سفارش قبلاً به کیف پول کاربر عودت داده شده است. در صورت ثبت استرداد درگاه زیبال، مبلغ استرداد به صورت خودکار از موجودی کیف پول کاربر کسر خواهد شد تا از استرداد مضاعف جلوگیری شود.
</div>
)}
<div className="grid grid-cols-2 gap-3">
<button
type="button"
disabled={refundModalOrder.refundStatus === 'wallet'}
onClick={() => setRefundTarget('wallet')}
className={`p-3.5 rounded-2xl border text-right transition-all cursor-pointer flex flex-col gap-1.5 ${
refundTarget === 'wallet'
? 'border-purple-600 bg-purple-50/60 ring-2 ring-purple-600/20'
: 'border-gray-200 hover:border-gray-300'
className={`p-3.5 rounded-2xl border text-right transition-all flex flex-col gap-1.5 ${
refundModalOrder.refundStatus === 'wallet'
? 'opacity-40 cursor-not-allowed bg-gray-50 border-gray-200'
: refundTarget === 'wallet'
? 'border-purple-600 bg-purple-50/60 ring-2 ring-purple-600/20 cursor-pointer'
: 'border-gray-200 hover:border-gray-300 cursor-pointer'
}`}
>
<div className="flex items-center gap-2 font-black text-gray-900">
@ -1172,7 +1210,9 @@ export default function Orders() {
<span>شارژ کیف پول کاربر</span>
</div>
<p className="text-[11px] text-gray-500 font-medium leading-relaxed">
افزایش فوری اعتبار کیف پول جهت خریدهای بعدی مشتری
{refundModalOrder.refundStatus === 'wallet'
? 'قبلاً به کیف پول مسترد شده است'
: 'افزایش فوری اعتبار کیف پول جهت خریدهای بعدی مشتری'}
</p>
</button>

File diff suppressed because it is too large Load Diff

View File

@ -59,8 +59,8 @@ export default function AnalyticsDeferred({ gtmId, gaId, clarityId }: AnalyticsD
(function (c: any, l: any, a: any, r: any, i: any) {
c[a] =
c[a] ||
function () {
(c[a].q = c[a].q || []).push(arguments);
function (...args: unknown[]) {
(c[a].q = c[a].q || []).push(args);
};
const t = l.createElement(r);
t.async = 1;

View File

@ -386,11 +386,11 @@ export default function OrderDetailsModal({
)}
{/* Content Scrollable */}
<div className="max-h-[60vh] overflow-y-auto px-6 sm:px-10 py-6 space-y-6 custom-scrollbar">
<div className="max-h-[65vh] overflow-y-auto px-6 sm:px-10 py-6 space-y-6 custom-scrollbar">
{/* Summary Stats */}
<div className="grid grid-cols-3 gap-3 sm:gap-4">
<div className="bg-medical-gray-50 p-4 rounded-2xl">
<div className="text-[9px] font-black text-medical-gray-400 uppercase tracking-widest mb-1">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
<div className="bg-medical-gray-50 p-4 rounded-2xl border border-medical-gray-100">
<div className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-1.5">
وضعیت سفارش
</div>
<div className="flex items-center gap-2">
@ -398,155 +398,178 @@ export default function OrderDetailsModal({
<span className="text-xs sm:text-sm font-black text-medical-gray-900">
{getStatusLabel(orderStatus)}
</span>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<ShoppingBag className="w-4 h-4 text-canina-blue" />
سبد اقلام سفارش
</h4>
<div className="space-y-3">
{items.map((item: Record<string, unknown>, idx: number) => {
const prod = item.product as Record<string, unknown> | undefined;
const pName = getProductName(prod);
const pImg = getProductImage(prod);
const pPrice = Number(prod?.priceValue || item.priceValue || 0);
const itemQty = Number(item.quantity || 1);
const pSlug = String(prod?.slug || prod?.id || '');
return (
<div
key={String(prod?.id || idx)}
className="flex items-center justify-between p-4 border border-medical-gray-100 rounded-2xl hover:border-canina-blue transition-all"
>
<div className="flex items-center gap-4">
<div
onClick={() => setZoomedImage(pImg)}
className="w-14 h-14 bg-medical-gray-50 rounded-xl p-1 flex items-center justify-center shrink-0 border border-medical-gray-200 cursor-zoom-in hover:scale-105 transition-transform overflow-hidden"
title="مشاهده تصویر بزرگ"
>
<SafeImage
src={pImg}
alt={pName}
className="w-full h-full"
imgClassName="w-full h-full object-contain"
/>
</div>
<div>
{pSlug ? (
<Link
href={`/shop/${pSlug}`}
onClick={onClose}
className="font-black text-medical-gray-900 hover:text-canina-blue transition-colors text-xs sm:text-sm inline-block font-vazir"
>
{pName}
</Link>
) : (
<h5 className="font-black text-medical-gray-900 text-xs sm:text-sm font-vazir">
{pName}
</h5>
)}
<p className="text-xs text-medical-gray-400 mt-0.5 font-vazir">
{toPersian(itemQty)} عدد × {toPersian(pPrice.toLocaleString())} تومان
</p>
</div>
</div>
<div className="text-left font-black text-canina-blue text-xs sm:text-sm font-vazir">
{toPersian((pPrice * itemQty).toLocaleString())} تومان
</div>
</div>
);
})}
</div>
</div>
{/* Shipping & Payment */}
<div className="grid md:grid-cols-2 gap-6 pt-4 border-t border-medical-gray-100">
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<MapPin className="w-4 h-4 text-canina-blue" />
اطلاعات تحویل و آدرس
</h4>
<div className="bg-medical-gray-50 p-4 rounded-2xl space-y-2 text-xs font-bold text-medical-gray-700">
<div>
<span className="text-medical-gray-400">گیرنده: </span>
{String(shippingAddress.recipientName || shippingAddress.name || 'کاربر')}
</div>
<div>
<span className="text-medical-gray-400">شماره تماس: </span>
<span className="font-mono">{toPersian(String(shippingAddress.phone || shippingAddress.mobile || '-'))}</span>
</div>
<div>
<span className="text-medical-gray-400">نشانی کامل: </span>
{String(shippingAddress.fullAddress || shippingAddress.address || '-')}
</div>
{Boolean(shippingAddress.postalCode) && (
<div>
<span className="text-medical-gray-400">کد پستی: </span>
<span className="font-mono">{toPersian(String(shippingAddress.postalCode))}</span>
</div>
)}
</div>
</div>
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<CreditCard className="w-4 h-4 text-canina-blue" />
اطلاعات و وضعیت پرداخت
</h4>
<div className="bg-medical-gray-50 p-4 rounded-2xl space-y-2.5 text-xs font-bold text-medical-gray-700">
<div className="flex justify-between items-center">
<span className="text-medical-gray-400">شیوه پرداخت:</span>
<span className="text-medical-gray-900">{getPaymentMethodTitle(order.paymentMethod)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-medical-gray-400">مجموع اقلام:</span>
<span>{toPersian(itemsSubtotal.toLocaleString())} تومان</span>
</div>
{charityAmount > 0 && (
<div className="flex justify-between items-center text-pink-500">
<span>ردپای مهربانی:</span>
<span>{toPersian(charityAmount.toLocaleString())}+ تومان</span>
</div>
)}
<div className="flex justify-between items-center">
<span className="text-medical-gray-400">هزینه ارسال:</span>
<span className="text-green-600">رایگان</span>
</div>
<div className="pt-2 border-t border-medical-gray-200/60 flex justify-between items-center text-sm font-black text-medical-gray-900">
<span>مبلغ نهایی فاکتور:</span>
<span className="text-canina-blue">{toPersian(totalAmount.toLocaleString())} تومان</span>
</div>
</div>
</div>
</div>
</div>
{/* Footer Actions */}
<div className="p-6 sm:px-10 bg-medical-gray-50 border-t border-medical-gray-100 flex flex-col sm:flex-row items-center justify-between gap-4">
<button
onClick={handlePrint}
className="w-full sm:w-auto px-5 py-2.5 bg-white border border-medical-gray-200 text-medical-gray-700 rounded-xl text-xs font-black hover:border-canina-blue hover:text-canina-blue transition-all flex items-center justify-center gap-2 shadow-xs cursor-pointer"
>
<Download className="w-4 h-4" />
<span>چاپ فاکتور رسمی</span>
</button>
<div className="bg-medical-gray-50 p-4 rounded-2xl border border-medical-gray-100">
<div className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-1.5">
کد رهگیری سفارش
</div>
<div className="text-xs sm:text-sm font-mono font-black text-canina-blue">
{toPersian(displayTrackingCode)}
</div>
</div>
<div className="flex items-center gap-3 w-full sm:w-auto">
{isPendingPayment && (
<button
onClick={() => setRetryModalOpen(true)}
className="w-full sm:w-auto px-6 py-2.5 bg-green-600 hover:bg-green-700 text-white rounded-xl text-xs font-black shadow-lg shadow-green-600/20 transition-all flex items-center justify-center gap-2 cursor-pointer"
<div className="bg-medical-gray-50 p-4 rounded-2xl border border-medical-gray-100">
<div className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-1.5">
زمان ثبت سفارش
</div>
<div className="flex items-center gap-1.5 text-xs sm:text-sm font-black text-medical-gray-900">
<Clock className="w-4 h-4 text-medical-gray-400" />
<span>{toPersian(formattedTime)} - {toPersian(formattedDate)}</span>
</div>
</div>
</div>
{/* Items Section */}
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<ShoppingBag className="w-4 h-4 text-canina-blue" />
سبد اقلام سفارش
</h4>
<div className="space-y-3">
{items.map((item: Record<string, unknown>, idx: number) => {
const prod = item.product as Record<string, unknown> | undefined;
const pName = getProductName(prod);
const pImg = getProductImage(prod);
const pPrice = Number(prod?.priceValue || item.priceValue || 0);
const itemQty = Number(item.quantity || 1);
const pSlug = String(prod?.slug || prod?.id || '');
return (
<div
key={String(prod?.id || idx)}
className="flex items-center justify-between p-4 border border-medical-gray-100 rounded-2xl hover:border-canina-blue transition-all"
>
<CreditCard className="w-4 h-4" />
<span>پرداخت سفارش</span>
</button>
<div className="flex items-center gap-4">
<div
onClick={() => setZoomedImage(pImg)}
className="w-14 h-14 bg-medical-gray-50 rounded-xl p-1 flex items-center justify-center shrink-0 border border-medical-gray-200 cursor-zoom-in hover:scale-105 transition-transform overflow-hidden"
title="مشاهده تصویر بزرگ"
>
<SafeImage
src={pImg}
alt={pName}
className="w-full h-full"
imgClassName="w-full h-full object-contain"
/>
</div>
<div>
{pSlug ? (
<Link
href={`/shop/${pSlug}`}
onClick={onClose}
className="font-black text-medical-gray-900 hover:text-canina-blue transition-colors text-xs sm:text-sm inline-block font-vazir"
>
{pName}
</Link>
) : (
<h5 className="font-black text-medical-gray-900 text-xs sm:text-sm font-vazir">
{pName}
</h5>
)}
<p className="text-xs text-medical-gray-400 mt-0.5 font-vazir">
{toPersian(itemQty)} عدد × {toPersian(pPrice.toLocaleString())} تومان
</p>
</div>
</div>
<div className="text-left font-black text-canina-blue text-xs sm:text-sm font-vazir">
{toPersian((pPrice * itemQty).toLocaleString())} تومان
</div>
</div>
);
})}
</div>
</div>
{/* Shipping & Payment */}
<div className="grid md:grid-cols-2 gap-6 pt-4 border-t border-medical-gray-100">
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<MapPin className="w-4 h-4 text-canina-blue" />
اطلاعات تحویل و آدرس
</h4>
<div className="bg-medical-gray-50 p-4 rounded-2xl space-y-2 text-xs font-bold text-medical-gray-700">
<div>
<span className="text-medical-gray-400">گیرنده: </span>
{String(shippingAddress.recipientName || shippingAddress.name || 'کاربر')}
</div>
<div>
<span className="text-medical-gray-400">شماره تماس: </span>
<span className="font-mono">{toPersian(String(shippingAddress.phone || shippingAddress.mobile || '-'))}</span>
</div>
<div>
<span className="text-medical-gray-400">نشانی کامل: </span>
{String(shippingAddress.fullAddress || shippingAddress.address || '-')}
</div>
{Boolean(shippingAddress.postalCode) && (
<div>
<span className="text-medical-gray-400">کد پستی: </span>
<span className="font-mono">{toPersian(String(shippingAddress.postalCode))}</span>
</div>
)}
<button
onClick={onClose}
className="w-full sm:w-auto px-6 py-2.5 bg-medical-gray-900 hover:bg-canina-blue text-white rounded-xl text-xs font-black transition-all cursor-pointer flex items-center justify-center gap-1.5"
>
<span>بازگشت</span>
<X className="w-4 h-4" />
</button>
</div>
</div>
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<CreditCard className="w-4 h-4 text-canina-blue" />
اطلاعات و وضعیت پرداخت
</h4>
<div className="bg-medical-gray-50 p-4 rounded-2xl space-y-2.5 text-xs font-bold text-medical-gray-700">
<div className="flex justify-between items-center">
<span className="text-medical-gray-400">شیوه پرداخت:</span>
<span className="text-medical-gray-900">{getPaymentMethodTitle(order.paymentMethod)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-medical-gray-400">مجموع اقلام:</span>
<span>{toPersian(itemsSubtotal.toLocaleString())} تومان</span>
</div>
{charityAmount > 0 && (
<div className="flex justify-between items-center text-pink-500">
<span>ردپای مهربانی:</span>
<span>{toPersian(charityAmount.toLocaleString())}+ تومان</span>
</div>
)}
<div className="flex justify-between items-center">
<span className="text-medical-gray-400">هزینه ارسال:</span>
<span className="text-green-600">رایگان</span>
</div>
<div className="pt-2 border-t border-medical-gray-200/60 flex justify-between items-center text-sm font-black text-medical-gray-900">
<span>مبلغ نهایی فاکتور:</span>
<span className="text-canina-blue">{toPersian(totalAmount.toLocaleString())} تومان</span>
</div>
</div>
</div>
</div>
</div>
{/* Footer Actions */}
<div className="p-6 sm:px-10 bg-medical-gray-50 border-t border-medical-gray-100 flex flex-col sm:flex-row items-center justify-between gap-4">
<button
onClick={handlePrint}
className="w-full sm:w-auto px-5 py-2.5 bg-white border border-medical-gray-200 text-medical-gray-700 rounded-xl text-xs font-black hover:border-canina-blue hover:text-canina-blue transition-all flex items-center justify-center gap-2 shadow-xs cursor-pointer"
>
<Download className="w-4 h-4" />
<span>چاپ فاکتور رسمی</span>
</button>
<div className="flex items-center gap-3 w-full sm:w-auto">
{isPendingPayment && (
<button
onClick={() => setRetryModalOpen(true)}
className="w-full sm:w-auto px-6 py-2.5 bg-green-600 hover:bg-green-700 text-white rounded-xl text-xs font-black shadow-lg shadow-green-600/20 transition-all flex items-center justify-center gap-2 cursor-pointer"
>
<CreditCard className="w-4 h-4" />
<span>پرداخت سفارش</span>
</button>
)}
<button
onClick={onClose}
className="w-full sm:w-auto px-6 py-2.5 bg-medical-gray-900 hover:bg-canina-blue text-white rounded-xl text-xs font-black transition-all cursor-pointer flex items-center justify-center gap-1.5"
>
<span>بازگشت</span>
<X className="w-4 h-4" />
</button>
</div>
</div>

View File

@ -55,6 +55,16 @@ export default function ProductImageZoomModal({
};
}, [isOpen, activeImage, resetZoom]);
const navigateImage = useCallback((direction: number) => {
if (!images || images.length <= 1) return;
const currentIndex = images.indexOf(activeImage);
if (currentIndex === -1) return;
let nextIndex = (currentIndex + direction) % images.length;
if (nextIndex < 0) nextIndex = images.length - 1;
onSelectImage(images[nextIndex]);
resetZoom();
}, [images, activeImage, onSelectImage, resetZoom]);
// Keyboard navigation & zoom shortcuts
useEffect(() => {
if (!isOpen) return;
@ -81,7 +91,7 @@ export default function ProductImageZoomModal({
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose, images, activeImage, resetZoom]);
}, [isOpen, onClose, navigateImage, resetZoom]);
const handleZoomIn = () => {
setScale((prev) => Math.min(prev + 0.5, 4));
@ -158,15 +168,7 @@ export default function ProductImageZoomModal({
}
};
const navigateImage = (direction: number) => {
if (!images || images.length <= 1) return;
const currentIndex = images.indexOf(activeImage);
if (currentIndex === -1) return;
let nextIndex = (currentIndex + direction) % images.length;
if (nextIndex < 0) nextIndex = images.length - 1;
onSelectImage(images[nextIndex]);
resetZoom();
};
if (!isOpen) return null;

View File

@ -1,7 +1,7 @@
"use client";
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "motion/react";
import { User, ShoppingBag, Wallet, MapPin, LogOut, ChevronRight, Package, Calendar, UserCircle, ShoppingCart, Trash2, Edit2, Phone, Hash, CheckCircle2, ArrowUpCircle, ArrowDownCircle, Info, Clock, CheckCircle, Heart, Sparkles, MessageSquare, Send, Stethoscope, FileText, Lock, Key, Eye, EyeOff, ShieldCheck, Dog, X, Plus, CreditCard, Activity } from "lucide-react";
import { User, ShoppingBag, Wallet, MapPin, LogOut, ChevronRight, Package, Calendar, UserCircle, ShoppingCart, Trash2, Edit2, Phone, Hash, CheckCircle2, ArrowUpCircle, ArrowDownCircle, Info, Clock, CheckCircle, Heart, Sparkles, MessageSquare, Send, Stethoscope, FileText, Lock, Key, Eye, EyeOff, ShieldCheck, Dog, X, Plus, CreditCard, Activity, RotateCcw } from "lucide-react";
import { OrderRowSkeleton } from "./Skeleton";
import { useUserStore, Address } from "../lib/store/userStore";
import { useCartStore } from "../lib/store/cartStore";
@ -987,37 +987,104 @@ export default function UserDashboard() {
<p className="text-medical-gray-400 font-bold">تراکنشی یافت نشد.</p>
</div>
) : (
(profile?.transactions || []).map((trx) => (
<div key={trx.id} className="p-4 sm:p-6 bg-white border border-medical-gray-100 rounded-[1.5rem] sm:rounded-[2rem] flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-6 hover:shadow-lg hover:border-canina-blue/10 transition-all group">
<div className="flex items-center gap-3 sm:gap-4">
<div className={cn(
"w-10 h-10 sm:w-12 sm:h-12 rounded-xl sm:rounded-2xl flex items-center justify-center transition-colors flex-shrink-0",
trx.type === 'top_up' ? "bg-green-50 text-green-500 group-hover:bg-green-500 group-hover:text-white" : "bg-red-50 text-red-500 group-hover:bg-red-500 group-hover:text-white"
)}>
{trx.type === 'top_up' ? <ArrowUpCircle className="w-5 h-5 sm:w-6 sm:h-6" /> : <ShoppingCart className="w-5 h-5 sm:w-6 sm:h-6" />}
</div>
<div className="min-w-0">
<div className="text-xs sm:text-sm font-black text-medical-gray-900 truncate">{trx.type === 'top_up' ? "افزایش موجودی (شارژ)" : "پرداخت سفارش"}</div>
<div className="text-[10px] font-bold text-medical-gray-400 flex flex-wrap items-center gap-1 sm:gap-2 mt-0.5">
<span className="truncate">کد: {trx.id}</span>
<span className="w-1 h-1 bg-medical-gray-200 rounded-full hidden sm:inline-block" />
<span>{toPersian(new Date(trx.date).toLocaleDateString("fa-IR"))}</span>
</div>
</div>
</div>
<div className="text-right sm:text-left flex sm:flex-col justify-between items-center sm:items-end border-t sm:border-t-0 pt-2 sm:pt-0 border-medical-gray-50">
<div className={cn(
"text-base sm:text-lg font-black italic",
trx.type === 'top_up' ? "text-green-600" : "text-red-500"
)}>
{trx.type === 'top_up' ? "+" : ""}{toPersian(trx.amount.toLocaleString())} <span className="text-xs not-italic">تومان</span>
</div>
<div className="text-[9px] font-black uppercase text-green-500 tracking-widest sm:mt-1">
تایید شده
</div>
</div>
</div>
))
(profile?.transactions || []).map((trx) => {
const desc = trx.description || "";
const isRefund = trx.type === 'refund' || desc.includes('استرداد') || desc.includes('لغو') || desc.includes('عودت');
const isAdmin = desc.includes('مدیر') || desc.includes('دستی');
const isOnlineTopUp = trx.type === 'top_up' && !isRefund && !isAdmin;
const isPurchase = trx.type === 'purchase' || desc.includes('پرداخت سفارش');
let title = "تراکنش کیف پول";
let badgeText = "تراکنش";
let badgeClass = "bg-medical-gray-100 text-medical-gray-600 border-medical-gray-200";
let iconBgClass = "bg-medical-gray-50 text-medical-gray-600 group-hover:bg-medical-gray-700 group-hover:text-white";
let IconComponent = ArrowUpCircle;
let isPositive = trx.type === 'top_up' || trx.type === 'refund';
if (isRefund) {
title = "استرداد وجه سفارش";
badgeText = "استرداد وجه";
badgeClass = "bg-blue-50 text-blue-700 border-blue-200/60";
iconBgClass = "bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white";
IconComponent = RotateCcw;
isPositive = true;
} else if (isAdmin) {
title = isPositive ? "شارژ توسط مدیریت" : "کسر توسط مدیریت";
badgeText = "تغییر توسط پشتیبانی";
badgeClass = "bg-amber-50 text-amber-700 border-amber-200/60";
iconBgClass = "bg-amber-50 text-amber-600 group-hover:bg-amber-600 group-hover:text-white";
IconComponent = ShieldCheck;
} else if (isOnlineTopUp) {
title = "افزایش اعتبار آنلاین (درگاه پرداخت)";
badgeText = "شارژ درگاه";
badgeClass = "bg-emerald-50 text-emerald-700 border-emerald-200/60";
iconBgClass = "bg-emerald-50 text-emerald-600 group-hover:bg-emerald-600 group-hover:text-white";
IconComponent = ArrowUpCircle;
isPositive = true;
} else if (isPurchase) {
title = "پرداخت سفارش از کیف پول";
badgeText = "پرداخت سفارش";
badgeClass = "bg-rose-50 text-rose-700 border-rose-200/60";
iconBgClass = "bg-rose-50 text-rose-600 group-hover:bg-rose-600 group-hover:text-white";
IconComponent = ShoppingCart;
isPositive = false;
}
return (
<div key={trx.id} className="p-4 sm:p-6 bg-white border border-medical-gray-100 rounded-[1.5rem] sm:rounded-[2rem] flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-6 hover:shadow-lg hover:border-canina-blue/20 transition-all group">
<div className="flex items-start sm:items-center gap-3 sm:gap-4 flex-1 min-w-0">
<div className={cn(
"w-10 h-10 sm:w-12 sm:h-12 rounded-xl sm:rounded-2xl flex items-center justify-center transition-colors flex-shrink-0 mt-0.5 sm:mt-0",
iconBgClass
)}>
<IconComponent className="w-5 h-5 sm:w-6 sm:h-6" />
</div>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2 mb-1">
<span className="text-xs sm:text-sm font-black text-medical-gray-900">{title}</span>
<span className={cn("text-[10px] px-2.5 py-0.5 rounded-full border font-black", badgeClass)}>
{badgeText}
</span>
</div>
{desc && (
<p className="text-xs font-bold text-medical-gray-700 line-clamp-2 leading-relaxed mb-1.5">
{desc}
</p>
)}
<div className="text-[10px] font-bold text-medical-gray-400 flex flex-wrap items-center gap-x-2.5 gap-y-1">
<span className="font-mono text-medical-gray-500">کد: {trx.id}</span>
{trx.transactionReference && (
<>
<span className="w-1 h-1 bg-medical-gray-200 rounded-full" />
<span className="font-mono text-canina-blue">مرجع/پیگیری: {trx.transactionReference}</span>
</>
)}
<span className="w-1 h-1 bg-medical-gray-200 rounded-full hidden sm:inline-block" />
<span>{toPersian(new Date(trx.date).toLocaleDateString("fa-IR"))}</span>
</div>
</div>
</div>
<div className="text-right sm:text-left flex sm:flex-col justify-between items-center sm:items-end border-t sm:border-t-0 pt-3 sm:pt-0 border-medical-gray-50 flex-shrink-0">
<div className={cn(
"text-base sm:text-lg font-black italic",
isPositive ? "text-emerald-600" : "text-rose-500"
)}>
{isPositive ? "+" : "-"}{toPersian(trx.amount.toLocaleString())} <span className="text-xs not-italic">تومان</span>
</div>
<div className={cn(
"text-[10px] font-black tracking-wide sm:mt-1 px-2 py-0.5 rounded-full",
trx.status === 'failed' ? "bg-rose-50 text-rose-600" :
trx.status === 'pending' ? "bg-amber-50 text-amber-600" :
"bg-emerald-50 text-emerald-600"
)}>
{trx.status === 'failed' ? "ناموفق" : trx.status === 'pending' ? "در انتظار تایید" : "موفق"}
</div>
</div>
</div>
);
})
)}
</div>
</div>

View File

@ -24,10 +24,12 @@ export interface Address {
export interface Transaction {
id: string;
type: 'top_up' | 'purchase';
type: 'top_up' | 'purchase' | 'refund';
amount: number;
date: string;
status: 'success' | 'failed' | 'pending';
description?: string;
transactionReference?: string;
}
interface UserProfile {
@ -237,12 +239,14 @@ export const useUserStore = create<UserStore>()(
const pd = profileData as unknown as Record<string, unknown>;
const backendWallet = Number(profileData.walletBalance || 0);
const backendCharity = Number(profileData.charityDonationTotal || 0);
const backendTransactions = (pd.walletTransactions as Array<{ id: string; type: string; amount: number; createdAt: string; status: string }>)?.map((t) => ({
const backendTransactions = (pd.walletTransactions as Array<{ id: string; type: string; amount: number; createdAt: string; status: string; description?: string; transactionReference?: string }>)?.map((t) => ({
id: t.id,
type: (t.type === 'deposit' ? 'top_up' : 'purchase') as Transaction['type'],
type: (t.type === 'deposit' ? 'top_up' : t.type === 'refund' ? 'refund' : 'purchase') as Transaction['type'],
amount: Number(t.amount),
date: t.createdAt,
status: (t.status === 'completed' ? 'success' : t.status === 'failed' ? 'failed' : 'pending') as Transaction['status']
status: (t.status === 'completed' ? 'success' : t.status === 'failed' ? 'failed' : 'pending') as Transaction['status'],
description: t.description || undefined,
transactionReference: t.transactionReference || undefined
})) || [];
return {

View File

@ -1,16 +1,16 @@
{
"0": "Roles",
"1": "app.module.ts",
"2": "AdminTransactionFilterDto",
"2": "payment.service.ts",
"3": "productService.ts",
"4": "PetProfile.tsx",
"4": "UserDashboard.tsx",
"5": "CmsController",
"6": "TicketsService",
"6": "tickets.controller.ts",
"7": "SmsSettingsPage.tsx",
"8": "SettingsController",
"8": "SmsService",
"9": "devDependencies",
"10": "CreateReviewDto",
"11": "WikiController",
"10": "ReviewsService",
"11": "ConfirmModal.tsx",
"12": "index.ts",
"13": "app-audit-verification.e2e-spec.js",
"14": "toPersian",
@ -20,8 +20,8 @@
"18": "JwtAuthGuard",
"19": "admin.controller.ts",
"20": "CreateVideoDto",
"21": ".getSmsConfig",
"22": "BlogsService",
"21": "auth.service.ts",
"22": "ProductDto",
"23": "MenuService",
"24": "BE-001",
"25": "FE-001",
@ -47,26 +47,26 @@
"45": "What You Must Do When Invoked",
"46": "20260526145407_init/migration.sql",
"47": "IngredientsService",
"48": "auth.controller.ts",
"48": "AuthController",
"49": "devDependencies",
"50": "devDependencies",
"51": "BlogsController",
"52": "PrescriptionsController",
"52": "PrescriptionsService",
"53": "SmartAdvisorService",
"54": "Button.tsx",
"55": "UITexts.tsx",
"56": "Orders.tsx",
"57": "Role & Core Objective",
"58": "UserDashboard.tsx",
"58": "AuthService",
"59": "compilerOptions",
"60": "CreateUserDto",
"61": "ProductPage.tsx",
"62": "WikiService",
"62": "torob.controller.ts",
"63": "dependencies",
"64": "compilerOptions",
"65": "admin.service.ts",
"66": "AdminQueryDto",
"67": "ReportsController",
"67": "admin.module.ts",
"68": "useSettingsStore",
"69": "Required Review Group Closures",
"70": "compilerOptions",
@ -84,17 +84,17 @@
"82": "scripts",
"83": "dependencies",
"84": "Role & Core Objective",
"85": "auth.module.ts",
"85": "RegisterDto",
"86": "zibal.service.ts",
"87": "dependencies",
"88": "payment.controller.ts",
"88": "CreateEBankCheckoutDto",
"89": "seed-products.ts",
"90": "OrderService",
"90": "useCartStore",
"91": "Reconciled Audit Roles & Assignments",
"92": "OrdersService",
"93": "ArchivePage.tsx",
"94": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
"95": "wiki/[slug]/page.tsx",
"95": "getSeoConfig",
"96": "compilerOptions",
"97": "PaymentService",
"98": "scripts",
@ -105,7 +105,7 @@
"103": "Comprehensive Change Log",
"104": "Products.tsx",
"105": "Operational Rules & Boundaries",
"106": "cms.controller.ts",
"106": "InitiatePaymentDto",
"107": "PaginationDto",
"108": "PrismaService",
"109": "1. Summary of Integrity Repairs Performed",
@ -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": "Body",
"129": "Reports.tsx",
"130": "Sahel-Font",
"131": "Role & Core Objective",
@ -143,15 +143,15 @@
"141": "application/package.json",
"142": "start-dev.js",
"143": "generate-openapi.js",
"144": "SafeImage.tsx",
"145": "AuthService",
"144": "PodcastPlayerModal.tsx",
"145": "AdminLoginDto",
"146": "System Discovery",
"147": "HomeController",
"148": "track/page.tsx",
"149": "trust-seals/page.tsx",
"148": "VerifyOtpDto",
"149": "wiki/[slug]/page.tsx",
"150": "Product Requirement Document (PRD)",
"151": "menu.module.ts",
"152": "auth.service.ts",
"151": "@types/node",
"152": "RevalidationService",
"153": "exclude",
"154": "Baseline Command Plan & Reconciled Command History",
"155": "seo-backfill.ts",
@ -183,8 +183,8 @@
"181": "⚙️ Backend Technical Review (05_dev_backend)",
"182": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
"183": "🚀 SEO & Content Strategy Review (12_seo_content)",
"184": "MetricsController",
"185": "RouteErrorBoundary",
"184": "AppModule",
"185": "eslint-config-next",
"186": "seed-ui-texts.ts",
"187": "seed-wiki.ts",
"188": "update-blog.dto.ts",
@ -203,7 +203,6 @@
"201": "deploy.sh",
"202": "🔒 Security & Performance Review (09_devops_security)",
"203": "👁️ UX & Persona Interface Review (08_visual_qa)",
"204": "zibal-ebank.service.ts",
"205": "prisma/scientificTerms.ts",
"206": "seed-blogs.ts",
"207": "seed-custom.ts",
@ -219,11 +218,9 @@
"217": "sync_honest_manifest.js",
"218": "sync_manifest.js",
"219": "FormField.tsx",
"220": "jest",
"221": "Textarea.tsx",
"222": "admin-panel/tsconfig.json",
"223": "tailwindcss",
"224": "class-transformer",
"225": "next.config.ts",
"226": "Shabnam Font README",
"227": "AGENTS.md",
@ -232,7 +229,6 @@
"230": "instructions.md",
"231": "@nestjs/schematics",
"232": "prisma",
"233": "Reviews.tsx",
"234": "source-map-support",
"235": "ts-loader",
"236": "ts-node",
@ -264,7 +260,6 @@
"262": "User Logout API",
"263": "generate-openapi.d.ts",
"264": "@types/compression",
"265": "helmet",
"266": "@types/express",
"267": "@types/jest",
"268": "@types/multer",
@ -294,23 +289,14 @@
"292": "Production Docker Compose",
"293": "Staging Docker Compose",
"294": "ZibalService",
"295": "js-yaml",
"296": "@nestjs/core",
"297": "ZibalEBankService",
"298": ".initiateOrderPayment",
"299": "@tailwindcss/postcss",
"300": "typescript",
"301": "@nestjs/jwt",
"302": "typescript-eslint",
"303": "@nestjs/throttler",
"304": "passport",
"305": "reflect-metadata",
"306": "@eslint/js",
"307": "swagger-ui-express",
"308": "eslint-config-prettier",
"309": "axios",
"310": "tailwindcss",
"311": "@eslint/eslintrc",
"312": "@types/passport-jwt",
"313": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql",
"314": "eslint-plugin-prettier",
@ -318,17 +304,9 @@
"316": "globals",
"317": "revalidate/route.ts",
"318": "MaskableField.tsx",
"319": "@nestjs/cli",
"320": "@nestjs/testing",
"321": "orders/page.tsx",
"322": "pets/page.tsx",
"323": "prettier",
"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"
"329": "typescript-eslint"
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,334 @@
{
"0": "Roles",
"1": "app.module.ts",
"2": "AdminTransactionFilterDto",
"3": "productService.ts",
"4": "PetProfile.tsx",
"5": "CmsController",
"6": "TicketsService",
"7": "SmsSettingsPage.tsx",
"8": "SettingsController",
"9": "devDependencies",
"10": "CreateReviewDto",
"11": "WikiController",
"12": "index.ts",
"13": "app-audit-verification.e2e-spec.js",
"14": "toPersian",
"15": "src/services/api.ts",
"16": "DoctorQueryDto",
"17": "schema.ts",
"18": "JwtAuthGuard",
"19": "admin.controller.ts",
"20": "CreateVideoDto",
"21": ".getSmsConfig",
"22": "BlogsService",
"23": "MenuService",
"24": "BE-001",
"25": "FE-001",
"26": "ADM-001",
"27": "DB-001",
"28": "TS-001",
"29": "TEST-001",
"30": "DEVOPS-001",
"31": "DOC-001",
"32": "adminRoutes.tsx",
"33": "WholesaleApplyDto",
"34": "B2BService",
"35": "ContactService",
"36": "FaqService",
"37": "راهنمای تست سیستم (Software Testing)",
"38": "Button",
"39": "CategoriesController",
"40": "MediaController",
"41": "What You Must Do When Invoked",
"42": "SslController",
"43": "BannersService",
"44": "TestimonialsService",
"45": "What You Must Do When Invoked",
"46": "20260526145407_init/migration.sql",
"47": "IngredientsService",
"48": "auth.controller.ts",
"49": "devDependencies",
"50": "devDependencies",
"51": "BlogsController",
"52": "PrescriptionsController",
"53": "SmartAdvisorService",
"54": "Button.tsx",
"55": "UITexts.tsx",
"56": "Orders.tsx",
"57": "Role & Core Objective",
"58": "UserDashboard.tsx",
"59": "compilerOptions",
"60": "CreateUserDto",
"61": "ProductPage.tsx",
"62": "WikiService",
"63": "dependencies",
"64": "compilerOptions",
"65": "admin.service.ts",
"66": "AdminQueryDto",
"67": "ReportsController",
"68": "useSettingsStore",
"69": "Required Review Group Closures",
"70": "compilerOptions",
"71": "getPageMetadata",
"72": "Operational Rules & Boundaries",
"73": "Operational Rules & Boundaries",
"74": "WikiController",
"75": "PetsController",
"76": "ProductsService",
"77": "seo.module.ts",
"78": "rss.xml/route.ts",
"79": "🏢 AI Software Agency — Master Orchestration Protocol v3",
"80": "Operational Rules & Boundaries",
"81": "Operational Rules & Boundaries",
"82": "scripts",
"83": "dependencies",
"84": "Role & Core Objective",
"85": "auth.module.ts",
"86": "zibal.service.ts",
"87": "dependencies",
"88": "payment.controller.ts",
"89": "seed-products.ts",
"90": "OrderService",
"91": "Reconciled Audit Roles & Assignments",
"92": "OrdersService",
"93": "ArchivePage.tsx",
"94": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
"95": "wiki/[slug]/page.tsx",
"96": "compilerOptions",
"97": "PaymentService",
"98": "scripts",
"99": "BlogsController",
"100": "Deep Audit Summary Report",
"101": "Operational Rules & Boundaries",
"102": "jest",
"103": "Comprehensive Change Log",
"104": "Products.tsx",
"105": "Operational Rules & Boundaries",
"106": "cms.controller.ts",
"107": "PaginationDto",
"108": "PrismaService",
"109": "1. Summary of Integrity Repairs Performed",
"110": "Operational Rules & Boundaries",
"111": "Operational Rules & Boundaries",
"112": "Operational Rules & Boundaries",
"113": "PetsController",
"114": "AppService",
"115": "Spinner.tsx",
"116": "Vazirmatn Changelog",
"117": "Vazirmatn Font فونت وزیرمتن",
"118": "Operational Rules & Boundaries",
"119": "compilerOptions",
"120": "compilerOptions",
"121": "backend/README.md",
"122": "AdminController",
"123": "UsersService",
"124": "Repository Map",
"125": "validate_integrity.js",
"126": "admin-panel/package.json",
"127": "Sahel-Font",
"128": "AdminService",
"129": "Reports.tsx",
"130": "Sahel-Font",
"131": "Role & Core Objective",
"132": "orchestrate.py",
"133": "backend/package.json",
"134": "blog/page.tsx",
"135": "graphify reference: extra exports and benchmark",
"136": "Phase 2 Final Quality Gate Summary Report",
"137": "Task Modifications Log",
"138": "Install",
"139": "layout.tsx",
"140": "ErrorBoundary",
"141": "application/package.json",
"142": "start-dev.js",
"143": "generate-openapi.js",
"144": "SafeImage.tsx",
"145": "AuthService",
"146": "System Discovery",
"147": "HomeController",
"148": "track/page.tsx",
"149": "trust-seals/page.tsx",
"150": "Product Requirement Document (PRD)",
"151": "menu.module.ts",
"152": "auth.service.ts",
"153": "exclude",
"154": "Baseline Command Plan & Reconciled Command History",
"155": "seo-backfill.ts",
"156": "manual-test-scenarios.md",
"157": "ErrorPages.tsx",
"158": "media/[...path]/route.ts",
"159": "with-vpn.sh",
"160": "Architecture Specification",
"161": "Project Health Audit Report",
"162": "nest-cli.json",
"163": "graphify reference: query, path, explain",
"164": "Open Questions",
"165": "Final Phase 2 Audit Closure Report",
"166": "open-browsers.js",
"167": "📝 Active Agent Working Scratchpad",
"168": "🔍 Code Health Audit Review (01_auditor)",
"169": "paginated-response.schema.ts",
"170": "Vazirmatn Font README",
"171": "Omitted File Inspection Report",
"172": "Phase 3.2 / 3.3 — Implementation Readiness & Architectural Finalization Report",
"173": "Phase 3 Audit Traceability Matrix",
"174": "rebuild_honest_ledger.js",
"175": "validate_evidence_grade.js",
"176": "uploads/[...path]/route.ts",
"177": "app/page.tsx",
"178": "نقشه جامع پوشش تست‌های سرتاسری (E2E Test Coverage Map) — پروژه Canina",
"179": "app.e2e-spec.js",
"180": "API Contract Specification",
"181": "⚙️ Backend Technical Review (05_dev_backend)",
"182": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
"183": "🚀 SEO & Content Strategy Review (12_seo_content)",
"184": "MetricsController",
"185": "RouteErrorBoundary",
"186": "seed-ui-texts.ts",
"187": "seed-wiki.ts",
"188": "update-blog.dto.ts",
"189": "update-home.dto.ts",
"190": "update-wiki.dto.ts",
"191": "graphify reference: add a URL and watch a folder",
"192": "graphify reference: commit hook and native CLAUDE.md integration",
"193": "graphify reference: incremental update and cluster-only",
"194": "Raw Finding Verification & Disposition Report",
"195": "React + TypeScript + Vite",
"196": "Select.tsx",
"197": "userStore.ts",
"198": "@tailwindcss/postcss",
"199": "SmsLogQueryDto",
"200": "application/README.md",
"201": "deploy.sh",
"202": "🔒 Security & Performance Review (09_devops_security)",
"203": "👁️ UX & Persona Interface Review (08_visual_qa)",
"204": "zibal-ebank.service.ts",
"205": "prisma/scientificTerms.ts",
"206": "seed-blogs.ts",
"207": "seed-custom.ts",
"208": "graphify reference: GitHub clone and cross-repo merge",
"209": "graphify reference: transcribe video and audio",
"210": "Compiler Diagnostic Dispositions",
"211": "Master Task Backlog (Phase 3.3)",
"212": "build_manifest.js",
"213": "generate_classification.js",
"214": "generate_evidence.js",
"215": "generate_ledger.js",
"216": "generate_manifest.js",
"217": "sync_honest_manifest.js",
"218": "sync_manifest.js",
"219": "FormField.tsx",
"220": "jest",
"221": "Textarea.tsx",
"222": "admin-panel/tsconfig.json",
"223": "tailwindcss",
"224": "class-transformer",
"225": "next.config.ts",
"226": "Shabnam Font README",
"227": "AGENTS.md",
"228": "rules/graphify.md",
"229": ".agents/workflows/graphify.md",
"230": "instructions.md",
"231": "@nestjs/schematics",
"232": "prisma",
"233": "Reviews.tsx",
"234": "source-map-support",
"235": "ts-loader",
"236": "ts-node",
"237": "tsconfig-paths",
"238": "@vitejs/plugin-react",
"239": "@types/bcrypt",
"240": "supertest",
"241": "blog.entity.ts",
"242": "home.entity.ts",
"243": "wiki.entity.ts",
"244": "User Profile Photo",
"245": "CLAUDE.md",
"246": ".claude/CLAUDE.md",
"247": "extraction-spec.md",
"248": "Products Table",
"249": "Users Table",
"250": "Architectural Audit Findings",
"251": "Cross Boundary Dependencies & Backend Architecture Specification",
"252": "Next.js Agent Rules & Brand Guidelines",
"253": "robots.ts",
"254": "application/eslint.config.mjs",
"255": "postcss.config.mjs",
"256": "vitest.setup.ts",
"257": "backup_db.sh",
"258": "start.sh",
"259": "reviews/README.md",
"260": "backend/eslint.config.mjs",
"261": "User Login API",
"262": "User Logout API",
"263": "generate-openapi.d.ts",
"264": "@types/compression",
"265": "helmet",
"266": "@types/express",
"267": "@types/jest",
"268": "@types/multer",
"269": "eslint-plugin-react-hooks",
"270": "app-audit-verification.e2e-spec.d.ts",
"271": "app.e2e-spec.d.ts",
"272": "Canina Pharma GmbH",
"273": "Pets Table",
"274": "Canina Iran Project Introduction",
"275": "Developer Standards and Architecture",
"276": "Frontend & Admin Architecture Route Map Specification",
"277": "Project Backlog and Tasks",
"278": "eslint.config.js",
"279": "postcss.config.js",
"280": "admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"281": "shabnam-font-v5.0.1/CHANGELOG.md",
"282": "tailwind.config.js",
"283": "vite.config.ts",
"284": "application/CLAUDE.md",
"285": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"286": "Sahel Font Sample",
"287": "Shabnam Font Changelog",
"288": "Vazirmatn Changelog",
"289": "vitest.config.ts",
"290": "Sahel Font Variable Sample",
"291": "Shabnam Font Sample",
"292": "Production Docker Compose",
"293": "Staging Docker Compose",
"294": "ZibalService",
"295": "js-yaml",
"296": "@nestjs/core",
"297": "ZibalEBankService",
"298": ".initiateOrderPayment",
"299": "@tailwindcss/postcss",
"300": "typescript",
"301": "@nestjs/jwt",
"302": "typescript-eslint",
"303": "@nestjs/throttler",
"304": "passport",
"305": "reflect-metadata",
"306": "@eslint/js",
"307": "swagger-ui-express",
"308": "eslint-config-prettier",
"309": "axios",
"310": "tailwindcss",
"311": "@eslint/eslintrc",
"312": "@types/passport-jwt",
"313": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql",
"314": "eslint-plugin-prettier",
"315": "typescript",
"316": "globals",
"317": "revalidate/route.ts",
"318": "MaskableField.tsx",
"319": "@nestjs/cli",
"320": "@nestjs/testing",
"321": "orders/page.tsx",
"322": "pets/page.tsx",
"323": "prettier",
"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"
}

View File

@ -0,0 +1 @@
{"output_tokens": 7105}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +1,32 @@
# Graph Report - canina (2026-09-02)
# Graph Report - canina (2026-09-05)
## Corpus Check
- 599 files · ~1,084,689 words
- 601 files · ~1,114,020 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 4208 nodes · 7643 edges · 332 communities (212 shown, 120 thin omitted)
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 288 edges (avg confidence: 0.79)
- 4234 nodes · 7717 edges · 310 communities (215 shown, 95 thin omitted)
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 298 edges (avg confidence: 0.79)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `ceaab10f`
- Built from commit: `e15c25da`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
## Community Hubs (Navigation)
- Roles
- app.module.ts
- AdminTransactionFilterDto
- payment.service.ts
- productService.ts
- PetProfile.tsx
- UserDashboard.tsx
- CmsController
- TicketsService
- tickets.controller.ts
- SmsSettingsPage.tsx
- SettingsController
- SmsService
- devDependencies
- CreateReviewDto
- WikiController
- ReviewsService
- ConfirmModal.tsx
- index.ts
- app-audit-verification.e2e-spec.js
- toPersian
@ -36,7 +36,8 @@
- JwtAuthGuard
- admin.controller.ts
- CreateVideoDto
- BlogsService
- auth.service.ts
- ProductDto
- MenuService
- BE-001
- FE-001
@ -62,26 +63,26 @@
- What You Must Do When Invoked
- 20260526145407_init/migration.sql
- IngredientsService
- auth.controller.ts
- AuthController
- devDependencies
- devDependencies
- BlogsController
- PrescriptionsController
- PrescriptionsService
- SmartAdvisorService
- Button.tsx
- UITexts.tsx
- Orders.tsx
- Role & Core Objective
- UserDashboard.tsx
- AuthService
- compilerOptions
- CreateUserDto
- ProductPage.tsx
- WikiService
- torob.controller.ts
- dependencies
- compilerOptions
- admin.service.ts
- AdminQueryDto
- ReportsController
- admin.module.ts
- useSettingsStore
- Required Review Group Closures
- compilerOptions
@ -99,17 +100,17 @@
- scripts
- dependencies
- Role & Core Objective
- auth.module.ts
- RegisterDto
- zibal.service.ts
- dependencies
- payment.controller.ts
- CreateEBankCheckoutDto
- seed-products.ts
- OrderService
- useCartStore
- Reconciled Audit Roles & Assignments
- OrdersService
- ArchivePage.tsx
- Phase 3.1 — Human Review Preparation and Master Backlog Critique
- wiki/[slug]/page.tsx
- getSeoConfig
- compilerOptions
- PaymentService
- scripts
@ -120,7 +121,7 @@
- Comprehensive Change Log
- Products.tsx
- Operational Rules & Boundaries
- cms.controller.ts
- InitiatePaymentDto
- PaginationDto
- PrismaService
- 1. Summary of Integrity Repairs Performed
@ -136,13 +137,13 @@
- compilerOptions
- compilerOptions
- backend/README.md
- AdminController
- AdminService
- UsersService
- Repository Map
- validate_integrity.js
- admin-panel/package.json
- Sahel-Font
- AdminService
- Body
- Reports.tsx
- Sahel-Font
- Role & Core Objective
@ -158,15 +159,15 @@
- application/package.json
- start-dev.js
- generate-openapi.js
- SafeImage.tsx
- AuthService
- PodcastPlayerModal.tsx
- AdminLoginDto
- System Discovery
- HomeController
- track/page.tsx
- trust-seals/page.tsx
- VerifyOtpDto
- wiki/[slug]/page.tsx
- Product Requirement Document (PRD)
- menu.module.ts
- auth.service.ts
- @types/node
- RevalidationService
- exclude
- Baseline Command Plan & Reconciled Command History
- seo-backfill.ts
@ -197,8 +198,8 @@
- ⚙️ Backend Technical Review (05_dev_backend)
- 🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)
- 🚀 SEO & Content Strategy Review (12_seo_content)
- MetricsController
- RouteErrorBoundary
- AppModule
- eslint-config-next
- seed-ui-texts.ts
- seed-wiki.ts
- update-blog.dto.ts
@ -217,7 +218,6 @@
- deploy.sh
- 🔒 Security & Performance Review (09_devops_security)
- 👁️ UX & Persona Interface Review (08_visual_qa)
- zibal-ebank.service.ts
- prisma/scientificTerms.ts
- seed-blogs.ts
- seed-custom.ts
@ -233,11 +233,9 @@
- sync_honest_manifest.js
- sync_manifest.js
- FormField.tsx
- jest
- Textarea.tsx
- admin-panel/tsconfig.json
- tailwindcss
- class-transformer
- next.config.ts
- Shabnam Font README
- AGENTS.md
@ -246,7 +244,6 @@
- instructions.md
- @nestjs/schematics
- prisma
- Reviews.tsx
- source-map-support
- ts-loader
- ts-node
@ -274,7 +271,6 @@
- User Login API
- User Logout API
- @types/compression
- helmet
- @types/express
- @types/jest
- @types/multer
@ -293,21 +289,12 @@
- Production Docker Compose
- Staging Docker Compose
- ZibalService
- js-yaml
- @nestjs/core
- ZibalEBankService
- .initiateOrderPayment
- @tailwindcss/postcss
- typescript
- @nestjs/jwt
- typescript-eslint
- @nestjs/throttler
- passport
- reflect-metadata
- @eslint/js
- swagger-ui-express
- eslint-config-prettier
- @eslint/eslintrc
- @types/passport-jwt
- 20260526160916_add_ui_texts_and_scientific_terms/migration.sql
- eslint-plugin-prettier
@ -315,24 +302,16 @@
- globals
- revalidate/route.ts
- MaskableField.tsx
- @nestjs/cli
- @nestjs/testing
- prettier
- eslint
- @types/react-dom
- @types/js-yaml
- eslint-plugin-react-refresh
- @types/supertest
- typescript-eslint
- @nestjs/swagger
- tailwindcss
## God Nodes (most connected - your core abstractions)
1. `Roles()` - 106 edges
1. `Roles()` - 108 edges
2. `PrismaService` - 89 edges
3. `useSettingsStore` - 61 edges
4. `api` - 44 edges
5. `SmsService` - 43 edges
4. `SmsService` - 51 edges
5. `api` - 44 edges
6. `PaginationDto` - 41 edges
7. `AdminService` - 40 edges
8. `AdminController` - 39 edges
@ -352,75 +331,75 @@
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)
## Communities (310 total, 95 thin omitted)
### Community 0 - "Roles"
Cohesion: 0.24
Nodes (13): Roles(), PaymentController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get (+5 more)
### Community 1 - "app.module.ts"
Cohesion: 0.06
Nodes (39): B2BModule, Module, BannersModule, Module, CmsModule, Module, RevalidationModule, Global (+31 more)
Cohesion: 0.07
Nodes (36): B2BModule, Module, BannersModule, Module, CmsModule, Module, SmsModule, Global (+28 more)
### Community 2 - "AdminTransactionFilterDto"
Cohesion: 0.22
Nodes (7): AdminTransactionFilterDto, ApiPropertyOptional, IsIn, IsNumber, IsOptional, IsString, Type
### Community 2 - "payment.service.ts"
Cohesion: 0.20
Nodes (8): AdminTransactionFilterDto, ApiPropertyOptional, IsIn, IsNumber, IsOptional, IsString, Type, ClientMetadata
### Community 3 - "productService.ts"
Cohesion: 0.06
Nodes (39): buildTorobProductSpec(), buildTorobProductTitle(), dynamic, GET(), revalidate, dynamic, GET(), revalidate (+31 more)
Cohesion: 0.05
Nodes (41): buildTorobProductSpec(), buildTorobProductTitle(), dynamic, GET(), revalidate, dynamic, GET(), revalidate (+33 more)
### Community 4 - "PetProfile.tsx"
Cohesion: 0.12
Nodes (18): metadata, FeaturedProducts(), ProductCard(), OrderSuccess(), PetProfile(), SearchResultsPage(), CURRENT_SYMPTOMS, MEDICAL_HISTORIES (+10 more)
### Community 4 - "UserDashboard.tsx"
Cohesion: 0.11
Nodes (20): metadata, OrderDetailsModal(), PetProfile(), SearchResultsPage(), CURRENT_SYMPTOMS, MEDICAL_HISTORIES, SmartAdvisor(), SmartAdvisorProps (+12 more)
### Community 5 - "CmsController"
Cohesion: 0.10
Nodes (14): CmsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
Cohesion: 0.09
Nodes (24): CmsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+16 more)
### Community 6 - "TicketsService"
### Community 6 - "tickets.controller.ts"
Cohesion: 0.09
Nodes (29): AdminUpdateTicketDto, CreateTicketDto, ReplyTicketDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString (+21 more)
### Community 7 - "SmsSettingsPage.tsx"
Cohesion: 0.29
Nodes (7): PatternItem, SmsConfigState, SmsLogItem, SmsLogStats, SmsSettingsPage(), toPersianDigits(), SmsSettingsPage
Cohesion: 0.20
Nodes (10): PatternItem, SmsConfigState, SmsEventDefinition, SmsEventVariable, SmsLogItem, SmsLogStats, SmsRule, SmsSettingsPage() (+2 more)
### Community 8 - "SettingsController"
Cohesion: 0.10
Nodes (17): SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Delete (+9 more)
### Community 8 - "SmsService"
Cohesion: 0.06
Nodes (20): SmsEventDefinition, SmsService, Injectable, SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags (+12 more)
### Community 9 - "devDependencies"
Cohesion: 0.22
Nodes (9): devDependencies, ts-jest, @types/bcryptjs, @types/node, typescript, @types/node, typescript, ts-jest (+1 more)
Cohesion: 0.08
Nodes (25): devDependencies, eslint, eslint-config-prettier, @eslint/eslintrc, jest, @nestjs/cli, @nestjs/testing, prettier (+17 more)
### Community 10 - "CreateReviewDto"
Cohesion: 0.07
Nodes (30): CreateReviewDto, ApiProperty, IsInt, IsNotEmpty, IsOptional, IsString, Min, ApiProperty (+22 more)
### Community 10 - "ReviewsService"
Cohesion: 0.09
Nodes (22): ApiProperty, IsIn, IsOptional, IsString, UpdateReviewDto, ReviewsController, ApiBearerAuth, ApiOperation (+14 more)
### Community 11 - "WikiController"
Cohesion: 0.21
Nodes (9): ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, Controller, Get, Param, Query (+1 more)
### Community 11 - "ConfirmModal.tsx"
Cohesion: 0.10
Nodes (16): ConfirmModal(), ConfirmModalProps, Pagination(), PaginationProps, Category, Pet, DoctorOption, Video (+8 more)
### Community 12 - "index.ts"
Cohesion: 0.06
Nodes (24): backend, frontend, playwright.config.ts, @playwright/test, tests/**/*.ts, authFile, test, compilerOptions (+16 more)
### Community 13 - "app-audit-verification.e2e-spec.js"
Cohesion: 0.06
Nodes (28): AppModule, Module, EnvironmentVariables, IsNotEmpty, IsOptional, IsString, validateEnv(), CustomHttpExceptionFilter (+20 more)
Cohesion: 0.07
Nodes (26): EnvironmentVariables, IsNotEmpty, IsOptional, IsString, validateEnv(), CustomHttpExceptionFilter, Catch, PrismaExceptionFilter (+18 more)
### Community 14 - "toPersian"
Cohesion: 0.17
Nodes (20): HomeClient(), VerifyContent(), AddressFormData, AddressModal(), AddressModalProps, normalizeDigits(), validateAddressForm(), CheckoutPage() (+12 more)
Nodes (19): VerifyContent(), AddressFormData, AddressModal(), AddressModalProps, normalizeDigits(), validateAddressForm(), BackButton(), BackButtonProps (+11 more)
### Community 15 - "src/services/api.ts"
Cohesion: 0.06
Nodes (36): Layout(), ProtectedRoute(), MenuGroup, Sidebar(), SidebarProps, SubMenuItem, LiveMonitoringSummary, SEARCHABLE_PAGES (+28 more)
Nodes (38): Layout(), ProtectedRoute(), MenuGroup, Sidebar(), SidebarProps, SubMenuItem, LiveMonitoringSummary, SEARCHABLE_PAGES (+30 more)
### Community 16 - "DoctorQueryDto"
Cohesion: 0.09
@ -431,16 +410,24 @@ Cohesion: 0.14
Nodes (18): B2BPage(), generateMetadata(), ContactPage(), generateMetadata(), generateMetadata(), getInitialVideos(), Videos(), ContactFormClient() (+10 more)
### Community 18 - "JwtAuthGuard"
Cohesion: 0.19
Nodes (8): JwtAuthGuard, Injectable, ROLES_KEY, RequestWithUser, RolesGuard, Injectable, UserReqPayload, ScientificTermData
Cohesion: 0.10
Nodes (19): JwtAuthGuard, Injectable, ROLES_KEY, RequestWithUser, RolesGuard, Injectable, ApiPropertyOptional, IsOptional (+11 more)
### 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"
Cohesion: 0.07
Nodes (31): CreateVideoDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEmpty, IsOptional, IsString, UpdateVideoDto (+23 more)
Nodes (32): CreateVideoDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEmpty, IsOptional, IsString, UpdateVideoDto (+24 more)
### Community 21 - "auth.service.ts"
Cohesion: 0.14
Nodes (13): AdminLoginInput, LoginInput, RegisterInput, LoginDto, ApiProperty, IsNotEmpty, IsString, MinLength (+5 more)
### Community 22 - "ProductDto"
Cohesion: 0.16
Nodes (7): ProductDto, ApiPropertyOptional, IsArray, IsBoolean, IsNumber, IsOptional, IsString
### Community 23 - "MenuService"
Cohesion: 0.12
@ -480,7 +467,7 @@ Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternati
### Community 32 - "adminRoutes.tsx"
Cohesion: 0.07
Nodes (21): App(), ContactInfoItem, ContactSubmission, CategoryDist, DashboardData, SslStatus, Ticket, TicketMessage (+13 more)
Nodes (16): App(), Props, RouteErrorBoundary, State, HeroBanner, VetTestimonial, SslStatus, AdminRouteConfig (+8 more)
### Community 33 - "WholesaleApplyDto"
Cohesion: 0.10
@ -495,8 +482,8 @@ Cohesion: 0.13
Nodes (12): ContactController, Body, Controller, Get, Param, Post, Put, Query (+4 more)
### Community 36 - "FaqService"
Cohesion: 0.14
Nodes (14): FaqController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
Cohesion: 0.12
Nodes (16): FaqController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+8 more)
### Community 37 - "راهنمای تست سیستم (Software Testing)"
Cohesion: 0.07
@ -519,8 +506,8 @@ Cohesion: 0.07
Nodes (26): For /graphify add and --watch, For /graphify query, For the commit hook and native CLAUDE.md integration, For --update and --cluster-only, /graphify, Honesty Rules, Interpreter guard for subcommands, Part A - Structural extraction for code files (+18 more)
### Community 42 - "SslController"
Cohesion: 0.11
Nodes (15): SslController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Post (+7 more)
Cohesion: 0.13
Nodes (14): SslController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Post (+6 more)
### Community 43 - "BannersService"
Cohesion: 0.13
@ -542,9 +529,9 @@ Nodes (14): "coupons", "health_logs", "order_items", "orders", "pet_medical_cond
Cohesion: 0.13
Nodes (14): IngredientsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
### Community 48 - "auth.controller.ts"
Cohesion: 0.06
Nodes (43): AuthController, ApiBadRequestResponse, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller (+35 more)
### Community 48 - "AuthController"
Cohesion: 0.25
Nodes (13): AuthController, ApiBadRequestResponse, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller (+5 more)
### Community 49 - "devDependencies"
Cohesion: 0.11
@ -552,27 +539,27 @@ 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, @testing-library/jest-dom, @testing-library/react, @types/node, @types/react (+9 more)
### Community 51 - "BlogsController"
Cohesion: 0.07
Nodes (18): BlogsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+10 more)
### Community 52 - "PrescriptionsController"
Cohesion: 0.16
Nodes (12): PrescriptionsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Param (+4 more)
### Community 52 - "PrescriptionsService"
Cohesion: 0.14
Nodes (14): PrescriptionsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Param (+6 more)
### Community 53 - "SmartAdvisorService"
Cohesion: 0.13
Nodes (14): SmartAdvisorController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
### Community 54 - "Button.tsx"
Cohesion: 0.10
Nodes (16): ButtonProps, ButtonSize, ButtonVariant, ConfirmModal(), ConfirmModalProps, HeroBanner, VetTestimonial, FAQ (+8 more)
Cohesion: 0.07
Nodes (24): ButtonProps, ButtonSize, ButtonVariant, maxWidthClasses, Modal(), ModalProps, ContactInfoItem, ContactSubmission (+16 more)
### Community 55 - "UITexts.tsx"
Cohesion: 0.07
Nodes (26): ICON_REGISTRY, IconPickerModal(), IconPickerModalProps, ActiveStates, PRESET_BG_COLORS, PRESET_COLORS, RichTextEditor(), RichTextEditorProps (+18 more)
Cohesion: 0.08
Nodes (23): ICON_REGISTRY, IconPickerModal(), IconPickerModalProps, ActiveStates, PRESET_BG_COLORS, PRESET_COLORS, RichTextEditor(), RichTextEditorProps (+15 more)
### Community 56 - "Orders.tsx"
Cohesion: 0.11
@ -582,45 +569,49 @@ Nodes (15): Badge(), BadgeProps, BadgeVariant, variantStyles, Skeleton(), Monito
Cohesion: 0.09
Nodes (22): CREATE MODE — Normal Operation, Expected JSON Output Schema, File Scope Boundary, Forbidden Actions, MODE 1: REVIEW (called during Review Phase), MODE 2: CONTENT CREATION (standalone task from backlog), Operating Modes, Operational Rules & Boundaries (+14 more)
### Community 58 - "UserDashboard.tsx"
Cohesion: 0.12
Nodes (11): DeleteConfirmModal(), DeleteConfirmModalProps, OrderDetailsModal(), OrderRowSkeleton(), PetProfileSkeleton(), Skeleton(), SkeletonProps, CreateTicketPayload (+3 more)
### Community 58 - "AuthService"
Cohesion: 0.19
Nodes (3): AuthService, Injectable, normalizeMobile()
### Community 59 - "compilerOptions"
Cohesion: 0.06
Nodes (30): compilerOptions, allowSyntheticDefaultImports, baseUrl, declaration, emitDecoratorMetadata, esModuleInterop, experimentalDecorators, forceConsistentCasingInFileNames (+22 more)
### Community 60 - "CreateUserDto"
Cohesion: 0.23
Cohesion: 0.21
Nodes (10): CreateUserDto, ApiProperty, ApiPropertyOptional, IsEmail, IsNotEmpty, IsNumber, IsOptional, IsString (+2 more)
### Community 61 - "ProductPage.tsx"
Cohesion: 0.10
Nodes (19): ProductImageZoomModalProps, CalculatorState, GalleryMediaItem, ICON_MAP, isVideoUrl(), ProductImageZoomModal, ProductPage(), ProductReviews (+11 more)
Cohesion: 0.09
Nodes (20): ProductImageZoomModalProps, CalculatorState, GalleryMediaItem, ICON_MAP, isVideoUrl(), ProductImageZoomModal, ProductPage(), ProductReviews (+12 more)
### Community 62 - "torob.controller.ts"
Cohesion: 0.22
Nodes (8): buildTorobProductSpec(), buildTorobProductTitle(), TorobController, ApiOperation, ApiTags, Controller, Get, Query
### Community 63 - "dependencies"
Cohesion: 0.09
Nodes (23): dependencies, bcrypt, bcryptjs, class-validator, compression, ioredis, @nestjs/common, @nestjs/passport (+15 more)
Cohesion: 0.05
Nodes (43): dependencies, bcrypt, bcryptjs, class-transformer, class-validator, compression, helmet, ioredis (+35 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.33
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
Nodes (11): ReportsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Get, Query (+3 more)
### Community 67 - "admin.module.ts"
Cohesion: 0.08
Nodes (17): AdminModule, Module, ReportsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller (+9 more)
### Community 68 - "useSettingsStore"
Cohesion: 0.08
Nodes (29): HomeClientProps, B2BLandingClient(), BlogPost, BlogPreviewSection(), BrandLogo(), BrandLogoProps, ContactInfoItem, FAQItem (+21 more)
Nodes (36): HomeClient(), HomeClientProps, B2BLandingClient(), BlogPostClientProps, BlogPost, BlogPreviewSection(), ContactInfoItem, EnamadBadge() (+28 more)
### Community 69 - "Required Review Group Closures"
Cohesion: 0.10
@ -631,8 +622,8 @@ Cohesion: 0.08
Nodes (23): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, jsx, lib, module, moduleDetection, moduleResolution (+15 more)
### Community 71 - "getPageMetadata"
Cohesion: 0.09
Nodes (14): generateMetadata(), CatalogClient(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata() (+6 more)
Cohesion: 0.08
Nodes (16): generateMetadata(), CatalogClient(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata() (+8 more)
### Community 72 - "Operational Rules & Boundaries"
Cohesion: 0.11
@ -643,7 +634,7 @@ Cohesion: 0.11
Nodes (18): 1. Framework-Aware Analysis, 2. DOM & Semantic Structure Analysis, 3. Accessibility Compliance, 4. Responsive Layout Verification, 5. Fallback Inspection Mode, 6. Defect Routing Protocol, 7. Forbidden Actions, Expected JSON Output Schema (+10 more)
### Community 74 - "WikiController"
Cohesion: 0.14
Cohesion: 0.13
Nodes (14): ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete, Get (+6 more)
### Community 75 - "PetsController"
@ -651,8 +642,8 @@ Cohesion: 0.05
Nodes (47): CreateHealthLogDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString, CreatePetDto, ApiProperty (+39 more)
### Community 76 - "ProductsService"
Cohesion: 0.07
Nodes (27): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, ProductsController, ApiOperation, ApiTags (+19 more)
Cohesion: 0.10
Nodes (19): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, ProductsController, ApiOperation, ApiTags (+11 more)
### Community 77 - "seo.module.ts"
Cohesion: 0.16
@ -686,9 +677,9 @@ Nodes (21): dependencies, axios, lucide-react, react, react-dom, react-hot-toast
Cohesion: 0.12
Nodes (16): 1. Active Secret Scanning (All Modified Files), 2. Docker Container Verification (if Dockerfile exists), 3. CI/CD Basic Check (if `.github/workflows/` exists), 4. Failure Routing Protocol, 5. Forbidden Actions, ENFORCE MODE — Normal Operation, Expected JSON Output Schema, Operational Rules & Boundaries (+8 more)
### Community 85 - "auth.module.ts"
Cohesion: 0.17
Nodes (10): DEFAULT_JWT_REFRESH_SECRET, DEFAULT_JWT_SECRET, getJwtSecret(), AuthModule, Module, JwtPayload, JwtStrategy, Injectable (+2 more)
### Community 85 - "RegisterDto"
Cohesion: 0.22
Nodes (8): RegisterDto, ApiProperty, ApiPropertyOptional, IsEmail, IsNotEmpty, IsOptional, IsString, MinLength
### Community 86 - "zibal.service.ts"
Cohesion: 0.16
@ -698,33 +689,37 @@ Nodes (9): GatewayHealthResult, IPaymentGateway, PaymentInquiryResult, PaymentRe
Cohesion: 0.11
Nodes (19): dependencies, axios, lucide-react, motion, next, nextjs-toploader, react, react-dom (+11 more)
### Community 88 - "payment.controller.ts"
Cohesion: 0.12
Nodes (19): CreateEBankCheckoutDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsNumber, IsOptional, IsString, Min (+11 more)
### Community 88 - "CreateEBankCheckoutDto"
Cohesion: 0.22
Nodes (8): CreateEBankCheckoutDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsNumber, IsOptional, IsString, Min
### Community 89 - "seed-products.ts"
Cohesion: 0.17
Nodes (12): prisma, main(), prisma, determineSuitableFor(), findOrCreateCategory(), getProductImageUrl(), main(), parseSize() (+4 more)
### Community 90 - "useCartStore"
Cohesion: 0.09
Nodes (19): B2BPortal, CartDrawer, B2BPortal(), CartDrawer(), DeleteConfirmModal(), DeleteConfirmModalProps, OrderSuccess(), OrderTracking() (+11 more)
### Community 91 - "Reconciled Audit Roles & Assignments"
Cohesion: 0.12
Nodes (15): 10. Documentation Engineer, 1. Lead Architect / Orchestrator, 2. React / Vite Storefront Auditor, 3. NestJS Backend Auditor, 4. Admin Features Auditor, 5. Database & Data Integrity Auditor, 6. Security Auditor, 7. TypeScript & Code Quality Auditor (+7 more)
### Community 92 - "OrdersService"
Cohesion: 0.07
Cohesion: 0.06
Nodes (35): CreateOrderDto, OrderItemDto, ApiProperty, ApiPropertyOptional, IsArray, IsNotEmpty, IsNumber, IsOptional (+27 more)
### Community 93 - "ArchivePage.tsx"
Cohesion: 0.13
Nodes (15): ArchivePage(), ArchiveProductCard(), CATEGORY_MAP, ICON_MAP, BannerPlacement(), BannerPlacementProps, ProductCardSkeleton(), B2BInquiry (+7 more)
Cohesion: 0.08
Nodes (21): ArchivePage(), ArchiveProductCard(), CATEGORY_MAP, ICON_MAP, BannerPlacement(), BannerPlacementProps, OrderRowSkeleton(), PetProfileSkeleton() (+13 more)
### Community 94 - "Phase 3.1 — Human Review Preparation and Master Backlog Critique"
Cohesion: 0.13
Nodes (14): 10. Dependency & Wave Adjustments, 11. Acceptance Criteria Refinements, 12. Business and Product Decision Classification, 13. Final Recommended Backlog Summary, 1. Executive Assessment, 2. Findings That Require No Change, 3. Findings Requiring Task Refinements, 4. Tasks Requiring Splitting (+6 more)
### Community 95 - "wiki/[slug]/page.tsx"
Cohesion: 0.19
Nodes (16): BlogPostPage(), generateMetadata(), getBlog(), revalidate, safeIso(), safeLocalDate(), generateMetadata(), revalidate (+8 more)
### Community 95 - "getSeoConfig"
Cohesion: 0.26
Nodes (11): BlogPostPage(), generateMetadata(), getBlog(), revalidate, safeIso(), safeLocalDate(), generateMetadata(), revalidate (+3 more)
### Community 96 - "compilerOptions"
Cohesion: 0.06
@ -735,8 +730,8 @@ Cohesion: 0.13
Nodes (15): scripts, build, build:nest, docs:generate, lint, seo:backfill, start, start:debug (+7 more)
### Community 99 - "BlogsController"
Cohesion: 0.14
Nodes (15): BlogsController, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Get (+7 more)
Cohesion: 0.18
Nodes (12): BlogsController, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Get (+4 more)
### Community 100 - "Deep Audit Summary Report"
Cohesion: 0.14
@ -755,24 +750,24 @@ Cohesion: 0.15
Nodes (12): 10. `TASK-VERIFY-001`, 1. `TASK-SEC-001`, 2. `TASK-SEC-002`, 3. `TASK-SEC-003`, 4. `TASK-FIN-001`, 5. `TASK-BUILD-001`, 6. `TASK-AUTH-001`, 7. `TASK-FE-001` (+4 more)
### Community 104 - "Products.tsx"
Cohesion: 0.10
Nodes (24): Input, InputProps, formatPriceWithCommas(), PriceInput(), PriceInputProps, toEnglishDigits(), Coupon, CouponFormData (+16 more)
Cohesion: 0.09
Nodes (25): Input, InputProps, formatPriceWithCommas(), PriceInput(), PriceInputProps, toEnglishDigits(), Coupon, CouponFormData (+17 more)
### Community 105 - "Operational Rules & Boundaries"
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.37
Nodes (10): CreateHeroBannerDto, CreateSmartAdvisorRuleDto, CreateVetTestimonialDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNumber, IsOptional (+2 more)
### Community 106 - "InitiatePaymentDto"
Cohesion: 0.43
Nodes (7): InitiatePaymentDto, InitiateWalletTopupDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString
### Community 107 - "PaginationDto"
Cohesion: 0.12
Nodes (15): AdminModule, Module, PaginationDto, SortOrder, ApiPropertyOptional, IsEnum, IsInt, IsOptional (+7 more)
Cohesion: 0.06
Nodes (27): BlogsModule, Module, BlogFilterDto, BlogsService, Injectable, PaginationDto, SortOrder, ApiPropertyOptional (+19 more)
### Community 108 - "PrismaService"
Cohesion: 0.06
Nodes (21): CategoryQuery, PetQuery, WikiQuery, RevalidationService, Injectable, MeliPayamakPattern, MeliPayamakResponse, SendPatternSmsOptions (+13 more)
Cohesion: 0.07
Nodes (23): CategoryQuery, DEFAULT_SMS_RULES, SMS_EVENT_DEFINITIONS, SmsEventVariable, SmsRule, MeliPayamakPattern, MeliPayamakResponse, SendPatternSmsOptions (+15 more)
### Community 109 - "1. Summary of Integrity Repairs Performed"
Cohesion: 0.17
@ -791,16 +786,16 @@ Cohesion: 0.18
Nodes (10): 1. Pre-Deployment Checklist, 2. Build Verification, 3. Deployment Strategy (Based on Target), 4. Post-Deployment Health Check, 5. Forbidden Actions, Expected JSON Output Schema, Operational Rules & Boundaries, Required Output Artifacts (What files to write/update) (+2 more)
### Community 113 - "PetsController"
Cohesion: 0.11
Nodes (13): PetsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Delete, Get (+5 more)
Cohesion: 0.10
Nodes (14): PetsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Delete, Get (+6 more)
### Community 114 - "AppService"
Cohesion: 0.29
Nodes (5): AppController, Controller, Get, AppService, Injectable
### Community 115 - "Spinner.tsx"
Cohesion: 0.07
Nodes (36): ImagePreviewModal(), ImagePreviewModalProps, getFileType(), Media, MediaSelector(), MediaSelectorProps, maxWidthClasses, Modal() (+28 more)
Cohesion: 0.10
Nodes (22): ImagePreviewModal(), ImagePreviewModalProps, getFileType(), Media, MediaSelector(), MediaSelectorProps, Spinner(), Doctor (+14 more)
### Community 116 - "Vazirmatn Changelog"
Cohesion: 0.18
@ -826,13 +821,13 @@ 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.09
Nodes (12): AdminController, ApiBearerAuth, ApiOperation, ApiTags, Controller, Delete, Get, Param (+4 more)
### Community 123 - "UsersService"
Cohesion: 0.07
Nodes (32): AddressDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEmpty, IsOptional, IsString, ApiPropertyOptional (+24 more)
Cohesion: 0.05
Nodes (42): DEFAULT_JWT_REFRESH_SECRET, DEFAULT_JWT_SECRET, getJwtSecret(), AuthModule, Module, JwtPayload, JwtStrategy, Injectable (+34 more)
### Community 124 - "Repository Map"
Cohesion: 0.20
@ -850,6 +845,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 - "Body"
Cohesion: 0.17
Nodes (3): Body, Post, CouponInput
### Community 129 - "Reports.tsx"
Cohesion: 0.20
Nodes (7): BestSellerItem, CategoryDistItem, COLORS, CustomTooltipProps, ReportData, TopCouponItem, Reports
@ -891,8 +890,8 @@ Cohesion: 0.22
Nodes (8): Arch Linux, bower, Install, npm, Shabnam Font, yarn, طریقه استفاده در صفحات وب:, نمونه متن Sample:
### Community 139 - "layout.tsx"
Cohesion: 0.43
Nodes (5): generateMetadata(), RootLayout(), lalezar, vazirmatn, generateOrganizationSchema()
Cohesion: 0.23
Nodes (8): generateMetadata(), RootLayout(), AnalyticsDeferred(), AnalyticsDeferredProps, Window, lalezar, vazirmatn, generateOrganizationSchema()
### Community 140 - "ErrorBoundary"
Cohesion: 0.22
@ -910,9 +909,13 @@ Nodes (7): backend, distMainPath, fs, http, path, { spawn, execSync }, waitForBa
Cohesion: 0.25
Nodes (6): app_module_1, core_1, fs, path, swagger_1, yaml
### Community 144 - "SafeImage.tsx"
Cohesion: 0.10
Nodes (20): BackButton(), BackButtonProps, BlogPostClientProps, PLAYBACK_RATES, PodcastInlinePlayer(), PodcastInlinePlayerProps, PLAYBACK_RATES, PodcastPlayerModal() (+12 more)
### Community 144 - "PodcastPlayerModal.tsx"
Cohesion: 0.40
Nodes (3): PLAYBACK_RATES, PodcastPlayerModal(), PodcastPlayerModalProps
### Community 145 - "AdminLoginDto"
Cohesion: 0.29
Nodes (6): AdminLoginDto, ApiProperty, IsEmail, IsNotEmpty, IsString, MinLength
### Community 146 - "System Discovery"
Cohesion: 0.25
@ -922,17 +925,21 @@ Nodes (7): Active Core Applications, Current Architecture, Evidence-Based Status
Cohesion: 0.16
Nodes (10): HomeController, ApiOkResponse, ApiOperation, ApiTags, Controller, Get, HomeModule, Module (+2 more)
### Community 148 - "VerifyOtpDto"
Cohesion: 0.33
Nodes (6): ApiProperty, IsNotEmpty, IsString, Matches, VerifyOtpDto, Length
### Community 149 - "wiki/[slug]/page.tsx"
Cohesion: 0.60
Nodes (5): generateMetadata(), getRelatedProducts(), getWikiTerm(), WikiTermPage(), generateMedicalWebPageSchema()
### Community 150 - "Product Requirement Document (PRD)"
Cohesion: 0.29
Nodes (6): 1. Executive Vision, 2. Target Audience, 3. Functional Requirements, 4. Non-Functional Requirements (Performance, Security), 5. Epic / Feature Breakdown, Product Requirement Document (PRD)
### Community 151 - "menu.module.ts"
Cohesion: 0.40
Nodes (3): MenuModule, Module, MenuType
### Community 152 - "auth.service.ts"
Cohesion: 0.09
Nodes (11): AdminLoginInput, AuthService, LoginInput, RegisterInput, Injectable, normalizeMobile(), RedisModule, Global (+3 more)
### Community 152 - "RevalidationService"
Cohesion: 0.10
Nodes (11): Optional, RevalidationModule, Global, Module, RevalidationService, Injectable, RedisModule, Global (+3 more)
### Community 153 - "exclude"
Cohesion: 0.22
@ -947,8 +954,8 @@ Cohesion: 0.67
Nodes (3): prisma, runSeoBackfill(), stripHtml()
### Community 158 - "media/[...path]/route.ts"
Cohesion: 0.60
Nodes (4): dynamic, GET(), getCandidateUrls(), HEAD()
Cohesion: 0.53
Nodes (5): dynamic, GET(), getCandidateUrls(), getMimeType(), HEAD()
### Community 159 - "with-vpn.sh"
Cohesion: 0.62
@ -1019,8 +1026,8 @@ Cohesion: 0.40
Nodes (4): activeFiles, errors, validationOutput, warnings
### Community 176 - "uploads/[...path]/route.ts"
Cohesion: 0.60
Nodes (4): dynamic, GET(), getCandidateUrls(), HEAD()
Cohesion: 0.53
Nodes (5): dynamic, GET(), getCandidateUrls(), getMimeType(), HEAD()
### Community 177 - "app/page.tsx"
Cohesion: 0.67
@ -1050,13 +1057,9 @@ Nodes (3): Architectural Overview, Critical Review Findings & Required Enhanceme
Cohesion: 0.50
Nodes (3): Overview & Content Foundation, SEO & Content Requirements, 🚀 SEO & Content Strategy Review (12_seo_content)
### Community 184 - "MetricsController"
Cohesion: 0.25
Nodes (5): ApiExcludeController, MetricsController, Controller, Get, Res
### Community 185 - "RouteErrorBoundary"
Cohesion: 0.22
Nodes (3): Props, RouteErrorBoundary, State
### Community 184 - "AppModule"
Cohesion: 0.12
Nodes (7): ApiExcludeController, AppModule, Module, MetricsController, Controller, Get, Res
### Community 191 - "graphify reference: add a URL and watch a folder"
Cohesion: 0.50
@ -1083,8 +1086,8 @@ Cohesion: 0.50
Nodes (3): Select, SelectOption, SelectProps
### Community 197 - "userStore.ts"
Cohesion: 0.08
Nodes (37): AuthModal, B2BPortal, CartDrawer, ClientLayout(), LoginModal, AuthModal(), AuthModalProps, extractOtpFromText() (+29 more)
Cohesion: 0.06
Nodes (33): AuthModal, ClientLayout(), LoginModal, AuthModal(), AuthModalProps, extractOtpFromText(), BrandLogo(), BrandLogoProps (+25 more)
### Community 199 - "SmsLogQueryDto"
Cohesion: 0.25
@ -1098,10 +1101,6 @@ Nodes (3): Deploy on Vercel, Getting Started, Learn More
Cohesion: 0.50
Nodes (3): COMPOSE_DOCKER_CLI_BUILD, DOCKER_BUILDKIT, deploy.sh script
### Community 204 - "zibal-ebank.service.ts"
Cohesion: 0.40
Nodes (4): EBankCheckoutListFilter, EBankCheckoutOptions, EBankIdentifiedPaymentFilter, EBankStatementFilter
### Community 211 - "Master Task Backlog (Phase 3.3)"
Cohesion: 0.67
Nodes (3): ADR-AUTH-001: Admin Panel Authentication Architecture & Token Management, Master Task Backlog (Phase 3.3), Phase 3 Master Execution Plan
@ -1110,12 +1109,8 @@ Nodes (3): ADR-AUTH-001: Admin Panel Authentication Architecture & Token Managem
Cohesion: 0.67
Nodes (3): Shabnam Font README, Shabnam Font Sample, Vazir Font
### Community 233 - "Reviews.tsx"
Cohesion: 0.40
Nodes (5): BlogCommentItem, ProductReview, Reviews(), toPersianDigits(), Reviews
### Community 298 - ".initiateOrderPayment"
Cohesion: 0.21
Cohesion: 0.28
Nodes (6): ApiBadRequestResponse, ApiOkResponse, Req, Res, Headers, Ip
### Community 317 - "revalidate/route.ts"
@ -1123,24 +1118,24 @@ Cohesion: 0.83
Nodes (3): GET(), handleRevalidate(), POST()
## Knowledge Gaps
- **1347 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1342 more)
- **1352 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1347 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.
- **95 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `Roles()` connect `Roles` to `CmsController`, `TicketsService`, `SettingsController`, `CreateReviewDto`, `JwtAuthGuard`, `MenuService`, `WholesaleApplyDto`, `B2BService`, `ContactService`, `FaqService`, `SslController`, `BannersService`, `TestimonialsService`, `IngredientsService`, `PrescriptionsController`, `SmartAdvisorService`, `ProductsService`, `payment.controller.ts`, `cms.controller.ts`?**
_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`?**
_High betweenness centrality (0.029) - this node is a cross-community bridge._
- **Why does `Roles()` connect `Roles` to `WholesaleApplyDto`, `B2BService`, `ContactService`, `FaqService`, `CmsController`, `tickets.controller.ts`, `SmsService`, `SslController`, `BannersService`, `ProductsService`, `ReviewsService`, `TestimonialsService`, `IngredientsService`, `JwtAuthGuard`, `PrescriptionsService`, `SmartAdvisorService`, `MenuService`?**
_High betweenness centrality (0.094) - this node is a cross-community bridge._
- **Why does `ApiResponse` connect `src/services/api.ts` to `BlogsController`, `PetsController`, `ProductsService`, `PaginationDto`, `AuthController`, `HomeController`, `UsersService`, `OrdersService`?**
_High betweenness centrality (0.066) - this node is a cross-community bridge._
- **Why does `JwtAuthGuard` connect `JwtAuthGuard` to `admin.module.ts`, `CmsController`, `tickets.controller.ts`, `PaginationDto`, `PetsController`, `ProductsService`, `DoctorQueryDto`, `PetsController`, `admin.controller.ts`, `CreateVideoDto`, `auth.service.ts`, `UsersService`, `OrdersService`?**
_High betweenness centrality (0.030) - 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._
_1352 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `app.module.ts` be split into smaller, more focused modules?**
_Cohesion score 0.06170598911070781 - nodes in this community are weakly interconnected._
_Cohesion score 0.06988120195667366 - nodes in this community are weakly interconnected._
- **Should `productService.ts` be split into smaller, more focused modules?**
_Cohesion score 0.05780885780885781 - nodes in this community are weakly interconnected._
- **Should `PetProfile.tsx` be split into smaller, more focused modules?**
_Cohesion score 0.11822660098522167 - nodes in this community are weakly interconnected._
_Cohesion score 0.05472837022132797 - nodes in this community are weakly interconnected._
- **Should `UserDashboard.tsx` be split into smaller, more focused modules?**
_Cohesion score 0.10752688172043011 - nodes in this community are weakly interconnected._

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