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