merge: bring latest origin/main into dev (keep local UI)
Resolved UI conflicts in favor of local Ant Design screens; kept non-UI changes from main.
This commit is contained in:
@@ -1,3 +1,15 @@
|
||||
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"];
|
||||
@@ -42,6 +54,8 @@ export interface AuthUserDto {
|
||||
enabledModules?: OrgModuleKey[];
|
||||
/** خلاصه اشتراک مالک محیط (برای اعضا فقط جهت نمایش/گیت ورود) */
|
||||
subscription?: SubscriptionSummaryDto | null;
|
||||
/** حجم فایل اشتراک مالک همین محیط */
|
||||
storage?: StorageUsageDto | null;
|
||||
/** اگر باشد، این کاربر سوئیچ «بایگانی اطلاعات قبل» را دارد */
|
||||
priorArchive?: { lens: "current" | "archive"; cutoffAt: string | null } | null;
|
||||
/**
|
||||
@@ -226,7 +240,7 @@ export const ORG_MODULE_LABELS: Record<OrgModuleKey, string> = {
|
||||
loyalty: "باشگاه مشتریان",
|
||||
reports: "گزارشها",
|
||||
notes: "یادداشتها",
|
||||
tahesab: "تهحساب",
|
||||
tahesab: "تهحساب (اتصال و گزارش فروش)",
|
||||
custom_fields: "فیلدهای سفارشی",
|
||||
currencies: "ارزها",
|
||||
team_settings: "تیم و تنظیمات سازمان",
|
||||
@@ -321,6 +335,34 @@ export function defaultEnabledModules(): OrgModuleKey[] {
|
||||
return ORG_MODULE_KEYS.filter((k) => !OPT_IN_ORG_MODULES.includes(k));
|
||||
}
|
||||
|
||||
export type StorageUsageLevel = "ok" | "warn" | "full" | "unlimited";
|
||||
|
||||
/** حجم فایل اشتراک مالک. quotaBytes خالی یعنی سقفی گذاشته نشده. */
|
||||
export interface StorageUsageDto {
|
||||
usedBytes: number;
|
||||
quotaBytes: number | null;
|
||||
percent: number | null;
|
||||
level: StorageUsageLevel;
|
||||
byOrganization?: Array<{ id: string; name: string; usedBytes: number }>;
|
||||
}
|
||||
|
||||
export interface SetStorageQuotaRequest {
|
||||
/** گیگابایت؛ null یعنی بدون سقف */
|
||||
quotaGb: number | null;
|
||||
}
|
||||
|
||||
/** سقفهای آمادهٔ تعداد کاربر. بالاتر از ۲۰ توافقی و با عدد جدا ذخیره میشود. */
|
||||
export const BILLING_USER_CAPS = [3, 10, 20] as const;
|
||||
|
||||
export interface OwnerSeatUsageDto {
|
||||
usedUsers: number;
|
||||
maxUsers: number | null;
|
||||
}
|
||||
|
||||
export interface SetUserCapRequest {
|
||||
maxUsers: number;
|
||||
}
|
||||
|
||||
export interface SubscriptionSummaryDto {
|
||||
planCode: BillingPlanCode;
|
||||
status: BillingAccountStatus;
|
||||
@@ -339,10 +381,13 @@ export interface BillingAccountDto {
|
||||
startsAt: string;
|
||||
expiresAt: string;
|
||||
maxOrganizations: number;
|
||||
/** null یعنی سقف کاربر هنوز تعیین نشده */
|
||||
maxUsers?: number | null;
|
||||
externalPaymentRef?: string | null;
|
||||
notes?: string | null;
|
||||
daysRemaining: number;
|
||||
warn: boolean;
|
||||
storage?: StorageUsageDto | null;
|
||||
}
|
||||
|
||||
export interface ApplyBillingPlanRequest {
|
||||
@@ -374,13 +419,33 @@ export interface OrgUserDto {
|
||||
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"];
|
||||
@@ -419,7 +484,7 @@ export const DATA_SCOPE_LABELS: Record<DataScope, string> = {
|
||||
contacts: "مشتریان (اشخاص و شرکتها)",
|
||||
deals: "معاملات و اهداف فروش",
|
||||
activities: "فعالیتها و کارها",
|
||||
leads: "سرنخها",
|
||||
leads: "لیدها",
|
||||
finance: "فاکتور، پیشفاکتور و چک",
|
||||
tickets: "تیکتها",
|
||||
notes: "یادداشتها",
|
||||
@@ -436,12 +501,240 @@ export interface UpdateUserVisibilityRequest {
|
||||
scopes?: Partial<Record<DataScope, DataVisibility | null>>;
|
||||
}
|
||||
|
||||
/* ——— Deep data access (customers + leads + sales + activities + notes/routines/projects/tickets) ——— */
|
||||
|
||||
export type DataAccessOwnerKind = "self" | "team";
|
||||
|
||||
export type DataAccessMode = "all" | "rules";
|
||||
|
||||
/**
|
||||
* بخشهای عمق داده.
|
||||
* customers/leads/sales/activities/notes ≈ RolePermissionKey؛
|
||||
* routines/projects/tickets ≈ ماژول / DataScope (بدون مجوز نقش جدا).
|
||||
*/
|
||||
export type DataAccessSection =
|
||||
| "customers"
|
||||
| "leads"
|
||||
| "sales"
|
||||
| "activities"
|
||||
| "notes"
|
||||
| "routines"
|
||||
| "projects"
|
||||
| "tickets";
|
||||
|
||||
export const DATA_ACCESS_SECTIONS: DataAccessSection[] = [
|
||||
"customers",
|
||||
"leads",
|
||||
"sales",
|
||||
"activities",
|
||||
"notes",
|
||||
"routines",
|
||||
"projects",
|
||||
"tickets",
|
||||
];
|
||||
|
||||
export const DATA_ACCESS_SECTION_LABELS: Record<DataAccessSection, string> = {
|
||||
customers: "مشتریان (اشخاص و شرکتها)",
|
||||
leads: "لیدها",
|
||||
sales: "فروش و معاملات",
|
||||
activities: "فعالیتها",
|
||||
notes: "یادداشتها",
|
||||
routines: "کارهای روتین",
|
||||
projects: "پروژهها",
|
||||
tickets: "تیکتها",
|
||||
};
|
||||
|
||||
/** نگاشت بخش عمقداده ← کلید قدیمی DataScope برای مهاجرت */
|
||||
export const DATA_ACCESS_LEGACY_SCOPE: Record<DataAccessSection, DataScope | null> = {
|
||||
customers: "contacts",
|
||||
leads: "leads",
|
||||
sales: "deals",
|
||||
activities: "activities",
|
||||
notes: "notes",
|
||||
routines: "routines",
|
||||
projects: "projects",
|
||||
tickets: "tickets",
|
||||
};
|
||||
|
||||
/**
|
||||
* یک قانون: فیلدهای پرشده با AND.
|
||||
* قوانین متعدد در SectionDataAccess با OR ترکیب میشوند.
|
||||
* ابعاد بلااستفاده برای یک بخش نادیده گرفته میشوند.
|
||||
*/
|
||||
export type DataAccessRule = {
|
||||
/** self / team → مالک / مسئول / نویسنده (بسته به بخش) */
|
||||
owners?: DataAccessOwnerKind[];
|
||||
/** مشتریان: گروهها */
|
||||
groupIds?: string[];
|
||||
/** مشتریان: importSource/sourceRef · لیدها: source · یادداشت: noteType · تیکت: status */
|
||||
sources?: string[];
|
||||
/** مشتریان: حال */
|
||||
moods?: PersonMood[];
|
||||
/** لید / معامله / فعالیت / پروژه: مرحله */
|
||||
stageIds?: string[];
|
||||
/** معامله: پایپلاین */
|
||||
pipelineIds?: string[];
|
||||
/** فعالیت / پروژه: بورد */
|
||||
boardIds?: string[];
|
||||
/** فعالیت: typeDefId · روتین: categoryId */
|
||||
typeIds?: string[];
|
||||
};
|
||||
|
||||
/** @deprecated نام قبلی؛ از DataAccessRule استفاده کنید */
|
||||
export type ContactAccessRule = DataAccessRule;
|
||||
|
||||
export type SectionDataAccess = {
|
||||
mode: DataAccessMode;
|
||||
rules?: DataAccessRule[];
|
||||
};
|
||||
|
||||
export type DataAccessPolicy = Partial<Record<DataAccessSection, SectionDataAccess>>;
|
||||
|
||||
export interface UpdateUserDataAccessRequest {
|
||||
/**
|
||||
* null یعنی حذف override و برگشت به ارث از نقش.
|
||||
* آبجکت یعنی جایگزینی کامل override (کلید غایب در آبجکت = آن بخش از override پاک میشود).
|
||||
*/
|
||||
dataAccessOverride?: DataAccessPolicy | null;
|
||||
}
|
||||
|
||||
export function emptySectionDataAccess(): SectionDataAccess {
|
||||
return { mode: "all", rules: [] };
|
||||
}
|
||||
|
||||
export function parseDataAccessPolicy(raw: unknown): DataAccessPolicy {
|
||||
const out: DataAccessPolicy = {};
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
|
||||
const obj = raw as Record<string, unknown>;
|
||||
for (const section of DATA_ACCESS_SECTIONS) {
|
||||
const parsed = parseSectionDataAccess(obj[section]);
|
||||
if (parsed) out[section] = parsed;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function parseSectionDataAccess(raw: unknown): SectionDataAccess | null {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||
const obj = raw as Record<string, unknown>;
|
||||
const mode: DataAccessMode = obj.mode === "rules" ? "rules" : "all";
|
||||
const rulesRaw = Array.isArray(obj.rules) ? obj.rules : [];
|
||||
const rules: DataAccessRule[] = [];
|
||||
for (const item of rulesRaw) {
|
||||
const rule = parseDataAccessRule(item);
|
||||
if (rule) rules.push(rule);
|
||||
}
|
||||
if (mode === "all") return { mode: "all", rules: [] };
|
||||
return { mode: "rules", rules };
|
||||
}
|
||||
|
||||
function parseStringIdList(raw: unknown): string[] | undefined {
|
||||
if (!Array.isArray(raw)) return undefined;
|
||||
const list = raw
|
||||
.filter((v): v is string => typeof v === "string" && v.trim().length > 0)
|
||||
.map((v) => v.trim());
|
||||
return list.length ? [...new Set(list)] : undefined;
|
||||
}
|
||||
|
||||
export function parseDataAccessRule(raw: unknown): DataAccessRule | null {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||
const obj = raw as Record<string, unknown>;
|
||||
const rule: DataAccessRule = {};
|
||||
if (Array.isArray(obj.owners)) {
|
||||
const owners = obj.owners.filter(
|
||||
(v): v is DataAccessOwnerKind => v === "self" || v === "team",
|
||||
);
|
||||
if (owners.length) rule.owners = [...new Set(owners)];
|
||||
}
|
||||
const groupIds = parseStringIdList(obj.groupIds);
|
||||
if (groupIds) rule.groupIds = groupIds;
|
||||
const sources = parseStringIdList(obj.sources);
|
||||
if (sources) rule.sources = sources;
|
||||
if (Array.isArray(obj.moods)) {
|
||||
const moods = obj.moods.filter(
|
||||
(v): v is PersonMood =>
|
||||
typeof v === "string" && (PERSON_MOODS as readonly string[]).includes(v),
|
||||
);
|
||||
if (moods.length) rule.moods = [...new Set(moods)];
|
||||
}
|
||||
const stageIds = parseStringIdList(obj.stageIds);
|
||||
if (stageIds) rule.stageIds = stageIds;
|
||||
const pipelineIds = parseStringIdList(obj.pipelineIds);
|
||||
if (pipelineIds) rule.pipelineIds = pipelineIds;
|
||||
const boardIds = parseStringIdList(obj.boardIds);
|
||||
if (boardIds) rule.boardIds = boardIds;
|
||||
const typeIds = parseStringIdList(obj.typeIds);
|
||||
if (typeIds) rule.typeIds = typeIds;
|
||||
|
||||
if (
|
||||
!rule.owners &&
|
||||
!rule.groupIds &&
|
||||
!rule.sources &&
|
||||
!rule.moods &&
|
||||
!rule.stageIds &&
|
||||
!rule.pipelineIds &&
|
||||
!rule.boardIds &&
|
||||
!rule.typeIds
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export const parseContactAccessRule = parseDataAccessRule;
|
||||
|
||||
/** آیا این بخش محدودیت عمقی دارد؟ mode=all یا بدون قانون معتبر = بدون محدودیت */
|
||||
export function sectionHasDataAccessLimits(section: SectionDataAccess | null | undefined): boolean {
|
||||
if (!section || section.mode !== "rules") return false;
|
||||
return Boolean(section.rules?.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* مهاجرت از مدل قدیمی own/team/org به SectionDataAccess.
|
||||
* اگر scopeKey داده شود، سطح تفکیکی همان بخش اولویت دارد.
|
||||
*/
|
||||
export function migrateVisibilityToSectionAccess(
|
||||
dataVisibility: DataVisibility | string | null | undefined,
|
||||
scopes?: DataVisibilityScopes | null,
|
||||
scopeKey?: DataScope | null,
|
||||
): SectionDataAccess {
|
||||
const scoped =
|
||||
scopeKey && scopes
|
||||
? scopes[scopeKey]
|
||||
: undefined;
|
||||
const level =
|
||||
scoped === "own" || scoped === "team" || scoped === "org"
|
||||
? scoped
|
||||
: dataVisibility === "team" || dataVisibility === "org" || dataVisibility === "own"
|
||||
? dataVisibility
|
||||
: "own";
|
||||
if (level === "org") return { mode: "all", rules: [] };
|
||||
if (level === "team") return { mode: "rules", rules: [{ owners: ["self", "team"] }] };
|
||||
return { mode: "rules", rules: [{ owners: ["self"] }] };
|
||||
}
|
||||
|
||||
/** @deprecated از migrateVisibilityToSectionAccess استفاده کنید */
|
||||
export function migrateVisibilityToCustomersAccess(
|
||||
dataVisibility: DataVisibility | string | null | undefined,
|
||||
scopes?: DataVisibilityScopes | null,
|
||||
): SectionDataAccess {
|
||||
return migrateVisibilityToSectionAccess(dataVisibility, scopes, "contacts");
|
||||
}
|
||||
|
||||
export type RolePermissionKey =
|
||||
| "customers"
|
||||
| "assign_customers"
|
||||
| "leads"
|
||||
| "assign_leads"
|
||||
| "sales"
|
||||
| "assign_sales"
|
||||
| "billing"
|
||||
| "products"
|
||||
| "activities"
|
||||
| "assign_activities"
|
||||
| "assign_routines"
|
||||
| "assign_projects"
|
||||
| "assign_tickets"
|
||||
| "notes"
|
||||
| "reports"
|
||||
| "audit"
|
||||
@@ -451,15 +744,43 @@ export type RolePermissionKey =
|
||||
| "treasury_payroll"
|
||||
| "behavior";
|
||||
|
||||
/** بخشی که مسئولش را میشود به کاربر دیگری داد */
|
||||
export type AssignSection =
|
||||
| "customers"
|
||||
| "leads"
|
||||
| "sales"
|
||||
| "activities"
|
||||
| "routines"
|
||||
| "projects"
|
||||
| "tickets";
|
||||
|
||||
export const ASSIGN_SECTION_PERMISSION: Record<AssignSection, RolePermissionKey> = {
|
||||
customers: "assign_customers",
|
||||
leads: "assign_leads",
|
||||
sales: "assign_sales",
|
||||
activities: "assign_activities",
|
||||
routines: "assign_routines",
|
||||
projects: "assign_projects",
|
||||
tickets: "assign_tickets",
|
||||
};
|
||||
|
||||
export const ROLE_PERMISSION_ITEMS: Array<{
|
||||
key: RolePermissionKey;
|
||||
label: string;
|
||||
}> = [
|
||||
{ key: "customers", label: "مشتریان" },
|
||||
{ key: "assign_customers", label: "تخصیص مشتری به دیگران" },
|
||||
{ key: "leads", label: "لیدها" },
|
||||
{ key: "assign_leads", label: "تخصیص لید به دیگران" },
|
||||
{ key: "sales", label: "فروش و معاملات" },
|
||||
{ key: "assign_sales", label: "تخصیص معامله به دیگران" },
|
||||
{ key: "billing", label: "پیشفاکتورها و فاکتورها" },
|
||||
{ key: "products", label: "محصولات و خدمات" },
|
||||
{ key: "activities", label: "فعالیتها" },
|
||||
{ key: "assign_activities", label: "تخصیص فعالیت به دیگران" },
|
||||
{ key: "assign_routines", label: "تخصیص کار روتین به دیگران" },
|
||||
{ key: "assign_projects", label: "تخصیص کارت پروژه به دیگران" },
|
||||
{ key: "assign_tickets", label: "تخصیص تیکت به دیگران" },
|
||||
{ key: "notes", label: "یادداشتها" },
|
||||
{ key: "reports", label: "گزارشها" },
|
||||
{ key: "audit", label: "لاگ فعالیت کاربران" },
|
||||
@@ -482,6 +803,8 @@ export interface OrgRoleDto {
|
||||
description: string;
|
||||
color: string;
|
||||
permissions: RolePermissionKey[];
|
||||
/** پیشفرض عمق داده برای اعضای این نقش */
|
||||
dataAccessPolicy?: DataAccessPolicy;
|
||||
systemKey: UserRole | null;
|
||||
/** نقش والد؛ null یعنی ریشه */
|
||||
parentId: string | null;
|
||||
@@ -500,6 +823,7 @@ export interface CreateOrgRoleRequest {
|
||||
description?: string;
|
||||
color?: string;
|
||||
permissions?: RolePermissionKey[];
|
||||
dataAccessPolicy?: DataAccessPolicy;
|
||||
/** اگر ست شود، این نقش زیرمجموعهٔ نقش والد خواهد بود */
|
||||
parentId?: string | null;
|
||||
}
|
||||
@@ -509,6 +833,7 @@ export interface UpdateOrgRoleRequest {
|
||||
description?: string;
|
||||
color?: string;
|
||||
permissions?: RolePermissionKey[];
|
||||
dataAccessPolicy?: DataAccessPolicy;
|
||||
/** null یعنی جدا کردن از والد و تبدیل به ریشه */
|
||||
parentId?: string | null;
|
||||
}
|
||||
@@ -770,6 +1095,53 @@ export interface HealthResponse {
|
||||
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;
|
||||
@@ -803,6 +1175,7 @@ export interface CompanyDto {
|
||||
registeredAt: string | null;
|
||||
tags: string[];
|
||||
customFields: Record<string, unknown>;
|
||||
groups: CustomerGroupSummaryDto[];
|
||||
peopleCount: number;
|
||||
owner: UserSummaryDto | null;
|
||||
createdAt: string;
|
||||
@@ -833,6 +1206,8 @@ export interface CreateCompanyRequest {
|
||||
tags?: string[];
|
||||
customFields?: Record<string, unknown>;
|
||||
ownerId?: string | null;
|
||||
/** همهٔ گروههای شرکت (جایگزین کامل) */
|
||||
groupIds?: string[];
|
||||
}
|
||||
|
||||
export type UpdateCompanyRequest = Partial<CreateCompanyRequest>;
|
||||
@@ -897,6 +1272,8 @@ export interface PersonDto {
|
||||
previousImportSource: PersonImportSource | string | null;
|
||||
previousSourceRef: string | null;
|
||||
fullName: string;
|
||||
gender: PersonGender;
|
||||
showGenderInName: boolean;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
mobile: string | null;
|
||||
@@ -910,6 +1287,7 @@ export interface PersonDto {
|
||||
mood: PersonMood;
|
||||
tags: string[];
|
||||
customFields: Record<string, unknown>;
|
||||
groups: CustomerGroupSummaryDto[];
|
||||
loyaltyPoints: number;
|
||||
loyaltyTier: LoyaltyTierSummaryDto | null;
|
||||
/** تیپ غالب یا ترکیبی تأییدشده؛ بدون مجوز رفتار خالی برمیگردد */
|
||||
@@ -925,6 +1303,8 @@ export interface PersonDto {
|
||||
export interface CreatePersonRequest {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
gender?: PersonGender;
|
||||
showGenderInName?: boolean;
|
||||
previousFirstName?: string | null;
|
||||
previousLastName?: string | null;
|
||||
importSource?: PersonImportSource | string;
|
||||
@@ -946,6 +1326,8 @@ export interface CreatePersonRequest {
|
||||
ownerId?: string | null;
|
||||
/** فقط برای ایمپورت اکسل — تاریخ ثبت مبدأ */
|
||||
registeredAt?: string | null;
|
||||
/** همهٔ گروههای شخص (جایگزین کامل؛ منبع گروههای تهحسابیِ باقیمانده حفظ میشود) */
|
||||
groupIds?: string[];
|
||||
}
|
||||
|
||||
export type UpdatePersonRequest = Partial<CreatePersonRequest>;
|
||||
@@ -1285,14 +1667,18 @@ export interface ActivityBoardDto {
|
||||
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 {
|
||||
@@ -1478,7 +1864,7 @@ export interface ReorderActivityTypesRequest {
|
||||
typeIds: string[];
|
||||
}
|
||||
|
||||
export type CustomFieldEntity = "person" | "company" | "deal";
|
||||
export type CustomFieldEntity = "person" | "company" | "deal" | "note";
|
||||
export type CustomFieldType = "text" | "number" | "date" | "boolean" | "select";
|
||||
|
||||
export interface CustomFieldDefinitionDto {
|
||||
@@ -1520,6 +1906,14 @@ export interface DealSummaryDto {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface ActivityImageDto {
|
||||
id: string;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
sizeBytes: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ActivityDto {
|
||||
id: string;
|
||||
type: ActivityType;
|
||||
@@ -1527,6 +1921,8 @@ export interface ActivityDto {
|
||||
priority: ActivityPriority;
|
||||
title: string;
|
||||
description: string | null;
|
||||
/** عکسهای یادداشت؛ فقط در جزئیات فعالیت پر میشود */
|
||||
images?: ActivityImageDto[];
|
||||
dueAt: string | null;
|
||||
durationMinutes: number | null;
|
||||
reminderAt: string | null;
|
||||
@@ -1586,6 +1982,8 @@ export interface CreateActivityRequest {
|
||||
personId?: string | null;
|
||||
companyId?: string | null;
|
||||
dealId?: string | null;
|
||||
/** لید مرتبط؛ فعالیت در پروندهٔ همان لید میماند */
|
||||
leadId?: string | null;
|
||||
/** سازگاری — اگر assigneeIds نباشد از این استفاده میشود */
|
||||
assigneeId?: string | null;
|
||||
/** چند مسئول؛ اولی بهعنوان assigneeId ذخیره میشود */
|
||||
@@ -1611,6 +2009,7 @@ export interface UpdateActivityRequest {
|
||||
personId?: string | null;
|
||||
companyId?: string | null;
|
||||
dealId?: string | null;
|
||||
leadId?: string | null;
|
||||
assigneeId?: string | null;
|
||||
assigneeIds?: string[];
|
||||
}
|
||||
@@ -1663,6 +2062,7 @@ export interface NoteDto {
|
||||
checklist: NoteChecklistItemDto[];
|
||||
locationText: string | null;
|
||||
analyzeEnabled: boolean;
|
||||
customFields: Record<string, unknown>;
|
||||
person: PersonSummaryDto | null;
|
||||
company: CompanySummaryDto | null;
|
||||
deal: DealSummaryDto | null;
|
||||
@@ -1684,6 +2084,43 @@ export const NOTE_VISIBILITY_LABELS: Record<NoteVisibility, string> = {
|
||||
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;
|
||||
@@ -1712,6 +2149,7 @@ export interface CreateNoteRequest {
|
||||
companyId?: string | null;
|
||||
dealId?: string | null;
|
||||
activityId?: string | null;
|
||||
customFields?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface UpdateNoteRequest {
|
||||
@@ -1730,6 +2168,7 @@ export interface UpdateNoteRequest {
|
||||
companyId?: string | null;
|
||||
dealId?: string | null;
|
||||
activityId?: string | null;
|
||||
customFields?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type NoteAiSuggestionStatus =
|
||||
@@ -2106,6 +2545,8 @@ export type LeadFormMapTo =
|
||||
| "lead.trade"
|
||||
| "lead.notes"
|
||||
| "lead.source"
|
||||
| "lead.gender"
|
||||
| "person.gender"
|
||||
| "notes"
|
||||
| "custom";
|
||||
|
||||
@@ -3809,6 +4250,224 @@ export interface PersonAccountingLinkDto {
|
||||
isDefaultConnection?: boolean;
|
||||
}
|
||||
|
||||
/** طبقهبندی تقریبی نوع سند از روی متن NO تهحساب */
|
||||
export const TAHESAB_ASNAD_KINDS = [
|
||||
"sale",
|
||||
"return",
|
||||
"purchase",
|
||||
"other",
|
||||
] as const;
|
||||
export type TahesabAsnadKind = (typeof TAHESAB_ASNAD_KINDS)[number];
|
||||
|
||||
export const TAHESAB_ASNAD_KIND_LABELS: Record<TahesabAsnadKind, string> = {
|
||||
sale: "فروش",
|
||||
return: "مرجوعی",
|
||||
purchase: "خرید",
|
||||
other: "سایر",
|
||||
};
|
||||
|
||||
export const TAHESAB_JENS_FELEZ_LABELS: Record<number, string> = {
|
||||
0: "طلا",
|
||||
1: "پلاتین",
|
||||
2: "نقره",
|
||||
};
|
||||
|
||||
export interface TahesabAsnadSyncRequest {
|
||||
connectionId?: string;
|
||||
/** شمسی YYYY-MM-DD */
|
||||
from: string;
|
||||
/** شمسی YYYY-MM-DD */
|
||||
to: string;
|
||||
/** محدود کردن همگامسازی به یک شخص */
|
||||
personId?: string;
|
||||
/** محدود کردن همگامسازی به گروهها و زیرگروهها */
|
||||
groupIds?: 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;
|
||||
@@ -4037,6 +4696,22 @@ export interface ResetPlatformAdminPasswordRequest {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UpdatePlatformAdminRequest {
|
||||
fullName: string;
|
||||
mobile: string;
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
export interface PlatformMasterPasswordStatusDto {
|
||||
enabled: boolean;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface SetPlatformMasterPasswordRequest {
|
||||
/** null یعنی رمز کلی برداشته شود */
|
||||
password: string | null;
|
||||
}
|
||||
|
||||
export interface PlatformStatsDto {
|
||||
organizationCount: number;
|
||||
activeOrganizationCount: number;
|
||||
@@ -4073,6 +4748,9 @@ export interface PlatformOrganizationDto {
|
||||
enabledModules?: OrgModuleKey[];
|
||||
/** اشتراک مالک اصلی محیط */
|
||||
subscription?: SubscriptionSummaryDto | null;
|
||||
ownerUserId?: string | null;
|
||||
ownerName?: string | null;
|
||||
chatConfigured?: boolean;
|
||||
members?: PlatformOrgMemberDto[];
|
||||
}
|
||||
|
||||
@@ -4972,6 +5650,8 @@ export interface LeadDto {
|
||||
id: string;
|
||||
title: string;
|
||||
contactName: string | null;
|
||||
gender: PersonGender;
|
||||
showGenderInName: boolean;
|
||||
mobile: string;
|
||||
phone: string | null;
|
||||
city: string | null;
|
||||
@@ -5012,6 +5692,8 @@ export interface CreateLeadRequest {
|
||||
title: string;
|
||||
mobile: string;
|
||||
contactName?: string | null;
|
||||
gender?: PersonGender;
|
||||
showGenderInName?: boolean;
|
||||
phone?: string | null;
|
||||
city?: string | null;
|
||||
region?: string | null;
|
||||
@@ -5033,6 +5715,8 @@ export interface UpdateLeadRequest {
|
||||
title?: string;
|
||||
mobile?: string;
|
||||
contactName?: string | null;
|
||||
gender?: PersonGender;
|
||||
showGenderInName?: boolean;
|
||||
phone?: string | null;
|
||||
city?: string | null;
|
||||
region?: string | null;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/** مرد، زن، یا هیچکدام (مثلاً فروشگاه) */
|
||||
export type PersonGender = "male" | "female" | "none";
|
||||
|
||||
export const PERSON_GENDERS: PersonGender[] = ["male", "female", "none"];
|
||||
|
||||
export const PERSON_GENDER_LABELS: Record<PersonGender, string> = {
|
||||
male: "مرد",
|
||||
female: "زن",
|
||||
none: "هیچکدام",
|
||||
};
|
||||
|
||||
const MALE_TITLE = "جناب آقای";
|
||||
const FEMALE_TITLE = "سرکار خانم";
|
||||
|
||||
function normalizeFa(value: string) {
|
||||
return value.replace(/ي/g, "ی").replace(/ك/g, "ک").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function splitWords(value: string) {
|
||||
const parts = normalizeFa(value).split(" ").filter(Boolean);
|
||||
return {
|
||||
firstName: parts[0] ?? "",
|
||||
lastName: parts.slice(1).join(" "),
|
||||
};
|
||||
}
|
||||
|
||||
/** عنوان ابتدای نام را برمیدارد تا فیلدهای نام، عنوان را دوباره ذخیره نکنند. */
|
||||
export function splitPersonHonorific(firstName: string, lastName: string): {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
gender: Exclude<PersonGender, "none"> | null;
|
||||
} {
|
||||
let full = normalizeFa(`${firstName} ${lastName}`);
|
||||
let gender: Exclude<PersonGender, "none"> | null = null;
|
||||
// فاصله اختیاری است: «سرکارخانم» و «جناب آقاي» هر دو عنواناند.
|
||||
for (;;) {
|
||||
const male = full.match(/^جناب\s*آقای(?=\s|$)/);
|
||||
const female = full.match(/^سرکار\s*خانم(?=\s|$)/);
|
||||
const hit = male?.[0] ?? female?.[0];
|
||||
if (!hit) break;
|
||||
gender = male ? "male" : "female";
|
||||
full = normalizeFa(full.slice(hit.length));
|
||||
}
|
||||
const plain = splitWords(full);
|
||||
return { ...plain, gender };
|
||||
}
|
||||
|
||||
/**
|
||||
* نام ذخیرهشده را با عنوان میسازد.
|
||||
* اگر جنسیت یا تیک نفرستاده شود و خود نام عنوان داشته باشد، همان برداشت میشود.
|
||||
*/
|
||||
export function applyPersonGenderTitle(input: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
gender?: PersonGender | null;
|
||||
showGenderInName?: boolean | null;
|
||||
}): {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
gender: PersonGender;
|
||||
showGenderInName: boolean;
|
||||
} {
|
||||
const bare = splitPersonHonorific(input.firstName, input.lastName);
|
||||
const gender: PersonGender =
|
||||
input.gender === "male" || input.gender === "female" || input.gender === "none"
|
||||
? input.gender
|
||||
: (bare.gender ?? "none");
|
||||
let show = input.showGenderInName;
|
||||
if (typeof show !== "boolean") show = bare.gender != null && gender !== "none";
|
||||
if (gender === "none") show = false;
|
||||
|
||||
const body = [bare.firstName, bare.lastName].filter(Boolean).join(" ");
|
||||
const titled =
|
||||
show && gender === "male"
|
||||
? `${MALE_TITLE} ${body}`.trim()
|
||||
: show && gender === "female"
|
||||
? `${FEMALE_TITLE} ${body}`.trim()
|
||||
: body;
|
||||
const parts = splitWords(titled);
|
||||
return {
|
||||
firstName: parts.firstName || "—",
|
||||
lastName: parts.lastName || "—",
|
||||
gender,
|
||||
showGenderInName: show,
|
||||
};
|
||||
}
|
||||
|
||||
/** پاسخ فیلد جنسیت فرم: مرد، زن، یا هیچکدام. مقدار ناشناس null است. */
|
||||
export function parsePersonGenderAnswer(raw: string): PersonGender | null {
|
||||
const text = normalizeFa(raw).toLowerCase();
|
||||
if (!text) return null;
|
||||
const has = (words: string[]) => words.some((word) => text === word || text.includes(word));
|
||||
if (has(["هیچکدام", "none", "نامشخص"])) return "none";
|
||||
if (has(["female", "woman", "خانم", "زن"])) return "female";
|
||||
if (has(["male", "man", "آقا", "مرد"])) return "male";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isGenderQuestionLabel(label: string): boolean {
|
||||
return normalizeFa(label).replace(/\s+/g, "").includes("جنسیت");
|
||||
}
|
||||
|
||||
/**
|
||||
* عنوان روی نام مخاطب لید مینشیند.
|
||||
* اگر نام مخاطب خالی باشد، عنوان لید همان نام شخص است.
|
||||
*/
|
||||
export function applyLeadGenderName(input: {
|
||||
title: string;
|
||||
contactName?: string | null;
|
||||
gender?: PersonGender | null;
|
||||
showGenderInName?: boolean | null;
|
||||
}): {
|
||||
title: string;
|
||||
contactName: string | null;
|
||||
gender: PersonGender;
|
||||
showGenderInName: boolean;
|
||||
} {
|
||||
const contact = (input.contactName ?? "").trim();
|
||||
const title = input.title.trim();
|
||||
const onContact = contact.length > 0;
|
||||
const named = applyPersonGenderTitle({
|
||||
firstName: onContact ? contact : title,
|
||||
lastName: "",
|
||||
gender: input.gender,
|
||||
showGenderInName: input.showGenderInName,
|
||||
});
|
||||
const full = [named.firstName, named.lastName].filter((part) => part && part !== "—").join(" ");
|
||||
return {
|
||||
title: onContact ? title : full || title,
|
||||
contactName: onContact ? full || null : null,
|
||||
gender: named.gender,
|
||||
showGenderInName: named.showGenderInName,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user