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 = { 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 = { trial: "دمو ۷ روزه", monthly: "ماهانه", quarterly: "سه‌ماهه", semiannual: "شش‌ماهه", yearly: "یکساله", lifetime: "دائمی", custom: "سفارشی", }; export const BILLING_TRIAL_DAYS = 7; /** مدت پیش‌فرض پلن‌ها (روز) — custom ندارد؛ lifetime عملاً بدون انقضا (۱۰۰ سال) */ export const BILLING_PLAN_DAYS: Record, 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, 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 = { 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 = { 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 = { contacts: "مشتریان (اشخاص و شرکت‌ها)", deals: "معاملات و اهداف فروش", activities: "فعالیت‌ها و کارها", leads: "لیدها", finance: "فاکتور، پیش‌فاکتور و چک", tickets: "تیکت‌ها", notes: "یادداشت‌ها", projects: "پروژه‌ها", routines: "کارهای روتین", }; export type DataVisibilityScopes = Partial>; export interface UpdateUserVisibilityRequest { /** سطح پیش‌فرض برای بخش‌هایی که جداگانه تنظیم نشده‌اند */ dataVisibility?: DataVisibility; /** سطح هر بخش؛ مقدار null یعنی «مثل پیش‌فرض» */ scopes?: Partial>; } /* ——— 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 = { customers: "مشتریان (اشخاص و شرکت‌ها)", leads: "لیدها", sales: "فروش و معاملات", activities: "فعالیت‌ها", notes: "یادداشت‌ها", routines: "کارهای روتین", projects: "پروژه‌ها", tickets: "تیکت‌ها", }; /** نگاشت بخش عمق‌داده ← کلید قدیمی DataScope برای مهاجرت */ export const DATA_ACCESS_LEGACY_SCOPE: Record = { 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>; 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; 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; 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; 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; 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 = { 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; 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; ownerId?: string | null; /** همهٔ گروه‌های شرکت (جایگزین کامل) */ groupIds?: string[]; } export type UpdateCompanyRequest = Partial; 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 = { 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 = { 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; 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; ownerId?: string | null; /** فقط برای ایمپورت اکسل — تاریخ ثبت مبدأ */ registeredAt?: string | null; /** همهٔ گروه‌های شخص (جایگزین کامل؛ منبع گروه‌های ته‌حسابیِ باقی‌مانده حفظ می‌شود) */ groupIds?: string[]; } export type UpdatePersonRequest = Partial; export interface PaginatedResponse { 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; 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; 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; 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; 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 = { ...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 = { green: "سبز", orange: "نارنجی", purple: "بنفش", blue: "آبی", rose: "قرمز", slate: "خاکستری", }; /** نوع مرحله = وضعیتی که فعالیت با رفتن به آن ستون می‌گیرد */ export const ACTIVITY_STAGE_KIND_LABELS: Record = { 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; total: number; } /** برچسب نوع پایه (برای گزارش‌ها و فعالیت‌های بدون نوع سفارشی) */ export const ACTIVITY_TYPE_LABELS: Record = { 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 = { 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; 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; 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 = { 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; } 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; } 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; 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; personId: string | null; leadId: string | null; status: string; utm: Record | 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; 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 = { 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; 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; 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; 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; 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 = { 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 = { 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 = { owner: "مسئول پیش‌فرض (مسئول پرونده)", user: "کاربر مشخص", team_round_robin: "نوبتی بین اعضای یک تیم", users_round_robin: "نوبتی بین کاربران انتخابی", }; export const AUTOMATION_CHAIN_MODE_LABELS: Record = { 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): 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; 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 { 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, ): 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 = { 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; 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 = { sale: "فروش", return: "مرجوعی", purchase: "خرید", other: "سایر", }; export const TAHESAB_JENS_FELEZ_LABELS: Record = { 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; 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; isShared: boolean; owner: UserSummaryDto | null; createdAt: string; updatedAt: string; } export interface CreateSavedViewRequest { name: string; scope: SavedViewScope; filters: Record; isShared?: boolean; } export type UpdateSavedViewRequest = Partial; /* ——— 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; 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 = { 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; 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 = { 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; 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 = { 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 = { 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 = { 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 = { 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 >; 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";