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
+103
View File
@@ -0,0 +1,103 @@
import type { GamificationEvent, GamificationTxDto } from '@mento/shared';
import { GAMIFICATION_EVENTS } from '@mento/shared';
import { toFaDigits } from '@/lib/jalali';
export { apiError } from '@/lib/routines';
type IconName =
| 'trophy'
| 'user-plus'
| 'funnel'
| 'briefcase'
| 'phone'
| 'calendar'
| 'document'
| 'check'
| 'refresh'
| 'dollar'
| 'chat'
| 'star'
| 'medal'
| 'gift'
| 'sliders'
| 'building'
| 'trend';
const EVENT_ICONS: Record<GamificationEvent, IconName> = {
LEAD_CREATED: 'funnel',
LEAD_STAGE_CHANGED: 'trend',
LEAD_CONVERTED: 'user-plus',
CUSTOMER_CREATED: 'user-plus',
COMPANY_CREATED: 'building',
DEAL_CREATED: 'briefcase',
DEAL_STAGE_CHANGED: 'trend',
DEAL_WON: 'trophy',
DEAL_LOST: 'briefcase',
CALL_LOGGED: 'phone',
MEETING_CREATED: 'calendar',
FOLLOW_UP_CREATED: 'calendar',
NOTE_CREATED: 'document',
ACTIVITY_CREATED: 'check',
TASK_COMPLETED: 'check',
ROUTINE_COMPLETED: 'refresh',
QUOTE_CREATED: 'document',
INVOICE_ISSUED: 'dollar',
PAYMENT_REGISTERED: 'dollar',
TICKET_CREATED: 'chat',
TICKET_RESOLVED: 'chat',
};
export function txIcon(tx: Pick<GamificationTxDto, 'kind' | 'event'>): IconName {
if (tx.kind === 'bonus') return 'star';
if (tx.kind === 'achievement') return 'medal';
if (tx.kind === 'redeem' || tx.kind === 'refund') return 'gift';
if (tx.kind === 'adjust') return 'sliders';
return tx.event ? EVENT_ICONS[tx.event] : 'star';
}
export function eventLabel(event: GamificationEvent | null | undefined): string {
return event ? GAMIFICATION_EVENTS[event]?.label ?? event : '';
}
/** عدد فارسی با جداکننده هزارگان */
export function faNum(value: number | null | undefined): string {
if (value == null || !Number.isFinite(value)) return '—';
return toFaDigits(Math.round(value).toLocaleString('en-US'));
}
export function signedFa(value: number): string {
if (!value) return toFaDigits(0);
return `${value > 0 ? '+' : '−'}${faNum(Math.abs(value))}`;
}
/** «۵ دقیقه پیش»، «دیروز ۱۴:۲۰» … */
export function timeAgo(iso: string): string {
const d = new Date(iso);
const diff = Date.now() - d.getTime();
const min = Math.floor(diff / 60_000);
if (min < 1) return 'همین الان';
if (min < 60) return `${toFaDigits(min)} دقیقه پیش`;
const hours = Math.floor(min / 60);
if (hours < 24) return `${toFaDigits(hours)} ساعت پیش`;
const days = Math.floor(hours / 24);
if (days < 7) return `${toFaDigits(days)} روز پیش`;
return new Intl.DateTimeFormat('fa-IR', { dateStyle: 'medium' }).format(d);
}
export function formatDateTime(iso: string): string {
try {
return new Intl.DateTimeFormat('fa-IR', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(iso));
} catch {
return iso;
}
}
export const REWARD_ICONS = ['gift', 'star', 'trophy', 'medal', 'crown', 'calendar-sun', 'bag', 'target', 'fire'] as const;
export const BADGE_ICONS = ['medal', 'trophy', 'crown', 'star', 'fire', 'phone', 'users', 'funnel', 'target', 'check'] as const;
export const ROLE_LABELS: Record<string, string> = {
owner: 'مالک',
admin: 'مدیر',
sales: 'فروش',
support: 'پشتیبانی',
};