Files
CRM_Montzar/packages/shared/src/index.ts
T
MahdiandCursor 81edf61065 feat(notes): let organizations edit the note category bar
The filter chips were fixed labels. Owners can rename, hide, reorder, and add categories.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-01 16:34:34 +03:30

6460 lines
162 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 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";