The filter chips were fixed labels. Owners can rename, hide, reorder, and add categories. Co-authored-by: Cursor <cursoragent@cursor.com>
6460 lines
162 KiB
TypeScript
6460 lines
162 KiB
TypeScript
import type { PersonGender } from "./person-name";
|
||
export type { PersonGender } from "./person-name";
|
||
export {
|
||
PERSON_GENDERS,
|
||
PERSON_GENDER_LABELS,
|
||
applyLeadGenderName,
|
||
applyPersonGenderTitle,
|
||
isGenderQuestionLabel,
|
||
parsePersonGenderAnswer,
|
||
splitPersonHonorific,
|
||
} from "./person-name";
|
||
|
||
export type UserRole = "owner" | "admin" | "sales" | "support";
|
||
|
||
export const USER_ROLES: UserRole[] = ["owner", "admin", "sales", "support"];
|
||
|
||
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";
|
||
}
|
||
|
||
export interface AuthUserDto {
|
||
id: string;
|
||
email: string;
|
||
mobile?: string;
|
||
fullName: string;
|
||
role: UserRole;
|
||
organizationId: string;
|
||
organizationName?: string;
|
||
hasAvatar?: boolean;
|
||
/** مالک محیط فعلی — اشتراک متعلق به مالک است نه عضو زیرمجموعه */
|
||
isOrgOwner?: boolean;
|
||
enabledModules?: OrgModuleKey[];
|
||
/** خلاصه اشتراک مالک محیط (برای اعضا فقط جهت نمایش/گیت ورود) */
|
||
subscription?: SubscriptionSummaryDto | null;
|
||
/** اگر باشد، این کاربر سوئیچ «بایگانی اطلاعات قبل» را دارد */
|
||
priorArchive?: { lens: "current" | "archive"; cutoffAt: string | null } | null;
|
||
/**
|
||
* مالک در حال مشاهدهٔ فقطخواندنی بهجای این کاربر است.
|
||
* role/id مربوط به کاربر مشاهدهشده است.
|
||
*/
|
||
viewAs?: {
|
||
impersonatorId: string;
|
||
impersonatorFullName: string;
|
||
/** صفحهٔ تیم برای مالک واقعی باز بماند */
|
||
canOpenTeamSettings: boolean;
|
||
} | null;
|
||
/** مجوزهای نقش سازمانی؛ مالک و مدیر همهٔ بخشها را دارند */
|
||
permissions?: RolePermissionKey[];
|
||
}
|
||
|
||
export type BillingPlanCode =
|
||
| "trial"
|
||
| "monthly"
|
||
| "quarterly"
|
||
| "semiannual"
|
||
| "yearly"
|
||
| "lifetime"
|
||
| "custom";
|
||
|
||
export type BillingAccountStatus = "active" | "expired" | "suspended";
|
||
|
||
export const BILLING_PLAN_CODES: BillingPlanCode[] = [
|
||
"trial",
|
||
"monthly",
|
||
"quarterly",
|
||
"semiannual",
|
||
"yearly",
|
||
"lifetime",
|
||
"custom",
|
||
];
|
||
|
||
export const BILLING_PLAN_LABELS: Record<BillingPlanCode, string> = {
|
||
trial: "دمو ۷ روزه",
|
||
monthly: "ماهانه",
|
||
quarterly: "سهماهه",
|
||
semiannual: "ششماهه",
|
||
yearly: "یکساله",
|
||
lifetime: "دائمی",
|
||
custom: "سفارشی",
|
||
};
|
||
|
||
export const BILLING_TRIAL_DAYS = 7;
|
||
|
||
/** مدت پیشفرض پلنها (روز) — custom ندارد؛ lifetime عملاً بدون انقضا (۱۰۰ سال) */
|
||
export const BILLING_PLAN_DAYS: Record<Exclude<BillingPlanCode, "custom">, number> = {
|
||
trial: BILLING_TRIAL_DAYS,
|
||
monthly: 30,
|
||
quarterly: 90,
|
||
semiannual: 180,
|
||
yearly: 365,
|
||
lifetime: 36500,
|
||
};
|
||
|
||
export function isPermanentBillingPlan(code: string | null | undefined): boolean {
|
||
return code === "lifetime";
|
||
}
|
||
|
||
export const BILLING_PLAN_DEFAULT_MAX_ORGS: Record<
|
||
Exclude<BillingPlanCode, "custom">,
|
||
number
|
||
> = {
|
||
trial: 1,
|
||
monthly: 3,
|
||
quarterly: 5,
|
||
semiannual: 10,
|
||
yearly: 20,
|
||
lifetime: 20,
|
||
};
|
||
|
||
export type OrgModuleKey =
|
||
| "customers"
|
||
| "pipeline"
|
||
| "products"
|
||
| "invoices"
|
||
| "quotes"
|
||
| "commissions"
|
||
| "sales_forecast"
|
||
| "activities"
|
||
| "tickets"
|
||
| "workflow_automations"
|
||
| "workflow_automations_advanced"
|
||
| "message_automations"
|
||
| "lead_forms"
|
||
| "project_boards"
|
||
| "inventory"
|
||
| "cheques"
|
||
| "attachments"
|
||
| "loyalty"
|
||
| "reports"
|
||
| "notes"
|
||
| "tahesab"
|
||
| "custom_fields"
|
||
| "currencies"
|
||
| "team_settings"
|
||
| "faq"
|
||
| "audit_logs"
|
||
| "saved_views"
|
||
| "public_api"
|
||
| "sms_campaigns"
|
||
| "teams"
|
||
| "woocommerce"
|
||
| "montify"
|
||
| "voip"
|
||
| "reservations"
|
||
| "leads"
|
||
| "conversations"
|
||
| "routines"
|
||
| "gamification"
|
||
| "prior_data_archive"
|
||
| "treasury"
|
||
| "behavior";
|
||
|
||
export const ORG_MODULE_KEYS: OrgModuleKey[] = [
|
||
"customers",
|
||
"pipeline",
|
||
"products",
|
||
"invoices",
|
||
"quotes",
|
||
"commissions",
|
||
"sales_forecast",
|
||
"activities",
|
||
"tickets",
|
||
"workflow_automations",
|
||
"workflow_automations_advanced",
|
||
"message_automations",
|
||
"lead_forms",
|
||
"leads",
|
||
"project_boards",
|
||
"inventory",
|
||
"cheques",
|
||
"attachments",
|
||
"loyalty",
|
||
"reports",
|
||
"notes",
|
||
"tahesab",
|
||
"custom_fields",
|
||
"currencies",
|
||
"team_settings",
|
||
"faq",
|
||
"audit_logs",
|
||
"saved_views",
|
||
"public_api",
|
||
"sms_campaigns",
|
||
"teams",
|
||
"woocommerce",
|
||
"montify",
|
||
"voip",
|
||
"reservations",
|
||
"conversations",
|
||
"routines",
|
||
"gamification",
|
||
"prior_data_archive",
|
||
"treasury",
|
||
"behavior",
|
||
];
|
||
|
||
export const ORG_MODULE_LABELS: Record<OrgModuleKey, string> = {
|
||
customers: "مشتریان",
|
||
pipeline: "مسیر فروش",
|
||
products: "محصولات",
|
||
invoices: "فاکتورها",
|
||
quotes: "پیشفاکتورها",
|
||
commissions: "پورسانت",
|
||
sales_forecast: "هدف و پیشبینی",
|
||
activities: "فعالیتها",
|
||
tickets: "تیکتها",
|
||
workflow_automations: "اتوماسیون گردش کار",
|
||
workflow_automations_advanced: "اتوماسیون گردش کار حرفهای (چندشاخه)",
|
||
message_automations: "اتوماسیون پیامک",
|
||
lead_forms: "فرمساز سرنخ",
|
||
leads: "مدیریت لیدها",
|
||
project_boards: "تابلوی پروژه",
|
||
inventory: "انبار",
|
||
cheques: "چکها",
|
||
attachments: "پیوستها",
|
||
loyalty: "باشگاه مشتریان",
|
||
reports: "گزارشها",
|
||
notes: "یادداشتها",
|
||
tahesab: "تهحساب (اتصال و گزارش فروش)",
|
||
custom_fields: "فیلدهای سفارشی",
|
||
currencies: "ارزها",
|
||
team_settings: "تیم و تنظیمات سازمان",
|
||
faq: "FAQ",
|
||
audit_logs: "لاگ فعالیت کاربران",
|
||
saved_views: "فیلترهای ذخیرهشده",
|
||
public_api: "API و وبهوک",
|
||
sms_campaigns: "پیامک گروهی",
|
||
teams: "تیمها و سطح دسترسی به پروندهها",
|
||
woocommerce: "ووکامرس",
|
||
montify: "فروشگاههای مونتیفای",
|
||
voip: "تلفن سازمانی",
|
||
reservations: "رزرو و نوبتدهی",
|
||
conversations: "گفتگو",
|
||
routines: "کارهای روتین",
|
||
gamification: "باشگاه امتیازات",
|
||
prior_data_archive: "بایگانی اطلاعات قبل",
|
||
treasury: "مدیریت مالی",
|
||
behavior: "شخصیتشناسی و دستیار فروش",
|
||
};
|
||
|
||
export const ORG_MODULE_GROUPS: Array<{
|
||
title: string;
|
||
keys: OrgModuleKey[];
|
||
}> = [
|
||
{ title: "مشتریان", keys: ["customers"] },
|
||
{ title: "لیدها", keys: ["leads", "lead_forms"] },
|
||
{
|
||
title: "فروش",
|
||
keys: [
|
||
"pipeline",
|
||
"behavior",
|
||
"products",
|
||
"invoices",
|
||
"quotes",
|
||
"commissions",
|
||
"sales_forecast",
|
||
],
|
||
},
|
||
{ title: "فعالیتها", keys: ["activities", "routines", "notes", "gamification"] },
|
||
{ title: "پشتیبانی", keys: ["tickets", "faq", "conversations"] },
|
||
{
|
||
title: "اتوماسیون",
|
||
keys: [
|
||
"workflow_automations",
|
||
"message_automations",
|
||
"sms_campaigns",
|
||
],
|
||
},
|
||
{
|
||
title: "ابزارها",
|
||
keys: [
|
||
"project_boards",
|
||
"inventory",
|
||
"cheques",
|
||
"treasury",
|
||
"attachments",
|
||
"loyalty",
|
||
"reports",
|
||
"saved_views",
|
||
"woocommerce",
|
||
"montify",
|
||
"voip",
|
||
"reservations",
|
||
],
|
||
},
|
||
{
|
||
title: "تنظیمات",
|
||
keys: [
|
||
"team_settings",
|
||
"teams",
|
||
"custom_fields",
|
||
"currencies",
|
||
"tahesab",
|
||
"audit_logs",
|
||
"public_api",
|
||
],
|
||
},
|
||
];
|
||
|
||
export function isOrgModuleKey(value: string): value is OrgModuleKey {
|
||
return (ORG_MODULE_KEYS as readonly string[]).includes(value);
|
||
}
|
||
|
||
/** ماژولهایی که باید صراحتاً توسط سوپرادمین فعال شوند */
|
||
export const OPT_IN_ORG_MODULES: OrgModuleKey[] = [
|
||
"workflow_automations_advanced",
|
||
"prior_data_archive",
|
||
];
|
||
|
||
export function defaultEnabledModules(): OrgModuleKey[] {
|
||
return ORG_MODULE_KEYS.filter((k) => !OPT_IN_ORG_MODULES.includes(k));
|
||
}
|
||
|
||
export interface SubscriptionSummaryDto {
|
||
planCode: BillingPlanCode;
|
||
status: BillingAccountStatus;
|
||
startsAt: string;
|
||
expiresAt: string;
|
||
maxOrganizations: number;
|
||
daysRemaining: number;
|
||
warn: boolean;
|
||
}
|
||
|
||
export interface BillingAccountDto {
|
||
id: string;
|
||
ownerUserId: string;
|
||
planCode: BillingPlanCode;
|
||
status: BillingAccountStatus;
|
||
startsAt: string;
|
||
expiresAt: string;
|
||
maxOrganizations: number;
|
||
externalPaymentRef?: string | null;
|
||
notes?: string | null;
|
||
daysRemaining: number;
|
||
warn: boolean;
|
||
}
|
||
|
||
export interface ApplyBillingPlanRequest {
|
||
planCode: BillingPlanCode;
|
||
maxOrganizations?: number;
|
||
expiresAt?: string;
|
||
notes?: string | null;
|
||
status?: BillingAccountStatus;
|
||
}
|
||
|
||
export interface UpdateOrgModulesRequest {
|
||
enabledModules: OrgModuleKey[];
|
||
}
|
||
|
||
export interface OrgUserDto {
|
||
id: string;
|
||
email: string;
|
||
mobile?: string | null;
|
||
fullName: string;
|
||
role: UserRole;
|
||
orgRoleId?: string | null;
|
||
orgRoleName?: string;
|
||
orgRoleColor?: string;
|
||
isActive?: boolean;
|
||
lastLoginAt?: string | null;
|
||
hasAvatar?: boolean;
|
||
commissionPercent?: number;
|
||
/** own = فقط خود | team = همتیمیها | org = کل سازمان */
|
||
dataVisibility?: DataVisibility;
|
||
/** سطح دسترسی تفکیکی هر بخش (فقط بخشهایی که با پیشفرض فرق دارند) */
|
||
dataVisibilityScopes?: DataVisibilityScopes;
|
||
/**
|
||
* Override عمق داده نسبت به نقش؛ کلید غایب = ارث از نقش.
|
||
* اگر کل آبجکت خالی باشد و نقش هم خالی باشد → بدون محدودیت عمقی.
|
||
*/
|
||
dataAccessOverride?: DataAccessPolicy;
|
||
/** سیاست مؤثر پس از resolve (نقش + override) — فقط برای نمایش UI */
|
||
effectiveDataAccess?: DataAccessPolicy;
|
||
/** @deprecated از effectiveDataAccess.customers استفاده کنید */
|
||
effectiveCustomersAccess?: SectionDataAccess;
|
||
/** تیک مالک: این کاربر سوئیچ بایگانی اطلاعات قبل را ببیند */
|
||
priorArchiveEnabled?: boolean;
|
||
/** ساعت خروج روزانه HH:mm به وقت تهران؛ خالی یعنی بدون محدودیت */
|
||
logoutTime?: string | null;
|
||
/** بعد از ساعت خروج تا پایان روز نمیتواند دوباره وارد شود */
|
||
blockLoginAfterLogout?: boolean;
|
||
/**
|
||
* ورود فقط با کد پیامکی؛ رمز عبور برای این کاربر در این محیط پذیرفته نمیشود
|
||
* (حتی اگر از قبل رمز داشته باشد).
|
||
*/
|
||
loginOtpOnly?: boolean;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface UpdateUserLoginOtpOnlyRequest {
|
||
loginOtpOnly: boolean;
|
||
}
|
||
|
||
export type DataVisibility = "own" | "team" | "org";
|
||
|
||
export const DATA_VISIBILITIES: DataVisibility[] = ["own", "team", "org"];
|
||
|
||
export const DATA_VISIBILITY_LABELS: Record<DataVisibility, string> = {
|
||
own: "فقط پروندههای خود",
|
||
team: "تیمهای عضو",
|
||
org: "کل سازمان",
|
||
};
|
||
|
||
/** بخشهایی که سطح دسترسی به پروندهها برایشان جداگانه تعیین میشود */
|
||
export type DataScope =
|
||
| "contacts"
|
||
| "deals"
|
||
| "activities"
|
||
| "leads"
|
||
| "finance"
|
||
| "tickets"
|
||
| "notes"
|
||
| "projects"
|
||
| "routines";
|
||
|
||
export const DATA_SCOPES: DataScope[] = [
|
||
"contacts",
|
||
"deals",
|
||
"activities",
|
||
"leads",
|
||
"finance",
|
||
"tickets",
|
||
"notes",
|
||
"projects",
|
||
"routines",
|
||
];
|
||
|
||
export const DATA_SCOPE_LABELS: Record<DataScope, string> = {
|
||
contacts: "مشتریان (اشخاص و شرکتها)",
|
||
deals: "معاملات و اهداف فروش",
|
||
activities: "فعالیتها و کارها",
|
||
leads: "لیدها",
|
||
finance: "فاکتور، پیشفاکتور و چک",
|
||
tickets: "تیکتها",
|
||
notes: "یادداشتها",
|
||
projects: "پروژهها",
|
||
routines: "کارهای روتین",
|
||
};
|
||
|
||
export type DataVisibilityScopes = Partial<Record<DataScope, DataVisibility>>;
|
||
|
||
export interface UpdateUserVisibilityRequest {
|
||
/** سطح پیشفرض برای بخشهایی که جداگانه تنظیم نشدهاند */
|
||
dataVisibility?: DataVisibility;
|
||
/** سطح هر بخش؛ مقدار null یعنی «مثل پیشفرض» */
|
||
scopes?: Partial<Record<DataScope, DataVisibility | null>>;
|
||
}
|
||
|
||
/* ——— Deep data access (customers + leads + sales + activities + notes/routines/projects/tickets) ——— */
|
||
|
||
export type DataAccessOwnerKind = "self" | "team";
|
||
|
||
export type DataAccessMode = "all" | "rules";
|
||
|
||
/**
|
||
* بخشهای عمق داده.
|
||
* customers/leads/sales/activities/notes ≈ RolePermissionKey؛
|
||
* routines/projects/tickets ≈ ماژول / DataScope (بدون مجوز نقش جدا).
|
||
*/
|
||
export type DataAccessSection =
|
||
| "customers"
|
||
| "leads"
|
||
| "sales"
|
||
| "activities"
|
||
| "notes"
|
||
| "routines"
|
||
| "projects"
|
||
| "tickets";
|
||
|
||
export const DATA_ACCESS_SECTIONS: DataAccessSection[] = [
|
||
"customers",
|
||
"leads",
|
||
"sales",
|
||
"activities",
|
||
"notes",
|
||
"routines",
|
||
"projects",
|
||
"tickets",
|
||
];
|
||
|
||
export const DATA_ACCESS_SECTION_LABELS: Record<DataAccessSection, string> = {
|
||
customers: "مشتریان (اشخاص و شرکتها)",
|
||
leads: "لیدها",
|
||
sales: "فروش و معاملات",
|
||
activities: "فعالیتها",
|
||
notes: "یادداشتها",
|
||
routines: "کارهای روتین",
|
||
projects: "پروژهها",
|
||
tickets: "تیکتها",
|
||
};
|
||
|
||
/** نگاشت بخش عمقداده ← کلید قدیمی DataScope برای مهاجرت */
|
||
export const DATA_ACCESS_LEGACY_SCOPE: Record<DataAccessSection, DataScope | null> = {
|
||
customers: "contacts",
|
||
leads: "leads",
|
||
sales: "deals",
|
||
activities: "activities",
|
||
notes: "notes",
|
||
routines: "routines",
|
||
projects: "projects",
|
||
tickets: "tickets",
|
||
};
|
||
|
||
/**
|
||
* یک قانون: فیلدهای پرشده با AND.
|
||
* قوانین متعدد در SectionDataAccess با OR ترکیب میشوند.
|
||
* ابعاد بلااستفاده برای یک بخش نادیده گرفته میشوند.
|
||
*/
|
||
export type DataAccessRule = {
|
||
/** self / team → مالک / مسئول / نویسنده (بسته به بخش) */
|
||
owners?: DataAccessOwnerKind[];
|
||
/** مشتریان: گروهها */
|
||
groupIds?: string[];
|
||
/** مشتریان: importSource/sourceRef · لیدها: source · یادداشت: noteType · تیکت: status */
|
||
sources?: string[];
|
||
/** مشتریان: حال */
|
||
moods?: PersonMood[];
|
||
/** لید / معامله / فعالیت / پروژه: مرحله */
|
||
stageIds?: string[];
|
||
/** معامله: پایپلاین */
|
||
pipelineIds?: string[];
|
||
/** فعالیت / پروژه: بورد */
|
||
boardIds?: string[];
|
||
/** فعالیت: typeDefId · روتین: categoryId */
|
||
typeIds?: string[];
|
||
};
|
||
|
||
/** @deprecated نام قبلی؛ از DataAccessRule استفاده کنید */
|
||
export type ContactAccessRule = DataAccessRule;
|
||
|
||
export type SectionDataAccess = {
|
||
mode: DataAccessMode;
|
||
rules?: DataAccessRule[];
|
||
};
|
||
|
||
export type DataAccessPolicy = Partial<Record<DataAccessSection, SectionDataAccess>>;
|
||
|
||
export interface UpdateUserDataAccessRequest {
|
||
/**
|
||
* null یعنی حذف override و برگشت به ارث از نقش.
|
||
* آبجکت یعنی جایگزینی کامل override (کلید غایب در آبجکت = آن بخش از override پاک میشود).
|
||
*/
|
||
dataAccessOverride?: DataAccessPolicy | null;
|
||
}
|
||
|
||
export function emptySectionDataAccess(): SectionDataAccess {
|
||
return { mode: "all", rules: [] };
|
||
}
|
||
|
||
export function parseDataAccessPolicy(raw: unknown): DataAccessPolicy {
|
||
const out: DataAccessPolicy = {};
|
||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
|
||
const obj = raw as Record<string, unknown>;
|
||
for (const section of DATA_ACCESS_SECTIONS) {
|
||
const parsed = parseSectionDataAccess(obj[section]);
|
||
if (parsed) out[section] = parsed;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
export function parseSectionDataAccess(raw: unknown): SectionDataAccess | null {
|
||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||
const obj = raw as Record<string, unknown>;
|
||
const mode: DataAccessMode = obj.mode === "rules" ? "rules" : "all";
|
||
const rulesRaw = Array.isArray(obj.rules) ? obj.rules : [];
|
||
const rules: DataAccessRule[] = [];
|
||
for (const item of rulesRaw) {
|
||
const rule = parseDataAccessRule(item);
|
||
if (rule) rules.push(rule);
|
||
}
|
||
if (mode === "all") return { mode: "all", rules: [] };
|
||
return { mode: "rules", rules };
|
||
}
|
||
|
||
function parseStringIdList(raw: unknown): string[] | undefined {
|
||
if (!Array.isArray(raw)) return undefined;
|
||
const list = raw
|
||
.filter((v): v is string => typeof v === "string" && v.trim().length > 0)
|
||
.map((v) => v.trim());
|
||
return list.length ? [...new Set(list)] : undefined;
|
||
}
|
||
|
||
export function parseDataAccessRule(raw: unknown): DataAccessRule | null {
|
||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||
const obj = raw as Record<string, unknown>;
|
||
const rule: DataAccessRule = {};
|
||
if (Array.isArray(obj.owners)) {
|
||
const owners = obj.owners.filter(
|
||
(v): v is DataAccessOwnerKind => v === "self" || v === "team",
|
||
);
|
||
if (owners.length) rule.owners = [...new Set(owners)];
|
||
}
|
||
const groupIds = parseStringIdList(obj.groupIds);
|
||
if (groupIds) rule.groupIds = groupIds;
|
||
const sources = parseStringIdList(obj.sources);
|
||
if (sources) rule.sources = sources;
|
||
if (Array.isArray(obj.moods)) {
|
||
const moods = obj.moods.filter(
|
||
(v): v is PersonMood =>
|
||
typeof v === "string" && (PERSON_MOODS as readonly string[]).includes(v),
|
||
);
|
||
if (moods.length) rule.moods = [...new Set(moods)];
|
||
}
|
||
const stageIds = parseStringIdList(obj.stageIds);
|
||
if (stageIds) rule.stageIds = stageIds;
|
||
const pipelineIds = parseStringIdList(obj.pipelineIds);
|
||
if (pipelineIds) rule.pipelineIds = pipelineIds;
|
||
const boardIds = parseStringIdList(obj.boardIds);
|
||
if (boardIds) rule.boardIds = boardIds;
|
||
const typeIds = parseStringIdList(obj.typeIds);
|
||
if (typeIds) rule.typeIds = typeIds;
|
||
|
||
if (
|
||
!rule.owners &&
|
||
!rule.groupIds &&
|
||
!rule.sources &&
|
||
!rule.moods &&
|
||
!rule.stageIds &&
|
||
!rule.pipelineIds &&
|
||
!rule.boardIds &&
|
||
!rule.typeIds
|
||
) {
|
||
return null;
|
||
}
|
||
return rule;
|
||
}
|
||
|
||
/** @deprecated */
|
||
export const parseContactAccessRule = parseDataAccessRule;
|
||
|
||
/** آیا این بخش محدودیت عمقی دارد؟ mode=all یا بدون قانون معتبر = بدون محدودیت */
|
||
export function sectionHasDataAccessLimits(section: SectionDataAccess | null | undefined): boolean {
|
||
if (!section || section.mode !== "rules") return false;
|
||
return Boolean(section.rules?.length);
|
||
}
|
||
|
||
/**
|
||
* مهاجرت از مدل قدیمی own/team/org به SectionDataAccess.
|
||
* اگر scopeKey داده شود، سطح تفکیکی همان بخش اولویت دارد.
|
||
*/
|
||
export function migrateVisibilityToSectionAccess(
|
||
dataVisibility: DataVisibility | string | null | undefined,
|
||
scopes?: DataVisibilityScopes | null,
|
||
scopeKey?: DataScope | null,
|
||
): SectionDataAccess {
|
||
const scoped =
|
||
scopeKey && scopes
|
||
? scopes[scopeKey]
|
||
: undefined;
|
||
const level =
|
||
scoped === "own" || scoped === "team" || scoped === "org"
|
||
? scoped
|
||
: dataVisibility === "team" || dataVisibility === "org" || dataVisibility === "own"
|
||
? dataVisibility
|
||
: "own";
|
||
if (level === "org") return { mode: "all", rules: [] };
|
||
if (level === "team") return { mode: "rules", rules: [{ owners: ["self", "team"] }] };
|
||
return { mode: "rules", rules: [{ owners: ["self"] }] };
|
||
}
|
||
|
||
/** @deprecated از migrateVisibilityToSectionAccess استفاده کنید */
|
||
export function migrateVisibilityToCustomersAccess(
|
||
dataVisibility: DataVisibility | string | null | undefined,
|
||
scopes?: DataVisibilityScopes | null,
|
||
): SectionDataAccess {
|
||
return migrateVisibilityToSectionAccess(dataVisibility, scopes, "contacts");
|
||
}
|
||
|
||
export type RolePermissionKey =
|
||
| "customers"
|
||
| "leads"
|
||
| "sales"
|
||
| "billing"
|
||
| "products"
|
||
| "activities"
|
||
| "notes"
|
||
| "reports"
|
||
| "audit"
|
||
| "conversations"
|
||
| "conversations_groups"
|
||
| "treasury"
|
||
| "treasury_payroll"
|
||
| "behavior";
|
||
|
||
export const ROLE_PERMISSION_ITEMS: Array<{
|
||
key: RolePermissionKey;
|
||
label: string;
|
||
}> = [
|
||
{ key: "customers", label: "مشتریان" },
|
||
{ key: "leads", label: "لیدها" },
|
||
{ key: "sales", label: "فروش و معاملات" },
|
||
{ key: "billing", label: "پیشفاکتورها و فاکتورها" },
|
||
{ key: "products", label: "محصولات و خدمات" },
|
||
{ key: "activities", label: "فعالیتها" },
|
||
{ key: "notes", label: "یادداشتها" },
|
||
{ key: "reports", label: "گزارشها" },
|
||
{ key: "audit", label: "لاگ فعالیت کاربران" },
|
||
{ key: "conversations", label: "گفتگو" },
|
||
{ key: "conversations_groups", label: "ساخت گروه گفتگو" },
|
||
{ key: "treasury", label: "مدیریت مالی" },
|
||
{ key: "treasury_payroll", label: "حقوق و دستمزد" },
|
||
{ key: "behavior", label: "شخصیتشناسی و دستیار فروش" },
|
||
];
|
||
|
||
export interface OrgRoleMemberDto {
|
||
id: string;
|
||
fullName: string;
|
||
hasAvatar: boolean;
|
||
}
|
||
|
||
export interface OrgRoleDto {
|
||
id: string;
|
||
name: string;
|
||
description: string;
|
||
color: string;
|
||
permissions: RolePermissionKey[];
|
||
/** پیشفرض عمق داده برای اعضای این نقش */
|
||
dataAccessPolicy?: DataAccessPolicy;
|
||
systemKey: UserRole | null;
|
||
/** نقش والد؛ null یعنی ریشه */
|
||
parentId: string | null;
|
||
/** نام نقش والد برای نمایش در UI */
|
||
parentName: string | null;
|
||
/** عمق در درخت (۰ = ریشه) */
|
||
depth: number;
|
||
/** تعداد زیرنقشهای مستقیم */
|
||
childCount: number;
|
||
userCount: number;
|
||
members: OrgRoleMemberDto[];
|
||
}
|
||
|
||
export interface CreateOrgRoleRequest {
|
||
name: string;
|
||
description?: string;
|
||
color?: string;
|
||
permissions?: RolePermissionKey[];
|
||
dataAccessPolicy?: DataAccessPolicy;
|
||
/** اگر ست شود، این نقش زیرمجموعهٔ نقش والد خواهد بود */
|
||
parentId?: string | null;
|
||
}
|
||
|
||
export interface UpdateOrgRoleRequest {
|
||
name?: string;
|
||
description?: string;
|
||
color?: string;
|
||
permissions?: RolePermissionKey[];
|
||
dataAccessPolicy?: DataAccessPolicy;
|
||
/** null یعنی جدا کردن از والد و تبدیل به ریشه */
|
||
parentId?: string | null;
|
||
}
|
||
|
||
export interface InviteUserRequest {
|
||
mobile: string;
|
||
fullName: string;
|
||
password?: string;
|
||
role?: Exclude<UserRole, "owner">;
|
||
orgRoleId?: string;
|
||
}
|
||
|
||
/** ویرایش مشخصات عضو تیم (نام، موبایل، رمز) — نه نقش/دسترسی */
|
||
export interface UpdateOrgUserRequest {
|
||
fullName: string;
|
||
mobile: string;
|
||
/** در صورت ارسال، رمز جدید تنظیم میشود (حداقل ۸ کاراکتر) */
|
||
password?: string;
|
||
}
|
||
|
||
export interface UpdateUserRoleRequest {
|
||
role?: UserRole;
|
||
orgRoleId?: string;
|
||
}
|
||
|
||
export interface UpdateUserStatusRequest {
|
||
isActive: boolean;
|
||
}
|
||
|
||
export interface UpdateUserCommissionRequest {
|
||
commissionPercent: number;
|
||
}
|
||
|
||
export interface UpdateProfileRequest {
|
||
fullName: string;
|
||
email: string;
|
||
mobile: string;
|
||
}
|
||
|
||
export type DealStatus = "open" | "won" | "lost";
|
||
|
||
export interface ApiErrorBody {
|
||
statusCode: number;
|
||
message: string | string[];
|
||
error?: string;
|
||
}
|
||
|
||
export interface LoginRequest {
|
||
/** ایمیل یا شماره موبایل */
|
||
identifier: string;
|
||
password: string;
|
||
}
|
||
|
||
export interface RegisterOrganizationRequest {
|
||
organizationName: string;
|
||
fullName: string;
|
||
email: string;
|
||
mobile: string;
|
||
password: string;
|
||
passwordConfirm: string;
|
||
businessField: BusinessField;
|
||
referralCode?: string;
|
||
acceptedTerms?: boolean;
|
||
}
|
||
|
||
export interface LoginChallengeResponse {
|
||
challengeId: string;
|
||
channel: 'email' | 'mobile';
|
||
expiresInSeconds: number;
|
||
maskedDestination?: string;
|
||
/** فقط در محیط توسعه / AUTH_RETURN_OTP */
|
||
devCode?: string;
|
||
}
|
||
|
||
export interface RegisterChallengeResponse {
|
||
challengeId: string;
|
||
expiresInSeconds: number;
|
||
maskedEmail?: string;
|
||
maskedMobile?: string;
|
||
/** وقتی سرویس ایمیل خاموش است false — فقط OTP موبایل لازم است */
|
||
emailOtpRequired?: boolean;
|
||
devEmailCode?: string;
|
||
devMobileCode?: string;
|
||
}
|
||
|
||
export interface LoginVerifyRequest {
|
||
challengeId: string;
|
||
code: string;
|
||
}
|
||
|
||
export interface RegisterVerifyRequest {
|
||
challengeId: string;
|
||
emailCode?: string;
|
||
mobileCode: string;
|
||
}
|
||
|
||
export interface LoginResponse {
|
||
accessToken: string;
|
||
user: AuthUserDto;
|
||
}
|
||
|
||
export interface OrganizationMembershipDto {
|
||
id: string;
|
||
name: string;
|
||
role: UserRole;
|
||
orgRoleName?: string;
|
||
}
|
||
|
||
export interface LoginOrganizationChoiceResponse {
|
||
requiresOrganizationChoice: true;
|
||
selectionToken: string;
|
||
organizations: OrganizationMembershipDto[];
|
||
}
|
||
|
||
export type LoginResult = LoginResponse | LoginOrganizationChoiceResponse;
|
||
|
||
export function isLoginOrganizationChoice(
|
||
data: LoginResult,
|
||
): data is LoginOrganizationChoiceResponse {
|
||
return (
|
||
typeof data === "object" &&
|
||
data !== null &&
|
||
"requiresOrganizationChoice" in data &&
|
||
data.requiresOrganizationChoice === true
|
||
);
|
||
}
|
||
|
||
export interface SelectOrganizationRequest {
|
||
selectionToken: string;
|
||
organizationId: string;
|
||
}
|
||
|
||
export interface SwitchOrganizationRequest {
|
||
organizationId: string;
|
||
}
|
||
|
||
export interface CreateOrganizationRequest {
|
||
name: string;
|
||
businessField?: BusinessField;
|
||
}
|
||
|
||
export interface OrganizationDto {
|
||
id: string;
|
||
name: string;
|
||
slug: string;
|
||
businessField: BusinessField | null;
|
||
baseCurrency: string;
|
||
enabledCurrencies: string[];
|
||
exchangeRates: ExchangeRateDto[];
|
||
enabledModules: OrgModuleKey[];
|
||
priorArchiveCutoffAt?: string | null;
|
||
}
|
||
|
||
export interface UpdatePriorArchiveCutoffRequest {
|
||
/** ISO datetime (start of selected Jalali day) or null to clear */
|
||
cutoffAt: string | null;
|
||
}
|
||
|
||
export interface UpdateOrganizationRequest {
|
||
name?: string;
|
||
businessField?: BusinessField;
|
||
}
|
||
|
||
/** زمینه فعالیت سازمان — همتراز صنایع دیدار */
|
||
export const BUSINESS_FIELDS = [
|
||
"education",
|
||
"beauty",
|
||
"real_estate",
|
||
"construction",
|
||
"advertising",
|
||
"insurance",
|
||
"consulting",
|
||
"startup",
|
||
"medical_devices",
|
||
"travel",
|
||
"contractor",
|
||
"trading",
|
||
"manufacturing",
|
||
"personal",
|
||
"tech",
|
||
"legal",
|
||
"repair",
|
||
"retail",
|
||
"jewelry",
|
||
"other",
|
||
] as const;
|
||
|
||
export type BusinessField = (typeof BUSINESS_FIELDS)[number];
|
||
|
||
export const BUSINESS_FIELD_LABELS: Record<BusinessField, string> = {
|
||
education: "آموزشگاه و مؤسسات آموزشی",
|
||
beauty: "کلینیک زیبایی و آرایشگاه",
|
||
real_estate: "املاک و مشاورین مسکن",
|
||
construction: "صنعت ساختمان",
|
||
advertising: "تبلیغات و مشاوره کسبوکار",
|
||
insurance: "بیمه",
|
||
consulting: "شرکتهای مشاور",
|
||
startup: "استارتاپ و کسبوکار آنلاین",
|
||
medical_devices: "تجهیزات پزشکی",
|
||
travel: "گردشگری و مهاجرت",
|
||
contractor: "پیمانکاری و پروژهای",
|
||
trading: "بازرگانی",
|
||
manufacturing: "تولیدی و صنعتی",
|
||
personal: "کسبوکار شخصی و خانگی",
|
||
tech: "نرمافزار و سختافزار",
|
||
legal: "وکلا و مشاورین حقوقی",
|
||
repair: "خدمات و تعمیرات",
|
||
retail: "خردهفروشی",
|
||
jewelry: "طلا و جواهر",
|
||
other: "سایر",
|
||
};
|
||
|
||
export function isBusinessField(value: string): value is BusinessField {
|
||
return (BUSINESS_FIELDS as readonly string[]).includes(value);
|
||
}
|
||
|
||
export interface ExchangeRateDto {
|
||
currency: string;
|
||
rateToBase: number;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface UpdateOrgCurrenciesRequest {
|
||
baseCurrency?: string;
|
||
enabledCurrencies?: string[];
|
||
/** نرخها نسبت به ارز پایه؛ ارز پایه نیازی به نرخ ندارد */
|
||
rates?: Array<{ currency: string; rateToBase: number }>;
|
||
}
|
||
|
||
export const CURRENCY_CATALOG: Array<{
|
||
code: string;
|
||
labelFa: string;
|
||
suffix: string;
|
||
}> = [
|
||
{ code: "IRR", labelFa: "ریال ایران", suffix: "ریال" },
|
||
{ code: "USD", labelFa: "دلار آمریکا", suffix: "دلار" },
|
||
{ code: "EUR", labelFa: "یورو", suffix: "یورو" },
|
||
{ code: "AED", labelFa: "درهم امارات", suffix: "درهم" },
|
||
{ code: "TRY", labelFa: "لیر ترکیه", suffix: "لیر" },
|
||
{ code: "GBP", labelFa: "پوند انگلیس", suffix: "پوند" },
|
||
];
|
||
|
||
export function currencySuffix(code: string): string {
|
||
return CURRENCY_CATALOG.find((c) => c.code === code)?.suffix ?? code;
|
||
}
|
||
|
||
export function formatMoneyAmount(amount: number, currency = "IRR"): string {
|
||
try {
|
||
const n = new Intl.NumberFormat("fa-IR").format(amount);
|
||
return `${n} ${currencySuffix(currency)}`;
|
||
} catch {
|
||
return `${amount} ${currency}`;
|
||
}
|
||
}
|
||
|
||
export interface HealthResponse {
|
||
status: "ok";
|
||
service: string;
|
||
timestamp: string;
|
||
}
|
||
|
||
/* ───────── گروه مشتری (اشخاص و شرکتها) ───────── */
|
||
|
||
export const CUSTOMER_GROUP_COLORS = [
|
||
"teal",
|
||
"sky",
|
||
"violet",
|
||
"rose",
|
||
"amber",
|
||
"pink",
|
||
"slate",
|
||
] as const;
|
||
export type CustomerGroupColor = (typeof CUSTOMER_GROUP_COLORS)[number];
|
||
|
||
export interface CustomerGroupSummaryDto {
|
||
id: string;
|
||
name: string;
|
||
color: CustomerGroupColor | string;
|
||
/** manual | tahesab — فقط برای اشخاص */
|
||
source?: "manual" | "tahesab";
|
||
}
|
||
|
||
export interface CustomerGroupDto {
|
||
id: string;
|
||
name: string;
|
||
color: CustomerGroupColor | string;
|
||
position: number;
|
||
/** گروه متناظر در تهحساب دارد */
|
||
fromTahesab: boolean;
|
||
personCount: number;
|
||
companyCount: number;
|
||
/** گروه اصلی؛ خالی یعنی خودش اصلی است */
|
||
parentId?: string | null;
|
||
parentName?: string | null;
|
||
}
|
||
|
||
export interface CreateCustomerGroupRequest {
|
||
name: string;
|
||
color?: CustomerGroupColor;
|
||
}
|
||
|
||
export interface UpdateCustomerGroupRequest {
|
||
name?: string;
|
||
color?: CustomerGroupColor;
|
||
/** null = برداشتن گروه اصلی */
|
||
parentId?: string | null;
|
||
}
|
||
|
||
export interface CompanySummaryDto {
|
||
id: string;
|
||
name: string;
|
||
province?: string | null;
|
||
city?: string | null;
|
||
}
|
||
|
||
export interface CompanyDto {
|
||
id: string;
|
||
name: string;
|
||
/** کد داخلی شرکت */
|
||
code: string | null;
|
||
importSource: PersonImportSource | string;
|
||
/** کد شرکت در مبدأ (مثلاً کد دیدار) */
|
||
sourceRef: string | null;
|
||
nationalId: string | null;
|
||
economicCode: string | null;
|
||
registrationNumber: string | null;
|
||
phone: string | null;
|
||
otherPhones: string | null;
|
||
fax: string | null;
|
||
email: string | null;
|
||
otherEmails: string | null;
|
||
website: string | null;
|
||
province: string | null;
|
||
city: string | null;
|
||
postalCode: string | null;
|
||
address: string | null;
|
||
groupName: string | null;
|
||
notes: string | null;
|
||
registeredAt: string | null;
|
||
tags: string[];
|
||
customFields: Record<string, unknown>;
|
||
groups: CustomerGroupSummaryDto[];
|
||
peopleCount: number;
|
||
owner: UserSummaryDto | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateCompanyRequest {
|
||
name: string;
|
||
code?: string | null;
|
||
importSource?: PersonImportSource | string;
|
||
sourceRef?: string | null;
|
||
nationalId?: string;
|
||
economicCode?: string | null;
|
||
registrationNumber?: string | null;
|
||
phone?: string;
|
||
otherPhones?: string | null;
|
||
fax?: string | null;
|
||
email?: string;
|
||
otherEmails?: string | null;
|
||
website?: string;
|
||
province?: string | null;
|
||
city?: string | null;
|
||
postalCode?: string | null;
|
||
address?: string;
|
||
groupName?: string | null;
|
||
notes?: string;
|
||
registeredAt?: string | null;
|
||
tags?: string[];
|
||
customFields?: Record<string, unknown>;
|
||
ownerId?: string | null;
|
||
/** همهٔ گروههای شرکت (جایگزین کامل) */
|
||
groupIds?: string[];
|
||
}
|
||
|
||
export type UpdateCompanyRequest = Partial<CreateCompanyRequest>;
|
||
|
||
export interface LoyaltyTierSummaryDto {
|
||
id: string;
|
||
name: string;
|
||
minPoints: number;
|
||
color: string | null;
|
||
}
|
||
|
||
export const PERSON_MOODS = ["newcomer", "happy", "unhappy", "special"] as const;
|
||
export type PersonMood = (typeof PERSON_MOODS)[number];
|
||
|
||
export const PERSON_MOOD_LABELS: Record<PersonMood, string> = {
|
||
newcomer: "مشتری تازه وارد",
|
||
happy: "مشتری خوشحال",
|
||
unhappy: "مشتری ناراضی",
|
||
special: "مشتری خاص",
|
||
};
|
||
|
||
/** منبع ورود شخص به CRM */
|
||
export const PERSON_IMPORT_SOURCES = [
|
||
"manual",
|
||
"tahesab",
|
||
"didar",
|
||
"excel",
|
||
"phone",
|
||
"lead_form",
|
||
"montify",
|
||
"other",
|
||
] as const;
|
||
export type PersonImportSource = (typeof PERSON_IMPORT_SOURCES)[number];
|
||
|
||
export const PERSON_IMPORT_SOURCE_LABELS: Record<PersonImportSource, string> = {
|
||
manual: "دستی",
|
||
tahesab: "تهحساب",
|
||
didar: "دیدار",
|
||
excel: "اکسل",
|
||
phone: "مخاطبین گوشی",
|
||
lead_form: "فرم لید",
|
||
montify: "فروشگاه مونتیفای",
|
||
other: "سایر",
|
||
};
|
||
|
||
export interface PersonImportSourceStatDto {
|
||
source: PersonImportSource | string;
|
||
label: string;
|
||
count: number;
|
||
}
|
||
|
||
export interface PersonDto {
|
||
id: string;
|
||
firstName: string;
|
||
lastName: string;
|
||
previousFirstName: string | null;
|
||
previousLastName: string | null;
|
||
previousFullName: string | null;
|
||
importSource: PersonImportSource | string;
|
||
sourceRef: string | null;
|
||
/** منبع/کد قبل از آخرین ورود از منبع دیگر (مثلاً کد دیدار پیش از تهحساب) */
|
||
previousImportSource: PersonImportSource | string | null;
|
||
previousSourceRef: string | null;
|
||
fullName: string;
|
||
gender: PersonGender;
|
||
showGenderInName: boolean;
|
||
email: string | null;
|
||
phone: string | null;
|
||
mobile: string | null;
|
||
position: string | null;
|
||
website: string | null;
|
||
instagram: string | null;
|
||
birthDate: string | null;
|
||
referrer: string | null;
|
||
addresses: string[];
|
||
notes: string | null;
|
||
mood: PersonMood;
|
||
tags: string[];
|
||
customFields: Record<string, unknown>;
|
||
groups: CustomerGroupSummaryDto[];
|
||
loyaltyPoints: number;
|
||
loyaltyTier: LoyaltyTierSummaryDto | null;
|
||
/** تیپ غالب یا ترکیبی تأییدشده؛ بدون مجوز رفتار خالی برمیگردد */
|
||
discLabel: string | null;
|
||
discReliable: boolean;
|
||
discAssessedAt: string | null;
|
||
company: CompanySummaryDto | null;
|
||
owner: UserSummaryDto | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreatePersonRequest {
|
||
firstName?: string;
|
||
lastName?: string;
|
||
gender?: PersonGender;
|
||
showGenderInName?: boolean;
|
||
previousFirstName?: string | null;
|
||
previousLastName?: string | null;
|
||
importSource?: PersonImportSource | string;
|
||
sourceRef?: string | null;
|
||
email?: string;
|
||
phone?: string;
|
||
mobile?: string;
|
||
position?: string;
|
||
website?: string;
|
||
instagram?: string;
|
||
birthDate?: string | null;
|
||
referrer?: string;
|
||
addresses?: string[];
|
||
notes?: string;
|
||
mood?: PersonMood;
|
||
tags?: string[];
|
||
companyId?: string | null;
|
||
customFields?: Record<string, unknown>;
|
||
ownerId?: string | null;
|
||
/** فقط برای ایمپورت اکسل — تاریخ ثبت مبدأ */
|
||
registeredAt?: string | null;
|
||
/** همهٔ گروههای شخص (جایگزین کامل؛ منبع گروههای تهحسابیِ باقیمانده حفظ میشود) */
|
||
groupIds?: string[];
|
||
}
|
||
|
||
export type UpdatePersonRequest = Partial<CreatePersonRequest>;
|
||
|
||
export interface PaginatedResponse<T> {
|
||
items: T[];
|
||
total: number;
|
||
page: number;
|
||
limit: number;
|
||
}
|
||
|
||
export interface PersonSummaryDto {
|
||
id: string;
|
||
fullName: string;
|
||
}
|
||
|
||
export interface UserSummaryDto {
|
||
id: string;
|
||
fullName: string;
|
||
}
|
||
|
||
export interface PipelineStageDto {
|
||
id: string;
|
||
name: string;
|
||
position: number;
|
||
isWon: boolean;
|
||
isLost: boolean;
|
||
}
|
||
|
||
export interface PipelineDto {
|
||
id: string;
|
||
name: string;
|
||
isDefault: boolean;
|
||
stages: PipelineStageDto[];
|
||
/** تعداد معاملات؛ در فهرست کاریزها پر میشود */
|
||
dealCount?: number;
|
||
}
|
||
|
||
export interface DealDto {
|
||
id: string;
|
||
title: string;
|
||
/** کد دستی معامله */
|
||
code: string | null;
|
||
/** منبع ورود: manual | didar | excel */
|
||
importSource: string;
|
||
promotedFromPriorAt: string | null;
|
||
/** شناسه در مبدأ (کد دیدار) */
|
||
sourceRef: string | null;
|
||
amount: number | null;
|
||
currency: string;
|
||
status: DealStatus;
|
||
/** احتمال موفقیت ۰ تا ۱۰۰ */
|
||
probability: number | null;
|
||
source: string | null;
|
||
sourceDetail: string | null;
|
||
referrer: string | null;
|
||
lostReason: string | null;
|
||
lostNote: string | null;
|
||
closedAt: string | null;
|
||
createdBy: UserSummaryDto | null;
|
||
notes: string | null;
|
||
tags: string[];
|
||
isStale: boolean;
|
||
isRotten: boolean;
|
||
customFields: Record<string, unknown>;
|
||
expectedCloseAt: string | null;
|
||
position: number;
|
||
pipelineId: string;
|
||
stageId: string;
|
||
person: PersonSummaryDto | null;
|
||
company: CompanySummaryDto | null;
|
||
owner: UserSummaryDto | null;
|
||
/** مسئولان مشترک معامله (علاوه بر مالک اصلی) */
|
||
assignees: UserSummaryDto[];
|
||
lineItems: DealLineItemDto[];
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface PipelineBoardStageDto extends PipelineStageDto {
|
||
deals: DealDto[];
|
||
totalAmount: number;
|
||
}
|
||
|
||
export interface PipelineBoardDto {
|
||
pipeline: PipelineDto;
|
||
stages: PipelineBoardStageDto[];
|
||
}
|
||
|
||
export interface CreatePipelineRequest {
|
||
name: string;
|
||
}
|
||
|
||
export interface CreatePipelineStageRequest {
|
||
name: string;
|
||
}
|
||
|
||
export interface UpdatePipelineStageRequest {
|
||
name?: string;
|
||
}
|
||
|
||
export interface ReorderPipelineStagesRequest {
|
||
stageIds: string[];
|
||
}
|
||
|
||
export type PipelineMergeLeadFormMode = 'retarget_all';
|
||
|
||
export interface PipelineMergePreviewStageDto {
|
||
id: string;
|
||
name: string;
|
||
isWon: boolean;
|
||
isLost: boolean;
|
||
dealCount: number;
|
||
}
|
||
|
||
export interface PipelineMergePreviewTargetDto {
|
||
id: string;
|
||
name: string;
|
||
isDefault: boolean;
|
||
stages: Array<{
|
||
id: string;
|
||
name: string;
|
||
isWon: boolean;
|
||
isLost: boolean;
|
||
}>;
|
||
}
|
||
|
||
export interface PipelineMergePreviewDto {
|
||
source: { id: string; name: string; isDefault: boolean };
|
||
stages: PipelineMergePreviewStageDto[];
|
||
dealCount: number;
|
||
leadForms: Array<{ id: string; name: string; isActive: boolean }>;
|
||
targets: PipelineMergePreviewTargetDto[];
|
||
}
|
||
|
||
export interface MergePipelineRequest {
|
||
targetPipelineId: string;
|
||
stageMap: Record<string, string>;
|
||
leadFormMode?: PipelineMergeLeadFormMode;
|
||
}
|
||
|
||
export interface MergePipelineResultDto {
|
||
ok: true;
|
||
targetPipelineId: string;
|
||
movedDeals: number;
|
||
retargetedLeadForms: number;
|
||
deletedPipelineId: string;
|
||
}
|
||
|
||
export interface CreateDealRequest {
|
||
title: string;
|
||
pipelineId?: string;
|
||
stageId?: string;
|
||
code?: string | null;
|
||
amount?: number | null;
|
||
currency?: string;
|
||
probability?: number | null;
|
||
source?: string;
|
||
sourceDetail?: string | null;
|
||
referrer?: string | null;
|
||
notes?: string;
|
||
tags?: string[];
|
||
isStale?: boolean;
|
||
isRotten?: boolean;
|
||
expectedCloseAt?: string | null;
|
||
lostReason?: string | null;
|
||
lostNote?: string | null;
|
||
closedAt?: string | null;
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
ownerId?: string | null;
|
||
assigneeIds?: string[];
|
||
customFields?: Record<string, unknown>;
|
||
lineItems?: DealLineItemInput[];
|
||
}
|
||
|
||
export interface UpdateDealRequest {
|
||
title?: string;
|
||
code?: string | null;
|
||
amount?: number | null;
|
||
currency?: string;
|
||
probability?: number | null;
|
||
source?: string;
|
||
sourceDetail?: string | null;
|
||
referrer?: string | null;
|
||
notes?: string;
|
||
tags?: string[];
|
||
isStale?: boolean;
|
||
isRotten?: boolean;
|
||
lostReason?: string | null;
|
||
lostNote?: string | null;
|
||
closedAt?: string | null;
|
||
expectedCloseAt?: string | null;
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
ownerId?: string | null;
|
||
assigneeIds?: string[];
|
||
customFields?: Record<string, unknown>;
|
||
lineItems?: DealLineItemInput[];
|
||
}
|
||
|
||
/** ستونهای اکسل معامله — همترتیب با خروجی معاملات دیدار */
|
||
export const DEAL_EXCEL_COLUMNS: Array<{ header: string; key: string; width?: number }> = [
|
||
{ header: "_type", key: "_type", width: 8 },
|
||
{ header: "کد دیدار معامله", key: "didarId", width: 14 },
|
||
{ header: "کد دستی معامله", key: "code", width: 14 },
|
||
{ header: "عنوان معامله", key: "title", width: 30 },
|
||
{ header: "ارزش معامله", key: "amount", width: 16 },
|
||
{ header: "ارز معامله", key: "currency", width: 10 },
|
||
{ header: "توضیحات معامله", key: "notes", width: 32 },
|
||
{ header: "وضعیت معامله", key: "status", width: 12 },
|
||
{ header: "تاریخ انجام معامله", key: "closedAt", width: 14 },
|
||
{ header: "تاریخ ایجاد معامله", key: "createdAt", width: 14 },
|
||
{ header: "تاریخ احتمالی انجام معامله", key: "expectedCloseAt", width: 16 },
|
||
{ header: "مسئول معامله", key: "ownerName", width: 18 },
|
||
{ header: "ثبت کننده معامله", key: "creatorName", width: 18 },
|
||
{ header: "احتمال موفقیت معامله", key: "probability", width: 12 },
|
||
{ header: "دلیل شکست معامله", key: "lostReason", width: 18 },
|
||
{ header: "یادداشت شکست معامله", key: "lostNote", width: 24 },
|
||
{ header: "مجوز مشاهده معامله", key: "visibility", width: 18 },
|
||
{ header: "شیوه آشنایی معامله", key: "source", width: 16 },
|
||
{ header: "معرف معامله", key: "referrer", width: 16 },
|
||
{ header: "شیوه آشنایی دلخواه", key: "sourceDetail", width: 16 },
|
||
{ header: "برچسب", key: "tags", width: 16 },
|
||
{ header: "مسیر فروش معامله", key: "pipelineName", width: 20 },
|
||
{ header: "مرحله مسیر فروش معامله", key: "stageName", width: 20 },
|
||
{ header: "نام شخص معامله", key: "personFirstName", width: 16 },
|
||
{ header: "نام خانوادگی شخص معامله", key: "personName", width: 22 },
|
||
{ header: "کد دیدار شخص معامله", key: "personCode", width: 14 },
|
||
{ header: "موبایل شخص معامله", key: "personMobile", width: 14 },
|
||
{ header: "شرکت معامله", key: "companyName", width: 20 },
|
||
{ header: "کد دیدار شرکت معامله", key: "companyCode", width: 14 },
|
||
{ header: "شماره تلفن شرکت", key: "companyPhone", width: 14 },
|
||
{ header: "عنوان محصول", key: "productName", width: 20 },
|
||
{ header: "کد محصول", key: "productCode", width: 12 },
|
||
{ header: "دسته بندی محصول", key: "productCategory", width: 14 },
|
||
{ header: "تعداد محصول", key: "productQty", width: 10 },
|
||
{ header: "قیمت", key: "productPrice", width: 14 },
|
||
{ header: "واحد قیمت محصول", key: "productCurrency", width: 12 },
|
||
{ header: "میزان تخفیف محصول", key: "productDiscount", width: 12 },
|
||
{ header: "مبلغ نهایی", key: "productTotal", width: 14 },
|
||
{ header: "گروه محصول", key: "productGroup", width: 14 },
|
||
];
|
||
|
||
/** کلید ستونهای ناشناخته اکسل که بهعنوان فیلد سفارشی ذخیره میشوند: cf:<عنوان ستون> */
|
||
export const EXCEL_CUSTOM_FIELD_PREFIX = "cf:";
|
||
|
||
export function compactExcelHeader(raw: string): string {
|
||
return raw
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[_-]+/g, "")
|
||
.replace(/\u200c/g, "")
|
||
.replace(/\s+/g, "")
|
||
.replace(/[()]/g, "");
|
||
}
|
||
|
||
export const DEAL_EXCEL_ALIASES: Record<string, string> = {
|
||
...Object.fromEntries(DEAL_EXCEL_COLUMNS.map((c) => [compactExcelHeader(c.header), c.key])),
|
||
type: "_type",
|
||
عنوان: "title",
|
||
مبلغمعامله: "amount",
|
||
مبلغ: "amount",
|
||
ارز: "currency",
|
||
وضعیت: "status",
|
||
توضیحات: "notes",
|
||
تاریخثبتمعامله: "createdAt",
|
||
کددیدارشخص: "personCode",
|
||
نامشرکتمعامله: "companyName",
|
||
شرکت: "companyName",
|
||
منبعمعامله: "source",
|
||
منبع: "source",
|
||
مسیرفروش: "pipelineName",
|
||
مسیر: "pipelineName",
|
||
// سرستونهای قدیمی (قبل از تغییر نام کاریز → مسیر فروش)
|
||
کاریز: "pipelineName",
|
||
کاریزمعامله: "pipelineName",
|
||
مرحلهکاریزمعامله: "stageName",
|
||
مرحله: "stageName",
|
||
};
|
||
/** فیلتر ذخیرهشدهٔ لیست اشخاص */
|
||
export interface PeopleViewFilters {
|
||
q?: string;
|
||
sort?: string;
|
||
mood?: string;
|
||
page?: number;
|
||
}
|
||
|
||
/** فیلتر ذخیرهشدهٔ مسیر فروش / معاملات */
|
||
export interface DealsViewFilters {
|
||
q?: string;
|
||
amountMin?: string;
|
||
amountMax?: string;
|
||
pipelineId?: string;
|
||
viewMode?: string;
|
||
}
|
||
export interface MoveDealRequest {
|
||
stageId: string;
|
||
position?: number;
|
||
}
|
||
|
||
export type ActivityType = "call" | "meeting" | "task" | "note";
|
||
export type ActivityStatus = "open" | "done" | "canceled";
|
||
export type ActivityPriority = "very_low" | "low" | "medium" | "high" | "very_high";
|
||
export type ActivityFilter = "today" | "overdue" | "upcoming" | "done" | "all";
|
||
|
||
export type ActivityStageColor = "green" | "orange" | "purple" | "blue" | "rose" | "slate";
|
||
|
||
export const ACTIVITY_STAGE_COLORS: ActivityStageColor[] = [
|
||
"green",
|
||
"orange",
|
||
"purple",
|
||
"blue",
|
||
"rose",
|
||
"slate",
|
||
];
|
||
|
||
export const ACTIVITY_STAGE_COLOR_LABELS: Record<ActivityStageColor, string> = {
|
||
green: "سبز",
|
||
orange: "نارنجی",
|
||
purple: "بنفش",
|
||
blue: "آبی",
|
||
rose: "قرمز",
|
||
slate: "خاکستری",
|
||
};
|
||
|
||
/** نوع مرحله = وضعیتی که فعالیت با رفتن به آن ستون میگیرد */
|
||
export const ACTIVITY_STAGE_KIND_LABELS: Record<ActivityStatus, string> = {
|
||
open: "باز (در جریان)",
|
||
done: "انجامشده",
|
||
canceled: "لغو شده",
|
||
};
|
||
|
||
/** گروه فعالیت (مثل مسیر فروش): هر گروه ستونهای خودش را دارد */
|
||
export interface ActivityBoardDto {
|
||
id: string;
|
||
name: string;
|
||
isDefault: boolean;
|
||
position: number;
|
||
/** خالی = گروه برای همهٔ اعضای محیط باز است */
|
||
memberIds: string[];
|
||
}
|
||
|
||
export interface CreateActivityBoardRequest {
|
||
name: string;
|
||
memberIds?: string[];
|
||
}
|
||
|
||
export interface UpdateActivityBoardRequest {
|
||
name?: string;
|
||
memberIds?: string[];
|
||
}
|
||
|
||
export interface ActivityStageDto {
|
||
id: string;
|
||
boardId: string;
|
||
name: string;
|
||
position: number;
|
||
color: ActivityStageColor;
|
||
kind: ActivityStatus;
|
||
}
|
||
|
||
export interface CreateActivityStageRequest {
|
||
/** خالی = گروه پیشفرض */
|
||
boardId?: string;
|
||
name: string;
|
||
color?: ActivityStageColor;
|
||
kind?: ActivityStatus;
|
||
}
|
||
|
||
export interface UpdateActivityStageRequest {
|
||
name?: string;
|
||
color?: ActivityStageColor;
|
||
kind?: ActivityStatus;
|
||
}
|
||
|
||
export interface ReorderActivityStagesRequest {
|
||
stageIds: string[];
|
||
}
|
||
|
||
export interface MoveActivityStageRequest {
|
||
stageId: string;
|
||
}
|
||
|
||
/** شمارش فعالیتها به تفکیک مرحله با فیلترهای فعلی */
|
||
export interface ActivityStageCountsDto {
|
||
counts: Record<string, number>;
|
||
total: number;
|
||
}
|
||
|
||
/** برچسب نوع پایه (برای گزارشها و فعالیتهای بدون نوع سفارشی) */
|
||
export const ACTIVITY_TYPE_LABELS: Record<ActivityType, string> = {
|
||
call: "تماس",
|
||
meeting: "جلسه",
|
||
task: "وظیفه",
|
||
note: "پیام",
|
||
};
|
||
|
||
export type ActivityFieldType =
|
||
| "text"
|
||
| "textarea"
|
||
| "number"
|
||
| "date"
|
||
| "url"
|
||
| "select"
|
||
| "multiselect"
|
||
| "checkbox"
|
||
| "checklist"
|
||
| "participants";
|
||
|
||
export const ACTIVITY_FIELD_TYPES: ActivityFieldType[] = [
|
||
"text",
|
||
"textarea",
|
||
"number",
|
||
"date",
|
||
"url",
|
||
"select",
|
||
"multiselect",
|
||
"checkbox",
|
||
"checklist",
|
||
"participants",
|
||
];
|
||
|
||
export const ACTIVITY_FIELD_TYPE_LABELS: Record<ActivityFieldType, string> = {
|
||
text: "متن کوتاه",
|
||
textarea: "متن بلند",
|
||
number: "عدد",
|
||
date: "تاریخ",
|
||
url: "لینک",
|
||
select: "انتخاب از لیست",
|
||
multiselect: "چندانتخابی",
|
||
checkbox: "بله / خیر",
|
||
checklist: "چکلیست (لیست کارها)",
|
||
participants: "شرکتکنندگان",
|
||
};
|
||
|
||
/** فیلدهایی که گزینه لازم دارند */
|
||
export const ACTIVITY_FIELD_TYPES_WITH_OPTIONS: ActivityFieldType[] = ["select", "multiselect"];
|
||
|
||
export interface ActivityFieldDef {
|
||
/** کلید پایدار (a-z، 0-9، _) — با تغییر برچسب عوض نمیشود */
|
||
key: string;
|
||
label: string;
|
||
type: ActivityFieldType;
|
||
options?: string[];
|
||
required?: boolean;
|
||
placeholder?: string | null;
|
||
}
|
||
|
||
export interface ActivityChecklistItem {
|
||
id: string;
|
||
text: string;
|
||
done: boolean;
|
||
}
|
||
|
||
export interface ActivityParticipantsValue {
|
||
/** کاربران سازمان — فعالیت در لیست آنها هم دیده میشود */
|
||
userIds: string[];
|
||
/** اشخاص (مشتریان) */
|
||
people: Array<{ id: string; name: string }>;
|
||
/** مهمانان آزاد (نام) */
|
||
guests: string[];
|
||
}
|
||
|
||
export type ActivityFieldValue =
|
||
| string
|
||
| number
|
||
| boolean
|
||
| string[]
|
||
| ActivityChecklistItem[]
|
||
| ActivityParticipantsValue
|
||
| null;
|
||
|
||
export type ActivityFieldValues = Record<string, ActivityFieldValue>;
|
||
|
||
export const ACTIVITY_TYPE_ICONS = [
|
||
"phone",
|
||
"users",
|
||
"check",
|
||
"chat",
|
||
"mail",
|
||
"building",
|
||
"calendar",
|
||
"star",
|
||
"flag",
|
||
"briefcase",
|
||
"document",
|
||
"target",
|
||
"truck",
|
||
"send",
|
||
"megaphone",
|
||
"pin",
|
||
"bell",
|
||
"sparkle",
|
||
] as const;
|
||
|
||
export type ActivityTypeIcon = (typeof ACTIVITY_TYPE_ICONS)[number];
|
||
|
||
export interface ActivityTypeSummaryDto {
|
||
id: string;
|
||
name: string;
|
||
icon: ActivityTypeIcon;
|
||
color: ActivityStageColor;
|
||
}
|
||
|
||
export interface ActivityTypeDefDto extends ActivityTypeSummaryDto {
|
||
/** نوع پایه — مبنای گزارشها و سازگاری */
|
||
baseType: ActivityType;
|
||
fields: ActivityFieldDef[];
|
||
position: number;
|
||
isActive: boolean;
|
||
/** چهار نوع اصلی — حذفشدنی نیستند */
|
||
isSystem: boolean;
|
||
}
|
||
|
||
export interface CreateActivityTypeRequest {
|
||
name: string;
|
||
baseType?: ActivityType;
|
||
icon?: ActivityTypeIcon;
|
||
color?: ActivityStageColor;
|
||
fields?: ActivityFieldDef[];
|
||
}
|
||
|
||
export interface UpdateActivityTypeRequest {
|
||
name?: string;
|
||
baseType?: ActivityType;
|
||
icon?: ActivityTypeIcon;
|
||
color?: ActivityStageColor;
|
||
fields?: ActivityFieldDef[];
|
||
isActive?: boolean;
|
||
}
|
||
|
||
export interface ReorderActivityTypesRequest {
|
||
typeIds: string[];
|
||
}
|
||
|
||
export type CustomFieldEntity = "person" | "company" | "deal" | "note";
|
||
export type CustomFieldType = "text" | "number" | "date" | "boolean" | "select";
|
||
|
||
export interface CustomFieldDefinitionDto {
|
||
id: string;
|
||
entityType: CustomFieldEntity;
|
||
key: string;
|
||
label: string;
|
||
fieldType: CustomFieldType;
|
||
options: string[];
|
||
required: boolean;
|
||
position: number;
|
||
isActive: boolean;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateCustomFieldRequest {
|
||
entityType: CustomFieldEntity;
|
||
label: string;
|
||
key?: string;
|
||
fieldType?: CustomFieldType;
|
||
options?: string[];
|
||
required?: boolean;
|
||
position?: number;
|
||
isActive?: boolean;
|
||
}
|
||
|
||
export interface UpdateCustomFieldRequest {
|
||
label?: string;
|
||
fieldType?: CustomFieldType;
|
||
options?: string[];
|
||
required?: boolean;
|
||
position?: number;
|
||
isActive?: boolean;
|
||
}
|
||
|
||
export interface DealSummaryDto {
|
||
id: string;
|
||
title: string;
|
||
}
|
||
|
||
export interface ActivityImageDto {
|
||
id: string;
|
||
fileName: string;
|
||
mimeType: string;
|
||
sizeBytes: number;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface ActivityDto {
|
||
id: string;
|
||
type: ActivityType;
|
||
status: ActivityStatus;
|
||
priority: ActivityPriority;
|
||
title: string;
|
||
description: string | null;
|
||
/** عکسهای یادداشت؛ فقط در جزئیات فعالیت پر میشود */
|
||
images?: ActivityImageDto[];
|
||
dueAt: string | null;
|
||
durationMinutes: number | null;
|
||
reminderAt: string | null;
|
||
reminderNotifyApp: boolean;
|
||
reminderNotifySms: boolean;
|
||
/** زمان ارسال یادآور (اگر ارسال شده باشد) */
|
||
reminderSentAt: string | null;
|
||
completedAt: string | null;
|
||
/** manual یا didar؛ برای جدا کردن سابقهٔ قبلی */
|
||
importSource?: string;
|
||
promotedFromPriorAt?: string | null;
|
||
tags: string[];
|
||
person: PersonSummaryDto | null;
|
||
company: CompanySummaryDto | null;
|
||
deal: DealSummaryDto | null;
|
||
/** لید مرتبط (مثلاً فعالیت ساختهشده از فرم) */
|
||
lead?: { id: string; title: string } | null;
|
||
/** مسئول اصلی (اولین مسئول) — سازگاری با فیلترها و یادآور */
|
||
assignee: UserSummaryDto | null;
|
||
/** همه مسئولان انجام فعالیت */
|
||
assignees: UserSummaryDto[];
|
||
createdBy: UserSummaryDto | null;
|
||
/** گروه مؤثر فعالیت (گروه پیشفرض اگر گروهی انتخاب نشده باشد) */
|
||
boardId?: string | null;
|
||
/** مرحلهٔ مؤثر (ستون کانبان) */
|
||
stageId?: string | null;
|
||
/** نوع مؤثر (سفارشی یا نوع اصلیِ همان type) */
|
||
typeDef?: ActivityTypeSummaryDto | null;
|
||
/** مقادیر فیلدهای اختصاصی نوع */
|
||
fields?: ActivityFieldValues;
|
||
/** کاربران شرکتکننده (از فیلدهای شرکتکنندگان) */
|
||
participantIds?: string[];
|
||
/** نام کاربران شرکتکننده — فقط در جزئیات فعالیت */
|
||
participantUsers?: UserSummaryDto[];
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateActivityRequest {
|
||
title: string;
|
||
/** گروه فعالیت؛ خالی = گروه پیشفرض. اگر stageId بیاید، گروه از همان مرحله گرفته میشود */
|
||
boardId?: string | null;
|
||
/** مرحلهٔ اولیه؛ وضعیت فعالیت از نوع مرحله گرفته میشود */
|
||
stageId?: string | null;
|
||
type?: ActivityType;
|
||
/** نوع سفارشی؛ type از نوع پایهٔ آن گرفته میشود */
|
||
typeDefId?: string | null;
|
||
fields?: ActivityFieldValues;
|
||
priority?: ActivityPriority;
|
||
description?: string;
|
||
dueAt?: string | null;
|
||
durationMinutes?: number | null;
|
||
reminderAt?: string | null;
|
||
reminderNotifyApp?: boolean;
|
||
reminderNotifySms?: boolean;
|
||
tags?: string[];
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
dealId?: string | null;
|
||
/** سازگاری — اگر assigneeIds نباشد از این استفاده میشود */
|
||
assigneeId?: string | null;
|
||
/** چند مسئول؛ اولی بهعنوان assigneeId ذخیره میشود */
|
||
assigneeIds?: string[];
|
||
}
|
||
|
||
export interface UpdateActivityRequest {
|
||
title?: string;
|
||
/** انتقال به گروه دیگر؛ فعالیت به ستون اولِ هموضعیت آن گروه میرود */
|
||
boardId?: string | null;
|
||
type?: ActivityType;
|
||
typeDefId?: string | null;
|
||
fields?: ActivityFieldValues;
|
||
status?: ActivityStatus;
|
||
priority?: ActivityPriority;
|
||
description?: string | null;
|
||
dueAt?: string | null;
|
||
durationMinutes?: number | null;
|
||
reminderAt?: string | null;
|
||
reminderNotifyApp?: boolean;
|
||
reminderNotifySms?: boolean;
|
||
tags?: string[];
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
dealId?: string | null;
|
||
assigneeId?: string | null;
|
||
assigneeIds?: string[];
|
||
}
|
||
|
||
export interface ActivitySummaryDto {
|
||
today: ActivityDto[];
|
||
overdue: ActivityDto[];
|
||
upcoming: ActivityDto[];
|
||
todayCount: number;
|
||
overdueCount: number;
|
||
}
|
||
|
||
export type NoteType =
|
||
| "general"
|
||
| "call_result"
|
||
| "meeting"
|
||
| "important"
|
||
| "warning"
|
||
| "next_action"
|
||
| "follow_up"
|
||
| "idea"
|
||
| "suggestion"
|
||
| "personal";
|
||
|
||
export interface NoteAttachmentDto {
|
||
id: string;
|
||
fileName: string;
|
||
mimeType: string;
|
||
sizeBytes: number;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface NoteChecklistItemDto {
|
||
id: string;
|
||
text: string;
|
||
done: boolean;
|
||
}
|
||
|
||
export interface NoteDto {
|
||
id: string;
|
||
title: string | null;
|
||
body: string;
|
||
noteType: NoteType | string;
|
||
/** team = تیمی | personal = فقط نویسنده */
|
||
visibility: NoteVisibility;
|
||
pinned: boolean;
|
||
color: string | null;
|
||
tags: string[];
|
||
reminderAt: string | null;
|
||
checklist: NoteChecklistItemDto[];
|
||
locationText: string | null;
|
||
analyzeEnabled: boolean;
|
||
customFields: Record<string, unknown>;
|
||
person: PersonSummaryDto | null;
|
||
company: CompanySummaryDto | null;
|
||
deal: DealSummaryDto | null;
|
||
activity: { id: string; title: string } | null;
|
||
author: UserSummaryDto | null;
|
||
attachments: NoteAttachmentDto[];
|
||
/** تعداد نسخههای قبلی (برای نمایش دکمه تاریخچه) */
|
||
revisionCount?: number;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export type NoteVisibility = "team" | "personal";
|
||
|
||
export const NOTE_VISIBILITIES: NoteVisibility[] = ["team", "personal"];
|
||
|
||
export const NOTE_VISIBILITY_LABELS: Record<NoteVisibility, string> = {
|
||
team: "تیمی",
|
||
personal: "شخصی (فقط من)",
|
||
};
|
||
|
||
export type NoteCategoryTone =
|
||
| "general"
|
||
| "follow_up"
|
||
| "idea"
|
||
| "suggestion"
|
||
| "meeting"
|
||
| "personal";
|
||
|
||
export interface NoteCategoryDto {
|
||
id: string;
|
||
key: string;
|
||
label: string;
|
||
tone: NoteCategoryTone | string;
|
||
position: number;
|
||
isActive: boolean;
|
||
showInBar: boolean;
|
||
isSystem: boolean;
|
||
}
|
||
|
||
export interface CreateNoteCategoryRequest {
|
||
label: string;
|
||
tone?: NoteCategoryTone | string;
|
||
showInBar?: boolean;
|
||
}
|
||
|
||
export interface UpdateNoteCategoryRequest {
|
||
label?: string;
|
||
tone?: NoteCategoryTone | string;
|
||
showInBar?: boolean;
|
||
isActive?: boolean;
|
||
position?: number;
|
||
}
|
||
|
||
export interface ReorderNoteCategoriesRequest {
|
||
ids: string[];
|
||
}
|
||
|
||
export interface NoteRevisionDto {
|
||
id: string;
|
||
title: string | null;
|
||
body: string;
|
||
noteType: string;
|
||
visibility: NoteVisibility;
|
||
tags: string[];
|
||
checklist: NoteChecklistItemDto[];
|
||
editor: UserSummaryDto | null;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface CreateNoteRequest {
|
||
title?: string;
|
||
body: string;
|
||
noteType?: NoteType | string;
|
||
visibility?: NoteVisibility;
|
||
pinned?: boolean;
|
||
color?: string;
|
||
tags?: string[];
|
||
reminderAt?: string | null;
|
||
checklist?: NoteChecklistItemDto[];
|
||
locationText?: string | null;
|
||
analyzeEnabled?: boolean;
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
dealId?: string | null;
|
||
activityId?: string | null;
|
||
customFields?: Record<string, unknown>;
|
||
}
|
||
|
||
export interface UpdateNoteRequest {
|
||
title?: string | null;
|
||
body?: string;
|
||
noteType?: NoteType | string;
|
||
visibility?: NoteVisibility;
|
||
pinned?: boolean;
|
||
color?: string | null;
|
||
tags?: string[];
|
||
reminderAt?: string | null;
|
||
checklist?: NoteChecklistItemDto[];
|
||
locationText?: string | null;
|
||
analyzeEnabled?: boolean;
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
dealId?: string | null;
|
||
activityId?: string | null;
|
||
customFields?: Record<string, unknown>;
|
||
}
|
||
|
||
export type NoteAiSuggestionStatus =
|
||
| 'pending'
|
||
| 'applied'
|
||
| 'dismissed'
|
||
| 'failed';
|
||
|
||
export type MemorySourceType = 'note' | 'manual';
|
||
|
||
export interface NoteAiNextActionDto {
|
||
title: string;
|
||
description?: string;
|
||
dueAt?: string | null;
|
||
}
|
||
|
||
export interface NoteAiMemoryExtractDto {
|
||
content: string;
|
||
category?: string;
|
||
}
|
||
|
||
export interface NoteAiPayloadDto {
|
||
nextAction: NoteAiNextActionDto | null;
|
||
memoryItems: NoteAiMemoryExtractDto[];
|
||
summary?: string;
|
||
}
|
||
|
||
export interface NoteAiSuggestionDto {
|
||
id: string;
|
||
noteId: string;
|
||
status: NoteAiSuggestionStatus;
|
||
payload: NoteAiPayloadDto | null;
|
||
errorMessage: string | null;
|
||
analyzedAt: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface ApplyNoteAiSuggestionRequest {
|
||
applyNextAction?: boolean;
|
||
memoryIndices?: number[];
|
||
nextAction?: NoteAiNextActionDto;
|
||
memoryItems?: Array<{ index: number; content: string }>;
|
||
}
|
||
|
||
export type AskMentoSourceType = 'person' | 'note' | 'activity' | 'deal' | 'memory';
|
||
|
||
export interface AskMentoSourceDto {
|
||
type: AskMentoSourceType;
|
||
id: string;
|
||
label: string;
|
||
}
|
||
|
||
export interface AskMentoRequest {
|
||
question: string;
|
||
personId?: string;
|
||
/** زمینهٔ اختیاری گفتگو برای پاسخ مرتبطتر داخل چت */
|
||
chatContext?: string;
|
||
}
|
||
|
||
export interface AskMentoResponse {
|
||
answer: string;
|
||
sources: AskMentoSourceDto[];
|
||
}
|
||
|
||
export type VoiceCommandIntent = 'create_person' | 'create_activity' | 'none';
|
||
|
||
export interface VoicePersonDraft {
|
||
firstName: string;
|
||
lastName: string;
|
||
mobile: string;
|
||
phone: string;
|
||
email: string;
|
||
website: string;
|
||
referrer: string;
|
||
position: string;
|
||
notes: string;
|
||
}
|
||
|
||
export interface VoiceActivityDraft {
|
||
title: string;
|
||
type: ActivityType;
|
||
description: string;
|
||
dueAt: string | null;
|
||
personName: string;
|
||
personId: string | null;
|
||
}
|
||
|
||
export interface VoiceCommandDraft {
|
||
intent: VoiceCommandIntent;
|
||
summary: string;
|
||
missing: string[];
|
||
canConfirm: boolean;
|
||
person: VoicePersonDraft | null;
|
||
activity: VoiceActivityDraft | null;
|
||
}
|
||
|
||
export interface PersonAiSummaryDto {
|
||
text: string;
|
||
cta: string;
|
||
ctaKind: 'call' | 'activity';
|
||
}
|
||
|
||
export interface MemoryItemDto {
|
||
id: string;
|
||
personId: string;
|
||
content: string;
|
||
sourceType: MemorySourceType;
|
||
sourceId: string | null;
|
||
confirmedBy: UserSummaryDto | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateMemoryItemRequest {
|
||
personId: string;
|
||
content: string;
|
||
}
|
||
|
||
export interface UpdateMemoryItemRequest {
|
||
content: string;
|
||
}
|
||
|
||
export interface ImportContactsResult {
|
||
created: number;
|
||
skipped: number;
|
||
errors: Array<{ row: number; message: string }>;
|
||
boardId?: string;
|
||
boards?: string[];
|
||
/** جلسه چندمرحلهای برای فایلهای بزرگ */
|
||
sessionId?: string;
|
||
/** تعداد کل ردیفهای دادهای فایل */
|
||
total?: number;
|
||
/** تا این آفست پردازش شده */
|
||
processed?: number;
|
||
/** مرحله بعدی وجود دارد */
|
||
hasMore?: boolean;
|
||
nextOffset?: number;
|
||
/** وضعیت آمادهسازی ناهمگام فایل بزرگ */
|
||
status?: "parsing" | "ready" | "error";
|
||
parsed?: number;
|
||
}
|
||
|
||
export interface ExcelImportPrepareStatus {
|
||
sessionId: string;
|
||
status: "parsing" | "ready" | "error";
|
||
total: number;
|
||
parsed: number;
|
||
error?: string;
|
||
hasMore?: boolean;
|
||
nextOffset?: number;
|
||
}
|
||
|
||
export interface ExcelFieldOption {
|
||
key: string;
|
||
label: string;
|
||
required?: boolean;
|
||
}
|
||
|
||
export interface ExcelColumnPreview {
|
||
header: string;
|
||
index: number;
|
||
samples: string[];
|
||
}
|
||
|
||
export interface ExcelPreviewResult {
|
||
columns: ExcelColumnPreview[];
|
||
fields: ExcelFieldOption[];
|
||
suggested: Record<string, string>;
|
||
needsMapping: boolean;
|
||
/** تعداد ردیفهای غیرخالی (بدون هدر) */
|
||
rowCount: number;
|
||
/** پیشنهاد ایمپورت چندمرحلهای */
|
||
largeFile?: boolean;
|
||
}
|
||
|
||
export interface ProductDto {
|
||
id: string;
|
||
kind: "product" | "service";
|
||
name: string;
|
||
sku: string | null;
|
||
description: string | null;
|
||
category: string | null;
|
||
unit: string | null;
|
||
stockQty: number | null;
|
||
brand: string | null;
|
||
model: string | null;
|
||
country: string | null;
|
||
tags: string[];
|
||
unitPrice: number | null;
|
||
currency: string;
|
||
isActive: boolean;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateProductRequest {
|
||
kind?: "product" | "service";
|
||
name: string;
|
||
sku?: string;
|
||
description?: string;
|
||
category?: string;
|
||
unit?: string;
|
||
stockQty?: number | null;
|
||
brand?: string;
|
||
model?: string;
|
||
country?: string;
|
||
tags?: string[];
|
||
unitPrice?: number | null;
|
||
currency?: string;
|
||
isActive?: boolean;
|
||
}
|
||
|
||
export interface UpdateProductRequest {
|
||
kind?: "product" | "service";
|
||
name?: string;
|
||
sku?: string | null;
|
||
description?: string | null;
|
||
category?: string | null;
|
||
unit?: string | null;
|
||
stockQty?: number | null;
|
||
brand?: string | null;
|
||
model?: string | null;
|
||
country?: string | null;
|
||
tags?: string[];
|
||
unitPrice?: number | null;
|
||
currency?: string;
|
||
isActive?: boolean;
|
||
}
|
||
|
||
export type StockMovementType =
|
||
| "opening"
|
||
| "purchase"
|
||
| "sale"
|
||
| "credit"
|
||
| "adjustment";
|
||
|
||
export interface InventoryStockRowDto {
|
||
productId: string;
|
||
name: string;
|
||
sku: string | null;
|
||
unit: string | null;
|
||
kind: "product" | "service";
|
||
stockQty: number;
|
||
currency: string;
|
||
unitPrice: number | null;
|
||
}
|
||
|
||
export interface StockMovementDto {
|
||
id: string;
|
||
productId: string;
|
||
productName: string;
|
||
type: StockMovementType;
|
||
quantity: number;
|
||
note: string | null;
|
||
invoiceId: string | null;
|
||
occurredAt: string;
|
||
}
|
||
|
||
export interface InventorySummaryDto {
|
||
enabled: boolean;
|
||
warehouseName: string | null;
|
||
products: InventoryStockRowDto[];
|
||
cashTotal: number;
|
||
receivablesTotal: number;
|
||
currency: string;
|
||
}
|
||
|
||
export interface CreateStockMovementRequest {
|
||
productId: string;
|
||
type: "purchase" | "adjustment";
|
||
quantity: number;
|
||
note?: string;
|
||
}
|
||
|
||
export interface PartyBalanceDto {
|
||
enabled: boolean;
|
||
personId: string;
|
||
openingAmount: number;
|
||
invoicedAmount: number;
|
||
paidAmount: number;
|
||
balance: number;
|
||
currency: string;
|
||
}
|
||
|
||
export interface UpdatePartyOpeningRequest {
|
||
amount: number;
|
||
note?: string;
|
||
}
|
||
|
||
export interface DealLineItemDto {
|
||
id: string;
|
||
productId: string | null;
|
||
name: string;
|
||
quantity: number;
|
||
unitPrice: number;
|
||
currency: string;
|
||
position: number;
|
||
lineTotal: number;
|
||
}
|
||
|
||
export interface DealLineItemInput {
|
||
productId?: string | null;
|
||
name: string;
|
||
quantity: number;
|
||
unitPrice: number;
|
||
currency?: string;
|
||
}
|
||
|
||
export type NotificationType =
|
||
| "activity_assigned"
|
||
| "activity_overdue"
|
||
| "activity_reminder"
|
||
| "deal_moved"
|
||
| "note_reminder"
|
||
| "cheque_due"
|
||
| "ticket_assigned"
|
||
| "automation"
|
||
| "routine_task_reminder"
|
||
| "routine_task_overdue"
|
||
| "routine_day_summary"
|
||
| "routine_task_assigned"
|
||
| "system";
|
||
|
||
export interface NotificationDto {
|
||
id: string;
|
||
type: NotificationType | string;
|
||
title: string;
|
||
body: string | null;
|
||
link: string | null;
|
||
entityType: string | null;
|
||
entityId: string | null;
|
||
readAt: string | null;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface NotificationsListDto {
|
||
items: NotificationDto[];
|
||
unreadCount: number;
|
||
}
|
||
|
||
export const LEAD_FORM_FIELD_TYPES = [
|
||
"text",
|
||
"textarea",
|
||
"email",
|
||
"phone",
|
||
"select",
|
||
"radio",
|
||
"checkbox",
|
||
"date",
|
||
"number",
|
||
"file",
|
||
"rating",
|
||
"heading",
|
||
] as const;
|
||
export type LeadFormFieldType = (typeof LEAD_FORM_FIELD_TYPES)[number];
|
||
|
||
export type LeadFormMapTo =
|
||
| "person.firstName"
|
||
| "person.lastName"
|
||
| "person.fullName"
|
||
| "person.email"
|
||
| "person.mobile"
|
||
| "person.phone"
|
||
| "person.position"
|
||
| "deal.title"
|
||
| "deal.notes"
|
||
| "lead.title"
|
||
| "lead.contactName"
|
||
| "lead.mobile"
|
||
| "lead.phone"
|
||
| "lead.city"
|
||
| "lead.region"
|
||
| "lead.trade"
|
||
| "lead.notes"
|
||
| "lead.source"
|
||
| "lead.gender"
|
||
| "person.gender"
|
||
| "notes"
|
||
| "custom";
|
||
|
||
export type LeadFormDuplicateMode = "skip" | "update" | "activity";
|
||
|
||
export interface LeadFormFieldDto {
|
||
id: string;
|
||
key: string;
|
||
label: string;
|
||
fieldType: LeadFormFieldType | string;
|
||
required: boolean;
|
||
mapTo: LeadFormMapTo | string;
|
||
position: number;
|
||
placeholder: string | null;
|
||
options: string[];
|
||
}
|
||
|
||
export interface LeadFormDto {
|
||
id: string;
|
||
name: string;
|
||
description: string | null;
|
||
publicToken: string;
|
||
isActive: boolean;
|
||
createLead: boolean;
|
||
createPerson: boolean;
|
||
createDeal: boolean;
|
||
pipelineId: string | null;
|
||
leadStageId: string | null;
|
||
duplicateMode: LeadFormDuplicateMode | string;
|
||
source: string | null;
|
||
thankYouMessage: string | null;
|
||
redirectUrl: string | null;
|
||
assigneeIds: string[];
|
||
submitLabel: string;
|
||
submitColor: string;
|
||
hasCover: boolean;
|
||
fields: LeadFormFieldDto[];
|
||
submissionCount?: number;
|
||
viewCount: number;
|
||
startCount: number;
|
||
submitCount: number;
|
||
leadCreateCount: number;
|
||
duplicateCount: number;
|
||
publicPath: string;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface LeadFormFieldInput {
|
||
key?: string;
|
||
label: string;
|
||
fieldType: LeadFormFieldType | string;
|
||
required?: boolean;
|
||
mapTo: LeadFormMapTo | string;
|
||
position?: number;
|
||
placeholder?: string | null;
|
||
options?: string[];
|
||
}
|
||
|
||
export interface CreateLeadFormRequest {
|
||
name: string;
|
||
description?: string | null;
|
||
createLead?: boolean;
|
||
createPerson?: boolean;
|
||
createDeal?: boolean;
|
||
pipelineId?: string | null;
|
||
leadStageId?: string | null;
|
||
duplicateMode?: LeadFormDuplicateMode | string;
|
||
source?: string | null;
|
||
thankYouMessage?: string | null;
|
||
redirectUrl?: string | null;
|
||
assigneeIds?: string[];
|
||
submitLabel?: string;
|
||
submitColor?: string;
|
||
fields?: LeadFormFieldInput[];
|
||
}
|
||
|
||
export interface UpdateLeadFormRequest {
|
||
name?: string;
|
||
description?: string | null;
|
||
isActive?: boolean;
|
||
createLead?: boolean;
|
||
createPerson?: boolean;
|
||
createDeal?: boolean;
|
||
pipelineId?: string | null;
|
||
leadStageId?: string | null;
|
||
duplicateMode?: LeadFormDuplicateMode | string;
|
||
source?: string | null;
|
||
thankYouMessage?: string | null;
|
||
redirectUrl?: string | null;
|
||
assigneeIds?: string[];
|
||
submitLabel?: string;
|
||
submitColor?: string;
|
||
fields?: LeadFormFieldInput[];
|
||
}
|
||
|
||
export interface PublicLeadFormDto {
|
||
name: string;
|
||
description: string | null;
|
||
thankYouMessage: string | null;
|
||
redirectUrl: string | null;
|
||
submitLabel: string;
|
||
submitColor: string;
|
||
hasCover: boolean;
|
||
coverPath: string | null;
|
||
fields: Array<{
|
||
key: string;
|
||
label: string;
|
||
fieldType: LeadFormFieldType | string;
|
||
required: boolean;
|
||
placeholder: string | null;
|
||
options: string[];
|
||
}>;
|
||
}
|
||
|
||
export interface LeadFormSubmissionDto {
|
||
id: string;
|
||
values: Record<string, string>;
|
||
personId: string | null;
|
||
leadId: string | null;
|
||
status: string;
|
||
utm: Record<string, string> | null;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface LeadFormUtmDto {
|
||
source?: string;
|
||
medium?: string;
|
||
campaign?: string;
|
||
term?: string;
|
||
content?: string;
|
||
landingPage?: string;
|
||
referrer?: string;
|
||
}
|
||
|
||
export interface SubmitLeadFormRequest {
|
||
values: Record<string, string>;
|
||
utm?: LeadFormUtmDto;
|
||
pageUrl?: string;
|
||
referrer?: string;
|
||
/** honeypot — must be empty */
|
||
website?: string;
|
||
}
|
||
|
||
export interface SubmitLeadFormResponse {
|
||
ok: true;
|
||
thankYouMessage: string | null;
|
||
redirectUrl: string | null;
|
||
outcome?: "created" | "updated" | "duplicate_skipped";
|
||
leadId?: string | null;
|
||
duplicate?: boolean;
|
||
message?: string | null;
|
||
}
|
||
|
||
export type LeadFormPublicEvent = "view" | "start";
|
||
|
||
export type QuoteStatus =
|
||
| "draft"
|
||
| "sent"
|
||
| "accepted"
|
||
| "rejected"
|
||
| "expired";
|
||
|
||
export interface QuoteLineItemDto {
|
||
id: string;
|
||
productId: string | null;
|
||
name: string;
|
||
quantity: number;
|
||
unitPrice: number;
|
||
currency: string;
|
||
position: number;
|
||
lineTotal: number;
|
||
}
|
||
|
||
export interface QuoteDto {
|
||
id: string;
|
||
number: string;
|
||
title: string;
|
||
status: QuoteStatus | string;
|
||
currency: string;
|
||
notes: string | null;
|
||
discountAmount?: number;
|
||
taxPercent?: number;
|
||
issuedAt?: string | null;
|
||
validUntil: string | null;
|
||
totalAmount: number;
|
||
subtotal: number;
|
||
taxAmount: number;
|
||
payableAmount: number;
|
||
person: PersonSummaryDto | null;
|
||
company: CompanySummaryDto | null;
|
||
deal: DealSummaryDto | null;
|
||
owner: UserSummaryDto | null;
|
||
lineItems: QuoteLineItemDto[];
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateQuoteRequest {
|
||
title: string;
|
||
status?: QuoteStatus | string;
|
||
currency?: string;
|
||
notes?: string;
|
||
discountAmount?: number;
|
||
taxPercent?: number;
|
||
issuedAt?: string | null;
|
||
validityDays?: number;
|
||
validUntil?: string | null;
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
dealId?: string | null;
|
||
fromDealId?: string | null;
|
||
lineItems?: DealLineItemInput[];
|
||
}
|
||
|
||
export interface UpdateQuoteRequest {
|
||
title?: string;
|
||
status?: QuoteStatus | string;
|
||
currency?: string;
|
||
notes?: string | null;
|
||
discountAmount?: number;
|
||
taxPercent?: number;
|
||
issuedAt?: string | null;
|
||
validUntil?: string | null;
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
dealId?: string | null;
|
||
lineItems?: DealLineItemInput[];
|
||
}
|
||
|
||
export type InvoiceStatus =
|
||
| "draft"
|
||
| "issued"
|
||
| "partially_paid"
|
||
| "paid"
|
||
| "canceled";
|
||
|
||
export type PaymentMethod =
|
||
| "cash"
|
||
| "transfer"
|
||
| "card"
|
||
| "check"
|
||
| "other";
|
||
|
||
export type InstallmentStatus = "pending" | "paid" | "canceled";
|
||
|
||
export interface InvoiceLineItemDto {
|
||
id: string;
|
||
productId: string | null;
|
||
name: string;
|
||
quantity: number;
|
||
unitPrice: number;
|
||
currency: string;
|
||
position: number;
|
||
lineTotal: number;
|
||
}
|
||
|
||
export type ChequeStatus = "pending" | "cleared" | "bounced";
|
||
export type ChequeDirection = "in" | "out";
|
||
|
||
export interface ChequeDto {
|
||
id: string;
|
||
paymentId: string | null;
|
||
invoiceId: string | null;
|
||
invoiceNumber: string | null;
|
||
invoiceTitle: string | null;
|
||
personId: string | null;
|
||
personName: string | null;
|
||
amount: number;
|
||
currency: string;
|
||
number: string;
|
||
bank: string;
|
||
dueDate: string;
|
||
status: ChequeStatus | string;
|
||
direction: ChequeDirection | string;
|
||
notes: string | null;
|
||
paidAt: string;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface ChequeInput {
|
||
number: string;
|
||
bank: string;
|
||
dueDate: string;
|
||
status?: ChequeStatus | string;
|
||
notes?: string;
|
||
}
|
||
|
||
export interface CreateChequeRequest {
|
||
personId: string;
|
||
amount: number;
|
||
currency?: string;
|
||
number: string;
|
||
bank: string;
|
||
dueDate: string;
|
||
status?: ChequeStatus | string;
|
||
direction?: ChequeDirection | string;
|
||
notes?: string;
|
||
financialAccountId?: string | null;
|
||
}
|
||
|
||
export interface UpdateChequeRequest {
|
||
number?: string;
|
||
bank?: string;
|
||
dueDate?: string;
|
||
status?: ChequeStatus | string;
|
||
notes?: string | null;
|
||
financialAccountId?: string | null;
|
||
}
|
||
|
||
export interface PaymentDto {
|
||
id: string;
|
||
amount: number;
|
||
currency: string;
|
||
method: PaymentMethod | string;
|
||
paidAt: string;
|
||
notes: string | null;
|
||
installmentId: string | null;
|
||
cheque?: ChequeDto | null;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface CommissionEntryDto {
|
||
id: string;
|
||
userId: string;
|
||
userName: string;
|
||
invoiceId: string;
|
||
invoiceNumber: string;
|
||
invoiceTitle: string;
|
||
percent: number;
|
||
amount: number;
|
||
currency: string;
|
||
status: "posted" | "void" | string;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface InvoiceInstallmentDto {
|
||
id: string;
|
||
position: number;
|
||
dueDate: string;
|
||
amount: number;
|
||
currency: string;
|
||
status: InstallmentStatus | string;
|
||
paidAt: string | null;
|
||
paymentId: string | null;
|
||
}
|
||
|
||
export interface InvoiceDto {
|
||
id: string;
|
||
number: string;
|
||
title: string;
|
||
status: InvoiceStatus | string;
|
||
kind?: "invoice" | "credit";
|
||
currency: string;
|
||
notes: string | null;
|
||
discountAmount?: number;
|
||
taxPercent?: number;
|
||
issuedAt: string | null;
|
||
dueDate: string | null;
|
||
totalAmount: number;
|
||
subtotal: number;
|
||
taxAmount: number;
|
||
payableAmount: number;
|
||
paidAmount: number;
|
||
remainingAmount: number;
|
||
person: PersonSummaryDto | null;
|
||
company: CompanySummaryDto | null;
|
||
deal: DealSummaryDto | null;
|
||
quote: { id: string; number: string; title: string } | null;
|
||
owner: UserSummaryDto | null;
|
||
lineItems: InvoiceLineItemDto[];
|
||
payments: PaymentDto[];
|
||
installments: InvoiceInstallmentDto[];
|
||
commissionPercent?: number | null;
|
||
commissionFixedAmount?: number | null;
|
||
commission?: {
|
||
percent: number;
|
||
amount: number;
|
||
status: string;
|
||
} | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface InvoiceInstallmentInput {
|
||
dueDate: string;
|
||
amount: number;
|
||
currency?: string;
|
||
}
|
||
|
||
export interface CreateInvoiceRequest {
|
||
title: string;
|
||
status?: InvoiceStatus | string;
|
||
kind?: "invoice" | "credit";
|
||
currency?: string;
|
||
notes?: string;
|
||
discountAmount?: number;
|
||
taxPercent?: number;
|
||
issuedAt?: string | null;
|
||
dueDate?: string | null;
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
dealId?: string | null;
|
||
quoteId?: string | null;
|
||
fromDealId?: string | null;
|
||
fromQuoteId?: string | null;
|
||
lineItems?: DealLineItemInput[];
|
||
installments?: InvoiceInstallmentInput[];
|
||
commissionPercent?: number | null;
|
||
commissionFixedAmount?: number | null;
|
||
}
|
||
|
||
export interface UpdateInvoiceRequest {
|
||
title?: string;
|
||
status?: InvoiceStatus | string;
|
||
kind?: "invoice" | "credit";
|
||
currency?: string;
|
||
notes?: string | null;
|
||
discountAmount?: number;
|
||
taxPercent?: number;
|
||
issuedAt?: string | null;
|
||
dueDate?: string | null;
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
dealId?: string | null;
|
||
quoteId?: string | null;
|
||
lineItems?: DealLineItemInput[];
|
||
installments?: InvoiceInstallmentInput[];
|
||
commissionPercent?: number | null;
|
||
commissionFixedAmount?: number | null;
|
||
}
|
||
|
||
export interface CreatePaymentRequest {
|
||
amount: number;
|
||
currency?: string;
|
||
method?: PaymentMethod | string;
|
||
paidAt?: string;
|
||
notes?: string;
|
||
installmentId?: string | null;
|
||
/** اگر پر باشد، پرداخت غیرچکی یا چک وصولشده در این حساب صندوق مینشیند */
|
||
financialAccountId?: string | null;
|
||
cheque?: ChequeInput;
|
||
}
|
||
|
||
export type SalesGoalType =
|
||
| "sales"
|
||
| "customer"
|
||
| "revenue"
|
||
| "deal"
|
||
| "activity"
|
||
| "quality";
|
||
|
||
export type SalesGoalStatus = "in_progress" | "completed" | "needs_attention";
|
||
|
||
export interface SalesGoalDto {
|
||
id: string;
|
||
title: string;
|
||
type: SalesGoalType | string;
|
||
periodStart: string;
|
||
periodEnd: string;
|
||
targetAmount: number;
|
||
forecastAmount: number | null;
|
||
probability: number | null;
|
||
currency: string;
|
||
notes: string | null;
|
||
user: UserSummaryDto | null;
|
||
currentAmount: number;
|
||
progressPercent: number;
|
||
status: SalesGoalStatus;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateSalesGoalRequest {
|
||
title: string;
|
||
periodStart: string;
|
||
periodEnd: string;
|
||
targetAmount: number;
|
||
type?: SalesGoalType | string;
|
||
forecastAmount?: number | null;
|
||
/** 0-100 */
|
||
probability?: number | null;
|
||
currency?: string;
|
||
notes?: string;
|
||
/** فقط مالک/مدیر؛ در غیر این صورت هدف برای خود کاربر */
|
||
userId?: string | null;
|
||
}
|
||
|
||
export interface UpdateSalesGoalRequest {
|
||
title?: string;
|
||
periodStart?: string;
|
||
periodEnd?: string;
|
||
targetAmount?: number;
|
||
type?: SalesGoalType | string;
|
||
forecastAmount?: number | null;
|
||
/** 0-100 */
|
||
probability?: number | null;
|
||
currency?: string;
|
||
notes?: string | null;
|
||
userId?: string | null;
|
||
}
|
||
|
||
export interface SalesForecastKpiDto {
|
||
key: "sales" | "customer" | "deal" | "forecast";
|
||
label: string;
|
||
current: number;
|
||
target: number;
|
||
percent: number;
|
||
unit: "money" | "count";
|
||
}
|
||
|
||
export interface SalesForecastDto {
|
||
periodStart: string;
|
||
periodEnd: string;
|
||
currency: string;
|
||
/** مجموع اهداف همپوشان با بازه */
|
||
targetAmount: number;
|
||
/** معاملات برندهشده در بازه */
|
||
wonAmount: number;
|
||
/** معاملات باز فعلی */
|
||
openPipelineAmount: number;
|
||
/** پیشبینی: تحقق + معاملات باز × میانگین احتمال */
|
||
forecastAmount: number;
|
||
progressPercent: number | null;
|
||
openDealsCount: number;
|
||
wonDealsCount: number;
|
||
goals: SalesGoalDto[];
|
||
kpis: SalesForecastKpiDto[];
|
||
newCustomersCount: number;
|
||
/** معاملات ایجادشده در بازه */
|
||
newDealsCount: number;
|
||
}
|
||
|
||
export type TicketStatus =
|
||
| "open"
|
||
| "in_progress"
|
||
| "waiting"
|
||
| "resolved"
|
||
| "closed";
|
||
|
||
export type TicketPriority = "low" | "normal" | "high" | "urgent";
|
||
|
||
export interface TicketMessageDto {
|
||
id: string;
|
||
body: string;
|
||
isInternal: boolean;
|
||
author: UserSummaryDto | null;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface TicketDto {
|
||
id: string;
|
||
number: string;
|
||
subject: string;
|
||
description: string | null;
|
||
status: TicketStatus | string;
|
||
priority: TicketPriority | string;
|
||
person: PersonSummaryDto | null;
|
||
company: CompanySummaryDto | null;
|
||
deal: DealSummaryDto | null;
|
||
assignee: UserSummaryDto | null;
|
||
reporter: UserSummaryDto | null;
|
||
resolvedAt: string | null;
|
||
slaDueAt: string | null;
|
||
firstRespondedAt: string | null;
|
||
slaBreached: boolean;
|
||
csatScore: number | null;
|
||
csatComment: string | null;
|
||
csatAt: string | null;
|
||
messages: TicketMessageDto[];
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateTicketRequest {
|
||
subject: string;
|
||
description?: string;
|
||
status?: TicketStatus | string;
|
||
priority?: TicketPriority | string;
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
dealId?: string | null;
|
||
assigneeId?: string | null;
|
||
}
|
||
|
||
export interface UpdateTicketRequest {
|
||
subject?: string;
|
||
description?: string | null;
|
||
status?: TicketStatus | string;
|
||
priority?: TicketPriority | string;
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
dealId?: string | null;
|
||
assigneeId?: string | null;
|
||
csatScore?: number | null;
|
||
csatComment?: string | null;
|
||
}
|
||
|
||
export interface CreateTicketMessageRequest {
|
||
body: string;
|
||
isInternal?: boolean;
|
||
}
|
||
|
||
export interface FaqArticleDto {
|
||
id: string;
|
||
question: string;
|
||
answer: string;
|
||
category: string | null;
|
||
isPublished: boolean;
|
||
position: number;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateFaqArticleRequest {
|
||
question: string;
|
||
answer: string;
|
||
category?: string;
|
||
isPublished?: boolean;
|
||
position?: number;
|
||
}
|
||
|
||
export interface UpdateFaqArticleRequest {
|
||
question?: string;
|
||
answer?: string;
|
||
category?: string | null;
|
||
isPublished?: boolean;
|
||
position?: number;
|
||
}
|
||
|
||
/** ساعات مهلت پاسخ اول بر اساس اولویت */
|
||
export const TICKET_SLA_HOURS: Record<TicketPriority, number> = {
|
||
urgent: 4,
|
||
high: 8,
|
||
normal: 24,
|
||
low: 48,
|
||
};
|
||
|
||
/* ——— پرتال مشتری (OTP) ——— */
|
||
|
||
export interface PortalOrgInfoDto {
|
||
name: string;
|
||
slug: string;
|
||
}
|
||
|
||
export interface PortalRequestOtpRequest {
|
||
mobile: string;
|
||
}
|
||
|
||
export interface PortalRequestOtpResponse {
|
||
ok: true;
|
||
/** فقط در حالت توسعه / دمو وقتی SMS واقعی وصل نیست */
|
||
devCode?: string;
|
||
expiresInSeconds: number;
|
||
}
|
||
|
||
export interface PortalVerifyOtpRequest {
|
||
mobile: string;
|
||
code: string;
|
||
}
|
||
|
||
export interface PortalPersonDto {
|
||
id: string;
|
||
fullName: string;
|
||
mobile: string | null;
|
||
}
|
||
|
||
export interface PortalLoginResponse {
|
||
accessToken: string;
|
||
person: PortalPersonDto;
|
||
organization: PortalOrgInfoDto;
|
||
}
|
||
|
||
export interface PortalCreateTicketRequest {
|
||
subject: string;
|
||
description?: string;
|
||
priority?: TicketPriority | string;
|
||
}
|
||
|
||
export interface PortalCreateMessageRequest {
|
||
body: string;
|
||
}
|
||
|
||
export interface PortalCsatRequest {
|
||
csatScore: number;
|
||
csatComment?: string | null;
|
||
}
|
||
|
||
export interface PortalTicketDto {
|
||
id: string;
|
||
number: string;
|
||
subject: string;
|
||
description: string | null;
|
||
status: TicketStatus | string;
|
||
priority: TicketPriority | string;
|
||
resolvedAt: string | null;
|
||
csatScore: number | null;
|
||
csatComment: string | null;
|
||
messages: Array<{
|
||
id: string;
|
||
body: string;
|
||
createdAt: string;
|
||
fromStaff: boolean;
|
||
}>;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
/* ——— گزارشات و داشبورد ——— */
|
||
|
||
export interface ReportsKpiDto {
|
||
key: string;
|
||
label: string;
|
||
value: number;
|
||
format: "money" | "number";
|
||
changePercent: number | null;
|
||
changeLabel: string;
|
||
}
|
||
|
||
export interface ReportsSeriesPointDto {
|
||
key: string;
|
||
label: string;
|
||
value: number;
|
||
}
|
||
|
||
export interface ReportsSliceDto {
|
||
label: string;
|
||
count: number;
|
||
percent: number;
|
||
amount?: number;
|
||
}
|
||
|
||
export interface ReportsTopProductDto {
|
||
name: string;
|
||
quantity: number;
|
||
amount: number;
|
||
}
|
||
|
||
export interface ReportsTeamRowDto {
|
||
userId: string;
|
||
fullName: string;
|
||
wonAmount: number;
|
||
wonCount: number;
|
||
activitiesDone: number;
|
||
}
|
||
|
||
/** گونههای drill-down گزارش عمیق (فاز ۱ BI) */
|
||
export type ReportsDrillKind =
|
||
| "stage_open"
|
||
| "team_won"
|
||
| "period_won"
|
||
| "period_lost"
|
||
| "source"
|
||
| "lost_reason";
|
||
|
||
export const REPORTS_DRILL_KINDS: ReportsDrillKind[] = [
|
||
"stage_open",
|
||
"team_won",
|
||
"period_won",
|
||
"period_lost",
|
||
"source",
|
||
"lost_reason",
|
||
];
|
||
|
||
export interface ReportsFunnelStageDto {
|
||
stageId: string;
|
||
stageName: string;
|
||
pipelineId: string;
|
||
pipelineName: string;
|
||
count: number;
|
||
amount: number;
|
||
/** درصد تبدیل نسبت به مرحله قبل همان مسیر فروش؛ null برای اولین مرحله */
|
||
conversionFromPrevPercent: number | null;
|
||
/** سهم از کل معاملات باز همان مسیر فروش */
|
||
shareOfPipelinePercent: number;
|
||
}
|
||
|
||
/** یک ردیف نرخ تبدیل (ورودی → موفق) مثل دیدار */
|
||
export interface ReportsConversionRateRowDto {
|
||
key: string;
|
||
label: string;
|
||
incomingCount: number;
|
||
wonCount: number;
|
||
/** درصد؛ اگر ورودی صفر باشد ۰ */
|
||
conversionPercent: number;
|
||
}
|
||
|
||
/**
|
||
* نرخ تبدیل به سبک دیدار:
|
||
* از معاملات تازهساختهشده در بازه، چند درصد الان برنده شدهاند.
|
||
*/
|
||
export interface ReportsConversionRateDto {
|
||
incomingCount: number;
|
||
wonCount: number;
|
||
conversionPercent: number;
|
||
previousIncomingCount: number;
|
||
previousWonCount: number;
|
||
previousConversionPercent: number;
|
||
changePercent: number | null;
|
||
byTime: ReportsConversionRateRowDto[];
|
||
byUser: ReportsConversionRateRowDto[];
|
||
bySource: ReportsConversionRateRowDto[];
|
||
}
|
||
|
||
export interface ReportsPeriodCompareRowDto {
|
||
key: string;
|
||
label: string;
|
||
format: "money" | "number" | "percent";
|
||
current: number;
|
||
previous: number;
|
||
changePercent: number | null;
|
||
/** اگر قابل کلیک برای لیست پرونده باشد */
|
||
drillKind?: ReportsDrillKind;
|
||
}
|
||
|
||
export interface ReportsDrillItemDto {
|
||
id: string;
|
||
title: string;
|
||
status: string;
|
||
statusLabel: string;
|
||
amount: number;
|
||
currency: string;
|
||
ownerName: string | null;
|
||
stageName: string | null;
|
||
updatedAt: string;
|
||
link: string;
|
||
}
|
||
|
||
export interface ReportsDrilldownDto {
|
||
kind: ReportsDrillKind;
|
||
title: string;
|
||
from: string;
|
||
to: string;
|
||
total: number;
|
||
items: ReportsDrillItemDto[];
|
||
}
|
||
|
||
export interface ReportsDashboardDto {
|
||
from: string;
|
||
to: string;
|
||
currency: string;
|
||
kpis: ReportsKpiDto[];
|
||
salesTrend: ReportsSeriesPointDto[];
|
||
monthlyComparison: ReportsSeriesPointDto[];
|
||
customerGrowth: ReportsSeriesPointDto[];
|
||
sources: ReportsSliceDto[];
|
||
lostReasons: ReportsSliceDto[];
|
||
salesByCategory: ReportsSliceDto[];
|
||
topProducts: ReportsTopProductDto[];
|
||
customerStatus: ReportsSliceDto[];
|
||
activities: {
|
||
open: number;
|
||
done: number;
|
||
overdue: number;
|
||
byType: Array<{ type: string; label: string; count: number }>;
|
||
};
|
||
team: ReportsTeamRowDto[];
|
||
insights: string[];
|
||
sales: {
|
||
openCount: number;
|
||
wonCount: number;
|
||
lostCount: number;
|
||
openAmount: number;
|
||
wonAmount: number;
|
||
lostAmount: number;
|
||
winRate: number;
|
||
};
|
||
funnel: ReportsFunnelStageDto[];
|
||
/** نرخ تبدیل ورودی→موفق (سبک دیدار) */
|
||
conversionRate: ReportsConversionRateDto;
|
||
/** مقایسه صریح بازه فعلی با دوره قبل همطول */
|
||
periodCompare: ReportsPeriodCompareRowDto[];
|
||
tickets: {
|
||
open: number;
|
||
resolved: number;
|
||
closed: number;
|
||
slaBreached: number;
|
||
avgCsat: number | null;
|
||
csatCount: number;
|
||
};
|
||
peopleCreated: number;
|
||
invoiceCount: number;
|
||
invoiceAmount: number;
|
||
}
|
||
|
||
export interface HomeDashboardKpiDto {
|
||
key: string;
|
||
label: string;
|
||
value: number;
|
||
changePercent: number | null;
|
||
changeLabel: string;
|
||
}
|
||
|
||
export interface HomeDashboardAlertDto {
|
||
key: string;
|
||
tone: 'urgent' | 'warning' | 'success' | 'info';
|
||
text: string;
|
||
link?: string;
|
||
}
|
||
|
||
export interface HomeDashboardTrendPointDto {
|
||
monthKey: string;
|
||
monthLabel: string;
|
||
amount: number;
|
||
}
|
||
|
||
export interface HomeDashboardSourceDto {
|
||
label: string;
|
||
count: number;
|
||
percent: number;
|
||
}
|
||
|
||
export interface HomeDashboardTopCustomerDto {
|
||
id: string;
|
||
name: string;
|
||
dealCount: number;
|
||
rank: number;
|
||
}
|
||
|
||
export interface HomeDashboardDto {
|
||
currency: string;
|
||
kpis: HomeDashboardKpiDto[];
|
||
alerts: HomeDashboardAlertDto[];
|
||
salesTrend: HomeDashboardTrendPointDto[];
|
||
sources: HomeDashboardSourceDto[];
|
||
topCustomers: HomeDashboardTopCustomerDto[];
|
||
}
|
||
|
||
/** تابلوی پروژه / کارت */
|
||
export interface ProjectBoardStageDto {
|
||
id: string;
|
||
name: string;
|
||
position: number;
|
||
}
|
||
|
||
export interface ProjectBoardDto {
|
||
id: string;
|
||
name: string;
|
||
isDefault: boolean;
|
||
stages: ProjectBoardStageDto[];
|
||
}
|
||
|
||
export interface ProjectCardDto {
|
||
id: string;
|
||
title: string;
|
||
notes: string | null;
|
||
position: number;
|
||
boardId: string;
|
||
stageId: string;
|
||
tags: string[];
|
||
person: PersonSummaryDto | null;
|
||
company: CompanySummaryDto | null;
|
||
deal: { id: string; title: string } | null;
|
||
owner: UserSummaryDto | null;
|
||
assignees: UserSummaryDto[];
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface ProjectBoardColumnDto extends ProjectBoardStageDto {
|
||
cards: ProjectCardDto[];
|
||
}
|
||
|
||
export interface ProjectBoardBoardDto {
|
||
board: ProjectBoardDto;
|
||
stages: ProjectBoardColumnDto[];
|
||
}
|
||
|
||
export interface CreateProjectBoardRequest {
|
||
name: string;
|
||
}
|
||
|
||
export interface CreateProjectCardRequest {
|
||
title: string;
|
||
boardId?: string;
|
||
stageId?: string;
|
||
notes?: string;
|
||
tags?: string[];
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
dealId?: string | null;
|
||
ownerId?: string | null;
|
||
assigneeIds?: string[];
|
||
}
|
||
|
||
export type UpdateProjectCardRequest = Partial<CreateProjectCardRequest>;
|
||
|
||
export interface MoveProjectCardRequest {
|
||
stageId: string;
|
||
position?: number;
|
||
}
|
||
|
||
/** باشگاه مشتریان */
|
||
export interface LoyaltyTierDto {
|
||
id: string;
|
||
name: string;
|
||
minPoints: number;
|
||
position: number;
|
||
color: string | null;
|
||
memberCount?: number;
|
||
}
|
||
|
||
export interface CreateLoyaltyTierRequest {
|
||
name: string;
|
||
minPoints: number;
|
||
color?: string | null;
|
||
}
|
||
|
||
export type UpdateLoyaltyTierRequest = Partial<CreateLoyaltyTierRequest>;
|
||
|
||
export interface AdjustLoyaltyPointsRequest {
|
||
delta: number;
|
||
reason?: string;
|
||
}
|
||
|
||
export interface LoyaltyMemberDto {
|
||
id: string;
|
||
fullName: string;
|
||
mobile: string | null;
|
||
email: string | null;
|
||
loyaltyPoints: number;
|
||
loyaltyTier: LoyaltyTierSummaryDto | null;
|
||
company: CompanySummaryDto | null;
|
||
lastActivityAt: string | null;
|
||
}
|
||
|
||
export interface LoyaltyStatsDto {
|
||
memberCount: number;
|
||
memberDeltaPct: number | null;
|
||
pointsIssued: number;
|
||
pointsIssuedDeltaPct: number | null;
|
||
rewardsUsed: number;
|
||
rewardsUsedDeltaPct: number | null;
|
||
retentionRate: number;
|
||
retentionDeltaPct: number | null;
|
||
}
|
||
|
||
export interface LoyaltySettingsDto {
|
||
enabled: boolean;
|
||
welcomePoints: number;
|
||
notifyOnAdjust: boolean;
|
||
}
|
||
|
||
export type UpdateLoyaltySettingsRequest = Partial<LoyaltySettingsDto>;
|
||
|
||
export interface LoyaltyRewardDto {
|
||
id: string;
|
||
name: string;
|
||
description: string | null;
|
||
pointsCost: number;
|
||
isActive: boolean;
|
||
position: number;
|
||
color: string | null;
|
||
redemptionCount?: number;
|
||
}
|
||
|
||
export interface CreateLoyaltyRewardRequest {
|
||
name: string;
|
||
description?: string | null;
|
||
pointsCost: number;
|
||
isActive?: boolean;
|
||
color?: string | null;
|
||
}
|
||
|
||
export type UpdateLoyaltyRewardRequest = Partial<CreateLoyaltyRewardRequest>;
|
||
|
||
export interface LoyaltyLedgerDto {
|
||
id: string;
|
||
delta: number;
|
||
reason: string | null;
|
||
source: string;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface BulkAdjustLoyaltyRequest {
|
||
personIds: string[];
|
||
delta: number;
|
||
reason?: string;
|
||
}
|
||
|
||
export interface LoyaltyMessageRequest {
|
||
personIds: string[];
|
||
text: string;
|
||
}
|
||
|
||
export interface LoyaltyMessageResult {
|
||
sent: number;
|
||
skipped: number;
|
||
failed: number;
|
||
}
|
||
|
||
export interface RedeemLoyaltyRewardRequest {
|
||
personId: string;
|
||
}
|
||
|
||
/** اتوماسیون اگر/آنگاه */
|
||
export type AutomationTrigger =
|
||
| "person_created"
|
||
| "lead_submitted"
|
||
| "deal_created"
|
||
| "deal_stage_changed"
|
||
| "deal_won"
|
||
| "deal_lost"
|
||
| "ticket_created"
|
||
| "ticket_resolved"
|
||
| "invoice_issued"
|
||
| "invoice_paid"
|
||
| "activity_done"
|
||
| "sms_link_clicked";
|
||
export type AutomationAction =
|
||
| "create_activity"
|
||
| "create_notification"
|
||
| "add_loyalty_points";
|
||
|
||
export const AUTOMATION_TRIGGER_LABELS: Record<AutomationTrigger, string> = {
|
||
person_created: "مخاطب جدید ساخته شد",
|
||
lead_submitted: "فرم سرنخ ارسال شد",
|
||
deal_created: "معامله جدید ساخته شد",
|
||
deal_stage_changed: "مرحله معامله عوض شد",
|
||
deal_won: "معامله برنده شد",
|
||
deal_lost: "معامله باخت",
|
||
ticket_created: "تیکت جدید ساخته شد",
|
||
ticket_resolved: "تیکت حل شد",
|
||
invoice_issued: "فاکتور صادر شد",
|
||
invoice_paid: "پرداخت روی فاکتور ثبت شد",
|
||
activity_done: "فعالیت تمام شد",
|
||
sms_link_clicked: "کلیک روی لینک پیامک",
|
||
};
|
||
|
||
export const AUTOMATION_ACTION_LABELS: Record<AutomationAction, string> = {
|
||
create_activity: "ساخت فعالیت",
|
||
create_notification: "ارسال اعلان",
|
||
add_loyalty_points: "افزودن امتیاز باشگاه",
|
||
};
|
||
|
||
export const MAX_AUTOMATION_STEPS = 20;
|
||
|
||
export const AUTOMATION_ACTIONS = [
|
||
"create_activity",
|
||
"create_notification",
|
||
"add_loyalty_points",
|
||
] as const satisfies readonly AutomationAction[];
|
||
|
||
export const AUTOMATION_TRIGGERS = [
|
||
"person_created",
|
||
"lead_submitted",
|
||
"deal_created",
|
||
"deal_stage_changed",
|
||
"deal_won",
|
||
"deal_lost",
|
||
"ticket_created",
|
||
"ticket_resolved",
|
||
"invoice_issued",
|
||
"invoice_paid",
|
||
"activity_done",
|
||
"sms_link_clicked",
|
||
] as const satisfies readonly AutomationTrigger[];
|
||
|
||
export const AUTOMATION_TRIGGER_GROUPS: {
|
||
label: string;
|
||
items: AutomationTrigger[];
|
||
}[] = [
|
||
{ label: "مخاطب", items: ["person_created", "lead_submitted"] },
|
||
{
|
||
label: "معامله",
|
||
items: ["deal_created", "deal_stage_changed", "deal_won", "deal_lost"],
|
||
},
|
||
{ label: "تیکت", items: ["ticket_created", "ticket_resolved"] },
|
||
{ label: "فعالیت", items: ["activity_done"] },
|
||
{ label: "مالی", items: ["invoice_issued", "invoice_paid"] },
|
||
{ label: "پیامک", items: ["sms_link_clicked"] },
|
||
];
|
||
|
||
export type AutomationStepKind = "action" | "condition";
|
||
|
||
export type AutomationConditionField =
|
||
| "deal_amount"
|
||
| "has_person"
|
||
| "has_company"
|
||
| "ticket_priority"
|
||
| "form_field";
|
||
|
||
export type AutomationConditionOp =
|
||
| "gte"
|
||
| "lte"
|
||
| "eq"
|
||
| "neq"
|
||
| "is_set"
|
||
| "is_empty"
|
||
| "contains"
|
||
| "contains_any"
|
||
| "not_contains";
|
||
|
||
/** stop: شرط ناموفق کل زنجیره را متوقف میکند — branches: هر شرط فقط عملهای بعدی خودش را تا شرط بعدی کنترل میکند */
|
||
export type AutomationChainMode = "stop" | "branches";
|
||
|
||
export type AutomationAssignMode =
|
||
| "owner"
|
||
| "user"
|
||
| "team_round_robin"
|
||
| "users_round_robin";
|
||
|
||
export const AUTOMATION_ASSIGN_MODE_LABELS: Record<AutomationAssignMode, string> = {
|
||
owner: "مسئول پیشفرض (مسئول پرونده)",
|
||
user: "کاربر مشخص",
|
||
team_round_robin: "نوبتی بین اعضای یک تیم",
|
||
users_round_robin: "نوبتی بین کاربران انتخابی",
|
||
};
|
||
|
||
export const AUTOMATION_CHAIN_MODE_LABELS: Record<AutomationChainMode, string> = {
|
||
branches: "شاخهای — هر شرط فقط عملهای بعد از خودش را کنترل میکند",
|
||
stop: "توقف — شرط ناموفق کل زنجیره را متوقف میکند",
|
||
};
|
||
|
||
export type AutomationNotifyChannel = "app" | "sms" | "both";
|
||
export type AutomationNotifyTarget = "staff" | "person";
|
||
|
||
export const AUTOMATION_CONDITION_FIELD_LABELS: Record<
|
||
AutomationConditionField,
|
||
string
|
||
> = {
|
||
deal_amount: "مبلغ معامله",
|
||
has_person: "مخاطب دارد",
|
||
has_company: "شرکت دارد",
|
||
ticket_priority: "اولویت تیکت",
|
||
form_field: "پاسخ فیلد فرم",
|
||
};
|
||
|
||
export const AUTOMATION_CONDITION_OP_LABELS: Record<
|
||
AutomationConditionOp,
|
||
string
|
||
> = {
|
||
gte: "بزرگتر یا مساوی",
|
||
lte: "کوچکتر یا مساوی",
|
||
eq: "مساوی",
|
||
neq: "نامساوی",
|
||
is_set: "دارد",
|
||
is_empty: "ندارد",
|
||
contains: "شامل",
|
||
contains_any: "شامل یکی از",
|
||
not_contains: "شامل نیست",
|
||
};
|
||
|
||
export const AUTOMATION_NOTIFY_CHANNEL_LABELS: Record<
|
||
AutomationNotifyChannel,
|
||
string
|
||
> = {
|
||
app: "ناتیفیکیشن داخلی",
|
||
sms: "پیامک",
|
||
both: "هر دو",
|
||
};
|
||
|
||
export const AUTOMATION_NOTIFY_CHANNELS = [
|
||
"app",
|
||
"sms",
|
||
"both",
|
||
] as const satisfies readonly AutomationNotifyChannel[];
|
||
|
||
export function isAutomationCondition(step: Pick<AutomationStep, "kind">): boolean {
|
||
return step.kind === "condition";
|
||
}
|
||
|
||
export function firstAutomationAction(
|
||
steps: AutomationStep[],
|
||
): AutomationAction | null {
|
||
const step = steps.find((s) => !isAutomationCondition(s) && s.action);
|
||
return step?.action ?? null;
|
||
}
|
||
|
||
export function conditionFieldsForTrigger(
|
||
trigger: AutomationTrigger,
|
||
): AutomationConditionField[] {
|
||
if (
|
||
trigger === "deal_won" ||
|
||
trigger === "deal_lost" ||
|
||
trigger === "deal_created" ||
|
||
trigger === "deal_stage_changed"
|
||
) {
|
||
return ["deal_amount", "has_person", "has_company"];
|
||
}
|
||
if (trigger === "ticket_resolved" || trigger === "ticket_created") {
|
||
return ["ticket_priority", "has_person", "has_company"];
|
||
}
|
||
if (trigger === "lead_submitted") {
|
||
return ["form_field", "has_person", "has_company"];
|
||
}
|
||
return ["has_person", "has_company"];
|
||
}
|
||
|
||
export function conditionOpsForField(
|
||
field: AutomationConditionField | undefined,
|
||
): AutomationConditionOp[] {
|
||
if (field === "deal_amount") return ["gte", "lte", "eq", "neq"];
|
||
if (field === "ticket_priority") return ["eq", "neq"];
|
||
if (field === "form_field") {
|
||
return ["contains", "contains_any", "not_contains", "eq", "neq", "is_set", "is_empty"];
|
||
}
|
||
return ["is_set", "is_empty"];
|
||
}
|
||
|
||
export interface AutomationStep {
|
||
id: string;
|
||
kind?: AutomationStepKind;
|
||
action?: AutomationAction;
|
||
title?: string;
|
||
body?: string;
|
||
assigneeId?: string | null;
|
||
notifyUserId?: string | null;
|
||
notifyChannel?: AutomationNotifyChannel;
|
||
notifyTarget?: AutomationNotifyTarget;
|
||
activityType?: "call" | "meeting" | "task" | "note";
|
||
points?: number;
|
||
conditionField?: AutomationConditionField;
|
||
conditionOp?: AutomationConditionOp;
|
||
conditionValue?: string | number;
|
||
/** چند گزینهٔ فیلد فرم (چندانتخابی)؛ اگر پر باشد بهجای conditionValue استفاده میشود */
|
||
conditionValues?: string[];
|
||
/** کلید فیلد فرم برای شرط form_field */
|
||
conditionKey?: string | null;
|
||
assignMode?: AutomationAssignMode;
|
||
assigneeTeamId?: string | null;
|
||
assigneeUserIds?: string[];
|
||
/** موعد فعالیت نسبت به زمان اجرا (دقیقه)؛ ۰ یا خالی = همان لحظه */
|
||
dueOffsetMinutes?: number | null;
|
||
activityTypeDefId?: string | null;
|
||
}
|
||
|
||
export interface AutomationRuleConfig {
|
||
title?: string;
|
||
body?: string;
|
||
assigneeId?: string | null;
|
||
notifyUserId?: string | null;
|
||
notifyChannel?: AutomationNotifyChannel;
|
||
notifyTarget?: AutomationNotifyTarget;
|
||
activityType?: "call" | "meeting" | "task" | "note";
|
||
points?: number;
|
||
assignMode?: AutomationAssignMode;
|
||
assigneeTeamId?: string | null;
|
||
assigneeUserIds?: string[];
|
||
dueOffsetMinutes?: number | null;
|
||
activityTypeDefId?: string | null;
|
||
/** فیلتر اختیاری: فقط برای منبع خاص (مثلاً id کمپین SMS یا id فرم سرنخ) */
|
||
sourceId?: string | null;
|
||
/** نبودِ مقدار = stop (رفتار قدیمی) */
|
||
chainMode?: AutomationChainMode;
|
||
/** زنجیرهٔ اعمال؛ اگر خالی باشد همان action تکی استفاده میشود */
|
||
steps?: AutomationStep[];
|
||
}
|
||
|
||
export interface AutomationRuleDto {
|
||
id: string;
|
||
name: string;
|
||
isActive: boolean;
|
||
trigger: AutomationTrigger;
|
||
action: AutomationAction;
|
||
config: AutomationRuleConfig;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateAutomationRuleRequest {
|
||
name: string;
|
||
trigger: AutomationTrigger;
|
||
action: AutomationAction;
|
||
isActive?: boolean;
|
||
config?: AutomationRuleConfig;
|
||
}
|
||
|
||
export type UpdateAutomationRuleRequest = Partial<CreateAutomationRuleRequest>;
|
||
|
||
export function normalizeAutomationSteps(
|
||
action: AutomationAction,
|
||
config?: AutomationRuleConfig | null,
|
||
): AutomationStep[] {
|
||
const cfg = config ?? {};
|
||
if (Array.isArray(cfg.steps) && cfg.steps.length) {
|
||
return cfg.steps
|
||
.filter((s) => s && (s.kind === "condition" || s.action))
|
||
.map((s, i) => ({
|
||
id: s.id || `step-${i}`,
|
||
kind: s.kind === "condition" ? "condition" : "action",
|
||
action: s.action,
|
||
title: s.title,
|
||
body: s.body,
|
||
assigneeId: s.assigneeId,
|
||
notifyUserId: s.notifyUserId,
|
||
notifyChannel: s.notifyChannel,
|
||
notifyTarget: s.notifyTarget,
|
||
activityType: s.activityType,
|
||
points: s.points,
|
||
conditionField: s.conditionField,
|
||
conditionOp: s.conditionOp,
|
||
conditionValue: s.conditionValue,
|
||
conditionValues: Array.isArray(s.conditionValues)
|
||
? s.conditionValues.map((v) => String(v).trim()).filter(Boolean)
|
||
: undefined,
|
||
conditionKey: s.conditionKey,
|
||
assignMode: s.assignMode,
|
||
assigneeTeamId: s.assigneeTeamId,
|
||
assigneeUserIds: Array.isArray(s.assigneeUserIds) ? s.assigneeUserIds : undefined,
|
||
dueOffsetMinutes: s.dueOffsetMinutes,
|
||
activityTypeDefId: s.activityTypeDefId,
|
||
}));
|
||
}
|
||
return [
|
||
{
|
||
id: "step-0",
|
||
kind: "action",
|
||
action,
|
||
title: cfg.title,
|
||
body: cfg.body,
|
||
assigneeId: cfg.assigneeId,
|
||
notifyUserId: cfg.notifyUserId,
|
||
notifyChannel: cfg.notifyChannel,
|
||
notifyTarget: cfg.notifyTarget,
|
||
activityType: cfg.activityType,
|
||
points: cfg.points,
|
||
assignMode: cfg.assignMode,
|
||
assigneeTeamId: cfg.assigneeTeamId,
|
||
assigneeUserIds: cfg.assigneeUserIds,
|
||
dueOffsetMinutes: cfg.dueOffsetMinutes,
|
||
activityTypeDefId: cfg.activityTypeDefId,
|
||
},
|
||
];
|
||
}
|
||
|
||
export function packAutomationConfig(
|
||
steps: AutomationStep[],
|
||
extra: Pick<AutomationRuleConfig, "sourceId" | "chainMode"> = {},
|
||
): AutomationRuleConfig {
|
||
const first =
|
||
steps.find((s) => (s.kind ?? "action") !== "condition") ?? steps[0];
|
||
return {
|
||
steps,
|
||
title: first?.title,
|
||
body: first?.body,
|
||
assigneeId: first?.assigneeId,
|
||
notifyUserId: first?.notifyUserId,
|
||
notifyChannel: first?.notifyChannel,
|
||
notifyTarget: first?.notifyTarget,
|
||
activityType: first?.activityType,
|
||
points: first?.points,
|
||
...(extra.sourceId !== undefined ? { sourceId: extra.sourceId || null } : {}),
|
||
...(extra.chainMode ? { chainMode: extra.chainMode } : {}),
|
||
};
|
||
}
|
||
|
||
export function automationStepToConfig(step: AutomationStep): AutomationRuleConfig {
|
||
return {
|
||
title: step.title,
|
||
body: step.body,
|
||
assigneeId: step.assigneeId,
|
||
notifyUserId: step.notifyUserId,
|
||
notifyChannel: step.notifyChannel,
|
||
notifyTarget: step.notifyTarget,
|
||
activityType: step.activityType,
|
||
points: step.points,
|
||
assignMode: step.assignMode,
|
||
assigneeTeamId: step.assigneeTeamId,
|
||
assigneeUserIds: step.assigneeUserIds,
|
||
dueOffsetMinutes: step.dueOffsetMinutes,
|
||
activityTypeDefId: step.activityTypeDefId,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* گامهای عملی که باید اجرا شوند.
|
||
* stop: اولین شرط ناموفق بقیهٔ زنجیره را قطع میکند.
|
||
* branches: هر دنبالهٔ پیاپی از شرطها (AND) فقط عملهای بعد از خودش را تا شرط بعدی کنترل میکند.
|
||
*/
|
||
export function planAutomationActions(
|
||
steps: AutomationStep[],
|
||
chainMode: AutomationChainMode | undefined,
|
||
evalCondition: (step: AutomationStep) => boolean,
|
||
): AutomationStep[] {
|
||
const out: AutomationStep[] = [];
|
||
if (chainMode !== "branches") {
|
||
for (const step of steps) {
|
||
if (isAutomationCondition(step)) {
|
||
if (!evalCondition(step)) break;
|
||
continue;
|
||
}
|
||
if (step.action) out.push(step);
|
||
}
|
||
return out;
|
||
}
|
||
let gateOpen = true;
|
||
let prevWasCondition = false;
|
||
for (const step of steps) {
|
||
if (isAutomationCondition(step)) {
|
||
const ok = evalCondition(step);
|
||
gateOpen = prevWasCondition ? gateOpen && ok : ok;
|
||
prevWasCondition = true;
|
||
continue;
|
||
}
|
||
prevWasCondition = false;
|
||
if (gateOpen && step.action) out.push(step);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
export interface AutomationBranchRange {
|
||
/** اندیس اولین شرط شاخه */
|
||
start: number;
|
||
/** اندیس بعد از آخرین گام شاخه */
|
||
end: number;
|
||
}
|
||
|
||
/** گامهای بدون شرط ابتدای زنجیره و بازهٔ هر شاخه (در حالت branches) */
|
||
export function splitAutomationBranches(steps: AutomationStep[]): {
|
||
prefixEnd: number;
|
||
branches: AutomationBranchRange[];
|
||
} {
|
||
const branches: AutomationBranchRange[] = [];
|
||
let prefixEnd = steps.length;
|
||
steps.forEach((step, i) => {
|
||
if (!isAutomationCondition(step)) return;
|
||
const prev = steps[i - 1];
|
||
if (prev && isAutomationCondition(prev)) return;
|
||
if (!branches.length) prefixEnd = i;
|
||
else branches[branches.length - 1].end = i;
|
||
branches.push({ start: i, end: steps.length });
|
||
});
|
||
return { prefixEnd, branches };
|
||
}
|
||
|
||
/** قانون از امکانات اتوماسیون حرفهای (شاخه، شرط فرم، تخصیص نوبتی) استفاده میکند */
|
||
export function automationUsesAdvanced(
|
||
steps: AutomationStep[],
|
||
chainMode: AutomationChainMode | undefined,
|
||
): boolean {
|
||
if (chainMode === "branches") return true;
|
||
return steps.some(
|
||
(s) =>
|
||
(isAutomationCondition(s) && s.conditionField === "form_field") ||
|
||
s.assignMode === "team_round_robin" ||
|
||
s.assignMode === "users_round_robin",
|
||
);
|
||
}
|
||
|
||
/** مقدار پاسخ فرم را به گزینهها میشکند (چندانتخابی با کاما ذخیره میشود) */
|
||
export function splitFormAnswer(raw: unknown): string[] {
|
||
if (raw == null) return [];
|
||
return String(raw)
|
||
.split(/[,،]/)
|
||
.map((s) => s.trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
/** گزینههای مورد انتظار شرط فرم: چندتایی اگر پر باشد، وگرنه مقدار تکی */
|
||
export function formConditionValues(
|
||
step: Pick<AutomationStep, "conditionValue" | "conditionValues">,
|
||
): string[] {
|
||
const many = (step.conditionValues ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||
if (many.length) return [...new Set(many)];
|
||
const one = String(step.conditionValue ?? "").trim();
|
||
return one ? [one] : [];
|
||
}
|
||
|
||
/**
|
||
* contains: همهٔ گزینهها انتخاب شده باشند — contains_any: حداقل یکی —
|
||
* not_contains: هیچکدام — eq: دقیقاً همین گزینهها (نه بیشتر، نه کمتر)
|
||
*/
|
||
export function matchFormFieldCondition(
|
||
op: AutomationConditionOp | undefined,
|
||
raw: unknown,
|
||
expected: string | number | string[] | null | undefined,
|
||
): boolean {
|
||
const parts = splitFormAnswer(raw);
|
||
const whole = raw == null ? "" : String(raw).trim();
|
||
const values = (Array.isArray(expected) ? expected : [expected])
|
||
.map((v) => String(v ?? "").trim())
|
||
.filter(Boolean);
|
||
const exact = () =>
|
||
values.length === 1
|
||
? whole === values[0] || (parts.length === 1 && parts[0] === values[0])
|
||
: new Set(parts).size === values.length && values.every((v) => parts.includes(v));
|
||
switch (op ?? "contains") {
|
||
case "is_set":
|
||
return parts.length > 0;
|
||
case "is_empty":
|
||
return parts.length === 0;
|
||
case "contains":
|
||
return values.length > 0 && values.every((v) => parts.includes(v));
|
||
case "contains_any":
|
||
return values.some((v) => parts.includes(v));
|
||
case "not_contains":
|
||
return !values.some((v) => parts.includes(v));
|
||
case "eq":
|
||
return values.length > 0 && exact();
|
||
case "neq":
|
||
return !(values.length > 0 && exact());
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// —— تهحساب / حسابداری ——
|
||
|
||
export const TAHSAB_SANAD_TYPES = [
|
||
"buy_sale_gold",
|
||
"buy_sale_sekeh",
|
||
"talab_bedehi",
|
||
"vkh_gold",
|
||
"vkh_bank",
|
||
] as const;
|
||
|
||
export type TahesabSanadType = (typeof TAHSAB_SANAD_TYPES)[number];
|
||
|
||
export const TAHSAB_SANAD_LABELS: Record<TahesabSanadType, string> = {
|
||
buy_sale_gold: "خرید/فروش آبشده",
|
||
buy_sale_sekeh: "خرید/فروش سکه",
|
||
talab_bedehi: "طلب/بدهی",
|
||
vkh_gold: "واریز/خروج طلا",
|
||
vkh_bank: "واریز/خروج بانکی",
|
||
};
|
||
|
||
export interface TahesabConnectionDto {
|
||
id: string;
|
||
organizationId: string;
|
||
provider: string;
|
||
label: string;
|
||
baseUrl: string;
|
||
dbName: string;
|
||
tokenMasked: string;
|
||
enabled: boolean;
|
||
isDefault: boolean;
|
||
allowInsecureTls: boolean;
|
||
lastHealthAt: string | null;
|
||
lastHealthOk: boolean | null;
|
||
lastError: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateTahesabConnectionRequest {
|
||
label: string;
|
||
baseUrl: string;
|
||
dbName: string;
|
||
token: string;
|
||
enabled?: boolean;
|
||
isDefault?: boolean;
|
||
allowInsecureTls?: boolean;
|
||
}
|
||
|
||
export type UpdateTahesabConnectionRequest = Partial<CreateTahesabConnectionRequest>;
|
||
|
||
export interface PersonAccountingLinkDto {
|
||
id: string;
|
||
personId: string;
|
||
connectionId: string;
|
||
moshtariCode: number;
|
||
matchMethod: string;
|
||
linkedAt: string;
|
||
lastBalanceSyncedAt: string | null;
|
||
connectionLabel?: string;
|
||
dbName?: string;
|
||
isDefaultConnection?: boolean;
|
||
}
|
||
|
||
/** طبقهبندی تقریبی نوع سند از روی متن NO تهحساب */
|
||
export const TAHESAB_ASNAD_KINDS = [
|
||
"sale",
|
||
"return",
|
||
"purchase",
|
||
"other",
|
||
] as const;
|
||
export type TahesabAsnadKind = (typeof TAHESAB_ASNAD_KINDS)[number];
|
||
|
||
export const TAHESAB_ASNAD_KIND_LABELS: Record<TahesabAsnadKind, string> = {
|
||
sale: "فروش",
|
||
return: "مرجوعی",
|
||
purchase: "خرید",
|
||
other: "سایر",
|
||
};
|
||
|
||
export const TAHESAB_JENS_FELEZ_LABELS: Record<number, string> = {
|
||
0: "طلا",
|
||
1: "پلاتین",
|
||
2: "نقره",
|
||
};
|
||
|
||
export interface TahesabAsnadSyncRequest {
|
||
connectionId?: string;
|
||
/** شمسی YYYY-MM-DD */
|
||
from: string;
|
||
/** شمسی YYYY-MM-DD */
|
||
to: string;
|
||
}
|
||
|
||
export interface TahesabAsnadSyncJobDto {
|
||
id: string;
|
||
status: "pending" | "running" | "done" | "failed" | string;
|
||
connectionId: string | null;
|
||
from: string;
|
||
to: string;
|
||
processed: number;
|
||
total: number;
|
||
linesUpserted: number;
|
||
truncatedPersonIds: string[];
|
||
errorMessage: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface TahesabAsnadLineDto {
|
||
id: string;
|
||
connectionId: string;
|
||
personId: string | null;
|
||
personName: string | null;
|
||
moshtariCode: number;
|
||
tarikh: string;
|
||
noRaw: string;
|
||
kind: TahesabAsnadKind | string;
|
||
jensFelez: number | null;
|
||
noKar: string | null;
|
||
vazn: number | null;
|
||
tabdilVazn: number | null;
|
||
mali: number | null;
|
||
count: number | null;
|
||
shFactor: number | null;
|
||
factorCode: string | null;
|
||
groups: CustomerGroupSummaryDto[];
|
||
syncedAt: string;
|
||
}
|
||
|
||
export interface TahesabAsnadReportQuery {
|
||
connectionId?: string;
|
||
from: string;
|
||
to: string;
|
||
groupIds?: string[];
|
||
personId?: string;
|
||
kinds?: TahesabAsnadKind[];
|
||
jensFelez?: number;
|
||
page?: number;
|
||
pageSize?: number;
|
||
}
|
||
|
||
export interface TahesabAsnadReportPageDto {
|
||
items: TahesabAsnadLineDto[];
|
||
total: number;
|
||
page: number;
|
||
pageSize: number;
|
||
}
|
||
|
||
export interface TahesabAsnadSummaryBucketDto {
|
||
key: string;
|
||
label: string;
|
||
lineCount: number;
|
||
maliSum: number;
|
||
vaznSum: number;
|
||
}
|
||
|
||
export interface TahesabAsnadSummaryDto {
|
||
totalLines: number;
|
||
maliSum: number;
|
||
vaznSum: number;
|
||
byKind: TahesabAsnadSummaryBucketDto[];
|
||
byMetal: TahesabAsnadSummaryBucketDto[];
|
||
byGroup: TahesabAsnadSummaryBucketDto[];
|
||
distinctNos: string[];
|
||
lastSyncedAt: string | null;
|
||
}
|
||
|
||
export interface SalesPercentStatsDto {
|
||
first: number | null;
|
||
last: number | null;
|
||
min: number | null;
|
||
max: number | null;
|
||
avg: number | null;
|
||
}
|
||
|
||
export interface SalesMovementStatsDto {
|
||
grossWeight: number;
|
||
returnWeight: number;
|
||
netWeight: number;
|
||
grossMali: number;
|
||
returnMali: number;
|
||
netMali: number;
|
||
grossCount: number;
|
||
returnCount: number;
|
||
netCount: number;
|
||
percent: SalesPercentStatsDto | null;
|
||
}
|
||
|
||
export interface SalesFactorOverviewDto {
|
||
key: string;
|
||
label: string;
|
||
sale: SalesMovementStatsDto;
|
||
purchase: SalesMovementStatsDto;
|
||
}
|
||
|
||
export interface SalesNamedMovementDto {
|
||
name: string;
|
||
sale: SalesMovementStatsDto;
|
||
purchase: SalesMovementStatsDto;
|
||
}
|
||
|
||
export interface SalesGroupChildDto {
|
||
id: string;
|
||
label: string;
|
||
netSaleWeight: number;
|
||
netSaleMali: number;
|
||
saleReturnWeight: number;
|
||
}
|
||
|
||
export interface SalesGroupOverviewDto {
|
||
id: string;
|
||
label: string;
|
||
netSaleWeight: number;
|
||
netSaleMali: number;
|
||
saleReturnWeight: number;
|
||
children: SalesGroupChildDto[];
|
||
}
|
||
|
||
export interface SalesExpertOverviewDto {
|
||
userId: string;
|
||
name: string;
|
||
percent: number;
|
||
silverWeight: number;
|
||
silverMali: number;
|
||
silverCommission: number;
|
||
investWeight: number;
|
||
investMali: number;
|
||
investCommission: number;
|
||
}
|
||
|
||
export interface SalesInternetOverviewDto {
|
||
orderCount: number;
|
||
returnCount: number;
|
||
netGold: number;
|
||
netSilver: number;
|
||
returnGold: number;
|
||
returnSilver: number;
|
||
netMali: number;
|
||
returnMali: number;
|
||
linkedSkipped: number;
|
||
}
|
||
|
||
export interface TahesabSalesOverviewDto {
|
||
factors: SalesFactorOverviewDto[];
|
||
karsakhte: SalesNamedMovementDto[];
|
||
groups: SalesGroupOverviewDto[];
|
||
experts: SalesExpertOverviewDto[];
|
||
internet: SalesInternetOverviewDto;
|
||
distinctNos: string[];
|
||
otherNos: string[];
|
||
percentReady: boolean;
|
||
}
|
||
|
||
/** تنظیمات ظاهر فاکتور PDF اسناد تهحساب (جدا از فاکتور CRM) */
|
||
export interface TahesabInvoiceSettingsDto {
|
||
id: string;
|
||
organizationId: string;
|
||
logoDataUrl: string | null;
|
||
companyName: string;
|
||
companySubtitle: string;
|
||
address: string;
|
||
phone: string;
|
||
headerText: string;
|
||
footerText: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface UpdateTahesabInvoiceSettingsRequest {
|
||
logoDataUrl?: string | null;
|
||
companyName?: string;
|
||
companySubtitle?: string;
|
||
address?: string;
|
||
phone?: string;
|
||
headerText?: string;
|
||
footerText?: string;
|
||
}
|
||
|
||
export interface MandehValueDto {
|
||
amount: string | number;
|
||
unit: string;
|
||
/** بس = بستانکار | بد = بدهکار */
|
||
status: "بس" | "بد" | string;
|
||
}
|
||
|
||
export interface MandehDetailItemDto {
|
||
Name: string;
|
||
values: MandehValueDto[];
|
||
}
|
||
|
||
/** یک فیلد سطح بالای پاسخ GetMandeHesabByCode، دقیقاً همان مقدار تهحساب */
|
||
export interface TahesabMandehFieldDto {
|
||
key: string;
|
||
value: string | null;
|
||
}
|
||
|
||
/** یک ردیف details پاسخ مانده، بدون هیچ تبدیل */
|
||
export interface TahesabMandehDetailRawDto {
|
||
Name: string;
|
||
Value: string | null;
|
||
Value1: string | null;
|
||
Value2: string | null;
|
||
Value3: string | null;
|
||
Value4: string | null;
|
||
}
|
||
|
||
export interface PersonAssetsDto {
|
||
connectionId: string;
|
||
connectionLabel: string;
|
||
dbName: string;
|
||
moshtariCode: number;
|
||
/** فیلدهای خام تهحساب به همان ترتیب پاسخ */
|
||
mandehFields?: TahesabMandehFieldDto[];
|
||
mandehDetailRaw?: TahesabMandehDetailRawDto[];
|
||
goldBalance: number | null;
|
||
rialBalance: number | null;
|
||
currencyBalance: number | null;
|
||
totalGoldEquiv: number | null;
|
||
mandehDetail: MandehDetailItemDto[];
|
||
fetchedAt: string;
|
||
}
|
||
|
||
// —— پیام خودکار (جدا از اتوماسیون گردش کار) ——
|
||
|
||
export type MessageAutomationTrigger =
|
||
| "person_created"
|
||
| "lead_submitted"
|
||
| "deal_created"
|
||
| "deal_stage_changed"
|
||
| "deal_won"
|
||
| "deal_lost"
|
||
| "ticket_created"
|
||
| "ticket_resolved"
|
||
| "invoice_issued"
|
||
| "invoice_paid"
|
||
| "activity_created"
|
||
| "activity_done"
|
||
| "sms_link_clicked";
|
||
|
||
export type MessageAutomationDelayUnit =
|
||
| "minutes"
|
||
| "hours"
|
||
| "days"
|
||
| "weeks"
|
||
| "months";
|
||
export type MessageAutomationChannel = "app" | "sms" | "both";
|
||
export type MessageAutomationTarget = "staff" | "person" | "both";
|
||
|
||
export const MESSAGE_AUTOMATION_TRIGGERS = [
|
||
"person_created",
|
||
"lead_submitted",
|
||
"deal_created",
|
||
"deal_stage_changed",
|
||
"deal_won",
|
||
"deal_lost",
|
||
"ticket_created",
|
||
"ticket_resolved",
|
||
"invoice_issued",
|
||
"invoice_paid",
|
||
"activity_created",
|
||
"activity_done",
|
||
"sms_link_clicked",
|
||
] as const satisfies readonly MessageAutomationTrigger[];
|
||
|
||
export const MESSAGE_AUTOMATION_TRIGGER_LABELS: Record<
|
||
MessageAutomationTrigger,
|
||
string
|
||
> = {
|
||
person_created: "مخاطب جدید ساخته شد",
|
||
lead_submitted: "فرم سرنخ ارسال شد",
|
||
deal_created: "معامله جدید ساخته شد",
|
||
deal_stage_changed: "مرحله معامله عوض شد",
|
||
deal_won: "معامله برنده شد",
|
||
deal_lost: "معامله باخت",
|
||
ticket_created: "تیکت جدید ساخته شد",
|
||
ticket_resolved: "تیکت حل شد",
|
||
invoice_issued: "فاکتور صادر شد",
|
||
invoice_paid: "پرداخت روی فاکتور ثبت شد",
|
||
activity_created: "فعالیت ثبت شد",
|
||
activity_done: "فعالیت تمام شد",
|
||
sms_link_clicked: "کلیک روی لینک پیامک",
|
||
};
|
||
|
||
export const MESSAGE_AUTOMATION_UNIT_LABELS: Record<
|
||
MessageAutomationDelayUnit,
|
||
string
|
||
> = {
|
||
minutes: "دقیقه",
|
||
hours: "ساعت",
|
||
days: "روز",
|
||
weeks: "هفته",
|
||
months: "ماه",
|
||
};
|
||
|
||
export const MESSAGE_AUTOMATION_FIELDS = [
|
||
{ token: "{نام}", label: "نام مشتری" },
|
||
{ token: "{شماره}", label: "شماره موبایل" },
|
||
{ token: "{تاریخ}", label: "تاریخ ارسال" },
|
||
{ token: "{عنوان}", label: "عنوان رکورد" },
|
||
{ token: "{شرکت}", label: "نام شرکت" },
|
||
] as const;
|
||
|
||
/** First-part Persian SMS limit (legacy UI estimate). Prefer countSmsParts(). */
|
||
export const MESSAGE_AUTOMATION_SMS_SEGMENT = 70;
|
||
|
||
/** Hamkar-compatible Persian multipart limits: 70 / 64 / 67 */
|
||
export function countSmsParts(text: string): number {
|
||
const length = [...String(text ?? "").trim()].length;
|
||
if (length <= 0) return 0;
|
||
const first = 70;
|
||
const second = 64;
|
||
const rest = 67;
|
||
if (length <= first) return 1;
|
||
if (length <= first + second) return 2;
|
||
const thirdThreshold = first + second + rest;
|
||
if (length <= thirdThreshold) return 3;
|
||
return 3 + Math.ceil((length - thirdThreshold) / rest);
|
||
}
|
||
|
||
export const MESSAGE_AUTOMATION_CHANNEL_LABELS: Record<
|
||
MessageAutomationChannel,
|
||
string
|
||
> = {
|
||
app: "ناتیف داخل برنامه",
|
||
sms: "پیامک",
|
||
both: "پیامک و ناتیف",
|
||
};
|
||
|
||
export const MESSAGE_AUTOMATION_TARGET_LABELS: Record<
|
||
MessageAutomationTarget,
|
||
string
|
||
> = {
|
||
staff: "مسئول پرونده",
|
||
person: "مشتری",
|
||
both: "مشتری و مسئول",
|
||
};
|
||
|
||
export interface MessageAutomationRuleDto {
|
||
id: string;
|
||
name: string;
|
||
isActive: boolean;
|
||
trigger: MessageAutomationTrigger;
|
||
startDelayValue: number;
|
||
startDelayUnit: MessageAutomationDelayUnit;
|
||
repeatCount: number;
|
||
intervalValue: number;
|
||
intervalUnit: MessageAutomationDelayUnit;
|
||
notifyTarget: MessageAutomationTarget;
|
||
channel: MessageAutomationChannel;
|
||
title: string;
|
||
description: string | null;
|
||
body: string;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateMessageAutomationRuleRequest {
|
||
name: string;
|
||
isActive?: boolean;
|
||
trigger: MessageAutomationTrigger;
|
||
startDelayValue?: number;
|
||
startDelayUnit?: MessageAutomationDelayUnit;
|
||
repeatCount?: number;
|
||
intervalValue?: number;
|
||
intervalUnit?: MessageAutomationDelayUnit;
|
||
notifyTarget?: MessageAutomationTarget;
|
||
channel?: MessageAutomationChannel;
|
||
title: string;
|
||
description?: string | null;
|
||
body: string;
|
||
}
|
||
|
||
export type UpdateMessageAutomationRuleRequest =
|
||
Partial<CreateMessageAutomationRuleRequest>;
|
||
|
||
export interface PlatformAdminDto {
|
||
id: string;
|
||
mobile?: string | null;
|
||
email?: string | null;
|
||
fullName: string;
|
||
isActive?: boolean;
|
||
lastLoginAt?: string | null;
|
||
createdAt?: string;
|
||
}
|
||
|
||
export interface PlatformLoginRequest {
|
||
mobile: string;
|
||
password: string;
|
||
}
|
||
|
||
export interface PlatformLoginResponse {
|
||
accessToken: string;
|
||
admin: PlatformAdminDto;
|
||
}
|
||
|
||
export interface CreatePlatformAdminRequest {
|
||
fullName: string;
|
||
mobile: string;
|
||
password: string;
|
||
email?: string;
|
||
}
|
||
|
||
export interface ResetPlatformAdminPasswordRequest {
|
||
password: string;
|
||
}
|
||
|
||
export interface PlatformStatsDto {
|
||
organizationCount: number;
|
||
activeOrganizationCount: number;
|
||
userCount: number;
|
||
activeUserCount: number;
|
||
membershipCount: number;
|
||
expiringSoonCount?: number;
|
||
expiredBillingCount?: number;
|
||
}
|
||
|
||
export interface PlatformOrgMemberDto {
|
||
membershipId: string;
|
||
userId: string;
|
||
fullName: string;
|
||
email: string;
|
||
mobile?: string | null;
|
||
role: UserRole;
|
||
isActive: boolean;
|
||
userIsActive: boolean;
|
||
isBillingOwner?: boolean;
|
||
billing?: BillingAccountDto | null;
|
||
}
|
||
|
||
export interface PlatformOrganizationDto {
|
||
id: string;
|
||
name: string;
|
||
slug: string;
|
||
businessField?: string | null;
|
||
isActive: boolean;
|
||
createdAt: string;
|
||
memberCount: number;
|
||
personCount: number;
|
||
dealCount: number;
|
||
enabledModules?: OrgModuleKey[];
|
||
/** اشتراک مالک اصلی محیط */
|
||
subscription?: SubscriptionSummaryDto | null;
|
||
members?: PlatformOrgMemberDto[];
|
||
}
|
||
|
||
export interface PlatformUserMembershipDto {
|
||
membershipId: string;
|
||
organizationId: string;
|
||
organizationName: string;
|
||
organizationIsActive: boolean;
|
||
role: UserRole;
|
||
isActive: boolean;
|
||
}
|
||
|
||
export interface PlatformUserDto {
|
||
id: string;
|
||
email: string;
|
||
mobile?: string | null;
|
||
fullName: string;
|
||
isActive: boolean;
|
||
lastLoginAt?: string | null;
|
||
createdAt: string;
|
||
memberships: PlatformUserMembershipDto[];
|
||
/** فقط اگر حداقل یک عضویت مالک فعال دارد */
|
||
isBillingOwner?: boolean;
|
||
billing?: BillingAccountDto | null;
|
||
}
|
||
|
||
export interface UpdatePlatformStatusRequest {
|
||
isActive: boolean;
|
||
}
|
||
|
||
export interface CreatePlatformOrganizationRequest {
|
||
name: string;
|
||
businessField?: string;
|
||
ownerFullName: string;
|
||
ownerEmail: string;
|
||
ownerPassword: string;
|
||
ownerMobile?: string;
|
||
}
|
||
|
||
export interface UpdatePlatformOrganizationRequest {
|
||
name?: string;
|
||
slug?: string;
|
||
businessField?: string | null;
|
||
isActive?: boolean;
|
||
}
|
||
|
||
export interface UpdatePlatformMembershipRoleRequest {
|
||
role: UserRole;
|
||
}
|
||
|
||
export interface ResetPlatformUserPasswordRequest {
|
||
password: string;
|
||
}
|
||
|
||
export { brand, APP_NAME, APP_NAME_FA, brandSuggestionFa } from "./brand";
|
||
export type { Brand } from "./brand";
|
||
export const API_PREFIX = "api";
|
||
|
||
/* ——— لاگ فعالیت کاربران ——— */
|
||
|
||
export interface AuditLogDto {
|
||
id: string;
|
||
action: string;
|
||
method: string;
|
||
path: string;
|
||
entityType: string | null;
|
||
entityId: string | null;
|
||
summary: string;
|
||
/** نام خوانای پرونده، مثل عنوان فعالیت یا نام مشتری */
|
||
label?: string | null;
|
||
/** چه چیزی ساخته یا عوض شده */
|
||
detail?: string | null;
|
||
actor: UserSummaryDto | null;
|
||
ip: string | null;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface AuditLogListResponse {
|
||
items: AuditLogDto[];
|
||
total: number;
|
||
page: number;
|
||
limit: number;
|
||
}
|
||
|
||
export type DeletionRetention =
|
||
| 'month'
|
||
| 'two_months'
|
||
| 'three_months'
|
||
| 'six_months'
|
||
| 'year'
|
||
| 'never';
|
||
|
||
export interface DeletionArchiveDto {
|
||
id: string;
|
||
model: string;
|
||
modelLabel: string;
|
||
entityId: string;
|
||
label: string;
|
||
deletedById: string | null;
|
||
deletedByName: string | null;
|
||
deletedAt: string;
|
||
}
|
||
|
||
export interface DeletionArchiveListResponse {
|
||
items: DeletionArchiveDto[];
|
||
total: number;
|
||
page: number;
|
||
limit: number;
|
||
retention: DeletionRetention;
|
||
}
|
||
|
||
/* ——— فیلترهای ذخیرهشده ——— */
|
||
|
||
export type SavedViewScope =
|
||
| "deals"
|
||
| "people"
|
||
| "companies"
|
||
| "activities"
|
||
| "project_cards"
|
||
| "reports";
|
||
|
||
export const SAVED_VIEW_SCOPES: SavedViewScope[] = [
|
||
"deals",
|
||
"people",
|
||
"companies",
|
||
"activities",
|
||
"project_cards",
|
||
"reports",
|
||
];
|
||
|
||
/** فیلترهای ذخیرهشدهٔ صفحه گزارشات */
|
||
export interface ReportsViewFilters {
|
||
/** today | week | month | quarter | year | custom */
|
||
period: string;
|
||
from?: string;
|
||
to?: string;
|
||
/** dashboard | builder */
|
||
mode?: string;
|
||
builder?: {
|
||
metric?: string;
|
||
dimension?: string;
|
||
userId?: string;
|
||
teamId?: string;
|
||
};
|
||
}
|
||
|
||
/** متریکهای گزارشساز ساده */
|
||
export type ReportsBuilderMetric =
|
||
| "won_amount"
|
||
| "won_count"
|
||
| "lost_count"
|
||
| "open_count"
|
||
| "open_amount"
|
||
| "activities_done"
|
||
| "people_created"
|
||
| "invoice_amount"
|
||
| "invoice_count";
|
||
|
||
export const REPORTS_BUILDER_METRICS: ReportsBuilderMetric[] = [
|
||
"won_amount",
|
||
"won_count",
|
||
"lost_count",
|
||
"open_count",
|
||
"open_amount",
|
||
"activities_done",
|
||
"people_created",
|
||
"invoice_amount",
|
||
"invoice_count",
|
||
];
|
||
|
||
export const REPORTS_BUILDER_METRIC_LABELS: Record<
|
||
ReportsBuilderMetric,
|
||
string
|
||
> = {
|
||
won_amount: "مبلغ معاملات برنده",
|
||
won_count: "تعداد معاملات برنده",
|
||
lost_count: "تعداد معاملات باخته",
|
||
open_count: "تعداد معاملات باز",
|
||
open_amount: "مبلغ معاملات باز",
|
||
activities_done: "فعالیت انجامشده",
|
||
people_created: "مشتریان جدید",
|
||
invoice_amount: "مبلغ فاکتور",
|
||
invoice_count: "تعداد فاکتور",
|
||
};
|
||
|
||
/** ابعاد گروهبندی گزارشساز */
|
||
export type ReportsBuilderDimension =
|
||
| "none"
|
||
| "owner"
|
||
| "team"
|
||
| "source"
|
||
| "stage"
|
||
| "month"
|
||
| "status";
|
||
|
||
export const REPORTS_BUILDER_DIMENSIONS: ReportsBuilderDimension[] = [
|
||
"none",
|
||
"owner",
|
||
"team",
|
||
"source",
|
||
"stage",
|
||
"month",
|
||
"status",
|
||
];
|
||
|
||
export const REPORTS_BUILDER_DIMENSION_LABELS: Record<
|
||
ReportsBuilderDimension,
|
||
string
|
||
> = {
|
||
none: "بدون گروهبندی (جمع کل)",
|
||
owner: "کاربر / مسئول",
|
||
team: "تیم",
|
||
source: "منبع",
|
||
stage: "مرحله مسیر فروش",
|
||
month: "ماه",
|
||
status: "وضعیت معامله",
|
||
};
|
||
|
||
export interface ReportsBuilderQuery {
|
||
metric: ReportsBuilderMetric;
|
||
dimension: ReportsBuilderDimension;
|
||
from?: string;
|
||
to?: string;
|
||
userId?: string;
|
||
teamId?: string;
|
||
}
|
||
|
||
export interface ReportsBuilderRowDto {
|
||
key: string;
|
||
label: string;
|
||
value: number;
|
||
}
|
||
|
||
export interface ReportsBuilderResultDto {
|
||
metric: ReportsBuilderMetric;
|
||
metricLabel: string;
|
||
dimension: ReportsBuilderDimension;
|
||
dimensionLabel: string;
|
||
from: string;
|
||
to: string;
|
||
currency: string;
|
||
format: "money" | "number";
|
||
total: number;
|
||
rows: ReportsBuilderRowDto[];
|
||
}
|
||
|
||
export interface SavedViewDto {
|
||
id: string;
|
||
name: string;
|
||
scope: SavedViewScope;
|
||
filters: Record<string, unknown>;
|
||
isShared: boolean;
|
||
owner: UserSummaryDto | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateSavedViewRequest {
|
||
name: string;
|
||
scope: SavedViewScope;
|
||
filters: Record<string, unknown>;
|
||
isShared?: boolean;
|
||
}
|
||
|
||
export type UpdateSavedViewRequest = Partial<CreateSavedViewRequest>;
|
||
|
||
/* ——— API عمومی و وبهوک ——— */
|
||
|
||
export const WEBHOOK_EVENTS = [
|
||
"person.created",
|
||
"person.updated",
|
||
"deal.created",
|
||
"deal.updated",
|
||
"deal.won",
|
||
"deal.lost",
|
||
"invoice.paid",
|
||
"activity.created",
|
||
] as const;
|
||
|
||
export type WebhookEvent = (typeof WEBHOOK_EVENTS)[number];
|
||
|
||
export interface OrganizationApiKeyDto {
|
||
id: string;
|
||
name: string;
|
||
prefix: string;
|
||
lastUsedAt: string | null;
|
||
revokedAt: string | null;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface CreateOrganizationApiKeyRequest {
|
||
name: string;
|
||
}
|
||
|
||
export interface CreateOrganizationApiKeyResponse extends OrganizationApiKeyDto {
|
||
/** فقط یکبار هنگام ساخت برگردانده میشود */
|
||
rawKey: string;
|
||
}
|
||
|
||
export interface WebhookEndpointDto {
|
||
id: string;
|
||
name: string;
|
||
url: string;
|
||
events: string[];
|
||
isActive: boolean;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateWebhookEndpointRequest {
|
||
name: string;
|
||
url: string;
|
||
events: string[];
|
||
secret?: string;
|
||
isActive?: boolean;
|
||
}
|
||
|
||
/** فقط هنگام ساخت؛ secret دوباره برنمیگردد */
|
||
export interface CreateWebhookEndpointResponse extends WebhookEndpointDto {
|
||
secret: string;
|
||
}
|
||
|
||
export type UpdateWebhookEndpointRequest = Partial<CreateWebhookEndpointRequest>;
|
||
|
||
export interface WebhookDeliveryDto {
|
||
id: string;
|
||
event: string;
|
||
status: string;
|
||
statusCode: number | null;
|
||
attempts: number;
|
||
lastError: string | null;
|
||
createdAt: string;
|
||
deliveredAt: string | null;
|
||
}
|
||
|
||
/* ——— لاگ پیامک خروجی ——— */
|
||
|
||
export type OutboundSmsSource = "manual" | "campaign" | "automation";
|
||
export type OutboundSmsStatus = "sent" | "failed";
|
||
|
||
export const OUTBOUND_SMS_SOURCE_LABELS: Record<OutboundSmsSource, string> = {
|
||
manual: "دستی",
|
||
campaign: "کمپین",
|
||
automation: "اتوماسیون",
|
||
};
|
||
|
||
export interface OutboundSmsLogDto {
|
||
id: string;
|
||
body: string;
|
||
mobile: string;
|
||
source: OutboundSmsSource;
|
||
sourceLabel: string;
|
||
status: OutboundSmsStatus;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface OutboundSmsHistoryResponse {
|
||
items: OutboundSmsLogDto[];
|
||
}
|
||
|
||
/* ——— پیامک گروهی ——— */
|
||
|
||
export type SmsCampaignStatus =
|
||
| "draft"
|
||
| "queued"
|
||
| "sending"
|
||
| "done"
|
||
| "cancelled";
|
||
|
||
export interface SmsCampaignDto {
|
||
id: string;
|
||
name: string;
|
||
body: string;
|
||
status: SmsCampaignStatus;
|
||
scheduledAt: string | null;
|
||
totalCount: number;
|
||
sentCount: number;
|
||
failedCount: number;
|
||
skippedCount: number;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
/** حداکثر گیرنده در هر درخواست ساخت/افزودن کمپین پیامک */
|
||
export const SMS_CAMPAIGN_CHUNK_SIZE = 50;
|
||
|
||
/** سقف شناسههای قابل دریافت از /people/ids و /leads/ids */
|
||
export const FILTERED_IDS_MAX = 50_000;
|
||
|
||
export interface FilteredIdsResponse {
|
||
ids: string[];
|
||
total: number;
|
||
}
|
||
|
||
export interface CreateSmsCampaignRequest {
|
||
name: string;
|
||
body: string;
|
||
/** دقیقاً یکی از personIds یا leadIds */
|
||
personIds?: string[];
|
||
leadIds?: string[];
|
||
scheduledAt?: string | null;
|
||
/** اگر true باشد بلافاصله صف میشود (فقط وقتی همه گیرندهها در همین درخواست باشند یا بعد از افزودن تکهها جداگانه queue شود) */
|
||
queueNow?: boolean;
|
||
}
|
||
|
||
export interface AddSmsCampaignRecipientsRequest {
|
||
personIds?: string[];
|
||
leadIds?: string[];
|
||
}
|
||
|
||
export interface SmsCampaignListResponse {
|
||
items: SmsCampaignDto[];
|
||
total: number;
|
||
}
|
||
|
||
export type SmsCampaignContactType = "customer" | "lead";
|
||
|
||
export interface SmsCampaignClickerDto {
|
||
contactType: SmsCampaignContactType;
|
||
personId: string | null;
|
||
leadId: string | null;
|
||
name: string;
|
||
mobile: string | null;
|
||
firstClickedAt: string | null;
|
||
lastClickedAt: string | null;
|
||
clickCount: number;
|
||
}
|
||
|
||
export interface SmsCampaignReportDto {
|
||
campaign: SmsCampaignDto;
|
||
totalCount: number;
|
||
sentCount: number;
|
||
failedCount: number;
|
||
skippedCount: number;
|
||
uniqueClickers: number;
|
||
totalClicks: number;
|
||
/** درصد یکتا نسبت به ارسالشده */
|
||
clickRate: number;
|
||
multiClickers: number;
|
||
clickedUsers: SmsCampaignClickerDto[];
|
||
}
|
||
|
||
/* ——— تیمها و سطح دسترسی به پروندهها ——— */
|
||
|
||
export type TeamMemberRole = "member" | "manager";
|
||
|
||
export interface TeamMemberDto {
|
||
userId: string;
|
||
fullName: string;
|
||
role: TeamMemberRole;
|
||
hasAvatar?: boolean;
|
||
}
|
||
|
||
export interface TeamDto {
|
||
id: string;
|
||
name: string;
|
||
description: string;
|
||
memberCount: number;
|
||
members: TeamMemberDto[];
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateTeamRequest {
|
||
name: string;
|
||
description?: string;
|
||
memberIds?: string[];
|
||
managerIds?: string[];
|
||
}
|
||
|
||
export interface UpdateTeamRequest {
|
||
name?: string;
|
||
description?: string;
|
||
}
|
||
|
||
export interface SetTeamMembersRequest {
|
||
members: Array<{ userId: string; role: TeamMemberRole }>;
|
||
}
|
||
|
||
/* ——— ووکامرس ——— */
|
||
|
||
export interface WooCommerceConnectionDto {
|
||
id: string;
|
||
label: string;
|
||
storeUrl: string;
|
||
consumerKeyMasked: string;
|
||
webhookSecretSet: boolean;
|
||
enabled: boolean;
|
||
lastSyncAt: string | null;
|
||
lastHealthAt: string | null;
|
||
lastHealthOk: boolean | null;
|
||
lastError: string | null;
|
||
importedOrders: number;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateWooCommerceConnectionRequest {
|
||
label: string;
|
||
storeUrl: string;
|
||
consumerKey: string;
|
||
consumerSecret: string;
|
||
webhookSecret?: string;
|
||
enabled?: boolean;
|
||
}
|
||
|
||
export interface UpdateWooCommerceConnectionRequest {
|
||
label?: string;
|
||
storeUrl?: string;
|
||
consumerKey?: string;
|
||
consumerSecret?: string;
|
||
webhookSecret?: string | null;
|
||
enabled?: boolean;
|
||
}
|
||
|
||
export interface WooCommerceSyncResultDto {
|
||
fetched: number;
|
||
created: number;
|
||
updated: number;
|
||
skipped: number;
|
||
errors: string[];
|
||
}
|
||
|
||
export interface WooCommerceOrderLinkDto {
|
||
id: string;
|
||
wooOrderId: string;
|
||
wooOrderNumber: string | null;
|
||
invoiceId: string;
|
||
invoiceNumber: string;
|
||
status: string;
|
||
total: number;
|
||
currency: string;
|
||
syncedAt: string;
|
||
}
|
||
|
||
/* ——— فروشگاههای Montify ——— */
|
||
|
||
export type MontifySyncStatus = "idle" | "running" | "ok" | "error";
|
||
|
||
export interface MontifySyncResultDto {
|
||
mode: "incremental" | "full";
|
||
customersFetched: number;
|
||
customersCreated: number;
|
||
customersUpdated: number;
|
||
ordersFetched: number;
|
||
transactionsFetched: number;
|
||
errors: string[];
|
||
startedAt: string;
|
||
finishedAt: string | null;
|
||
}
|
||
|
||
export interface MontifyConnectionDto {
|
||
id: string;
|
||
label: string;
|
||
baseUrl: string;
|
||
storeId: string;
|
||
tokenMasked: string;
|
||
enabled: boolean;
|
||
webhookRegistered: boolean;
|
||
webhookUrl: string;
|
||
syncStatus: MontifySyncStatus;
|
||
syncStartedAt: string | null;
|
||
lastSyncAt: string | null;
|
||
lastSyncResult: MontifySyncResultDto | null;
|
||
lastHealthAt: string | null;
|
||
lastHealthOk: boolean | null;
|
||
lastError: string | null;
|
||
lastWebhookAt: string | null;
|
||
linkedCustomers: number;
|
||
orders: number;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateMontifyConnectionRequest {
|
||
label: string;
|
||
baseUrl: string;
|
||
token: string;
|
||
storeId?: string;
|
||
enabled?: boolean;
|
||
}
|
||
|
||
export type UpdateMontifyConnectionRequest = Partial<CreateMontifyConnectionRequest>;
|
||
|
||
export interface MontifyTestResultDto {
|
||
ok: boolean;
|
||
totalCustomers: number | null;
|
||
}
|
||
|
||
export interface MontifyDiagnoseCheckDto {
|
||
name: string;
|
||
path: string;
|
||
status: number;
|
||
ok: boolean;
|
||
info: string;
|
||
}
|
||
|
||
export interface MontifyDiagnoseDto {
|
||
customerId: string | null;
|
||
checks: MontifyDiagnoseCheckDto[];
|
||
}
|
||
|
||
export interface MontifyOrderItemDto {
|
||
productId: string | null;
|
||
productName: string | null;
|
||
qty: number | null;
|
||
price: number | null;
|
||
weight: number | null;
|
||
totalPrice: number | null;
|
||
}
|
||
|
||
/** سفارش فروشگاه — orderReference = code، orderStatus = sts */
|
||
export interface MontifyOrderDto {
|
||
id: string;
|
||
connectionId: string;
|
||
connectionLabel: string;
|
||
externalId: string;
|
||
customerExternalId: string | null;
|
||
personId: string | null;
|
||
personName: string | null;
|
||
orderReference: string | null;
|
||
orderStatus: string | null;
|
||
paymentStatus: string | null;
|
||
paymentType: string | null;
|
||
totalPrice: number | null;
|
||
discountTotalPrice: number | null;
|
||
totalDiscounts: number | null;
|
||
totalQty: number | null;
|
||
sumGold: number | null;
|
||
sumSilver: number | null;
|
||
note: string | null;
|
||
cancelReason: string | null;
|
||
cancelledAt: string | null;
|
||
closedAt: string | null;
|
||
items: MontifyOrderItemDto[];
|
||
createdAt: string | null;
|
||
updatedAt: string | null;
|
||
}
|
||
|
||
export interface MontifyTransactionDto {
|
||
id: string;
|
||
externalId: string;
|
||
type: string | null;
|
||
amount: number | null;
|
||
currency: string | null;
|
||
status: string | null;
|
||
referenceCode: string | null;
|
||
createdAt: string | null;
|
||
}
|
||
|
||
export interface MontifyEngagementDto {
|
||
saved: number;
|
||
likes: number;
|
||
comments: number;
|
||
}
|
||
|
||
/** پروفایل فروشگاهی یک شخص (wallet_balance / gold_wallet_balance طبق نگاشت مستند) */
|
||
export interface MontifyPersonStoreDto {
|
||
connectionId: string;
|
||
connectionLabel: string;
|
||
externalCustomerId: string;
|
||
fullName: string | null;
|
||
username: string | null;
|
||
customerType: string | null;
|
||
walletBalance: number | null;
|
||
goldWalletBalance: number | null;
|
||
totalOrders: number | null;
|
||
totalSpent: number | null;
|
||
engagement: MontifyEngagementDto | null;
|
||
orders: MontifyOrderDto[];
|
||
transactions: MontifyTransactionDto[];
|
||
syncedAt: string;
|
||
}
|
||
|
||
/* ——— تلفن سازمانی / VoIP ——— */
|
||
|
||
export type VoipCallDirection = "inbound" | "outbound";
|
||
|
||
export type VoipCallStatus =
|
||
| "ringing"
|
||
| "answered"
|
||
| "missed"
|
||
| "completed"
|
||
| "failed";
|
||
|
||
export interface VoipConnectionDto {
|
||
id: string;
|
||
label: string;
|
||
enabled: boolean;
|
||
webhookSecretSet: boolean;
|
||
/** قالب URL برای شروع تماس؛ {number} جایگزین میشود */
|
||
clickToCallUrl: string | null;
|
||
lastEventAt: string | null;
|
||
lastError: string | null;
|
||
callCount: number;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateVoipConnectionRequest {
|
||
label: string;
|
||
webhookSecret?: string;
|
||
clickToCallUrl?: string | null;
|
||
enabled?: boolean;
|
||
}
|
||
|
||
export interface UpdateVoipConnectionRequest {
|
||
label?: string;
|
||
webhookSecret?: string | null;
|
||
clickToCallUrl?: string | null;
|
||
enabled?: boolean;
|
||
}
|
||
|
||
export type VoipSummaryStatus =
|
||
| "none"
|
||
| "pending"
|
||
| "processing"
|
||
| "done"
|
||
| "failed"
|
||
| "skipped";
|
||
|
||
export interface VoipCallLogDto {
|
||
id: string;
|
||
direction: VoipCallDirection;
|
||
status: VoipCallStatus | string;
|
||
fromNumber: string | null;
|
||
toNumber: string | null;
|
||
personId: string | null;
|
||
personName: string | null;
|
||
activityId: string | null;
|
||
durationSec: number | null;
|
||
startedAt: string;
|
||
syncedAt: string;
|
||
recordingUrl: string | null;
|
||
transcript: string | null;
|
||
summary: string | null;
|
||
summaryStatus: VoipSummaryStatus | string;
|
||
summaryNoteId: string | null;
|
||
summaryError: string | null;
|
||
}
|
||
|
||
export interface VoipClickToCallRequest {
|
||
number: string;
|
||
personId?: string;
|
||
}
|
||
|
||
export interface VoipWebhookEvent {
|
||
/** شناسه یکتای تماس در سانترال */
|
||
callId: string;
|
||
direction: VoipCallDirection;
|
||
status: VoipCallStatus | string;
|
||
from?: string;
|
||
to?: string;
|
||
startedAt?: string;
|
||
endedAt?: string;
|
||
durationSec?: number;
|
||
/** لینک فایل ضبط (برای خلاصه خودکار) */
|
||
recordingUrl?: string;
|
||
/** متن آمادهٔ تماس؛ اگر باشد STT رد میشود */
|
||
transcript?: string;
|
||
/** برای سازگاری با فیلدهای رایج دیگر */
|
||
caller?: string;
|
||
callee?: string;
|
||
}
|
||
|
||
/* ——— رزرو و نوبتدهی ——— */
|
||
|
||
export type ReservationStatus =
|
||
| "pending"
|
||
| "confirmed"
|
||
| "canceled"
|
||
| "done";
|
||
|
||
export const RESERVATION_STATUSES: ReservationStatus[] = [
|
||
"pending",
|
||
"confirmed",
|
||
"canceled",
|
||
"done",
|
||
];
|
||
|
||
export const RESERVATION_STATUS_LABELS: Record<ReservationStatus, string> = {
|
||
pending: "در انتظار",
|
||
confirmed: "تأیید شده",
|
||
canceled: "لغو شده",
|
||
done: "انجام شده",
|
||
};
|
||
|
||
export interface ReservationServiceDto {
|
||
id: string;
|
||
name: string;
|
||
description: string;
|
||
durationMinutes: number;
|
||
/** روزهای هفته ۰=یکشنبه … ۶=شنبه (JS getDay) */
|
||
workDays: number[];
|
||
dayStartHour: number;
|
||
dayEndHour: number;
|
||
isActive: boolean;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateReservationServiceRequest {
|
||
name: string;
|
||
description?: string;
|
||
durationMinutes: number;
|
||
workDays?: number[];
|
||
dayStartHour?: number;
|
||
dayEndHour?: number;
|
||
isActive?: boolean;
|
||
}
|
||
|
||
export type UpdateReservationServiceRequest =
|
||
Partial<CreateReservationServiceRequest>;
|
||
|
||
export interface ReservationAppointmentDto {
|
||
id: string;
|
||
serviceId: string;
|
||
serviceName: string;
|
||
personId: string;
|
||
personName: string;
|
||
assigneeId: string | null;
|
||
assigneeName: string | null;
|
||
startsAt: string;
|
||
endsAt: string;
|
||
status: ReservationStatus;
|
||
notes: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface CreateReservationAppointmentRequest {
|
||
serviceId: string;
|
||
personId: string;
|
||
assigneeId?: string | null;
|
||
startsAt: string;
|
||
status?: ReservationStatus;
|
||
notes?: string | null;
|
||
}
|
||
|
||
export interface UpdateReservationAppointmentRequest {
|
||
serviceId?: string;
|
||
personId?: string;
|
||
assigneeId?: string | null;
|
||
startsAt?: string;
|
||
status?: ReservationStatus;
|
||
notes?: string | null;
|
||
}
|
||
|
||
export interface ReservationSlotDto {
|
||
startsAt: string;
|
||
endsAt: string;
|
||
}
|
||
|
||
export interface ReservationListResponse {
|
||
items: ReservationAppointmentDto[];
|
||
total: number;
|
||
}
|
||
|
||
|
||
|
||
/* ——— مدیریت لیدها ——— */
|
||
|
||
export type LeadAssignRule = "round_robin" | "equal_split";
|
||
|
||
export interface LeadStageDto {
|
||
id: string;
|
||
name: string;
|
||
position: number;
|
||
color: string;
|
||
showOnBoard: boolean;
|
||
isConverted: boolean;
|
||
isLost: boolean;
|
||
isUnqualified: boolean;
|
||
leadCount?: number;
|
||
}
|
||
|
||
export interface CreateLeadStageRequest {
|
||
name: string;
|
||
color?: string;
|
||
showOnBoard?: boolean;
|
||
isConverted?: boolean;
|
||
isLost?: boolean;
|
||
isUnqualified?: boolean;
|
||
}
|
||
|
||
export interface UpdateLeadStageRequest {
|
||
name?: string;
|
||
color?: string;
|
||
showOnBoard?: boolean;
|
||
isConverted?: boolean;
|
||
isLost?: boolean;
|
||
isUnqualified?: boolean;
|
||
position?: number;
|
||
}
|
||
|
||
export interface LeadDto {
|
||
id: string;
|
||
title: string;
|
||
contactName: string | null;
|
||
gender: PersonGender;
|
||
showGenderInName: boolean;
|
||
mobile: string;
|
||
phone: string | null;
|
||
city: string | null;
|
||
region: string | null;
|
||
instagram: string | null;
|
||
birthDate: string | null;
|
||
addresses: string[];
|
||
trade: string | null;
|
||
source: string | null;
|
||
notes: string | null;
|
||
estimatedValue: number | null;
|
||
stageId: string;
|
||
stageName: string;
|
||
position: number;
|
||
assigneeId: string | null;
|
||
assigneeName: string | null;
|
||
lastContactAt: string | null;
|
||
nextFollowUpAt: string | null;
|
||
convertedPersonId: string | null;
|
||
convertedAt: string | null;
|
||
archivedAt: string | null;
|
||
daysInStage: number;
|
||
hasLogo: boolean;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface LeadEventDto {
|
||
id: string;
|
||
type: string;
|
||
title: string;
|
||
body: string | null;
|
||
actorId: string | null;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface CreateLeadRequest {
|
||
title: string;
|
||
mobile: string;
|
||
contactName?: string | null;
|
||
gender?: PersonGender;
|
||
showGenderInName?: boolean;
|
||
phone?: string | null;
|
||
city?: string | null;
|
||
region?: string | null;
|
||
instagram?: string | null;
|
||
birthDate?: string | null;
|
||
addresses?: string[];
|
||
trade?: string | null;
|
||
source?: string | null;
|
||
notes?: string | null;
|
||
estimatedValue?: number | null;
|
||
stageId?: string;
|
||
assigneeId?: string | null;
|
||
nextFollowUpAt?: string | null;
|
||
/** فقط برای ایمپورت اکسل — تاریخ ثبت مبدأ */
|
||
registeredAt?: string | null;
|
||
}
|
||
|
||
export interface UpdateLeadRequest {
|
||
title?: string;
|
||
mobile?: string;
|
||
contactName?: string | null;
|
||
gender?: PersonGender;
|
||
showGenderInName?: boolean;
|
||
phone?: string | null;
|
||
city?: string | null;
|
||
region?: string | null;
|
||
instagram?: string | null;
|
||
birthDate?: string | null;
|
||
addresses?: string[];
|
||
trade?: string | null;
|
||
source?: string | null;
|
||
notes?: string | null;
|
||
estimatedValue?: number | null;
|
||
stageId?: string;
|
||
assigneeId?: string | null;
|
||
nextFollowUpAt?: string | null;
|
||
lastContactAt?: string | null;
|
||
}
|
||
|
||
export interface MoveLeadRequest {
|
||
stageId: string;
|
||
position?: number;
|
||
}
|
||
|
||
export interface AssignLeadsRequest {
|
||
leadIds: string[];
|
||
assigneeId: string;
|
||
}
|
||
|
||
export interface BulkLeadStageRequest {
|
||
leadIds: string[];
|
||
stageId: string;
|
||
}
|
||
|
||
export interface AutoAssignLeadsRequest {
|
||
rule?: LeadAssignRule;
|
||
leadIds?: string[];
|
||
assigneeIds?: string[];
|
||
onlyUnassigned?: boolean;
|
||
}
|
||
|
||
export interface LeadCallLogRequest {
|
||
outcome?: string;
|
||
body?: string;
|
||
nextFollowUpAt?: string | null;
|
||
stageId?: string;
|
||
}
|
||
|
||
export interface LeadNoteRequest {
|
||
body: string;
|
||
}
|
||
|
||
export interface LeadFollowUpRequest {
|
||
nextFollowUpAt: string;
|
||
body?: string;
|
||
}
|
||
|
||
export interface ConvertLeadRequest {
|
||
firstName?: string;
|
||
lastName?: string;
|
||
createIfMissing?: boolean;
|
||
}
|
||
|
||
export interface ConvertLeadResult {
|
||
lead: LeadDto;
|
||
personId: string;
|
||
createdPerson: boolean;
|
||
}
|
||
|
||
export interface LeadDuplicateCheckResult {
|
||
exists: boolean;
|
||
kind: "lead" | "person" | null;
|
||
id: string | null;
|
||
title: string | null;
|
||
}
|
||
|
||
export interface LeadImportJobDto {
|
||
id: string;
|
||
fileName: string | null;
|
||
status: string;
|
||
totalCount: number;
|
||
processedCount: number;
|
||
successCount: number;
|
||
duplicateCount: number;
|
||
errorCount: number;
|
||
errorSamples: string[];
|
||
startedAt: string | null;
|
||
finishedAt: string | null;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface LeadImportRow {
|
||
title?: string;
|
||
contactName?: string;
|
||
mobile?: string;
|
||
phone?: string;
|
||
city?: string;
|
||
region?: string;
|
||
address?: string;
|
||
instagram?: string;
|
||
registeredAt?: string;
|
||
trade?: string;
|
||
source?: string;
|
||
notes?: string;
|
||
}
|
||
|
||
export interface LeadStatsDto {
|
||
total: number;
|
||
newCount: number;
|
||
unassigned: number;
|
||
byAssignee: Array<{ userId: string; fullName: string; count: number }>;
|
||
callsLogged: number;
|
||
withFollowUp: number;
|
||
interested: number;
|
||
converted: number;
|
||
lost: number;
|
||
conversionRate: number;
|
||
}
|
||
|
||
export interface LeadBoardColumnDto {
|
||
stage: LeadStageDto;
|
||
/** تعداد واقعی لیدهای این مرحله، حتی اگر همهٔ کارتها برنگردد */
|
||
total: number;
|
||
leads: LeadDto[];
|
||
}
|
||
|
||
export interface ConversationSettingsDto {
|
||
chatHost: string;
|
||
uploadUrl: string;
|
||
downloadUrl: string;
|
||
apiKeySet: boolean;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface UpdateConversationSettingsRequest {
|
||
chatHost: string;
|
||
apiKey?: string;
|
||
uploadUrl?: string;
|
||
downloadUrl?: string;
|
||
}
|
||
|
||
export interface ConversationAccessDto {
|
||
canUse: boolean;
|
||
canCreateGroup: boolean;
|
||
/** ساخت کانال — فعلاً همان مجوز ساخت گروه */
|
||
canCreateChannel: boolean;
|
||
}
|
||
|
||
export interface ConversationSessionDto {
|
||
chatUserId: string;
|
||
wsUrl: string;
|
||
messagesBaseUrl: string;
|
||
uploadUrl: string;
|
||
downloadUrl: string;
|
||
}
|
||
|
||
export interface ConversationPeerDto {
|
||
id: string;
|
||
fullName: string;
|
||
hasAvatar: boolean;
|
||
chatUserId: string;
|
||
}
|
||
|
||
export interface ConversationRoomDto {
|
||
id: string;
|
||
externalRoomId: string;
|
||
kind: "dm" | "group" | "channel";
|
||
name: string;
|
||
unreadCount: number;
|
||
lastMessagePreview: string | null;
|
||
lastMessageAt: string | null;
|
||
peerUserId: string | null;
|
||
/** سنجاق در لیست گفتگو — فقط برای کاربر جاری */
|
||
pinned?: boolean;
|
||
/** بیصدا برای کاربر جاری */
|
||
muted?: boolean;
|
||
/** در حالت mute، منشن همچنان اعلان میدهد */
|
||
muteAllowMentions?: boolean;
|
||
/** در کانال: آیا کاربر جاری میتواند پیام بفرستد */
|
||
canPost?: boolean;
|
||
/** آیا کاربر جاری میتواند اعضا را مدیریت کند */
|
||
canManageMembers?: boolean;
|
||
}
|
||
|
||
export interface ConversationRoomMemberDto {
|
||
userId: string;
|
||
fullName: string;
|
||
role: "admin" | "member";
|
||
hasAvatar: boolean;
|
||
isMe?: boolean;
|
||
}
|
||
|
||
export interface ConversationPinnedMessageDto {
|
||
messageId: string;
|
||
preview: string;
|
||
pinnedByUserId: string;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface ConversationMessageReactionDto {
|
||
messageId: string;
|
||
emoji: string;
|
||
count: number;
|
||
me: boolean;
|
||
}
|
||
|
||
export interface CreateConversationDmRequest {
|
||
peerUserId: string;
|
||
}
|
||
|
||
export interface CreateConversationGroupRequest {
|
||
name: string;
|
||
memberUserIds: string[];
|
||
}
|
||
|
||
export interface PlatformConversationStatsDto {
|
||
organizationsWithChat: number;
|
||
totalConfiguredOrgs: number;
|
||
totalRoomsTracked: number;
|
||
}
|
||
|
||
/* ───────────── کارهای روتین ───────────── */
|
||
|
||
export type RoutineTaskStatus =
|
||
| "todo"
|
||
| "in_progress"
|
||
| "completed"
|
||
| "overdue"
|
||
| "skipped"
|
||
| "cancelled";
|
||
|
||
export const ROUTINE_TASK_STATUSES: RoutineTaskStatus[] = [
|
||
"todo",
|
||
"in_progress",
|
||
"completed",
|
||
"overdue",
|
||
"skipped",
|
||
"cancelled",
|
||
];
|
||
|
||
export const ROUTINE_TASK_STATUS_LABELS: Record<RoutineTaskStatus, string> = {
|
||
todo: "در انتظار",
|
||
in_progress: "در حال انجام",
|
||
completed: "انجام شد",
|
||
overdue: "عقبافتاده",
|
||
skipped: "رد شده",
|
||
cancelled: "لغو شده",
|
||
};
|
||
|
||
export type RoutineRecurrence =
|
||
| "daily"
|
||
| "weekdays"
|
||
| "weekly"
|
||
| "biweekly"
|
||
| "monthly"
|
||
| "every_n_days"
|
||
| "specific_dates";
|
||
|
||
export const ROUTINE_RECURRENCES: RoutineRecurrence[] = [
|
||
"daily",
|
||
"weekdays",
|
||
"weekly",
|
||
"biweekly",
|
||
"monthly",
|
||
"every_n_days",
|
||
"specific_dates",
|
||
];
|
||
|
||
export const ROUTINE_RECURRENCE_LABELS: Record<RoutineRecurrence, string> = {
|
||
daily: "هر روز",
|
||
weekdays: "روزهای مشخص هفته",
|
||
weekly: "هر هفته",
|
||
biweekly: "هر دو هفته",
|
||
monthly: "هر ماه",
|
||
every_n_days: "هر چند روز یکبار",
|
||
specific_dates: "تاریخهای مشخص",
|
||
};
|
||
|
||
/** ۰ = شنبه … ۶ = جمعه */
|
||
export const ROUTINE_WEEKDAY_LABELS = [
|
||
"شنبه",
|
||
"یکشنبه",
|
||
"دوشنبه",
|
||
"سهشنبه",
|
||
"چهارشنبه",
|
||
"پنجشنبه",
|
||
"جمعه",
|
||
];
|
||
|
||
export type RoutineTaskSource =
|
||
| "routine"
|
||
| "manual"
|
||
| "follow_up"
|
||
| "lead"
|
||
| "customer"
|
||
| "deal";
|
||
|
||
export const ROUTINE_TASK_SOURCE_LABELS: Record<RoutineTaskSource, string> = {
|
||
routine: "روتین",
|
||
manual: "دستی",
|
||
follow_up: "پیگیری",
|
||
lead: "لید",
|
||
customer: "مشتری",
|
||
deal: "معامله",
|
||
};
|
||
|
||
/** اتصال Task به پرونده: none = ندارد | optional = اختیاری | lead/customer = الزامی */
|
||
export type RoutineEntityLink = "none" | "optional" | "lead" | "customer";
|
||
|
||
export const ROUTINE_ENTITY_LINK_LABELS: Record<RoutineEntityLink, string> = {
|
||
none: "بدون اتصال",
|
||
optional: "اختیاری",
|
||
lead: "الزامی به لید",
|
||
customer: "الزامی به مشتری",
|
||
};
|
||
|
||
/** وضعیت یک روز: کامل | ناقص | از دست رفته | بدون Task */
|
||
export type RoutineDayState = "complete" | "partial" | "missed" | "empty";
|
||
|
||
export interface RoutineCategoryDto {
|
||
id: string;
|
||
name: string;
|
||
color: string;
|
||
sortOrder: number;
|
||
}
|
||
|
||
export interface SaveRoutineCategoryRequest {
|
||
name: string;
|
||
color?: string;
|
||
sortOrder?: number;
|
||
}
|
||
|
||
export type RoutineEntityType =
|
||
| "lead"
|
||
| "person"
|
||
| "company"
|
||
| "deal"
|
||
| "invoice"
|
||
| "activity";
|
||
|
||
export interface RoutineEntityRefDto {
|
||
type: RoutineEntityType;
|
||
id: string;
|
||
label: string;
|
||
}
|
||
|
||
export interface RoutineTaskRulesDto {
|
||
allowSkip: boolean;
|
||
skipReasonRequired: boolean;
|
||
reportRequired: boolean;
|
||
allowAfterDeadline: boolean;
|
||
entityLink: RoutineEntityLink;
|
||
}
|
||
|
||
export interface RoutineTaskDto {
|
||
id: string;
|
||
routineId: string | null;
|
||
title: string;
|
||
description: string | null;
|
||
status: RoutineTaskStatus;
|
||
priority: ActivityPriority;
|
||
category: RoutineCategoryDto | null;
|
||
/** روز برنامه (YYYY-MM-DD به وقت تهران) */
|
||
scheduledFor: string;
|
||
dueAt: string | null;
|
||
startedAt: string | null;
|
||
completedAt: string | null;
|
||
resultNote: string | null;
|
||
skipReason: string | null;
|
||
source: RoutineTaskSource;
|
||
isMandatory: boolean;
|
||
rules: RoutineTaskRulesDto;
|
||
assignee: UserSummaryDto | null;
|
||
createdBy: UserSummaryDto | null;
|
||
entities: RoutineEntityRefDto[];
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface RoutineTaskEventDto {
|
||
id: string;
|
||
type: "created" | "status_changed" | "rescheduled" | "note" | "reassigned" | "updated";
|
||
fromStatus: RoutineTaskStatus | null;
|
||
toStatus: RoutineTaskStatus | null;
|
||
note: string | null;
|
||
actor: UserSummaryDto | null;
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface RoutineTaskDetailDto extends RoutineTaskDto {
|
||
events: RoutineTaskEventDto[];
|
||
}
|
||
|
||
export interface RoutineDayStatsDto {
|
||
total: number;
|
||
completed: number;
|
||
remaining: number;
|
||
overdue: number;
|
||
/** null وقتی Task ندارد */
|
||
percent: number | null;
|
||
}
|
||
|
||
export interface RoutineScoreDto {
|
||
today: number | null;
|
||
yesterday: number | null;
|
||
week: number | null;
|
||
month: number | null;
|
||
/** روزهای متوالی با انجام همه Taskهای اجباری */
|
||
streak: number;
|
||
}
|
||
|
||
export interface RoutineTodayDto {
|
||
date: string;
|
||
tasks: RoutineTaskDto[];
|
||
overdue: RoutineTaskDto[];
|
||
stats: RoutineDayStatsDto;
|
||
score: RoutineScoreDto;
|
||
}
|
||
|
||
export interface CreateRoutineTaskRequest {
|
||
title: string;
|
||
description?: string | null;
|
||
/** ISO؛ اگر خالی باشد scheduledFor یا امروز */
|
||
dueAt?: string | null;
|
||
scheduledFor?: string | null;
|
||
priority?: ActivityPriority;
|
||
categoryId?: string | null;
|
||
assigneeId?: string | null;
|
||
isMandatory?: boolean;
|
||
source?: RoutineTaskSource;
|
||
leadId?: string | null;
|
||
personId?: string | null;
|
||
companyId?: string | null;
|
||
dealId?: string | null;
|
||
invoiceId?: string | null;
|
||
activityId?: string | null;
|
||
}
|
||
|
||
export type UpdateRoutineTaskRequest = Partial<
|
||
Omit<CreateRoutineTaskRequest, "source" | "activityId">
|
||
>;
|
||
|
||
export interface CompleteRoutineTaskRequest {
|
||
note?: string | null;
|
||
}
|
||
|
||
export interface SkipRoutineTaskRequest {
|
||
reason?: string | null;
|
||
}
|
||
|
||
export interface RescheduleRoutineTaskRequest {
|
||
dueAt: string;
|
||
}
|
||
|
||
export interface RoutineDto {
|
||
id: string;
|
||
title: string;
|
||
description: string | null;
|
||
assignee: UserSummaryDto | null;
|
||
team: { id: string; name: string } | null;
|
||
category: RoutineCategoryDto | null;
|
||
priority: ActivityPriority;
|
||
/** HH:mm */
|
||
timeOfDay: string;
|
||
durationMinutes: number | null;
|
||
startDate: string;
|
||
endDate: string | null;
|
||
recurrence: RoutineRecurrence;
|
||
weekdays: number[];
|
||
intervalDays: number | null;
|
||
monthDay: number | null;
|
||
specificDates: string[];
|
||
isMandatory: boolean;
|
||
allowSkip: boolean;
|
||
skipReasonRequired: boolean;
|
||
reportRequired: boolean;
|
||
allowAfterDeadline: boolean;
|
||
carryOverOverdue: boolean;
|
||
entityLink: RoutineEntityLink;
|
||
reminderMinutesBefore: number | null;
|
||
isActive: boolean;
|
||
recurrenceLabel: string;
|
||
/** درصد اجرا در ۳۰ روز اخیر */
|
||
executionRate: number | null;
|
||
executionTotal: number;
|
||
executionDone: number;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export interface SaveRoutineRequest {
|
||
title: string;
|
||
description?: string | null;
|
||
assigneeId?: string | null;
|
||
teamId?: string | null;
|
||
categoryId?: string | null;
|
||
priority?: ActivityPriority;
|
||
timeOfDay: string;
|
||
durationMinutes?: number | null;
|
||
startDate: string;
|
||
endDate?: string | null;
|
||
recurrence: RoutineRecurrence;
|
||
weekdays?: number[];
|
||
intervalDays?: number | null;
|
||
monthDay?: number | null;
|
||
specificDates?: string[];
|
||
isMandatory?: boolean;
|
||
allowSkip?: boolean;
|
||
skipReasonRequired?: boolean;
|
||
reportRequired?: boolean;
|
||
allowAfterDeadline?: boolean;
|
||
carryOverOverdue?: boolean;
|
||
entityLink?: RoutineEntityLink;
|
||
reminderMinutesBefore?: number | null;
|
||
isActive?: boolean;
|
||
}
|
||
|
||
export interface RoutineContextDto {
|
||
isManager: boolean;
|
||
users: UserSummaryDto[];
|
||
teams: Array<{ id: string; name: string }>;
|
||
categories: RoutineCategoryDto[];
|
||
}
|
||
|
||
export interface RoutineCalendarDayDto {
|
||
date: string;
|
||
total: number;
|
||
completed: number;
|
||
overdue: number;
|
||
percent: number | null;
|
||
state: RoutineDayState;
|
||
}
|
||
|
||
export interface RoutineCalendarDto {
|
||
days: RoutineCalendarDayDto[];
|
||
tasks: RoutineTaskDto[];
|
||
}
|
||
|
||
export interface RoutineTrendPointDto {
|
||
date: string;
|
||
total: number;
|
||
completed: number;
|
||
percent: number | null;
|
||
}
|
||
|
||
export interface RoutineCategoryStatDto {
|
||
categoryId: string | null;
|
||
name: string;
|
||
color: string;
|
||
total: number;
|
||
completed: number;
|
||
percent: number | null;
|
||
}
|
||
|
||
export interface RoutineStatusBreakdownDto {
|
||
completed: number;
|
||
notDone: number;
|
||
overdue: number;
|
||
skipped: number;
|
||
cancelled: number;
|
||
}
|
||
|
||
export interface RoutinePerformanceDto {
|
||
from: string;
|
||
to: string;
|
||
total: number;
|
||
completed: number;
|
||
notDone: number;
|
||
overdue: number;
|
||
percent: number | null;
|
||
avgCompletionMinutes: number | null;
|
||
completeDays: number;
|
||
partialDays: number;
|
||
missedDays: number;
|
||
streak: number;
|
||
status: RoutineStatusBreakdownDto;
|
||
trend: RoutineTrendPointDto[];
|
||
byCategory: RoutineCategoryStatDto[];
|
||
}
|
||
|
||
export interface RoutineMemberStatDto {
|
||
user: UserSummaryDto;
|
||
total: number;
|
||
completed: number;
|
||
overdue: number;
|
||
skipped: number;
|
||
percent: number | null;
|
||
streak: number;
|
||
avgCompletionMinutes: number | null;
|
||
}
|
||
|
||
export interface RoutineTeamPerformanceDto {
|
||
from: string;
|
||
to: string;
|
||
total: number;
|
||
completed: number;
|
||
overdue: number;
|
||
percent: number | null;
|
||
status: RoutineStatusBreakdownDto;
|
||
trend: RoutineTrendPointDto[];
|
||
byCategory: RoutineCategoryStatDto[];
|
||
members: RoutineMemberStatDto[];
|
||
}
|
||
|
||
export interface RoutinePeriodTotalsDto {
|
||
total: number;
|
||
completed: number;
|
||
notDone: number;
|
||
overdue: number;
|
||
percent: number | null;
|
||
completeDays: number;
|
||
partialDays: number;
|
||
missedDays: number;
|
||
avgPercent: number | null;
|
||
}
|
||
|
||
export interface RoutineWeeklyReportDto {
|
||
from: string;
|
||
to: string;
|
||
days: RoutineCalendarDayDto[];
|
||
totals: RoutinePeriodTotalsDto;
|
||
previous: RoutinePeriodTotalsDto;
|
||
}
|
||
|
||
export interface RoutineMonthlyReportDto {
|
||
from: string;
|
||
to: string;
|
||
totals: RoutinePeriodTotalsDto;
|
||
previous: RoutinePeriodTotalsDto;
|
||
status: RoutineStatusBreakdownDto;
|
||
days: RoutineCalendarDayDto[];
|
||
byUser: RoutineMemberStatDto[];
|
||
}
|
||
|
||
export interface RoutineExecutionRowDto {
|
||
routineId: string;
|
||
title: string;
|
||
assigneeLabel: string;
|
||
recurrenceLabel: string;
|
||
occurrences: number;
|
||
completed: number;
|
||
notDone: number;
|
||
skipped: number;
|
||
percent: number | null;
|
||
}
|
||
|
||
export interface RoutineHistoryDayDto {
|
||
date: string;
|
||
total: number;
|
||
completed: number;
|
||
overdue: number;
|
||
skipped: number;
|
||
cancelled: number;
|
||
percent: number | null;
|
||
state: RoutineDayState;
|
||
}
|
||
|
||
export interface RoutineStatsQuery {
|
||
from?: string;
|
||
to?: string;
|
||
userId?: string;
|
||
teamId?: string;
|
||
categoryId?: string;
|
||
source?: RoutineTaskSource;
|
||
status?: RoutineTaskStatus;
|
||
}
|
||
|
||
export interface RoutineUserSettingsDto {
|
||
/** چند دقیقه قبل از Deadline یادآوری شود؛ null = خاموش */
|
||
remindBeforeMinutes: number | null;
|
||
endOfDaySummary: boolean;
|
||
overdueAlert: boolean;
|
||
/** ساعت ارسال خلاصه پایان روز (۰ تا ۲۳) */
|
||
summaryHour: number;
|
||
}
|
||
|
||
/* ───────── دفتر مالی داخلی ───────── */
|
||
|
||
export type FinancialAccountType = "bank" | "cash" | "petty";
|
||
export type FinancialDocKind = "income" | "expense";
|
||
export type FinancialDocStatus = "open" | "partial" | "settled" | "void";
|
||
export type FinancialPayMethod = "cash" | "card_to_card" | "pos" | "transfer" | "other";
|
||
|
||
export interface FinancialAccountDto {
|
||
id: string;
|
||
name: string;
|
||
type: FinancialAccountType;
|
||
bankName: string | null;
|
||
accountRef: string | null;
|
||
responsibleName: string | null;
|
||
openingBalance: string;
|
||
openingDate: string;
|
||
notes: string | null;
|
||
isActive: boolean;
|
||
currency: string;
|
||
balance: string;
|
||
periodIn: string;
|
||
periodOut: string;
|
||
lastMovementAt: string | null;
|
||
}
|
||
|
||
export interface FinancialCategoryDto {
|
||
id: string;
|
||
name: string;
|
||
kind: FinancialDocKind;
|
||
isActive: boolean;
|
||
}
|
||
|
||
export interface FinancialDocumentDto {
|
||
id: string;
|
||
kind: FinancialDocKind;
|
||
title: string;
|
||
amount: string;
|
||
settledAmount: string;
|
||
remaining: string;
|
||
bookedAt: string;
|
||
dueAt: string | null;
|
||
categoryId: string | null;
|
||
categoryName: string | null;
|
||
personId: string | null;
|
||
partyName: string | null;
|
||
accountId: string | null;
|
||
accountName: string | null;
|
||
method: string | null;
|
||
status: FinancialDocStatus;
|
||
reference: string | null;
|
||
notes: string | null;
|
||
createdAt: string;
|
||
attachments?: Array<{ id: string; fileName: string }>;
|
||
}
|
||
|
||
export interface FinancialMovementDto {
|
||
id: string;
|
||
accountId: string;
|
||
direction: "in" | "out";
|
||
amount: string;
|
||
kind: string;
|
||
kindLabel: string;
|
||
transferGroupId: string | null;
|
||
documentId: string | null;
|
||
occurredAt: string;
|
||
memo: string | null;
|
||
reference: string | null;
|
||
balanceAfter: string | null;
|
||
}
|
||
|
||
export type FinancialInstallmentStatus = "open" | "partial" | "settled" | "overdue";
|
||
|
||
export interface FinancialInstallmentDto {
|
||
id: string;
|
||
position: number;
|
||
dueDate: string;
|
||
amount: string;
|
||
paidAmount: string;
|
||
remaining: string;
|
||
status: FinancialInstallmentStatus;
|
||
}
|
||
|
||
export interface FinancialReceivableDto {
|
||
id: string;
|
||
title: string;
|
||
personId: string | null;
|
||
partyName: string | null;
|
||
totalAmount: string;
|
||
paidAmount: string;
|
||
remaining: string;
|
||
status: "open" | "settled" | "void";
|
||
notes: string | null;
|
||
installments: FinancialInstallmentDto[];
|
||
createdAt: string;
|
||
}
|
||
|
||
export interface TreasuryDashboardDto {
|
||
bookedIncome: string;
|
||
actualReceipts: string;
|
||
bookedExpense: string;
|
||
actualPayments: string;
|
||
operatingProfit: string;
|
||
receivables: string;
|
||
payables: string;
|
||
totalBalance: string;
|
||
recent: FinancialDocumentDto[];
|
||
upcomingInstallments: FinancialInstallmentDto[];
|
||
upcomingPayables: FinancialDocumentDto[];
|
||
}
|
||
|
||
/* ───────── کیفپول پیامک (صورتحساب محیط) ───────── */
|
||
|
||
export type SmsBalanceLevel = "healthy" | "warning" | "critical" | "unknown";
|
||
export type SmsPanelMode = "platform" | "own";
|
||
|
||
export interface SmsWalletSummaryDto {
|
||
billingEnabled: boolean;
|
||
balanceRials: number;
|
||
pricePerPartRials: number;
|
||
isBlocked: boolean;
|
||
autoBlocked: boolean;
|
||
balanceLevel: SmsBalanceLevel;
|
||
panelMode: SmsPanelMode | string;
|
||
chargeInfo: SmsChargeInfoDto | null;
|
||
}
|
||
|
||
export interface SmsChargeInfoDto {
|
||
card: string;
|
||
iban: string;
|
||
accountHolder: string;
|
||
instruction: string;
|
||
}
|
||
|
||
export interface PlatformSmsTenantRowDto {
|
||
id: string;
|
||
name: string;
|
||
slug: string;
|
||
isActive: boolean;
|
||
isBlocked: boolean;
|
||
panelMode: string;
|
||
sms: {
|
||
balanceRials: number;
|
||
pricePerPartRials: number;
|
||
autoBlockBelowRials: number | null;
|
||
balanceLevel: SmsBalanceLevel;
|
||
billingEnabled: boolean;
|
||
};
|
||
}
|
||
|
||
export interface PlatformSmsOverviewDto {
|
||
organization: {
|
||
id: string;
|
||
name: string;
|
||
slug: string;
|
||
isActive: boolean;
|
||
};
|
||
account: {
|
||
id: string;
|
||
organizationId: string;
|
||
balanceRials: number;
|
||
pricePerPartRials: number;
|
||
isBlocked: boolean;
|
||
autoBlockBelowRials: number | null;
|
||
alertCriticalRials: number;
|
||
alertWarningRials: number;
|
||
panelMode: SmsPanelMode;
|
||
ownPanelType: string;
|
||
ownPanelUsername: string | null;
|
||
ownPanelFrom: string | null;
|
||
hasOwnPanelPassword: boolean;
|
||
balanceLevel: SmsBalanceLevel;
|
||
billingEnabled: boolean;
|
||
};
|
||
chargeInfo: SmsChargeInfoDto;
|
||
today: { messages: number; parts: number; amountRials: number };
|
||
month: { messages: number; parts: number; amountRials: number };
|
||
ledgers: Array<{
|
||
id: string;
|
||
type: string;
|
||
amountRials: number;
|
||
balanceAfterRials: number;
|
||
parts: number | null;
|
||
note: string | null;
|
||
createdAt: string;
|
||
}>;
|
||
sendLogs: Array<{
|
||
id: string;
|
||
receiver: string;
|
||
body: string;
|
||
parts: number;
|
||
totalAmountRials: number;
|
||
provider: string | null;
|
||
source: string | null;
|
||
isAlert: boolean;
|
||
createdAt: string;
|
||
}>;
|
||
}
|
||
|
||
/* ───────── باشگاه امتیازات (گیمیفیکیشن کارکنان) ───────── */
|
||
|
||
export * from "./gamification";
|
||
export * from "./behavior";
|