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
+29
View File
@@ -18,6 +18,7 @@ import { NotificationsService } from '../notifications/notifications.service';
import { AutomationsService } from '../automations/automations.service';
import { MessageAutomationsService } from '../message-automations/message-automations.service';
import { WebhooksService } from '../public-api/webhooks.service';
import { GamificationService } from '../gamification/gamification.service';
import {
type Actor,
canReassignOwner,
@@ -49,6 +50,7 @@ export class DealsService {
private readonly automations: AutomationsService,
private readonly messageAutomations: MessageAutomationsService,
private readonly webhooks: WebhooksService,
private readonly game: GamificationService,
) {}
async get(
@@ -188,6 +190,15 @@ export class DealsService {
};
await this.automations.runTrigger('deal_created', createdCtx);
await this.messageAutomations.onEvent('deal_created', createdCtx);
this.game.emit({
organizationId,
userId: actor.userId,
event: 'DEAL_CREATED',
entityType: 'deal',
entityId: created.id,
entityTitle: created.title,
facts: { amount: created.amount == null ? null : Number(created.amount) },
});
this.webhooks.emit(organizationId, 'deal.created', {
id: created.id,
title: created.title,
@@ -390,6 +401,24 @@ export class DealsService {
};
await this.automations.runTrigger('deal_stage_changed', ctx);
await this.messageAutomations.onEvent('deal_stage_changed', ctx);
const gameBase = {
organizationId,
userId: deal.ownerId ?? actor.userId,
entityType: 'deal',
entityId: deal.id,
entityTitle: deal.title,
facts: {
stageName: stage.name,
amount: row.amount == null ? null : Number(row.amount),
},
};
this.game.emit({ ...gameBase, event: 'DEAL_STAGE_CHANGED', eventKey: `${deal.id}:${stage.id}` });
if (status === 'won' && deal.status !== 'won') {
this.game.emit({ ...gameBase, event: 'DEAL_WON' });
}
if (status === 'lost' && deal.status !== 'lost') {
this.game.emit({ ...gameBase, event: 'DEAL_LOST' });
}
if (status === 'won' && deal.status !== 'won') {
await this.automations.runTrigger('deal_won', ctx);
await this.messageAutomations.onEvent('deal_won', ctx);