Initial Mento CRM: portal OTP, reports, SMS, and deploy for crm.montzar.ir.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: '/api',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const token =
|
||||
localStorage.getItem('mento_token') ?? localStorage.getItem('ment_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
if (typeof FormData !== 'undefined' && config.data instanceof FormData) {
|
||||
if (config.headers && 'Content-Type' in config.headers) {
|
||||
delete config.headers['Content-Type'];
|
||||
}
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||
const path = window.location.pathname;
|
||||
if (path.startsWith('/p/')) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
localStorage.removeItem('mento_token');
|
||||
localStorage.removeItem('mento_user');
|
||||
localStorage.removeItem('ment_token');
|
||||
localStorage.removeItem('ment_user');
|
||||
if (!path.startsWith('/login')) {
|
||||
const redirect = encodeURIComponent(path + window.location.search);
|
||||
window.location.assign(`/login?redirect=${redirect}`);
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,182 @@
|
||||
/** تبدیل و ابزار تقویم شمسی (الگوریتم استاندارد جلالی) */
|
||||
|
||||
export type JalaliDate = { jy: number; jm: number; jd: number };
|
||||
|
||||
const MONTH_NAMES = [
|
||||
'فروردین',
|
||||
'اردیبهشت',
|
||||
'خرداد',
|
||||
'تیر',
|
||||
'مرداد',
|
||||
'شهریور',
|
||||
'مهر',
|
||||
'آبان',
|
||||
'آذر',
|
||||
'دی',
|
||||
'بهمن',
|
||||
'اسفند',
|
||||
] as const;
|
||||
|
||||
const WEEKDAY_NAMES = [
|
||||
'یکشنبه',
|
||||
'دوشنبه',
|
||||
'سهشنبه',
|
||||
'چهارشنبه',
|
||||
'پنجشنبه',
|
||||
'جمعه',
|
||||
'شنبه',
|
||||
] as const;
|
||||
|
||||
function div(a: number, b: number) {
|
||||
return Math.trunc(a / b);
|
||||
}
|
||||
|
||||
export function isLeapJalaliYear(jy: number): boolean {
|
||||
const breaks = [
|
||||
-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097, 2192,
|
||||
2262, 2324, 2394, 2456, 3178,
|
||||
];
|
||||
const bl = breaks.length;
|
||||
let jp = breaks[0];
|
||||
let jump = 0;
|
||||
for (let i = 1; i < bl; i += 1) {
|
||||
const jm = breaks[i];
|
||||
jump = jm - jp;
|
||||
if (jy < jm) break;
|
||||
jp = jm;
|
||||
}
|
||||
let n = jy - jp;
|
||||
if (jump - n < 6) n = n - jump + div(jump + 4, 33) * 33;
|
||||
let leap = mod(n + 1, 33) - 1;
|
||||
if (leap === -1) leap = 32;
|
||||
return leap === 0 || leap % 4 === 0;
|
||||
}
|
||||
|
||||
function mod(a: number, b: number) {
|
||||
return a - Math.trunc(a / b) * b;
|
||||
}
|
||||
|
||||
export function jalaliMonthLength(jy: number, jm: number): number {
|
||||
if (jm <= 6) return 31;
|
||||
if (jm <= 11) return 30;
|
||||
return isLeapJalaliYear(jy) ? 30 : 29;
|
||||
}
|
||||
|
||||
export function toJalali(gy: number, gm: number, gd: number): JalaliDate {
|
||||
const gdm = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
||||
let gy2 = gm > 2 ? gy + 1 : gy;
|
||||
let days =
|
||||
355666 +
|
||||
365 * gy +
|
||||
div(gy2 + 3, 4) -
|
||||
div(gy2 + 99, 100) +
|
||||
div(gy2 + 399, 400) +
|
||||
gd +
|
||||
gdm[gm - 1];
|
||||
let jy = -1595 + 33 * div(days, 12053);
|
||||
days %= 12053;
|
||||
jy += 4 * div(days, 1461);
|
||||
days %= 1461;
|
||||
if (days > 365) {
|
||||
jy += div(days - 1, 365);
|
||||
days = (days - 1) % 365;
|
||||
}
|
||||
const jm = days < 186 ? 1 + div(days, 31) : 7 + div(days - 186, 30);
|
||||
const jd = 1 + (days < 186 ? days % 31 : (days - 186) % 30);
|
||||
return { jy, jm, jd };
|
||||
}
|
||||
|
||||
export function toGregorian(jy: number, jm: number, jd: number): {
|
||||
gy: number;
|
||||
gm: number;
|
||||
gd: number;
|
||||
} {
|
||||
const jy2 = jy + 1595;
|
||||
let days =
|
||||
-355668 +
|
||||
365 * jy2 +
|
||||
div(jy2, 33) * 8 +
|
||||
div(mod(jy2, 33) + 3, 4) +
|
||||
jd +
|
||||
(jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186);
|
||||
let gy = 400 * div(days, 146097);
|
||||
days %= 146097;
|
||||
if (days > 36524) {
|
||||
gy += 100 * div(--days, 36524);
|
||||
days %= 36524;
|
||||
if (days >= 365) days += 1;
|
||||
}
|
||||
gy += 4 * div(days, 1461);
|
||||
days %= 1461;
|
||||
if (days > 365) {
|
||||
gy += div(days - 1, 365);
|
||||
days = (days - 1) % 365;
|
||||
}
|
||||
const gd = days + 1;
|
||||
const salA = [
|
||||
0,
|
||||
31,
|
||||
(gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0 ? 29 : 28,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
];
|
||||
let gm = 0;
|
||||
let dayAcc = gd;
|
||||
for (gm = 1; gm <= 12 && dayAcc > salA[gm]; gm += 1) {
|
||||
dayAcc -= salA[gm];
|
||||
}
|
||||
return { gy, gm, gd: dayAcc };
|
||||
}
|
||||
|
||||
export function dateToJalali(d: Date): JalaliDate {
|
||||
return toJalali(d.getFullYear(), d.getMonth() + 1, d.getDate());
|
||||
}
|
||||
|
||||
export function jalaliToDate(jy: number, jm: number, jd: number): Date {
|
||||
const { gy, gm, gd } = toGregorian(jy, jm, jd);
|
||||
return new Date(gy, gm - 1, gd);
|
||||
}
|
||||
|
||||
export function addJalaliMonths(jy: number, jm: number, delta: number): {
|
||||
jy: number;
|
||||
jm: number;
|
||||
} {
|
||||
const idx = jy * 12 + (jm - 1) + delta;
|
||||
return {
|
||||
jy: Math.floor(idx / 12),
|
||||
jm: (idx % 12) + 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function jalaliMonthName(jm: number): string {
|
||||
return MONTH_NAMES[jm - 1] ?? '';
|
||||
}
|
||||
|
||||
export function weekdayName(d: Date): string {
|
||||
return WEEKDAY_NAMES[d.getDay()] ?? '';
|
||||
}
|
||||
|
||||
/** مثل: پنجشنبه ۵ شهریور ۱۴۰۵ */
|
||||
export function formatJalaliLong(d: Date): string {
|
||||
const { jy, jm, jd } = dateToJalali(d);
|
||||
return `${weekdayName(d)} ${toFaDigits(jd)} ${jalaliMonthName(jm)} ${toFaDigits(jy)}`;
|
||||
}
|
||||
|
||||
export function toFaDigits(value: number | string): string {
|
||||
return String(value).replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[Number(d)]!);
|
||||
}
|
||||
|
||||
export function gregorianDayKey(d: Date): string {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export const CURRENCY_OPTIONS = [
|
||||
{ code: 'IRR', labelFa: 'ریال ایران', suffix: 'ریال' },
|
||||
{ code: 'USD', labelFa: 'دلار آمریکا', suffix: 'دلار' },
|
||||
{ code: 'EUR', labelFa: 'یورو', suffix: 'یورو' },
|
||||
{ code: 'AED', labelFa: 'درهم امارات', suffix: 'درهم' },
|
||||
{ code: 'TRY', labelFa: 'لیر ترکیه', suffix: 'لیر' },
|
||||
{ code: 'GBP', labelFa: 'پوند انگلیس', suffix: 'پوند' },
|
||||
] as const;
|
||||
|
||||
export function currencySuffix(code: string): string {
|
||||
return CURRENCY_OPTIONS.find((c) => c.code === code)?.suffix ?? code;
|
||||
}
|
||||
|
||||
export function formatMoney(amount: number, currency = 'IRR'): string {
|
||||
try {
|
||||
return (
|
||||
new Intl.NumberFormat('fa-IR').format(amount) +
|
||||
' ' +
|
||||
currencySuffix(currency)
|
||||
);
|
||||
} catch {
|
||||
return `${amount} ${currency}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const PORTAL_TOKEN_KEY = 'mento_portal_token';
|
||||
|
||||
export function getPortalToken() {
|
||||
return localStorage.getItem(PORTAL_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setPortalToken(token: string | null) {
|
||||
if (token) localStorage.setItem(PORTAL_TOKEN_KEY, token);
|
||||
else localStorage.removeItem(PORTAL_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export const portalApi = axios.create({
|
||||
baseURL: '/api',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
portalApi.interceptors.request.use((config) => {
|
||||
const token = getPortalToken();
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
});
|
||||
|
||||
portalApi.interceptors.response.use(
|
||||
(r) => r,
|
||||
(error) => {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||
setPortalToken(null);
|
||||
const path = window.location.pathname;
|
||||
const m = path.match(/^\/p\/([^/]+)/);
|
||||
if (m && !path.endsWith('/login')) {
|
||||
window.location.assign(`/p/${m[1]}`);
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { UserRole } from '@mento/shared';
|
||||
|
||||
export const USER_ROLE_LABELS: Record<UserRole, string> = {
|
||||
owner: 'مالک',
|
||||
admin: 'مدیر',
|
||||
sales: 'فروش',
|
||||
support: 'پشتیبانی',
|
||||
};
|
||||
|
||||
export function canManageSettings(role: UserRole | string | null | undefined): boolean {
|
||||
return role === 'owner' || role === 'admin';
|
||||
}
|
||||
|
||||
export function canManageUsers(role: UserRole | string | null | undefined): boolean {
|
||||
return role === 'owner' || role === 'admin';
|
||||
}
|
||||
|
||||
export function canViewAllRecords(role: UserRole | string | null | undefined): boolean {
|
||||
return role === 'owner' || role === 'admin';
|
||||
}
|
||||
|
||||
export function canReassignRecords(role: UserRole | string | null | undefined): boolean {
|
||||
return role === 'owner' || role === 'admin';
|
||||
}
|
||||
Reference in New Issue
Block a user