Add Didar gap features: lost-reason reports, audit logs, tags, card assignees, and saved-views API.

Ignore Graphify local artifacts and expose new modules in superadmin.
This commit is contained in:
2026-09-26 14:59:48 +03:30
parent 35a618d0af
commit 6463275c3d
30 changed files with 1406 additions and 21 deletions
+34
View File
@@ -76,6 +76,7 @@ export class ReportsService {
stageId: true,
ownerId: true,
source: true,
lostReason: true,
updatedAt: true,
createdAt: true,
lineItems: {
@@ -402,6 +403,27 @@ export class ReportsService {
}))
.sort((a, b) => b.count - a.count);
const lostReasonMap = new Map<string, { count: number; amount: number }>();
for (const d of lostInPeriod) {
const label = normalizeLostReason(d.lostReason);
const prev = lostReasonMap.get(label) ?? { count: 0, amount: 0 };
prev.count += 1;
prev.amount += decimalToBase(d.amount, d.currency, rates);
lostReasonMap.set(label, prev);
}
const lostReasonTotal =
[...lostReasonMap.values()].reduce((a, b) => a + b.count, 0) || 1;
const lostReasons: ReportsDashboardDto['lostReasons'] = [
...lostReasonMap.entries(),
]
.map(([label, row]) => ({
label,
count: row.count,
amount: Math.round(row.amount),
percent: Math.round((row.count / lostReasonTotal) * 1000) / 10,
}))
.sort((a, b) => b.count - a.count);
type LineAgg = {
name: string;
category: string;
@@ -636,6 +658,11 @@ export class ReportsService {
`بیشترین منبع معاملات برنده «${sources[0].label}» بوده است.`,
);
}
if (lostReasons[0] && lostReasons[0].count > 0) {
insights.push(
`بیشترین دلیل شکست معاملات «${lostReasons[0].label}» بوده است (${lostReasons[0].count} مورد).`,
);
}
}
return {
@@ -647,6 +674,7 @@ export class ReportsService {
monthlyComparison,
customerGrowth,
sources,
lostReasons,
salesByCategory,
topProducts,
customerStatus,
@@ -1052,6 +1080,12 @@ function normalizeSource(raw: string | null | undefined): string {
return map[value.toLowerCase()] ?? value;
}
function normalizeLostReason(raw: string | null | undefined): string {
const value = raw?.trim();
if (!value) return 'بدون دلیل ثبت‌شده';
return value;
}
function activityTypeLabel(type: string): string {
const map: Record<string, string> = {
call: 'تماس',