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

257 lines
7.6 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 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';
}