feat: باشگاه امتیازات کارکنان (موتور امتیاز، جدول رتبه، فروشگاه پاداش، نشان‌ها، پنل مدیریت و گزارش ROI)

- موتور رویدادمحور با شرط، محدودیت، تأیید مدیر و idempotency
- اتصال به لید، مشتری، معامله، فعالیت، روتین، پیش‌فاکتور، فاکتور و تیکت
- صفحه /club و /club/admin، اعلان زنده امتیاز، آیتم منو
- انتقال فرم‌ساز سرنخ به ابزارها

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-10 21:28:49 +03:30
co-authored by Cursor
parent b87ebb22e0
commit 60d797f184
51 changed files with 11298 additions and 806 deletions
+491
View File
@@ -0,0 +1,491 @@
/** رویدادهای امتیازدهی — موتور پاداش فقط به همین رویدادها گوش می‌دهد */
export type GamificationEvent =
| "LEAD_CREATED"
| "LEAD_STAGE_CHANGED"
| "LEAD_CONVERTED"
| "CUSTOMER_CREATED"
| "COMPANY_CREATED"
| "DEAL_CREATED"
| "DEAL_STAGE_CHANGED"
| "DEAL_WON"
| "DEAL_LOST"
| "CALL_LOGGED"
| "MEETING_CREATED"
| "FOLLOW_UP_CREATED"
| "NOTE_CREATED"
| "ACTIVITY_CREATED"
| "TASK_COMPLETED"
| "ROUTINE_COMPLETED"
| "QUOTE_CREATED"
| "INVOICE_ISSUED"
| "PAYMENT_REGISTERED"
| "TICKET_CREATED"
| "TICKET_RESOLVED";
export type GamificationFieldType = "number" | "string" | "boolean";
export interface GamificationFieldMeta {
label: string;
type: GamificationFieldType;
/** مقادیر پیشنهادی برای فیلدهای متنی */
options?: Array<{ value: string; label: string }>;
}
/** فیلدهای قابل استفاده در شرط‌های Rule Builder */
export const GAMIFICATION_FIELDS: Record<string, GamificationFieldMeta> = {
amount: { label: "مبلغ", type: "number" },
stageName: { label: "نام مرحله", type: "string" },
source: { label: "منبع", type: "string" },
activityType: {
label: "نوع فعالیت",
type: "string",
options: [
{ value: "call", label: "تماس" },
{ value: "meeting", label: "جلسه" },
{ value: "task", label: "کار" },
{ value: "note", label: "یادداشت" },
],
},
typeName: { label: "عنوان نوع فعالیت", type: "string" },
onTime: { label: "انجام به‌موقع", type: "boolean" },
isMandatory: { label: "اجباری", type: "boolean" },
priority: {
label: "اولویت",
type: "string",
options: [
{ value: "very_low", label: "خیلی کم" },
{ value: "low", label: "کم" },
{ value: "medium", label: "متوسط" },
{ value: "normal", label: "عادی (تیکت)" },
{ value: "high", label: "زیاد" },
{ value: "very_high", label: "خیلی زیاد" },
{ value: "urgent", label: "فوری (تیکت)" },
],
},
hasDescription: { label: "توضیحات دارد", type: "boolean" },
hour: { label: "ساعت انجام (۰ تا ۲۳)", type: "number" },
weekday: { label: "روز هفته (۰=شنبه … ۶=جمعه)", type: "number" },
};
const COMMON_FIELDS = ["hour", "weekday"];
export interface GamificationEventMeta {
label: string;
/** متن کوتاه برای فید امتیازات، مثل «معامله برنده شد» */
feed: string;
group: "leads" | "customers" | "sales" | "activities" | "finance" | "support";
fields: string[];
}
export const GAMIFICATION_EVENTS: Record<GamificationEvent, GamificationEventMeta> = {
LEAD_CREATED: { label: "ثبت لید جدید", feed: "لید جدید ثبت شد", group: "leads", fields: ["source", "amount", ...COMMON_FIELDS] },
LEAD_STAGE_CHANGED: { label: "تغییر مرحله لید", feed: "لید به مرحله بعد رفت", group: "leads", fields: ["stageName", "source", ...COMMON_FIELDS] },
LEAD_CONVERTED: { label: "تبدیل لید به مشتری", feed: "لید به مشتری تبدیل شد", group: "leads", fields: ["source", "amount", ...COMMON_FIELDS] },
CUSTOMER_CREATED: { label: "ثبت مشتری جدید", feed: "مشتری جدید ثبت شد", group: "customers", fields: ["source", ...COMMON_FIELDS] },
COMPANY_CREATED: { label: "ثبت شرکت جدید", feed: "شرکت جدید ثبت شد", group: "customers", fields: [...COMMON_FIELDS] },
DEAL_CREATED: { label: "ایجاد معامله", feed: "معامله جدید ایجاد شد", group: "sales", fields: ["amount", ...COMMON_FIELDS] },
DEAL_STAGE_CHANGED: { label: "جابه‌جایی معامله در مسیر فروش", feed: "معامله به مرحله بعد رفت", group: "sales", fields: ["stageName", "amount", ...COMMON_FIELDS] },
DEAL_WON: { label: "برنده شدن معامله", feed: "معامله برنده شد", group: "sales", fields: ["amount", "stageName", ...COMMON_FIELDS] },
DEAL_LOST: { label: "از دست رفتن معامله", feed: "معامله بسته شد", group: "sales", fields: ["amount", ...COMMON_FIELDS] },
CALL_LOGGED: { label: "ثبت تماس", feed: "تماس ثبت شد", group: "activities", fields: ["hasDescription", ...COMMON_FIELDS] },
MEETING_CREATED: { label: "ثبت جلسه", feed: "جلسه ثبت شد", group: "activities", fields: ["hasDescription", ...COMMON_FIELDS] },
FOLLOW_UP_CREATED: { label: "ثبت پیگیری", feed: "پیگیری ثبت شد", group: "activities", fields: ["hasDescription", ...COMMON_FIELDS] },
NOTE_CREATED: { label: "ثبت یادداشت", feed: "یادداشت ثبت شد", group: "activities", fields: ["hasDescription", ...COMMON_FIELDS] },
ACTIVITY_CREATED: { label: "ثبت فعالیت", feed: "فعالیت ثبت شد", group: "activities", fields: ["activityType", "typeName", "priority", "hasDescription", ...COMMON_FIELDS] },
TASK_COMPLETED: { label: "انجام کار", feed: "کار انجام شد", group: "activities", fields: ["activityType", "typeName", "onTime", "priority", ...COMMON_FIELDS] },
ROUTINE_COMPLETED: { label: "انجام کار روتین", feed: "کار روتین انجام شد", group: "activities", fields: ["onTime", "isMandatory", "priority", ...COMMON_FIELDS] },
QUOTE_CREATED: { label: "ثبت پیش‌فاکتور", feed: "پیش‌فاکتور ثبت شد", group: "finance", fields: ["amount", ...COMMON_FIELDS] },
INVOICE_ISSUED: { label: "صدور فاکتور", feed: "فاکتور صادر شد", group: "finance", fields: ["amount", ...COMMON_FIELDS] },
PAYMENT_REGISTERED: { label: "ثبت پرداخت", feed: "پرداخت ثبت شد", group: "finance", fields: ["amount", ...COMMON_FIELDS] },
TICKET_CREATED: { label: "ثبت تیکت", feed: "تیکت ثبت شد", group: "support", fields: ["priority", ...COMMON_FIELDS] },
TICKET_RESOLVED: { label: "حل تیکت", feed: "تیکت حل شد", group: "support", fields: ["priority", ...COMMON_FIELDS] },
};
export const GAMIFICATION_EVENT_KEYS = Object.keys(GAMIFICATION_EVENTS) as GamificationEvent[];
export const GAMIFICATION_EVENT_GROUP_LABELS: Record<GamificationEventMeta["group"], string> = {
leads: "لیدها",
customers: "مشتریان",
sales: "فروش",
activities: "فعالیت‌ها",
finance: "مالی",
support: "پشتیبانی",
};
export function isGamificationEvent(value: unknown): value is GamificationEvent {
return typeof value === "string" && value in GAMIFICATION_EVENTS;
}
export type GamificationConditionOp =
| "eq"
| "neq"
| "gt"
| "gte"
| "lt"
| "lte"
| "contains"
| "in"
| "is_true"
| "is_false";
export const GAMIFICATION_CONDITION_OPS: Record<GamificationConditionOp, string> = {
eq: "برابر",
neq: "مخالف",
gt: "بیشتر از",
gte: "حداقل",
lt: "کمتر از",
lte: "حداکثر",
contains: "شامل",
in: "یکی از (با ویرگول)",
is_true: "بله باشد",
is_false: "خیر باشد",
};
export interface GamificationCondition {
field: string;
op: GamificationConditionOp;
value?: string | number | null;
}
export type GamificationPeriod = "today" | "week" | "month" | "quarter" | "year" | "all";
export const GAMIFICATION_PERIOD_LABELS: Record<GamificationPeriod, string> = {
today: "امروز",
week: "این هفته",
month: "این ماه",
quarter: "این فصل",
year: "امسال",
all: "همه زمان‌ها",
};
export type GamificationTxKind = "rule" | "bonus" | "achievement" | "adjust" | "redeem" | "refund";
export type GamificationTxStatus = "approved" | "pending" | "rejected";
export interface GamificationLevelDef {
name: string;
minScore: number;
}
export interface GamificationLevelDto {
level: number;
name: string;
minScore: number;
nextName: string | null;
nextMinScore: number | null;
/** درصد پیشرفت تا سطح بعد */
progress: number;
toNext: number | null;
}
export interface GamificationRuleDto {
id: string;
name: string;
description: string | null;
event: GamificationEvent;
score: number;
coins: number;
conditions: GamificationCondition[];
isActive: boolean;
maxPerDay: number | null;
maxCoinsPerDay: number | null;
maxPerUser: number | null;
cooldownMinutes: number | null;
oncePerEntity: boolean;
firstTimeOnly: boolean;
requiresApproval: boolean;
/** نقش‌های مشمول؛ خالی = همه */
roles: string[];
/** تیم‌های مشمول؛ خالی = همه */
teamIds: string[];
position: number;
awardedCount: number;
createdAt: string;
updatedAt: string;
}
export interface SaveGamificationRuleRequest {
name: string;
description?: string | null;
event: GamificationEvent;
score: number;
coins: number;
conditions?: GamificationCondition[];
isActive?: boolean;
maxPerDay?: number | null;
maxCoinsPerDay?: number | null;
maxPerUser?: number | null;
cooldownMinutes?: number | null;
oncePerEntity?: boolean;
firstTimeOnly?: boolean;
requiresApproval?: boolean;
roles?: string[];
teamIds?: string[];
}
export interface GamificationTxDto {
id: string;
userId: string;
userName: string;
kind: GamificationTxKind;
event: GamificationEvent | null;
title: string;
score: number;
coins: number;
status: GamificationTxStatus;
entityType: string | null;
entityId: string | null;
entityTitle: string | null;
/** لینک پرونده مرتبط در CRM */
link: string | null;
reason: string | null;
createdByName: string | null;
createdAt: string;
}
export interface GamificationLeaderEntryDto {
rank: number;
userId: string;
fullName: string;
hasAvatar: boolean;
score: number;
/** تغییر رتبه نسبت به ۲۴ ساعت قبل؛ مثبت = صعود */
change: number | null;
levelName: string;
isMe: boolean;
}
export interface GamificationLeaderboardDto {
period: GamificationPeriod;
from: string | null;
totalUsers: number;
items: GamificationLeaderEntryDto[];
/** کاربر جاری همیشه برمی‌گردد، حتی اگر در فهرست نباشد */
me: GamificationLeaderEntryDto | null;
}
export interface GamificationTeamEntryDto {
rank: number;
teamId: string;
name: string;
members: number;
score: number;
avgScore: number;
isMine: boolean;
}
export interface GamificationRewardDto {
id: string;
name: string;
description: string | null;
cost: number;
icon: string;
color: string;
stock: number | null;
isActive: boolean;
requiresApproval: boolean;
position: number;
}
export interface SaveGamificationRewardRequest {
name: string;
description?: string | null;
cost: number;
icon?: string;
color?: string;
stock?: number | null;
isActive?: boolean;
requiresApproval?: boolean;
}
export type GamificationRedemptionStatus = "pending" | "approved" | "rejected" | "delivered";
export const GAMIFICATION_REDEMPTION_LABELS: Record<GamificationRedemptionStatus, string> = {
pending: "در انتظار تأیید",
approved: "تأیید شده",
rejected: "رد شده",
delivered: "تحویل شده",
};
export interface GamificationRedemptionDto {
id: string;
userId: string;
userName: string;
rewardId: string;
rewardName: string;
rewardIcon: string;
cost: number;
status: GamificationRedemptionStatus;
note: string | null;
decisionNote: string | null;
decidedByName: string | null;
decidedAt: string | null;
createdAt: string;
}
export type GamificationBadgeMetric =
| "event_count"
| "score_total"
| "routine_streak"
| "single_deal_amount";
export const GAMIFICATION_BADGE_METRICS: Record<GamificationBadgeMetric, string> = {
event_count: "تعداد دفعات یک رویداد",
score_total: "مجموع امتیاز",
routine_streak: "روزهای پیاپی انجام کامل روتین‌ها",
single_deal_amount: "مبلغ یک معامله برنده",
};
export interface GamificationBadgeDto {
id: string;
name: string;
description: string | null;
icon: string;
color: string;
metric: GamificationBadgeMetric;
event: GamificationEvent | null;
threshold: number;
coins: number;
isActive: boolean;
earnedCount: number;
}
export interface SaveGamificationBadgeRequest {
name: string;
description?: string | null;
icon?: string;
color?: string;
metric: GamificationBadgeMetric;
event?: GamificationEvent | null;
threshold: number;
coins?: number;
isActive?: boolean;
}
export interface GamificationAchievementDto extends GamificationBadgeDto {
earned: boolean;
earnedAt: string | null;
/** مقدار فعلی کاربر برای این شاخص */
current: number;
progress: number;
}
export type GamificationBonusKind = "event_count_period" | "routine_streak" | "top_of_month";
export const GAMIFICATION_BONUS_KINDS: Record<GamificationBonusKind, string> = {
event_count_period: "تعداد رویداد در بازه",
routine_streak: "روزهای پیاپی انجام کامل روتین",
top_of_month: "نفر اول امتیاز ماه",
};
export interface GamificationBonusDto {
id: string;
name: string;
description: string | null;
kind: GamificationBonusKind;
event: GamificationEvent | null;
threshold: number;
period: "day" | "week" | "month";
score: number;
coins: number;
isActive: boolean;
awardedCount: number;
}
export interface SaveGamificationBonusRequest {
name: string;
description?: string | null;
kind: GamificationBonusKind;
event?: GamificationEvent | null;
threshold: number;
period?: "day" | "week" | "month";
score?: number;
coins?: number;
isActive?: boolean;
}
export interface GamificationSettingsDto {
enabled: boolean;
levels: GamificationLevelDef[];
/** هدف ماهانه امتیاز برای کارت «هدف بعدی» */
monthlyGoal: number;
/** نمایش کامل جدول برای همه؛ اگر خاموش باشد فقط ۱۰ نفر اول و خود کاربر */
showFullLeaderboard: boolean;
enabledAt: string | null;
}
export interface GamificationMeDto {
user: { id: string; fullName: string; hasAvatar: boolean };
isManager: boolean;
settings: GamificationSettingsDto;
score: number;
coins: number;
period: GamificationPeriod;
periodScore: number;
todayScore: number;
rank: number | null;
totalUsers: number;
/** فاصله تا نفر بالاتر */
gapToNext: number | null;
nextRankName: string | null;
gapToFirst: number | null;
level: GamificationLevelDto;
/** بهترین امتیاز ماهانه کاربر تا امروز */
personalBest: number;
goal: { target: number; current: number; progress: number };
nextReward: { id: string; name: string; cost: number; icon: string; remaining: number } | null;
streakDays: number;
badgesEarned: number;
pendingRedemptions: number;
recent: GamificationTxDto[];
}
export interface GamificationPulseDto {
serverTime: string;
score: number;
coins: number;
rank: number | null;
items: GamificationTxDto[];
}
export interface GamificationAdjustRequest {
userId: string;
amount: number;
reason: string;
}
export interface GamificationReportUserRow {
userId: string;
fullName: string;
value: number;
}
export interface GamificationRoiMetric {
key: string;
label: string;
before: number;
after: number;
/** درصد تغییر؛ null وقتی مبنا صفر است */
changePct: number | null;
}
export interface GamificationReportDto {
period: GamificationPeriod;
totals: {
score: number;
coinsEarned: number;
coinsSpent: number;
activeUsers: number;
totalUsers: number;
transactions: number;
pendingApprovals: number;
pendingRedemptions: number;
};
byEvent: Array<{ event: GamificationEvent; label: string; count: number; score: number; coins: number }>;
topActive: GamificationReportUserRow[];
topGrowth: GamificationReportUserRow[];
topDeals: GamificationReportUserRow[];
topLeads: GamificationReportUserRow[];
topTasks: GamificationReportUserRow[];
inactive: GamificationReportUserRow[];
roi: { since: string | null; windowDays: number; metrics: GamificationRoiMetric[] };
}
+9 -2
View File
@@ -139,7 +139,8 @@ export type OrgModuleKey =
| "reservations"
| "leads"
| "conversations"
| "routines";
| "routines"
| "gamification";
export const ORG_MODULE_KEYS: OrgModuleKey[] = [
"customers",
@@ -178,6 +179,7 @@ export const ORG_MODULE_KEYS: OrgModuleKey[] = [
"reservations",
"conversations",
"routines",
"gamification",
];
export const ORG_MODULE_LABELS: Record<OrgModuleKey, string> = {
@@ -217,6 +219,7 @@ export const ORG_MODULE_LABELS: Record<OrgModuleKey, string> = {
reservations: "رزرو و نوبت‌دهی",
conversations: "گفتگو",
routines: "کارهای روتین",
gamification: "باشگاه امتیازات",
};
export const ORG_MODULE_GROUPS: Array<{
@@ -236,7 +239,7 @@ export const ORG_MODULE_GROUPS: Array<{
"sales_forecast",
],
},
{ title: "فعالیت‌ها", keys: ["activities", "routines", "notes"] },
{ title: "فعالیت‌ها", keys: ["activities", "routines", "notes", "gamification"] },
{ title: "پشتیبانی", keys: ["tickets", "faq", "conversations"] },
{
title: "اتوماسیون",
@@ -5211,3 +5214,7 @@ export interface RoutineUserSettingsDto {
/** ساعت ارسال خلاصه پایان روز (۰ تا ۲۳) */
summaryHour: number;
}
/* ───────── باشگاه امتیازات (گیمیفیکیشن کارکنان) ───────── */
export * from "./gamification";