104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
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: 'پشتیبانی',
|
|||
|
|
};
|