Files
CRM_Montzar/apps/web/src/lib/routines.ts
T

257 lines
7.6 KiB
TypeScript
Raw Normal View History

import axios from 'axios';
import type { InjectionKey, Ref } from 'vue';
import type {
ActivityPriority,
RoutineContextDto,
RoutineDayState,
RoutineEntityRefDto,
RoutineTaskDto,
RoutineTaskStatus,
} from '@mento/shared';
import {
addJalaliMonths,
dateToJalali,
formatJalaliLong,
formatJalaliShort,
jalaliMonthLength,
jalaliMonthName,
jalaliToDate,
toFaDigits,
} from '@/lib/jalali';
export {
ROUTINE_ENTITY_LINK_LABELS,
ROUTINE_RECURRENCE_LABELS,
ROUTINE_TASK_SOURCE_LABELS,
ROUTINE_TASK_STATUS_LABELS,
ROUTINE_WEEKDAY_LABELS,
} from '@mento/shared';
type BadgeVariant = 'neutral' | 'accent' | 'success' | 'warning' | 'danger' | 'info';
export const PRIORITY_LABELS: Record<ActivityPriority, string> = {
very_low: 'خیلی کم',
low: 'کم',
medium: 'متوسط',
high: 'زیاد',
very_high: 'خیلی زیاد',
};
export const PRIORITY_VARIANTS: Record<ActivityPriority, BadgeVariant> = {
very_low: 'neutral',
low: 'neutral',
medium: 'info',
high: 'warning',
very_high: 'danger',
};
export const STATUS_VARIANTS: Record<RoutineTaskStatus, BadgeVariant> = {
todo: 'neutral',
in_progress: 'info',
completed: 'success',
overdue: 'danger',
skipped: 'warning',
cancelled: 'neutral',
};
export const DAY_STATE_LABELS: Record<RoutineDayState, string> = {
complete: 'روز کامل',
partial: 'روز ناقص',
missed: 'روز از دست رفته',
empty: 'بدون کار',
};
export const DAY_STATE_SYMBOLS: Record<RoutineDayState, string> = {
complete: '✓',
partial: '△',
missed: '×',
empty: '·',
};
/** زمینه مشترک بخش کارهای روتین (کاربران، دسته‌ها، نقش مدیر، رفرش) */
export type RoutinesShellContext = {
context: Ref<RoutineContextDto | null>;
refreshTick: Ref<number>;
notifyChanged: () => void;
openTask: (id: string) => void;
};
export const ROUTINES_CTX: InjectionKey<RoutinesShellContext> = Symbol('routines-ctx');
/* ───────── تاریخ‌ها (کلید روز YYYY-MM-DD به وقت تهران) ───────── */
const TEHRAN_DAY = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Tehran',
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
const TEHRAN_TIME = new Intl.DateTimeFormat('en-GB', {
timeZone: 'Asia/Tehran',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
});
export function todayKey(): string {
return TEHRAN_DAY.format(new Date());
}
export function dayKeyOf(iso: string | Date): string {
return TEHRAN_DAY.format(typeof iso === 'string' ? new Date(iso) : iso);
}
/** Date محلی (نیمه‌شب) برای نمایش شمسی */
export function keyToDate(key: string): Date {
const [y, m, d] = key.split('-').map(Number);
return new Date(y!, m! - 1, d!);
}
export function dateToKey(d: Date): string {
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
export function addDaysKey(key: string, n: number): string {
const d = keyToDate(key);
d.setDate(d.getDate() + n);
return dateToKey(d);
}
/** ۰ = شنبه */
export function weekdayIndex(key: string): number {
return (keyToDate(key).getDay() + 1) % 7;
}
export function weekStart(key: string): string {
return addDaysKey(key, -weekdayIndex(key));
}
export function jalaliMonthRange(key: string): { from: string; to: string; label: string } {
const { jy, jm } = dateToJalali(keyToDate(key));
const from = dateToKey(jalaliToDate(jy, jm, 1));
const to = dateToKey(jalaliToDate(jy, jm, jalaliMonthLength(jy, jm)));
return { from, to, label: `${jalaliMonthName(jm)} ${toFaDigits(jy)}` };
}
export function shiftJalaliMonth(key: string, delta: number): string {
const { jy, jm } = dateToJalali(keyToDate(key));
const next = addJalaliMonths(jy, jm, delta);
return dateToKey(jalaliToDate(next.jy, next.jm, 1));
}
export function formatDayLong(key: string): string {
return formatJalaliLong(keyToDate(key));
}
export function formatDayShort(key: string): string {
return formatJalaliShort(keyToDate(key));
}
export function formatDayNumeric(key: string): string {
const { jm, jd } = dateToJalali(keyToDate(key));
return `${toFaDigits(jd)} ${jalaliMonthName(jm)}`;
}
export function formatTime(iso: string | null | undefined): string {
if (!iso) return '—';
return toFaDigits(TEHRAN_TIME.format(new Date(iso)));
}
export function timeOf(iso: string | null | undefined): string {
if (!iso) return '';
return TEHRAN_TIME.format(new Date(iso));
}
/** زمان پایان روز بدون ساعت مشخص (۲۳:۵۹) در UI نمایش داده نمی‌شود */
export function hasExplicitTime(task: Pick<RoutineTaskDto, 'dueAt'>): boolean {
return !!task.dueAt && timeOf(task.dueAt) !== '23:59';
}
export function formatDelay(iso: string | null | undefined): string {
if (!iso) return '—';
const diff = Date.now() - new Date(iso).getTime();
if (diff <= 0) return '—';
const minutes = Math.floor(diff / 60_000);
if (minutes < 60) return `${toFaDigits(minutes)} دقیقه`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${toFaDigits(hours)} ساعت`;
return `${toFaDigits(Math.floor(hours / 24))} روز`;
}
export function formatMinutes(min: number | null | undefined): string {
if (min == null) return '—';
if (min < 60) return `${toFaDigits(min)} دقیقه`;
const h = Math.floor(min / 60);
const m = min % 60;
return m ? `${toFaDigits(h)} ساعت و ${toFaDigits(m)} دقیقه` : `${toFaDigits(h)} ساعت`;
}
export function formatPercent(p: number | null | undefined): string {
return p == null ? '—' : `${toFaDigits(p)}٪`;
}
export function percentTone(p: number | null | undefined): 'good' | 'mid' | 'low' | 'none' {
if (p == null) return 'none';
if (p >= 85) return 'good';
if (p >= 60) return 'mid';
return 'low';
}
export function isoToLocalInput(iso: string | null | undefined): { date: string; time: string } {
if (!iso) return { date: '', time: '' };
return { date: dayKeyOf(iso), time: timeOf(iso) };
}
/** ورودی تاریخ/ساعت همیشه به وقت تهران تفسیر می‌شود */
export function localInputToIso(date: string, time: string): string | null {
if (!date) return null;
const d = new Date(`${date}T${time || '23:59'}:00+03:30`);
return Number.isNaN(d.getTime()) ? null : d.toISOString();
}
/* ───────── پیوند پرونده‌ها ───────── */
export const ENTITY_TYPE_LABELS: Record<RoutineEntityRefDto['type'], string> = {
lead: 'لید',
person: 'مشتری',
company: 'شرکت',
deal: 'معامله',
invoice: 'فاکتور',
activity: 'پیگیری',
};
export function entityRoute(ref: RoutineEntityRefDto): string {
switch (ref.type) {
case 'lead':
return `/leads/${ref.id}`;
case 'person':
return `/contacts/people/${ref.id}`;
case 'company':
return `/contacts/companies/${ref.id}`;
case 'deal':
return `/deals/${ref.id}`;
case 'invoice':
return `/invoices/${ref.id}`;
case 'activity':
return `/activities/${ref.id}`;
default:
return '/';
}
}
export function apiError(e: unknown, fallback: string): string {
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
if (Array.isArray(msg)) return msg.join('، ');
if (msg) return msg;
if (!e.response) return 'اتصال به سرور برقرار نشد.';
}
return fallback;
}
export function isOpenStatus(status: RoutineTaskStatus): boolean {
return status === 'todo' || status === 'in_progress' || status === 'overdue';
}