Files
CRM_Montzar/apps/web/src/lib/gamification.ts
T
MahdiandCursor 60d797f184 feat: باشگاه امتیازات کارکنان (موتور امتیاز، جدول رتبه، فروشگاه پاداش، نشان‌ها، پنل مدیریت و گزارش ROI)
- موتور رویدادمحور با شرط، محدودیت، تأیید مدیر و idempotency
- اتصال به لید، مشتری، معامله، فعالیت، روتین، پیش‌فاکتور، فاکتور و تیکت
- صفحه /club و /club/admin، اعلان زنده امتیاز، آیتم منو
- انتقال فرم‌ساز سرنخ به ابزارها

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 21:28:49 +03:30

104 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: 'پشتیبانی',
};